myokit 1.37.1__py3-none-any.whl → 1.37.2__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.
@@ -0,0 +1,355 @@
1
+ #
2
+ # SBML Writer: Writes an SBML Model to disk
3
+ #
4
+ # This file is part of Myokit.
5
+ # See http://myokit.org for copyright, sharing, and licensing details.
6
+ #
7
+ from typing import Tuple
8
+ from lxml import etree
9
+
10
+ import myokit
11
+ from myokit._unit import Quantity
12
+ from myokit.formats.mathml._ewriter import MathMLExpressionWriter
13
+ from myokit.formats.sbml._api import (
14
+ Model, Compartment,
15
+ Parameter, Species,
16
+ Reaction, SpeciesReference
17
+ )
18
+
19
+
20
+ def write_file(path: str, model: Model):
21
+ """
22
+ Writes an SBML model to the given path.
23
+ """
24
+ return SBMLWriter.write_file(path, model)
25
+
26
+
27
+ def write_string(model: Model) -> str:
28
+ """
29
+ Writes an SBML model to a string and returns it.
30
+ """
31
+ return SBMLWriter.write_string(model)
32
+
33
+
34
+ class SBMLWriter:
35
+ """
36
+ Writes SBML documents
37
+ """
38
+
39
+ @staticmethod
40
+ def write_file(path: str, model: Model):
41
+ tree = SBMLWriter._model(model)
42
+ # Write to disk
43
+ tree.write(
44
+ path,
45
+ encoding='utf-8',
46
+ method='xml',
47
+ xml_declaration=True,
48
+ pretty_print=True,
49
+ )
50
+
51
+ @staticmethod
52
+ def write_string(model: Model) -> str:
53
+ tree = SBMLWriter._model(model)
54
+ return etree.tostring(
55
+ tree,
56
+ encoding='utf-8',
57
+ method='xml',
58
+ pretty_print=True,
59
+ )
60
+
61
+ @staticmethod
62
+ def _compartment(
63
+ compartment: Compartment,
64
+ unit_to_str: dict
65
+ ) -> etree.Element:
66
+ node = etree.Element('compartment', id=compartment.sid())
67
+ if compartment.size_units() != myokit.units.dimensionless:
68
+ node.attrib['units'] = unit_to_str[compartment.size_units()]
69
+ if compartment.spatial_dimensions() is not None:
70
+ node.attrib['spatialDimensions'] = str(
71
+ compartment.spatial_dimensions()
72
+ )
73
+ return node
74
+
75
+ @staticmethod
76
+ def _unit(sid: str, unit: myokit.Unit) -> etree.Element:
77
+ node = etree.Element('unitDefinition', id=sid)
78
+ kinds = [
79
+ 'gram', 'metre', 'second',
80
+ 'ampere', 'kelvin', 'candela', 'mole'
81
+ ]
82
+ multiplier = unit.multiplier()
83
+ for exponent, kind in zip(unit.exponents(), kinds):
84
+ if exponent != 0:
85
+ child = etree.Element('unit')
86
+ child.attrib['kind'] = kind
87
+ child.attrib['exponent'] = str(exponent)
88
+ if multiplier is not None:
89
+ child.attrib['multiplier'] = str(multiplier)
90
+ multiplier = None
91
+ node.append(child)
92
+ # might also have a dimensionless unit and a multiplier
93
+ if multiplier is not None:
94
+ child = etree.Element('unit')
95
+ child.attrib['kind'] = 'dimensionless'
96
+ child.attrib['multiplier'] = str(multiplier)
97
+ node.append(child)
98
+ return node
99
+
100
+ @staticmethod
101
+ def _parameter(
102
+ parameter: Parameter,
103
+ unit_to_str_map: dict
104
+ ) -> Tuple[etree.Element, etree.Element, etree.Element]:
105
+ """
106
+ returns the XML representation of this parameter as a tuple of
107
+ (parameter, initial_assignment, rule).
108
+ """
109
+ parameter_xml = etree.Element('parameter', id=parameter.sid())
110
+ if (
111
+ parameter.units() is not None and
112
+ parameter.units() != myokit.units.dimensionless
113
+ ):
114
+ parameter_xml.attrib['units'] = unit_to_str_map[parameter.units()]
115
+
116
+ if parameter.is_constant():
117
+ parameter_xml.attrib['constant'] = 'true'
118
+
119
+ if parameter.is_literal():
120
+ value = parameter.value().eval()
121
+ parameter_xml.attrib['value'] = str(value)
122
+ return parameter_xml, None, None
123
+ else:
124
+ initial_assignment, rule = SBMLWriter._quantity(parameter)
125
+ return parameter_xml, initial_assignment, rule
126
+
127
+ @staticmethod
128
+ def _math(expression: myokit.Expression) -> etree.Element:
129
+ math = etree.Element(
130
+ 'math',
131
+ xmlns='http://www.w3.org/1998/Math/MathML'
132
+ )
133
+
134
+ def flhs(lhs):
135
+ var = lhs.var()
136
+ if isinstance(var, str):
137
+ return var
138
+ if var.binding() == 'time':
139
+ return "http://www.sbml.org/sbml/symbols/time"
140
+ return var.uname()
141
+
142
+ mathml_writer = MathMLExpressionWriter()
143
+ mathml_writer.set_lhs_function(flhs)
144
+ mathml_writer.ex(expression, math)
145
+ return math
146
+
147
+ @staticmethod
148
+ def _quantity(quantity: Quantity) -> etree.Element:
149
+ initial_value = quantity.initial_value()
150
+ initial_assignment = None
151
+ if initial_value is not None:
152
+ initial_assignment = etree.Element(
153
+ 'initialAssignment', symbol=quantity.sid()
154
+ )
155
+ math = SBMLWriter._math(initial_value)
156
+ initial_assignment.append(math)
157
+
158
+ value = quantity.value()
159
+ rule = None
160
+ if value is not None:
161
+ if quantity.is_rate():
162
+ rule_type = 'rateRule'
163
+ else:
164
+ rule_type = 'assignmentRule'
165
+ rule = etree.Element(rule_type, variable=quantity.sid())
166
+ math = SBMLWriter._math(quantity.value())
167
+ rule.append(math)
168
+ return initial_assignment, rule
169
+
170
+ @staticmethod
171
+ def _reaction(reaction: Reaction) -> etree.Element:
172
+ reaction_xml = etree.Element('reaction', id=reaction.sid())
173
+ list_of_reactants = etree.Element('listOfReactants')
174
+ for reactant in reaction.reactants():
175
+ node = SBMLWriter._species_reference(reactant)
176
+ list_of_reactants.append(node)
177
+ reaction_xml.append(list_of_reactants)
178
+ list_of_products = etree.Element('listOfProducts')
179
+ for product in reaction.products():
180
+ node = SBMLWriter._species_reference(product)
181
+ list_of_products.append(node)
182
+ reaction_xml.append(list_of_products)
183
+ list_of_modifiers = etree.Element('listOfModifiers')
184
+ for modifier in reaction.modifiers():
185
+ node = SBMLWriter._modifier_species_reference(modifier)
186
+ list_of_modifiers.append(node)
187
+ reaction_xml.append(list_of_modifiers)
188
+ if reaction.kinetic_law() is not None:
189
+ kinetic_law = etree.Element('kineticLaw')
190
+ math = SBMLWriter._math(reaction.kinetic_law())
191
+ kinetic_law.append(math)
192
+ reaction_xml.append(kinetic_law)
193
+ return reaction_xml
194
+
195
+ @staticmethod
196
+ def _species(species: Species, unit_to_str_map: dict) -> Tuple[
197
+ etree.Element, etree.Element
198
+ ]:
199
+ """
200
+ Returns the XML representation of this species as a tuple of
201
+ (species, rule).
202
+ """
203
+ species_xml = etree.Element('species', id=species.sid())
204
+ species_xml.attrib['compartment'] = species.compartment().sid()
205
+ initial_value, initial_value_in_amount = species.initial_value()
206
+ if initial_value_in_amount is None:
207
+ if species.is_amount():
208
+ attrib_name = 'initialAmount'
209
+ else:
210
+ attrib_name = 'initialConcentration'
211
+ else:
212
+ if initial_value_in_amount:
213
+ attrib_name = 'initialAmount'
214
+ else:
215
+ attrib_name = 'initialConcentration'
216
+ if initial_value is not None:
217
+ initial_value_eval = initial_value.eval()
218
+ species_xml.attrib[attrib_name] = str(initial_value_eval)
219
+ species_xml.attrib['constant'] = str(species.is_constant())
220
+ if species.substance_units() != myokit.units.dimensionless:
221
+ species_xml.attrib['units'] = unit_to_str_map[
222
+ species.substance_units()
223
+ ]
224
+ species_xml.attrib['boundaryCondition'] = str(species.is_boundary())
225
+
226
+ if species.value() is None:
227
+ return species_xml, None
228
+
229
+ if species.is_rate():
230
+ rule_type = 'rateRule'
231
+ else:
232
+ rule_type = 'assignmentRule'
233
+ rule = etree.Element(rule_type, variable=species.sid())
234
+ math = SBMLWriter._math(species.value())
235
+ rule.append(math)
236
+ return species_xml, rule
237
+
238
+ @staticmethod
239
+ def _species_reference(ref: SpeciesReference) -> etree.Element:
240
+ species_reference = etree.Element(
241
+ 'speciesReference',
242
+ species=ref.species().sid()
243
+ )
244
+ if ref.sid() is not None:
245
+ species_reference.attrib['id'] = ref.sid()
246
+ value = ref.value()
247
+ if value is not None:
248
+ value_eval = value.eval()
249
+ species_reference.attrib['stoichiometry'] = str(value_eval)
250
+ return species_reference
251
+
252
+ @staticmethod
253
+ def _modifier_species_reference(ref: SpeciesReference) -> etree.Element:
254
+ species_reference = etree.Element(
255
+ 'modifierSpeciesReference',
256
+ species=ref.species().sid()
257
+ )
258
+ if ref.sid() is not None:
259
+ species_reference.attrib['id'] = ref.sid()
260
+ return species_reference
261
+
262
+ @staticmethod
263
+ def _model(model: Model) -> etree.ElementTree:
264
+ root = etree.Element(
265
+ 'sbml',
266
+ xmlns='http://www.sbml.org/sbml/level3/version2/core',
267
+ level='3',
268
+ version='2'
269
+ )
270
+
271
+ # setup a map from unit to string
272
+ unit_map_to_str = {
273
+ unit: string for string, unit in Model.base_units.items()
274
+ }
275
+ for unitid, unit in model.units().items():
276
+ unit_map_to_str[unit] = unitid
277
+
278
+ name = model.name() if model.name() else 'unnamed_model'
279
+ model_root = etree.Element('model', id=name)
280
+ if model.time_units() != myokit.units.dimensionless:
281
+ model_root.attrib['timeUnits'] = unit_map_to_str[
282
+ model.time_units()
283
+ ]
284
+ if model.area_units() != myokit.units.dimensionless:
285
+ model_root.attrib['areaUnits'] = unit_map_to_str[
286
+ model.area_units()
287
+ ]
288
+ if model.length_units() != myokit.units.dimensionless:
289
+ model_root.attrib['lengthUnits'] = unit_map_to_str[
290
+ model.length_units()
291
+ ]
292
+ if model.substance_units() != myokit.units.dimensionless:
293
+ model_root.attrib['substanceUnits'] = unit_map_to_str[
294
+ model.substance_units()
295
+ ]
296
+ if model.extent_units() != myokit.units.dimensionless:
297
+ model_root.attrib['extentUnits'] = unit_map_to_str[
298
+ model.extent_units()
299
+ ]
300
+ if model.volume_units() != myokit.units.dimensionless:
301
+ model_root.attrib['volumeUnits'] = unit_map_to_str[
302
+ model.volume_units()
303
+ ]
304
+
305
+ if model.has_units():
306
+ list_of_units = etree.Element('listOfUnitDefinitions')
307
+ for sid, unit in model.units().items():
308
+ node = SBMLWriter._unit(sid, unit)
309
+ list_of_units.append(node)
310
+ model_root.append(list_of_units)
311
+ if model.compartments():
312
+ list_of_compartments = etree.Element('listOfCompartments')
313
+ for compartment in model.compartments():
314
+ node = SBMLWriter._compartment(compartment, unit_map_to_str)
315
+ list_of_compartments.append(node)
316
+ model_root.append(list_of_compartments)
317
+ list_of_rules = etree.Element('listOfRules')
318
+ list_of_initial_assignments = etree.Element('listOfInitialAssignments')
319
+ if model.parameters():
320
+ list_of_parameters = etree.Element('listOfParameters')
321
+ for parameter in model.parameters():
322
+ param_node, initial_value_node, rule_node = \
323
+ SBMLWriter._parameter(parameter, unit_map_to_str)
324
+ list_of_parameters.append(param_node)
325
+ if initial_value_node is not None:
326
+ list_of_initial_assignments.append(initial_value_node)
327
+ if rule_node is not None:
328
+ list_of_rules.append(rule_node)
329
+ model_root.append(list_of_parameters)
330
+ if model.species_list():
331
+ list_of_species = etree.Element('listOfSpecies')
332
+ for species in model.species_list():
333
+ species_node, rule_node = SBMLWriter._species(
334
+ species,
335
+ unit_map_to_str
336
+ )
337
+ if rule_node is not None:
338
+ list_of_rules.append(rule_node)
339
+ list_of_species.append(species_node)
340
+ model_root.append(list_of_species)
341
+ if model.reactions():
342
+ list_of_reactions = etree.Element('listOfReactions')
343
+ for reaction in model.reactions():
344
+ node = SBMLWriter._reaction(reaction)
345
+ list_of_reactions.append(node)
346
+ model_root.append(list_of_reactions)
347
+
348
+ if len(list_of_initial_assignments) > 0:
349
+ model_root.append(list_of_initial_assignments)
350
+
351
+ if len(list_of_rules) > 0:
352
+ model_root.append(list_of_rules)
353
+
354
+ root.append(model_root)
355
+ return etree.ElementTree(root)
@@ -104,6 +104,9 @@ class ExportTest(unittest.TestCase):
104
104
  name = 'test_' + name + '_exporter'
