myokit 1.35.0__py3-none-any.whl → 1.35.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. myokit/__init__.py +11 -14
  2. myokit/__main__.py +0 -3
  3. myokit/_config.py +1 -3
  4. myokit/_datablock.py +914 -12
  5. myokit/_model_api.py +1 -3
  6. myokit/_myokit_version.py +1 -1
  7. myokit/_protocol.py +14 -28
  8. myokit/_sim/cable.c +1 -1
  9. myokit/_sim/cable.py +3 -2
  10. myokit/_sim/cmodel.h +1 -0
  11. myokit/_sim/cvodessim.c +79 -42
  12. myokit/_sim/cvodessim.py +20 -8
  13. myokit/_sim/fiber_tissue.c +1 -1
  14. myokit/_sim/fiber_tissue.py +3 -2
  15. myokit/_sim/openclsim.c +1 -1
  16. myokit/_sim/openclsim.py +8 -11
  17. myokit/_sim/pacing.h +121 -106
  18. myokit/_unit.py +1 -1
  19. myokit/formats/__init__.py +178 -0
  20. myokit/formats/axon/_abf.py +911 -841
  21. myokit/formats/axon/_atf.py +62 -59
  22. myokit/formats/axon/_importer.py +2 -2
  23. myokit/formats/heka/__init__.py +38 -0
  24. myokit/formats/heka/_importer.py +39 -0
  25. myokit/formats/heka/_patchmaster.py +2512 -0
  26. myokit/formats/wcp/_wcp.py +318 -133
  27. myokit/gui/datablock_viewer.py +144 -77
  28. myokit/gui/datalog_viewer.py +212 -231
  29. myokit/tests/ansic_event_based_pacing.py +3 -3
  30. myokit/tests/{ansic_fixed_form_pacing.py → ansic_time_series_pacing.py} +6 -6
  31. myokit/tests/data/formats/abf-v2.abf +0 -0
  32. myokit/tests/test_datablock.py +84 -0
  33. myokit/tests/test_datalog.py +2 -1
  34. myokit/tests/test_formats_axon.py +589 -136
  35. myokit/tests/test_formats_wcp.py +191 -22
  36. myokit/tests/test_pacing_system_c.py +51 -23
  37. myokit/tests/test_pacing_system_py.py +18 -0
  38. myokit/tests/test_simulation_1d.py +62 -22
  39. myokit/tests/test_simulation_cvodes.py +52 -3
  40. myokit/tests/test_simulation_fiber_tissue.py +35 -4
  41. myokit/tests/test_simulation_opencl.py +28 -4
  42. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/LICENSE.txt +1 -1
  43. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/METADATA +1 -1
  44. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/RECORD +47 -44
  45. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/WHEEL +0 -0
  46. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/entry_points.txt +0 -0
  47. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/top_level.txt +0 -0
@@ -28,7 +28,7 @@ SETTINGS_FILE = os.path.join(myokit.DIR_USER, 'DataBlockViewer.ini')
28
28
  N_RECENT_FILES = 5
29
29
 
30
30
  # About
31
- ABOUT = '<h1>' + TITLE + '</h1>' + """
31
+ ABOUT = f'<h1>{TITLE}</h1>' + f"""
32
32
  <p>
33
33
  Myokit's DataBlock viewer is a utility to examine
34
34
  <code>DataBlock1d</code> and <code>DataBlock2d</code> objects
