mxlpy 0.8.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 +165 -0
- mxlpy/distributions.py +339 -0
- mxlpy/experimental/__init__.py +12 -0
- mxlpy/experimental/diff.py +226 -0
- mxlpy/fit.py +291 -0
- mxlpy/fns.py +191 -0
- mxlpy/integrators/__init__.py +19 -0
- mxlpy/integrators/int_assimulo.py +146 -0
- mxlpy/integrators/int_scipy.py +146 -0
- mxlpy/label_map.py +610 -0
- mxlpy/linear_label_map.py +303 -0
- mxlpy/mc.py +548 -0
- mxlpy/mca.py +280 -0
- mxlpy/meta/__init__.py +11 -0
- mxlpy/meta/codegen_latex.py +516 -0
- mxlpy/meta/codegen_modebase.py +110 -0
- mxlpy/meta/codegen_py.py +107 -0
- mxlpy/meta/source_tools.py +320 -0
- mxlpy/model.py +1737 -0
- mxlpy/nn/__init__.py +10 -0
- mxlpy/nn/_tensorflow.py +0 -0
- mxlpy/nn/_torch.py +129 -0
- mxlpy/npe.py +277 -0
- mxlpy/parallel.py +171 -0
- mxlpy/parameterise.py +27 -0
- mxlpy/paths.py +36 -0
- mxlpy/plot.py +875 -0
- mxlpy/py.typed +0 -0
- mxlpy/sbml/__init__.py +14 -0
- mxlpy/sbml/_data.py +77 -0
- mxlpy/sbml/_export.py +644 -0
- mxlpy/sbml/_import.py +599 -0
- mxlpy/sbml/_mathml.py +691 -0
- mxlpy/sbml/_name_conversion.py +52 -0
- mxlpy/sbml/_unit_conversion.py +74 -0
- mxlpy/scan.py +629 -0
- mxlpy/simulator.py +655 -0
- mxlpy/surrogates/__init__.py +31 -0
- mxlpy/surrogates/_poly.py +97 -0
- mxlpy/surrogates/_torch.py +196 -0
- mxlpy/symbolic/__init__.py +10 -0
- mxlpy/symbolic/strikepy.py +582 -0
- mxlpy/symbolic/symbolic_model.py +75 -0
- mxlpy/types.py +474 -0
- mxlpy-0.8.0.dist-info/METADATA +106 -0
- mxlpy-0.8.0.dist-info/RECORD +48 -0
- mxlpy-0.8.0.dist-info/WHEEL +4 -0
- mxlpy-0.8.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,226 @@
|
|
1
|
+
"""Diffing utilities for comparing models."""
|
2
|
+
|
3
|
+
from collections.abc import Mapping
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
|
6
|
+
from mxlpy.model import Model
|
7
|
+
from mxlpy.types import Derived
|
8
|
+
|
9
|
+
__all__ = ["DerivedDiff", "ModelDiff", "ReactionDiff", "model_diff", "soft_eq"]
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass
|
13
|
+
class DerivedDiff:
|
14
|
+
"""Difference between two derived variables."""
|
15
|
+
|
16
|
+
args1: list[str] = field(default_factory=list)
|
17
|
+
args2: list[str] = field(default_factory=list)
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class ReactionDiff:
|
22
|
+
"""Difference between two reactions."""
|
23
|
+
|
24
|
+
args1: list[str] = field(default_factory=list)
|
25
|
+
args2: list[str] = field(default_factory=list)
|
26
|
+
stoichiometry1: dict[str, float | Derived] = field(default_factory=dict)
|
27
|
+
stoichiometry2: dict[str, float | Derived] = field(default_factory=dict)
|
28
|
+
|
29
|
+
|
30
|
+
@dataclass
|
31
|
+
class ModelDiff:
|
32
|
+
"""Difference between two models."""
|
33
|
+
|
34
|
+
missing_parameters: set[str] = field(default_factory=set)
|
35
|
+
missing_variables: set[str] = field(default_factory=set)
|
36
|
+
missing_reactions: set[str] = field(default_factory=set)
|
37
|
+
missing_surrogates: set[str] = field(default_factory=set)
|
38
|
+
missing_readouts: set[str] = field(default_factory=set)
|
39
|
+
missing_derived: set[str] = field(default_factory=set)
|
40
|
+
different_parameters: dict[str, tuple[float, float]] = field(default_factory=dict)
|
41
|
+
different_variables: dict[str, tuple[float, float]] = field(default_factory=dict)
|
42
|
+
different_reactions: dict[str, ReactionDiff] = field(default_factory=dict)
|
43
|
+
different_surrogates: dict[str, ReactionDiff] = field(default_factory=dict)
|
44
|
+
different_readouts: dict[str, DerivedDiff] = field(default_factory=dict)
|
45
|
+
different_derived: dict[str, DerivedDiff] = field(default_factory=dict)
|
46
|
+
|
47
|
+
def __str__(self) -> str:
|
48
|
+
"""Return a human-readable string representation of the diff."""
|
49
|
+
content = ["Model Diff", "----------"]
|
50
|
+
|
51
|
+
# Parameters
|
52
|
+
if self.missing_parameters:
|
53
|
+
content.append(
|
54
|
+
"Missing Parameters: {}".format(", ".join(self.missing_parameters))
|
55
|
+
)
|
56
|
+
if self.different_parameters:
|
57
|
+
content.append("Different Parameters:")
|
58
|
+
for k, (v1, v2) in self.different_parameters.items():
|
59
|
+
content.append(f" {k}: {v1} != {v2}")
|
60
|
+
|
61
|
+
# Variables
|
62
|
+
if self.missing_variables:
|
63
|
+
content.append(
|
64
|
+
"Missing Variables: {}".format(", ".join(self.missing_variables))
|
65
|
+
)
|
66
|
+
if self.different_variables:
|
67
|
+
content.append("Different Variables:")
|
68
|
+
for k, (v1, v2) in self.different_variables.items():
|
69
|
+
content.append(f" {k}: {v1} != {v2}")
|
70
|
+
|
71
|
+
# Derived
|
72
|
+
if self.missing_derived:
|
73
|
+
content.append(
|
74
|
+
"Missing Derived: {}".format(", ".join(self.missing_derived))
|
75
|
+
)
|
76
|
+
if self.different_derived:
|
77
|
+
content.append("Different Derived:")
|
78
|
+
for k, diff in self.different_derived.items():
|
79
|
+
content.append(f" {k}:")
|
80
|
+
if diff.args1 != diff.args2:
|
81
|
+
content.append(f" Args: {diff.args1} != {diff.args2}")
|
82
|
+
|
83
|
+
# Reactions
|
84
|
+
if self.missing_reactions:
|
85
|
+
content.append(
|
86
|
+
"Missing Reactions: {}".format(", ".join(self.missing_reactions))
|
87
|
+
)
|
88
|
+
if self.different_reactions:
|
89
|
+
content.append("Different Reactions:")
|
90
|
+
for k, diff in self.different_reactions.items():
|
91
|
+
content.append(f" {k}:")
|
92
|
+
if diff.args1 != diff.args2:
|
93
|
+
content.append(f" Args: {diff.args1} != {diff.args2}")
|
94
|
+
if diff.stoichiometry1 != diff.stoichiometry2:
|
95
|
+
content.append(
|
96
|
+
f" Stoichiometry: {diff.stoichiometry1} != {diff.stoichiometry2}"
|
97
|
+
)
|
98
|
+
|
99
|
+
# Surrogates
|
100
|
+
if self.missing_surrogates:
|
101
|
+
content.append(
|
102
|
+
"Missing Surrogates: {}".format(", ".join(self.missing_surrogates))
|
103
|
+
)
|
104
|
+
if self.different_surrogates:
|
105
|
+
content.append("Different Surrogates:")
|
106
|
+
for k, diff in self.different_surrogates.items():
|
107
|
+
content.append(f" {k}:")
|
108
|
+
if diff.args1 != diff.args2:
|
109
|
+
content.append(f" Args: {diff.args1} != {diff.args2}")
|
110
|
+
if diff.stoichiometry1 != diff.stoichiometry2:
|
111
|
+
content.append(
|
112
|
+
f" Stoichiometry: {diff.stoichiometry1} != {diff.stoichiometry2}"
|
113
|
+
)
|
114
|
+
return "\n".join(content)
|
115
|
+
|
116
|
+
|
117
|
+
def _soft_eq_stoichiometries(
|
118
|
+
s1: Mapping[str, float | Derived], s2: Mapping[str, float | Derived]
|
119
|
+
) -> bool:
|
120
|
+
"""Check if two stoichiometries are equal, ignoring the functions."""
|
121
|
+
if s1.keys() != s2.keys():
|
122
|
+
return False
|
123
|
+
|
124
|
+
for k, v1 in s1.items():
|
125
|
+
v2 = s2[k]
|
126
|
+
if isinstance(v1, Derived):
|
127
|
+
if not isinstance(v2, Derived):
|
128
|
+
return False
|
129
|
+
if v1.args != v2.args:
|
130
|
+
return False
|
131
|
+
elif v1 != v2:
|
132
|
+
return False
|
133
|
+
|
134
|
+
return True
|
135
|
+
|
136
|
+
|
137
|
+
def soft_eq(m1: Model, m2: Model) -> bool:
|
138
|
+
"""Check if two models are equal, ignoring the functions."""
|
139
|
+
if m1._parameters != m2._parameters: # noqa: SLF001
|
140
|
+
return False
|
141
|
+
if m1._variables != m2._variables: # noqa: SLF001
|
142
|
+
return False
|
143
|
+
for k, d1 in m1._derived.items(): # noqa: SLF001
|
144
|
+
if (d2 := m2._derived.get(k)) is None: # noqa: SLF001
|
145
|
+
return False
|
146
|
+
if d1.args != d2.args:
|
147
|
+
return False
|
148
|
+
for k, r1 in m1._readouts.items(): # noqa: SLF001
|
149
|
+
if (r2 := m2._readouts.get(k)) is None: # noqa: SLF001
|
150
|
+
return False
|
151
|
+
if r1.args != r2.args:
|
152
|
+
return False
|
153
|
+
for k, v1 in m1._reactions.items(): # noqa: SLF001
|
154
|
+
if (v2 := m2._reactions.get(k)) is None: # noqa: SLF001
|
155
|
+
return False
|
156
|
+
if v1.args != v2.args:
|
157
|
+
return False
|
158
|
+
if not _soft_eq_stoichiometries(v1.stoichiometry, v2.stoichiometry):
|
159
|
+
return False
|
160
|
+
for k, s1 in m1._surrogates.items(): # noqa: SLF001
|
161
|
+
if (s2 := m2._surrogates.get(k)) is None: # noqa: SLF001
|
162
|
+
return False
|
163
|
+
if s1.args != s2.args:
|
164
|
+
return False
|
165
|
+
if s1.stoichiometries != s2.stoichiometries:
|
166
|
+
return False
|
167
|
+
return True
|
168
|
+
|
169
|
+
|
170
|
+
def model_diff(m1: Model, m2: Model) -> ModelDiff:
|
171
|
+
"""Compute the difference between two models."""
|
172
|
+
diff = ModelDiff()
|
173
|
+
|
174
|
+
for k, v1 in m1._parameters.items(): # noqa: SLF001
|
175
|
+
if (v2 := m2._parameters.get(k)) is None: # noqa: SLF001
|
176
|
+
diff.missing_parameters.add(k)
|
177
|
+
elif v1 != v2:
|
178
|
+
diff.different_parameters[k] = (v1, v2)
|
179
|
+
|
180
|
+
for k, v1 in m1._variables.items(): # noqa: SLF001
|
181
|
+
if (v2 := m2._variables.get(k)) is None: # noqa: SLF001
|
182
|
+
diff.missing_variables.add(k)
|
183
|
+
elif v1 != v2:
|
184
|
+
diff.different_variables[k] = (v1, v2)
|
185
|
+
|
186
|
+
for k, v1 in m1._readouts.items(): # noqa: SLF001
|
187
|
+
if (v2 := m2._readouts.get(k)) is None: # noqa: SLF001
|
188
|
+
diff.missing_readouts.add(k)
|
189
|
+
elif v1.args != v2.args:
|
190
|
+
diff.different_readouts[k] = DerivedDiff(v1.args, v2.args)
|
191
|
+
|
192
|
+
for k, v1 in m1._derived.items(): # noqa: SLF001
|
193
|
+
if (v2 := m2._derived.get(k)) is None: # noqa: SLF001
|
194
|
+
diff.missing_derived.add(k)
|
195
|
+
elif v1.args != v2.args:
|
196
|
+
diff.different_derived[k] = DerivedDiff(v1.args, v2.args)
|
197
|
+
|
198
|
+
for k, v1 in m1._reactions.items(): # noqa: SLF001
|
199
|
+
if (v2 := m2._reactions.get(k)) is None: # noqa: SLF001
|
200
|
+
diff.missing_reactions.add(k)
|
201
|
+
else:
|
202
|
+
if v1.args != v2.args:
|
203
|
+
rxn_diff: ReactionDiff = diff.different_reactions.get(k, ReactionDiff())
|
204
|
+
rxn_diff.args1 = v1.args
|
205
|
+
rxn_diff.args2 = v2.args
|
206
|
+
diff.different_reactions[k] = rxn_diff
|
207
|
+
if v1.stoichiometry != v2.stoichiometry:
|
208
|
+
rxn_diff = diff.different_reactions.get(k, ReactionDiff())
|
209
|
+
rxn_diff.stoichiometry1 = dict(v1.stoichiometry)
|
210
|
+
rxn_diff.stoichiometry2 = dict(v2.stoichiometry)
|
211
|
+
diff.different_reactions[k] = rxn_diff
|
212
|
+
|
213
|
+
for k, v1 in m1._surrogates.items(): # noqa: SLF001
|
214
|
+
if (v2 := m2._surrogates.get(k)) is None: # noqa: SLF001
|
215
|
+
diff.missing_surrogates.add(k)
|
216
|
+
else:
|
217
|
+
if v1.args != v2.args:
|
218
|
+
rxn_diff = diff.different_surrogates.get(k, ReactionDiff())
|
219
|
+
rxn_diff.args1 = v1.args
|
220
|
+
rxn_diff.args2 = v2.args
|
221
|
+
if v1.stoichiometries != v2.stoichiometries:
|
222
|
+
rxn_diff = diff.different_surrogates.get(k, ReactionDiff())
|
223
|
+
rxn_diff.stoichiometry1 = dict(v1.stoichiometries) # type: ignore
|
224
|
+
rxn_diff.stoichiometry2 = dict(v2.stoichiometries) # type: ignore
|
225
|
+
|
226
|
+
return diff
|
mxlpy/fit.py
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
"""Parameter Fitting Module for Metabolic Models.
|
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
|
9
|
+
"""
|
10
|
+
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
from functools import partial
|
14
|
+
from typing import TYPE_CHECKING, Protocol
|
15
|
+
|
16
|
+
import numpy as np
|
17
|
+
from scipy.optimize import minimize
|
18
|
+
|
19
|
+
from mxlpy.integrators import DefaultIntegrator
|
20
|
+
from mxlpy.simulator import Simulator
|
21
|
+
from mxlpy.types import (
|
22
|
+
Array,
|
23
|
+
ArrayLike,
|
24
|
+
Callable,
|
25
|
+
IntegratorProtocol,
|
26
|
+
cast,
|
27
|
+
)
|
28
|
+
|
29
|
+
__all__ = [
|
30
|
+
"InitialGuess",
|
31
|
+
"MinimizeFn",
|
32
|
+
"ResidualFn",
|
33
|
+
"SteadyStateResidualFn",
|
34
|
+
"TimeSeriesResidualFn",
|
35
|
+
"steady_state",
|
36
|
+
"time_course",
|
37
|
+
]
|
38
|
+
|
39
|
+
if TYPE_CHECKING:
|
40
|
+
import pandas as pd
|
41
|
+
|
42
|
+
from mxlpy.model import Model
|
43
|
+
|
44
|
+
type InitialGuess = dict[str, float]
|
45
|
+
type ResidualFn = Callable[[Array], float]
|
46
|
+
type MinimizeFn = Callable[[ResidualFn, InitialGuess], dict[str, float]]
|
47
|
+
|
48
|
+
|
49
|
+
class SteadyStateResidualFn(Protocol):
|
50
|
+
"""Protocol for steady state residual functions."""
|
51
|
+
|
52
|
+
def __call__(
|
53
|
+
self,
|
54
|
+
par_values: Array,
|
55
|
+
# This will be filled out by partial
|
56
|
+
par_names: list[str],
|
57
|
+
data: pd.Series,
|
58
|
+
model: Model,
|
59
|
+
y0: dict[str, float],
|
60
|
+
integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
|
61
|
+
) -> float:
|
62
|
+
"""Calculate residual error between model steady state and experimental data."""
|
63
|
+
...
|
64
|
+
|
65
|
+
|
66
|
+
class TimeSeriesResidualFn(Protocol):
|
67
|
+
"""Protocol for time series residual functions."""
|
68
|
+
|
69
|
+
def __call__(
|
70
|
+
self,
|
71
|
+
par_values: Array,
|
72
|
+
# This will be filled out by partial
|
73
|
+
par_names: list[str],
|
74
|
+
data: pd.DataFrame,
|
75
|
+
model: Model,
|
76
|
+
y0: dict[str, float],
|
77
|
+
integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
|
78
|
+
) -> float:
|
79
|
+
"""Calculate residual error between model time course and experimental data."""
|
80
|
+
...
|
81
|
+
|
82
|
+
|
83
|
+
def _default_minimize_fn(
|
84
|
+
residual_fn: ResidualFn,
|
85
|
+
p0: dict[str, float],
|
86
|
+
) -> dict[str, float]:
|
87
|
+
res = minimize(
|
88
|
+
residual_fn,
|
89
|
+
x0=list(p0.values()),
|
90
|
+
bounds=[(1e-12, 1e6) for _ in range(len(p0))],
|
91
|
+
method="L-BFGS-B",
|
92
|
+
)
|
93
|
+
if res.success:
|
94
|
+
return dict(
|
95
|
+
zip(
|
96
|
+
p0,
|
97
|
+
res.x,
|
98
|
+
strict=True,
|
99
|
+
)
|
100
|
+
)
|
101
|
+
return dict(zip(p0, np.full(len(p0), np.nan, dtype=float), strict=True))
|
102
|
+
|
103
|
+
|
104
|
+
def _steady_state_residual(
|
105
|
+
par_values: Array,
|
106
|
+
# This will be filled out by partial
|
107
|
+
par_names: list[str],
|
108
|
+
data: pd.Series,
|
109
|
+
model: Model,
|
110
|
+
y0: dict[str, float] | None,
|
111
|
+
integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
|
112
|
+
) -> float:
|
113
|
+
"""Calculate residual error between model steady state and experimental data.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
par_values: Parameter values to test
|
117
|
+
data: Experimental steady state data
|
118
|
+
model: Model instance to simulate
|
119
|
+
y0: Initial conditions
|
120
|
+
par_names: Names of parameters being fit
|
121
|
+
integrator: ODE integrator class to use
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
float: Root mean square error between model and data
|
125
|
+
|
126
|
+
"""
|
127
|
+
res = (
|
128
|
+
Simulator(
|
129
|
+
model.update_parameters(
|
130
|
+
dict(
|
131
|
+
zip(
|
132
|
+
par_names,
|
133
|
+
par_values,
|
134
|
+
strict=True,
|
135
|
+
)
|
136
|
+
)
|
137
|
+
),
|
138
|
+
y0=y0,
|
139
|
+
integrator=integrator,
|
140
|
+
)
|
141
|
+
.simulate_to_steady_state()
|
142
|
+
.get_result()
|
143
|
+
)
|
144
|
+
if res is None:
|
145
|
+
return cast(float, np.inf)
|
146
|
+
results_ss = res.get_combined()
|
147
|
+
diff = data - results_ss.loc[:, data.index] # type: ignore
|
148
|
+
return cast(float, np.sqrt(np.mean(np.square(diff))))
|
149
|
+
|
150
|
+
|
151
|
+
def _time_course_residual(
|
152
|
+
par_values: ArrayLike,
|
153
|
+
# This will be filled out by partial
|
154
|
+
par_names: list[str],
|
155
|
+
data: pd.DataFrame,
|
156
|
+
model: Model,
|
157
|
+
y0: dict[str, float],
|
158
|
+
integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
|
159
|
+
) -> float:
|
160
|
+
"""Calculate residual error between model time course and experimental data.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
par_values: Parameter values to test
|
164
|
+
data: Experimental time course data
|
165
|
+
model: Model instance to simulate
|
166
|
+
y0: Initial conditions
|
167
|
+
par_names: Names of parameters being fit
|
168
|
+
integrator: ODE integrator class to use
|
169
|
+
|
170
|
+
Returns:
|
171
|
+
float: Root mean square error between model and data
|
172
|
+
|
173
|
+
"""
|
174
|
+
res = (
|
175
|
+
Simulator(
|
176
|
+
model.update_parameters(dict(zip(par_names, par_values, strict=True))),
|
177
|
+
y0=y0,
|
178
|
+
integrator=integrator,
|
179
|
+
)
|
180
|
+
.simulate_time_course(data.index) # type: ignore
|
181
|
+
.get_result()
|
182
|
+
)
|
183
|
+
if res is None:
|
184
|
+
return cast(float, np.inf)
|
185
|
+
results_ss = res.get_combined()
|
186
|
+
diff = data - results_ss.loc[:, data.columns] # type: ignore
|
187
|
+
return cast(float, np.sqrt(np.mean(np.square(diff))))
|
188
|
+
|
189
|
+
|
190
|
+
def steady_state(
|
191
|
+
model: Model,
|
192
|
+
p0: dict[str, float],
|
193
|
+
data: pd.Series,
|
194
|
+
y0: dict[str, float] | None = None,
|
195
|
+
minimize_fn: MinimizeFn = _default_minimize_fn,
|
196
|
+
residual_fn: SteadyStateResidualFn = _steady_state_residual,
|
197
|
+
integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
|
198
|
+
) -> dict[str, float]:
|
199
|
+
"""Fit model parameters to steady-state experimental data.
|
200
|
+
|
201
|
+
Examples:
|
202
|
+
>>> steady_state(model, p0, data)
|
203
|
+
{'k1': 0.1, 'k2': 0.2}
|
204
|
+
|
205
|
+
Args:
|
206
|
+
model: Model instance to fit
|
207
|
+
data: Experimental steady state data as pandas Series
|
208
|
+
p0: Initial parameter guesses as {parameter_name: value}
|
209
|
+
y0: Initial conditions as {species_name: value}
|
210
|
+
minimize_fn: Function to minimize fitting error
|
211
|
+
residual_fn: Function to calculate fitting error
|
212
|
+
integrator: ODE integrator class
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
dict[str, float]: Fitted parameters as {parameter_name: fitted_value}
|
216
|
+
|
217
|
+
Note:
|
218
|
+
Uses L-BFGS-B optimization with bounds [1e-12, 1e6] for all parameters
|
219
|
+
|
220
|
+
"""
|
221
|
+
par_names = list(p0.keys())
|
222
|
+
|
223
|
+
# Copy to restore
|
224
|
+
p_orig = model.parameters
|
225
|
+
|
226
|
+
fn = cast(
|
227
|
+
ResidualFn,
|
228
|
+
partial(
|
229
|
+
residual_fn,
|
230
|
+
data=data,
|
231
|
+
model=model,
|
232
|
+
y0=y0,
|
233
|
+
par_names=par_names,
|
234
|
+
integrator=integrator,
|
235
|
+
),
|
236
|
+
)
|
237
|
+
res = minimize_fn(fn, p0)
|
238
|
+
|
239
|
+
# Restore
|
240
|
+
model.update_parameters(p_orig)
|
241
|
+
return res
|
242
|
+
|
243
|
+
|
244
|
+
def time_course(
|
245
|
+
model: Model,
|
246
|
+
p0: dict[str, float],
|
247
|
+
data: pd.DataFrame,
|
248
|
+
y0: dict[str, float] | None = None,
|
249
|
+
minimize_fn: MinimizeFn = _default_minimize_fn,
|
250
|
+
residual_fn: TimeSeriesResidualFn = _time_course_residual,
|
251
|
+
integrator: Callable[[Callable, ArrayLike], IntegratorProtocol] = DefaultIntegrator,
|
252
|
+
) -> dict[str, float]:
|
253
|
+
"""Fit model parameters to time course of experimental data.
|
254
|
+
|
255
|
+
Examples:
|
256
|
+
>>> time_course(model, p0, data)
|
257
|
+
{'k1': 0.1, 'k2': 0.2}
|
258
|
+
|
259
|
+
Args:
|
260
|
+
model: Model instance to fit
|
261
|
+
data: Experimental time course data as pandas DataFrame
|
262
|
+
p0: Initial parameter guesses as {parameter_name: value}
|
263
|
+
y0: Initial conditions as {species_name: value}
|
264
|
+
minimize_fn: Function to minimize fitting error
|
265
|
+
residual_fn: Function to calculate fitting error
|
266
|
+
integrator: ODE integrator class
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
dict[str, float]: Fitted parameters as {parameter_name: fitted_value}
|
270
|
+
|
271
|
+
Note:
|
272
|
+
Uses L-BFGS-B optimization with bounds [1e-12, 1e6] for all parameters
|
273
|
+
|
274
|
+
"""
|
275
|
+
par_names = list(p0.keys())
|
276
|
+
p_orig = model.parameters
|
277
|
+
|
278
|
+
fn = cast(
|
279
|
+
ResidualFn,
|
280
|
+
partial(
|
281
|
+
residual_fn,
|
282
|
+
data=data,
|
283
|
+
model=model,
|
284
|
+
y0=y0,
|
285
|
+
par_names=par_names,
|
286
|
+
integrator=integrator,
|
287
|
+
),
|
288
|
+
)
|
289
|
+
res = minimize_fn(fn, p0)
|
290
|
+
model.update_parameters(p_orig)
|
291
|
+
return res
|
mxlpy/fns.py
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
"""Module containing functions for reactions and derived quatities."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from mxlpy.types import Float
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"add",
|
12
|
+
"constant",
|
13
|
+
"diffusion_1s_1p",
|
14
|
+
"div",
|
15
|
+
"mass_action_1s",
|
16
|
+
"mass_action_1s_1p",
|
17
|
+
"mass_action_2s",
|
18
|
+
"mass_action_2s_1p",
|
19
|
+
"michaelis_menten_1s",
|
20
|
+
"michaelis_menten_2s",
|
21
|
+
"michaelis_menten_3s",
|
22
|
+
"minus",
|
23
|
+
"moiety_1s",
|
24
|
+
"moiety_2s",
|
25
|
+
"mul",
|
26
|
+
"neg",
|
27
|
+
"neg_div",
|
28
|
+
"one_div",
|
29
|
+
"proportional",
|
30
|
+
"twice",
|
31
|
+
]
|
32
|
+
|
33
|
+
|
34
|
+
###############################################################################
|
35
|
+
# General functions
|
36
|
+
###############################################################################
|
37
|
+
|
38
|
+
|
39
|
+
def constant(x: Float) -> Float:
|
40
|
+
"""Constant function."""
|
41
|
+
return x
|
42
|
+
|
43
|
+
|
44
|
+
def neg(x: Float) -> Float:
|
45
|
+
"""Negation function."""
|
46
|
+
return -x
|
47
|
+
|
48
|
+
|
49
|
+
def minus(x: Float, y: Float) -> Float:
|
50
|
+
"""Subtraction function."""
|
51
|
+
return x - y
|
52
|
+
|
53
|
+
|
54
|
+
def mul(x: Float, y: Float) -> Float:
|
55
|
+
"""Multiplication function."""
|
56
|
+
return x * y
|
57
|
+
|
58
|
+
|
59
|
+
def div(x: Float, y: Float) -> Float:
|
60
|
+
"""Division function."""
|
61
|
+
return x / y
|
62
|
+
|
63
|
+
|
64
|
+
def one_div(x: Float) -> Float:
|
65
|
+
"""Reciprocal function."""
|
66
|
+
return 1.0 / x
|
67
|
+
|
68
|
+
|
69
|
+
def neg_div(x: Float, y: Float) -> Float:
|
70
|
+
"""Negated division function."""
|
71
|
+
return -x / y
|
72
|
+
|
73
|
+
|
74
|
+
def twice(x: Float) -> Float:
|
75
|
+
"""Twice function."""
|
76
|
+
return x * 2
|
77
|
+
|
78
|
+
|
79
|
+
def add(x: Float, y: Float) -> Float:
|
80
|
+
"""Proportional function."""
|
81
|
+
return x + y
|
82
|
+
|
83
|
+
|
84
|
+
def proportional(x: Float, y: Float) -> Float:
|
85
|
+
"""Proportional function."""
|
86
|
+
return x * y
|
87
|
+
|
88
|
+
|
89
|
+
###############################################################################
|
90
|
+
# Derived functions
|
91
|
+
###############################################################################
|
92
|
+
|
93
|
+
|
94
|
+
def moiety_1s(
|
95
|
+
x: Float,
|
96
|
+
x_total: Float,
|
97
|
+
) -> Float:
|
98
|
+
"""General moiety for one substrate."""
|
99
|
+
return x_total - x
|
100
|
+
|
101
|
+
|
102
|
+
def moiety_2s(
|
103
|
+
x1: Float,
|
104
|
+
x2: Float,
|
105
|
+
x_total: Float,
|
106
|
+
) -> Float:
|
107
|
+
"""General moiety for two substrates."""
|
108
|
+
return x_total - x1 - x2
|
109
|
+
|
110
|
+
|
111
|
+
###############################################################################
|
112
|
+
# Reactions: mass action type
|
113
|
+
###############################################################################
|
114
|
+
|
115
|
+
|
116
|
+
def mass_action_1s(s1: Float, k: Float) -> Float:
|
117
|
+
"""Irreversible mass action reaction with one substrate."""
|
118
|
+
return k * s1
|
119
|
+
|
120
|
+
|
121
|
+
def mass_action_1s_1p(s1: Float, p1: Float, kf: Float, kr: Float) -> Float:
|
122
|
+
"""Reversible mass action reaction with one substrate and one product."""
|
123
|
+
return kf * s1 - kr * p1
|
124
|
+
|
125
|
+
|
126
|
+
def mass_action_2s(s1: Float, s2: Float, k: Float) -> Float:
|
127
|
+
"""Irreversible mass action reaction with two substrates."""
|
128
|
+
return k * s1 * s2
|
129
|
+
|
130
|
+
|
131
|
+
def mass_action_2s_1p(s1: Float, s2: Float, p1: Float, kf: Float, kr: Float) -> Float:
|
132
|
+
"""Reversible mass action reaction with two substrates and one product."""
|
133
|
+
return kf * s1 * s2 - kr * p1
|
134
|
+
|
135
|
+
|
136
|
+
###############################################################################
|
137
|
+
# Reactions: michaelis-menten type
|
138
|
+
# For multi-molecular reactions use ping-pong kinetics as default
|
139
|
+
###############################################################################
|
140
|
+
|
141
|
+
|
142
|
+
def michaelis_menten_1s(s1: Float, vmax: Float, km1: Float) -> Float:
|
143
|
+
"""Irreversible Michaelis-Menten equation for one substrate."""
|
144
|
+
return s1 * vmax / (s1 + km1)
|
145
|
+
|
146
|
+
|
147
|
+
# def michaelis_menten_1s_1i(
|
148
|
+
# s: float,
|
149
|
+
# i: float,
|
150
|
+
# vmax: float,
|
151
|
+
# km: float,
|
152
|
+
# ki: float,
|
153
|
+
# ) -> float:
|
154
|
+
# """Irreversible Michaelis-Menten equation for one substrate and one inhibitor."""
|
155
|
+
# return vmax * s / (s + km * (1 + i / ki))
|
156
|
+
|
157
|
+
|
158
|
+
def michaelis_menten_2s(
|
159
|
+
s1: Float,
|
160
|
+
s2: Float,
|
161
|
+
vmax: Float,
|
162
|
+
km1: Float,
|
163
|
+
km2: Float,
|
164
|
+
) -> Float:
|
165
|
+
"""Michaelis-Menten equation (ping-pong) for two substrates."""
|
166
|
+
return vmax * s1 * s2 / (s1 * s2 + km1 * s2 + km2 * s1)
|
167
|
+
|
168
|
+
|
169
|
+
def michaelis_menten_3s(
|
170
|
+
s1: Float,
|
171
|
+
s2: Float,
|
172
|
+
s3: Float,
|
173
|
+
vmax: Float,
|
174
|
+
km1: Float,
|
175
|
+
km2: Float,
|
176
|
+
km3: Float,
|
177
|
+
) -> Float:
|
178
|
+
"""Michaelis-Menten equation (ping-pong) for three substrates."""
|
179
|
+
return (
|
180
|
+
vmax * s1 * s2 * s3 / (s1 * s2 + km1 * s2 * s3 + km2 * s1 * s3 + km3 * s1 * s2)
|
181
|
+
)
|
182
|
+
|
183
|
+
|
184
|
+
###############################################################################
|
185
|
+
# Reactions: michaelis-menten type
|
186
|
+
###############################################################################
|
187
|
+
|
188
|
+
|
189
|
+
def diffusion_1s_1p(inside: Float, outside: Float, k: Float) -> Float:
|
190
|
+
"""Diffusion reaction with one substrate and one product."""
|
191
|
+
return k * (outside - inside)
|