myokit 1.36.0__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 (47) 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/cvodessim.c +221 -149
  9. myokit/_sim/jacobian.py +3 -3
  10. myokit/_sim/mcl.h +54 -0
  11. myokit/_sim/openclsim.py +5 -5
  12. myokit/_sim/rhs.py +1 -1
  13. myokit/formats/__init__.py +4 -9
  14. myokit/formats/ansic/_ewriter.py +4 -20
  15. myokit/formats/heka/_patchmaster.py +16 -10
  16. myokit/formats/opencl/_ewriter.py +3 -42
  17. myokit/formats/opencl/template/minilog.py +1 -1
  18. myokit/formats/sympy/_ereader.py +2 -1
  19. myokit/formats/wcp/_wcp.py +3 -3
  20. myokit/gui/datalog_viewer.py +12 -7
  21. myokit/lib/hh.py +3 -0
  22. myokit/lib/markov.py +2 -2
  23. myokit/lib/plots.py +4 -4
  24. myokit/tests/data/formats/wcp-file-empty.wcp +0 -0
  25. myokit/tests/test_datablock.py +10 -10
  26. myokit/tests/test_datalog.py +4 -1
  27. myokit/tests/test_expressions.py +532 -251
  28. myokit/tests/test_formats_ansic.py +6 -18
  29. myokit/tests/test_formats_cpp.py +0 -5
  30. myokit/tests/test_formats_cuda.py +7 -15
  31. myokit/tests/test_formats_easyml.py +4 -9
  32. myokit/tests/test_formats_latex.py +10 -11
  33. myokit/tests/test_formats_matlab.py +0 -8
  34. myokit/tests/test_formats_opencl.py +0 -29
  35. myokit/tests/test_formats_python.py +2 -19
  36. myokit/tests/test_formats_stan.py +0 -13
  37. myokit/tests/test_formats_sympy.py +3 -3
  38. myokit/tests/test_formats_wcp.py +15 -0
  39. myokit/tests/test_lib_hh.py +36 -0
  40. myokit/tests/test_model.py +20 -20
  41. myokit/tests/test_parsing.py +19 -0
  42. {myokit-1.36.0.dist-info → myokit-1.37.0.dist-info}/METADATA +1 -1
  43. {myokit-1.36.0.dist-info → myokit-1.37.0.dist-info}/RECORD +47 -46
  44. {myokit-1.36.0.dist-info → myokit-1.37.0.dist-info}/LICENSE.txt +0 -0
  45. {myokit-1.36.0.dist-info → myokit-1.37.0.dist-info}/WHEEL +0 -0
  46. {myokit-1.36.0.dist-info → myokit-1.37.0.dist-info}/entry_points.txt +0 -0
  47. {myokit-1.36.0.dist-info → myokit-1.37.0.dist-info}/top_level.txt +0 -0
myokit/_expressions.py CHANGED
@@ -11,8 +11,6 @@ import numpy
11
11
 
12
12
  import myokit
13
13
 
14
- from myokit import IntegrityError
15
-
16
14
 
17
15
  # Expression precedence levels
18
16
  FUNCTION_CALL = 70
@@ -48,6 +46,11 @@ class Expression:
48
46
 
49
47
  # Store operands
50
48
  self._operands = () if operands is None else operands
49
+ for op in self._operands:
50
+ if not isinstance(op, Expression):
51
+ raise myokit.IntegrityError(
52
+ 'Expression operands must be other Expression objects.'
53
+ f' Found: {type(op)}.', self._token)
51
54
 
52
55
  # Store references
53
56
  self._references = set()
@@ -745,28 +748,27 @@ class Expression:
745
748
 
746
749
  # Check for cyclical dependency
747
750
  if id(self) in trail:
748
- raise IntegrityError('Cyclical expression found', self._token)
751
+ raise myokit.IntegrityError(
752
+ 'Cyclical expression found', self._token)
749
753
  trail2 = trail + [id(self)]
750
-
751
754
  # It's okay to do this check with id's. Even if there are multiple
752
755
  # objects that are equal, if they're cyclical you'll get back round to
753
756
  # the same ones eventually. Doing this with the value requires hash()
754
757
  # which requires code() which may not be safe to use before the
755
758
  # expressions have been validated.
759
+
760
+ # Check kids
756
761
  for op in self:
757
- if not isinstance(op, Expression):
758
- raise IntegrityError(
759
- 'Expression operands must be other Expression objects.'
760
- ' Found: ' + str(type(op)) + '.', self._token)
761
762
  op._validate(trail2)
762
763
 
763
764
  # Cache validation status
764
765
  self._cached_validation = True
765
766
 
767
+ # This is a relatively slow operation. Do _not_ use in performance
768
+ # sensitive parts of the expression system code.
766
769
  def walk(self, allowed_types=None):
