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
myokit/_sim/cvodessim.py CHANGED
@@ -238,6 +238,7 @@ class Simulation(myokit.CModule):
238
238
 
239
239
  # Last state reached before error
240
240
  self._error_state = None
241
+ self._error_inputs = None
241
242
 
242
243
  # Starting time
243
244
  self._time = 0
@@ -387,6 +388,28 @@ class Simulation(myokit.CModule):
387
388
  finally:
388
389
  myokit.tools.rmtree(d_build, silent=True)
389
390
 
391
+ def crash_inputs(self):
392
+ """
393
+ If the last call to :meth:`Simulation.pre()` or
394
+ :meth:`Simulation.run()` resulted in an error, this will return the
395
+ last "inputs" (time, pace, etc) reached during that simulation.
396
+
397
+ Will return ``None`` if no simulation was run or the simulation did not
398
+ result in an error.
399
+ """
400
+ return dict(self._error_inputs) if self._error_inputs else None
401
+
402
+ def crash_state(self):
403
+ """
404
+ If the last call to :meth:`Simulation.pre()` or
405
+ :meth:`Simulation.run()` resulted in an error, this will return the
406
+ last state reached during that simulation.
407
+
408
+ Will return ``None`` if no simulation was run or the simulation did not
409
+ result in an error.
410
+ """
411
+ return list(self._error_state) if self._error_state else None
412
+
390
413
  def default_state(self):
391
414
  """
392
415
  Returns the default state.
@@ -402,60 +425,83 @@ class Simulation(myokit.CModule):
402
425
  return [list(x) for x in self._s_default_state]
403
426
  return None
404
427
 
405
- def last_state(self):
428
+ def eval_derivatives(self, y=None, pacing=None):
406
429
  """
407
- If the last call to :meth:`Simulation.pre()` or
408
- :meth:`Simulation.run()` resulted in an error, this will return the
409
- last state reached during that simulation.
430
+ Deprecated alias of :meth:`evaluate_derivatives`.
410
431
 
411
- Will return ``None`` if no simulation was run or the simulation did not
412
- result in an error.
432
+ Note that while :meth:`eval_derivatives` used the :meth:`state()` as
433
+ default, the new method uses :meth:`default_state()`.
413
434
  """
414
- return list(self._error_state) if self._error_state else None
435
+ # Deprecated on 2024-03-08
436
+ import warnings
437
+ warnings.warn(
438
+ 'The method `myokit.Simulation.eval_derivatives` is deprecated,'
439
+ ' please use `evaluate_derivatives(state=sim.state())` instead.'
440
+ )
441
+ if y is None:
442
+ y = self.state()
443
+ return self.evaluate_derivatives(y, pacing)
415
444
 
416
- def eval_derivatives(self, y=None, pacing=None):
445
+ def evaluate_derivatives(self, state=None, inputs=None):
417
446
  """
418
- Evaluates and returns the state derivatives.
447
+ Evaluates and returns the state derivatives for the given ``state`` and
448
+ ``inputs``.
449
+
450
+ If no ``state`` is given, the value returned by :meth:`default_state()`
451
+ is used.
419
452
 
420
- The state to evaluate for can be given as ``y``. If no state is given
421
- the current simulation state is used.
453
+ An optional dict ``inputs`` can be passed in to set the values of
454
+ time and other inputs (e.g. pacing values). If "time" is not set in
455
+ ``inputs``, the value 0 will be used, regardless of the current
456
+ simulation. Similarly, if pacing values are not set in ``inputs``, 0
457
+ will be used regardless of the protocols or the current time.
422
458
 
