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.
- 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.78.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.78.dist-info/METADATA +0 -44
- modelbase2-0.1.78.dist-info/RECORD +0 -22
- {modelbase2-0.1.78.dist-info → modelbase2-0.2.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,585 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import math # noqa: F401 # models might need it
|
4
|
+
import re
|
5
|
+
import warnings
|
6
|
+
from collections import defaultdict
|
7
|
+
from dataclasses import dataclass, field
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import TYPE_CHECKING, Self
|
10
|
+
|
11
|
+
import libsbml
|
12
|
+
import numpy as np # noqa: F401 # models might need it
|
13
|
+
import sympy
|
14
|
+
|
15
|
+
from modelbase2.model import Model, _sort_dependencies
|
16
|
+
from modelbase2.paths import default_tmp_dir
|
17
|
+
from modelbase2.sbml._data import (
|
18
|
+
AtomicUnit,
|
19
|
+
Compartment,
|
20
|
+
CompositeUnit,
|
21
|
+
Compound,
|
22
|
+
Derived,
|
23
|
+
Function,
|
24
|
+
Parameter,
|
25
|
+
Reaction,
|
26
|
+
)
|
27
|
+
from modelbase2.sbml._mathml import parse_sbml_math
|
28
|
+
from modelbase2.sbml._name_conversion import _name_to_py
|
29
|
+
from modelbase2.sbml._unit_conversion import get_operator_mappings, get_unit_conversion
|
30
|
+
from modelbase2.types import unwrap
|
31
|
+
|
32
|
+
if TYPE_CHECKING:
|
33
|
+
from collections.abc import Callable
|
34
|
+
|
35
|
+
__all__ = [
|
36
|
+
"INDENT",
|
37
|
+
"OPERATOR_MAPPINGS",
|
38
|
+
"Parser",
|
39
|
+
"UNIT_CONVERSION",
|
40
|
+
"import_from_path",
|
41
|
+
"read",
|
42
|
+
"valid_filename",
|
43
|
+
]
|
44
|
+
|
45
|
+
UNIT_CONVERSION = get_unit_conversion()
|
46
|
+
OPERATOR_MAPPINGS = get_operator_mappings()
|
47
|
+
INDENT = " "
|
48
|
+
|
49
|
+
|
50
|
+
def _nan_to_zero(value: float) -> float:
|
51
|
+
return 0 if str(value) == "nan" else value
|
52
|
+
|
53
|
+
|
54
|
+
@dataclass(slots=True)
|
55
|
+
class Parser:
|
56
|
+
# Collections
|
57
|
+
boundary_species: set[str] = field(default_factory=set)
|
58
|
+
# Parsed stuff
|
59
|
+
atomic_units: dict[str, AtomicUnit] = field(default_factory=dict)
|
60
|
+
composite_units: dict[str, CompositeUnit] = field(default_factory=dict)
|
61
|
+
compartments: dict[str, Compartment] = field(default_factory=dict)
|
62
|
+
parameters: dict[str, Parameter] = field(default_factory=dict)
|
63
|
+
variables: dict[str, Compound] = field(default_factory=dict)
|
64
|
+
derived: dict[str, Derived] = field(default_factory=dict)
|
65
|
+
initial_assignment: dict[str, Derived] = field(default_factory=dict)
|
66
|
+
functions: dict[str, Function] = field(default_factory=dict)
|
67
|
+
reactions: dict[str, Reaction] = field(default_factory=dict)
|
68
|
+
|
69
|
+
def parse(self, file: str | Path) -> Self:
|
70
|
+
if not Path(file).exists():
|
71
|
+
msg = "Model file does not exist"
|
72
|
+
raise OSError(msg)
|
73
|
+
doc = libsbml.readSBMLFromFile(str(file))
|
74
|
+
|
75
|
+
# Check for unsupported packages
|
76
|
+
for i in range(doc.num_plugins):
|
77
|
+
if doc.getPlugin(i).getPackageName() == "comp":
|
78
|
+
msg = "No support for comp package"
|
79
|
+
raise NotImplementedError(msg)
|
80
|
+
|
81
|
+
sbml_model = doc.getModel()
|
82
|
+
if sbml_model is None:
|
83
|
+
return self
|
84
|
+
|
85
|
+
if bool(sbml_model.getConversionFactor()):
|
86
|
+
msg = "Conversion factors are currently not supported"
|
87
|
+
raise NotImplementedError(msg)
|
88
|
+
|
89
|
+
self.parse_functions(sbml_model)
|
90
|
+
self.parse_units(sbml_model)
|
91
|
+
self.parse_compartments(sbml_model)
|
92
|
+
self.parse_variables(sbml_model)
|
93
|
+
self.parse_parameters(sbml_model)
|
94
|
+
self.parse_initial_assignments(sbml_model)
|
95
|
+
self.parse_rules(sbml_model)
|
96
|
+
self.parse_constraints(sbml_model)
|
97
|
+
self.parse_reactions(sbml_model)
|
98
|
+
self.parse_events(sbml_model)
|
99
|
+
|
100
|
+
# Modifications
|
101
|
+
self._convert_substance_amount_to_concentration()
|
102
|
+
# self._rename_species_references()
|
103
|
+
return self
|
104
|
+
|
105
|
+
###############################################################################
|
106
|
+
# PARSING STAGE
|
107
|
+
###############################################################################
|
108
|
+
|
109
|
+
def parse_constraints(self, sbml_model: libsbml.Model) -> None:
|
110
|
+
if len(sbml_model.getListOfConstraints()) > 0:
|
111
|
+
msg = "modelbase does not support model constraints. "
|
112
|
+
raise NotImplementedError(msg)
|
113
|
+
|
114
|
+
def parse_events(self, sbml_model: libsbml.Model) -> None:
|
115
|
+
if len(sbml_model.getListOfEvents()) > 0:
|
116
|
+
msg = (
|
117
|
+
"modelbase does not current support events. "
|
118
|
+
"Check the file for how to integrate properly."
|
119
|
+
)
|
120
|
+
raise NotImplementedError(msg)
|
121
|
+
|
122
|
+
def parse_units(self, sbml_model: libsbml.Model) -> None:
|
123
|
+
for unit_definition in sbml_model.getListOfUnitDefinitions():
|
124
|
+
composite_id = unit_definition.getId()
|
125
|
+
local_units = []
|
126
|
+
for unit in unit_definition.getListOfUnits():
|
127
|
+
atomic_unit = AtomicUnit(
|
128
|
+
kind=UNIT_CONVERSION[unit.getKind()],
|
129
|
+
scale=unit.getScale(),
|
130
|
+
exponent=unit.getExponent(),
|
131
|
+
multiplier=unit.getMultiplier(),
|
132
|
+
)
|
133
|
+
local_units.append(atomic_unit.kind)
|
134
|
+
self.atomic_units[atomic_unit.kind] = atomic_unit
|
135
|
+
self.composite_units[composite_id] = CompositeUnit(
|
136
|
+
sbml_id=composite_id,
|
137
|
+
units=local_units,
|
138
|
+
)
|
139
|
+
|
140
|
+
def parse_compartments(self, sbml_model: libsbml.Model) -> None:
|
141
|
+
for compartment in sbml_model.getListOfCompartments():
|
142
|
+
sbml_id = _name_to_py(compartment.getId())
|
143
|
+
size = compartment.getSize()
|
144
|
+
if str(size) == "nan":
|
145
|
+
size = 0
|
146
|
+
self.compartments[sbml_id] = Compartment(
|
147
|
+
name=compartment.getName(),
|
148
|
+
dimensions=compartment.getSpatialDimensions(),
|
149
|
+
size=size,
|
150
|
+
units=compartment.getUnits(),
|
151
|
+
is_constant=compartment.getConstant(),
|
152
|
+
)
|
153
|
+
|
154
|
+
def parse_parameters(self, sbml_model: libsbml.Model) -> None:
|
155
|
+
for parameter in sbml_model.getListOfParameters():
|
156
|
+
self.parameters[_name_to_py(parameter.getId())] = Parameter(
|
157
|
+
value=_nan_to_zero(parameter.getValue()),
|
158
|
+
is_constant=parameter.getConstant(),
|
159
|
+
)
|
160
|
+
|
161
|
+
def parse_variables(self, sbml_model: libsbml.Model) -> None:
|
162
|
+
for compound in sbml_model.getListOfSpecies():
|
163
|
+
compound_id = _name_to_py(compound.getId())
|
164
|
+
if bool(compound.getConversionFactor()):
|
165
|
+
msg = "Conversion factors are not implemented. "
|
166
|
+
raise NotImplementedError(msg)
|
167
|
+
|
168
|
+
# NOTE: What the shit is this?
|
169
|
+
initial_amount = compound.getInitialAmount()
|
170
|
+
if str(initial_amount) == "nan":
|
171
|
+
initial_amount = compound.getInitialConcentration()
|
172
|
+
is_concentration = str(initial_amount) != "nan"
|
173
|
+
else:
|
174
|
+
is_concentration = False
|
175
|
+
|
176
|
+
has_boundary_condition = compound.getBoundaryCondition()
|
177
|
+
if has_boundary_condition:
|
178
|
+
self.boundary_species.add(compound_id)
|
179
|
+
|
180
|
+
self.variables[compound_id] = Compound(
|
181
|
+
compartment=compound.getCompartment(),
|
182
|
+
initial_amount=_nan_to_zero(initial_amount),
|
183
|
+
substance_units=compound.getSubstanceUnits(),
|
184
|
+
has_only_substance_units=compound.getHasOnlySubstanceUnits(),
|
185
|
+
has_boundary_condition=has_boundary_condition,
|
186
|
+
is_constant=compound.getConstant(),
|
187
|
+
is_concentration=is_concentration,
|
188
|
+
)
|
189
|
+
|
190
|
+
def parse_functions(self, sbml_model: libsbml.Model) -> None:
|
191
|
+
for func in sbml_model.getListOfFunctionDefinitions():
|
192
|
+
func_name = func.getName()
|
193
|
+
sbml_id = func.getId()
|
194
|
+
if sbml_id is None or sbml_id == "":
|
195
|
+
sbml_id = func_name
|
196
|
+
elif func_name is None or func_name == "":
|
197
|
+
func_name = sbml_id
|
198
|
+
func_name = _name_to_py(func_name)
|
199
|
+
|
200
|
+
if (node := func.getMath()) is None:
|
201
|
+
continue
|
202
|
+
body, args = parse_sbml_math(node=node)
|
203
|
+
|
204
|
+
self.functions[func_name] = Function(
|
205
|
+
body=body,
|
206
|
+
args=args,
|
207
|
+
)
|
208
|
+
|
209
|
+
###############################################################################
|
210
|
+
# Different kinds of derived values
|
211
|
+
###############################################################################
|
212
|
+
|
213
|
+
def parse_initial_assignments(self, sbml_model: libsbml.Model) -> None:
|
214
|
+
for assignment in sbml_model.getListOfInitialAssignments():
|
215
|
+
name = _name_to_py(assignment.getSymbol())
|
216
|
+
|
217
|
+
node = assignment.getMath()
|
218
|
+
if node is None:
|
219
|
+
warnings.warn(
|
220
|
+
f"Unusable math for {name}",
|
221
|
+
stacklevel=1,
|
222
|
+
)
|
223
|
+
continue
|
224
|
+
|
225
|
+
body, args = parse_sbml_math(node)
|
226
|
+
self.initial_assignment[name] = Derived(
|
227
|
+
body=body,
|
228
|
+
args=args,
|
229
|
+
)
|
230
|
+
|
231
|
+
def _parse_algebraic_rule(self, rule: libsbml.AlgebraicRule) -> None:
|
232
|
+
msg = f"Algebraic rules are not implemented for {rule.getId()}"
|
233
|
+
raise NotImplementedError(msg)
|
234
|
+
|
235
|
+
def _parse_assignment_rule(self, rule: libsbml.AssignmentRule) -> None:
|
236
|
+
if (node := rule.getMath()) is None:
|
237
|
+
return
|
238
|
+
|
239
|
+
name: str = _name_to_py(rule.getId())
|
240
|
+
body, args = parse_sbml_math(node=node)
|
241
|
+
|
242
|
+
self.derived[name] = Derived(
|
243
|
+
body=body,
|
244
|
+
args=args,
|
245
|
+
)
|
246
|
+
|
247
|
+
def _parse_rate_rule(self, rule: libsbml.RateRule) -> None:
|
248
|
+
msg = f"Skipping rate rule {rule.getId()}"
|
249
|
+
raise NotImplementedError(msg)
|
250
|
+
|
251
|
+
def parse_rules(self, sbml_model: libsbml.Model) -> None:
|
252
|
+
"""Parse rules and separate them by type."""
|
253
|
+
for rule in sbml_model.getListOfRules():
|
254
|
+
if rule.element_name == "algebraicRule":
|
255
|
+
self._parse_algebraic_rule(rule=rule)
|
256
|
+
elif rule.element_name == "assignmentRule":
|
257
|
+
self._parse_assignment_rule(rule=rule)
|
258
|
+
elif rule.element_name == "rateRule":
|
259
|
+
self._parse_rate_rule(rule=rule)
|
260
|
+
else:
|
261
|
+
msg = "Unknown rate type"
|
262
|
+
raise ValueError(msg)
|
263
|
+
|
264
|
+
def _parse_local_parameters(
|
265
|
+
self, reaction_id: str, kinetic_law: libsbml.KineticLaw
|
266
|
+
) -> dict[str, str]:
|
267
|
+
"""Parse local parameters."""
|
268
|
+
parameters_to_update = {}
|
269
|
+
for parameter in kinetic_law.getListOfLocalParameters():
|
270
|
+
old_id = _name_to_py(parameter.getId())
|
271
|
+
if old_id in self.parameters:
|
272
|
+
new_id = f"{reaction_id}__{old_id}"
|
273
|
+
parameters_to_update[old_id] = new_id
|
274
|
+
else:
|
275
|
+
new_id = old_id
|
276
|
+
self.parameters[new_id] = Parameter(
|
277
|
+
value=_nan_to_zero(parameter.getValue()),
|
278
|
+
is_constant=parameter.getConstant(),
|
279
|
+
)
|
280
|
+
# Some models apparently also write local parameters in this
|
281
|
+
for parameter in kinetic_law.getListOfParameters():
|
282
|
+
old_id = _name_to_py(parameter.getId())
|
283
|
+
if old_id in self.parameters:
|
284
|
+
new_id = f"{reaction_id}__{old_id}"
|
285
|
+
parameters_to_update[old_id] = new_id
|
286
|
+
else:
|
287
|
+
new_id = old_id
|
288
|
+
self.parameters[new_id] = Parameter(
|
289
|
+
value=_nan_to_zero(parameter.getValue()),
|
290
|
+
is_constant=parameter.getConstant(),
|
291
|
+
)
|
292
|
+
return parameters_to_update
|
293
|
+
|
294
|
+
def parse_reactions(self, sbml_model: libsbml.Model) -> None:
|
295
|
+
for reaction in sbml_model.getListOfReactions():
|
296
|
+
sbml_id = _name_to_py(reaction.getId())
|
297
|
+
kinetic_law = reaction.getKineticLaw()
|
298
|
+
if kinetic_law is None:
|
299
|
+
continue
|
300
|
+
parameters_to_update = self._parse_local_parameters(
|
301
|
+
reaction_id=sbml_id,
|
302
|
+
kinetic_law=kinetic_law,
|
303
|
+
)
|
304
|
+
|
305
|
+
node = reaction.getKineticLaw().getMath()
|
306
|
+
# FIXME: convert substance amount to concentration here
|
307
|
+
body, args = parse_sbml_math(node=node)
|
308
|
+
|
309
|
+
# Update parameter references
|
310
|
+
for old, new in parameters_to_update.items():
|
311
|
+
pat = re.compile(f"({old})" + r"\b")
|
312
|
+
body = pat.sub(new, body)
|
313
|
+
|
314
|
+
for i, arg in enumerate(args):
|
315
|
+
args[i] = parameters_to_update.get(arg, arg)
|
316
|
+
|
317
|
+
dynamic_stoichiometry: dict[str, str] = {}
|
318
|
+
parsed_reactants: defaultdict[str, int] = defaultdict(int)
|
319
|
+
for substrate in reaction.getListOfReactants():
|
320
|
+
species = _name_to_py(substrate.getSpecies())
|
321
|
+
if (ref := substrate.getId()) != "":
|
322
|
+
dynamic_stoichiometry[species] = ref
|
323
|
+
self.parameters[ref] = Parameter(0.0, is_constant=False)
|
324
|
+
|
325
|
+
elif species not in self.boundary_species:
|
326
|
+
factor = substrate.getStoichiometry()
|
327
|
+
if str(factor) == "nan":
|
328
|
+
msg = f"Cannot parse stoichiometry: {factor}"
|
329
|
+
raise ValueError(msg)
|
330
|
+
parsed_reactants[species] -= factor
|
331
|
+
|
332
|
+
parsed_products: defaultdict[str, int] = defaultdict(int)
|
333
|
+
for product in reaction.getListOfProducts():
|
334
|
+
species = _name_to_py(product.getSpecies())
|
335
|
+
if (ref := product.getId()) != "":
|
336
|
+
dynamic_stoichiometry[species] = ref
|
337
|
+
self.parameters[ref] = Parameter(0.0, is_constant=False)
|
338
|
+
elif species not in self.boundary_species:
|
339
|
+
factor = product.getStoichiometry()
|
340
|
+
if str(factor) == "nan":
|
341
|
+
msg = f"Cannot parse stoichiometry: {factor}"
|
342
|
+
raise ValueError(msg)
|
343
|
+
parsed_products[species] += factor
|
344
|
+
|
345
|
+
# Combine stoichiometries
|
346
|
+
# Hint: you can't just combine the dictionaries, as you have cases like
|
347
|
+
# S1 + S2 -> 2S2, which have to be combined to S1 -> S2
|
348
|
+
stoichiometries = dict(parsed_reactants)
|
349
|
+
for species, value in parsed_products.items():
|
350
|
+
if species in stoichiometries:
|
351
|
+
stoichiometries[species] = stoichiometries[species] + value
|
352
|
+
else:
|
353
|
+
stoichiometries[species] = value
|
354
|
+
|
355
|
+
self.reactions[sbml_id] = Reaction(
|
356
|
+
body=body,
|
357
|
+
stoichiometry=stoichiometries | dynamic_stoichiometry,
|
358
|
+
args=args,
|
359
|
+
)
|
360
|
+
|
361
|
+
def _convert_substance_amount_to_concentration(
|
362
|
+
self,
|
363
|
+
) -> None:
|
364
|
+
"""Convert substance amount to concentration if has_only_substance_units is false.
|
365
|
+
|
366
|
+
The compounds in the test are supplied in mole if has_only_substance_units is false.
|
367
|
+
In that case, the reaction equation has to be reformed like this:
|
368
|
+
k1 * S1 * compartment -> k1 * S1
|
369
|
+
or in other words the species have to be divided by the compartment to get
|
370
|
+
concentration units.
|
371
|
+
"""
|
372
|
+
for reaction in self.reactions.values():
|
373
|
+
function_body = reaction.body
|
374
|
+
removed_compartments = set()
|
375
|
+
for arg in reaction.args:
|
376
|
+
# the parsed species part is important to not
|
377
|
+
# introduce conversion on things that aren't species
|
378
|
+
if (species := self.variables.get(arg, None)) is None:
|
379
|
+
continue
|
380
|
+
|
381
|
+
if not species.has_only_substance_units:
|
382
|
+
compartment = species.compartment
|
383
|
+
if compartment is not None:
|
384
|
+
if self.compartments[compartment].dimensions == 0:
|
385
|
+
continue
|
386
|
+
if species.is_concentration:
|
387
|
+
if compartment not in removed_compartments:
|
388
|
+
pattern = f"({compartment})" + r"\b"
|
389
|
+
repl = f"({compartment} / {compartment})"
|
390
|
+
function_body = re.sub(pattern, repl, function_body)
|
391
|
+
removed_compartments.add(compartment)
|
392
|
+
else:
|
393
|
+
# \b is word boundary
|
394
|
+
pattern = f"({arg})" + r"\b"
|
395
|
+
repl = f"({arg} / {compartment})"
|
396
|
+
function_body = re.sub(pattern, repl, function_body)
|
397
|
+
if compartment not in reaction.args:
|
398
|
+
reaction.args.append(compartment)
|
399
|
+
|
400
|
+
# Simplify the function
|
401
|
+
try:
|
402
|
+
reaction.body = str(sympy.parse_expr(function_body))
|
403
|
+
except AttributeError:
|
404
|
+
# E.g. when math.factorial is called
|
405
|
+
# FIXME: do the sympy conversion before?
|
406
|
+
reaction.body = function_body
|
407
|
+
|
408
|
+
|
409
|
+
def _handle_fn(name: str, body: str, args: list[str]) -> Callable[..., float]:
|
410
|
+
func_args = ", ".join(args)
|
411
|
+
func_str = "\n".join(
|
412
|
+
[
|
413
|
+
f"def {name}({func_args}):",
|
414
|
+
f"{INDENT}return {body}",
|
415
|
+
"",
|
416
|
+
]
|
417
|
+
)
|
418
|
+
try:
|
419
|
+
exec(func_str, globals(), None) # noqa: S102
|
420
|
+
except SyntaxError as e:
|
421
|
+
msg = f"Invalid function definition: {func_str}"
|
422
|
+
raise SyntaxError(msg) from e
|
423
|
+
python_func = globals()[name]
|
424
|
+
python_func.__source__ = func_str
|
425
|
+
return python_func # type: ignore
|
426
|
+
|
427
|
+
|
428
|
+
def _codegen_fn(name: str, body: str, args: list[str]) -> str:
|
429
|
+
func_args = ", ".join(args)
|
430
|
+
return "\n".join(
|
431
|
+
[
|
432
|
+
f"def {name}({func_args}):",
|
433
|
+
f"{INDENT}return {body}",
|
434
|
+
"",
|
435
|
+
]
|
436
|
+
)
|
437
|
+
|
438
|
+
|
439
|
+
def _codegen_initial_assignment(
|
440
|
+
k: str,
|
441
|
+
sbml: Parser,
|
442
|
+
parameters: dict[str, float],
|
443
|
+
) -> str:
|
444
|
+
if k in parameters:
|
445
|
+
return f"m.update_parameter('{k}', _init_{k}(*(args[i] for i in {sbml.initial_assignment[k].args})) )"
|
446
|
+
|
447
|
+
ass = sbml.initial_assignment[k]
|
448
|
+
fn = f"_init_{k}(*(args[i] for i in {ass.args}))"
|
449
|
+
|
450
|
+
if (species := sbml.variables.get(k)) is not None:
|
451
|
+
compartment = species.compartment
|
452
|
+
if compartment is not None and (
|
453
|
+
not species.is_concentration
|
454
|
+
or (species.has_only_substance_units and species.is_concentration)
|
455
|
+
):
|
456
|
+
size = 1 if (c := sbml.compartments.get(compartment)) is None else c.size
|
457
|
+
if size != 0:
|
458
|
+
fn = f"{fn} * {size}"
|
459
|
+
elif k in sbml.compartments:
|
460
|
+
pass
|
461
|
+
else:
|
462
|
+
msg = "Initial assignment targeting unknown type"
|
463
|
+
raise NotImplementedError(msg)
|
464
|
+
|
465
|
+
return f"m.update_variable('{k}', {fn})"
|
466
|
+
|
467
|
+
|
468
|
+
def _codgen(name: str, sbml: Parser) -> Path:
|
469
|
+
import itertools as it
|
470
|
+
|
471
|
+
functions = {
|
472
|
+
k: _codegen_fn(k, body=v.body, args=v.args)
|
473
|
+
for k, v in it.chain(
|
474
|
+
sbml.functions.items(),
|
475
|
+
sbml.derived.items(),
|
476
|
+
sbml.reactions.items(),
|
477
|
+
{f"_init_{k}": v for k, v in sbml.initial_assignment.items()}.items(),
|
478
|
+
)
|
479
|
+
}
|
480
|
+
|
481
|
+
parameters = {
|
482
|
+
k: v.value for k, v in sbml.parameters.items() if k not in sbml.derived
|
483
|
+
}
|
484
|
+
variables = {
|
485
|
+
k: v.initial_amount for k, v in sbml.variables.items() if k not in sbml.derived
|
486
|
+
}
|
487
|
+
for k, v in sbml.compartments.items():
|
488
|
+
if k in sbml.derived:
|
489
|
+
continue
|
490
|
+
if v.is_constant:
|
491
|
+
parameters[k] = v.size
|
492
|
+
else:
|
493
|
+
variables[k] = v.size
|
494
|
+
|
495
|
+
derived_str = "\n ".join(
|
496
|
+
f"m.add_derived('{k}', fn={k}, args={v.args})" for k, v in sbml.derived.items()
|
497
|
+
)
|
498
|
+
rxn_str = "\n ".join(
|
499
|
+
f"m.add_reaction('{k}', fn={k}, args={rxn.args}, stoichiometry={rxn.stoichiometry})"
|
500
|
+
for k, rxn in sbml.reactions.items()
|
501
|
+
)
|
502
|
+
|
503
|
+
functions_str = "\n\n".join(functions.values())
|
504
|
+
|
505
|
+
parameters_str = f"m.add_parameters({parameters})" if len(parameters) > 0 else ""
|
506
|
+
variables_str = f"m.add_variables({variables})" if len(variables) > 0 else ""
|
507
|
+
|
508
|
+
# Initial assignments
|
509
|
+
initial_assignment_order = _sort_dependencies(
|
510
|
+
available=set(sbml.initial_assignment) ^ set(parameters) ^ set(variables),
|
511
|
+
elements=[(k, set(v.args)) for k, v in sbml.initial_assignment.items()],
|
512
|
+
)
|
513
|
+
|
514
|
+
if len(initial_assignment_order) > 0:
|
515
|
+
initial_assignment_source = "\n ".join(
|
516
|
+
_codegen_initial_assignment(k, sbml, parameters=parameters)
|
517
|
+
for k in initial_assignment_order
|
518
|
+
)
|
519
|
+
else:
|
520
|
+
initial_assignment_source = ""
|
521
|
+
|
522
|
+
file = f"""
|
523
|
+
import math
|
524
|
+
|
525
|
+
import numpy as np
|
526
|
+
import scipy
|
527
|
+
|
528
|
+
from modelbase2 import Model
|
529
|
+
|
530
|
+
{functions_str}
|
531
|
+
|
532
|
+
def get_model() -> Model:
|
533
|
+
m = Model()
|
534
|
+
{parameters_str}
|
535
|
+
{variables_str}
|
536
|
+
{derived_str}
|
537
|
+
{rxn_str}
|
538
|
+
args = m.get_args()
|
539
|
+
{initial_assignment_source}
|
540
|
+
return m
|
541
|
+
"""
|
542
|
+
path = default_tmp_dir(None, remove_old_cache=False) / f"{name}.py"
|
543
|
+
with path.open("w+") as f:
|
544
|
+
f.write(file)
|
545
|
+
return path
|
546
|
+
|
547
|
+
|
548
|
+
def import_from_path(module_name: str, file_path: Path) -> Callable[[], Model]:
|
549
|
+
import sys
|
550
|
+
from importlib import util
|
551
|
+
|
552
|
+
spec = unwrap(util.spec_from_file_location(module_name, file_path))
|
553
|
+
module = util.module_from_spec(spec)
|
554
|
+
sys.modules[module_name] = module
|
555
|
+
unwrap(spec.loader).exec_module(module)
|
556
|
+
return module.get_model
|
557
|
+
|
558
|
+
|
559
|
+
def valid_filename(value: str) -> str:
|
560
|
+
import re
|
561
|
+
import unicodedata
|
562
|
+
|
563
|
+
value = (
|
564
|
+
unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
|
565
|
+
)
|
566
|
+
value = re.sub(r"[^\w\s-]", "", value.lower())
|
567
|
+
value = re.sub(r"[-\s]+", "_", value).strip("-_")
|
568
|
+
return f"mb_{value}"
|
569
|
+
|
570
|
+
|
571
|
+
def read(file: Path) -> Model:
|
572
|
+
"""Import a metabolic model from an SBML file.
|
573
|
+
|
574
|
+
Args:
|
575
|
+
file: Path to the SBML file to import.
|
576
|
+
|
577
|
+
Returns:
|
578
|
+
Model: Imported model instance.
|
579
|
+
|
580
|
+
"""
|
581
|
+
name = valid_filename(file.stem)
|
582
|
+
sbml = Parser().parse(file=file)
|
583
|
+
|
584
|
+
model_fn = import_from_path(name, _codgen(name, sbml))
|
585
|
+
return model_fn()
|