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
myokit/__init__.py CHANGED
@@ -79,7 +79,7 @@ Copyright (c) 2017-2020 University of Oxford. All rights reserved.
79
79
  (University of Oxford means the Chancellor, Masters and Scholars of the
80
80
  University of Oxford, having an administrative office at Wellington Square,
81
81
  Oxford OX1 2JD, UK).
82
- Copyright (c) 2020-2024 University of Nottingham. All rights reserved.
82
+ Copyright (c) 2020-2025 University of Nottingham. All rights reserved.
83
83
 
84
84
  Redistribution and use in source and binary forms, with or without
85
85
  modification, are permitted provided that the following conditions are met:
@@ -118,7 +118,7 @@ LICENSE_HTML = """
118
118
  <br />(University of Oxford means the Chancellor, Masters and Scholars of
119
119
  the University of Oxford, having an administrative office at Wellington
120
120
  Square, Oxford OX1 2JD, UK).
121
- <br />Copyright (c) 2020-2024 University of Nottingham. All rights
121
+ <br />Copyright (c) 2020-2025 University of Nottingham. All rights
122
122
  reserved.</br></p>
123
123
  <p>
124
124
  Redistribution and use in source and binary forms, with or without
myokit/_aux.py CHANGED
@@ -591,6 +591,10 @@ def step(model, initial=None, reference=None, ignore_errors=False):
591
591
  # Get initial state
592
592
  if initial is None:
593
593
  initial = model.initial_values(as_floats=True)
594
+ else:
595
+ # Convert initial values etc to floats. Will also be performed by
596
+ # evaluate_derivatives, but done here for the printing code below.
597
+ initial = model.map_to_state(initial)
594
598
 
595
599
  # Get evaluation at initial state
596
600
  values = model.evaluate_derivatives(
myokit/_datablock.py CHANGED
@@ -562,8 +562,8 @@ class DataBlock1d:
562
562
  end += n0
563
563
  if end > nb:
564
564
  raise myokit.DataBlockReadError(
565
- 'Unable to read DataBlock1d: Header indicates larger data'
566
- ' than found in the body.')
565
+ 'Unable to read DataBlock1d: Header indicates more time'
566
+ ' data than found in the body.')
567
567
  data = array.array(dtype)
568
568
  data.frombytes(body[start:end])
569
569
  if sys.byteorder == 'big': # pragma: no cover
@@ -583,7 +583,7 @@ class DataBlock1d:
583
583
  end += n0
584
584
  if end > nb:
585
585
  raise myokit.DataBlockReadError(
586
- 'Unable to read DataBlock1d: Header indicates larger'
586
+ 'Unable to read DataBlock1d: Header indicates more 0d'
587
587
  ' data than found in the body.')
588
588
  data = array.array(dtype)
589
589
  data.frombytes(body[start:end])
@@ -602,7 +602,7 @@ class DataBlock1d:
602
602
  end += n1
603
603
  if end > nb:
604
604
  raise myokit.DataBlockReadError(
605
- 'Unable to read DataBlock1d: Header indicates larger'
605
+ 'Unable to read DataBlock1d: Header indicates more 1d'
606
606
  ' data than found in the body.')
607
607
  data = array.array(dtype)
608
608
  data.frombytes(body[start:end])
@@ -1351,8 +1351,8 @@ class DataBlock2d:
1351
1351
  end += n0
1352
1352
  if end > nb:
1353
1353
  raise myokit.DataBlockReadError(
1354
- 'Unable to read DataBlock2d: Header indicates larger data'
1355
- ' than found in the body.')
1354
+ 'Unable to read DataBlock2d: Header indicates more time'
1355
+ ' data than found in the body.')
1356
1356
 
1357
1357
  data = array.array(dtype)
1358
1358
  data.frombytes(body[start:end])
@@ -1373,8 +1373,8 @@ class DataBlock2d:
1373
1373
  end += n0
1374
1374
  if end > nb:
1375
1375
  raise myokit.DataBlockReadError(
1376
- 'Unable to read DataBlock2d: Header indicates larger'
1377
- ' data than found in the body.')
1376
+ 'Unable to read DataBlock2d: Header indicates more'
1377
+ ' 0d data than found in the body.')
1378
1378
  data = array.array(dtype)
1379
1379
  data.frombytes(body[start:end])
1380
1380
  if sys.byteorder == 'big': # pragma: no cover
@@ -1392,8 +1392,8 @@ class DataBlock2d:
1392
1392
  end += n2
1393
1393
  if end > nb:
1394
1394
  raise myokit.DataBlockReadError(
1395
- 'Unable to read DataBlock2d: Header indicates larger'
1396
- ' data than found in the body.')
1395
+ 'Unable to read DataBlock2d: Header indicates more'
1396
+ ' 2d data than found in the body.')
1397
1397
  data = array.array(dtype)
1398
1398
  data.frombytes(body[start:end])
1399
1399
  if sys.byteorder == 'big': # pragma: no cover
myokit/_datalog.py CHANGED
@@ -329,23 +329,27 @@ class DataLog(OrderedDict):
329
329
  """
