Plan: the workbench, and the MLP, Kalman and DSP patterns (ADR 0005, step 1)
Issue: anatomy#89.
This is step 1 of ADR 0005. It adds a workbench in anatomy where the graph machinery is developed on graph patterns rather than on missions. The workbench is a driver, three patterns and their oracles, with no vehicle, world, simulator or scorer. It runs on the C++ builder and the CPU stack of ADR 0004, step 2 (plan).
Until now, every feature of the graph machinery was pulled by a mission and accepted on byte-equality with a mission’s slot stack. The flow tests (plan) added oracles of their own, but on toy operators over a time and an unsigned integer. So the parts of the domain flow model that matter most to the compiler have never run:
- domains with real shapes and a structure: a 32 × 16 matrix, a symmetric covariance;
- constants with values: an MLP’s weights, a filter’s taps;
- an element type that differs from one edge to the next;
- complex elements;
- any rate rule but
hold.
This plan adds what the first three patterns need of those, and the patterns themselves:
- in the core (
branes::anatomy):flow::data<D>, which stores the value of a domain;- complex element types;
- the
accumulaterate rule, through the builder, the stack, the canonical form, the Mermaid view, the reference interpreter and the mutations;
- the workbench (
workbench/,branes::anatomy_workbench): the driver, its synthetic sources, and three patterns with their oracles:- the MLP: two layers in different element types, with weights as constants;
- the Kalman filter: constant velocity, with a symmetric covariance as state, predicting on a clock and updating on a bounded measurement;
- the DSP spectral pipeline: a FIR,
accumulateinto frames, a window, an FFT and a magnitude;
workbench_run: a tool that runs a pattern, reports its oracle, and writes its trace and its graph. It is the first graph tool that needs no mission.
What doesn’t change:
- the builder’s API for what exists today, and the invariants G1–G9, except G7, which admits
accumulatebesidehold; - the canonical form and the hash of every graph that has no
accumulateedge, so every mission’scomposition_hash; - the log format, the trace format, and every gate.
ADR 0004, decision 7 holds: racing-drone’s and scout-drone’s outputs must not change.
The workbench is not linked by any mission. Its operators are reference implementations, written plainly to be checked, not to be fast (ADR 0005, decision 1). It isn’t installed or exported, so no mission can link it.
What the tests must establish
Section titled “What the tests must establish”Each property has an id, which the tests name.
| Id | Property |
|---|---|
| W1 | Fidelity: each pattern’s stack writes, bit for bit, what its operators write when called by hand in the order the graph declares, on the same inputs. The oracle is exact: it shows that the graph runs what it says |
| W2 | Correctness: each pattern’s outputs meet its oracle, to the tolerance the pattern states. This shows that the operators compute what the pattern means |
| W3 | Determinism: a pattern’s run, with its seed, writes byte-identical traces run to run. Across element types, and across GCC, Clang and MSVC, the outputs agree with the oracle to its tolerance, not byte for byte |
| W4 | Shapes and structure: data<D> stores stored_elements_v<D> elements; a symmetric domain’s (i, j) and (j, i) are one element; a bounded extent’s count never exceeds its bound. An edge between domains of different shapes, structures or element types doesn’t compile (G4) |
| W5 | Constants with values: a constant’s value reaches every firing that reads it, unchanged, and isn’t copied per firing |
| W6 | accumulate: when its region begins, a reader sees, oldest first, the values its producer wrote since that region last began: the latest N of them, and how many were dropped. Before the producer first writes, it reads an empty window. flow::stack and the reference interpreter agree on random graphs with accumulate edges |
| W7 | The checks refuse what accumulate breaks: an accumulate edge within a region or from a constant is refused, naming G7. A capacity of zero doesn’t compile |
| W8 | No allocation once built holds with accumulate edges and with the patterns’ messages |
| W9 | The missions are unchanged: every existing graph’s canonical form and hash are unchanged, and racing-drone’s and scout-drone’s outputs are byte-identical to their main’s |
Domains that run: flow::data<D>
Section titled “Domains that run: flow::data<D>”Step 1 of ADR 0004 made domains types, and the missions’ messages give a domain to their own structs (domain_of). A pattern’s edges are shaped values: a 16-vector, a 32 × 16 matrix, a symmetric 4 × 4 covariance, 256 complex samples. Writing a struct for each would be noise. flow/data.hpp adds the storage of a domain:
namespace branes::anatomy::flow {
// The value of domain D, whose element type is arithmetic, complex, an enum or Timestamp.template <class D>struct data { using domain = D; std::array<typename D::element_type, stored_elements_v<D>> elements{};
// A bounded leading extent's count; its bound otherwise. [[nodiscard]] constexpr std::size_t count() const noexcept; // A bounded leading extent: sets the count if n is at most the bound. A static extent: // accepts only its size. Otherwise returns false and leaves the count unchanged. [[nodiscard]] constexpr bool resize(std::size_t n) noexcept;
// Row-major for dense. Symmetric and upper triangular: packed, the upper triangle row by row. // Lower triangular: the lower triangle, row by row. // Diagonal: the diagonal. [[nodiscard]] constexpr E& operator()(std::size_t i) noexcept; // rank 1 [[nodiscard]] constexpr E& operator()(std::size_t i, std::size_t j) noexcept; // rank 2 // const access to an element outside a triangle or a diagonal reads zero};
template <class D> struct domain_of<data<D>> { using type = D; };}- A message of type
data<D>is its own domain. Two ports that carrydata<D>match on domain and on message type, and G4 refuses a port of any other shape, structure or element type at compile time. - The storage is a
std::arrayof fixed size, so adata<D>holds nothing on the heap, and a slot of it allocates once, when the stack is built. - Bounded extents: only the leading extent may be bounded, and
count()is the number of its rows in use. The count is private, andresize(n)refuses a count above the bound, so the bound holds for everydata<D>(W4). A bounded value starts empty (count 0), and rows past the count hold value-initialized elements, so two values with the same rows in use compare equal. A domain with no bounded extent has its full count, andresizeaccepts only that. More than one bounded extent is refused by astatic_assert, until a pattern needs it. - Structure:
(i, j)of a symmetric matrix is(j, i): one stored element. Writing outside a triangle is a precondition violation, checked in debug builds. - A record’s element type has no C++ type to store, so
data<D>takes domains of the element types above only. A message that is a record stays a struct with itsdomain_of, as the missions’ are.
The layout is fixed here because step 3’s document and step 6’s lowering will read it. It is the layout a CPU kernel sees, not one a target must use. The model’s layouts are the lowering’s (section 4).
Complex elements
Section titled “Complex elements”std::complex<float>, std::complex<double> and std::complex<long double> become element types (element_traits, in flow/domain.hpp). The DSP pattern’s FFT is their first client.
The accumulate rate rule
Section titled “The accumulate rate rule”hold gives a reader the latest value its producer wrote. The DSP pattern needs the other rule the model names for edges between regions of different rates (section 8): accumulate, an accumulator domain. Here, the producer’s region runs once per sample and the reader’s once per frame of N samples.
// In the frame region, read the samples the sample region wrote since the frame region last began.auto [w] = flow::fire<"apply">(frame, window_op, flow::accumulate<256>(filtered));- What the reader reads:
flow::window<M, N>. It holds the values written since the reader’s region last began, oldest first:items(astd::array<M, N>), theircount, anddropped, how many older values didn’t fit. Its domain isrecord<field<"items", vector<E, bounded<N>>>, field<"dropped", scalar<std::uint64_t>>>, whereEis the element type ofM’s domain if that is a scalar, orM’s record. A message whose domain has a shape is refused, until a pattern needs it. - When: the stack latches each accumulator as the reader’s region begins, and empties it for the producer. So every firing of the region reads the same window, and the window doesn’t depend on the order of firings within the region. Before the producer first writes, the window is empty:
countanddroppedare 0, as aholdreads its message value-initialized (flow-testing, P4). - Overflow: a producer that writes more than N values between two reader regions keeps the latest N and counts the rest in
dropped. That is deterministic, allocates nothing, and lets the reader see what it lost. The DSP pattern’s driver writes exactly N per frame, and its oracle requiresdropped == 0. - In the graph:
Refgainsrate_rule::accumulateand acapacity.canonical()writesaccumulate N, with N the capacity, where it writeshold, and nothing for any other edge, so the canonical form and the hash of every graph without anaccumulateedge are unchanged (W9).mermaid()draws the edge dotted, labelledaccumulate N. - G7 now reads: an edge between regions has a rate rule,
holdoraccumulate; an edge within a region, or from a constant, has none. - Each
accumulateedge has its own accumulator. One out port read by two regions fills two windows.
The model’s other uses of an accumulator, such as IMU samples integrated between frames, are an operator applied to the window, not another rule. queue stays for when a pattern or a mission needs it.
The workbench
Section titled “The workbench”workbench/├── include/branes/anatomy/workbench/│ ├── driver.hpp # sources at declared rates, merged into one stream; runs a pattern's stack│ ├── sources.hpp # random numbers, a clock and a sequence; a pattern's signals (a trajectory, tones) are its own│ ├── codecs.hpp # trace codecs for data<D> and window<M, N>│ ├── mtl5.hpp # MTL5 views of data<D>, and packing for its structured domains (B4)│ ├── pattern.hpp # what a pattern provides (below)│ └── patterns/ # mlp.hpp, kalman.hpp, dsp.hpp: operators, bindings, graph, oracle└── apps/workbench_run/branes::anatomy_workbench is an interface library over the core. It is built when ANATOMY_BUILD_WORKBENCH is on, which defaults to ANATOMY_BUILD_TESTS, and it is not installed or exported. Its tests are in tests/workbench/.
A pattern provides:
- its name;
- its configuration, a struct with defaults: shapes, rates, parameters, a seed;
- its operators, registered under names that carry their element type and shape (
Linear_f32_32x16); - its graph: a template over its builder, as a mission’s is, so
flow::builderchecks it andflow::stack_builderruns it; - its sources;
- its hand-wired run (W1);
- its oracle (W2): given the configuration and the outputs, a report of each check, with its value, its tolerance, and whether it passed.
The operators are built from a workbench context that holds the configuration, with from_context, as a mission’s are. pattern.hpp states the interface a pattern meets in C++: graph, sources, a sample that collect takes after each input, oracle over the samples, and codecs; workbench::run<P> builds, drives and traces any pattern that meets it.
The driver merges the sources into one stream in time order, hands each input to the stack, and collects the outputs after each. Two inputs at the same time go in the order the pattern declares its sources: the fixed order between simultaneous triggers that the model requires (section 8). The noise is drawn from std::mt19937_64’s raw output, mapped by the workbench’s own code, as the flow tests’ generator does, because the standard distributions differ between GCC, Clang and MSVC.
The patterns
Section titled “The patterns”The MLP. y = W₂·tanh(W₁·x + b₁) + b₂, with x a 16-vector, a hidden layer of 32, and an output of 8.
- Layer 1 is
f32and layer 2 isf64, joined by aCastoperator, so the element type is per edge. - The weights and biases are constants with values, drawn from the seed.
- Its operators are
Linear(y = W·x),BiasAdd,TanhandCast, each a template over its element type and shape. One region, triggered by each inputx. - Oracle: a direct evaluation in
long double, written without the operators. The output agrees with it to a relative 1e-5, which layer 1’sf32sets. Layer 2, given layer 1’s output, agrees to 1e-12. The operation count in closed form,32·16 + 8·32multiply-adds plus the epilogues, is written beside the pattern for step 5’s cost record to check against. - A compile-fail test: an
f32vector into anf64port doesn’t compile (G4).
The Kalman filter. Constant velocity in the plane: state x = (px, py, vx, vy), covariance P a symmetric 4 × 4 data.
Kalmanis one operator with four handlers. Two update its state:predict, a time handler fired by a 100 Hz clock, andupdate, fired byfixes. Two read it:priorandposterior.fixesis a measurement of up to 4 position fixes with a count (data<matrix<double, bounded<4>, dim<2>>>), arriving at 10 Hz on clock ticks, and sometimes with none.- At a time shared by the clock and the fixes, the clock goes first, so the update applies to the predicted state.
- The update is in Joseph form.
priorruns in the clock region afterpredictand writesxandPto theprioroutput.posteriorruns in the fixes region afterupdateand writes them to theposterioroutput, which the oracle’s checks of the update read. - The signature declares
xandPas its state domains. - Its arithmetic is MTL5’s, through the workbench’s adapter (below). Between firings,
Pis kept packed in itsdata(10 elements, symmetric by construction). A handler unpacks it, computes, and packs the result again. - Oracle:
Pis positive definite after every firing: its Cholesky factorization succeeds;- with one fix per update,
Pafter the update converges to the steady-state solution of the discrete Riccati equation, which the oracle iterates inlong double, to a relative 1e-9 within 500 updates; - over fixed seeds, the normalized estimation error squared (NEES) of 2000 updates averages within its 95 % chi-square bounds. The NEES is taken at every 25th update: a filter’s errors are correlated from one update to the next, so the mean over every update varies about 3.5 times more than the bounds of independent samples allow, while 25 updates (2.5 s) apart the samples are nearly independent. As a 95 % test, it fails about one seed in twenty (of seeds 1 to 100, six), so the tests’ seeds are fixed, each with its mean well inside the bounds;
- the
f32andf64instantiations agree to 1e-4 relative. The graph runs both,Kalman_f64andKalman_f32, thef32one fed the fixes through aCast, so one run’s samples hold both.
The DSP spectral pipeline. Samples at 8192 Hz, frames of N = 256, so bins are 32 Hz apart. 1/8192 s isn’t a whole number of nanoseconds, so the sample period is 122 070 ns; the operators count samples, not time.
- The sample region, fired by each sample, runs
Fir_f64_63, a band-pass FIR of 63 taps. Its delay line is its state, and its taps are a constant with values: a Hamming-windowed sinc, designed from the configuration when the graph is built, 6 dB down at 1024 and 2048 Hz. - The frame region, fired by a frame tick at every 256th sample’s time, after the sample, reads
accumulate<256>of the filtered samples.Framerturns the window into a frame (its count kept) and the number it dropped. Then a periodic Hann window (a constant), a cast tocomplex<double>, a radix-2 FFT whose twiddles it computesfrom_context, and a magnitude. - Its outputs are the filtered sample, and per frame the filtered frame, the number dropped, the spectrum (256 complex bins) and the magnitude of bins 0 to 128. The frame and the number dropped are outputs so the oracle can check the window.
- The source is a sum of tones at bin centers, two in the pass band (bins 40 and 52, amplitudes 1 and 0.5) and one in the stop band (bin 100, amplitude 1), with phases drawn from the seed, plus seeded noise of standard deviation 10⁻³. With a periodic Hann window, a tone at a bin’s center leaks into its two neighbours only, so the tones don’t disturb one another’s bins.
- Oracle, in
long double, without the operators:- every frame’s window has
count == 256anddropped == 0; - the filtered frame agrees with the FIR evaluated from the samples and the taps, to a relative 1e-12;
- the spectrum and the magnitude agree with a direct DFT of the windowed frame to a relative 1e-12 (of the largest bin), and Parseval holds between them to 1e-12;
- from the second frame, since the first holds the filter’s transient: the two largest magnitudes are at the pass-band tones’ bins;
- each pass-band tone’s magnitude is within 1 % of
A·|H(f)|·Σw/2, withH(f)evaluated directly from the taps; - the stop-band tone’s magnitude is within 1 % of the same prediction, and the design puts it at least 40 dB down (64 dB).
- With the defaults, every check has at least 15 times its margin: the tones agree with their predictions to 6.4e-4 at worst, and the rest to under 1e-15.
- every frame’s window has
A first frame that begins before 256 samples have arrived isn’t possible with this driver. W6’s tests cover short and overfull windows on the toy graphs.
The MTL5 adapter
Section titled “The MTL5 adapter”data<D> is what an edge carries: its type is its domain, a symmetric matrix stores one element for (i, j) and (j, i), and two values with the same rows in use compare equal (A2). A trace holds its elements and count as the workbench’s codec writes them, not the object’s bytes, which can include padding. It does no arithmetic. A pattern’s operators do theirs in MTL5, through workbench/mtl5.hpp:
mtl5::view(d): for a densedata<D>of rank 1 or 2, an MTL5dense_vectoror row-majordense2Doverd.elements, built with MTL5’s external-pointer constructors. It doesn’t copy or allocate. For a bounded leading extent, the view hasd.count()rows, so a caller resizesdbefore it takes the view.- In MTL5 5.20.0, copying such a view, or copy-assigning one view to another, detaches the destination: it allocates storage of its own, and the
data<D>behind it isn’t written. Assigning an expression (y = A * x,P += Q) writes in place only when its dimensions match the view’s; otherwise MTL5 resizes, which detaches too. Soview(d)returns a wrapper that can’t be copied or copy-assigned, and gives its MTL5 object only by const reference, to read and to use in expressions. Writes go through the wrapper: element access, and[[nodiscard]] bool mtl5::assign(dst, expr),mtl5::add(dst, expr)andmtl5::mult(a, b, dst), asresizeis. MTL5 5.20.0’soperator*of two matrices, or of a matrix and a vector, allocates its result, so a product ismtl5::mult, which callsmtl::multintodst’s own elements;dstmay also be a fixed-size stack temporary. Each returns false and writes nothing if the expression’s dimensions differ from the view’s, and otherwise writes in place.exprmay be another view, which is copied element by element. mtl5::unpack(d)andmtl5::pack(m, d): for a symmetric, triangular or diagonaldata<D>, a fixed-size dense MTL5 matrix (stack storage, so no allocation) and back.packof a symmetric domain takes the upper triangle, so a result’s asymmetry is dropped rather than stored; in debug buildspackasserts that the two triangles agree to a tolerance it is given.- Tests (
tests/workbench/): a view reads and writes the elements in place;assignandaddof an expression, andassignof one view to another, write thedata<D>behind it; an expression of other dimensions, such as a 3-vector into the view of a bounded value whose count is 0, returns false and leaves thedata<D>unchanged; the wrapper is neither copy constructible nor copy assignable (a static check);pack(unpack(d)) == dfor every structure; an MTL5 product through the views equals the same product written as loops; and the adapter allocates nothing (the counting allocator, W8).
The workbench links MTL5::mtl5, the target that reflex’s fetch already defines, at reflex’s pin (5.20.0). Anatomy adds no fetch of its own. The SDK doesn’t link MTL5 (design choice 7).
Universal’s number types aren’t element types (D3), so a posit can’t be a data<D>’s element. Admitting them is a change to element_traits, and is left to the step that sweeps a pattern’s precision.
workbench_run
Section titled “workbench_run”workbench_run <mlp|kalman|dsp> [--inputs N] [--seed S] [--trace run.brtrace] [--mermaid g.mmd] [--canonical g.txt] [--tolerance-scale K]workbench_run runs a pattern with its default configuration and prints its oracle’s report. It writes the trace (with the workbench’s codecs) and the graph’s Mermaid or canonical form when asked. It exits 0 if every check passes, 1 if one fails, and 2 on a usage error, as trace_compare does. --tolerance-scale K multiplies every check’s upper tolerance by K, to show a pattern’s margin; its test runs K = 0 for the exit code 1. trace_compare reads its traces, so two runs, or two element types, can be compared edge by edge. The graph host, which runs a graph document without a rebuild, is step 4’s. This tool runs only the three patterns compiled into it.
Acceptance
Section titled “Acceptance”Six PRs, each accepted on its own criteria. ADR 0005 was accepted with this plan’s PR (B0).
B0, this plan. The plan, its design choices confirmed, and ADR 0005 accepted.
B1, core: flow::data<D> and complex elements.
flow/data.hpp, and complex element types inflow/domain.hpp.- Tests of each structure’s storage size, index mapping and symmetric access, and of a bounded extent’s count (W4).
- Compile-fail tests: an edge between
dataof different shapes, of different structures, and of different element types (W4). - Only headers are added, so no mission’s build changes.
B2, core: accumulate.
flow::accumulate<N>,flow::window<M, N>,rate_rule::accumulateandRef::capacity. Also the stack’s accumulators,canonical(),mermaid()and G7.- The reference interpreter and the generator of the flow tests draw
accumulateedges, andflow_differentialruns them (W6). - Tests of an empty window, a short window, an overfull window with its
dropped, and two readers of one out port (W6). A counting-allocator test withaccumulateedges (W8). flow_mutationsrefuses anaccumulatewithin a region and from a constant, naming G7 (W7), and a compile-fail test refuses a capacity of zero.- A test that the canonical forms and hashes of
test_mission.hpp’s graphs and of 200 generated graphs withoutaccumulateequal main’s, which are recorded as goldens in this PR before the change (W9). - racing-drone’s and scout-drone’s
tools/compare_outputs.sh, built against this branch: byte-identical to their main’s (W9).
B3, the workbench and the MLP.
workbench/,ANATOMY_BUILD_WORKBENCH, the driver, its sources and codecs, and the MLP pattern.workbench_mlp(tests/workbench/): W1, W2 and W3 on the MLP, and W5, a constant read by reference by every firing.workbench_run mlp, with a test that runs the tool on each exit code.
B4, the Kalman filter.
workbench/mtl5.hppand its tests (the MTL5 adapter, above), withMTL5::mtl5linked by the workbench only.- The pattern, its arithmetic in MTL5 through the adapter, and
workbench_kalman: W1, W2 and W3. - B4 has the first operator with two handlers that update its state in two regions, fired by two triggers at shared times.
B5, the DSP spectral pipeline. The pattern, and workbench_dsp: W1, W2 and W3. B5 is the first client of accumulate and of complex elements.
B6, docs. This plan’s progress, ADR 0005’s current state, the repository layout, the documentation map, and a guide to the workbench: how to run a pattern, read its graph and trace, and add one.
In every PR: GCC and Clang with no warnings, debug and release, and every test passing; MSVC in CI. The workbench’s tests run in under 10 s in a debug build.
Design choices
Section titled “Design choices”Confirmed with this plan’s PR (#90).
- The workbench is a directory and a library of its own, not part of
tests/. Step 4’s graph host and step 5’s evaluation command will link it, andworkbench_runalready does. It isn’t installed or exported, so ADR 0005’s rule holds: no mission links a reference operator. - Two oracles per pattern, one exact and one mathematical. The hand-wired run (W1) is bit-exact on the same compiler, so any difference is a fault in the graph’s execution, as in the flow tests. The closed-form oracle (W2) is to a tolerance, and judges the operators. A failure says which of the two it is.
data<D>is in the core. A pattern’s edges need it now, and step 3’s document, step 6’s lowering, and a mission’s first shaped operator (a camera’s pixels, ADR 0004) will need one storage, not one per repo. The missions’ message structs stay as they are.accumulatelatches when the reader’s region begins, and drops the oldest on overflow. Latching makes every firing of the reader see one window, whatever their order. Dropping the oldest and counting them is deterministic and allocates nothing, and the reader can tell that it lost values. The alternative, refusing to run, would make a run’s result depend on whether a rate assumption held, with no record of it.- No interpreter support for the patterns. The reference interpreter knows the toy operators, and gains
accumulateon them (B2). A second implementation of an FFT or a Kalman update in the interpreter would be a third oracle. The closed-form one is a stronger check, and the hand-wired run already checks the graph’s execution. - Tolerances, not byte-identity, across compilers. Floating point differs between GCC, Clang and MSVC, and between their libms. The missions’ determinism gates are byte-exact on one compiler, and so is W3 run to run. Across compilers, the oracle’s tolerance is the check.
data<D>carries values; MTL5 computes with them, and the SDK stays free of MTL5. An MTL5 type records a matrix’s size, not its structure, a bounded extent or its name, so it can’t be the domain G4 compares. Its symmetric and triangular matrices are views over full storage, so a covariance could drift asymmetric. Its fixed sizes can’t hold a count, and its dynamic ones allocate.data<D>has none of those gaps but no arithmetic, so operators convert at their own boundary, in the workbench. Keeping MTL5 out of the SDK means a mission that builds against anatomy doesn’t have to agree on an MTL5 version with reflex, synapse and cortex. The cost is the copy into and out of a packed domain at each firing: 16 and 10 elements for the Kalman filter’sP.
Not in this plan
Section titled “Not in this plan”- The six other patterns (attitude, PID, convolution, stencils, SOR/SSOR, QR) and the computational domain of a leaf: step 2.
- The graph document, the reader, the graph host, the cost record and the evaluation: steps 3 to 5.
workbench_runruns only compiled patterns. - The
queuerate rule, conditional regions, and iteration to convergence. - Checking an operator’s declared state domains against its handlers’ state roles. The Kalman filter declares its state, but nothing checks the declaration, which is step 3’s to carry in the document.
- Any change to a mission, other than the gate in B2.
- Performance. The reference operators are written to be checked, not to be fast.
- Optical flow, a ResNet18 CNN, and a transformer (a ViT or DETR). These are the patterns closest to an Embodied AI pipeline’s perception, and they follow in a step of their own, with an amendment to ADR 0005, whose table doesn’t list them. They need three things step 1 doesn’t have:
- A subgraph defined once and used many times. A ResNet is a residual block, repeated per stage with its own shapes and weights, and stages of blocks; a transformer is an encoder layer repeated N times. Today a body is a C++ type that fills one composite of a scoreboard graph. What these patterns need is a subgraph with ports, which a graph instantiates as often as it needs, nested, with its own constants, in a research graph or a pattern as well as in a composite. The checks must see through it: an instance’s edges, state and rate rules are checked as if it were written out, and the canonical form, the trace and the Mermaid view name each instance’s firings by its path (
stage2.block1.conv1). This is the feature these patterns are the client for. - Tensor domains of rank 3 and 4, for images and feature maps.
data<D>here stores rank 1 and 2. The convolution pattern of step 2 brings rank 3 and the sliding-window computational domain, and these patterns build on it. - Operators and weights from synapse. ResNet18 and a transformer come with GGUF weights, and their kernels are synapse’s on MTL5. The workbench’s operators are reference implementations that no mission links (ADR 0005, decision 1). Whether the workbench may link synapse’s operators, or registers its own reference ones and checks synapse’s against them, is the amendment’s to decide. Optical flow is the bridge between the two: a classical pattern (Lucas–Kanade over an image pyramid) whose oracle is a known translation or rotation of a synthetic image.
- A subgraph defined once and used many times. A ResNet is a residual block, repeated per stage with its own shapes and weights, and stages of blocks; a transformer is an encoder layer repeated N times. Today a body is a C++ type that fills one composite of a scoreboard graph. What these patterns need is a subgraph with ports, which a graph instantiates as often as it needs, nested, with its own constants, in a research graph or a pattern as well as in a composite. The checks must see through it: an instance’s edges, state and rate rules are checked as if it were written out, and the canonical form, the trace and the Mermaid view name each instance’s firings by its path (
Progress
Section titled “Progress”| Step | PR | State |
|---|---|---|
| B0: this plan | #90 | merged |
B1: flow::data<D> and complex elements | #91 | merged |
B2: accumulate | #93 | merged |
| B3: the workbench and the MLP | #94 | merged |
| B4: the Kalman filter | #95 | merged |
| B5: the DSP spectral pipeline | #96 | merged |
| B6: docs | #97 | merged, v0.12.0 |