mxlpy 0.22.0__py3-none-any.whl → 0.24.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.
- mxlpy/__init__.py +13 -6
- mxlpy/carousel.py +52 -5
- mxlpy/compare.py +15 -2
- mxlpy/experimental/diff.py +14 -0
- mxlpy/fit.py +20 -1
- mxlpy/integrators/__init__.py +4 -0
- mxlpy/integrators/int_assimulo.py +3 -3
- mxlpy/integrators/int_diffrax.py +119 -0
- mxlpy/integrators/int_scipy.py +12 -6
- mxlpy/label_map.py +1 -2
- mxlpy/mc.py +85 -24
- mxlpy/mca.py +8 -4
- mxlpy/meta/__init__.py +6 -1
- mxlpy/meta/codegen_latex.py +9 -0
- mxlpy/meta/codegen_model.py +55 -12
- mxlpy/meta/codegen_mxlpy.py +215 -58
- mxlpy/meta/source_tools.py +129 -80
- mxlpy/meta/sympy_tools.py +12 -6
- mxlpy/model.py +314 -96
- mxlpy/plot.py +60 -29
- mxlpy/sbml/_data.py +34 -0
- mxlpy/sbml/_export.py +17 -8
- mxlpy/sbml/_import.py +68 -547
- mxlpy/scan.py +163 -249
- mxlpy/simulator.py +9 -359
- mxlpy/types.py +723 -80
- mxlpy/units.py +5 -0
- {mxlpy-0.22.0.dist-info → mxlpy-0.24.0.dist-info}/METADATA +7 -1
- mxlpy-0.24.0.dist-info/RECORD +57 -0
- mxlpy/sbml/_mathml.py +0 -692
- mxlpy/sbml/_unit_conversion.py +0 -74
- mxlpy-0.22.0.dist-info/RECORD +0 -58
- {mxlpy-0.22.0.dist-info → mxlpy-0.24.0.dist-info}/WHEEL +0 -0
- {mxlpy-0.22.0.dist-info → mxlpy-0.24.0.dist-info}/licenses/LICENSE +0 -0
mxlpy/sbml/_import.py
CHANGED
@@ -1,582 +1,104 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import logging
|
4
|
-
import math # noqa: F401 # models might need it
|
5
3
|
import re
|
6
|
-
|
7
|
-
|
8
|
-
from
|
9
|
-
from typing import TYPE_CHECKING
|
4
|
+
import sys
|
5
|
+
import unicodedata
|
6
|
+
from importlib import util
|
7
|
+
from typing import TYPE_CHECKING
|
10
8
|
|
11
|
-
import
|
12
|
-
import numpy as np # noqa: F401 # models might need it
|
9
|
+
import pysbml
|
13
10
|
import sympy
|
14
11
|
|
15
|
-
from mxlpy.
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
Derived,
|
23
|
-
Function,
|
24
|
-
Parameter,
|
25
|
-
Reaction,
|
12
|
+
from mxlpy.meta.codegen_mxlpy import (
|
13
|
+
SymbolicFn,
|
14
|
+
SymbolicParameter,
|
15
|
+
SymbolicReaction,
|
16
|
+
SymbolicRepr,
|
17
|
+
SymbolicVariable,
|
18
|
+
generate_mxlpy_code_from_symbolic_repr,
|
26
19
|
)
|
27
|
-
from mxlpy.
|
28
|
-
from mxlpy.sbml._name_conversion import _name_to_py
|
29
|
-
from mxlpy.sbml._unit_conversion import get_operator_mappings, get_unit_conversion
|
20
|
+
from mxlpy.paths import default_tmp_dir
|
30
21
|
from mxlpy.types import unwrap
|
31
22
|
|
23
|
+
__all__ = ["free_symbols", "import_from_path", "read", "valid_filename"]
|
24
|
+
|
32
25
|
if TYPE_CHECKING:
|
33
26
|
from collections.abc import Callable
|
27
|
+
from pathlib import Path
|
34
28
|
|
35
|
-
|
36
|
-
|
37
|
-
__all__ = [
|
38
|
-
"INDENT",
|
39
|
-
"OPERATOR_MAPPINGS",
|
40
|
-
"Parser",
|
41
|
-
"UNIT_CONVERSION",
|
42
|
-
"import_from_path",
|
43
|
-
"read",
|
44
|
-
"valid_filename",
|
45
|
-
]
|
46
|
-
|
47
|
-
UNIT_CONVERSION = get_unit_conversion()
|
48
|
-
OPERATOR_MAPPINGS = get_operator_mappings()
|
49
|
-
INDENT = " "
|
50
|
-
|
51
|
-
|
52
|
-
def _nan_to_zero(value: float) -> float:
|
53
|
-
return 0 if str(value) == "nan" else value
|
54
|
-
|
55
|
-
|
56
|
-
@dataclass(slots=True)
|
57
|
-
class Parser:
|
58
|
-
# Collections
|
59
|
-
boundary_species: set[str] = field(default_factory=set)
|
60
|
-
# Parsed stuff
|
61
|
-
atomic_units: dict[str, AtomicUnit] = field(default_factory=dict)
|
62
|
-
composite_units: dict[str, CompositeUnit] = field(default_factory=dict)
|
63
|
-
compartments: dict[str, Compartment] = field(default_factory=dict)
|
64
|
-
parameters: dict[str, Parameter] = field(default_factory=dict)
|
65
|
-
variables: dict[str, Compound] = field(default_factory=dict)
|
66
|
-
derived: dict[str, Derived] = field(default_factory=dict)
|
67
|
-
initial_assignment: dict[str, Derived] = field(default_factory=dict)
|
68
|
-
functions: dict[str, Function] = field(default_factory=dict)
|
69
|
-
reactions: dict[str, Reaction] = field(default_factory=dict)
|
70
|
-
|
71
|
-
def parse(self, file: str | Path) -> Self:
|
72
|
-
if not Path(file).exists():
|
73
|
-
msg = "Model file does not exist"
|
74
|
-
raise OSError(msg)
|
75
|
-
doc = libsbml.readSBMLFromFile(str(file))
|
76
|
-
|
77
|
-
# Check for unsupported packages
|
78
|
-
for i in range(doc.num_plugins):
|
79
|
-
if doc.getPlugin(i).getPackageName() == "comp":
|
80
|
-
msg = "No support for comp package"
|
81
|
-
raise NotImplementedError(msg)
|
82
|
-
|
83
|
-
sbml_model = doc.getModel()
|
84
|
-
if sbml_model is None:
|
85
|
-
return self
|
86
|
-
|
87
|
-
if bool(sbml_model.getConversionFactor()):
|
88
|
-
msg = "Conversion factors are currently not supported"
|
89
|
-
raise NotImplementedError(msg)
|
90
|
-
|
91
|
-
self.parse_functions(sbml_model)
|
92
|
-
self.parse_units(sbml_model)
|
93
|
-
self.parse_compartments(sbml_model)
|
94
|
-
self.parse_variables(sbml_model)
|
95
|
-
self.parse_parameters(sbml_model)
|
96
|
-
self.parse_initial_assignments(sbml_model)
|
97
|
-
self.parse_rules(sbml_model)
|
98
|
-
self.parse_constraints(sbml_model)
|
99
|
-
self.parse_reactions(sbml_model)
|
100
|
-
self.parse_events(sbml_model)
|
101
|
-
|
102
|
-
# Modifications
|
103
|
-
self._convert_substance_amount_to_concentration()
|
104
|
-
# self._rename_species_references()
|
105
|
-
return self
|
106
|
-
|
107
|
-
###############################################################################
|
108
|
-
# PARSING STAGE
|
109
|
-
###############################################################################
|
110
|
-
|
111
|
-
def parse_constraints(self, sbml_model: libsbml.Model) -> None:
|
112
|
-
if len(sbml_model.getListOfConstraints()) > 0:
|
113
|
-
msg = "mxlpy does not support model constraints. "
|
114
|
-
raise NotImplementedError(msg)
|
115
|
-
|
116
|
-
def parse_events(self, sbml_model: libsbml.Model) -> None:
|
117
|
-
if len(sbml_model.getListOfEvents()) > 0:
|
118
|
-
msg = (
|
119
|
-
"mxlpy does not current support events. "
|
120
|
-
"Check the file for how to integrate properly."
|
121
|
-
)
|
122
|
-
raise NotImplementedError(msg)
|
123
|
-
|
124
|
-
def parse_units(self, sbml_model: libsbml.Model) -> None:
|
125
|
-
for unit_definition in sbml_model.getListOfUnitDefinitions():
|
126
|
-
composite_id = unit_definition.getId()
|
127
|
-
local_units = []
|
128
|
-
for unit in unit_definition.getListOfUnits():
|
129
|
-
atomic_unit = AtomicUnit(
|
130
|
-
kind=UNIT_CONVERSION[unit.getKind()],
|
131
|
-
scale=unit.getScale(),
|
132
|
-
exponent=unit.getExponent(),
|
133
|
-
multiplier=unit.getMultiplier(),
|
134
|
-
)
|
135
|
-
local_units.append(atomic_unit.kind)
|
136
|
-
self.atomic_units[atomic_unit.kind] = atomic_unit
|
137
|
-
self.composite_units[composite_id] = CompositeUnit(
|
138
|
-
sbml_id=composite_id,
|
139
|
-
units=local_units,
|
140
|
-
)
|
141
|
-
|
142
|
-
def parse_compartments(self, sbml_model: libsbml.Model) -> None:
|
143
|
-
for compartment in sbml_model.getListOfCompartments():
|
144
|
-
sbml_id = _name_to_py(compartment.getId())
|
145
|
-
size = compartment.getSize()
|
146
|
-
if str(size) == "nan":
|
147
|
-
size = 0
|
148
|
-
self.compartments[sbml_id] = Compartment(
|
149
|
-
name=compartment.getName(),
|
150
|
-
dimensions=compartment.getSpatialDimensions(),
|
151
|
-
size=size,
|
152
|
-
units=compartment.getUnits(),
|
153
|
-
is_constant=compartment.getConstant(),
|
154
|
-
)
|
155
|
-
|
156
|
-
def parse_parameters(self, sbml_model: libsbml.Model) -> None:
|
157
|
-
for parameter in sbml_model.getListOfParameters():
|
158
|
-
self.parameters[_name_to_py(parameter.getId())] = Parameter(
|
159
|
-
value=_nan_to_zero(parameter.getValue()),
|
160
|
-
is_constant=parameter.getConstant(),
|
161
|
-
)
|
162
|
-
|
163
|
-
def parse_variables(self, sbml_model: libsbml.Model) -> None:
|
164
|
-
for compound in sbml_model.getListOfSpecies():
|
165
|
-
compound_id = _name_to_py(compound.getId())
|
166
|
-
if bool(compound.getConversionFactor()):
|
167
|
-
msg = "Conversion factors are not implemented. "
|
168
|
-
raise NotImplementedError(msg)
|
169
|
-
|
170
|
-
# NOTE: What the shit is this?
|
171
|
-
initial_amount = compound.getInitialAmount()
|
172
|
-
if str(initial_amount) == "nan":
|
173
|
-
initial_amount = compound.getInitialConcentration()
|
174
|
-
is_concentration = str(initial_amount) != "nan"
|
175
|
-
else:
|
176
|
-
is_concentration = False
|
177
|
-
|
178
|
-
has_boundary_condition = compound.getBoundaryCondition()
|
179
|
-
if has_boundary_condition:
|
180
|
-
self.boundary_species.add(compound_id)
|
181
|
-
|
182
|
-
self.variables[compound_id] = Compound(
|
183
|
-
compartment=compound.getCompartment(),
|
184
|
-
initial_amount=_nan_to_zero(initial_amount),
|
185
|
-
substance_units=compound.getSubstanceUnits(),
|
186
|
-
has_only_substance_units=compound.getHasOnlySubstanceUnits(),
|
187
|
-
has_boundary_condition=has_boundary_condition,
|
188
|
-
is_constant=compound.getConstant(),
|
189
|
-
is_concentration=is_concentration,
|
190
|
-
)
|
191
|
-
|
192
|
-
def parse_functions(self, sbml_model: libsbml.Model) -> None:
|
193
|
-
for func in sbml_model.getListOfFunctionDefinitions():
|
194
|
-
func_name = func.getName()
|
195
|
-
sbml_id = func.getId()
|
196
|
-
if sbml_id is None or sbml_id == "":
|
197
|
-
sbml_id = func_name
|
198
|
-
elif func_name is None or func_name == "":
|
199
|
-
func_name = sbml_id
|
200
|
-
func_name = _name_to_py(func_name)
|
201
|
-
|
202
|
-
if (node := func.getMath()) is None:
|
203
|
-
continue
|
204
|
-
body, args = parse_sbml_math(node=node)
|
205
|
-
|
206
|
-
self.functions[func_name] = Function(
|
207
|
-
body=body,
|
208
|
-
args=args,
|
209
|
-
)
|
210
|
-
|
211
|
-
###############################################################################
|
212
|
-
# Different kinds of derived values
|
213
|
-
###############################################################################
|
214
|
-
|
215
|
-
def parse_initial_assignments(self, sbml_model: libsbml.Model) -> None:
|
216
|
-
for assignment in sbml_model.getListOfInitialAssignments():
|
217
|
-
name = _name_to_py(assignment.getSymbol())
|
218
|
-
|
219
|
-
node = assignment.getMath()
|
220
|
-
if node is None:
|
221
|
-
msg = f"Unusable math for {name}"
|
222
|
-
_LOGGER.warning(msg)
|
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)
|
29
|
+
from mxlpy.model import Model
|
263
30
|
|
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
31
|
|
314
|
-
|
315
|
-
|
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
|
-
)
|
32
|
+
def free_symbols(expr: sympy.Expr) -> list[str]:
|
33
|
+
return [i.name for i in expr.free_symbols if isinstance(i, sympy.Symbol)]
|
360
34
|
|
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
35
|
|
366
|
-
|
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(
|
36
|
+
def _transform_stoichiometry(
|
440
37
|
k: str,
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
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(),
|
38
|
+
v: pysbml.transform.data.Expr,
|
39
|
+
) -> SymbolicFn | str | sympy.Float:
|
40
|
+
if isinstance(v, sympy.Float):
|
41
|
+
return v
|
42
|
+
if isinstance(v, sympy.Symbol):
|
43
|
+
return v.name
|
44
|
+
|
45
|
+
return SymbolicFn(k, expr=v, args=free_symbols(v))
|
46
|
+
|
47
|
+
|
48
|
+
def _codegen(name: str, model: pysbml.transform.data.Model) -> Path:
|
49
|
+
sym = SymbolicRepr()
|
50
|
+
for key, var in model.variables.items():
|
51
|
+
sym.variables[key] = SymbolicVariable(
|
52
|
+
value=var.value,
|
53
|
+
unit=var.unit,
|
478
54
|
)
|
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
55
|
|
495
|
-
|
496
|
-
|
497
|
-
# otherwise we run into a ton of DivisionByZero errors.
|
498
|
-
# Since the values are overwritte afterwards, it doesn't really matter anyways
|
499
|
-
for k in sbml.initial_assignment:
|
500
|
-
if k in parameters and parameters[k] == 0:
|
501
|
-
parameters[k] = 1
|
502
|
-
if k in variables and variables[k] == 0:
|
503
|
-
variables[k] = 1
|
56
|
+
for key, par in model.parameters.items():
|
57
|
+
sym.parameters[key] = SymbolicParameter(value=par.value, unit=par.unit)
|
504
58
|
|
505
|
-
|
506
|
-
|
507
|
-
)
|
508
|
-
rxn_str = "\n ".join(
|
509
|
-
f"m.add_reaction('{k}', fn={k}, args={rxn.args}, stoichiometry={rxn.stoichiometry})"
|
510
|
-
for k, rxn in sbml.reactions.items()
|
511
|
-
)
|
512
|
-
|
513
|
-
functions_str = "\n\n".join(functions.values())
|
514
|
-
|
515
|
-
parameters_str = f"m.add_parameters({parameters})" if len(parameters) > 0 else ""
|
516
|
-
variables_str = f"m.add_variables({variables})" if len(variables) > 0 else ""
|
517
|
-
|
518
|
-
# Initial assignments
|
519
|
-
initial_assignment_order = _sort_dependencies(
|
520
|
-
available=set(sbml.initial_assignment)
|
521
|
-
^ set(parameters)
|
522
|
-
^ set(variables)
|
523
|
-
^ set(sbml.derived)
|
524
|
-
| {"time"},
|
525
|
-
elements=[
|
526
|
-
Dependency(name=k, required=set(v.args), provided={k})
|
527
|
-
for k, v in sbml.initial_assignment.items()
|
528
|
-
],
|
529
|
-
)
|
59
|
+
for key, der in model.derived.items():
|
60
|
+
sym.derived[key] = SymbolicFn(fn_name=key, expr=der, args=free_symbols(der))
|
530
61
|
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
62
|
+
for key, rxn in model.reactions.items():
|
63
|
+
sym.reactions[key] = SymbolicReaction(
|
64
|
+
fn=SymbolicFn(fn_name=key, expr=rxn.expr, args=free_symbols(rxn.expr)),
|
65
|
+
stoichiometry={
|
66
|
+
k: _transform_stoichiometry(k, v) for k, v in rxn.stoichiometry.items()
|
67
|
+
},
|
535
68
|
)
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
from mxlpy import Model
|
546
|
-
|
547
|
-
{functions_str}
|
69
|
+
for key, der in model.initial_assignments.items():
|
70
|
+
if key in model.parameters:
|
71
|
+
sym.parameters[key].value = SymbolicFn(
|
72
|
+
fn_name=key, expr=der, args=free_symbols(der)
|
73
|
+
)
|
74
|
+
elif key in model.variables:
|
75
|
+
sym.variables[key].value = SymbolicFn(
|
76
|
+
fn_name=key, expr=der, args=free_symbols(der)
|
77
|
+
)
|
548
78
|
|
549
|
-
def get_model() -> Model:
|
550
|
-
m = Model()
|
551
|
-
{parameters_str}
|
552
|
-
{variables_str}
|
553
|
-
{derived_str}
|
554
|
-
{rxn_str}
|
555
|
-
args = m.get_args()
|
556
|
-
{initial_assignment_source}
|
557
|
-
return m
|
558
|
-
"""
|
559
79
|
path = default_tmp_dir(None, remove_old_cache=False) / f"{name}.py"
|
560
80
|
with path.open("w+") as f:
|
561
|
-
f.write(
|
81
|
+
f.write(
|
82
|
+
generate_mxlpy_code_from_symbolic_repr(
|
83
|
+
sym,
|
84
|
+
imports=[
|
85
|
+
"import math",
|
86
|
+
"import scipy",
|
87
|
+
],
|
88
|
+
)
|
89
|
+
)
|
562
90
|
return path
|
563
91
|
|
564
92
|
|
565
93
|
def import_from_path(module_name: str, file_path: Path) -> Callable[[], Model]:
|
566
|
-
import sys
|
567
|
-
from importlib import util
|
568
|
-
|
569
94
|
spec = unwrap(util.spec_from_file_location(module_name, file_path))
|
570
95
|
module = util.module_from_spec(spec)
|
571
96
|
sys.modules[module_name] = module
|
572
97
|
unwrap(spec.loader).exec_module(module)
|
573
|
-
return module.
|
98
|
+
return module.create_model
|
574
99
|
|
575
100
|
|
576
101
|
def valid_filename(value: str) -> str:
|
577
|
-
import re
|
578
|
-
import unicodedata
|
579
|
-
|
580
102
|
value = (
|
581
103
|
unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
|
582
104
|
)
|
@@ -595,8 +117,7 @@ def read(file: Path) -> Model:
|
|
595
117
|
Model: Imported model instance.
|
596
118
|
|
597
119
|
"""
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
model_fn = import_from_path(name, _codgen(name, sbml))
|
120
|
+
model = pysbml.load_and_transform_model(file)
|
121
|
+
out_name = valid_filename(file.stem)
|
122
|
+
model_fn = import_from_path(out_name, _codegen(out_name, model))
|
602
123
|
return model_fn()
|