mxlpy 0.9.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/fit.py CHANGED
@@ -154,7 +154,7 @@ def _time_course_residual(
154
154
  par_names: list[str],
155
155
  data: pd.DataFrame,
156
156
  model: Model,
157
- y0: dict[str, float],
157
+ y0: dict[str, float] | None,
158
158
  integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
159
159
  ) -> float:
160
160
  """Calculate residual error between model time course and experimental data.
mxlpy/identify.py ADDED
@@ -0,0 +1,68 @@
1
+ """Numerical parameter identification estimations."""
2
+
3
+ from functools import partial
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from tqdm import tqdm
8
+
9
+ from mxlpy import fit
10
+ from mxlpy.distributions import LogNormal, sample
11
+ from mxlpy.model import Model
12
+ from mxlpy.parallel import parallelise
13
+ from mxlpy.types import Array
14
+
15
+ __all__ = ["profile_likelihood"]
16
+
17
+
18
+ def _mc_fit_time_course_worker(
19
+ p0: pd.Series,
20
+ model: Model,
21
+ data: pd.DataFrame,
22
+ ) -> float:
23
+ p_fit = fit.time_course(model=model, p0=p0.to_dict(), data=data)
24
+ return fit._time_course_residual( # noqa: SLF001
25
+ par_values=list(p_fit.values()),
26
+ par_names=list(p_fit.keys()),
27
+ data=data,
28
+ model=model,
29
+ y0=None,
30
+ integrator=fit.DefaultIntegrator,
31
+ )
32
+
33
+
34
+ def profile_likelihood(
35
+ model: Model,
36
+ data: pd.DataFrame,
37
+ parameter_name: str,
38
+ parameter_values: Array,
39
+ n_random: int = 10,
40
+ ) -> pd.Series:
41
+ """Estimate the profile likelihood of model parameters given data.
42
+
43
+ Args:
44
+ model: The model to be fitted.
45
+ data: The data to fit the model to.
46
+ parameter_name: The name of the parameter to profile.
47
+ parameter_values: The values of the parameter to profile.
48
+ n_random: Number of Monte Carlo samples.
49
+
50
+ """
51
+ parameter_distributions = sample(
52
+ {k: LogNormal(np.log(v), sigma=1) for k, v in model.parameters.items()},
53
+ n=n_random,
54
+ )
55
+
56
+ res = {}
57
+ for value in tqdm(parameter_values, desc=parameter_name):
58
+ model.update_parameter(parameter_name, value)
59
+ res[value] = parallelise(
60
+ partial(_mc_fit_time_course_worker, model=model, data=data),
61
+ inputs=list(
62
+ parameter_distributions.drop(columns=parameter_name).iterrows()
63
+ ),
64
+ disable_tqdm=True,
65
+ )
66
+ errors = pd.DataFrame(res, dtype=float).T.abs().mean(axis=1)
67
+ errors.index.name = "fitting error"
68
+ return errors
mxlpy/plot.py CHANGED
@@ -18,12 +18,17 @@ Functions:
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ import contextlib
22
+
23
+ from cycler import cycler
24
+
21
25
  __all__ = [
22
26
  "FigAx",
23
27
  "FigAxs",
24
28
  "Linestyle",
25
29
  "add_grid",
26
30
  "bars",
31
+ "context",
27
32
  "grid_layout",
28
33
  "heatmap",
29
34
  "heatmap_from_2d_idx",
@@ -66,6 +71,8 @@ from mpl_toolkits.mplot3d import Axes3D
66
71
  from mxlpy.label_map import LabelMapper
67
72
 
68
73
  if TYPE_CHECKING:
74
+ from collections.abc import Generator
75
+
69
76
  from matplotlib.collections import QuadMesh
70
77
 
71
78
  from mxlpy.linear_label_map import LinearLabelMapper
@@ -259,6 +266,12 @@ def rotate_xlabels(
259
266
 
260
267
 
261
268
  def show(fig: Figure | None = None) -> None:
269
+ """Show the given figure or the current figure.
270
+
271
+ Args:
272
+ fig: Figure to show.
273
+
274
+ """
262
275
  if fig is None:
263
276
  plt.show()
264
277
  else:
@@ -266,9 +279,39 @@ def show(fig: Figure | None = None) -> None:
266
279
 
267
280
 
268
281
  def reset_prop_cycle(ax: Axes) -> None:
282
+ """Reset the property cycle of the given axis.
283
+
284
+ Args:
285
+ ax: Axis to reset the property cycle of.
286
+
287
+ """
269
288
  ax.set_prop_cycle(plt.rcParams["axes.prop_cycle"])
270
289
 
271
290
 
291
+ @contextlib.contextmanager
292
+ def context(
293
+ colors: list[str] | None = None,
294
+ line_width: float | None = None,
295
+ ) -> Generator[None, None, None]:
296
+ """Context manager to set the defaults for plots.
297
+
298
+ Args:
299
+ colors: colors to use for the plot.
300
+ line_width: line width to use for the plot.
301
+
302
+ """
303
+ rc = {}
304
+
305
+ if colors is not None:
306
+ rc["axes.prop_cycle"] = cycler(color=colors)
307
+
308
+ if line_width is not None:
309
+ rc["lines.linewidth"] = line_width
310
+
311
+ with plt.rc_context(rc):
312
+ yield
313
+
314
+
272
315
  ##########################################################################
273
316
  # General plot layout
274
317
  ##########################################################################
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.9.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>
@@ -59,6 +59,8 @@ Provides-Extra: torch
59
59
  Requires-Dist: torch>=2.5.1; extra == 'torch'
60
60
  Description-Content-Type: text/markdown
61
61
 
62
+ <img src="docs/assets/logo-diagram.png" style="display: block; max-height: 30rem; margin: auto; padding: 0" alt='mxlpy-logo'>
63
+
62
64
  # mxlpy
63
65
 
64
66
  [![pypi](https://img.shields.io/pypi/v/mxlpy.svg)](https://pypi.python.org/pypi/mxlpy)
@@ -84,6 +86,13 @@ pixi add python assimulo
84
86
  pixi add --pypi mxlpy[torch]
85
87
  ```
86
88
 
89
+ ## How to cite
90
+
91
+ If you use this software in your scientific work, please cite [this article](...):
92
+
93
+ - [doi](https://doi.org/)
94
+ - [bibtex file](https://fillme.out)
95
+
87
96
 
88
97
  ## Development setup
89
98
 
@@ -1,7 +1,8 @@
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
- mxlpy/fit.py,sha256=M8F5s9RUoUPr2s-pCLFZD_ebP2uGvajNCn7XYh1w7Tc,8078
3
+ mxlpy/fit.py,sha256=vJ0AWCvERxPkxgwuOmL9rsH4vXnlBSco4vG-5X98RK8,8085
4
4
  mxlpy/fns.py,sha256=ct_RFj9koW8vXHyr27GnbZUHUS_zfs4rDysybuFiOaU,4599
5
+ mxlpy/identify.py,sha256=af52SCG4nlY9sSw22goaIheuvXR09QYK4ksCT24QHWI,1946
5
6
  mxlpy/label_map.py,sha256=urv-QTb0MUEKjwWvKtJSB8H2kvhLn1EKfRIH7awQQ8Y,17769
6
7
  mxlpy/linear_label_map.py,sha256=2lgERcUVDLXruRI08HBYJo_wK654y46voLUeBTzBy3k,10312
7
8
  mxlpy/mc.py,sha256=GIuJJ-9QRqGsd2xl1LmjmMc-bOdihVShbFmXvu4o5p4,17305
@@ -11,8 +12,9 @@ mxlpy/npe.py,sha256=oiRLA43-qf-AcS2KpQfJIOt7-Ev9Aj5sF6TMq9bJn84,8747
11
12
  mxlpy/parallel.py,sha256=kX4Td5YoovDwZp6kX_3cfO6QtHSS9ieJ0bMZiKs3Xv8,5002
12
13
  mxlpy/parameterise.py,sha256=2jMhhO-bHTFP_0kXercJekeATAZYBg5FrK1MQ_mWGpk,654
13
14
  mxlpy/paths.py,sha256=TK2wO4N9lG-UV1JGfeB64q48JVDbwqIUj63rl55MKuQ,1022
14
- mxlpy/plot.py,sha256=1sI18HAQXeBNo_H6ctS9pwLdmp3MSkGgaGQK7IrTUBc,23498
15
+ mxlpy/plot.py,sha256=z1JW7Si1JQyNMj_MMLkgbLkOkSjVcfAZJGjm_WqCgT4,24355
15
16
  mxlpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ mxlpy/report.py,sha256=TYG-cKo9u6izlnjB4vgu6I2hYrtS9e8wYc_A4MrHq-g,6982
16
18
  mxlpy/scan.py,sha256=3k4084d4eg-3Ok24cL8KWPF7P1L6fbEtiaOM9VC5Eko,19210
17
19
  mxlpy/simulator.py,sha256=T9t2jZ6U5NyK1ICF1UkST8M8v4EPV_H98kzZ4TvQK-w,20115
18
20
  mxlpy/types.py,sha256=ksdn76Sdw0XhQEpQepcETvuGqcJolfrmbIRBT0R_2Bg,13612
@@ -42,7 +44,7 @@ mxlpy/surrogates/_torch.py,sha256=E_1eDUlPSVFwROkdMDCqYwwHE-61pjNMJWotnhjzge0,58
42
44
  mxlpy/symbolic/__init__.py,sha256=3hQjCMw8-6iOxeUdfnCg8449fF_BRF2u6lCM1GPpkRY,222
43
45
  mxlpy/symbolic/strikepy.py,sha256=r6nRtckV1nxKq3i1bYYWZOkzwZ5XeKQuZM5ck44vUo0,20010
44
46
  mxlpy/symbolic/symbolic_model.py,sha256=YL9noEeP3_0DoKXwMPELtfmPuP6mgNcLIJgDRCkyB7A,2434
45
- mxlpy-0.9.0.dist-info/METADATA,sha256=LThtovqTN7iDo4vJSmmuhP6rZ9no5IlOrRkMjN4vJTU,4245
46
- mxlpy-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
47
- mxlpy-0.9.0.dist-info/licenses/LICENSE,sha256=bEzjyjy1stQhfRDVaVHa3xV1x-V8emwdlbMvYO8Zo84,35073
48
- mxlpy-0.9.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