mxlpy 0.20.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/__init__.py +5 -1
- mxlpy/carousel.py +166 -0
- mxlpy/compare.py +2 -6
- mxlpy/experimental/diff.py +1 -1
- mxlpy/fit.py +386 -44
- mxlpy/identify.py +20 -16
- mxlpy/integrators/int_scipy.py +3 -0
- mxlpy/label_map.py +5 -5
- mxlpy/linear_label_map.py +3 -1
- mxlpy/mc.py +24 -20
- mxlpy/mca.py +9 -7
- mxlpy/meta/__init__.py +5 -3
- mxlpy/meta/codegen_latex.py +44 -30
- mxlpy/meta/codegen_model.py +174 -0
- mxlpy/meta/{codegen_modebase.py → codegen_mxlpy.py} +35 -29
- mxlpy/meta/source_tools.py +408 -167
- mxlpy/meta/sympy_tools.py +117 -0
- mxlpy/model.py +528 -224
- mxlpy/parallel.py +7 -6
- mxlpy/report.py +153 -90
- mxlpy/sbml/_export.py +11 -8
- mxlpy/sbml/_import.py +7 -7
- mxlpy/scan.py +32 -20
- mxlpy/simulator.py +240 -59
- mxlpy/symbolic/symbolic_model.py +29 -17
- mxlpy/types.py +45 -20
- mxlpy/units.py +128 -0
- {mxlpy-0.20.0.dist-info → mxlpy-0.22.0.dist-info}/METADATA +3 -1
- mxlpy-0.22.0.dist-info/RECORD +58 -0
- mxlpy/meta/codegen_py.py +0 -115
- mxlpy-0.20.0.dist-info/RECORD +0 -55
- {mxlpy-0.20.0.dist-info → mxlpy-0.22.0.dist-info}/WHEEL +0 -0
- {mxlpy-0.20.0.dist-info → mxlpy-0.22.0.dist-info}/licenses/LICENSE +0 -0
mxlpy/scan.py
CHANGED
@@ -22,7 +22,6 @@ from typing import TYPE_CHECKING, Protocol, Self, cast
|
|
22
22
|
import numpy as np
|
23
23
|
import pandas as pd
|
24
24
|
|
25
|
-
from mxlpy.integrators import DefaultIntegrator
|
26
25
|
from mxlpy.parallel import Cache, parallelise
|
27
26
|
from mxlpy.simulator import Result, Simulator
|
28
27
|
from mxlpy.types import IntegratorType, ProtocolByPars, SteadyStates, TimeCourseByPars
|
@@ -281,7 +280,8 @@ class SteadyStateWorker(Protocol):
|
|
281
280
|
model: Model,
|
282
281
|
*,
|
283
282
|
rel_norm: bool,
|
284
|
-
integrator: IntegratorType,
|
283
|
+
integrator: IntegratorType | None,
|
284
|
+
y0: dict[str, float] | None,
|
285
285
|
) -> TimePoint:
|
286
286
|
"""Call the worker function."""
|
287
287
|
...
|
@@ -295,7 +295,8 @@ class TimeCourseWorker(Protocol):
|
|
295
295
|
model: Model,
|
296
296
|
time_points: Array,
|
297
297
|
*,
|
298
|
-
integrator: IntegratorType,
|
298
|
+
integrator: IntegratorType | None,
|
299
|
+
y0: dict[str, float] | None,
|
299
300
|
) -> TimeCourse:
|
300
301
|
"""Call the worker function."""
|
301
302
|
...
|
@@ -309,7 +310,8 @@ class ProtocolWorker(Protocol):
|
|
309
310
|
model: Model,
|
310
311
|
protocol: pd.DataFrame,
|
311
312
|
*,
|
312
|
-
integrator: IntegratorType,
|
313
|
+
integrator: IntegratorType | None,
|
314
|
+
y0: dict[str, float] | None,
|
313
315
|
time_points_per_step: int = 10,
|
314
316
|
) -> TimeCourse:
|
315
317
|
"""Call the worker function."""
|
@@ -320,7 +322,8 @@ def _steady_state_worker(
|
|
320
322
|
model: Model,
|
321
323
|
*,
|
322
324
|
rel_norm: bool,
|
323
|
-
integrator: IntegratorType,
|
325
|
+
integrator: IntegratorType | None,
|
326
|
+
y0: dict[str, float] | None,
|
324
327
|
) -> TimePoint:
|
325
328
|
"""Simulate the model to steady state and return concentrations and fluxes.
|
326
329
|
|
@@ -336,7 +339,7 @@ def _steady_state_worker(
|
|
336
339
|
"""
|
337
340
|
try:
|
338
341
|
res = (
|
339
|
-
Simulator(model, integrator=integrator)
|
342
|
+
Simulator(model, integrator=integrator, y0=y0)
|
340
343
|
.simulate_to_steady_state(rel_norm=rel_norm)
|
341
344
|
.get_result()
|
342
345
|
)
|
@@ -348,7 +351,8 @@ def _steady_state_worker(
|
|
348
351
|
def _time_course_worker(
|
349
352
|
model: Model,
|
350
353
|
time_points: Array,
|
351
|
-
|
354
|
+
y0: dict[str, float] | None,
|
355
|
+
integrator: IntegratorType | None,
|
352
356
|
) -> TimeCourse:
|
353
357
|
"""Simulate the model to steady state and return concentrations and fluxes.
|
354
358
|
|
@@ -364,7 +368,7 @@ def _time_course_worker(
|
|
364
368
|
"""
|
365
369
|
try:
|
366
370
|
res = (
|
367
|
-
Simulator(model, integrator=integrator)
|
371
|
+
Simulator(model, integrator=integrator, y0=y0)
|
368
372
|
.simulate_time_course(time_points=time_points)
|
369
373
|
.get_result()
|
370
374
|
)
|
@@ -381,7 +385,8 @@ def _protocol_worker(
|
|
381
385
|
model: Model,
|
382
386
|
protocol: pd.DataFrame,
|
383
387
|
*,
|
384
|
-
integrator: IntegratorType
|
388
|
+
integrator: IntegratorType | None,
|
389
|
+
y0: dict[str, float] | None,
|
385
390
|
time_points_per_step: int = 10,
|
386
391
|
) -> TimeCourse:
|
387
392
|
"""Simulate the model over a protocol and return concentrations and fluxes.
|
@@ -399,8 +404,8 @@ def _protocol_worker(
|
|
399
404
|
"""
|
400
405
|
try:
|
401
406
|
res = (
|
402
|
-
Simulator(model, integrator=integrator)
|
403
|
-
.
|
407
|
+
Simulator(model, integrator=integrator, y0=y0)
|
408
|
+
.simulate_protocol(
|
404
409
|
protocol=protocol,
|
405
410
|
time_points_per_step=time_points_per_step,
|
406
411
|
)
|
@@ -430,7 +435,7 @@ def steady_state(
|
|
430
435
|
rel_norm: bool = False,
|
431
436
|
cache: Cache | None = None,
|
432
437
|
worker: SteadyStateWorker = _steady_state_worker,
|
433
|
-
integrator: IntegratorType =
|
438
|
+
integrator: IntegratorType | None = None,
|
434
439
|
) -> SteadyStates:
|
435
440
|
"""Get steady-state results over supplied values.
|
436
441
|
|
@@ -479,6 +484,7 @@ def steady_state(
|
|
479
484
|
worker,
|
480
485
|
rel_norm=rel_norm,
|
481
486
|
integrator=integrator,
|
487
|
+
y0=None,
|
482
488
|
),
|
483
489
|
model=model,
|
484
490
|
),
|
@@ -486,8 +492,8 @@ def steady_state(
|
|
486
492
|
cache=cache,
|
487
493
|
parallel=parallel,
|
488
494
|
)
|
489
|
-
concs = pd.DataFrame({k: v.variables.T for k, v in res
|
490
|
-
fluxes = pd.DataFrame({k: v.fluxes.T for k, v in res
|
495
|
+
concs = pd.DataFrame({k: v.variables.T for k, v in res}).T
|
496
|
+
fluxes = pd.DataFrame({k: v.fluxes.T for k, v in res}).T
|
491
497
|
idx = (
|
492
498
|
pd.Index(to_scan.iloc[:, 0])
|
493
499
|
if to_scan.shape[1] == 1
|
@@ -506,8 +512,8 @@ def time_course(
|
|
506
512
|
y0: dict[str, float] | None = None,
|
507
513
|
parallel: bool = True,
|
508
514
|
cache: Cache | None = None,
|
515
|
+
integrator: IntegratorType | None = None,
|
509
516
|
worker: TimeCourseWorker = _time_course_worker,
|
510
|
-
integrator: IntegratorType = DefaultIntegrator,
|
511
517
|
) -> TimeCourseByPars:
|
512
518
|
"""Get time course for each supplied parameter.
|
513
519
|
|
@@ -559,6 +565,8 @@ def time_course(
|
|
559
565
|
|
560
566
|
|
561
567
|
"""
|
568
|
+
# We update the initial conditions separately here, because `to_scan` might also
|
569
|
+
# contain initial conditions.
|
562
570
|
if y0 is not None:
|
563
571
|
model.update_variables(y0)
|
564
572
|
|
@@ -569,6 +577,7 @@ def time_course(
|
|
569
577
|
worker,
|
570
578
|
time_points=time_points,
|
571
579
|
integrator=integrator,
|
580
|
+
y0=None, # See comment above
|
572
581
|
),
|
573
582
|
model=model,
|
574
583
|
),
|
@@ -576,8 +585,8 @@ def time_course(
|
|
576
585
|
cache=cache,
|
577
586
|
parallel=parallel,
|
578
587
|
)
|
579
|
-
concs = cast(dict, {k: v.variables for k, v in res
|
580
|
-
fluxes = cast(dict, {k: v.fluxes for k, v in res
|
588
|
+
concs = cast(dict, {k: v.variables for k, v in res})
|
589
|
+
fluxes = cast(dict, {k: v.fluxes for k, v in res})
|
581
590
|
return TimeCourseByPars(
|
582
591
|
parameters=to_scan,
|
583
592
|
variables=pd.concat(concs, names=["n", "time"]),
|
@@ -595,7 +604,7 @@ def time_course_over_protocol(
|
|
595
604
|
parallel: bool = True,
|
596
605
|
cache: Cache | None = None,
|
597
606
|
worker: ProtocolWorker = _protocol_worker,
|
598
|
-
integrator: IntegratorType =
|
607
|
+
integrator: IntegratorType | None = None,
|
599
608
|
) -> ProtocolByPars:
|
600
609
|
"""Get protocol series for each supplied parameter.
|
601
610
|
|
@@ -626,6 +635,8 @@ def time_course_over_protocol(
|
|
626
635
|
TimeCourseByPars: Protocol series results for each parameter set.
|
627
636
|
|
628
637
|
"""
|
638
|
+
# We update the initial conditions separately here, because `to_scan` might also
|
639
|
+
# contain initial conditions.
|
629
640
|
if y0 is not None:
|
630
641
|
model.update_variables(y0)
|
631
642
|
|
@@ -637,6 +648,7 @@ def time_course_over_protocol(
|
|
637
648
|
protocol=protocol,
|
638
649
|
time_points_per_step=time_points_per_step,
|
639
650
|
integrator=integrator,
|
651
|
+
y0=None,
|
640
652
|
),
|
641
653
|
model=model,
|
642
654
|
),
|
@@ -644,8 +656,8 @@ def time_course_over_protocol(
|
|
644
656
|
cache=cache,
|
645
657
|
parallel=parallel,
|
646
658
|
)
|
647
|
-
concs = cast(dict, {k: v.variables for k, v in res
|
648
|
-
fluxes = cast(dict, {k: v.fluxes for k, v in res
|
659
|
+
concs = cast(dict, {k: v.variables for k, v in res})
|
660
|
+
fluxes = cast(dict, {k: v.fluxes for k, v in res})
|
649
661
|
return ProtocolByPars(
|
650
662
|
parameters=to_scan,
|
651
663
|
protocol=protocol,
|
mxlpy/simulator.py
CHANGED
@@ -10,7 +10,7 @@ Classes:
|
|
10
10
|
|
11
11
|
from __future__ import annotations
|
12
12
|
|
13
|
-
import
|
13
|
+
import logging
|
14
14
|
from dataclasses import dataclass, field
|
15
15
|
from typing import TYPE_CHECKING, Literal, Self, cast, overload
|
16
16
|
|
@@ -27,6 +27,8 @@ if TYPE_CHECKING:
|
|
27
27
|
from mxlpy.model import Model
|
28
28
|
from mxlpy.types import Array, ArrayLike, IntegratorProtocol, IntegratorType
|
29
29
|
|
30
|
+
_LOGGER = logging.getLogger(__name__)
|
31
|
+
|
30
32
|
__all__ = [
|
31
33
|
"Result",
|
32
34
|
"Simulator",
|
@@ -67,15 +69,15 @@ class Result:
|
|
67
69
|
"""Simulation results."""
|
68
70
|
|
69
71
|
model: Model
|
70
|
-
|
71
|
-
|
72
|
-
|
72
|
+
raw_variables: list[pd.DataFrame]
|
73
|
+
raw_parameters: list[dict[str, float]]
|
74
|
+
raw_args: list[pd.DataFrame] = field(default_factory=list)
|
73
75
|
|
74
76
|
@property
|
75
77
|
def variables(self) -> pd.DataFrame:
|
76
78
|
"""Simulation variables."""
|
77
79
|
return self.get_variables(
|
78
|
-
|
80
|
+
include_derived_variables=True,
|
79
81
|
include_readouts=True,
|
80
82
|
concatenated=True,
|
81
83
|
normalise=None,
|
@@ -90,49 +92,50 @@ class Result:
|
|
90
92
|
"""Iterate over the concentration and flux response coefficients."""
|
91
93
|
return iter((self.variables, self.fluxes))
|
92
94
|
|
93
|
-
def
|
94
|
-
self,
|
95
|
-
*,
|
96
|
-
include_readouts: bool = True,
|
97
|
-
) -> list[pd.DataFrame]:
|
95
|
+
def _compute_args(self) -> list[pd.DataFrame]:
|
98
96
|
# Already computed
|
99
|
-
if len(self.
|
100
|
-
return self.
|
97
|
+
if len(self.raw_args) > 0:
|
98
|
+
return self.raw_args
|
101
99
|
|
102
100
|
# Compute new otherwise
|
103
|
-
for res, p in zip(self.
|
101
|
+
for res, p in zip(self.raw_variables, self.raw_parameters, strict=True):
|
104
102
|
self.model.update_parameters(p)
|
105
|
-
self.
|
106
|
-
self.model.
|
103
|
+
self.raw_args.append(
|
104
|
+
self.model.get_args_time_course(
|
107
105
|
variables=res,
|
108
|
-
|
106
|
+
include_variables=True,
|
107
|
+
include_parameters=True,
|
108
|
+
include_derived_parameters=True,
|
109
|
+
include_derived_variables=True,
|
110
|
+
include_reactions=True,
|
111
|
+
include_surrogate_outputs=True,
|
112
|
+
include_readouts=True,
|
109
113
|
)
|
110
114
|
)
|
111
|
-
return self.
|
115
|
+
return self.raw_args
|
112
116
|
|
113
|
-
def
|
117
|
+
def _select_data(
|
114
118
|
self,
|
115
119
|
dependent: list[pd.DataFrame],
|
116
120
|
*,
|
117
|
-
|
118
|
-
|
121
|
+
include_variables: bool = False,
|
122
|
+
include_parameters: bool = False,
|
123
|
+
include_derived_parameters: bool = False,
|
124
|
+
include_derived_variables: bool = False,
|
125
|
+
include_reactions: bool = False,
|
126
|
+
include_surrogate_outputs: bool = False,
|
127
|
+
include_readouts: bool = False,
|
119
128
|
) -> list[pd.DataFrame]:
|
120
|
-
names = self.model.
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
*,
|
131
|
-
include_surrogates: bool,
|
132
|
-
) -> list[pd.DataFrame]:
|
133
|
-
names = self.model.get_reaction_names()
|
134
|
-
if include_surrogates:
|
135
|
-
names.extend(self.model.get_surrogate_reaction_names())
|
129
|
+
names = self.model.get_arg_names(
|
130
|
+
include_time=False,
|
131
|
+
include_variables=include_variables,
|
132
|
+
include_parameters=include_parameters,
|
133
|
+
include_derived_parameters=include_derived_parameters,
|
134
|
+
include_derived_variables=include_derived_variables,
|
135
|
+
include_reactions=include_reactions,
|
136
|
+
include_surrogate_outputs=include_surrogate_outputs,
|
137
|
+
include_readouts=include_readouts,
|
138
|
+
)
|
136
139
|
return [i.loc[:, names] for i in dependent]
|
137
140
|
|
138
141
|
def _adjust_data(
|
@@ -148,11 +151,93 @@ class Result:
|
|
148
151
|
return pd.concat(data, axis=0)
|
149
152
|
return data
|
150
153
|
|
154
|
+
@overload
|
155
|
+
def get_args( # type: ignore
|
156
|
+
self,
|
157
|
+
*,
|
158
|
+
include_variables: bool = True,
|
159
|
+
include_parameters: bool = False,
|
160
|
+
include_derived_parameters: bool = False,
|
161
|
+
include_derived_variables: bool = True,
|
162
|
+
include_reactions: bool = True,
|
163
|
+
include_surrogate_outputs: bool = False,
|
164
|
+
include_readouts: bool = False,
|
165
|
+
concatenated: Literal[False],
|
166
|
+
normalise: float | ArrayLike | None = None,
|
167
|
+
) -> list[pd.DataFrame]: ...
|
168
|
+
|
169
|
+
@overload
|
170
|
+
def get_args(
|
171
|
+
self,
|
172
|
+
*,
|
173
|
+
include_variables: bool = True,
|
174
|
+
include_parameters: bool = False,
|
175
|
+
include_derived_parameters: bool = False,
|
176
|
+
include_derived_variables: bool = True,
|
177
|
+
include_reactions: bool = True,
|
178
|
+
include_surrogate_outputs: bool = False,
|
179
|
+
include_readouts: bool = False,
|
180
|
+
concatenated: Literal[True],
|
181
|
+
normalise: float | ArrayLike | None = None,
|
182
|
+
) -> pd.DataFrame: ...
|
183
|
+
|
184
|
+
@overload
|
185
|
+
def get_args(
|
186
|
+
self,
|
187
|
+
*,
|
188
|
+
include_variables: bool = True,
|
189
|
+
include_parameters: bool = False,
|
190
|
+
include_derived_parameters: bool = False,
|
191
|
+
include_derived_variables: bool = True,
|
192
|
+
include_reactions: bool = True,
|
193
|
+
include_surrogate_outputs: bool = False,
|
194
|
+
include_readouts: bool = False,
|
195
|
+
concatenated: bool = True,
|
196
|
+
normalise: float | ArrayLike | None = None,
|
197
|
+
) -> pd.DataFrame: ...
|
198
|
+
|
199
|
+
def get_args(
|
200
|
+
self,
|
201
|
+
*,
|
202
|
+
include_variables: bool = True,
|
203
|
+
include_parameters: bool = False,
|
204
|
+
include_derived_parameters: bool = False,
|
205
|
+
include_derived_variables: bool = True,
|
206
|
+
include_reactions: bool = True,
|
207
|
+
include_surrogate_outputs: bool = False,
|
208
|
+
include_readouts: bool = False,
|
209
|
+
concatenated: bool = True,
|
210
|
+
normalise: float | ArrayLike | None = None,
|
211
|
+
) -> pd.DataFrame | list[pd.DataFrame]:
|
212
|
+
"""Get the variables over time.
|
213
|
+
|
214
|
+
Examples:
|
215
|
+
>>> Result().get_variables()
|
216
|
+
Time ATP NADPH
|
217
|
+
0.000000 1.000000 1.000000
|
218
|
+
0.000100 0.999900 0.999900
|
219
|
+
0.000200 0.999800 0.999800
|
220
|
+
|
221
|
+
"""
|
222
|
+
variables = self._select_data(
|
223
|
+
self._compute_args(),
|
224
|
+
include_variables=include_variables,
|
225
|
+
include_parameters=include_parameters,
|
226
|
+
include_derived_parameters=include_derived_parameters,
|
227
|
+
include_derived_variables=include_derived_variables,
|
228
|
+
include_reactions=include_reactions,
|
229
|
+
include_surrogate_outputs=include_surrogate_outputs,
|
230
|
+
include_readouts=include_readouts,
|
231
|
+
)
|
232
|
+
return self._adjust_data(
|
233
|
+
variables, normalise=normalise, concatenated=concatenated
|
234
|
+
)
|
235
|
+
|
151
236
|
@overload
|
152
237
|
def get_variables( # type: ignore
|
153
238
|
self,
|
154
239
|
*,
|
155
|
-
|
240
|
+
include_derived_variables: bool = True,
|
156
241
|
include_readouts: bool = True,
|
157
242
|
concatenated: Literal[False],
|
158
243
|
normalise: float | ArrayLike | None = None,
|
@@ -162,7 +247,7 @@ class Result:
|
|
162
247
|
def get_variables(
|
163
248
|
self,
|
164
249
|
*,
|
165
|
-
|
250
|
+
include_derived_variables: bool = True,
|
166
251
|
include_readouts: bool = True,
|
167
252
|
concatenated: Literal[True],
|
168
253
|
normalise: float | ArrayLike | None = None,
|
@@ -172,7 +257,7 @@ class Result:
|
|
172
257
|
def get_variables(
|
173
258
|
self,
|
174
259
|
*,
|
175
|
-
|
260
|
+
include_derived_variables: bool = True,
|
176
261
|
include_readouts: bool = True,
|
177
262
|
concatenated: bool = True,
|
178
263
|
normalise: float | ArrayLike | None = None,
|
@@ -181,7 +266,7 @@ class Result:
|
|
181
266
|
def get_variables(
|
182
267
|
self,
|
183
268
|
*,
|
184
|
-
|
269
|
+
include_derived_variables: bool = True,
|
185
270
|
include_readouts: bool = True,
|
186
271
|
concatenated: bool = True,
|
187
272
|
normalise: float | ArrayLike | None = None,
|
@@ -196,16 +281,17 @@ class Result:
|
|
196
281
|
0.000200 0.999800 0.999800
|
197
282
|
|
198
283
|
"""
|
199
|
-
if not
|
284
|
+
if not include_derived_variables and not include_readouts:
|
200
285
|
return self._adjust_data(
|
201
|
-
self.
|
286
|
+
self.raw_variables,
|
202
287
|
normalise=normalise,
|
203
288
|
concatenated=concatenated,
|
204
289
|
)
|
205
290
|
|
206
|
-
variables = self.
|
207
|
-
self.
|
208
|
-
|
291
|
+
variables = self._select_data(
|
292
|
+
self._compute_args(),
|
293
|
+
include_variables=True,
|
294
|
+
include_derived_variables=include_derived_variables,
|
209
295
|
include_readouts=include_readouts,
|
210
296
|
)
|
211
297
|
return self._adjust_data(
|
@@ -259,9 +345,10 @@ class Result:
|
|
259
345
|
pd.DataFrame: DataFrame of fluxes.
|
260
346
|
|
261
347
|
"""
|
262
|
-
fluxes = self.
|
263
|
-
self.
|
264
|
-
|
348
|
+
fluxes = self._select_data(
|
349
|
+
self._compute_args(),
|
350
|
+
include_reactions=True,
|
351
|
+
include_surrogate_outputs=include_surrogates,
|
265
352
|
)
|
266
353
|
return self._adjust_data(
|
267
354
|
fluxes,
|
@@ -295,7 +382,7 @@ class Result:
|
|
295
382
|
"""
|
296
383
|
return dict(
|
297
384
|
self.get_variables(
|
298
|
-
|
385
|
+
include_derived_variables=False,
|
299
386
|
include_readouts=False,
|
300
387
|
).iloc[-1]
|
301
388
|
)
|
@@ -335,7 +422,7 @@ class Simulator:
|
|
335
422
|
self,
|
336
423
|
model: Model,
|
337
424
|
y0: dict[str, float] | None = None,
|
338
|
-
integrator: IntegratorType =
|
425
|
+
integrator: IntegratorType | None = None,
|
339
426
|
*,
|
340
427
|
use_jacobian: bool = False,
|
341
428
|
test_run: bool = True,
|
@@ -354,7 +441,7 @@ class Simulator:
|
|
354
441
|
self.model = model
|
355
442
|
self.y0 = model.get_initial_conditions() if y0 is None else y0
|
356
443
|
|
357
|
-
self._integrator_type = integrator
|
444
|
+
self._integrator_type = DefaultIntegrator if integrator is None else integrator
|
358
445
|
self._time_shift = None
|
359
446
|
self.variables = None
|
360
447
|
self.dependent = None
|
@@ -386,7 +473,7 @@ class Simulator:
|
|
386
473
|
)
|
387
474
|
|
388
475
|
except Exception as e: # noqa: BLE001
|
389
|
-
|
476
|
+
_LOGGER.warning(str(e), stacklevel=2)
|
390
477
|
|
391
478
|
y0 = self.y0
|
392
479
|
self.integrator = self._integrator_type(
|
@@ -431,7 +518,7 @@ class Simulator:
|
|
431
518
|
# model._get_rhs sorts the return array by model.get_variable_names()
|
432
519
|
# Do NOT change this ordering
|
433
520
|
results_df = pd.DataFrame(
|
434
|
-
results,
|
521
|
+
data=results,
|
435
522
|
index=time,
|
436
523
|
columns=self.model.get_variable_names(),
|
437
524
|
)
|
@@ -445,7 +532,7 @@ class Simulator:
|
|
445
532
|
|
446
533
|
if self.simulation_parameters is None:
|
447
534
|
self.simulation_parameters = []
|
448
|
-
self.simulation_parameters.append(self.model.
|
535
|
+
self.simulation_parameters.append(self.model.get_parameter_values())
|
449
536
|
|
450
537
|
def simulate(
|
451
538
|
self,
|
@@ -472,6 +559,13 @@ class Simulator:
|
|
472
559
|
if self._time_shift is not None:
|
473
560
|
t_end -= self._time_shift
|
474
561
|
|
562
|
+
prior_t_end: float = (
|
563
|
+
0.0 if (variables := self.variables) is None else variables[-1].index[-1]
|
564
|
+
)
|
565
|
+
if t_end <= prior_t_end:
|
566
|
+
msg = "End time point has to be larger than previous end time point"
|
567
|
+
raise ValueError(msg)
|
568
|
+
|
475
569
|
time, results = self.integrator.integrate(t_end=t_end, steps=steps)
|
476
570
|
|
477
571
|
self._handle_simulation_results(time, results, skipfirst=True)
|
@@ -495,17 +589,33 @@ class Simulator:
|
|
495
589
|
Self: The Simulator instance with updated results.
|
496
590
|
|
497
591
|
"""
|
592
|
+
time_points = np.array(time_points, dtype=float)
|
593
|
+
|
498
594
|
if self._time_shift is not None:
|
499
|
-
time_points = np.array(time_points, dtype=float)
|
500
595
|
time_points -= self._time_shift
|
501
596
|
|
597
|
+
# Check if end is actually larger
|
598
|
+
prior_t_end: float = (
|
599
|
+
0.0 if (variables := self.variables) is None else variables[-1].index[-1]
|
600
|
+
)
|
601
|
+
if time_points[-1] <= prior_t_end:
|
602
|
+
msg = "End time point has to be larger than previous end time point"
|
603
|
+
raise ValueError(msg)
|
604
|
+
|
605
|
+
# Remove points which are smaller than previous t_end
|
606
|
+
if not (larger := time_points >= prior_t_end).all():
|
607
|
+
msg = f"Overlapping time points. Removing: {time_points[~larger]}"
|
608
|
+
_LOGGER.warning(msg)
|
609
|
+
time_points = time_points[larger]
|
610
|
+
|
502
611
|
time, results = self.integrator.integrate_time_course(time_points=time_points)
|
503
612
|
self._handle_simulation_results(time, results, skipfirst=True)
|
504
613
|
return self
|
505
614
|
|
506
|
-
def
|
615
|
+
def simulate_protocol(
|
507
616
|
self,
|
508
617
|
protocol: pd.DataFrame,
|
618
|
+
*,
|
509
619
|
time_points_per_step: int = 10,
|
510
620
|
) -> Self:
|
511
621
|
"""Simulate the model over a given protocol.
|
@@ -524,10 +634,81 @@ class Simulator:
|
|
524
634
|
The Simulator instance with updated results.
|
525
635
|
|
526
636
|
"""
|
637
|
+
t_start = (
|
638
|
+
0.0 if (variables := self.variables) is None else variables[-1].index[-1]
|
639
|
+
)
|
640
|
+
|
527
641
|
for t_end, pars in protocol.iterrows():
|
528
642
|
t_end = cast(pd.Timedelta, t_end)
|
529
643
|
self.model.update_parameters(pars.to_dict())
|
530
|
-
self.simulate(t_end.total_seconds(), steps=time_points_per_step)
|
644
|
+
self.simulate(t_start + t_end.total_seconds(), steps=time_points_per_step)
|
645
|
+
if self.variables is None:
|
646
|
+
break
|
647
|
+
return self
|
648
|
+
|
649
|
+
def simulate_protocol_time_course(
|
650
|
+
self,
|
651
|
+
protocol: pd.DataFrame,
|
652
|
+
time_points: ArrayLike,
|
653
|
+
*,
|
654
|
+
time_points_as_relative: bool = False,
|
655
|
+
) -> Self:
|
656
|
+
"""Simulate the model over a given protocol.
|
657
|
+
|
658
|
+
Examples:
|
659
|
+
>>> Simulator(model).simulate_over_protocol(
|
660
|
+
... protocol,
|
661
|
+
... time_points=np.array([1.0, 2.0, 3.0], dtype=float),
|
662
|
+
... )
|
663
|
+
|
664
|
+
Args:
|
665
|
+
protocol: DataFrame containing the protocol.
|
666
|
+
time_points: Array of time points for which to return the simulation values.
|
667
|
+
time_points_as_relative: Interpret time points as relative time
|
668
|
+
|
669
|
+
Notes:
|
670
|
+
This function will return **both** the control points of the protocol as well
|
671
|
+
as the time points supplied in case they don't match.
|
672
|
+
|
673
|
+
Returns:
|
674
|
+
The Simulator instance with updated results.
|
675
|
+
|
676
|
+
"""
|
677
|
+
t_start = (
|
678
|
+
0.0 if (variables := self.variables) is None else variables[-1].index[-1]
|
679
|
+
)
|
680
|
+
|
681
|
+
protocol = protocol.copy()
|
682
|
+
protocol.index = (
|
683
|
+
cast(pd.TimedeltaIndex, protocol.index) + pd.Timedelta(t_start, unit="s")
|
684
|
+
).total_seconds()
|
685
|
+
|
686
|
+
time_points = np.array(time_points, dtype=float)
|
687
|
+
if time_points_as_relative:
|
688
|
+
time_points += t_start
|
689
|
+
|
690
|
+
# Error handling
|
691
|
+
if time_points[-1] <= t_start:
|
692
|
+
msg = "End time point has to be larger than previous end time point"
|
693
|
+
raise ValueError(msg)
|
694
|
+
|
695
|
+
larger = time_points > protocol.index[-1]
|
696
|
+
if any(larger):
|
697
|
+
msg = f"Ignoring time points outside of protocol range:\n {time_points[larger]}"
|
698
|
+
_LOGGER.warning(msg)
|
699
|
+
|
700
|
+
# Continue with logic
|
701
|
+
full_time_points = protocol.index.join(time_points, how="outer")
|
702
|
+
|
703
|
+
for t_end, pars in protocol.iterrows():
|
704
|
+
self.model.update_parameters(pars.to_dict())
|
705
|
+
|
706
|
+
self.simulate_time_course(
|
707
|
+
time_points=full_time_points[
|
708
|
+
(full_time_points > t_start) & (full_time_points <= t_end)
|
709
|
+
]
|
710
|
+
)
|
711
|
+
t_start = t_end
|
531
712
|
if self.variables is None:
|
532
713
|
break
|
533
714
|
return self
|
@@ -590,8 +771,8 @@ class Simulator:
|
|
590
771
|
return None
|
591
772
|
return Result(
|
592
773
|
model=self.model,
|
593
|
-
|
594
|
-
|
774
|
+
raw_variables=variables,
|
775
|
+
raw_parameters=parameters,
|
595
776
|
)
|
596
777
|
|
597
778
|
def update_parameter(self, parameter: str, value: float) -> Self:
|