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/fit.py
CHANGED
@@ -10,39 +10,89 @@ Functions:
|
|
10
10
|
|
11
11
|
from __future__ import annotations
|
12
12
|
|
13
|
+
import logging
|
14
|
+
from copy import deepcopy
|
15
|
+
from dataclasses import dataclass
|
13
16
|
from functools import partial
|
14
17
|
from typing import TYPE_CHECKING, Protocol
|
15
18
|
|
16
19
|
import numpy as np
|
17
20
|
from scipy.optimize import minimize
|
18
21
|
|
19
|
-
from mxlpy
|
22
|
+
from mxlpy import parallel
|
20
23
|
from mxlpy.simulator import Simulator
|
21
24
|
from mxlpy.types import Array, ArrayLike, Callable, IntegratorType, cast
|
22
25
|
|
23
26
|
if TYPE_CHECKING:
|
24
27
|
import pandas as pd
|
25
28
|
|
29
|
+
from mxlpy.carousel import Carousel
|
26
30
|
from mxlpy.model import Model
|
27
31
|
|
32
|
+
LOGGER = logging.getLogger(__name__)
|
33
|
+
|
28
34
|
__all__ = [
|
35
|
+
"Bounds",
|
36
|
+
"CarouselFit",
|
37
|
+
"FitResult",
|
29
38
|
"InitialGuess",
|
39
|
+
"LOGGER",
|
30
40
|
"LossFn",
|
41
|
+
"MinResult",
|
31
42
|
"MinimizeFn",
|
32
43
|
"ProtocolResidualFn",
|
33
44
|
"ResidualFn",
|
34
45
|
"SteadyStateResidualFn",
|
35
46
|
"TimeSeriesResidualFn",
|
47
|
+
"carousel_protocol_time_course",
|
48
|
+
"carousel_steady_state",
|
49
|
+
"carousel_time_course",
|
50
|
+
"protocol_time_course",
|
36
51
|
"rmse",
|
37
52
|
"steady_state",
|
38
53
|
"time_course",
|
39
|
-
"time_course_over_protocol",
|
40
54
|
]
|
41
55
|
|
42
56
|
|
57
|
+
@dataclass
|
58
|
+
class MinResult:
|
59
|
+
"""Result of a minimization operation."""
|
60
|
+
|
61
|
+
parameters: dict[str, float]
|
62
|
+
residual: float
|
63
|
+
|
64
|
+
|
65
|
+
@dataclass
|
66
|
+
class FitResult:
|
67
|
+
"""Result of a fit operation."""
|
68
|
+
|
69
|
+
model: Model
|
70
|
+
best_pars: dict[str, float]
|
71
|
+
loss: float
|
72
|
+
|
73
|
+
|
74
|
+
@dataclass
|
75
|
+
class CarouselFit:
|
76
|
+
"""Result of a carousel fit operation."""
|
77
|
+
|
78
|
+
fits: list[FitResult]
|
79
|
+
|
80
|
+
def get_best_fit(self) -> FitResult:
|
81
|
+
"""Get the best fit from the carousel."""
|
82
|
+
return min(self.fits, key=lambda x: x.loss)
|
83
|
+
|
84
|
+
|
43
85
|
type InitialGuess = dict[str, float]
|
44
86
|
type ResidualFn = Callable[[Array], float]
|
45
|
-
type
|
87
|
+
type Bounds = dict[str, tuple[float | None, float | None]]
|
88
|
+
type MinimizeFn = Callable[
|
89
|
+
[
|
90
|
+
ResidualFn,
|
91
|
+
InitialGuess,
|
92
|
+
Bounds,
|
93
|
+
],
|
94
|
+
MinResult | None,
|
95
|
+
]
|
46
96
|
type LossFn = Callable[
|
47
97
|
[
|
48
98
|
pd.DataFrame | pd.Series,
|
@@ -70,7 +120,7 @@ class SteadyStateResidualFn(Protocol):
|
|
70
120
|
par_names: list[str],
|
71
121
|
data: pd.Series,
|
72
122
|
model: Model,
|
73
|
-
y0: dict[str, float],
|
123
|
+
y0: dict[str, float] | None,
|
74
124
|
integrator: IntegratorType,
|
75
125
|
loss_fn: LossFn,
|
76
126
|
) -> float:
|
@@ -88,7 +138,7 @@ class TimeSeriesResidualFn(Protocol):
|
|
88
138
|
par_names: list[str],
|
89
139
|
data: pd.DataFrame,
|
90
140
|
model: Model,
|
91
|
-
y0: dict[str, float],
|
141
|
+
y0: dict[str, float] | None,
|
92
142
|
integrator: IntegratorType,
|
93
143
|
loss_fn: LossFn,
|
94
144
|
) -> float:
|
@@ -106,11 +156,10 @@ class ProtocolResidualFn(Protocol):
|
|
106
156
|
par_names: list[str],
|
107
157
|
data: pd.DataFrame,
|
108
158
|
model: Model,
|
109
|
-
y0: dict[str, float],
|
159
|
+
y0: dict[str, float] | None,
|
110
160
|
integrator: IntegratorType,
|
111
161
|
loss_fn: LossFn,
|
112
162
|
protocol: pd.DataFrame,
|
113
|
-
time_points_per_step: int = 10,
|
114
163
|
) -> float:
|
115
164
|
"""Calculate residual error between model time course and experimental data."""
|
116
165
|
...
|
@@ -119,22 +168,28 @@ class ProtocolResidualFn(Protocol):
|
|
119
168
|
def _default_minimize_fn(
|
120
169
|
residual_fn: ResidualFn,
|
121
170
|
p0: dict[str, float],
|
122
|
-
|
171
|
+
bounds: Bounds,
|
172
|
+
) -> MinResult | None:
|
123
173
|
res = minimize(
|
124
174
|
residual_fn,
|
125
175
|
x0=list(p0.values()),
|
126
|
-
bounds=[(1e-
|
176
|
+
bounds=[bounds.get(name, (1e-6, 1e6)) for name in p0],
|
127
177
|
method="L-BFGS-B",
|
128
178
|
)
|
129
179
|
if res.success:
|
130
|
-
return
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
180
|
+
return MinResult(
|
181
|
+
parameters=dict(
|
182
|
+
zip(
|
183
|
+
p0,
|
184
|
+
res.x,
|
185
|
+
strict=True,
|
186
|
+
),
|
187
|
+
),
|
188
|
+
residual=res.fun,
|
136
189
|
)
|
137
|
-
|
190
|
+
|
191
|
+
LOGGER.warning("Minimisation failed.")
|
192
|
+
return None
|
138
193
|
|
139
194
|
|
140
195
|
def _steady_state_residual(
|
@@ -232,7 +287,7 @@ def _time_course_residual(
|
|
232
287
|
)
|
233
288
|
|
234
289
|
|
235
|
-
def
|
290
|
+
def _protocol_time_course_residual(
|
236
291
|
par_values: ArrayLike,
|
237
292
|
# This will be filled out by partial
|
238
293
|
par_names: list[str],
|
@@ -242,7 +297,6 @@ def _protocol_residual(
|
|
242
297
|
integrator: IntegratorType,
|
243
298
|
loss_fn: LossFn,
|
244
299
|
protocol: pd.DataFrame,
|
245
|
-
time_points_per_step: int = 10,
|
246
300
|
) -> float:
|
247
301
|
"""Calculate residual error between model time course and experimental data.
|
248
302
|
|
@@ -267,9 +321,9 @@ def _protocol_residual(
|
|
267
321
|
y0=y0,
|
268
322
|
integrator=integrator,
|
269
323
|
)
|
270
|
-
.
|
324
|
+
.simulate_protocol_time_course(
|
271
325
|
protocol=protocol,
|
272
|
-
|
326
|
+
time_points=data.index,
|
273
327
|
)
|
274
328
|
.get_result()
|
275
329
|
)
|
@@ -283,16 +337,96 @@ def _protocol_residual(
|
|
283
337
|
)
|
284
338
|
|
285
339
|
|
340
|
+
def _carousel_steady_state_worker(
|
341
|
+
model: Model,
|
342
|
+
p0: dict[str, float],
|
343
|
+
data: pd.Series,
|
344
|
+
y0: dict[str, float] | None,
|
345
|
+
integrator: IntegratorType | None,
|
346
|
+
loss_fn: LossFn,
|
347
|
+
minimize_fn: MinimizeFn,
|
348
|
+
residual_fn: SteadyStateResidualFn,
|
349
|
+
bounds: Bounds | None,
|
350
|
+
) -> FitResult | None:
|
351
|
+
model_pars = model.get_parameter_values()
|
352
|
+
|
353
|
+
return steady_state(
|
354
|
+
model,
|
355
|
+
p0={k: v for k, v in p0.items() if k in model_pars},
|
356
|
+
y0=y0,
|
357
|
+
data=data,
|
358
|
+
minimize_fn=minimize_fn,
|
359
|
+
residual_fn=residual_fn,
|
360
|
+
integrator=integrator,
|
361
|
+
loss_fn=loss_fn,
|
362
|
+
bounds=bounds,
|
363
|
+
)
|
364
|
+
|
365
|
+
|
366
|
+
def _carousel_time_course_worker(
|
367
|
+
model: Model,
|
368
|
+
p0: dict[str, float],
|
369
|
+
data: pd.DataFrame,
|
370
|
+
y0: dict[str, float] | None,
|
371
|
+
integrator: IntegratorType | None,
|
372
|
+
loss_fn: LossFn,
|
373
|
+
minimize_fn: MinimizeFn,
|
374
|
+
residual_fn: TimeSeriesResidualFn,
|
375
|
+
bounds: Bounds | None,
|
376
|
+
) -> FitResult | None:
|
377
|
+
model_pars = model.get_parameter_values()
|
378
|
+
return time_course(
|
379
|
+
model,
|
380
|
+
p0={k: v for k, v in p0.items() if k in model_pars},
|
381
|
+
y0=y0,
|
382
|
+
data=data,
|
383
|
+
minimize_fn=minimize_fn,
|
384
|
+
residual_fn=residual_fn,
|
385
|
+
integrator=integrator,
|
386
|
+
loss_fn=loss_fn,
|
387
|
+
bounds=bounds,
|
388
|
+
)
|
389
|
+
|
390
|
+
|
391
|
+
def _carousel_protocol_worker(
|
392
|
+
model: Model,
|
393
|
+
p0: dict[str, float],
|
394
|
+
data: pd.DataFrame,
|
395
|
+
protocol: pd.DataFrame,
|
396
|
+
y0: dict[str, float] | None,
|
397
|
+
integrator: IntegratorType | None,
|
398
|
+
loss_fn: LossFn,
|
399
|
+
minimize_fn: MinimizeFn,
|
400
|
+
residual_fn: ProtocolResidualFn,
|
401
|
+
bounds: Bounds | None,
|
402
|
+
) -> FitResult | None:
|
403
|
+
model_pars = model.get_parameter_values()
|
404
|
+
return protocol_time_course(
|
405
|
+
model,
|
406
|
+
p0={k: v for k, v in p0.items() if k in model_pars},
|
407
|
+
y0=y0,
|
408
|
+
protocol=protocol,
|
409
|
+
data=data,
|
410
|
+
minimize_fn=minimize_fn,
|
411
|
+
residual_fn=residual_fn,
|
412
|
+
integrator=integrator,
|
413
|
+
loss_fn=loss_fn,
|
414
|
+
bounds=bounds,
|
415
|
+
)
|
416
|
+
|
417
|
+
|
286
418
|
def steady_state(
|
287
419
|
model: Model,
|
420
|
+
*,
|
288
421
|
p0: dict[str, float],
|
289
422
|
data: pd.Series,
|
290
423
|
y0: dict[str, float] | None = None,
|
291
424
|
minimize_fn: MinimizeFn = _default_minimize_fn,
|
292
425
|
residual_fn: SteadyStateResidualFn = _steady_state_residual,
|
293
|
-
integrator: IntegratorType =
|
426
|
+
integrator: IntegratorType | None = None,
|
294
427
|
loss_fn: LossFn = rmse,
|
295
|
-
|
428
|
+
bounds: Bounds | None = None,
|
429
|
+
) -> FitResult | None:
|
296
430
|
"""Fit model parameters to steady-state experimental data.
|
297
431
|
|
298
432
|
Examples:
|
@@ -308,18 +442,19 @@ def steady_state(
|
|
308
442
|
residual_fn: Function to calculate fitting error
|
309
443
|
integrator: ODE integrator class
|
310
444
|
loss_fn: Loss function to use for residual calculation
|
445
|
+
bounds: Mapping of bounds per parameter
|
311
446
|
|
312
447
|
Returns:
|
313
448
|
dict[str, float]: Fitted parameters as {parameter_name: fitted_value}
|
314
449
|
|
315
450
|
Note:
|
316
|
-
Uses L-BFGS-B optimization with bounds [1e-
|
451
|
+
Uses L-BFGS-B optimization with bounds [1e-6, 1e6] for all parameters
|
317
452
|
|
318
453
|
"""
|
319
454
|
par_names = list(p0.keys())
|
320
455
|
|
321
456
|
# Copy to restore
|
322
|
-
p_orig = model.
|
457
|
+
p_orig = model.get_parameter_values()
|
323
458
|
|
324
459
|
fn = cast(
|
325
460
|
ResidualFn,
|
@@ -333,23 +468,31 @@ def steady_state(
|
|
333
468
|
loss_fn=loss_fn,
|
334
469
|
),
|
335
470
|
)
|
336
|
-
|
337
|
-
|
338
|
-
# Restore
|
471
|
+
min_result = minimize_fn(fn, p0, {} if bounds is None else bounds)
|
472
|
+
# Restore original model
|
339
473
|
model.update_parameters(p_orig)
|
340
|
-
|
474
|
+
if min_result is None:
|
475
|
+
return min_result
|
476
|
+
|
477
|
+
return FitResult(
|
478
|
+
model=deepcopy(model).update_parameters(min_result.parameters),
|
479
|
+
best_pars=min_result.parameters,
|
480
|
+
loss=min_result.residual,
|
481
|
+
)
|
341
482
|
|
342
483
|
|
343
484
|
def time_course(
|
344
485
|
model: Model,
|
486
|
+
*,
|
345
487
|
p0: dict[str, float],
|
346
488
|
data: pd.DataFrame,
|
347
489
|
y0: dict[str, float] | None = None,
|
348
490
|
minimize_fn: MinimizeFn = _default_minimize_fn,
|
349
491
|
residual_fn: TimeSeriesResidualFn = _time_course_residual,
|
350
|
-
integrator: IntegratorType =
|
492
|
+
integrator: IntegratorType | None = None,
|
351
493
|
loss_fn: LossFn = rmse,
|
352
|
-
|
494
|
+
bounds: Bounds | None = None,
|
495
|
+
) -> FitResult | None:
|
353
496
|
"""Fit model parameters to time course of experimental data.
|
354
497
|
|
355
498
|
Examples:
|
@@ -365,16 +508,17 @@ def time_course(
|
|
365
508
|
residual_fn: Function to calculate fitting error
|
366
509
|
integrator: ODE integrator class
|
367
510
|
loss_fn: Loss function to use for residual calculation
|
511
|
+
bounds: Mapping of bounds per parameter
|
368
512
|
|
369
513
|
Returns:
|
370
514
|
dict[str, float]: Fitted parameters as {parameter_name: fitted_value}
|
371
515
|
|
372
516
|
Note:
|
373
|
-
Uses L-BFGS-B optimization with bounds [1e-
|
517
|
+
Uses L-BFGS-B optimization with bounds [1e-6, 1e6] for all parameters
|
374
518
|
|
375
519
|
"""
|
376
520
|
par_names = list(p0.keys())
|
377
|
-
p_orig = model.
|
521
|
+
p_orig = model.get_parameter_values()
|
378
522
|
|
379
523
|
fn = cast(
|
380
524
|
ResidualFn,
|
@@ -388,23 +532,33 @@ def time_course(
|
|
388
532
|
loss_fn=loss_fn,
|
389
533
|
),
|
390
534
|
)
|
391
|
-
|
535
|
+
|
536
|
+
min_result = minimize_fn(fn, p0, {} if bounds is None else bounds)
|
537
|
+
# Restore original model
|
392
538
|
model.update_parameters(p_orig)
|
393
|
-
|
539
|
+
if min_result is None:
|
540
|
+
return min_result
|
394
541
|
|
542
|
+
return FitResult(
|
543
|
+
model=deepcopy(model).update_parameters(min_result.parameters),
|
544
|
+
best_pars=min_result.parameters,
|
545
|
+
loss=min_result.residual,
|
546
|
+
)
|
395
547
|
|
396
|
-
|
548
|
+
|
549
|
+
def protocol_time_course(
|
397
550
|
model: Model,
|
551
|
+
*,
|
398
552
|
p0: dict[str, float],
|
399
553
|
data: pd.DataFrame,
|
400
554
|
protocol: pd.DataFrame,
|
401
555
|
y0: dict[str, float] | None = None,
|
402
556
|
minimize_fn: MinimizeFn = _default_minimize_fn,
|
403
|
-
residual_fn: ProtocolResidualFn =
|
404
|
-
integrator: IntegratorType =
|
557
|
+
residual_fn: ProtocolResidualFn = _protocol_time_course_residual,
|
558
|
+
integrator: IntegratorType | None = None,
|
405
559
|
loss_fn: LossFn = rmse,
|
406
|
-
|
407
|
-
) ->
|
560
|
+
bounds: Bounds | None = None,
|
561
|
+
) -> FitResult | None:
|
408
562
|
"""Fit model parameters to time course of experimental data.
|
409
563
|
|
410
564
|
Examples:
|
@@ -422,16 +576,17 @@ def time_course_over_protocol(
|
|
422
576
|
integrator: ODE integrator class
|
423
577
|
loss_fn: Loss function to use for residual calculation
|
424
578
|
time_points_per_step: Number of time points per step in the protocol
|
579
|
+
bounds: Mapping of bounds per parameter
|
425
580
|
|
426
581
|
Returns:
|
427
582
|
dict[str, float]: Fitted parameters as {parameter_name: fitted_value}
|
428
583
|
|
429
584
|
Note:
|
430
|
-
Uses L-BFGS-B optimization with bounds [1e-
|
585
|
+
Uses L-BFGS-B optimization with bounds [1e-6, 1e6] for all parameters
|
431
586
|
|
432
587
|
"""
|
433
588
|
par_names = list(p0.keys())
|
434
|
-
p_orig = model.
|
589
|
+
p_orig = model.get_parameter_values()
|
435
590
|
|
436
591
|
fn = cast(
|
437
592
|
ResidualFn,
|
@@ -444,9 +599,196 @@ def time_course_over_protocol(
|
|
444
599
|
integrator=integrator,
|
445
600
|
loss_fn=loss_fn,
|
446
601
|
protocol=protocol,
|
447
|
-
time_points_per_step=time_points_per_step,
|
448
602
|
),
|
449
603
|
)
|
450
|
-
|
604
|
+
|
605
|
+
min_result = minimize_fn(fn, p0, {} if bounds is None else bounds)
|
606
|
+
# Restore original model
|
451
607
|
model.update_parameters(p_orig)
|
452
|
-
|
608
|
+
if min_result is None:
|
609
|
+
return min_result
|
610
|
+
|
611
|
+
return FitResult(
|
612
|
+
model=deepcopy(model).update_parameters(min_result.parameters),
|
613
|
+
best_pars=min_result.parameters,
|
614
|
+
loss=min_result.residual,
|
615
|
+
)
|
616
|
+
|
617
|
+
|
618
|
+
def carousel_steady_state(
|
619
|
+
carousel: Carousel,
|
620
|
+
*,
|
621
|
+
p0: dict[str, float],
|
622
|
+
data: pd.Series,
|
623
|
+
y0: dict[str, float] | None = None,
|
624
|
+
minimize_fn: MinimizeFn = _default_minimize_fn,
|
625
|
+
residual_fn: SteadyStateResidualFn = _steady_state_residual,
|
626
|
+
integrator: IntegratorType | None = None,
|
627
|
+
loss_fn: LossFn = rmse,
|
628
|
+
bounds: Bounds | None = None,
|
629
|
+
) -> CarouselFit:
|
630
|
+
"""Fit model parameters to steady-state experimental data over a carousel.
|
631
|
+
|
632
|
+
Examples:
|
633
|
+
>>> carousel_steady_state(carousel, p0=p0, data=data)
|
634
|
+
|
635
|
+
Args:
|
636
|
+
carousel: Model carousel to fit
|
637
|
+
p0: Initial parameter guesses as {parameter_name: value}
|
638
|
+
data: Experimental time course data
|
639
|
+
protocol: Experimental protocol
|
640
|
+
y0: Initial conditions as {species_name: value}
|
641
|
+
minimize_fn: Function to minimize fitting error
|
642
|
+
residual_fn: Function to calculate fitting error
|
643
|
+
integrator: ODE integrator class
|
644
|
+
loss_fn: Loss function to use for residual calculation
|
645
|
+
time_points_per_step: Number of time points per step in the protocol
|
646
|
+
bounds: Mapping of bounds per parameter
|
647
|
+
|
648
|
+
Returns:
|
649
|
+
dict[str, float]: Fitted parameters as {parameter_name: fitted_value}
|
650
|
+
|
651
|
+
Note:
|
652
|
+
Uses L-BFGS-B optimization with bounds [1e-6, 1e6] for all parameters
|
653
|
+
|
654
|
+
"""
|
655
|
+
return CarouselFit(
|
656
|
+
[
|
657
|
+
fit
|
658
|
+
for i in parallel.parallelise(
|
659
|
+
partial(
|
660
|
+
_carousel_steady_state_worker,
|
661
|
+
p0=p0,
|
662
|
+
data=data,
|
663
|
+
y0=y0,
|
664
|
+
integrator=integrator,
|
665
|
+
loss_fn=loss_fn,
|
666
|
+
minimize_fn=minimize_fn,
|
667
|
+
residual_fn=residual_fn,
|
668
|
+
bounds=bounds,
|
669
|
+
),
|
670
|
+
inputs=list(enumerate(carousel.variants)),
|
671
|
+
)
|
672
|
+
if (fit := i[1]) is not None
|
673
|
+
]
|
674
|
+
)
|
675
|
+
|
676
|
+
|
677
|
+
def carousel_time_course(
|
678
|
+
carousel: Carousel,
|
679
|
+
*,
|
680
|
+
p0: dict[str, float],
|
681
|
+
data: pd.DataFrame,
|
682
|
+
y0: dict[str, float] | None = None,
|
683
|
+
minimize_fn: MinimizeFn = _default_minimize_fn,
|
684
|
+
residual_fn: TimeSeriesResidualFn = _time_course_residual,
|
685
|
+
integrator: IntegratorType | None = None,
|
686
|
+
loss_fn: LossFn = rmse,
|
687
|
+
bounds: Bounds | None = None,
|
688
|
+
) -> CarouselFit:
|
689
|
+
"""Fit model parameters to time course of experimental data over a carousel.
|
690
|
+
|
691
|
+
Examples:
|
692
|
+
>>> carousel_steady_state(carousel, p0=p0, data=data)
|
693
|
+
|
694
|
+
Args:
|
695
|
+
carousel: Model carousel to fit
|
696
|
+
p0: Initial parameter guesses as {parameter_name: value}
|
697
|
+
data: Experimental time course data
|
698
|
+
protocol: Experimental protocol
|
699
|
+
y0: Initial conditions as {species_name: value}
|
700
|
+
minimize_fn: Function to minimize fitting error
|
701
|
+
residual_fn: Function to calculate fitting error
|
702
|
+
integrator: ODE integrator class
|
703
|
+
loss_fn: Loss function to use for residual calculation
|
704
|
+
time_points_per_step: Number of time points per step in the protocol
|
705
|
+
bounds: Mapping of bounds per parameter
|
706
|
+
|
707
|
+
Returns:
|
708
|
+
dict[str, float]: Fitted parameters as {parameter_name: fitted_value}
|
709
|
+
|
710
|
+
Note:
|
711
|
+
Uses L-BFGS-B optimization with bounds [1e-6, 1e6] for all parameters
|
712
|
+
|
713
|
+
"""
|
714
|
+
return CarouselFit(
|
715
|
+
[
|
716
|
+
fit
|
717
|
+
for i in parallel.parallelise(
|
718
|
+
partial(
|
719
|
+
_carousel_time_course_worker,
|
720
|
+
p0=p0,
|
721
|
+
data=data,
|
722
|
+
y0=y0,
|
723
|
+
integrator=integrator,
|
724
|
+
loss_fn=loss_fn,
|
725
|
+
minimize_fn=minimize_fn,
|
726
|
+
residual_fn=residual_fn,
|
727
|
+
bounds=bounds,
|
728
|
+
),
|
729
|
+
inputs=list(enumerate(carousel.variants)),
|
730
|
+
)
|
731
|
+
if (fit := i[1]) is not None
|
732
|
+
]
|
733
|
+
)
|
734
|
+
|
735
|
+
|
736
|
+
def carousel_protocol_time_course(
|
737
|
+
carousel: Carousel,
|
738
|
+
*,
|
739
|
+
p0: dict[str, float],
|
740
|
+
data: pd.DataFrame,
|
741
|
+
protocol: pd.DataFrame,
|
742
|
+
y0: dict[str, float] | None = None,
|
743
|
+
minimize_fn: MinimizeFn = _default_minimize_fn,
|
744
|
+
residual_fn: ProtocolResidualFn = _protocol_time_course_residual,
|
745
|
+
integrator: IntegratorType | None = None,
|
746
|
+
loss_fn: LossFn = rmse,
|
747
|
+
bounds: Bounds | None = None,
|
748
|
+
) -> CarouselFit:
|
749
|
+
"""Fit model parameters to time course of experimental data over a protocol.
|
750
|
+
|
751
|
+
Examples:
|
752
|
+
>>> carousel_steady_state(carousel, p0=p0, data=data)
|
753
|
+
|
754
|
+
Args:
|
755
|
+
carousel: Model carousel to fit
|
756
|
+
p0: Initial parameter guesses as {parameter_name: value}
|
757
|
+
data: Experimental time course data
|
758
|
+
protocol: Experimental protocol
|
759
|
+
y0: Initial conditions as {species_name: value}
|
760
|
+
minimize_fn: Function to minimize fitting error
|
761
|
+
residual_fn: Function to calculate fitting error
|
762
|
+
integrator: ODE integrator class
|
763
|
+
loss_fn: Loss function to use for residual calculation
|
764
|
+
time_points_per_step: Number of time points per step in the protocol
|
765
|
+
bounds: Mapping of bounds per parameter
|
766
|
+
|
767
|
+
Returns:
|
768
|
+
dict[str, float]: Fitted parameters as {parameter_name: fitted_value}
|
769
|
+
|
770
|
+
Note:
|
771
|
+
Uses L-BFGS-B optimization with bounds [1e-6, 1e6] for all parameters
|
772
|
+
|
773
|
+
"""
|
774
|
+
return CarouselFit(
|
775
|
+
[
|
776
|
+
fit
|
777
|
+
for i in parallel.parallelise(
|
778
|
+
partial(
|
779
|
+
_carousel_protocol_worker,
|
780
|
+
p0=p0,
|
781
|
+
data=data,
|
782
|
+
protocol=protocol,
|
783
|
+
y0=y0,
|
784
|
+
integrator=integrator,
|
785
|
+
loss_fn=loss_fn,
|
786
|
+
minimize_fn=minimize_fn,
|
787
|
+
residual_fn=residual_fn,
|
788
|
+
bounds=bounds,
|
789
|
+
),
|
790
|
+
inputs=list(enumerate(carousel.variants)),
|
791
|
+
)
|
792
|
+
if (fit := i[1]) is not None
|
793
|
+
]
|
794
|
+
)
|
mxlpy/identify.py
CHANGED
@@ -28,16 +28,15 @@ def _mc_fit_time_course_worker(
|
|
28
28
|
data: pd.DataFrame,
|
29
29
|
loss_fn: fit.LossFn,
|
30
30
|
) -> float:
|
31
|
-
|
32
|
-
return fit._time_course_residual( # noqa: SLF001
|
33
|
-
par_values=list(p_fit.values()),
|
34
|
-
par_names=list(p_fit.keys()),
|
35
|
-
data=data,
|
31
|
+
fit_result = fit.time_course(
|
36
32
|
model=model,
|
37
|
-
|
38
|
-
|
33
|
+
p0=p0.to_dict(),
|
34
|
+
data=data,
|
39
35
|
loss_fn=loss_fn,
|
40
36
|
)
|
37
|
+
if fit_result is None:
|
38
|
+
return np.inf
|
39
|
+
return fit_result.loss
|
41
40
|
|
42
41
|
|
43
42
|
def profile_likelihood(
|
@@ -60,21 +59,26 @@ def profile_likelihood(
|
|
60
59
|
|
61
60
|
"""
|
62
61
|
parameter_distributions = sample(
|
63
|
-
{
|
62
|
+
{
|
63
|
+
k: LogNormal(np.log(v), sigma=1)
|
64
|
+
for k, v in model.get_parameter_values().items()
|
65
|
+
},
|
64
66
|
n=n_random,
|
65
67
|
)
|
66
68
|
|
67
69
|
res = {}
|
68
70
|
for value in tqdm(parameter_values, desc=parameter_name):
|
69
71
|
model.update_parameter(parameter_name, value)
|
70
|
-
res[value] =
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
72
|
+
res[value] = dict(
|
73
|
+
parallelise(
|
74
|
+
partial(
|
75
|
+
_mc_fit_time_course_worker, model=model, data=data, loss_fn=loss_fn
|
76
|
+
),
|
77
|
+
inputs=list(
|
78
|
+
parameter_distributions.drop(columns=parameter_name).iterrows()
|
79
|
+
),
|
80
|
+
disable_tqdm=True,
|
81
|
+
)
|
78
82
|
)
|
79
83
|
errors = pd.DataFrame(res, dtype=float).T.abs().mean(axis=1)
|
80
84
|
errors.index.name = "fitting error"
|
mxlpy/integrators/int_scipy.py
CHANGED
@@ -98,6 +98,9 @@ class Scipy:
|
|
98
98
|
tuple[ArrayLike, ArrayLike]: Tuple containing the time points and the integrated values.
|
99
99
|
|
100
100
|
"""
|
101
|
+
if time_points[0] != self.t0:
|
102
|
+
time_points = np.insert(time_points, 0, self.t0)
|
103
|
+
|
101
104
|
res = spi.solve_ivp(
|
102
105
|
fun=self.rhs,
|
103
106
|
y0=self.y0,
|