@@ -42,10 +42,10 @@ ABOUT = '<h1>' + TITLE + '</h1>' + """
42
42
  </p>
43
43
  <p>
44
44
  System info:
45
- <br />Python: PYTHON
46
- <br />Using the BACKEND GUI backend.
45
+ <br />Python: {sys.version}
46
+ <br />Using the {myokit.gui.backend} GUI backend.
47
47
  </p>
48
- """.replace('BACKEND', myokit.gui.backend).replace('PYTHON', sys.version)
48
+ """
49
49
 
50
50
  # License
51
51
  LICENSE = myokit.LICENSE_HTML
@@ -194,7 +194,7 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
194
194
  # Variable selection
195
195
  self._variable_select = QtWidgets.QComboBox()
196
196
  self._variable_select.activated.connect(self.event_variable_selected)
197
- self._variable_select.setMinimumWidth(120)
197
+ self._variable_select.setMinimumWidth(160)
198
198
 
199
199
  # Colormap selection
200
200
  self._colormap = next(iter(myokit.ColorMap.names()))
@@ -204,6 +204,26 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
204
204
  self._colormap_select.activated.connect(self.event_colormap_selected)
205
205
  self._colormap_select.setMinimumWidth(120)
206
206
 
207
+ self._colormap_lower_label = QtWidgets.QLabel('Range')
208
+ self._colormap_lower_label.setMaximumWidth(50)
209
+ self._colormap_lower_label.setAlignment(
210
+ Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
211
+ self._colormap_lower_field = AutoFloatField()
212
+ self._colormap_lower_field.setMaxLength(6)
213
+ self._colormap_lower_field.setMaximumWidth(80)
214
+ self._colormap_lower_field.editingFinished.connect(
215
+ self.event_variable_selected)
216
+
217
+ self._colormap_upper_label = QtWidgets.QLabel('to')
218
+ self._colormap_upper_label.setMaximumWidth(20)
219
+ self._colormap_upper_label.setAlignment(
220
+ Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
221
+ self._colormap_upper_field = AutoFloatField()
222
+ self._colormap_upper_field.setMaxLength(6)
223
+ self._colormap_upper_field.setMaximumWidth(80)
224
+ self._colormap_upper_field.editingFinished.connect(
225
+ self.event_variable_selected)
226
+
207
227
  # Control layout
208
228
  self._control_layout = QtWidgets.QHBoxLayout()
209
229
  self._control_layout.addWidget(self._play_button)
@@ -216,6 +236,10 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
216
236
  self._control_layout.addWidget(self._graph_clear_button)
217
237
  self._control_layout.addWidget(self._variable_select)
218
238
  self._control_layout.addWidget(self._colormap_select)
239
+ self._control_layout.addWidget(self._colormap_lower_label)
240
+ self._control_layout.addWidget(self._colormap_lower_field)
241
+ self._control_layout.addWidget(self._colormap_upper_label)
242
+ self._control_layout.addWidget(self._colormap_upper_field)
219
243
 
220
244
  # Graph area
221
245
  self._graph_area = GraphArea()
@@ -256,8 +280,8 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
256
280
  self.update_window_title()
257
281
 
258
282
  # Set controls to correct values
259
- self._colormap_select.setCurrentIndex(self._colormap_select.findText(
260
- self._colormap))
283
+ self._colormap_select.setCurrentIndex(
284
+ self._colormap_select.findText(self._colormap))
261
285
  self._rate_field.setText(str(self._timer_interval))
262
286
  self._timer.setInterval(self._timer_interval)
263
287
 
@@ -297,20 +321,21 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
297
321
  self._timer.start()
298
322
 
299
323
  def action_extract_colormap_image(self):
300
- """
301
- Extracts the current colormap to an image file.
302
- """
324
+ """ Extracts the current colormap to an image file. """
325
+
303
326
  if self._data is None:
304
327
  QtWidgets.QMessageBox.warning(
305
328
  self, TITLE,
306
329
  '<h1>No data to export.</h1>'
307
330
  '<p>Please open a data file first.</p>')
308
331
  return
332
+
309
333
  fname = QtWidgets.QFileDialog.getSaveFileName(
310
334
  self,
311
335
  'Extract colormap to image file',
312
336
  self._path,
313
337
  filter=FILTER_IMG)[0]
338
+
314
339
  if fname:
315
340
  fname = str(fname)
316
341
  ext = os.path.splitext(fname)[1][1:].upper()
@@ -318,22 +343,28 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
318
343
  QtWidgets.QMessageBox.warning(
319
344
  self, TITLE,
320
345
  '<h1>Image type not recognized.</h1>'
321
- '<p>Unknown image type "' + str(ext) + '".</p>')
346
+ f'<p>Unknown image type "{ext}".</p>')
322
347
  return
323
- # Get min and max of data
324
- data = self._data.get2d(self._variable)
325
- lower = np.min(data)
326
- upper = np.max(data)
348
+
327
349
  # Create image
328
350
  nx = 200
329
351
  ny = 800
330
352
  image = myokit.ColorMap.image(self._colormap, nx, ny)
331
353
  image = QtGui.QImage(
332
354
  image, nx, ny, QtGui.QImage.Format.Format_ARGB32)
355
+
356
+ # Add lower and upper bounds
357
+ lower = self._colormap_lower_field.value()
358
+ upper = self._colormap_upper_field.value()
359
+ if lower is None or upper is None:
360
+ data = self._data.get2d(self._variable)
361
+ lower = np.min(data) if lower is None else lower
362
+ upper = np.max(data) if upper is None else upper
333
363
  painter = QtGui.QPainter(image)
334
364
  painter.drawText(10, 15, str(upper))
335
365
  painter.drawText(10, ny - 5, str(lower))
336
366
  painter.end()
367
+
337
368
  # Save
338
369
  image.save(fname, ext)
339
370
 
@@ -379,7 +410,7 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
379
410
  QtWidgets.QMessageBox.warning(
380
411
  self, TITLE,
381
412
  '<h1>Image type not recognized.</h1>'
382
- '<p>Unknown image type "' + str(ext) + '".</p>')
413
+ f'<p>Unknown image type "{ext}".</p>')
383
414
  return
384
415
  nt, ny, nx = self._data.shape()
385
416
  image = self._video_frames[self._video_iframe]
@@ -474,17 +505,23 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
474
505
  def action_set_colormap(self, name):
475
506
  """
