mxlpy 0.10.0__py3-none-any.whl → 0.11.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 CHANGED
@@ -49,6 +49,7 @@ from . import (
49
49
  mc,
50
50
  mca,
51
51
  plot,
52
+ report,
52
53
  sbml,
53
54
  surrogates,
54
55
  )
@@ -95,6 +96,7 @@ __all__ = [
95
96
  "mc",
96
97
  "mca",
97
98
  "plot",
99
+ "report",
98
100
  "sbml",
99
101
  "steady_state",
100
102
  "surrogates",
mxlpy/report.py ADDED
@@ -0,0 +1,209 @@
1
+ """Generate a report comparing two models."""
2
+
3
+ from collections.abc import Callable
4
+ from datetime import UTC, datetime
5
+ from pathlib import Path
6
+ from typing import cast
7
+
8
+ import sympy
9
+
10
+ from mxlpy.meta.source_tools import fn_to_sympy
11
+ from mxlpy.model import Model
12
+
13
+ __all__ = ["AnalysisFn", "markdown"]
14
+
15
+ type AnalysisFn = Callable[[Model, Model, Path], tuple[str, Path]]
16
+
17
+
18
+ def _list_of_symbols(args: list[str]) -> list[sympy.Symbol | sympy.Expr]:
19
+ return [sympy.Symbol(arg) for arg in args]
20
+
21
+
22
+ def _new_removed_changed[T](
23
+ d1: dict[str, T], d2: dict[str, T]
24
+ ) -> tuple[dict[str, T], list[str], dict[str, tuple[T, T]]]:
25
+ s1 = set(d1)
26
+ s2 = set(d2)
27
+
28
+ removed = sorted(s1 - s2)
29
+ new = {k: d2[k] for k in s2 - s1}
30
+ changed = {k: (v1, v2) for k in s1 - set(removed) if (v1 := d1[k]) != (v2 := d2[k])}
31
+ return new, removed, changed
32
+
33
+
34
+ def markdown(
35
+ m1: Model,
36
+ m2: Model,
37
+ analyses: list[AnalysisFn] | None = None,
38
+ rel_change: float = 1e-2,
39
+ img_path: Path = Path(),
40
+ ) -> str:
41
+ """Generate a markdown report comparing two models.
42
+
43
+ Args:
44
+ m1: The first model to compare.
45
+ m2: The second model to compare.
46
+ analyses: A list of functions that take a Path and return a tuple of a string and a Path. Defaults to None.
47
+ rel_change: The relative change threshold for numerical differences. Defaults to 1e-2.
48
+ img_path: The path to save images. Defaults to Path().
49
+
50
+ """
51
+ content: list[str] = [
52
+ f"# Report: {datetime.now(UTC).strftime('%Y-%m-%d')}",
53
+ ]
54
+
55
+ # Variables
56
+ new_variables, removed_variables, changed_variables = _new_removed_changed(
57
+ m1.variables, m2.variables
58
+ )
59
+ variables = []
60
+ variables.extend(
61
+ f"| <span style='color:green'>{k}<span> | - | {v} |"
62
+ for k, v in new_variables.items()
63
+ )
64
+ variables.extend(
65
+ f"| <span style='color: orange'>{k}</span> | {v1} | {v2} |"
66
+ for k, (v1, v2) in changed_variables.items()
67
+ )
68
+ variables.extend(
69
+ f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_variables
70
+ )
71
+ if len(variables) >= 1:
72
+ content.extend(
73
+ (
74
+ "## Variables\n",
75
+ "| Name | Old Value | New Value |",
76
+ "| ---- | --------- | --------- |",
77
+ )
78
+ )
79
+ content.append("\n".join(variables))
80
+
81
+ # Parameters
82
+ new_parameters, removed_parameters, changed_parameters = _new_removed_changed(
83
+ m1.parameters, m2.parameters
84
+ )
85
+ pars = []
86
+ pars.extend(
87
+ f"| <span style='color:green'>{k}<span> | - | {v} |"
88
+ for k, v in new_parameters.items()
89
+ )
90
+ pars.extend(
91
+ f"| <span style='color: orange'>{k}</span> | {v1} | {v2} |"
92
+ for k, (v1, v2) in changed_parameters.items()
93
+ )
94
+ pars.extend(
95
+ f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_parameters
96
+ )
97
+ if len(pars) >= 1:
98
+ content.extend(
99
+ (
100
+ "## Parameters\n",
101
+ "| Name | Old Value | New Value |",
102
+ "| ---- | --------- | --------- |",
103
+ )
104
+ )
105
+ content.append("\n".join(pars))
106
+
107
+ # Derived
108
+ new_derived, removed_derived, changed_derived = _new_removed_changed(
109
+ m1.derived, m2.derived
110
+ )
111
+ derived = []
112
+ for k, v in new_derived.items():
113
+ expr = sympy.latex(fn_to_sympy(v.fn, _list_of_symbols(v.args)))
114
+ derived.append(f"| <span style='color:green'>{k}<span> | - | ${expr}$ |")
115
+ for k, (v1, v2) in changed_derived.items():
116
+ expr1 = sympy.latex(fn_to_sympy(v1.fn, _list_of_symbols(v1.args)))
117
+ expr2 = sympy.latex(fn_to_sympy(v2.fn, _list_of_symbols(v2.args)))
118
+ derived.append(
119
+ f"| <span style='color: orange'>{k}</span> | ${expr1}$ | ${expr2}$ |"
120
+ )
121
+ derived.extend(
122
+ f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_derived
123
+ )
124
+ if len(derived) >= 1:
125
+ content.extend(
126
+ (
127
+ "## Derived\n",
128
+ "| Name | Old Value | New Value |",
129
+ "| ---- | --------- | --------- |",
130
+ )
131
+ )
132
+ content.append("\n".join(derived))
133
+
134
+ # Reactions
135
+ new_reactions, removed_reactions, changed_reactions = _new_removed_changed(
136
+ m1.reactions, m2.reactions
137
+ )
138
+ reactions = []
139
+ for k, v in new_reactions.items():
140
+ expr = sympy.latex(fn_to_sympy(v.fn, _list_of_symbols(v.args)))
141
+ reactions.append(f"| <span style='color:green'>{k}<span> | - | ${expr}$ |")
142
+ for k, (v1, v2) in changed_reactions.items():
143
+ expr1 = sympy.latex(fn_to_sympy(v1.fn, _list_of_symbols(v1.args)))
144
+ expr2 = sympy.latex(fn_to_sympy(v2.fn, _list_of_symbols(v2.args)))
145
+ reactions.append(
146
+ f"| <span style='color: orange'>{k}</span> | ${expr1}$ | ${expr2}$ |"
147
+ )
148
+ reactions.extend(
149
+ f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_reactions
150
+ )
151
+
152
+ if len(reactions) >= 1:
153
+ content.extend(
154
+ (
155
+ "## Reactions\n",
156
+ "| Name | Old Value | New Value |",
157
+ "| ---- | --------- | --------- |",
158
+ )
159
+ )
160
+ content.append("\n".join(reactions))
161
+
162
+ # Now check for any numerical differences
163
+ dependent = []
164
+ d1 = m1.get_dependent()
165
+ d2 = m2.get_dependent()
166
+ rel_diff = ((d1 - d2) / d1).dropna()
167
+ for k, v in rel_diff.loc[rel_diff.abs() >= rel_change].items():
168
+ k = cast(str, k)
169
+ dependent.append(
170
+ f"| <span style='color:orange'>{k}</span> | {d1[k]:.2f} | {d2[k]:.2f} | {v:.1%} "
171
+ )
172
+ if len(dependent) >= 1:
173
+ content.extend(
174
+ (
175
+ "## Numerical differences of dependent values\n",
176
+ "| Name | Old Value | New Value | Relative Change | ",
177
+ "| ---- | --------- | --------- | --------------- | ",
178
+ )
179
+ )
180
+ content.append("\n".join(dependent))
181
+
182
+ rhs = []
183
+ r1 = m1.get_right_hand_side()
184
+ r2 = m2.get_right_hand_side()
185
+ rel_diff = ((r1 - r2) / r1).dropna()
186
+ for k, v in rel_diff.loc[rel_diff.abs() >= rel_change].items():
187
+ k = cast(str, k)
188
+ rhs.append(
189
+ f"| <span style='color:orange'>{k}</span> | {r1[k]:.2f} | {r2[k]:.2f} | {v:.1%} "
190
+ )
191
+ if len(rhs) >= 1:
192
+ content.extend(
193
+ (
194
+ "## Numerical differences of right hand side values\n",
195
+ "| Name | Old Value | New Value | Relative Change | ",
196
+ "| ---- | --------- | --------- | --------------- | ",
197
+ )
198
+ )
199
+ content.append("\n".join(rhs))
200
+
201
+ # Comparison functions
202
+ if analyses is not None:
203
+ for f in analyses:
204
+ name, img_path = f(m1, m2, img_path)
205
+ content.append(name)
206
+ # content.append(f"![{name}]({img_path})")
207
+ content.append(f"<img src='{img_path}' alt='{name}' width='500'/>")
208
+
209
+ return "\n".join(content)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mxlpy
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: A package to build metabolic models
5
5
  Author-email: Marvin van Aalst <marvin.vanaalst@gmail.com>
6
6
  Maintainer-email: Marvin van Aalst <marvin.vanaalst@gmail.com>
@@ -1,4 +1,4 @@
1
- mxlpy/__init__.py,sha256=HwV_l1PqCqWsn1TFwUMslbfuPjX6cUGzytyWPaUz4FM,4168
1
+ mxlpy/__init__.py,sha256=XZYNFyDC5rWcKi6139mq04cROI7LwJvxB2_3ApKwcvY,4194
2
2
  mxlpy/distributions.py,sha256=ce6RTqn19YzMMec-u09fSIUA8A92M6rehCuHuXWcX7A,8734
3
3
  mxlpy/fit.py,sha256=vJ0AWCvERxPkxgwuOmL9rsH4vXnlBSco4vG-5X98RK8,8085
4
4
  mxlpy/fns.py,sha256=ct_RFj9koW8vXHyr27GnbZUHUS_zfs4rDysybuFiOaU,4599
@@ -14,6 +14,7 @@ mxlpy/parameterise.py,sha256=2jMhhO-bHTFP_0kXercJekeATAZYBg5FrK1MQ_mWGpk,654
14
14
  mxlpy/paths.py,sha256=TK2wO4N9lG-UV1JGfeB64q48JVDbwqIUj63rl55MKuQ,1022
15
15
  mxlpy/plot.py,sha256=z1JW7Si1JQyNMj_MMLkgbLkOkSjVcfAZJGjm_WqCgT4,24355
16
16
  mxlpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ mxlpy/report.py,sha256=TYG-cKo9u6izlnjB4vgu6I2hYrtS9e8wYc_A4MrHq-g,6982
17
18
  mxlpy/scan.py,sha256=3k4084d4eg-3Ok24cL8KWPF7P1L6fbEtiaOM9VC5Eko,19210
18
19
  mxlpy/simulator.py,sha256=T9t2jZ6U5NyK1ICF1UkST8M8v4EPV_H98kzZ4TvQK-w,20115
19
20
  mxlpy/types.py,sha256=ksdn76Sdw0XhQEpQepcETvuGqcJolfrmbIRBT0R_2Bg,13612
@@ -43,7 +44,7 @@ mxlpy/surrogates/_torch.py,sha256=E_1eDUlPSVFwROkdMDCqYwwHE-61pjNMJWotnhjzge0,58
43
44
  mxlpy/symbolic/__init__.py,sha256=3hQjCMw8-6iOxeUdfnCg8449fF_BRF2u6lCM1GPpkRY,222
44
45
  mxlpy/symbolic/strikepy.py,sha256=r6nRtckV1nxKq3i1bYYWZOkzwZ5XeKQuZM5ck44vUo0,20010
45
46
  mxlpy/symbolic/symbolic_model.py,sha256=YL9noEeP3_0DoKXwMPELtfmPuP6mgNcLIJgDRCkyB7A,2434
46
- mxlpy-0.10.0.dist-info/METADATA,sha256=wwkTfCscHZzrydDqVdVhVNI7isEc2YcwN4ZC5OAWA8M,4536
47
- mxlpy-0.10.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
48
- mxlpy-0.10.0.dist-info/licenses/LICENSE,sha256=bEzjyjy1stQhfRDVaVHa3xV1x-V8emwdlbMvYO8Zo84,35073
49
- mxlpy-0.10.0.dist-info/RECORD,,
47
+ mxlpy-0.11.0.dist-info/METADATA,sha256=N1-dMC1k0QUlSF2ZN_URFUyg119f9QUz7xcBS2Ia_PQ,4536
48
+ mxlpy-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
49
+ mxlpy-0.11.0.dist-info/licenses/LICENSE,sha256=bEzjyjy1stQhfRDVaVHa3xV1x-V8emwdlbMvYO8Zo84,35073
50
+ mxlpy-0.11.0.dist-info/RECORD,,
File without changes