modelbase2 0.1.79__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.
- modelbase2/__init__.py +138 -26
- modelbase2/distributions.py +306 -0
- modelbase2/experimental/__init__.py +17 -0
- modelbase2/experimental/codegen.py +239 -0
- modelbase2/experimental/diff.py +227 -0
- modelbase2/experimental/notes.md +4 -0
- modelbase2/experimental/tex.py +521 -0
- modelbase2/fit.py +284 -0
- modelbase2/fns.py +185 -0
- modelbase2/integrators/__init__.py +19 -0
- modelbase2/integrators/int_assimulo.py +146 -0
- modelbase2/integrators/int_scipy.py +147 -0
- modelbase2/label_map.py +610 -0
- modelbase2/linear_label_map.py +301 -0
- modelbase2/mc.py +548 -0
- modelbase2/mca.py +280 -0
- modelbase2/model.py +1621 -0
- modelbase2/npe.py +343 -0
- modelbase2/parallel.py +171 -0
- modelbase2/parameterise.py +28 -0
- modelbase2/paths.py +36 -0
- modelbase2/plot.py +829 -0
- modelbase2/sbml/__init__.py +14 -0
- modelbase2/sbml/_data.py +77 -0
- modelbase2/sbml/_export.py +656 -0
- modelbase2/sbml/_import.py +585 -0
- modelbase2/sbml/_mathml.py +691 -0
- modelbase2/sbml/_name_conversion.py +52 -0
- modelbase2/sbml/_unit_conversion.py +74 -0
- modelbase2/scan.py +616 -0
- modelbase2/scope.py +96 -0
- modelbase2/simulator.py +635 -0
- modelbase2/surrogates/__init__.py +32 -0
- modelbase2/surrogates/_poly.py +66 -0
- modelbase2/surrogates/_torch.py +249 -0
- modelbase2/surrogates.py +316 -0
- modelbase2/types.py +352 -11
- modelbase2-0.2.0.dist-info/METADATA +81 -0
- modelbase2-0.2.0.dist-info/RECORD +42 -0
- {modelbase2-0.1.79.dist-info → modelbase2-0.2.0.dist-info}/WHEEL +1 -1
- modelbase2/core/__init__.py +0 -29
- modelbase2/core/algebraic_module_container.py +0 -130
- modelbase2/core/constant_container.py +0 -113
- modelbase2/core/data.py +0 -109
- modelbase2/core/name_container.py +0 -29
- modelbase2/core/reaction_container.py +0 -115
- modelbase2/core/utils.py +0 -28
- modelbase2/core/variable_container.py +0 -24
- modelbase2/ode/__init__.py +0 -13
- modelbase2/ode/integrator.py +0 -80
- modelbase2/ode/mca.py +0 -270
- modelbase2/ode/model.py +0 -470
- modelbase2/ode/simulator.py +0 -153
- modelbase2/utils/__init__.py +0 -0
- modelbase2/utils/plotting.py +0 -372
- modelbase2-0.1.79.dist-info/METADATA +0 -44
- modelbase2-0.1.79.dist-info/RECORD +0 -22
- {modelbase2-0.1.79.dist-info → modelbase2-0.2.0.dist-info/licenses}/LICENSE +0 -0
modelbase2/label_map.py
ADDED
@@ -0,0 +1,610 @@
|
|
1
|
+
"""Label Mapping Module for Metabolic Models.
|
2
|
+
|
3
|
+
This module provides functionality for mapping between labeled metabolites and their
|
4
|
+
isotopomers in metabolic models. It handles:
|
5
|
+
|
6
|
+
- Mapping between labeled and unlabeled species
|
7
|
+
- Generation of isotopomer combinations
|
8
|
+
- Calculation of total concentrations across isotopomers
|
9
|
+
|
10
|
+
Classes:
|
11
|
+
LabelMapper: Maps between labeled and unlabeled metabolites to their isotopomers
|
12
|
+
|
13
|
+
"""
|
14
|
+
|
15
|
+
from __future__ import annotations
|
16
|
+
|
17
|
+
import itertools as it
|
18
|
+
import re
|
19
|
+
from collections import defaultdict
|
20
|
+
from dataclasses import dataclass, field
|
21
|
+
from typing import TYPE_CHECKING, cast
|
22
|
+
|
23
|
+
import numpy as np
|
24
|
+
|
25
|
+
from modelbase2.model import Model
|
26
|
+
|
27
|
+
__all__ = ["LabelMapper"]
|
28
|
+
|
29
|
+
if TYPE_CHECKING:
|
30
|
+
from collections.abc import Callable, Mapping
|
31
|
+
|
32
|
+
|
33
|
+
def _total_concentration(*args: float) -> float:
|
34
|
+
"""Calculate sum of isotopomer concentrations.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
*args: Individual isotopomer concentrations to sum
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
float: Total concentration across all isotopomers
|
41
|
+
|
42
|
+
Examples:
|
43
|
+
>>> total_concentration(0.1, 0.2, 0.3)
|
44
|
+
0.6
|
45
|
+
|
46
|
+
"""
|
47
|
+
return cast(float, np.sum(args, axis=0))
|
48
|
+
|
49
|
+
|
50
|
+
def _generate_binary_labels(
|
51
|
+
base_name: str,
|
52
|
+
num_labels: int,
|
53
|
+
) -> list[str]:
|
54
|
+
"""Create binary label string.
|
55
|
+
|
56
|
+
Examples:
|
57
|
+
>>> _generate_binary_labels(base_name='cpd', num_labels=0)
|
58
|
+
['cpd']
|
59
|
+
|
60
|
+
>>> _generate_binary_labels(base_name='cpd', num_labels=1)
|
61
|
+
['cpd__0', 'cpd__1']
|
62
|
+
|
63
|
+
>>> _generate_binary_labels(base_name='cpd', num_labels=2)
|
64
|
+
['cpd__00', 'cpd__01', 'cpd__10', 'cpd__11']
|
65
|
+
|
66
|
+
Args:
|
67
|
+
base_name : str
|
68
|
+
Name of the compound
|
69
|
+
num_labels : int
|
70
|
+
Number of label positions in the compound
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
isotopomers : list(str)
|
74
|
+
Returns a list of all label isotopomers of the compound
|
75
|
+
|
76
|
+
"""
|
77
|
+
if num_labels > 0:
|
78
|
+
return [
|
79
|
+
base_name + "__" + "".join(i)
|
80
|
+
for i in it.product(("0", "1"), repeat=num_labels)
|
81
|
+
]
|
82
|
+
return [base_name]
|
83
|
+
|
84
|
+
|
85
|
+
def _split_label_string(
|
86
|
+
label: str,
|
87
|
+
labels_per_compound: list[int],
|
88
|
+
) -> list[str]:
|
89
|
+
"""Split label string according to labels given in label list.
|
90
|
+
|
91
|
+
The labels in the label list correspond to the number of
|
92
|
+
label positions in the compound.
|
93
|
+
|
94
|
+
Examples:
|
95
|
+
>>> _split_label_string(label="01", labels_per_compound=[2])
|
96
|
+
["01"]
|
97
|
+
|
98
|
+
>>> _split_label_string(label="01", labels_per_compound=[1, 1])
|
99
|
+
["0", "1"]
|
100
|
+
|
101
|
+
>>> _split_label_string(label="0011", labels_per_compound=[4])
|
102
|
+
["0011"]
|
103
|
+
|
104
|
+
>>> _split_label_string(label="0011", labels_per_compound=[3, 1])
|
105
|
+
["001", "1"]
|
106
|
+
|
107
|
+
>>> _split_label_string(label="0011", labels_per_compound=[2, 2])
|
108
|
+
["00", "11"]
|
109
|
+
|
110
|
+
>>> _split_label_string(label="0011", labels_per_compound=[1, 3])
|
111
|
+
["0", "011"]
|
112
|
+
|
113
|
+
Args:
|
114
|
+
label : str
|
115
|
+
Label string to split
|
116
|
+
labels_per_compound : list(int)
|
117
|
+
List of label positions per compound
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
split_labels: List of split labels
|
121
|
+
|
122
|
+
"""
|
123
|
+
split_labels = []
|
124
|
+
cnt = 0
|
125
|
+
for i in range(len(labels_per_compound)):
|
126
|
+
split_labels.append(label[cnt : cnt + labels_per_compound[i]])
|
127
|
+
cnt += labels_per_compound[i]
|
128
|
+
return split_labels
|
129
|
+
|
130
|
+
|
131
|
+
def _map_substrates_to_products(
|
132
|
+
rate_suffix: str,
|
133
|
+
labelmap: list[int],
|
134
|
+
) -> str:
|
135
|
+
"""Map the rate_suffix to products using the labelmap.
|
136
|
+
|
137
|
+
Examples:
|
138
|
+
>>> _map_substrates_to_products(rate_suffix="01", labelmap=[1, 0])
|
139
|
+
"10"
|
140
|
+
|
141
|
+
>>> _map_substrates_to_products(rate_suffix="01", labelmap=[0, 1])
|
142
|
+
"01"
|
143
|
+
|
144
|
+
>>> _map_substrates_to_products(rate_suffix="01", labelmap=[1, 1])
|
145
|
+
"11"
|
146
|
+
|
147
|
+
Args:
|
148
|
+
rate_suffix : str
|
149
|
+
Label string of the substrate
|
150
|
+
labelmap : list(int)
|
151
|
+
List of label positions per compound
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
str: Label string of the product
|
155
|
+
|
156
|
+
"""
|
157
|
+
return "".join([rate_suffix[i] for i in labelmap])
|
158
|
+
|
159
|
+
|
160
|
+
def _unpack_stoichiometries(
|
161
|
+
stoichiometries: Mapping[str, int],
|
162
|
+
) -> tuple[list[str], list[str]]:
|
163
|
+
"""Split stoichiometries into substrates and products.
|
164
|
+
|
165
|
+
Examples:
|
166
|
+
>>> _unpack_stoichiometries({"A": -1, "B": -2, "C": 1})
|
167
|
+
(["A", "B", "B"], ["C"])
|
168
|
+
|
169
|
+
Args:
|
170
|
+
stoichiometries : dict(str: int)
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
substrates : list(str)
|
174
|
+
products : list(str)
|
175
|
+
|
176
|
+
"""
|
177
|
+
substrates = []
|
178
|
+
products = []
|
179
|
+
for k, v in stoichiometries.items():
|
180
|
+
if v < 0:
|
181
|
+
substrates.extend([k] * -v)
|
182
|
+
else:
|
183
|
+
products.extend([k] * v)
|
184
|
+
return substrates, products
|
185
|
+
|
186
|
+
|
187
|
+
def _get_labels_per_variable(
|
188
|
+
label_variables: dict[str, int],
|
189
|
+
compounds: list[str],
|
190
|
+
) -> list[int]:
|
191
|
+
"""Get labels per compound.
|
192
|
+
|
193
|
+
This is used for _split_label string.
|
194
|
+
Adds 0 for non-label compounds, to show that they get no label.
|
195
|
+
|
196
|
+
Examples:
|
197
|
+
>>> _get_labels_per_variable({"A": 1, "B": 2}, ["A", "B", "C"])
|
198
|
+
[1, 2, 0]
|
199
|
+
|
200
|
+
Args:
|
201
|
+
label_variables : dict(str: int)
|
202
|
+
compounds : list(str)
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
list(int)
|
206
|
+
|
207
|
+
"""
|
208
|
+
return [label_variables.get(compound, 0) for compound in compounds]
|
209
|
+
|
210
|
+
|
211
|
+
def _repack_stoichiometries(
|
212
|
+
new_substrates: list[str],
|
213
|
+
new_products: list[str],
|
214
|
+
) -> dict[str, float]:
|
215
|
+
"""Pack substrates and products into stoichiometric dict.
|
216
|
+
|
217
|
+
Examples:
|
218
|
+
>>> _repack_stoichiometries(["A", "B"], ["C"])
|
219
|
+
{"A": -1, "B": -1, "C": 1}
|
220
|
+
|
221
|
+
Args:
|
222
|
+
new_substrates : list(str)
|
223
|
+
new_products : list(str)
|
224
|
+
|
225
|
+
Returns:
|
226
|
+
dict(str: int)
|
227
|
+
|
228
|
+
"""
|
229
|
+
new_stoichiometries: defaultdict[str, int] = defaultdict(int)
|
230
|
+
for arg in new_substrates:
|
231
|
+
new_stoichiometries[arg] -= 1
|
232
|
+
for arg in new_products:
|
233
|
+
new_stoichiometries[arg] += 1
|
234
|
+
return dict(new_stoichiometries)
|
235
|
+
|
236
|
+
|
237
|
+
def _assign_compound_labels(
|
238
|
+
base_compounds: list[str],
|
239
|
+
label_suffixes: list[str],
|
240
|
+
) -> list[str]:
|
241
|
+
"""Assign the correct suffixes.
|
242
|
+
|
243
|
+
Examples:
|
244
|
+
>>> _assign_compound_labels(["A", "B"], ["", "01"])
|
245
|
+
["A", "B__01"]
|
246
|
+
|
247
|
+
Args:
|
248
|
+
base_compounds: the names of the compounds without labels
|
249
|
+
label_suffixes: the labels to add to the compounds
|
250
|
+
|
251
|
+
Returns:
|
252
|
+
new compounds labels
|
253
|
+
|
254
|
+
"""
|
255
|
+
new_compounds = []
|
256
|
+
for i, compound in enumerate(base_compounds):
|
257
|
+
if label_suffixes[i] != "":
|
258
|
+
new_compounds.append(compound + "__" + label_suffixes[i])
|
259
|
+
else:
|
260
|
+
new_compounds.append(compound)
|
261
|
+
return new_compounds
|
262
|
+
|
263
|
+
|
264
|
+
def _get_external_labels(
|
265
|
+
*,
|
266
|
+
total_product_labels: int,
|
267
|
+
total_substrate_labels: int,
|
268
|
+
) -> str:
|
269
|
+
"""Get external labels.
|
270
|
+
|
271
|
+
Examples:
|
272
|
+
>>> _get_external_labels(total_product_labels=2, total_substrate_labels=1)
|
273
|
+
"1"
|
274
|
+
|
275
|
+
>>> _get_external_labels(total_product_labels=1, total_substrate_labels=1)
|
276
|
+
""
|
277
|
+
|
278
|
+
Args:
|
279
|
+
total_product_labels: total number of labels in the product
|
280
|
+
total_substrate_labels: total number of labels in the substrate
|
281
|
+
|
282
|
+
"""
|
283
|
+
n_external_labels = total_product_labels - total_substrate_labels
|
284
|
+
if n_external_labels > 0:
|
285
|
+
external_label_string = ["1"] * n_external_labels
|
286
|
+
return "".join(external_label_string)
|
287
|
+
return ""
|
288
|
+
|
289
|
+
|
290
|
+
def _create_isotopomer_reactions(
|
291
|
+
model: Model,
|
292
|
+
label_variables: dict[str, int],
|
293
|
+
rate_name: str,
|
294
|
+
function: Callable,
|
295
|
+
stoichiometry: Mapping[str, int],
|
296
|
+
labelmap: list[int],
|
297
|
+
args: list[str],
|
298
|
+
) -> None:
|
299
|
+
"""Create isotopomer reactions.
|
300
|
+
|
301
|
+
Examples:
|
302
|
+
>>> _create_isotopomer_reactions(
|
303
|
+
... model,
|
304
|
+
... label_variables={"A": 1, "B": 2},
|
305
|
+
... rate_name="rxn",
|
306
|
+
... function=lambda x: x,
|
307
|
+
... stoichiometry={"A": -1, "B": -2, "C": 1},
|
308
|
+
... labelmap=[0, 1],
|
309
|
+
... args=["A", "B", "C"]
|
310
|
+
... )
|
311
|
+
|
312
|
+
Args:
|
313
|
+
model: Model instance
|
314
|
+
label_variables: dict(str: int)
|
315
|
+
rate_name: str
|
316
|
+
function: Callable
|
317
|
+
stoichiometry: dict(str: int)
|
318
|
+
labelmap: list(int)
|
319
|
+
args: list(str)
|
320
|
+
|
321
|
+
"""
|
322
|
+
base_substrates, base_products = _unpack_stoichiometries(
|
323
|
+
stoichiometries=stoichiometry
|
324
|
+
)
|
325
|
+
labels_per_substrate = _get_labels_per_variable(
|
326
|
+
label_variables=label_variables,
|
327
|
+
compounds=base_substrates,
|
328
|
+
)
|
329
|
+
labels_per_product = _get_labels_per_variable(
|
330
|
+
label_variables=label_variables,
|
331
|
+
compounds=base_products,
|
332
|
+
)
|
333
|
+
total_substrate_labels = sum(labels_per_substrate)
|
334
|
+
total_product_labels = sum(labels_per_product)
|
335
|
+
|
336
|
+
if len(labelmap) - total_substrate_labels < 0:
|
337
|
+
msg = (
|
338
|
+
f"Labelmap 'missing' {abs(len(labelmap) - total_substrate_labels)} label(s)"
|
339
|
+
)
|
340
|
+
raise ValueError(msg)
|
341
|
+
|
342
|
+
external_labels = _get_external_labels(
|
343
|
+
total_product_labels=total_product_labels,
|
344
|
+
total_substrate_labels=total_substrate_labels,
|
345
|
+
)
|
346
|
+
|
347
|
+
for rate_suffix in (
|
348
|
+
"".join(i) for i in it.product(("0", "1"), repeat=total_substrate_labels)
|
349
|
+
):
|
350
|
+
rate_suffix += external_labels # noqa: PLW2901
|
351
|
+
# This is the magic
|
352
|
+
product_suffix = _map_substrates_to_products(
|
353
|
+
rate_suffix=rate_suffix, labelmap=labelmap
|
354
|
+
)
|
355
|
+
product_labels = _split_label_string(
|
356
|
+
label=product_suffix, labels_per_compound=labels_per_product
|
357
|
+
)
|
358
|
+
substrate_labels = _split_label_string(
|
359
|
+
label=rate_suffix, labels_per_compound=labels_per_substrate
|
360
|
+
)
|
361
|
+
|
362
|
+
new_substrates = _assign_compound_labels(
|
363
|
+
base_compounds=base_substrates, label_suffixes=substrate_labels
|
364
|
+
)
|
365
|
+
new_products = _assign_compound_labels(
|
366
|
+
base_compounds=base_products, label_suffixes=product_labels
|
367
|
+
)
|
368
|
+
new_stoichiometry = _repack_stoichiometries(
|
369
|
+
new_substrates=new_substrates, new_products=new_products
|
370
|
+
)
|
371
|
+
new_rate_name = rate_name + "__" + rate_suffix
|
372
|
+
|
373
|
+
replacements = dict(zip(base_substrates, new_substrates, strict=True)) | dict(
|
374
|
+
zip(base_products, new_products, strict=True)
|
375
|
+
)
|
376
|
+
|
377
|
+
model.add_reaction(
|
378
|
+
name=new_rate_name,
|
379
|
+
fn=function,
|
380
|
+
stoichiometry=new_stoichiometry,
|
381
|
+
args=[replacements.get(k, k) for k in args],
|
382
|
+
)
|
383
|
+
|
384
|
+
|
385
|
+
@dataclass(slots=True)
|
386
|
+
class LabelMapper:
|
387
|
+
"""Maps between labeled and unlabeled species in metabolic models.
|
388
|
+
|
389
|
+
Handles generation and mapping of isotopomers, including:
|
390
|
+
- Creating all possible isotopomer combinations
|
391
|
+
- Building labeled reaction networks
|
392
|
+
- Calculating total concentrations
|
393
|
+
|
394
|
+
Args:
|
395
|
+
model: Model instance to map labels for
|
396
|
+
label_variables: Dict mapping species to number of labels
|
397
|
+
label_maps: Dict mapping reactions to label transfer patterns
|
398
|
+
|
399
|
+
Examples:
|
400
|
+
>>> mapper = LabelMapper(model)
|
401
|
+
>>> isotopomers = mapper.get_isotopomers()
|
402
|
+
|
403
|
+
"""
|
404
|
+
|
405
|
+
model: Model
|
406
|
+
label_variables: dict[str, int] = field(default_factory=dict)
|
407
|
+
label_maps: dict[str, list[int]] = field(default_factory=dict)
|
408
|
+
|
409
|
+
def get_isotopomers(self) -> dict[str, list[str]]:
|
410
|
+
"""Get all possible isotopomers for each labeled species.
|
411
|
+
|
412
|
+
Examples:
|
413
|
+
>>> mapper.get_isotopomers()
|
414
|
+
{cpd: [cpd__0, cpd__1], ...}
|
415
|
+
|
416
|
+
Returns:
|
417
|
+
Dict mapping species names to lists of isotopomer names
|
418
|
+
|
419
|
+
"""
|
420
|
+
return {
|
421
|
+
name: _generate_binary_labels(base_name=name, num_labels=num)
|
422
|
+
for name, num in self.label_variables.items()
|
423
|
+
}
|
424
|
+
|
425
|
+
def get_isotopomer_of(self, name: str) -> list[str]:
|
426
|
+
"""Get all possible isotopomers for a specific species.
|
427
|
+
|
428
|
+
Examples:
|
429
|
+
>>> mapper.get_isotopomer_of("GAP")
|
430
|
+
['GAP__0', 'GAP__1']
|
431
|
+
|
432
|
+
Args:
|
433
|
+
name: Name of the labeled species
|
434
|
+
|
435
|
+
Returns:
|
436
|
+
List of isotopomer names
|
437
|
+
|
438
|
+
"""
|
439
|
+
return _generate_binary_labels(
|
440
|
+
base_name=name,
|
441
|
+
num_labels=self.label_variables[name],
|
442
|
+
)
|
443
|
+
|
444
|
+
def get_isotopomers_by_regex(self, name: str, regex: str) -> list[str]:
|
445
|
+
"""Get isotopomers matching a regex pattern.
|
446
|
+
|
447
|
+
Examples:
|
448
|
+
>>> mapper.get_isotopomers_by_regex("GAP", "GAP__1[01]")
|
449
|
+
['GAP__10', 'GAP__11]
|
450
|
+
|
451
|
+
Args:
|
452
|
+
name: Name of the labeled species
|
453
|
+
regex: Regular expression pattern to match
|
454
|
+
|
455
|
+
Returns:
|
456
|
+
List of matching isotopomer names
|
457
|
+
|
458
|
+
"""
|
459
|
+
pattern = re.compile(regex)
|
460
|
+
isotopomers = self.get_isotopomer_of(name=name)
|
461
|
+
return [i for i in isotopomers if pattern.match(i)]
|
462
|
+
|
463
|
+
def get_isotopomers_of_at_position(
|
464
|
+
self, name: str, positions: int | list[int]
|
465
|
+
) -> list[str]:
|
466
|
+
"""Get isotopomers with specific label positions.
|
467
|
+
|
468
|
+
Examples:
|
469
|
+
>>> mapper.get_isotopomers_of_at_position("cpd", 0)
|
470
|
+
['cpd__10', 'cpd__00']
|
471
|
+
>>> mapper.get_isotopomers_of_at_position("cpd", 1)
|
472
|
+
['cpd__01', 'cpd__00']
|
473
|
+
|
474
|
+
Args:
|
475
|
+
name: Name of the labeled species
|
476
|
+
positions: Single position or list of positions to match
|
477
|
+
|
478
|
+
Returns:
|
479
|
+
List of matching isotopomer names
|
480
|
+
|
481
|
+
"""
|
482
|
+
if isinstance(positions, int):
|
483
|
+
positions = [positions]
|
484
|
+
|
485
|
+
# Example for a variable with 3 labels
|
486
|
+
# position 0 => GAP__1[01][01]
|
487
|
+
# position 1 => GAP__[01]1[01]
|
488
|
+
# position 2 => GAP__[01][01]1
|
489
|
+
|
490
|
+
num_labels = self.label_variables[name]
|
491
|
+
label_positions = ["[01]"] * num_labels
|
492
|
+
for position in positions:
|
493
|
+
label_positions[position] = "1"
|
494
|
+
|
495
|
+
return self.get_isotopomers_by_regex(
|
496
|
+
name, f"{name}__{''.join(label_positions)}"
|
497
|
+
)
|
498
|
+
|
499
|
+
def get_isotopomers_of_with_n_labels(self, name: str, n_labels: int) -> list[str]:
|
500
|
+
"""Get all isotopomers of a compound that have exactly n labels.
|
501
|
+
|
502
|
+
Examples:
|
503
|
+
>>> mapper.get_isotopomers_of_with_n_labels("GAP", 2)
|
504
|
+
['GAP__110', 'GAP__101', 'GAP__011']
|
505
|
+
|
506
|
+
Args:
|
507
|
+
name: Name of the labeled species
|
508
|
+
n_labels: Number of labels to match
|
509
|
+
|
510
|
+
Returns:
|
511
|
+
List of isotopomer names with exactly n labels
|
512
|
+
|
513
|
+
|
514
|
+
"""
|
515
|
+
label_positions = self.label_variables[name]
|
516
|
+
label_patterns = [
|
517
|
+
["1" if i in positions else "0" for i in range(label_positions)]
|
518
|
+
for positions in it.combinations(range(label_positions), n_labels)
|
519
|
+
]
|
520
|
+
return [f"{name}__{''.join(i)}" for i in label_patterns]
|
521
|
+
|
522
|
+
def build_model(
|
523
|
+
self, initial_labels: dict[str, int | list[int]] | None = None
|
524
|
+
) -> Model:
|
525
|
+
"""Build new model with labeled species and reactions.
|
526
|
+
|
527
|
+
Examples:
|
528
|
+
>>> mapper = LabelMapper(
|
529
|
+
... model,
|
530
|
+
... label_variables={"A": 2, "B": 2},
|
531
|
+
... label_maps={"v1": [0, 1], "v2": [1, 2]},
|
532
|
+
... )
|
533
|
+
>>> mapper.build_model()
|
534
|
+
>>> mapper.build_model(initial_labels={"A": 1})
|
535
|
+
>>> mapper.build_model(initial_labels={"A": 1, "B": [0, 1]})
|
536
|
+
|
537
|
+
Args:
|
538
|
+
initial_labels: Dict mapping species to initial label positions.
|
539
|
+
Can be single position (int) or multiple (list).
|
540
|
+
|
541
|
+
Returns:
|
542
|
+
New Model instance with labeled components
|
543
|
+
|
544
|
+
"""
|
545
|
+
isotopomers = self.get_isotopomers()
|
546
|
+
initial_labels = {} if initial_labels is None else initial_labels
|
547
|
+
|
548
|
+
m = Model()
|
549
|
+
|
550
|
+
m.add_parameters(self.model.parameters)
|
551
|
+
|
552
|
+
for name, dp in self.model.derived_parameters.items():
|
553
|
+
m.add_derived(name, fn=dp.fn, args=dp.args)
|
554
|
+
|
555
|
+
variables: dict[str, float] = {}
|
556
|
+
for k, v in self.model.variables.items():
|
557
|
+
if (isos := isotopomers.get(k)) is None:
|
558
|
+
variables[k] = v
|
559
|
+
else:
|
560
|
+
label_pos = initial_labels.get(k)
|
561
|
+
d = zip(isos, it.repeat(0), strict=False)
|
562
|
+
variables.update(d)
|
563
|
+
if label_pos is None:
|
564
|
+
variables[isos[0]] = v
|
565
|
+
else:
|
566
|
+
if isinstance(label_pos, int):
|
567
|
+
label_pos = [label_pos]
|
568
|
+
|
569
|
+
suffix = "__" + "".join(
|
570
|
+
"1" if idx in label_pos else "0"
|
571
|
+
for idx in range(self.label_variables[k])
|
572
|
+
)
|
573
|
+
variables[f"{k}{suffix}"] = v
|
574
|
+
|
575
|
+
m.add_variables(variables)
|
576
|
+
|
577
|
+
for base_name, label_names in isotopomers.items():
|
578
|
+
m.add_derived(
|
579
|
+
name=f"{base_name}__total",
|
580
|
+
fn=_total_concentration,
|
581
|
+
args=label_names,
|
582
|
+
)
|
583
|
+
|
584
|
+
for name, dv in self.model.derived_variables.items():
|
585
|
+
m.add_derived(
|
586
|
+
name,
|
587
|
+
fn=dv.fn,
|
588
|
+
args=[f"{i}__total" if i in isotopomers else i for i in dv.args],
|
589
|
+
)
|
590
|
+
|
591
|
+
for rxn_name, rxn in self.model.reactions.items():
|
592
|
+
if (label_map := self.label_maps.get(rxn_name)) is None:
|
593
|
+
m.add_reaction(
|
594
|
+
rxn_name,
|
595
|
+
rxn.fn,
|
596
|
+
args=[f"{i}__total" if i in isotopomers else i for i in rxn.args],
|
597
|
+
stoichiometry=rxn.stoichiometry,
|
598
|
+
)
|
599
|
+
else:
|
600
|
+
_create_isotopomer_reactions(
|
601
|
+
model=m,
|
602
|
+
label_variables=self.label_variables,
|
603
|
+
rate_name=rxn_name,
|
604
|
+
stoichiometry=rxn.stoichiometry, # type: ignore
|
605
|
+
function=rxn.fn,
|
606
|
+
labelmap=label_map,
|
607
|
+
args=rxn.args,
|
608
|
+
)
|
609
|
+
|
610
|
+
return m
|