476
507
  Loads the ColorMap specified by ``name``.
508
+
509
+ If data is loaded, this will also call :meth:`action_set_variable` to
510
+ update the video frames.
477
511
  """
478
512
  name = str(name)
479
513
  if not myokit.ColorMap.exists(name):
480
514
  return # Silent return?
515
+
481
516
  # Set colormap
482
517
  self._colormap = name
518
+
483
519
  # Update colormap controls
484
- self._colormap_select.setCurrentIndex(self._colormap_select.findText(
485
- self._colormap))
520
+ self._colormap_select.setCurrentIndex(
521
+ self._colormap_select.findText(self._colormap))
522
+
523
+ # Update colorbar
486
524
  if self._data is not None:
487
- # Update colorbar
488
525
  nx = self._colorbar_width
489
526
  ny = self._colorbar_height
490
527
  image = myokit.ColorMap.image(self._colormap, nx, ny)
@@ -497,21 +534,31 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
497
534
  def action_set_variable(self, var):
498
535
  """
499
536
  Loads the variable specified by the name ``var`` into the main display.
537
+
538
+ This method is also responsible for converting the data into frames to
539
+ be displayed on the video widget.
500
540
  """
501
541
  if self._data is None:
502
542
  return
503
543
  self._variable = str(var)
504
- self._variable_select.setCurrentIndex(self._variable_select.findText(
505
- self._variable))
544
+ self._variable_select.setCurrentIndex(
545
+ self._variable_select.findText(self._variable))
506
546
  self.action_pause_timer()
507
547
  self._video_frames = self._data.images(
508
- self._variable, colormap=self._colormap)
548
+ self._variable,
549
+ self._colormap,
550
+ self._colormap_lower_field.value(),
551
+ self._colormap_upper_field.value()
552
+ )
509
553
  self.action_set_frame(self._video_iframe)
510
554
  self.action_depause_timer()
511
555
 
512
556
  def action_set_frame(self, frame):
513
557
  """
