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
@@ -625,7 +625,7 @@ class Series(TreeNode, myokit.formats.SweepSource):
625
625
 
626
626
  1. In the individual :class:`Trace` objects. This is somewhat
627
627
  counter-intuitive as some of these properties (e.g.
628
- :meth:Trace.pipette_resistance()`) were set before a series was
628
+ :meth:Trace.r_pipette()`) were set before a series was
629
629
  acquired and do no change between channels or sweeps.
630
630
  2. In the series' :class:`AmplifierState`, which can be accessed via
631
631
  the :meth:`amplifier_state()` method.
@@ -679,6 +679,13 @@ class Series(TreeNode, myokit.formats.SweepSource):
679
679
  'Unexpected amplifier state offset: expecting 472 (information'
680
680
  ' stored with Series) or 0 (information stored in .amp file).')
681
681
 
682
+ # Read user parameters
683
+ # handle.seek(i + 344) # SeSeUserParams1 = 344; (* 4 x LONGREAL *)
684
+ # p = reader.read('dddd')
685
+ # handle.seek(i + 1120) # SeSeUserParams2 = 1120; (* 4x LONGREAL *)
686
+ # p += reader.read('dddd')
687
+ # All zero!
688
+
682
689
  def _read_finalize(self):
683
690
  # See TreeNode._read_finalize
684
691
 
@@ -928,6 +935,7 @@ class Series(TreeNode, myokit.formats.SweepSource):
928
935
  include_da = False
929
936
 
930
937
  # Populate log
938
+ log.set_time_key('time')
931
939
  if join_sweeps:
932
940
  # Join sweeps
933
941
  offsets = (self._sweep_starts_r if use_real_start_times
@@ -964,31 +972,61 @@ class Series(TreeNode, myokit.formats.SweepSource):
964
972
  log.cmeta[name]['unit'] = self._da_units[0]
965
973
 
966
974
  # Add meta data
967
- log.set_time_key('time')
975
+ log.meta['time'] = self._time.strftime(myokit.DATE_FORMAT)
968
976
  a = self.amplifier_state()
977
+ t = self[0][0] if len(self) and len(self[0]) else None
969
978
  log.meta['current_gain_mV_per_pA'] = a.current_gain()
979
+ log.meta['filter1'] = a.filter1_str()
980
+ log.meta['filter2'] = a.filter2_str()
981
+ log.meta['stimulus_filter'] = a.stimulus_filter_str()
970
982
  log.meta['ljp_correction_mV'] = a.ljp()
971
- log.meta['c_slow_compensation_pF'] = a.c_slow()
983
+ log.meta['voltage_offset_mV'] = a.v_off()
984
+ log.meta['holding_potential_mV'] = a.v_hold()
985
+ if t is not None:
986
+ log.meta['r_pipette_MOhm'] = t.r_pipette()
987
+ log.meta['r_seal_MOhm'] = t.r_seal()
972
988
  if a.c_fast_enabled():
973
989
  log.meta['c_fast_compensation_enabled'] = 'true'
974
990
  log.meta['c_fast_pF'] = a.c_fast()
975
- log.meta['c_fast_tau_micro_s'] = a.c_fast_tau()
991
+ log.meta['c_fast_tau_us'] = a.c_fast_tau()
976
992
  else:
977
993
  log.meta['c_fast_compensation_enabled'] = 'false'
994
+ log.meta['c_slow_pF'] = a.c_slow()
995
+ if a.c_slow_enabled():
996
+ log.meta['c_slow_compensation_enabled'] = 'true'
997
+ log.meta['c_slow_range'] = a.c_slow_range()
998
+ css = a.c_slow_auto_settings()
999
+ log.meta['c_slow_auto_amplitude_mV'] = css[0]
1000
+ log.meta['c_slow_auto_cycles'] = css[1]
1001
+ log.meta['c_slow_auto_timeout'] = css[2]
1002
+ else:
1003
+ log.meta['c_slow_compensation_enabled'] = 'false'
978
1004
  log.meta['r_series_MOhm'] = a.r_series()
979
1005
  if a.r_series_enabled():
980
1006
  log.meta['r_series_compensation_enabled'] = 'true'
981
1007
  log.meta['r_series_compensation_percent'] = round(
982
1008
  a.r_series_fraction() * 100, 1)
1009
+ log.meta['r_series_compensation_tau_us'] = a.r_series_tau()
983
1010
  else:
984
1011
  log.meta['r_series_compensation_enabled'] = 'false'
985
- if len(self) and len(self[0]):
986
- t = self[0][0]
987
- log.meta['r_pipette_MOhm'] = t.r_pipette()
988
- log.meta['r_seal_MOhm'] = t.r_series()
989
- log.meta['r_series_post_compensation_MOhm'] = \
990
- t.r_series_remaining()
991
- log.meta['c_slow_pF'] = t.c_slow()
1012
+
1013
+ # Add protocol to meta data
1014
+ stimulus = self.stimulus()
1015
+ stimulus_channel = stimulus.supported_channel()
1016
+ if stimulus_channel is not None:
1017
+ log.meta['amplifier_mode'] = str(stimulus_channel.amplifier_mode())
1018
+ log.meta['protocol'] = stimulus.protocol().code()
1019
+
1020
+ # Add completion status
1021
+ if stimulus_channel is None:
1022
+ c = 'Unknown'
1023
+ elif self.is_complete():
1024
+ c = 'All sweeps ran and completed'
1025
+ elif self._intended_sweep_count == len(self):
1026
+ c = 'Final sweep incomplete'
1027
+ else:
1028
+ c = f'Ran {len(self)} out of {self._intended_sweep_count} sweeps'
1029
+ log.meta['completed'] = c
992
1030
 
993
1031
  return log
994
1032
 
@@ -1003,7 +1041,7 @@ class Series(TreeNode, myokit.formats.SweepSource):
1003
1041
  out.append(f' version {self._file.version()}')
1004
1042
  out.append(f'Recorded on {self._time}')
1005
1043
  out.append(f'{len(self)} sweeps,'
1006
- f' {len(self._channel_names)} channels.')
1044
+ f' {len(self._channel_names)} channels')
1007
1045
 
