myokit 1.36.1__py3-none-any.whl → 1.37.1__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 (65) hide show
  1. myokit/__init__.py +6 -19
  2. myokit/_aux.py +4 -0
  3. myokit/_datablock.py +55 -65
  4. myokit/_datalog.py +42 -7
  5. myokit/_err.py +26 -3
  6. myokit/_expressions.py +241 -127
  7. myokit/_model_api.py +19 -13
  8. myokit/_myokit_version.py +1 -1
  9. myokit/_sim/jacobian.py +3 -3
  10. myokit/_sim/openclsim.py +5 -5
  11. myokit/_sim/rhs.py +1 -1
  12. myokit/formats/__init__.py +4 -9
  13. myokit/formats/ansic/_ewriter.py +4 -20
  14. myokit/formats/axon/_abf.py +11 -4
  15. myokit/formats/diffsl/__init__.py +60 -0
  16. myokit/formats/diffsl/_ewriter.py +145 -0
  17. myokit/formats/diffsl/_exporter.py +435 -0
  18. myokit/formats/heka/_patchmaster.py +345 -115
  19. myokit/formats/opencl/_ewriter.py +3 -42
  20. myokit/formats/opencl/template/minilog.py +1 -1
  21. myokit/formats/sympy/_ereader.py +2 -1
  22. myokit/formats/wcp/_wcp.py +3 -3
  23. myokit/gui/datalog_viewer.py +28 -9
  24. myokit/lib/markov.py +2 -2
  25. myokit/lib/plots.py +4 -4
  26. myokit/tests/data/formats/wcp-file-empty.wcp +0 -0
  27. myokit/tests/data/io/bad1d-2-no-header.zip +0 -0
  28. myokit/tests/data/io/bad1d-3-no-data.zip +0 -0
  29. myokit/tests/data/io/bad1d-4-not-a-zip.zip +1 -105
  30. myokit/tests/data/io/bad1d-5-bad-data-type.zip +0 -0
  31. myokit/tests/data/io/bad1d-6-time-too-short.zip +0 -0
  32. myokit/tests/data/io/bad1d-7-0d-too-short.zip +0 -0
  33. myokit/tests/data/io/bad1d-8-1d-too-short.zip +0 -0
  34. myokit/tests/data/io/bad2d-2-no-header.zip +0 -0
  35. myokit/tests/data/io/bad2d-3-no-data.zip +0 -0
  36. myokit/tests/data/io/bad2d-4-not-a-zip.zip +1 -105
  37. myokit/tests/data/io/bad2d-5-bad-data-type.zip +0 -0
  38. myokit/tests/data/io/bad2d-8-2d-too-short.zip +0 -0
  39. myokit/tests/data/io/block1d.mmt +187 -0
  40. myokit/tests/data/io/datalog-18-duplicate-keys.csv +4 -0
  41. myokit/tests/test_aux.py +4 -0
  42. myokit/tests/test_datablock.py +16 -16
  43. myokit/tests/test_datalog.py +24 -1
  44. myokit/tests/test_expressions.py +532 -251
  45. myokit/tests/test_formats_ansic.py +6 -18
  46. myokit/tests/test_formats_cpp.py +0 -5
  47. myokit/tests/test_formats_cuda.py +7 -15
  48. myokit/tests/test_formats_diffsl.py +728 -0
  49. myokit/tests/test_formats_easyml.py +4 -9
  50. myokit/tests/test_formats_exporters_run.py +3 -0
  51. myokit/tests/test_formats_latex.py +10 -11
  52. myokit/tests/test_formats_matlab.py +0 -8
  53. myokit/tests/test_formats_opencl.py +0 -29
  54. myokit/tests/test_formats_python.py +2 -19
  55. myokit/tests/test_formats_stan.py +0 -13
  56. myokit/tests/test_formats_sympy.py +3 -3
  57. myokit/tests/test_formats_wcp.py +15 -0
  58. myokit/tests/test_model.py +20 -20
  59. myokit/tests/test_parsing.py +19 -0
  60. {myokit-1.36.1.dist-info → myokit-1.37.1.dist-info}/METADATA +1 -1
  61. {myokit-1.36.1.dist-info → myokit-1.37.1.dist-info}/RECORD +65 -58
  62. {myokit-1.36.1.dist-info → myokit-1.37.1.dist-info}/LICENSE.txt +0 -0
  63. {myokit-1.36.1.dist-info → myokit-1.37.1.dist-info}/WHEEL +0 -0
  64. {myokit-1.36.1.dist-info → myokit-1.37.1.dist-info}/entry_points.txt +0 -0
  65. {myokit-1.36.1.dist-info → myokit-1.37.1.dist-info}/top_level.txt +0 -0