767
770
  """
768
- Returns an iterator over this expression tree (depth-first). This is a
769
- slow operation. Do _not_ use in performance sensitive code!
771
+ Returns an iterator over this expression tree (depth-first).
770
772
 
771
773
  Example::
772
774
 
@@ -854,6 +856,7 @@ class Number(Expression):
854
856
  else:
855
857
  raise ValueError(
856
858
  'Unit in myokit.Number should be a myokit.Unit or None.')
859
+
857
860
  # Create nice string representation
858
861
  self._str = myokit.float.str(self._value)
859
862
  if self._str[-2:] == '.0':
@@ -892,8 +895,10 @@ class Number(Expression):
892
895
 
893
896
  def convert(self, unit):
894
897
  """
895
- Returns a copy of this number in a different unit. If the two units are
896
- not compatible a :class:`myokit.IncompatibleUnitError` is raised.
898
+ Returns a copy of this number in a different unit.
899
+
900
+ If the two units are not compatible a
901
+ :class:`myokit.IncompatibleUnitError` is raised.
897
902
  """
898
903
  return Number(myokit.Unit.convert(self._value, self._unit, unit), unit)
899
904
 
@@ -1137,8 +1142,8 @@ class Name(LhsExpression):
1137
1142
  # Check value: String is allowed at construction for debugging, but
1138
1143
  # not here!
1139
1144
  if not self._proper:
1140
- raise IntegrityError(
1141
- 'Name value "' + repr(self._value) + '" is not an instance of'
1145
+ raise myokit.IntegrityError(
1146
+ f'Name value "{repr(self._value)}" is not an instance of'
1142
1147
  ' class myokit.Variable', self._token)
1143
1148
 
1144
1149
  def var(self):
@@ -1159,8 +1164,9 @@ class Derivative(LhsExpression):
1159
1164
  def __init__(self, op):
1160
1165
  super().__init__((op,))
1161
1166
  if not isinstance(op, Name):
1162
- raise IntegrityError(
1163
- 'The dot() operator can only be used on variables.',
1167
+ raise myokit.TypeError(
1168
+ 'The dot() operator can only be used on variables (a'
1169
+ ' myokit.Derivative requires a myokit.Name as argument).',
1164
1170
  self._token)
1165
1171
  self._op = op
1166
1172
  self._proper = self._op._proper
@@ -1258,7 +1264,7 @@ class Derivative(LhsExpression):
1258
1264
  # Check that value is a variable has already been performed by name
1259
1265
  # Check if value is the name of a state variable
1260
1266
  if not self._op._value.is_state():
1261
- raise IntegrityError(
1267
+ raise myokit.IntegrityError(
1262
1268
  'Derivatives can only be defined for state variables.',
1263
1269
  self._token)
1264
1270
 
@@ -1278,15 +1284,17 @@ class PartialDerivative(LhsExpression):
1278
1284
  __hash__ = LhsExpression.__hash__
1279
1285
 
1280
1286
  def __init__(self, var1, var2):
1287
+ super().__init__((var1, var2))
1288
+
1289
+ # Type checking
1281
1290
  if not isinstance(var1, (Name, Derivative)):
1282
- raise IntegrityError(
1291
+ raise myokit.TypeError(
1283
1292
  'The first argument to a partial derivative must be a'
1284
- ' variable name or a dot() expression.')
1293
+ ' variable name or a dot() expression.', self._token)
1285
1294
  if not isinstance(var2, (Name, InitialValue)):
1286
- raise IntegrityError(
1295
+ raise myokit.TypeError(
1287
1296
  'The second argument to a partial derivative must be a'
1288
- ' variable name or an initial value.')
1289
- super().__init__((var1, var2))
1297
+ ' variable name or an initial value.', self._token)
1290
1298
 
1291
1299
  self._var1 = var1
1292
1300
  self._var2 = var2
@@ -1390,10 +1398,11 @@ class InitialValue(LhsExpression):
1390
1398
 
1391
1399
  def __init__(self, var):
1392
1400
  super().__init__((var, ))
1401
+
1402
+ # Type checking
1393
1403
  if not isinstance(var, Name):
1394
- raise IntegrityError(
1395
- 'The first argument to an initial value must be a variable'
1396
- ' name.', self._token)
1404
+ raise myokit.TypeError('The argument to an initial value must be a'
1405
+ ' variable name.', self._token)
1397
1406
 
1398
1407
  self._var = var
1399
1408
  self._references = set([self])
@@ -1452,7 +1461,7 @@ class InitialValue(LhsExpression):
1452
1461
  # Check if value is the name of a state variable
1453
1462
  var = self._var._value
1454
1463
  if not (isinstance(var, myokit.Variable) and var.is_state()):
1455
- raise IntegrityError(
1464
+ raise myokit.IntegrityError(
1456
1465
  'Initial values can only be defined for state variables.',
1457
1466
  self._token)
1458
1467
 
@@ -1499,7 +1508,17 @@ class PrefixExpression(Expression):
1499
1508
  self._op._tree_str(b, n + self._treeDent)
1500
1509
 
1501
1510
 
1502
- class PrefixPlus(PrefixExpression):
1511
+ class NumericalPrefixExpression(PrefixExpression):
1512
+ """ Base class for expressions with a single numerical operand. """
1513
+ def __init__(self, op):
1514
+ super().__init__(op)
1515
+ if isinstance(op, myokit.Condition):
1516
+ raise myokit.TypeError(
1517
+ 'Invalid operand type: expected a numerical operand but'
1518
+ f' found a condition ({type(op)}).', self._token)
1519
+
1520
+
1521
+ class PrefixPlus(NumericalPrefixExpression):
1503
1522
  """
