forked from LoLab-MSM/pysb
/
stochkit.py
314 lines (265 loc) · 11.8 KB
/
stochkit.py
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
"""
Module containing a class to return the StochKit XML equivalent of a model
Contains code based on the `gillespy <https://github.com/JohnAbel/gillespy>`
library with permission from author Brian Drawert.
For information on how to use the model exporters, see the documentation
for :py:mod:`pysb.export`.
"""
from pysb.export import Exporter, CompartmentsNotSupported
from pysb.core import as_complex_pattern, Expression, Parameter
from pysb.bng import generate_equations
import numpy as np
import sympy
import re
from collections import defaultdict
try:
import lxml.etree as etree
pretty_print = True
except ImportError:
import xml.etree.ElementTree as etree
import xml.dom.minidom
pretty_print = False
class StochKitExporter(Exporter):
"""A class for returning the Kappa for a given PySB model.
Inherits from :py:class:`pysb.export.Exporter`, which implements
basic functionality for all exporters.
"""
@staticmethod
def _species_to_element(species_num, species_val):
e = etree.Element('Species')
idElement = etree.Element('Id')
idElement.text = species_num
e.append(idElement)
initialPopulationElement = etree.Element('InitialPopulation')
initialPopulationElement.text = str(species_val)
e.append(initialPopulationElement)
return e
@staticmethod
def _parameter_to_element(param_name, param_val):
e = etree.Element('Parameter')
idElement = etree.Element('Id')
idElement.text = param_name
e.append(idElement)
expressionElement = etree.Element('Expression')
expressionElement.text = str(param_val)
e.append(expressionElement)
return e
@staticmethod
def _reaction_to_element(rxn_name, rxn_desc, propensity_fxn, reactants,
products):
e = etree.Element('Reaction')
idElement = etree.Element('Id')
idElement.text = rxn_name
e.append(idElement)
descriptionElement = etree.Element('Description')
descriptionElement.text = rxn_desc
e.append(descriptionElement)
typeElement = etree.Element('Type')
if isinstance(propensity_fxn, (Parameter, float)):
typeElement.text = 'mass-action'
e.append(typeElement)
rateElement = etree.Element('Rate')
rateElement.text = propensity_fxn.name if isinstance(
propensity_fxn, Parameter) else str(propensity_fxn)
e.append(rateElement)
else:
typeElement.text = 'customized'
e.append(typeElement)
functionElement = etree.Element('PropensityFunction')
functionElement.text = propensity_fxn
e.append(functionElement)
reactantElement = etree.Element('Reactants')
for reactant, stoichiometry in reactants.items():
srElement = etree.Element('SpeciesReference')
srElement.set('id', reactant)
srElement.set('stoichiometry', str(stoichiometry))
reactantElement.append(srElement)
e.append(reactantElement)
productElement = etree.Element('Products')
for product, stoichiometry in products.items():
srElement = etree.Element('SpeciesReference')
srElement.set('id', product)
srElement.set('stoichiometry', str(stoichiometry))
productElement.append(srElement)
e.append(productElement)
return e
def export(self, initials=None, param_values=None):
"""Generate the corresponding StochKit2 XML for a PySB model
Parameters
----------
initials : list of numbers
List of initial species concentrations overrides
(must be same length as model.species). If None,
the concentrations from the model are used.
param_values : list
List of parameter value overrides (must be same length as
model.parameters). If None, the parameter values from the model
are used.
Returns
-------
string
The model in StochKit2 XML format
"""
if self.model.compartments:
raise CompartmentsNotSupported()
generate_equations(self.model)
document = etree.Element("Model")
d = etree.Element('Description')
d.text = 'Exported from PySB Model: %s' % self.model.name
document.append(d)
# Number of Reactions
nr = etree.Element('NumberOfReactions')
nr.text = str(len(self.model.reactions))
document.append(nr)
# Number of Species
ns = etree.Element('NumberOfSpecies')
ns.text = str(len(self.model.species))
document.append(ns)
if param_values is None:
# Get parameter values from model if not supplied
param_values = [p.value for p in self.model.parameters]
else:
# Validate length
if len(param_values) != len(self.model.parameters):
raise Exception('param_values must be a list of numeric '
'parameter values the same length as '
'model.parameters')
# Get initial species concentrations from model if not supplied
if initials is None:
initials = np.zeros((len(self.model.species),))
subs = dict((p, param_values[i]) for i, p in
enumerate(self.model.parameters))
for ic in self.model.initials:
cp = as_complex_pattern(ic.pattern)
si = self.model.get_species_index(cp)
if si is None:
raise IndexError("Species not found in model: %s" %
repr(cp))
if ic.value in self.model.parameters:
pi = self.model.parameters.index(ic.value)
value = param_values[pi]
elif ic.value in self.model.expressions:
value = ic.value.expand_expr().evalf(subs=subs)
else:
raise ValueError(
"Unexpected initial condition value type")
initials[si] = value
else:
# Validate length
if len(initials) != len(self.model.species):
raise Exception('initials must be a list of numeric initial '
'concentrations the same length as '
'model.species')
# Species
spec = etree.Element('SpeciesList')
for s_id in range(len(self.model.species)):
spec.append(self._species_to_element('__s%d' % s_id,
initials[s_id]))
document.append(spec)
# Parameters
params = etree.Element('ParametersList')
for p_id, param in enumerate(self.model.parameters):
p_name = param.name
if p_name == 'vol':
p_name = '__vol'
p_value = param.value if param_values is None else \
param_values[p_id]
params.append(self._parameter_to_element(p_name, p_value))
# Default volume parameter value
params.append(self._parameter_to_element('vol', 1.0))
document.append(params)
# Expressions and observables
expr_strings = {
e.name: '(%s)' % sympy.ccode(
e.expand_expr(expand_observables=True)
)
for e in self.model.expressions
}
# Reactions
reacs = etree.Element('ReactionsList')
pattern = re.compile("(__s\d+)\*\*(\d+)")
for rxn_id, rxn in enumerate(self.model.reactions):
rxn_name = 'Rxn%d' % rxn_id
rxn_desc = 'Rules: %s' % str(rxn["rule"])
reactants = defaultdict(int)
products = defaultdict(int)
# reactants
for r in rxn["reactants"]:
reactants["__s%d" % r] += 1
# products
for p in rxn["products"]:
products["__s%d" % p] += 1
# replace terms like __s**2 with __s*(__s-1)
rate = str(rxn["rate"])
matches = pattern.findall(rate)
for m in matches:
repl = m[0]
for i in range(1, int(m[1])):
repl += "*(%s-%d)" % (m[0], i)
rate = re.sub(pattern, repl, rate, count=1)
# expand only expressions used in the rate eqn
for e in {sym for sym in rxn["rate"].atoms()
if isinstance(sym, Expression)}:
rate = re.sub(r'\b%s\b' % e.name,
expr_strings[e.name],
rate)
total_reactants = sum(reactants.values())
rxn_params = rxn["rate"].atoms(Parameter)
rate = None
if total_reactants <= 2 and len(rxn_params) == 1:
# Try to parse as mass action to avoid compiling custom
# propensity functions in StochKit (slow for big models)
rxn_param = rxn_params.pop()
putative_rate = sympy.Mul(*[sympy.symbols(r) ** r_stoich for
r, r_stoich in
reactants.items()]) * rxn_param
rxn_floats = rxn["rate"].atoms(sympy.Float)
rate_mul = 1.0
if len(rxn_floats) == 1:
rate_mul = next(iter(rxn_floats))
putative_rate *= rate_mul
if putative_rate == rxn["rate"]:
# Reaction is mass-action, set rate to a Parameter or float
if len(rxn_floats) == 0:
rate = rxn_param
elif len(rxn_floats) == 1:
rate = rxn_param.value * float(rate_mul)
if rate is not None and len(reactants) == 1 and \
max(reactants.values()) == 2:
# Need rate * 2 in addition to any rate factor
rate = (rate.value if isinstance(rate, Parameter)
else rate) * 2.0
if rate is None:
# Custom propensity function needed
rxn_atoms = rxn["rate"].atoms()
# replace terms like __s**2 with __s*(__s-1)
rate = str(rxn["rate"])
matches = pattern.findall(rate)
for m in matches:
repl = m[0]
for i in range(1, int(m[1])):
repl += "*(%s-%d)" % (m[0], i)
rate = re.sub(pattern, repl, rate, count=1)
# expand only expressions used in the rate eqn
for e in {sym for sym in rxn_atoms
if isinstance(sym, Expression)}:
rate = re.sub(r'\b%s\b' % e.name,
expr_strings[e.name],
rate)
reacs.append(self._reaction_to_element(rxn_name,
rxn_desc,
rate,
reactants,
products))
document.append(reacs)
if pretty_print:
return etree.tostring(document, pretty_print=True).decode('utf8')
else:
# Hack to print pretty xml without pretty-print
# (requires the lxml module).
doc = etree.tostring(document)
xmldoc = xml.dom.minidom.parseString(doc)
uglyXml = xmldoc.toprettyxml(indent=' ')
text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
prettyXml = text_re.sub(">\g<1></", uglyXml)
return prettyXml