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.
Files changed (48) hide show
  1. mxlpy/__init__.py +165 -0
  2. mxlpy/distributions.py +339 -0
  3. mxlpy/experimental/__init__.py +12 -0
  4. mxlpy/experimental/diff.py +226 -0
  5. mxlpy/fit.py +291 -0
  6. mxlpy/fns.py +191 -0
  7. mxlpy/integrators/__init__.py +19 -0
  8. mxlpy/integrators/int_assimulo.py +146 -0
  9. mxlpy/integrators/int_scipy.py +146 -0
  10. mxlpy/label_map.py +610 -0
  11. mxlpy/linear_label_map.py +303 -0
  12. mxlpy/mc.py +548 -0
  13. mxlpy/mca.py +280 -0
  14. mxlpy/meta/__init__.py +11 -0
  15. mxlpy/meta/codegen_latex.py +516 -0
  16. mxlpy/meta/codegen_modebase.py +110 -0
  17. mxlpy/meta/codegen_py.py +107 -0
  18. mxlpy/meta/source_tools.py +320 -0
  19. mxlpy/model.py +1737 -0
  20. mxlpy/nn/__init__.py +10 -0
  21. mxlpy/nn/_tensorflow.py +0 -0
  22. mxlpy/nn/_torch.py +129 -0
  23. mxlpy/npe.py +277 -0
  24. mxlpy/parallel.py +171 -0
  25. mxlpy/parameterise.py +27 -0
  26. mxlpy/paths.py +36 -0
  27. mxlpy/plot.py +875 -0
  28. mxlpy/py.typed +0 -0
  29. mxlpy/sbml/__init__.py +14 -0
  30. mxlpy/sbml/_data.py +77 -0
  31. mxlpy/sbml/_export.py +644 -0
  32. mxlpy/sbml/_import.py +599 -0
  33. mxlpy/sbml/_mathml.py +691 -0
  34. mxlpy/sbml/_name_conversion.py +52 -0
  35. mxlpy/sbml/_unit_conversion.py +74 -0
  36. mxlpy/scan.py +629 -0
  37. mxlpy/simulator.py +655 -0
  38. mxlpy/surrogates/__init__.py +31 -0
  39. mxlpy/surrogates/_poly.py +97 -0
  40. mxlpy/surrogates/_torch.py +196 -0
  41. mxlpy/symbolic/__init__.py +10 -0
  42. mxlpy/symbolic/strikepy.py +582 -0
  43. mxlpy/symbolic/symbolic_model.py +75 -0
  44. mxlpy/types.py +474 -0
  45. mxlpy-0.8.0.dist-info/METADATA +106 -0
  46. mxlpy-0.8.0.dist-info/RECORD +48 -0
  47. mxlpy-0.8.0.dist-info/WHEEL +4 -0
  48. 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)