Model composition¶
Large models are easier to build, test and maintain when assembled from smaller, independently validated submodels — for example a metabolic core plus a separate regulatory layer.
mxlpy.compose merges two or more Model objects into a single new model. Components are identified by name across the whole namespace (variables, parameters, derived quantities, reactions, readouts, surrogates and data), and the input models are never mutated.
from mxlpy import compose
full_model = compose(submodel_a, submodel_b)
from __future__ import annotations
import matplotlib.pyplot as plt
from mxlpy import Model, Simulator, compose, fns, plot
Merging disjoint submodels¶
If two models share no names, compose simply returns a new model containing the union of all their components.
def production() -> Model:
return (
Model()
.add_parameter("k_in", 1.0)
.add_variable("x", 1.0)
.add_reaction("v_in", fns.constant, args=["k_in"], stoichiometry={"x": 1})
)
def independent() -> Model:
return (
Model()
.add_parameter("k2", 2.0)
.add_variable("y", 3.0)
.add_reaction("v2", fns.constant, args=["k2"], stoichiometry={"y": 1})
)
merged = compose(production(), independent())
sorted(merged.ids)
['k2', 'k_in', 'v2', 'v_in', 'x', 'y']
Name conflicts¶
By default any name defined in more than one model is treated as a conflict and raises a ValueError. This protects you from silently merging two unrelated components that happen to share a name.
def consumption() -> Model:
return (
Model()
.add_parameter("k_out", 1.0)
.add_variable("x", 1.0) # same name as in `production`
.add_reaction(
"v_out", fns.proportional, args=["k_out", "x"], stoichiometry={"x": -1}
)
)
try:
compose(production(), consumption())
except ValueError as e:
print(e)
Cannot compose models: duplicate names ['x']. Rename the conflicting components in one of the models, or pass raise_on_conflict=False to let the later model override them.
Sharing components on purpose¶
To deliberately connect submodels through a shared component — here the species x, produced by one submodel and consumed by the other — pass raise_on_conflict=False. A warning is emitted for each overridden name and the component from the later model wins.
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
model = compose(production(), consumption(), raise_on_conflict=False)
variables, _ = Simulator(model).simulate(10).get_result().unwrap_or_err()
fig, ax = plot.lines(variables)
ax.set(xlabel="time / a.u.", ylabel="concentration / a.u.")
plt.show()
The composed model behaves exactly like a hand-written one: x relaxes towards its steady state k_in / k_out = 1.0.
compose accepts any number of models (compose(a, b, c, ...)), folding them left-to-right, so you can stack several modules in a single call.