myokit 1.37.0__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 (35) hide show
  1. myokit/_aux.py +4 -0
  2. myokit/_datablock.py +10 -10
  3. myokit/_datalog.py +40 -5
  4. myokit/_myokit_version.py +1 -1
  5. myokit/formats/axon/_abf.py +11 -4
  6. myokit/formats/diffsl/__init__.py +60 -0
  7. myokit/formats/diffsl/_ewriter.py +145 -0
  8. myokit/formats/diffsl/_exporter.py +435 -0
  9. myokit/formats/heka/_patchmaster.py +337 -113
  10. myokit/gui/datalog_viewer.py +17 -3
  11. myokit/tests/data/io/bad1d-2-no-header.zip +0 -0
  12. myokit/tests/data/io/bad1d-3-no-data.zip +0 -0
  13. myokit/tests/data/io/bad1d-4-not-a-zip.zip +1 -105
  14. myokit/tests/data/io/bad1d-5-bad-data-type.zip +0 -0
  15. myokit/tests/data/io/bad1d-6-time-too-short.zip +0 -0
  16. myokit/tests/data/io/bad1d-7-0d-too-short.zip +0 -0
  17. myokit/tests/data/io/bad1d-8-1d-too-short.zip +0 -0
  18. myokit/tests/data/io/bad2d-2-no-header.zip +0 -0
  19. myokit/tests/data/io/bad2d-3-no-data.zip +0 -0
  20. myokit/tests/data/io/bad2d-4-not-a-zip.zip +1 -105
  21. myokit/tests/data/io/bad2d-5-bad-data-type.zip +0 -0
  22. myokit/tests/data/io/bad2d-8-2d-too-short.zip +0 -0
  23. myokit/tests/data/io/block1d.mmt +187 -0
  24. myokit/tests/data/io/datalog-18-duplicate-keys.csv +4 -0
  25. myokit/tests/test_aux.py +4 -0
  26. myokit/tests/test_datablock.py +6 -6
  27. myokit/tests/test_datalog.py +20 -0
  28. myokit/tests/test_formats_diffsl.py +728 -0
  29. myokit/tests/test_formats_exporters_run.py +3 -0
  30. {myokit-1.37.0.dist-info → myokit-1.37.1.dist-info}/METADATA +1 -1
  31. {myokit-1.37.0.dist-info → myokit-1.37.1.dist-info}/RECORD +35 -29
  32. {myokit-1.37.0.dist-info → myokit-1.37.1.dist-info}/LICENSE.txt +0 -0
  33. {myokit-1.37.0.dist-info → myokit-1.37.1.dist-info}/WHEEL +0 -0
  34. {myokit-1.37.0.dist-info → myokit-1.37.1.dist-info}/entry_points.txt +0 -0
  35. {myokit-1.37.0.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
 
@@ -969,12 +976,15 @@ class Series(TreeNode, myokit.formats.SweepSource):
969
976
  a = self.amplifier_state()
970
977
  t = self[0][0] if len(self) and len(self[0]) else None
971
978
  log.meta['current_gain_mV_per_pA'] = a.current_gain()
972
- log.meta['filter1'] = a.filter1()
979
+ log.meta['filter1'] = a.filter1_str()
980
+ log.meta['filter2'] = a.filter2_str()
981
+ log.meta['stimulus_filter'] = a.stimulus_filter_str()
982
+ log.meta['ljp_correction_mV'] = a.ljp()
983
+ log.meta['voltage_offset_mV'] = a.v_off()
984
+ log.meta['holding_potential_mV'] = a.v_hold()
973
985
  if t is not None:
974
986
  log.meta['r_pipette_MOhm'] = t.r_pipette()
975
987
  log.meta['r_seal_MOhm'] = t.r_seal()
976
- log.meta['ljp_correction_mV'] = a.ljp()
977
- log.meta['voltage_offset_mV'] = a.v_off()
978
988
  if a.c_fast_enabled():
979
989
  log.meta['c_fast_compensation_enabled'] = 'true'
980
990
  log.meta['c_fast_pF'] = a.c_fast()
@@ -982,18 +992,42 @@ class Series(TreeNode, myokit.formats.SweepSource):
982
992
  else:
983
993
  log.meta['c_fast_compensation_enabled'] = 'false'
984
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'
985
1004
  log.meta['r_series_MOhm'] = a.r_series()
986
1005
  if a.r_series_enabled():
987
1006
  log.meta['r_series_compensation_enabled'] = 'true'
988
1007
  log.meta['r_series_compensation_percent'] = round(
989
1008
  a.r_series_fraction() * 100, 1)
990
1009
  log.meta['r_series_compensation_tau_us'] = a.r_series_tau()
991
- if t is not None:
992
- log.meta['r_series_post_compensation_MOhm'] = \
993
- t.r_series_remaining()
994
1010
  else:
995
1011
  log.meta['r_series_compensation_enabled'] = 'false'
996
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
1030
+
997
1031
  return log
998
1032
 
999
1033
  def meta_str(self, verbose=False):
@@ -1007,7 +1041,7 @@ class Series(TreeNode, myokit.formats.SweepSource):
1007
1041
  out.append(f' version {self._file.version()}')
1008
1042
  out.append(f'Recorded on {self._time}')
1009
1043
  out.append(f'{len(self)} sweeps,'
1010
- f' {len(self._channel_names)} channels.')
1044
+ f' {len(self._channel_names)} channels')
1011
1045
 
