Runtime

The runtime executes circuits under different mathematical interpretations called calculi. The same circuit can be evaluated as Taylor series, stochastic processes, or discrete sequences.

The Three Calculi

RealCalculus

Taylor series expansion for deterministic computation. Use for ODEs, symbolic differentiation, and classical analysis.

StochasticCalculus

Monte Carlo simulation of stochastic differential equations. Use for finance, physics simulations, and SDEs.

DiscreteCalculus

Discrete sequences and finite differences. Use for time series, difference equations, and signal processing.

Execution Flow

Circuit (Parse Tree)
         │
         │ JAXCircuitCompiler.compile()
         ▼
JAX Function
         │
         │ execute(input_streams, state)
         ▼
Stream (coefficient representation)
         │
         │ StreamEvaluator.evaluate() with Calculus
         ▼
    ┌────┴────┬──────────────┐
    ▼         ▼              ▼
RealCalculus  Stochastic     Discrete
Taylor series SDE paths      Sequences

Streams

Streams represent functions as coefficient expansions:

from gimle.asgard.runtime.stream import Stream
import jax.numpy as jnp

# Coefficients for f(x) = 2 + 3x + x^2/2
stream = Stream(
    data=jnp.array([[2.0, 3.0, 1.0]]),
    dim_labels=("x",),
    chunk_size=1
)

Key Properties:

RealCalculus (Deterministic)

Represents functions as Taylor series:

from gimle.asgard.runtime.stream_evaluator import StreamEvaluator, RealCalculus
import jax.numpy as jnp

# Coefficients: [2, 3, 1] means f(x) = 2 + 3x + x^2/2
stream = Stream(
    data=jnp.array([[2.0, 3.0, 1.0]]),
    dim_labels=("x",),
    chunk_size=1
)

# Create evaluator
evaluator = StreamEvaluator(stream, {"x": RealCalculus(center=0.0)})

# Evaluate at specific points
x_points = jnp.array([0.0, 1.0, 2.0])
values = evaluator.evaluate(x=x_points)

for x, val in zip(x_points, values):
    print(f"f({x}) = {val:.2f}")
# f(0.0) = 2.00
# f(1.0) = 5.50  (2 + 3 + 0.5)
# f(2.0) = 10.00 (2 + 6 + 2)

Use Cases:

StochasticCalculus (Stochastic)

Simulates stochastic differential equations using Monte Carlo:

from gimle.asgard.runtime.stream_evaluator import StochasticCalculus

# Create stochastic calculus
calculus = StochasticCalculus(
    drift=0.0,            # Deterministic drift
    diffusion=1.0,        # Stochastic diffusion
    n_paths=1000,         # Number of Monte Carlo paths
    dt=0.01,              # Time step
    seed=42,              # Random seed for reproducibility
    interpretation="ito"  # "ito" (default) or "stratonovich"
)

# Generate Brownian motion paths
paths = calculus.generate_brownian_paths(t_start=0.0, t_end=2.0, n_steps=200)
# Returns: (1000, 201) array of Brownian motion paths

# Verify statistical properties
print(f"Mean at t=2: {jnp.mean(paths[:, -1]):.3f}")     # ~ 0
print(f"Variance at t=2: {jnp.var(paths[:, -1]):.3f}")  # ~ 2

Simulating SDEs

Solve stochastic differential equations of the form dX = mu(X,t)dt + sigma(X,t)dW:

# Ornstein-Uhlenbeck process: dX = -theta(X - mu)dt + sigma dW
theta = 0.5   # Mean reversion speed
mu = 1.0      # Long-term mean
sigma = 0.3   # Volatility
x0 = 2.0      # Initial value

calculus = StochasticCalculus(n_paths=10000, dt=0.01)
paths = calculus.simulate_sde(
    x0=x0,
    drift_fn=lambda x, t: -theta * (x - mu),
    diffusion_fn=lambda x, t: sigma,
    t_start=0.0,
    t_end=5.0,
    n_steps=500
)

Black-Scholes Example

# Geometric Brownian Motion: dS = mu*S*dt + sigma*S*dW
S0 = 100.0     # Initial stock price
mu = 0.05      # Expected return (5% annually)
sigma = 0.2    # Volatility (20% annually)
T = 1.0        # Time horizon

calculus = StochasticCalculus(n_paths=100000, dt=0.001, seed=123)
paths = calculus.simulate_sde(
    x0=S0,
    drift_fn=lambda x, t: mu * x,
    diffusion_fn=lambda x, t: sigma * x,
    t_start=0.0,
    t_end=T,
    n_steps=1000
)

