forked from PEtab-dev/libpetab-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsbml_model.py
More file actions
315 lines (252 loc) · 9.43 KB
/
sbml_model.py
File metadata and controls
315 lines (252 loc) · 9.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
"""Functions for handling SBML models"""
from __future__ import annotations
import itertools
from collections.abc import Iterable
from pathlib import Path
import libsbml
import sympy as sp
from sympy.abc import _clash
from ..sbml import (
get_sbml_model,
is_sbml_consistent,
load_sbml_from_string,
write_sbml,
)
from . import MODEL_TYPE_SBML
from .model import Model
__all__ = ["SbmlModel"]
class SbmlModel(Model):
"""PEtab wrapper for SBML models"""
type_id = MODEL_TYPE_SBML
def __init__(
self,
sbml_model: libsbml.Model = None,
sbml_reader: libsbml.SBMLReader = None,
sbml_document: libsbml.SBMLDocument = None,
model_id: str = None,
):
"""Constructor.
:param sbml_model: SBML model. Optional if `sbml_document` is given.
:param sbml_reader: SBML reader. Optional.
:param sbml_document: SBML document. Optional if `sbml_model` is given.
:param model_id: Model ID. Defaults to the SBML model ID."""
super().__init__()
if sbml_model is None and sbml_document is None:
raise ValueError(
"Either sbml_model or sbml_document must be given."
)
if sbml_model is None:
sbml_model = sbml_document.getModel()
if sbml_document is None:
sbml_document = sbml_model.getSBMLDocument()
self.sbml_reader: libsbml.SBMLReader | None = sbml_reader
self.sbml_document: libsbml.SBMLDocument | None = sbml_document
self.sbml_model: libsbml.Model | None = sbml_model
self._model_id = model_id or sbml_model.getIdAttribute()
def __getstate__(self):
"""Return state for pickling"""
state = self.__dict__.copy()
# libsbml stuff cannot be serialized directly
if self.sbml_model:
sbml_document = self.sbml_model.getSBMLDocument()
sbml_writer = libsbml.SBMLWriter()
state["sbml_string"] = sbml_writer.writeSBMLToString(sbml_document)
exclude = ["sbml_reader", "sbml_document", "sbml_model"]
for key in exclude:
state.pop(key)
return state
def __setstate__(self, state):
"""Set state after unpickling"""
# load SBML model from pickled string
sbml_string = state.pop("sbml_string", None)
if sbml_string:
(
self.sbml_reader,
self.sbml_document,
self.sbml_model,
) = load_sbml_from_string(sbml_string)
self.__dict__.update(state)
@staticmethod
def from_file(filepath_or_buffer, model_id: str = None) -> SbmlModel:
sbml_reader, sbml_document, sbml_model = get_sbml_model(
filepath_or_buffer
)
return SbmlModel(
sbml_model=sbml_model,
sbml_reader=sbml_reader,
sbml_document=sbml_document,
model_id=model_id,
)
@staticmethod
def from_string(sbml_string, model_id: str = None) -> SbmlModel:
"""Create SBML model from an SBML string.
:param sbml_string: SBML model as string.
:param model_id: Model ID. Defaults to the SBML model ID.
"""
sbml_reader, sbml_document, sbml_model = load_sbml_from_string(
sbml_string
)
if not model_id:
model_id = sbml_model.getIdAttribute()
return SbmlModel(
sbml_model=sbml_model,
sbml_reader=sbml_reader,
sbml_document=sbml_document,
model_id=model_id,
)
@staticmethod
def from_antimony(ant_model: str | Path) -> SbmlModel:
"""Create SBML model from an Antimony model.
Requires the `antimony` package (https://github.com/sys-bio/antimony).
:param ant_model: Antimony model as string or path to file.
Strings are interpreted as Antimony model strings.
"""
sbml_str = antimony2sbml(ant_model)
return SbmlModel.from_string(sbml_str)
@property
def model_id(self):
return self._model_id
@model_id.setter
def model_id(self, model_id):
self._model_id = model_id
def to_file(self, filename: [str, Path]):
write_sbml(
self.sbml_document or self.sbml_model.getSBMLDocument(), filename
)
def get_parameter_value(self, id_: str) -> float:
parameter = self.sbml_model.getParameter(id_)
if not parameter:
raise ValueError(f"Parameter {id_} does not exist.")
return parameter.getValue()
def get_free_parameter_ids_with_values(
self,
) -> Iterable[tuple[str, float]]:
rule_targets = {
ar.getVariable() for ar in self.sbml_model.getListOfRules()
}
def get_initial(p):
# return the initial assignment value if there is one, and it is a
# number; `None`, if there is a non-numeric initial assignment;
# otherwise, the parameter value
if ia := self.sbml_model.getInitialAssignmentBySymbol(p.getId()):
sym_expr = sympify_sbml(ia.getMath())
return (
float(sym_expr.evalf())
if sym_expr.evalf().is_Number
else None
)
return p.getValue()
return (
(p.getId(), initial)
for p in self.sbml_model.getListOfParameters()
if p.getId() not in rule_targets
and (initial := get_initial(p)) is not None
)
def get_parameter_ids(self) -> Iterable[str]:
rule_targets = {
ar.getVariable() for ar in self.sbml_model.getListOfRules()
}
return (
p.getId()
for p in self.sbml_model.getListOfParameters()
if p.getId() not in rule_targets
)
def get_parameter_ids_with_values(self) -> Iterable[tuple[str, float]]:
rule_targets = {
ar.getVariable() for ar in self.sbml_model.getListOfRules()
}
return (
(p.getId(), p.getValue())
for p in self.sbml_model.getListOfParameters()
if p.getId() not in rule_targets
)
def has_entity_with_id(self, entity_id) -> bool:
return self.sbml_model.getElementBySId(entity_id) is not None
def get_valid_parameters_for_parameter_table(self) -> Iterable[str]:
# All parameters except rule-targets
disallowed_set = {
ar.getVariable() for ar in self.sbml_model.getListOfRules()
}
return (
p.getId()
for p in self.sbml_model.getListOfParameters()
if p.getId() not in disallowed_set
)
def get_valid_ids_for_condition_table(self) -> Iterable[str]:
return (
x.getId()
for x in itertools.chain(
self.sbml_model.getListOfParameters(),
self.sbml_model.getListOfSpecies(),
self.sbml_model.getListOfCompartments(),
)
)
def symbol_allowed_in_observable_formula(self, id_: str) -> bool:
return self.sbml_model.getElementBySId(id_) or id_ == "time"
def is_valid(self) -> bool:
return is_sbml_consistent(self.sbml_model.getSBMLDocument())
def is_state_variable(self, id_: str) -> bool:
return (
self.sbml_model.getSpecies(id_) is not None
or self.sbml_model.getCompartment(id_) is not None
or self.sbml_model.getRuleByVariable(id_) is not None
)
def sympify_sbml(sbml_obj: libsbml.ASTNode | libsbml.SBase) -> sp.Expr:
"""Convert SBML math expression to sympy expression.
Parameters
----------
sbml_obj:
SBML math element or an SBML object with a math element.
Returns
-------
The sympy expression corresponding to ``sbml_obj``.
"""
ast_node = (
sbml_obj
if isinstance(sbml_obj, libsbml.ASTNode)
else sbml_obj.getMath()
)
parser_settings = libsbml.L3ParserSettings(
ast_node.getParentSBMLObject().getModel(),
libsbml.L3P_PARSE_LOG_AS_LOG10,
libsbml.L3P_EXPAND_UNARY_MINUS,
libsbml.L3P_NO_UNITS,
libsbml.L3P_AVOGADRO_IS_CSYMBOL,
libsbml.L3P_COMPARE_BUILTINS_CASE_INSENSITIVE,
None,
libsbml.L3P_MODULO_IS_PIECEWISE,
)
formula_str = libsbml.formulaToL3StringWithSettings(
ast_node, parser_settings
)
return sp.sympify(formula_str, locals=_clash)
def antimony2sbml(ant_model: str | Path) -> str:
"""Convert Antimony model to SBML.
:param ant_model: Antimony model as string or path to file.
Strings are interpreted as Antimony model strings.
:returns:
The SBML model as string.
"""
import antimony as ant
# Unload everything / free memory
ant.clearPreviousLoads()
ant.freeAll()
try:
# potentially fails because of too long file name
is_file = ant_model and Path(ant_model).exists()
except OSError:
is_file = False
if is_file:
status = ant.loadAntimonyFile(str(ant_model))
else:
status = ant.loadAntimonyString(ant_model)
if status < 0:
raise RuntimeError(
f"Antimony model could not be loaded: {ant.getLastError()}"
)
if (main_module_name := ant.getMainModuleName()) is None:
raise AssertionError("There is no Antimony module.")
sbml_str = ant.getSBMLString(main_module_name)
if not sbml_str:
raise ValueError("Antimony model could not be converted to SBML.")
return sbml_str