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.
- myokit/__init__.py +6 -19
- myokit/_datablock.py +45 -55
- myokit/_datalog.py +2 -2
- myokit/_err.py +26 -3
- myokit/_expressions.py +241 -127
- myokit/_model_api.py +19 -13
- myokit/_myokit_version.py +1 -1
- myokit/_sim/cvodessim.c +221 -149
- myokit/_sim/jacobian.py +3 -3
- myokit/_sim/mcl.h +54 -0
- myokit/_sim/openclsim.py +5 -5
- myokit/_sim/rhs.py +1 -1
- myokit/formats/__init__.py +4 -9
- myokit/formats/ansic/_ewriter.py +4 -20
- myokit/formats/heka/_patchmaster.py +16 -10
- myokit/formats/opencl/_ewriter.py +3 -42
- myokit/formats/opencl/template/minilog.py +1 -1
- myokit/formats/sympy/_ereader.py +2 -1
- myokit/formats/wcp/_wcp.py +3 -3
- myokit/gui/datalog_viewer.py +12 -7
- myokit/lib/hh.py +3 -0
- myokit/lib/markov.py +2 -2
- myokit/lib/plots.py +4 -4
- myokit/tests/data/formats/wcp-file-empty.wcp +0 -0
- myokit/tests/test_datablock.py +10 -10
- myokit/tests/test_datalog.py +4 -1
- myokit/tests/test_expressions.py +532 -251
- myokit/tests/test_formats_ansic.py +6 -18
- myokit/tests/test_formats_cpp.py +0 -5
- myokit/tests/test_formats_cuda.py +7 -15
- myokit/tests/test_formats_easyml.py +4 -9
- myokit/tests/test_formats_latex.py +10 -11
- myokit/tests/test_formats_matlab.py +0 -8
- myokit/tests/test_formats_opencl.py +0 -29
- myokit/tests/test_formats_python.py +2 -19
- myokit/tests/test_formats_stan.py +0 -13
- myokit/tests/test_formats_sympy.py +3 -3
- myokit/tests/test_formats_wcp.py +15 -0
- myokit/tests/test_lib_hh.py +36 -0
- myokit/tests/test_model.py +20 -20
- myokit/tests/test_parsing.py +19 -0
- {myokit-1.36.0.dist-info → myokit-1.37.0.dist-info}/METADATA +1 -1
- {myokit-1.36.0.dist-info → myokit-1.37.0.dist-info}/RECORD +47 -46
- {myokit-1.36.0.dist-info → myokit-1.37.0.dist-info}/LICENSE.txt +0 -0
- {myokit-1.36.0.dist-info → myokit-1.37.0.dist-info}/WHEEL +0 -0
- {myokit-1.36.0.dist-info → myokit-1.37.0.dist-info}/entry_points.txt +0 -0
- {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(
|
|
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).
|
|
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.
|
|
896
|
-
|
|
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 "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1395
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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 (
|
|
2077
|
-
'
|
|
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
|
|
2159
|
+
class UnaryNumericalFunction(Function):
|
|
2121
2160
|
"""
|
|
2122
|
-
Function with a single operand
|
|
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(
|
|
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(
|
|
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:`
|
|
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(
|
|
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:`
|
|
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(
|
|
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:`
|
|
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(
|
|
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:`
|
|
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(
|
|
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:`
|
|
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(
|
|
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:`
|
|
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(
|
|
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:`
|
|
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(
|
|
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:`
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
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
|
|
3031
|
+
return myokit.units.dimensionless
|
|
2927
3032
|
|
|
2928
|
-
# In tolerant mode, a
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
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
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
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):
|