Skip to content

Mental Model

Back to Codebase Guide

Commol builds compartment models from a declarative definition and runs them as difference equations, as ordinary differential equations, or as stochastic processes of whole units. Python owns the user-facing API and validation; Rust owns every rule about how a model becomes numbers. A model has a process written once (bins, stratifications, transitions), a structure that lays it out and sets its kind (today aggregate: amounts per compartment), and an engine that runs it; the generic layers work with any kind.

The golden rule

The Rust engine is the single source of truth. Python validates input and presents results. When it needs to know how the engine expands, initializes, plans or simulates a model, it asks the engine through a binding. It never re-implements the rule.

Commol has three layers (Architecture draws them). The user writes a model in Python. Python validates it and serializes it to JSON. The Rust engine builds and runs it.

User code
  |
  v
py-commol/commol/api/        ModelBuilder, Simulation, Calibrator, reports
  |
  v
py-commol/commol/context/    Pydantic model: validation and serialization
  |
  |  Model.model_dump_json()
  v
py-commol/src/               PyO3 bindings (module commol.commol_rs._commol_rs)
  |
  v
crates/commol-core/          model types, expressions (Cranelift JIT), SimulationEngine trait
crates/commol-model/         expansion, initial state, flow planning, rates, derivatives
crates/commol-difference/    the discrete step: X += derivatives
crates/commol-differential/  the ODE engine: diffsol integrates derivatives
crates/commol-stochastic/    the stochastic engines of whole units
crates/commol-random/        the generator, replicate seeds, samplers
crates/commol-statistics/    numerical helpers, log densities, the one quantile
crates/commol-calibration/   the problem in layers, optimizers (argmin fork), starts
crates/commol-inference/     parameter sample, predictive, scores

What Commol is

It is Evidence
A Python library with a fluent builder of the process py-commol/commol/api/model_builder.py
A validated, serializable model definition: a header, a process, a structure and an engine py-commol/commol/context/ (Pydantic): model.py, process.py, structure.py, aggregate.py
Generic layers that work with any model kind Simulation, Calibrator, reports and figures use only ModelStructure; Rust calibration uses only SimulationEngine (see Design decisions)
One model compiler for every engine: expansion, flows, rates, the change of one event of each flow, the right-hand side crates/commol-model/
A Rust difference-equations engine crates/commol-difference/
A Rust ODE engine on diffsol crates/commol-differential/
Two Rust stochastic engines of whole units: events in continuous time (direct method, tau-leaping) and the chain binomial crates/commol-stochastic/
Reproducible random numbers for stochastic engines and inference methods: the generator, replicate seeds, samplers, and a sum tree of rates that the stochastic engine of the test kind uses crates/commol-random/
A JIT compiler for rate expressions crates/commol-core/src/math_expression/jit/ (Cranelift)
One copy of each numerical helper, log density and weighted statistic that calibration and inference share: log Σ exp, the special functions, the normal, log-normal and Student t densities, the one quantile, the weighted summary crates/commol-statistics/
A calibration problem in layers: the forward model, the observation operator, feasibility, the discrepancy and the optimization objective; optimizers from one or several starts crates/commol-calibration/, py-commol/commol/api/calibrator.py
What every uncertainty method returns or reads: a weighted parameter sample, its predictive, scores, one entry point crates/commol-inference/, py-commol/commol/api/inference/, py-commol/commol/context/inference/

What Commol is not

It is not Boundary
A domain library It knows bins, categories and rates, not diseases or drugs. Domain vocabulary belongs in user scripts.
A numerical-methods library The differential engine delegates the integration to diffsol; Commol keeps no ODE method of its own (see Design decisions).
A clock A model has no physical units: rates are per step and positions and periods are step counts, and the axis is the independent variable of the model, whatever quantity it is. AxisUnits converts at the edges (see Design decisions).
A rule engine for transitions A transition has no condition. A rate expresses a gate, such as if(A > 0, k, 0) (see Mathematical expressions), and stratified rates set a rate per category.