330
330
  Creates a copy of the log, split with the given ``period``.
331
331
 
332
- Split signals are given indexes so that "current" becomes "0.current",
333
- "1.current", "2.current", etc.
332
+ Split signals are given indexes so that ``current`` becomes
333
+ ``0.current``, ``1.current``, ``2.current``, etc.
334
334
 
335
335
  If the logs entries do not divide well by ``period``, the remainder
336
336
  will be ignored. This happens commonly due to rounding point errors (in
337
337
  which case the remainder is a single entry). To disable this behavior,
338
338
  set ``discard_remainder=False``.
339
+
340
+ To split a log into a list of logs, use :meth:`DataLog.split_periodic`.
339
341
  """
340
342
  # Note: Using closed intervals can lead to logs of unequal length, so
341
343
  # it should be disabled here to ensure a valid log
342
344
  logs = self.split_periodic(period, adjust=True, closed_intervals=False)
345
+
343
346
  # Discard remainder if present
344
347
  if discard_remainder:
345
348
  if len(logs) > 1:
346
349
  n = logs[0].length()
347
350
  if logs[-1].length() < n:
348
351
  logs = logs[:-1]
352
+
349
353
  # Create new log with folded data
350
354
  out = myokit.DataLog()
351
355
  out._time = self._time
@@ -666,9 +670,11 @@ class DataLog(OrderedDict):
666
670
  """
667
671
  Loads a CSV file from disk and returns it as a :class:`DataLog`.
668
672
 
669
- The CSV file must start with a header line indicating the variable
670
- names, separated by commas. Each subsequent row should contain the
671
- values at a single point in time for all logged variables.
673
+ The CSV file must start with a header line indicating the column names,
674
+ separated by commas. Each subsequent row should contain the values at a
675
+ single point in time for all logged variables. Duplicate column names
676
+ will be given a suffix "-i", where "i" is an integer chosen to create
677
+ a unique name.
672
678
 
673
679
  The ``DataLog`` is created using the data type specified by the
674
680
  argument ``precision``, regardless of the data type of the stored data.
@@ -691,9 +697,18 @@ class DataLog(OrderedDict):
691
697
  'Syntax error on line ' + str(line) + ', character '
692
698
  + str(1 + char) + ': ' + msg)
693
699
 
700
+ # Detect Microsoft's annyoing utf-8-sig encoding
701
+ # Note: We have to perform the first read in utf-8 mode, because the
702
+ # default encoding on windows will turn the BOM into a different set of
703
+ # characters, so that f.read(1) won't actually equal \ufeff
704
+ encoding = None
705
+ with open(filename, 'r', encoding='utf-8') as f:
706
+ if f.read(1) == '\ufeff':
707
+ encoding = 'utf-8-sig'
708
+
694
709
  quote = '"'
695
710
  delim = ','
696
- with open(filename, 'r', newline=None) as f:
711
+ with open(filename, 'r', newline=None, encoding=encoding) as f:
697
712
  # Read header
698
713
  keys = [] # The log keys, in order of appearance
699
714
 
@@ -784,8 +799,32 @@ class DataLog(OrderedDict):
784
799
  if c == delim:
785
800
  e(1, i, 'Empty field in header.')
786
801
 
787
- # Create data structure
802
+ # Handle duplicate keys by adding "-i" to all keys that appear
803
+ # more than once. If an entry key-i already exists, i is
804
+ # incremented.
788
805
  m = len(keys)