105
105
  self.assertIn(name, methods)
106
106
 
107
+ def test_sbml_exporter(self):
108
+ self._test(myokit.formats.exporter('sbml'))
109
+
107
110
  def test_ansic_exporter(self):
108
111
  self._test(myokit.formats.exporter('ansic'))
109
112
 
@@ -15,7 +15,63 @@ import myokit.formats
15
15
  import myokit.formats.sbml
16
16
 
17
17
  # from shared import DIR_FORMATS, WarningCollector
18
- from myokit.tests import DIR_FORMATS, WarningCollector
18
+ from myokit.tests import DIR_FORMATS, WarningCollector, TemporaryDirectory
19
+
20
+
21
+ class SBMLExporterTest(unittest.TestCase):
22
+ """
23
+ Tests for :class:`myokit.formats.sbml.SBMLExporter`.
24
+ """
25
+
26
+ def test_capability_reporting(self):
27
+ # Test if the right capabilities are reported.
28
+ e = myokit.formats.exporter('sbml')
29
+ self.assertTrue(e.supports_model())
30
+ self.assertFalse(e.supports_runnable())
31
+
32
+ def test_stimulus_generation(self):
33
+ # Tests if protocols allow a stimulus current to be added
34
+
35
+ e = myokit.formats.exporter('sbml')
36
+ i = myokit.formats.importer('sbml')
37
+
38
+ # Load input model
39
+ m1, p1, _ = myokit.load('example')
40
+ org_code = m1.code()
41
+
42
+ # 1. Export without a protocol
43
+ with TemporaryDirectory() as d:
44
+ path = d.path('model.sbml')
45
+ with WarningCollector() as w:
46
+ e.model(path, m1)
47
+ m2 = i.model(path)
48
+ self.assertFalse(w.has_warnings())
49
+ self.assertTrue(isinstance(m2.get('global.pace').rhs(), myokit.Number))
50
+
51
+ # 2. Export with protocol, but without variable bound to pacing
52
+ m1.get('engine.pace').set_binding(None)
53
+ with TemporaryDirectory() as d:
54
+ path = d.path('model.sbml')
55
+ with WarningCollector() as w:
56
+ e.model(path, m1, p1)
57
+ m2 = i.model(path)
58
+ self.assertTrue(w.has_warnings())
59
+ self.assertTrue(isinstance(m2.get('global.pace').rhs(), myokit.Number))
60
+
61
+ # 3. Export with protocol and variable bound to pacing
62
+ m1.get('engine.pace').set_binding('pace')
63
+ with TemporaryDirectory() as d:
64
+ path = d.path('model.cellml')
65
+ with WarningCollector() as w:
66
+ e.model(path, m1, p1)
67
+ m2 = i.model(path)
68
+ self.assertFalse(w.has_warnings())
69
+ rhs = m2.get('global.i_stim').rhs()
70
+ self.assertTrue(rhs, myokit.Multiply)
71
+ self.assertTrue(isinstance(rhs[0], myokit.Piecewise))
72
+
73
+ # Check original model is unchanged
74
+ self.assertEqual(org_code, m1.code())
19
75
 