myokit/_model_api.py CHANGED
@@ -716,7 +716,10 @@ class VarOwner(ModelPart, VarProvider):
716
716
  If ``recursive`` is ``True``, any child variables will be deleted as
717
717
  well.
718
718
 
719
- A :class:`myokit.IntegrityError` will be raised if
719
+ A :class:`myokit.IntegrityError` will be raised if the variable cannot
720
+ be removed because other variables depend on it. (Although dependencies
721
+ from child variables will be ignored if ``recursive`` is set to
722
+ ``True``).
720
723
  """
721
724
  if variable.parent() != self:
722
725
  raise ValueError(
@@ -1095,9 +1098,7 @@ class Model(ObjectWithMetaData, VarProvider):
1095
1098
  raise myokit.IncompatibleUnitError(msg, var._token)
1096
1099
 
1097
1100
  def clone(self):
1098
- """
1099
- Returns a (deep) clone of this model.
1100
- """
1101
+ """ Returns a (deep) clone of this model. """
1101
1102
  clone = Model()
1102
1103
 
1103
1104
  # Copy meta data
@@ -4520,16 +4521,10 @@ class Variable(VarOwner):
4520
4521
  warnings.warn('The keyword argument `state_value` is deprecated.'
4521
4522
  ' Please use `initial_value` instead.')
4522
4523
 
4523
- # Handle string and number rhs's
4524
- model = self.model()
4525
- if not isinstance(initial_value, myokit.Expression):
4526
- if isinstance(initial_value, str):
4527
- # Expressions are evaluated in model context
4528
- initial_value = myokit.parse_expression(
4529
- initial_value, context=model)
4530
- elif initial_value is not None:
4531
- initial_value = myokit.Number(initial_value)
4524
+ # Parse initial value
4525
+ initial_value = self._set_initial_value(initial_value, False)
4532
4526
 
4527
+ model = self.model()
4533
4528
  try:
4534
4529
  # Set lhs to derivative expression
4535
4530
  self._lhs = myokit.Derivative(myokit.Name(self))
@@ -4854,6 +4849,9 @@ class Variable(VarOwner):
4854
4849
  x.set_rhs(myokit.Plus(myokit.Number(1), myokit.Name(y)))
4855
4850
  x.set_rhs('1 + y')
4856
4851
 
4852
+ Expressions used as a variable's right-hand side must be numerical:
4853
+ :class:`myokit.Condition` operators can not be used as RHS.
4854
+
4857
4855
  Calling `set_rhs` will reset the validation status of the model this
4858
4856
  variable belongs to.
4859
4857
  """
@@ -5107,6 +5105,14 @@ class Equation:
5107
5105
  def __init__(self, lhs, rhs):
5108
5106
  self._lhs = lhs
5109
5107
  self._rhs = rhs
5108
+ if not isinstance(lhs, myokit.Expression):
5109
+ raise myokit.IntegrityError(
5110
+ 'Both sides of an equation must be myokit.Expression objects.'
5111
+ f' Found {type(lhs)} for LHS.')
5112
+ if not isinstance(rhs, myokit.Expression):
5113
+ raise myokit.IntegrityError(
5114
+ 'Both sides of an equation must be myokit.Expression objects.'
5115
+ f' Found {type(lhs)} for RHS.')
5110
5116
 
5111
5117
  def __eq__(self, other):
5112
5118
  if not isinstance(other, Equation):
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, 36, 1
17
+ __version_tuple__ = 1, 37, 1
18
18
 
19
19
  # String version of the version number
20
20
  __version__ = '.'.join([str(x) for x in __version_tuple__])