806
+ if len(set(keys)) != m:
807
+ first_seen = {} # key: index first seen at
808
+ duplicates = {} # key: list of indices of duplicates
809
+ for i, key in enumerate(keys):
810
+ if key in first_seen:
811
+ try:
812
+ duplicates[key].append(i)
813
+ except KeyError:
814
+ duplicates[key] = [first_seen[key], i]
815
+ else:
816
+ first_seen[key] = i
817
+ for key, indices in duplicates.items():
818
+ j = 1
819
+ for i in indices:
820
+ k = key
821
+ while k in first_seen:
822
+ k = f'{key}-{j}'
823
+ j += 1
824
+ keys[i] = k
825
+ first_seen[k] = i
826
+
827
+ # Create data structure
789
828
  lists = []
790
829
  for key in keys:
791
830
  x = array.array(typecode)
@@ -1270,10 +1309,12 @@ class DataLog(OrderedDict):
1270
1309
 
1271
1310
  def split_periodic(self, period, adjust=False, closed_intervals=True):
1272
1311
  """
1273
- Splits this log into multiple logs, each covering an equal period of
1274
- time. For example a log covering the time span ``[0, 10000]`` can be
1275
- split with period ``1000`` to obtain ten logs covering ``[0, 1000]``,
1276
- ``[1000, 2000]`` etc.
1312
+ Splits this log into multiple logs, returning a list of
1313
+ :class:`DataLog` objects.
1314
+
1315
+ Each returned log covers an equal ``period`` of time. For example a log
1316
+ covering the time span ``[0, 10000]`` split with period ``1000`` will
1317
+ result in a list of ten logs ``[0, 1000]``, ``[1000, 2000]`` etc.
1277
1318
 
1278
1319
  The split log files can be returned as-is, or with the time variable's
1279
1320
  value adjusted so that all logs appear to cover the same span. To
@@ -1284,6 +1325,9 @@ class DataLog(OrderedDict):
1284
1325
  the duplication of some data points. To disable this behavior and
1285
1326
  return half-closed endpoints (containing only the left point), set
1286
1327
  ``closed_intervals`` to ``False``.
1328
+
1329
+ To return a single log with entries ``x`` split as ``x.0``, ``x.1``,
1330
+ etc., use :meth:`DataLog.fold`.
1287
1331
  """
1288
1332
  # Validate log before starting
1289
1333
  self.validate()
myokit/_myokit_version.py CHANGED
@@ -14,7 +14,7 @@ __release__ = True
14
14
  # incompatibility
15
15
  # - Changes to revision indicate bugfixes, tiny new features
16
16
  # - There is no significance to odd/even numbers
17
- __version_tuple__ = 1, 37, 0
17
+ __version_tuple__ = 1, 37, 2
18
18
 
19
19
  # String version of the version number
20
20
  __version__ = '.'.join([str(x) for x in __version_tuple__])
myokit/_sim/cvodessim.py CHANGED
@@ -695,9 +695,9 @@ class Simulation(myokit.CModule):
695
695
  An optional fixed size log interval. Must be ``None`` if
696
696
  ``log_times`` is used. If both are ``None`` every step is logged.
697
697
  ``log_times``
698
- An optional set of pre-determined logging times. Must be ``None``
699
- if ``log_interval`` is used. If both are ``None`` every step is
700
- logged.
698
+ An optional sequence (e.g. a list or a numpy array) of
699
+ pre-determined logging times. Must be ``None`` if ``log_interval``
700
+ is used. If both are ``None`` every step is logged.
701
701
  ``sensitivities``
702
702
  An optional list-of-lists to append the calculated sensitivities
703
703
  to.
@@ -574,8 +574,8 @@ class AbfFile(myokit.formats.SweepSource):
574
574
  # Only episodic stimulation is supported.
575
575
  if self._mode != ACMODE_EPISODIC_STIMULATION: # pragma: no cover
576
576
  warnings.warn(
577
- 'Unsupported acquisition method '
578
- + acquisition_modes[self._mode] + '; unable to read D/A'
577
+ 'Unsupported acquisition method'
578
+ f' {acquisition_modes[self._mode]}; unable to read D/A'
579
579
  ' channels.')
580
580
 
581
581
  # Remaining code is all about reading D/A info for episodic
@@ -713,7 +713,7 @@ class AbfFile(myokit.formats.SweepSource):
713
713
  elif t != EPOCH_DISABLED: # pragma: no cover
714
714
  use = False
715
715
  warnings.warn(
716
- f'Unsupported epoch type: {epoch_types(t)}')
716
+ f'Unsupported epoch type: {epoch_types[t]}')
717
717
  break
