from example_models import get_linear_chain_1v, get_linear_chain_2v
from mxlpy import meta
Metaprogramming¶
mxlpy can inspect the Python source of your model functions and transform them into other representations — whether that is runnable code in another language, a symbolic LaTeX description, or a diff between two model variants.
This works for any model whose rate and derived functions are pure Python (no numpy, no closures over mutable state). The source is parsed with Python's ast module and then re-emitted in the target language.
Use cases include:
- Sharing a model with collaborators who work in Rust, Julia, or TypeScript
- Generating a LaTeX methods section directly from code
- Tracking what changed between two model versions
Code generation¶
generate_mxlpy_code round-trips your model back into mxlpy builder syntax. This is useful for serialising a model that was constructed programmatically (e.g. after fitting or after reading from SBML) into a standalone Python file that can be shared or version-controlled without any runtime dependencies on the original construction logic.
print(meta.generate_mxlpy_code(get_linear_chain_1v()))
from mxlpy import Model
def constant(x: float) -> float:
return x
def mass_action_1s(s1: float, k: float) -> float:
return k * s1
def create_model() -> Model:
return (
Model()
.add_variable('x', initial_value=1.0)
.add_parameter('k_in', value=1.0)
.add_parameter('k_out', value=1.0)
.add_reaction(
'v_in',
fn=constant,
args=['k_in'],
stoichiometry={"x": 1},
)
.add_reaction(
'v_out',
fn=mass_action_1s,
args=['k_out', 'x'],
stoichiometry={"x": -1},
)
)
The generate_model_code_* family translates your model into a self-contained ODE function in the target language. Each function unpacks the variable vector, evaluates all derived quantities and rates inline, and returns the derivative vector — ready to hand to a native solver.
Currently supported targets: Python, Rust, TypeScript, Julia. The returned object exposes a .model attribute with the function source, plus .derived and .inits for the supporting code (see below).
print(meta.generate_model_code_py(get_linear_chain_2v()).model)
def model(time: float, variables: Iterable[float]) -> Iterable[float]:
x, y = variables
k2 = 2.0
k_in = 1.0
k_out = 1.0
v1 = k_in
v2 = k2*x
v3 = k_out*y
dxdt = 1.0*v1 - 1.0*v2
dydt = 1.0*v2 - 1.0*v3
return dxdt, dydt
print(meta.generate_model_code_rs(get_linear_chain_2v()).model)
fn model(time: f64, variables: &[f64; 2]) -> [f64; 2] {
let [x, y] = *variables;
let k2: f64 = 2.0;
let k_in: f64 = 1.0;
let k_out: f64 = 1.0;
let v1: f64 = k_in;
let v2: f64 = k2*x;
let v3: f64 = k_out*y;
let dxdt: f64 = 1.0*v1 - 1.0*v2;
let dydt: f64 = 1.0*v2 - 1.0*v3;
return [dxdt, dydt]
}
print(meta.generate_model_code_ts(get_linear_chain_2v()).model)
function model(time: number, variables: number[]): number[] {
const [x, y] = variables;
const k2: number = 2.0;
const k_in: number = 1.0;
const k_out: number = 1.0;
const v1: number = k_in;
const v2: number = k2*x;
const v3: number = k_out*y;
const dxdt: number = 1.0*v1 - 1.0*v2;
const dydt: number = 1.0*v2 - 1.0*v3;
return [dxdt, dydt];
};
print(meta.generate_model_code_jl(get_linear_chain_2v()).model)
function model(time, variables)
x, y = variables
k2 = 2.0
k_in = 1.0
k_out = 1.0
v1 = k_in
v2 = k2 .* x
v3 = k_out .* y
dxdt = 1.0 * v1 - 1.0 * v2
dydt = 1.0 * v2 - 1.0 * v3
return [dxdt, dydt]
end
Free parameters¶
By default, all parameters are baked into the generated function as constants. If you want to leave some parameters as inputs — for example to drive a parameter scan or a fitting loop in the target environment — pass their names as free_parameters. They will appear as additional function arguments instead of inlined literals.
Derived quantities and initial conditions¶
Beyond the ODE function itself, a complete simulation needs two more pieces: a function that computes derived quantities from the current state (observables, intermediate fluxes), and the initial values for all variables.
The codegen object exposes these as .derived and .inits respectively. Both are emitted in the same target language as .model, so they can be dropped directly into the same file.
codegen = meta.generate_model_code_py(get_linear_chain_2v(), free_parameters=["k_in"])
print(codegen.derived, end="\n\n")
print(codegen.inits)
def derived(time: float, variables: Iterable[float], k_in: float) -> Iterable[float]:
x, y = variables
k2 = 2.0
k_out = 1.0
v1 = k_in
v2 = k2*x
v3 = k_out*y
return v1, v2, v3
def inits(k_in: float) -> tuple[Iterable[float], Iterable[float]]:
x = 1.0
y = 1.0
return [x, y], []
Pruning derived quantities¶
By default, the generated .derived function computes every derived quantity in the model. If you only need a subset — for example, you want to export a stripped-down function that outputs a single flux for a downstream optimiser — pass derived_to_calculate with the names you actually need. Quantities not in the list are omitted from the output function, and any intermediate computations required only by those omitted quantities are pruned automatically.
codegen = meta.generate_model_code_py(
get_linear_chain_2v(), derived_to_calculate=["v2"]
)
print(codegen.derived, end="\n\n")
def derived(time: float, variables: Iterable[float]) -> Iterable[float]:
x, y = variables
k2 = 2.0
v2 = k2*x
return v2
LaTeX export¶
generate_model_code_latex produces a structured LaTeX representation of your model — parameters, variables, rate equations, and stoichiometry — formatted as tables ready to paste into a manuscript or supplementary material.
Two optional arguments let you customise the output:
gls— adictmapping Python names to LaTeX names (e.g.{"k_in": r"\k_{in}"). Names without an entry are used verbatim.long_name_cutoff— names longer than this threshold are automatically abbreviated to avoid overflowing table columns.
Call .as_tables() on the result to get the individual LaTeX table strings.
print(*meta.generate_model_code_latex(get_linear_chain_1v()).as_tables(), sep="\n\n")
\begin{longtable}{c|c}
Parameter name & Parameter value \\
\hline
\endhead
$\mathrm{k\_in}$ & $1.0$ \\
$\mathrm{k\_out}$ & $1.0$ \\
\caption[Model parameters]{Model parameters}
\label{table:model-pars}
\end{longtable}
\begin{longtable}{c|c}
Model name & Initial concentration \\
\hline
\endhead
$\mathrm{x}$ & $1.0$ \\
\caption[Model variables]{Model variables}
\label{table:model-vars}
\end{longtable}
\begin{longtable}{c|c}
Name & Reaction \\
\hline
\endhead
$\mathrm{v\_in}$ & $k_{in}$ \\
$\mathrm{v\_out}$ & $k_{out} \cdot x$ \\
\caption[Model parameters]{Model parameters}
\label{table:model-rxn}
\end{longtable}
\begin{longtable}{c|c}
Lhs & Rhs \\
\hline
\endhead
$\frac{d\ \mathrm{x}}{dt}$ & $1.0 \cdot v_{in} - 1.0 \cdot v_{out}$ \\
\caption[Model parameters]{Model parameters}
\label{table:model-eqs}
\end{longtable}
Exporting diffs¶
When you modify a model — changing a parameter value, swapping a rate law, adding a species — generate_latex_diff highlights exactly what changed between two model versions as a LaTeX table. Unchanged rows are shown in normal weight; added or modified rows are highlighted.
Pass only_changes=True to suppress unchanged rows entirely (useful when models are large and only a handful of values differ). With only_changes=False (shown below) every row is included, giving the full context alongside the highlighted changes.
print(
*meta.generate_latex_diff(
get_linear_chain_1v(),
get_linear_chain_1v().update_parameter("k_in", 2.0),
only_changes=False,
).as_default(),
sep="\n\n",
)
\begin{longtable}{c|c}
Parameter name & Parameter value \\
\hline
\endhead
$\mathrm{k\_in}$ & ${\color{red} \stkout{1.0}} {\color{green} 2.0}$ \\
$\mathrm{k\_out}$ & $1.0$ \\
\caption[Model parameters]{Model parameters}
\label{table:model-pars}
\end{longtable}
\begin{longtable}{c|c}
Model name & Initial concentration \\
\hline
\endhead
$\mathrm{x}$ & $1.0$ \\
\caption[Model variables]{Model variables}
\label{table:model-vars}
\end{longtable}
\begin{dmath*}
\mathrm{v\_in} = k_{in}
\end{dmath*}
\begin{dmath*}
\mathrm{v\_out} = k_{out} \cdot x
\end{dmath*}
\begin{align*}
\frac{d\ \mathrm{x}}{dt} &= 1.0 \cdot v_{in} - 1.0 \cdot v_{out}
\end{align*}
print(
*meta.generate_latex_diff(
get_linear_chain_1v(),
get_linear_chain_1v().update_parameter("k_in", 2.0),
only_changes=False,
).as_default(),
sep="\n\n",
)
\begin{longtable}{c|c}
Parameter name & Parameter value \\
\hline
\endhead
$\mathrm{k\_in}$ & ${\color{red} \stkout{1.0}} {\color{green} 2.0}$ \\
$\mathrm{k\_out}$ & $1.0$ \\
\caption[Model parameters]{Model parameters}
\label{table:model-pars}
\end{longtable}
\begin{longtable}{c|c}
Model name & Initial concentration \\
\hline
\endhead
$\mathrm{x}$ & $1.0$ \\
\caption[Model variables]{Model variables}
\label{table:model-vars}
\end{longtable}
\begin{dmath*}
\mathrm{v\_in} = k_{in}
\end{dmath*}
\begin{dmath*}
\mathrm{v\_out} = k_{out} \cdot x
\end{dmath*}
\begin{align*}
\frac{d\ \mathrm{x}}{dt} &= 1.0 \cdot v_{in} - 1.0 \cdot v_{out}
\end{align*}
Exporting a document¶
generate_latex_document wraps any codegen result — either a single model or a diff — in a complete, compilable LaTeX document with the necessary preamble and package imports. This lets you go from a Model object to a .tex file you can compile directly with pdflatex, with no manual formatting needed.
print(
meta.generate_latex_document(
meta.generate_model_code_latex(
get_linear_chain_1v(),
)
)
)
\documentclass[fleqn]{article}
\usepackage[english]{babel}
\usepackage[a4paper,top=2cm,bottom=2cm,left=2cm,right=2cm,marginparwidth=1.75cm]{geometry}
\usepackage{amsmath, amssymb, array, booktabs,
breqn, caption, longtable, mathtools, placeins,
ragged2e, tabularx, titlesec, titling, ulem, xcolor}
\newcommand{\stkout}[1]{\ifmmode\text{\sout{\ensuremath{#1}}}\else\sout{#1}\fi}
\newcommand{\sectionbreak}{\clearpage}
\setlength{\parindent}{0pt}
\allowdisplaybreaks
\title{Model construction}
\date{} % clear date
\author{mxlpy}
\begin{document}
\maketitle
\FloatBarrier\subsection*{Parameters}
\begin{longtable}{c|c}
Parameter name & Parameter value \\
\hline
\endhead
$\mathrm{k\_in}$ & $1.0$ \\
$\mathrm{k\_out}$ & $1.0$ \\
\caption[Model parameters]{Model parameters}
\label{table:model-pars}
\end{longtable}
\FloatBarrier\subsection*{Variables}
\begin{longtable}{c|c}
Model name & Initial concentration \\
\hline
\endhead
$\mathrm{x}$ & $1.0$ \\
\caption[Model variables]{Model variables}
\label{table:model-vars}
\end{longtable}
\FloatBarrier\subsection*{Reactions}
\begin{dmath*}
\mathrm{v\_in} = k_{in}
\end{dmath*}
\begin{dmath*}
\mathrm{v\_out} = k_{out} \cdot x
\end{dmath*}
\FloatBarrier\subsection*{Differential Equations}
\begin{align*}
\frac{d\ \mathrm{x}}{dt} &= 1.0 \cdot v_{in} - 1.0 \cdot v_{out}
\end{align*}
\end{document}
print(
meta.generate_latex_document(
meta.generate_latex_diff(
get_linear_chain_1v(),
get_linear_chain_1v().update_parameter("k_in", 2.0),
)
)
)
\documentclass[fleqn]{article}
\usepackage[english]{babel}
\usepackage[a4paper,top=2cm,bottom=2cm,left=2cm,right=2cm,marginparwidth=1.75cm]{geometry}
\usepackage{amsmath, amssymb, array, booktabs,
breqn, caption, longtable, mathtools, placeins,
ragged2e, tabularx, titlesec, titling, ulem, xcolor}
\newcommand{\stkout}[1]{\ifmmode\text{\sout{\ensuremath{#1}}}\else\sout{#1}\fi}
\newcommand{\sectionbreak}{\clearpage}
\setlength{\parindent}{0pt}
\allowdisplaybreaks
\title{Model construction}
\date{} % clear date
\author{mxlpy}
\begin{document}
\maketitle
\FloatBarrier\subsection*{Parameters}
\begin{longtable}{c|c}
Parameter name & Parameter value \\
\hline
\endhead
$\mathrm{k\_in}$ & ${\color{red} \stkout{1.0}} {\color{green} 2.0}$ \\
$\mathrm{k\_out}$ & $1.0$ \\
\caption[Model parameters]{Model parameters}
\label{table:model-pars}
\end{longtable}
\FloatBarrier\subsection*{Variables}
\begin{longtable}{c|c}
Model name & Initial concentration \\
\hline
\endhead
$\mathrm{x}$ & $1.0$ \\
\caption[Model variables]{Model variables}
\label{table:model-vars}
\end{longtable}
\FloatBarrier\subsection*{Reactions}
\begin{dmath*}
\mathrm{v\_in} = k_{in}
\end{dmath*}
\begin{dmath*}
\mathrm{v\_out} = k_{out} \cdot x
\end{dmath*}
\FloatBarrier\subsection*{Differential Equations}
\begin{align*}
\frac{d\ \mathrm{x}}{dt} &= 1.0 \cdot v_{in} - 1.0 \cdot v_{out}
\end{align*}
\end{document}
First finish line
With that you now know most of what you will need from a day-to-day basis about meta programming in mxlpy.Congratulations!
Placeholders / error handling¶
If a model function uses constructs that mxlpy cannot translate - such as numpy calls, comprehensions, or external library calls - the LaTeX exporter inserts a red warning placeholder in place of that equation. The rest of the export is unaffected, so you still get a usable document for the parseable parts of your model.
import numpy as np
from mxlpy import Model
def broken_fn() -> float:
return np.sum(np.linalg.inv(np.array([[1.0, 2.0], [3.0, 4.0]]))) # type: ignore
print(
meta.generate_model_code_latex(
Model().add_derived("d1", broken_fn, args=[])
).as_default()
)
WARNING:mxlpy.meta.source_tools:Failed parsing function of d1
WARNING:root:Unable to parse fn for 'd1'
('', '', '\\begin{dmath*}\n \\mathrm{d1} = \\text{NaN}\n\\end{dmath*}', '', '')
Code generation targets (Python, Rust, etc.) are stricter: they raise a ValueError rather than inserting a placeholder. Silent bad output is worse than a loud error when the generated function is part of a larger pipeline — a placeholder that compiles but computes nonsense would be very hard to debug downstream.
try:
meta.generate_model_code_py(Model().add_derived("d1", broken_fn, args=[]))
except ValueError as e:
print("Errored:", e)
WARNING:mxlpy.meta.source_tools:Failed parsing function of d1
Errored: Unable to parse fn for 'd1'
try:
meta.generate_model_code_rs(Model().add_derived("d1", broken_fn, args=[]))
except ValueError as e:
print("Errored:", e)
WARNING:mxlpy.meta.source_tools:Failed parsing function of d1
Errored: Unable to parse fn for 'd1'