423
- An optional dict ``pacing`` can be passed in that maps labels to
424
- numerical values. If a label is used but not provided in ``pacing``,
425
- it's value will be set to 0.
459
+ If any variables have been changed with `set_constant` their new value
460
+ will be used.
426
461
  """
427
462
  # Get state
428
- if y is None:
429
- y = list(self._state)
463
+ if state is None:
464
+ y = list(self._default_state)
430
465
  else:
431
- y = self._model.map_to_state(y)
466
+ y = self._model.map_to_state(state)
432
467
 
468
+ # Get inputs
469
+ time = realtime = evaluations = 0
433
470
  pacing_values = [0.0] * len(self._pacing_labels)
434
- if pacing is not None:
435
- for k, v in pacing.items():
471
+ if inputs is not None:
472
+ if 'time' in inputs:
473
+ time = float(inputs['time'])
474
+ del inputs['time']
475
+ if 'realtime' in inputs: # User really shouldn't, but can do this
476
+ realtime = float(inputs['realtime'])
477
+ del inputs['realtime']
478
+ if 'evaluations' in inputs:
479
+ evaluations = float(inputs['evaluations'])
480
+ del inputs['evaluations']
481
+ for k, v in inputs.items():
436
482
  try:
437
483
  ki = self._pacing_labels.index(k)
438
- pacing_values[ki] = float(v)
439
484
  except ValueError:
440
- pass
485
+ raise ValueError(f'Unknown binding or pacing label `{k}`.')
486
+ pacing_values[ki] = float(v)
487
+
488
+ # Literals and parameters: Can be changed with set_constant
489
+ literals = list(self._literals.values())
490
+ parameters = list(self._parameters.values())
441
491
 
442
492
  # Create space to store derivatives
443
493
  dy = list(self._state)
444
494
 
445
495
  # Evaluate and return
446
- self._sim.eval_derivatives(
447
- # 0. Time
448
- 0,
449
- # 1. Pace
450
- pacing_values,
451
- # 2. State
452
- y,
453
- # 3. Space to store the state derivatives
454
- dy,
455
- # 4. Literal values
456
- list(self._literals.values()),
457
- # 5. Parameter values
458
- list(self._parameters.values()),
496
+ self._sim.evaluate_derivatives(
497
+ time, # 0. Time
498
+ pacing_values, # 1. Pacing values
499
+ realtime, # 2. Realtime
500
+ evaluations, # 3. Evaluations
501
+ literals, # 4. Literals
502
+ parameters, # 5. Parameters
503
+ y, # 6. State
504
+ dy, # 7. Derivatives (out)
459
505
  )
460
506
  return dy
461
507
 
@@ -473,6 +519,21 @@ class Simulation(myokit.CModule):
473
519
  """
474
520
  return self._sim.number_of_steps()
475
521
 
522
+ def last_state(self):
523
+ """
524
+ If the last call to :meth:`Simulation.pre()` or
525
+ :meth:`Simulation.run()` resulted in an error, this will return the
526
+ last state reached during that simulation.
527
+
528
+ Will return ``None`` if no simulation was run or the simulation did not
529
+ result in an error.
530
+ """
531
+ # Deprecated on 2024-03-08
532
+ import warnings
533
+ warnings.warn('The method `myokit.Simulation.last_state` is'
534
+ ' deprecated. Please use `crash_state` instead.')
535
+ return self.crash_state()
536
+
476
537
  def pre(self, duration, progress=None, msg='Pre-pacing simulation'):