514
558
  Move to a specific frame.
559
+
560
+ This method updates the display to show a video frame created by an
561
+ earlier call to :meth:`action_set_variable`.
515
562
  """
516
563
  # Check frame index
517
564
  nt, ny, nx = self._data.shape()
@@ -551,9 +598,7 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
551
598
  self._play_button.setIcon(self._play_icon_play)
552
599
 
553
600
  def closeEvent(self, event=None):
554
- """
555
- Called when window is closed.)
556
- """
601
+ # Called when window is closed.
557
602
  self.save_config()
558
603
  if event:
559
604
  event.accept()
@@ -630,18 +675,14 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
630
675
  self._menu_help.addAction(self._tool_license)
631
676
 
632
677
  def display_exception(self):
633
- """
634
- Displays the last exception.
635
- """
678
+ """ Displays the last exception in a messagebox. """
636
679
  QtWidgets.QMessageBox.warning(
637
680
  self, TITLE,
638
681
  '<h1>An error has occurred.</h1>'
639
- '<pre>' + traceback.format_exc() + '</pre>')
682
+ f'<pre>{traceback.format_exc()}</pre>')
640
683
 
641
684
  def event_colormap_selected(self):
642
- """
643
- Colormap is selected by user.
644
- """
685
+ """ Colormap is selected by user. """
645
686
  self.action_set_colormap(str(self._colormap_select.currentText()))
646
687
 
647
688
  def event_graph_mouse_move(self, x, y):
@@ -649,29 +690,21 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
649
690
  Graph cursur moved: Display the current cursor position on the graph
650
691
  scene in the status bar.
651
692
  """
652
- F = '{:< 1.6g}'
653
- x, y = F.format(x), F.format(y)
654
- self._label_cursor.setText('(' + x + ', ' + y + ')')
693
+ self._label_cursor.setText(f'({x:< 1.6g}, {y:< 1.6g})')
655
694
 
656
695
  def event_rate_changed(self, e=None):
657
- """
658
- User changed frame interval.
659
- """
696
+ """ User changed frame interval. """
660
697
  self._timer_interval = int(self._rate_field.text())
661
698
  self._timer.setInterval(self._timer_interval)
662
699
 
663
700
  def event_variable_selected(self):
664
- """
665
- Variable is selected by user.
666
- """
701
+ """ Variable is selected by user. """
667
702
  self._variable = self._variable_select.currentText()
668
703
  if self._data is not None:
669
704
  self.action_set_variable(self._variable)
670
705
 
671
706
  def event_video_single_click(self, x, y):
672
- """
673
- Video clicked: Add a graph at the location of the click
674
- """
707
+ """ Video clicked: Add a graph at the location of the click. """
675
708
  self._graph_area.graph(self._variable, x, y)
676
709
 
677
710
  def event_video_double_click(self, x, y):
@@ -689,19 +722,16 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
689
722
  # Cursor position is in scene coordinates, so already matches datablock
690
723
  # dimensions!
691
724
  if self._data is not None:
692
- F = '{:< 1.6g}'
693
725
  try:
694
726
  z = self._data.get2d(self._variable)[self._video_iframe, y, x]
695
- z = F.format(z)
727
+ z = '{:< 1.6g}'.format(z)
696
728
  except IndexError:
697
729
  z = '?'
698
- x, y = F.format(x), F.format(y)
699
- self._label_cursor.setText('(' + x + ', ' + y + ', ' + z + ')')
730
+ self._label_cursor.setText(f'({x:< 1.6g}, {y:< 1.6g}, {z}')
700
731
 
701
732
  def keyPressEvent(self, e):
702
- """
703
- Catch key presses?
704
- """
733
+ # A key has been pressed
734
+
705
735
  if e.key() == Qt.Key.Key_Space:
706
736
  self.action_start_stop()
707
737
 
@@ -797,14 +827,16 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
797
827
  try:
