# MxlPy - Advanced Features

[← back to main reference](llms.txt)

## SBML import / export (`mxlpy.sbml`)

```python
from mxlpy import sbml

# Import an SBML file into a Model
model = sbml.read("path/to/model.xml")

# Export a Model to SBML
sbml.write(model, "path/to/output.xml")
```

---

## Native JSON format (`mxlpy.save` / `mxlpy.load`)

Version-controllable `.mxl.json` format that captures the full model structure
(variables, parameters, reactions, derived quantities, readouts). Rate
expressions are stored as trees of math nodes from the node set shared with
MxlWeb, so the same files can be consumed by both tools.

```python
import mxlpy

# Save a model to the native JSON format
mxlpy.save(model, "model.mxl.json")

# Load it back - all constructs restored, simulates identically
model = mxlpy.load("model.mxl.json")
```

- Rate functions go through SymPy, so a loaded model uses generated (not the
  original named) functions and is behaviourally identical. `save -> load ->
  save` reaches a stable fixed point.
- Optional metadata: `mxlpy.save(model, path, model_id="glycolysis", description="...")`.
- Raises `mxlpy.types.SerializationError` for surrogates or rate functions that
  cannot be parsed into an expression.
- Each file references a `$schema` (the shared `mxl-schemas` repo) for editor
  validation and autocompletion. Use SBML instead for cross-tool interchange.

---

## Model comparison (`mxlpy.compare`)

Compare simulations of two models side-by-side (e.g. before/after refactoring, or two hypotheses):

> Steady-state comparison

```python
from mxlpy import compare

cmp = compare.steady_states(model_a, model_b)
print(cmp.variables)   # pd.DataFrame: variable values from both models
print(cmp.fluxes)      # pd.DataFrame: flux values from both models
cmp.plot_variables()
cmp.plot_fluxes()
```

> Time-course comparison

```python
import numpy as np

time_points = np.linspace(0, 100, 300)
cmp = compare.time_courses(model_a, model_b, time_points=time_points)
cmp.plot_variables()
```

> Protocol comparison

```python
from mxlpy import make_protocol

protocol = make_protocol([(10, {"k1": 1.0}), (10, {"k1": 3.0})])
cmp = compare.protocol_time_courses(model_a, model_b, protocol=protocol)
cmp.plot_variables()
```

---

## Symbolic analysis (`mxlpy.symbolic`)

Convert a `Model` to a symbolic representation using SymPy for analytical inspection and identifiability analysis.

> Convert to symbolic model

```python
from mxlpy import to_symbolic_model

sym_model = to_symbolic_model(model)
# sym_model exposes SymPy expressions for ODEs and derived quantities
```

> Structural identifiability analysis (requires StrikeGOLDD)

```python
from mxlpy import symbolic

result = symbolic.check_identifiability(model, observed=["x", "y"])
```

---

## Neural network surrogates (`mxlpy.surrogates`, `mxlpy.nn`)

Surrogates replace expensive sub-models with trained neural networks while preserving the ODE interface.

> Add a pre-trained surrogate to a model

```python
# surrogate must implement SurrogateProtocol
model.add_surrogate(
    name="v_surrogate",
    surrogate=trained_surrogate,
    args=["s", "p", "vmax"],
    outputs=["v_fwd", "v_rev"],
    stoichiometries={"v_fwd": {"s": -1, "p": 1}, "v_rev": {"s": 1, "p": -1}},
)
```

> Update or remove a surrogate

```python
model.update_surrogate("v_surrogate", surrogate=new_surrogate, args=["s", "p"])
model.remove_surrogate("v_surrogate")
```

Available backends (lazy-loaded):
- `mxlpy.nn` - PyTorch (default), Keras, Equinox (JAX-based)
- `mxlpy.npe` - Neural Posterior Estimation (amortized Bayesian inference)

---

## Parallel execution and caching

Scans, MC, and fitting all support parallelization via `pebble` + `dill`. Provide a `Cache` object to persist results to disk.

```python
from mxlpy import scan, Cache, cartesian_product

cache = Cache(path="./results/scan_cache")
parameters = cartesian_product({"k1": [0.5, 1.0, 2.0], "k2": [0.1, 1.0]})

result = scan.steady_state(model, parameters=parameters, cache=cache)
# Second call with same parameters loads from disk instead of recomputing
```

---

## Readouts and initial assignments

> Readout: an observable quantity computed from variables/parameters but not part of the ODE

```python
from mxlpy.fns import add

model.add_readout("total", add, args=["x", "y"])
# Appears in simulation results alongside variables
```

> Initial assignment: compute initial conditions symbolically

```python
model.add_variables({"atp": 1.0, "adp": 0.0})
model.add_parameters({"adenine_total": 5.0})
# adp_0 = adenine_total - atp_0  evaluated once before integration
from mxlpy.fns import moiety_1s
model.add_initial_assignment("adp", moiety_1s, args=["atp", "adenine_total"])
```

---

## Utility functions

> Generate parameter combinations for scans

```python
from mxlpy import cartesian_product

# Returns pd.DataFrame with one row per combination
params = cartesian_product({
    "k1": [0.5, 1.0, 2.0],
    "k2": [0.1, 1.0, 5.0],
})
# 9 rows (3 × 3)
```

> Inspect model structure

```python
model.get_variable_names()         # list[str]
model.get_parameter_names()        # list[str]
model.get_reaction_names()         # list[str]
model.get_parameter_values()       # dict[str, float]
model.get_initial_conditions()     # dict[str, float]
model.get_unused_parameters()      # set[str] - parameters not referenced by any fn
model.get_stoichiometries()        # pd.DataFrame: reactions × variables
```

> Evaluate model at a point

```python
# RHS at current initial conditions and t=0
rhs = model.get_right_hand_side(model.get_initial_conditions(), time=0.0)

# All fluxes at given variable values
import pandas as pd
vars_df = pd.DataFrame({"x": [1.0, 0.8], "y": [0.5, 0.6]})
fluxes_df = model.get_fluxes(vars_df)
```

> `make_variable_static` / `make_parameter_dynamic`

```python
# Remove a variable from the ODE (clamp it to a fixed value)
model.make_variable_static("atp", value=2.0)

# Promote a parameter to a dynamic variable (add it to the ODE)
model.make_parameter_dynamic("k_enz", initial_value=1.0, stoichiometries={"v_synthesis": 1})
```