1008
1046
  # Completion status
1009
1047
  c = self._stimulus.supported_channel()
@@ -1018,62 +1056,79 @@ class Series(TreeNode, myokit.formats.SweepSource):
1018
1056
  out.append(f'Incomplete recording: {len(self)} out of'
1019
1057
  f' {self._intended_sweep_count} ran.')
1020
1058
 
1021
- # Resistance, capacitance, etc.
1059
+ # Info from amplifier state
1022
1060
  a = self.amplifier_state()
1023
1061
  out.append('Information from amplifier state:')
1024
1062
  out.append(f' Current gain: {a.current_gain()} mV/pA')
1063
+ out.append(f' Filter 1: {a.filter1_str()}')
1064
+ out.append(f' Filter 2: {a.filter2_str()}')
1065
+ out.append(f' Stimulus filter: {a.stimulus_filter_str()}')
1066
+ # Voltage info
1067
+ out.append(f' Holding potential: {a.v_hold()} mV')
1025
1068
  if a.ljp():
1026
- out.append(' LJP correction applied using'
1027
- f' LJP={round(a.ljp(), 4)} mV.')
1069
+ out.append(f' LJP correction: {round(a.ljp(), 4)} mV')
1070
+ else:
1071
+ out.append(' LJP correction: no correction')
1072
+ out.append(f' Voltage offset: {a.v_off()} mV')
1073
+ # C fast
1028
1074
  if a.c_fast_enabled():
1029
1075
  out.append(f' C fast compensation: {a.c_fast()} pF,'
1030
- f' {round(a.c_fast_tau(), 4)} us.')
1076
+ f' {round(a.c_fast_tau(), 4)} us')
1031
1077
  else:
1032
- out.append(' C fast compensation: not enabled.')
1033
- out.append(f' C slow compensation: {a.c_slow()} pF.')
1034
- out.append(f' R series: {a.r_series()} MOhm.')
1078
+ out.append(' C fast compensation: not enabled')
1079
+ # C slow
1080
+ if a.c_slow_enabled():
1081
+ out.append(f' C slow compensation: {a.c_slow()} pF')
1082
+ amp, cyc, tim = a.c_slow_auto_settings()
1083
+ out.append(f' C slow auto settings: amplitude {amp} mV,'
1084
+ f' cycles {cyc}, timeout {tim} s')
1085
+ else:
1086
+ out.append(' C slow compensation: not enabled')
1087
+ # Rs comp
1088
+ out.append(f' R series: {a.r_series()} MOhm')
1035
1089
  if a.r_series_enabled():
1036
1090
  p = round(a.r_series_fraction() * 100, 1)
