myokit 1.35.4__py3-none-any.whl → 1.36.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 (59) hide show
  1. myokit/__init__.py +5 -3
  2. myokit/__main__.py +9 -159
  3. myokit/_config.py +2 -2
  4. myokit/_expressions.py +6 -6
  5. myokit/_model_api.py +11 -7
  6. myokit/_myokit_version.py +1 -1
  7. myokit/_protocol.py +4 -0
  8. myokit/_sim/__init__.py +1 -0
  9. myokit/_sim/cvodessim.c +321 -177
  10. myokit/_sim/cvodessim.py +107 -43
  11. myokit/_sim/mcl.h +54 -0
  12. myokit/formats/__init__.py +63 -12
  13. myokit/formats/ansic/__init__.py +2 -1
  14. myokit/formats/ansic/_ewriter.py +159 -40
  15. myokit/formats/cpp/_ewriter.py +12 -1
  16. myokit/formats/cuda/_ewriter.py +15 -51
  17. myokit/formats/easyml/_ewriter.py +26 -54
  18. myokit/formats/heka/_patchmaster.py +15 -3
  19. myokit/formats/latex/_ewriter.py +103 -88
  20. myokit/formats/latex/_exporter.py +1 -1
  21. myokit/formats/mathml/_ewriter.py +2 -2
  22. myokit/formats/matlab/_ewriter.py +50 -28
  23. myokit/formats/opencl/_ewriter.py +61 -78
  24. myokit/formats/python/_ewriter.py +81 -50
  25. myokit/formats/stan/_ewriter.py +29 -37
  26. myokit/gui/source.py +1 -1
  27. myokit/lib/hh.py +3 -0
  28. myokit/lib/markov.py +6 -0
  29. myokit/tests/__init__.py +70 -0
  30. myokit/tests/data/decker.model +59 -59
  31. myokit/tests/test_formats.py +115 -7
  32. myokit/tests/test_formats_ansic.py +344 -0
  33. myokit/tests/test_formats_axon.py +17 -0
  34. myokit/tests/test_formats_cpp.py +97 -0
  35. myokit/tests/test_formats_cuda.py +226 -0
  36. myokit/tests/test_formats_easyml.py +169 -152
  37. myokit/tests/{test_formats_exporters.py → test_formats_exporters_run.py} +1 -69
  38. myokit/tests/test_formats_html.py +1 -3
  39. myokit/tests/test_formats_latex.py +211 -0
  40. myokit/tests/test_formats_mathml_content.py +13 -0
  41. myokit/tests/test_formats_mathml_presentation.py +54 -42
  42. myokit/tests/test_formats_matlab.py +218 -0
  43. myokit/tests/test_formats_opencl.py +206 -380
  44. myokit/tests/test_formats_python.py +557 -0
  45. myokit/tests/test_formats_stan.py +175 -0
  46. myokit/tests/test_formats_sympy.py +9 -2
  47. myokit/tests/test_lib_hh.py +36 -0
  48. myokit/tests/test_lib_plots.py +0 -16
  49. myokit/tests/test_model.py +21 -1
  50. myokit/tests/test_simulation_cvodes.py +137 -56
  51. myokit/tools.py +3 -2
  52. {myokit-1.35.4.dist-info → myokit-1.36.1.dist-info}/LICENSE.txt +1 -1
  53. {myokit-1.35.4.dist-info → myokit-1.36.1.dist-info}/METADATA +19 -8
  54. {myokit-1.35.4.dist-info → myokit-1.36.1.dist-info}/RECORD +57 -52
  55. {myokit-1.35.4.dist-info → myokit-1.36.1.dist-info}/WHEEL +1 -1
  56. myokit/tests/test_formats_expression_writers.py +0 -1281
  57. myokit/tests/test_formats_importers.py +0 -53
  58. {myokit-1.35.4.dist-info → myokit-1.36.1.dist-info}/entry_points.txt +0 -0
  59. {myokit-1.35.4.dist-info → myokit-1.36.1.dist-info}/top_level.txt +0 -0
@@ -20,19 +20,29 @@ class LatexExpressionWriter(myokit.formats.ExpressionWriter):
20
20
  self.set_time_variable_name()
21
21
 
22
22
  # Default lhs function
23
- def fhls(lhs):
24
- var = self._prepare_name(lhs.var().uname())
25
- var = '\\text{' + var + '}' # Depends on amsmath package!
26
- if isinstance(lhs, myokit.Derivative):
27
- var = '\\frac{d}{d\\text{t}}' + var
28
- return var
23
+ def flhs(lhs):
24
+ v = r'\text{' + self._prepare_name(lhs.var().uname()) + '}'
25
+ if isinstance(lhs, myokit.Name):
26
+ return v
27
+ elif isinstance(lhs, myokit.Derivative):
28
+ return r'\frac{d' + v + r'}{d\text{' + self._time + '}}'
29
+ elif isinstance(lhs, myokit.PartialDerivative):
30
+ v2 = self._prepare_name(
31
+ lhs.independent_expression().var().uname())
32
+ return r'\frac{\partial' + v + r'}{\partial\text{' + v2 + '}}'
33
+ elif isinstance(lhs, myokit.InitialValue):
34
+ return v + r'(\text{' + self._time + '} = 0)'
35
+ else: # pragma: no cover
36
+ raise ValueError(f'Unsupported LHS type {type(lhs)}')
37
+
29
38
  self._flhs = None
30
- self.set_lhs_function(fhls)
39
+ self.set_lhs_function(flhs)
31
40
 
32
41
  def set_lhs_function(self, f):