Model kind and engine

Term What it is Where
Process What changes and how, written once for every kind: bins, accumulators, stratifications, transitions. Process (crates/commol-core/src/types/process.rs, context/process.py); Model.process
Model kind How the process is laid out: the kind of its structure. aggregate: the bins expanded over the stratifications into compartments, each holding an amount. A variant is a narrower form of a structure with the same kind and engines. ModelKind (crates/commol-core/src/types/engine.rs); ModelStructure and its subclasses in Python
Engine A method: how a model advances along the axis, and what a rate means: DifferenceEquations, DifferentialEquations, StochasticEvents, StochasticSteps. EngineType, the one list (commol-core/src/types/engine.rs); Model.engine

Each engine runs some kinds (EngineType::kinds). A model whose engine does not run its kind is rejected when it is made, and the error lists the engines of its kind.

How a model flows through the layers

ModelBuilder ... .build(Aggregate(...), engine)
                                   -> commol.context.Model[Aggregate]
                                      (Pydantic validation; the engine's list
                                      checks model.engine and its kind; the
                                      structure checks itself against the process)
Simulation(model)                  -> engine_state.build_engine(model JSON)
                                   -> commol_rs.core.Model.from_json(...)
                                   -> commol_rs.engine.Engine(...): the implementation of
                                      the engine model.engine.type names (EngineType list)
                                      for the kind of model.structure
                                   every engine of the aggregate kind compiles it with commol-model (CompiledModel):
                                        expand the bins of the process over its stratifications
                                        build the initial state
                                        plan the transition flows, resolve the neighbors, compile the rates
                                        flow_rates and flow_changes (one event of each flow)
                                        derivatives(step, t, X) = Σ changes × flows
Simulation.run(n) / run_at(at)     -> difference: X += derivatives, once per step
                                      differential: diffsol integrates derivatives
                                      stochastic events: one event (or a leap) at a time
                                      stochastic steps: a chain binomial draw per step
                                      (a stochastic engine: set_seed, then reset)
Simulation.replicates(count).run(…) -> Engine.run_replicates: one seed per replicate,
                                      run in parallel (run_replicates, in the rayon
                                      pool of commol.threads: parallel, Parallelism)
Calibrator(simulation, problem)    -> commol_rs.inference.CalibrationProblem(engine, ...),
                                      built once; run() calls its optimize(...):
                                      parameters by id; values of the kind, such as
                                      initial conditions, through the engine

The same model functions also answer questions about a process and its aggregate structure without a simulation: Aggregate.initial_state(process), Model.describe and the unit checker call initial_state, transition_flows and expanded_outputs of commol_rs.model through py-commol/commol/context/_model/engine_state.py.

Invariants

These hold for every change. A pull request that breaks one needs a design discussion first.

  • Rust is the single source of truth (The golden rule).
  • Compartments are identified by labels, not by names. A compartment is a bin plus one category per applied stratification (OutputLabels, CompartmentLayout). Code never parses a compartment name or matches it by prefix: bin A and bin A_x must never be confused.
  • A model that cannot be simulated as written is rejected when it is built. The flow planner returns an error instead of dropping a transition.
  • Stratification declaration order is part of the model. Stratifications expand in declaration order, and a conditional stratification may only reference stratifications declared before it.
  • A run is reproducible from its model and its seed, on one platform and one commol version. The same formula always runs the same compiled code (global JIT cache); a stochastic engine draws every random number from one generator seeded by SimulationEngine::set_seed, which reset restores; calibrations accept a seed. See Stochastic engines and seeds.
  • What the user saves, the engine reloads. Model.to_json writes every field, and calibrated values written back to a model reproduce the state the optimizer scored.

Checklist

  • [ ] Name the user-facing behavior first.
  • [ ] Find the layer that owns it in the Repository map.
  • [ ] If Python needs an engine rule, add or use a binding instead of re-implementing it.
  • [ ] Read the tests next to that code before you add new patterns.

Previous: Codebase Guide | Next: Architecture