1504
1523
  Prefixed plus. Indicates a positive number ``+op``.
1505
1524
 
@@ -1512,6 +1531,9 @@ class PrefixPlus(PrefixExpression):
1512
1531
  """
1513
1532
  _rep = '+'
1514
1533
 
1534
+ def __init__(self, op):
1535
+ super().__init__(op)
1536
+
1515
1537
  def _diff(self, lhs, idstates):
1516
1538
  return self._op._diff(lhs, idstates)
1517
1539
 
@@ -1525,7 +1547,7 @@ class PrefixPlus(PrefixExpression):
1525
1547
  self._op._polishb(b)
1526
1548
 
1527
1549
 
1528
- class PrefixMinus(PrefixExpression):
1550
+ class PrefixMinus(NumericalPrefixExpression):
1529
1551
  """
1530
1552
  Prefixed minus. Indicates a negative number ``-op``.
1531
1553
 
@@ -1619,7 +1641,23 @@ class InfixExpression(Expression):
1619
1641
  self._op2._tree_str(b, n + self._treeDent)
1620
1642
 
1621
1643
 
1622
- class Plus(InfixExpression):
1644
+ class NumericalInfixExpression(InfixExpression):
1645
+ """ Base class for infix expressions with numerical operands. """
1646
+ def __init__(self, left, right):
1647
+ super().__init__(left, right)
1648
+ if isinstance(left, myokit.Condition):
1649
+ raise myokit.TypeError(
1650
+ 'Invalid type for first operand: expected a numerical operand'
1651
+ f' but found a condition ({type(left)}).',
1652
+ self._token)
1653
+ if isinstance(right, myokit.Condition):
1654
+ raise myokit.TypeError(
1655
+ 'Invalid type for second operand: expected a numerical operand'
1656
+ f' but found a condition ({type(left)}).',
1657
+ self._token)
1658
+
1659
+
1660
+ class Plus(NumericalInfixExpression):
1623
1661
  """
1624
1662
  Represents the addition of two operands: ``left + right``.
1625
1663
 
@@ -1667,7 +1705,7 @@ class Plus(InfixExpression):
1667
1705
  + unit1.clarify() + ' and ' + unit2.clarify() + '.')
1668
1706
 
1669
1707
 
1670
- class Minus(InfixExpression):
1708
+ class Minus(NumericalInfixExpression):
1671
1709
  """
1672
1710
  Represents subtraction: ``left - right``.
1673
1711
 
@@ -1716,7 +1754,7 @@ class Minus(InfixExpression):
1716
1754
  + unit1.clarify() + ' and ' + unit2.clarify() + '.')
1717
1755
 
1718
1756
 
1719
- class Multiply(InfixExpression):
1757
+ class Multiply(NumericalInfixExpression):
1720
1758
  """
1721
1759
  Represents multiplication: ``left * right``.
1722
1760
 
@@ -1761,7 +1799,7 @@ class Multiply(InfixExpression):
1761
1799
  return unit1 * unit2
1762
1800
 
1763
1801
 
1764
- class Divide(InfixExpression):
1802
+ class Divide(NumericalInfixExpression):
1765
1803
  """
1766
1804
  Represents division: ``left / right``.
1767
1805
 
@@ -1818,7 +1856,7 @@ class Divide(InfixExpression):
1818
1856
  return unit1 / unit2
1819
1857
 
1820
1858
 
1821
- class Quotient(InfixExpression):
1859
+ class Quotient(NumericalInfixExpression):
1822
1860
  """
1823
1861
  Represents the quotient of a division ``left // right``, also known as
1824
1862
  integer division.
@@ -1884,7 +1922,7 @@ class Quotient(InfixExpression):
1884
1922
  return unit1 / unit2
1885
1923
 
1886
1924
 
1887
- class Remainder(InfixExpression):
1925
+ class Remainder(NumericalInfixExpression):
1888
1926
  """
1889
1927
  Represents the remainder of a division (the "modulo"), expressed in ``mmt``
1890
1928
  syntax as ``left % right``.
@@ -1962,7 +2000,7 @@ class Remainder(InfixExpression):
1962
2000
  return unit1
1963
2001
 
1964
2002
 
1965
- class Power(InfixExpression):
2003
+ class Power(NumericalInfixExpression):
1966
2004
  """
1967
2005
  Represents exponentiation: ``left ^ right``.
1968
2006
 
@@ -2060,7 +2098,8 @@ class Function(Expression):
2060
2098
  has ``_nargs=[1]`` while :class:`Log` has ``_nargs=[1,2]``, showing that
2061
2099
  ``Log`` can be called with either ``1`` or ``2`` arguments.
2062
2100
 
2063
- If errors occur when creating a function, an IntegrityError may be thrown.
2101
+ If errors occur when creating a function, a :class:`myokit.IntegrityError`
2102
+ may be thrown.
2064
2103
 
2065
2104
  *Abstract class, extends:* :class:`Expression`
2066
2105
  """