myokit/_sim/jacobian.py CHANGED
@@ -166,7 +166,7 @@ class JacobianTracer(myokit.CppModule):
166
166
  self._ext.calculate(state, bound, deriv, partial)
167
167
  # Discard derivatives
168
168
  # Convert partial derivatives to numpy array and store
169
- partial = np.array(partial, copy=False)
169
+ partial = np.asarray(partial)
170
170
  partial = partial.reshape((ns, ns))
171
171
  partials.append(partial)
172
172
  partials = np.array(partials)
@@ -282,8 +282,8 @@ class JacobianCalculator(myokit.CppModule):
282
282
  self._ext.calculate(state, inputs, deriv, partial)
283
283
 
284
284
  # Create numpy versions and return
285
- deriv = np.array(deriv, copy=False)
286
- partial = np.array(partial, copy=False).reshape((n, n))
285
+ deriv = np.asarray(deriv)
286
+ partial = np.asarray(partial).reshape((n, n))
287
287
  return deriv, partial
288
288
 
289
289
  def newton_root(self, x=None, accuracy=0, max_iter=50, damping=1):
myokit/_sim/openclsim.py CHANGED
@@ -534,7 +534,7 @@ class SimulationOpenCL(myokit.CModule):
534
534
  lower, upper = safe_range
535
535
  for dims in myokit._dimco(*self._dims):
536
536
  key = '.'.join([str(x) for x in dims]) + post
537
- ar = np.array(_log[key], copy=False)
537
+ ar = np.asarray(_log[key])
538
538
  i = np.where(
539
539
  (ar < lower)
540
540
  | (ar > upper)
@@ -1080,7 +1080,7 @@ class SimulationOpenCL(myokit.CModule):
1080
1080
  n = len(self._fields) * self._nx * self._ny
1081
1081
  if n:
1082
1082
  field_data = self._fields.values()
1083
- field_data = [np.array(x, copy=False) for x in field_data]
1083
+ field_data = [np.asarray(x) for x in field_data]
1084
1084
  field_data = np.vstack(field_data)
1085
1085
  field_data = list(field_data.reshape(n, order='F'))
1086
1086
  else:
@@ -1342,7 +1342,7 @@ class SimulationOpenCL(myokit.CModule):
1342
1342
  'This method is unavailable when diffusion is disabled.')
1343
1343
 
1344
1344
  # Check the field's size
1345
- gx = np.array(gx, copy=False, dtype=float)
1345
+ gx = np.asarray(gx, dtype=float)
1346
1346
  if len(self._dims) == 1:
1347
1347
  s = self._nx - 1
1348
1348
  if gx.shape != (s, ):
@@ -1360,7 +1360,7 @@ class SimulationOpenCL(myokit.CModule):
1360
1360
  if gy is None:
1361
1361
  raise ValueError(
1362
1362
  'The argument `gy` must be set for 2-d simulations.')
1363
- gy = np.array(gy, copy=False, dtype=float)
1363
+ gy = np.asarray(gy, dtype=float)
1364
1364
  s = (self._ny - 1, self._nx)
1365
1365
  if gy.shape != s:
1366
1366
  raise ValueError(
@@ -1514,7 +1514,7 @@ class SimulationOpenCL(myokit.CModule):
1514
1514
  if not var.is_constant():
1515
1515
  raise ValueError('Only constants can be used for fields.')
1516
1516
  # Check values
1517
- values = np.array(values, copy=False, dtype=float)
1517
+ values = np.asarray(values, dtype=float)
1518
1518
  if len(self._dims) == 1:
1519
1519
  if values.shape != (self._nx, ):
1520
1520
  raise ValueError(
myokit/_sim/rhs.py CHANGED
@@ -153,7 +153,7 @@ class RhsBenchmarker(myokit.CModule):
153
153
  the given benchmarked times.
154
154
  """
155
155
  import numpy as np
156
- times = np.array(times, copy=False)
156
+ times = np.asarray(times)
157
157
  # Remove outliers twice
158
158
  for i in range(0, 2):
159
159
  avg = np.mean(times)
@@ -128,15 +128,7 @@ class ExpressionWriter:
128
128
  ``a**b**c`` is interpreted as ``a**(b**c)``, necessitating a different
129
129
  bracket-adding logic than used in Myokit.
130
130
 
131
- 3. Binary operators are sometimes implemented as n-ary operators. In
132
- Myokit, ``0 == 0 == 0`` is a sequence of two binary operators,
133
- interpreted as ``(0 == 0) == 0``. Because ``(0 == 0)`` evaluates to
134
- ``1``, this expression returns ``0`` (1 does not equal 0). In Python,
135
- the expression ``0 == 0 == 0`` is a ternary (n-ary) operator,
136
- interpreted as ``all_equal(0, 0, 0)``, which evaluates to ``1``. For
137
- languages that use this convention, extra brackets must be added.
138
-
139
- 4. Myokit does not have increment or decrement operators ``--`` and ``++``,
131
+ 3. Myokit does not have increment or decrement operators ``--`` and ``++``,
140
132
  so the expression ``--x`` is interpreted as ``-(-x)``. This is the same
141
133
  in Python. But in C-based languages, this is interpreted as a decrement
142
134
  operator so care must be taken to add extra brackets.
@@ -886,6 +878,9 @@ class SweepSource:
886
878
 
887
879
  Note that a source with zero recorded channels may still report a
888
880
  non-zero number of sweeps if it can provide D/A outputs.
881
+
882
+ Similarly, formats like WCP can report zero sweeps but have a non-zero
883
+ channel count (if no data was recorded).
889
884
  """
890
885
  raise NotImplementedError
891
886
 
@@ -102,23 +102,14 @@ class CBasedExpressionWriter(PythonExpressionWriter):
102
102
 
103
103
  def _ex_not(self, e):
104
104
  # C conditions all have brackets, so don't add more
105
- if isinstance(e[0], (myokit.Condition)):
106
- return f'(!{self.ex(e[0])})'
107
- # But do add more if the user's being silly
108
- return f'(!({self.ex(e[0])}))'
105
+ return f'(!{self.ex(e[0])})'
109
106
 
110
107
  def _ex_if(self, e):
111
- _if, _then, _else = self.ex(e._i), self.ex(e._t), self.ex(e._e)
112
- # If i is not a condtion (which always gets brackets from this writer)
113
- # then add brackets
114
- if not isinstance(e._i, myokit.Condition):
115
- _if = f'({_if})'
116
- return f'({_if} ? {_then} : {_else})'
108
+ return f'({self.ex(e._i)} ? {self.ex(e._t)} : {self.ex(e._e)})'
117
109
 
118
110
  def _ex_piecewise(self, e):
119
111
  # Render ifs; add extra bracket if not a condition (see _ex_if)
120
- _ifs = [self.ex(x) if isinstance(x, myokit.Condition)
121
- else f'({self.ex(x)})' for x in e._i]
112
+ _ifs = [self.ex(x) for x in e._i]
122
113
  _thens = [self.ex(x) for x in e._e]
123
114
 
124
115
  s = []
@@ -200,13 +191,7 @@ class AnsiCExpressionWriter(CBasedExpressionWriter):
200
191
  #def _ex_not(self, e):
201
192
 
202
193
  def _ex_if(self, e):
203
- # Allow _fcond
204
-
205
194
  _if, _then, _else = self.ex(e._i), self.ex(e._t), self.ex(e._e)
206
- # If i is not a condtion (which always gets brackets from this writer)
207
- # then add brackets
208
- if not isinstance(e._i, myokit.Condition):
209
- _if = f'({_if})'
210
195
 
211
196
  # Use if-then-else function?
212
197
  if self._fcond is not None:
@@ -219,8 +204,7 @@ class AnsiCExpressionWriter(CBasedExpressionWriter):
219
204
  # Allow _fcond
220
205
 
221
206
  # Render ifs; add extra bracket if not a condition (see _ex_if)
222
- _ifs = [self.ex(x) if isinstance(x, myokit.Condition)
223
- else f'({self.ex(x)})' for x in e._i]
207
+ _ifs = [self.ex(x) for x in e._i]
224
208
  _thens = [self.ex(x) for x in e._e]
225
209
 
226
210
  s = []
@@ -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)