477
538
  """
478
539
  This method can be used to perform an unlogged simulation, typically to
@@ -690,6 +751,7 @@ class Simulation(myokit.CModule):
690
751
 
691
752
  # Reset error state
692
753
  self._error_state = None
754
+ self._error_inputs = None
693
755
 
694
756
  # Simulation times
695
757
  if duration < 0:
@@ -847,26 +909,28 @@ class Simulation(myokit.CModule):
847
909
 
848
910
  # Store error state
849
911
  self._error_state = state
912
+ self._error_inputs = {'time': bound[0]}
913
+ for i, label in enumerate(('realtime', 'evaluations')):
914
+ if self._model.binding(label) is not None:
915
+ self._error_inputs[label] = bound[1 + i]
916
+ for i, label in enumerate(self._pacing_labels):
917
+ self._error_inputs[label] = bound[3 + i]
850
918
 
851
919
  # Create long error message
852
920
  txt = ['A numerical error occurred during simulation at'
853
- ' t = ' + str(t) + '.', 'Last reached state: ']
921
+ ' t = ' + myokit.float.str(bound[0]) + '.',
922
+ 'Last reached state: ']
854
923
  txt.extend([' ' + x for x
855
924
  in self._model.format_state(state).splitlines()])
856
925
  txt.append('Inputs for binding:')
857
- txt.append(' time = ' + myokit.float.str(bound[0]))
858
- txt.append(' realtime = ' + myokit.float.str(bound[1]))
859
- txt.append(' evaluations = ' + myokit.float.str(bound[2]))
860
- for i, label in enumerate(self._pacing_labels):
861
- txt.append(
862
- ' ' + label + ' = ' + myokit.float.str(bound[3 + i])
863
- )
926
+ for key, value in self._error_inputs.items():
927
+ txt.append(f' {key} = {myokit.float.str(value)}')
864
928
  txt.append(str(e))
865
929
 
866
930
  # Check if state derivatives can be evaluated in Python, if
867
931
  # not, add the error to the error message.
868
932
  try:
869
- self._model.evaluate_derivatives(state)
933
+ self._model.evaluate_derivatives(state, self._error_inputs)
870
934
  except Exception as en:
871
935
  txt.append(str(en))
872
936
 
myokit/_sim/mcl.h CHANGED
@@ -651,6 +651,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
651
651
  }
652
652
 
653
653
  // Name
654
+ #ifdef MYOKIT_DEBUG_MESSAGES
655
+ printf(" Querying name\n");
656
+ #endif
654
657
  flag = clGetDeviceInfo(device_id, CL_DEVICE_NAME, bufsize, buffer, NULL);
655
658
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
656
659
  val = PyUnicode_FromString(buffer);
@@ -658,6 +661,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
658
661
  Py_CLEAR(val);
659
662
 
660
663
  // Vendor
664
+ #ifdef MYOKIT_DEBUG_MESSAGES
665
+ printf(" Querying vendor\n");
666
+ #endif
661
667
  flag = clGetDeviceInfo(device_id, CL_DEVICE_VENDOR, bufsize, buffer, NULL);
662
668
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
663
669
  val = PyUnicode_FromString(buffer);
@@ -665,6 +671,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
665
671
  Py_CLEAR(val);
666
672
 
667
673
  // Device version
674
+ #ifdef MYOKIT_DEBUG_MESSAGES
675
+ printf(" Querying device version\n");
676
+ #endif
668
677
  flag = clGetDeviceInfo(device_id, CL_DEVICE_VERSION, bufsize, buffer, NULL);
669
678
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
670
679
  val = PyUnicode_FromString(buffer);
@@ -672,6 +681,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
672
681
  Py_CLEAR(val);
673
682
 
674
683
  // Driver version
684
+ #ifdef MYOKIT_DEBUG_MESSAGES
685
+ printf(" Querying driver version\n");
686
+ #endif
675
687
  flag = clGetDeviceInfo(device_id, CL_DRIVER_VERSION, bufsize, buffer, NULL);
676
688
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
677
689
  val = PyUnicode_FromString(buffer);
@@ -679,6 +691,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
679
691
  Py_CLEAR(val);
680
692
 
681
693
  // Clock speed (MHz)
694
+ #ifdef MYOKIT_DEBUG_MESSAGES
695
+ printf(" Querying clock speed\n");
696
+ #endif
682
697
  flag = clGetDeviceInfo(device_id, CL_DEVICE_MAX_CLOCK_FREQUENCY, sizeof(buf_uint), &buf_uint, NULL);
683
698
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
684
699
  val = PyLong_FromUnsignedLong(buf_uint);
@@ -686,6 +701,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
686
701
  Py_CLEAR(val);
687
702
 
688
703
  // Global memory (bytes)
704
+ #ifdef MYOKIT_DEBUG_MESSAGES
705
+ printf(" Querying global memory size\n");
706
+ #endif
689
707
  flag = clGetDeviceInfo(device_id, CL_DEVICE_GLOBAL_MEM_SIZE, sizeof(buf_ulong), &buf_ulong, NULL);
690
708
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
691
709
  val = PyLong_FromUnsignedLongLong(buf_ulong);
@@ -693,6 +711,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
693
711
  Py_CLEAR(val);
694
712
 
695
713
  // Local memory (bytes)
714
+ #ifdef MYOKIT_DEBUG_MESSAGES
715
+ printf(" Querying local memory size\n");
716
+ #endif
696
717
  flag = clGetDeviceInfo(device_id, CL_DEVICE_LOCAL_MEM_SIZE, sizeof(buf_ulong), &buf_ulong, NULL);
697
718
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
698
719
  val = PyLong_FromUnsignedLongLong(buf_ulong);
@@ -700,6 +721,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
700
721
  Py_CLEAR(val);
701
722
 
702
723
  // Const memory (bytes)
724
+ #ifdef MYOKIT_DEBUG_MESSAGES
725
+ printf(" Querying max buffer size\n");
726
+ #endif
703
727
  flag = clGetDeviceInfo(device_id, CL_DEVICE_MAX_CONSTANT_BUFFER_SIZE, sizeof(buf_ulong), &buf_ulong, NULL);
704
728
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
705
729
  val = PyLong_FromUnsignedLongLong(buf_ulong);
@@ -707,6 +731,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
707
731
  Py_CLEAR(val);
708
732
 
709
733
  // Computing units
734
+ #ifdef MYOKIT_DEBUG_MESSAGES
735
+ printf(" Querying max computing units\n");
736
+ #endif
710
737
  flag = clGetDeviceInfo(device_id, CL_DEVICE_MAX_COMPUTE_UNITS, sizeof(buf_uint), &buf_uint, NULL);
711
738
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
712
739
  val = PyLong_FromUnsignedLong(buf_uint);
@@ -714,6 +741,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
714
741
  Py_CLEAR(val);
715
742
 
716
743
  // Max workgroup size
744
+ #ifdef MYOKIT_DEBUG_MESSAGES
745
+ printf(" Querying max work group size\n");
746
+ #endif
717
747
  flag = clGetDeviceInfo(device_id, CL_DEVICE_MAX_WORK_GROUP_SIZE, sizeof(buf_size_t), &buf_size_t, NULL);
718
748
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
719
749
  val = PyLong_FromSize_t(buf_size_t);
@@ -721,6 +751,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
721
751
  Py_CLEAR(val);
722
752
 
723
753
  // Max workitem sizes
754
+ #ifdef MYOKIT_DEBUG_MESSAGES
755
+ printf(" Querying max work item sizes\n");
756
+ #endif
724
757
  flag = clGetDeviceInfo(device_id, CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS, sizeof(buf_uint), &buf_uint, NULL);
725
758
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
726
759
  val = PyLong_FromUnsignedLong(buf_uint);
@@ -739,6 +772,9 @@ PyObject* mcl_info_device_dict(cl_device_id device_id, size_t bufsize, char* buf
739
772
  Py_CLEAR(items_sizes_tuple);
740
773
 
741
774
  // Maximum size of a kernel parameter
775
+ #ifdef MYOKIT_DEBUG_MESSAGES
776
+ printf(" Querying maximum kernel parameter size\n");
777
+ #endif
742
778
  flag = clGetDeviceInfo(device_id, CL_DEVICE_MAX_PARAMETER_SIZE, sizeof(buf_size_t), &buf_size_t, NULL);
743
779
  if(mcl_flag(flag)) { Py_DECREF(device); return NULL; }
744
780
  val = PyLong_FromSize_t(buf_size_t);
@@ -834,29 +870,47 @@ mcl_info(void)
834
870
 
835
871
  // Check all platforms
836
872
  for (i=0; i<n_platforms; i++) {
873
+ #ifdef MYOKIT_DEBUG_MESSAGES
874
+ printf("Checking platform %u\n", (unsigned int)i);
875
+ #endif
837
876
 
838
877
  // Create platform dict
839
878
  platform = mcl_info_platform_dict(platform_ids[i], sizeof(buffer), buffer);
840
879
  if (platform == NULL) { Py_DECREF(platforms); return NULL; }
880
+ #ifdef MYOKIT_DEBUG_MESSAGES
881
+ printf(" Obtained platform info dict.\n");
882
+ #endif
841
883
 
842
884
  // Count devices
843
885
  flag = clGetDeviceIDs(platform_ids[i], CL_DEVICE_TYPE_ALL, MCL_MAX_DEVICES, device_ids, &n_devices);
844
886
  if (flag == CL_DEVICE_NOT_FOUND) {
845
887
  n_devices = 0;
888
+ } else if (flag == CL_INVALID_VALUE) {
889
+ // This seems to happen on Mac OS 14.4.1
890
+ n_devices = 0;
846
891
  } else if (mcl_flag(flag)) {
847
892
  Py_DECREF(platforms);
848
893
  return NULL;
849
894
  }
895
+ #ifdef MYOKIT_DEBUG_MESSAGES
896
+ printf(" Found %u devices.\n", (unsigned int)n_devices);
897
+ #endif
850
898
 
851
899
  // Create devices tuple, must decref on exception
852
900
  devices = PyTuple_New((size_t)n_devices);
853
901
 
854
902
  // Add devices
855
903
  for (j=0; j<n_devices; j++) {
904
+ #ifdef MYOKIT_DEBUG_MESSAGES
905
+ printf(" Checking device %u\n", (unsigned int)j);
906
+ #endif
856
907
 
857
908
  // Create device dict, must decref on exception
858
909
  device = mcl_info_device_dict(device_ids[j], sizeof(buffer), buffer);
859
910
  if (device == NULL) { Py_DECREF(platforms); Py_DECREF(devices); return NULL; }
911
+ #ifdef MYOKIT_DEBUG_MESSAGES
912
+ printf(" Obtained device info dict.\n");
913
+ #endif
860
914
 
861
915
  // Add device to devices tuple (steals reference)
862
916
  PyTuple_SetItem(devices, j, device);
@@ -24,6 +24,9 @@ _EWRITERS = None
24
24
  class Exporter:
25
25
  """
