mxlpy 0.10.0__py3-none-any.whl → 0.12.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
@@ -22,7 +22,7 @@ from mxlpy.types import (
22
22
  Array,
23
23
  ArrayLike,
24
24
  Callable,
25
- IntegratorProtocol,
25
+ IntegratorType,
26
26
  cast,
27
27
  )
28
28
 
@@ -57,7 +57,7 @@ class SteadyStateResidualFn(Protocol):
57
57
  data: pd.Series,
58
58
  model: Model,
59
59
  y0: dict[str, float],
60
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
60
+ integrator: IntegratorType,
61
61
  ) -> float:
62
62
  """Calculate residual error between model steady state and experimental data."""
63
63
  ...
@@ -74,7 +74,7 @@ class TimeSeriesResidualFn(Protocol):
74
74
  data: pd.DataFrame,
75
75
  model: Model,
76
76
  y0: dict[str, float],
77
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
77
+ integrator: IntegratorType,
78
78
  ) -> float:
79
79
  """Calculate residual error between model time course and experimental data."""
80
80
  ...
@@ -108,7 +108,7 @@ def _steady_state_residual(
108
108
  data: pd.Series,
109
109
  model: Model,
110
110
  y0: dict[str, float] | None,
111
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
111
+ integrator: IntegratorType,
112
112
  ) -> float:
113
113
  """Calculate residual error between model steady state and experimental data.
114
114
 
@@ -155,7 +155,7 @@ def _time_course_residual(
155
155
  data: pd.DataFrame,
156
156
  model: Model,
157
157
  y0: dict[str, float] | None,
158
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
158
+ integrator: IntegratorType,
159
159
  ) -> float:
160
160
  """Calculate residual error between model time course and experimental data.
161
161
 
@@ -194,7 +194,7 @@ def steady_state(
194
194
  y0: dict[str, float] | None = None,
195
195
  minimize_fn: MinimizeFn = _default_minimize_fn,
196
196
  residual_fn: SteadyStateResidualFn = _steady_state_residual,
197
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
197
+ integrator: IntegratorType = DefaultIntegrator,
198
198
  ) -> dict[str, float]:
199
199
  """Fit model parameters to steady-state experimental data.
200
200
 
@@ -248,7 +248,7 @@ def time_course(
248
248
  y0: dict[str, float] | None = None,
249
249
  minimize_fn: MinimizeFn = _default_minimize_fn,
250
250
  residual_fn: TimeSeriesResidualFn = _time_course_residual,
251
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
251
+ integrator: IntegratorType = DefaultIntegrator,
252
252
  ) -> dict[str, float]:
253
253
  """Fit model parameters to time course of experimental data.
254
254
 
@@ -45,6 +45,7 @@ class Assimulo:
45
45
 
46
46
  rhs: Callable
47
47
  y0: ArrayLike
48
+ jacobian: Callable | None = None
48
49
  atol: float = 1e-8
49
50
  rtol: float = 1e-8
50
51
  maxnef: int = 4 # max error failures
@@ -61,7 +62,11 @@ class Assimulo:
61
62
  number of convergence failures (`self.maxncf`), and verbosity level (`self.verbosity`).
62
63
 
63
64
  """
64
- self.integrator = CVode(Explicit_Problem(self.rhs, self.y0))
65
+ problem = Explicit_Problem(self.rhs, self.y0)
66
+ if self.jacobian is not None:
67
+ problem.jac = self.jacobian
68
+
69
+ self.integrator = CVode(problem)
65
70
  self.integrator.atol = self.atol
66
71
  self.integrator.rtol = self.rtol
67
72
  self.integrator.maxnef = self.maxnef
@@ -42,6 +42,7 @@ class Scipy:
42
42
 
43
43
  rhs: Callable
44
44
  y0: ArrayLike
45
+ jacobian: Callable | None = None
45
46
  atol: float = 1e-8
46
47
  rtol: float = 1e-8
47
48
  t0: float = 0.0
@@ -82,7 +83,7 @@ class Scipy:
82
83
  steps = 100 if steps is None else steps + 1
83
84
 
84
85
  return self.integrate_time_course(
85
- time_points=np.linspace(self.t0, t_end, steps)
86
+ time_points=np.linspace(self.t0, t_end, steps, dtype=float)
86
87
  )
87
88
 
88
89
  def integrate_time_course(
@@ -97,17 +98,21 @@ class Scipy:
97
98
  tuple[ArrayLike, ArrayLike]: Tuple containing the time points and the integrated values.
98
99
 
99
100
  """