1037
- out.append(f' R series compensation: {p} %.')
1091
+ q = round(a.r_series_tau(), 1)
1092
+ out.append(f' R series compensation: {p} %, {q} us')
1038
1093
  else:
1039
1094
  out.append(' R series compensation: not enabled')
1095
+
1096
+ # Info from first trace
1040
1097
  if len(self) and len(self[0]):
1041
1098
  t = self[0][0]
1042
1099
  out.append('Information from first trace:')
1043
1100
 
1044
- out.append(f' Pipette resistance: {t.r_pipette()} MOhm.')
1045
- out.append(f' Seal resistance: {t.r_seal()} MOhm.')
1046
- out.append(f' Series resistance: {t.r_series()} MOhm.')
1047
- out.append(f' after compensation: {t.r_series_remaining()}'
1048
- f' MOhm.')
1049
- out.append(f' C slow: {t.c_slow()} pF.')
1101
+ out.append(f' Pipette resistance: {t.r_pipette()} MOhm')
1102
+ out.append(f' Seal resistance: {t.r_seal()} MOhm')
1103
+ out.append(f' Series resistance: {t.r_series()} MOhm')
1104
+ out.append(f' C slow: {t.c_slow()} pF')
1050
1105
 
1051
1106
  # Sweeps and channels
1052
1107
  if verbose:
1053
1108
  out.append('-' * 60)
1054
1109
  for i, sweep in enumerate(self):
1055
1110
  out.append(f'Sweep {i}, label: "{sweep.label()}", recorded on'
1056
- f' {sweep.time()}.')
1111
+ f' {sweep.time()}')
1057
1112
  if i == 0:
1058
1113
  for j, trace in enumerate(self[0]):
1059
1114
  out.append(f' Trace {j}, label: "{trace.label()}",'
1060
1115
  f' in {trace.time_unit()} and'
1061
- f' {trace.value_unit()}.')
1116
+ f' {trace.value_unit()}')
1062
1117
 
1063
1118
  # Stimulus
1064
1119
  if verbose:
1065
1120
  stim = self._stimulus
1066
1121
  out.append('-' * 60)
1067
- out.append(f'Stimulus "{stim.label()}".')
1068
- out.append(f' {stim.sweep_count()} sweeps.')
1069
- out.append(f' Delay between sweeps: {stim.sweep_interval()} s.')
1070
- out.append(f' Sampling interval: {stim.sampling_interval()} s.')
1122
+ out.append(f'Stimulus "{stim.label()}"')
1123
+ out.append(f' {stim.sweep_count()} sweeps')
1124
+ out.append(f' Delay between sweeps: {stim.sweep_interval()} s')
1125
+ out.append(f' Sampling interval: {stim.sampling_interval()} s')
1071
1126
  for i, ch in enumerate(stim):
1072
1127
  out.append(f' Channel {i}, in {ch.unit()}, amplifier in'
1073
- f' {ch.amplifier_mode()} mode.')
1074
- out.append(f' Stimulus reconstruction: {ch.support_str()}.')
1128
+ f' {ch.amplifier_mode()} mode')
1129
+ out.append(f' Stimulus reconstruction: {ch.support_str()}')
1075
1130
  z = ch.zero_segment() or '0 (disabled)'
1076
- out.append(f' Zero segment: {z}.')
1131
+ out.append(f' Zero segment: {z}')
1077
1132
  for j, seg in enumerate(ch):
1078
1133
  out.append(f' Segment {j}, {seg.storage()}')
1079
1134
  out.append(f' {seg.segment_class()}, {seg}')
@@ -1274,7 +1329,6 @@ class Trace(TreeNode):
1274
1329
  # Meta data
1275
1330
  self._r_pipette = None
1276
1331
  self._r_seal = None
1277
- self._r_series_comp = None
1278
1332
  self._g_series = None
1279
1333
  self._c_slow = None
1280
1334
 
@@ -1329,8 +1383,8 @@ class Trace(TreeNode):
1329
1383
  self._c_slow = reader.read1('d')
1330
1384
  handle.seek(i + 184) # TrGSeries = 184; (* LONGREAL *)
1331
1385
  self._g_series = reader.read1('d')
1332
- handle.seek(i + 192) # TrRsValue = 192; (* LONGREAL *)
1333
- self._r_series_comp = reader.read1('d')
1386
+ #handle.seek(i + 192) # TrRsValue = 192; (* LONGREAL *)
1387
+ #self._r_series_comp = reader.read1('d')
1334
1388
 
1335
1389
  # Convert unit
