Template catalog — every .iscript
42 templates under src/ICoreSDK/ICoreBlockLibrary/Templates — 42 compiled into the binary (named in ICoreTemplatesRegistry.qrc), 0 on disk only. A .iscript is a recipe the interpreter replays: block(Type), move, resize, setConfig, connect. Authoring rules and the measured placement geometry are in ADDING_NEW_TEMPLATES.md; the board is TEMPLATES_TO_ADD.md.
| Template | Category | Kind | Blocks placed | Links | In binary |
|---|---|---|---|---|---|
| Adaptive and Frequency Tools | System Identification | subsystem | 7 | 12 | yes |
| Anomaly Watch | Machine Learning | subsystem | 11 | 14 | yes |
| Arithmetic and Reductions | Control Systems | subsystem | 16 | 28 | yes |
| Attention Stack | Machine Learning | subsystem | 7 | 6 | yes |
| Classical Predictors Bench | Machine Learning | subsystem | 13 | 22 | yes |
| Continuous Delays and Holds | Control Systems | subsystem | 8 | 13 | yes |
| Continuous Dynamics Zoo | Control Systems | subsystem | 10 | 15 | yes |
| Convolution Stack | Machine Learning | subsystem | 8 | 7 | yes |
| Coordinate Transforms Tour | Robotics | subsystem | 13 | 35 | yes |
| Custom Code Blocks | Control Systems | subsystem | 4 | 4 | yes |
| Decision Postprocessing | Machine Learning | subsystem | 11 | 13 | yes |
| Dense Network From Layers | Machine Learning | subsystem | 11 | 12 | yes |
| Discrete Basics | Control Systems | subsystem | 10 | 16 | yes |
| Discrete Filters and Transfer Forms | Control Systems | subsystem | 12 | 20 | yes |
| First-Order Lag | Control Systems | subsystem | 3 | 2 | yes |
| Increment and Decrement Steppers | Control Systems | subsystem | 8 | 13 | yes |
| Integrators and Constraints | Control Systems | subsystem | 10 | 13 | yes |
| Lookup Tables Tour | Control Systems | subsystem | 14 | 24 | yes |
| LPV Continuous Suite | Control Systems | subsystem | 10 | 44 | yes |
| LPV Discrete Suite | Control Systems | subsystem | 12 | 51 | yes |
| Matrix Workbench | Control Systems | subsystem | 16 | 29 | yes |
| Model Verification Bench | Control Systems | subsystem | 16 | 29 | yes |
| Motion Profile Shapers | Robotics | subsystem | 5 | 16 | yes |
| Nonlinearity Gallery | Control Systems | subsystem | 15 | 30 | yes |
| Offline Identification Pair | System Identification | subsystem | 5 | 8 | yes |
| Perception Filters | Robotics | subsystem | 9 | 12 | yes |
| PID Closed Loop | Control Systems | subsystem | 5 | 5 | yes |
| Planar Arm Kinematics | Robotics | subsystem | 8 | 12 | yes |
| Preprocessing Line | Machine Learning | subsystem | 12 | 11 | yes |
| PWM Drive | Control Systems | subsystem | 6 | 7 | yes |
| Quaternion Orientation Pipeline | Robotics | subsystem | 9 | 15 | yes |
| Random and Counting Sources | Control Systems | subsystem | 8 | 6 | yes |
| Recurrent Cells | Machine Learning | subsystem | 5 | 7 | yes |
| Resettable and Special Delays | Control Systems | subsystem | 10 | 17 | yes |
| Safety Supervision Chain | Robotics | subsystem | 8 | 13 | yes |
| Saturated Gain | Control Systems | subsystem | 4 | 3 | yes |
| Scalar Math Tour | Control Systems | subsystem | 13 | 22 | yes |
| Signal Feature Extraction | Machine Learning | subsystem | 8 | 10 | yes |
| Signal Sources Gallery | Control Systems | subsystem | 14 | 13 | yes |
| Sinks and Data Sampler | Control Systems | subsystem | 14 | 9 | yes |
| Varying Filter Sweep | Control Systems | subsystem | 6 | 9 | yes |
| Vector and Bus Routing | Control Systems | subsystem | 12 | 26 | yes |
Adaptive and Frequency Tools#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/adaptive_and_frequency_tools.iscript · category System Identification · kind subsystem · compiled into the binary
One excitation and one unknown plant, watched by an LMS filter, a delay estimator and a frequency-point estimator at once.
Places: Uniform_Random_Number, Discrete_Transfer_Function, LMS_Filter, Time_Delay_Estimator, Frequency_Point_Estimator, Scope, Display
// @title: Adaptive and Frequency Tools
// @category: System Identification
// @summary: One excitation and one unknown plant, watched by an LMS filter, a delay estimator and a frequency-point estimator at once.
// @kind: subsystem
//
// The three online identification tools, all reading the same pair of signals:
// the excitation u that drives the plant, and the response y that comes back.
// That pairing is the whole idea — each block answers a different question
// about the same unknown system, and running them side by side is how you see
// which question you actually have.
//
// LMS Filter fits an 8-tap FIR model of the plant. Watch e:
// it shrinks as the weights w converge.
// Time Delay Estimator finds the lag between u and y by correlation.
// Frequency Point Estimator measures the plant's gain and phase at one
// frequency, as Re G and Im G.
//
// Swap the Discrete Transfer Function for the system you care about and the
// three answers follow it. A broadband excitation is not decoration: the
// uniform random source is what makes the fit identifiable, and a pure sine
// here would leave the LMS weights and the correlation peak ill-determined.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// lane (800 LMS, 440 delay, 260 frequency), sizes pinned with resize().
//
// Two layout facts this diagram is built around, both measured from a probe
// rather than assumed:
//
// * On a block with n ports the i-th sits at centre + ((n-1)/2 - i)*h/(n+1),
// NOT at the centre — so each sink's move-y is computed from ITS port's
// row, which is why they look unrounded.
// * The planner routes around blocks, so a lane whose corridor is blocked
// comes back as a wandering staircase. The LMS lane sits ABOVE the plant
// for that reason (at 640 its feed had to climb over it), and each lane's
// second sink sits below the first rather than beside it, so it is never
// standing in the first one's corridor.
// ---- the unknown system: broadband excitation in, response out
u = block(Uniform_Random_Number)
u.move(40, 675)
u.resize(80, 70)
u.setConfig(Minimum, -1)
u.setConfig(Maximum, 1)
u.setConfig(Seed, 7)
plant = block(Discrete_Transfer_Function)
plant.move(240, 685)
plant.resize(130, 90)
plant.setConfig(Numerator - Discrete, [0.4])
plant.setConfig(Denominator - Discrete, [1, -0.5])
// ---- the three tools, each fed the same (u, y)
lms = block(LMS_Filter)
lms.move(520, 845)
lms.resize(100, 90)
lms.setConfig(Filter Length, 8)
lms.setConfig(Step Size, 0.05)
tde = block(Time_Delay_Estimator)
tde.move(520, 475)
tde.resize(100, 70)
tde.setConfig(Maximum Lag (samples), 8)
fpe = block(Frequency_Point_Estimator)
fpe.move(520, 295)
fpe.resize(110, 70)
fpe.setConfig(Frequency (Hz), 1)
// ---- one sink per answer, each on its own output's port row
err = block(Scope)
err.move(760, 835)
err.resize(70, 70)
weights = block(Display)
weights.move(760, 745)
weights.resize(90, 50)
lag = block(Display)
lag.move(760, 477)
lag.resize(90, 50)
reG = block(Display)
reG.move(760, 297)
reG.resize(90, 50)
imG = block(Display)
imG.move(760, 240)
imG.resize(90, 50)
// ---- u fans out to the plant and all three tools; y follows it
drive = connect(u<0>, plant<0>)
connect(drive, lms<0>)
connect(drive, tde<0>)
connect(drive, fpe<0>)
resp = connect(plant<0>, lms<1>)
connect(resp, tde<1>)
connect(resp, fpe<1>)
connect(lms<1>, err<0>)
connect(lms<2>, weights<0>)
connect(tde<0>, lag<0>)
connect(fpe<0>, reG<0>)
connect(fpe<1>, imG<0>)
// Group washes, in the identity sheet's data accents: the plant on its own,
// then one per tool lane.
a = area()
a.move(15, 740)
a.resize(380, 170)
a.setColor(232, 168, 124)
a.setTitle("Unknown plant")
a2 = area()
a2.move(495, 900)
a2.resize(405, 230)
a2.setColor(168, 152, 232)
a2.setTitle("Adaptive FIR fit")
a3 = area()
a3.move(495, 532)
a3.resize(405, 152)
a3.setColor(124, 200, 232)
a3.setTitle("Delay")
a4 = area()
a4.move(495, 352)
a4.resize(405, 197)
a4.setColor(152, 216, 168)
a4.setTitle("Frequency response at one point")
t = textbox()
t.move(500, 130)
t.setText("Swap the Discrete Transfer Function for your own plant. Keep the excitation broadband — a pure sine leaves the LMS weights and the correlation peak ill-determined.")
Anomaly Watch#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/anomaly_watch.iscript · category Machine Learning · kind subsystem · compiled into the binary
Six anomaly detectors watching one input, their scores combined into a gated, debounced decision.
Places: Constant, Isolation_Forest_Score, Mux, Confidence_Gate, Debounced_Decision, Display, Mahalanobis_Distance, Drift_Detector, Input_Domain_Monitor, One_Class_SVM_Score, Local_Outlier_Factor_Score
// @title: Anomaly Watch
// @category: Machine Learning
// @summary: Six anomaly detectors watching one input, their scores combined into a gated, debounced decision.
// @kind: subsystem
//
// Six different answers to "is this input strange?", all watching the same
// sample, and one decision made out of them.
//
// Isolation Forest how few splits it takes to isolate the sample
// One-Class SVM which side of a learned boundary it falls
// Local Outlier Factor how its neighbourhood density compares
// Mahalanobis Distance how far it is in units of the fitted covariance
// Drift Detector whether that distance is TRENDING, not just large
// Input Domain Monitor whether it left the range the model was fitted on
//
// The last two are the ones people forget. Drift Detector reads the Mahalanobis
// distance rather than the raw input, because drift is a property of a statistic
// over time; Input Domain Monitor answers the separate question of whether the
// model is being asked about a region it never saw.
//
// The decision half is deliberately conservative: two scores are muxed into a
// column, Confidence Gate takes the strongest ONLY if it clears its threshold
// (and reports `valid` so a downstream block can tell a real decision from a
// fallback), and Debounced Decision refuses to change its mind until the new
// class has been seen twice running.
//
// TWO sources, because the shipped example parameters are not all the same
// width: the 3-feature vector drives the detectors whose fits are three wide,
// the 2-feature one drives the kernel and neighbourhood models.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: five lanes, every forward link
// straight, solved from the measured port offsets. The one long route -- the
// One-Class SVM score climbing to the Mux's second input -- is pinned up the
// x = 800 corridor, which threads between the Drift Detector and its display.
// ---- 3-feature detectors (port row y = 1200)
src3 = block(Constant)
src3.move(60, 1235)
src3.resize(70, 70)
src3.setConfig(Constant Value, [0.6; -0.4; 0.2])
iso = block(Isolation_Forest_Score)
iso.move(340, 1243)
iso.resize(150, 86)
mux = block(Mux)
mux.move(900, 1235)
mux.resize(20, 60)
gate = block(Confidence_Gate)
gate.move(1000, 1250)
gate.resize(130, 90)
deb = block(Debounced_Decision)
deb.move(1220, 1268)
deb.resize(140, 80)
deb.setConfig(Number Of Classes, 2)
d_deb = block(Display)
d_deb.move(1440, 1267)
d_deb.resize(90, 50)
mah = block(Mahalanobis_Distance)
mah.move(340, 1000)
mah.resize(130, 80)
drift = block(Drift_Detector)
drift.move(600, 1000)
drift.resize(130, 80)
d_drift = block(Display)
d_drift.move(850, 999)
d_drift.resize(90, 50)
dom = block(Input_Domain_Monitor)
dom.move(340, 765)
dom.resize(140, 90)
d_dom = block(Display)
d_dom.move(600, 768)
d_dom.resize(90, 50)
// ---- 2-feature detectors (port row y = 480)
src2 = block(Constant)
src2.move(60, 515)
src2.resize(70, 70)
src2.setConfig(Constant Value, [0.6; -0.4])
ocs = block(One_Class_SVM_Score)
ocs.move(340, 525)
ocs.resize(140, 90)
lof = block(Local_Outlier_Factor_Score)
lof.move(340, 280)
lof.resize(150, 80)
d_lof = block(Display)
d_lof.move(600, 279)
d_lof.resize(90, 50)
// ---- Links
l1 = connect(src3<0>, iso<0>)
l2 = connect(src2<0>, ocs<0>)
l3 = connect(iso<0>, mux<0>)
l4 = connect(mux<0>, gate<0>)
l5 = connect(gate<0>, deb<0>)
l6 = connect(deb<0>, d_deb<0>)
l7 = connect(mah<0>, drift<0>)
l8 = connect(drift<0>, d_drift<0>)
l9 = connect(dom<0>, d_dom<0>)
l10 = connect(lof<0>, d_lof<0>)
b1 = connect(l1, mah<0>)
b1.setCorners((200, 1200), (200, 960), (345, 960))
b2 = connect(l1, dom<0>)
b2.setCorners((200, 1200), (200, 720), (345, 720))
b3 = connect(l2, lof<0>)
b3.setCorners((200, 480), (200, 240), (345, 240))
// The One-Class SVM's score has to climb four lanes to reach the
// Mux's SECOND input. Left to the planner it wanders across the
// detectors; pinned here up the empty x = 800 corridor, which runs
// between the Drift Detector's frame and the displays column.
climb = connect(ocs<0>, mux<1>)
climb.setCorners((475, 495), (800, 495), (800, 1196), (905, 1196))
a = area()
a.move(315, 1298)
a.resize(440, 698)
a.setColor(168, 152, 232)
a.setTitle("Score the sample")
a2 = area()
a2.move(315, 580)
a2.resize(425, 460)
a2.setColor(124, 200, 168)
a2.setTitle("Boundary and neighbourhood")
a3 = area()
a3.move(875, 1323)
a3.resize(680, 243)
a3.setColor(232, 168, 124)
a3.setTitle("Gate and debounce")
t = textbox()
t.move(320, 80)
t.setText("Raise the Confidence Gate threshold and the decision falls back instead of firing; raise Confirmations and it takes longer to change its mind.")
Arithmetic and Reductions#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/arithmetic_and_reductions.iscript · category Control Systems · kind subsystem · compiled into the binary
The two-input arithmetic blocks, the two gains, and the blocks that reduce a whole vector to one number.
Places: Constant, Sum, Display, Subtract, Product, Divide, Gain, Slider_Gain, Sum_Of_Elements, Product_Of_Elements, MinMax, Dot_Product, Sine_Wave, Pulse_Generator, MinMax_Running_Resettable, Scope
// @title: Arithmetic and Reductions
// @category: Control Systems
// @summary: The two-input arithmetic blocks, the two gains, and the blocks that reduce a whole vector to one number.
// @kind: subsystem
//
// Four bands, read from the top down. The first is the arithmetic every model
// is made of; the second is the two ways to scale a signal; the third is the
// reductions -- blocks whose output is ONE number no matter how long the input
// vector is; the fourth is the two that need more than a constant to be
// interesting.
//
// Sum and Subtract are the same block: Subtract is a Sum whose second input
// starts negated, and either one takes more inputs and any sign pattern. The
// same is true of MinMax, whose dropdown chooses between min and max.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7. Each lane has its own port row
// (360, 660, 860, 1160) and a block's move-y is that row plus half its height.
// Where a block has TWO inputs the ports are NOT both at the block's centre --
// they are spread evenly about it -- so each lane's PRIMARY input sits on the
// port row and is dead straight, and the second input comes in from below with
// one bend, which is what a junction is supposed to look like.
// =================================================== two-input arithmetic
sa1 = block(Constant)
sa1.move(40, 1195)
sa1.resize(70, 70)
sa1.setConfig(Constant Value, 6)
sa2 = block(Constant)
sa2.move(40, 1095)
sa2.resize(70, 70)
sa2.setConfig(Constant Value, 4)
add = block(Sum)
add.move(220, 1190)
add.resize(60, 60)
d_add = block(Display)
d_add.move(380, 1185)
d_add.resize(90, 50)
connect(sa1<0>, add<0>)
connect(sa2<0>, add<1>)
connect(add<0>, d_add<0>)
sb1 = block(Constant)
sb1.move(540, 1195)
sb1.resize(70, 70)
sb1.setConfig(Constant Value, 6)
sb2 = block(Constant)
sb2.move(540, 1095)
sb2.resize(70, 70)
sb2.setConfig(Constant Value, 4)
sub = block(Subtract)
sub.move(720, 1190)
sub.resize(60, 60)
d_sub = block(Display)
d_sub.move(880, 1185)
d_sub.resize(90, 50)
connect(sb1<0>, sub<0>)
connect(sb2<0>, sub<1>)
connect(sub<0>, d_sub<0>)
// Product and Divide are rectangular, so input 0 sits ABOVE the block centre
// (at centre + height/6) -- hence the 1207 rather than 1195 on the first feed.
sc1 = block(Constant)
sc1.move(1040, 1207)
sc1.resize(70, 70)
sc1.setConfig(Constant Value, 6)
sc2 = block(Constant)
sc2.move(1040, 1095)
sc2.resize(70, 70)
sc2.setConfig(Constant Value, 4)
mul = block(Product)
mul.move(1220, 1195)
mul.resize(70, 70)
d_mul = block(Display)
d_mul.move(1380, 1185)
d_mul.resize(90, 50)
connect(sc1<0>, mul<0>)
connect(sc2<0>, mul<1>)
connect(mul<0>, d_mul<0>)
sd1 = block(Constant)
sd1.move(1540, 1207)
sd1.resize(70, 70)
sd1.setConfig(Constant Value, 6)
sd2 = block(Constant)
sd2.move(1540, 1095)
sd2.resize(70, 70)
sd2.setConfig(Constant Value, 4)
dv = block(Divide)
dv.move(1720, 1195)
dv.resize(70, 70)
d_dv = block(Display)
d_dv.move(1880, 1185)
d_dv.resize(90, 50)
connect(sd1<0>, dv<0>)
connect(sd2<0>, dv<1>)
connect(dv<0>, d_dv<0>)
// =================================================== scaling
sg1 = block(Constant)
sg1.move(40, 895)
sg1.resize(70, 70)
sg1.setConfig(Constant Value, 4)
k = block(Gain)
k.move(220, 895)
k.resize(70, 70)
k.setConfig(Gain Value, 2.5)
d_k = block(Display)
d_k.move(380, 885)
d_k.resize(90, 50)
connect(sg1<0>, k<0>)
connect(k<0>, d_k<0>)
sg2 = block(Constant)
sg2.move(540, 895)
sg2.resize(70, 70)
sg2.setConfig(Constant Value, 4)
sk = block(Slider_Gain)
sk.move(720, 895)
sk.resize(80, 70)
sk.setConfig(Minimum, 0)
sk.setConfig(Maximum, 3)
sk.setConfig(Gain, 1.5)
d_sk = block(Display)
d_sk.move(880, 885)
d_sk.resize(90, 50)
connect(sg2<0>, sk<0>)
connect(sk<0>, d_sk<0>)
// =================================================== reductions over a vector
sr1 = block(Constant)
sr1.move(40, 695)
sr1.resize(70, 70)
sr1.setConfig(Constant Value, [4 2 6 1])
soe = block(Sum_Of_Elements)
soe.move(220, 695)
soe.resize(70, 70)
d_soe = block(Display)
d_soe.move(380, 685)
d_soe.resize(90, 50)
connect(sr1<0>, soe<0>)
connect(soe<0>, d_soe<0>)
sr2 = block(Constant)
sr2.move(540, 695)
sr2.resize(70, 70)
sr2.setConfig(Constant Value, [4 2 6 1])
poe = block(Product_Of_Elements)
poe.move(720, 695)
poe.resize(70, 70)
d_poe = block(Display)
d_poe.move(880, 685)
d_poe.resize(90, 50)
connect(sr2<0>, poe<0>)
connect(poe<0>, d_poe<0>)
sr3 = block(Constant)
sr3.move(1040, 695)
sr3.resize(70, 70)
sr3.setConfig(Constant Value, [4 2 6 1])
mnx = block(MinMax)
mnx.move(1220, 695)
mnx.resize(80, 70)
d_mnx = block(Display)
d_mnx.move(1380, 685)
d_mnx.resize(90, 50)
connect(sr3<0>, mnx<0>)
connect(mnx<0>, d_mnx<0>)
// =================================================== vector product, running extremum
sp1 = block(Constant)
sp1.move(40, 395)
sp1.resize(70, 70)
sp1.setConfig(Constant Value, [4 2 6])
sp2 = block(Constant)
sp2.move(40, 295)
sp2.resize(70, 70)
sp2.setConfig(Constant Value, [1 5 3])
dot = block(Dot_Product)
dot.move(220, 390)
dot.resize(60, 60)
d_dot = block(Display)
d_dot.move(380, 385)
d_dot.resize(90, 50)
// Dot Product is a circle block like Sum, but it orders its two ports the
// other way round: port 1 is the one on the centre line, port 0 sits 25 below
// it. The upper constant therefore goes to port 1 so the links do not cross --
// the dot product is symmetric, so which vector is which makes no difference.
connect(sp1<0>, dot<1>)
connect(sp2<0>, dot<0>)
connect(dot<0>, d_dot<0>)
// The running extremum needs a signal that moves and a reset that fires: a
// sine into u, a pulse into R. Its input 0 sits at centre + height/6 = 374,
// which is why the sine is 14 higher than the lane row.
osc = block(Sine_Wave)
osc.move(540, 409)
osc.resize(70, 70)
osc.setConfig(Amplitude, 1)
rst = block(Pulse_Generator)
rst.move(540, 295)
rst.resize(80, 70)
rst.setConfig(Period, 4)
rst.setConfig(Pulse Width, 25)
run = block(MinMax_Running_Resettable)
run.move(720, 400)
run.resize(110, 80)
d_run = block(Scope)
d_run.move(900, 395)
d_run.resize(70, 70)
connect(osc<0>, run<0>)
connect(rst<0>, run<1>)
connect(run<0>, d_run<0>)
// One titled wash per band, in the identity sheet's accents.
a1 = area()
a1.move(15, 1262)
a1.resize(1980, 272)
a1.setColor(168, 152, 232)
a1.setTitle("Two-input arithmetic")
a2 = area()
a2.move(15, 950)
a2.resize(1975, 190)
a2.setColor(232, 168, 124)
a2.setTitle("Scaling")
a3 = area()
a3.move(15, 750)
a3.resize(1975, 190)
a3.setColor(43, 160, 184)
a3.setTitle("Reductions over a vector")
a4 = area()
a4.move(15, 464)
a4.resize(1975, 274)
a4.setColor(48, 160, 106)
a4.setTitle("Vector product and running extremum")
t = textbox()
t.move(40, 150)
t.setText("A reduction turns a whole vector into one number; the running extremum keeps its answer until the reset fires.")
Attention Stack#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/attention_stack.iscript · category Machine Learning · kind subsystem · compiled into the binary
A token index turned into an embedding, positioned, normalised and passed through scaled dot-product attention.
Places: Counter_Limited, Embedding_Lookup, Positional_Encoding, Layer_Normalization, Scaled_Dot_Product_Attention, Softmax, Display
// @title: Attention Stack
// @category: Machine Learning
// @summary: A token index turned into an embedding, positioned, normalised and passed through scaled dot-product attention.
// @kind: subsystem
//
// The front of a transformer, in the order the data moves. A counter walks the
// token indices 0..3; the embedding table turns each into a 3-wide vector;
// positional encoding adds the sinusoidal signal that tells the stack WHERE in
// the sequence a token sat -- without it, attention is order-blind; layer
// normalisation puts the result on a common scale (the pre-norm arrangement,
// before attention rather than after); and scaled dot-product attention projects
// to queries, keys and values and mixes them. A softmax reads the result as a
// distribution.
//
// Widths, since they are what usually goes wrong: the counter emits a scalar
// index, the embedding table has THREE columns so the vector is 3-wide, the
// positional encoding and the layer norm keep it 3-wide, and the attention
// block's projection matrices are 3x2 -- so what comes out of it, and into the
// softmax, is 2-wide.
//
// The counter stops at 3 because the shipped embedding table has four rows.
// Raise both together, or the out-of-range rows come back as zeros.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: two lanes with their own port rows,
// every forward link straight, and the wrap between them pinned through the
// empty band at y = 400 -- clear of the upper row's name labels, which hang
// about 20 under its frames.
// ---- Token to positioned embedding (port row y = 560)
tok = block(Counter_Limited)
tok.move(60, 595)
tok.resize(80, 70)
tok.setConfig(Upper Limit, 3)
emb = block(Embedding_Lookup)
emb.move(300, 602)
emb.resize(126, 84)
pos = block(Positional_Encoding)
pos.move(560, 600)
pos.resize(140, 80)
ln = block(Layer_Normalization)
ln.move(820, 600)
ln.resize(120, 80)
// ---- Attend, then read as a distribution (port row y = 260)
attn = block(Scaled_Dot_Product_Attention)
attn.move(300, 305)
attn.resize(150, 90)
// The three projections MUST be [d_model, d_k], and d_model is decided upstream,
// not here: Embedding Lookup emits its table's width as ROWS, so what arrives is
// [T, 1] -- three positions, one feature each. The block's own defaults are
// [3, 2], which describes a three-feature model and is refused against this
// window ("Invalid \"Query Weights\""). Left unset, the template did not run at
// all. Set to [1, 2]: one feature in, a two-wide key/value space, so the scores
// are a real 3x3 attention over the three positions rather than a formality.
attn.setConfig(Query Weights, [0.6 -0.4])
attn.setConfig(Key Weights, [0.5 0.7])
// d_v = 1, deliberately: the attention output goes straight into Softmax, and
// Softmax takes a COLUMN -- one logit per class, refusing a matrix rather than
// guessing which axis to normalise over. A two-wide value space would hand it a
// [3,2] and stop the run. One value per position makes the output [3,1]: a
// score per position, which is exactly what the softmax then reads as a
// distribution over the three positions.
attn.setConfig(Value Weights, [1.1])
sm = block(Softmax)
sm.move(580, 300)
sm.resize(110, 80)
d = block(Display)
d.move(800, 285)
d.resize(90, 50)
// ---- Links
l1 = connect(tok<0>, emb<0>)
l2 = connect(emb<0>, pos<0>)
l3 = connect(pos<0>, ln<0>)
l4 = connect(attn<0>, sm<0>)
l5 = connect(sm<0>, d<0>)
// The wrap from the end of the upper row to the start of the lower
// one, pinned through the empty band at y = 400. The planner's own
// route for this one goes back over the embedding row.
wrap = connect(ln<0>, attn<0>)
wrap.setCorners((935, 560), (990, 560), (990, 400), (200, 400), (200, 260), (305, 260))
a = area()
a.move(275, 657)
a.resize(690, 197)
a.setColor(168, 152, 232)
a.setTitle("Embed and position")
a2 = area()
a2.move(275, 360)
a2.resize(640, 200)
a2.setColor(232, 168, 124)
a2.setTitle("Attention")
t = textbox()
t.move(280, 120)
t.setText("Take the Positional Encoding out and the attention becomes order-blind: the same tokens in any order give the same answer.")
Classical Predictors Bench#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/classical_predictors_bench.iscript · category Machine Learning · kind subsystem · compiled into the binary
Ten classical models scoring the same feature vector side by side, with Top K reading the Naive Bayes posteriors.
Places: Constant, Decision_Tree, Display, Gaussian_Naive_Bayes, Top_K, Gradient_Boosted_Trees, K_Means_Assign, SVM_Predictor, Gaussian_Mixture_Model, KNN_Classifier, Gaussian_Process_Predictor, Random_Forest, Multinomial_Naive_Bayes
// @title: Classical Predictors Bench
// @category: Machine Learning
// @summary: Ten classical models scoring the same feature vector side by side, with Top K reading the Naive Bayes posteriors.
// @kind: subsystem
//
// A bench, not a pipeline: one feature vector goes to every classical predictor
// in the library at once, and each reports on its own display. Read down the
// column to compare what the same input means to a tree, to a kernel machine, to
// a nearest-neighbour vote and to a density model.
//
// Every model carries the small worked example it ships with -- a fitted tree,
// a handful of support vectors, three class means -- so the bench answers
// immediately and you can swap in your own fit one block at a time.
//
// TWO sources, because the shipped examples do not all take the same width. The
// 2-feature vector drives the eight models whose parameters are two columns
// wide; Random Forest (which splits on feature 2) and Multinomial Naive Bayes
// (three log-probability columns) need a third, so they sit on their own row and
// share the 3-feature source. An empty or wrong-width input is refused rather
// than broadcast, so the widths have to line up.
//
// Top K does not sit on the bus. It reads Gaussian Naive Bayes's SECOND output
// -- the per-class log-score vector -- which is what that port is documented for:
// "feed the whole vector to Softmax for posteriors, or to Confidence Gate or
// Top K for a decision with a threshold".
//
// Laid out per ADDING_NEW_TEMPLATES.md §7. Five lanes, two columns; every
// forward link is a dead-straight line, solved from the measured port offsets
// rather than from block centres (a block's ports are centred only when there is
// one of them on that side -- most of these have two). The column-B feed is one
// bus: four branches sharing a riser at x = 700, up over the top lane at
// y = 1320 and down to each row.
// ---- Trees and kernels / probabilistic models (port row y = 1160)
feat2 = block(Constant)
feat2.move(60, 1195)
feat2.resize(70, 70)
feat2.setConfig(Constant Value, [0.6; -0.4])
dt = block(Decision_Tree)
dt.move(300, 1200)
dt.resize(130, 80)
d_dt = block(Display)
d_dt.move(540, 1185)
d_dt.resize(90, 50)
gnb = block(Gaussian_Naive_Bayes)
gnb.move(800, 1204)
gnb.resize(134, 88)
d_gnb = block(Display)
d_gnb.move(1040, 1200)
d_gnb.resize(90, 50)
topk = block(Top_K)
topk.move(1240, 1189)
topk.resize(120, 86)
d_topk = block(Display)
d_topk.move(1440, 1186)
d_topk.resize(90, 50)
gbt = block(Gradient_Boosted_Trees)
gbt.move(300, 960)
gbt.resize(150, 80)
d_gbt = block(Display)
d_gbt.move(540, 945)
d_gbt.resize(90, 50)
km = block(K_Means_Assign)
km.move(800, 962)
km.resize(126, 84)
d_km = block(Display)
d_km.move(1040, 959)
d_km.resize(90, 50)
svm = block(SVM_Predictor)
svm.move(300, 725)
svm.resize(130, 90)
d_svm = block(Display)
d_svm.move(540, 720)
d_svm.resize(90, 50)
gmm = block(Gaussian_Mixture_Model)
gmm.move(800, 725)
gmm.resize(138, 90)
d_gmm = block(Display)
d_gmm.move(1040, 720)
d_gmm.resize(90, 50)
knn = block(KNN_Classifier)
knn.move(300, 480)
knn.resize(140, 80)
d_knn = block(Display)
d_knn.move(540, 479)
d_knn.resize(90, 50)
gp = block(Gaussian_Process_Predictor)
gp.move(800, 485)
gp.resize(130, 90)
d_gp = block(Display)
d_gp.move(1040, 480)
d_gp.resize(90, 50)
// ---- The two models that need a third feature (port row y = 200)
feat3 = block(Constant)
feat3.move(60, 235)
feat3.resize(70, 70)
feat3.setConfig(Constant Value, [0.6; -0.4; 0.2])
rf = block(Random_Forest)
rf.move(300, 243)
rf.resize(140, 86)
d_rf = block(Display)
d_rf.move(540, 225)
d_rf.resize(90, 50)
mnb = block(Multinomial_Naive_Bayes)
mnb.move(800, 244)
mnb.resize(140, 88)
d_mnb = block(Display)
d_mnb.move(1040, 240)
d_mnb.resize(90, 50)
// ---- Links
l1 = connect(feat2<0>, dt<0>)
l2 = connect(feat3<0>, rf<0>)
l3 = connect(dt<0>, d_dt<0>)
l4 = connect(gbt<0>, d_gbt<0>)
l5 = connect(svm<0>, d_svm<0>)
l6 = connect(knn<0>, d_knn<0>)
l7 = connect(gnb<0>, d_gnb<0>)
l8 = connect(km<0>, d_km<0>)
l9 = connect(gmm<0>, d_gmm<0>)
l10 = connect(gp<0>, d_gp<0>)
l11 = connect(rf<0>, d_rf<0>)
l12 = connect(mnb<0>, d_mnb<0>)
l13 = connect(gnb<1>, topk<0>)
l14 = connect(topk<0>, d_topk<0>)
b1 = connect(l1, gbt<0>)
b1.setCorners((200, 1160), (200, 920), (305, 920))
b2 = connect(l1, svm<0>)
b2.setCorners((200, 1160), (200, 680), (305, 680))
b3 = connect(l1, knn<0>)
b3.setCorners((200, 1160), (200, 440), (305, 440))
b4 = connect(l1, gnb<0>)
b4.setCorners((250, 1160), (250, 1320), (700, 1320), (700, 1160), (805, 1160))
b5 = connect(l1, km<0>)
b5.setCorners((250, 1160), (250, 1320), (700, 1320), (700, 920), (805, 920))
b6 = connect(l1, gmm<0>)
b6.setCorners((250, 1160), (250, 1320), (700, 1320), (700, 680), (805, 680))
b7 = connect(l1, gp<0>)
b7.setCorners((250, 1160), (250, 1320), (700, 1320), (700, 440), (805, 440))
b8 = connect(l2, mnb<0>)
b8.setCorners((250, 200), (250, 320), (700, 320), (700, 200), (805, 200))
a = area()
a.move(275, 1255)
a.resize(385, 1135)
a.setColor(168, 152, 232)
a.setTitle("Trees, kernels, neighbours")
a2 = area()
a2.move(775, 1259)
a2.resize(385, 1139)
a2.setColor(124, 200, 168)
a2.setTitle("Probabilistic and density models")
a3 = area()
a3.move(1215, 1244)
a3.resize(340, 194)
a3.setColor(232, 168, 124)
a3.setTitle("Decision from the posteriors")
t = textbox()
t.move(280, 90)
t.setText("One feature vector, ten fitted models. The bottom row takes a third feature because Random Forest splits on it and Multinomial Naive Bayes has three log-probability columns.")
Continuous Delays and Holds#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/continuous_delays_and_holds.iscript · category Control Systems · kind subsystem · compiled into the binary
One pulse train through every continuous delay the library has, three of them with the delay itself arriving on a port.
Places: Pulse_Generator, Transport_Delay, Scope, First_Order_Hold, Variable_Transport_Delay, Variable_Time_Delay, Entity_Transport_Delay, Sine_Wave
// @title: Continuous Delays and Holds
// @category: Control Systems
// @summary: One pulse train through every continuous delay the library has, three of them with the delay itself arriving on a port.
// @kind: subsystem
//
// Dead time, five ways. A pulse train is the input on purpose: a delayed
// pulse is unmistakable on a scope, where a delayed sine just looks like a
// phase shift you have to measure.
//
// Transport Delay - a fixed delay, set in the config.
// Variable Transport Delay - the delay arrives on port d and is swept
// Variable Time Delay between 0.5 s and 1.5 s by the sine below,
// Entity Transport Delay so the output stretches and compresses.
// First Order Hold - not a delay at all: it holds and ramps
// between updates instead of stepping.
//
// Delay blocks all carry a Maximum Delay (s), and a delay command above it
// is clipped rather than honoured -- the sweep here stays well inside the
// default of 10 s.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// lane, sizes pinned with resize(). The pulse branches to every lane and
// the delay command to the three variable ones, so branches are the only
// links that step.
pulse = block(Pulse_Generator)
pulse.move(40, 1155)
pulse.resize(80, 70)
pulse.setConfig(Pulse Type, Time based)
pulse.setConfig(Amplitude, 1)
pulse.setConfig(Period, 4)
pulse.setConfig(Pulse Width, 25)
pulse.setConfig(Phase Delay, 0)
// A fixed delay, and a hold that ramps rather than steps.
td = block(Transport_Delay)
td.move(460, 1155)
td.resize(100, 70)
td.setConfig(Delay Time (s), 1)
td.setConfig(Initial Output, 0)
l_u = connect(pulse<0>, td<0>)
td_s = block(Scope)
td_s.move(720, 1155)
td_s.resize(70, 70)
connect(td<0>, td_s<0>)
foh = block(First_Order_Hold)
foh.move(460, 935)
foh.resize(80, 70)
foh.setConfig(Initial Output, 0)
foh.setConfig(Update Period (s), 1)
connect(l_u, foh<0>)
foh_s = block(Scope)
foh_s.move(720, 935)
foh_s.resize(70, 70)
connect(foh<0>, foh_s<0>)
// Three blocks that take the delay on a port, all fed by one sine
// sweeping the command between 0.5 s and 1.5 s.
vtd = block(Variable_Transport_Delay)
vtd.move(460, 706.7)
vtd.resize(120, 80)
vtd.setConfig(Maximum Delay (s), 10)
vtd.setConfig(Initial Output, 0)
connect(l_u, vtd<0>)
vtd_s = block(Scope)
vtd_s.move(720, 701.7)
vtd_s.resize(70, 70)
connect(vtd<0>, vtd_s<0>)
vtm = block(Variable_Time_Delay)
vtm.move(460, 486.7)
vtm.resize(110, 80)
vtm.setConfig(Maximum Delay (s), 10)
vtm.setConfig(Initial Output, 0)
connect(l_u, vtm<0>)
vtm_s = block(Scope)
vtm_s.move(720, 481.7)
vtm_s.resize(70, 70)
connect(vtm<0>, vtm_s<0>)
etd = block(Entity_Transport_Delay)
etd.move(460, 266.7)
etd.resize(110, 80)
etd.setConfig(Initial Output, 0)
etd.setConfig(Capacity (samples), 64)
connect(l_u, etd<0>)
etd_s = block(Scope)
etd_s.move(720, 261.7)
etd_s.resize(70, 70)
connect(etd<0>, etd_s<0>)
dcmd = block(Sine_Wave)
dcmd.move(240, 688.3)
dcmd.resize(70, 70)
dcmd.setConfig(Amplitude, 0.5)
dcmd.setConfig(Bias, 1)
dcmd.setConfig(Frequency (Rad), 0.4)
l_dcmd = connect(dcmd<0>, vtd<1>)
connect(l_dcmd, vtm<1>)
connect(l_dcmd, etd<1>)
// Group washes in the identity sheet's data accents; only the hue is ours.
a1 = area()
a1.move(415, 1220)
a1.resize(420, 420)
a1.setColor(232, 168, 124)
a1.setTitle("Fixed delay, and a hold")
a2 = area()
a2.move(415, 772)
a2.resize(420, 650)
a2.setColor(168, 152, 232)
a2.setTitle("Delay commanded on a port")
t = textbox()
t.move(340, 110)
t.setText("Read the top lane first: the output is the input, one second later. The three commanded lanes then stretch and compress that same pulse as the sine sweeps their delay, and the first-order hold at the bottom is the odd one out -- it ramps between updates rather than delaying anything.")
Continuous Dynamics Zoo#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/continuous_dynamics_zoo.iscript · category Control Systems · kind subsystem · compiled into the binary
One step into every continuous plant form the library offers, each on its own scope, so the same response can be read off seven different descriptions.
Places: Step, Transfer_Function, Scope, State_Space, Zero_Pole, Descriptor_State_Space, Nonlinear_State_Space, Constant, Sparse_Second_Order, Derivative
// @title: Continuous Dynamics Zoo
// @category: Control Systems
// @summary: One step into every continuous plant form the library offers, each on its own scope, so the same response can be read off seven different descriptions.
// @kind: subsystem
//
// Seven ways to say the same thing. One step drives a transfer function, a
// state space, a zero-pole-gain form, a descriptor (mass-matrix) form, a
// nonlinear state space, a sparse second-order mechanical form and a plain
// derivative -- and the first four are deliberately the SAME second-order
// system, 1/(s^2 + 2s + 1), written four different ways. Their scopes
// should agree; that is the point of the template.
//
// Reach for whichever description your data already has: a measured
// frequency response is a transfer function, a linearised model is a state
// space, a finite-element model is descriptor or second-order.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// plant, sizes pinned with resize(). The step branches to every lane, so
// the branches are the only links that step.
step = block(Step)
step.move(40, 1595)
step.resize(70, 70)
step.setConfig(Step Time (sec), 1)
step.setConfig(Initial Value, 0)
step.setConfig(Final Value, 1)
// The same second-order system, four ways. Every scope in this group
// shows the same curve -- if one differs, that description is wrong.
tf = block(Transfer_Function)
tf.move(420, 1605)
tf.resize(130, 90)
tf.setConfig(Numerator, [1])
tf.setConfig(Denominator, [1, 2, 1])
l_step = connect(step<0>, tf<0>)
tf_s = block(Scope)
tf_s.move(700, 1595)
tf_s.resize(70, 70)
connect(tf<0>, tf_s<0>)
ss = block(State_Space)
ss.move(420, 1385)
ss.resize(130, 90)
ss.setConfig(A, [0 1;-1 -2])
ss.setConfig(B, [0; 1])
ss.setConfig(C, [1 0])
ss.setConfig(D, [0])
ss.setConfig(Initial State Vector, [0; 0])
connect(l_step, ss<0>)
ss_s = block(Scope)
ss_s.move(700, 1375)
ss_s.resize(70, 70)
connect(ss<0>, ss_s<0>)
zp = block(Zero_Pole)
zp.move(420, 1165)
zp.resize(140, 90)
zp.setConfig(Zeros, [])
zp.setConfig(Poles, [-1 -1])
zp.setConfig(Gain, 1)
connect(l_step, zp<0>)
zp_s = block(Scope)
zp_s.move(700, 1155)
zp_s.resize(70, 70)
connect(zp<0>, zp_s<0>)
dss = block(Descriptor_State_Space)
dss.move(420, 945)
dss.resize(140, 90)
dss.setConfig(E, [1 0; 0 1])
dss.setConfig(A, [0 1;-1 -2])
dss.setConfig(B, [0; 1])
dss.setConfig(C, [1 0])
dss.setConfig(D, [0])
dss.setConfig(Initial State Vector, [0; 0])
connect(l_step, dss<0>)
dss_s = block(Scope)
dss_s.move(700, 935)
dss_s.resize(70, 70)
connect(dss<0>, dss_s<0>)
// Forms that carry something the four above cannot express.
nls = block(Nonlinear_State_Space)
nls.move(420, 710)
nls.resize(130, 90)
nls.setConfig(A, [0 1;-1 -2])
nls.setConfig(Bu, [0; 1])
nls.setConfig(Bf, [0; 1])
nls.setConfig(C, [1 0])
nls.setConfig(Du, [0])
nls.setConfig(Df, [0])
nls.setConfig(Initial State Vector, [0; 0])
connect(l_step, nls<0>)
nls_s = block(Scope)
nls_s.move(700, 700)
nls_s.resize(70, 70)
connect(nls<0>, nls_s<0>)
fterm = block(Constant)
fterm.move(220, 686)
fterm.resize(70, 70)
fterm.setConfig(Constant Value, 0)
connect(fterm<0>, nls<1>)
so = block(Sparse_Second_Order)
so.move(420, 505)
so.resize(130, 90)
so.setConfig(M, 1)
so.setConfig(C, 2)
so.setConfig(K, 1)
so.setConfig(B, 1)
so.setConfig(F, 1)
so.setConfig(G, 0)
so.setConfig(D, 0)
so.setConfig(q0, 0)
so.setConfig(dq0, 0)
connect(l_step, so<0>)
so_s = block(Scope)
so_s.move(700, 495)
so_s.resize(70, 70)
connect(so<0>, so_s<0>)
der = block(Derivative)
der.move(420, 275)
der.resize(80, 70)
connect(l_step, der<0>)
der_s = block(Scope)
der_s.move(700, 275)
der_s.resize(70, 70)
connect(der<0>, der_s<0>)
// Group washes in the identity sheet's data accents; only the hue is ours.
a1 = area()
a1.move(375, 1670)
a1.resize(440, 880)
a1.setColor(168, 152, 232)
a1.setTitle("One system, four descriptions")
a2 = area()
a2.move(375, 775)
a2.resize(440, 635)
a2.setColor(232, 168, 124)
a2.setTitle("Forms with something extra")
t = textbox()
t.move(320, 110)
t.setText("The top four lanes are the same plant written four ways, so their scopes should agree -- a lane that disagrees is a transcription error, not a modelling choice. The nonlinear form takes a second forcing term on port f (held at zero here), and the second-order form is stated as M, C, K rather than as a polynomial.")
Convolution Stack#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/convolution_stack.iscript · category Machine Learning · kind subsystem · compiled into the binary
A 1-D signal convolved, pooled and expanded again, with the 2-D pair doing the same to an image patch.
Places: Constant, Conv_1D, Pooling_1D, Transposed_Conv_1D, Scope, Conv_2D, Pooling_2D, Display
// @title: Convolution Stack
// @category: Machine Learning
// @summary: A 1-D signal convolved, pooled and expanded again, with the 2-D pair doing the same to an image patch.
// @kind: subsystem
//
// Two rows, one idea. The upper one is the encoder/decoder shape a 1-D network
// is built from and the lengths are worth following, because every one of these
// blocks changes them:
//
// 8 samples in
// Conv 1D kernel 3, stride 1, no padding -> 6
// Pooling 1D window 3, stride 2 -> 2
// Transposed Conv kernel 3, stride 2 -> 5
//
// That is the point of the transposed convolution: it is the block that puts
// length BACK, which is how a segmentation or an autoencoder head returns to
// something the size of its input.
//
// The lower row is the 2-D pair on a 4x4 patch: a 2x2 kernel takes it to 3x3,
// and 2x2 max pooling at stride 2 takes that to a single number. Change the
// strides and the paddings on either row and the sizes downstream follow by
// themselves -- nothing here has a shape written into it twice.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: two independent lanes, each with its
// own port row, every link a dead-straight line.
// ---- 1-D: convolve, pool, expand (port row y = 620)
sig = block(Constant)
sig.move(60, 655)
sig.resize(70, 70)
sig.setConfig(Constant Value, [0.2; 0.5; -0.1; 0.9; 0.3; -0.4; 0.7; 0.1])
conv1 = block(Conv_1D)
conv1.move(340, 660)
conv1.resize(120, 80)
pool1 = block(Pooling_1D)
pool1.move(580, 660)
pool1.resize(110, 80)
tconv = block(Transposed_Conv_1D)
tconv.move(800, 661)
tconv.resize(128, 82)
sc = block(Scope)
sc.move(1060, 655)
sc.resize(70, 70)
// ---- 2-D: convolve and pool a patch (port row y = 260)
patch = block(Constant)
patch.move(60, 295)
patch.resize(70, 70)
patch.setConfig(Constant Value, [0.2 0.5 -0.1 0.9; 0.3 -0.4 0.7 0.1; 0.6 0.2 -0.3 0.8; -0.5 0.9 0.4 0.1])
conv2 = block(Conv_2D)
conv2.move(340, 302)
conv2.resize(120, 84)
pool2 = block(Pooling_2D)
pool2.move(580, 302)
pool2.resize(116, 84)
d2 = block(Display)
d2.move(820, 285)
d2.resize(90, 50)
// ---- Links
l1 = connect(sig<0>, conv1<0>)
l2 = connect(conv1<0>, pool1<0>)
l3 = connect(pool1<0>, tconv<0>)
l4 = connect(tconv<0>, sc<0>)
l5 = connect(patch<0>, conv2<0>)
l6 = connect(conv2<0>, pool2<0>)
l7 = connect(pool2<0>, d2<0>)
a = area()
a.move(315, 716)
a.resize(840, 196)
a.setColor(168, 152, 232)
a.setTitle("1-D stack")
a2 = area()
a2.move(315, 357)
a2.resize(620, 197)
a2.setColor(232, 168, 124)
a2.setTitle("2-D stack")
t = textbox()
t.move(320, 120)
t.setText("Lengths: 8 in, 6 after the convolution, 2 after pooling, 5 back out of the transposed convolution. Change a stride and everything downstream resizes itself.")
Coordinate Transforms Tour#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/coordinate_transforms_tour.iscript · category Robotics · kind subsystem · compiled into the binary
Every 2D and 3D coordinate mapping in the library, each run on a worked example, plus the two angle-conditioning blocks.
Places: Constant, Demux, Cartesian_To_Polar, Mux, Display, Polar_To_Cartesian, Cartesian_To_Spherical, Spherical_To_Cartesian, Rotation_2D, Ramp, Angle_Wrap, Angle_Unwrap, Scope
// @title: Coordinate Transforms Tour
// @category: Robotics
// @summary: Every 2D and 3D coordinate mapping in the library, each run on a worked example, plus the two angle-conditioning blocks.
// @kind: subsystem
//
// Each transform lane is the same five-block shape: one Constant holding the
// whole input vector, a Demux to spread it over the block's ports, the
// transform, a Mux to gather the results, and a Display. That keeps the
// numbers together where they can be read as a point rather than as loose
// scalars -- and because a Demux and a Mux pinned to the transform's own
// height put their ports on exactly the transform's port rows, every link in
// the lane is dead straight.
//
// The four lanes come in inverse PAIRS, and the numbers are chosen so the
// second lane of each pair undoes the first:
//
// (3, 4) -> Cartesian to Polar -> (5, 0.9273)
// (5, 0.9273) -> Polar to Cartesian -> (3, 4)
// (1, 2, 2) -> Cartesian to Spherical -> (3, 1.1071, 0.8411)
// (3, 1.1071, 0.8411) -> Spherical to Cartesian -> (1, 2, 2)
//
// Rotation 2D is not a mapping between coordinate systems but a rotation
// within one, so it takes its angle on a port: (1, 0) turned by +pi/2 comes
// out as (0, 1). Its two inputs are different shapes -- a [2,1] vector and a
// scalar -- so it is the one lane that cannot be fed from a single Demux.
//
// The bottom band is the pair that conditions an angle rather than converting
// one: a ramp climbing past pi is wrapped into (-pi, pi], and the unwrap then
// puts the ramp back. Reading the two scopes together is the point.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7.
// ============================================== cartesian <-> polar
in_c2p = block(Constant)
in_c2p.move(40, 1595)
in_c2p.resize(70, 70)
in_c2p.setConfig(Constant Value, [3; 4])
dx_c2p = block(Demux)
dx_c2p.move(240, 1602)
dx_c2p.resize(20, 84)
t_c2p = block(Cartesian_To_Polar)
t_c2p.move(400, 1602)
t_c2p.resize(126, 84)
mx_c2p = block(Mux)
mx_c2p.move(700, 1602)
mx_c2p.resize(20, 84)
d_c2p = block(Display)
d_c2p.move(850, 1585)
d_c2p.resize(90, 50)
connect(in_c2p<0>, dx_c2p<0>)
connect(dx_c2p<0>, t_c2p<0>)
connect(dx_c2p<1>, t_c2p<1>)
connect(t_c2p<0>, mx_c2p<0>)
connect(t_c2p<1>, mx_c2p<1>)
connect(mx_c2p<0>, d_c2p<0>)
in_p2c = block(Constant)
in_p2c.move(40, 1395)
in_p2c.resize(70, 70)
in_p2c.setConfig(Constant Value, [5; 0.9272952])
dx_p2c = block(Demux)
dx_p2c.move(240, 1402)
dx_p2c.resize(20, 84)
t_p2c = block(Polar_To_Cartesian)
t_p2c.move(400, 1402)
t_p2c.resize(126, 84)
mx_p2c = block(Mux)
mx_p2c.move(700, 1402)
mx_p2c.resize(20, 84)
d_p2c = block(Display)
d_p2c.move(850, 1385)
d_p2c.resize(90, 50)
connect(in_p2c<0>, dx_p2c<0>)
connect(dx_p2c<0>, t_p2c<0>)
connect(dx_p2c<1>, t_p2c<1>)
connect(t_p2c<0>, mx_p2c<0>)
connect(t_p2c<1>, mx_p2c<1>)
connect(mx_p2c<0>, d_p2c<0>)
// ============================================== cartesian <-> spherical
in_c2s = block(Constant)
in_c2s.move(40, 1175)
in_c2s.resize(70, 70)
in_c2s.setConfig(Constant Value, [1; 2; 2])
dx_c2s = block(Demux)
dx_c2s.addPort(out)
dx_c2s.move(240, 1188)
dx_c2s.resize(20, 96)
t_c2s = block(Cartesian_To_Spherical)
t_c2s.move(400, 1188)
t_c2s.resize(132, 96)
mx_c2s = block(Mux)
mx_c2s.addPort(in)
mx_c2s.move(700, 1188)
mx_c2s.resize(20, 96)
d_c2s = block(Display)
d_c2s.move(850, 1165)
d_c2s.resize(90, 50)
connect(in_c2s<0>, dx_c2s<0>)
connect(dx_c2s<0>, t_c2s<0>)
connect(dx_c2s<1>, t_c2s<1>)
connect(dx_c2s<2>, t_c2s<2>)
connect(t_c2s<0>, mx_c2s<0>)
connect(t_c2s<1>, mx_c2s<1>)
connect(t_c2s<2>, mx_c2s<2>)
connect(mx_c2s<0>, d_c2s<0>)
in_s2c = block(Constant)
in_s2c.move(40, 955)
in_s2c.resize(70, 70)
in_s2c.setConfig(Constant Value, [3; 1.1071487; 0.8410687])
dx_s2c = block(Demux)
dx_s2c.addPort(out)
dx_s2c.move(240, 968)
dx_s2c.resize(20, 96)
t_s2c = block(Spherical_To_Cartesian)
t_s2c.move(400, 968)
t_s2c.resize(132, 96)
mx_s2c = block(Mux)
mx_s2c.addPort(in)
mx_s2c.move(700, 968)
mx_s2c.resize(20, 96)
d_s2c = block(Display)
d_s2c.move(850, 945)
d_s2c.resize(90, 50)
connect(in_s2c<0>, dx_s2c<0>)
connect(dx_s2c<0>, t_s2c<0>)
connect(dx_s2c<1>, t_s2c<1>)
connect(dx_s2c<2>, t_s2c<2>)
connect(t_s2c<0>, mx_s2c<0>)
connect(t_s2c<1>, mx_s2c<1>)
connect(t_s2c<2>, mx_s2c<2>)
connect(mx_s2c<0>, d_s2c<0>)
// ============================================== rotation within one frame
in_rotu = block(Constant)
in_rotu.move(40, 795)
in_rotu.resize(70, 70)
in_rotu.setConfig(Constant Value, [1; 0])
in_rotth = block(Constant)
in_rotth.move(40, 595)
in_rotth.resize(70, 70)
in_rotth.setConfig(Constant Value, 1.5707963)
t_rot = block(Rotation_2D)
t_rot.move(400, 702)
t_rot.resize(126, 84)
d_rot = block(Display)
d_rot.move(850, 685)
d_rot.resize(90, 50)
connect(in_rotu<0>, t_rot<0>)
connect(in_rotth<0>, t_rot<1>)
connect(t_rot<0>, d_rot<0>)
// ============================================== wrapping and unwrapping an angle
src_ang = block(Ramp)
src_ang.move(40, 315)
src_ang.resize(70, 70)
src_ang.setConfig(Slope, 1)
src_ang.setConfig(Start Time (s), 0)
t_wrap = block(Angle_Wrap)
t_wrap.move(300, 316)
t_wrap.resize(118, 72)
t_unwrap = block(Angle_Unwrap)
t_unwrap.move(600, 316)
t_unwrap.resize(122, 72)
v_unwrap = block(Scope)
v_unwrap.move(880, 315)
v_unwrap.resize(70, 70)
v_wrap = block(Scope)
v_wrap.move(600, 455)
v_wrap.resize(70, 70)
connect(src_ang<0>, t_wrap<0>)
wrapped = connect(t_wrap<0>, t_unwrap<0>)
connect(wrapped, v_wrap<0>)
connect(t_unwrap<0>, v_unwrap<0>)
a1 = area()
a1.move(15, 1660)
a1.resize(950, 380)
a1.setColor(168, 152, 232)
a1.setTitle("Cartesian and polar, each way")
a2 = area()
a2.move(15, 1243)
a2.resize(950, 403)
a2.setColor(232, 168, 124)
a2.setTitle("Cartesian and spherical, each way")
a3 = area()
a3.move(15, 860)
a3.resize(950, 360)
a3.setColor(43, 160, 184)
a3.setTitle("Rotating a vector inside one frame")
a4 = area()
a4.move(15, 540)
a4.resize(960, 350)
a4.setColor(48, 160, 106)
a4.setTitle("Wrapping an angle, and putting it back")
t = textbox()
t.move(40, 120)
t.setText("Each pair of lanes is an inverse pair: the second lane's display should read back the first lane's input.")
Custom Code Blocks#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/custom_code_blocks.iscript · category Control Systems · kind subsystem · compiled into the binary
A C block and a Python block side by side, each passing one signal through its own source.
Places: Sine_Wave, C_Code, Python_Code, Scope
// @title: Custom Code Blocks
// @category: Control Systems
// @summary: A C block and a Python block side by side, each passing one signal through its own source.
// @kind: subsystem
//
// When the library does not have the block you need, write it. Both blocks
// here ship with a working pass-through body — the C block compiles its
// source and runs it in-process; the Python block hands each step to a Python
// runtime. Open either one's editor, replace compute() with your own maths,
// and the block behaves like any other in the diagram.
//
// The two lanes are deliberately independent so the comparison is honest: the
// same sine drives both, and the two scopes should agree while the bodies
// agree. Change one body and the lanes separate on screen.
//
// The Python lane needs a Python runtime on this machine to PRODUCE numbers.
// It costs nothing to have in the template regardless: listing, inserting and
// saving are pure diagram operations, so the template loads cleanly with no
// Python installed, and the C lane runs either way.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// lane (420 for the C lane, 260 for the Python lane), sizes pinned with
// resize() so the alignment cannot drift, and the note kept clear of both.
src = block(Sine_Wave)
src.move(40, 375)
src.resize(70, 70)
src.setConfig(Amplitude, 1)
src.setConfig(Frequency (Rad), 2)
cc = block(C_Code)
cc.move(280, 460)
cc.resize(100, 80)
pc = block(Python_Code)
pc.move(280, 300)
pc.resize(100, 80)
cOut = block(Scope)
cOut.move(520, 455)
cOut.resize(70, 70)
pyOut = block(Scope)
pyOut.move(520, 295)
pyOut.resize(70, 70)
l = connect(src<0>, cc<0>)
connect(l, pc<0>)
connect(cc<0>, cOut<0>)
connect(pc<0>, pyOut<0>)
// One wash per language, in the identity sheet's data accents.
a = area()
a.move(250, 515)
a.resize(365, 160)
a.setColor(168, 152, 232)
a.setTitle("C")
a2 = area()
a2.move(250, 355)
a2.resize(365, 160)
a2.setColor(232, 168, 124)
a2.setTitle("Python")
t = textbox()
t.move(250, 190)
t.setText("Open either block's code editor and rewrite compute(); the block then behaves like any other. The Python lane needs a Python runtime to produce numbers, the C lane does not.")
Decision Postprocessing#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/decision_postprocessing.iscript · category Machine Learning · kind subsystem · compiled into the binary
A three-class score vector turned into a decision, then thresholded, ranked and smoothed in time.
Places: Sine_Wave, Constant, Uniform_Random_Number, Mux, Confidence_Gate, Top_K, Multilabel_Threshold, Temporal_Majority_Vote, Debounced_Decision, Scope, Display
// @title: Decision Postprocessing
// @category: Machine Learning
// @summary: A three-class score vector turned into a decision, then thresholded, ranked and smoothed in time.
// @kind: subsystem
//
// What happens AFTER a model produces scores. Everything here reads the same
// three-class score vector and turns it into something a controller can act
// on:
//
// Confidence Gate argmax, but only when the winning score clears the
// threshold — otherwise the fallback index. valid
// says which of the two you are looking at.
// Top K the K best classes, as indices and values.
// Multilabel Threshold one independent yes/no per class, for the case
// where classes are not mutually exclusive.
// Temporal Majority Vote the most common class over a window.
// Debounced Decision only switches after the new class repeats.
//
// The two smoothers sit downstream of the gate on purpose: raw argmax
// chatters whenever two scores are close, and both blocks are answers to that
// — a vote answers "what has it mostly been", a debounce answers "has it
// really changed". Run it and compare the two scope traces.
//
// The scores are deliberately moving: one class ramps up and down as a sine,
// one sits flat, one is noise. That is what makes the chatter visible; a
// static score vector would make every block here look like a wire.
//
// Index Base is left at zero-based on every block, so the class indices agree
// with each other. Change it in one place and the vote will be smoothing a
// different numbering than the gate emits.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, sizes pinned with
// resize(), and each sink on ITS OWN output's port row — on a block with n
// ports the i-th sits at centre + ((n-1)/2 - i) * h/(n+1), not at the centre.
// ---- three class scores, muxed into one vector
s0 = block(Sine_Wave)
s0.move(40, 735)
s0.resize(70, 70)
s0.setConfig(Amplitude, 1)
s0.setConfig(Frequency (Rad), 0.5)
s1 = block(Constant)
s1.move(40, 635)
s1.resize(70, 70)
s1.setConfig(Constant Value, 0.6)
s2 = block(Uniform_Random_Number)
s2.move(40, 535)
s2.resize(80, 70)
s2.setConfig(Minimum, 0)
s2.setConfig(Maximum, 1)
s2.setConfig(Seed, 5)
scores = block(Mux)
scores.move(220, 630)
scores.resize(20, 60)
scores.clearPorts()
scores.addPort(in, ICoreDouble, West)
scores.addPort(in, ICoreDouble, West)
scores.addPort(in, ICoreDouble, West)
// ---- three readings of the same score vector
gate = block(Confidence_Gate)
gate.move(340, 790)
gate.resize(130, 90)
gate.setConfig(Threshold, 0.5)
gate.setConfig(Fallback Index, -1)
topk = block(Top_K)
topk.move(340, 563)
topk.resize(120, 85)
topk.setConfig(K, 2)
multi = block(Multilabel_Threshold)
multi.move(340, 395)
multi.resize(120, 80)
multi.setConfig(Thresholds, [0.5; 0.5; 0.5])
// ---- two ways to stop the decision chattering
vote = block(Temporal_Majority_Vote)
vote.move(560, 865)
vote.resize(150, 80)
vote.setConfig(Number Of Classes, 3)
vote.setConfig(Window Length, 7)
debounce = block(Debounced_Decision)
debounce.move(560, 725)
debounce.resize(140, 80)
debounce.setConfig(Number Of Classes, 3)
debounce.setConfig(Confirmations, 3)
// ---- one sink per answer, each on its own output's port row
voted = block(Scope)
voted.move(800, 874)
voted.resize(70, 70)
settled = block(Scope)
settled.move(800, 734)
settled.resize(70, 70)
valid = block(Display)
valid.move(800, 770)
valid.resize(90, 50)
ranked = block(Display)
ranked.move(800, 560)
ranked.resize(90, 50)
flags = block(Display)
flags.move(800, 380)
flags.resize(90, 50)
// ---- scores in, decision out, smoothing after
connect(s0<0>, scores<0>)
connect(s1<0>, scores<1>)
connect(s2<0>, scores<2>)
vec = connect(scores<0>, gate<0>)
connect(vec, topk<0>)
connect(vec, multi<0>)
idx = connect(gate<0>, vote<0>)
connect(idx, debounce<0>)
connect(gate<1>, valid<0>)
connect(vote<0>, voted<0>)
connect(debounce<0>, settled<0>)
connect(topk<0>, ranked<0>)
connect(multi<0>, flags<0>)
// Group washes, in the identity sheet's data accents: one per stage.
a = area()
a.move(15, 790)
a.resize(255, 350)
a.setColor(168, 152, 232)
a.setTitle("Class scores")
a2 = area()
a2.move(315, 929)
a2.resize(600, 329)
a2.setColor(232, 168, 124)
a2.setTitle("Confidence gate, then smoothing")
a3 = area()
a3.move(315, 618)
a3.resize(600, 165)
a3.setColor(124, 200, 232)
a3.setTitle("Ranking")
a4 = area()
a4.move(315, 450)
a4.resize(600, 160)
a4.setColor(152, 216, 168)
a4.setTitle("Independent per-class decisions")
t = textbox()
t.move(320, 285)
t.setText("Compare the two scope traces: the vote reports what the class has mostly been, the debounce holds the old class until the new one repeats. Raise the gate threshold to see the fallback index appear.")
Dense Network From Layers#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/dense_network_from_layers.iscript · category Machine Learning · kind subsystem · compiled into the binary
A small classifier spelled out one layer at a time, with the same network as a single block beside it.
Places: Constant, Dense_Layer, Batch_Normalization, Activation_Function, Layer_Normalization, Softmax, Scope, MLP_Network, RBF_Layer, Display, Quantized_Dense_Layer
// @title: Dense Network From Layers
// @category: Machine Learning
// @summary: A small classifier spelled out one layer at a time, with the same network as a single block beside it.
// @kind: subsystem
//
// The top row is a network written out the long way: a dense layer lifts two
// features to three, batch normalisation rescales them with the statistics it was
// fitted on, the activation bends them, layer normalisation re-centres them
// per-sample, and a softmax turns them into something that sums to one. Every
// one of those is a block you can retune, reorder or delete.
//
// The second row is the SAME idea as one block. MLP Network carries its layers
// internally -- fewer things on the canvas, nothing to wire wrong, and no way to
// tap a hidden activation. Which of the two rows you want depends entirely on
// whether you need to see inside.
//
// The last two rows are the layer types that are not part of that stack:
//
// RBF Layer distance to fitted centres, not a matrix multiply
// Quantized Dense Layer the same arithmetic in int8, with the scales and
// zero points an embedded target actually ships
//
// The quantized layer is fed from the LAYER NORM output rather than from the
// source, because its int8 weights are 2x3 -- it expects the three-wide hidden
// vector, not the two-wide input.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: four lanes, every forward link
// straight, solved from the measured port offsets. The tap down to the quantized
// layer is pinned to the x = 1100 corridor, which is clear of every block below.
// ---- One layer at a time (port row y = 860)
src = block(Constant)
src.move(60, 895)
src.resize(70, 70)
src.setConfig(Constant Value, [0.6; -0.4])
dense = block(Dense_Layer)
dense.move(300, 905)
dense.resize(120, 90)
bn = block(Batch_Normalization)
bn.move(520, 900)
bn.resize(120, 80)
act = block(Activation_Function)
act.move(740, 900)
act.resize(110, 80)
ln = block(Layer_Normalization)
ln.move(940, 900)
ln.resize(120, 80)
sm = block(Softmax)
sm.move(1160, 900)
sm.resize(110, 80)
sc = block(Scope)
sc.move(1380, 895)
sc.resize(70, 70)
// ---- The same network as one block (port row y = 620)
mlp = block(MLP_Network)
mlp.move(300, 670)
mlp.resize(130, 100)
sc2 = block(Scope)
sc2.move(560, 655)
sc2.resize(70, 70)
// ---- A different kind of layer (port row y = 380)
rbf = block(RBF_Layer)
rbf.move(300, 422)
rbf.resize(126, 84)
d_rbf = block(Display)
d_rbf.move(560, 405)
d_rbf.resize(90, 50)
// ---- The same layer in int8 (port row y = 140)
quant = block(Quantized_Dense_Layer)
quant.move(1180, 182)
quant.resize(140, 84)
d_quant = block(Display)
d_quant.move(1400, 165)
d_quant.resize(90, 50)
// ---- Links
l1 = connect(src<0>, dense<0>)
l2 = connect(dense<0>, bn<0>)
l3 = connect(bn<0>, act<0>)
l4 = connect(act<0>, ln<0>)
l5 = connect(ln<0>, sm<0>)
l6 = connect(sm<0>, sc<0>)
l7 = connect(mlp<0>, sc2<0>)
l8 = connect(rbf<0>, d_rbf<0>)
l9 = connect(quant<0>, d_quant<0>)
b1 = connect(l1, mlp<0>)
b1.setCorners((200, 860), (200, 620), (305, 620))
b2 = connect(l1, rbf<0>)
b2.setCorners((200, 860), (200, 380), (305, 380))
b3 = connect(l5, quant<0>)
b3.setCorners((1100, 860), (1100, 140), (1185, 140))
a = area()
a.move(275, 960)
a.resize(1205, 200)
a.setColor(168, 152, 232)
a.setTitle("Layer by layer")
a2 = area()
a2.move(275, 725)
a2.resize(405, 205)
a2.setColor(124, 200, 168)
a2.setTitle("All of it in one block")
a3 = area()
a3.move(275, 477)
a3.resize(405, 197)
a3.setColor(232, 168, 124)
a3.setTitle("Radial basis")
a4 = area()
a4.move(1155, 237)
a4.resize(365, 197)
a4.setColor(232, 168, 124)
a4.setTitle("Quantized")
t = textbox()
t.move(280, 140)
t.setText("The top row and the second row compute the same shape of thing. Spell it out when you need to see a hidden layer; collapse it when you do not.")
Discrete Basics#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/discrete_basics.iscript · category Control Systems · kind subsystem · compiled into the binary
One staircase signal into all eight discrete primitives at once — delays, holds, differences and the discrete integrator, on their own scopes.
Places: Repeating_Sequence_Stair, Unit_Delay, Scope, Delay, Memory, Zero_Order_Hold, Tapped_Delay, Difference, Discrete_Derivative, Discrete_Time_Integrator
// @title: Discrete Basics
// @category: Control Systems
// @summary: One staircase signal into all eight discrete primitives at once — delays, holds, differences and the discrete integrator, on their own scopes.
// @kind: subsystem
//
// The point of this one is the SHARED input: a single repeating staircase is
// branched down the left of the canvas into every block, so the eight scopes
// are eight answers to the same question. Delay by one sample, delay by three,
// hold the previous value, hold at a slower rate, keep the last four, subtract
// the previous sample, divide that by the step, accumulate it.
//
// Unit Delay and Memory look alike and are not the same: Unit Delay is a
// sampled z^-1 that runs at the block's own rate, Memory holds the value from
// the previous MAJOR time step whatever the solver did in between. Zero-Order
// Hold is the one that changes the rate — its Sampling Time is set to 0.5 here
// so that its staircase is visibly coarser than the source's.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7. One lane per row, port rows 160
// apart, every block's move-y its row plus half its height. The seven branches
// all tee off the first link at the same x, so they draw as one vertical bus
// rather than seven separate lines.
src = block(Repeating_Sequence_Stair)
src.move(40, 1415)
src.resize(80, 70)
src.setConfig(Output Values, [0 1 3 2 4 1])
// ------------------------------------------------------------ delays and holds
d_unit = block(Unit_Delay)
d_unit.move(400, 1415)
d_unit.resize(90, 70)
d_unit.setConfig(Initial Condition, 0)
v_unit = block(Scope)
v_unit.move(750, 1415)
v_unit.resize(70, 70)
d_delay = block(Delay)
d_delay.move(400, 1255)
d_delay.resize(110, 70)
d_delay.setConfig(Delay Length (samples), 3)
d_delay.setConfig(Initial Condition, 0)
v_delay = block(Scope)
v_delay.move(750, 1255)
v_delay.resize(70, 70)
d_mem = block(Memory)
d_mem.move(400, 1095)
d_mem.resize(90, 70)
d_mem.setConfig(Initial Condition, 0)
v_mem = block(Scope)
v_mem.move(750, 1095)
v_mem.resize(70, 70)
d_zoh = block(Zero_Order_Hold)
d_zoh.move(400, 935)
d_zoh.resize(90, 70)
d_zoh.setConfig(Sampling Time (s), 0.5)
v_zoh = block(Scope)
v_zoh.move(750, 935)
v_zoh.resize(70, 70)
// ------------------------------------------------------------ taps and differences
d_tap = block(Tapped_Delay)
d_tap.move(400, 780)
d_tap.resize(110, 80)
d_tap.setConfig(Number of Delays, 4)
d_tap.setConfig(Initial Condition, 0)
v_tap = block(Scope)
v_tap.move(750, 775)
v_tap.resize(70, 70)
d_diff = block(Difference)
d_diff.move(400, 615)
d_diff.resize(100, 70)
d_diff.setConfig(Initial Condition (Previous Input), 0)
v_diff = block(Scope)
v_diff.move(750, 615)
v_diff.resize(70, 70)
// ------------------------------------------------------- rates and accumulation
d_der = block(Discrete_Derivative)
d_der.move(400, 460)
d_der.resize(120, 80)
d_der.setConfig(Gain Value (K), 1)
d_der.setConfig(Initial Condition (Previous Scaled Input), 0)
v_der = block(Scope)
v_der.move(750, 455)
v_der.resize(70, 70)
d_int = block(Discrete_Time_Integrator)
d_int.move(400, 300)
d_int.resize(110, 80)
d_int.setConfig(Gain (K), 1)
d_int.setConfig(Initial Condition, 0)
v_int = block(Scope)
v_int.move(750, 295)
v_int.resize(70, 70)
// The first link carries the source; every other lane BRANCHES off it, which
// is what puts them all on one riser instead of eight parallel lines.
bus = connect(src<0>, d_unit<0>)
connect(bus, d_delay<0>)
connect(bus, d_mem<0>)
connect(bus, d_zoh<0>)
connect(bus, d_tap<0>)
connect(bus, d_diff<0>)
connect(bus, d_der<0>)
connect(bus, d_int<0>)
connect(d_unit<0>, v_unit<0>)
connect(d_delay<0>, v_delay<0>)
connect(d_mem<0>, v_mem<0>)
connect(d_zoh<0>, v_zoh<0>)
connect(d_tap<0>, v_tap<0>)
connect(d_diff<0>, v_diff<0>)
connect(d_der<0>, v_der<0>)
connect(d_int<0>, v_int<0>)
a1 = area()
a1.move(15, 1470)
a1.resize(830, 640)
a1.setColor(168, 152, 232)
a1.setTitle("Delays and holds")
a2 = area()
a2.move(20, 835)
a2.resize(825, 325)
a2.setColor(232, 168, 124)
a2.setTitle("Taps and differences")
a3 = area()
a3.move(20, 515)
a3.resize(825, 325)
a3.setColor(43, 160, 184)
a3.setTitle("Rates and accumulation")
t = textbox()
t.move(40, 140)
t.setText("All eight see the same staircase. Compare the scopes, then change the source's Output Values and run again.")
Discrete Filters and Transfer Forms#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/discrete_filters_and_transfer_forms.iscript · category Control Systems · kind subsystem · compiled into the binary
One square wave through all nine discrete transfer realizations, so their step and settling behaviour can be compared side by side.
Places: Pulse_Generator, Discrete_Filter, Scope, Discrete_FIR_Filter, Discrete_Transfer_Function, Discrete_Zero_Pole, Transfer_Fcn_Direct_Form_II, Transfer_Fcn_Direct_Form_II_Time_Varying, Transfer_Fcn_First_Order, Transfer_Fcn_Lead_Or_Lag, Transfer_Fcn_Real_Zero, Constant
// @title: Discrete Filters and Transfer Forms
// @category: Control Systems
// @summary: One square wave through all nine discrete transfer realizations, so their step and settling behaviour can be compared side by side.
// @kind: subsystem
//
// Every discrete transfer form in the library, driven by one square wave.
// A square wave rather than a step on purpose: it steps repeatedly, so each
// lane shows the transient AND what it settles to, over and over, without
// having to rerun anything.
//
// The upper group states a filter as polynomials or as zeros and poles. The
// lower group is the direct-form family -- the same algebra arranged the way
// fixed-point hardware actually implements it, plus the two first-order
// special cases and the time-varying form whose coefficients arrive on
// ports rather than sitting in the config.
//
// All nine inherit the solver's rate: Sampling Time (s) is left at -1, so
// changing the solver step changes every lane together.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// lane, sizes pinned with resize(). The square wave branches to every lane,
// so the branches are the only links that step.
u = block(Pulse_Generator)
u.move(40, 1935)
u.resize(80, 70)
u.setConfig(Pulse Type, Time based)
u.setConfig(Amplitude, 1)
u.setConfig(Period, 4)
u.setConfig(Pulse Width, 50)
u.setConfig(Phase Delay, 0)
// Stated as polynomials, or as zeros and poles.
df = block(Discrete_Filter)
df.move(500, 1945)
df.resize(130, 90)
df.setConfig(Numerator, [1])
df.setConfig(Denominator, [1 -0.5])
df.setConfig(Initial States, 0)
l_u = connect(u<0>, df<0>)
df_s = block(Scope)
df_s.move(760, 1935)
df_s.resize(70, 70)
connect(df<0>, df_s<0>)
fir = block(Discrete_FIR_Filter)
fir.move(500, 1742.5)
fir.resize(130, 85)
fir.setConfig(Coefficients, [0.25 0.25 0.25 0.25])
fir.setConfig(Initial States, 0)
connect(l_u, fir<0>)
fir_s = block(Scope)
fir_s.move(760, 1735)
fir_s.resize(70, 70)
connect(fir<0>, fir_s<0>)
dtf = block(Discrete_Transfer_Function)
dtf.move(500, 1545)
dtf.resize(130, 90)
dtf.setConfig(Numerator - Discrete, [1])
dtf.setConfig(Denominator - Discrete, [1 -0.5])
connect(l_u, dtf<0>)
dtf_s = block(Scope)
dtf_s.move(760, 1535)
dtf_s.resize(70, 70)
connect(dtf<0>, dtf_s<0>)
dzp = block(Discrete_Zero_Pole)
dzp.move(500, 1340)
dzp.resize(130, 80)
dzp.setConfig(Zeros, [0])
dzp.setConfig(Poles, [0.6])
dzp.setConfig(Gain, 0.4)
connect(l_u, dzp<0>)
dzp_s = block(Scope)
dzp_s.move(760, 1335)
dzp_s.resize(70, 70)
connect(dzp<0>, dzp_s<0>)
// The direct-form family: the same algebra, arranged the way
// fixed-point hardware implements it.
df2 = block(Transfer_Fcn_Direct_Form_II)
df2.move(500, 1145)
df2.resize(130, 90)
df2.setConfig(Numerator Coefficients, [0.2 0.3 0.2])
df2.setConfig(Denominator Coefficients, [-0.9 0.6])
df2.setConfig(Initial Condition, 0)
connect(l_u, df2<0>)
df2_s = block(Scope)
df2_s.move(760, 1135)
df2_s.resize(70, 70)
connect(df2<0>, df2_s<0>)
df2tv = block(Transfer_Fcn_Direct_Form_II_Time_Varying)
df2tv.move(500, 925)
df2tv.resize(140, 100)
df2tv.setConfig(Initial Condition, 0)
connect(l_u, df2tv<0>)
df2tv_s = block(Scope)
df2tv_s.move(760, 910)
df2tv_s.resize(70, 70)
connect(df2tv<0>, df2tv_s<0>)
fo = block(Transfer_Fcn_First_Order)
fo.move(500, 740)
fo.resize(130, 80)
fo.setConfig(Pole (in Z Plane), 0.95)
fo.setConfig(Initial Condition (Previous Output), 0)
connect(l_u, fo<0>)
fo_s = block(Scope)
fo_s.move(760, 735)
fo_s.resize(70, 70)
connect(fo<0>, fo_s<0>)
ll = block(Transfer_Fcn_Lead_Or_Lag)
ll.move(500, 540)
ll.resize(120, 80)
ll.setConfig(Pole (in Z Plane), 0.95)
ll.setConfig(Zero (in Z Plane), 0.75)
ll.setConfig(Initial Condition (Previous Output), 0)
ll.setConfig(Initial Condition (Previous Input), 0)
connect(l_u, ll<0>)
ll_s = block(Scope)
ll_s.move(760, 535)
ll_s.resize(70, 70)
connect(ll<0>, ll_s<0>)
rz = block(Transfer_Fcn_Real_Zero)
rz.move(500, 340)
rz.resize(110, 80)
rz.setConfig(Zero (in Z Plane), 0.75)
rz.setConfig(Initial Condition (Previous Input), 0)
connect(l_u, rz<0>)
rz_s = block(Scope)
rz_s.move(760, 335)
rz_s.resize(70, 70)
connect(rz<0>, rz_s<0>)
// The time-varying form takes its coefficients on ports; held constant
// here, it matches the Direct Form II lane above it exactly.
num = block(Constant)
num.move(250, 910)
num.resize(70, 70)
num.setConfig(Constant Value, [0.2 0.3 0.2])
connect(num<0>, df2tv<1>)
den = block(Constant)
den.move(250, 1075)
den.resize(70, 70)
den.setConfig(Constant Value, [-0.9 0.6])
connect(den<0>, df2tv<2>)
// Group washes in the identity sheet's data accents; only the hue is ours.
a1 = area()
a1.move(455, 2010)
a1.resize(420, 815)
a1.setColor(168, 152, 232)
a1.setTitle("Polynomial and zero-pole forms")
a2 = area()
a2.move(455, 1210)
a2.resize(420, 1015)
a2.setColor(232, 168, 124)
a2.setTitle("Direct-form realizations")
t = textbox()
t.move(380, 110)
t.setText("The Direct Form II lane and the time-varying lane below it are the same filter, so their scopes should agree while the coefficient constants stay put -- drive those two constants from a source instead and only the time-varying lane will move.")
First-Order Lag#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/first_order_lag.iscript · category Control Systems · kind subsystem · compiled into the binary
A step into a 1/(s+1) transfer function, watched on a scope.
Places: Step, Transfer_Function, Scope
// @title: First-Order Lag
// @category: Control Systems
// @summary: A step into a 1/(s+1) transfer function, watched on a scope.
// @kind: subsystem
//
// The smallest thing that is still a simulation: a source, a plant, a scope.
// Useful as the starting point for trying a solver setting, or as a shape to
// copy when wiring up a bigger model.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: the whole diagram sits in the
// first quadrant (the canvas opens with the origin anchor at the bottom-left
// of the view), and every block's port row is at y = 260 (move() places the
// top edge; ports sit at the vertical center), so both links are straight.
src = block(Step)
src.move(40, 295)
src.resize(70, 70)
src.setConfig(Initial Value, 0)
src.setConfig(Final Value, 1)
plant = block(Transfer_Function)
plant.move(240, 305)
plant.resize(130, 90)
plant.setConfig(Numerator, [1])
plant.setConfig(Denominator, [1, 1])
out = block(Scope)
out.move(460, 295)
out.resize(70, 70)
connect(src<0>, plant<0>)
connect(plant<0>, out<0>)
Increment and Decrement Steppers#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/increment_decrement_steppers.iscript · category Control Systems · kind subsystem · compiled into the binary
All six one-step blocks reading the same staircase, with the input itself on a seventh scope for comparison.
Places: Repeating_Sequence_Stair, Increment_Real_World, Scope, Increment_Stored_Integer, Decrement_Real_World, Decrement_Stored_Integer, Decrement_Time_To_Zero, Decrement_To_Zero
// @title: Increment and Decrement Steppers
// @category: Control Systems
// @summary: All six one-step blocks reading the same staircase, with the input itself on a seventh scope for comparison.
// @kind: subsystem
//
// Six blocks that each move a signal by exactly one step, and the differences
// between them are the whole point:
//
// Increment / Decrement Real World u + 1, u - 1 in the signal's own units.
// Increment / Decrement Stored Integer the same step applied to the STORED
// integer. On a double signal the two agree; on a scaled fixed-point one
// they do not, which is the reason both exist.
// Decrement To Zero u - 1, but never below zero.
// Decrement Time To Zero u - the block's own sample time, never below zero
// -- a countdown in seconds rather than in counts, which is why its
// Sampling Time is set here rather than inherited.
//
// The bottom scope carries the raw input, so every other scope can be read as
// "the input, moved by one step" rather than in isolation. The staircase has a
// 0.5 and a 0 in it deliberately: those are the samples where the two clamped
// blocks stop tracking their unclamped neighbours.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: one lane per row 160 apart, every
// block 70 tall so each move-y is its row + 35, and the six branches share one
// riser off the first link.
src = block(Repeating_Sequence_Stair)
src.move(40, 1255)
src.resize(80, 70)
src.setConfig(Output Values, [3 1 0.5 2 0])
// ------------------------------------------------------------------ increment
i_rw = block(Increment_Real_World)
i_rw.move(400, 1255)
i_rw.resize(70, 70)
v_irw = block(Scope)
v_irw.move(750, 1255)
v_irw.resize(70, 70)
i_si = block(Increment_Stored_Integer)
i_si.move(400, 1095)
i_si.resize(70, 70)
v_isi = block(Scope)
v_isi.move(750, 1095)
v_isi.resize(70, 70)
// ------------------------------------------------------------------ decrement
d_rw = block(Decrement_Real_World)
d_rw.move(400, 935)
d_rw.resize(70, 70)
v_drw = block(Scope)
v_drw.move(750, 935)
v_drw.resize(70, 70)
d_si = block(Decrement_Stored_Integer)
d_si.move(400, 775)
d_si.resize(70, 70)
v_dsi = block(Scope)
v_dsi.move(750, 775)
v_dsi.resize(70, 70)
// -------------------------------------------------------- decrement, clamped at zero
d_ttz = block(Decrement_Time_To_Zero)
d_ttz.move(400, 615)
d_ttz.resize(70, 70)
d_ttz.setConfig(Sampling Time (s), 0.2)
v_dttz = block(Scope)
v_dttz.move(750, 615)
v_dttz.resize(70, 70)
d_tz = block(Decrement_To_Zero)
d_tz.move(400, 455)
d_tz.resize(70, 70)
v_dtz = block(Scope)
v_dtz.move(750, 455)
v_dtz.resize(70, 70)
// ------------------------------------------------------------------ the input itself
v_src = block(Scope)
v_src.move(400, 295)
v_src.resize(70, 70)
bus = connect(src<0>, i_rw<0>)
connect(bus, i_si<0>)
connect(bus, d_rw<0>)
connect(bus, d_si<0>)
connect(bus, d_ttz<0>)
connect(bus, d_tz<0>)
connect(bus, v_src<0>)
connect(i_rw<0>, v_irw<0>)
connect(i_si<0>, v_isi<0>)
connect(d_rw<0>, v_drw<0>)
connect(d_si<0>, v_dsi<0>)
connect(d_ttz<0>, v_dttz<0>)
connect(d_tz<0>, v_dtz<0>)
a1 = area()
a1.move(15, 1310)
a1.resize(830, 320)
a1.setColor(168, 152, 232)
a1.setTitle("Increment")
a2 = area()
a2.move(20, 990)
a2.resize(825, 320)
a2.setColor(232, 168, 124)
a2.setTitle("Decrement")
a3 = area()
a3.move(20, 670)
a3.resize(825, 320)
a3.setColor(43, 160, 184)
a3.setTitle("Decrement, clamped at zero")
a4 = area()
a4.move(20, 350)
a4.resize(810, 160)
a4.setColor(48, 160, 106)
a4.setTitle("The input, for comparison")
t = textbox()
t.move(40, 140)
t.setText("Read every scope against the bottom one. The 0.5 and the 0 in the staircase are where the clamped blocks part company with the others.")
Integrators and Constraints#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/integrators_and_constraints.iscript · category Control Systems · kind subsystem · compiled into the binary
The three integrating blocks with their limiters engaged, plus an algebraic loop solving 2z - 3 = 0.
Places: Step, Integrator, Scope, Sine_Wave, Weighted_Sample_Time_Math, Second_Order_Integrator, Mux, Algebraic_Constraint, Gain, Bias
// @title: Integrators and Constraints
// @category: Control Systems
// @summary: The three integrating blocks with their limiters engaged, plus an algebraic loop solving 2z - 3 = 0.
// @kind: subsystem
//
// Four independent lanes, simplest at the bottom. The first three show what an
// integrator does when its LIMITER is reached -- the default limits are +/-1e308,
// which never clip, so every lane here is deliberately tuned to hit them. The top
// lane is the odd one out: an algebraic constraint has no dynamics of its own, it
// relaxes a residual to zero, so it is drawn as a ring rather than a chain.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per lane
// (260 / 500 / 740 / 1000), sizes pinned with resize() so alignment cannot drift.
// move() places a block's TOP edge and port k of n sits at moveY - h*(k+1)/(n+1),
// so a single-port block's row is moveY - h/2 and every move-y below is
// "port row + half the height".
// ---------------------------------------------------------------- lane 1: Integrator
// A step into 1/s is a ramp, and the ramp runs into the upper limit at t = 3 s.
step1 = block(Step)
step1.move(40, 295)
step1.resize(70, 70)
step1.setConfig(Step Time (sec), 1)
step1.setConfig(Initial Value, 0)
step1.setConfig(Final Value, 1)
integ = block(Integrator)
integ.move(240, 295)
integ.resize(70, 70)
integ.setConfig(Initial Value, 0)
integ.setConfig(Upper Saturation Limit, 2)
integ.setConfig(Lower Saturation Limit, -2)
scope1 = block(Scope)
scope1.move(440, 295)
scope1.resize(70, 70)
connect(step1<0>, integ<0>)
connect(integ<0>, scope1<0>)
// ------------------------------------- lane 2: Weighted Sample Time Math
// The one block whose arithmetic depends on the SOLVER's rate rather than on its
// own config alone: y = u * (Ts*w). At the default 0.1 s step and w = 20 that is
// a gain of exactly 2, so changing the model's sample time visibly rescales it.
sine2 = block(Sine_Wave)
sine2.move(40, 535)
sine2.resize(70, 70)
sine2.setConfig(Amplitude, 1)
sine2.setConfig(Frequency (Rad), 2)
wstm = block(Weighted_Sample_Time_Math)
wstm.move(240, 535)
wstm.resize(90, 70)
wstm.setConfig(Operation, "u * (Ts*w)")
wstm.setConfig(Weight, 20)
scope2 = block(Scope)
scope2.move(460, 535)
scope2.resize(70, 70)
connect(sine2<0>, wstm<0>)
connect(wstm<0>, scope2<0>)
// -------------------------------------- lane 3: Second-Order Integrator
// One block, two states: x and dx/dt, each with its own pair of limits. A unit
// step gives dx/dt = t (clipped at 2) and x = t^2/2 (clipped at 3), so both
// limiters engage inside a 10 s run and the two curves separate on the scope.
// The Mux is resized to 80 tall ON PURPOSE: port k of n sits at
// moveY - h*(k+1)/(n+1), so a 2-port block only aligns with another 2-port block
// of the SAME height. At 80 tall both blocks put their ports at 753 and 727 and
// the two links are dead straight; at the Mux's default 60 they would jog.
step3 = block(Step)
step3.move(40, 775)
step3.resize(70, 70)
step3.setConfig(Step Time (sec), 1)
step3.setConfig(Initial Value, 0)
step3.setConfig(Final Value, 1)
soi = block(Second_Order_Integrator)
soi.move(240, 780)
soi.resize(80, 80)
soi.setConfig(Initial Value x, 0)
soi.setConfig(Initial Value dx/dt, 0)
soi.setConfig(Upper Limit x, 3)
soi.setConfig(Lower Limit x, -3)
soi.setConfig(Upper Limit dx/dt, 2)
soi.setConfig(Lower Limit dx/dt, -2)
mux3 = block(Mux)
mux3.move(440, 780)
mux3.resize(20, 80)
scope3 = block(Scope)
scope3.move(540, 775)
scope3.resize(70, 70)
connect(step3<0>, soi<0>)
connect(soi<0>, mux3<0>)
connect(soi<1>, mux3<1>)
connect(mux3<0>, scope3<0>)
// ------------------------------------------- lane 4: Algebraic Constraint
// A RING, not a chain: the block drives its own output z until the residual on
// its input reaches zero. Here the residual is built as f(z) = 2z - 3, so the
// loop settles at z = 1.5 -- the solution of an equation the diagram never
// solves symbolically.
//
// Convergence is the whole point of this lane, so the numbers are chosen for it
// rather than for looks. The block relaxes dz/dt = -K*f, which is stable while
// K*df/dz is positive and K*df/dz*Ts < 2. Here df/dz = 2 and Ts = 0.1, so the
// gain K = 2 gives a per-step factor of 1 - 2*2*0.1 = 0.6: it settles in about
// ten samples and cannot ring. K = 5 would land exactly on dead-beat and
// anything past it oscillates. The initial guess is -1 rather than the default 0
// so there is a transient to watch at all.
// The ring starts at x = 80 rather than 40 so the return riser has a margin of
// its own: left to itself the planner brought the feedback back at x = 15,
// hugging the origin anchor, and ran it 25 above the block tops.
ac = block(Algebraic_Constraint)
ac.move(80, 1035)
ac.resize(80, 70)
ac.setConfig(Constraint, "f(z) = 0")
ac.setConfig(Initial Guess, -1)
ac.setConfig(Solver Gain, 2)
acgain = block(Gain)
acgain.move(300, 1035)
acgain.resize(70, 70)
acgain.setConfig(Gain Value, 2)
acbias = block(Bias)
acbias.move(480, 1035)
acbias.resize(70, 70)
acbias.setConfig(Bias, -3)
fscope = block(Scope)
fscope.move(680, 1035)
fscope.resize(70, 70)
zscope = block(Scope)
zscope.move(300, 1275)
zscope.resize(70, 70)
lz = connect(ac<0>, acgain<0>)
connect(acgain<0>, acbias<0>)
lf = connect(acbias<0>, fscope<0>)
fb = connect(lf, ac<0>)
connect(lz, zscope<0>)
// Pinned, not invented: read back from the canonical save. The corridor sits at
// y = 900 -- the ring's deepest frame bottom is 965 and its name labels hang to
// about 945, so this clears the labels by the ~50 §7 asks for, and it stays
// above lane 3, whose area tops out at 850.
fb.setCorners((585, 1000), (585, 900), (45, 900), (45, 1000), (85, 1000))
// Group washes in the identity sheet's data accents. Only the HUE is ours -- the
// theme re-imposes its own alpha on every area, so none is passed here.
a1 = area()
a1.move(15, 360)
a1.resize(520, 160)
a1.setColor(168, 152, 232)
a1.setTitle("Integrator (limits at +/-2)")
a2 = area()
a2.move(15, 600)
a2.resize(555, 160)
a2.setColor(232, 168, 124)
a2.setTitle("Weighted Sample Time Math")
a3 = area()
a3.move(15, 850)
a3.resize(620, 175)
a3.setColor(124, 200, 176)
a3.setTitle("Second-Order Integrator (x and dx/dt)")
a4 = area()
a4.move(20, 1330)
a4.resize(755, 470)
a4.setColor(232, 152, 176)
a4.setTitle("Algebraic Constraint: solve 2z - 3 = 0")
t = textbox()
t.move(40, 110)
t.setText("Every limiter here is engaged on purpose -- the defaults are +/-1e308 and never clip. Watch the top ring settle: z reaches 1.5 and the residual reaches 0.")
Lookup Tables Tour#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/lookup_tables_tour.iscript · category Control Systems · kind subsystem · compiled into the binary
One sweep from -5 to +5 through every lookup form — 1D, 2D, nD, direct-index, runtime table, the prelookup pair, and fixed-point sine and cosine.
Places: Ramp, Sine_Wave, Constant, Lookup_Table_1D, Prelookup, Interpolation_Using_Prelookup, Lookup_Table_2D, Lookup_Table_nD, Direct_Lookup_Table_nD, Lookup_Table_Dynamic, Sine, Cosine, Scope, Display
// @title: Lookup Tables Tour
// @category: Control Systems
// @summary: One sweep from -5 to +5 through every lookup form — 1D, 2D, nD, direct-index, runtime table, the prelookup pair, and fixed-point sine and cosine.
// @kind: subsystem
//
// Every way this library has of turning a number into another number by
// table, driven by one ramp so the shapes are directly comparable:
//
// Lookup Table 1D breakpoints and data, interpolated. The shipped table
// is a tanh, which is why lane one looks like a squash.
// Prelookup + the same lookup split in two: Prelookup finds the
// Interpolation interval (k) and the fraction within it (f) once, and
// any number of Interpolation blocks reuse that work.
// This is the form to reach for when several tables
// share one axis.
// Lookup Table 2D two independent axes into a grid.
// Lookup Table nD the same idea in three axes.
// Direct Lookup nD no interpolation at all: the inputs ARE indices, so
// this one is fed integers, not the sweep.
// Lookup Table Dynamic the table itself arrives on ports, so it can change
// while the model runs.
// Sine / Cosine quarter-wave fixed-point tables — the trig an FPGA or
// a small MCU actually runs, on one scope together.
//
// The sweep starts at -5 so it lands inside the shipped breakpoint ranges
// rather than extrapolating off the end of them, and every table's
// breakpoints here are set to match it. Change the ramp and the lanes start
// telling you about each block's extrapolation setting instead.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// lane, sizes pinned with resize(). The odd-height tables are pinned to an
// EVEN height on purpose — a block's output port sits at its vertical centre,
// so an odd height puts that centre on a half-pixel and every sink link comes
// back as a 1px staircase instead of a straight line.
// ---- one sweep, plus the side inputs the multi-axis tables need
sweep = block(Ramp)
sweep.move(40, 935)
sweep.resize(70, 70)
sweep.setConfig(Slope, 1)
sweep.setConfig(Start Time (s), 0)
sweep.setConfig(Initial Output, -5)
axis2 = block(Sine_Wave)
axis2.move(40, 775)
axis2.resize(70, 70)
axis2.setConfig(Amplitude, 5)
axis2.setConfig(Frequency (Rad), 0.5)
axis3 = block(Constant)
axis3.move(40, 655)
axis3.resize(70, 70)
axis3.setConfig(Constant Value, 0.5)
index = block(Constant)
index.move(40, 535)
index.resize(70, 70)
index.setConfig(Constant Value, 1)
xdata = block(Constant)
xdata.move(40, 415)
xdata.resize(70, 70)
xdata.setConfig(Constant Value, [-5 0 5])
ydata = block(Constant)
ydata.move(40, 295)
ydata.resize(70, 70)
ydata.setConfig(Constant Value, [-1 0 1])
// ---- the interpolated tables
oneD = block(Lookup_Table_1D)
oneD.move(420, 1435)
oneD.resize(80, 70)
pre = block(Prelookup)
pre.move(420, 1275)
pre.resize(90, 70)
pre.setConfig(Breakpoints, [-5 -2.5 0 2.5 5])
interp = block(Interpolation_Using_Prelookup)
interp.move(620, 1275)
interp.resize(95, 70)
interp.setConfig(Table Data, [0 0.5 1.5 3 5])
twoD = block(Lookup_Table_2D)
twoD.move(420, 1118)
twoD.resize(85, 76)
twoD.setConfig(Row Breakpoints, [-5 0 5])
twoD.setConfig(Column Breakpoints, [-5 0 5])
nD = block(Lookup_Table_nD)
nD.move(420, 964)
nD.resize(90, 88)
nD.setConfig(Breakpoints 1, [-5 0 5])
nD.setConfig(Breakpoints 2, [-5 0 5])
nD.setConfig(Breakpoints 3, [0 1])
// ---- the two that do not interpolate the sweep
direct = block(Direct_Lookup_Table_nD)
direct.move(420, 798)
direct.resize(85, 76)
dynamic = block(Lookup_Table_Dynamic)
dynamic.move(420, 640)
dynamic.resize(95, 80)
// ---- fixed-point trig
sine = block(Sine)
sine.move(420, 475)
sine.resize(75, 70)
sine.setConfig(Number Of Data Points, 33)
cosine = block(Cosine)
cosine.move(420, 335)
cosine.resize(75, 70)
cosine.setConfig(Number Of Data Points, 33)
// ---- one sink per lane, on that lane's port row
sOneD = block(Scope)
sOneD.move(860, 1435)
sOneD.resize(70, 70)
sInterp = block(Scope)
sInterp.move(860, 1275)
sInterp.resize(70, 70)
sTwoD = block(Scope)
sTwoD.move(860, 1115)
sTwoD.resize(70, 70)
sND = block(Scope)
sND.move(860, 955)
sND.resize(70, 70)
sDirect = block(Display)
sDirect.move(860, 785)
sDirect.resize(90, 50)
sDynamic = block(Scope)
sDynamic.move(860, 635)
sDynamic.resize(70, 70)
sTrig = block(Scope)
sTrig.move(860, 405)
sTrig.resize(70, 70)
sTrig.clearPorts()
sTrig.addPort(in, ICoreDouble, West)
sTrig.addPort(in, ICoreDouble, West)
// ---- the sweep reaches every interpolating table and both trig tables
drive = connect(sweep<0>, oneD<0>)
connect(drive, pre<0>)
connect(drive, twoD<0>)
connect(drive, nD<0>)
connect(drive, dynamic<0>)
connect(drive, sine<0>)
connect(drive, cosine<0>)
second = connect(axis2<0>, twoD<1>)
connect(second, nD<1>)
connect(axis3<0>, nD<2>)
idxSig = connect(index<0>, direct<0>)
connect(idxSig, direct<1>)
connect(xdata<0>, dynamic<1>)
connect(ydata<0>, dynamic<2>)
// The prelookup pair: interval and fraction travel together to the
// interpolation, which is the whole point of splitting them.
connect(pre<0>, interp<0>)
connect(pre<1>, interp<1>)
connect(oneD<0>, sOneD<0>)
connect(interp<0>, sInterp<0>)
connect(twoD<0>, sTwoD<0>)
connect(nD<0>, sND<0>)
connect(direct<0>, sDirect<0>)
connect(dynamic<0>, sDynamic<0>)
connect(sine<0>, sTrig<0>)
connect(cosine<0>, sTrig<1>)
// Group washes, in the identity sheet's data accents.
a = area()
a.move(15, 990)
a.resize(285, 790)
a.setColor(168, 152, 232)
a.setTitle("Sweep and axis inputs")
a2 = area()
a2.move(395, 1490)
a2.resize(565, 638)
a2.setColor(232, 168, 124)
a2.setTitle("Interpolated tables")
a3 = area()
a3.move(395, 852)
a3.resize(580, 317)
a3.setColor(124, 200, 232)
a3.setTitle("Direct index, and a table that arrives on ports")
a4 = area()
a4.move(395, 530)
a4.resize(565, 290)
a4.setColor(152, 216, 168)
a4.setTitle("Fixed-point trig")
t = textbox()
t.move(400, 200)
t.setText("Direct Lookup takes indices, not values, so it is fed a constant rather than the sweep. The dynamic table gets its breakpoints and data on ports — drive those from anything and the curve itself changes while the model runs.")
LPV Continuous Suite#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/lpv_continuous_suite.iscript · category Control Systems · kind subsystem · compiled into the binary
Every continuous Linear Parameter Varying block, all six retuned live by one shared scheduling signal.
Places: Sine_Wave, Varying_Delay, Scope, Varying_Transfer_Function, Constant, Varying_PID_Controller, Varying_State_Space, Mux, Varying_Observer_Form, Varying_2DOF_PID
// @title: LPV Continuous Suite
// @category: Control Systems
// @summary: Every continuous Linear Parameter Varying block, all six retuned live by one shared scheduling signal.
// @kind: subsystem
//
// ONE scheduling signal, every varying block in the family hanging off it. That is
// the whole idea of a linear parameter-varying model: the coefficients are ports,
// not settings, so a single measured quantity can retune the entire diagram while
// it runs.
//
// theta is a slow sine bounded to [0.3, 0.7], and the bound is load-bearing rather
// than decorative -- every lane below was chosen so that the block stays STABLE
// across that whole range. Where scheduling the obvious coefficient would have
// been unstable, a different one is scheduled and the lane comment says why.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per lane,
// sizes pinned with resize(). Port k of n sits at moveY - h*(k+1)/(n+1), so every
// block below is resized to a height DIVISIBLE BY n+1 -- that is what puts its
// port rows on whole numbers instead of a third of a pixel.
//
// The fixed coefficients fan in from a column of Constants on the left. Ports 18
// to 30 apart cannot be met head-on by 70-tall Constants, so those links staircase
// by design; the SIGNAL path into port 0 of each block is the one kept straight.
// ---------------------------------------------------- the shared scheduling signal
// Amplitude 0.2 about a bias of 0.5 keeps theta in [0.3, 0.7] and never at zero.
// Slow on purpose: at 0.4 rad/s it takes ~16 s to complete a cycle, so a 10 s run
// shows the coefficients travelling rather than buzzing.
sched = block(Sine_Wave)
sched.move(40, 195)
sched.resize(70, 70)
sched.setConfig(Amplitude, 0.2)
sched.setConfig(Frequency (Rad), 0.4)
sched.setConfig(Bias, 0.5)
// The signal under test: fast enough that a filter whose bandwidth is moving has
// something to bite on.
sig = block(Sine_Wave)
sig.move(40, 315)
sig.resize(70, 70)
sig.setConfig(Amplitude, 1)
sig.setConfig(Frequency (Rad), 6)
// A separate measurement channel, used only by the 2DOF lane, which needs r and y
// to be genuinely different signals or its setpoint weights do nothing.
meas = block(Sine_Wave)
meas.move(40, 435)
meas.resize(70, 70)
meas.setConfig(Amplitude, 0.4)
meas.setConfig(Frequency (Rad), 2.5)
// --------------------------------------------------------------------------
// Varying Delay -- the transport delay itself is scheduled: d rides between 0.3 s and 0.7 s
vdelay = block(Varying_Delay)
vdelay.move(680, 295)
vdelay.resize(110, 90)
vdelay_sc = block(Scope)
vdelay_sc.move(930, 285)
vdelay_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Varying Transfer Function -- y = (b0*s + b1)/(s + a1)*u with b0=0, b1=1: a first-order lowpass whose
// pole -a1 is scheduled, so its bandwidth breathes
vtransfer_func = block(Varying_Transfer_Function)
vtransfer_func.move(680, 600)
vtransfer_func.resize(130, 100)
// the b0 coefficient, held fixed
vtransfer_func_b0 = block(Constant)
vtransfer_func_b0.move(430, 535)
vtransfer_func_b0.resize(70, 70)
vtransfer_func_b0.setConfig(Constant Value, 0)
// the b1 coefficient, held fixed
vtransfer_func_b1 = block(Constant)
vtransfer_func_b1.move(430, 635)
vtransfer_func_b1.resize(70, 70)
vtransfer_func_b1.setConfig(Constant Value, 1)
vtransfer_func_sc = block(Scope)
vtransfer_func_sc.move(930, 585)
vtransfer_func_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Varying PID Controller -- gain scheduling in its most literal form: P moves, I, D and N hold
vpid_controlle = block(Varying_PID_Controller)
vpid_controlle.move(680, 1010)
vpid_controlle.resize(140, 120)
// the I coefficient, held fixed
vpid_controlle_I = block(Constant)
vpid_controlle_I.move(430, 885)
vpid_controlle_I.resize(70, 70)
vpid_controlle_I.setConfig(Constant Value, 0.2)
// the D coefficient, held fixed
vpid_controlle_D = block(Constant)
vpid_controlle_D.move(430, 985)
vpid_controlle_D.resize(70, 70)
vpid_controlle_D.setConfig(Constant Value, 0.05)
// the N coefficient, held fixed
vpid_controlle_N = block(Constant)
vpid_controlle_N.move(430, 1085)
vpid_controlle_N.resize(70, 70)
vpid_controlle_N.setConfig(Constant Value, 20)
vpid_controlle_sc = block(Scope)
vpid_controlle_sc.move(930, 985)
vpid_controlle_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Varying State Space -- B is the scheduled matrix, NOT A: dx/dt = A*x + B*u is stable only while
// A stays negative, and the shared theta is positive by construction
vstate_space = block(Varying_State_Space)
vstate_space.move(680, 1410)
vstate_space.resize(150, 120)
// the A coefficient, held fixed
vstate_space_A = block(Constant)
vstate_space_A.move(430, 1285)
vstate_space_A.resize(70, 70)
vstate_space_A.setConfig(Constant Value, -1)
// the C coefficient, held fixed
vstate_space_C = block(Constant)
vstate_space_C.move(430, 1385)
vstate_space_C.resize(70, 70)
vstate_space_C.setConfig(Constant Value, 1)
// the D coefficient, held fixed
vstate_space_D = block(Constant)
vstate_space_D.move(430, 1485)
vstate_space_D.resize(70, 70)
vstate_space_D.setConfig(Constant Value, 0)
// The Mux is resized to the block's OWN height and given the same move-y, so
// its three input rows coincide exactly with the three output rows and all
// three links are dead straight.
vstate_space_mux = block(Mux)
vstate_space_mux.move(930, 1410)
vstate_space_mux.resize(20, 120)
vstate_space_mux.addPort(in)
vstate_space_sc = block(Scope)
vstate_space_sc.move(1020, 1385)
vstate_space_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Varying Observer Form -- the observer GAIN L is scheduled. Effective dynamics are A - B*K - L*C =
// -1.5 - L, negative for every value theta takes, so the estimator cannot
// be scheduled into instability
vobserver_form = block(Varying_Observer_Form)
vobserver_form.move(680, 1976)
vobserver_form.resize(160, 152)
// the A coefficient, held fixed
vobserver_form_A = block(Constant)
vobserver_form_A.move(430, 1735)
vobserver_form_A.resize(70, 70)
vobserver_form_A.setConfig(Constant Value, -1)
// the B coefficient, held fixed
vobserver_form_B = block(Constant)
vobserver_form_B.move(430, 1835)
vobserver_form_B.resize(70, 70)
vobserver_form_B.setConfig(Constant Value, 1)
// the C coefficient, held fixed
vobserver_form_C = block(Constant)
vobserver_form_C.move(430, 1935)
vobserver_form_C.resize(70, 70)
vobserver_form_C.setConfig(Constant Value, 1)
// the D coefficient, held fixed
vobserver_form_D = block(Constant)
vobserver_form_D.move(430, 2035)
vobserver_form_D.resize(70, 70)
vobserver_form_D.setConfig(Constant Value, 0)
// the K coefficient, held fixed
vobserver_form_K = block(Constant)
vobserver_form_K.move(430, 2135)
vobserver_form_K.resize(70, 70)
vobserver_form_K.setConfig(Constant Value, 0.5)
// The Mux is resized to the block's OWN height and given the same move-y, so
// its three input rows coincide exactly with the three output rows and all
// three links are dead straight.
vobserver_form_mux = block(Mux)
vobserver_form_mux.move(930, 1976)
vobserver_form_mux.resize(20, 152)
vobserver_form_mux.addPort(in)
vobserver_form_sc = block(Scope)
vobserver_form_sc.move(1020, 1935)
vobserver_form_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Varying 2DOF PID -- the only lane with two signal inputs: the setpoint r and a separate
// measurement y, so the setpoint weights b and c actually do something
v2dof_pid = block(Varying_2DOF_PID)
v2dof_pid.move(680, 2581)
v2dof_pid.resize(150, 162)
// the I coefficient, held fixed
v2dof_pid_I = block(Constant)
v2dof_pid_I.move(430, 2335)
v2dof_pid_I.resize(70, 70)
v2dof_pid_I.setConfig(Constant Value, 0.2)
// the D coefficient, held fixed
v2dof_pid_D = block(Constant)
v2dof_pid_D.move(430, 2435)
v2dof_pid_D.resize(70, 70)
v2dof_pid_D.setConfig(Constant Value, 0.05)
// the N coefficient, held fixed
v2dof_pid_N = block(Constant)
v2dof_pid_N.move(430, 2535)
v2dof_pid_N.resize(70, 70)
v2dof_pid_N.setConfig(Constant Value, 20)
// the b coefficient, held fixed
v2dof_pid_b = block(Constant)
v2dof_pid_b.move(430, 2635)
v2dof_pid_b.resize(70, 70)
v2dof_pid_b.setConfig(Constant Value, 1)
// the c coefficient, held fixed
v2dof_pid_c = block(Constant)
v2dof_pid_c.move(430, 2735)
v2dof_pid_c.resize(70, 70)
v2dof_pid_c.setConfig(Constant Value, 0)
v2dof_pid_sc = block(Scope)
v2dof_pid_sc.move(930, 2535)
v2dof_pid_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Wiring. A source feeding many ports is ONE link plus branches off it: the first
// connect() binds a handle, every later one branches that handle rather than
// starting a second link from the same output port.
lsig = connect(sig<0>, vdelay<0>)
lsched = connect(sched<0>, vdelay<1>)
connect(vdelay<0>, vdelay_sc<0>)
connect(lsig, vtransfer_func<0>)
connect(vtransfer_func_b0<0>, vtransfer_func<1>)
connect(lsched, vtransfer_func<2>)
connect(vtransfer_func_b1<0>, vtransfer_func<3>)
connect(vtransfer_func<0>, vtransfer_func_sc<0>)
connect(lsig, vpid_controlle<0>)
connect(lsched, vpid_controlle<1>)
connect(vpid_controlle_I<0>, vpid_controlle<2>)
connect(vpid_controlle_D<0>, vpid_controlle<3>)
connect(vpid_controlle_N<0>, vpid_controlle<4>)
connect(vpid_controlle<0>, vpid_controlle_sc<0>)
connect(lsig, vstate_space<0>)
connect(vstate_space_A<0>, vstate_space<1>)
connect(lsched, vstate_space<2>)
connect(vstate_space_C<0>, vstate_space<3>)
connect(vstate_space_D<0>, vstate_space<4>)
connect(vstate_space<0>, vstate_space_mux<0>)
connect(vstate_space<1>, vstate_space_mux<1>)
connect(vstate_space<2>, vstate_space_mux<2>)
connect(vstate_space_mux<0>, vstate_space_sc<0>)
connect(lsig, vobserver_form<0>)
connect(vobserver_form_A<0>, vobserver_form<1>)
connect(vobserver_form_B<0>, vobserver_form<2>)
connect(vobserver_form_C<0>, vobserver_form<3>)
connect(vobserver_form_D<0>, vobserver_form<4>)
connect(vobserver_form_K<0>, vobserver_form<5>)
connect(lsched, vobserver_form<6>)
connect(vobserver_form<0>, vobserver_form_mux<0>)
connect(vobserver_form<1>, vobserver_form_mux<1>)
connect(vobserver_form<2>, vobserver_form_mux<2>)
connect(vobserver_form_mux<0>, vobserver_form_sc<0>)
connect(lsig, v2dof_pid<0>)
lmeas = connect(meas<0>, v2dof_pid<1>)
connect(lsched, v2dof_pid<2>)
connect(v2dof_pid_I<0>, v2dof_pid<3>)
connect(v2dof_pid_D<0>, v2dof_pid<4>)
connect(v2dof_pid_N<0>, v2dof_pid<5>)
connect(v2dof_pid_b<0>, v2dof_pid<6>)
connect(v2dof_pid_c<0>, v2dof_pid<7>)
connect(v2dof_pid<0>, v2dof_pid_sc<0>)
t = textbox()
t.move(200, 90)
t.setText("One scheduling signal, six varying blocks. theta is bounded to [0.3, 0.7] because each lane is only stable over that range -- widen it and read the lane comments first.")
LPV Discrete Suite#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/lpv_discrete_suite.iscript · category Control Systems · kind subsystem · compiled into the binary
Every discrete Linear Parameter Varying block, all seven retuned live by one shared scheduling signal.
Places: Sine_Wave, Gain, Discrete_Varying_Lowpass, Scope, Discrete_Varying_Transfer_Function, Constant, Discrete_Varying_PID, Discrete_Varying_State_Space, Mux, Discrete_Varying_Notch, Discrete_Varying_Observer_Form, Discrete_Varying_2DOF_PID
// @title: LPV Discrete Suite
// @category: Control Systems
// @summary: Every discrete Linear Parameter Varying block, all seven retuned live by one shared scheduling signal.
// @kind: subsystem
//
// ONE scheduling signal, every varying block in the family hanging off it. That is
// the whole idea of a linear parameter-varying model: the coefficients are ports,
// not settings, so a single measured quantity can retune the entire diagram while
// it runs.
//
// theta is a slow sine bounded to [0.3, 0.7], and the bound is load-bearing rather
// than decorative -- every lane below was chosen so that the block stays STABLE
// across that whole range. Where scheduling the obvious coefficient would have
// been unstable, a different one is scheduled and the lane comment says why.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per lane,
// sizes pinned with resize(). Port k of n sits at moveY - h*(k+1)/(n+1), so every
// block below is resized to a height DIVISIBLE BY n+1 -- that is what puts its
// port rows on whole numbers instead of a third of a pixel.
//
// The fixed coefficients fan in from a column of Constants on the left. Ports 18
// to 30 apart cannot be met head-on by 70-tall Constants, so those links staircase
// by design; the SIGNAL path into port 0 of each block is the one kept straight.
// ---------------------------------------------------- the shared scheduling signal
// Amplitude 0.2 about a bias of 0.5 keeps theta in [0.3, 0.7] and never at zero.
// Slow on purpose: at 0.4 rad/s it takes ~16 s to complete a cycle, so a 10 s run
// shows the coefficients travelling rather than buzzing.
sched = block(Sine_Wave)
sched.move(40, 195)
sched.resize(70, 70)
sched.setConfig(Amplitude, 0.2)
sched.setConfig(Frequency (Rad), 0.4)
sched.setConfig(Bias, 0.5)
// The signal under test: fast enough that a filter whose bandwidth is moving has
// something to bite on.
sig = block(Sine_Wave)
sig.move(40, 315)
sig.resize(70, 70)
sig.setConfig(Amplitude, 1)
sig.setConfig(Frequency (Rad), 6)
// A separate measurement channel, used only by the 2DOF lane, which needs r and y
// to be genuinely different signals or its setpoint weights do nothing.
meas = block(Sine_Wave)
meas.move(40, 435)
meas.resize(70, 70)
meas.setConfig(Amplitude, 0.4)
meas.setConfig(Frequency (Rad), 2.5)
// Two lanes want a frequency in rad/s rather than a coefficient near 1, so theta
// is rescaled once here and branched from, instead of every lane carrying its own
// shaper. 8*theta sweeps 2.4 to 5.6 rad/s across the input's own 6 rad/s.
schedw = block(Gain)
schedw.move(200, 195)
schedw.resize(70, 70)
schedw.setConfig(Gain Value, 8)
// --------------------------------------------------------------------------
// Discrete Varying Lowpass -- the cutoff w0 arrives as a signal in rad/s; it is the one port that wants a
// wider sweep than the shared theta, so it takes the scaled branch
dvlowpass = block(Discrete_Varying_Lowpass)
dvlowpass.move(680, 295)
dvlowpass.resize(130, 90)
dvlowpass_sc = block(Scope)
dvlowpass_sc.move(930, 285)
dvlowpass_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Discrete Varying Transfer Function -- the DISCRETE first-order form, realized transposed direct form II -- the
// realization that matters the moment coefficients move (see the LPV notes
// in BLOCKS_TO_ADD.md)
dvtransfer_fun = block(Discrete_Varying_Transfer_Function)
dvtransfer_fun.move(680, 605)
dvtransfer_fun.resize(140, 110)
// the b0 coefficient, held fixed
dvtransfer_fun_b0 = block(Constant)
dvtransfer_fun_b0.move(430, 535)
dvtransfer_fun_b0.resize(70, 70)
dvtransfer_fun_b0.setConfig(Constant Value, 0)
// the b1 coefficient, held fixed
dvtransfer_fun_b1 = block(Constant)
dvtransfer_fun_b1.move(430, 635)
dvtransfer_fun_b1.resize(70, 70)
dvtransfer_fun_b1.setConfig(Constant Value, 1)
dvtransfer_fun_sc = block(Scope)
dvtransfer_fun_sc.move(930, 585)
dvtransfer_fun_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Discrete Varying PID -- the discrete counterpart of the continuous lane, same scheduled gain
dvpid = block(Discrete_Varying_PID)
dvpid.move(680, 1010)
dvpid.resize(140, 120)
// the I coefficient, held fixed
dvpid_I = block(Constant)
dvpid_I.move(430, 885)
dvpid_I.resize(70, 70)
dvpid_I.setConfig(Constant Value, 0.2)
// the D coefficient, held fixed
dvpid_D = block(Constant)
dvpid_D.move(430, 985)
dvpid_D.resize(70, 70)
dvpid_D.setConfig(Constant Value, 0.05)
// the N coefficient, held fixed
dvpid_N = block(Constant)
dvpid_N.move(430, 1085)
dvpid_N.resize(70, 70)
dvpid_N.setConfig(Constant Value, 20)
dvpid_sc = block(Scope)
dvpid_sc.move(930, 985)
dvpid_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Discrete Varying State Space -- A is 0.9 here, not -1: a DISCRETE A is stable inside the unit circle, so the
// continuous lane's negative value would be wrong in this family
dvstate_space = block(Discrete_Varying_State_Space)
dvstate_space.move(680, 1410)
dvstate_space.resize(150, 120)
// the A coefficient, held fixed
dvstate_space_A = block(Constant)
dvstate_space_A.move(430, 1285)
dvstate_space_A.resize(70, 70)
dvstate_space_A.setConfig(Constant Value, 0.9)
// the C coefficient, held fixed
dvstate_space_C = block(Constant)
dvstate_space_C.move(430, 1385)
dvstate_space_C.resize(70, 70)
dvstate_space_C.setConfig(Constant Value, 1)
// the D coefficient, held fixed
dvstate_space_D = block(Constant)
dvstate_space_D.move(430, 1485)
dvstate_space_D.resize(70, 70)
dvstate_space_D.setConfig(Constant Value, 0)
// The Mux is resized to the block's OWN height and given the same move-y, so
// its three input rows coincide exactly with the three output rows and all
// three links are dead straight.
dvstate_space_mux = block(Mux)
dvstate_space_mux.move(930, 1410)
dvstate_space_mux.resize(20, 120)
dvstate_space_mux.addPort(in)
dvstate_space_sc = block(Scope)
dvstate_space_sc.move(1020, 1385)
dvstate_space_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Discrete Varying Notch -- a notch that walks across the input spectrum: only its centre frequency is
// scheduled, the depth gmin and the damping hold still
dvnotch = block(Discrete_Varying_Notch)
dvnotch.move(680, 1805)
dvnotch.resize(140, 110)
// the gmin coefficient, held fixed
dvnotch_gmin = block(Constant)
dvnotch_gmin.move(430, 1735)
dvnotch_gmin.resize(70, 70)
dvnotch_gmin.setConfig(Constant Value, 0.05)
// the damping coefficient, held fixed
dvnotch_damping = block(Constant)
dvnotch_damping.move(430, 1835)
dvnotch_damping.resize(70, 70)
dvnotch_damping.setConfig(Constant Value, 0.7)
dvnotch_sc = block(Scope)
dvnotch_sc.move(930, 1785)
dvnotch_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Discrete Varying Observer Form -- discrete observer: A - B*K - L*C = 0.7 - L stays inside the unit circle for
// every value theta takes
dvobserver_for = block(Discrete_Varying_Observer_Form)
dvobserver_for.move(680, 2276)
dvobserver_for.resize(160, 152)
// the A coefficient, held fixed
dvobserver_for_A = block(Constant)
dvobserver_for_A.move(430, 2035)
dvobserver_for_A.resize(70, 70)
dvobserver_for_A.setConfig(Constant Value, 0.9)
// the B coefficient, held fixed
dvobserver_for_B = block(Constant)
dvobserver_for_B.move(430, 2135)
dvobserver_for_B.resize(70, 70)
dvobserver_for_B.setConfig(Constant Value, 1)
// the C coefficient, held fixed
dvobserver_for_C = block(Constant)
dvobserver_for_C.move(430, 2235)
dvobserver_for_C.resize(70, 70)
dvobserver_for_C.setConfig(Constant Value, 1)
// the D coefficient, held fixed
dvobserver_for_D = block(Constant)
dvobserver_for_D.move(430, 2335)
dvobserver_for_D.resize(70, 70)
dvobserver_for_D.setConfig(Constant Value, 0)
// the K coefficient, held fixed
dvobserver_for_K = block(Constant)
dvobserver_for_K.move(430, 2435)
dvobserver_for_K.resize(70, 70)
dvobserver_for_K.setConfig(Constant Value, 0.2)
// The Mux is resized to the block's OWN height and given the same move-y, so
// its three input rows coincide exactly with the three output rows and all
// three links are dead straight.
dvobserver_for_mux = block(Mux)
dvobserver_for_mux.move(930, 2276)
dvobserver_for_mux.resize(20, 152)
dvobserver_for_mux.addPort(in)
dvobserver_for_sc = block(Scope)
dvobserver_for_sc.move(1020, 2235)
dvobserver_for_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Discrete Varying 2DOF PID -- two signal inputs again: setpoint r and measurement y, so b and c matter
dv2dof_pid = block(Discrete_Varying_2DOF_PID)
dv2dof_pid.move(680, 2881)
dv2dof_pid.resize(150, 162)
// the I coefficient, held fixed
dv2dof_pid_I = block(Constant)
dv2dof_pid_I.move(430, 2635)
dv2dof_pid_I.resize(70, 70)
dv2dof_pid_I.setConfig(Constant Value, 0.2)
// the D coefficient, held fixed
dv2dof_pid_D = block(Constant)
dv2dof_pid_D.move(430, 2735)
dv2dof_pid_D.resize(70, 70)
dv2dof_pid_D.setConfig(Constant Value, 0.05)
// the N coefficient, held fixed
dv2dof_pid_N = block(Constant)
dv2dof_pid_N.move(430, 2835)
dv2dof_pid_N.resize(70, 70)
dv2dof_pid_N.setConfig(Constant Value, 20)
// the b coefficient, held fixed
dv2dof_pid_b = block(Constant)
dv2dof_pid_b.move(430, 2935)
dv2dof_pid_b.resize(70, 70)
dv2dof_pid_b.setConfig(Constant Value, 1)
// the c coefficient, held fixed
dv2dof_pid_c = block(Constant)
dv2dof_pid_c.move(430, 3035)
dv2dof_pid_c.resize(70, 70)
dv2dof_pid_c.setConfig(Constant Value, 0)
dv2dof_pid_sc = block(Scope)
dv2dof_pid_sc.move(930, 2835)
dv2dof_pid_sc.resize(70, 70)
// --------------------------------------------------------------------------
// Wiring. A source feeding many ports is ONE link plus branches off it: the first
// connect() binds a handle, every later one branches that handle rather than
// starting a second link from the same output port.
// theta's FIRST link is the one that binds the bus handle. A second plain
// connect() from the same output port is refused -- every later consumer
// has to branch this handle instead.
lsched = connect(sched<0>, schedw<0>)
lsig = connect(sig<0>, dvlowpass<0>)
lschedw = connect(schedw<0>, dvlowpass<1>)
connect(dvlowpass<0>, dvlowpass_sc<0>)
connect(lsig, dvtransfer_fun<0>)
connect(dvtransfer_fun_b0<0>, dvtransfer_fun<1>)
connect(lsched, dvtransfer_fun<2>)
connect(dvtransfer_fun_b1<0>, dvtransfer_fun<3>)
connect(dvtransfer_fun<0>, dvtransfer_fun_sc<0>)
connect(lsig, dvpid<0>)
connect(lsched, dvpid<1>)
connect(dvpid_I<0>, dvpid<2>)
connect(dvpid_D<0>, dvpid<3>)
connect(dvpid_N<0>, dvpid<4>)
connect(dvpid<0>, dvpid_sc<0>)
connect(lsig, dvstate_space<0>)
connect(dvstate_space_A<0>, dvstate_space<1>)
connect(lsched, dvstate_space<2>)
connect(dvstate_space_C<0>, dvstate_space<3>)
connect(dvstate_space_D<0>, dvstate_space<4>)
connect(dvstate_space<0>, dvstate_space_mux<0>)
connect(dvstate_space<1>, dvstate_space_mux<1>)
connect(dvstate_space<2>, dvstate_space_mux<2>)
connect(dvstate_space_mux<0>, dvstate_space_sc<0>)
connect(lsig, dvnotch<0>)
connect(lschedw, dvnotch<1>)
connect(dvnotch_gmin<0>, dvnotch<2>)
connect(dvnotch_damping<0>, dvnotch<3>)
connect(dvnotch<0>, dvnotch_sc<0>)
connect(lsig, dvobserver_for<0>)
connect(dvobserver_for_A<0>, dvobserver_for<1>)
connect(dvobserver_for_B<0>, dvobserver_for<2>)
connect(dvobserver_for_C<0>, dvobserver_for<3>)
connect(dvobserver_for_D<0>, dvobserver_for<4>)
connect(dvobserver_for_K<0>, dvobserver_for<5>)
connect(lsched, dvobserver_for<6>)
connect(dvobserver_for<0>, dvobserver_for_mux<0>)
connect(dvobserver_for<1>, dvobserver_for_mux<1>)
connect(dvobserver_for<2>, dvobserver_for_mux<2>)
connect(dvobserver_for_mux<0>, dvobserver_for_sc<0>)
connect(lsig, dv2dof_pid<0>)
lmeas = connect(meas<0>, dv2dof_pid<1>)
connect(lsched, dv2dof_pid<2>)
connect(dv2dof_pid_I<0>, dv2dof_pid<3>)
connect(dv2dof_pid_D<0>, dv2dof_pid<4>)
connect(dv2dof_pid_N<0>, dv2dof_pid<5>)
connect(dv2dof_pid_b<0>, dv2dof_pid<6>)
connect(dv2dof_pid_c<0>, dv2dof_pid<7>)
connect(dv2dof_pid<0>, dv2dof_pid_sc<0>)
t = textbox()
t.move(200, 90)
t.setText("One scheduling signal, seven varying blocks. A discrete A is stable INSIDE the unit circle, which is why these lanes schedule different coefficients from their continuous counterparts.")
Matrix Workbench#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/matrix_workbench.iscript · category Control Systems · kind subsystem · compiled into the binary
Every matrix block in the library on worked examples — five views of one matrix, six ways to build or combine matrices, and the three predicates.
Places: Constant, Transpose, Display, Hermitian_Transpose, Extract_Diagonal, Submatrix, Matrix_Square, Identity_Matrix, Expand_Scalar, Create_Diagonal_Matrix, Cross_Product, Matrix_Multiply, Permute_Matrix, IsSymmetric, IsHermitian, IsTriangular
// @title: Matrix Workbench
// @category: Control Systems
// @summary: Every matrix block in the library on worked examples — five views of one matrix, six ways to build or combine matrices, and the three predicates.
// @kind: subsystem
//
// Three columns, and the middle one is where most of the work happens.
//
// LEFT one 3x3 matrix, A = [1 2 3; 4 5 6; 7 8 10], branched into the five
// blocks that reshape or reduce it. Deliberately NOT symmetric, so
// the transpose is visibly different from the input.
// MIDDLE the blocks that make a matrix out of something smaller, and the
// two that combine a pair. Each lane carries its own operands.
// RIGHT the three predicates, fed by a DIAGONAL matrix, which is symmetric,
// Hermitian and upper-triangular all at once -- so all three displays
// read 1. Point them at A instead and all three read 0; that is what
// a predicate is for.
//
// Cross Product is the one lane that is not really about matrices: it takes
// two 3-vectors, and (1,0,0) x (0,1,0) = (0,0,1) is the right-hand rule in
// three numbers.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7. Lanes are 160 apart where one
// source feeds a block, and wider where a block needs two: a two-input block
// spreads its ports about its centre rather than putting one on it, so each
// lane's FIRST operand sits on the port row and is straight, and the second
// comes in from below.
// ================================================== left: one matrix, five views
src_a = block(Constant)
src_a.move(40, 1435)
src_a.resize(70, 70)
src_a.setConfig(Constant Value, [1 2 3; 4 5 6; 7 8 10])
m_tr = block(Transpose)
m_tr.move(400, 1435)
m_tr.resize(90, 70)
d_tr = block(Display)
d_tr.move(700, 1425)
d_tr.resize(90, 50)
m_htr = block(Hermitian_Transpose)
m_htr.move(400, 1275)
m_htr.resize(70, 70)
d_htr = block(Display)
d_htr.move(700, 1265)
d_htr.resize(90, 50)
m_diag = block(Extract_Diagonal)
m_diag.move(400, 1115)
m_diag.resize(70, 70)
d_diag = block(Display)
d_diag.move(700, 1105)
d_diag.resize(90, 50)
// "One row" with the start anchor left at First: the top row of A.
m_sub = block(Submatrix)
m_sub.move(400, 955)
m_sub.resize(100, 70)
m_sub.setConfig(Row Span, All rows%~%One row%~%Range of rows~~One row)
d_sub = block(Display)
d_sub.move(700, 945)
d_sub.resize(90, 50)
m_sq = block(Matrix_Square)
m_sq.move(400, 795)
m_sq.resize(90, 70)
d_sq = block(Display)
d_sq.move(700, 785)
d_sq.resize(90, 50)
bus_a = connect(src_a<0>, m_tr<0>)
connect(bus_a, m_htr<0>)
connect(bus_a, m_diag<0>)
connect(bus_a, m_sub<0>)
connect(bus_a, m_sq<0>)
connect(m_tr<0>, d_tr<0>)
connect(m_htr<0>, d_htr<0>)
connect(m_diag<0>, d_diag<0>)
connect(m_sub<0>, d_sub<0>)
connect(m_sq<0>, d_sq<0>)
// ================================================== middle: building and combining
m_eye = block(Identity_Matrix)
m_eye.move(900, 1435)
m_eye.resize(70, 70)
m_eye.setConfig(Output Dimensions, 3)
d_eye = block(Display)
d_eye.move(1650, 1425)
d_eye.resize(90, 50)
connect(m_eye<0>, d_eye<0>)
m_exp = block(Expand_Scalar)
m_exp.move(900, 1275)
m_exp.resize(75, 70)
m_exp.setConfig(Element Value, 2)
m_exp.setConfig(Output Dimensions, 3)
d_exp = block(Display)
d_exp.move(1650, 1265)
d_exp.resize(90, 50)
connect(m_exp<0>, d_exp<0>)
src_v = block(Constant)
src_v.move(900, 1115)
src_v.resize(70, 70)
src_v.setConfig(Constant Value, [1; 2; 3])
m_mkdiag = block(Create_Diagonal_Matrix)
m_mkdiag.move(1300, 1115)
m_mkdiag.resize(80, 70)
d_mkdiag = block(Display)
d_mkdiag.move(1650, 1105)
d_mkdiag.resize(90, 50)
connect(src_v<0>, m_mkdiag<0>)
connect(m_mkdiag<0>, d_mkdiag<0>)
src_c1 = block(Constant)
src_c1.move(900, 907)
src_c1.resize(70, 70)
src_c1.setConfig(Constant Value, [1; 0; 0])
src_c2 = block(Constant)
src_c2.move(900, 795)
src_c2.resize(70, 70)
src_c2.setConfig(Constant Value, [0; 1; 0])
m_cross = block(Cross_Product)
m_cross.move(1300, 895)
m_cross.resize(70, 70)
d_cross = block(Display)
d_cross.move(1650, 885)
d_cross.resize(90, 50)
connect(src_c1<0>, m_cross<0>)
connect(src_c2<0>, m_cross<1>)
connect(m_cross<0>, d_cross<0>)
src_m1 = block(Constant)
src_m1.move(900, 649)
src_m1.resize(70, 70)
src_m1.setConfig(Constant Value, [1 2; 3 4])
src_m2 = block(Constant)
src_m2.move(900, 535)
src_m2.resize(70, 70)
src_m2.setConfig(Constant Value, [5 6; 7 8])
m_mul = block(Matrix_Multiply)
m_mul.move(1300, 640)
m_mul.resize(90, 80)
d_mul = block(Display)
d_mul.move(1650, 625)
d_mul.resize(90, 50)
connect(src_m1<0>, m_mul<0>)
connect(src_m2<0>, m_mul<1>)
connect(m_mul<0>, d_mul<0>)
src_p1 = block(Constant)
src_p1.move(900, 389)
src_p1.resize(70, 70)
src_p1.setConfig(Constant Value, [1 2; 3 4; 5 6])
src_p2 = block(Constant)
src_p2.move(900, 275)
src_p2.resize(70, 70)
src_p2.setConfig(Constant Value, [3; 1; 2])
m_perm = block(Permute_Matrix)
m_perm.move(1300, 380)
m_perm.resize(100, 80)
m_perm.setConfig(Permute, Rows%~%Columns~~Rows)
d_perm = block(Display)
d_perm.move(1650, 365)
d_perm.resize(90, 50)
connect(src_p1<0>, m_perm<0>)
connect(src_p2<0>, m_perm<1>)
connect(m_perm<0>, d_perm<0>)
// ================================================== right: the three predicates
src_d = block(Constant)
src_d.move(1850, 1435)
src_d.resize(70, 70)
src_d.setConfig(Constant Value, [2 0 0; 0 3 0; 0 0 4])
m_sym = block(IsSymmetric)
m_sym.move(2200, 1435)
m_sym.resize(75, 70)
d_sym = block(Display)
d_sym.move(2500, 1425)
d_sym.resize(90, 50)
m_herm = block(IsHermitian)
m_herm.move(2200, 1275)
m_herm.resize(75, 70)
d_herm = block(Display)
d_herm.move(2500, 1265)
d_herm.resize(90, 50)
m_tri = block(IsTriangular)
m_tri.move(2200, 1115)
m_tri.resize(75, 70)
d_tri = block(Display)
d_tri.move(2500, 1105)
d_tri.resize(90, 50)
bus_d = connect(src_d<0>, m_sym<0>)
connect(bus_d, m_herm<0>)
connect(bus_d, m_tri<0>)
connect(m_sym<0>, d_sym<0>)
connect(m_herm<0>, d_herm<0>)
connect(m_tri<0>, d_tri<0>)
a1 = area()
a1.move(15, 1490)
a1.resize(800, 800)
a1.setColor(168, 152, 232)
a1.setTitle("One matrix, five views")
a2 = area()
a2.move(860, 1490)
a2.resize(905, 1330)
a2.setColor(232, 168, 124)
a2.setTitle("Building matrices, and combining them")
a3 = area()
a3.move(1810, 1490)
a3.resize(805, 480)
a3.setColor(43, 160, 184)
a3.setTitle("Predicates")
t = textbox()
t.move(40, 100)
t.setText("Swap the right-hand column's diagonal matrix for the one on the left and all three predicates fall to zero.")
Model Verification Bench#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/model_verification_bench.iscript · category Control Systems · kind subsystem · compiled into the binary
Every Check block and the Assertion watching one sine, with limits chosen so the whole bench passes and nothing stops the run.
Places: Sine_Wave, Check_Static_Range, Scope, Check_Static_Gap, Check_Static_Lower_Bound, Check_Static_Upper_Bound, Check_Discrete_Gradient, Quantizer, Check_Input_Resolution, Bias, Assertion, Constant, Check_Dynamic_Range, Check_Dynamic_Gap, Check_Dynamic_Lower_Bound, Check_Dynamic_Upper_Bound
// @title: Model Verification Bench
// @category: Control Systems
// @summary: Every Check block and the Assertion watching one sine, with limits chosen so the whole bench passes and nothing stops the run.
// @kind: subsystem
//
// A verification block is a claim about a signal that the simulation checks on
// every step. The left column makes eleven claims about the SAME sine, with
// its limits in the dialog; the right column makes the same kind of claim with
// the limits arriving on ports, so a running model can tighten or relax them.
//
// Two of the claims cannot be made about a raw sine, and each gets one block
// to make it true, which is the clearest way to show what the check means:
//
// Check Input Resolution asserts the signal is a whole multiple of a step,
// so its lane quantises the sine to 0.25 first.
// Assertion asserts the signal is never zero, and a sine is zero twice a
// cycle -- so its lane adds a bias of 2 to lift it clear.
//
// Static Range and Static Gap are opposites and cannot both be satisfied by
// one setting: Range asserts the signal stays INSIDE its limits, Gap that it
// stays OUTSIDE them. So Range is set to [-2, 2] (which the sine never
// leaves) and Gap to [2, 3] (which it never enters).
//
// EVERY block here has "Stop When Assertion Fails" turned OFF. Left on -- the
// shipped default -- the first failed claim ends the run, which is right in a
// model you are testing and wrong in a template someone is about to edit. Turn
// it back on once the limits are yours.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7. Note that on the dynamic blocks the
// SIGNAL is the last port, not the first: the order is max, min, u.
src = block(Sine_Wave)
src.move(40, 1255)
src.resize(70, 70)
src.setConfig(Amplitude, 1)
src.setConfig(Frequency (Rad), 1)
// ================================================== static: limits in the dialog
c_range = block(Check_Static_Range)
c_range.move(400, 1260)
c_range.resize(80, 80)
c_range.setConfig(Minimum, -2)
c_range.setConfig(Maximum, 2)
c_range.setConfig(Stop When Assertion Fails, on%~%off~~off)
v_range = block(Scope)
v_range.move(900, 1255)
v_range.resize(70, 70)
c_gap = block(Check_Static_Gap)
c_gap.move(400, 1100)
c_gap.resize(80, 80)
c_gap.setConfig(Minimum, 2)
c_gap.setConfig(Maximum, 3)
c_gap.setConfig(Stop When Assertion Fails, on%~%off~~off)
v_gap = block(Scope)
v_gap.move(900, 1095)
v_gap.resize(70, 70)
c_lo = block(Check_Static_Lower_Bound)
c_lo.move(400, 940)
c_lo.resize(80, 80)
c_lo.setConfig(Minimum, -2)
c_lo.setConfig(Stop When Assertion Fails, on%~%off~~off)
v_lo = block(Scope)
v_lo.move(900, 935)
v_lo.resize(70, 70)
c_hi = block(Check_Static_Upper_Bound)
c_hi.move(400, 780)
c_hi.resize(80, 80)
c_hi.setConfig(Maximum, 2)
c_hi.setConfig(Stop When Assertion Fails, on%~%off~~off)
v_hi = block(Scope)
v_hi.move(900, 775)
v_hi.resize(70, 70)
c_grad = block(Check_Discrete_Gradient)
c_grad.move(400, 620)
c_grad.resize(80, 80)
c_grad.setConfig(Maximum Gradient, 10)
c_grad.setConfig(Stop When Assertion Fails, on%~%off~~off)
v_grad = block(Scope)
v_grad.move(900, 615)
v_grad.resize(70, 70)
// The resolution claim needs a signal that HAS that resolution.
q_res = block(Quantizer)
q_res.move(400, 455)
q_res.resize(70, 70)
q_res.setConfig(Quantization Interval, 0.25)
c_res = block(Check_Input_Resolution)
c_res.move(620, 460)
c_res.resize(80, 80)
c_res.setConfig(Resolution, 0.25)
c_res.setConfig(Stop When Assertion Fails, on%~%off~~off)
v_res = block(Scope)
v_res.move(900, 455)
v_res.resize(70, 70)
// The assertion claim needs a signal that never reaches zero.
b_assert = block(Bias)
b_assert.move(400, 295)
b_assert.resize(70, 70)
b_assert.setConfig(Bias, 2)
c_assert = block(Assertion)
c_assert.move(620, 295)
c_assert.resize(70, 70)
c_assert.setConfig(Stop When Assertion Fails, on%~%off~~off)
bus = connect(src<0>, c_range<0>)
connect(bus, c_gap<0>)
connect(bus, c_lo<0>)
connect(bus, c_hi<0>)
connect(bus, c_grad<0>)
connect(bus, q_res<0>)
connect(bus, b_assert<0>)
connect(c_range<0>, v_range<0>)
connect(c_gap<0>, v_gap<0>)
connect(c_lo<0>, v_lo<0>)
connect(c_hi<0>, v_hi<0>)
connect(c_grad<0>, v_grad<0>)
connect(q_res<0>, c_res<0>)
connect(c_res<0>, v_res<0>)
connect(b_assert<0>, c_assert<0>)
// ================================================== dynamic: limits on ports
dmax_r = block(Constant)
dmax_r.move(1200, 1455)
dmax_r.resize(70, 70)
dmax_r.setConfig(Constant Value, 2)
dmin_r = block(Constant)
dmin_r.move(1200, 1335)
dmin_r.resize(70, 70)
dmin_r.setConfig(Constant Value, -2)
dsrc_r = block(Sine_Wave)
dsrc_r.move(1200, 1215)
dsrc_r.resize(70, 70)
dsrc_r.setConfig(Amplitude, 1)
dsrc_r.setConfig(Frequency (Rad), 1)
d_range = block(Check_Dynamic_Range)
d_range.move(1600, 1340)
d_range.resize(80, 80)
d_range.setConfig(Stop When Assertion Fails, on%~%off~~off)
vd_range = block(Scope)
vd_range.move(1950, 1335)
vd_range.resize(70, 70)
connect(dmax_r<0>, d_range<0>)
connect(dmin_r<0>, d_range<1>)
connect(dsrc_r<0>, d_range<2>)
connect(d_range<0>, vd_range<0>)
dmax_g = block(Constant)
dmax_g.move(1200, 1155)
dmax_g.resize(70, 70)
dmax_g.setConfig(Constant Value, 3)
dmin_g = block(Constant)
dmin_g.move(1200, 1035)
dmin_g.resize(70, 70)
dmin_g.setConfig(Constant Value, 2)
dsrc_g = block(Sine_Wave)
dsrc_g.move(1200, 915)
dsrc_g.resize(70, 70)
dsrc_g.setConfig(Amplitude, 1)
dsrc_g.setConfig(Frequency (Rad), 1)
d_gap = block(Check_Dynamic_Gap)
d_gap.move(1600, 1040)
d_gap.resize(80, 80)
d_gap.setConfig(Stop When Assertion Fails, on%~%off~~off)
vd_gap = block(Scope)
vd_gap.move(1950, 1035)
vd_gap.resize(70, 70)
connect(dmax_g<0>, d_gap<0>)
connect(dmin_g<0>, d_gap<1>)
connect(dsrc_g<0>, d_gap<2>)
connect(d_gap<0>, vd_gap<0>)
// Two ports only, so the limit sits on the port row and the sine comes up
// from below.
dmin_l = block(Constant)
dmin_l.move(1200, 749)
dmin_l.resize(70, 70)
dmin_l.setConfig(Constant Value, -2)
dsrc_l = block(Sine_Wave)
dsrc_l.move(1200, 635)
dsrc_l.resize(70, 70)
dsrc_l.setConfig(Amplitude, 1)
dsrc_l.setConfig(Frequency (Rad), 1)
d_lo = block(Check_Dynamic_Lower_Bound)
d_lo.move(1600, 740)
d_lo.resize(80, 80)
d_lo.setConfig(Stop When Assertion Fails, on%~%off~~off)
vd_lo = block(Scope)
vd_lo.move(1950, 735)
vd_lo.resize(70, 70)
connect(dmin_l<0>, d_lo<0>)
connect(dsrc_l<0>, d_lo<1>)
connect(d_lo<0>, vd_lo<0>)
dmax_u = block(Constant)
dmax_u.move(1200, 449)
dmax_u.resize(70, 70)
dmax_u.setConfig(Constant Value, 2)
dsrc_u = block(Sine_Wave)
dsrc_u.move(1200, 335)
dsrc_u.resize(70, 70)
dsrc_u.setConfig(Amplitude, 1)
dsrc_u.setConfig(Frequency (Rad), 1)
d_hi = block(Check_Dynamic_Upper_Bound)
d_hi.move(1600, 440)
d_hi.resize(80, 80)
d_hi.setConfig(Stop When Assertion Fails, on%~%off~~off)
vd_hi = block(Scope)
vd_hi.move(1950, 435)
vd_hi.resize(70, 70)
connect(dmax_u<0>, d_hi<0>)
connect(dsrc_u<0>, d_hi<1>)
connect(d_hi<0>, vd_hi<0>)
a1 = area()
a1.move(15, 1315)
a1.resize(980, 1125)
a1.setColor(168, 152, 232)
a1.setTitle("One sine, eleven claims — limits in the dialog")
a2 = area()
a2.move(1010, 1510)
a2.resize(1035, 1330)
a2.setColor(232, 168, 124)
a2.setTitle("The same claims, limits on ports")
t = textbox()
t.move(40, 120)
t.setText("Every block passes as shipped. Tighten a limit until one fails, then turn its Stop When Assertion Fails back on to see the run halt there.")
Motion Profile Shapers#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/motion_profile_shapers.iscript · category Robotics · kind subsystem · compiled into the binary
Trapezoidal, S-curve and quintic profiles between the same two positions, each on its own scope and all three positions on a fourth.
Places: Trapezoidal_Velocity_Profile, Mux, Scope, S_Curve_Profile, Quintic_Trajectory
// @title: Motion Profile Shapers
// @category: Robotics
// @summary: Trapezoidal, S-curve and quintic profiles between the same two positions, each on its own scope and all three positions on a fourth.
// @kind: subsystem
//
// Three ways to get an axis from 0 to 1, all held to the same limits so the
// difference is the SHAPE and not the size of the move:
//
// Trapezoidal accelerate at a constant rate, coast, decelerate. Fastest of
// the three, and its acceleration steps -- which a real drive
// feels as a jolt.
// S-Curve the same profile with the acceleration ramped rather than
// stepped, so jerk is bounded. Slower, and much kinder to the
// mechanics.
// Quintic a single fifth-order polynomial with zero velocity AND zero
// acceleration at both ends. Smoothest, but it respects a
// DURATION rather than a velocity limit, so its peak speed is
// whatever the duration implies.
//
// Every block puts out three signals -- q, qd, qdd -- so each lane muxes its
// own three onto one scope, and the bottom lane muxes the three POSITIONS
// together so the profiles can be read against one another.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7. A 90-tall block with three ports
// spreads them at +23 / 0 / -22 about its centre, so putting each Mux on the
// same port row as its profile makes all nine lane links dead straight. The
// three comparison branches are PINNED to their own risers (250, 300, 350),
// because the planner puts them all on one x, where three different signals
// would draw as a single line.
// ------------------------------------------------------------------ trapezoidal
p_trap = block(Trapezoidal_Velocity_Profile)
p_trap.move(40, 1445)
p_trap.resize(150, 90)
p_trap.setConfig(Start Position, 0)
p_trap.setConfig(End Position, 1)
p_trap.setConfig(Max Velocity, 0.5)
p_trap.setConfig(Max Acceleration, 1)
m_trap = block(Mux)
m_trap.addPort(in)
m_trap.move(600, 1445)
m_trap.resize(20, 90)
v_trap = block(Scope)
v_trap.move(850, 1435)
v_trap.resize(70, 70)
q_trap = connect(p_trap<0>, m_trap<0>)
connect(p_trap<1>, m_trap<1>)
connect(p_trap<2>, m_trap<2>)
connect(m_trap<0>, v_trap<0>)
// ------------------------------------------------------------------ s-curve
p_scurve = block(S_Curve_Profile)
p_scurve.move(40, 1145)
p_scurve.resize(150, 90)
p_scurve.setConfig(Start Position, 0)
p_scurve.setConfig(End Position, 1)
p_scurve.setConfig(Max Velocity, 0.5)
p_scurve.setConfig(Max Acceleration, 1)
p_scurve.setConfig(Max Jerk, 4)
m_scurve = block(Mux)
m_scurve.addPort(in)
m_scurve.move(600, 1145)
m_scurve.resize(20, 90)
v_scurve = block(Scope)
v_scurve.move(850, 1135)
v_scurve.resize(70, 70)
q_scurve = connect(p_scurve<0>, m_scurve<0>)
connect(p_scurve<1>, m_scurve<1>)
connect(p_scurve<2>, m_scurve<2>)
connect(m_scurve<0>, v_scurve<0>)
// ------------------------------------------------------------------ quintic
p_quintic = block(Quintic_Trajectory)
p_quintic.move(40, 845)
p_quintic.resize(132, 90)
p_quintic.setConfig(Start Position, 0)
p_quintic.setConfig(End Position, 1)
p_quintic.setConfig(Duration (s), 2)
m_quintic = block(Mux)
m_quintic.addPort(in)
m_quintic.move(600, 845)
m_quintic.resize(20, 90)
v_quintic = block(Scope)
v_quintic.move(850, 835)
v_quintic.resize(70, 70)
q_quintic = connect(p_quintic<0>, m_quintic<0>)
connect(p_quintic<1>, m_quintic<1>)
connect(p_quintic<2>, m_quintic<2>)
connect(m_quintic<0>, v_quintic<0>)
// -------------------------------------------------- the three positions together
m_cmp = block(Mux)
m_cmp.addPort(in)
m_cmp.move(600, 445)
m_cmp.resize(20, 90)
v_cmp = block(Scope)
v_cmp.move(850, 435)
v_cmp.resize(70, 70)
// Left to itself the planner tees all three of these at x = 565 and drops them
// down the SAME line, so three different position signals draw as one wire.
// The y values below are the planner's own (read back from a canonical save);
// only the riser x is ours, one per branch, 70 apart.
b_trap = connect(q_trap, m_cmp<0>)
b_trap.setCorners((250, 1423), (250, 423), (605, 423))
b_scurve = connect(q_scurve, m_cmp<1>)
b_scurve.setCorners((320, 1123), (320, 400), (605, 400))
b_quintic = connect(q_quintic, m_cmp<2>)
b_quintic.setCorners((390, 823), (390, 378), (605, 378))
connect(m_cmp<0>, v_cmp<0>)
a1 = area()
a1.move(15, 1560)
a1.resize(930, 900)
a1.setColor(168, 152, 232)
a1.setTitle("One profile per lane: position, velocity, acceleration")
a2 = area()
a2.move(20, 560)
a2.resize(925, 300)
a2.setColor(232, 168, 124)
a2.setTitle("The three positions, together")
t = textbox()
t.move(40, 180)
t.setText("Same move, same limits, three shapes. Watch the acceleration traces: trapezoidal steps, S-curve ramps, quintic starts and ends at zero.")
Nonlinearity Gallery#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/nonlinearity_gallery.iscript · category Control Systems · kind subsystem · compiled into the binary
One sine through every static nonlinearity in the library, and the three whose limits arrive on ports rather than in a dialog.
Places: Sine_Wave, Saturation, Scope, Dead_Zone, Backlash, Relay, Quantizer, Rate_Limiter, Coulomb_And_Viscous_Friction, Wrap_To_Zero, Hit_Crossing, Constant, Saturation_Dynamic, Dead_Zone_Dynamic, Rate_Limiter_Dynamic
// @title: Nonlinearity Gallery
// @category: Control Systems
// @summary: One sine through every static nonlinearity in the library, and the three whose limits arrive on ports rather than in a dialog.
// @kind: subsystem
//
// The left column is the point of the template: ONE sine, branched into nine
// nonlinearities, nine scopes. Every scope answers the same question about the
// same wave -- what does this block do to it -- so the shapes can be read
// against one another instead of one at a time.
//
// The right column is the same idea for the three DYNAMIC blocks. They do the
// same job as their static neighbours (clip, deaden, slew-limit), except that
// the limits are input signals rather than dialog values, so a model can move
// them while it runs. Their port order is up, u, lo -- the signal is the
// MIDDLE port -- which is why each sine here goes to port 1 and the two
// constants come in from above and below.
//
// The sine runs at 2 rad/s so that its steepest slope (2 per second) is faster
// than the rate limiters allow (1 per second) and they visibly lag it. Slow
// the source down and they stop limiting at all.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7. One lane per row; the nine static
// lanes are 160 apart and share a single branch riser, the three dynamic lanes
// are 400 apart to leave room for their limit constants.
// ====================================================== static: one sine, nine shapes
src = block(Sine_Wave)
src.move(40, 1575)
src.resize(70, 70)
src.setConfig(Amplitude, 1)
src.setConfig(Frequency (Rad), 2)
n_sat = block(Saturation)
n_sat.move(400, 1575)
n_sat.resize(70, 70)
n_sat.setConfig(Upper Limit, 0.6)
n_sat.setConfig(Lower Limit, -0.6)
v_sat = block(Scope)
v_sat.move(750, 1575)
v_sat.resize(70, 70)
n_dz = block(Dead_Zone)
n_dz.move(400, 1415)
n_dz.resize(70, 70)
n_dz.setConfig(Start of Dead Zone, -0.3)
n_dz.setConfig(End of Dead Zone, 0.3)
v_dz = block(Scope)
v_dz.move(750, 1415)
v_dz.resize(70, 70)
n_bl = block(Backlash)
n_bl.move(400, 1255)
n_bl.resize(70, 70)
n_bl.setConfig(Deadband Width, 0.5)
n_bl.setConfig(Initial Output, 0)
v_bl = block(Scope)
v_bl.move(750, 1255)
v_bl.resize(70, 70)
n_rel = block(Relay)
n_rel.move(400, 1095)
n_rel.resize(70, 70)
n_rel.setConfig(Switch On Point, 0.5)
n_rel.setConfig(Switch Off Point, -0.5)
n_rel.setConfig(Output When On, 1)
n_rel.setConfig(Output When Off, -1)
v_rel = block(Scope)
v_rel.move(750, 1095)
v_rel.resize(70, 70)
n_q = block(Quantizer)
n_q.move(400, 935)
n_q.resize(70, 70)
n_q.setConfig(Quantization Interval, 0.25)
v_q = block(Scope)
v_q.move(750, 935)
v_q.resize(70, 70)
n_rl = block(Rate_Limiter)
n_rl.move(400, 775)
n_rl.resize(70, 70)
n_rl.setConfig(Rising Slew Rate, 1)
n_rl.setConfig(Falling Slew Rate, -1)
n_rl.setConfig(Initial Condition, 0)
v_rl = block(Scope)
v_rl.move(750, 775)
v_rl.resize(70, 70)
n_fr = block(Coulomb_And_Viscous_Friction)
n_fr.move(400, 615)
n_fr.resize(70, 70)
n_fr.setConfig(Coulomb Friction Value, 0.3)
n_fr.setConfig(Coefficient of Viscous Friction, 0.5)
v_fr = block(Scope)
v_fr.move(750, 615)
v_fr.resize(70, 70)
n_wz = block(Wrap_To_Zero)
n_wz.move(400, 455)
n_wz.resize(70, 70)
n_wz.setConfig(Threshold, 0.5)
v_wz = block(Scope)
v_wz.move(750, 455)
v_wz.resize(70, 70)
n_hc = block(Hit_Crossing)
n_hc.move(400, 295)
n_hc.resize(70, 70)
n_hc.setConfig(Hit Crossing Offset, 0)
v_hc = block(Scope)
v_hc.move(750, 295)
v_hc.resize(70, 70)
bus = connect(src<0>, n_sat<0>)
connect(bus, n_dz<0>)
connect(bus, n_bl<0>)
connect(bus, n_rel<0>)
connect(bus, n_q<0>)
connect(bus, n_rl<0>)
connect(bus, n_fr<0>)
connect(bus, n_wz<0>)
connect(bus, n_hc<0>)
connect(n_sat<0>, v_sat<0>)
connect(n_dz<0>, v_dz<0>)
connect(n_bl<0>, v_bl<0>)
connect(n_rel<0>, v_rel<0>)
connect(n_q<0>, v_q<0>)
connect(n_rl<0>, v_rl<0>)
connect(n_fr<0>, v_fr<0>)
connect(n_wz<0>, v_wz<0>)
connect(n_hc<0>, v_hc<0>)
// ====================================================== dynamic: limits on ports
u_satd = block(Constant)
u_satd.move(1000, 1455)
u_satd.resize(70, 70)
u_satd.setConfig(Constant Value, 0.6)
s_satd = block(Sine_Wave)
s_satd.move(1000, 1335)
s_satd.resize(70, 70)
s_satd.setConfig(Amplitude, 1)
s_satd.setConfig(Frequency (Rad), 2)
l_satd = block(Constant)
l_satd.move(1000, 1215)
l_satd.resize(70, 70)
l_satd.setConfig(Constant Value, -0.6)
n_satd = block(Saturation_Dynamic)
n_satd.move(1400, 1340)
n_satd.resize(80, 80)
v_satd = block(Scope)
v_satd.move(1750, 1335)
v_satd.resize(70, 70)
connect(u_satd<0>, n_satd<0>)
connect(s_satd<0>, n_satd<1>)
connect(l_satd<0>, n_satd<2>)
connect(n_satd<0>, v_satd<0>)
u_dzd = block(Constant)
u_dzd.move(1000, 1055)
u_dzd.resize(70, 70)
u_dzd.setConfig(Constant Value, 0.3)
s_dzd = block(Sine_Wave)
s_dzd.move(1000, 935)
s_dzd.resize(70, 70)
s_dzd.setConfig(Amplitude, 1)
s_dzd.setConfig(Frequency (Rad), 2)
l_dzd = block(Constant)
l_dzd.move(1000, 815)
l_dzd.resize(70, 70)
l_dzd.setConfig(Constant Value, -0.3)
n_dzd = block(Dead_Zone_Dynamic)
n_dzd.move(1400, 940)
n_dzd.resize(120, 80)
v_dzd = block(Scope)
v_dzd.move(1750, 935)
v_dzd.resize(70, 70)
connect(u_dzd<0>, n_dzd<0>)
connect(s_dzd<0>, n_dzd<1>)
connect(l_dzd<0>, n_dzd<2>)
connect(n_dzd<0>, v_dzd<0>)
u_rld = block(Constant)
u_rld.move(1000, 655)
u_rld.resize(70, 70)
u_rld.setConfig(Constant Value, 1)
s_rld = block(Sine_Wave)
s_rld.move(1000, 535)
s_rld.resize(70, 70)
s_rld.setConfig(Amplitude, 1)
s_rld.setConfig(Frequency (Rad), 2)
l_rld = block(Constant)
l_rld.move(1000, 415)
l_rld.resize(70, 70)
l_rld.setConfig(Constant Value, -1)
n_rld = block(Rate_Limiter_Dynamic)
n_rld.move(1400, 540)
n_rld.resize(80, 80)
v_rld = block(Scope)
v_rld.move(1750, 535)
v_rld.resize(70, 70)
connect(u_rld<0>, n_rld<0>)
connect(s_rld<0>, n_rld<1>)
connect(l_rld<0>, n_rld<2>)
connect(n_rld<0>, v_rld<0>)
a1 = area()
a1.move(15, 1630)
a1.resize(830, 1440)
a1.setColor(168, 152, 232)
a1.setTitle("One sine, nine static nonlinearities")
a2 = area()
a2.move(870, 1610)
a2.resize(980, 1420)
a2.setColor(232, 168, 124)
a2.setTitle("Limits that arrive on ports")
t = textbox()
t.move(40, 150)
t.setText("Every scope on the left shows the same sine, shaped differently. On the right the limits are signals, so a running model can move them.")
Offline Identification Pair#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/offline_identification_pair.iscript · category System Identification · kind subsystem · compiled into the binary
One excitation and one unknown plant fitted twice — as an FIR model and as an IIR model — with the coefficients on displays.
Places: Uniform_Random_Number, Discrete_Transfer_Function, FIR_Identification, IIR_Identification, Display
// @title: Offline Identification Pair
// @category: System Identification
// @summary: One excitation and one unknown plant fitted twice — as an FIR model and as an IIR model — with the coefficients on displays.
// @kind: subsystem
//
// The two batch estimators, side by side on the same data. Both watch the
// excitation u going into the plant and the response y coming back, and both
// answer the same question in a different model class:
//
// FIR Identification fits an all-zero model. Its b port carries the
// impulse-response taps — always stable, needs more
// taps to capture a slow pole.
// IIR Identification fits a rational model. Num and Den carry the two
// polynomials, so a resonance or a slow pole costs a
// couple of coefficients instead of a dozen taps.
//
// The plant here is second order on purpose: it matches the IIR block's
// default orders, so the fitted Den should come back close to the plant's own
// denominator, while the FIR taps show the same dynamics spread over eight.
//
// **The coefficients appear when the run FINISHES, not while it runs.** Both
// blocks fit in one batch at the end of the simulation (onSolverFinish), so
// the displays sit empty for the whole run and fill in at the last step. That
// is the block working, not the template failing — and it is also why the two
// blocks want a decent stop time: a fit is only as good as the record it saw.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// lane (640 FIR, 320 IIR), sizes pinned with resize(). Each display sits on
// ITS OWN output's port row — on a block with n ports the i-th sits at
// centre + ((n-1)/2 - i) * h/(n+1) — and the second display of a pair sits
// below the first rather than beside it, so it never blocks the first one's
// corridor.
// ---- the unknown system
u = block(Uniform_Random_Number)
u.move(40, 535)
u.resize(80, 70)
u.setConfig(Minimum, -1)
u.setConfig(Maximum, 1)
u.setConfig(Seed, 11)
plant = block(Discrete_Transfer_Function)
plant.move(240, 545)
plant.resize(130, 90)
plant.setConfig(Numerator - Discrete, [0.4])
plant.setConfig(Denominator - Discrete, [1, -0.9, 0.2])
// ---- fitted two ways
fir = block(FIR_Identification)
fir.move(520, 675)
fir.resize(95, 70)
fir.setConfig(Filter Length, 8)
iir = block(IIR_Identification)
iir.move(520, 355)
iir.resize(90, 70)
iir.setConfig(Numerator Order, 1)
iir.setConfig(Denominator Order, 2)
// ---- the coefficients, each on its own port's row
taps = block(Display)
taps.move(760, 665)
taps.resize(90, 50)
num = block(Display)
num.move(760, 357)
num.resize(90, 50)
den = block(Display)
den.move(760, 295)
den.resize(90, 50)
// ---- u drives the plant and both estimators; y follows it
drive = connect(u<0>, plant<0>)
connect(drive, fir<0>)
connect(drive, iir<0>)
resp = connect(plant<0>, fir<1>)
connect(resp, iir<1>)
connect(fir<0>, taps<0>)
connect(iir<0>, num<0>)
connect(iir<1>, den<0>)
// Group washes, in the identity sheet's data accents.
a = area()
a.move(15, 600)
a.resize(380, 175)
a.setColor(232, 168, 124)
a.setTitle("Unknown plant")
a2 = area()
a2.move(495, 730)
a2.resize(405, 150)
a2.setColor(168, 152, 232)
a2.setTitle("FIR fit — impulse-response taps")
a3 = area()
a3.move(495, 412)
a3.resize(405, 192)
a3.setColor(124, 200, 232)
a3.setTitle("IIR fit — Num and Den")
t = textbox()
t.move(500, 190)
t.setText("Both fits are computed in one batch when the run finishes, so the displays stay empty until the last step. Give the run enough stop time to be worth fitting.")
Perception Filters#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/perception_filters.iscript · category Robotics · kind subsystem · compiled into the binary
One noisy measurement through all four perception filters at once, so their different ideas of what noise is can be compared on one canvas.
Places: Sum, Sine_Wave, Band_Limited_White_Noise, Alpha_Beta_Filter, Scope, Moving_Median, Outlier_Reject_Hold, Complementary_Filter, Constant
// @title: Perception Filters
// @category: Robotics
// @summary: One noisy measurement through all four perception filters at once, so their different ideas of what noise is can be compared on one canvas.
// @kind: subsystem
//
// The same corrupted measurement, filtered four ways. A clean sine is the
// truth; band-limited noise is added to it; and the sum is what every
// filter sees. Each filter assumes something different about the noise, and
// the assumption is the whole difference between them:
//
// Alpha Beta Filter - the signal has position AND velocity, so track
// both and let the velocity carry the estimate
// between measurements.
// Moving Median - the noise is occasional and large. A median is
// indifferent to outliers in a way a mean is not.
// Outlier Reject Hold - a sample too far from the last accepted one is a
// fault, not data; hold the last good value and
// count how often that happens.
// Complementary Filter - there is a SECOND sensor. Trust the rate source
// over the short term and the angle source over
// the long term, and blend the two.
//
// The complementary filter is the odd one out because it needs that second
// input: the noisy signal is its angle measurement, and a constant stands
// in for a gyro. The other three see only the one signal.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// filter, sizes pinned with resize().
noisy = block(Sum)
noisy.move(300, 1110)
noisy.resize(60, 60)
truth = block(Sine_Wave)
truth.move(40, 1115)
truth.resize(70, 70)
truth.setConfig(Amplitude, 1)
truth.setConfig(Frequency (Rad), 0.8)
connect(truth<0>, noisy<0>)
noise = block(Band_Limited_White_Noise)
noise.move(180, 895)
noise.resize(80, 70)
noise.setConfig(Noise Power, 0.02)
noise.setConfig(Noise Sample Time (s), 0.05)
noise.setConfig(Seed, 23341)
l_n = connect(noise<0>, noisy<1>)
l_n.setCorners((255, 860), (330, 860), (330, 1055))
ab = block(Alpha_Beta_Filter)
ab.move(540, 1122)
ab.resize(140, 84)
ab.setConfig(Alpha (position gain), 0.5)
ab.setConfig(Beta (velocity gain), 0.1)
ab.setConfig(Initial Position, 0)
ab.setConfig(Initial Velocity, 0)
l_z = connect(noisy<0>, ab<0>)
ab_o0 = block(Scope)
ab_o0.move(820, 1115)
ab_o0.resize(70, 70)
connect(ab<0>, ab_o0<0>)
med = block(Moving_Median)
med.move(540, 856)
med.resize(124, 72)
med.setConfig(Window Length, 7)
connect(l_z, med<0>)
med_o0 = block(Scope)
med_o0.move(820, 855)
med_o0.resize(70, 70)
connect(med<0>, med_o0<0>)
orh = block(Outlier_Reject_Hold)
orh.move(540, 600)
orh.resize(140, 80)
orh.setConfig(Reject Threshold, 0.35)
orh.setConfig(Max Consecutive Rejects, 5)
orh.setConfig(Initial Value, 0)
connect(l_z, orh<0>)
orh_o0 = block(Scope)
orh_o0.move(820, 608.3)
orh_o0.resize(70, 70)
connect(orh<0>, orh_o0<0>)
orh_o1 = block(Scope)
orh_o1.move(970, 478.3)
orh_o1.resize(70, 70)
connect(orh<1>, orh_o1<0>)
comp = block(Complementary_Filter)
comp.move(540, 336)
comp.resize(140, 84)
comp.setConfig(Filter Coefficient (alpha), 0.98)
comp.setConfig(Initial Angle (rad), 0)
connect(l_z, comp<1>)
gyro = block(Constant)
gyro.move(300, 343)
gyro.resize(70, 70)
gyro.setConfig(Constant Value, 0)
connect(gyro<0>, comp<0>)
comp_o0 = block(Scope)
comp_o0.move(820, 329)
comp_o0.resize(70, 70)
connect(comp<0>, comp_o0<0>)
// Group washes in the identity sheet's data accents; only the hue is ours.
a1 = area()
a1.move(-5, 1180)
a1.resize(410, 420)
a1.setColor(232, 168, 124)
a1.setTitle("Truth plus noise")
a2 = area()
a2.move(495, 1187)
a2.resize(440, 1000)
a2.setColor(168, 152, 232)
a2.setTitle("Four filters, one measurement")
t = textbox()
t.move(380, 110)
t.setText("Raise Noise Power on the source and watch which filters degrade gracefully. Outlier Reject Hold also reports how many samples it threw away -- if that count climbs steadily its threshold is too tight for this much noise, not the signal's fault.")
PID Closed Loop#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/pid_closed_loop.iscript · category Control Systems · kind subsystem · compiled into the binary
A step setpoint through a PID controller around a second-order plant, closed with negative feedback.
Places: Step, Subtract, PID_Controller, Transfer_Function, Scope
// @title: PID Closed Loop
// @category: Control Systems
// @summary: A step setpoint through a PID controller around a second-order plant, closed with negative feedback.
// @kind: subsystem
//
// The textbook feedback loop, ready to tune: swap the plant for yours, then
// dial the three gains. The subtract junction computes setpoint minus
// measurement, so the loop is negative feedback out of the box.
//
// This file is also the worked example for ADDING_NEW_TEMPLATES.md §7 (visual
// polish). The whole diagram sits in the FIRST QUADRANT — the canvas opens
// with the origin anchor at the bottom-left of the view, so anything at
// negative x or y materializes off screen. Every block's PORT ROW is at
// y = 260: move() places the TOP edge and ports sit at the vertical center,
// so each block's move-y is 260 + half its height — that is what keeps every
// forward link a dead-straight line. The sizes are pinned with resize() so
// the alignment cannot drift if a type's default size changes, and the
// feedback route is pinned with setCorners() (values read back from a
// canonical save, not invented).
sp = block(Step)
sp.move(40, 295)
sp.resize(70, 70)
sp.setConfig(Initial Value, 0)
sp.setConfig(Final Value, 1)
err = block(Subtract)
err.move(220, 290)
err.resize(60, 60)
pid = block(PID_Controller)
pid.move(400, 300)
pid.resize(110, 80)
pid.setConfig(Proportional (P), 4)
pid.setConfig(Integral (I), 2)
pid.setConfig(Derivative (D), 0.4)
plant = block(Transfer_Function)
plant.move(600, 305)
plant.resize(130, 90)
plant.setConfig(Numerator, [1])
plant.setConfig(Denominator, [1, 2, 1])
out = block(Scope)
out.move(800, 295)
out.resize(70, 70)
connect(sp<0>, err<0>)
connect(err<0>, pid<0>)
connect(pid<0>, plant<0>)
l = connect(plant<0>, out<0>)
fb = connect(l, err<1>)
fb.setCorners((762, 260), (762, 160), (247, 160), (247, 235))
// Group washes, in the identity sheet's data accents: purple for the
// controller half, orange for the plant. Only the HUE is ours — the theme
// re-imposes its own wash alpha on every area, so none is passed here.
a = area()
a.move(190, 355)
a.resize(350, 215)
a.setColor(168, 152, 232)
a.setTitle("Controller")
a2 = area()
a2.move(570, 360)
a2.resize(340, 220)
a2.setColor(232, 168, 124)
a2.setTitle("Plant")
// A note is part of the template like any block: placed clear of the
// feedback corridor at y = 160. No resize — a textbox sizes itself to
// its text.
t = textbox()
t.move(400, 110)
t.setText("Tune P, I and D on the controller; swap the transfer function for your plant.")
Planar Arm Kinematics#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/planar_arm_kinematics.iscript · category Robotics · kind subsystem · compiled into the binary
A moving target solved for joint angles, put back through forward kinematics to prove the round trip, with the arm's Jacobian alongside.
Places: Planar_2R_Inverse_Kinematics, Sine_Wave, Constant, Mux, Scope, Planar_Arm_Forward_Kinematics, Planar_Arm_Jacobian, Pose_Transform_2D
// @title: Planar Arm Kinematics
// @category: Robotics
// @summary: A moving target solved for joint angles, put back through forward kinematics to prove the round trip, with the arm's Jacobian alongside.
// @kind: subsystem
//
// The kinematics of a two-link planar arm, wired as a round trip so it
// checks itself. A target point sweeps left and right; inverse kinematics
// turns it into two joint angles; the two angles are muxed back into a
// joint vector; and forward kinematics turns that vector back into a pose.
// The pose scope should reproduce the target -- if it does not, the two
// blocks disagree about the arm.
//
// That is why all three arm blocks carry the SAME Link Lengths, [0.65;
// 0.4]. The config is the only place the arm is defined: its HEIGHT sets
// the number of joints, so changing it here changes the port sizes, and a
// forward block that disagrees with the inverse one is the usual reason a
// round trip like this stops closing.
//
// Watch the valid flag as well as the pose. The target sweeps inside the
// reachable annulus on purpose, so valid stays 1; push the sweep wider and
// the angles stay finite and simply stop reaching, which is exactly the
// case worth gating on in real code.
//
// The Jacobian is the same arm differentiated -- the [3,2] matrix mapping
// joint rates to tool rates. Pose Transform 2D at the bottom is the frame
// algebra the family shares: it composes the pure +90 degree rotation with
// a 0.1 translation along x, which comes out as a 0.1 translation along y.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, sizes pinned
// with resize().
ik = block(Planar_2R_Inverse_Kinematics)
ik.move(300, 933.3)
ik.resize(140, 100)
ik.setConfig(Link Lengths, [0.65; 0.4])
ik.setConfig(Elbow Configuration, Up)
xt = block(Sine_Wave)
xt.move(40, 935)
xt.resize(70, 70)
xt.setConfig(Amplitude, 0.3)
xt.setConfig(Bias, 0.5)
xt.setConfig(Frequency (Rad), 0.5)
connect(xt<0>, ik<0>)
yt = block(Constant)
yt.move(40, 765)
yt.resize(70, 70)
yt.setConfig(Constant Value, 0.4)
connect(yt<0>, ik<1>)
mux = block(Mux)
mux.move(560, 933.3)
mux.resize(20, 75)
connect(ik<0>, mux<0>)
connect(ik<1>, mux<1>)
// valid is the flag to gate on: 1 while the target is reachable.
valid_s = block(Scope)
valid_s.move(560, 663.3)
valid_s.resize(70, 70)
connect(ik<2>, valid_s<0>)
fk = block(Planar_Arm_Forward_Kinematics)
fk.move(720, 939.8)
fk.resize(140, 88)
fk.setConfig(Link Lengths, [0.65; 0.4])
l_q = connect(mux<0>, fk<0>)
fk_s = block(Scope)
fk_s.move(1000, 930.8)
fk_s.resize(70, 70)
connect(fk<0>, fk_s<0>)
jac = block(Planar_Arm_Jacobian)
jac.move(720, 644)
jac.resize(140, 88)
jac.setConfig(Link Lengths, [0.65; 0.4])
connect(l_q, jac<0>)
jac_s = block(Scope)
jac_s.move(1000, 635)
jac_s.resize(70, 70)
connect(jac<0>, jac_s<0>)
pose = block(Pose_Transform_2D)
pose.move(300, 328)
pose.resize(140, 84)
Ta = block(Constant)
Ta.move(40, 335)
Ta.resize(70, 70)
Ta.setConfig(Constant Value, [0; 0; 1.5708])
connect(Ta<0>, pose<0>)
Tb = block(Constant)
Tb.move(40, 175)
Tb.resize(70, 70)
Tb.setConfig(Constant Value, [0.1; 0; 0])
connect(Tb<0>, pose<1>)
pose_s = block(Scope)
pose_s.move(560, 321)
pose_s.resize(70, 70)
connect(pose<0>, pose_s<0>)
// Group washes in the identity sheet's data accents; only the hue is ours.
a1 = area()
a1.move(255, 1005)
a1.resize(860, 237)
a1.setColor(168, 152, 232)
a1.setTitle("Inverse, then forward again")
a2 = area()
a2.move(675, 709)
a2.resize(440, 218)
a2.setColor(232, 168, 124)
a2.setTitle("The same arm, differentiated")
a3 = area()
a3.move(255, 393)
a3.resize(420, 214)
a3.setColor(124, 200, 160)
a3.setTitle("Frame algebra")
t = textbox()
t.move(700, 110)
t.setText("The pose scope should trace the target the sine is generating -- that round trip closing is the check. Give the three arm blocks different Link Lengths and watch it stop closing; make the column taller and every port on all three resizes with it.")
Preprocessing Line#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/preprocessing_line.iscript · category Machine Learning · kind subsystem · compiled into the binary
A raw feature vector cleaned, scaled four ways, reduced, and turned into a one-hot decision.
Places: Constant, Missing_Value_Imputer, Standard_Scaler, Robust_Scaler, Max_Abs_Scaler, Min_Max_Scaler, Power_Transformer, Normalizer, Feature_Selector, Argmax_Decision, One_Hot_Encoder, Display
// @title: Preprocessing Line
// @category: Machine Learning
// @summary: A raw feature vector cleaned, scaled four ways, reduced, and turned into a one-hot decision.
// @kind: subsystem
//
// Everything a feature vector goes through before a model ever sees it, in the
// order it happens: the missing-value marker is replaced, the vector is put on a
// common scale four different ways, its distribution is squared up, it is cut
// down to the features that matter, and the argmax is spread back out as a
// one-hot row.
//
// Every block keeps the parameter set it ships with, so the line runs as it
// stands -- point the Constant at your own signal and retune from there. The
// widths are consistent end to end: three features as far as the Feature
// Selector, which cuts them to two, then a scalar class index, then a two-wide
// one-hot row.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7, with one correction that section does
// not make: a block's ports sit at its vertical centre only when there is ONE of
// them on that side. The Missing Value Imputer and the Argmax Decision each have
// TWO outputs, spread apart, so their successors are placed against the port
// rather than the frame -- which is why the Standard Scaler sits 14 above the
// Imputer's row. Aligning those on block centres instead is what turns a link
// into a four-corner staircase. Every position here was solved from the measured
// port offsets and checked against the canonical save.
// ---- Clean and scale (port row y = 620)
raw = block(Constant)
raw.move(40, 655)
raw.resize(70, 70)
raw.setConfig(Constant Value, [0.8; -1.5; 3.2])
imp = block(Missing_Value_Imputer)
imp.move(200, 660)
imp.resize(140, 80)
std = block(Standard_Scaler)
std.move(420, 674)
std.resize(120, 80)
rob = block(Robust_Scaler)
rob.move(620, 674)
rob.resize(120, 80)
mab = block(Max_Abs_Scaler)
mab.move(820, 674)
mab.resize(120, 80)
mms = block(Min_Max_Scaler)
mms.move(1020, 674)
mms.resize(120, 80)
// ---- Shape and decide (port row y = 260)
pwr = block(Power_Transformer)
pwr.move(200, 300)
pwr.resize(120, 80)
nrm = block(Normalizer)
nrm.move(400, 300)
nrm.resize(120, 80)
sel = block(Feature_Selector)
sel.move(600, 300)
sel.resize(120, 80)
sel.setConfig(Indices, [0; 2])
arg = block(Argmax_Decision)
arg.move(800, 300)
arg.resize(120, 80)
ohe = block(One_Hot_Encoder)
ohe.move(1000, 316)
ohe.resize(120, 84)
ohe.setConfig(Number Of Classes, 2)
out = block(Display)
out.move(1200, 299)
out.resize(90, 50)
// ---- Links
l1 = connect(raw<0>, imp<0>)
l2 = connect(imp<0>, std<0>)
l3 = connect(std<0>, rob<0>)
l4 = connect(rob<0>, mab<0>)
l5 = connect(mab<0>, mms<0>)
l6 = connect(pwr<0>, nrm<0>)
l7 = connect(nrm<0>, sel<0>)
l8 = connect(sel<0>, arg<0>)
l9 = connect(arg<0>, ohe<0>)
l10 = connect(ohe<0>, out<0>)
// The one link the planner cannot route well on its own: the end of
// the upper chain back to the start of the lower one. Pinned through
// the empty band between the two rows (y = 440), which clears the
// upper chain's name labels -- they hang about 20 under its frames.
wrap = connect(mms<0>, pwr<0>)
wrap.setCorners((1135, 634), (1170, 634), (1170, 440), (100, 440), (100, 260), (205, 260))
a = area()
a.move(15, 729)
a.resize(1185, 209)
a.setColor(168, 152, 232)
a.setTitle("Clean and scale")
a2 = area()
a2.move(175, 371)
a2.resize(1185, 211)
a2.setColor(232, 168, 124)
a2.setTitle("Shape and decide")
t = textbox()
t.move(560, 120)
t.setText("Three features in; the Feature Selector cuts them to two, and the argmax comes back out as a one-hot row.")
PWM Drive#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/pwm_drive.iscript · category Control Systems · kind subsystem · compiled into the binary
Two pulse-width modulators driven by one duty-cycle sweep, each averaged by a first-order lag so the duty is visible in the output.
Places: Sine_Wave, PWM, Transfer_Function, Scope, Constant, Variable_Pulse_Generator
// @title: PWM Drive
// @category: Control Systems
// @summary: Two pulse-width modulators driven by one duty-cycle sweep, each averaged by a first-order lag so the duty is visible in the output.
// @kind: subsystem
//
// A switching stage and what it looks like after the load smooths it. One
// duty-cycle signal sweeps 0 -> 1 -> 0 and drives both modulators: the
// fixed-period PWM block, and the Variable Pulse Generator whose period
// arrives on a port. Each pulse train feeds a 1/(s+1) lag standing in for
// the load, so the lag output traces the duty cycle while the raw train
// switches underneath it.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// lane (260 and 520), sizes pinned with resize(). The duty source sits on
// the upper lane, so every forward link is dead straight and the only one
// that steps is the branch down to the second modulator.
duty = block(Sine_Wave)
duty.move(40, 555)
duty.resize(70, 70)
duty.setConfig(Amplitude, 0.5)
duty.setConfig(Bias, 0.5)
duty.setConfig(Frequency (Rad), 0.5)
pwm = block(PWM)
pwm.move(280, 555)
pwm.resize(70, 70)
pwm.setConfig(Period (s), 0.2)
pwm.setConfig(Initial Delay (s), 0)
laga = block(Transfer_Function)
laga.move(480, 565)
laga.resize(130, 90)
laga.setConfig(Numerator, [1])
laga.setConfig(Denominator, [1, 1])
scpa = block(Scope)
scpa.move(720, 555)
scpa.resize(70, 70)
period = block(Constant)
period.move(40, 281.7)
period.resize(70, 70)
period.setConfig(Constant Value, 0.2)
vpg = block(Variable_Pulse_Generator)
vpg.move(280, 300)
vpg.resize(80, 80)
lagb = block(Transfer_Function)
lagb.move(480, 305)
lagb.resize(130, 90)
lagb.setConfig(Numerator, [1])
lagb.setConfig(Denominator, [1, 1])
scpb = block(Scope)
scpb.move(720, 295)
scpb.resize(70, 70)
// The duty branches to both modulators; everything downstream is straight.
d1 = connect(duty<0>, pwm<0>)
connect(d1, vpg<0>)
connect(pwm<0>, laga<0>)
connect(laga<0>, scpa<0>)
connect(period<0>, vpg<1>)
connect(vpg<0>, lagb<0>)
connect(lagb<0>, scpb<0>)
// Group washes in the identity sheet's data accents; only the hue is ours.
a1 = area()
a1.move(235, 630)
a1.resize(600, 220)
a1.setColor(168, 152, 232)
a1.setTitle("Fixed period")
a2 = area()
a2.move(235, 370)
a2.resize(600, 220)
a2.setColor(232, 168, 124)
a2.setTitle("Period on a port")
t = textbox()
t.move(300, 110)
t.setText("The lag output traces the duty cycle; the pulse train switching under it is what the load is averaging. Change Period (s) on the PWM block, or the constant feeding T, to see the ripple grow.")
Quaternion Orientation Pipeline#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/quaternion_orientation_pipeline.iscript · category Robotics · kind subsystem · compiled into the binary
Euler angles and a gyro rate composed into one orientation quaternion, then read back out as angles, as a rotation matrix, and as a rotated vector.
Places: Euler_To_Quaternion, Sine_Wave, Constant, Gyro_Quaternion_Integration, Quaternion_Multiply, Quaternion_To_Euler, Scope, Quaternion_To_Rotation_Matrix, Quaternion_Rotate_Vector
// @title: Quaternion Orientation Pipeline
// @category: Robotics
// @summary: Euler angles and a gyro rate composed into one orientation quaternion, then read back out as angles, as a rotation matrix, and as a rotated vector.
// @kind: subsystem
//
// A whole orientation pipeline, end to end. Two independent sources of
// attitude are combined and then consumed three different ways:
//
// in a yaw/pitch/roll triple becomes a quaternion, while a constant
// body rate is integrated into a second quaternion
// mid the two are composed -- quaternion multiply is how rotations
// compose, and the order matters
// out the result is read back as Euler angles, as a 3x3 rotation
// matrix, and by rotating the body x-axis into world coordinates
//
// The yaw source sweeps, so everything downstream moves; pitch and roll are
// held so the angles coming back out stay easy to read against the ones
// going in. Quaternion To Euler is the inverse of Euler To Quaternion, so
// with the gyro rate set to zero its three outputs reproduce the three
// inputs exactly -- a check worth running before trusting the rest.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, sizes pinned
// with resize(). The pipeline's spine is straight; the links that step are
// the ones feeding a second or third port, which cannot share a row.
e2q = block(Euler_To_Quaternion)
e2q.move(330, 924)
e2q.resize(132, 96)
yaw = block(Sine_Wave)
yaw.move(40, 935)
yaw.resize(70, 70)
yaw.setConfig(Amplitude, 3.1416)
yaw.setConfig(Frequency (Rad), 0.4)
connect(yaw<0>, e2q<0>)
pitch = block(Constant)
pitch.move(40, 805)
pitch.resize(70, 70)
pitch.setConfig(Constant Value, 0.2)
connect(pitch<0>, e2q<1>)
roll = block(Constant)
roll.move(40, 675)
roll.resize(70, 70)
roll.setConfig(Constant Value, 0)
connect(roll<0>, e2q<2>)
gyro = block(Gyro_Quaternion_Integration)
gyro.move(330, 522)
gyro.resize(140, 84)
gyro.setConfig(Initial Quaternion, [1; 0; 0; 0])
omega = block(Constant)
omega.move(40, 515)
omega.resize(70, 70)
omega.setConfig(Constant Value, [0; 0; 0.3])
connect(omega<0>, gyro<0>)
mul = block(Quaternion_Multiply)
mul.move(620, 904)
mul.resize(126, 84)
l_q1 = connect(e2q<0>, mul<0>)
connect(gyro<0>, mul<1>)
q2e = block(Quaternion_To_Euler)
q2e.move(960, 910)
q2e.resize(132, 96)
l_q = connect(mul<0>, q2e<0>)
q2e_o0 = block(Scope)
q2e_o0.move(1200, 921)
q2e_o0.resize(70, 70)
connect(q2e<0>, q2e_o0<0>)
q2e_o1 = block(Scope)
q2e_o1.move(1350, 781)
q2e_o1.resize(70, 70)
connect(q2e<1>, q2e_o1<0>)
q2e_o2 = block(Scope)
q2e_o2.move(1500, 641)
q2e_o2.resize(70, 70)
connect(q2e<2>, q2e_o2<0>)
q2r = block(Quaternion_To_Rotation_Matrix)
q2r.move(960, 462)
q2r.resize(126, 84)
connect(l_q, q2r<0>)
q2r_s = block(Scope)
q2r_s.move(1200, 455)
q2r_s.resize(70, 70)
connect(q2r<0>, q2r_s<0>)
qrot = block(Quaternion_Rotate_Vector)
qrot.move(960, 268)
qrot.resize(126, 84)
connect(l_q, qrot<0>)
vbody = block(Constant)
vbody.move(800, 395)
vbody.resize(70, 70)
vbody.setConfig(Constant Value, [1; 0; 0])
connect(vbody<0>, qrot<1>)
qrot_s = block(Scope)
qrot_s.move(1200, 261)
qrot_s.resize(70, 70)
connect(qrot<0>, qrot_s<0>)
// Group washes in the identity sheet's data accents; only the hue is ours.
a1 = area()
a1.move(-5, 1000)
a1.resize(520, 627)
a1.setColor(168, 152, 232)
a1.setTitle("Attitude in")
a2 = area()
a2.move(575, 969)
a2.resize(216, 214)
a2.setColor(232, 168, 124)
a2.setTitle("Compose")
a3 = area()
a3.move(915, 986)
a3.resize(400, 867)
a3.setColor(124, 200, 160)
a3.setTitle("Attitude out")
t = textbox()
t.move(420, 110)
t.setText("Set the gyro rate constant to [0; 0; 0] and the three angles coming out of Quaternion To Euler should match the three going in -- that round trip is the quickest way to confirm the conventions match whatever your sensor reports. Then put the rate back to see the two rotations compose.")
Random and Counting Sources#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/random_and_counting_sources.iscript · category Control Systems · kind subsystem · compiled into the binary
The three random sources and the three counting ones, each on its own sink, with the seeds spelled out.
Places: Band_Limited_White_Noise, Scope, Random_Number, Uniform_Random_Number, Counter_Free_Running, Counter_Limited, Enumerated_Constant, Display
// @title: Random and Counting Sources
// @category: Control Systems
// @summary: The three random sources and the three counting ones, each on its own sink, with the seeds spelled out.
// @kind: subsystem
//
// The sources that are not a fixed shape: three that produce noise and three
// that count. Drop one in front of a plant to shake it, or in front of a
// filter to see what the filter removes.
//
// Every random source here is SEEDED, and the seed is a parameter like any
// other -- so two runs of this template produce the same numbers, and changing
// a seed is how you get a different draw. That is deliberate: a model whose
// noise changed on every run could not be compared against itself.
//
// Band-Limited White Noise is the one to reach for in a continuous model: its
// "Noise Sample Time" sets how often a new value is drawn, so the noise has a
// bandwidth rather than being white at whatever rate the solver happens to
// step. Random Number and Uniform Random Number draw once per sample.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7. Every block is 70 tall, so each row
// shares one port row (260, 460) and every move-y is that row + 35 for the
// 70-tall blocks, + 25 for the 50-tall Display.
// ------------------------------------------------------------------- random
n_band = block(Band_Limited_White_Noise)
n_band.move(40, 495)
n_band.resize(80, 70)
n_band.setConfig(Noise Power, 0.05)
n_band.setConfig(Noise Sample Time (s), 0.1)
n_band.setConfig(Seed, 23341)
v_band = block(Scope)
v_band.move(240, 495)
v_band.resize(70, 70)
connect(n_band<0>, v_band<0>)
n_norm = block(Random_Number)
n_norm.move(460, 495)
n_norm.resize(80, 70)
n_norm.setConfig(Mean, 0)
n_norm.setConfig(Variance, 1)
n_norm.setConfig(Seed, 7)
v_norm = block(Scope)
v_norm.move(660, 495)
v_norm.resize(70, 70)
connect(n_norm<0>, v_norm<0>)
n_unif = block(Uniform_Random_Number)
n_unif.move(880, 495)
n_unif.resize(80, 70)
n_unif.setConfig(Minimum, -1)
n_unif.setConfig(Maximum, 1)
n_unif.setConfig(Seed, 13)
v_unif = block(Scope)
v_unif.move(1080, 495)
v_unif.resize(70, 70)
connect(n_unif<0>, v_unif<0>)
// ------------------------------------------------------- counting and enumerated
c_free = block(Counter_Free_Running)
c_free.move(40, 295)
c_free.resize(80, 70)
c_free.setConfig(Number of Bits, 4)
v_free = block(Scope)
v_free.move(240, 295)
v_free.resize(70, 70)
connect(c_free<0>, v_free<0>)
c_lim = block(Counter_Limited)
c_lim.move(460, 295)
c_lim.resize(80, 70)
c_lim.setConfig(Upper Limit, 7)
v_lim = block(Scope)
v_lim.move(660, 295)
v_lim.resize(70, 70)
connect(c_lim<0>, v_lim<0>)
// An enumerated constant does not move, so it goes on a display rather than a
// scope: what matters is the underlying value the rest of the model sees.
c_enum = block(Enumerated_Constant)
c_enum.move(880, 295)
c_enum.resize(90, 70)
v_enum = block(Display)
v_enum.move(1080, 285)
v_enum.resize(90, 50)
connect(c_enum<0>, v_enum<0>)
a1 = area()
a1.move(15, 550)
a1.resize(1160, 190)
a1.setColor(168, 152, 232)
a1.setTitle("Random, and seeded")
a2 = area()
a2.move(15, 350)
a2.resize(1180, 190)
a2.setColor(232, 168, 124)
a2.setTitle("Counting and enumerated")
t = textbox()
t.move(40, 120)
t.setText("Change a seed to get a different draw; leave it alone and every run repeats exactly.")
Recurrent Cells#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/recurrent_cells.iscript · category Machine Learning · kind subsystem · compiled into the binary
One sine driving a simple RNN, a GRU and an LSTM at once, so the three hidden states can be compared.
Places: Sine_Wave, Simple_RNN_Cell, Scope, GRU_Cell, LSTM_Cell
// @title: Recurrent Cells
// @category: Machine Learning
// @summary: One sine driving a simple RNN, a GRU and an LSTM at once, so the three hidden states can be compared.
// @kind: subsystem
//
// The three recurrent cells the library ships, all fed the SAME sequence, so the
// scopes are directly comparable. That is the whole point of the diagram: on a
// smooth input the three look alike, and the differences only show up when the
// input moves faster than they can follow -- which is when the gates earn their
// keep.
//
// Simple RNN one tanh over [input, previous hidden]. Nothing forgets.
// GRU an update gate and a reset gate: three matrices per step.
// LSTM input, forget, cell and output gates -- four matrices, and a
// CELL state kept separately from the hidden one. That second
// state is the bottom scope: it is what carries information past
// a long stretch of uninformative input, and it is the reason an
// LSTM has two outputs where the other two have one.
//
// All three keep a 2-wide hidden state, and each has had its INPUT weights set to
// a single column so it takes the scalar sine directly -- as shipped they expect
// a 3-wide input. The recurrent weights, biases and initial states are untouched,
// so the gate arithmetic is the library's own.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: four lanes, every forward link
// straight, the sine's link branching twice to feed the lower two cells. The
// cell-state scope gets a lane of its own rather than sitting beside the hidden
// one: the two LSTM outputs are only 31 apart and a scope is 70 tall, so side by
// side the second link has to detour around the first scope -- which the
// canonical save reports as a six-corner wander.
// ---- Simple RNN (port row y = 1080)
src = block(Sine_Wave)
src.move(60, 1115)
src.resize(70, 70)
src.setConfig(Amplitude, 1)
src.setConfig(Frequency (Rad), 2)
rnn = block(Simple_RNN_Cell)
rnn.move(340, 1124)
rnn.resize(126, 88)
rnn.setConfig(Input Weights, [0.8; -0.3])
sc1 = block(Scope)
sc1.move(640, 1115)
sc1.resize(70, 70)
// ---- GRU (port row y = 780)
gru = block(GRU_Cell)
gru.move(340, 824)
gru.resize(126, 88)
gru.setConfig(Input Weights, [0.5; 0.4; -0.35; 0.2; 0.6; -0.15])
sc2 = block(Scope)
sc2.move(640, 815)
sc2.resize(70, 70)
// ---- LSTM — hidden state (port row y = 480)
lstm = block(LSTM_Cell)
lstm.move(340, 526)
lstm.resize(130, 92)
lstm.setConfig(Input Weights, [0.5; 0.4; -0.35; 0.2; 0.6; -0.15; 0.35; -0.3])
sc3 = block(Scope)
sc3.move(640, 531)
sc3.resize(70, 70)
// ---- LSTM — cell state (port row y = 240)
sc4 = block(Scope)
sc4.move(640, 275)
sc4.resize(70, 70)
// ---- Links
l1 = connect(src<0>, rnn<0>)
l2 = connect(rnn<0>, sc1<0>)
l3 = connect(gru<0>, sc2<0>)
l4 = connect(lstm<0>, sc3<0>)
b1 = connect(l1, gru<0>)
b1.setCorners((220, 1080), (220, 780), (345, 780))
b2 = connect(l1, lstm<0>)
b2.setCorners((220, 1080), (220, 480), (345, 480))
// The cell state drops to its own lane before it goes right, so it
// never has to get past the hidden-state scope.
cell = connect(lstm<1>, sc4<0>)
cell.setCorners((465, 465), (560, 465), (560, 240), (645, 240))
a = area()
a.move(315, 1179)
a.resize(425, 199)
a.setColor(168, 152, 232)
a.setTitle("No gates")
a2 = area()
a2.move(315, 879)
a2.resize(425, 199)
a2.setColor(124, 200, 168)
a2.setTitle("Two gates")
a3 = area()
a3.move(315, 586)
a3.resize(425, 426)
a3.setColor(232, 168, 124)
a3.setTitle("Four gates, two states")
t = textbox()
t.move(320, 110)
t.setText("Same input to all three. Raise the sine's frequency until the input changes faster than the cells can follow, and the gated ones pull away from the plain RNN.")
Resettable and Special Delays#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/resettable_and_special_delays.iscript · category Control Systems · kind subsystem · compiled into the binary
One sine through four controllable delays and two discrete state-space forms, each on its own scope.
Places: Sine_Wave, Pulse_Generator, Constant, Enabled_Delay, Resettable_Delay, Variable_Integer_Delay, Propagation_Delay, Fixed_Point_State_Space, Discrete_Nonlinear_State_Space, Scope
// @title: Resettable and Special Delays
// @category: Control Systems
// @summary: One sine through four controllable delays and two discrete state-space forms, each on its own scope.
// @kind: subsystem
//
// The delays that take an input other than the signal. An ordinary Unit Delay
// holds for exactly one step forever; these four are told what to do while
// they run:
//
// Enabled Delay shifts only while E is high, and holds otherwise.
// Resettable Delay returns to its initial condition on R.
// Variable Integer Delay delays by d samples, and d may change every step.
// Propagation Delay delays by d, buffering what is in flight.
//
// Underneath them are the two discrete state-space forms that are not plain
// Discrete State Space:
//
// Fixed Point State Space ships a third-order filter in fixed-point
// arithmetic — the same maths an FPGA or a small
// MCU would run.
// Discrete Nonlinear State Space takes a second input f, which is where a
// nonlinear term enters the state update. It is
// fed a constant here so the template loads with
// no algebraic loop; wire your own nonlinearity of
// the state or input into f.
//
// One sine drives all six, so the scopes are directly comparable: the delay
// lanes differ only by their control input, and the two state-space lanes
// show what the same excitation does through each form.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// lane (1060, 900, 740, 580, 420, 260), sizes pinned with resize(). The
// control inputs land on the SECOND port of each block, which on a 2-input
// block of height h sits at centre - h/6, not at the centre.
// ---- the shared drive, and the three control signals
u = block(Sine_Wave)
u.move(40, 695)
u.resize(70, 70)
u.setConfig(Amplitude, 1)
u.setConfig(Frequency (Rad), 1)
ctrl = block(Pulse_Generator)
ctrl.move(40, 535)
ctrl.resize(80, 70)
ctrl.setConfig(Period, 5)
ctrl.setConfig(Pulse Width, 50)
dsteps = block(Constant)
dsteps.move(40, 415)
dsteps.resize(70, 70)
dsteps.setConfig(Constant Value, 3)
fterm = block(Constant)
fterm.move(40, 295)
fterm.resize(70, 70)
fterm.setConfig(Constant Value, 0)
// ---- four controllable delays
enabled = block(Enabled_Delay)
enabled.move(400, 1100)
enabled.resize(110, 80)
enabled.setConfig(Delay Length (samples), 4)
resettable = block(Resettable_Delay)
resettable.move(400, 940)
resettable.resize(110, 80)
resettable.setConfig(Delay Length (samples), 4)
variable = block(Variable_Integer_Delay)
variable.move(400, 780)
variable.resize(110, 80)
variable.setConfig(Maximum Delay (samples), 20)
propagation = block(Propagation_Delay)
propagation.move(400, 620)
propagation.resize(110, 80)
propagation.setConfig(Buffer Capacity (samples), 64)
// ---- the two special discrete state-space forms
fixedpt = block(Fixed_Point_State_Space)
fixedpt.move(400, 465)
fixedpt.resize(130, 90)
nonlinear = block(Discrete_Nonlinear_State_Space)
nonlinear.move(400, 305)
nonlinear.resize(130, 90)
// ---- one scope per lane, each on the lane's port row
sEnabled = block(Scope)
sEnabled.move(700, 1095)
sEnabled.resize(70, 70)
sResettable = block(Scope)
sResettable.move(700, 935)
sResettable.resize(70, 70)
sVariable = block(Scope)
sVariable.move(700, 775)
sVariable.resize(70, 70)
sPropagation = block(Scope)
sPropagation.move(700, 615)
sPropagation.resize(70, 70)
sFixedpt = block(Scope)
sFixedpt.move(700, 455)
sFixedpt.resize(70, 70)
sNonlinear = block(Scope)
sNonlinear.move(700, 295)
sNonlinear.resize(70, 70)
// ---- the sine reaches every lane; the controls reach the lanes that take one
drive = connect(u<0>, enabled<0>)
connect(drive, resettable<0>)
connect(drive, variable<0>)
connect(drive, propagation<0>)
connect(drive, fixedpt<0>)
connect(drive, nonlinear<0>)
gateSig = connect(ctrl<0>, enabled<1>)
connect(gateSig, resettable<1>)
amount = connect(dsteps<0>, variable<1>)
connect(amount, propagation<1>)
connect(fterm<0>, nonlinear<1>)
connect(enabled<0>, sEnabled<0>)
connect(resettable<0>, sResettable<0>)
connect(variable<0>, sVariable<0>)
connect(propagation<0>, sPropagation<0>)
connect(fixedpt<0>, sFixedpt<0>)
connect(nonlinear<0>, sNonlinear<0>)
// Group washes, in the identity sheet's data accents.
a = area()
a.move(15, 750)
a.resize(285, 550)
a.setColor(168, 152, 232)
a.setTitle("Drive and controls")
a2 = area()
a2.move(375, 1155)
a2.resize(425, 320)
a2.setColor(232, 168, 124)
a2.setTitle("Gated by a signal")
a3 = area()
a3.move(375, 835)
a3.resize(425, 318)
a3.setColor(124, 200, 232)
a3.setTitle("Delayed by an amount")
a4 = area()
a4.move(375, 517)
a4.resize(425, 327)
a4.setColor(152, 216, 168)
a4.setTitle("Discrete state-space forms")
t = textbox()
t.move(380, 160)
t.setText("The pulse drives E and R, so the top two lanes freeze and reset on it. The constant sets d for the two variable delays — drive d from a signal instead and the delay itself becomes dynamic.")
Safety Supervision Chain#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/safety_supervision_chain.iscript · category Robotics · kind subsystem · compiled into the binary
A motor command watched by a watchdog, a stall detector and an emergency stop ramp, each reporting on its own scope.
Places: Sine_Wave, Saturation, Watchdog_Timer, Scope, Stall_Detector, Constant, Emergency_Stop_Ramp, Pulse_Generator
// @title: Safety Supervision Chain
// @category: Robotics
// @summary: A motor command watched by a watchdog, a stall detector and an emergency stop ramp, each reporting on its own scope.
// @kind: subsystem
//
// The supervision a motion command passes through before it is trusted. One
// command is generated, clipped to what the actuator can deliver, and then
// three independent supervisors watch it:
//
// Watchdog Timer - the clipped command stops changing on the flat
// tops, which is exactly the frozen-signal case
// the watchdog exists to catch.
// Stall Detector - high torque with the speed near zero. The speed
// crosses zero every half period, so the stall
// flag asserts and clears on its own.
// Emergency Stop Ramp - the e-stop pulse takes the command to zero on a
// controlled deceleration rather than dropping it,
// and the reset pulse hands control back.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// lane, sizes pinned with resize(). Each supervisor's first output continues
// its lane straight; the others step down to their own scopes.
cmd = block(Sine_Wave)
cmd.move(40, 915)
cmd.resize(70, 70)
cmd.setConfig(Amplitude, 1)
cmd.setConfig(Frequency (Rad), 0.6)
sat = block(Saturation)
sat.move(240, 915)
sat.resize(70, 70)
sat.setConfig(Upper Limit, 0.6)
sat.setConfig(Lower Limit, -0.6)
connect(cmd<0>, sat<0>)
wd = block(Watchdog_Timer)
wd.move(460, 920)
wd.resize(132, 80)
wd.setConfig(Freeze Tolerance, 1e-6)
wd.setConfig(Stuck Samples, 10)
wd.setConfig(Initial Input, 0)
l_sat = connect(sat<0>, wd<0>)
wd_o0 = block(Scope)
wd_o0.move(700, 928.3)
wd_o0.resize(70, 70)
connect(wd<0>, wd_o0<0>)
wd_o1 = block(Scope)
wd_o1.move(850, 798.3)
wd_o1.resize(70, 70)
connect(wd<1>, wd_o1<0>)
stall = block(Stall_Detector)
stall.move(460, 588)
stall.resize(132, 84)
stall.setConfig(Torque Threshold, 0.8)
stall.setConfig(Speed Threshold, 0.1)
stall.setConfig(Stall Samples, 10)
tau = block(Constant)
tau.move(40, 595)
tau.resize(70, 70)
tau.setConfig(Constant Value, 1)
connect(tau<0>, stall<0>)
omega = block(Sine_Wave)
omega.move(240, 568)
omega.resize(70, 70)
omega.setConfig(Amplitude, 1)
omega.setConfig(Frequency (Rad), 0.6)
connect(omega<0>, stall<1>)
stall_o0 = block(Scope)
stall_o0.move(700, 595)
stall_o0.resize(70, 70)
connect(stall<0>, stall_o0<0>)
stall_o1 = block(Scope)
stall_o1.move(850, 465)
stall_o1.resize(70, 70)
connect(stall<1>, stall_o1<0>)
est = block(Emergency_Stop_Ramp)
est.move(460, 283)
est.resize(140, 92)
est.setConfig(Deceleration, 1)
est.setConfig(Start Stopped, No)
estop = block(Pulse_Generator)
estop.move(40, 272)
estop.resize(80, 70)
estop.setConfig(Amplitude, 1)
estop.setConfig(Period, 12)
estop.setConfig(Pulse Width, 25)
estop.setConfig(Phase Delay, 4)
connect(estop<0>, est<1>)
reset = block(Pulse_Generator)
reset.move(240, 250)
reset.resize(80, 70)
reset.setConfig(Amplitude, 1)
reset.setConfig(Period, 12)
reset.setConfig(Pulse Width, 10)
reset.setConfig(Phase Delay, 9)
connect(reset<0>, est<2>)
est_o0 = block(Scope)
est_o0.move(700, 287.3)
est_o0.resize(70, 70)
connect(est<0>, est_o0<0>)
est_o1 = block(Scope)
est_o1.move(850, 157.3)
est_o1.resize(70, 70)
connect(est<1>, est_o1<0>)
// The clipped command also drives the e-stop lane; that branch is the
// only link here that steps, which is what a fan-out looks like.
connect(l_sat, est<0>)
// Group washes in the identity sheet's data accents; only the hue is ours.
a1 = area()
a1.move(415, 993)
a1.resize(550, 330)
a1.setColor(168, 152, 232)
a1.setTitle("Watchdog")
a2 = area()
a2.move(415, 660)
a2.resize(550, 330)
a2.setColor(232, 168, 124)
a2.setTitle("Stall detection")
a3 = area()
a3.move(415, 352)
a3.resize(550, 330)
a3.setColor(124, 200, 160)
a3.setTitle("Emergency stop")
t = textbox()
t.move(300, 110)
t.setText("Every supervisor here trips on its own during a run: the watchdog on the clipped flat tops, the stall flag as the speed crosses zero under load, and the stop ramp on the e-stop pulse. Raise Stuck Samples or Stall Samples to make a supervisor harder to trip.")
Saturated Gain#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/saturated_gain.iscript · category Control Systems · kind subsystem · compiled into the binary
A gain followed by a saturation, so the limits can be seen clipping.
Places: Sine_Wave, Gain, Saturation, Scope
// @title: Saturated Gain
// @category: Control Systems
// @summary: A gain followed by a saturation, so the limits can be seen clipping.
// @kind: subsystem
//
// The actuator end of a control loop in miniature: amplify, then clip at what
// the hardware can actually deliver. Drop it in front of a plant to give a
// model a realistic output limit.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row at
// y = 260. All four blocks are 70 tall, so one move-y aligns them all.
src = block(Sine_Wave)
src.move(40, 295)
src.resize(70, 70)
src.setConfig(Amplitude, 1)
k = block(Gain)
k.move(240, 295)
k.resize(70, 70)
k.setConfig(Gain Value, 2.5)
lim = block(Saturation)
lim.move(420, 295)
lim.resize(70, 70)
lim.setConfig(Upper Limit, 1.5)
lim.setConfig(Lower Limit, -1.5)
out = block(Scope)
out.move(600, 295)
out.resize(70, 70)
connect(src<0>, k<0>)
connect(k<0>, lim<0>)
connect(lim<0>, out<0>)
Scalar Math Tour#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/scalar_math_tour.iscript · category Control Systems · kind subsystem · compiled into the binary
Every scalar math operator in the Base Blocks family, each on its own constant, each read on a display.
Places: Constant, Abs, Display, Sign, Unary_Minus, Sqrt, Signed_Sqrt, Reciprocal_Sqrt, Bias, Rounding_Function, Polynomial, Math_Function, Trigonometric_Function
// @title: Scalar Math Tour
// @category: Control Systems
// @summary: Every scalar math operator in the Base Blocks family, each on its own constant, each read on a display.
// @kind: subsystem
//
// A reference card you can run. Eleven lanes, one per operator, each the same
// three-block shape: a Constant chosen to show what the operator does, the
// operator itself, and a Display holding the answer. Run it once and read the
// displays down the canvas; change a constant and run it again.
//
// The four blocks with a dropdown -- Sqrt, Rounding Function, Math Function and
// Trigonometric Function -- are ONE block each with an operator list, not four
// separate blocks. Sqrt, Math Function and Trigonometric Function are left on
// their default selection (square root, e^u, sin(u)); Rounding Function is set
// to Round so the tour shows a dropdown carrying a non-default choice. Open any
// of them to see the rest of the list.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7. This one is a GALLERY rather than a
// chain, so the port-row rule applies per LANE: each row of lanes has its own
// port row (260, 460, 660, 860), and within a row every block's move-y is that
// row plus half its height -- 70-tall blocks at row+35, the 50-tall Displays at
// row+25 -- which is what keeps all twenty-two links dead straight.
// ---------------------------------------------------------------- sign and magnitude
c_abs = block(Constant)
c_abs.move(40, 895)
c_abs.resize(70, 70)
c_abs.setConfig(Constant Value, -3.5)
f_abs = block(Abs)
f_abs.move(200, 895)
f_abs.resize(70, 70)
d_abs = block(Display)
d_abs.move(380, 885)
d_abs.resize(90, 50)
connect(c_abs<0>, f_abs<0>)
connect(f_abs<0>, d_abs<0>)
c_sign = block(Constant)
c_sign.move(560, 895)
c_sign.resize(70, 70)
c_sign.setConfig(Constant Value, -3.5)
f_sign = block(Sign)
f_sign.move(720, 895)
f_sign.resize(70, 70)
d_sign = block(Display)
d_sign.move(900, 885)
d_sign.resize(90, 50)
connect(c_sign<0>, f_sign<0>)
connect(f_sign<0>, d_sign<0>)
c_neg = block(Constant)
c_neg.move(1080, 895)
c_neg.resize(70, 70)
c_neg.setConfig(Constant Value, 2.5)
f_neg = block(Unary_Minus)
f_neg.move(1240, 895)
f_neg.resize(70, 70)
d_neg = block(Display)
d_neg.move(1420, 885)
d_neg.resize(90, 50)
connect(c_neg<0>, f_neg<0>)
connect(f_neg<0>, d_neg<0>)
// ---------------------------------------------------------------- roots
c_sqrt = block(Constant)
c_sqrt.move(40, 695)
c_sqrt.resize(70, 70)
c_sqrt.setConfig(Constant Value, 9)
f_sqrt = block(Sqrt)
f_sqrt.move(200, 695)
f_sqrt.resize(70, 70)
d_sqrt = block(Display)
d_sqrt.move(380, 685)
d_sqrt.resize(90, 50)
connect(c_sqrt<0>, f_sqrt<0>)
connect(f_sqrt<0>, d_sqrt<0>)
c_ssqrt = block(Constant)
c_ssqrt.move(560, 695)
c_ssqrt.resize(70, 70)
c_ssqrt.setConfig(Constant Value, -9)
f_ssqrt = block(Signed_Sqrt)
f_ssqrt.move(720, 695)
f_ssqrt.resize(70, 70)
d_ssqrt = block(Display)
d_ssqrt.move(900, 685)
d_ssqrt.resize(90, 50)
connect(c_ssqrt<0>, f_ssqrt<0>)
connect(f_ssqrt<0>, d_ssqrt<0>)
c_rsqrt = block(Constant)
c_rsqrt.move(1080, 695)
c_rsqrt.resize(70, 70)
c_rsqrt.setConfig(Constant Value, 4)
f_rsqrt = block(Reciprocal_Sqrt)
f_rsqrt.move(1240, 695)
f_rsqrt.resize(70, 70)
d_rsqrt = block(Display)
d_rsqrt.move(1420, 685)
d_rsqrt.resize(90, 50)
connect(c_rsqrt<0>, f_rsqrt<0>)
connect(f_rsqrt<0>, d_rsqrt<0>)
// ---------------------------------------------------------------- shaping
c_bias = block(Constant)
c_bias.move(40, 495)
c_bias.resize(70, 70)
c_bias.setConfig(Constant Value, 2)
f_bias = block(Bias)
f_bias.move(200, 495)
f_bias.resize(70, 70)
f_bias.setConfig(Bias, 5)
d_bias = block(Display)
d_bias.move(380, 485)
d_bias.resize(90, 50)
connect(c_bias<0>, f_bias<0>)
connect(f_bias<0>, d_bias<0>)
c_round = block(Constant)
c_round.move(560, 495)
c_round.resize(70, 70)
c_round.setConfig(Constant Value, 2.7)
// The whole option list travels with the value, selection after the "~~" --
// that is how an options config variable is stored, and writing the bare
// selection alone would turn the block's dropdown into a plain text field.
f_round = block(Rounding_Function)
f_round.move(720, 495)
f_round.resize(80, 70)
f_round.setConfig(Operator, Floor (toward -Inf)%~%Ceiling (toward +Inf)%~%Round (nearest, ties away from zero)%~%Fix (toward zero)~~Round (nearest, ties away from zero))
d_round = block(Display)
d_round.move(900, 485)
d_round.resize(90, 50)
connect(c_round<0>, f_round<0>)
connect(f_round<0>, d_round<0>)
c_poly = block(Constant)
c_poly.move(1080, 495)
c_poly.resize(70, 70)
c_poly.setConfig(Constant Value, 2)
f_poly = block(Polynomial)
f_poly.move(1240, 495)
f_poly.resize(90, 70)
f_poly.setConfig(Polynomial Coefficients, [1 0 -1])
d_poly = block(Display)
d_poly.move(1420, 485)
d_poly.resize(90, 50)
connect(c_poly<0>, f_poly<0>)
connect(f_poly<0>, d_poly<0>)
// ---------------------------------------------------------------- functions
c_math = block(Constant)
c_math.move(40, 295)
c_math.resize(70, 70)
c_math.setConfig(Constant Value, 1)
f_math = block(Math_Function)
f_math.move(200, 295)
f_math.resize(80, 70)
d_math = block(Display)
d_math.move(380, 285)
d_math.resize(90, 50)
connect(c_math<0>, f_math<0>)
connect(f_math<0>, d_math<0>)
c_trig = block(Constant)
c_trig.move(560, 295)
c_trig.resize(70, 70)
c_trig.setConfig(Constant Value, 1.5707963)
f_trig = block(Trigonometric_Function)
f_trig.move(720, 295)
f_trig.resize(80, 70)
d_trig = block(Display)
d_trig.move(900, 285)
d_trig.resize(90, 50)
connect(c_trig<0>, f_trig<0>)
connect(f_trig<0>, d_trig<0>)
// One titled wash per row, in the identity sheet's accents. Each is 170 tall
// against a 200 row pitch, so the washes clear one another by 30 and the
// title bar clears the block tops by 35.
a1 = area()
a1.move(15, 950)
a1.resize(1520, 190)
a1.setColor(168, 152, 232)
a1.setTitle("Sign and magnitude")
a2 = area()
a2.move(15, 750)
a2.resize(1520, 190)
a2.setColor(232, 168, 124)
a2.setTitle("Roots")
a3 = area()
a3.move(15, 550)
a3.resize(1520, 190)
a3.setColor(43, 160, 184)
a3.setTitle("Shaping")
a4 = area()
a4.move(15, 350)
a4.resize(1515, 190)
a4.setColor(48, 160, 106)
a4.setTitle("Functions")
t = textbox()
t.move(40, 120)
t.setText("Every lane is independent: a constant, one operator, one display. Change a constant and run to see the operator answer it.")
Signal Feature Extraction#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/signal_feature_extraction.iscript · category Machine Learning · kind subsystem · compiled into the binary
One swept sine read four ways at once: rolling statistics, autocorrelation, polynomial terms and a spectrum.
Places: Chirp_Signal, Rolling_Statistics, Scope, Autocorrelation_Features, Polynomial_Features, Tapped_Delay, FFT_Magnitude, Spectral_Features
// @title: Signal Feature Extraction
// @category: Machine Learning
// @summary: One swept sine read four ways at once: rolling statistics, autocorrelation, polynomial terms and a spectrum.
// @kind: subsystem
//
// The step between a raw signal and a model that expects a feature vector. One
// chirp -- a sine sweeping from 0.1 Hz to 1 Hz -- is tapped four times, and each
// tap turns the same samples into a different kind of feature:
//
// Rolling Statistics the moving mean over the last 5 samples
// Autocorrelation lags 1, 2 and 3 over an 8-sample window
// Polynomial Features the products and powers of the raw terms
// FFT Magnitude the spectrum, read by Spectral Features into
// centroid / spread / roll-off
//
// Because all four see the same input, the scopes are directly comparable: what
// the sweep does to a moving average and what it does to a spectrum show up side
// by side.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: four lanes, each with its own port
// row, every forward link straight. The chirp's output is one link with three
// branches teeing off it, which is how one output feeds several inputs -- the
// same form the shipped PID template's feedback uses.
// ---- Rolling statistics (port row y = 980)
src = block(Chirp_Signal)
src.move(60, 1015)
src.resize(70, 70)
roll = block(Rolling_Statistics)
roll.move(360, 1022)
roll.resize(124, 84)
sc1 = block(Scope)
sc1.move(680, 1015)
sc1.resize(70, 70)
// ---- Autocorrelation (port row y = 740)
acf = block(Autocorrelation_Features)
acf.move(360, 782)
acf.resize(132, 84)
sc2 = block(Scope)
sc2.move(680, 775)
sc2.resize(70, 70)
// ---- Polynomial terms (port row y = 500)
poly = block(Polynomial_Features)
poly.move(360, 540)
poly.resize(130, 80)
sc3 = block(Scope)
sc3.move(680, 535)
sc3.resize(70, 70)
// ---- Spectrum (port row y = 260)
// An FFT needs a WINDOW, and the chirp is one sample per step. Tapped Delay is
// what turns the stream into one: 8 delays, no current input, so its output is
// an [8,1] column of the last eight samples. Without it the FFT saw the raw
// [1,1] scalar and refused it -- "Invalid window length", because 1 is not a
// power of two -- and this template did not run at all.
//
// 8 and not some other length: FFT Magnitude wants a power of two, and it emits
// as many bins as it was given, so Spectral Features (two-sided by default, and
// so wanting an EVEN bin count) gets 8. A 4-sample window would work as well; 8
// gives the sweep enough resolution to see the peak move.
buf = block(Tapped_Delay)
buf.move(180, 300)
buf.resize(120, 80)
buf.setConfig(Number of Delays, 8)
fft = block(FFT_Magnitude)
fft.move(360, 300)
fft.resize(130, 80)
spec = block(Spectral_Features)
spec.move(620, 302)
spec.resize(132, 84)
sc4 = block(Scope)
sc4.move(880, 295)
sc4.resize(70, 70)
// ---- Links
l1 = connect(src<0>, roll<0>)
l2 = connect(roll<0>, sc1<0>)
l3 = connect(acf<0>, sc2<0>)
l4 = connect(poly<0>, sc3<0>)
l5 = connect(fft<0>, spec<0>)
l6 = connect(spec<0>, sc4<0>)
b1 = connect(l1, acf<0>)
b1.setCorners((250, 980), (250, 740), (365, 740))
b2 = connect(l1, poly<0>)
b2.setCorners((250, 980), (250, 500), (365, 500))
b3 = connect(l1, buf<0>)
connect(buf<0>, fft<0>)
b3.setCorners((250, 980), (250, 260), (365, 260))
a = area()
a.move(335, 1077)
a.resize(805, 457)
a.setColor(168, 152, 232)
a.setTitle("Time-domain features")
a2 = area()
a2.move(335, 595)
a2.resize(805, 195)
a2.setColor(124, 200, 168)
a2.setTitle("Shape features")
a3 = area()
// Reaches further left than its two neighbours on purpose: the Tapped Delay that
// windows the stream is part of producing the spectrum, not a source sitting
// outside the group like the chirp is.
a3.move(155, 357)
a3.resize(985, 197)
a3.setColor(232, 168, 124)
a3.setTitle("Frequency-domain features")
t = textbox()
t.move(340, 120)
t.setText("One chirp, four feature views. Widen the Rolling Statistics window or add lags to the autocorrelation and watch the scopes separate.")
Signal Sources Gallery#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/signal_sources_gallery.iscript · category Control Systems · kind subsystem · compiled into the binary
Every deterministic source block in the library, each on its own scope, so their shapes can be compared side by side.
Places: Step, Scope, Ramp, Constant, Sine_Wave, Pulse_Generator, Chirp_Signal, Signal_Generator, Waveform_Generator, Repeating_Sequence, Repeating_Sequence_Stair, Repeating_Sequence_Interpolated, Clock, Digital_Clock
// @title: Signal Sources Gallery
// @category: Control Systems
// @summary: Every deterministic source block in the library, each on its own scope, so their shapes can be compared side by side.
// @kind: subsystem
//
// Run it and read the scopes: thirteen sources, thirteen shapes. Use it to
// pick the stimulus a model needs, or to see what a parameter actually does --
// change a period, a slope, a waveform, and run again.
//
// Two of these are one block with a dropdown rather than a family: Signal
// Generator (sine / square / sawtooth / random) and Waveform Generator
// (sinusoid / square / sawtooth / pulse / step / constant). They are set to
// square and sawtooth here so the gallery shows what the lists hold; the
// whole option list travels with the value, which is why those two setConfig
// lines are long.
//
// The random sources are deliberately NOT here -- they have their own
// template, Random and Counting Sources, because a noise source needs a seed
// and a sample rate to mean anything.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7. Every block in the gallery is 70
// tall, so each row of lanes shares one port row (260, 460, 660, 860, 1060)
// and every block's move-y is that row + 35 -- all thirteen links straight.
// --------------------------------------------------------------- steps and ramps
s_step = block(Step)
s_step.move(40, 1095)
s_step.resize(70, 70)
s_step.setConfig(Step Time (sec), 1)
s_step.setConfig(Initial Value, 0)
s_step.setConfig(Final Value, 1)
v_step = block(Scope)
v_step.move(220, 1095)
v_step.resize(70, 70)
connect(s_step<0>, v_step<0>)
s_ramp = block(Ramp)
s_ramp.move(440, 1095)
s_ramp.resize(70, 70)
s_ramp.setConfig(Slope, 0.5)
s_ramp.setConfig(Start Time (s), 1)
v_ramp = block(Scope)
v_ramp.move(620, 1095)
v_ramp.resize(70, 70)
connect(s_ramp<0>, v_ramp<0>)
s_const = block(Constant)
s_const.move(840, 1095)
s_const.resize(70, 70)
s_const.setConfig(Constant Value, 0.75)
v_const = block(Scope)
v_const.move(1020, 1095)
v_const.resize(70, 70)
connect(s_const<0>, v_const<0>)
// --------------------------------------------------------------- periodic
s_sine = block(Sine_Wave)
s_sine.move(40, 895)
s_sine.resize(70, 70)
s_sine.setConfig(Amplitude, 1)
s_sine.setConfig(Frequency (Rad), 2)
v_sine = block(Scope)
v_sine.move(220, 895)
v_sine.resize(70, 70)
connect(s_sine<0>, v_sine<0>)
s_pulse = block(Pulse_Generator)
s_pulse.move(440, 895)
s_pulse.resize(80, 70)
s_pulse.setConfig(Period, 4)
s_pulse.setConfig(Pulse Width, 30)
v_pulse = block(Scope)
v_pulse.move(620, 895)
v_pulse.resize(70, 70)
connect(s_pulse<0>, v_pulse<0>)
s_chirp = block(Chirp_Signal)
s_chirp.move(840, 895)
s_chirp.resize(70, 70)
s_chirp.setConfig(Initial Frequency (Hz), 0.1)
s_chirp.setConfig(Target Time (s), 10)
s_chirp.setConfig(Frequency at Target Time (Hz), 2)
v_chirp = block(Scope)
v_chirp.move(1020, 895)
v_chirp.resize(70, 70)
connect(s_chirp<0>, v_chirp<0>)
// --------------------------------------------------------------- selectable waveform
s_gen = block(Signal_Generator)
s_gen.move(40, 695)
s_gen.resize(70, 70)
s_gen.setConfig(Waveform, sine%~%square%~%sawtooth%~%random~~square)
s_gen.setConfig(Amplitude, 1)
s_gen.setConfig(Frequency, 2)
v_gen = block(Scope)
v_gen.move(220, 695)
v_gen.resize(70, 70)
connect(s_gen<0>, v_gen<0>)
s_wave = block(Waveform_Generator)
s_wave.move(440, 695)
s_wave.resize(80, 70)
s_wave.setConfig(Waveform, sinusoid%~%square%~%sawtooth%~%pulse%~%step%~%constant~~sawtooth)
s_wave.setConfig(Amplitude, 1)
s_wave.setConfig(Frequency (Hz), 0.5)
v_wave = block(Scope)
v_wave.move(620, 695)
v_wave.resize(70, 70)
connect(s_wave<0>, v_wave<0>)
// --------------------------------------------------------------- repeating sequences
s_rep = block(Repeating_Sequence)
s_rep.move(40, 495)
s_rep.resize(80, 70)
s_rep.setConfig(Time Values, [0 1 2])
s_rep.setConfig(Output Values, [0 2 0])
v_rep = block(Scope)
v_rep.move(220, 495)
v_rep.resize(70, 70)
connect(s_rep<0>, v_rep<0>)
s_stair = block(Repeating_Sequence_Stair)
s_stair.move(440, 495)
s_stair.resize(80, 70)
s_stair.setConfig(Output Values, [3 1 4 2 1])
v_stair = block(Scope)
v_stair.move(620, 495)
v_stair.resize(70, 70)
connect(s_stair<0>, v_stair<0>)
s_interp = block(Repeating_Sequence_Interpolated)
s_interp.move(840, 495)
s_interp.resize(85, 70)
s_interp.setConfig(Output Values, [3 1 4 2 1])
s_interp.setConfig(Time Values, [0 0.1 0.5 0.6 1])
v_interp = block(Scope)
v_interp.move(1020, 495)
v_interp.resize(70, 70)
connect(s_interp<0>, v_interp<0>)
// --------------------------------------------------------------- time
s_clock = block(Clock)
s_clock.move(40, 295)
s_clock.resize(70, 70)
v_clock = block(Scope)
v_clock.move(220, 295)
v_clock.resize(70, 70)
connect(s_clock<0>, v_clock<0>)
s_dclock = block(Digital_Clock)
s_dclock.move(440, 295)
s_dclock.resize(70, 70)
v_dclock = block(Scope)
v_dclock.move(620, 295)
v_dclock.resize(70, 70)
connect(s_dclock<0>, v_dclock<0>)
// One titled wash per family, in the identity sheet's accents.
a1 = area()
a1.move(15, 1150)
a1.resize(1100, 190)
a1.setColor(168, 152, 232)
a1.setTitle("Steps and ramps")
a2 = area()
a2.move(15, 950)
a2.resize(1100, 190)
a2.setColor(232, 168, 124)
a2.setTitle("Periodic")
a3 = area()
a3.move(15, 750)
a3.resize(1095, 190)
a3.setColor(43, 160, 184)
a3.setTitle("One block, a list of waveforms")
a4 = area()
a4.move(15, 550)
a4.resize(1100, 190)
a4.setColor(48, 160, 106)
a4.setTitle("Repeating sequences")
a5 = area()
a5.move(15, 350)
a5.resize(1095, 190)
a5.setColor(74, 124, 232)
a5.setTitle("Time")
t = textbox()
t.move(40, 120)
t.setText("Thirteen sources, thirteen scopes. Change a period, a slope or a waveform and run again to see the shape follow.")
Sinks and Data Sampler#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/sinks_and_data_sampler.iscript · category Control Systems · kind subsystem · compiled into the binary
Every sink in the library, each fed by one of the four data-carrying sources, with the decimation and variable names filled in.
Places: Signal_Editor, Scope, Display, From_Workspace, Record, To_File, Playback, Signal_Recorder, Terminator, Ground, Stop_Simulation, Floating_Scope, Sine_Wave, XY_Graph
// @title: Sinks and Data Sampler
// @category: Control Systems
// @summary: Every sink in the library, each fed by one of the four data-carrying sources, with the decimation and variable names filled in.
// @kind: subsystem
//
// Where a signal can END. Five lanes, one data source each, every sink in the
// Sinks family present: Scope and Display, Record and Signal Recorder, To File,
// Terminator, Stop Simulation, XY Graph and the Floating Scope.
//
// Two of these sinks are quietly different from the rest and the template is
// arranged to show it:
//
// * STOP SIMULATION ends the run the moment its input is NONZERO. It is fed
// from Ground here precisely so it never fires -- wire it to a real signal
// and the run stops the first time that signal leaves zero.
// * FLOATING SCOPE has no input port at all. It plots links by NAME, listed
// comma-separated in its "Signals" config, which is why it stands alone
// with nothing wired to it. Left empty it plots nothing; that is the
// correct shipped state, since link names belong to the diagram it lands
// in rather than to this file.
//
// The four sources all carry data of their own out of the box -- Signal Editor a
// breakpoint table, From Workspace a two-point series, Playback a four-point
// recording -- so nothing here depends on a variables space that a fresh project
// has not filled in yet.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per lane
// (260 / 560 / 860 / 1160 / 1460), a second sink stacked 130 above it where a
// lane feeds two, sizes pinned with resize(). move() places the TOP edge and
// port k of n sits at moveY - h*(k+1)/(n+1), so every move-y below is
// "port row + half the height".
// ------------------------------------------- lane 1: Signal Editor -> Scope, Display
sed = block(Signal_Editor)
sed.move(40, 295)
sed.resize(80, 70)
sed.setConfig(Time Breakpoints, [0 2 4 6 8 10])
sed.setConfig(Signal Values, [0 1 1 -0.5 -0.5 0])
sc1 = block(Scope)
sc1.move(300, 295)
sc1.resize(70, 70)
disp = block(Display)
disp.move(300, 415)
disp.resize(90, 50)
disp.setConfig(Decimation, 5)
l1 = connect(sed<0>, sc1<0>)
connect(l1, disp<0>)
// ------------------------------------ lane 2: From Workspace -> Record, To File
fws = block(From_Workspace)
fws.move(40, 595)
fws.resize(90, 70)
fws.setConfig(Data, [0 0; 2 1; 5 -1; 10 0.5])
rec = block(Record)
rec.move(300, 595)
rec.resize(80, 70)
rec.setConfig(Variable Name, recorded_from_workspace)
rec.setConfig(Decimation, 1)
tof = block(To_File)
tof.move(300, 715)
tof.resize(80, 70)
tof.setConfig(File Name, sink_tour.csv)
tof.setConfig(Variable Name, from_workspace_trace)
tof.setConfig(Decimation, 2)
l2 = connect(fws<0>, rec<0>)
connect(l2, tof<0>)
// ------------------------------ lane 3: Playback -> Signal Recorder, Terminator
pbk = block(Playback)
pbk.move(40, 895)
pbk.resize(80, 70)
sigr = block(Signal_Recorder)
sigr.move(300, 895)
sigr.resize(80, 70)
sigr.setConfig(Variable Name, playback_trace)
sigr.setConfig(Decimation, 1)
// A Terminator is what a deliberately unused signal is wired to: it consumes the
// branch so the diagram reads as "read and discarded" rather than "forgotten".
term = block(Terminator)
term.move(300, 1005)
term.resize(50, 50)
l3 = connect(pbk<0>, sigr<0>)
connect(l3, term<0>)
// ------------------------- lane 4: Ground -> Stop Simulation; Floating Scope alone
grd = block(Ground)
grd.move(40, 1195)
grd.resize(70, 70)
stop = block(Stop_Simulation)
stop.move(300, 1195)
stop.resize(70, 70)
fsc = block(Floating_Scope)
fsc.move(560, 1195)
fsc.resize(70, 70)
connect(grd<0>, stop<0>)
// ------------------------------------------------- lane 5: two sines -> XY Graph
// The XY Graph plots one input against the other rather than against time, so it
// needs a PAIR. Two sines a quarter period apart trace a circle, which is the
// one picture that cannot be mistaken for a time plot. Sine Wave is reused here
// (it belongs to the Sources Gallery roster) because none of this lane's own
// four sources produces two independent channels.
//
// The XY Graph is resized to 90 tall so its two input rows land on whole numbers
// (moveY - 90/3 and moveY - 180/3, i.e. 1475 and 1445); the x source aligns dead
// straight with the first, and the y source takes one clean staircase in open
// space to the second -- two ports 30 apart cannot both be met by 70-tall
// sources without their frames overlapping.
xsrc = block(Sine_Wave)
xsrc.move(40, 1510)
xsrc.resize(70, 70)
xsrc.setConfig(Amplitude, 1)
xsrc.setConfig(Frequency (Rad), 1)
xsrc.setConfig(Phase Shift, 0)
ysrc = block(Sine_Wave)
ysrc.move(40, 1420)
ysrc.resize(70, 70)
ysrc.setConfig(Amplitude, 1)
ysrc.setConfig(Frequency (Rad), 1)
ysrc.setConfig(Phase Shift, 1.5707963267948966)
xy = block(XY_Graph)
xy.move(300, 1505)
xy.resize(90, 90)
connect(xsrc<0>, xy<0>)
connect(ysrc<0>, xy<1>)
// Group washes in the identity sheet's data accents. Only the HUE is ours -- the
// theme re-imposes its own alpha on every area, so none is passed here.
a1 = area()
a1.move(15, 470)
a1.resize(405, 280)
a1.setColor(168, 152, 232)
a1.setTitle("Signal Editor -> Scope + Display")
a2 = area()
a2.move(15, 770)
a2.resize(405, 280)
a2.setColor(232, 168, 124)
a2.setTitle("From Workspace -> Record + To File")
a3 = area()
a3.move(15, 1060)
a3.resize(405, 270)
a3.setColor(124, 200, 176)
a3.setTitle("Playback -> Signal Recorder + Terminator")
a4 = area()
a4.move(15, 1250)
a4.resize(640, 160)
a4.setColor(232, 152, 176)
a4.setTitle("Ground -> Stop Simulation; Floating Scope stands alone")
a5 = area()
a5.move(15, 1565)
a5.resize(405, 255)
a5.setColor(152, 184, 232)
a5.setTitle("Two sines -> XY Graph")
t = textbox()
t.move(40, 110)
t.setText("Stop Simulation is fed from Ground so it never fires; wire it to a real signal and the run ends when that signal leaves zero. The Floating Scope has no input port -- list link names in its Signals config instead.")
Varying Filter Sweep#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/varying_filter_sweep.iscript · category Control Systems · kind subsystem · compiled into the binary
A chirp through a lowpass and a notch whose cutoff is driven by a ramp, so the filters retune while the model runs.
Places: Chirp_Signal, Ramp, Constant, Varying_Lowpass_Filter, Varying_Notch_Filter, Scope
// @title: Varying Filter Sweep
// @category: Control Systems
// @summary: A chirp through a lowpass and a notch whose cutoff is driven by a ramp, so the filters retune while the model runs.
// @kind: subsystem
//
// Two filters whose tuning is an INPUT, not a setting. Both take the signal
// on u and the corner frequency on w0, so the ramp on w0 retunes them
// continuously while the chirp sweeps past — the interesting case is where
// the two sweeps cross.
//
// Varying Lowpass Filter passes below w0. As the ramp lifts w0 past the
// chirp, the lane opens up.
// Varying Notch Filter rejects AT w0, and takes two more schedules:
// gmin sets how deep the notch cuts, damping how
// wide it is.
//
// The raw chirp is on its own scope so the two filtered lanes have something
// to be compared against. Watching all three at once is the point: a filter
// whose parameters move cannot be understood from a single frequency
// response, only from what it does to a sweep.
//
// To hold a filter still instead, replace the Ramp with a Constant — the
// blocks do not care that the schedule happens to be constant, and that is
// the quickest way to check the fixed-frequency behaviour you expect before
// letting the schedule move.
//
// KNOWN, and not this template's doing: dropping a Varying Lowpass Filter
// anywhere — here, or onto a blank canvas — logs
// "Unable to retrieve config Filter Order" once. Its constructor calls
// rebuildButterworth() before loadBlockConfig() has cached the config, and the
// getter both logs and stops the simulator when a key is missing. Insert and
// save are unaffected; a run is not, until the block is fixed.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7: first quadrant, one port row per
// lane (800 raw, 640 lowpass, 380 notch), sizes pinned with resize(). The
// notch's four inputs sit at centre + (1.5 - i) * h/5 — the schedules land
// on the lower three, which is why their sources are stacked rather than
// aligned with the lane.
// ---- the signal, and the schedules that tune the filters
sweep = block(Chirp_Signal)
sweep.move(40, 835)
sweep.resize(70, 70)
sweep.setConfig(Initial Frequency (Hz), 0.1)
sweep.setConfig(Target Time (s), 20)
sweep.setConfig(Frequency at Target Time (Hz), 5)
cutoff = block(Ramp)
cutoff.move(40, 655)
cutoff.resize(70, 70)
cutoff.setConfig(Slope, 3)
cutoff.setConfig(Start Time (s), 0)
cutoff.setConfig(Initial Output, 1)
depth = block(Constant)
depth.move(40, 515)
depth.resize(70, 70)
depth.setConfig(Constant Value, 0.1)
width = block(Constant)
width.move(40, 375)
width.resize(70, 70)
width.setConfig(Constant Value, 0.7)
// ---- the two filters
lowpass = block(Varying_Lowpass_Filter)
lowpass.move(340, 685)
lowpass.resize(120, 90)
lowpass.setConfig(Filter Order, 2)
notch = block(Varying_Notch_Filter)
notch.move(340, 440)
notch.resize(120, 120)
// ---- one scope per lane
sRaw = block(Scope)
sRaw.move(200, 835)
sRaw.resize(70, 70)
sLow = block(Scope)
sLow.move(640, 675)
sLow.resize(70, 70)
sNotch = block(Scope)
sNotch.move(640, 415)
sNotch.resize(70, 70)
// ---- the chirp drives both filters and its own scope; the schedules follow
drive = connect(sweep<0>, sRaw<0>)
connect(drive, lowpass<0>)
connect(drive, notch<0>)
schedule = connect(cutoff<0>, lowpass<1>)
connect(schedule, notch<1>)
connect(depth<0>, notch<2>)
connect(width<0>, notch<3>)
connect(lowpass<0>, sLow<0>)
connect(notch<0>, sNotch<0>)
// Group washes, in the identity sheet's data accents.
a = area()
a.move(15, 890)
a.resize(285, 610)
a.setColor(168, 152, 232)
a.setTitle("The sweep, its schedules, and the raw signal")
a3 = area()
a3.move(315, 740)
a3.resize(425, 170)
a3.setColor(124, 200, 232)
a3.setTitle("Varying lowpass")
a4 = area()
a4.move(315, 495)
a4.resize(425, 200)
a4.setColor(152, 216, 168)
a4.setTitle("Varying notch")
t = textbox()
t.move(320, 250)
t.setText("Watch where the rising cutoff crosses the rising chirp — that is where each lane opens or cuts. Swap the Ramp for a Constant to check the fixed-frequency behaviour first.")
Vector and Bus Routing#
src/ICoreSDK/ICoreBlockLibrary/Templates/Subsystems/vector_and_bus_routing.iscript · category Control Systems · kind subsystem · compiled into the binary
The blocks that carry signals around rather than compute with them — stacking, splitting, reshaping, editing in place, and buses.
Places: Constant, Mux, Display, Demux, Out_Bus_Element, In_Bus_Element, Reshape, Squeeze, Permute_Dimensions, Assignment, Matrix_Concatenate, Find_NonZero_Elements
// @title: Vector and Bus Routing
// @category: Control Systems
// @summary: The blocks that carry signals around rather than compute with them — stacking, splitting, reshaping, editing in place, and buses.
// @kind: subsystem
//
// None of these blocks do arithmetic. They decide what travels on a wire, and
// three columns is how they group:
//
// LEFT stacking a set of signals into one wire and taking it apart again,
// first with Mux and Demux and then with the pair that names the
// parts -- Out Bus Element builds a bus out of named elements, In
// Bus Element pulls a run of them back out.
// MIDDLE changing the SHAPE of a signal without changing its contents.
// RIGHT editing one in place, joining two, and finding what is non-zero.
//
// The bus lane is worth reading closely. The two constants are a [2,1] and a
// [3,1]; Out Bus Element stacks them into one [5,1] wire, and each input's
// DESCRIPTION LABEL is the element's name -- that is why the two setDescription
// calls are here and not decoration. In Bus Element then takes elements 3..5
// back out, which is the [3,1] that went in.
//
// Squeeze is included for completeness and is deliberately a no-op on a 2-D
// signal: it drops singleton dimensions, and a matrix has none to drop.
//
// Laid out per ADDING_NEW_TEMPLATES.md §7. Where a block has two inputs its
// FIRST operand sits on the lane's port row and is straight, and the second
// comes in from below; where a block has two OUTPUTS the two displays are
// placed 70 either side of the row, because two 50-tall displays cannot both
// sit on ports 19 apart.
// ============================================== left: stacking and splitting
mux_a = block(Constant)
mux_a.move(40, 1245)
mux_a.resize(70, 70)
mux_a.setConfig(Constant Value, 7)
mux_b = block(Constant)
mux_b.move(40, 1125)
mux_b.resize(70, 70)
mux_b.setConfig(Constant Value, 9)
r_mux = block(Mux)
r_mux.move(400, 1230)
r_mux.resize(20, 60)
d_mux = block(Display)
d_mux.move(900, 1225)
d_mux.resize(90, 50)
connect(mux_a<0>, r_mux<0>)
connect(mux_b<0>, r_mux<1>)
connect(r_mux<0>, d_mux<0>)
dmx_src = block(Constant)
dmx_src.move(40, 935)
dmx_src.resize(70, 70)
dmx_src.setConfig(Constant Value, [7; 9])
r_dmx = block(Demux)
r_dmx.move(400, 930)
r_dmx.resize(20, 60)
d_dmx1 = block(Display)
d_dmx1.move(900, 995)
d_dmx1.resize(90, 50)
d_dmx2 = block(Display)
d_dmx2.move(900, 855)
d_dmx2.resize(90, 50)
connect(dmx_src<0>, r_dmx<0>)
connect(r_dmx<0>, d_dmx1<0>)
connect(r_dmx<1>, d_dmx2<0>)
bus_a = block(Constant)
bus_a.move(40, 609)
bus_a.resize(70, 70)
bus_a.setConfig(Constant Value, [10; 20])
bus_b = block(Constant)
bus_b.move(40, 475)
bus_b.resize(70, 70)
bus_b.setConfig(Constant Value, [30; 40; 50])
r_outbus = block(Out_Bus_Element)
r_outbus.move(400, 600)
r_outbus.resize(40, 80)
// The description label IS the element's name on a bus, so these two lines
// are part of the model, not a comment on it.
p_pos = r_outbus.inputPort(0)
p_pos.setDescription(position)
p_rate = r_outbus.inputPort(1)
p_rate.setDescription(rates)
r_inbus = block(In_Bus_Element)
r_inbus.move(620, 595)
r_inbus.resize(90, 70)
r_inbus.setConfig(Element Index, 3)
r_inbus.setConfig(Number of Elements, 3)
d_inbus = block(Display)
d_inbus.move(900, 585)
d_inbus.resize(90, 50)
d_bus = block(Display)
d_bus.move(620, 735)
d_bus.resize(90, 50)
connect(bus_a<0>, r_outbus<0>)
connect(bus_b<0>, r_outbus<1>)
whole_bus = connect(r_outbus<0>, r_inbus<0>)
connect(whole_bus, d_bus<0>)
connect(r_inbus<0>, d_inbus<0>)
// ============================================== middle: changing the shape
rs_src = block(Constant)
rs_src.move(1100, 1235)
rs_src.resize(70, 70)
rs_src.setConfig(Constant Value, [1 2 3; 4 5 6])
r_reshape = block(Reshape)
r_reshape.move(1450, 1235)
r_reshape.resize(80, 70)
r_reshape.setConfig(Output Dimensionality, 1-D array%~%Column vector (2-D)%~%Row vector (2-D)%~%Customize%~%Derive from reference input port~~Column vector (2-D))
d_reshape = block(Display)
d_reshape.move(1800, 1225)
d_reshape.resize(90, 50)
connect(rs_src<0>, r_reshape<0>)
connect(r_reshape<0>, d_reshape<0>)
sq_src = block(Constant)
sq_src.move(1100, 1075)
sq_src.resize(70, 70)
sq_src.setConfig(Constant Value, [1 2 3])
r_squeeze = block(Squeeze)
r_squeeze.move(1450, 1075)
r_squeeze.resize(70, 70)
d_squeeze = block(Display)
d_squeeze.move(1800, 1065)
d_squeeze.resize(90, 50)
connect(sq_src<0>, r_squeeze<0>)
connect(r_squeeze<0>, d_squeeze<0>)
pd_src = block(Constant)
pd_src.move(1100, 915)
pd_src.resize(70, 70)
pd_src.setConfig(Constant Value, [1 2 3; 4 5 6])
r_perm = block(Permute_Dimensions)
r_perm.move(1450, 915)
r_perm.resize(90, 70)
r_perm.setConfig(Order, [2 1])
d_perm = block(Display)
d_perm.move(1800, 905)
d_perm.resize(90, 50)
connect(pd_src<0>, r_perm<0>)
connect(r_perm<0>, d_perm<0>)
// ============================================== right: editing, joining, finding
as_y0 = block(Constant)
as_y0.move(1990, 1247)
as_y0.resize(70, 70)
as_y0.setConfig(Constant Value, [1 2 3 4])
as_u = block(Constant)
as_u.move(1990, 1135)
as_u.resize(70, 70)
as_u.setConfig(Constant Value, 99)
r_assign = block(Assignment)
r_assign.move(2350, 1235)
r_assign.resize(80, 70)
r_assign.setConfig(Indices, 2)
d_assign = block(Display)
d_assign.move(2700, 1225)
d_assign.resize(90, 50)
connect(as_y0<0>, r_assign<0>)
connect(as_u<0>, r_assign<1>)
connect(r_assign<0>, d_assign<0>)
cat_a = block(Constant)
cat_a.move(1990, 989)
cat_a.resize(70, 70)
cat_a.setConfig(Constant Value, [1 2])
cat_b = block(Constant)
cat_b.move(1990, 875)
cat_b.resize(70, 70)
cat_b.setConfig(Constant Value, [3 4])
r_cat = block(Matrix_Concatenate)
r_cat.move(2350, 980)
r_cat.resize(80, 80)
d_cat = block(Display)
d_cat.move(2700, 965)
d_cat.resize(90, 50)
connect(cat_a<0>, r_cat<0>)
connect(cat_b<0>, r_cat<1>)
connect(r_cat<0>, d_cat<0>)
nz_src = block(Constant)
nz_src.move(1990, 675)
nz_src.resize(70, 70)
nz_src.setConfig(Constant Value, [0; 3; 0; 5])
r_nz = block(Find_NonZero_Elements)
r_nz.move(2350, 675)
r_nz.resize(90, 70)
d_nz_idx = block(Display)
d_nz_idx.move(2700, 735)
d_nz_idx.resize(90, 50)
d_nz_n = block(Display)
d_nz_n.move(2700, 595)
d_nz_n.resize(90, 50)
connect(nz_src<0>, r_nz<0>)
connect(r_nz<0>, d_nz_idx<0>)
connect(r_nz<1>, d_nz_n<0>)
a1 = area()
a1.move(15, 1320)
a1.resize(1000, 1000)
a1.setColor(168, 152, 232)
a1.setTitle("Stacking a wire, and taking it apart")
a2 = area()
a2.move(1060, 1320)
a2.resize(870, 500)
a2.setColor(232, 168, 124)
a2.setTitle("Changing the shape, not the contents")
a3 = area()
a3.move(1965, 1320)
a3.resize(875, 800)
a3.setColor(43, 160, 184)
a3.setTitle("Editing, joining, finding")
t = textbox()
t.move(40, 240)
t.setText("Out Bus Element names its elements with their port description labels; In Bus Element takes elements 3 to 5 back out again.")