mxlpy 0.24.0__py3-none-any.whl → 0.25.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.
@@ -1,28 +1,39 @@
1
- """Parameter Fitting Module for Metabolic Models.
1
+ """Parameter local fitting Module for Metabolic Models.
2
2
 
3
- This module provides functions foru fitting model parameters to experimental data,
4
- including both steadyd-state and time-series data fitting capabilities.e
5
-
6
- Functions:
7
- fit_steady_state: Fits parameters to steady-state experimental data
8
- fit_time_course: Fits parameters to time-series experimental data
3
+ This module provides functions for fitting model parameters to experimental data,
4
+ including both steadyd-state and time-series data fitting capabilities.
9
5
  """
10
6
 
11
7
  from __future__ import annotations
12
8
 
13
9
  import logging
10
+ from collections.abc import Callable
14
11
  from copy import deepcopy
15
12
  from dataclasses import dataclass
16
13
  from functools import partial
17
- from typing import TYPE_CHECKING, Protocol
14
+ from typing import TYPE_CHECKING, Literal
18
15
 
19
- import numpy as np
20
16
  from scipy.optimize import minimize
21
- from wadler_lindig import pformat
22
17
 
23
18
  from mxlpy import parallel
24
- from mxlpy.simulator import Simulator
25
- from mxlpy.types import Array, ArrayLike, Callable, IntegratorType, cast
19
+ from mxlpy.types import IntegratorType, cast
20
+
21
+ from .common import (
22
+ Bounds,
23
+ CarouselFit,
24
+ FitResult,
25
+ InitialGuess,
26
+ LossFn,
27
+ MinResult,
28
+ ProtocolResidualFn,
29
+ ResidualFn,
30
+ SteadyStateResidualFn,
31
+ TimeSeriesResidualFn,
32
+ _protocol_time_course_residual,
33
+ _steady_state_residual,
34
+ _time_course_residual,
35
+ rmse,
36
+ )
26
37
 
27
38
  if TYPE_CHECKING:
28
39
  import pandas as pd
@@ -33,72 +44,19 @@ if TYPE_CHECKING:
33
44
  LOGGER = logging.getLogger(__name__)
34
45
 
35
46
  __all__ = [
36
- "Bounds",
37
- "CarouselFit",
38
- "FitResult",
39
- "InitialGuess",
40
47
  "LOGGER",
41
- "LossFn",
42
- "MinResult",
43
- "MinimizeFn",
44
- "ProtocolResidualFn",
45
- "ResidualFn",
46
- "SteadyStateResidualFn",
47
- "TimeSeriesResidualFn",
48
+ "Minimizer",
49
+ "ScipyMinimizer",
48
50
  "carousel_protocol_time_course",
49
51
  "carousel_steady_state",
50
52
  "carousel_time_course",
51
53
  "protocol_time_course",
52
- "rmse",
53
54
  "steady_state",
54
55
  "time_course",
55
56
  ]
56
57
 
57
58
 
58
- @dataclass
59
- class MinResult:
60
- """Result of a minimization operation."""
61
-
62
- parameters: dict[str, float]
63
- residual: float
64
-
65
- def __repr__(self) -> str:
66
- """Return default representation."""
67
- return pformat(self)
68
-
69
-
70
- @dataclass
71
- class FitResult:
72
- """Result of a fit operation."""
73
-
74
- model: Model
75
- best_pars: dict[str, float]
76
- loss: float
77
-
78
- def __repr__(self) -> str:
79
- """Return default representation."""
80
- return pformat(self)
81
-
82
-
83
- @dataclass
84
- class CarouselFit:
85
- """Result of a carousel fit operation."""
86
-
87
- fits: list[FitResult]
88
-
89
- def __repr__(self) -> str:
90
- """Return default representation."""
91
- return pformat(self)
92
-
93
- def get_best_fit(self) -> FitResult:
94
- """Get the best fit from the carousel."""
95
- return min(self.fits, key=lambda x: x.loss)
96
-
97
-
98
- type InitialGuess = dict[str, float]
99
- type ResidualFn = Callable[[Array], float]
100
- type Bounds = dict[str, tuple[float | None, float | None]]
101
- type MinimizeFn = Callable[
59
+ type Minimizer = Callable[
102
60
  [
103
61
  ResidualFn,
104
62
  InitialGuess,
@@ -106,248 +64,68 @@ type MinimizeFn = Callable[
106
64
  ],
107
65
  MinResult | None,
108
66
  ]
109
- type LossFn = Callable[
110
- [
111
- pd.DataFrame | pd.Series,
112
- pd.DataFrame | pd.Series,
113
- ],
114
- float,
115
- ]
116
-
117
67
 
118
- def rmse(
119
- y_pred: pd.DataFrame | pd.Series,
120
- y_true: pd.DataFrame | pd.Series,
121
- ) -> float:
122
- """Calculate root mean square error between model and data."""
123
- return cast(float, np.sqrt(np.mean(np.square(y_pred - y_true))))
124
68
 
69
+ @dataclass
70
+ class ScipyMinimizer:
71
+ """Local multivariate minimization using scipy.optimize.
125
72
 
