Circuits
Circuits are computational networks that transform streams. They are built from atomic operations (add, multiply, integrate, etc.) and combinators (composition, monoidal, trace, compose).
Circuit Syntax
Circuits use a functional composition style:
from gimle.asgard.circuit.circuit import Circuit
# Simple integration circuit
circuit = Circuit.from_string("register(x)")
# Fundamental theorem: integrate then differentiate
circuit = Circuit.from_string("composition(register(x), deregister(x))")
# Parallel operations
circuit = Circuit.from_string("monoidal(scalar(2.0), scalar(3.0))")
# Power series composition: exp(2t)
circuit = Circuit.from_string("compose(exp(8), scalar(2.0), t)")
print(circuit)
# Output: composition(register(x), deregister(x)) [1->1]
Combinators
Combinators define how circuits connect. Every circuit has an input degree and output degree — the number of wires in and out.
Composition (Sequential)
Syntax: composition(f, g) — Apply f, then apply g to the result.
Signature: If f : A -> B and g : B -> C, then composition(f, g) : A -> C
# Integrate then differentiate
circuit = Circuit.from_string("composition(register(x), deregister(x))")
# Chain three operations
circuit = Circuit.from_string("composition(composition(add, scalar(2.0)), register(t))")
The output degree of f must match the input degree of g. This is validated at parse time.
Monoidal (Parallel)
Syntax: monoidal(f, g) — Apply f and g independently to separate inputs.
Signature: If f : A -> B and g : C -> D, then monoidal(f, g) : A+C -> B+D
# Apply different scalars to two inputs
circuit = Circuit.from_string("monoidal(scalar(2.0), scalar(3.0))")
# One operation, one pass-through
circuit = Circuit.from_string("monoidal(register(x), id)")
Trace (Feedback)
Syntax: trace(f) — Create a feedback loop connecting the last output to the last input.
Signature: If f : A+X -> B+X, then trace(f) : A -> B
# Feedback loop for solving differential equations
circuit = Circuit.from_string(
"trace(composition(composition(add, split), monoidal(id, register(x))))"
)
The trace operator is how differential equations become executable: the feedback carries the running solution, with register accumulating coefficients iteratively.
Compose (Power Series Composition)
Syntax: compose(f, g, dim) — Compute the power series composition $f(g(x))$.
Signature: f must be 0->1 (generator) or 1->1 (transformer), g must be 1->1. Result is always 1->1.
# exp(2t): compose exp series with 2t scaling
circuit = Circuit.from_string("compose(exp(8), scalar(2.0), t)")
# sin(3x): compose sin series with 3x scaling
circuit = Circuit.from_string("compose(sin(8), scalar(3.0), x)")
This is essential for nonlinear operations on power series — it computes the Taylor coefficients of the composed function.
Atomic Operations
Complete Reference
Generators (0 inputs):
| Operation | Signature | Description |
|---|---|---|
const(n) |
0 -> 1 |
Constant stream |
param(name) |
0 -> 1 |
Runtime parameter (resolved from var_values) |
sin(n) |
0 -> 1 |
Sine Taylor coefficients (up to degree n) |
cos(n) |
0 -> 1 |
Cosine Taylor coefficients (up to degree n) |
exp(n) |
0 -> 1 |
Exponential Taylor coefficients (up to degree n) |
black_box_source(name) |
0 -> 1 |
Custom external data source |
Unary (1 input, 1 output):
| Operation | Signature | Description |
|---|---|---|
id |
1 -> 1 |
Identity (pass-through) |
scalar(n) |
1 -> 1 |
Multiply by constant |
register(dim) |
1 -> 1 |
Integration along dimension |
deregister(dim) |
1 -> 1 |
Differentiation along dimension |
stochastic_register(dim) |
1 -> 1 |
Stochastic integration (dW) |
var(name) |
1 -> 1 |
Variable reference |
exp |
1 -> 1 |
Exponential (coefficient transformer) |
power(n) |
1 -> 1 |
Raise to fixed exponent |
abs |
1 -> 1 |
Absolute value |
log |
1 -> 1 |
Natural logarithm |
black_box(name) |
1 -> 1 |
Custom external function |
Binary (2 inputs):
| Operation | Signature | Description |
|---|---|---|
add |
2 -> 1 |
Element-wise addition |
multiplication |
2 -> 1 |
Element-wise multiplication |
division |
2 -> 1 |
Element-wise division |
power |
2 -> 1 |
Binary power ($X^Y$) |
convolution(dim) |
2 -> 1 |
Convolution along dimension |
Routing:
| Operation | Signature | Description |
|---|---|---|
split |
1 -> 2 |
Duplicate input to two outputs |
swap |
2 -> 2 |
Swap two input wires |
terminal |
1 -> 0 |
Terminate a wire (discard) |
Register (Integration)
Shifts coefficients right, mathematically equivalent to integration:
circuit = Circuit.from_string("register(x)")
# Behavior on Taylor coefficients:
# [a, b, c] -> [0, a, b]
# Represents: f(x) -> integral of f(x) dx
Deregister (Differentiation)
Shifts coefficients left, mathematically equivalent to differentiation:
circuit = Circuit.from_string("deregister(x)")
# Behavior on Taylor coefficients:
# [a, b, c] -> [b, c]
# Represents: f(x) -> df/dx
Stochastic Register
Like register, but integrates against Brownian motion (dW) instead of time (dt). Used in the diffusion term of SDEs:
circuit = Circuit.from_string("stochastic_register(t)")
When executed under the stochastic runtime, this produces Monte Carlo paths rather than deterministic coefficients.
Transcendental Generators
sin(n), cos(n), and exp(n) generate Taylor coefficients with factorial scaling up to degree n:
# exp coefficients: [1, 1, 1, 1, ...] (factorial-scaled)
circuit = Circuit.from_string("exp(8)")
# sin coefficients: [0, 1, 0, -1, 0, 1, ...]
circuit = Circuit.from_string("sin(8)")
# cos coefficients: [1, 0, -1, 0, 1, 0, ...]
circuit = Circuit.from_string("cos(8)")
Note: bare exp (without a degree argument) is a transformer (1->1) that applies the exponential coefficient-by-coefficient. The generator form exp(n) is 0->1.
Black-Box Operations
Wrap external functions as circuit primitives:
# A learned function (e.g., neural network)
circuit = Circuit.from_string("black_box(learned_model)")
# An external data source
circuit = Circuit.from_string("black_box_source(observed_data)")
Black-box operations are first-class citizens — they compose with all other operations and support backpropagation for end-to-end training.
Executing Circuits
Circuits transform streams of coefficients:
from gimle.asgard.circuit.circuit import Circuit
from gimle.asgard.runtime.stream import Stream, StreamState
import jax.numpy as jnp
# Create integration circuit
circuit = Circuit.from_string("register(x)")
# Input: f(x) = x (Taylor coefficients [0, 1])
input_stream = Stream(
data=jnp.array([[0.0, 1.0]]),
dim_labels=("x",),
chunk_size=1
)
# Execute circuit
outputs, state = circuit.execute([input_stream], StreamState())
print(f"Input: {input_stream.data[0]}") # [0, 1] = x
print(f"Output: {outputs[0].data[0]}") # [0, 0, 1] = x^2/2
Type Checking
Circuits are validated at parse time. The output degree of f must match the input degree of g in composition(f, g):
# Valid: add (2->1) then scalar(2.0) (1->1)
circuit = Circuit.from_string("composition(add, scalar(2.0))")
# Valid: register(x) (1->1) then deregister(x) (1->1)
circuit = Circuit.from_string("composition(register(x), deregister(x))")
# Check degrees
print(f"Input degree: {circuit.input_degree}")
print(f"Output degree: {circuit.output_degree}")
Circuit Properties
Every parsed circuit exposes structural metadata:
circuit = Circuit.from_string(
"trace(composition(composition(add, split), monoidal(id, register(x))))"
)
circuit.input_degree # 1
circuit.output_degree # 1
circuit.has_trace # True
circuit.has_register # True
circuit.has_stochastic_register # False
circuit.dimension_identifiers # frozenset({'x'})
These flags determine how the runtime executes the circuit — traced circuits with registers use the stream calculus evaluator for correct coefficient-by-coefficient semantics.