mxlpy 0.21.0__py3-none-any.whl → 0.22.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/mca.py CHANGED
@@ -71,7 +71,7 @@ def _response_coefficient_worker(
71
71
  - Series of flux response coefficients
72
72
 
73
73
  """
74
- old = model.parameters[parameter]
74
+ old = model.get_parameter_values()[parameter]
75
75
  if y0 is not None:
76
76
  model.update_variables(y0)
77
77
 
@@ -205,7 +205,7 @@ def parameter_elasticities(
205
205
 
206
206
  variables = model.get_initial_conditions() if variables is None else variables
207
207
  for par in to_scan:
208
- old = model.parameters[par]
208
+ old = model.get_parameter_values()[par]
209
209
 
210
210
  model.update_parameters({par: old * (1 + displacement)})
211
211
  upper = model.get_fluxes(variables=variables, time=time)
mxlpy/meta/__init__.py CHANGED
@@ -2,12 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from .codegen_latex import generate_latex_code
6
- from .codegen_modebase import generate_mxlpy_code
7
- from .codegen_py import generate_model_code_py
5
+ from .codegen_latex import generate_latex_code, to_tex_export
6
+ from .codegen_model import generate_model_code_py, generate_model_code_rs
7
+ from .codegen_mxlpy import generate_mxlpy_code
8
8
 
9
9
  __all__ = [
10
10
  "generate_latex_code",
11
11
  "generate_model_code_py",
12
+ "generate_model_code_rs",
12
13
  "generate_mxlpy_code",
14
+ "to_tex_export",
13
15
  ]
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
7
7
 
8
8
  import sympy
9
9
 
10
- from mxlpy.meta.source_tools import fn_to_sympy
10
+ from mxlpy.meta.sympy_tools import fn_to_sympy, list_of_symbols
11
11
  from mxlpy.types import Derived, RateFn
12
12
 
13
13
  if TYPE_CHECKING:
@@ -21,6 +21,7 @@ __all__ = [
21
21
  "default_init",
22
22
  "generate_latex_code",
23
23
  "get_model_tex_diff",
24
+ "to_tex_export",
24
25
  ]
25
26
 
26
27
  cdot = r"\cdot"
@@ -31,10 +32,6 @@ newline = r"\\" + "\n"
31
32
  floatbarrier = r"\FloatBarrier"
32
33
 
33
34
 
34
- def _list_of_symbols(args: list[str]) -> list[sympy.Symbol | sympy.Expr]:
35
- return [sympy.Symbol(arg) for arg in args]
36
-
37
-
38
35
  def default_init[T1, T2](d: dict[T1, T2] | None) -> dict[T1, T2]:
39
36
  """Return empty dict if d is None.
40
37
 
@@ -63,10 +60,6 @@ def _gls(s: str) -> str:
63
60
  return rf"\gls{{{s}}}"
64
61
 
65
62
 
66
- def _abbrev_and_full(s: str) -> str:
67
- return rf"\acrfull{{{s}}}"
68
-
69
-
70
63
  def _gls_short(s: str) -> str:
71
64
  return rf"\acrshort{{{s}}}"
72
65
 
@@ -75,6 +68,10 @@ def _gls_full(s: str) -> str:
75
68
  return rf"\acrlong{{{s}}}"
76
69
 
77
70
 
71
+ def _gls_short_and_full(s: str) -> str:
72
+ return rf"\acrfull{{{s}}}"
73
+
74
+
78
75
  def _rename_latex(s: str) -> str:
79
76
  if s[0].isdigit():
80
77
  s = s[1:]
@@ -109,6 +106,8 @@ def _sympy_to_latex(expr: sympy.Expr) -> str:
109
106
 
110
107
  def _fn_to_latex(
111
108
  fn: Callable,
109
+ *,
110
+ origin: str,
112
111
  arg_names: list[str],
113
112
  long_name_cutoff: int,
114
113
  ) -> tuple[str, dict[str, str]]:
@@ -121,10 +120,13 @@ def _fn_to_latex(
121
120
  replacements = {k: _name_to_latex(f"_x{i}") for i, k in enumerate(long_names)}
122
121
 
123
122
  expr = fn_to_sympy(
124
- fn, _list_of_symbols([replacements.get(k, k) for k in tex_names])
123
+ fn,
124
+ origin=origin,
125
+ model_args=list_of_symbols([replacements.get(k, k) for k in tex_names]),
125
126
  )
126
- fn_str = _sympy_to_latex(expr)
127
- return fn_str, replacements
127
+ if expr is None:
128
+ return rf"\textcolor{{red}}{{{origin}}}", replacements
129
+ return _sympy_to_latex(expr), replacements
128
130
 
129
131
 
130
132
  def _table(
@@ -323,7 +325,8 @@ def _stoichs_to_latex(
323
325
  )
324
326
  sympy_fn = fn_to_sympy(
325
327
  rxn_stoich.fn,
326
- _list_of_symbols([replacements.get(k, k) for k in arg_names]),
328
+ origin=rxn_name,
329
+ model_args=list_of_symbols([replacements.get(k, k) for k in arg_names]),
327
330
  )
328
331
  expr = expr + sympy_fn * sympy.Symbol(rxn_name) # type: ignore
329
332
  else:
@@ -480,7 +483,7 @@ class TexExport:
480
483
 
481
484
  def _add_gls_if_found(k: str) -> str:
482
485
  if (new := gls.get(k)) is not None:
483
- return _abbrev_and_full(new)
486
+ return _gls_short_and_full(new)
484
487
  return k
485
488
 
486
489
  return TexExport(
@@ -564,7 +567,7 @@ class TexExport:
564
567
 
565
568
  def export_derived(
566
569
  self,
567
- long_name_cutoff: int,
570
+ long_name_cutoff: int = 10,
568
571
  ) -> str:
569
572
  """Export derived quantities as LaTeX equations.
570
573
 
@@ -587,16 +590,20 @@ class TexExport:
587
590
  for k, v in sorted(self.derived.items()):
588
591
  fn_str, repls = _fn_to_latex(
589
592
  v.fn,
593
+ origin=k,
590
594
  arg_names=v.args,
591
595
  long_name_cutoff=long_name_cutoff,
592
596
  )
593
- rows.append(f"{_mathrm(_name_to_latex(k))} &= {fn_str} \\\\")
597
+ rows.append(f" {_mathrm(_name_to_latex(k))} &= {fn_str} \\\\")
594
598
  if repls:
595
599
  rows.append(_replacements_in_align(repls))
596
600
 
597
601
  return _latex_align(rows)
598
602
 
599
- def export_reactions(self, long_name_cutoff: int) -> str:
603
+ def export_reactions(
604
+ self,
605
+ long_name_cutoff: int = 10,
606
+ ) -> str:
600
607
  """Export reactions as LaTeX equations.
601
608
 
602
609
  Returns
@@ -618,17 +625,18 @@ class TexExport:
618
625
  for k, v in sorted(self.reactions.items()):
619
626
  fn_str, repls = _fn_to_latex(
620
627
  v.fn,
628
+ origin=k,
621
629
  arg_names=v.args,
622
630
  long_name_cutoff=long_name_cutoff,
623
631
  )
624
- rows.append(f"{_mathrm(_name_to_latex(k))} &= {fn_str} \\\\")
632
+ rows.append(f" {_mathrm(_name_to_latex(k))} &= {fn_str} \\\\")
625
633
  if repls:
626
634
  rows.append(_replacements_in_align(repls))
627
635
  return _latex_align(rows)
628
636
 
629
637
  def export_diff_eqs(
630
638
  self,
631
- long_name_cutoff: int,
639
+ long_name_cutoff: int = 10,
632
640
  ) -> str:
633
641
  """Export stoichiometries as LaTeX table.
634
642
 
@@ -654,12 +662,15 @@ class TexExport:
654
662
  long_name_cutoff=long_name_cutoff,
655
663
  )
656
664
 
657
- rows.append(f"{dxdt} &= {stoich_str} \\\\")
665
+ rows.append(f" {dxdt} &= {stoich_str} \\\\")
658
666
  if repls:
659
667
  rows.append(_replacements_in_align(repls))
660
668
  return _latex_align(rows)
661
669
 
662
- def export_all(self, long_name_cutoff: int = 10) -> str:
670
+ def export_all(
671
+ self,
672
+ long_name_cutoff: int = 10,
673
+ ) -> str:
663
674
  """Export all model parts as a complete LaTeX document section.
664
675
 
665
676
  Returns
@@ -754,7 +765,7 @@ class TexExport:
754
765
  \usepackage[a4paper,top=2cm,bottom=2cm,left=2cm,right=2cm,marginparwidth=1.75cm]{{geometry}}
755
766
  \usepackage{{amsmath, amssymb, array, booktabs,
756
767
  breqn, caption, longtable, mathtools, placeins,
757
- ragged2e, tabularx, titlesec, titling}}
768
+ ragged2e, tabularx, titlesec, titling, xcolor}}
758
769
  \newcommand{{\sectionbreak}}{{\clearpage}}
759
770
  \setlength{{\parindent}}{{0pt}}
760
771
  \allowdisplaybreaks
@@ -769,17 +780,20 @@ class TexExport:
769
780
  """
770
781
 
771
782
 
772
- def _to_tex_export(self: Model) -> TexExport:
783
+ def to_tex_export(model: Model) -> TexExport:
784
+ """Create TexExport object from a model."""
773
785
  diff_eqs = {}
774
- for rxn_name, rxn in self.reactions.items():
786
+ for rxn_name, rxn in model.get_raw_reactions().items():
775
787
  for var_name, factor in rxn.stoichiometry.items():
776
788
  diff_eqs.setdefault(var_name, {})[rxn_name] = factor
777
789
 
778
790
  return TexExport(
779
- parameters=self.parameters,
780
- variables=self.get_initial_conditions(), # FIXME: think about this later
781
- derived=self.derived,
782
- reactions={k: TexReaction(v.fn, v.args) for k, v in self.reactions.items()},
791
+ parameters=model.get_parameter_values(),
792
+ variables=model.get_initial_conditions(), # FIXME: think about this later
793
+ derived=model.get_raw_derived(),
794
+ reactions={
795
+ k: TexReaction(v.fn, v.args) for k, v in model.get_raw_reactions().items()
796
+ },
783
797
  diff_eqs=diff_eqs,
784
798
  )
785
799
 
@@ -820,7 +834,7 @@ def generate_latex_code(
820
834
  """
821
835
  gls = default_init(gls)
822
836
  return (
823
- _to_tex_export(model)
837
+ to_tex_export(model)
824
838
  .rename_with_glossary(gls)
825
839
  .export_document(long_name_cutoff=long_name_cutoff)
826
840
  )
@@ -866,7 +880,7 @@ def get_model_tex_diff(
866
880
  return f"""{" start autogenerated ":%^60}
867
881
  {_clearpage()}
868
882
  {_subsubsection("Model changes")}{_label(section_label)}
869
- {((_to_tex_export(m1) - _to_tex_export(m2)).rename_with_glossary(gls).export_all(long_name_cutoff=long_name_cutoff))}
883
+ {((to_tex_export(m1) - to_tex_export(m2)).rename_with_glossary(gls).export_all(long_name_cutoff=long_name_cutoff))}
870
884
  {_clearpage()}
871
885
  {" end autogenerated ":%^60}
872
886
  """
@@ -0,0 +1,174 @@
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 = ", ".join(f"d{i}dt" for i in diff_eqs) if len(diff_eqs) > 0 else "()"
114
+ source.append(return_template.format(ret))
115
+
116
+ if end is not None:
117
+ source.append(end)
118
+
119
+ # print(source)
120
+ return "\n".join(source)
121
+
122
+
123
+ def generate_model_code_py(
124
+ model: Model,
125
+ free_parameters: list[str] | None = None,
126
+ ) -> str:
127
+ """Transform the model into a python function, inlining the function calls."""
128
+ if free_parameters is None:
129
+ model_fn = (
130
+ "def model(time: float, variables: Iterable[float]) -> Iterable[float]:"
131
+ )
132
+ else:
133
+ args = ", ".join(f"{k}: float" for k in free_parameters)
134
+ model_fn = f"def model(time: float, variables: Iterable[float], {args}) -> Iterable[float]:"
135
+
136
+ return _generate_model_code(
137
+ model,
138
+ imports=[
139
+ "from collections.abc import Iterable\n",
140
+ ],
141
+ sized=False,
142
+ model_fn=model_fn,
143
+ variables_template=" {} = variables",
144
+ assignment_template=" {k} = {v}",
145
+ sympy_inline_fn=sympy_to_inline_py,
146
+ return_template=" return {}",
147
+ end=None,
148
+ free_parameters=free_parameters,
149
+ )
150
+
151
+
152
+ def generate_model_code_rs(
153
+ model: Model,
154
+ free_parameters: list[str] | None = None,
155
+ ) -> str:
156
+ """Transform the model into a rust function, inlining the function calls."""
157
+ if free_parameters is None:
158
+ model_fn = "fn model(time: f64, variables: &[f64; {n}]) -> [f64; {n}] {{"
159
+ else:
160
+ args = ", ".join(f"{k}: f64" for k in free_parameters)
161
+ model_fn = f"fn model(time: f64, variables: &[f64; {{n}}], {args}) -> [f64; {{n}}] {{{{"
162
+
163
+ return _generate_model_code(
164
+ model,
165
+ imports=None,
166
+ sized=True,
167
+ model_fn=model_fn,
168
+ variables_template=" let [{}] = *variables;",
169
+ assignment_template=" let {k}: f64 = {v};",
170
+ sympy_inline_fn=sympy_to_inline_rust,
171
+ return_template=" return [{}]",
172
+ end="}",
173
+ free_parameters=free_parameters,
174
+ )
@@ -2,43 +2,44 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import warnings
5
+ import logging
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- import sympy
9
-
10
- from mxlpy.meta.source_tools import fn_to_sympy, sympy_to_fn
8
+ from mxlpy.meta.sympy_tools import fn_to_sympy, list_of_symbols, sympy_to_python_fn
11
9
  from mxlpy.types import Derived
12
10
 
13
11
  if TYPE_CHECKING:
12
+ import sympy
13
+
14
14
  from mxlpy.model import Model
15
15
 
16
16
  __all__ = [
17
17
  "generate_mxlpy_code",
18
18
  ]
19
19
 
20
-
21
- def _list_of_symbols(args: list[str]) -> list[sympy.Symbol | sympy.Expr]:
22
- return [sympy.Symbol(arg) for arg in args]
20
+ _LOGGER = logging.getLogger()
23
21
 
24
22
 
25
23
  def generate_mxlpy_code(model: Model) -> str:
26
24
  """Generate a mxlpy model from a model."""
27
- functions = {}
25
+ functions: dict[str, tuple[sympy.Expr, list[str]]] = {}
28
26
 
29
27
  # Variables and parameters
30
- variables = model.variables
31
- parameters = model.parameters
28
+ variables = model.get_raw_variables()
29
+ parameters = model.get_parameter_values()
32
30
 
33
31
  # Derived
34
32
  derived_source = []
35
- for k, der in model.derived.items():
33
+ for k, der in model.get_raw_derived().items():
36
34
  fn = der.fn
37
35
  fn_name = fn.__name__
38
- functions[fn_name] = (
39
- fn_to_sympy(fn, model_args=_list_of_symbols(der.args)),
40
- der.args,
41
- )
36
+ if (
37
+ expr := fn_to_sympy(fn, origin=k, model_args=list_of_symbols(der.args))
38
+ ) is None:
39
+ msg = f"Unable to parse fn for derived value '{k}'"
40
+ raise ValueError(msg)
41
+
42
+ functions[fn_name] = (expr, der.args)
42
43
 
43
44
  derived_source.append(
44
45
  f""" .add_derived(
@@ -50,20 +51,27 @@ def generate_mxlpy_code(model: Model) -> str:
50
51
 
51
52
  # Reactions
52
53
  reactions_source = []
53
- for k, rxn in model.reactions.items():
54
+ for k, rxn in model.get_raw_reactions().items():
54
55
  fn = rxn.fn
55
56
  fn_name = fn.__name__
56
- functions[fn_name] = (
57
- fn_to_sympy(fn, model_args=_list_of_symbols(rxn.args)),
58
- rxn.args,
59
- )
57
+ if (
58
+ expr := fn_to_sympy(fn, origin=k, model_args=list_of_symbols(rxn.args))
59
+ ) is None:
60
+ msg = f"Unable to parse fn for reaction '{k}'"
61
+ raise ValueError(msg)
62
+
63
+ functions[fn_name] = (expr, rxn.args)
60
64
  stoichiometry: list[str] = []
61
65
  for var, stoich in rxn.stoichiometry.items():
62
66
  if isinstance(stoich, Derived):
63
- functions[fn_name] = (
64
- fn_to_sympy(fn, model_args=_list_of_symbols(stoich.args)),
65
- rxn.args,
66
- )
67
+ if (
68
+ expr := fn_to_sympy(
69
+ fn, origin=var, model_args=list_of_symbols(stoich.args)
70
+ )
71
+ ) is None:
72
+ msg = f"Unable to parse fn for stoichiometry '{var}'"
73
+ raise ValueError(msg)
74
+ functions[fn_name] = (expr, rxn.args)
67
75
  args = ", ".join(f'"{k}"' for k in stoich.args)
68
76
  stoich = ( # noqa: PLW2901
69
77
  f"""Derived(fn={fn.__name__}, args=[{args}])"""
@@ -81,14 +89,12 @@ def generate_mxlpy_code(model: Model) -> str:
81
89
 
82
90
  # Surrogates
83
91
  if len(model._surrogates) > 0: # noqa: SLF001
84
- warnings.warn(
85
- "Generating code for Surrogates not yet supported.",
86
- stacklevel=1,
87
- )
92
+ msg = "Generating code for Surrogates not yet supported."
93
+ _LOGGER.warning(msg)
88
94
 
89
95
  # Combine all the sources
90
96
  functions_source = "\n\n".join(
91
- sympy_to_fn(fn_name=name, args=args, expr=expr)
97
+ sympy_to_python_fn(fn_name=name, args=args, expr=expr)
92
98
  for name, (expr, args) in functions.items()
93
99
  )
94
100
  source = [