1336
1390
  self._data_unit = myokit.parse_unit(self._data_unit)
@@ -1359,17 +1413,27 @@ class Trace(TreeNode):
1359
1413
 
1360
1414
  def r_seal(self):
1361
1415
  """
1362
- Returns the "seal resistance" (MOhm) determined in the waiting time
1363
- before the trace was acquired.
1416
+ Returns the "seal resistance" (MOhm) that was determined the last time
1417
+ that the "amplifier window" was active (so at some indeterminate time
1418
+ before the Trace was aquired).
1419
+
1420
+ The values returned by :meth:`r_seal` and :meth:`r_pipette` are the
1421
+ same measurement ("R-memb") performed at a different time. The value
1422
+ ``r_seal`` is updated whenever the "amplifier window" is active. Using
1423
+ a button or a programmed command, the same value can be stored as
1424
+ ``r_pipette``, which should be done before breaking the seal.
1364
1425
 
1365
- This is equal to the value "R-memb" on the display. If a test pulse is
1366
- being used, it is calculated as dV/dI where dV and dI are the
1367
- differences in (command) voltage and current before and during the
1368
- pulse. If no test pulse is used it is simply the ratio between the V
1369
- and I measurements.
1426
+ "R-memb" is determined either using a test pulse or from the current
1427
+ V and I values. If a test pulse is used (the default), this is
1428
+ specified as a ``dV`` from the holding potential, and
1429
+ ``R-memb = dV / dI`` where ``dV`` is the difference in command
1430
+ potential and ``dI`` is the measured difference in current (I during
1431
+ the step minus I at holding potential).
1370
1432
 
1371
- This is the same measurement as :meth:`r_pipette`, but logged
1372
- automatically before each trace.
1433
+ Users should be careful when interpreting this value, as it depends on
1434
+ (1) the last time that the amplifier window was active, (2) the test
1435
+ pulse settings, (3) the holding potential, (4) any currents active at
1436
+ the holding potential or during the step.
1373
1437
  """
1374
1438
  return self._r_seal * 1e-6
1375
1439
 
@@ -1380,24 +1444,14 @@ class Trace(TreeNode):
1380
1444
  """
1381
1445
  return 1e-6 / self._g_series
1382
1446
 
1383
- def r_series_remaining(self):
1384
- """
1385
- Returns the series resistance (MOhm) remaining after compensation.
1386
- """
1387
- # "Absolute fraction of the compensated R-series value. The value
1388
- # depends on the % of R-series compensation."
1389
- return (1 / self._g_series - self._r_series_comp) * 1e-6
1390
-
1391
1447
  def r_pipette(self):
1392
1448
  """
1393
- Returns the pipette resistance (MOhm) determined from the test pulse
1394
- before breaking the seal.
1449
+ Returns the pipette resistance (MOhm) stored with this Trace, but
1450
+ calculated at an earlier point.
1395
1451
 
1396
- This is equal to the value "R-memb" on the display, but logged when a
1397
- "R-memb to R-pip" button was pressed (or called programmatically). It
1398
- uses the same measurement as :meth:`r_seal`, but logged at a different
1399
- time. The intended use is to store the resistance of the pipette tip
1400
- before touching a cell.
1452
+ Like the "seal resistance", the "pipette resistance" is a stored
1453
+ measurement of what patchmaster calls "R-memb". For details, see
1454
+ :meth:`r_seal`.
1401
1455
  """
1402
1456
  return self._r_pipette * 1e-6
1403
1457
 
@@ -1572,13 +1626,6 @@ class AmplifierState:
1572
1626
  """
1573
1627
  Describes the state of an amplifier used by PatchMaster.
