Config-based RHS specification + compilation utilities for ODE/PDE systems
Project description
op_system
Domain-agnostic model specification and compilation for ODE, PDE, and multi-physics/multi-scale hybrids, with templates, helpers, and preserved metadata.
Statement of Need
Modelers often combine compartment-style hazards, templated populations, and metadata such as axes, kernels, and operators that must be validated and preserved for downstream solvers. op_system provides:
- YAML/JSON-friendly specs for model definitions (expr and transitions styles).
- Normalization with strict validation and metadata preservation.
- Compilation to safe, fast callables usable by engines like
op_engineor custom integrators.
Quick start (Python dict)
from op_system import compile_spec
spec = {
"kind": "expr",
"state": ["S", "I", "R"],
"aliases": {"N": "S + I + R"},
"equations": {
"S": "-beta * S * I / N",
"I": "beta * S * I / N - gamma * I",
"R": "gamma * I",
},
}
compiled = compile_spec(spec)
dydt = compiled.eval_fn(0.0, [999.0, 1.0, 0.0], beta=0.3, gamma=0.1)
YAML examples (organized and API-current)
The example set below is intentionally small but complete: each core modeling pattern is shown for both expr and transitions pathways where applicable.
1) Baseline SIR in both pathways
Smallest valid SIR written once; demonstrates expr vs transitions parity.
expr
system:
- module: op_system
spec:
kind: expr
state: [S, I, R]
equations:
S: -beta * S * I / sum_state()
I: beta * S * I / sum_state() - gamma * I
R: gamma * I
transitions
system:
- module: op_system
spec:
kind: transitions
state: [S, I, R]
transitions:
- from: S
to: I
rate: beta * I / sum_state()
- from: I
to: R
rate: gamma
2) Vaccination symmetry vs asymmetry (age × vax)
Shows how axis templates keep model structure concise while rate terms decide symmetry.
Symmetric across vax within each age (expr)
system:
- module: op_system
spec:
kind: expr
axes:
- name: age
coords: [child, adult]
- name: vax
coords: [u, v]
state: [S[age,vax], I[age,vax], R[age,vax]]
aliases:
lambda[age]: beta * sum_over(vax=j, I[age,j]) / sum_state()
equations:
S[age,vax]: -lambda[age] * S[age,vax]
I[age,vax]: lambda[age] * S[age,vax] - gamma * I[age,vax]
R[age,vax]: gamma * I[age,vax]
Asymmetric by vax (expr)
system:
- module: op_system
spec:
kind: expr
axes:
- name: age
coords: [child, adult]
- name: vax
coords: [u, v]
state: [S[age,vax], I[age,vax], R[age,vax]]
aliases:
lambda[age,vax]: beta * (1 - ve[vax]) * sum_over(vax=j, I[age,j]) / sum_state()
equations:
S[age,vax]: -lambda[age,vax] * S[age,vax]
I[age,vax]: lambda[age,vax] * S[age,vax] - gamma[vax] * I[age,vax]
R[age,vax]: gamma[vax] * I[age,vax]
What differs:
- In the symmetric config, the force term is
lambda[age](novaxindex), so vaccinated and unvaccinated groups within the same age use the same infection pressure and recovery rate. - In the asymmetric config, vaccine-specific terms are introduced (
ve[vax],gamma[vax]), so dynamics can diverge by vaccination status. - The structural template (
S[age,vax],I[age,vax],R[age,vax]) is identical in both; only the rate expressions change.
Symmetric vs asymmetric using transitions rates
# symmetric
spec:
kind: transitions
axes:
- name: age
coords: [child, adult]
- name: vax
coords: [u, v]
state: [S[age,vax], I[age,vax], R[age,vax]]
aliases:
lambda[age]: beta * sum_over(vax=j, I[age,j]) / sum_state()
transitions:
- from: S[age,vax]
to: I[age,vax]
rate: lambda[age]
- from: I[age,vax]
to: R[age,vax]
rate: gamma
# asymmetric
spec:
kind: transitions
axes:
- name: age
coords: [child, adult]
- name: vax
coords: [u, v]
state: [S[age,vax], I[age,vax], R[age,vax]]
transitions:
- from: S[age,vax]
to: I[age,vax]
rate: beta * (1 - ve[vax]) * sum_over(vax=j, I[age,j]) / sum_state()
- from: I[age,vax]
to: R[age,vax]
rate: gamma[vax]
Transitions pathway interpretation is the same:
- Symmetric: transition rates omit
vaxdependence (or depend only through shared aggregates). - Asymmetric: transition rates include explicit
vax-indexed terms. - There is no implicit symmetry constraint; symmetry/asymmetry is determined entirely by your rate expressions.
3) Multi-axis templates + helpers in both pathways
Highlights asymmetric axis membership and reuse of helper expressions across pathways.
expr (age × vax × strain; asymmetric axis membership)
system:
- module: op_system
spec:
kind: expr
axes:
- name: age
coords: [child, adult]
- name: vax
coords: [u, v]
- name: strain
coords: [wt, var]
state: [S[age,vax], I[age,vax,strain], R[age,vax]]
aliases:
foi[age,vax,strain]: beta[strain] * I[age,vax,strain] / sum_state()
equations:
S[age,vax]: -sum_over(strain=s, foi[age,vax,s] * S[age,vax])
I[age,vax,strain]: foi[age,vax,strain] * S[age,vax] - gamma[strain] * I[age,vax,strain]
R[age,vax]: sum_over(strain=s, gamma[strain] * I[age,vax,s])
transitions (same axis pattern)
system:
- module: op_system
spec:
kind: transitions
axes:
- name: age
coords: [child, adult]
- name: vax
coords: [u, v]
- name: strain
coords: [wt, var]
state: [S[age,vax], I[age,vax,strain], R[age,vax]]
transitions:
- from: S[age,vax]
to: I[age,vax,strain]
rate: beta[strain] * I[age,vax,strain] / sum_state()
- from: I[age,vax,strain]
to: R[age,vax]
rate: gamma[strain]
3b) Axis available only for a subgroup (no empty compartments)
When an axis should apply only to a subset (for example vaccination only for 65+), keep that axis off ineligible states to avoid empty compartments, and stratify only the eligible subgroup.
transitions with vax only for 65+
axes:
- name: age
coords: [u65, o65]
- name: vax
coords: [u, v]
state:
- S_u65
- I_u65
- R_u65
- S_o65[vax]
- I_o65[vax]
- R_o65[vax]
aliases:
I_total: sum_prefix("I_")
transitions:
# under 65 (unstratified by vax)
- from: S_u65
to: I_u65
rate: beta_u65 * I_total / sum_state()
- from: I_u65
to: R_u65
rate: gamma_u65
# 65+ (stratified by vax)
- from: S_o65[vax]
to: I_o65[vax]
rate: beta_o65[vax] * I_total / sum_state()
- from: I_o65[vax]
to: R_o65[vax]
rate: gamma_o65[vax]
# vaccination only available to 65+
- from: S_o65[u]
to: S_o65[v]
rate: vax_rate_o65
Key points:
- No coordinate-level masking exists today; adding an axis to a state expands over all its coordinates.
- To avoid empty vaccinated compartments for ineligible groups, split states so only eligible subpopulations carry that axis.
4) Chain helper in both pathways (no predeclared I1..Ik)
Even when using chain, declare the base staged compartment name in state (for example I below); the helper synthesizes I1..Ik so you do not enumerate them.
expr chain with synthesized staged states
system:
- module: op_system
spec:
kind: expr
state: [S, I, R]
chain:
- name: I
length: 3
forward: [gamma12, gamma23]
exit:
to: R
rate: gamma3r
equations:
S: -beta * S * I1 / sum_state()
R: gamma3r * I3
transitions chain-only flow generation
system:
- module: op_system
spec:
kind: transitions
state: [S, I, R]
chain:
- name: I
length: 3
entry:
from: S
rate: beta * S / sum_state()
forward: [gamma12, gamma23]
exit:
to: R
rate: gamma3r
transitions: []
5) Continuous axis + integrate_over (expr pathway)
system:
- module: op_system
spec:
kind: expr
axes:
- name: x
type: continuous
domain:
lb: 0.0
ub: 10.0
size: 5
spacing: linear
state: [u[x]]
state_axes:
u: [x]
kernels:
- name: K
axes: [x]
form: gaussian
params:
scale: 1.0
sigma: 0.5
equations:
u[x]: integrate_over(x=xi, K[xi] * u[xi]) - decay * u[x]
integrate_over uses trapezoidal weights derived from axis coordinates; non-uniform spacing is respected.
Installation
pip install op-system
# or locally
uv pip install .
Supports Python >= 3.11.
Testing
just ci # ruff + pytest + mypy (core + provider mirror)
# or individually
just pytest
just ruff
just mypy
Features
- Model kinds:
expr(explicit equations) andtransitions(hazard/flow style). - Transitions support optional
namemetadata preserved inmeta.transitions; templated transitions expand before hazard assembly with metadata intact. - Templates:
State[axis,...]expand over categorical axes; equations may be written once per template. - Aliases and inline placeholders like
theta[age]expand over categorical axes using the same assignments, removing per-axis parameter duplication. - Transitions now accept templated states and rates over categorical axes; templated
from/to/rateare expanded before hazard assembly. sum_over(axis=var, expr): unrolls over categorical coords; continuous axes are rejected.integrate_over(axis=var, expr): trapezoidal integrate along continuous axes using axis-derived deltas (non-uniform spacing supported).- Chain helper:
chainblock auto-fills equations/transitions for staged compartments (expr or transitions kinds). Keep your base model structure explicit (for exampleIorI[age,vax]must appear instate), while staged chain internals (I1..Ik) are synthesized automatically and do not need explicitstateentries. - Reducers in expressions:
sum_state(),sum_prefix(prefix). - Axes: categorical or continuous; continuous can be generated via
domain+size+spacing(linear/log/geom). - Metadata passthrough: axes, state_axes, kernels, operators, reserved blocks (
sources,couplings,constraints) inNormalizedRhs.meta.
Specification behavior notes
- Alias usage: aliases are expanded expressions evaluated against state, params, and earlier aliases; they are best used to reduce repeated symbolic expressions.
- Multi-axis grouping: forms like
S[age,vax,strain]are supported when all listed axes are defined inaxes. - Axis asymmetry (
expr): states/equations may use different axis subsets (for exampleS[age],I[age,strain]), and substitutions only apply where placeholders are present. - Axis asymmetry (
transitions): placeholder expansion uses all placeholders appearing infrom,to,rate, and optionalname, then renders each field with its own placeholders. - Chain helper state behavior:
chainsynthesizes staged names fromnameandlength, but does not replace explicit top-level state/axis declarations in your model. It can generate the first infection transition viaentryso transitions specs do not need a manualS -> I1edge.
Chain schema
chain:
- name: I
length: 3
entry: {from: S, rate: beta * force} # optional
forward: gamma # scalar broadcast
# or forward: [gamma12, gamma23] # per internal edge, length-1 values
exit: {to: R, rate: gamma3r} # optional; rate defaults to last forward rate
forwardmay be scalar or a list oflength - 1rates.entryis optional and, when provided, generatesentry.from -> I1.exitis optional and, when provided, generatesI_last -> exit.to.
Validation & AST guardrails
Expressions are parsed with ast and restricted to:
- Arithmetic, comparisons, ternary, bool ops; names/constants; calls/attributes on an allowlist.
- NumPy calls with
np.root:abs,exp,expm1,log,log1p,log2,log10,sqrt,maximum,minimum,clip,where,sin,cos,tan,sinh,cosh,tanh,hypot,arctan2. - Helpers:
sum_state(),sum_prefix(prefix).
Disallowed: non-np attribute access, non-whitelisted helpers, imports, or other AST node types. Errors are raised as ValueError / TypeError / NotImplementedError.
API (brief)
compile_spec(spec) -> CompiledRhs: normalize + compile in one step.normalize_rhs(spec) -> NormalizedRhs: validation + metadata preservation.compile_rhs(rhs) -> CompiledRhs: compile a pre-normalized model specification.NormalizedRhs.meta: access normalized metadata (axes, state_axes, kernels, operators, reserved blocks).
License & support
- License: GPL-3.0 (see LICENSE).
- Issues/feedback: use the GitHub issue tracker.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file op_system-0.1.2.tar.gz.
File metadata
- Download URL: op_system-0.1.2.tar.gz
- Upload date:
- Size: 66.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7350a960838092e2310ac41815422d7ec0f4ba5654fa541f01ca869977abe544
|
|
| MD5 |
324654132e1e53e73f7790389bbc3a44
|
|
| BLAKE2b-256 |
c22f6d8d1c5ec3108aa1d54e3cea0f8ac6a65a1b9836b4270073d8ad4ef2b6ab
|
Provenance
The following attestation bundles were made for op_system-0.1.2.tar.gz:
Publisher:
release.yaml on ACCIDDA/op_system
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
op_system-0.1.2.tar.gz -
Subject digest:
7350a960838092e2310ac41815422d7ec0f4ba5654fa541f01ca869977abe544 - Sigstore transparency entry: 1393349173
- Sigstore integration time:
-
Permalink:
ACCIDDA/op_system@159a21648fc297739ef92840b5e33ff983529471 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ACCIDDA
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@159a21648fc297739ef92840b5e33ff983529471 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file op_system-0.1.2-py3-none-any.whl.
File metadata
- Download URL: op_system-0.1.2-py3-none-any.whl
- Upload date:
- Size: 32.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a95121f7eeaf289ce9bebad0c4aa7bfc69062a3fa4316b392058b347f2f4706e
|
|
| MD5 |
f8514ac61c3cca68225ca38a3472f638
|
|
| BLAKE2b-256 |
41bd915cfe979af174d781fe4aef59c82de97ec0ac41adeeb27929e2a064c175
|
Provenance
The following attestation bundles were made for op_system-0.1.2-py3-none-any.whl:
Publisher:
release.yaml on ACCIDDA/op_system
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
op_system-0.1.2-py3-none-any.whl -
Subject digest:
a95121f7eeaf289ce9bebad0c4aa7bfc69062a3fa4316b392058b347f2f4706e - Sigstore transparency entry: 1393349179
- Sigstore integration time:
-
Permalink:
ACCIDDA/op_system@159a21648fc297739ef92840b5e33ff983529471 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ACCIDDA
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@159a21648fc297739ef92840b5e33ff983529471 -
Trigger Event:
workflow_dispatch
-
Statement type: