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

Labs Serve UI and platform layers

Labs Serve UI 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