myokit 1.36.1__py3-none-any.whl → 1.37.0__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 (43) hide show
  1. myokit/__init__.py +6 -19
  2. myokit/_datablock.py +45 -55
  3. myokit/_datalog.py +2 -2
  4. myokit/_err.py +26 -3
  5. myokit/_expressions.py +241 -127
  6. myokit/_model_api.py +19 -13
  7. myokit/_myokit_version.py +1 -1
  8. myokit/_sim/jacobian.py +3 -3
  9. myokit/_sim/openclsim.py +5 -5
  10. myokit/_sim/rhs.py +1 -1
  11. myokit/formats/__init__.py +4 -9
  12. myokit/formats/ansic/_ewriter.py +4 -20
  13. myokit/formats/heka/_patchmaster.py +16 -10
  14. myokit/formats/opencl/_ewriter.py +3 -42
  15. myokit/formats/opencl/template/minilog.py +1 -1
  16. myokit/formats/sympy/_ereader.py +2 -1
  17. myokit/formats/wcp/_wcp.py +3 -3
  18. myokit/gui/datalog_viewer.py +12 -7
  19. myokit/lib/markov.py +2 -2
  20. myokit/lib/plots.py +4 -4
  21. myokit/tests/data/formats/wcp-file-empty.wcp +0 -0
  22. myokit/tests/test_datablock.py +10 -10
  23. myokit/tests/test_datalog.py +4 -1
  24. myokit/tests/test_expressions.py +532 -251
  25. myokit/tests/test_formats_ansic.py +6 -18
  26. myokit/tests/test_formats_cpp.py +0 -5
  27. myokit/tests/test_formats_cuda.py +7 -15
  28. myokit/tests/test_formats_easyml.py +4 -9
  29. myokit/tests/test_formats_latex.py +10 -11
  30. myokit/tests/test_formats_matlab.py +0 -8
  31. myokit/tests/test_formats_opencl.py +0 -29
  32. myokit/tests/test_formats_python.py +2 -19
  33. myokit/tests/test_formats_stan.py +0 -13
  34. myokit/tests/test_formats_sympy.py +3 -3
  35. myokit/tests/test_formats_wcp.py +15 -0
  36. myokit/tests/test_model.py +20 -20
  37. myokit/tests/test_parsing.py +19 -0
  38. {myokit-1.36.1.dist-info → myokit-1.37.0.dist-info}/METADATA +1 -1
  39. {myokit-1.36.1.dist-info → myokit-1.37.0.dist-info}/RECORD +43 -42
  40. {myokit-1.36.1.dist-info → myokit-1.37.0.dist-info}/LICENSE.txt +0 -0
  41. {myokit-1.36.1.dist-info → myokit-1.37.0.dist-info}/WHEEL +0 -0
  42. {myokit-1.36.1.dist-info → myokit-1.37.0.dist-info}/entry_points.txt +0 -0
  43. {myokit-1.36.1.dist-info → myokit-1.37.0.dist-info}/top_level.txt +0 -0
myokit/__init__.py CHANGED
@@ -336,29 +336,12 @@ from ._err import ( # noqa
336
336
  SimulationCancelledError,
337
337
  SimulationError,
338
338
  SimultaneousProtocolEventError,
339
+ TypeError,
339
340
  UnresolvedReferenceError,
340
341
  UnusedVariableError,
341
342
  VariableMappingError,
342
343
  )
343
344
 
344
- # Check if all errors imported
345
- # Dynamically importing them doesn't seem to be possible, and forgetting to
346
- # import an error creates a hard to debug bug (something needs to go wrong
347
- # before the interpreter reaches the code raising the error and notices it's
348
- # not there).
349
- if not __release__:
350
- from . import _err # noqa
351
- import inspect # noqa
352
- _globals = globals()
353
- ex, name, clas = None, None, None
354
- for ex in inspect.getmembers(_err):
355
- name, clas = ex
356
- if type(clas) == type(MyokitError) and issubclass(clas, MyokitError):
357
- if name not in _globals: # pragma: no cover
358
- raise Exception('Failed to import exception: ' + name)
359
- del ex, name, clas, _globals, inspect # Prevent public visibility
360
- del _err
361
-
362
345
  # Tools
363
346
  from . import float # noqa
364
347
  from . import tools # noqa