33
42
  """
34
43
  Sets a naming function, will be called to get the variable name from a
35
- ``myokit.LhsExpression`` object.
44
+ :class:`myokit.LhsExpression`. This function is also responsible for
45
+ converting it to a suitable Latex form (e.g. adding `\\text{}`).
36
46
 
37
47
  The argument ``f`` should be a function that takes an ``LhsExpression``
38
48
  as input and returns a string.
@@ -40,31 +50,22 @@ class LatexExpressionWriter(myokit.formats.ExpressionWriter):
40
50
  self._flhs = f
41
51
 
42
52
  def set_time_variable_name(self, name='t'):
43
- """
44
- Sets a name to use for the time variable in derivatives
45
- """
53
+ """ Sets a name to use for the time variable in derivatives. """
46
54
  self._time = self._prepare_name(name)
47
55
 
48
56
  def eq(self, eq):
49
- """
50
- Converts an equation to a string.
51
- """
57
+ """ See :meth:`myokit.formats.ExpressionWriter.eq()`. """
52
58
  return self.ex(eq.lhs) + ' = ' + self.ex(eq.rhs)
53
59
 
54
60
  def ex(self, e):
55
- """
56
- Converts an expression to a string.
57
- """
61
+ """ See :meth:`myokit.formats.ExpressionWriter.ex()`. """
58
62
  b = []
59
63
  self._ex(e, b)
60
64
  return ''.join(b)
61
65
 
62
66
  def _prepare_name(self, text):
63
- """
64
- Prepares a name for use in latex
65
- """
66
- text = str(text)
67
- return text.replace('_', '\_')
67
+ """ Sanitises a name for use in latex. """
68
+ return str(text).replace('_', r'\_')
68
69
 
69
70
  def _ex(self, e, b):
70
71
  try:
@@ -73,40 +74,43 @@ class LatexExpressionWriter(myokit.formats.ExpressionWriter):
73
74
  raise ValueError('Unknown expression type: ' + str(type(e)))
74
75
  action(e, b)
75
76
 
77
+ def _ex_prefix(self, e, b, op):
78
+ """ Handles _ex() for prefix operators. """
79
+ b.append(op)
80
+ if e.bracket(e[0]):
81
+ b.append(r'\left(')
82
+ self._ex(e[0], b)
83
+ if e.bracket(e[0]):
84
+ b.append(r'\right)')
85
+
76
86
  def _ex_infix(self, e, b, op):
77
- """
78
- Handles _ex() for infix operators
79
- """
87
+ """ Handles _ex() for infix operators. """
80
88
  if e.bracket(e[0]):
81
- b.append('\\left(')
89
+ b.append(r'\left(')
82
90
  self._ex(e[0], b)
83
91
  if e.bracket(e[0]):
84
- b.append('\\right)')
92
+ b.append(r'\right)')
85
93
  b.append(op)
86
94
  if e.bracket(e[1]):
87
- b.append('\\left(')
95
+ b.append(r'\left(')
88
96
  self._ex(e[1], b)
89
97
  if e.bracket(e[1]):
90
- b.append('\\right)')
98
+ b.append(r'\right)')
91
99
 
92
100
  def _ex_function(self, e, b, func):
93
- """
94
- Handles _ex() for function operators
95
- """
101
+ """ Handles _ex() for function operators. """
96
102
  b.append(func)
97
- b.append('\\left(')
103
+ b.append(r'\left(')
98
104
  b.append(','.join([self.ex(x) for x in e]))
99
- b.append('\\right)')
105
+ b.append(r'\right)')
100
106
 
101
107
  def _ex_infix_condition(self, e, b, op):
102
- """
103
- Handles _ex() for infix condition operators
104
- """
105
- b.append('\\left(')
108
+ """ Handles _ex() for infix condition operators. """
109
+ b.append(r'\left(')
106
110
  self._ex(e[0], b)
107
111
  b.append(op)
108
112
  self._ex(e[1], b)
109
- b.append('\\right)')
113
+ b.append(r'\right)')
110
114
 
111
115
  def _ex_name(self, e, b):
112
116
  b.append(self._flhs(e))
@@ -114,21 +118,24 @@ class LatexExpressionWriter(myokit.formats.ExpressionWriter):
114
118
  def _ex_derivative(self, e, b):
115
119
  b.append(self._flhs(e))
116
120
 
121
+ def _ex_partial_derivative(self, e, b):
122
+ b.append(self._flhs(e))
123
+
124
+ def _ex_initial_value(self, e, b):
125
+ b.append(self._flhs(e))
126
+
117
127
  def _ex_number(self, e, b):
118
- b.append(myokit.float.str(e))
128
+ b.append(myokit.float.str(e).strip())
129
+ u = e.unit()
130
+ if u is not None and u is not myokit.units.dimensionless:
131
+ u = str(u)[1:-1]
132
+ b.append(r' \text{' + u + '}')
119
133
 
120
134
  def _ex_prefix_plus(self, e, b):
121
- self._ex(e[0], b)
135
+ self._ex_prefix(e, b, '+')
122
136
 
123
137
  def _ex_prefix_minus(self, e, b):
124
- b.append('\\left(')
125
- b.append('-')
126
- if e.bracket(e[0]):
127
- b.append('\\left(')
128
- self._ex(e[0], b)
129
- if e.bracket(e[0]):
130
- b.append('\\right)')
131
- b.append('\\right)')
138
+ self._ex_prefix(e, b, '-')
132
139
 
133
140
  def _ex_plus(self, e, b):
134
141
  self._ex_infix(e, b, '+')
@@ -137,10 +144,10 @@ class LatexExpressionWriter(myokit.formats.ExpressionWriter):
137
144
  self._ex_infix(e, b, '-')
138
145
 
139
146
  def _ex_multiply(self, e, b):
140
- self._ex_infix(e, b, '*')
147
+ self._ex_infix(e, b, r'\cdot')
141
148
 
142
149
  def _ex_divide(self, e, b):
143
- b.append('\\frac{')
150
+ b.append(r'\frac{')
144
151
  self._ex(e[0], b)
145
152
  b.append('}{')
146
153
  self._ex(e[1], b)
@@ -149,85 +156,88 @@ class LatexExpressionWriter(myokit.formats.ExpressionWriter):
149
156
  def _ex_quotient(self, e, b):
150
157
  # Note: Quotient in myokit uses round-to-zero (like Python does!)
151
158
  # See: myokit.Quotient
152
- b.append('\\left\\lfloor')
159
+ b.append(r'\left\lfloor')
153
160
  self._ex_divide(e, b)
154
- b.append('\\right\\rfloor')
161
+ b.append(r'\right\rfloor')
155
162
 
156
163
  def _ex_remainder(self, e, b):
157
- self._ex_function(e, b, '\\bmod')
164
+ self._ex_infix(e, b, r'\bmod')
158
165
 
159
166
  def _ex_power(self, e, b):
160
- if e.bracket(e[0]):
161
- b.append('\\left(')
167
+ if e.bracket(e[0]) or isinstance(e[0], myokit.Power):
168
+ b.append(r'\left(')
162
169
  self._ex(e[0], b)
163
- if e.bracket(e[0]):
164
- b.append('\\right)')
165
- b.append('^{')
170
+ if e.bracket(e[0]) or isinstance(e[0], myokit.Power):
171
+ b.append(r'\right)')
172
+ b.append('^')
173
+ if e.bracket(e[1]) or isinstance(e[1], myokit.Power):
174
+ b.append('{')
166
175
  self._ex(e[1], b)
167
- b.append('}')
176
+ if e.bracket(e[1]) or isinstance(e[1], myokit.Power):
177
+ b.append('}')
168
178
 
169
179
  def _ex_sqrt(self, e, b):
170
- b.append('\\sqrt{')
180
+ b.append(r'\sqrt{')
171
181
  self._ex(e[0], b)
172
182
  b.append('}')
173
183
 
174
184
  def _ex_sin(self, e, b):
175
- self._ex_function(e, b, '\\sin')
185
+ self._ex_function(e, b, r'\sin')
176
186
 
177
187
  def _ex_cos(self, e, b):
178
- self._ex_function(e, b, '\\cos')
188
+ self._ex_function(e, b, r'\cos')
179
189
 
180
190
  def _ex_tan(self, e, b):
181
- self._ex_function(e, b, '\\tan')
191
+ self._ex_function(e, b, r'\tan')
182
192
 
183
193
  def _ex_asin(self, e, b):
184
- self._ex_function(e, b, '\\arcsin')
194
+ self._ex_function(e, b, r'\arcsin')
185
195
 
186
196
  def _ex_acos(self, e, b):
187
- self._ex_function(e, b, '\\arccos')
197
+ self._ex_function(e, b, r'\arccos')
188
198
 
189
199
  def _ex_atan(self, e, b):
190
- self._ex_function(e, b, '\\arctan')
200
+ self._ex_function(e, b, r'\arctan')
191
201
 
192
202
  def _ex_exp(self, e, b):
193
- self._ex_function(e, b, '\\exp')
203
+ self._ex_function(e, b, r'\exp')
194
204
 
195
205
  def _ex_log(self, e, b):
196
- b.append('\\log')
206
+ b.append(r'\log')
197
207
  if len(e) > 1:
198
208
  b.append('_{')
199
209
  self._ex(e[1], b)
200
210
  b.append('}')
201
- b.append('\\left(')
211
+ b.append(r'\left(')
202
212
  self._ex(e[0], b)
203
- b.append('\\right)')
213
+ b.append(r'\right)')
204
214
 
205
215
  def _ex_log10(self, e, b):
206
- return self._ex_log(myokit.Log(e[0], myokit.Number(10)), b)
216
+ b.append(r'\log_{10}')
217
+ b.append(r'\left(')
218
+ self._ex(e[0], b)
219
+ b.append(r'\right)')
207
220
 
208
221
  def _ex_floor(self, e, b):
209
- b.append('\\left\\lfloor{')
222
+ b.append(r'\left\lfloor{')
210
223
  self._ex(e[0], b)
211
- b.append('}\\right\\rfloor')
224
+ b.append(r'}\right\rfloor')
212
225
 
213
226
  def _ex_ceil(self, e, b):
214
- b.append('\\left\\lceil{')
227
+ b.append(r'\left\lceil{')
215
228
  self._ex(e[0], b)
216
- b.append('}\\right\\rceil')
229
+ b.append(r'}\right\rceil')
217
230
 
218
231
  def _ex_abs(self, e, b):
219
- b.append('\\lvert{')
232
+ b.append(r'\lvert{')
220
233
  self._ex(e[0], b)
221
- b.append('}\\rvert')
222
-
223
- def _ex_not(self, e, b):
224
- self._ex_function(e, b, '\\not')
234
+ b.append(r'}\rvert')
225
235
 
226
236
  def _ex_equal(self, e, b):
227
237
  self._ex_infix_condition(e, b, '=')
228
238
 
229
239
  def _ex_not_equal(self, e, b):
230
- self._ex_infix_condition(e, b, '\\neq')
240
+ self._ex_infix_condition(e, b, r'\neq')
231
241
 
232
242
  def _ex_more(self, e, b):
233
243
  self._ex_infix_condition(e, b, '>')
@@ -236,21 +246,26 @@ class LatexExpressionWriter(myokit.formats.ExpressionWriter):
236
246
  self._ex_infix_condition(e, b, '<')
237
247
 
238
248
  def _ex_more_equal(self, e, b):
239
- self._ex_infix_condition(e, b, '\\geq')
249
+ self._ex_infix_condition(e, b, r'\geq')
240
250
 
241
251
  def _ex_less_equal(self, e, b):
242
- self._ex_infix_condition(e, b, '\\leq')
252
+ self._ex_infix_condition(e, b, r'\leq')
253
+
254
+ def _ex_not(self, e, b):
255
+ b.append(r'\left(\not')
256
+ self._ex(e[0], b)
257
+ b.append(r'\right)')
243
258
 
244
259
  def _ex_and(self, e, b):
245
- self._ex_infix_condition(e, b, '\\and')
260
+ self._ex_infix_condition(e, b, r'\and')
246
261
 
247
262
  def _ex_or(self, e, b):
248
- self._ex_infix_condition(e, b, '\\or')
263
+ self._ex_infix_condition(e, b, r'\or')
249
264
 
250
265
  def _ex_if(self, e, b):
251
266
  # Not suported
252
- self._ex_function(e, b, 'if')
267
+ self._ex_function(e, b, r'\text{if}')
253
268
 
254
269
  def _ex_piecewise(self, e, b):
255
270
  # Not suported
256
- self._ex_function(e, b, 'piecewise')
271
+ self._ex_function(e, b, r'\text{piecewise}')
@@ -23,7 +23,7 @@ class PdfExporter(myokit.formats.Exporter):
23
23
  """