26
26
  Abstract base class for exporters.
27
+
28
+ An exporter is a class that can produce model code or runnable code from a
29
+ Myokit model or a model & protocol combination (runnable).
27
30
  """
28
31
  def __init__(self):
29
32
  super().__init__()
@@ -106,15 +109,46 @@ def exporters():
106
109
 
107
110
  class ExpressionWriter:
108
111
  """
109
- Base class for expression writers, that take myokit expressions as input
110
- and convert them to text or other formats.
112
+ Base class for expression writers.
113
+
114
+ Expression writers take :class:`myokit.Expression` objects and produce code
115
+ in some language other than Myokit.
116
+
117
+ When implementing an expression writer, there are a few important edge
118
+ cases to consider:
119
+
120
+ 1. Simplifications must not be made at this stage. For example, if the user
121
+ writes "+1" instead of "1", the code should output "+1". If
122
+ simplifications are desired, these should be made at an earlier stage,
123
+ when everything is still in symbolic form.
124
+
125
+ 2. Powers (exponentiation) can be left or right-associative. In Myokit and,
126
+ for example, in Matlab, the expression ``a^b^c`` is interpreted as
127
+ ``(a^b)^c``. By in Python (and e.g. in many spreadsheet applications)
128
+ ``a**b**c`` is interpreted as ``a**(b**c)``, necessitating a different
129
+ bracket-adding logic than used in Myokit.
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 ``++``,
140
+ so the expression ``--x`` is interpreted as ``-(-x)``. This is the same
141
+ in Python. But in C-based languages, this is interpreted as a decrement
142
+ operator so care must be taken to add extra brackets.
143
+
111
144
  """
112
145
  def __init__(self):
113
146
  self._op_map = self._build_op_map()
114
147
 
115
148
  def _build_op_map(self):
116
149
  """
