mxlpy 0.21.0__py3-none-any.whl → 0.23.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mxlpy/__init__.py +15 -2
- mxlpy/carousel.py +6 -4
- mxlpy/compare.py +4 -8
- mxlpy/experimental/diff.py +1 -1
- mxlpy/fit.py +195 -99
- mxlpy/identify.py +14 -9
- mxlpy/integrators/__init__.py +4 -0
- mxlpy/integrators/int_assimulo.py +3 -3
- mxlpy/integrators/int_diffrax.py +119 -0
- mxlpy/integrators/int_scipy.py +15 -6
- mxlpy/label_map.py +6 -7
- mxlpy/linear_label_map.py +3 -1
- mxlpy/mc.py +25 -22
- mxlpy/mca.py +10 -6
- mxlpy/meta/__init__.py +5 -3
- mxlpy/meta/codegen_latex.py +44 -30
- mxlpy/meta/codegen_model.py +175 -0
- mxlpy/meta/codegen_mxlpy.py +254 -0
- mxlpy/meta/source_tools.py +506 -221
- mxlpy/meta/sympy_tools.py +117 -0
- mxlpy/model.py +758 -257
- mxlpy/plot.py +16 -14
- mxlpy/report.py +153 -90
- mxlpy/sbml/_export.py +22 -11
- mxlpy/sbml/_import.py +68 -547
- mxlpy/scan.py +39 -243
- mxlpy/simulator.py +109 -283
- mxlpy/symbolic/symbolic_model.py +29 -17
- mxlpy/types.py +694 -97
- mxlpy/units.py +133 -0
- {mxlpy-0.21.0.dist-info → mxlpy-0.23.0.dist-info}/METADATA +4 -1
- mxlpy-0.23.0.dist-info/RECORD +57 -0
- mxlpy/meta/codegen_modebase.py +0 -112
- mxlpy/meta/codegen_py.py +0 -115
- mxlpy/sbml/_mathml.py +0 -692
- mxlpy/sbml/_unit_conversion.py +0 -74
- mxlpy-0.21.0.dist-info/RECORD +0 -56
- {mxlpy-0.21.0.dist-info → mxlpy-0.23.0.dist-info}/WHEEL +0 -0
- {mxlpy-0.21.0.dist-info → mxlpy-0.23.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,175 @@
|
|
1
|
+
"""Module to export models as code."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from typing import TYPE_CHECKING
|
7
|
+
|
8
|
+
from mxlpy.meta.sympy_tools import (
|
9
|
+
fn_to_sympy,
|
10
|
+
list_of_symbols,
|
11
|
+
stoichiometries_to_sympy,
|
12
|
+
sympy_to_inline_py,
|
13
|
+
sympy_to_inline_rust,
|
14
|
+
)
|
15
|
+
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
from collections.abc import Callable
|
18
|
+
|
19
|
+
import sympy
|
20
|
+
|
21
|
+
from mxlpy.model import Model
|
22
|
+
|
23
|
+
__all__ = [
|
24
|
+
"generate_model_code_py",
|
25
|
+
"generate_model_code_rs",
|
26
|
+
]
|
27
|
+
|
28
|
+
_LOGGER = logging.getLogger(__name__)
|
29
|
+
|
30
|
+
|
31
|
+
def _generate_model_code(
|
32
|
+
model: Model,
|
33
|
+
*,
|
34
|
+
sized: bool,
|
35
|
+
model_fn: str,
|
36
|
+
variables_template: str,
|
37
|
+
assignment_template: str,
|
38
|
+
sympy_inline_fn: Callable[[sympy.Expr], str],
|
39
|
+
return_template: str,
|
40
|
+
imports: list[str] | None = None,
|
41
|
+
end: str | None = None,
|
42
|
+
free_parameters: list[str] | None = None,
|
43
|
+
) -> str:
|
44
|
+
source: list[str] = []
|
45
|
+
# Model components
|
46
|
+
variables = model.get_initial_conditions()
|
47
|
+
parameters = model.get_parameter_values()
|
48
|
+
|
49
|
+
if imports is not None:
|
50
|
+
source.extend(imports)
|
51
|
+
|
52
|
+
if not sized:
|
53
|
+
source.append(model_fn)
|
54
|
+
else:
|
55
|
+
source.append(model_fn.format(n=len(variables)))
|
56
|
+
|
57
|
+
if len(variables) > 0:
|
58
|
+
source.append(variables_template.format(", ".join(variables)))
|
59
|
+
|
60
|
+
# Parameters
|
61
|
+
if free_parameters is not None:
|
62
|
+
for key in free_parameters:
|
63
|
+
parameters.pop(key)
|
64
|
+
if len(parameters) > 0:
|
65
|
+
source.append(
|
66
|
+
"\n".join(
|
67
|
+
assignment_template.format(k=k, v=v) for k, v in parameters.items()
|
68
|
+
)
|
69
|
+
)
|
70
|
+
|
71
|
+
# Derived
|
72
|
+
for name, derived in model.get_raw_derived().items():
|
73
|
+
expr = fn_to_sympy(
|
74
|
+
derived.fn,
|
75
|
+
origin=name,
|
76
|
+
model_args=list_of_symbols(derived.args),
|
77
|
+
)
|
78
|
+
if expr is None:
|
79
|
+
msg = f"Unable to parse fn for derived value '{name}'"
|
80
|
+
raise ValueError(msg)
|
81
|
+
source.append(assignment_template.format(k=name, v=sympy_inline_fn(expr)))
|
82
|
+
|
83
|
+
# Reactions
|
84
|
+
for name, rxn in model.get_raw_reactions().items():
|
85
|
+
expr = fn_to_sympy(
|
86
|
+
rxn.fn,
|
87
|
+
origin=name,
|
88
|
+
model_args=list_of_symbols(rxn.args),
|
89
|
+
)
|
90
|
+
if expr is None:
|
91
|
+
msg = f"Unable to parse fn for reaction value '{name}'"
|
92
|
+
raise ValueError(msg)
|
93
|
+
source.append(assignment_template.format(k=name, v=sympy_inline_fn(expr)))
|
94
|
+
|
95
|
+
# Diff eqs
|
96
|
+
diff_eqs = {}
|
97
|
+
for rxn_name, rxn in model.get_raw_reactions().items():
|
98
|
+
for var_name, factor in rxn.stoichiometry.items():
|
99
|
+
diff_eqs.setdefault(var_name, {})[rxn_name] = factor
|
100
|
+
|
101
|
+
for variable, stoich in diff_eqs.items():
|
102
|
+
expr = stoichiometries_to_sympy(origin=variable, stoichs=stoich)
|
103
|
+
source.append(
|
104
|
+
assignment_template.format(k=f"d{variable}dt", v=sympy_inline_fn(expr))
|
105
|
+
)
|
106
|
+
|
107
|
+
# Surrogates
|
108
|
+
if len(model._surrogates) > 0: # noqa: SLF001
|
109
|
+
msg = "Generating code for Surrogates not yet supported."
|
110
|
+
_LOGGER.warning(msg)
|
111
|
+
|
112
|
+
# Return
|
113
|
+
ret_order = [i for i in variables if i in diff_eqs]
|
114
|
+
ret = ", ".join(f"d{i}dt" for i in ret_order) if len(diff_eqs) > 0 else "()"
|
115
|
+
source.append(return_template.format(ret))
|
116
|
+
|
117
|
+
if end is not None:
|
118
|
+
source.append(end)
|
119
|
+
|
120
|
+
# print(source)
|
121
|
+
return "\n".join(source)
|
122
|
+
|
123
|
+
|
124
|
+
def generate_model_code_py(
|
125
|
+
model: Model,
|
126
|
+
free_parameters: list[str] | None = None,
|
127
|
+
) -> str:
|
128
|
+
"""Transform the model into a python function, inlining the function calls."""
|
129
|
+
if free_parameters is None:
|
130
|
+
model_fn = (
|
131
|
+
"def model(time: float, variables: Iterable[float]) -> Iterable[float]:"
|
132
|
+
)
|
133
|
+
else:
|
134
|
+
args = ", ".join(f"{k}: float" for k in free_parameters)
|
135
|
+
model_fn = f"def model(time: float, variables: Iterable[float], {args}) -> Iterable[float]:"
|
136
|
+
|
137
|
+
return _generate_model_code(
|
138
|
+
model,
|
139
|
+
imports=[
|
140
|
+
"from collections.abc import Iterable\n",
|
141
|
+
],
|
142
|
+
sized=False,
|
143
|
+
model_fn=model_fn,
|
144
|
+
variables_template=" {} = variables",
|
145
|
+
assignment_template=" {k} = {v}",
|
146
|
+
sympy_inline_fn=sympy_to_inline_py,
|
147
|
+
return_template=" return {}",
|
148
|
+
end=None,
|
149
|
+
free_parameters=free_parameters,
|
150
|
+
)
|
151
|
+
|
152
|
+
|
153
|
+
def generate_model_code_rs(
|
154
|
+
model: Model,
|
155
|
+
free_parameters: list[str] | None = None,
|
156
|
+
) -> str:
|
157
|
+
"""Transform the model into a rust function, inlining the function calls."""
|
158
|
+
if free_parameters is None:
|
159
|
+
model_fn = "fn model(time: f64, variables: &[f64; {n}]) -> [f64; {n}] {{"
|
160
|
+
else:
|
161
|
+
args = ", ".join(f"{k}: f64" for k in free_parameters)
|
162
|
+
model_fn = f"fn model(time: f64, variables: &[f64; {{n}}], {args}) -> [f64; {{n}}] {{{{"
|
163
|
+
|
164
|
+
return _generate_model_code(
|
165
|
+
model,
|
166
|
+
imports=None,
|
167
|
+
sized=True,
|
168
|
+
model_fn=model_fn,
|
169
|
+
variables_template=" let [{}] = *variables;",
|
170
|
+
assignment_template=" let {k}: f64 = {v};",
|
171
|
+
sympy_inline_fn=sympy_to_inline_rust,
|
172
|
+
return_template=" return [{}]",
|
173
|
+
end="}",
|
174
|
+
free_parameters=free_parameters,
|
175
|
+
)
|
@@ -0,0 +1,254 @@
|
|
1
|
+
"""Generate mxlpy code from a model."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from dataclasses import dataclass, field
|
7
|
+
from typing import TYPE_CHECKING, cast
|
8
|
+
|
9
|
+
import sympy
|
10
|
+
|
11
|
+
from mxlpy.meta.sympy_tools import (
|
12
|
+
fn_to_sympy,
|
13
|
+
list_of_symbols,
|
14
|
+
sympy_to_inline_py,
|
15
|
+
sympy_to_python_fn,
|
16
|
+
)
|
17
|
+
from mxlpy.types import Derived, InitialAssignment
|
18
|
+
from mxlpy.units import Quantity
|
19
|
+
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from collections.abc import Callable
|
22
|
+
|
23
|
+
from mxlpy.model import Model
|
24
|
+
|
25
|
+
__all__ = [
|
26
|
+
"SymbolicFn",
|
27
|
+
"SymbolicParameter",
|
28
|
+
"SymbolicReaction",
|
29
|
+
"SymbolicRepr",
|
30
|
+
"SymbolicVariable",
|
31
|
+
"generate_mxlpy_code",
|
32
|
+
"generate_mxlpy_code_from_symbolic_repr",
|
33
|
+
]
|
34
|
+
|
35
|
+
_LOGGER = logging.getLogger()
|
36
|
+
|
37
|
+
|
38
|
+
@dataclass
|
39
|
+
class SymbolicFn:
|
40
|
+
"""Container for symbolic fn."""
|
41
|
+
|
42
|
+
fn_name: str
|
43
|
+
expr: sympy.Expr
|
44
|
+
args: list[str]
|
45
|
+
|
46
|
+
|
47
|
+
@dataclass
|
48
|
+
class SymbolicVariable:
|
49
|
+
"""Container for symbolic variable."""
|
50
|
+
|
51
|
+
value: sympy.Float | SymbolicFn # initial assignment
|
52
|
+
unit: Quantity | None
|
53
|
+
|
54
|
+
|
55
|
+
@dataclass
|
56
|
+
class SymbolicParameter:
|
57
|
+
"""Container for symbolic par."""
|
58
|
+
|
59
|
+
value: sympy.Float | SymbolicFn # initial assignment
|
60
|
+
unit: Quantity | None
|
61
|
+
|
62
|
+
|
63
|
+
@dataclass
|
64
|
+
class SymbolicReaction:
|
65
|
+
"""Container for symbolic rxn."""
|
66
|
+
|
67
|
+
fn: SymbolicFn
|
68
|
+
stoichiometry: dict[str, sympy.Float | str | SymbolicFn]
|
69
|
+
|
70
|
+
|
71
|
+
@dataclass
|
72
|
+
class SymbolicRepr:
|
73
|
+
"""Container for symbolic model."""
|
74
|
+
|
75
|
+
variables: dict[str, SymbolicVariable] = field(default_factory=dict)
|
76
|
+
parameters: dict[str, SymbolicParameter] = field(default_factory=dict)
|
77
|
+
derived: dict[str, SymbolicFn] = field(default_factory=dict)
|
78
|
+
reactions: dict[str, SymbolicReaction] = field(default_factory=dict)
|
79
|
+
|
80
|
+
|
81
|
+
def _fn_to_symbolic_repr(k: str, fn: Callable, model_args: list[str]) -> SymbolicFn:
|
82
|
+
fn_name = fn.__name__
|
83
|
+
args = cast(list, list_of_symbols(model_args))
|
84
|
+
if (expr := fn_to_sympy(fn, origin=k, model_args=args)) is None:
|
85
|
+
msg = f"Unable to parse fn for '{k}'"
|
86
|
+
raise ValueError(msg)
|
87
|
+
return SymbolicFn(fn_name=fn_name, expr=expr, args=model_args)
|
88
|
+
|
89
|
+
|
90
|
+
def _to_symbolic_repr(model: Model) -> SymbolicRepr:
|
91
|
+
sym = SymbolicRepr()
|
92
|
+
|
93
|
+
for k, variable in model.get_raw_variables().items():
|
94
|
+
sym.variables[k] = SymbolicVariable(
|
95
|
+
value=_fn_to_symbolic_repr(k, val.fn, val.args)
|
96
|
+
if isinstance(val := variable.initial_value, InitialAssignment)
|
97
|
+
else sympy.Float(val),
|
98
|
+
unit=cast(Quantity, variable.unit),
|
99
|
+
)
|
100
|
+
|
101
|
+
for k, parameter in model.get_raw_parameters().items():
|
102
|
+
sym.parameters[k] = SymbolicParameter(
|
103
|
+
value=_fn_to_symbolic_repr(k, val.fn, val.args)
|
104
|
+
if isinstance(val := parameter.value, InitialAssignment)
|
105
|
+
else sympy.Float(val),
|
106
|
+
unit=cast(Quantity, parameter.unit),
|
107
|
+
)
|
108
|
+
|
109
|
+
for k, der in model.get_raw_derived().items():
|
110
|
+
sym.derived[k] = _fn_to_symbolic_repr(k, der.fn, der.args)
|
111
|
+
|
112
|
+
for k, rxn in model.get_raw_reactions().items():
|
113
|
+
sym.reactions[k] = SymbolicReaction(
|
114
|
+
fn=_fn_to_symbolic_repr(k, rxn.fn, rxn.args),
|
115
|
+
stoichiometry={
|
116
|
+
k: _fn_to_symbolic_repr(k, v.fn, v.args)
|
117
|
+
if isinstance(v, Derived)
|
118
|
+
else sympy.Float(v)
|
119
|
+
for k, v in rxn.stoichiometry.items()
|
120
|
+
},
|
121
|
+
)
|
122
|
+
|
123
|
+
if len(model._surrogates) > 0: # noqa: SLF001
|
124
|
+
msg = "Generating code for Surrogates not yet supported."
|
125
|
+
_LOGGER.warning(msg)
|
126
|
+
return sym
|
127
|
+
|
128
|
+
|
129
|
+
def _codegen_variable(
|
130
|
+
k: str, var: SymbolicVariable, functions: dict[str, tuple[sympy.Expr, list[str]]]
|
131
|
+
) -> str:
|
132
|
+
if isinstance(init := var.value, SymbolicFn):
|
133
|
+
fn_name = f"init_{init.fn_name}"
|
134
|
+
functions[fn_name] = (init.expr, init.args)
|
135
|
+
return f""" .add_variable(
|
136
|
+
{k!r},
|
137
|
+
initial_value=InitialAssignment(fn={fn_name}, args={init.args!r}),
|
138
|
+
)"""
|
139
|
+
|
140
|
+
value = sympy_to_inline_py(init)
|
141
|
+
if (unit := var.unit) is not None:
|
142
|
+
return f" .add_variable({k!r}, value={value}, unit={sympy_to_inline_py(unit)})"
|
143
|
+
return f" .add_variable({k!r}, initial_value={value})"
|
144
|
+
|
145
|
+
|
146
|
+
def _codegen_parameter(
|
147
|
+
k: str, par: SymbolicParameter, functions: dict[str, tuple[sympy.Expr, list[str]]]
|
148
|
+
) -> str:
|
149
|
+
if isinstance(init := par.value, SymbolicFn):
|
150
|
+
fn_name = f"init_{init.fn_name}"
|
151
|
+
functions[fn_name] = (init.expr, init.args)
|
152
|
+
return f""" .add_parameter(
|
153
|
+
{k!r},
|
154
|
+
value=InitialAssignment(fn={fn_name}, args={init.args!r}),
|
155
|
+
)"""
|
156
|
+
|
157
|
+
value = sympy_to_inline_py(init)
|
158
|
+
if (unit := par.unit) is not None:
|
159
|
+
return f" .add_parameter({k!r}, value={value}, unit={sympy_to_inline_py(unit)})"
|
160
|
+
return f" .add_parameter({k!r}, value={value})"
|
161
|
+
|
162
|
+
|
163
|
+
def generate_mxlpy_code_from_symbolic_repr(
|
164
|
+
model: SymbolicRepr, imports: list[str] | None = None
|
165
|
+
) -> str:
|
166
|
+
"""Generate MxlPy source code from symbolic representation.
|
167
|
+
|
168
|
+
This is both used by MxlPy internally to codegen an existing model again and by the
|
169
|
+
SBML import to generate the file.
|
170
|
+
"""
|
171
|
+
imports = [] if imports is None else imports
|
172
|
+
|
173
|
+
functions: dict[str, tuple[sympy.Expr, list[str]]] = {}
|
174
|
+
|
175
|
+
# Variables
|
176
|
+
variable_source = []
|
177
|
+
for k, var in model.variables.items():
|
178
|
+
variable_source.append(_codegen_variable(k, var, functions=functions))
|
179
|
+
|
180
|
+
# Parameters
|
181
|
+
parameter_source = []
|
182
|
+
for k, par in model.parameters.items():
|
183
|
+
parameter_source.append(_codegen_parameter(k, par, functions=functions))
|
184
|
+
|
185
|
+
# Derived
|
186
|
+
derived_source = []
|
187
|
+
for k, fn in model.derived.items():
|
188
|
+
functions[fn.fn_name] = (fn.expr, fn.args)
|
189
|
+
derived_source.append(
|
190
|
+
f""" .add_derived(
|
191
|
+
{k!r},
|
192
|
+
fn={fn.fn_name},
|
193
|
+
args={fn.args},
|
194
|
+
)"""
|
195
|
+
)
|
196
|
+
|
197
|
+
# Reactions
|
198
|
+
reactions_source = []
|
199
|
+
for k, rxn in model.reactions.items():
|
200
|
+
fn = rxn.fn
|
201
|
+
functions[fn.fn_name] = (fn.expr, fn.args)
|
202
|
+
|
203
|
+
stoichiometry: list[str] = []
|
204
|
+
for var, stoich in rxn.stoichiometry.items():
|
205
|
+
if isinstance(stoich, SymbolicFn):
|
206
|
+
fn_name = f"{k}_stoich_{stoich.fn_name}"
|
207
|
+
functions[fn_name] = (stoich.expr, stoich.args)
|
208
|
+
stoichiometry.append(
|
209
|
+
f""""{var}": Derived(fn={fn_name}, args={stoich.args!r})"""
|
210
|
+
)
|
211
|
+
elif isinstance(stoich, str):
|
212
|
+
stoichiometry.append(f""""{var}": {stoich!r}""")
|
213
|
+
else:
|
214
|
+
stoichiometry.append(f""""{var}": {sympy_to_inline_py(stoich)}""")
|
215
|
+
reactions_source.append(
|
216
|
+
f""" .add_reaction(
|
217
|
+
"{k}",
|
218
|
+
fn={fn.fn_name},
|
219
|
+
args={fn.args},
|
220
|
+
stoichiometry={{{",".join(stoichiometry)}}},
|
221
|
+
)"""
|
222
|
+
)
|
223
|
+
|
224
|
+
# Surrogates
|
225
|
+
|
226
|
+
# Combine all the sources
|
227
|
+
functions_source = "\n\n".join(
|
228
|
+
sympy_to_python_fn(fn_name=name, args=args, expr=expr)
|
229
|
+
for name, (expr, args) in functions.items()
|
230
|
+
)
|
231
|
+
source = [
|
232
|
+
*imports,
|
233
|
+
"from mxlpy import Model, Derived, InitialAssignment\n",
|
234
|
+
functions_source,
|
235
|
+
"",
|
236
|
+
"def create_model() -> Model:",
|
237
|
+
" return (",
|
238
|
+
" Model()",
|
239
|
+
]
|
240
|
+
if len(variable_source) > 0:
|
241
|
+
source.append("\n".join(variable_source))
|
242
|
+
if len(parameter_source) > 0:
|
243
|
+
source.append("\n".join(parameter_source))
|
244
|
+
if len(derived_source) > 0:
|
245
|
+
source.append("\n".join(derived_source))
|
246
|
+
if len(reactions_source) > 0:
|
247
|
+
source.append("\n".join(reactions_source))
|
248
|
+
source.append(" )")
|
249
|
+
return "\n".join(source)
|
250
|
+
|
251
|
+
|
252
|
+
def generate_mxlpy_code(model: Model) -> str:
|
253
|
+
"""Generate a mxlpy model from a model."""
|
254
|
+
return generate_mxlpy_code_from_symbolic_repr(_to_symbolic_repr(model))
|