1574
1628
  """
1575
- _filter1_options = [
1576
- 'Bessel 100 kHz',
1577
- 'Bessel 30 kHz',
1578
- 'Bessel 10 kHz',
1579
- 'HQ 30 kHz',
1580
- ]
1581
-
1582
1629
  def __init__(self, handle, reader):
1583
1630
 
1584
1631
  # Read properties
@@ -1588,6 +1635,14 @@ class AmplifierState:
1588
1635
  handle.seek(i + 8) # sCurrentGain = 8; (* LONGREAL *)
1589
1636
  self._current_gain = reader.read1('d')
1590
1637
 
1638
+ # Holding potential and offsets
1639
+ handle.seek(i + 112) # sVHold = 112; (* LONGREAL *)
1640
+ self._holding = reader.read1('d')
1641
+ handle.seek(i + 128) # sVpOffset = 128; (* LONGREAL *)
1642
+ self._voff = reader.read1('d')
1643
+ handle.seek(i + 136) # sVLiquidJunction = 136; (* LONGREAL *)
1644
+ self._ljp = reader.read1('d')
1645
+
1591
1646
  # Series resistance and compensation
1592
1647
  handle.seek(i + 40) # sRsFraction = 40; (* LONGREAL *)
1593
1648
  self._rs_fraction = reader.read1('d')
@@ -1605,12 +1660,15 @@ class AmplifierState:
1605
1660
  self._cf_amp2 = reader.read1('d')
1606
1661
  handle.seek(i + 72) # sCFastTau = 72; (* LONGREAL *)
1607
1662
  self._cf_tau = reader.read1('d')
1608
- handle.seek(i + 285) # sCCCFastOn = 285; (* BYTE *)
1609
- self._cf_enabled = bool(reader.read1('b'))
1663
+ #handle.seek(i + 285) # sCCCFastOn = 285; (* BYTE *)
1664
+ #self._cf_enabled = bool(reader.read1('b'))
1610
1665
 
1611
1666
  # Slow capacitance correction
1612
1667
  handle.seek(i + 80) # sCSlow = 80; (* LONGREAL *)
1613
1668
  self._cs = reader.read1('d')
1669
+ handle.seek(i + 241) # sCSlowRange = 241; (* BYTE *)
1670
+ self._cs_range = CSlowRange(reader.read1('b'))
1671
+
1614
1672
  # Auto CSlow settings. See 4.9 "EPC 10 USB Menu" in the manual.
1615
1673
  handle.seek(i + 152) # sCSlowStimVolts = 152; (* LONGREAL *)
1616
1674
  self._cs_auto_stim = reader.read1('d')
@@ -1619,71 +1677,78 @@ class AmplifierState:
1619
1677
  handle.seek(i + 210) # sCSlowCycles = 210; (* INT16 *)
1620
1678
  self._cs_auto_cycles = reader.read1('h')
1621
1679
 
1622
- # Voltage offsets
1623
- handle.seek(i + 128) # sVpOffset = 128; (* LONGREAL *)
1624
- self._voff = reader.read1('d')
1625
- handle.seek(i + 136) # sVLiquidJunction = 136; (* LONGREAL *)
1626
- self._ljp = reader.read1('d')
1627
-
1628
1680
  # Filter 1
1681
+ # Set with a byte that controls type and frequency
1629
1682
  handle.seek(i + 281) # sFilter1 = 281; (* BYTE *)
1630
- self._filter1 = reader.read1('b')
1683
+ self._filter1 = Filter1Setting(reader.read1('b'))
1684
+
1685
+ # Filter 2
1686
+ # Set with a type and a separate frequency. The user setting for Filter
1687
+ # 2 is the combined bandwidth of Filter 1 and Filter 2.
1688
+ handle.seek(i + 239) # sF2Response = 239; (* BYTE *)
1689
+ self._filter2_type = Filter2Type(reader.read1('b'))
1690
+ handle.seek(i + 24) # sF2Frequency = 24; (* LONGREAL *)
1691
+ self._filter2_freq_solo = reader.read1('d')
1692
+ handle.seek(i + 16) # sF2Bandwidth = 16; (* LONGREAL *)
1693
+ self._filter2_freq_both = reader.read1('d')
1694
+
1695
+ # Suspect this indicates external filter 2
1696
+ #handle.seek(i + 287) # sF2Source = 287; (* BYTE *)
1697
+ #self._temp['sF2Source'] = reader.read1('b')
1698
+ # Don't know what Mode does.
1699
+ #handle.seek(i + 231) # sF2Mode = 231; (* BYTE *)
1700
+ #self._temp['sF2Mode'] = reader.read1('b')
1631
1701
 
1632
- #TODO: Add these are proper properties with a docstring'd method that
1633
- # returns them and says what the units are etc. or stop reading them.
1634
1702
  # self._temp = {}
1635
- # handle.seek(i + 264) # sImon1Bandwidth = 264; (* LONGREAL *)
1636
- # self._temp['sImon1Bandwidth'] = reader.read1('d')
1637
-
1638
- # handle.seek(i + 16) # sF2Bandwidth = 16; (* LONGREAL *)
1639
- # self._temp['sF2Bandwidth'] = reader.read1('d')
1640
- # handle.seek(i + 24) # sF2Frequency = 24; (* LONGREAL *)
1641
- # self._temp['sF2Frequency'] = reader.read1('d')
1642
- # handle.seek(i + 230) # sHasF2Bypass = 230; (* BYTE *)
1643
- # self._temp['sHasF2Bypass'] = reader.read1('b')
1644
- # handle.seek(i + 231) # sF2Mode = 231; (* BYTE *)
1645
- # self._temp['sF2Mode'] = reader.read1('b')
1646
- # handle.seek(i + 239) # sF2Response = 239; (* BYTE *)
1647
- # self._temp['sF2Response'] = reader.read1('b')
1648
- # handle.seek(i + 287) # sF2Source = 287; (* BYTE *)
1649
- # self._temp['sF2Source'] = reader.read1('b')
1650
-
1651
1703
  # handle.seek(i + 296) # sStimFilterHz = 296; (* LONGREAL *)
1652
- # self._temp['sStimFilterHz'] = reader.read1('d')
1704
+ # print('STIM', reader.read1('d'))
1653
1705
  # handle.seek(i + 320) # sInputFilterTau = 320; (* LONGREAL *)
1706
+ # print('InputFilterTau', reader.read1('d'))
1654
1707
  # self._temp['sInputFilterTau'] = reader.read1('d')
1655
1708
  # handle.seek(i + 328) # sOutputFilterTau = 328; (* LONGREAL *)
1709
+ # print('OutputFilterTau', reader.read1('d'))
1656
1710
  # self._temp['sOutputFilterTau'] = reader.read1('d')
1657
-
1658
1711
  # handle.seek(i + 384) # sVmonFiltBandwidth = 384; (* LONGREAL *)
1659
1712
  # self._temp['sVmonFiltBandwidth'] = reader.read1('d')
1660
1713
  # handle.seek(i + 392) # sVmonFiltFrequency = 392; (* LONGREAL *)
1661
1714
  # self._temp['sVmonFiltFrequency'] = reader.read1('d')
1715
+ # handle.seek(i + 264) # sImon1Bandwidth = 264; (* LONGREAL *)
1716
+ # self._temp['sImon1Bandwidth'] = reader.read1('d')
1662
1717
 
1663
- # handle.seek(i + 112) # sVHold = 112; (* LONGREAL *)
1664
- # self._temp['sVHold'] = reader.read1('d')
1665
- # handle.seek(i + 120) # sLastVHold = 120; (* LONGREAL *)
1666
- # self._temp['sLastVHold'] = reader.read1('d')
1718
+ # Stimulus filter
1719
+ handle.seek(i + 282) # sStimFilterOn = 282; (* BYTE *)
1720
+ self._stimulus_filter = StimulusFilterSetting(reader.read1('b'))
1667
1721
 
1668
1722
  def c_fast(self):
1669
1723
  """
