mxlpy 0.8.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 +165 -0
- mxlpy/distributions.py +339 -0
- mxlpy/experimental/__init__.py +12 -0
- mxlpy/experimental/diff.py +226 -0
- mxlpy/fit.py +291 -0
- mxlpy/fns.py +191 -0
- mxlpy/integrators/__init__.py +19 -0
- mxlpy/integrators/int_assimulo.py +146 -0
- mxlpy/integrators/int_scipy.py +146 -0
- mxlpy/label_map.py +610 -0
- mxlpy/linear_label_map.py +303 -0
- mxlpy/mc.py +548 -0
- mxlpy/mca.py +280 -0
- mxlpy/meta/__init__.py +11 -0
- mxlpy/meta/codegen_latex.py +516 -0
- mxlpy/meta/codegen_modebase.py +110 -0
- mxlpy/meta/codegen_py.py +107 -0
- mxlpy/meta/source_tools.py +320 -0
- mxlpy/model.py +1737 -0
- mxlpy/nn/__init__.py +10 -0
- mxlpy/nn/_tensorflow.py +0 -0
- mxlpy/nn/_torch.py +129 -0
- mxlpy/npe.py +277 -0
- mxlpy/parallel.py +171 -0
- mxlpy/parameterise.py +27 -0
- mxlpy/paths.py +36 -0
- mxlpy/plot.py +875 -0
- mxlpy/py.typed +0 -0
- mxlpy/sbml/__init__.py +14 -0
- mxlpy/sbml/_data.py +77 -0
- mxlpy/sbml/_export.py +644 -0
- mxlpy/sbml/_import.py +599 -0
- mxlpy/sbml/_mathml.py +691 -0
- mxlpy/sbml/_name_conversion.py +52 -0
- mxlpy/sbml/_unit_conversion.py +74 -0
- mxlpy/scan.py +629 -0
- mxlpy/simulator.py +655 -0
- mxlpy/surrogates/__init__.py +31 -0
- mxlpy/surrogates/_poly.py +97 -0
- mxlpy/surrogates/_torch.py +196 -0
- mxlpy/symbolic/__init__.py +10 -0
- mxlpy/symbolic/strikepy.py +582 -0
- mxlpy/symbolic/symbolic_model.py +75 -0
- mxlpy/types.py +474 -0
- mxlpy-0.8.0.dist-info/METADATA +106 -0
- mxlpy-0.8.0.dist-info/RECORD +48 -0
- mxlpy-0.8.0.dist-info/WHEEL +4 -0
- mxlpy-0.8.0.dist-info/licenses/LICENSE +674 -0
mxlpy/meta/codegen_py.py
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
"""Module to export models as code."""
|
2
|
+
|
3
|
+
import warnings
|
4
|
+
from collections.abc import Callable, Generator, Iterable, Iterator
|
5
|
+
|
6
|
+
import sympy
|
7
|
+
|
8
|
+
from mxlpy.meta.source_tools import fn_to_sympy, sympy_to_inline
|
9
|
+
from mxlpy.model import Model
|
10
|
+
from mxlpy.types import Derived
|
11
|
+
|
12
|
+
__all__ = ["generate_model_code_py"]
|
13
|
+
|
14
|
+
|
15
|
+
def _conditional_join[T](
|
16
|
+
iterable: Iterable[T],
|
17
|
+
question: Callable[[T], bool],
|
18
|
+
true_pat: str,
|
19
|
+
false_pat: str,
|
20
|
+
) -> str:
|
21
|
+
"""Join an iterable, applying a pattern to each element based on a condition."""
|
22
|
+
|
23
|
+
def inner(it: Iterator[T]) -> Generator[str, None, None]:
|
24
|
+
yield str(next(it))
|
25
|
+
while True:
|
26
|
+
try:
|
27
|
+
el = next(it)
|
28
|
+
if question(el):
|
29
|
+
yield f"{true_pat}{el}"
|
30
|
+
else:
|
31
|
+
yield f"{false_pat}{el}"
|
32
|
+
except StopIteration:
|
33
|
+
break
|
34
|
+
|
35
|
+
return "".join(inner(iter(iterable)))
|
36
|
+
|
37
|
+
|
38
|
+
def _list_of_symbols(args: list[str]) -> list[sympy.Symbol | sympy.Expr]:
|
39
|
+
return [sympy.Symbol(arg) for arg in args]
|
40
|
+
|
41
|
+
|
42
|
+
# FIXME: generate from SymbolicModel, should be easier?
|
43
|
+
def generate_model_code_py(model: Model) -> str:
|
44
|
+
"""Transform the model into a single function, inlining the function calls."""
|
45
|
+
source = [
|
46
|
+
"from collections.abc import Iterable\n",
|
47
|
+
"from mxlpy.types import Float\n",
|
48
|
+
"def model(t: Float, variables: Float) -> Iterable[Float]:",
|
49
|
+
]
|
50
|
+
|
51
|
+
# Variables
|
52
|
+
variables = model.variables
|
53
|
+
if len(variables) > 0:
|
54
|
+
source.append(" {} = variables".format(", ".join(variables)))
|
55
|
+
|
56
|
+
# Parameters
|
57
|
+
parameters = model.parameters
|
58
|
+
if len(parameters) > 0:
|
59
|
+
source.append("\n".join(f" {k} = {v}" for k, v in model.parameters.items()))
|
60
|
+
|
61
|
+
# Derived
|
62
|
+
for name, derived in model.derived.items():
|
63
|
+
expr = fn_to_sympy(derived.fn, model_args=_list_of_symbols(derived.args))
|
64
|
+
source.append(f" {name} = {sympy_to_inline(expr)}")
|
65
|
+
|
66
|
+
# Reactions
|
67
|
+
for name, rxn in model.reactions.items():
|
68
|
+
expr = fn_to_sympy(rxn.fn, model_args=_list_of_symbols(rxn.args))
|
69
|
+
source.append(f" {name} = {sympy_to_inline(expr)}")
|
70
|
+
|
71
|
+
# Stoichiometries; FIXME: do this with sympy instead as well?
|
72
|
+
stoich_srcs = {}
|
73
|
+
for rxn_name, rxn in model.reactions.items():
|
74
|
+
for i, (cpd_name, factor) in enumerate(rxn.stoichiometry.items()):
|
75
|
+
if isinstance(factor, Derived):
|
76
|
+
expr = fn_to_sympy(factor.fn, model_args=_list_of_symbols(factor.args))
|
77
|
+
src = f"{sympy_to_inline(expr)} * {rxn_name}"
|
78
|
+
elif factor == 1:
|
79
|
+
src = rxn_name
|
80
|
+
elif factor == -1:
|
81
|
+
src = f"-{rxn_name}" if i == 0 else f"- {rxn_name}"
|
82
|
+
else:
|
83
|
+
src = f"{factor} * {rxn_name}"
|
84
|
+
stoich_srcs.setdefault(cpd_name, []).append(src)
|
85
|
+
for variable, stoich in stoich_srcs.items():
|
86
|
+
source.append(
|
87
|
+
f" d{variable}dt = {_conditional_join(stoich, lambda x: x.startswith('-'), ' ', ' + ')}"
|
88
|
+
)
|
89
|
+
|
90
|
+
# Surrogates
|
91
|
+
if len(model._surrogates) > 0: # noqa: SLF001
|
92
|
+
warnings.warn(
|
93
|
+
"Generating code for Surrogates not yet supported.",
|
94
|
+
stacklevel=1,
|
95
|
+
)
|
96
|
+
|
97
|
+
# Return
|
98
|
+
if len(variables) > 0:
|
99
|
+
source.append(
|
100
|
+
" return {}".format(
|
101
|
+
", ".join(f"d{i}dt" for i in variables),
|
102
|
+
),
|
103
|
+
)
|
104
|
+
else:
|
105
|
+
source.append(" return ()")
|
106
|
+
|
107
|
+
return "\n".join(source)
|
@@ -0,0 +1,320 @@
|
|
1
|
+
"""Tools for working with python source files."""
|
2
|
+
|
3
|
+
import ast
|
4
|
+
import inspect
|
5
|
+
import textwrap
|
6
|
+
from collections.abc import Callable
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from types import ModuleType
|
9
|
+
from typing import Any, cast
|
10
|
+
|
11
|
+
import dill
|
12
|
+
import sympy
|
13
|
+
from sympy.printing.pycode import pycode
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"Context",
|
17
|
+
"fn_to_sympy",
|
18
|
+
"get_fn_ast",
|
19
|
+
"get_fn_source",
|
20
|
+
"sympy_to_fn",
|
21
|
+
"sympy_to_inline",
|
22
|
+
]
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class Context:
|
27
|
+
symbols: dict[str, sympy.Symbol | sympy.Expr]
|
28
|
+
caller: Callable
|
29
|
+
parent_module: ModuleType | None
|
30
|
+
|
31
|
+
def updated(
|
32
|
+
self,
|
33
|
+
symbols: dict[str, sympy.Symbol | sympy.Expr] | None = None,
|
34
|
+
caller: Callable | None = None,
|
35
|
+
parent_module: ModuleType | None = None,
|
36
|
+
) -> "Context":
|
37
|
+
return Context(
|
38
|
+
symbols=self.symbols if symbols is None else symbols,
|
39
|
+
caller=self.caller if caller is None else caller,
|
40
|
+
parent_module=self.parent_module
|
41
|
+
if parent_module is None
|
42
|
+
else parent_module,
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
def get_fn_source(fn: Callable) -> str:
|
47
|
+
"""Get the string representation of a function."""
|
48
|
+
try:
|
49
|
+
return inspect.getsource(fn)
|
50
|
+
except OSError: # could not get source code
|
51
|
+
return dill.source.getsource(fn)
|
52
|
+
|
53
|
+
|
54
|
+
def get_fn_ast(fn: Callable) -> ast.FunctionDef:
|
55
|
+
"""Get the source code of a function as an AST."""
|
56
|
+
tree = ast.parse(textwrap.dedent(get_fn_source(fn)))
|
57
|
+
if not isinstance(fn_def := tree.body[0], ast.FunctionDef):
|
58
|
+
msg = "Not a function"
|
59
|
+
raise TypeError(msg)
|
60
|
+
return fn_def
|
61
|
+
|
62
|
+
|
63
|
+
def sympy_to_inline(expr: sympy.Expr) -> str:
|
64
|
+
return cast(str, pycode(expr, fully_qualified_modules=True))
|
65
|
+
|
66
|
+
|
67
|
+
def sympy_to_fn(
|
68
|
+
*,
|
69
|
+
fn_name: str,
|
70
|
+
args: list[str],
|
71
|
+
expr: sympy.Expr,
|
72
|
+
) -> str:
|
73
|
+
"""Convert a sympy expression to a python function."""
|
74
|
+
fn_args = ", ".join(f"{i}: float" for i in args)
|
75
|
+
|
76
|
+
return f"""def {fn_name}({fn_args}) -> float:
|
77
|
+
return {pycode(expr)}
|
78
|
+
"""
|
79
|
+
|
80
|
+
|
81
|
+
def fn_to_sympy(
|
82
|
+
fn: Callable,
|
83
|
+
model_args: list[sympy.Symbol | sympy.Expr] | None = None,
|
84
|
+
) -> sympy.Expr:
|
85
|
+
"""Convert a python function to a sympy expression."""
|
86
|
+
fn_def = get_fn_ast(fn)
|
87
|
+
fn_args = [str(arg.arg) for arg in fn_def.args.args]
|
88
|
+
sympy_expr = _handle_fn_body(
|
89
|
+
fn_def.body,
|
90
|
+
ctx=Context(
|
91
|
+
symbols={name: sympy.Symbol(name) for name in fn_args},
|
92
|
+
caller=fn,
|
93
|
+
parent_module=inspect.getmodule(fn),
|
94
|
+
),
|
95
|
+
)
|
96
|
+
if model_args is not None:
|
97
|
+
sympy_expr = sympy_expr.subs(dict(zip(fn_args, model_args, strict=True)))
|
98
|
+
return cast(sympy.Expr, sympy_expr)
|
99
|
+
|
100
|
+
|
101
|
+
def _handle_name(node: ast.Name, ctx: Context) -> sympy.Symbol | sympy.Expr:
|
102
|
+
return ctx.symbols[node.id]
|
103
|
+
|
104
|
+
|
105
|
+
def _handle_expr(node: ast.expr, ctx: Context) -> sympy.Expr:
|
106
|
+
if isinstance(node, ast.UnaryOp):
|
107
|
+
return _handle_unaryop(node, ctx)
|
108
|
+
if isinstance(node, ast.BinOp):
|
109
|
+
return _handle_binop(node, ctx)
|
110
|
+
if isinstance(node, ast.Name):
|
111
|
+
return _handle_name(node, ctx)
|
112
|
+
if isinstance(node, ast.Constant):
|
113
|
+
return node.value
|
114
|
+
if isinstance(node, ast.Compare):
|
115
|
+
# Handle chained comparisons like 1 < a < 2
|
116
|
+
left = cast(Any, _handle_expr(node.left, ctx))
|
117
|
+
comparisons = []
|
118
|
+
|
119
|
+
# Build all individual comparisons from the chain
|
120
|
+
prev_value = left
|
121
|
+
for op, comparator in zip(node.ops, node.comparators, strict=True):
|
122
|
+
right = cast(Any, _handle_expr(comparator, ctx))
|
123
|
+
|
124
|
+
if isinstance(op, ast.Gt):
|
125
|
+
comparisons.append(prev_value > right)
|
126
|
+
elif isinstance(op, ast.GtE):
|
127
|
+
comparisons.append(prev_value >= right)
|
128
|
+
elif isinstance(op, ast.Lt):
|
129
|
+
comparisons.append(prev_value < right)
|
130
|
+
elif isinstance(op, ast.LtE):
|
131
|
+
comparisons.append(prev_value <= right)
|
132
|
+
elif isinstance(op, ast.Eq):
|
133
|
+
comparisons.append(prev_value == right)
|
134
|
+
elif isinstance(op, ast.NotEq):
|
135
|
+
comparisons.append(prev_value != right)
|
136
|
+
|
137
|
+
prev_value = right
|
138
|
+
|
139
|
+
# Combine all comparisons with logical AND
|
140
|
+
result = comparisons[0]
|
141
|
+
for comp in comparisons[1:]:
|
142
|
+
result = sympy.And(result, comp)
|
143
|
+
return cast(sympy.Expr, result)
|
144
|
+
if isinstance(node, ast.Call):
|
145
|
+
return _handle_call(node, ctx)
|
146
|
+
|
147
|
+
# Handle conditional expressions (ternary operators)
|
148
|
+
if isinstance(node, ast.IfExp):
|
149
|
+
condition = _handle_expr(node.test, ctx)
|
150
|
+
if_true = _handle_expr(node.body, ctx)
|
151
|
+
if_false = _handle_expr(node.orelse, ctx)
|
152
|
+
return sympy.Piecewise((if_true, condition), (if_false, True))
|
153
|
+
|
154
|
+
msg = f"Expression type {type(node).__name__} not implemented"
|
155
|
+
raise NotImplementedError(msg)
|
156
|
+
|
157
|
+
|
158
|
+
def _handle_fn_body(body: list[ast.stmt], ctx: Context) -> sympy.Expr:
|
159
|
+
pieces = []
|
160
|
+
remaining_body = list(body)
|
161
|
+
|
162
|
+
while remaining_body:
|
163
|
+
node = remaining_body.pop(0)
|
164
|
+
|
165
|
+
if isinstance(node, ast.If):
|
166
|
+
condition = _handle_expr(node.test, ctx)
|
167
|
+
if_expr = _handle_fn_body(node.body, ctx)
|
168
|
+
pieces.append((if_expr, condition))
|
169
|
+
|
170
|
+
# If there's an else clause
|
171
|
+
if node.orelse:
|
172
|
+
# Check if it's an elif (an If node in orelse)
|
173
|
+
if len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If):
|
174
|
+
# Push the elif back to the beginning of remaining_body to process next
|
175
|
+
remaining_body.insert(0, node.orelse[0])
|
176
|
+
else:
|
177
|
+
# It's a regular else
|
178
|
+
else_expr = _handle_fn_body(node.orelse, ctx) # FIXME: copy here
|
179
|
+
pieces.append((else_expr, True))
|
180
|
+
break # We're done with this chain
|
181
|
+
|
182
|
+
elif not remaining_body and any(
|
183
|
+
isinstance(n, ast.Return) for n in body[body.index(node) + 1 :]
|
184
|
+
):
|
185
|
+
else_expr = _handle_fn_body(
|
186
|
+
body[body.index(node) + 1 :], ctx
|
187
|
+
) # FIXME: copy here
|
188
|
+
pieces.append((else_expr, True))
|
189
|
+
|
190
|
+
elif isinstance(node, ast.Return):
|
191
|
+
if (value := node.value) is None:
|
192
|
+
msg = "Return value cannot be None"
|
193
|
+
raise ValueError(msg)
|
194
|
+
|
195
|
+
expr = _handle_expr(value, ctx)
|
196
|
+
if not pieces:
|
197
|
+
return expr
|
198
|
+
pieces.append((expr, True))
|
199
|
+
break
|
200
|
+
|
201
|
+
elif isinstance(node, ast.Assign):
|
202
|
+
# Handle tuple assignments like c, d = a, b
|
203
|
+
if isinstance(node.targets[0], ast.Tuple):
|
204
|
+
# Handle tuple unpacking
|
205
|
+
target_elements = node.targets[0].elts
|
206
|
+
|
207
|
+
if isinstance(node.value, ast.Tuple):
|
208
|
+
# Direct unpacking like c, d = a, b
|
209
|
+
value_elements = node.value.elts
|
210
|
+
for target, value_expr in zip(
|
211
|
+
target_elements, value_elements, strict=True
|
212
|
+
):
|
213
|
+
if isinstance(target, ast.Name):
|
214
|
+
ctx.symbols[target.id] = _handle_expr(value_expr, ctx)
|
215
|
+
else:
|
216
|
+
# Handle potential iterable unpacking
|
217
|
+
value = _handle_expr(node.value, ctx)
|
218
|
+
else:
|
219
|
+
# Regular single assignment
|
220
|
+
if not isinstance(target := node.targets[0], ast.Name):
|
221
|
+
msg = "Only single variable assignments are supported"
|
222
|
+
raise TypeError(msg)
|
223
|
+
target_name = target.id
|
224
|
+
value = _handle_expr(node.value, ctx)
|
225
|
+
ctx.symbols[target_name] = value
|
226
|
+
|
227
|
+
# If we have pieces to combine into a Piecewise
|
228
|
+
if pieces:
|
229
|
+
return sympy.Piecewise(*pieces)
|
230
|
+
|
231
|
+
# If no return was found but we have assignments, return the last assigned variable
|
232
|
+
for node in reversed(body):
|
233
|
+
if isinstance(node, ast.Assign) and isinstance(node.targets[0], ast.Name):
|
234
|
+
target_name = node.targets[0].id
|
235
|
+
return ctx.symbols[target_name]
|
236
|
+
|
237
|
+
msg = "No return value found in function body"
|
238
|
+
raise ValueError(msg)
|
239
|
+
|
240
|
+
|
241
|
+
def _handle_unaryop(node: ast.UnaryOp, ctx: Context) -> sympy.Expr:
|
242
|
+
left = _handle_expr(node.operand, ctx)
|
243
|
+
left = cast(Any, left) # stupid sympy types don't allow ops on symbols
|
244
|
+
|
245
|
+
if isinstance(node.op, ast.UAdd):
|
246
|
+
return +left
|
247
|
+
if isinstance(node.op, ast.USub):
|
248
|
+
return -left
|
249
|
+
|
250
|
+
msg = f"Operation {type(node.op).__name__} not implemented"
|
251
|
+
raise NotImplementedError(msg)
|
252
|
+
|
253
|
+
|
254
|
+
def _handle_binop(node: ast.BinOp, ctx: Context) -> sympy.Expr:
|
255
|
+
left = _handle_expr(node.left, ctx)
|
256
|
+
left = cast(Any, left) # stupid sympy types don't allow ops on symbols
|
257
|
+
|
258
|
+
right = _handle_expr(node.right, ctx)
|
259
|
+
right = cast(Any, right) # stupid sympy types don't allow ops on symbols
|
260
|
+
|
261
|
+
if isinstance(node.op, ast.Add):
|
262
|
+
return left + right
|
263
|
+
if isinstance(node.op, ast.Sub):
|
264
|
+
return left - right
|
265
|
+
if isinstance(node.op, ast.Mult):
|
266
|
+
return left * right
|
267
|
+
if isinstance(node.op, ast.Div):
|
268
|
+
return left / right
|
269
|
+
if isinstance(node.op, ast.Pow):
|
270
|
+
return left**right
|
271
|
+
if isinstance(node.op, ast.Mod):
|
272
|
+
return left % right
|
273
|
+
if isinstance(node.op, ast.FloorDiv):
|
274
|
+
return left // right
|
275
|
+
|
276
|
+
msg = f"Operation {type(node.op).__name__} not implemented"
|
277
|
+
raise NotImplementedError(msg)
|
278
|
+
|
279
|
+
|
280
|
+
def _handle_call(node: ast.Call, ctx: Context) -> sympy.Expr:
|
281
|
+
# direct call, e.g. mass_action(x, k1)
|
282
|
+
if isinstance(callee := node.func, ast.Name):
|
283
|
+
fn_name = str(callee.id)
|
284
|
+
fns = dict(inspect.getmembers(ctx.parent_module, predicate=callable))
|
285
|
+
|
286
|
+
return fn_to_sympy(
|
287
|
+
fns[fn_name],
|
288
|
+
model_args=[_handle_expr(i, ctx) for i in node.args],
|
289
|
+
)
|
290
|
+
|
291
|
+
# search for fn in other namespace
|
292
|
+
if isinstance(attr := node.func, ast.Attribute):
|
293
|
+
imports = dict(inspect.getmembers(ctx.parent_module, inspect.ismodule))
|
294
|
+
|
295
|
+
# Single level, e.g. fns.mass_action(x, k1)
|
296
|
+
if isinstance(module_name := attr.value, ast.Name):
|
297
|
+
return _handle_call(
|
298
|
+
ast.Call(func=ast.Name(attr.attr), args=node.args, keywords=[]),
|
299
|
+
ctx=ctx.updated(parent_module=imports[module_name.id]),
|
300
|
+
)
|
301
|
+
|
302
|
+
# Multiple levels, e.g. mxlpy.fns.mass_action(x, k1)
|
303
|
+
if isinstance(inner_attr := attr.value, ast.Attribute):
|
304
|
+
if not isinstance(module_name := inner_attr.value, ast.Name):
|
305
|
+
msg = f"Unknown target kind {module_name}"
|
306
|
+
raise NotImplementedError(msg)
|
307
|
+
return _handle_call(
|
308
|
+
ast.Call(
|
309
|
+
func=ast.Attribute(
|
310
|
+
value=ast.Name(inner_attr.attr),
|
311
|
+
attr=attr.attr,
|
312
|
+
),
|
313
|
+
args=node.args,
|
314
|
+
keywords=[],
|
315
|
+
),
|
316
|
+
ctx=ctx.updated(parent_module=imports[module_name.id]),
|
317
|
+
)
|
318
|
+
|
319
|
+
msg = f"Onsupported function type {node.func}"
|
320
|
+
raise NotImplementedError(msg)
|