modelbase2 0.1.79__py3-none-any.whl → 0.3.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 (59) hide show
  1. modelbase2/__init__.py +148 -25
  2. modelbase2/distributions.py +336 -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/nnarchitectures.py +128 -0
  19. modelbase2/npe.py +271 -0
  20. modelbase2/parallel.py +171 -0
  21. modelbase2/parameterise.py +28 -0
  22. modelbase2/paths.py +36 -0
  23. modelbase2/plot.py +832 -0
  24. modelbase2/sbml/__init__.py +14 -0
  25. modelbase2/sbml/_data.py +77 -0
  26. modelbase2/sbml/_export.py +656 -0
  27. modelbase2/sbml/_import.py +585 -0
  28. modelbase2/sbml/_mathml.py +691 -0
  29. modelbase2/sbml/_name_conversion.py +52 -0
  30. modelbase2/sbml/_unit_conversion.py +74 -0
  31. modelbase2/scan.py +616 -0
  32. modelbase2/scope.py +96 -0
  33. modelbase2/simulator.py +635 -0
  34. modelbase2/surrogates/__init__.py +31 -0
  35. modelbase2/surrogates/_poly.py +91 -0
  36. modelbase2/surrogates/_torch.py +191 -0
  37. modelbase2/surrogates.py +316 -0
  38. modelbase2/types.py +352 -11
  39. modelbase2-0.3.0.dist-info/METADATA +93 -0
  40. modelbase2-0.3.0.dist-info/RECORD +43 -0
  41. {modelbase2-0.1.79.dist-info → modelbase2-0.3.0.dist-info}/WHEEL +1 -1
  42. modelbase2/core/__init__.py +0 -29
  43. modelbase2/core/algebraic_module_container.py +0 -130
  44. modelbase2/core/constant_container.py +0 -113
  45. modelbase2/core/data.py +0 -109
  46. modelbase2/core/name_container.py +0 -29
  47. modelbase2/core/reaction_container.py +0 -115
  48. modelbase2/core/utils.py +0 -28
  49. modelbase2/core/variable_container.py +0 -24
  50. modelbase2/ode/__init__.py +0 -13
  51. modelbase2/ode/integrator.py +0 -80
  52. modelbase2/ode/mca.py +0 -270
  53. modelbase2/ode/model.py +0 -470
  54. modelbase2/ode/simulator.py +0 -153
  55. modelbase2/utils/__init__.py +0 -0
  56. modelbase2/utils/plotting.py +0 -372
  57. modelbase2-0.1.79.dist-info/METADATA +0 -44
  58. modelbase2-0.1.79.dist-info/RECORD +0 -22
  59. {modelbase2-0.1.79.dist-info → modelbase2-0.3.0.dist-info/licenses}/LICENSE +0 -0
@@ -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