1012
1046
  # Completion status
1013
1047
  c = self._stimulus.supported_channel()
@@ -1022,64 +1056,79 @@ class Series(TreeNode, myokit.formats.SweepSource):
1022
1056
  out.append(f'Incomplete recording: {len(self)} out of'
1023
1057
  f' {self._intended_sweep_count} ran.')
1024
1058
 
1025
- # Resistance, capacitance, etc.
1059
+ # Info from amplifier state
1026
1060
  a = self.amplifier_state()
1027
1061
  out.append('Information from amplifier state:')
1028
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')
1029
1068
  if a.ljp():
1030
- out.append(' LJP correction applied using'
1031
- 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
1032
1074
  if a.c_fast_enabled():
1033
1075
  out.append(f' C fast compensation: {a.c_fast()} pF,'
1034
- f' {round(a.c_fast_tau(), 4)} us.')
1076
+ f' {round(a.c_fast_tau(), 4)} us')
1077
+ else:
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')
1035
1085
  else:
1036
- out.append(' C fast compensation: not enabled.')
1037
- out.append(f' C slow compensation: {a.c_slow()} pF.')
1038
- out.append(f' R series: {a.r_series()} MOhm.')
1086
+ out.append(' C slow compensation: not enabled')
1087
+ # Rs comp
1088
+ out.append(f' R series: {a.r_series()} MOhm')
1039
1089
  if a.r_series_enabled():
1040
1090
  p = round(a.r_series_fraction() * 100, 1)
1041
- out.append(f' R series compensation: {p} %.')
1042
- p = round(a.r_series_tau(), 1)
1043
- out.append(f' R series compensation tau: {p} us')
1091
+ q = round(a.r_series_tau(), 1)
1092
+ out.append(f' R series compensation: {p} %, {q} us')
1044
1093
  else:
1045
1094
  out.append(' R series compensation: not enabled')
1095
+
1096
+ # Info from first trace
1046
1097
  if len(self) and len(self[0]):
1047
1098
  t = self[0][0]
1048
1099
  out.append('Information from first trace:')
1049
1100
 
1050
- out.append(f' Pipette resistance: {t.r_pipette()} MOhm.')
1051
- out.append(f' Seal resistance: {t.r_seal()} MOhm.')
1052
- out.append(f' Series resistance: {t.r_series()} MOhm.')
1053
- out.append(f' after compensation: {t.r_series_remaining()}'
1054
- f' MOhm.')
1055
- 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')
1056
1105
 
1057
1106
  # Sweeps and channels
1058
1107
  if verbose:
1059
1108
  out.append('-' * 60)
1060
1109
  for i, sweep in enumerate(self):
1061
1110
  out.append(f'Sweep {i}, label: "{sweep.label()}", recorded on'
1062
- f' {sweep.time()}.')
1111
+ f' {sweep.time()}')
1063
1112
  if i == 0:
1064
1113
  for j, trace in enumerate(self[0]):
1065
1114
  out.append(f' Trace {j}, label: "{trace.label()}",'
1066
1115
  f' in {trace.time_unit()} and'
1067
- f' {trace.value_unit()}.')
1116
+ f' {trace.value_unit()}')
1068
1117
 
1069
1118
  # Stimulus
1070
1119
  if verbose:
1071
1120
  stim = self._stimulus
1072
1121
  out.append('-' * 60)
