Skip to content

pathsim/fastsim

Repository files navigation

FastSim Logo

A Rust block-diagram simulation engine

Drop-in replacement for PathSim


FastSim is a Rust reimplementation of PathSim with an identical Python API via PyO3. Python callbacks are automatically traced into an optimized SSA graph, differentiated symbolically, and evaluated in Rust.

Features

  • Drop-in compatible: same API as PathSim; swap the import
  • Rust engine: zero-copy data paths, flat DAG evaluation, dynamic block sizing
  • 21 ODE solvers: explicit and implicit, adaptive and fixed-step
  • Standalone solvers: RKDP54.integrate(func, x0, time_end=50) with automatic JIT compilation
  • JIT compiler: Python functions traced into flat-tape IR with CSE, constant folding, strength reduction, and FMA detection
  • Automatic differentiation: symbolic forward-mode AD for analytical Jacobians
  • Standalone JIT + AD API: jit(func) and jacobian(func) as JAX-style transformations
  • Event handling: zero-crossing detection, scheduled events, conditions for hybrid systems
  • Hierarchical: nest subsystems for modular designs
  • Mutable parameters: change block parameters at runtime with automatic reconstruction
  • Dynamic ports: blocks adapt their state dimension to connected inputs

Quick Example

from fastsim import Simulation, Connection
from fastsim.blocks import Integrator, Amplifier, Adder, Scope

# Damped harmonic oscillator: x'' + 0.5x' + 2x = 0
int_v = Integrator(5)       # velocity, v0=5
int_x = Integrator(2)       # position, x0=2
amp_c = Amplifier(-0.5)     # damping
amp_k = Amplifier(-2)       # spring
add = Adder()
scp = Scope()

sim = Simulation(
    blocks=[int_v, int_x, amp_c, amp_k, add, scp],
    connections=[
        Connection(int_v, int_x, amp_c),
        Connection(int_x, amp_k, scp),
        Connection(amp_c, add),
        Connection(amp_k, add[1]),
        Connection(add, int_v),
    ],
)

sim.run(30)
time, [x] = scp.read()

Standalone ODE Solvers

All 21 solvers are available as standalone integrators with automatic JIT compilation of the right-hand side and symbolic Jacobian generation for implicit solvers.

from fastsim.solvers import RKDP54, ESDIRK43

def lorenz(x, t):
    sigma, rho, beta = 10.0, 28.0, 8.0/3.0
    return [sigma*(x[1]-x[0]), x[0]*(rho-x[2])-x[1], x[0]*x[1]-beta*x[2]]

# Explicit adaptive solver
t, x = RKDP54.integrate(lorenz, [1, 1, 1], time_end=50.0)

# Implicit solver for stiff systems (Jacobian generated automatically via AD)
t, x = ESDIRK43.integrate(robertson, [1, 0, 0], time_end=1.0, tolerance_lte_abs=1e-8)

JIT Compilation and Automatic Differentiation

Python functions are automatically traced into an optimized SSA computation graph and evaluated in Rust. Available as standalone transformations (JAX-style):

from fastsim.jit import jit, jacobian

# JIT compile (lazy tracing on first call)
f = jit(lorenz)
result = f([1.0, 1.0, 1.0], 0.0)

# Eager compilation with known input size
f = jit(lorenz, n_x=3)

# Automatic Jacobian via symbolic AD
jac_fn = jacobian(lorenz)
J = jac_fn([1.0, 1.0, 1.0], 0.0)  # 3x3 numpy array

Supported operations: arithmetic, np.sin/cos/tan/exp/log/tanh/..., np.dot, np.clip, np.where, np.linalg.norm, np.cross, matrix multiply (@), np.sum, branching via fastsim.where(), and more. Falls back to Python for unsupported patterns.

Automatic Compilation in Blocks

Python functions in ODE, Function, and DynamicalSystem blocks are automatically traced and compiled. No configuration needed.

from fastsim.blocks import ODE
import numpy as np

a, b, c = 0.04, 1e4, 3e7

def robertson(x, u, t):
    return np.array([
        -a*x[0] + b*x[1]*x[2],
         a*x[0] - b*x[1]*x[2] - c*x[1]**2,
         c*x[1]**2
    ])

ode = ODE(robertson, initial_value=[1.0, 0.0, 0.0])
print(ode.jit_compiled)  # True

Custom Blocks

from fastsim.blocks import StateSpace

class ButterworthLowpass(StateSpace):
    def __init__(self, cutoff, order=2):
        from scipy.signal import butter, tf2ss
        b, a = butter(order, cutoff, analog=True)
        A, B, C, D = tf2ss(b, a)
        super().__init__(A=A.tolist(), B=B.tolist(), C=C.tolist(), D=D.tolist())

Custom blocks run at full Rust speed; only the constructor runs in Python.

Mutable Parameters

All block parameters can be modified at runtime. Setting a parameter reconstructs the Rust block automatically while preserving engine state.

from fastsim.blocks import Amplifier, PT1

amp = Amplifier(gain=5.0)
amp.gain = 10.0  # instant, no performance cost

pt1 = PT1(K=1.0, T=0.5)
pt1.set(K=5.0, T=1.0)  # batched update, single reinit

WebAssembly / Pyodide build

fastsim compiles to wasm32-unknown-emscripten and runs in the browser via Pyodide. The FMI feature (FMU import) relies on runtime dynamic-library loading and is excluded from WASM builds; everything else (solvers, JIT tape interpreter, all blocks) runs unchanged.

# one-time toolchain setup
rustup toolchain install nightly
rustup component add rust-src --toolchain nightly
rustup target add wasm32-unknown-emscripten --toolchain nightly
pip install pyodide-build                 # into your venv
# emscripten matching the pinned Pyodide version (0.29.4 -> emcc 4.0.9), e.g. via emsdk

# build the wheel (writes dist/fastsim-*-wasm32.whl)
EMSDK_DIR=/path/to/emsdk ./scripts/build_pyodide.sh

The wheel installs in Pyodide with micropip.install and exposes the full Python API (import fastsim). Override the target release with PYODIDE_VERSION=... (must match your installed emscripten).

License

fastsim is licensed under the PolyForm Noncommercial License 1.0.0: free for noncommercial use (research, teaching, academia, personal and hobby projects). Commercial use, including shipping fastsim-generated C code in a commercial product, requires a commercial license.

The generated C code is "Output" under the license and carries the same noncommercial limitation; each generated file is stamped with this notice.

For commercial licensing, contact info@pathsim.org.

Need a fully open-source option? The pure-Python implementation, pathsim, is available separately under the MIT License with no field-of-use restriction.