modelbase2 0.1.78__py3-none-any.whl → 0.2.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 (58) hide show
  1. modelbase2/__init__.py +138 -26
  2. modelbase2/distributions.py +306 -0
  3. modelbase2/experimental/__init__.py +17 -0
  4. modelbase2/experimental/codegen.py +239 -0
  5. modelbase2/experimental/diff.py +227 -0
  6. modelbase2/experimental/notes.md +4 -0
  7. modelbase2/experimental/tex.py +521 -0
  8. modelbase2/fit.py +284 -0
  9. modelbase2/fns.py +185 -0
  10. modelbase2/integrators/__init__.py +19 -0
  11. modelbase2/integrators/int_assimulo.py +146 -0
  12. modelbase2/integrators/int_scipy.py +147 -0
  13. modelbase2/label_map.py +610 -0
  14. modelbase2/linear_label_map.py +301 -0
  15. modelbase2/mc.py +548 -0
  16. modelbase2/mca.py +280 -0
  17. modelbase2/model.py +1621 -0
  18. modelbase2/npe.py +343 -0
  19. modelbase2/parallel.py +171 -0
  20. modelbase2/parameterise.py +28 -0
  21. modelbase2/paths.py +36 -0
  22. modelbase2/plot.py +829 -0
  23. modelbase2/sbml/__init__.py +14 -0
  24. modelbase2/sbml/_data.py +77 -0
  25. modelbase2/sbml/_export.py +656 -0
  26. modelbase2/sbml/_import.py +585 -0
  27. modelbase2/sbml/_mathml.py +691 -0
  28. modelbase2/sbml/_name_conversion.py +52 -0
  29. modelbase2/sbml/_unit_conversion.py +74 -0
  30. modelbase2/scan.py +616 -0
  31. modelbase2/scope.py +96 -0
  32. modelbase2/simulator.py +635 -0
  33. modelbase2/surrogates/__init__.py +32 -0
  34. modelbase2/surrogates/_poly.py +66 -0
  35. modelbase2/surrogates/_torch.py +249 -0
  36. modelbase2/surrogates.py +316 -0
  37. modelbase2/types.py +352 -11
  38. modelbase2-0.2.0.dist-info/METADATA +81 -0
  39. modelbase2-0.2.0.dist-info/RECORD +42 -0
  40. {modelbase2-0.1.78.dist-info → modelbase2-0.2.0.dist-info}/WHEEL +1 -1
  41. modelbase2/core/__init__.py +0 -29
  42. modelbase2/core/algebraic_module_container.py +0 -130
  43. modelbase2/core/constant_container.py +0 -113
  44. modelbase2/core/data.py +0 -109
  45. modelbase2/core/name_container.py +0 -29
  46. modelbase2/core/reaction_container.py +0 -115
  47. modelbase2/core/utils.py +0 -28
  48. modelbase2/core/variable_container.py +0 -24
  49. modelbase2/ode/__init__.py +0 -13
  50. modelbase2/ode/integrator.py +0 -80
  51. modelbase2/ode/mca.py +0 -270
  52. modelbase2/ode/model.py +0 -470
  53. modelbase2/ode/simulator.py +0 -153
  54. modelbase2/utils/__init__.py +0 -0
  55. modelbase2/utils/plotting.py +0 -372
  56. modelbase2-0.1.78.dist-info/METADATA +0 -44
  57. modelbase2-0.1.78.dist-info/RECORD +0 -22
  58. {modelbase2-0.1.78.dist-info → modelbase2-0.2.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,301 @@
1
+ """Linear Label Mapping Module for Metabolic Models.
2
+
3
+ This module implements linear label mapping functionality for tracking isotope labels
4
+ through metabolic networks. It provides utilities for:
5
+
6
+ - Generating linear isotope label combinations
7
+ - Mapping labels between substrates and products
8
+ - Processing stoichiometric coefficients for label transfer
9
+
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+ from typing import TYPE_CHECKING
16
+
17
+ from modelbase2.model import Derived, Model
18
+
19
+ __all__ = ["LinearLabelMapper"]
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Mapping
23
+
24
+ import pandas as pd
25
+
26
+
27
+ def _generate_isotope_labels(base_name: str, num_labels: int) -> list[str]:
28
+ """Generate list of isotopomer names for a compound.
29
+
30
+ Args:
31
+ base_name: Base name of the compound
32
+ num_labels: Number of label positions
33
+
34
+ Returns:
35
+ List of isotopomer names in format base_name__position
36
+
37
+ Raises:
38
+ ValueError: If num_labels <= 0
39
+
40
+ Examples:
41
+ >>> _generate_isotope_labels("x", 2)
42
+
43
+ ['x__0', 'x__1']
44
+
45
+ """
46
+ if num_labels > 0:
47
+ return [f"{base_name}__{i}" for i in range(num_labels)]
48
+ msg = f"Compound {base_name} must have labels"
49
+ raise ValueError(msg)
50
+
51
+
52
+ def _unpack_stoichiometries(
53
+ stoichiometries: Mapping[str, float | Derived],
54
+ ) -> tuple[dict[str, int], dict[str, int]]:
55
+ """Split reaction stoichiometry into substrates and products.
56
+
57
+ Args:
58
+ stoichiometries: Dictionary of {species: coefficient} pairs
59
+
60
+ Returns:
61
+ Tuple of (substrates, products) dictionaries with integer coefficients
62
+
63
+ Raises:
64
+ NotImplementedError: If derived quantities are used in stoichiometry
65
+
66
+ Examples:
67
+ >>> _unpack_stoichiometries({"A": -1, "B": 2})
68
+ ({"A": 1}, {"B": 2})
69
+
70
+ """
71
+ substrates = {}
72
+ products = {}
73
+ for k, v in stoichiometries.items():
74
+ if isinstance(v, Derived):
75
+ raise NotImplementedError
76
+
77
+ if v < 0:
78
+ substrates[k] = int(-v)
79
+ else:
80
+ products[k] = int(v)
81
+ return substrates, products
82
+
83
+
84
+ def _stoichiometry_to_duplicate_list(stoichiometry: dict[str, int]) -> list[str]:
85
+ """Convert stoichiometry dictionary to expanded list of species.
86
+
87
+ Args:
88
+ stoichiometry: Dictionary of {species: coefficient} pairs
89
+
90
+ Returns:
91
+ List with species repeated according to coefficients
92
+
93
+ Examples:
94
+ >>> _stoichiometry_to_duplicate_list({"A": 2, "B": 1})
95
+ ['A', 'A', 'B']
96
+
97
+ """
98
+ long_form: list[str] = []
99
+ for k, v in stoichiometry.items():
100
+ long_form.extend([k] * v)
101
+ return long_form
102
+
103
+
104
+ def _map_substrates_to_labelmap(
105
+ substrates: list[str], labelmap: list[int]
106
+ ) -> list[str]:
107
+ """Map substrate labels to product label positions.
108
+
109
+ Args:
110
+ substrates: List of substrate names
111
+ labelmap: List of integers mapping substrate positions to product positions
112
+
113
+ Returns:
114
+ Dictionary mapping substrate names to product label positions
115
+
116
+ Examples:
117
+ >>> _map_substrates_to_labelmap(['A', 'B'], [1, 0])
118
+ {'A': 1, 'B': 0}
119
+
120
+ """
121
+ res = ["EXT"] * len(substrates)
122
+ for substrate, pos in zip(substrates, labelmap, strict=True):
123
+ res[pos] = substrate
124
+ return res
125
+
126
+
127
+ def _add_label_influx_or_efflux(
128
+ substrates: list[str],
129
+ products: list[str],
130
+ labelmap: list[int],
131
+ ) -> tuple[list[str], list[str]]:
132
+ """Add label influx or efflux to balance substrate and product lists.
133
+
134
+ This function ensures that the substrate and product lists have equal length by adding
135
+ external ("EXT") placeholders where needed. It also validates that the labelmap contains
136
+ enough labels for all substrates.
137
+
138
+ Args:
139
+ substrates: List of substrate identifiers
140
+ products: List of product identifiers
141
+ labelmap: List of integer labels corresponding to substrate positions
142
+
143
+ Returns:
144
+ tuple: A tuple containing:
145
+ - List of substrates (possibly extended with "EXT")
146
+ - List of products (possibly extended with "EXT")
147
+
148
+ Raises:
149
+ ValueError: If the labelmap length is less than the number of substrates
150
+
151
+ """
152
+ # Add label outfluxes
153
+ if (diff := len(substrates) - len(products)) > 0:
154
+ products.extend(["EXT"] * diff)
155
+
156
+ # Label influxes
157
+ if (diff := len(products) - len(substrates)) > 0:
158
+ substrates.extend(["EXT"] * diff)
159
+
160
+ # Broken labelmap
161
+ if (diff := len(labelmap) - len(substrates)) < 0:
162
+ msg = f"Labelmap 'missing' {abs(diff)} label(s)"
163
+ raise ValueError(msg)
164
+ return substrates, products
165
+
166
+
167
+ def _relative_label_flux(label_percentage: float, v_ss: float) -> float:
168
+ """Calculate relative label flux based on label percentage and steady-state flux."""
169
+ return label_percentage * v_ss
170
+
171
+
172
+ def _one_div(y: float) -> float:
173
+ """Calculate 1/y."""
174
+ return 1 / y
175
+
176
+
177
+ def _neg_one_div(y: float) -> float:
178
+ """Calculate -1/y."""
179
+ return -1 / y
180
+
181
+
182
+ @dataclass(slots=True)
183
+ class LinearLabelMapper:
184
+ """A class to map linear labels for a given model and build a model with isotopomers.
185
+
186
+ Attributes:
187
+ model (Model): The model to which the labels are mapped.
188
+ label_variables (dict[str, int]): A dictionary mapping label names to their respective counts.
189
+ label_maps (dict[str, list[int]]): A dictionary mapping reaction names to their respective label maps.
190
+
191
+ Methods:
192
+ get_isotopomers(variables: list[str]) -> dict[str, list[str]]:
193
+ Generates isotopomers for the given variables based on label variables.
194
+
195
+ build_model(
196
+ initial_labels: dict[str, int | list[int]] | None = None
197
+ Builds and returns a model with the given concentrations, fluxes, external label, and initial labels.
198
+
199
+ """
200
+
201
+ model: Model
202
+ label_variables: dict[str, int] = field(default_factory=dict)
203
+ label_maps: dict[str, list[int]] = field(default_factory=dict)
204
+
205
+ def get_isotopomers(self, variables: list[str]) -> dict[str, list[str]]:
206
+ """Generate a dictionary of isotopomers for the given variables.
207
+
208
+ This method creates a dictionary where the keys are the variable names
209
+ provided in the `variables` list, and the values are lists of isotopomer
210
+ labels generated for each variable.
211
+
212
+ Args:
213
+ variables (list[str]): A list of variable names for which to generate
214
+ isotopomer labels.
215
+
216
+ Returns:
217
+ dict[str, list[str]]: A dictionary where the keys are the variable names
218
+ from the `variables` list, and the values are lists
219
+ of isotopomer labels for each variable.
220
+
221
+ """
222
+ isotopomers = {
223
+ name: _generate_isotope_labels(name, num)
224
+ for name, num in self.label_variables.items()
225
+ }
226
+ return {k: isotopomers[k] for k in variables}
227
+
228
+ def build_model(
229
+ self,
230
+ concs: pd.Series,
231
+ fluxes: pd.Series,
232
+ external_label: float = 1.0,
233
+ initial_labels: dict[str, int | list[int]] | None = None,
234
+ ) -> Model:
235
+ """Build a metabolic model with labeled isotopomers and reactions.
236
+
237
+ Examples:
238
+ >>> mapper = LinearLabelMapper(
239
+ ... model,
240
+ ... label_variables={"A": 2, "B": 2},
241
+ ... label_maps={"v1": [0, 1], "v2": [1, 2]},
242
+ ... )
243
+ >>> mapper.build_model(concs, fluxes)
244
+
245
+ Args:
246
+ concs : pd.Series
247
+ A pandas Series containing concentration values for metabolites.
248
+ fluxes : pd.Series
249
+ A pandas Series containing flux values for reactions.
250
+ external_label : float, optional
251
+ The label value for external metabolites, by default 1.0.
252
+ initial_labels : dict[str, int | list[int]] | None, optional
253
+ A dictionary specifying initial labeling positions for base compounds.
254
+ Keys are compound names, and values are either a single integer or a list of integers
255
+ indicating the positions to be labeled. Default is None.
256
+
257
+ """
258
+ isotopomers = {
259
+ name: _generate_isotope_labels(name, num)
260
+ for name, num in self.label_variables.items()
261
+ }
262
+ variables = {k: 0.0 for iso in isotopomers.values() for k in iso}
263
+ if initial_labels is not None:
264
+ for base_compound, label_positions in initial_labels.items():
265
+ if isinstance(label_positions, int):
266
+ label_positions = [label_positions] # noqa: PLW2901
267
+ for pos in label_positions:
268
+ variables[f"{base_compound}__{pos}"] = 1 / len(label_positions)
269
+
270
+ m = Model()
271
+ m.add_variables(variables)
272
+ m.add_parameters(concs.to_dict() | fluxes.to_dict() | {"EXT": external_label})
273
+ for rxn_name, label_map in self.label_maps.items():
274
+ rxn = self.model.reactions[rxn_name]
275
+ subs, prods = _unpack_stoichiometries(rxn.stoichiometry)
276
+
277
+ subs = _stoichiometry_to_duplicate_list(subs)
278
+ prods = _stoichiometry_to_duplicate_list(prods)
279
+ subs = [j for i in subs for j in isotopomers[i]]
280
+ prods = [j for i in prods for j in isotopomers[i]]
281
+ subs, prods = _add_label_influx_or_efflux(subs, prods, label_map)
282
+ subs = _map_substrates_to_labelmap(subs, label_map)
283
+ for i, (substrate, product) in enumerate(zip(subs, prods, strict=True)):
284
+ if substrate == product:
285
+ continue
286
+
287
+ stoichiometry = {}
288
+ if substrate != "EXT":
289
+ stoichiometry[substrate] = Derived(
290
+ _neg_one_div, [substrate.split("__")[0]]
291
+ )
292
+ if product != "EXT":
293
+ stoichiometry[product] = Derived(_one_div, [product.split("__")[0]])
294
+
295
+ m.add_reaction(
296
+ name=f"{rxn_name}__{i}",
297
+ fn=_relative_label_flux,
298
+ stoichiometry=stoichiometry,
299
+ args=[substrate, rxn_name],
300
+ )
301
+ return m