# Price European call option
K = 100.0
payoffs = jnp.maximum(paths[:, -1] - K, 0)
option_price = jnp.mean(payoffs) * jnp.exp(-mu * T)
print(f"Option price: ${option_price:.2f}")

Use Cases:

Itô vs Stratonovich Interpretation

The interpretation parameter controls how the SDE is discretized:

Itô (default) — Uses the Euler-Maruyama method:

X(t+dt) = X(t) + drift(X(t), t) * dt + diffusion(X(t), t) * dW

Stratonovich — Uses the Heun predictor-corrector method:

K₁ = drift(X, t)*dt + diffusion(X, t)*dW
X̃  = X + K₁
K₂ = drift(X̃, t+dt)*dt + diffusion(X̃, t+dt)*dW   (same dW)
X(t+dt) = X + 0.5 * (K₁ + K₂)
# Itô interpretation (default)
ito_calculus = StochasticCalculus(
    n_paths=1000, dt=0.01, interpretation="ito"
)

# Stratonovich interpretation
strat_calculus = StochasticCalculus(
    n_paths=1000, dt=0.01, interpretation="stratonovich"
)

When to choose which:

Scenario Interpretation
Finance (Black-Scholes, interest rates) Itô
Physics (Langevin, Brownian motion) Stratonovich
Fast prototyping / lower accuracy needed Itô
Higher accuracy needed Stratonovich

Circuit-Driven SDEs

Asgard supports two ways to define stochastic differential equations:

1. Expression-based — Define drift and diffusion as Python functions:

calculus = StochasticCalculus(n_paths=1000, dt=0.01)
paths = calculus.simulate_sde(
    x0=1.0,
    drift_fn=lambda x, t: -0.5 * x,
    diffusion_fn=lambda x, t: 0.3,
    t_start=0.0, t_end=5.0, n_steps=500
)

2. Circuit-driven — Define the SDE as an equation and compile it:

from gimle.asgard.equation.equation import Equation
from gimle.asgard.compile.compiler import compile_equation_to_circuit

eq = Equation.from_string("Y = sde($drift, $sigma, X)")
circuit = compile_equation_to_circuit(eq)

In YAML examples, the circuit-driven approach uses the stochastic block:

equation: "Y = sde($drift, $sigma, X)"
stochastic:
  calculus: stratonovich   # or "ito"
  n_paths: 1000
  dt: 0.01
  seed: 42
params:
  drift: -0.5
  sigma: 0.3

DiscreteCalculus (Sequences)

Represents discrete-time sequences:

from gimle.asgard.runtime.stream_evaluator import DiscreteCalculus

# Sequence: f(n) = n^2 for n = 0, 1, 2, 3
stream = Stream(
    data=jnp.array([[0.0, 1.0, 4.0, 9.0]]),
    dim_labels=("n",),
    chunk_size=1
)

evaluator = StreamEvaluator(stream, {"n": DiscreteCalculus()})

# Evaluate at n = 2
result = evaluator.evaluate(n=2.0)
print(f"f(2) = {result}")  # 4.0

Operations:

Use Cases:

Mixed Calculi

Use different calculi for different dimensions:

# Discrete time, continuous space
evaluator = StreamEvaluator(
    stream,
    calculi={
        "n": DiscreteCalculus(),       # Discrete time
        "x": RealCalculus(center=0.0)  # Continuous space
    }
)

# Evaluate at discrete time n=5, continuous position x=1.5
result = evaluator.evaluate(n=5.0, x=1.5)

Two-Phase Execution

Asgard separates compilation and execution for efficiency:

from gimle.asgard.circuit.circuit import Circuit
from gimle.asgard.runtime.stream import Stream, StreamState

# Phase 1: Compile once (slow)
circuit = Circuit.from_string("composition(register(x), deregister(x))")

# Phase 2: Execute many times (fast, JIT-compiled)
for input_data in dataset:
    input_stream = Stream(data=input_data, dim_labels=("x",), chunk_size=1)
    outputs, state = circuit.execute([input_stream], StreamState())

Benefits:

Performance Comparison

Calculus Speed Memory Accuracy
RealCalculus Fast Low Exact (up to truncation)
StochasticCalculus Slow High Statistical (1/sqrt(n_paths))
DiscreteCalculus Fast Low Exact

Optimization Tips:

Next Steps