@@ -386,6 +369,7 @@ from ._expressions import ( # noqa
386
369
  And,
387
370
  ASin,
388
371
  ATan,
372
+ BinaryComparison,
389
373
  Ceil,
390
374
  Condition,
391
375
  Cos,
@@ -411,6 +395,8 @@ from ._expressions import ( # noqa
411
395
  Multiply,
412
396
  Name,
413
397
  Number,
398
+ NumericalInfixExpression,
399
+ NumericalPrefixExpression,
414
400
  Not,
415
401
  NotEqual,
416
402
  Or,
@@ -418,7 +404,6 @@ from ._expressions import ( # noqa
418
404
  Piecewise,
419
405
  Plus,
420
406
  Power,
421
- PrefixCondition,
422
407
  PrefixExpression,
423
408
  PrefixMinus,
424
409
  PrefixPlus,
@@ -427,6 +412,8 @@ from ._expressions import ( # noqa
427
412
  Sin,
428
413
  Sqrt,
429
414
  Tan,
415
+ UnaryNumericalFunction,
416
+ UnaryNumericalDimensionlessFunction,
430
417
  )
431
418
 
432
419
  # Unit and quantity
myokit/_datablock.py CHANGED
@@ -121,16 +121,19 @@ class DataBlock1d:
121
121
  if w < 1:
122
122
  raise ValueError('Minimum w is 1.')
123
123
  self._nx = w
124
+
124
125
  # Time
125
- time = np.array(time, copy=copy)
126
+ time = np.array(time) if copy else np.asarray(time)
126
127
  if len(time.shape) != 1:
127
128
  raise ValueError('Time must be a sequence.')
128
129
  if np.any(np.diff(time) < 0):
129
130
  raise ValueError('Time must be non-decreasing.')
130
131
  self._time = time
131
132
  self._nt = len(time)
133
+
132
134
  # 0d variables
133
135
  self._0d = {}
136
+
134
137
  # 1d variables
135
138
  self._1d = {}
136
139
 
@@ -235,7 +238,7 @@ class DataBlock1d:
235
238
  return 0
236
239
 
237
240
  # Get times in seconds, lengths in cm
238
- t = np.array(t, copy=False) * time_multiplier
241
+ t = np.asarray(t) * time_multiplier
239
242
  x = np.arange(i1, 1 + i2, dtype=float) * length
240
243
 
241
244
  # Use linear least squares to find the conduction velocity
@@ -287,8 +290,7 @@ class DataBlock1d:
287
290
  if d not in (0, 1):
288
291
  raise ValueError(
289
292
  'The given simulation log should only contain 0d or 1d'
290
- ' variables. Found <' + str(name) + '> with d = '
291
- + str(d) + '.')
293
+ f' variables. Found <{name}> with d = {d}.')
292
294
  if d == 1:
293
295
  if size is None:
294
296
  size = info.size()
@@ -322,7 +324,7 @@ class DataBlock1d:
322
324
  data = data.reshape((nt, nx), order='F')
323
325
  # If this is a view of existing data, make a copy!
324
326
  if data.base is not None:
325
- data = np.array(data)
327
+ data = np.copy(data)
326
328
  block.set1d(name, data, copy=False)
327
329
 
328
330
  return block
@@ -426,7 +428,7 @@ class DataBlock1d:
426
428
  z = np.reshape(z, (self._nt, self._nx), order='C')
427
429
  # If z is a view, create a copy
428
430
  if z.base is not None:
429
- z = np.array(z, copy=True)
431
+ z = np.copy(z)
430
432
  return x, y, z
431
433
 
432
434
  def keys0d(self):
@@ -667,12 +669,12 @@ class DataBlock1d:
667
669
  head_str = []
668
670
  head_str.append(str(self._nt))
669
671
  head_str.append(str(self._nx))
670
- head_str.append('"' + dtype + '"')
672
+ head_str.append(f'"{dtype}"')
671
673
  for name in self._0d:
672
- head_str.append('"' + name + '"')
674
+ head_str.append(f'"{name}"')
673
675
  head_str.append(str(1))
674
676
  for name in self._1d:
675
- head_str.append('"' + name + '"')
677
+ head_str.append(f'"{name}"')
676
678
  head_str = '\n'.join(head_str)
677
679
 
678
680
  # Create body
@@ -714,10 +716,9 @@ class DataBlock1d:
714
716
  name = str(name)
715
717
  if not name:
716
718
  raise ValueError('Name cannot be empty.')
717
- data = np.array(data, copy=copy)
719
+ data = np.array(data) if copy else np.asarray(data)
718
720
  if data.shape != (self._nt,):
719
- raise ValueError(
720
- 'Data must be sequence of length ' + str(self._nt) + '.')
721
+ raise ValueError(f'Data must be sequence of length {self._nt}.')
721
722
  self._0d[name] = data
722
723
 
723
724
  def set1d(self, name, data, copy=True):
@@ -734,10 +735,10 @@ class DataBlock1d:
734
735
  name = str(name)
735
736
  if not name:
736
737
  raise ValueError('Name cannot be empty.')
737
- data = np.array(data, copy=copy)
738
+ data = np.array(data) if copy else np.asarray(data)
738
739
  shape = (self._nt, self._nx)
739
740
  if data.shape != shape:
740
- raise ValueError('Data must have shape ' + str(shape) + '.')
741
+ raise ValueError(f'Data must have shape {shape}.')
741
742
  self._1d[name] = data
742
743
 
743
744
  def shape(self):
@@ -761,14 +762,16 @@ class DataBlock1d:
761
762
 
762
763
  The data will be copied, unless ``copy`` is set to ``False``.
763
764
  """
765
+ array = np.array if copy else np.asarray
766
+
764
767
  d = myokit.DataLog()
765
768
  d.set_time_key('time')
766
- d['time'] = np.array(self._time, copy=copy)
769
+ d['time'] = array(self._time)
767
770
  for k, v in self._0d.items():
768
- d[k] = np.array(v, copy=copy)
771
+ d[k] = array(v)
769
772
  for k, v in self._1d.items():
770
773
  for i in range(self._nx):
771
- d[str(i) + '.' + k] = np.array(v[:, i], copy=copy)
774
+ d[f'{i}.{k}'] = array(v[:, i])
772
775
  return d
773
776
 
774
777
  def trace(self, variable, x):
@@ -823,7 +826,7 @@ class DataBlock2d:
823
826
  self._ny = h
824
827
  self._nx = w
825
828
  # Time
826
- time = np.array(time, copy=copy)
829
+ time = np.array(time) if copy else np.asarray(time)
827
830
  if len(time.shape) != 1:
828
831
  raise ValueError('Time must be a sequence.')
829
832
  if not np.all(np.diff(time) >= 0):
@@ -951,8 +954,7 @@ class DataBlock2d:
951
954
  x1, y1 = [int(i) for i in pos1]
952
955
  if x1 < 0 or y1 < 0:
953
956
  raise ValueError(
954
- 'Negative indices not supported: pos1=('
955
- + str(x1) + ', ' + str(y1) + ').')
957
+ f'Negative indices not supported: pos1=({x1}, {y1}).')
956
958
  else:
957
959
  x1, y1 = 0, 0
958
960
 
@@ -960,8 +962,7 @@ class DataBlock2d:
960
962
  x2, y2 = [int(i) for i in pos2]
961
963
  if x2 < 0 or y2 < 0:
962
964
  raise ValueError(
963
- 'Negative indices not supported: pos2=('
964
- + str(x2) + ', ' + str(y2) + ').')
965
+ f'Negative indices not supported: pos2=({x2}, {y2}).')
965
966
  else:
966
967
  x2, y2 = x1 + w1, 0
967
968
 
@@ -1091,8 +1092,7 @@ class DataBlock2d:
1091
1092
  if d not in (0, 2):
1092
1093
  raise ValueError(
1093
1094
  'The given simulation log should only contain 0d or 2d'
1094
- ' variables. Found <' + str(name) + '> with d = '
1095
- + str(d) + '.')
1095
+ f' variables. Found <{name}> with d = {d}.')
1096
1096
  if d == 2:
1097
1097
  if size is None:
1098
1098
  size = info.size()
@@ -1122,7 +1122,7 @@ class DataBlock2d:
1122
1122
  data = data.reshape((nt, ny, nx), order='F')
1123
1123
  # If this is a view of existing data, make a copy!
1124
1124
  if data.base is not None:
1125
- data = np.array(data)
1125
+ data = np.copy(data)
1126
1126
  block.set2d(name, data, copy=False)
1127
1127
  return block
1128
1128
 
@@ -1164,9 +1164,7 @@ class DataBlock2d:
1164
1164
  return frames
1165
1165
 
1166
1166
  def is_square(self):
1167
- """
1168
- Returns True if this data block's grid is square.
1169
- """
1167
+ """ Returns True if this data block's grid is square. """
1170
1168
  return self._nx == self._ny
1171
1169
 
1172
1170
  def items0d(self):
@@ -1467,12 +1465,12 @@ class DataBlock2d:
1467
1465
  head_str.append(str(self._nt))
1468
1466
  head_str.append(str(self._ny))
1469
1467
  head_str.append(str(self._nx))
1470
- head_str.append('"' + dtype + '"')
1468
+ head_str.append(f'"{dtype}"')
1471
1469
  for name in self._0d:
1472
- head_str.append('"' + name + '"')
1470
+ head_str.append(f'"{name}"')
1473
1471
  head_str.append(str(2))
1474
1472
  for name in self._2d:
1475
- head_str.append('"' + name + '"')
1473
+ head_str.append(f'"{name}"')
1476
1474
  head_str = '\n'.join(head_str)
1477
1475
 
1478
1476
  # Create body
@@ -1514,7 +1512,7 @@ class DataBlock2d:
1514
1512
  delimy = '\n'
1515
1513
  data = self._2d[name]
1516
1514
  data = data[frame]
1517
- text = [delimx.join('"' + str(x) + '"' for x in [xname, yname, zname])]
1515
+ text = [delimx.join(f'"{x}"' for x in [xname, yname, zname])]
1518
1516
  for y, row in enumerate(data):
1519
1517
  for x, z in enumerate(row):
1520
1518
  text.append(delimx.join([str(x), str(y), myokit.float.str(z)]))
@@ -1559,10 +1557,9 @@ class DataBlock2d:
1559
1557
  name = str(name)
1560
1558
  if not name:
1561
1559
  raise ValueError('Name cannot be empty.')
1562
- data = np.array(data, copy=copy)
1560
+ data = np.array(data) if copy else np.asarray(data)
1563
1561
  if data.shape != (self._nt,):
1564
- raise ValueError(
1565
- 'Data must be sequence of length ' + str(self._nt) + '.')
1562
+ raise ValueError(f'Data must be sequence of length {self._nt}.')
1566
1563
  self._0d[name] = data
1567
1564
 
1568
1565
  def set2d(self, name, data, copy=True):
@@ -1579,10 +1576,10 @@ class DataBlock2d:
1579
1576
  name = str(name)
1580
1577
  if not name:
1581
1578
  raise ValueError('Name cannot be empty.')
1582
- data = np.array(data, copy=copy)
1579
+ data = np.array(data) if copy else np.asarray(data)
1583
1580
  shape = (self._nt, self._ny, self._nx)
1584
1581
  if data.shape != shape:
1585
- raise ValueError('Data must have shape ' + str(shape) + '.')
1582
+ raise ValueError(f'Data must have shape {shape}.')
1586
1583
  self._2d[name] = data
1587
1584
 
1588
1585
  def shape(self):
@@ -1606,20 +1603,20 @@ class DataBlock2d:
1606
1603
 
1607
1604
  The data will be copied, unless ``copy`` is set to ``False``.
1608
1605
  """
1606
+ array = np.array if copy else np.asarray
1609
1607
  d = myokit.DataLog()
1610
1608
 
1611
1609
  # Add 0d vectors
1612
1610
  d.set_time_key('time')
1613
- d['time'] = np.array(self._time, copy=copy)
1611
+ d['time'] = array(self._time)
1614
1612
  for k, v in self._0d.items():
1615
- d[k] = np.array(v, copy=copy)
1613
+ d[k] = array(v)
1616
1614
 
1617
1615
  # Add 2d fields
1618
1616
  for k, v in self._2d.items():
1619
1617
  for x in range(self._nx):
1620
- s = str(x) + '.'
1621
1618
  for y in range(self._ny):
1622
- d[s + str(y) + '.' + k] = np.array(v[:, y, x], copy=copy)
1619
+ d[f'{x}.{y}.{k}'] = array(v[:, y, x])
1623
1620
  return d
1624
1621
 
1625
1622
  def trace(self, variable, x, y):
@@ -1673,20 +1670,16 @@ class ColorMap:
1673
1670
 
1674
1671
  @staticmethod
1675
1672
  def exists(name):
1676
- """
1677
- Returns True if the given name corresponds to a colormap.
1678
- """
1673
+ """ Returns True if the given name corresponds to a colormap. """
1679
1674
  return name in ColorMap._colormaps
1680
1675
 
1681
1676
  @staticmethod
1682
1677
  def get(name):
1683
- """
1684
- Returns the colormap method indicated by the given name.
1685
- """
1678
+ """ Returns the colormap method indicated by the given name. """
1686
1679
  try:
1687
1680
  return ColorMap._colormaps[name]()
1688
1681
  except KeyError:
1689
- raise KeyError('Non-existent ColorMap "' + str(name) + '".')
1682
+ raise KeyError(f'Non-existent ColorMap "{name}".')
1690
1683
 
1691
1684
  @staticmethod
1692
1685
  def hsv_to_rgb(h, s, v):
@@ -1712,12 +1705,9 @@ class ColorMap:
1712
1705
  r[idx], g[idx], b[idx] = t[idx], p[idx], v[idx]
1713
1706
  idx = (i == 5)
1714
1707
  r[idx], g[idx], b[idx] = v[idx], p[idx], q[idx]
1715
- out = (
1716
- np.array(r * 255, dtype=np.uint8, copy=False),
1717
- np.array(g * 255, dtype=np.uint8, copy=False),
1718
- np.array(b * 255, dtype=np.uint8, copy=False),
1719
- )
1720
- return out
1708
+ return (np.array(r * 255, dtype=np.uint8),
1709
+ np.array(g * 255, dtype=np.uint8),
1710
+ np.array(b * 255, dtype=np.uint8))
1721
1711
 
1722
1712
  @staticmethod
1723
1713
  def image(name, x, y):
@@ -1746,7 +1736,7 @@ class ColorMap:
1746
1736
  Normalizes the given float data based on the specified lower and upper
1747
1737
  bounds.
1748
1738
  """
1749
- floats = np.array(floats, copy=True)
1739
+ floats = np.copy(floats)
1750
1740
  # Enforce lower and upper bounds
1751
1741
  floats[floats < lower] = lower
1752
1742
  floats[floats > upper] = upper
myokit/_datalog.py CHANGED
@@ -829,7 +829,7 @@ class DataLog(OrderedDict):
829
829
 
830
830
  # Guess time variable
831
831
  for key in keys:
832
- x = np.array(log[key], copy=False)
832
+ x = np.asarray(log[key])
833
833
  y = x[1:] - x[:-1]
834
834
  if np.all(y > 0):
835
835
  log.set_time_key(key)
@@ -1305,7 +1305,7 @@ class DataLog(OrderedDict):
1305
1305
 
1306
1306
  # No splitting needed? Return clone!
1307
1307
  if nlogs < 2:
1308
- return self.clone()
1308
+ return [self.clone()]
1309
1309
 
1310
1310
  # Find split points
1311
1311
  tstarts = tmin + np.arange(nlogs) * period
myokit/_err.py CHANGED
@@ -27,10 +27,19 @@ class MyokitError(Exception):
27
27
 
28
28
  class IntegrityError(MyokitError):
29
29
  """
30
- Raised if an integrity error is found in a model.
30
+ Raised if an "integrity" issue is found or created in a model or its
31
+ components and variables, for example missing parents or children, or
32
+ invalid references.
31
33
 
32
- The error message is stored in the property ``message``. An optional parser
33
- token may be obtained with :meth:`token()`.
34
+ Integrity errors are usually raised by the ``validate`` method, but can
35
+ also arise from certain manipulations, e.g. deleting or moving a variable
36
+ or component. Integrity errors can also be raised if they are detected
37
+ during some other operation.
38
+
39
+ The error message is stored in the property ``message``.
40
+
41
+ Integrity errors detected during parsing may set a token (pointing to the
42
+ position in the parsed text) retrievable with :meth:`token()`.
34
43
 
35
44
  *Extends:* :class:`myokit.MyokitError`
36
45
  """
@@ -393,6 +402,20 @@ class SimultaneousProtocolEventError(MyokitError):
393
402
  """
394
403
 
395
404
 
405
+ class TypeError(IntegrityError):
406
+ """
407
+ Raised by the expression system if expressions of one type are required but
408
+ others are found.
409
+
410
+ For example, when a Derivative is created with an argument that is not a
411
+ Name, when a condition is given as input to a numerical operator (e.g.
412
+ ``log(1 == 2)``), or when a conditional operator is applied to a number
413
+ (e.g. ``and(1, 2)``).
414
+
415
+ *Extends:* :class:`myokit.IntegrityError`
416
+ """
417
+
418
+
396
419
  class UnresolvedReferenceError(IntegrityError):
397
420
  """
398
421
  Raised when a reference to a variable cannot be resolved.