1670
- Return the capacitance (pF) used in fast capacitance correction
1671
- (CFast2).
1724
+ Return the capacitance (pF) used in fast capacitance correction.
1725
+
1726
+ HEKA amplifiers use a two-component fast capacitance cancellation, with
1727
+ an instantaneous part (component 1) and a delayed part (component 2).
1728
+
1729
+ The "total" capacitance, e.g. the sum of components 1 and 2 is
1730
+ returned.
1672
1731
  """
1673
- # Not sure why there are two. They are almost identical. Older EPC9
1674
- # manual has a Fast1 and Fast2 as well, but only for GET, for SET there
1675
- # is only 2. So... going with that for now
1676
- return self._cf_amp2 * 1e12
1732
+ # The total fast capacitance correction is a sum of two capacitances.
1733
+ # See the EPC-10 manual for details.
1734
+ return (self._cf_amp1 + self._cf_amp2) * 1e12
1677
1735
 
1678
1736
  def c_fast_tau(self):
1679
1737
  """
1680
- Returns the time constant (us) used in fast capacitance correction.
1738
+ Returns the time constant (in microseconds) used in fast capacitance
1739
+ correction.
1740
+
1741
+ This is the time constant for the non-instantaneous component of the
1742
+ cancellation, see :meth:`c_fast`.
1681
1743
  """
1682
1744
  return self._cf_tau * 1e6
1683
1745
 
1684
1746
  def c_fast_enabled(self):
1685
- """ Returns ``True`` if fast capacitance compensation was enabled. """
1686
- return self._cf_enabled
1747
+ """
1748
+ Returns ``True`` if the fast capacitance compensation is set to a
1749
+ non-zero capacitance.
1750
+ """
1751
+ return (self._cf_amp1 + self._cf_amp2) > 0
1687
1752
 
1688
1753
  def c_slow(self):
1689
1754
  """