20
76
 
21
77
  class SBMLImporterTest(unittest.TestCase):
@@ -2181,6 +2181,96 @@ class SBMLTestMyokitModel(unittest.TestCase):
2181
2181
  myokit.Plus(myokit.Number(1), myokit.Name(m.time())))
2182
2182
 
2183
2183
 
2184
+ class SBMLTestModelFromMyokit(unittest.TestCase):
2185
+ def test_basic(self):
2186
+ m = myokit.Model()
2187
+ c = m.add_component('comp')
2188
+ v = c.add_variable('var')
2189
+ v.set_rhs(myokit.Number(3))
2190
+ t = c.add_variable('time')
2191
+ t.set_binding('time')
2192
+ t.set_rhs(myokit.Number(0))
2193
+
2194
+ s = sbml.Model.from_myokit_model(m)
2195
+ compartment_names = [c.sid() for c in s.compartments()]
2196
+ self.assertCountEqual(compartment_names, [])
2197
+ parameter_names = [v.sid() for v in s.parameters()]
2198
+ self.assertCountEqual(parameter_names, ['var'])
2199
+
2200
+ def test_vars_with_same_name(self):
2201
+ m = myokit.Model()
2202
+ c = m.add_component('comp')
2203
+ v = c.add_variable('var')
2204
+ v.set_rhs(myokit.Number(3))
2205
+ c = m.add_component('comp2')
2206
+ v = c.add_variable('var')
2207
+ v.set_rhs(myokit.Number(3))
2208
+ t = c.add_variable('time')
2209
+ t.set_binding('time')
2210
+ t.set_rhs(myokit.Number(0))
2211
+
2212
+ s = sbml.Model.from_myokit_model(m)
2213
+ compartment_names = [c.sid() for c in s.compartments()]
2214
+ self.assertCountEqual(compartment_names, [])
2215
+ parameter_names = [v.sid() for v in s.parameters()]
2216
+ self.assertCountEqual(parameter_names, ['comp2_var', 'comp_var'])
2217
+
2218
+ def test_rate_eqn_with_unit(self):
2219
+ m = myokit.Model()
2220
+ c = m.add_component('comp')
2221
+
2222
+ t = c.add_variable('time', rhs=myokit.Number(0))
2223
+ t.set_unit(myokit.units.second)
2224
+ t.set_binding('time')
2225
+
2226
+ p = c.add_variable('param')
2227
+ p.set_rhs(myokit.Number(2))
2228
+
2229
+ v = c.add_variable('var', initial_value=myokit.Number(1))
2230
+ v_unit = 1e3 * myokit.units.meter
2231
+ v.set_unit(v_unit)
2232
+ v.set_rhs(myokit.Multiply(myokit.Number(4), myokit.Name(p)))
2233
+
2234
+ v = c.add_variable('var2', initial_value=myokit.Number(1))
2235
+ v.set_unit(myokit.units.meter)
2236
+ v.set_rhs(myokit.Number(4))
2237
+
2238
+ s = sbml.Model.from_myokit_model(m)
2239
+ parameter_names = [v.sid() for v in s.parameters()]
2240
+ self.assertCountEqual(parameter_names, ['var', 'var2', 'param'])
2241
+ # only non-base unit is kilometer
2242
+ self.assertCountEqual(s.units().values(), [v_unit])
2243
+ self.assertEqual(s.time_units(), myokit.units.second)
2244
+ v = s.parameter('var')
2245
+ self.assertEqual(v.initial_value(), myokit.Number(1))
2246
+ self.assertEqual(
2247
+ v.value(),
2248
+ myokit.Multiply(myokit.Number(4), myokit.Name(p))
2249
+ )
2250
+
2251
+ def test_incompatible_unit(self):
2252
+ m = myokit.Model()
2253
+ c = m.add_component('comp')
2254
+
2255
+ t = c.add_variable('time', rhs=myokit.Number(0))
2256
+ t.set_unit(myokit.units.second)
2257
+ t.set_binding('time')
2258
+
2259
+ p = c.add_variable('param')
2260
+ p.set_rhs(myokit.Number(2))
2261
+ p.set_unit(myokit.units.meter)
2262
+
2263
+ v = c.add_variable('var', initial_value=myokit.Number(1))
2264
+ v.set_rhs(myokit.Plus(myokit.Name(p), myokit.Name(t)))
2265
+
2266
+ s = sbml.Model.from_myokit_model(m)
2267
+ parameter_names = [v.sid() for v in s.parameters()]
2268
+ self.assertCountEqual(parameter_names, ['var', 'param'])
2269
+
2270
+ p = s.parameter('var')
2271
+ self.assertIsNone(p.units())
2272
+
2273
+
2184
2274
  if __name__ == '__main__':
2185
2275
  import warnings
2186
2276
  warnings.simplefilter('always')