798
828
  data = myokit.DataBlock2d.load(fname, progress=reporter)
799
829
  del reporter
800
- except myokit.DataBlockReadError:
830
+ except myokit.DataBlockReadError as e:
801
831
  pd.reset()
802
832
  self.statusBar().showMessage('Load failed.')
803
833
  QtWidgets.QMessageBox.warning(
804
834
  self, TITLE,
805
835
  '<h1>Unable to read file.</h1>'
806
- '<p>The given filename <code>' + str(fname) + '</code>'
807
- ' could not be read as a <code>myokit.DataBlock2d</code>.</p>')
836
+ f'<p>The given filename <code>{fname}</code>'
837
+ ' could not be read as a <code>myokit.DataBlock2d</code>.</p>'
838
+ f'<p>{e}</p>'
839
+ )
808
840
  return
809
841
  except Exception:
810
842
  pd.reset()
@@ -822,7 +854,7 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
822
854
  QtWidgets.QMessageBox.warning(
823
855
  self, TITLE,
824
856
  '<h1>Unable to read file.</h1>'
825
- '<p>The given filename <code>' + str(fname) + '</code>'
857
+ f'<p>The given filename <code>{fname}</code>'
826
858
  ' does not contain any 2d data.</p>')
827
859
  return
828
860
 
@@ -853,8 +885,7 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
853
885
 
854
886
  # Add empty video item to video scene
855
887
  self._video_pixmap = QtGui.QPixmap(nx, ny)
856
- self._video_item = QtWidgets.QGraphicsPixmapItem(
857
- self._video_pixmap)
888
+ self._video_item = QtWidgets.QGraphicsPixmapItem(self._video_pixmap)
858
889
  self._video_scene.addItem(self._video_item)
859
890
 
860
891
  # Update colormap scene
@@ -944,7 +975,7 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
944
975
  """
945
976
  for k, fname in enumerate(self._recent_files):
946
977
  t = self._recent_file_tools[k]
947
- t.setText(str(k + 1) + '. ' + os.path.basename(fname))
978
+ t.setText(f'{k + 1}. {os.path.basename(fname)}')
948
979
  t.setData(fname)
949
980
  t.setVisible(True)
950
981
  for i in range(len(self._recent_files), N_RECENT_FILES):
@@ -954,15 +985,22 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
954
985
  """
955
986
  Sets this window's title based on the current state.
956
987
  """
957
- title = TITLE + ' ' + myokit.__version__
988
+ title = f'{TITLE} {myokit.__version__}'
958
989
  if self._file:
959
- title = os.path.basename(self._file) + ' - ' + title
990
+ title = f'{os.path.basename(self._file)} - {title}'
960
991
  self.setWindowTitle(title)
961
992
 
962
993
 
963
994
  class VideoScene(QtWidgets.QGraphicsScene):
964
995
  """
965
996
  Color data display scene.