24
24
  Cleans some text for use in latex.
25
25
  """
26
- return text.replace('_', '\_')
26
+ return text.replace('_', r'\_')
27
27
 
28
28
  def post_export_info(self):
29
29
  return '\n'.join((
@@ -130,11 +130,11 @@ class MathMLExpressionWriter(myokit.formats.ExpressionWriter):
130
130
  bra = e.bracket(e[0])
131
131
  if self._pres:
132
132
  row = etree.SubElement(t, 'mrow')
133
+ x = etree.SubElement(row, 'mo')
134
+ x.text = e.operator_rep()
133
135
  if bra:
134
136
  x = etree.SubElement(row, 'mo')
135
137
  x.text = '('
136
- x = etree.SubElement(row, 'mo')
137
- x.text = e.operator_rep()
138
138
  self._ex(e[0], row)
139
139
  if bra:
140
140
  x = etree.SubElement(row, 'mo')
@@ -4,6 +4,8 @@
4
4
  # This file is part of Myokit.
5
5
  # See http://myokit.org for copyright, sharing, and licensing details.
6
6
  #
7
+ import myokit
8
+
7
9
  from myokit.formats.python import PythonExpressionWriter
8
10
 
9
11
 
@@ -17,15 +19,32 @@ class MatlabExpressionWriter(PythonExpressionWriter):
17
19
  self._function_prefix = ''
18
20
  self._fcond = 'ifthenelse'
19
21
 
20
- def set_condition_function(self, func=None):
22
+ def set_condition_function(self, func):
21
23
  """