117
- Returns a mapping from myokit operators to lambda functions on expr.
150
+ Returns a mapping from Myokit operators to lambda functions on
151
+ expressions.
118
152
  """
119
153
  return {
120
154
  myokit.Name: self._ex_name,
@@ -158,15 +192,11 @@ class ExpressionWriter:
158
192
  }
159
193
 
160
194
  def eq(self, q):
161
- """
162
- Converts an equation to a string
163
- """
195
+ """ Converts a :class:`myokit.Equation` to a string. """
164
196
  return self.ex(q.lhs) + ' = ' + self.ex(q.rhs)
165
197
 
166
198
  def ex(self, e):
167
- """
168
- Converts an Expression to a string.
169
- """
199
+ """ Converts a :class:`myokit.Expression` to a string. """
170
200
  t = type(e)
171
201
  if t not in self._op_map: # pragma: no cover
172
202
  raise ValueError('Unknown expression type: ' + str(t))
@@ -188,9 +218,17 @@ class ExpressionWriter:
188
218
  raise NotImplementedError
189
219
 
190
220
  def _ex_prefix_plus(self, e):
221
+ #
222
+ # Note: Spaces or brackets should be used in languages where ++ is an
223
+ # operator
224
+ #
191
225
  raise NotImplementedError
192
226
 
193
227
  def _ex_prefix_minus(self, e):
228
+ #
229
+ # Note: Spaces or brackets should be used in languages where -- is an
230
+ # operator
231
+ #
194
232
  raise NotImplementedError
195
233
 
196
234
  def _ex_plus(self, e):
@@ -212,6 +250,10 @@ class ExpressionWriter:
212
250
  raise NotImplementedError
213
251
 
214
252
  def _ex_power(self, e):
253
+ #
254
+ # Note: Most languages agree that -a**b means -(a**b)
255
+ # https://codeplea.com/exponentiation-associativity-options
256
+ #
215
257
  raise NotImplementedError
216
258
 
217
259
  def _ex_sqrt(self, e):
@@ -239,6 +281,11 @@ class ExpressionWriter:
239
281
  raise NotImplementedError
240
282
 
241
283
  def _ex_log(self, e):
284
+ #
285
+ # Log can have 1 or 2 arguments. If the language does not support
286
+ # 2-argument log, use (log(a) / log(b)), including the outer
287
+ # brackets (because a function is expected, which is never bracketed).
288
+ #
242
289
  raise NotImplementedError
243
290
 
244
291
  def _ex_log10(self, e):
@@ -253,10 +300,11 @@ class ExpressionWriter:
253
300
  def _ex_abs(self, e):
254
301
  raise NotImplementedError
255
302
 
256
- def _ex_not(self, e):
257
- raise NotImplementedError
258
-
259
303
  def _ex_equal(self, e):
304
+ #
305
+ # To obtain the desired `(0 == 0) == 0` behaviour, this and other
306
+ # conditions are best rendered with brackets.
307
+ #
260
308
  raise NotImplementedError
261
309
 
262
310
  def _ex_not_equal(self, e):
@@ -280,6 +328,9 @@ class ExpressionWriter:
280
328
  def _ex_or(self, e):
281
329
  raise NotImplementedError
282
330
 
331
+ def _ex_not(self, e):
332
+ raise NotImplementedError
333
+
283
334
  def _ex_if(self, e):
284
335
  raise NotImplementedError
285
336
 
@@ -5,7 +5,7 @@
5
5
  # See http://myokit.org for copyright, sharing, and licensing details.
6
6
  #
7
7
  from ._exporter import AnsiCExporter, AnsiCCableExporter, AnsiCEulerExporter
8
- from ._ewriter import AnsiCExpressionWriter
8
+ from ._ewriter import AnsiCExpressionWriter, CBasedExpressionWriter # noqa
9
9
 
10
10
 
11
11
  # Importers
@@ -28,6 +28,7 @@ def exporters():
28
28
  # Expression writers
29
29
  _ewriters = {
30
30
  'ansic': AnsiCExpressionWriter,
31
+ # c-based is intended as base class only
31
32
  }
32
33
 
33
34