ReferencesBiosimulant LibraryHow Biosimulant Works

How Biosimulant Works

Biosimulant is an open-source Python engine for composing biological simulators out of small modules with explicit typed ports. The current kernel is a communication-step orchestrator: every module advances across the same window, and outputs are committed atomically at the boundary.

If you want a runnable introduction, start with the Library Quickstart (Python). This page explains the architecture behind that example.

The five core primitives

PrimitiveRole
BioModuleA runnable simulation component
BioWorldThe communication-step orchestrator
SignalSpecA declared port contract
Typed BioSignal subclassesRuntime payloads crossing module boundaries
WiringBuilderA validated graph builder for named module ports

BioWorld: communication windows

Create a world with a required communication step:

world = biosim.BioWorld(communication_step=0.1)

For each window [t, t + communication_step]:

  1. the world reads committed signals at t
  2. it delivers those inputs to every connected target module
  3. every module advances across the same window via advance_window(start, end)
  4. the world commits all outputs atomically at t + communication_step

This means:

  • signal exchange is synchronized at shared communication boundaries
  • state signals use hold-last-value semantics until replaced
  • event signals are delivered once per connection per source timestamp
  • stale reads are checked from the consuming port’s SignalSpec

The kernel does not expose an execution-order scheduling contract, a global external tick parameter, rollback, or algebraic-loop solving.

Final outputs are still committed at the final boundary. Downstream modules see those outputs on a later communication turn. For workflow-style labs with report, export, or visualization modules downstream of a final producer, runners can use settle_steps / world.settle() to perform zero-time propagation turns after the simulation duration without changing simulated time.

BioModule: the runnable unit

Modules no longer participate in a per-module due-time scheduler. The world-facing hook is:

def advance_window(self, start: float, end: float) -> None:
    ...

The full module surface includes:

  • setup(config=None)
  • set_inputs(signals)
  • advance_window(start, end)
  • get_outputs()
  • inputs() / outputs() returning port -> SignalSpec
  • snapshot() / restore(snapshot)
  • optional visualize()

reset() still exists as a convenience method on the base class, but snapshot/restore is the kernel durability mechanism for branching and replay.

Signals: declared contracts plus typed payloads

Port declarations use SignalSpec:

def outputs(self):
    return {
        "membrane_potential": biosim.SignalSpec.scalar(
            dtype="float64",
            emitted_unit="mV",
        )
    }

Runtime payloads are emitted as one of:

  • ScalarSignal
  • ArraySignal
  • RecordSignal
  • EventSignal

All runtime signals carry:

  • source
  • name
  • value
  • emitted_at
  • optional bound SignalSpec

BioSignal itself is an abstract base class. Do not instantiate it directly.

Signal freshness and event semantics

The world preserves emitted_at when routing signals. Consumers can declare:

  • max_age: how stale a state signal may be before it is considered too old
  • stale_policy: warn, error, or ignore

Event signals are handled differently from state signals:

  • state signals remain available until replaced by a later non-empty output mapping
  • event signals remain in the store but are delivered once per connection per source timestamp

SignalSpec.interpolation is currently a declared policy, not a runtime interpolation engine. The world reads committed boundary values.

Wiring and composition

Graphs are built explicitly:

builder = biosim.WiringBuilder(world)
builder.add("eye", Eye()).add("lgn", LGN())
builder.connect("eye.visual_stream", ["lgn.retina"])
builder.apply()

Connection validation happens against declared SignalSpec contracts. That keeps the orchestration layer generic: a module can be an ODE solver, an ONNX wrapper, or a bridge to an external simulator, and the world still treats it the same way.

Snapshots and branching

BioWorld.snapshot() captures:

  • current simulation time
  • committed signal store
  • connection delivery state
  • per-module snapshots
  • setup config

restore() rehydrates that world state, and branch() deep-copies the world so two futures can diverge from the same communication boundary.

Manifests and packaged composition

The current manifest surface matches the current kernel:

  • models declare biosim.entrypoint and required biosim.communication_step
  • labs declare required runtime.communication_step
  • labs may declare optional runtime.settle_steps for final graph propagation
  • labs should not declare legacy per-entry timing, ordering, or external tick fields
  • port metadata should prefer structured typed io entries over bare strings

SimUI and platform layers

SimUI and the hosted platform sit above the same kernel. They consume:

  • world lifecycle events such as STARTED, STEP, and FINISHED
  • transport-safe visualization specs from visualize()
  • snapshots and status payloads for streaming or replay

Those layers do not change the kernel semantics. They expose them through browser and API surfaces.

See Also