718
718
  elif source == DAC_DACFILEWAVEFORM: # pragma: no cover
719
719
  # Stimulus file? Then don't use
@@ -1375,7 +1375,14 @@ class AbfFile(myokit.formats.SweepSource):
1375
1375
  try:
1376
1376
  return self._unit_cache[unit_string]
1377
1377
  except KeyError:
1378
- unit = myokit.parse_unit(unit_string.replace(MU, 'u'))
1378
+ try:
1379
+ unit = myokit.parse_unit(unit_string.replace(MU, 'u'))
1380
+ except myokit.ParseError: # pragma: no cover
1381
+ if unit_string == 'oC':
1382
+ warnings.warn('Unsupported units degrees C.')
1383
+ else:
1384
+ warnings.warn(f'Unsupported units {unit_string}.')
1385
+ unit = myokit.units.dimensionless
1379
1386
  self._unit_cache[unit_string] = unit
1380
1387
  return unit
1381
1388
 
@@ -0,0 +1,60 @@
1
+ #
2
+ # Provides DiffSL support
3
+ #
4
+ # This file is part of Myokit.
5
+ # See http://myokit.org for copyright, sharing, and licensing details.
6
+ #
7
+ from ._ewriter import DiffSLExpressionWriter
8
+ from ._exporter import DiffSLExporter
9
+
10
+ # Importers
11
+
12
+ # Exporters
13
+ _exporters = {
14
+ 'diffsl': DiffSLExporter,
15
+ }
16
+
17
+
18
+ def exporters():
19
+ """
20
+ Returns a dict of all exporters available in this module.
21
+ """
22
+ return dict(_exporters)
23
+
24
+
25
+ # Expression writers
26
+ _ewriters = {
27
+ 'diffsl': DiffSLExpressionWriter,
28
+ }
29
+
30
+
31
+ def ewriters():
32
+ """
33
+ Returns a dict of all expression writers available in this module.
34
+ """
35
+ return dict(_ewriters)
36
+
37
+
38
+ #
39
+ # Language keywords
40
+ #
41
+ keywords = [
42
+ 'abs',
43
+ 'cos',
44
+ 'dudt',
45
+ 'exp',
46
+ 'F',
47
+ 'G',
48
+ 'heaviside',
49
+ 'in',
50
+ 'log',
51
+ 'M',
52
+ 'out',
53
+ 'pow',
54
+ 'sigmoid',
55
+ 'sin',
56
+ 'sqrt',
57
+ 't',
58
+ 'tan',
59
+ 'u',
60
+ ]
@@ -0,0 +1,145 @@
1
+ #
2
+ # DiffSL expression writer
3
+ #
4
+ # Supported functions:
5
+ # https://martinjrobins.github.io/diffsl/functions.html
6
+ #
7
+ # This file is part of Myokit.
8
+ # See http://myokit.org for copyright, sharing, and licensing details.
9
+ #
10
+ import warnings
11
+
12
+ from myokit import And, Equal, If, LessEqual, Log, MoreEqual, Not, Number
13
+ from myokit.formats.ansic import CBasedExpressionWriter
14
+
15
+
16
+ class DiffSLExpressionWriter(CBasedExpressionWriter):
17
+ """
18
+ This :class:`ExpressionWriter <myokit.formats.ExpressionWriter>` writes
19
+ equations for variables in DiffSL syntax.
20
+
21
+ For details of the language, see https://martinjrobins.github.io/diffsl/.
22
+
23
+ Warnings will be generated if unsupported functions are used in the model.
24
+ Unsupported functions: `acos`, `asin`, `atan`, `ceil`, `floor`.
25
+
26
+ Support for logic expressions is implemented with heaviside functions.
27
+ For example, `(a >= b)` is converted to `heaviside(a - b)`.
28
+
29
+ """
30
+
31
+ def __init__(self):
32
+ super().__init__()
33
+
34
+ # -- Literals and identifiers
35
+
36
+ # def _ex_name(self, e):
37
+ # def _ex_number(self, e):
38
+
39
+ # -- Functions
40
+
41
+ def _ex_abs(self, e):
42
+ return self._ex_function(e, 'abs')
43
+
44
+ def _ex_acos(self, e):
45
+ warnings.warn('Unsupported function: acos()')
46
+ return super()._ex_acos(e)
47
+
48
+ def _ex_asin(self, e):
49
+ warnings.warn('Unsupported function: asin()')
50
+ return super()._ex_asin(e)
51
+
52
+ def _ex_atan(self, e):
53
+ warnings.warn('Unsupported function: atan()')
54
+ return super()._ex_atan(e)
55
+
56
+ def _ex_ceil(self, e):
57
+ warnings.warn('Unsupported function: ceil()')
58
+ return super()._ex_ceil(e)
59
+
60
+ # def _ex_cos(self, e):
61
+ # def _ex_derivative(self, e):
62
+ # def _ex_divide(self, e):
63
+ # def _ex_exp(self, e):
64
+
65
+ def _ex_floor(self, e):
66
+ warnings.warn('Unsupported function: floor()')
67
+ return super()._ex_floor(e)
68
+
69
+ # def _ex_log(self, e):
70
+
71
+ def _ex_log10(self, e):
72
+ # Log10(a) = Log(a, 10.0) -> '(log(a) / log(10.0))'
73
+ return super()._ex_log(Log(e[0], Number(10)))
74
+
75
+ # def _ex_minus(self, e):
76
+ # def _ex_multiply(self, e):
77
+ # def _ex_plus(self, e):
78
+ # def _ex_power(self, e):
79
+ # def _ex_prefix_minus(self, e):
80
+ # def _ex_prefix_plus(self, e):
81
+ # def _ex_quotient(self, e):
82
+ # def _ex_remainder(self, e):
83
+ # def _ex_sin(self, e):
84
+ # def _ex_sqrt(self, e):
85
+ # def _ex_tan(self, e):
86
+
87
+ # -- Conditional operators
88
+
89
+ def _ex_and(self, e):
90
+ # (a and b) == a * b, where a, b are in {0, 1}
91
+ return f'{self.ex(e[0])} * {self.ex(e[1])}'
92
+
93
+ def _ex_equal(self, e):
94
+ # (a == b) == heaviside(a - b) * heaviside(b - a)
95
+ return self.ex(And(MoreEqual(e[0], e[1]), LessEqual(e[0], e[1])))
96
+
97
+ def _ex_less(self, e):
98
+ # (a < b) == 1 - heaviside(a - b)
99
+ return self.ex(Not(MoreEqual(e[0], e[1])))
100
+
101
+ def _ex_less_equal(self, e):
102
+ # (a <= b) == heaviside(b - a)
103
+ return f'heaviside({self.ex(e[1])} - {self.ex(e[0])})'
104
+
105
+ def _ex_more(self, e):
106
+ # (a > b) == 1 - heaviside(b - a)
107
+ return self.ex(Not(LessEqual(e[0], e[1])))
108
+
109
+ def _ex_more_equal(self, e):
110
+ # (a >= b) == heaviside(a - b)
111
+ return f'heaviside({self.ex(e[0])} - {self.ex(e[1])})'
112
+
113
+ def _ex_not(self, e):
114
+ # not(a) == (1 - a), where a is in {0, 1}
115
+ return f'(1 - {self.ex(e[0])})'
116
+
117
+ def _ex_not_equal(self, e):
118
+ # (a != b) == 1 - heaviside(a - b) * heaviside(b - a)
119
+ return self.ex(Not(Equal(e[0], e[1])))
120
+
121
+ def _ex_or(self, e):
122
+ # a or b == not(not(a) and not(b)), where a, b are in {0, 1}
123
+ return self.ex(Not(And(Not(e[0]), Not(e[1]))))
124
+
125
+ # -- Conditional expressions
126
+
127
+ def _ex_if(self, e):
128
+ _if = self.ex(e._i)
129
+ _then = self.ex(e._t)
130
+ _not_if = self.ex(Not(e._i))
131
+ _else = self.ex(e._e)
132
+
133
+ return f'({_if} * {_then} + {_not_if} * {_else})'
134
+
135
+ def _ex_piecewise(self, e):
136
+ # Convert piecewise to nested ifs
137
+ # e.g. piecewise(a, b, c, d, e) -> if(a, b, if(c, d, e))
138
+ n = len(e._i)
139
+
140
+ _nested_ifs = e._e[n]
141
+
142
+ for i in range(n - 1, -1, -1):
143
+ _nested_ifs = If(e._i[i], e._e[i], _nested_ifs)
144
+
145
+ return self._ex_if(_nested_ifs)