Operon v0.14: Coalgebra, Diffusion, Optics
Formal State Machines, Spatially Varying Gradients, and Conditional Wire Routing
operon-ai v0.14.0Summary
Operon v0.14 implements three paper gaps: Coalgebraic State Machines (Paper §4.2) for composable observation and evolution with bisimulation equivalence, Morphogen Diffusion (Paper §6.4) for graph-based spatially varying concentrations that replace the global gradient model, and Optic-Based Wiring (Paper §3.3) for conditional routing (prism) and collection processing (traversal) on wires. The release adds 67 new tests (827 total, zero regressions), 3 examples, and 17 new public exports.
1. The Algebra–Coalgebra Duality
v0.13 built the multicellular hierarchy: cells specialize, form tissues, exchange tools.
But the individual cell’s behavior was still described informally. HistoneStore has
a retrieve() method and a store() method. ATP_Store has
report() and consume(). CellCycleController has
get_phase() and advance(). The pattern is the same each time:
observe state and evolve state. But without a shared interface, these
machines cannot be composed or compared.
The paper (§4.2) identifies this as a coalgebra—the categorical dual of an algebra. Where an algebra builds structure up through constructors, a coalgebra observes structure through destructors. The fundamental signature is:
This is a Mealy machine: a state space $S$, an input alphabet $I$, and an output alphabet $O$. The coalgebraic framing gives us composition for free.
1.1 The Coalgebra Protocol
The Coalgebra protocol requires exactly two methods:
@runtime_checkable
class Coalgebra(Protocol[S, I, O]):
def readout(self, state: S) -> O: ...
def update(self, state: S, inp: I) -> S: ...
The simplest concrete implementation is FunctionalCoalgebra—two
plain functions wrapped into the protocol:
counter = FunctionalCoalgebra(
readout_fn=lambda s: s, # observe: current total
update_fn=lambda s, i: s + i, # evolve: add delta
)
counter.readout(0) # 0
counter.update(0, 5) # 5
counter.readout(5) # 5
1.2 StateMachine: Mutable State with Trace
StateMachine wraps a coalgebra with a current state, providing an
imperative interface and recording every transition as a TransitionRecord:
sm = StateMachine(state=0, coalgebra=counter) outputs = sm.run([1, 2, 3, 4, 5]) # outputs = [0, 1, 3, 6, 10] (readout before each update) # sm.state = 15 # sm.trace = [TransitionRecord(...), ...]
The trace is a full audit log: for each step, it records state before, input,
output, and state after. Tracing can be disabled per-step with record=False
for performance-critical paths.
1.3 Composition
Two composition operators mirror the biological reality:
- ParallelCoalgebra—Two pathways activated by the same signal. State is $(S_1, S_2)$, output is $(O_1, O_2)$. Both receive the same input. Analogy: MAPK and PI3K both triggered by EGF binding.
- SequentialCoalgebra—Signal transduction cascade. Input goes to the first coalgebra; its readout becomes the input to the second. Analogy: receptor activation produces a second messenger that activates an effector.
1.4 Bisimulation
Two state machines are bisimilar if no observer can distinguish them—for
every input sequence, they produce the same output sequence. check_bisimulation
tests this over a bounded input sequence:
a = StateMachine(state=0, coalgebra=counter_coalgebra()) b = StateMachine(state=0, coalgebra=counter_coalgebra()) result = check_bisimulation(a, b, [1, 2, 3, 4, 5]) # result.equivalent = True # result.states_explored = 5 c = StateMachine(state=0, coalgebra=counter_coalgebra()) d = StateMachine(state=100, coalgebra=counter_coalgebra()) result = check_bisimulation(c, d, [1, 2, 3]) # result.equivalent = False # result.witness = (1, 0, 100) -- first diverging input and outputs
Biological Parallel
Two cells are bisimilar if they respond identically to all stimuli—the definition of “same cell type” in functional terms. You never compare internal state directly; you compare observable behavior. This is precisely the coalgebraic perspective: identity through observation, not through structure.
operon_ai/core/coalgebra.py
2. Morphogen Diffusion
v0.13’s MorphogenGradient is global: every agent reads the same
concentrations. This is like broadcasting a hormone through the bloodstream—useful,
but it misses the key mechanism that patterns embryonic development: spatial diffusion.
In real embryogenesis, morphogens like Bicoid in Drosophila are secreted from localized sources and diffuse through tissue. Cells near the source see high concentration; cells far away see low concentration. The resulting gradient drives spatially patterned gene expression without any central controller.
2.1 Graph-Based Spatial Model
Operon agents don’t have physical positions (Paper §6.5, line 135). Instead, we use graph adjacency from the wiring topology as the spatial model. Each node is an agent or cell; each edge is a connection through which morphogens can diffuse.
field = DiffusionField()
for n in ["Source", "Near", "Mid", "Far"]:
field.add_node(n)
field.add_edge("Source", "Near")
field.add_edge("Near", "Mid")
field.add_edge("Mid", "Far")
field.add_source(MorphogenSource(
"Source", MorphogenType.COMPLEXITY, emission_rate=0.5
))
field.run(50)
# Gradient forms: Source > Near > Mid > Far
for n in ["Source", "Near", "Mid", "Far"]:
c = field.get_concentration(n, MorphogenType.COMPLEXITY)
print(f"{n}: {c:.3f}")
2.2 The Diffusion Algorithm
Each step() applies four phases:
-
Emit—Each
MorphogenSourceadds itsemission_rateto its node, capped atmax_concentration. -
Diffuse—For each node, a
diffusion_ratefraction of concentration flows outward, split evenly among neighbors. This is a discrete approximation of Fick’s law on the graph Laplacian. - Decay—All concentrations are multiplied by $(1 - \text{decay\_rate})$, modeling morphogen degradation.
-
Clamp—Values above 1.0 are capped; values below
min_concentrationsnap to zero, preventing floating-point dust.
Definition: Discrete Graph Diffusion
For node $v$ with neighbors $N(v)$ and concentration $c_v$:
where $d$ is the diffusion rate and $\lambda$ is the decay rate.
2.3 Tissue Integration
Tissue gains an optional diffusion_field parameter. When
present, add_cell() registers nodes and connect_cells()
registers edges automatically. Two new methods bridge to the existing gradient API:
-
tissue.diffuse(steps=1)—Advance the diffusion field. -
tissue.get_cell_gradient(name)—Returns aMorphogenGradientreflecting local concentrations at that cell. Falls back to the shared tissue gradient when no diffusion field is configured.
When diffusion_field is None (the default), all behavior
is identical to v0.13. The integration is fully backward compatible.
Why Graph Adjacency?
Physical coordinates would require inventing a spatial embedding for every agent topology. Graph adjacency is intrinsic—it comes directly from the wiring diagram that already exists. An agent’s “position” is defined by its connectivity, not by arbitrary coordinates. Two agents that are three hops apart in the wiring graph are “far” in the diffusion model.
operon_ai/coordination/diffusion.py,
Tissue.diffuse() and .get_cell_gradient() in
operon_ai/multicell/tissue.py
3. Optic-Based Wiring
v0.13’s wiring system uses PortType matching—a lens-like
pattern where data flows through if the types align. This handles the common case
but lacks two capabilities the paper describes:
- Conditional routing—Send data to different destinations based on its DataType. Currently requires the application to build routing logic inside module handlers.
- Collection processing—Apply a transformation to each element of a list as it flows through a wire. Currently requires the receiving module to handle iteration.
Optics (Paper §3.3) solve both by adding three optic types to the wiring layer:
| Optic | Behavior | Biological Analogy |
|---|---|---|
LensOptic |
Pass-through (always transmits) | Constitutive expression—always active |
PrismOptic |
Conditional routing by DataType | Receptor specificity—only responds to matching ligand |
TraversalOptic |
Map transform over list elements | Polymerase processivity—walks a sequence |
ComposedOptic |
Sequential composition of optics | Multi-step enzyme cascade |
3.1 Prism Routing
The prism is the most interesting optic. It accepts a set of DataType
values and rejects everything else. When used with fan-out wiring—one output port
connected to multiple prism-filtered wires—data is routed to the correct
destination based on its runtime type:
diagram = WiringDiagram()
diagram.add_module(ModuleSpec(name="Router", ...))
diagram.add_module(ModuleSpec(name="JSONHandler", ...))
diagram.add_module(ModuleSpec(name="ErrorHandler", ...))
diagram.connect("Router", "out", "JSONHandler", "in",
optic=PrismOptic(accept=frozenset({DataType.JSON})))
diagram.connect("Router", "out", "ErrorHandler", "in",
optic=PrismOptic(accept=frozenset({DataType.ERROR})))
# At runtime: if Router emits JSON, only JSONHandler receives it.
# If Router emits ERROR, only ErrorHandler receives it.
# The rejected module is skipped entirely.
Three changes to DiagramExecutor make this work:
-
Static type bypass—When a wire carries an optic,
require_flow_tois skipped. The optic takes responsibility for type correctness at runtime. -
Output coercion relaxation—When all outgoing wires from
a port carry optics, the executor accepts
TypedValueas-is without enforcing DataType matching against the port spec. -
Prism-rejection skip—When a module’s unfilled inputs
all come from optic wires whose sources have already executed, the module is skipped
with an empty
ModuleExecutionrather than causing a deadlock.
3.2 Traversal Processing
TraversalOptic maps a transform over list elements on the wire:
doubler = TraversalOptic(transform=lambda x: x * 2) doubler.transmit([1, 2, 3], DataType.JSON, IntegrityLabel.VALIDATED) # [2, 4, 6] # Single values are treated as one-element collections: doubler.transmit(5, DataType.JSON, IntegrityLabel.VALIDATED) # 10
3.3 Composition and Coexistence
ComposedOptic chains optics sequentially: all must accept, and transforms
are applied left-to-right. Optics also coexist with DenatureFilters
on the same wire—denaturation applies first, then the optic:
diagram.connect("A", "out", "B", "in",
denature=StripMarkupFilter(),
optic=TraversalOptic(transform=str.upper),
)
# Data flow: raw value -> strip markup -> uppercase each element -> deliver
Why Skip, Not Fallback?
When a prism rejects data, the destination module is skipped entirely rather than
receiving a default value. This mirrors biology: a receptor that doesn’t bind
its ligand doesn’t activate the downstream pathway with a “nothing bound”
signal. It simply doesn’t activate. Skipping also avoids forcing every handler
behind a prism to include if input is None boilerplate.
operon_ai/core/optics.py,
Wire.optic in operon_ai/core/wagent.py,
optic handling in DiagramExecutor.execute() in operon_ai/core/wiring_runtime.py
4. Test Coverage
| Suite | Tests | Status |
|---|---|---|
| Coalgebraic State Machines | 19 | All pass |
| Optic-Based Wiring | 27 | All pass |
| Morphogen Diffusion | 21 | All pass |
| Existing (regression) | 760 | All pass |
| Total | 827 | All pass |
5. Examples
| Example | Feature |
|---|---|
61_coalgebraic_state_machines.py | Counter coalgebra, StateMachine with trace, parallel & sequential composition, bisimulation |
62_morphogen_diffusion.py | Linear chain, star topology, competing sources, local gradients |
63_optic_based_wiring.py | Lens, prism routing, traversal transforms, composition, denature coexistence |
6. What’s Next
With coalgebra, diffusion, and optics in place, the remaining paper gaps center on higher-order composition and formal verification:
- Bisimulation-Based Testing—Use coalgebraic equivalence as a regression test: if a refactored agent is bisimilar to the original over a representative input suite, the refactor preserves behavior.
-
Diffusion-Guided Orchestration—Replace the global
GradientOrchestratorwith diffusion-field-aware orchestration where each agent reads its local gradient, not the shared one. - Optic Composition in Tissue—Allow tissues to declare optic policies on their boundary ports, so inter-tissue wires automatically carry the tissue’s routing and transform constraints.
Code and examples: github.com/coredipper/operon