@@ -2072,9 +2111,9 @@ class Function(Expression):
2072
2111
  super().__init__(ops)
2073
2112
  if self._nargs is not None:
2074
2113
  if not len(ops) in self._nargs:
2075
- raise IntegrityError(
2076
- 'Function (' + str(self._fname) + ') created with wrong'
2077
- ' number of arguments (' + str(len(ops)) + ', expecting '
2114
+ raise myokit.IntegrityError(
2115
+ f'Function ({self._fname}) created with wrong number of'
2116
+ ' arguments ({len(ops)}, expecting '
2078
2117
  + ' or '.join([str(x) for x in self._nargs]) + ').',
2079
2118
  self._token)
2080
2119
 
@@ -2117,12 +2156,28 @@ class Function(Expression):
2117
2156
  op._tree_str(b, n + self._treeDent)
2118
2157
 
2119
2158
 
2120
- class UnaryDimensionlessFunction(Function):
2159
+ class UnaryNumericalFunction(Function):
2121
2160
  """
2122
- Function with a single operand that has dimensionless input and output.
2161
+ Function with a single numerical operand and numerical output.
2123
2162
 
2124
2163
  *Abstract class, extends:* :class:`Function`
2125
2164
  """
2165
+ def __init__(self, *ops):
2166
+ super().__init__(*ops)
2167
+ if isinstance(self._operands[0], myokit.Condition):
2168
+ raise myokit.TypeError(
2169
+ f'Function {self._fname}() expects a numerical operand, got a'
2170
+ f' {type(self._operands[0])}.', self._token)
2171
+
2172
+
2173
+ class UnaryNumericalDimensionlessFunction(UnaryNumericalFunction):
2174
+ """
2175
+ Function with a single operand that has numerical, dimensionless input and
2176
+ output.
2177
+
2178
+ *Abstract class, extends:* :class:`UnaryNumericalFunction`
2179
+ """
2180
+
2126
2181
  def _eval_unit(self, mode):
2127
2182
  unit = self._operands[0]._eval_unit(mode)
2128
2183
 
@@ -2140,7 +2195,7 @@ class UnaryDimensionlessFunction(Function):
2140
2195
  return myokit.units.dimensionless
2141
2196
 
2142
2197
 
2143
- class Sqrt(Function):
2198
+ class Sqrt(UnaryNumericalFunction):
2144
2199
  """
2145
2200
  Represents the square root ``sqrt(x)``.
2146
2201
 
@@ -2173,7 +2228,7 @@ class Sqrt(Function):
2173
2228
  return unit ** 0.5
2174
2229
 
2175
2230
 
2176
- class Sin(UnaryDimensionlessFunction):
2231
+ class Sin(UnaryNumericalDimensionlessFunction):
2177
2232
  """
2178
2233
  Represents the sine function ``sin(x)``.
2179
2234
 
@@ -2185,7 +2240,7 @@ class Sin(UnaryDimensionlessFunction):
2185
2240
  >>> print(round(x.eval(), 1))
2186
2241
  1.0
2187
2242
 
2188
- *Extends:* :class:`UnaryDimensionlessFunction`
2243
+ *Extends:* :class:`UnaryNumericalDimensionlessFunction`
2189
2244
  """
2190
2245
  _fname = 'sin'
2191
2246
 
@@ -2203,7 +2258,7 @@ class Sin(UnaryDimensionlessFunction):
2203
2258
  raise EvalError(self, subst, e)
2204
2259
 
2205
2260
 
2206
- class Cos(UnaryDimensionlessFunction):
2261
+ class Cos(UnaryNumericalDimensionlessFunction):
2207
2262
  """
2208
2263
  Represents the cosine function ``cos(x)``.
2209
2264
 
@@ -2215,7 +2270,7 @@ class Cos(UnaryDimensionlessFunction):
2215
2270
  >>> print(round(x.eval(), 1))
2216
2271
  0.0
2217
2272
 
2218
- *Extends:* :class:`UnaryDimensionlessFunction`
2273
+ *Extends:* :class:`UnaryNumericalDimensionlessFunction`
2219
2274
  """
2220
2275
  _fname = 'cos'
2221
2276
 
@@ -2233,7 +2288,7 @@ class Cos(UnaryDimensionlessFunction):
2233
2288
  raise EvalError(self, subst, e)
2234
2289
 
2235
2290
 
2236
- class Tan(UnaryDimensionlessFunction):
2291
+ class Tan(UnaryNumericalDimensionlessFunction):
2237
2292
  """
2238
2293
  Represents the tangent function ``tan(x)``.
2239
2294
 
@@ -2242,7 +2297,7 @@ class Tan(UnaryDimensionlessFunction):
2242
2297
  >>> print(round(x.eval(), 1))
2243
2298
  1.0
2244
2299
 
2245
- *Extends:* :class:`UnaryDimensionlessFunction`
2300
+ *Extends:* :class:`UnaryNumericalDimensionlessFunction`
2246
2301
  """
2247
2302
  _fname = 'tan'
2248
2303
 
@@ -2260,7 +2315,7 @@ class Tan(UnaryDimensionlessFunction):
2260
2315
  raise EvalError(self, subst, e)
2261
2316
 
2262
2317
 
2263
- class ASin(UnaryDimensionlessFunction):
2318
+ class ASin(UnaryNumericalDimensionlessFunction):
2264
2319
  """
2265
2320
  Represents the inverse sine function ``asin(x)``.
2266
2321
 
@@ -2269,7 +2324,7 @@ class ASin(UnaryDimensionlessFunction):
2269
2324
  >>> print(round(x.eval(), 1))
2270
2325
  1.0
2271
2326
 
2272
- *Extends:* :class:`UnaryDimensionlessFunction`
2327
+ *Extends:* :class:`UnaryNumericalDimensionlessFunction`
2273
2328
  """
2274
2329
  _fname = 'asin'
2275
2330
 
@@ -2287,7 +2342,7 @@ class ASin(UnaryDimensionlessFunction):
2287
2342
  raise EvalError(self, subst, e)
2288
2343
 
2289
2344
 
2290
- class ACos(UnaryDimensionlessFunction):
2345
+ class ACos(UnaryNumericalDimensionlessFunction):
2291
2346
  """
2292
2347
  Represents the inverse cosine ``acos(x)``.
2293
2348
 
@@ -2296,7 +2351,7 @@ class ACos(UnaryDimensionlessFunction):
2296
2351
  >>> print(round(x.eval(), 1))
2297
2352
  3.0
2298
2353
 
2299
- *Extends:* :class:`UnaryDimensionlessFunction`
2354
+ *Extends:* :class:`UnaryNumericalDimensionlessFunction`
2300
2355
  """
2301
2356
  _fname = 'acos'
2302
2357
 
@@ -2317,7 +2372,7 @@ class ACos(UnaryDimensionlessFunction):
2317
2372
  raise EvalError(self, subst, e)
2318
2373
 
2319
2374
 
2320
- class ATan(UnaryDimensionlessFunction):
2375
+ class ATan(UnaryNumericalDimensionlessFunction):
2321
2376
  """
2322
2377
  Represents the inverse tangent function ``atan(x)``.
2323
2378
 
@@ -2331,7 +2386,7 @@ class ATan(UnaryDimensionlessFunction):
2331
2386
  (positive) x-axis. In this case, the returned value will be in the range
2332
2387
  (-pi, pi].
2333
2388
 
2334
- *Extends:* :class:`UnaryDimensionlessFunction`
2389
+ *Extends:* :class:`UnaryNumericalDimensionlessFunction`
2335
2390
  """
2336
2391
  _fname = 'atan'
2337
2392
 
@@ -2349,7 +2404,7 @@ class ATan(UnaryDimensionlessFunction):
2349
2404
  raise EvalError(self, subst, e)
2350
2405
 
2351
2406
 
2352
- class Exp(UnaryDimensionlessFunction):
2407
+ class Exp(UnaryNumericalDimensionlessFunction):
2353
2408
  """
2354
2409
  Represents a power of *e*. Written ``exp(x)`` in ``.mmt`` syntax.
2355
2410
 
@@ -2358,7 +2413,7 @@ class Exp(UnaryDimensionlessFunction):
2358
2413
  >>> print(round(x.eval(), 4))
2359
2414
  2.7183
2360
2415
 
2361
- *Extends:* :class:`UnaryDimensionlessFunction`
2416
+ *Extends:* :class:`UnaryNumericalDimensionlessFunction`
2362
2417
  """
2363
2418
  _fname = 'exp'
2364
2419
 
@@ -2398,6 +2453,20 @@ class Log(Function):
2398
2453
  _fname = 'log'
2399
2454
  _nargs = [1, 2]
2400
2455
 
2456
+ def __init__(self, *ops):
2457
+ super().__init__(*ops)
2458
+ if isinstance(self._operands[0], myokit.Condition):
2459
+ raise myokit.TypeError(
2460
+ 'Invalid type for first operand: function log() expects'
2461
+ f' numerical operands but found a {type(self._operands[0])}.',
2462
+ self._token)
2463
+ if len(self._operands) == 2 and isinstance(
2464
+ self._operands[1], myokit.Condition):
2465
+ raise myokit.TypeError(
2466
+ 'Invalid type for second operand: function log() expects'
2467
+ f' numerical operands but found a {type(self._operands[0])}.',
2468
+ self._token)
2469
+
2401
2470
  def _diff(self, lhs, idstates):
2402
2471
 
2403
2472
  if len(self._operands) == 1:
@@ -2485,7 +2554,7 @@ class Log(Function):
2485
2554
  return myokit.units.dimensionless
2486
2555
 
2487
2556
 
2488
- class Log10(UnaryDimensionlessFunction):
2557
+ class Log10(UnaryNumericalDimensionlessFunction):
2489
2558
  """
2490
2559
  Represents the base-10 logarithm ``log10(x)``.
2491
2560
 
@@ -2494,7 +2563,7 @@ class Log10(UnaryDimensionlessFunction):
2494
2563
  >>> print(round(x.eval(), 1))
2495
2564
  2.0
2496
2565
 
2497
- *Extends:* :class:`UnaryDimensionlessFunction`
2566
+ *Extends:* :class:`UnaryNumericalDimensionlessFunction`
2498
2567
  """
2499
2568
  _fname = 'log10'
2500
2569
 
@@ -2512,7 +2581,7 @@ class Log10(UnaryDimensionlessFunction):
2512
2581
  raise EvalError(self, subst, e)
2513
2582
 
2514
2583
 
2515
- class Floor(Function):
2584
+ class Floor(UnaryNumericalFunction):
2516
2585
  """
2517
2586
  Represents a rounding towards minus infinity ``floor(x)``.
2518
2587
 
@@ -2544,7 +2613,7 @@ class Floor(Function):
2544
2613
  return self._operands[0]._eval_unit(mode)
2545
2614
 
2546
2615
 
2547
- class Ceil(Function):
2616
+ class Ceil(UnaryNumericalFunction):
2548
2617
  """
2549
2618
  Represents a rounding towards positve infinity ``ceil(x)``.
2550
2619
 
@@ -2576,7 +2645,7 @@ class Ceil(Function):
2576
2645
  return self._operands[0]._eval_unit(mode)
2577
2646
 
2578
2647
 
2579
- class Abs(Function):
2648
+ class Abs(UnaryNumericalFunction):
2580
2649
  """
2581
2650
  Returns the absolute value of a number ``abs(x)``.
2582
2651
 
@@ -2645,6 +2714,19 @@ class If(Function):
2645
2714
  self._t = t # then
2646
2715
  self._e = e # else
2647
2716
 
2717
+ if not isinstance(i, myokit.Condition):
2718
+ raise myokit.TypeError(
2719
+ 'Invalid type for first operand: expected a condition but'
2720
+ f' found {type(i)}.', self._token)
2721
+ if isinstance(t, myokit.Condition):
2722
+ raise myokit.TypeError(
2723
+ 'Invalid type for second operand: expected a numerical operand'
2724
+ f' but found a condition ({type(t)}).', self._token)
2725
+ if isinstance(e, myokit.Condition):
2726
+ raise myokit.TypeError(
2727
+ 'Invalid type for third operand: expected a numerical operand'
2728
+ f' but found a condition ({type(e)}).', self._token)
2729
+
2648
2730
  def condition(self):
2649
2731
  """
2650
2732
  Returns this if-function's condition.
@@ -2692,16 +2774,14 @@ class If(Function):
2692
2774
 
2693
2775
  # Mismatching units
2694
2776
  raise EvalUnitError(
2695
- self, 'Units of `then` and `else` part of an `if`'
2696
- ' must match. Got ' + str(unit2) + ' and ' + str(unit3) + '.')
2777
+ self, 'Units of `then` and `else` part of an `if` must match.'
2778
+ f' Got {unit2} and {unit3}.')
2697
2779
 
2698
2780
  def is_conditional(self):
2699
2781
  return True
2700
2782
 
2701
2783
  def piecewise(self):
2702
- """
2703
- Returns an equivalent ``Piecewise`` object.
2704
- """
2784
+ """ Returns an equivalent ``Piecewise`` object. """
2705
2785
  return Piecewise(self._i, self._t, self._e)
2706
2786
 
2707
2787
  def value(self, which):
@@ -2751,11 +2831,11 @@ class Piecewise(Function):
2751
2831
  # Check number of arguments
2752
2832
  n = len(self._operands)
2753
2833
  if n % 2 == 0:
2754
- raise IntegrityError(
2834
+ raise myokit.IntegrityError(
2755
2835
  'Piecewise function must have odd number of arguments:'
2756
2836
  ' ([condition, value]+, else_value).', self._token)
2757
2837
  if n < 3:
2758
- raise IntegrityError(
2838
+ raise myokit.IntegrityError(
2759
2839
  'Piecewise function must have 3 or more arguments.',
2760
2840
  self._token)
2761
2841
 
@@ -2769,6 +2849,19 @@ class Piecewise(Function):
2769
2849
  self._e[i] = next(oper)
2770
2850
  self._e[m] = next(oper)
2771
2851
 
2852
+ # Check argument types
2853
+ for i, e in enumerate(self._i):
2854
+ if not isinstance(e, myokit.Condition):
2855
+ raise myokit.TypeError(
2856
+ f'operand at index {2 * i} must be a condition, but found'
2857
+ f' {type(e)}.', self._token)
2858
+ for i, e in enumerate(self._e):
2859
+ if isinstance(e, myokit.Condition):
2860
+ j = 2 * i if i == len(self._e) - 1 else 1 + 2 * i
2861
+ raise myokit.TypeError(
2862
+ f'operand at index {j} must be numerical, but found'
2863
+ f' {type(e)}.', self._token)
2864
+
2772
2865
  def conditions(self):
2773
2866
  """
2774
2867
  Returns an iterator over the conditions used by this Piecewise.
@@ -2836,29 +2929,16 @@ class Piecewise(Function):
2836
2929
 
2837
2930
  class Condition:
2838
2931
  """
2839
- *Abstract class*
2840
-
2841
- Interface for conditional expressions that can be evaluated to True or
2842
- False. Doesn't add any methods but simply indicates that this is a
2843
- condition.
2932
+ Base class for conditional expressions that evaluate to True or False.
2844
2933
  """
2845
-
2846
2934
  def _diff(self, lhs, idstates):
2847
2935
  raise NotImplementedError(
2848
2936
  'Conditions do not have partial derivatives.')
2849
2937
 
2850
2938
 
2851
- class PrefixCondition(PrefixExpression, Condition):
2852
- """
2853
- Interface for prefix conditions.
2854
-
2855
- *Abstract class, extends:* :class:`Condition`, :class:`PrefixExpression`
2856
- """
2857
-
2858
-
2859
- class Not(PrefixCondition):
2939
+ class Not(PrefixExpression, Condition):
2860
2940
  """
2861
- Negates a condition. Written as ``not x``.
2941
+ Negates a condition: ``not x``.
2862
2942
 
2863
2943
  >>> from myokit import *
2864
2944
  >>> x = parse_expression('1 == 1')
@@ -2871,10 +2951,19 @@ class Not(PrefixCondition):
2871
2951
  >>> print(x.eval())
2872
2952
  True
2873
2953
 
2874
- *Extends:* :class:`PrefixCondition`
2954
+ The operand to ``Not`` must be a condition.
2955
+
2956
+ *Extends:* :class:`PrefixExpression`, :class:`Condition`
2875
2957
  """
2876
2958
  _rep = 'not'
2877
2959
 
2960
+ def __init__(self, op):
2961
+ super().__init__(op)
2962
+ if not isinstance(op, myokit.Condition):
2963
+ raise myokit.TypeError(
2964
+ 'Invalid operand type: expected a condition but found a'
2965
+ f' {type(op)}.', self._token)
2966
+
2878
2967
  def _code(self, b, c):
2879
2968
  b.write('not ')
2880
2969
  brackets = self._op._rbp > LITERAL and self._op._rbp < self._rbp
@@ -2891,11 +2980,12 @@ class Not(PrefixCondition):
2891
2980
  raise EvalError(self, subst, e)
2892
2981
 
2893
2982
  def _eval_unit(self, mode):
2894
- unit = self._op._eval_unit(mode)
2895
- if unit not in (None, myokit.units.dimensionless):
2896
- raise EvalUnitError(
2897
- self, 'Operator `not` expects a dimensionless operand.')
2898
- return unit
2983
+ # Make child check units
2984
+ self._op._eval_unit(mode)
2985
+
2986
+ # No further checking necessary: operand is a condition so must return
2987
+ # unitless.
2988
+ return myokit.units.dimensionless
2899
2989
 
2900
2990
  def _polishb(self, b):
2901
2991
  b.write('not ')
@@ -2915,24 +3005,38 @@ class BinaryComparison(InfixCondition):
2915
3005
  """
2916
3006
  Base class for infix comparisons of two entities.
2917
3007
 
3008
+ Takes numerical operands as input, but returns a condition.
3009
+
2918
3010
  *Abstract class, extends:* :class:`InfixCondition`
2919
3011
  """
3012
+ def __init__(self, left, right):
3013
+ super().__init__(left, right)
3014
+ if isinstance(left, myokit.Condition):
3015
+ raise myokit.TypeError(
3016
+ 'Invalid type for first operand: expected a numerical operand'
3017
+ f' but found a {type(left)}.',
3018
+ self._token)
3019
+ if isinstance(right, myokit.Condition):
3020
+ raise myokit.TypeError(
3021
+ 'Invalid type for second operand: expected a numerical operand'
3022
+ f' but found a {type(right)}.',
3023
+ self._token)
3024
+
2920
3025
  def _eval_unit(self, mode):
2921
3026
  unit1 = self._op1._eval_unit(mode)
2922
3027
  unit2 = self._op2._eval_unit(mode)
2923
3028
 
2924
- # Equal (including both None) is always ok
3029
+ # Equal (including both None-or-dimensionless) is always ok.
2925
3030
  if unit1 == unit2:
2926
- return None if unit1 is None else myokit.units.dimensionless
3031
+ return myokit.units.dimensionless
2927
3032
 
2928
- # In tolerant mode, a single None is OK
3033
+ # In tolerant mode, we might still have a None, but this is OK too.
2929
3034
  if unit1 is None or unit2 is None:
2930
3035
  return myokit.units.dimensionless
2931
3036
 
2932
3037
  # Otherwise must match
2933
- raise EvalUnitError(
2934
- self, 'Condition ' + self._rep + ' requires equal units on both'
2935
- ' sides, got ' + str(unit1) + ' and ' + str(unit2) + '.')
3038
+ raise EvalUnitError(self, f'Condition {self._rep} requires equal units'
3039
+ f' on both sides, got {unit1} and {unit2}.')
2936
3040
 
2937
3041
 
2938
3042
  class Equal(BinaryComparison):
@@ -3025,7 +3129,7 @@ class Less(BinaryComparison):
3025
3129
 
3026
3130
  class MoreEqual(BinaryComparison):
3027
3131
  """
3028
- Represents an is-more-than-or-equal check ``x > y``.
3132
+ Represents an is-more-than-or-equal check ``x >= y``.
3029
3133
 
3030
3134
  >>> from myokit import *
3031
3135
  >>> print(parse_expression('2 >= 2').eval())
@@ -3075,11 +3179,26 @@ class And(InfixCondition):
3075
3179
  >>> print(parse_expression('1 == 1 and 4 == 4').eval())
3076
3180
  True
3077
3181
 
3182
+ Both operands must be a condition.
3183
+
3078
3184
  *Extends:* :class:`InfixCondition`
3079
3185
  """
3080
3186
  _rbp = CONDITION_AND
3081
3187
  _rep = 'and'
3082
3188
 
3189
+ def __init__(self, left, right):
3190
+ super().__init__(left, right)
3191
+ if not isinstance(left, myokit.Condition):
3192
+ raise myokit.TypeError(
3193
+ 'Invalid type for first operand: expected a condition but'
3194
+ f' found a {type(left)}.',
3195
+ self._token)
3196
+ if not isinstance(right, myokit.Condition):
3197
+ raise myokit.TypeError(
3198
+ 'Invalid type for second operand: expected a condition but'
3199
+ f' found a {type(right)}.',
3200
+ self._token)
3201
+
3083
3202
  def _eval(self, subst, precision):
3084
3203
  try:
3085
3204
  return (
@@ -3089,21 +3208,11 @@ class And(InfixCondition):
3089
3208
  raise EvalError(self, subst, e)
3090
3209
 
3091
3210
  def _eval_unit(self, mode):
3092
- unit1 = self._op1._eval_unit(mode)
3093
- unit2 = self._op2._eval_unit(mode)
3094
-
3095
- # Propagate both None in tolerant mode
3096
- if unit1 is None and unit2 is None:
3097
- return None
3098
-
3099
- # Ideal: both dimensionless
3100
- unit1 = myokit.units.dimensionless if unit1 is None else unit1
3101
- unit2 = myokit.units.dimensionless if unit2 is None else unit2
3102
- if unit1 == unit2 == myokit.units.dimensionless:
3103
- return unit1
3104
-
3105
- raise EvalUnitError(
3106
- self, 'Operator `and` expects dimensionless operands.')
3211
+ # Get children to check units: both must be conditions so result does
3212
+ # not matter and no further checking is necessary.
3213
+ self._op1._eval_unit(mode)
3214
+ self._op2._eval_unit(mode)
3215
+ return myokit.units.dimensionless
3107
3216
 
3108
3217
 
3109
3218
  class Or(InfixCondition):
@@ -3114,11 +3223,26 @@ class Or(InfixCondition):
3114
3223
  >>> print(parse_expression('1 == 1 or 2 == 4').eval())
3115
3224
  True
3116
3225
 
3226
+ Both operands must be a condition.
3227
+
3117
3228
  *Extends:* :class:`InfixCondition`
3118
3229
  """
3119
3230
  _rbp = CONDITION_AND
3120
3231
  _rep = 'or'
3121
3232
 
3233
+ def __init__(self, left, right):
3234
+ super().__init__(left, right)
3235
+ if not isinstance(left, myokit.Condition):
3236
+ raise myokit.TypeError(
3237
+ 'Invalid type for first operand: expected a condition but'
3238
+ f' found a {type(left)}.',
3239
+ self._token)
3240
+ if not isinstance(right, myokit.Condition):
3241
+ raise myokit.TypeError(
3242
+ 'Invalid type for second operand: expected a condition but'
3243
+ f' found a {type(right)}.',
3244
+ self._token)
3245
+
3122
3246
  def _eval(self, subst, precision):
3123
3247
  try:
3124
3248
  return (
@@ -3128,21 +3252,11 @@ class Or(InfixCondition):
3128
3252
  raise EvalError(self, subst, e)
3129
3253
 
3130
3254
  def _eval_unit(self, mode):
3131
- unit1 = self._op1._eval_unit(mode)
3132
- unit2 = self._op2._eval_unit(mode)
3133
-
3134
- # Propagate both None in tolerant mode
3135
- if unit1 is None and unit2 is None:
3136
- return None
3137
-
3138
- # Ideal: both dimensionless
3139
- unit1 = myokit.units.dimensionless if unit1 is None else unit1
3140
- unit2 = myokit.units.dimensionless if unit2 is None else unit2
3141
- if unit1 == unit2 == myokit.units.dimensionless:
3142
- return unit1
3143
-
3144
- raise EvalUnitError(
3145
- self, 'Operator `or` expects dimensionless operands.')
3255
+ # Get children to check units: both must be conditions so result does
3256
+ # not matter and no further checking is necessary.
3257
+ self._op1._eval_unit(mode)
3258
+ self._op2._eval_unit(mode)
3259
+ return myokit.units.dimensionless
3146
3260
 
3147
3261
 
3148
3262
  class EvalError(Exception):