997
+
998
+ Note that, despite the name, this item does not manage the conversion from
999
+ data to the image. The actual drawing happens by calling ``setPixmap`` on a
1000
+ ``QGraphicsPixmapItem`` that gets added to this scene.
1001
+
1002
+ See :meth:`DataBlockViewer.action_set_variable()` and
1003
+ :meth:`DataBlockViewer.action_set_frame()`.
966
1004
  """
967
1005
  # Signals
968
1006
  # Somebody moved the mouse
@@ -984,11 +1022,9 @@ class VideoScene(QtWidgets.QGraphicsScene):
984
1022
  self.resize(1, 1)
985
1023
 
986
1024
  def mousePressEvent(self, event):
987
- """
988
- Single-click
989
- """
1025
+ # Single-click event
990
1026
  if event.button() == Qt.MouseButton.LeftButton:
991
- if event.modifiers() == Qt.KeyBoardModifier.NoModifier:
1027
+ if event.modifiers() == Qt.KeyboardModifier.NoModifier:
992
1028
  p = event.scenePos()
993
1029
  x, y = int(p.x()), int(p.y())
994
1030
  if x >= 0 and x < self._w and y >= 0 and y < self._h:
@@ -996,11 +1032,9 @@ class VideoScene(QtWidgets.QGraphicsScene):
996
1032
  return
997
1033
 
998
1034
  def mouseDoubleClickEvent(self, event):
999
- """
1000
- Double-click
1001
- """
1035
+ # Double-click event
1002
1036
  if event.button() == Qt.MouseButton.LeftButton:
1003
- if event.modifiers() == Qt.KeyBoardModifier.NoModifier:
1037
+ if event.modifiers() == Qt.KeyboardModifier.NoModifier:
1004
1038
  p = event.scenePos()
1005
1039
  x, y = int(p.x()), int(p.y())
1006
1040
  if x >= 0 and x < self._w and y >= 0 and y < self._h:
@@ -1008,17 +1042,13 @@ class VideoScene(QtWidgets.QGraphicsScene):
1008
1042
  return
1009
1043
 
1010
1044
  def mouseMoveEvent(self, event):
1011
- """
1012
- Show mouse position in status bar
1013
- """
1045
+ # The mouse has moved!
1014
1046
  p = event.scenePos()
1015
1047
  x, y = int(p.x()), int(p.y())
1016
1048
  self.mouse_moved.emit(x, y)
1017
1049
 
1018
1050
  def resize(self, w, h):
1019
- """
1020
- Resizes the scene to match the given dimensions.
1021
- """
1051
+ """ Resize the scene to match the given dimensions. """
1022
1052
  self._w = float(w)
1023
1053
  self._h = float(h)
1024
1054
  self.setSceneRect(0, 0, self._w, self._h)
@@ -1081,9 +1111,8 @@ class VideoView(QtWidgets.QGraphicsView):
1081
1111
  self.centerOn(rect.center())
1082
1112
 
1083
1113
  def resizeEvent(self, event=None):
1084
- """
1085
- Called when the view is resized.
1086
- """
1114
+ # Called when the view is resized.
1115
+
1087
1116
  # Tell others a resize is happening
1088
1117
  # (This is used to pause the video playback)
1089
1118
  self.resize_event.emit()
@@ -1365,3 +1394,41 @@ class GraphArea(QtWidgets.QWidget):
1365
1394
  Tells Qt that this widget shout expand.
1366
1395
  """
1367
1396
  return QtCore.QSizePolicy.Expanding
1397
+
1398
+
1399
+ class AutoFloatField(QtWidgets.QLineEdit):
1400
+ """
1401
+ A QLineEdit that requires floats as input, but will show "(Auto)" when a
1402
+ non-float is entered and return ``None`` as its value.
1403
+ """
1404
+ def __init__(self, parent=None):
1405
+ super().__init__(parent)
1406
+
1407
+ # Colors
1408
+ self._color_ok = QtGui.QTextCharFormat().foreground().color().getRgb()
1409
+ self._color_auto = (127, 127, 127)
1410
+ self._auto_text = '(Auto)'
1411
+
1412
+ # Show text
1413
+ self.editingFinished.connect(self._autofy)
1414
+ self._autofy()
1415
+
1416
+ def _autofy(self):
1417
+ """ Put (Auto) if not valid """
1418
+ if self.value() is None:
1419
+ self.setText(self._auto_text)
1420
+ c = self._color_auto
1421
+ else:
1422
+ c = self._color_ok
1423
+ self.setStyleSheet(f'color: rgb({c[0]}, {c[1]}, {c[2]})')
1424
+
1425
+ def focusInEvent(self, event):
1426
+ super().focusInEvent(event)
1427
+ QtCore.QTimer.singleShot(0, self.selectAll)
1428
+
1429
+ def value(self):
1430
+ try:
1431
+ return float(super().text())
1432
+ except ValueError:
1433
+ return None
1434
+