100
- y = spi.odeint(
101
- func=self.rhs,
101
+ res = spi.solve_ivp(
102
+ fun=self.rhs,
102
103
  y0=self.y0,
103
- t=time_points,
104
- tfirst=True,
104
+ t_span=(time_points[0], time_points[-1]),
105
+ t_eval=time_points,
106
+ jac=self.jacobian,
105
107
  atol=self.atol,
106
108
  rtol=self.rtol,
109
+ method="LSODA",
107
110
  )
108
- self.t0 = time_points[-1]
109
- self.y0 = y[-1, :]
110
- return np.array(time_points, dtype=float), y
111
+ if res.success:
112
+ self.t0 = time_points[-1]
113
+ self.y0 = res.y[:, -1]
114
+ return np.array(time_points, dtype=float), res.y.T
115
+ return None, None
111
116
 
112
117
  def integrate_to_steady_state(
113
118
  self,
@@ -131,7 +136,7 @@ class Scipy:
131
136
 
132
137
  """
133
138
  self.reset()
134
- integ = spi.ode(self.rhs)
139
+ integ = spi.ode(self.rhs, jac=self.jacobian)
135
140
  integ.set_integrator(name="lsoda")
136
141
  integ.set_initial_value(self.y0)
137
142
  t = self.t0 + step_size
mxlpy/mc.py CHANGED
@@ -34,8 +34,7 @@ from mxlpy.scan import (
34
34
  _update_parameters_and,
35
35
  )
36
36
  from mxlpy.types import (
37
- ArrayLike,
38
- IntegratorProtocol,
37
+ IntegratorType,
39
38
  McSteadyStates,
40
39
  ProtocolByPars,
41
40
  ResponseCoefficientsByPars,
@@ -55,8 +54,6 @@ __all__ = [
55
54
  ]
56
55
 
57
56
  if TYPE_CHECKING:
58
- from collections.abc import Callable
59
-
60
57
  from mxlpy.model import Model
61
58
  from mxlpy.types import Array
62
59
 
@@ -81,7 +78,7 @@ class ParameterScanWorker(Protocol):
81
78
  *,
82
79
  parameters: pd.DataFrame,
83
80
  rel_norm: bool,
84
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
81
+ integrator: IntegratorType,
85
82
  ) -> SteadyStates:
86
83
  """Call the worker function."""
87
84
  ...
@@ -93,7 +90,7 @@ def _parameter_scan_worker(
93
90
  *,
94
91
  parameters: pd.DataFrame,
95
92
  rel_norm: bool,
96
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
93
+ integrator: IntegratorType,
97
94
  ) -> SteadyStates:
98
95
  """Worker function for parallel steady state scanning across parameter sets.
99
96
 
@@ -137,7 +134,7 @@ def steady_state(
137
134
  cache: Cache | None = None,
138
135
  rel_norm: bool = False,
139
136
  worker: SteadyStateWorker = _steady_state_worker,
140
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
137
+ integrator: IntegratorType = DefaultIntegrator,
141
138
  ) -> SteadyStates:
142
139
  """Monte-carlo scan of steady states.
143
140
 
@@ -188,7 +185,7 @@ def time_course(
188
185
  max_workers: int | None = None,
189
186
  cache: Cache | None = None,
190
187
  worker: TimeCourseWorker = _time_course_worker,
191
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
188
+ integrator: IntegratorType = DefaultIntegrator,
192
189
  ) -> TimeCourseByPars:
193
190
  """MC time course.
194
191
 
@@ -241,7 +238,7 @@ def time_course_over_protocol(
241
238
  max_workers: int | None = None,
242
239
  cache: Cache | None = None,
243
240
  worker: ProtocolWorker = _protocol_worker,
244
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
241
+ integrator: IntegratorType = DefaultIntegrator,
245
242
  ) -> ProtocolByPars:
246
243
  """MC time course.
247
244
 
@@ -299,7 +296,7 @@ def scan_steady_state(
299
296
  cache: Cache | None = None,
300
297
  rel_norm: bool = False,
301
298
  worker: ParameterScanWorker = _parameter_scan_worker,
302
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
299
+ integrator: IntegratorType = DefaultIntegrator,
303
300
  ) -> McSteadyStates:
304
301
  """Parameter scan of mc distributed steady states.
305
302
 
@@ -501,7 +498,7 @@ def response_coefficients(
501
498
  disable_tqdm: bool = False,
502
499
  max_workers: int | None = None,
503
500
  rel_norm: bool = False,
504
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
501
+ integrator: IntegratorType = DefaultIntegrator,
505
502
  ) -> ResponseCoefficientsByPars:
506
503
  """Calculate response coefficients using Monte Carlo analysis.
507
504
 
mxlpy/mca.py CHANGED
@@ -25,7 +25,7 @@ import pandas as pd
25
25
  from mxlpy.integrators import DefaultIntegrator
26
26
  from mxlpy.parallel import parallelise
27
27
  from mxlpy.scan import _steady_state_worker
28
- from mxlpy.types import ArrayLike, ResponseCoefficients
28
+ from mxlpy.types import ResponseCoefficients
29
29
 
30
30
  __all__ = [
31
31
  "parameter_elasticities",
@@ -34,10 +34,8 @@ __all__ = [
34
34
  ]
35
35
 
36
36
  if TYPE_CHECKING:
37
- from collections.abc import Callable
38
-
39
- from mxlpy import IntegratorProtocol
40
37
  from mxlpy.model import Model
38
+ from mxlpy.types import IntegratorType
41
39
 
42
40
 
43
41
  ###############################################################################
@@ -169,7 +167,7 @@ def _response_coefficient_worker(
169
167
  normalized: bool,
170
168
  rel_norm: bool,
171
169
  displacement: float = 1e-4,
172
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
170
+ integrator: IntegratorType,
173
171
  ) -> tuple[pd.Series, pd.Series]:
174
172
  """Calculate response coefficients for a single parameter.
175
173
 
@@ -240,7 +238,7 @@ def response_coefficients(
240
238
  parallel: bool = True,
241
239
  max_workers: int | None = None,
242
240
  rel_norm: bool = False,
243
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
241
+ integrator: IntegratorType = DefaultIntegrator,
244
242
  ) -> ResponseCoefficients:
245
243
  """Calculate response coefficients.
246
244
 
mxlpy/report.py ADDED
@@ -0,0 +1,217 @@
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 _table_row(items: list[str]) -> str:
35
+ return f"| {' | '.join(items)} |"
36
+
37
+
38
+ def _table_header(items: list[str]) -> str:
39
+ return f"{_table_row(items)}\n{_table_row(['---'] * len(items))}"
40
+
41
+
42
+ def markdown(
43
+ m1: Model,
44
+ m2: Model,
45
+ analyses: list[AnalysisFn] | None = None,
46
+ rel_change: float = 1e-2,
47
+ img_path: Path = Path(),
48
+ ) -> str:
49
+ """Generate a markdown report comparing two models.
50
+
51
+ Args:
52
+ m1: The first model to compare.
53
+ m2: The second model to compare.
54
+ analyses: A list of functions that take a Path and return a tuple of a string and a Path. Defaults to None.
55
+ rel_change: The relative change threshold for numerical differences. Defaults to 1e-2.
56
+ img_path: The path to save images. Defaults to Path().
57
+
58
+ """
59
+ content: list[str] = [
60
+ f"# Report: {datetime.now(UTC).strftime('%Y-%m-%d')}",
61
+ ]
62
+
63
+ # Variables
64
+ new_variables, removed_variables, changed_variables = _new_removed_changed(
65
+ m1.variables, m2.variables
66
+ )
67
+ variables = []
68
+ variables.extend(
69
+ f"| <span style='color:green'>{k}<span> | - | {v} |"
70
+ for k, v in new_variables.items()
71
+ )
72
+ variables.extend(
73
+ f"| <span style='color: orange'>{k}</span> | {v1} | {v2} |"
74
+ for k, (v1, v2) in changed_variables.items()
75
+ )
76
+ variables.extend(
77
+ f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_variables
78
+ )
79
+ if len(variables) >= 1:
80
+ content.extend(
81
+ (
82
+ "## Variables\n",
83
+ "| Name | Old Value | New Value |",
84
+ "| ---- | --------- | --------- |",
85
+ )
86
+ )
87
+ content.append("\n".join(variables))
88
+
89
+ # Parameters
90
+ new_parameters, removed_parameters, changed_parameters = _new_removed_changed(
91
+ m1.parameters, m2.parameters
92
+ )
93
+ pars = []
94
+ pars.extend(
95
+ f"| <span style='color:green'>{k}<span> | - | {v} |"
96
+ for k, v in new_parameters.items()
97
+ )
98
+ pars.extend(
99
+ f"| <span style='color: orange'>{k}</span> | {v1} | {v2} |"
100
+ for k, (v1, v2) in changed_parameters.items()
101
+ )
102
+ pars.extend(
103
+ f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_parameters
104
+ )
105
+ if len(pars) >= 1:
106
+ content.extend(
107
+ (
108
+ "## Parameters\n",
109
+ "| Name | Old Value | New Value |",
110
+ "| ---- | --------- | --------- |",
111
+ )
112
+ )
113
+ content.append("\n".join(pars))
114
+
115
+ # Derived
116
+ new_derived, removed_derived, changed_derived = _new_removed_changed(
117
+ m1.derived, m2.derived
118
+ )
119
+ derived = []
120
+ for k, v in new_derived.items():
121
+ expr = sympy.latex(fn_to_sympy(v.fn, _list_of_symbols(v.args)))
122
+ derived.append(f"| <span style='color:green'>{k}<span> | - | ${expr}$ |")
123
+ for k, (v1, v2) in changed_derived.items():
124
+ expr1 = sympy.latex(fn_to_sympy(v1.fn, _list_of_symbols(v1.args)))
125
+ expr2 = sympy.latex(fn_to_sympy(v2.fn, _list_of_symbols(v2.args)))
126
+ derived.append(
127
+ f"| <span style='color: orange'>{k}</span> | ${expr1}$ | ${expr2}$ |"
128
+ )
129
+ derived.extend(
130
+ f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_derived
131
+ )
132
+ if len(derived) >= 1:
133
+ content.extend(
134
+ (
135
+ "## Derived\n",
136
+ "| Name | Old Value | New Value |",
137
+ "| ---- | --------- | --------- |",
138
+ )
139
+ )
140
+ content.append("\n".join(derived))
141
+
142
+ # Reactions
143
+ new_reactions, removed_reactions, changed_reactions = _new_removed_changed(
144
+ m1.reactions, m2.reactions
145
+ )
146
+ reactions = []
147
+ for k, v in new_reactions.items():
148
+ expr = sympy.latex(fn_to_sympy(v.fn, _list_of_symbols(v.args)))
149
+ reactions.append(f"| <span style='color:green'>{k}<span> | - | ${expr}$ |")
150
+ for k, (v1, v2) in changed_reactions.items():
151
+ expr1 = sympy.latex(fn_to_sympy(v1.fn, _list_of_symbols(v1.args)))
152
+ expr2 = sympy.latex(fn_to_sympy(v2.fn, _list_of_symbols(v2.args)))
153
+ reactions.append(
154
+ f"| <span style='color: orange'>{k}</span> | ${expr1}$ | ${expr2}$ |"
155
+ )
156
+ reactions.extend(
157
+ f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_reactions
158
+ )
159
+
160
+ if len(reactions) >= 1:
161
+ content.extend(
162
+ (
163
+ "## Reactions\n",
164
+ "| Name | Old Value | New Value |",
165
+ "| ---- | --------- | --------- |",
166
+ )
167
+ )
168
+ content.append("\n".join(reactions))
169
+
170
+ # Now check for any numerical differences
171
+ dependent = []
172
+ d1 = m1.get_dependent()
173
+ d2 = m2.get_dependent()
174
+ rel_diff = ((d1 - d2) / d1).dropna()
175
+ for k, v in rel_diff.loc[rel_diff.abs() >= rel_change].items():
176
+ k = cast(str, k)
177
+ dependent.append(
178
+ f"| <span style='color:orange'>{k}</span> | {d1[k]:.2f} | {d2[k]:.2f} | {v:.1%} |"
179
+ )
180
+ if len(dependent) >= 1:
181
+ content.extend(
182
+ (
183
+ "## Numerical differences of dependent values\n",
184
+ "| Name | Old Value | New Value | Relative Change | ",
185
+ "| ---- | --------- | --------- | --------------- | ",
186
+ )
187
+ )
188
+ content.append("\n".join(dependent))
189
+
190
+ rhs = []
191
+ r1 = m1.get_right_hand_side()
192
+ r2 = m2.get_right_hand_side()
193
+ rel_diff = ((r1 - r2) / r1).dropna()
194
+ for k, v in rel_diff.loc[rel_diff.abs() >= rel_change].items():
195
+ k = cast(str, k)
196
+ rhs.append(
197
+ f"| <span style='color:orange'>{k}</span> | {r1[k]:.2f} | {r2[k]:.2f} | {v:.1%} |"
198
+ )
199
+ if len(rhs) >= 1:
200
+ content.extend(
201
+ (
202
+ "## Numerical differences of right hand side values\n",
203
+ "| Name | Old Value | New Value | Relative Change | ",
204
+ "| ---- | --------- | --------- | --------------- | ",
205
+ )
206
+ )
207
+ content.append("\n".join(rhs))
208
+
209
+ # Comparison functions
210
+ if analyses is not None:
211
+ for f in analyses:
212
+ name, img_path = f(m1, m2, img_path)
213
+ content.append(name)
214
+ # content.append(f"![{name}]({img_path})")
215
+ content.append(f"<img src='{img_path}' alt='{name}' width='500'/>")
216
+
217
+ return "\n".join(content)
mxlpy/scan.py CHANGED
@@ -38,8 +38,7 @@ import pandas as pd
38
38
  from mxlpy.parallel import Cache, parallelise
39
39
  from mxlpy.simulator import Result, Simulator
40
40
  from mxlpy.types import (
41
- ArrayLike,
42
- IntegratorProtocol,
41
+ IntegratorType,
43
42
  ProtocolByPars,
44
43
  SteadyStates,
45
44
  TimeCourseByPars,
@@ -286,7 +285,7 @@ class SteadyStateWorker(Protocol):
286
285
  y0: dict[str, float] | None,
287
286
  *,
288
287
  rel_norm: bool,
289
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
288
+ integrator: IntegratorType,
290
289
  ) -> TimePoint:
291
290
  """Call the worker function."""
292
291
  ...
@@ -301,7 +300,7 @@ class TimeCourseWorker(Protocol):
301
300
  y0: dict[str, float] | None,
302
301
  time_points: Array,
303
302
  *,
304
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
303
+ integrator: IntegratorType,
305
304
  ) -> TimeCourse:
306
305
  """Call the worker function."""
307
306
  ...
@@ -316,7 +315,7 @@ class ProtocolWorker(Protocol):
316
315
  y0: dict[str, float] | None,
317
316
  protocol: pd.DataFrame,
318
317
  *,
319
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
318
+ integrator: IntegratorType,
320
319
  time_points_per_step: int = 10,
321
320
  ) -> TimeCourse:
322
321
  """Call the worker function."""
@@ -328,7 +327,7 @@ def _steady_state_worker(
328
327
  y0: dict[str, float] | None,
329
328
  *,
330
329
  rel_norm: bool,
331
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
330
+ integrator: IntegratorType,
332
331
  ) -> TimePoint:
333
332
  """Simulate the model to steady state and return concentrations and fluxes.
334
333
 
@@ -357,7 +356,7 @@ def _time_course_worker(
357
356
  model: Model,
358
357
  y0: dict[str, float] | None,
359
358
  time_points: Array,
360
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
359
+ integrator: IntegratorType,
361
360
  ) -> TimeCourse:
362
361
  """Simulate the model to steady state and return concentrations and fluxes.
363
362
 
@@ -391,7 +390,7 @@ def _protocol_worker(
391
390
  y0: dict[str, float] | None,
392
391
  protocol: pd.DataFrame,
393
392
  *,
394
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
393
+ integrator: IntegratorType = DefaultIntegrator,
395
394
  time_points_per_step: int = 10,
396
395
  ) -> TimeCourse:
397
396
  """Simulate the model over a protocol and return concentrations and fluxes.
@@ -440,7 +439,7 @@ def steady_state(
440
439
  rel_norm: bool = False,
441
440
  cache: Cache | None = None,
442
441
  worker: SteadyStateWorker = _steady_state_worker,
443
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
442
+ integrator: IntegratorType = DefaultIntegrator,
444
443
  ) -> SteadyStates:
445
444
  """Get steady-state results over supplied parameters.
446
445
 
@@ -515,7 +514,7 @@ def time_course(
515
514
  parallel: bool = True,
516
515
  cache: Cache | None = None,
517
516
  worker: TimeCourseWorker = _time_course_worker,
518
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
517
+ integrator: IntegratorType = DefaultIntegrator,
519
518
  ) -> TimeCourseByPars:
520
519
  """Get time course for each supplied parameter.
521
520
 
@@ -601,7 +600,7 @@ def time_course_over_protocol(
601
600
  parallel: bool = True,
602
601
  cache: Cache | None = None,
603
602
  worker: ProtocolWorker = _protocol_worker,
604
- integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
603
+ integrator: IntegratorType = DefaultIntegrator,
605
604
  ) -> ProtocolByPars:
606
605
  """Get protocol series for each supplied parameter.
607
606
 
mxlpy/simulator.py CHANGED
@@ -21,10 +21,10 @@ from mxlpy.integrators import DefaultIntegrator
21
21
  __all__ = ["Result", "Simulator"]
22
22
 
23
23
  if TYPE_CHECKING:
24
- from collections.abc import Callable, Iterator
24
+ from collections.abc import Iterator
25
25
 
26
26
  from mxlpy.model import Model
27
- from mxlpy.types import Array, ArrayLike, IntegratorProtocol
27
+ from mxlpy.types import Array, ArrayLike, IntegratorProtocol, IntegratorType
28
28
 
29
29
 
30
30
  def _normalise_split_results(
@@ -321,16 +321,14 @@ class Simulator:
321
321
  simulation_parameters: list[dict[str, float]] | None
322
322
 
323
323
  # For resets (e.g. update variable)
324
- _integrator_type: Callable[[Callable, ArrayLike], IntegratorProtocol]
324
+ _integrator_type: IntegratorType
325
325
  _time_shift: float | None
326
326
 
327
327
  def __init__(
328
328
  self,
329
329
  model: Model,
330
330
  y0: dict[str, float] | None = None,
331
- integrator: Callable[
332
- [Callable, ArrayLike], IntegratorProtocol
333
- ] = DefaultIntegrator,
331
+ integrator: IntegratorType = DefaultIntegrator,
334
332
  *,
335
333
  test_run: bool = True,
336
334
  ) -> None:
@@ -358,10 +356,26 @@ class Simulator:
358
356
  self._initialise_integrator()
359
357
 
360
358
  def _initialise_integrator(self) -> None:
359
+ from sympy import lambdify
360
+
361
+ from mxlpy.symbolic import to_symbolic_model
362
+
363
+ try:
364
+ jac = to_symbolic_model(self.model).jacobian()
365
+
366
+ _jacobian = lambda t, y: lambdify( # noqa: E731
367
+ ("time", self.model.get_variable_names()),
368
+ jac.subs(self.model._parameters), # noqa: SLF001
369
+ )(t, y)
370
+
371
+ except: # noqa: E722
372
+ _jacobian = None # type: ignore
373
+
361
374
  y0 = self.y0
362
375
  self.integrator = self._integrator_type(
363
376
  self.model,
364
377
  [y0[k] for k in self.model.get_variable_names()],
378
+ _jacobian,
365
379
  )
366
380
 
367
381
  def clear_results(self) -> None:
mxlpy/types.py CHANGED
@@ -28,6 +28,7 @@ __all__ = [
28
28
  "Derived",
29
29
  "Float",
30
30
  "IntegratorProtocol",
31
+ "IntegratorType",
31
32
  "McSteadyStates",
32
33
  "MockSurrogate",
33
34
  "Param",
@@ -114,6 +115,7 @@ class IntegratorProtocol(Protocol):
114
115
  self,
115
116
  rhs: Callable,
116
117
  y0: ArrayLike,
118
+ jacobian: Callable | None = None,
117
119
  ) -> None:
118
120
  """Initialise the integrator."""
119
121
  ...
@@ -147,6 +149,11 @@ class IntegratorProtocol(Protocol):
147
149
  ...
148
150
 
149
151
 
152
+ type IntegratorType = Callable[
153
+ [Callable, ArrayLike, Callable | None], IntegratorProtocol
154
+ ]
155
+
156
+
150
157
  @dataclass(kw_only=True, slots=True)
151
158
  class Derived:
152
159
  """Container for a derived value."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mxlpy
3
- Version: 0.10.0
3
+ Version: 0.12.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>
@@ -32,6 +32,7 @@ Requires-Dist: pandas>=2.2.3
32
32
  Requires-Dist: parameteriser>=0.1.0
33
33
  Requires-Dist: pebble>=5.0.7
34
34
  Requires-Dist: python-libsbml>=5.20.4
35
+ Requires-Dist: salib>=1.5.1
35
36
  Requires-Dist: scipy>=1.14.1
36
37
  Requires-Dist: seaborn>=0.13.2
37
38
  Requires-Dist: symbtools>=0.4.0
@@ -1,12 +1,12 @@
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=vJ0AWCvERxPkxgwuOmL9rsH4vXnlBSco4vG-5X98RK8,8085
3
+ mxlpy/fit.py,sha256=LwSoLfNVrqSlTtuUApwH36LjzGU0HLs4C_2qqTTjXFE,7859
4
4
  mxlpy/fns.py,sha256=ct_RFj9koW8vXHyr27GnbZUHUS_zfs4rDysybuFiOaU,4599
5
5
  mxlpy/identify.py,sha256=af52SCG4nlY9sSw22goaIheuvXR09QYK4ksCT24QHWI,1946
6
6
  mxlpy/label_map.py,sha256=urv-QTb0MUEKjwWvKtJSB8H2kvhLn1EKfRIH7awQQ8Y,17769
7
7
  mxlpy/linear_label_map.py,sha256=2lgERcUVDLXruRI08HBYJo_wK654y46voLUeBTzBy3k,10312
8
- mxlpy/mc.py,sha256=GIuJJ-9QRqGsd2xl1LmjmMc-bOdihVShbFmXvu4o5p4,17305
9
- mxlpy/mca.py,sha256=MjhH0CcHmXGTR4PCHTTeCbZWGBUa5TyXWtWzcg7M4Vs,9453
8
+ mxlpy/mc.py,sha256=HWuJq4fV_wfTDERbLJRSF3fjCCYMxzLdqAyO53Z_uF8,16985
9
+ mxlpy/mca.py,sha256=H0dfV45Kz5nMIW8s2V61op7x6LmI21wWgRf94i6iIY4,9328
10
10
  mxlpy/model.py,sha256=-BmS4bGCq_fMUnRLZG5uwl86ip_SiaWxsbgLPeV0nHQ,57656
11
11
  mxlpy/npe.py,sha256=oiRLA43-qf-AcS2KpQfJIOt7-Ev9Aj5sF6TMq9bJn84,8747
12
12
  mxlpy/parallel.py,sha256=kX4Td5YoovDwZp6kX_3cfO6QtHSS9ieJ0bMZiKs3Xv8,5002
@@ -14,14 +14,15 @@ 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/scan.py,sha256=3k4084d4eg-3Ok24cL8KWPF7P1L6fbEtiaOM9VC5Eko,19210
18
- mxlpy/simulator.py,sha256=T9t2jZ6U5NyK1ICF1UkST8M8v4EPV_H98kzZ4TvQK-w,20115
19
- mxlpy/types.py,sha256=ksdn76Sdw0XhQEpQepcETvuGqcJolfrmbIRBT0R_2Bg,13612
17
+ mxlpy/report.py,sha256=h7dhcBzPFydLPxdsEXokzDf7Ce4PirXMsvLqlDZLSWM,7181
18
+ mxlpy/scan.py,sha256=-1SLyXJOX3U3CxeP1dEC4ytAoBMCH0Ql89wGvsG3LbI,18858
19
+ mxlpy/simulator.py,sha256=CeMmm5SdgJphQWWBygnv0rxiXjBUJPWHV8-bh6miJNM,20494
20
+ mxlpy/types.py,sha256=aWDtWOTZo4UQJ3rYGq1m6aV4hGxIjvycfJGc8CIDQLE,13775
20
21
  mxlpy/experimental/__init__.py,sha256=kZTE-92OErpHzNRqmgSQYH4CGXrogGJ5EL35XGZQ81M,206
21
22
  mxlpy/experimental/diff.py,sha256=4bztagJzFMsQJM7dlun_kv-WrWssM8CIw7gcL63hFf8,8952
22
23
  mxlpy/integrators/__init__.py,sha256=kqmV6a0TRyLGR_XqbyAI652AfptYnXAUpqbSFg0CpP8,450
23
- mxlpy/integrators/int_assimulo.py,sha256=Y9jvpJWXkmueUyjDnu_6SqDntyMdG_DU370EtuRyMqA,4605
24
- mxlpy/integrators/int_scipy.py,sha256=Z9LSwqBKgYfwmrNiMxJ3-vRad5n_yQo1Ime4WDAAZEI,4343
24
+ mxlpy/integrators/int_assimulo.py,sha256=d-4HHOj4vmGpg8ig2IXMO5CPiIrq89_quEKvCxIKrhw,4747
25
+ mxlpy/integrators/int_scipy.py,sha256=dFHlYTeb2zX97f3VuNdMJdI7WEYshF4JAIgprKKk2z4,4581
25
26
  mxlpy/meta/__init__.py,sha256=Jyy4063fZy6iT4LSwjPyEAVr4N_3xxcLc8wDBoDPyKc,278
26
27
  mxlpy/meta/codegen_latex.py,sha256=1z0waYPmohY9GTJ_5DBuwXOXGTCHCH8E14tg6MYsf2A,13460
27
28
  mxlpy/meta/codegen_modebase.py,sha256=qQ8p6_KqMYCw7SRmf2HHRGvl25sYPKVI_zWKhzFhFLw,3138
@@ -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.12.0.dist-info/METADATA,sha256=Ul9QrtiIlIBZSsRGrTF2J-D_ssjpQtpsw_4IA3LR_lg,4564
48
+ mxlpy-0.12.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
49
+ mxlpy-0.12.0.dist-info/licenses/LICENSE,sha256=bEzjyjy1stQhfRDVaVHa3xV1x-V8emwdlbMvYO8Zo84,35073
50
+ mxlpy-0.12.0.dist-info/RECORD,,
File without changes