1073
- out.append(f'Stimulus "{stim.label()}".')
1074
- out.append(f' {stim.sweep_count()} sweeps.')
1075
- out.append(f' Delay between sweeps: {stim.sweep_interval()} s.')
1076
- 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')
1077
1126
  for i, ch in enumerate(stim):
1078
1127
  out.append(f' Channel {i}, in {ch.unit()}, amplifier in'
1079
- f' {ch.amplifier_mode()} mode.')
1080
- out.append(f' Stimulus reconstruction: {ch.support_str()}.')
1128
+ f' {ch.amplifier_mode()} mode')
1129
+ out.append(f' Stimulus reconstruction: {ch.support_str()}')
1081
1130
  z = ch.zero_segment() or '0 (disabled)'
1082
- out.append(f' Zero segment: {z}.')
1131
+ out.append(f' Zero segment: {z}')
1083
1132
  for j, seg in enumerate(ch):
1084
1133
  out.append(f' Segment {j}, {seg.storage()}')
1085
1134
  out.append(f' {seg.segment_class()}, {seg}')
@@ -1280,7 +1329,6 @@ class Trace(TreeNode):
1280
1329
  # Meta data
1281
1330
  self._r_pipette = None
1282
1331
  self._r_seal = None
1283
- self._r_series_comp = None
1284
1332
  self._g_series = None
1285
1333
  self._c_slow = None
1286
1334
 
@@ -1335,8 +1383,8 @@ class Trace(TreeNode):
1335
1383
  self._c_slow = reader.read1('d')
1336
1384
  handle.seek(i + 184) # TrGSeries = 184; (* LONGREAL *)
1337
1385
  self._g_series = reader.read1('d')
1338
- handle.seek(i + 192) # TrRsValue = 192; (* LONGREAL *)
1339
- self._r_series_comp = reader.read1('d')
1386
+ #handle.seek(i + 192) # TrRsValue = 192; (* LONGREAL *)
1387
+ #self._r_series_comp = reader.read1('d')
1340
1388
 
1341
1389
  # Convert unit
1342
1390
  self._data_unit = myokit.parse_unit(self._data_unit)
@@ -1365,17 +1413,27 @@ class Trace(TreeNode):
1365
1413
 
1366
1414
  def r_seal(self):
1367
1415
  """
1368
- Returns the "seal resistance" (MOhm) determined in the waiting time
1369
- 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.
1370
1425
 
1371
- This is equal to the value "R-memb" on the display. If a test pulse is
1372
- being used, it is calculated as dV/dI where dV and dI are the
1373
- differences in (command) voltage and current before and during the
1374
- pulse. If no test pulse is used it is simply the ratio between the V
1375
- 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).
1376
1432
 
1377
- This is the same measurement as :meth:`r_pipette`, but logged
1378
- 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.
1379
1437
  """
1380
1438
  return self._r_seal * 1e-6
1381
1439
 
@@ -1386,24 +1444,14 @@ class Trace(TreeNode):
1386
1444
  """
1387
1445
  return 1e-6 / self._g_series
1388
1446
 
1389
- def r_series_remaining(self):
1390
- """
1391
- Returns the series resistance (MOhm) remaining after compensation.
1392
- """
1393
- # "Absolute fraction of the compensated R-series value. The value
1394
- # depends on the % of R-series compensation."
1395
- return (1 / self._g_series - self._r_series_comp) * 1e-6
1396
-
1397
1447
  def r_pipette(self):
1398
1448
  """
1399
- Returns the pipette resistance (MOhm) determined from the test pulse
1400
- before breaking the seal.
1449
+ Returns the pipette resistance (MOhm) stored with this Trace, but
1450
+ calculated at an earlier point.
1401
1451
 