@@ -1701,6 +1766,18 @@ class AmplifierState:
1701
1766
  return (self._cs_auto_stim * 1e3, self._cs_auto_cycles,
1702
1767
  self._cs_auto_timeout)
1703
1768
 
1769
+ def c_slow_enabled(self):
1770
+ """
1771
+ Returns ``True`` if the C-slow range is not set to 'Off'.
1772
+ """
1773
+ return self._cs_range is not CSlowRange.OFF
1774
+
1775
+ def c_slow_range(self):
1776
+ """
1777
+ Returns the :class:`CSlowRange` used for slow capacitance correction.
1778
+ """
1779
+ return self._cs_range
1780
+
1704
1781
  def current_gain(self):
1705
1782
  """
1706
1783
  The gain setting for current measurements, in mV/pA.
@@ -1709,9 +1786,50 @@ class AmplifierState:
1709
1786
 
1710
1787
  def filter1(self):
1711
1788
  """
1712
- Returns a string describing the used (always-on) analog Filter 1.
1789
+ Returns a :class:`Filter1Setting` describing filter 1.
1790
+
1791
+ For more information on Filter 1 and 2, see :class:`Filter1Setting`.
1792
+ """
1793
+ return self._filter1
1794
+
1795
+ def filter1_str(self):
1713
1796
  """
1714
- return self._filter1_options[self._filter1]
1797
+ Returns a string representing the Filter 1 settings.
1798
+
1799
+ For more information on Filter 1 and 2, see :class:`Filter1Setting`.
1800
+ """
1801
+ return str(self._filter1)
1802
+
1803
+ def filter2(self):
1804
+ """
1805
+ Returns a tuple ``(type, f_both, f_solo)`` describing the Filter 2
1806
+ settings, where ``type`` is a :class:`Filter2Type`, where ``f_both`` is
1807
+ the frequency in Hz of both filters combined, and when ``f_solo`` is
1808
+ the frequency of Filter 2 alone.
1809
+
1810
+ The frequency shown in PatchMaster is that of both filters combined.
1811
+ If the "bypass" filter is selected, the frequency settings are unused.
1812
+
1813
+ For more information on Filter 1 and 2, see :class:`Filter1Setting`.
1814
+ """
1815
+ return (
1816
+ self._filter2_type,
1817
+ self._filter2_freq_both,
1818
+ self._filter2_freq_solo,
1819
+ )
1820
+
1821
+ def filter2_str(self):
1822
+ """
1823
+ Returns a string describing Filter 2.
1824
+
1825
+ For more information on Filter 1 and 2, see :class:`Filter1Setting`.
1826
+ """
1827
+ if self._filter2_type is Filter2Type.BYPASS:
1828
+ return str(self._filter2_type)
1829
+ fb = round(self._filter2_freq_both * 1e-3, 2)
1830
+ fs = round(self._filter2_freq_solo * 1e-3, 2)
1831
+ fb = int(fb) if fb == int(fb) else fb
1832
+ return f'{self._filter2_type} {fb} kHz combined, {fs} kHz f2-only'
1715
1833
 
1716
1834
  def ljp(self):
1717
1835
  """
@@ -1757,12 +1875,122 @@ class AmplifierState:
1757
1875
  """
1758
1876
  return self._rs_tau * 1e6 if self._rs_enabled else 0
1759
1877
 
1760
- def v_off(self):
1878
+ def stimulus_filter(self):
1879
+ """
1880
+ Returns a :class:`StimulusFilterSetting` descibing the stimulus filter
1881
+ settings.
1882
+
1883
+ For more information, see :class:`StimulusFilterSetting`.
1884
+ """
1885
+ return self._stimulus_filter
1886
+
1887
+ def stimulus_filter_str(self):
1761
1888
  """