126
- class SteadyStateResidualFn(Protocol):
127
- """Protocol for steady state residual functions."""
73
+ See Also
74
+ --------
75
+ https://docs.scipy.org/doc/scipy/reference/optimize.html#local-multivariate-optimization
128
76
 
129
- def __call__(
130
- self,
131
- par_values: Array,
132
- # This will be filled out by partial
133
- par_names: list[str],
134
- data: pd.Series,
135
- model: Model,
136
- y0: dict[str, float] | None,
137
- integrator: IntegratorType,
138
- loss_fn: LossFn,
139
- ) -> float:
140
- """Calculate residual error between model steady state and experimental data."""
141
- ...
142
-
143
-
144
- class TimeSeriesResidualFn(Protocol):
145
- """Protocol for time series residual functions."""
77
+ """
146
78
 
147
- def __call__(
148
- self,
149
- par_values: Array,
150
- # This will be filled out by partial
151
- par_names: list[str],
152
- data: pd.DataFrame,
153
- model: Model,
154
- y0: dict[str, float] | None,
155
- integrator: IntegratorType,
156
- loss_fn: LossFn,
157
- ) -> float:
158
- """Calculate residual error between model time course and experimental data."""
159
- ...
160
-
161
-
162
- class ProtocolResidualFn(Protocol):
163
- """Protocol for time series residual functions."""
79
+ tol: float = 1e-6
80
+ method: Literal[
81
+ "Nelder-Mead",
82
+ "Powell",
83
+ "CG",
84
+ "BFGS",
85
+ "Newton-CG",
86
+ "L-BFGS-B",
87
+ "TNC",
88
+ "COBYLA",
89
+ "COBYQA",
90
+ "SLSQP",
91
+ "trust-constr",
92
+ "dogleg",
93
+ "trust-ncg",
94
+ "trust-exact",
95
+ "trust-krylov",
96
+ ] = "L-BFGS-B"
164
97
 
165
98
  def __call__(
166
99
  self,
167
- par_values: Array,
168
- # This will be filled out by partial
169
- par_names: list[str],
170
- data: pd.DataFrame,
171
- model: Model,
172
- y0: dict[str, float] | None,
173
- integrator: IntegratorType,
174
- loss_fn: LossFn,
175
- protocol: pd.DataFrame,
176
- ) -> float:
177
- """Calculate residual error between model time course and experimental data."""
178
- ...
179
-
180
-
181
- def _default_minimize_fn(
182
- residual_fn: ResidualFn,
183
- p0: dict[str, float],
184
- bounds: Bounds,
185
- ) -> MinResult | None:
186
- res = minimize(
187
- residual_fn,
188
- x0=list(p0.values()),
189
- bounds=[bounds.get(name, (1e-6, 1e6)) for name in p0],
190
- method="L-BFGS-B",
191
- )
192
- if res.success:
193
- return MinResult(
194
- parameters=dict(
195
- zip(
196
- p0,
197
- res.x,
198
- strict=True,
199
- ),
200
- ),
201
- residual=res.fun,
100
+ residual_fn: ResidualFn,
101
+ p0: dict[str, float],
102
+ bounds: Bounds,
103
+ ) -> MinResult | None:
104
+ """Call minimzer."""
105
+ res = minimize(
106
+ residual_fn,
107
+ x0=list(p0.values()),
108
+ bounds=[bounds.get(name, (1e-6, 1e6)) for name in p0],
109
+ method=self.method,
110
+ tol=self.tol,
202
111
  )
203
-
204
- LOGGER.warning("Minimisation failed.")
205
- return None
206
-
207
-
208
- def _steady_state_residual(
209
- par_values: Array,
210
- # This will be filled out by partial
211
- par_names: list[str],
212
- data: pd.Series,
213
- model: Model,
214
- y0: dict[str, float] | None,
215
- integrator: IntegratorType,
216
- loss_fn: LossFn,
217
- ) -> float:
218
- """Calculate residual error between model steady state and experimental data.
219
-
220
- Args:
221
- par_values: Parameter values to test
222
- data: Experimental steady state data
223
- model: Model instance to simulate
224
- y0: Initial conditions
225
- par_names: Names of parameters being fit
226
- integrator: ODE integrator class to use
227
- loss_fn: Loss function to use for residual calculation
228
-
229
- Returns:
230
- float: Root mean square error between model and data
231
-
232
- """
233
- res = (
234
- Simulator(
235
- model.update_parameters(
236
- dict(
112
+ if res.success:
113
+ return MinResult(
114
+ parameters=dict(
237
115
  zip(
238
- par_names,
239
- par_values,
116
+ p0,
117
+ res.x,
240
118
  strict=True,
241
- )
242
- )
243
- ),
244
- y0=y0,
245
- integrator=integrator,
246
- )
247
- .simulate_to_steady_state()
248
- .get_result()
249
- )
250
- if res is None:
251
- return cast(float, np.inf)
252
-
253
- return loss_fn(
254
- res.get_combined().loc[:, cast(list, data.index)],
255
- data,
256
- )
257
-
258
-
259
- def _time_course_residual(
260
- par_values: ArrayLike,
261
- # This will be filled out by partial
262
- par_names: list[str],
263
- data: pd.DataFrame,
264
- model: Model,
265
- y0: dict[str, float] | None,
266
- integrator: IntegratorType,
267
- loss_fn: LossFn,
268
- ) -> float:
269
- """Calculate residual error between model time course and experimental data.
270
-
271
- Args:
272
- par_values: Parameter values to test
273
- data: Experimental time course data
274
- model: Model instance to simulate
275
- y0: Initial conditions
276
- par_names: Names of parameters being fit
277
- integrator: ODE integrator class to use
278
- loss_fn: Loss function to use for residual calculation
279
-
280
- Returns:
281
- float: Root mean square error between model and data
282
-
283
- """
284
- res = (
285
- Simulator(
286
- model.update_parameters(dict(zip(par_names, par_values, strict=True))),
287
- y0=y0,
288
- integrator=integrator,
289
- )
290
- .simulate_time_course(cast(list, data.index))
291
- .get_result()
292
- )
293
- if res is None:
294
- return cast(float, np.inf)
295
- results_ss = res.get_combined()
296
-
297
- return loss_fn(
298
- results_ss.loc[:, cast(list, data.columns)],
299
- data,
300
- )
301
-
302
-
303
- def _protocol_time_course_residual(
304
- par_values: ArrayLike,
305
- # This will be filled out by partial
306
- par_names: list[str],
307
- data: pd.DataFrame,
308
- model: Model,
309
- y0: dict[str, float] | None,
310
- integrator: IntegratorType,
311
- loss_fn: LossFn,
312
- protocol: pd.DataFrame,
313
- ) -> float:
314
- """Calculate residual error between model time course and experimental data.
315
-
316
- Args:
317
- par_values: Parameter values to test
318
- data: Experimental time course data
319
- model: Model instance to simulate
320
- y0: Initial conditions
321
- par_names: Names of parameters being fit
322
- integrator: ODE integrator class to use
323
- loss_fn: Loss function to use for residual calculation
324
- protocol: Experimental protocol
325
- time_points_per_step: Number of time points per step in the protocol
119
+ ),
120
+ ),
121
+ residual=res.fun,
122
+ )
326
123
 
