Native JSON serialization¶
MxlPy can read and write models in a native, version-controllable JSON format
(.mxl.json). Unlike SBML, which is the standard for cross-tool interchange,
the native format captures the full MxlPy model structure - variables,
parameters, reactions, derived quantities and readouts - and is designed to
produce clean, meaningful diffs in version control.
Rate expressions are stored as trees of math nodes using the node set shared with MxlWeb, so the same files can be consumed by both tools.
from pathlib import Path
from tempfile import TemporaryDirectory
import mxlpy
from mxlpy import Model, fns
def glycolysis() -> Model:
return (
Model()
.add_variables({"A": 1.0, "B": 0.0})
.add_parameters({"k1": 0.1, "k2": 0.05})
.add_reaction(
"r1",
fns.mass_action_1s,
args=["A", "k1"],
stoichiometry={"A": -1, "B": 1},
)
)
model = glycolysis()
Saving and loading¶
mxlpy.save writes the model, mxlpy.load reconstructs it. A loaded model is
behaviourally identical to the original and simulates to the same result.
with TemporaryDirectory() as tmp:
path = Path(tmp) / "glycolysis.mxl.json"
mxlpy.save(model, path, model_id="glycolysis", description="Minimal example")
print(path.read_text())
restored = mxlpy.load(path)
restored
{
"$schema": "https://raw.githubusercontent.com/Computational-Biology-Aachen/mxl-schemas/main/v1/kinetic-model.schema.json",
"spec_version": "1.0",
"model_id": "glycolysis",
"description": "Minimal example",
"model": {
"variables": {
"A": {
"value": {
"type": "Num",
"value": 1.0
}
},
"B": {
"value": {
"type": "Num",
"value": 0.0
}
}
},
"parameters": {
"k1": {
"value": {
"type": "Num",
"value": 0.1
}
},
"k2": {
"value": {
"type": "Num",
"value": 0.05
}
}
},
"reactions": {
"r1": {
"fn": {
"type": "Mul",
"children": [
{
"type": "Name",
"value": "A"
},
{
"type": "Name",
"value": "k1"
}
]
},
"stoichiometry": {
"A": {
"type": "Num",
"value": -1.0
},
"B": {
"type": "Num",
"value": 1.0
}
}
}
},
"derived": {},
"readouts": {}
}
}
Model(
_variables={
'A': Variable(initial_value=1.0, annotations=[]),
'B': Variable(initial_value=0.0, annotations=[])
},
_parameters={
'k1': Parameter(value=0.1, annotations=[]),
'k2': Parameter(value=0.05, annotations=[])
},
_derived={},
_readouts={},
_reactions={
'r1':
Reaction(
fn=<function _mxl_fn>,
stoichiometry={'A': -1.0, 'B': 1.0},
args=['A', 'k1'],
annotations=[]
)
},
_surrogates={},
_data={},
_annotations=[]
)
Lossless round-trip¶
The format round-trips through SymPy, so the loaded model uses generated
rate functions (not the original named ones) but produces identical dynamics.
save -> load -> save also reaches a stable fixed point.
original = mxlpy.Simulator(model).simulate(50).get_result().unwrap_or_err()
loaded = mxlpy.Simulator(restored).simulate(50).get_result().unwrap_or_err()
(original.get_combined() - loaded.get_combined()).abs().max()
A 0.0 B 0.0 r1 0.0 dtype: float64
Notes¶
- The file references a
$schemafrom the sharedmxl-schemasrepository, giving editor validation and autocompletion for.mxl.jsonfiles. - Surrogates and rate functions that cannot be parsed into an expression raise
mxlpy.types.SerializationError. Use SBML (mxlpy.sbml) for those, or for cross-tool interchange. - The
spec_versionfield tracks the format version; it is shared with MxlWeb and decoupled from the MxlPy package version.