22
- Sets a function name to use for if statements
24
+ Sets a function name to output to handle :class:`myokit.If`.
25
+
26
+ The function must take arguments ``(condition, value_if_true,
27
+ value_if_false)`` and will be used to handle both :class:`myokit.If`
28
+ and :class:`myokit.Piecewise`.
29
+
30
+ By default, `ifthenelse` is used, which the user is expected to define
31
+ if ``If`` or ``Piecewise`` will be used. For example::
32
+
33
+ function y = ifthenelse(condition, value_if_true, value_if_false)
34
+ if (condition)
35
+ y = value_if_true;
36
+ else
37
+ y = value_if_false;
38
+ end
39
+ end
23
40
 
24
- By setting func to None you can revert back to the default behavior
25
- (the ternary operator). Any other value will be interpreted as the
26
- name of a function taking arguments (condition, value_if_true,
27
- value_if_false).
28
41
  """
42
+ if func is not None:
43
+ func = str(func).strip()
44
+ if func is None or func == '':
45
+ raise ValueError(
46
+ 'The MatlabExpressionWriter needs a condition function to be'
47
+ ' set.')
29
48
  self._fcond = func
30
49
 
31
50
  #def _ex_name(self, e):
@@ -40,14 +59,17 @@ class MatlabExpressionWriter(PythonExpressionWriter):
40
59
 
41
60
  def _ex_quotient(self, e):
42
61
  # Round towards minus infinity
43
- return 'floor(' + self._ex_infix(e, '/') + ')'
62
+ return self.ex(myokit.Floor(myokit.Divide(e[0], e[1])))
44
63
 
45
64
  def _ex_remainder(self, e):
46
65
  # Uses the round-towards-minus-infinity convention!
47
- return 'mod(' + self.ex(e[0]) + ', ' + self.ex(e[1]) + ')'
66
+ return f'mod({self.ex(e[0])}, {self.ex(e[1])})'
48
67
 
49
68
  def _ex_power(self, e):
50
- return self._ex_infix(e, '^')
69
+ # Same associativity as Myokit, not Python! So override
70
+ e1 = f'({self.ex(e[0])})' if e.bracket(e[0]) else f'{self.ex(e[0])}'
71
+ e2 = f'({self.ex(e[1])})' if e.bracket(e[1]) else f'{self.ex(e[1])}'
72
+ return f'{e1}^{e2}'
51
73
 
52
74
  #def _ex_sqrt(self, e):
53
75
  # Ignore imaginary part
@@ -63,8 +85,9 @@ class MatlabExpressionWriter(PythonExpressionWriter):
63
85
  def _ex_log(self, e):
64
86
  if len(e) == 1:
65
87
  return self._ex_function(e, 'log')
66
- else:
67
- return '(log(' + self.ex(e[0]) + ') / log(' + self.ex(e[1]) + '))'
88
+ # Always add brackets: Parent was expecting a function so will never
89
+ # have added them.
90
+ return f'(log({self.ex(e[0])}) / log({self.ex(e[1])}))'
68
91
 
69
92
  #def _ex_log10(self, e):
70
93
  #def _ex_floor(self, e):
@@ -73,9 +96,6 @@ class MatlabExpressionWriter(PythonExpressionWriter):
73
96
  def _ex_abs(self, e):
74
97
  return self._ex_function(e, 'abs')
75
98
 
76
- def _ex_not(self, e):
77
- return '!(' + self.ex(e[0]) + ')'
78
-
79
99
  #def _ex_equal(self, e):
80
100
  #def _ex_not_equal(self, e):
81
101
  #def _ex_more(self, e):
@@ -84,26 +104,28 @@ class MatlabExpressionWriter(PythonExpressionWriter):
84
104
  #def _ex_less_equal(self, e):
85
105
 
86
106
  def _ex_and(self, e):
87
- return self._ex_infix_condition(e, '&&')
107
+ return self._ex_infix_logical(e, '&&')
88
108
 
89
109
  def _ex_or(self, e):
90
- return self._ex_infix_condition(e, '||')
110
+ return self._ex_infix_logical(e, '||')
111
+
112
+ def _ex_not(self, e):
113
+ return f'(!{self.ex(e[0])})'
91
114
 
92
115
  def _ex_if(self, e):
93
- return '%s(%s, %s, %s)' % (
94
- self._fcond, self.ex(e._i), self.ex(e._t), self.ex(e._e))
116
+ _if, _then, _else = self.ex(e._i), self.ex(e._t), self.ex(e._e)
117
+ return f'{self._fcond}({_if}, {_then}, {_else})'
95
118
 
96
119
  def _ex_piecewise(self, e):
120
+ # Render ifs; add extra bracket if not a condition (see _ex_if)
121
+ _ifs = [self.ex(x) for x in e._i]
122
+ _thens = [self.ex(x) for x in e._e]
123
+
97
124
  s = []
98
- n = len(e._i)
99
- for i in range(0, n):
100
- s.append(self._fcond)
101
- s.append('(')
102
- s.append(self.ex(e._i[i]))
103
- s.append(', ')
104
- s.append(self.ex(e._e[i]))
105
- s.append(', ')
106
- s.append(self.ex(e._e[n]))
107
- s.append(')' * n)
125
+ n = len(_ifs)
126
+ for _if, _then in zip(_ifs, _thens):
127
+ s.append(f'{self._fcond}({_if}, {_then}, ')
128
+ s.append(_thens[-1])
129
+ s.append(')' * len(_ifs))
108
130
  return ''.join(s)
109
131