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,516 @@
1
+ """Export model to latex."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ import sympy
9
+
10
+ from mxlpy.meta.source_tools import fn_to_sympy
11
+ from mxlpy.types import Derived, RateFn
12
+
13
+ __all__ = [
14
+ "TexExport",
15
+ "TexReaction",
16
+ "default_init",
17
+ "generate_latex_code",
18
+ "get_model_tex_diff",
19
+ ]
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Callable, Mapping
23
+
24
+ from mxlpy import Model
25
+
26
+
27
+ cdot = r"\cdot"
28
+ empty_set = r"\emptyset"
29
+ left_right_arrows = r"\xleftrightharpoons{}"
30
+ right_arrow = r"\xrightarrow{}"
31
+ newline = r"\\" + "\n"
32
+ floatbarrier = r"\FloatBarrier"
33
+
34
+
35
+ def _list_of_symbols(args: list[str]) -> list[sympy.Symbol | sympy.Expr]:
36
+ return [sympy.Symbol(arg) for arg in args]
37
+
38
+
39
+ def default_init[T1, T2](d: dict[T1, T2] | None) -> dict[T1, T2]:
40
+ """Return empty dict if d is None."""
41
+ return {} if d is None else d
42
+
43
+
44
+ def _gls(s: str) -> str:
45
+ return rf"\gls{{{s}}}"
46
+
47
+
48
+ def _abbrev_and_full(s: str) -> str:
49
+ return rf"\acrfull{{{s}}}"
50
+
51
+
52
+ def _gls_short(s: str) -> str:
53
+ return rf"\acrshort{{{s}}}"
54
+
55
+
56
+ def _gls_full(s: str) -> str:
57
+ return rf"\acrlong{{{s}}}"
58
+
59
+
60
+ def _rename_latex(s: str) -> str:
61
+ if s[0].isdigit():
62
+ s = s[1:]
63
+ if s[0] == "-":
64
+ s = s[1:]
65
+ return (
66
+ s.replace(" ", "_")
67
+ .replace("(", "")
68
+ .replace(")", "")
69
+ .replace("-", "_")
70
+ .replace("*", "")
71
+ )
72
+
73
+
74
+ def _escape_non_math(s: str) -> str:
75
+ return s.replace("_", r"\_")
76
+
77
+
78
+ def _fn_to_latex(fn: Callable, arg_names: list[str]) -> str:
79
+ return sympy.latex(fn_to_sympy(fn, _list_of_symbols(arg_names)))
80
+
81
+
82
+ def _table(
83
+ headers: list[str],
84
+ rows: list[list[str]],
85
+ n_columns: int,
86
+ label: str,
87
+ short_desc: str,
88
+ long_desc: str,
89
+ ) -> str:
90
+ columns = "|".join(["c"] * n_columns)
91
+ tab = " "
92
+
93
+ return "\n".join(
94
+ [
95
+ r"\begin{longtable}" + f"{{{columns}}}",
96
+ tab + " & ".join(headers) + r" \\",
97
+ tab + r"\hline",
98
+ tab + r"\endhead",
99
+ ]
100
+ + [tab + " & ".join(i) + r" \\" for i in rows]
101
+ + [
102
+ tab + rf"\caption[{short_desc}]{{{long_desc}}}",
103
+ tab + rf"\label{{table:{label}}}",
104
+ r"\end{longtable}",
105
+ ]
106
+ )
107
+
108
+
109
+ def _label(content: str) -> str:
110
+ return rf"\label{{{content}}}"
111
+
112
+
113
+ def _dmath(content: str) -> str:
114
+ return rf"""\begin{{dmath*}}
115
+ {content}
116
+ \end{{dmath*}}"""
117
+
118
+
119
+ # def _dmath_il(content: str) -> str:
120
+ # return rf"\begin{{dmath*}}{content}\end{{dmath*}}"
121
+
122
+
123
+ def _part(s: str) -> str:
124
+ # depth = -1
125
+ return floatbarrier + rf"\part{{{s}}}"
126
+
127
+
128
+ def _chapter(s: str) -> str:
129
+ # depth = 0
130
+ return floatbarrier + rf"\part{{{s}}}"
131
+
132
+
133
+ def _section(s: str) -> str:
134
+ # depth = 1
135
+ return floatbarrier + rf"\section{{{s}}}"
136
+
137
+
138
+ def _section_(s: str) -> str:
139
+ # depth = 1
140
+ return floatbarrier + rf"\section*{{{s}}}"
141
+
142
+
143
+ def _subsection(s: str) -> str:
144
+ # depth = 2
145
+ return floatbarrier + rf"\subsection{{{s}}}"
146
+
147
+
148
+ def _subsection_(s: str) -> str:
149
+ # depth = 2
150
+ return floatbarrier + rf"\subsection*{{{s}}}"
151
+
152
+
153
+ def _subsubsection(s: str) -> str:
154
+ # depth = 3
155
+ return floatbarrier + rf"\subsubsection{{{s}}}"
156
+
157
+
158
+ def _subsubsection_(s: str) -> str:
159
+ # depth = 3
160
+ return floatbarrier + rf"\subsubsection*{{{s}}}"
161
+
162
+
163
+ def _paragraph(s: str) -> str:
164
+ # depth = 4
165
+ return rf"\paragraph{{{s}}}"
166
+
167
+
168
+ def _subparagraph(s: str) -> str:
169
+ # depth = 5
170
+ return rf"\subparagraph{{{s}}}"
171
+
172
+
173
+ def _math_il(s: str) -> str:
174
+ return f"${s}$"
175
+
176
+
177
+ def _math(s: str) -> str:
178
+ return f"$${s}$$"
179
+
180
+
181
+ def _mathrm(s: str) -> str:
182
+ return rf"\mathrm{{{s}}}"
183
+
184
+
185
+ def _bold(s: str) -> str:
186
+ return rf"\textbf{{{s}}}"
187
+
188
+
189
+ def _clearpage() -> str:
190
+ return r"\clearpage"
191
+
192
+
193
+ def _latex_list(rows: list[str]) -> str:
194
+ return "\n\n".join(rows)
195
+
196
+
197
+ def _latex_list_as_sections(
198
+ rows: list[tuple[str, str]], sec_fn: Callable[[str], str]
199
+ ) -> str:
200
+ return "\n\n".join(
201
+ [
202
+ "\n".join(
203
+ (
204
+ sec_fn(_escape_non_math(name)),
205
+ content,
206
+ )
207
+ )
208
+ for name, content in rows
209
+ ]
210
+ )
211
+
212
+
213
+ def _latex_list_as_bold(rows: list[tuple[str, str]]) -> str:
214
+ return "\n\n".join(
215
+ [
216
+ "\n".join(
217
+ (
218
+ _bold(_escape_non_math(name)) + r"\\",
219
+ content,
220
+ r"\vspace{20pt}",
221
+ )
222
+ )
223
+ for name, content in rows
224
+ ]
225
+ )
226
+
227
+
228
+ def _stoichiometries_to_latex(stoich: Mapping[str, float | Derived]) -> str:
229
+ def optional_factor(k: str, v: float) -> str:
230
+ if v == 1:
231
+ return _mathrm(k)
232
+ if v == -1:
233
+ return f"-{_mathrm(k)}"
234
+ return f"{v} {cdot} {_mathrm(k)}"
235
+
236
+ def latex_for_empty(s: str) -> str:
237
+ if len(s) == 0:
238
+ return empty_set
239
+ return s
240
+
241
+ line = []
242
+ for k, v in stoich.items():
243
+ if isinstance(v, int | float):
244
+ line.append(optional_factor(k, v))
245
+ else:
246
+ line.append(_fn_to_latex(v.fn, [_rename_latex(i) for i in v.args]))
247
+
248
+ line_str = f"{line[0]}"
249
+ for element in line[1:]:
250
+ if element.startswith("-"):
251
+ line_str += f" {element}"
252
+ else:
253
+ line_str += f" + {element}"
254
+ return _math_il(line_str)
255
+
256
+
257
+ @dataclass
258
+ class TexReaction:
259
+ """Collection for reaction."""
260
+
261
+ fn: RateFn
262
+ args: list[str]
263
+
264
+
265
+ @dataclass
266
+ class TexExport:
267
+ """Container for LaTeX export."""
268
+
269
+ parameters: dict[str, float]
270
+ variables: dict[str, float]
271
+ derived: dict[str, Derived]
272
+ reactions: dict[str, TexReaction]
273
+ stoichiometries: dict[str, Mapping[str, float | Derived]]
274
+
275
+ @staticmethod
276
+ def _diff_parameters(
277
+ p1: dict[str, float],
278
+ p2: dict[str, float],
279
+ ) -> dict[str, float]:
280
+ return {k: v for k, v in p2.items() if k not in p1 or p1[k] != v}
281
+
282
+ @staticmethod
283
+ def _diff_variables(p1: dict[str, float], p2: dict[str, float]) -> dict[str, float]:
284
+ return {k: v for k, v in p2.items() if k not in p1 or p1[k] != v}
285
+
286
+ @staticmethod
287
+ def _diff_derived(
288
+ p1: dict[str, Derived],
289
+ p2: dict[str, Derived],
290
+ ) -> dict[str, Derived]:
291
+ return {k: v for k, v in p2.items() if k not in p1 or p1[k] != v}
292
+
293
+ @staticmethod
294
+ def _diff_reactions(
295
+ p1: dict[str, TexReaction],
296
+ p2: dict[str, TexReaction],
297
+ ) -> dict[str, TexReaction]:
298
+ return {k: v for k, v in p2.items() if k not in p1 or p1[k] != v}
299
+
300
+ def __sub__(self, other: object) -> TexExport:
301
+ """Return difference of two tex exports."""
302
+ if not isinstance(other, TexExport):
303
+ raise TypeError
304
+
305
+ return TexExport(
306
+ parameters=self._diff_parameters(self.parameters, other.parameters),
307
+ variables=self._diff_variables(self.variables, other.variables),
308
+ derived=self._diff_derived(self.derived, other.derived),
309
+ reactions=self._diff_reactions(self.reactions, other.reactions),
310
+ stoichiometries={
311
+ k: v
312
+ for k, v in other.stoichiometries.items()
313
+ if self.stoichiometries.get(k, {}) != v
314
+ },
315
+ )
316
+
317
+ def rename_with_glossary(self, gls: dict[str, str]) -> TexExport:
318
+ """Rename all elements according to glossary."""
319
+
320
+ def _add_gls_if_found(k: str) -> str:
321
+ if (new := gls.get(k)) is not None:
322
+ return _abbrev_and_full(new)
323
+ return k
324
+
325
+ return TexExport(
326
+ parameters={gls.get(k, k): v for k, v in self.parameters.items()},
327
+ variables={gls.get(k, k): v for k, v in self.variables.items()},
328
+ derived={
329
+ gls.get(k, k): Derived(
330
+ name=k, fn=v.fn, args=[gls.get(i, i) for i in v.args]
331
+ )
332
+ for k, v in self.derived.items()
333
+ },
334
+ reactions={
335
+ _add_gls_if_found(k): TexReaction(
336
+ fn=v.fn,
337
+ args=[gls.get(i, i) for i in v.args],
338
+ )
339
+ for k, v in self.reactions.items()
340
+ },
341
+ stoichiometries={
342
+ _add_gls_if_found(k): {gls.get(k2, k2): v2 for k2, v2 in v.items()}
343
+ for k, v in self.stoichiometries.items()
344
+ },
345
+ )
346
+
347
+ def export_variables(self) -> str:
348
+ """Export variables."""
349
+ return _table(
350
+ headers=["Model name", "Initial concentration"],
351
+ rows=[
352
+ [
353
+ k,
354
+ f"{v:.2e}",
355
+ ]
356
+ for k, v in self.variables.items()
357
+ ],
358
+ n_columns=2,
359
+ label="model-vars",
360
+ short_desc="Model variables",
361
+ long_desc="Model variables",
362
+ )
363
+
364
+ def export_parameters(self) -> str:
365
+ """Export parameters."""
366
+ return _table(
367
+ headers=["Parameter name", "Parameter value"],
368
+ rows=[
369
+ [_math_il(_mathrm(_escape_non_math(_rename_latex(k)))), f"{v:.2e}"]
370
+ for k, v in sorted(self.parameters.items())
371
+ ],
372
+ n_columns=2,
373
+ label="model-pars",
374
+ short_desc="Model parameters",
375
+ long_desc="Model parameters",
376
+ )
377
+
378
+ def export_derived(self) -> str:
379
+ """Export derived quantities."""
380
+ return _latex_list(
381
+ rows=[
382
+ _dmath(
383
+ f"{_rename_latex(k)} = {_fn_to_latex(v.fn, [_rename_latex(i) for i in v.args])}"
384
+ )
385
+ for k, v in sorted(self.derived.items())
386
+ ]
387
+ )
388
+
389
+ def export_reactions(self) -> str:
390
+ """Export reactions."""
391
+ return _latex_list(
392
+ rows=[
393
+ _dmath(
394
+ f"{_rename_latex(k)} = {_fn_to_latex(v.fn, [_rename_latex(i) for i in v.args])}"
395
+ )
396
+ for k, v in sorted(self.reactions.items())
397
+ ]
398
+ )
399
+
400
+ def export_stoichiometries(self) -> str:
401
+ """Export stoichiometries."""
402
+ return _table(
403
+ headers=["Rate name", "Stoichiometry"],
404
+ rows=[
405
+ [
406
+ _escape_non_math(_rename_latex(k)),
407
+ _stoichiometries_to_latex(v),
408
+ ]
409
+ for k, v in sorted(self.stoichiometries.items())
410
+ ],
411
+ n_columns=2,
412
+ label="model-stoichs",
413
+ short_desc="Model stoichiometries.",
414
+ long_desc="Model stoichiometries.",
415
+ )
416
+
417
+ def export_all(self) -> str:
418
+ """Export all model parts."""
419
+ sections = []
420
+ if len(self.variables) > 0:
421
+ sections.append(
422
+ (
423
+ "Variables",
424
+ self.export_variables(),
425
+ )
426
+ )
427
+ if len(self.parameters) > 0:
428
+ sections.append(
429
+ (
430
+ "Parameters",
431
+ self.export_parameters(),
432
+ )
433
+ )
434
+ if len(self.derived) > 0:
435
+ sections.append(
436
+ (
437
+ "Derived",
438
+ self.export_derived(),
439
+ )
440
+ )
441
+ if len(self.reactions) > 0:
442
+ sections.append(
443
+ (
444
+ "Reactions",
445
+ self.export_reactions(),
446
+ )
447
+ )
448
+ sections.append(
449
+ (
450
+ "Stoichiometries",
451
+ self.export_stoichiometries(),
452
+ )
453
+ )
454
+ return _latex_list_as_sections(sections, _subsubsection_)
455
+
456
+ def export_document(
457
+ self,
458
+ author: str = "mxlpy",
459
+ title: str = "Model construction",
460
+ ) -> str:
461
+ """Export latex document."""
462
+ content = self.export_all()
463
+ return rf"""\documentclass{{article}}
464
+ \usepackage[english]{{babel}}
465
+ \usepackage[a4paper,top=2cm,bottom=2cm,left=2cm,right=2cm,marginparwidth=1.75cm]{{geometry}}
466
+ \usepackage{{amsmath, amssymb, array, booktabs,
467
+ breqn, caption, longtable, mathtools, placeins,
468
+ ragged2e, tabularx, titlesec, titling}}
469
+ \newcommand{{\sectionbreak}}{{\clearpage}}
470
+ \setlength{{\parindent}}{{0pt}}
471
+
472
+ \title{{{title}}}
473
+ \date{{}} % clear date
474
+ \author{{{author}}}
475
+ \begin{{document}}
476
+ \maketitle
477
+ {content}
478
+ \end{{document}}
479
+ """
480
+
481
+
482
+ def _to_tex_export(self: Model) -> TexExport:
483
+ return TexExport(
484
+ parameters=self.parameters,
485
+ variables=self.variables,
486
+ derived=self.derived,
487
+ reactions={k: TexReaction(v.fn, v.args) for k, v in self.reactions.items()},
488
+ stoichiometries={k: v.stoichiometry for k, v in self.reactions.items()},
489
+ )
490
+
491
+
492
+ def generate_latex_code(
493
+ model: Model,
494
+ gls: dict[str, str] | None = None,
495
+ ) -> str:
496
+ """Export as LaTeX."""
497
+ gls = default_init(gls)
498
+ return _to_tex_export(model).rename_with_glossary(gls).export_document()
499
+
500
+
501
+ def get_model_tex_diff(
502
+ m1: Model,
503
+ m2: Model,
504
+ gls: dict[str, str] | None = None,
505
+ ) -> str:
506
+ """Create LaTeX diff of two models."""
507
+ gls = default_init(gls)
508
+ section_label = "sec:model-diff"
509
+
510
+ return f"""{" start autogenerated ":%^60}
511
+ {_clearpage()}
512
+ {_subsubsection("Model changes")}{_label(section_label)}
513
+ {((_to_tex_export(m1) - _to_tex_export(m2)).rename_with_glossary(gls).export_all())}
514
+ {_clearpage()}
515
+ {" end autogenerated ":%^60}
516
+ """
@@ -0,0 +1,110 @@
1
+ """Generate mxlpy code from a model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from typing import TYPE_CHECKING
7
+
8
+ import sympy
9
+
10
+ from mxlpy.meta.source_tools import fn_to_sympy, sympy_to_fn
11
+ from mxlpy.types import Derived
12
+
13
+ if TYPE_CHECKING:
14
+ from mxlpy.model import Model
15
+
16
+ __all__ = ["generate_mxlpy_code"]
17
+
18
+
19
+ def _list_of_symbols(args: list[str]) -> list[sympy.Symbol | sympy.Expr]:
20
+ return [sympy.Symbol(arg) for arg in args]
21
+
22
+
23
+ def generate_mxlpy_code(model: Model) -> str:
24
+ """Generate a mxlpy model from a model."""
25
+ functions = {}
26
+
27
+ # Variables and parameters
28
+ variables = model.variables
29
+ parameters = model.parameters
30
+
31
+ # Derived
32
+ derived_source = []
33
+ for k, der in model.derived.items():
34
+ fn = der.fn
35
+ fn_name = fn.__name__
36
+ functions[fn_name] = (
37
+ fn_to_sympy(fn, model_args=_list_of_symbols(der.args)),
38
+ der.args,
39
+ )
40
+
41
+ derived_source.append(
42
+ f""" .add_derived(
43
+ "{k}",
44
+ fn={fn_name},
45
+ args={der.args},
46
+ )"""
47
+ )
48
+
49
+ # Reactions
50
+ reactions_source = []
51
+ for k, rxn in model.reactions.items():
52
+ fn = rxn.fn
53
+ fn_name = fn.__name__
54
+ functions[fn_name] = (
55
+ fn_to_sympy(fn, model_args=_list_of_symbols(rxn.args)),
56
+ rxn.args,
57
+ )
58
+ stoichiometry: list[str] = []
59
+ for var, stoich in rxn.stoichiometry.items():
60
+ if isinstance(stoich, Derived):
61
+ functions[fn_name] = (
62
+ fn_to_sympy(fn, model_args=_list_of_symbols(stoich.args)),
63
+ rxn.args,
64
+ )
65
+ args = ", ".join(f'"{k}"' for k in stoich.args)
66
+ stoich = ( # noqa: PLW2901
67
+ f"""Derived(name="{var}", fn={fn.__name__}, args=[{args}])"""
68
+ )
69
+ stoichiometry.append(f""""{var}": {stoich}""")
70
+
71
+ reactions_source.append(
72
+ f""" .add_reaction(
73
+ "{k}",
74
+ fn={fn_name},
75
+ args={rxn.args},
76
+ stoichiometry={{{",".join(stoichiometry)}}},
77
+ )"""
78
+ )
79
+
80
+ # Surrogates
81
+ if len(model._surrogates) > 0: # noqa: SLF001
82
+ warnings.warn(
83
+ "Generating code for Surrogates not yet supported.",
84
+ stacklevel=1,
85
+ )
86
+
87
+ # Combine all the sources
88
+ functions_source = "\n\n".join(
89
+ sympy_to_fn(fn_name=name, args=args, expr=expr)
90
+ for name, (expr, args) in functions.items()
91
+ )
92
+ source = [
93
+ "from mxlpy import Model\n",
94
+ functions_source,
95
+ "def create_model() -> Model:",
96
+ " return (",
97
+ " Model()",
98
+ ]
99
+ if len(parameters) > 0:
100
+ source.append(f" .add_parameters({parameters})")
101
+ if len(variables) > 0:
102
+ source.append(f" .add_variables({variables})")
103
+ if len(derived_source) > 0:
104
+ source.append("\n".join(derived_source))
105
+ if len(reactions_source) > 0:
106
+ source.append("\n".join(reactions_source))
107
+
108
+ source.append(" )")
109
+
110
+ return "\n".join(source)