1762
- Returns the used voltage offset (in mV), also called V0.
1889
+ Returns a string descibing the stimulus filter settings.
1890
+
1891
+ For more information, see :class:`StimulusFilterSetting`.
1763
1892
  """
1893
+ return self._stimulus_filter
1894
+
1895
+ def v_off(self):
1896
+ """ Returns the used voltage offset (in mV), also called V0. """
1764
1897
  return self._voff * 1e3
1765
1898
 
1899
+ def v_hold(self):
1900
+ """ Returns the holding potential (in mV). """
1901
+ return self._holding * 1e3
1902
+
1903
+
1904
+ class Filter1Setting(enum.Enum):
1905
+ """
1906
+ Settings for filter 1, which is applied before filter 2.
1907
+
1908
+ Both filter 1 and filter 2 are hardware filters, implemented on the EPC 9
1909
+ and 10. Filter 1 is used in voltage control, while filter 2 is used to
1910
+ perform filtering before digitisation. Filter 1 is always on, but some
1911
+ amplifiers allow filter 2 to be bypassed. The setting for filter 1
1912
+ determines both the type of filter (Bessel etc.) and the bandwidth. The
1913
+ user setting for filter 2 sets the combined bandwidth of both filters.
1914
+
1915
+ Measurements that passed only through filter 1 can be obtained from
1916
+ Imon1, while Imon2 provides output passed through both filters.
1917
+ """
1918
+ BESSEL_100K = 0
1919
+ BESSEL_30K = 1
1920
+ BESSEL_10K = 2
1921
+ HQ_30K = 3
1922
+
1923
+ def __str__(self):
1924
+ if self is Filter1Setting.BESSEL_100K:
1925
+ return 'Bessel 100 kHz'
1926
+ elif self is Filter1Setting.BESSEL_30K:
1927
+ return 'Bessel 30 kHz'
1928
+ elif self is Filter1Setting.BESSEL_10K:
1929
+ return 'Bessel 10 kHz'
1930
+ else:
1931
+ return 'HQ 30 kHz'
1932
+
1933
+
1934
+ class Filter2Type(enum.Enum):
1935
+ """
1936
+ Setting for filter 2, which is applied after filter 1.
1937
+
1938
+ Unlike Filter 1, this filter can be disabled, and the frequency is set
1939
+ separately.
1940
+
1941
+ For more information on the filters, see :class:`Filter1Setting`.
1942
+ """
1943
+ BESSEL = 0
1944
+ BUTTERWORTH = 1
1945
+ BYPASS = 2
1946
+ #V_BESSEL = 3 Maybe!
1947
+ #V_Butterworth = 4 Maybe!
1948
+
1949
+ def __str__(self):
1950
+ if self is Filter2Type.BESSEL:
1951
+ return 'Bessel'
1952
+ elif self is Filter2Type.BUTTERWORTH:
1953
+ return 'Butterworth'
1954
+ else:
1955
+ return 'Bypass'
1956
+
1957
+
1958
+ class CSlowRange(enum.Enum):
1959
+ """ Available options for slow capacitance cancelling range. """
1960
+ OFF = 0
1961
+ pF30 = 1
1962
+ pF100 = 2
1963
+ pF1000 = 3
1964
+
1965
+ def __str__(self):
1966
+ if self is CSlowRange.OFF:
1967
+ return 'Off'
1968
+ elif self is CSlowRange.pF30:
1969
+ return '30 pF'
1970
+ elif self is CSlowRange.pF100:
1971
+ return '100 pF'
1972
+ else:
1973
+ return '1000 pF'
1974
+
1975
+
1976
+ class StimulusFilterSetting(enum.Enum):
1977
+ """
1978
+ Setting for the stimulus filter.
1979
+
1980
+
1981
+ The stimulus filter is a 2-pole Bessel filter applied over the stimulus
1982
+ signal to reduce fast capacitative currents. It is applied to voltages, the
1983
+ manual is less clear whether it is applied to currents too.
1984
+ """
1985
+ BW2 = 0
1986
+ BW20 = 1
1987
+
1988
+ def __str__(self):
1989
+ if self is StimulusFilterSetting.BW2:
1990
+ return 'Bessel 2 us (off)'
1991
+ else:
1992
+ return 'Bessel 20 us (on)'
1993
+
1766
1994
 
1767
1995
  #
1768
1996
  # From StimFile_v9.txt
@@ -1879,6 +2107,8 @@ class Stimulus(TreeNode):
1879
2107
  handle.seek(start + 144) # stNumberSweeps = 144; (* INT32 *)
1880
2108
  self._sweep_count = reader.read1('i')
1881
2109
 
2110
+ # Filterfactor: Determines desired filtering frequency as a function of
2111
+ # sampling rate. Is overruled by autofilter when enabled.
1882
2112
  # handle.seek(start + 136) # stFilterFactor = 136; (* LONGREAL *)
1883
2113
  # self._filter_factor = reader.read1('d')
1884
2114