myokit 1.37.0__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.
Files changed (45) hide show
  1. myokit/__init__.py +2 -2
  2. myokit/_aux.py +4 -0
  3. myokit/_datablock.py +10 -10
  4. myokit/_datalog.py +55 -11
  5. myokit/_myokit_version.py +1 -1
  6. myokit/_sim/cvodessim.py +3 -3
  7. myokit/formats/axon/_abf.py +11 -4
  8. myokit/formats/diffsl/__init__.py +60 -0
  9. myokit/formats/diffsl/_ewriter.py +145 -0
  10. myokit/formats/diffsl/_exporter.py +435 -0
  11. myokit/formats/heka/__init__.py +4 -0
  12. myokit/formats/heka/_patchmaster.py +408 -156
  13. myokit/formats/sbml/__init__.py +21 -1
  14. myokit/formats/sbml/_api.py +160 -6
  15. myokit/formats/sbml/_exporter.py +53 -0
  16. myokit/formats/sbml/_writer.py +355 -0
  17. myokit/gui/datalog_viewer.py +17 -3
  18. myokit/tests/data/io/bad1d-2-no-header.zip +0 -0
  19. myokit/tests/data/io/bad1d-3-no-data.zip +0 -0
  20. myokit/tests/data/io/bad1d-4-not-a-zip.zip +1 -105
  21. myokit/tests/data/io/bad1d-5-bad-data-type.zip +0 -0
  22. myokit/tests/data/io/bad1d-6-time-too-short.zip +0 -0
  23. myokit/tests/data/io/bad1d-7-0d-too-short.zip +0 -0
  24. myokit/tests/data/io/bad1d-8-1d-too-short.zip +0 -0
  25. myokit/tests/data/io/bad2d-2-no-header.zip +0 -0
  26. myokit/tests/data/io/bad2d-3-no-data.zip +0 -0
  27. myokit/tests/data/io/bad2d-4-not-a-zip.zip +1 -105
  28. myokit/tests/data/io/bad2d-5-bad-data-type.zip +0 -0
  29. myokit/tests/data/io/bad2d-8-2d-too-short.zip +0 -0
  30. myokit/tests/data/io/block1d.mmt +187 -0
  31. myokit/tests/data/io/datalog-18-duplicate-keys.csv +4 -0
  32. myokit/tests/test_aux.py +4 -0
  33. myokit/tests/test_datablock.py +6 -6
  34. myokit/tests/test_datalog.py +20 -0
  35. myokit/tests/test_formats_diffsl.py +728 -0
  36. myokit/tests/test_formats_exporters_run.py +6 -0
  37. myokit/tests/test_formats_sbml.py +57 -1
  38. myokit/tests/test_sbml_api.py +90 -0
  39. myokit/tests/test_sbml_export.py +327 -0
  40. {myokit-1.37.0.dist-info → myokit-1.37.2.dist-info}/LICENSE.txt +1 -1
  41. {myokit-1.37.0.dist-info → myokit-1.37.2.dist-info}/METADATA +4 -4
  42. {myokit-1.37.0.dist-info → myokit-1.37.2.dist-info}/RECORD +45 -36
  43. {myokit-1.37.0.dist-info → myokit-1.37.2.dist-info}/WHEEL +1 -1
  44. {myokit-1.37.0.dist-info → myokit-1.37.2.dist-info}/entry_points.txt +0 -0
  45. {myokit-1.37.0.dist-info → myokit-1.37.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,435 @@
1
+ #
2
+ # Export as a DiffSL model.
3
+ #
4
+ # This file is part of Myokit.
5
+ # See http://myokit.org for copyright, sharing, and licensing details.
6
+ #
7
+ import collections
8
+ import warnings
9
+
10
+ import myokit
11
+ import myokit.formats
12
+ import myokit.formats.diffsl as diffsl
13
+ import myokit.lib.guess as guess
14
+ import myokit.lib.markov as markov
15
+
16
+
17
+ class DiffSLExporter(myokit.formats.Exporter):
18
+ """
19
+ This :class:`Exporter <myokit.formats.Exporter>` generates a DiffSL
20
+ implementation of a Myokit model.
21
+
22
+ Only the model definition is exported. No inputs are provided, there is
23
+ no protocol defined, and only state variables are output.
24
+
25
+ For details of the language, see https://martinjrobins.github.io/diffsl/
26
+ """
27
+
28
+ def model(self, path, model, protocol=None, convert_units=True):
29
+ """
30
+ Exports a :class:`myokit.Model` in DiffSL format, writing the result to
31
+ the file indicated by ``path``.
32
+
33
+ A :class:`myokit.ExportError` will be raised if any errors occur.
34
+ Warnings will be generated if unsupported functions (e.g. atan) are
35
+ used in the model. For a full list, see
36
+ :class:`myokit.formats.diffsl.DiffSLExpressionWriter
37
+ <DiffSLExpressionWriter>`.
38
+
39
+ Arguments:
40
+
41
+ ``path``
42
+ The path to write the generated model to.
43
+ ``model``
44
+ The :class:`myokit.Model` to export.
45
+ ``protocol``
46
+ Not implemented!
47
+ ``convert_units``
48
+ If set to ``True`` (default), the method will attempt to convert to
49
+ preferred units for voltage (mV), current (A/F), and time (ms).
50
+ """
51
+ # Raise exception if protocol is set
52
+ if protocol is not None:
53
+ raise ValueError(
54
+ 'DiffSL export does not support an input protocol.'
55
+ )
56
+
57
+ # Check model validity
58
+ try:
59
+ model.validate()
60
+ except myokit.MyokitError as e:
61
+ raise myokit.ExportError(
62
+ 'DiffSL export requires a valid model.'
63
+ ) from e
64
+
65
+ # Prepare model for export
66
+ model = self._prep_model(model, convert_units)
67
+
68
+ # Generate DiffSL model
69
+ diffsl_model = self._generate_diffsl(model)
70
+
71
+ # Write DiffSL model to file
72
+ with open(path, 'w') as f:
73
+ f.write(diffsl_model)
74
+
75
+ def _prep_model(self, model, convert_units):
76
+ """
77
+ Prepare the model for export to DiffSL.
78
+ """
79
+ # Rewrite model so that any Markov models have a 1-sum(...) state
80
+ # This also clones the model, so that changes can be made
81
+ model = markov.convert_markov_models_to_compact_form(model)
82
+
83
+ # Remove all model bindings apart from time and pace
84
+ _ = myokit._prepare_bindings(
85
+ model,
86
+ {
87
+ 'time': 't',
88
+ 'pace': 'pace',
89
+ },
90
+ )
91
+
92
+ # Remove hardcoded stimulus protocol, if any
93
+ guess.remove_embedded_protocol(model)
94
+
95
+ # Get / try to guess some model variables
96
+ time = model.time() # engine.time
97
+ vm = guess.membrane_potential(model) # Vm
98
+ cm = guess.membrane_capacitance(model) # Cm
99
+ currents = self._guess_currents(model)
100
+
101
+ # Check for explicit time dependence
102
+ time_refs = list(time.refs_by())
103
+ if time_refs:
104
+ raise myokit.ExportError(
105
+ 'DiffSL export does not support explicit time dependence:\n'
106
+ + '\n'.join([f'{v} = {v.rhs()}' for v in time_refs])
107
+ )
108
+
109
+ if convert_units:
110
+ # Convert currents to A/F
111
+ helpers = [] if cm is None else [cm.rhs()]
112
+ for var in currents:
113
+ self._convert_current_unit(var, helpers=helpers)
114
+
115
+ # Convert potentials to mV
116
+ self._convert_potential_unit(vm)
117
+
118
+ # Convert time to ms
119
+ if time.unit() != myokit.units.ms:
120
+ self._convert_unit(time, 'ms')
121
+
122
+ # Add intermediary variables for state derivatives with rhs references
123
+ # Before:
124
+ # dot(x) = x / 5
125
+ # y = 1 + dot(x)
126
+ # After:
127
+ # dot_x = x / 5
128
+ # dot(x) = dot_x
129
+ # y = 1 + dot_x
130
+ model.remove_derivative_references()
131
+
132
+ return model
133
+
134
+ def _generate_diffsl(self, model):
135
+ """
136
+ Generate a DiffSL model from a prepped Myokit model.
137
+ DiffSL inputs will be left empty, and outputs will be set to
138
+ state variables in alphabetical order.
139
+ """
140
+
141
+ # Create DiffSL-compatible variable names
142
+ var_to_name = self._create_diffsl_variable_names(model)
143
+
144
+ # Create a naming function
145
+ def var_name(e):
146
+ if isinstance(e, myokit.Derivative):
147
+ return var_to_name[e]
148
+ elif isinstance(e, myokit.LhsExpression):
149
+ return var_to_name[e.var()]
150
+ elif isinstance(e, myokit.Variable):
151
+ return var_to_name[e]
152
+ raise ValueError( # pragma: no cover
153
+ 'Not a variable or LhsExpression: ' + str(e)
154
+ )
155
+
156
+ # Create an expression writer
157
+ e = diffsl.DiffSLExpressionWriter()
158
+ e.set_lhs_function(var_name)
159
+
160
+ export_lines = [] # DiffSL export lines
161
+ tab = ' ' # Tab character
162
+
163
+ # Sort equations in solvable order (grouped by component)
164
+ sorted_eqs = model.solvable_order()
165
+
166
+ # Variables to be excluded from output or handled separately.
167
+ # State derivatives are handled in dudt_i, F_i and G_i; time is
168
+ # excluded from the output; pace is handled separately.
169
+ time = model.time()
170
+ pace = model.binding('pace')
171
+ special_vars = set(v for v in model.states())
172
+ special_vars.add(time)
173
+ if pace is not None:
174
+ special_vars.add(pace)
175
+
176
+ # Add metadata
177
+ export_lines.append('/*')
178
+ export_lines.append('This file was generated by Myokit.')
179
+ if model.meta:
180
+ export_lines.append('')
181
+ for key, val in sorted(model.meta.items()):
182
+ export_lines.append(f'{key}: {val}')
183
+ export_lines.append('')
184
+ export_lines.append('*/')
185
+ export_lines.append('')
186
+
187
+ # Add empty input parameter list
188
+ export_lines.append('/* Input parameters */')
189
+ export_lines.append('/* E.g. in = [ varZero, varOne, varTwo ] */')
190
+ export_lines.append('in = [ ]')
191
+ export_lines.append('')
192
+
193
+ # Add pace
194
+ if pace is not None:
195
+ export_lines.append('/* Engine: pace */')
196
+ export_lines.append('/* E.g.')
197
+ export_lines.append(' -80 * (1 - sigmoid((t-100)*5000))')
198
+ export_lines.append(
199
+ ' -120 * (sigmoid((t-100)*5000) - sigmoid((t-200)*5000))'
200
+ )
201
+ export_lines.append('*/')
202
+
203
+ lhs = var_name(pace)
204
+ rhs = e.ex(pace.rhs())
205
+ qname = pace.qname()
206
+ unit = '' if pace.unit() is None else f' {pace.unit()}'
207
+ export_lines.append(f'{lhs} {{ {rhs} }} /* {qname}{unit} */')
208
+ export_lines.append('')
209
+
210
+ # Add constants
211
+ const_vars = (
212
+ set(model.variables(const=True, deep=True, state=False))
213
+ - special_vars
214
+ )
215
+ for component_label, eq_list in sorted_eqs.items():
216
+ const_eqs = [
217
+ eq for eq in eq_list.equations() if eq.lhs.var() in const_vars
218
+ ]
219
+ if const_eqs:
220
+ export_lines.append(f'/* Constants: {component_label} */')
221
+ for eq in const_eqs:
222
+ v = eq.lhs.var()
223
+ lhs = var_name(v)
224
+ rhs = e.ex(eq.rhs)
225
+ qname = v.qname()
226
+ unit = '' if v.unit() is None else f' {v.unit()}'
227
+ export_lines.append(
228
+ f'{lhs} {{ {rhs} }} /* {qname}{unit} */'
229
+ )
230
+ export_lines.append('')
231
+
232
+ # Add initial conditions `u_i`
233
+ export_lines.append('/* Initial conditions */')
234
+ export_lines.append('u_i {')
235
+ for v in model.states():
236
+ lhs = var_name(v)
237
+ rhs = v.initial_value()
238
+ qname = v.qname()
239
+ unit = '' if v.unit() is None else f' {v.unit()}'
240
+ export_lines.append(f'{tab}{lhs} = {rhs}, /* {qname}{unit} */')
241
+ export_lines.append('}')
242
+ export_lines.append('')
243
+
244
+ # Add state derivatives `dudt_i`
245
+ export_lines.append('dudt_i {')
246
+ for v in model.states():
247
+ lhs = var_name(v.lhs())
248
+ export_lines.append(f'{tab}{lhs} = 0,')
249
+ export_lines.append('}')
250
+ export_lines.append('')
251
+
252
+ # Add remaining variables
253
+ todo_vars = (
254
+ set(model.variables(const=False, deep=True, state=False))
255
+ - special_vars
256
+ )
257
+ for component_label, eq_list in sorted_eqs.items():
258
+ todo_eqs = [
259
+ eq for eq in eq_list.equations() if eq.lhs.var() in todo_vars
260
+ ]
261
+ if todo_eqs:
262
+ export_lines.append(f'/* Variables: {component_label} */')
263
+ for eq in todo_eqs:
264
+ v = eq.lhs.var()
265
+ lhs = var_name(v)
266
+ rhs = e.ex(eq.rhs)
267
+ qname = v.qname()
268
+ unit = '' if v.unit() is None else f' {v.unit()}'
269
+ export_lines.append(
270
+ f'{lhs} {{ {rhs} }} /* {qname}{unit} */'
271
+ )
272
+ export_lines.append('')
273
+
274
+ # Add `F_i`
275
+ export_lines.append('/* Solve */')
276
+ export_lines.append('F_i {')
277
+ for v in model.states():
278
+ lhs = var_name(v.lhs())
279
+ export_lines.append(f'{tab}{lhs},')
280
+ export_lines.append('}')
281
+ export_lines.append('')
282
+
283
+ # Add `G_i`
284
+ export_lines.append('G_i {')
285
+ for v in model.states():
286
+ rhs = e.ex(v.rhs())
287
+ export_lines.append(f'{tab}{rhs},')
288
+ export_lines.append('}')
289
+ export_lines.append('')
290
+
291
+ # Output state variables in alphabetical order
292
+ export_lines.append('/* Output */')
293
+ export_lines.append('out_i {')
294
+ vars = list(model.states())
295
+ for v in sorted(vars, key=lambda x: var_name(x).swapcase()):
296
+ lhs = var_name(v)
297
+ export_lines.append(f'{tab}{lhs},')
298
+ export_lines.append('}')
299
+ export_lines.append('')
300
+
301
+ return '\n'.join(export_lines)
302
+
303
+ def _convert_current_unit(self, var, helpers=None):
304
+ """
305
+ Convert a current to A/F if its present unit isn't recommended.
306
+ """
307
+ recommended_units = [
308
+ myokit.parse_unit('pA/pF'),
309
+ myokit.parse_unit('uA/cm^2'),
310
+ ]
311
+
312
+ if var.unit() not in recommended_units:
313
+ self._convert_unit(var, 'A/F', helpers=helpers)
314
+
315
+ def _convert_potential_unit(self, var, helpers=None):
316
+ """
317
+ Convert a potential to mV if its present unit isn't recommended.
318
+ """
319
+ recommended_units = [myokit.units.mV]
320
+
321
+ if var.unit() not in recommended_units:
322
+ self._convert_unit(var, 'mV', helpers=helpers)
323
+
324
+ def _convert_unit(self, var, unit, helpers=None):
325
+ """
326
+ Convert a variable to the given unit if possible. Throws a warning if
327
+ the conversion is not possible.
328
+ """
329
+ if var.unit() is None:
330
+ return
331
+
332
+ try:
333
+ var.convert_unit(unit, helpers=helpers)
334
+ except myokit.IncompatibleUnitError:
335
+ warnings.warn(
336
+ 'Unable to convert ' + var.qname() + ' to recommended'
337
+ ' units of ' + str(unit) + '.'
338
+ )
339
+
340
+ def _create_diffsl_variable_names(self, model):
341
+ """
342
+ Create DiffSL-compatible names for all variables in the model.
343
+
344
+ The following strategy is followed:
345
+ - Fully qualified names are used for all variables.
346
+ - Variables are checked for special names, and changed if necessary.
347
+ - Unsupported characters like '.' and '_' are replaced.
348
+ - Any conflicts are resolved in a final step by appending a number.
349
+ """
350
+
351
+ # Convert name to a DiffSL-compatible variable name
352
+ def convert_name(name):
353
+ # Remove unsupported chars like '_' and '.', and stagger case.
354
+ # Preserves existing staggered case in names e.g.
355
+ # voltage_clamp.R_seal_MOhm -> voltageClampRSealMOhm
356
+ # voltageClamp.RSealMOhm -> voltageClampRSealMOhm
357
+ name_chars = []
358
+ caps_flag = False
359
+ for ch in name:
360
+ if ch.isalpha():
361
+ if caps_flag:
362
+ name_chars.append(ch.upper())
363
+ caps_flag = False
364
+ else:
365
+ name_chars.append(ch)
366
+ elif ch.isdigit():
367
+ name_chars.append(ch)
368
+ caps_flag = True
369
+ else:
370
+ caps_flag = True
371
+
372
+ return ''.join(name_chars)
373
+
374
+ var_to_name = collections.OrderedDict()
375
+
376
+ # Store initial names for variables
377
+ for var in model.variables(deep=True, sort=True):
378
+ var_to_name[var] = convert_name(var.qname())
379
+ if var.is_state():
380
+ var_to_name[var.lhs()] = convert_name('diff_' + var.qname())
381
+
382
+ # Check for conflicts with known keywords
383
+ from . import keywords
384
+
385
+ needs_renaming = collections.OrderedDict()
386
+ for keyword in keywords:
387
+ needs_renaming[keyword] = []
388
+
389
+ # Find naming conflicts, create inverse mapping
390
+ name_to_var = collections.OrderedDict()
391
+ for var, name in var_to_name.items():
392
+
393
+ # Known conflict?
394
+ if name in needs_renaming:
395
+ needs_renaming[name].append(var)
396
+ continue
397
+
398
+ # Test for new conflicts
399
+ var2 = name_to_var.get(name, None)
400
+ if var2 is not None:
401
+ needs_renaming[name] = [var2, var]
402
+ continue
403
+
404
+ name_to_var[name] = var
405
+
406
+ # Resolve naming conflicts
407
+ for name, variables in needs_renaming.items():
408
+ # Add a number to the end of the name, increasing until unique
409
+ i = 1
410
+ root = name
411
+ for var in variables:
412
+ name = f'{root}{i}'
413
+ while name in name_to_var:
414
+ i += 1
415
+ name = f'{root}{i}'
416
+ var_to_name[var] = name
417
+ name_to_var[name] = var
418
+
419
+ return var_to_name
420
+
421
+ def _guess_currents(self, model):
422
+ """
423
+ Tries to make a list of membrane currents. Removes potentials
424
+ from the guessed list.
425
+ """
426
+ unmatch_units = [
427
+ myokit.units.mV,
428
+ myokit.units.V,
429
+ ]
430
+ guessed_currents = guess.membrane_currents(model)
431
+ return [x for x in guessed_currents if x.unit() not in unmatch_units]
432
+
433
+ def supports_model(self):
434
+ """See :meth:`myokit.formats.Exporter.supports_model()`."""
435
+ return True
@@ -9,7 +9,10 @@ from ._patchmaster import ( # noqa
9
9
  AmplifierSeries,
10
10
  AmplifierState,
11
11
  AmplifierStateRecord,
12
+ CSlowRange,
12
13
  EndianAwareReader,
14
+ Filter1Setting,
15
+ Filter2Type,
13
16
  Group,
14
17
  NoSupportedDAChannelError,
15
18
  PatchMasterFile,
@@ -22,6 +25,7 @@ from ._patchmaster import ( # noqa
22
25
  Stimulus,
23
26
  StimulusChannel,
24
27
  StimulusFile,
28
+ StimulusFilterSetting,
25
29
  Sweep,
26
30
  Trace,
27
31
  TreeNode,