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.
Files changed (48) hide show
  1. mxlpy/__init__.py +165 -0
  2. mxlpy/distributions.py +339 -0
  3. mxlpy/experimental/__init__.py +12 -0
  4. mxlpy/experimental/diff.py +226 -0
  5. mxlpy/fit.py +291 -0
  6. mxlpy/fns.py +191 -0
  7. mxlpy/integrators/__init__.py +19 -0
  8. mxlpy/integrators/int_assimulo.py +146 -0
  9. mxlpy/integrators/int_scipy.py +146 -0
  10. mxlpy/label_map.py +610 -0
  11. mxlpy/linear_label_map.py +303 -0
  12. mxlpy/mc.py +548 -0
  13. mxlpy/mca.py +280 -0
  14. mxlpy/meta/__init__.py +11 -0
  15. mxlpy/meta/codegen_latex.py +516 -0
  16. mxlpy/meta/codegen_modebase.py +110 -0
  17. mxlpy/meta/codegen_py.py +107 -0
  18. mxlpy/meta/source_tools.py +320 -0
  19. mxlpy/model.py +1737 -0
  20. mxlpy/nn/__init__.py +10 -0
  21. mxlpy/nn/_tensorflow.py +0 -0
  22. mxlpy/nn/_torch.py +129 -0
  23. mxlpy/npe.py +277 -0
  24. mxlpy/parallel.py +171 -0
  25. mxlpy/parameterise.py +27 -0
  26. mxlpy/paths.py +36 -0
  27. mxlpy/plot.py +875 -0
  28. mxlpy/py.typed +0 -0
  29. mxlpy/sbml/__init__.py +14 -0
  30. mxlpy/sbml/_data.py +77 -0
  31. mxlpy/sbml/_export.py +644 -0
  32. mxlpy/sbml/_import.py +599 -0
  33. mxlpy/sbml/_mathml.py +691 -0
  34. mxlpy/sbml/_name_conversion.py +52 -0
  35. mxlpy/sbml/_unit_conversion.py +74 -0
  36. mxlpy/scan.py +629 -0
  37. mxlpy/simulator.py +655 -0
  38. mxlpy/surrogates/__init__.py +31 -0
  39. mxlpy/surrogates/_poly.py +97 -0
  40. mxlpy/surrogates/_torch.py +196 -0
  41. mxlpy/symbolic/__init__.py +10 -0
  42. mxlpy/symbolic/strikepy.py +582 -0
  43. mxlpy/symbolic/symbolic_model.py +75 -0
  44. mxlpy/types.py +474 -0
  45. mxlpy-0.8.0.dist-info/METADATA +106 -0
  46. mxlpy-0.8.0.dist-info/RECORD +48 -0
  47. mxlpy-0.8.0.dist-info/WHEEL +4 -0
  48. mxlpy-0.8.0.dist-info/licenses/LICENSE +674 -0
@@ -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)