1402
- This is equal to the value "R-memb" on the display, but logged when a
1403
- "R-memb to R-pip" button was pressed (or called programmatically). It
1404
- uses the same measurement as :meth:`r_seal`, but logged at a different
1405
- time. The intended use is to store the resistance of the pipette tip
1406
- 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`.
1407
1455
  """
1408
1456
  return self._r_pipette * 1e-6
1409
1457
 
@@ -1578,13 +1626,6 @@ class AmplifierState:
1578
1626
  """
1579
1627
  Describes the state of an amplifier used by PatchMaster.
1580
1628
  """
1581
- _filter1_options = [
1582
- 'Bessel 100 kHz',
1583
- 'Bessel 30 kHz',
1584
- 'Bessel 10 kHz',
1585
- 'HQ 30 kHz',
1586
- ]
1587
-
1588
1629
  def __init__(self, handle, reader):
1589
1630
 
1590
1631
  # Read properties
@@ -1594,6 +1635,14 @@ class AmplifierState:
1594
1635
  handle.seek(i + 8) # sCurrentGain = 8; (* LONGREAL *)
1595
1636
  self._current_gain = reader.read1('d')
1596
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
+
1597
1646
  # Series resistance and compensation
1598
1647
  handle.seek(i + 40) # sRsFraction = 40; (* LONGREAL *)
1599
1648
  self._rs_fraction = reader.read1('d')
@@ -1611,12 +1660,15 @@ class AmplifierState:
1611
1660
  self._cf_amp2 = reader.read1('d')
1612
1661
  handle.seek(i + 72) # sCFastTau = 72; (* LONGREAL *)
1613
1662
  self._cf_tau = reader.read1('d')
1614
- handle.seek(i + 285) # sCCCFastOn = 285; (* BYTE *)
1615
- self._cf_enabled = bool(reader.read1('b'))
1663
+ #handle.seek(i + 285) # sCCCFastOn = 285; (* BYTE *)
1664
+ #self._cf_enabled = bool(reader.read1('b'))
1616
1665
 
1617
1666
  # Slow capacitance correction
1618
1667
  handle.seek(i + 80) # sCSlow = 80; (* LONGREAL *)
1619
1668
  self._cs = reader.read1('d')
1669
+ handle.seek(i + 241) # sCSlowRange = 241; (* BYTE *)
1670
+ self._cs_range = CSlowRange(reader.read1('b'))
1671
+
1620
1672
  # Auto CSlow settings. See 4.9 "EPC 10 USB Menu" in the manual.
1621
1673
  handle.seek(i + 152) # sCSlowStimVolts = 152; (* LONGREAL *)
1622
1674
  self._cs_auto_stim = reader.read1('d')
@@ -1625,71 +1677,78 @@ class AmplifierState:
1625
1677
  handle.seek(i + 210) # sCSlowCycles = 210; (* INT16 *)
1626
1678
  self._cs_auto_cycles = reader.read1('h')
1627
1679
 
1628
- # Voltage offsets
1629
- handle.seek(i + 128) # sVpOffset = 128; (* LONGREAL *)
1630
- self._voff = reader.read1('d')
1631
- handle.seek(i + 136) # sVLiquidJunction = 136; (* LONGREAL *)
1632
- self._ljp = reader.read1('d')
1633
-
1634
1680
  # Filter 1
1681
+ # Set with a byte that controls type and frequency
1635
1682
  handle.seek(i + 281) # sFilter1 = 281; (* BYTE *)
1636
- 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')
1637
1701
 
1638
- #TODO: Add these are proper properties with a docstring'd method that
1639
- # returns them and says what the units are etc. or stop reading them.
1640
1702
  # self._temp = {}
1641
- # handle.seek(i + 264) # sImon1Bandwidth = 264; (* LONGREAL *)
1642
- # self._temp['sImon1Bandwidth'] = reader.read1('d')
1643
-
1644
- # handle.seek(i + 16) # sF2Bandwidth = 16; (* LONGREAL *)
1645
- # self._temp['sF2Bandwidth'] = reader.read1('d')
1646
- # handle.seek(i + 24) # sF2Frequency = 24; (* LONGREAL *)
1647
- # self._temp['sF2Frequency'] = reader.read1('d')
1648
- # handle.seek(i + 230) # sHasF2Bypass = 230; (* BYTE *)
1649
- # self._temp['sHasF2Bypass'] = reader.read1('b')
1650
- # handle.seek(i + 231) # sF2Mode = 231; (* BYTE *)
1651
- # self._temp['sF2Mode'] = reader.read1('b')
1652
- # handle.seek(i + 239) # sF2Response = 239; (* BYTE *)
1653
- # self._temp['sF2Response'] = reader.read1('b')
1654
- # handle.seek(i + 287) # sF2Source = 287; (* BYTE *)
1655
- # self._temp['sF2Source'] = reader.read1('b')
1656
-
1657
1703
  # handle.seek(i + 296) # sStimFilterHz = 296; (* LONGREAL *)
1658
- # self._temp['sStimFilterHz'] = reader.read1('d')
1704
+ # print('STIM', reader.read1('d'))
1659
1705
  # handle.seek(i + 320) # sInputFilterTau = 320; (* LONGREAL *)
1706
+ # print('InputFilterTau', reader.read1('d'))
1660
1707
  # self._temp['sInputFilterTau'] = reader.read1('d')
1661
1708
  # handle.seek(i + 328) # sOutputFilterTau = 328; (* LONGREAL *)
1709
+ # print('OutputFilterTau', reader.read1('d'))
1662
1710
  # self._temp['sOutputFilterTau'] = reader.read1('d')
1663
-
1664
1711
  # handle.seek(i + 384) # sVmonFiltBandwidth = 384; (* LONGREAL *)
1665
1712
  # self._temp['sVmonFiltBandwidth'] = reader.read1('d')
1666
1713
  # handle.seek(i + 392) # sVmonFiltFrequency = 392; (* LONGREAL *)
1667
1714
  # self._temp['sVmonFiltFrequency'] = reader.read1('d')
1715
+ # handle.seek(i + 264) # sImon1Bandwidth = 264; (* LONGREAL *)
1716
+ # self._temp['sImon1Bandwidth'] = reader.read1('d')
1668
1717
 
1669
- # handle.seek(i + 112) # sVHold = 112; (* LONGREAL *)
1670
- # self._temp['sVHold'] = reader.read1('d')
1671
- # handle.seek(i + 120) # sLastVHold = 120; (* LONGREAL *)
1672
- # 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'))
1673
1721
 
1674
1722
  def c_fast(self):
1675
1723
  """
1676
- Return the capacitance (pF) used in fast capacitance correction
1677
- (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.
1678
1731
  """
1679
- # Not sure why there are two. They are almost identical. Older EPC9
1680
- # manual has a Fast1 and Fast2 as well, but only for GET, for SET there
1681
- # is only 2. So... going with that for now
1682
- 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
1683
1735
 
1684
1736
  def c_fast_tau(self):
1685
1737
  """
1686
- 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`.
1687
1743
  """
1688
1744
  return self._cf_tau * 1e6
1689
1745
 
1690
1746
  def c_fast_enabled(self):
1691
- """ Returns ``True`` if fast capacitance compensation was enabled. """
1692
- 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
1693
1752
 
1694
1753
  def c_slow(self):
1695
1754
  """
