Skip to content

CU2 — Executing a graph of filters

g.node("add", Add(1))
g.node("double", Double())
g.edge("add", "double")
g.forward(41) # → 84.0

Status: closed. 29 tests in Rust, 25 in Python.

The engine goes in Rust, Python is a wrapper. It is your decision and it has a consequence worth keeping in mind: it forces Value to exist already. If the core executes, the data has to have a shape Rust understands.

The four roles, which are easy to confuse:

piecerolewhere
Graphthe structuresoma-core/src/graph.rs
Catalogthe store of implementationssoma-core/src/filter.rs
Filterthe contract of an executable unitsoma-core/src/filter.rs
Graph::runthe enginesoma-core/src/execution.rs
  1. Value with four variants: Null, Text, Bytes, Tensor. There is no Json because it would require serde_json and the core depends on nothing; there is no opaque Object because it is only good for sending something down a wire and there is no wire. The conversion error says what is missing rather than inventing a representation.
  2. Filter has one method: forward(&Value) -> Result<Value, FilterError>. No fit (training is another use case), no config_hash (caching), no meta (compiler), no composite_fit (autograd).
  3. No state parameter. The original passes state on every forward, even to stateless filters. State arrives with fit.
  4. Send + Sync on the trait — and it is not decoration: PyO3 requires a #[pyclass] to be Send, the graph carries the catalog inside, and the bound climbs to the trait. The compiler found it on its own.
  5. An object without forward fails when registered, not halfway through a run.
  6. A bool does not cross the boundary. True as the tensor 1.0 is the kind of silent conversion nobody understands later.