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.
@@ -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))