@@ -1707,6 +1766,18 @@ class AmplifierState:
1707
1766
  return (self._cs_auto_stim * 1e3, self._cs_auto_cycles,
1708
1767
  self._cs_auto_timeout)
1709
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
+
1710
1781
  def current_gain(self):
1711
1782
  """
1712
1783
  The gain setting for current measurements, in mV/pA.
@@ -1715,9 +1786,50 @@ class AmplifierState:
1715
1786
 
1716
1787
  def filter1(self):
1717
1788
  """
1718
- 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):
1796
+ """
1797
+ Returns a string representing the Filter 1 settings.
1798
+
1799
+ For more information on Filter 1 and 2, see :class:`Filter1Setting`.
1719
1800
  """
1720
- return self._filter1_options[self._filter1]
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'
1721
1833
 
1722
1834
  def ljp(self):
1723
1835
  """
@@ -1763,12 +1875,122 @@ class AmplifierState:
1763
1875
  """
1764
1876
  return self._rs_tau * 1e6 if self._rs_enabled else 0
1765
1877
 
1766
- 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`.
1767
1884
  """
1768
- Returns the used voltage offset (in mV), also called V0.
1885
+ return self._stimulus_filter
1886
+
1887
+ def stimulus_filter_str(self):
1888
+ """
1889
+ Returns a string descibing the stimulus filter settings.
1890
+
1891
+ For more information, see :class:`StimulusFilterSetting`.
1769
1892
  """
1893
+ return self._stimulus_filter
1894
+
1895
+ def v_off(self):
1896
+ """ Returns the used voltage offset (in mV), also called V0. """
1770
1897
  return self._voff * 1e3
1771
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
+
1772
1994
 
1773
1995
  #
1774
1996
  # From StimFile_v9.txt
@@ -1885,6 +2107,8 @@ class Stimulus(TreeNode):
1885
2107
  handle.seek(start + 144) # stNumberSweeps = 144; (* INT32 *)
1886
2108
  self._sweep_count = reader.read1('i')
1887
2109
 
2110
+ # Filterfactor: Determines desired filtering frequency as a function of
2111
+ # sampling rate. Is overruled by autofilter when enabled.
1888
2112
  # handle.seek(start + 136) # stFilterFactor = 136; (* LONGREAL *)
1889
2113
  # self._filter_factor = reader.read1('d')
1890
2114