327
- Returns:
328
- float: Root mean square error between model and data
124
+ LOGGER.warning("Minimisation failed due to %s", res.message)
125
+ return None
329
126
 
330
- """
331
- res = (
332
- Simulator(
333
- model.update_parameters(dict(zip(par_names, par_values, strict=True))),
334
- y0=y0,
335
- integrator=integrator,
336
- )
337
- .simulate_protocol_time_course(
338
- protocol=protocol,
339
- time_points=data.index,
340
- )
341
- .get_result()
342
- )
343
- if res is None:
344
- return cast(float, np.inf)
345
- results_ss = res.get_combined()
346
127
 
347
- return loss_fn(
348
- results_ss.loc[:, cast(list, data.columns)],
349
- data,
350
- )
128
+ _default_minimizer = ScipyMinimizer()
351
129
 
352
130
 
353
131
  def _carousel_steady_state_worker(
@@ -357,7 +135,7 @@ def _carousel_steady_state_worker(
357
135
  y0: dict[str, float] | None,
358
136
  integrator: IntegratorType | None,
359
137
  loss_fn: LossFn,
360
- minimize_fn: MinimizeFn,
138
+ minimizer: Minimizer,
361
139
  residual_fn: SteadyStateResidualFn,
362
140
  bounds: Bounds | None,
363
141
  ) -> FitResult | None:
@@ -368,7 +146,7 @@ def _carousel_steady_state_worker(
368
146
  p0={k: v for k, v in p0.items() if k in model_pars},
369
147
  y0=y0,
370
148
  data=data,
371
- minimize_fn=minimize_fn,
149
+ minimizer=minimizer,
372
150
  residual_fn=residual_fn,
373
151
  integrator=integrator,
374
152
  loss_fn=loss_fn,
@@ -383,7 +161,7 @@ def _carousel_time_course_worker(
383
161
  y0: dict[str, float] | None,
384
162
  integrator: IntegratorType | None,
385
163
  loss_fn: LossFn,
386
- minimize_fn: MinimizeFn,
164
+ minimizer: Minimizer,
387
165
  residual_fn: TimeSeriesResidualFn,
388
166
  bounds: Bounds | None,
389
167
  ) -> FitResult | None:
@@ -393,7 +171,7 @@ def _carousel_time_course_worker(
393
171
  p0={k: v for k, v in p0.items() if k in model_pars},
394
172
  y0=y0,
395
173
  data=data,
396
- minimize_fn=minimize_fn,
174
+ minimizer=minimizer,
397
175
  residual_fn=residual_fn,
398
176
  integrator=integrator,
399
177
  loss_fn=loss_fn,
@@ -409,7 +187,7 @@ def _carousel_protocol_worker(
409
187
  y0: dict[str, float] | None,
410
188
  integrator: IntegratorType | None,
411
189
  loss_fn: LossFn,
412
- minimize_fn: MinimizeFn,
190
+ minimizer: Minimizer,
413
191
  residual_fn: ProtocolResidualFn,
414
192
  bounds: Bounds | None,
415
193
  ) -> FitResult | None:
@@ -420,7 +198,7 @@ def _carousel_protocol_worker(
420
198
  y0=y0,
421
199
  protocol=protocol,
422
200
  data=data,
423
- minimize_fn=minimize_fn,
201
+ minimizer=minimizer,
424
202
  residual_fn=residual_fn,
425
203
  integrator=integrator,
426
204
  loss_fn=loss_fn,
@@ -434,7 +212,7 @@ def steady_state(
434
212
  p0: dict[str, float],
435
213
  data: pd.Series,
436
214
  y0: dict[str, float] | None = None,
437
- minimize_fn: MinimizeFn = _default_minimize_fn,
215
+ minimizer: Minimizer = _default_minimizer,
438
216
  residual_fn: SteadyStateResidualFn = _steady_state_residual,
439
217
  integrator: IntegratorType | None = None,
440
218
  loss_fn: LossFn = rmse,
@@ -451,7 +229,7 @@ def steady_state(
451
229
  data: Experimental steady state data as pandas Series
452
230
  p0: Initial parameter guesses as {parameter_name: value}
453
231
  y0: Initial conditions as {species_name: value}
454
- minimize_fn: Function to minimize fitting error
232
+ minimizer: Function to minimize fitting error
455
233
  residual_fn: Function to calculate fitting error
456
234
  integrator: ODE integrator class
457
235
  loss_fn: Loss function to use for residual calculation
@@ -481,7 +259,7 @@ def steady_state(
481
259
  loss_fn=loss_fn,
482
260
  ),
483
261
  )
484
- min_result = minimize_fn(fn, p0, {} if bounds is None else bounds)
262
+ min_result = minimizer(fn, p0, {} if bounds is None else bounds)
485
263
  # Restore original model
486
264
  model.update_parameters(p_orig)
487
265
  if min_result is None:
@@ -500,7 +278,7 @@ def time_course(
500
278
  p0: dict[str, float],
501
279
  data: pd.DataFrame,
502
280
  y0: dict[str, float] | None = None,
503
- minimize_fn: MinimizeFn = _default_minimize_fn,
281
+ minimizer: Minimizer = _default_minimizer,
504
282
  residual_fn: TimeSeriesResidualFn = _time_course_residual,
505
283
  integrator: IntegratorType | None = None,
506
284
  loss_fn: LossFn = rmse,
@@ -517,7 +295,7 @@ def time_course(
517
295
  data: Experimental time course data
518
296
  p0: Initial parameter guesses as {parameter_name: value}
519
297
  y0: Initial conditions as {species_name: value}
520
- minimize_fn: Function to minimize fitting error
298
+ minimizer: Function to minimize fitting error
521
299
  residual_fn: Function to calculate fitting error
522
300
  integrator: ODE integrator class
523
301
  loss_fn: Loss function to use for residual calculation
@@ -546,7 +324,7 @@ def time_course(
546
324
  ),
547
325
  )
548
326
 
549
- min_result = minimize_fn(fn, p0, {} if bounds is None else bounds)
327
+ min_result = minimizer(fn, p0, {} if bounds is None else bounds)
550
328
  # Restore original model
551
329
  model.update_parameters(p_orig)
552
330
  if min_result is None:
@@ -566,7 +344,7 @@ def protocol_time_course(
566
344
  data: pd.DataFrame,
567
345
  protocol: pd.DataFrame,
568
346
  y0: dict[str, float] | None = None,
569
- minimize_fn: MinimizeFn = _default_minimize_fn,
347
+ minimizer: Minimizer = _default_minimizer,
570
348
  residual_fn: ProtocolResidualFn = _protocol_time_course_residual,
571
349
  integrator: IntegratorType | None = None,
572
350
  loss_fn: LossFn = rmse,
@@ -586,7 +364,7 @@ def protocol_time_course(
586
364
  data: Experimental time course data
587
365
  protocol: Experimental protocol
588
366
  y0: Initial conditions as {species_name: value}
589
- minimize_fn: Function to minimize fitting error
367
+ minimizer: Function to minimize fitting error
590
368
  residual_fn: Function to calculate fitting error
591
369
  integrator: ODE integrator class
592
370
  loss_fn: Loss function to use for residual calculation
@@ -617,7 +395,7 @@ def protocol_time_course(
617
395
  ),
618
396
  )
619
397
 
620
- min_result = minimize_fn(fn, p0, {} if bounds is None else bounds)
398
+ min_result = minimizer(fn, p0, {} if bounds is None else bounds)
621
399
  # Restore original model
622
400
  model.update_parameters(p_orig)
623
401
  if min_result is None:
@@ -636,7 +414,7 @@ def carousel_steady_state(
636
414
  p0: dict[str, float],
637
415
  data: pd.Series,
638
416
  y0: dict[str, float] | None = None,
639
- minimize_fn: MinimizeFn = _default_minimize_fn,
417
+ minimizer: Minimizer = _default_minimizer,
640
418
  residual_fn: SteadyStateResidualFn = _steady_state_residual,
641
419
  integrator: IntegratorType | None = None,
642
420
  loss_fn: LossFn = rmse,
@@ -653,7 +431,7 @@ def carousel_steady_state(
653
431
  data: Experimental time course data
654
432
  protocol: Experimental protocol
655
433
  y0: Initial conditions as {species_name: value}
656
- minimize_fn: Function to minimize fitting error
434
+ minimizer: Function to minimize fitting error
657
435
  residual_fn: Function to calculate fitting error
658
436
  integrator: ODE integrator class
659
437
  loss_fn: Loss function to use for residual calculation
@@ -678,7 +456,7 @@ def carousel_steady_state(
678
456
  y0=y0,
679
457
  integrator=integrator,
680
458
  loss_fn=loss_fn,
681
- minimize_fn=minimize_fn,
459
+ minimizer=minimizer,
682
460
  residual_fn=residual_fn,
683
461
  bounds=bounds,
684
462
  ),
@@ -695,7 +473,7 @@ def carousel_time_course(
695
473
  p0: dict[str, float],
696
474
  data: pd.DataFrame,
697
475
  y0: dict[str, float] | None = None,
698
- minimize_fn: MinimizeFn = _default_minimize_fn,
476
+ minimizer: Minimizer = _default_minimizer,
699
477
  residual_fn: TimeSeriesResidualFn = _time_course_residual,
700
478
  integrator: IntegratorType | None = None,
701
479
  loss_fn: LossFn = rmse,
@@ -714,7 +492,7 @@ def carousel_time_course(
714
492
  data: Experimental time course data
715
493
  protocol: Experimental protocol
716
494
  y0: Initial conditions as {species_name: value}
717
- minimize_fn: Function to minimize fitting error
495
+ minimizer: Function to minimize fitting error
718
496
  residual_fn: Function to calculate fitting error
719
497
  integrator: ODE integrator class
720
498
  loss_fn: Loss function to use for residual calculation
@@ -739,7 +517,7 @@ def carousel_time_course(
739
517
  y0=y0,
740
518
  integrator=integrator,
741
519
  loss_fn=loss_fn,
742
- minimize_fn=minimize_fn,
520
+ minimizer=minimizer,
743
521
  residual_fn=residual_fn,
744
522
  bounds=bounds,
745
523
  ),
@@ -757,7 +535,7 @@ def carousel_protocol_time_course(
757
535
  data: pd.DataFrame,
758
536
  protocol: pd.DataFrame,
759
537
  y0: dict[str, float] | None = None,
760
- minimize_fn: MinimizeFn = _default_minimize_fn,
538
+ minimizer: Minimizer = _default_minimizer,
761
539
  residual_fn: ProtocolResidualFn = _protocol_time_course_residual,
762
540
  integrator: IntegratorType | None = None,
763
541
  loss_fn: LossFn = rmse,
@@ -776,7 +554,7 @@ def carousel_protocol_time_course(
776
554
  data: Experimental time course data
777
555
  protocol: Experimental protocol
778
556
  y0: Initial conditions as {species_name: value}
779
- minimize_fn: Function to minimize fitting error
557
+ minimizer: Function to minimize fitting error
780
558
  residual_fn: Function to calculate fitting error
781
559
  integrator: ODE integrator class
782
560
  loss_fn: Loss function to use for residual calculation
@@ -802,7 +580,7 @@ def carousel_protocol_time_course(
802
580
  y0=y0,
803
581
  integrator=integrator,
804
582
  loss_fn=loss_fn,
805
- minimize_fn=minimize_fn,
583
+ minimizer=minimizer,
806
584
  residual_fn=residual_fn,
807
585
  bounds=bounds,
808
586
  ),
mxlpy/identify.py CHANGED
@@ -9,8 +9,9 @@ import numpy as np
9
9
  import pandas as pd
10
10
  from tqdm import tqdm
11
11
 
12
- from mxlpy import fit
12
+ from mxlpy import fit_local
13
13
  from mxlpy.distributions import LogNormal, sample
14
+ from mxlpy.fit.common import LossFn, rmse
14
15
  from mxlpy.parallel import parallelise
15
16
 
16
17
  if TYPE_CHECKING:
@@ -26,9 +27,9 @@ def _mc_fit_time_course_worker(
26
27
  p0: pd.Series,
27
28
  model: Model,
28
29
  data: pd.DataFrame,
29
- loss_fn: fit.LossFn,
30
+ loss_fn: fit_local.LossFn,
30
31
  ) -> float:
31
- fit_result = fit.time_course(
32
+ fit_result = fit_local.time_course(
32
33
  model=model,
33
34
  p0=p0.to_dict(),
34
35
  data=data,
@@ -45,7 +46,7 @@ def profile_likelihood(
45
46
  parameter_name: str,
46
47
  parameter_values: Array,
47
48
  n_random: int = 10,
48
- loss_fn: fit.LossFn = fit.rmse,
49
+ loss_fn: LossFn = rmse,
49
50
  ) -> pd.Series:
50
51
  """Estimate the profile likelihood of model parameters given data.
51
52