myokit 1.37.0__py3-none-any.whl → 1.37.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 (45) hide show
  1. myokit/__init__.py +2 -2
  2. myokit/_aux.py +4 -0
  3. myokit/_datablock.py +10 -10
  4. myokit/_datalog.py +55 -11
  5. myokit/_myokit_version.py +1 -1
  6. myokit/_sim/cvodessim.py +3 -3
  7. myokit/formats/axon/_abf.py +11 -4
  8. myokit/formats/diffsl/__init__.py +60 -0
  9. myokit/formats/diffsl/_ewriter.py +145 -0
  10. myokit/formats/diffsl/_exporter.py +435 -0
  11. myokit/formats/heka/__init__.py +4 -0
  12. myokit/formats/heka/_patchmaster.py +408 -156
  13. myokit/formats/sbml/__init__.py +21 -1
  14. myokit/formats/sbml/_api.py +160 -6
  15. myokit/formats/sbml/_exporter.py +53 -0
  16. myokit/formats/sbml/_writer.py +355 -0
  17. myokit/gui/datalog_viewer.py +17 -3
  18. myokit/tests/data/io/bad1d-2-no-header.zip +0 -0
  19. myokit/tests/data/io/bad1d-3-no-data.zip +0 -0
  20. myokit/tests/data/io/bad1d-4-not-a-zip.zip +1 -105
  21. myokit/tests/data/io/bad1d-5-bad-data-type.zip +0 -0
  22. myokit/tests/data/io/bad1d-6-time-too-short.zip +0 -0
  23. myokit/tests/data/io/bad1d-7-0d-too-short.zip +0 -0
  24. myokit/tests/data/io/bad1d-8-1d-too-short.zip +0 -0
  25. myokit/tests/data/io/bad2d-2-no-header.zip +0 -0
  26. myokit/tests/data/io/bad2d-3-no-data.zip +0 -0
  27. myokit/tests/data/io/bad2d-4-not-a-zip.zip +1 -105
  28. myokit/tests/data/io/bad2d-5-bad-data-type.zip +0 -0
  29. myokit/tests/data/io/bad2d-8-2d-too-short.zip +0 -0
  30. myokit/tests/data/io/block1d.mmt +187 -0
  31. myokit/tests/data/io/datalog-18-duplicate-keys.csv +4 -0
  32. myokit/tests/test_aux.py +4 -0
  33. myokit/tests/test_datablock.py +6 -6
  34. myokit/tests/test_datalog.py +20 -0
  35. myokit/tests/test_formats_diffsl.py +728 -0
  36. myokit/tests/test_formats_exporters_run.py +6 -0
  37. myokit/tests/test_formats_sbml.py +57 -1
  38. myokit/tests/test_sbml_api.py +90 -0
  39. myokit/tests/test_sbml_export.py +327 -0
  40. {myokit-1.37.0.dist-info → myokit-1.37.2.dist-info}/LICENSE.txt +1 -1
  41. {myokit-1.37.0.dist-info → myokit-1.37.2.dist-info}/METADATA +4 -4
  42. {myokit-1.37.0.dist-info → myokit-1.37.2.dist-info}/RECORD +45 -36
  43. {myokit-1.37.0.dist-info → myokit-1.37.2.dist-info}/WHEEL +1 -1
  44. {myokit-1.37.0.dist-info → myokit-1.37.2.dist-info}/entry_points.txt +0 -0
  45. {myokit-1.37.0.dist-info → myokit-1.37.2.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
 
@@ -966,33 +973,65 @@ class Series(TreeNode, myokit.formats.SweepSource):
966
973
 
967
974
  # Add meta data
968
975
  log.meta['time'] = self._time.strftime(myokit.DATE_FORMAT)
969
- a = self.amplifier_state()
970
976
  t = self[0][0] if len(self) and len(self[0]) else None
971
- log.meta['current_gain_mV_per_pA'] = a.current_gain()
972
- log.meta['filter1'] = a.filter1()
973
977
  if t is not None:
974
978
  log.meta['r_pipette_MOhm'] = t.r_pipette()
975
979
  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
- if a.c_fast_enabled():
979
- log.meta['c_fast_compensation_enabled'] = 'true'
980
- log.meta['c_fast_pF'] = a.c_fast()
981
- log.meta['c_fast_tau_us'] = a.c_fast_tau()
982
- else:
983
- log.meta['c_fast_compensation_enabled'] = 'false'
984
- log.meta['c_slow_pF'] = a.c_slow()
985
- log.meta['r_series_MOhm'] = a.r_series()
986
- if a.r_series_enabled():
987
- log.meta['r_series_compensation_enabled'] = 'true'
988
- log.meta['r_series_compensation_percent'] = round(
989
- a.r_series_fraction() * 100, 1)
990
- 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()
980
+ a = self.amplifier_state()
981
+ amps = [a] if a is not None else self.amplifier_states()
982
+ for k, a in enumerate(amps):
983
+ pre = '' if len(amps) == 1 else f'amp{1 + k}_'
984
+ log.meta[f'{pre}current_gain_mV_per_pA'] = a.current_gain()
985
+ log.meta[f'{pre}filter1'] = a.filter1_str()
986
+ log.meta[f'{pre}filter2'] = a.filter2_str()
987
+ log.meta[f'{pre}stimulus_filter'] = a.stimulus_filter_str()
988
+ log.meta[f'{pre}ljp_correction_mV'] = a.ljp()
989
+ log.meta[f'{pre}voltage_offset_mV'] = a.v_off()
990
+ log.meta[f'{pre}holding_potential_mV'] = a.v_hold()
991
+ if a.c_fast_enabled():
992
+ log.meta[f'{pre}c_fast_compensation_enabled'] = 'true'
993
+ log.meta[f'{pre}c_fast_pF'] = a.c_fast()
994
+ else:
995
+ log.meta[f'{pre}c_fast_compensation_enabled'] = 'false'
996
+ log.meta[f'{pre}c_slow_pF'] = a.c_slow()
997
+ if a.c_slow_enabled():
998
+ log.meta[f'{pre}c_slow_compensation_enabled'] = 'true'
999
+ log.meta[f'{pre}c_slow_range'] = a.c_slow_range()
1000
+ css = a.c_slow_auto_settings()
1001
+ log.meta[f'{pre}c_slow_auto_amplitude_mV'] = css[0]
1002
+ log.meta[f'{pre}c_slow_auto_cycles'] = css[1]
1003
+ log.meta[f'{pre}c_slow_auto_timeout'] = css[2]
1004
+ else:
1005
+ log.meta[f'{pre}c_slow_compensation_enabled'] = 'false'
1006
+ log.meta[f'{pre}r_series_MOhm'] = a.r_series()
1007
+ if a.r_series_enabled():
1008
+ log.meta[f'{pre}r_series_compensation_enabled'] = 'true'
1009
+ log.meta[f'{pre}r_series_compensation_percent'] = round(
1010
+ a.r_series_fraction() * 100, 1)
1011
+ log.meta[f'{pre}r_series_compensation_tau_us'] = \
1012
+ a.r_series_tau()
1013
+ else:
1014
+ log.meta[f'{pre}r_series_compensation_enabled'] = 'false'
1015
+
1016
+ # Add protocol to meta data
1017
+ stimulus = self.stimulus()
1018
+ log.meta['sampling_interval_ms'] = stimulus.sampling_interval() * 1000
1019
+ log.meta['sweep_count'] = stimulus.sweep_count()
1020
+ stimulus_channel = stimulus.supported_channel()
1021
+ if stimulus_channel is not None:
1022
+ log.meta['amplifier_mode'] = str(stimulus_channel.amplifier_mode())
1023
+ log.meta['protocol'] = stimulus.protocol().code()
1024
+
1025
+ # Add completion status
1026
+ if stimulus_channel is None:
1027
+ c = 'Unknown'
1028
+ elif self.is_complete():
1029
+ c = 'All sweeps ran and completed'
1030
+ elif self._intended_sweep_count == len(self):
1031
+ c = 'Final sweep incomplete'
994
1032
  else:
995
- log.meta['r_series_compensation_enabled'] = 'false'
1033
+ c = f'Ran {len(self)} out of {self._intended_sweep_count} sweeps'
1034
+ log.meta['completed'] = c
996
1035
 
997
1036
  return log
998
1037
 
@@ -1007,7 +1046,7 @@ class Series(TreeNode, myokit.formats.SweepSource):
1007
1046
  out.append(f' version {self._file.version()}')
1008
1047
  out.append(f'Recorded on {self._time}')
1009
1048
  out.append(f'{len(self)} sweeps,'
1010
- f' {len(self._channel_names)} channels.')
1049
+ f' {len(self._channel_names)} channels')
1011
1050
 
1012
1051
  # Completion status
1013
1052
  c = self._stimulus.supported_channel()
@@ -1022,64 +1061,81 @@ class Series(TreeNode, myokit.formats.SweepSource):
1022
1061
  out.append(f'Incomplete recording: {len(self)} out of'
1023
1062
  f' {self._intended_sweep_count} ran.')
1024
1063
 
1025
- # Resistance, capacitance, etc.
1064
+ # Info from amplifier state
1026
1065
  a = self.amplifier_state()
1027
- out.append('Information from amplifier state:')
1028
- out.append(f' Current gain: {a.current_gain()} mV/pA')
1029
- if a.ljp():
1030
- out.append(' LJP correction applied using'
1031
- f' LJP={round(a.ljp(), 4)} mV.')
1032
- if a.c_fast_enabled():
1033
- out.append(f' C fast compensation: {a.c_fast()} pF,'
1034
- f' {round(a.c_fast_tau(), 4)} us.')
1035
- 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.')
1039
- if a.r_series_enabled():
1040
- 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')
1044
- else:
1045
- out.append(' R series compensation: not enabled')
1066
+ amps = [a] if a is not None else self.amplifier_states()
1067
+ for k, a in enumerate(amps):
1068
+ out.append(f'Information from amplifier state {1 + k}:')
1069
+ out.append(f' Current gain: {a.current_gain()} mV/pA')
1070
+ out.append(f' Filter 1: {a.filter1_str()}')
1071
+ out.append(f' Filter 2: {a.filter2_str()}')
1072
+ out.append(f' Stimulus filter: {a.stimulus_filter_str()}')
1073
+ # Voltage info
1074
+ out.append(f' Holding potential: {a.v_hold()} mV')
1075
+ if a.ljp():
1076
+ out.append(f' LJP correction: {round(a.ljp(), 4)} mV')
1077
+ else:
1078
+ out.append(' LJP correction: no correction')
1079
+ out.append(f' Voltage offset: {a.v_off()} mV')
1080
+ # C fast
1081
+ if a.c_fast_enabled():
1082
+ out.append(f' C fast compensation: {a.c_fast()} pF,')
1083
+ else:
1084
+ out.append(' C fast compensation: not enabled')
1085
+ # C slow
1086
+ if a.c_slow_enabled():
1087
+ out.append(f' C slow compensation: {a.c_slow()} pF')
1088
+ amp, cyc, tim = a.c_slow_auto_settings()
1089
+ out.append(f' C slow auto settings: amplitude {amp} mV,'
1090
+ f' cycles {cyc}, timeout {tim} s')
1091
+ else:
1092
+ out.append(' C slow compensation: not enabled')
1093
+ # Rs comp
1094
+ out.append(f' R series: {a.r_series()} MOhm')
1095
+ if a.r_series_enabled():
1096
+ p = round(a.r_series_fraction() * 100, 1)
1097
+ q = round(a.r_series_tau(), 1)
1098
+ out.append(f' R series compensation: {p} %, {q} us')
1099
+ else:
1100
+ out.append(' R series compensation: not enabled')
1101
+
1102
+ # Info from first trace
1046
1103
  if len(self) and len(self[0]):
1047
1104
  t = self[0][0]
1048
1105
  out.append('Information from first trace:')
1049
-
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.')
1106
+ out.append(f' Pipette resistance: {t.r_pipette()} MOhm')
1107
+ out.append(f' Seal resistance: {t.r_seal()} MOhm')
1108
+ out.append(f' Series resistance: {t.r_series()} MOhm')
1109
+ out.append(f' C slow: {t.c_slow()} pF')
1056
1110
 
1057
1111
  # Sweeps and channels
1058
1112
  if verbose:
1059
1113
  out.append('-' * 60)
1060
1114
  for i, sweep in enumerate(self):
1061
1115
  out.append(f'Sweep {i}, label: "{sweep.label()}", recorded on'
1062
- f' {sweep.time()}.')
1116
+ f' {sweep.time()}')
1063
1117
  if i == 0:
1064
1118
  for j, trace in enumerate(self[0]):
1065
1119
  out.append(f' Trace {j}, label: "{trace.label()}",'
1066
1120
  f' in {trace.time_unit()} and'
1067
- f' {trace.value_unit()}.')
1121
+ f' {trace.value_unit()}')
1068
1122
 
1069
1123
  # Stimulus
1070
1124
  if verbose:
1071
1125
  stim = self._stimulus
1072
1126
  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.')
1127
+ out.append(f'Stimulus "{stim.label()}"')
1128
+ out.append(f' {stim.sweep_count()} sweeps')
1129
+ out.append(' Delay between sweeps: '
1130
+ f'{stim.sweep_interval() * 1000} ms')
1131
+ out.append(' Sampling interval: '
1132
+ f'{stim.sampling_interval() * 1000} ms')
1077
1133
  for i, ch in enumerate(stim):
1078
1134
  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()}.')
1135
+ f' {ch.amplifier_mode()} mode')
1136
+ out.append(f' Stimulus reconstruction: {ch.support_str()}')
1081
1137
  z = ch.zero_segment() or '0 (disabled)'
1082
- out.append(f' Zero segment: {z}.')
1138
+ out.append(f' Zero segment: {z}')
1083
1139
  for j, seg in enumerate(ch):
1084
1140
  out.append(f' Segment {j}, {seg.storage()}')
1085
1141
  out.append(f' {seg.segment_class()}, {seg}')
@@ -1280,7 +1336,6 @@ class Trace(TreeNode):
1280
1336
  # Meta data
1281
1337
  self._r_pipette = None
1282
1338
  self._r_seal = None
1283
- self._r_series_comp = None
1284
1339
  self._g_series = None
1285
1340
  self._c_slow = None
1286
1341
 
@@ -1335,8 +1390,8 @@ class Trace(TreeNode):
1335
1390
  self._c_slow = reader.read1('d')
1336
1391
  handle.seek(i + 184) # TrGSeries = 184; (* LONGREAL *)
1337
1392
  self._g_series = reader.read1('d')
1338
- handle.seek(i + 192) # TrRsValue = 192; (* LONGREAL *)
1339
- self._r_series_comp = reader.read1('d')
1393
+ #handle.seek(i + 192) # TrRsValue = 192; (* LONGREAL *)
1394
+ #self._r_series_comp = reader.read1('d')
1340
1395
 
1341
1396
  # Convert unit
1342
1397
  self._data_unit = myokit.parse_unit(self._data_unit)
@@ -1365,17 +1420,27 @@ class Trace(TreeNode):
1365
1420
 
1366
1421
  def r_seal(self):
1367
1422
  """
1368
- Returns the "seal resistance" (MOhm) determined in the waiting time
1369
- before the trace was acquired.
1423
+ Returns the "seal resistance" (MOhm) that was determined the last time
1424
+ that the "amplifier window" was active (so at some indeterminate time
1425
+ before the Trace was aquired).
1426
+
1427
+ The values returned by :meth:`r_seal` and :meth:`r_pipette` are the
1428
+ same measurement ("R-memb") performed at a different time. The value
1429
+ ``r_seal`` is updated whenever the "amplifier window" is active. Using
1430
+ a button or a programmed command, the same value can be stored as
1431
+ ``r_pipette``, which should be done before breaking the seal.
1370
1432
 
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.
1433
+ "R-memb" is determined either using a test pulse or from the current
1434
+ V and I values. If a test pulse is used (the default), this is
1435
+ specified as a ``dV`` from the holding potential, and
1436
+ ``R-memb = dV / dI`` where ``dV`` is the difference in command
1437
+ potential and ``dI`` is the measured difference in current (I during
1438
+ the step minus I at holding potential).
1376
1439
 
1377
- This is the same measurement as :meth:`r_pipette`, but logged
1378
- automatically before each trace.
1440
+ Users should be careful when interpreting this value, as it depends on
1441
+ (1) the last time that the amplifier window was active, (2) the test
1442
+ pulse settings, (3) the holding potential, (4) any currents active at
1443
+ the holding potential or during the step.
1379
1444
  """
1380
1445
  return self._r_seal * 1e-6
1381
1446
 
@@ -1384,26 +1449,16 @@ class Trace(TreeNode):
1384
1449
  Returns the last (uncompensated) series resistance (MOhm) before
1385
1450
  acquiring the trace.
1386
1451
  """
1387
- return 1e-6 / self._g_series
1388
-
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
1452
+ return 0 if self._g_series == 0 else 1e-6 / self._g_series
1396
1453
 
1397
1454
  def r_pipette(self):
1398
1455
  """
1399
- Returns the pipette resistance (MOhm) determined from the test pulse
1400
- before breaking the seal.
1456
+ Returns the pipette resistance (MOhm) stored with this Trace, but
1457
+ calculated at an earlier point.
1401
1458
 
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.
1459
+ Like the "seal resistance", the "pipette resistance" is a stored
1460
+ measurement of what patchmaster calls "R-memb". For details, see
1461
+ :meth:`r_seal`.
1407
1462
  """
1408
1463
  return self._r_pipette * 1e-6
1409
1464
 
@@ -1549,6 +1604,9 @@ class Trace(TreeNode):
1549
1604
  # sOld3 = 295; (* BYTE *)
1550
1605
  #
1551
1606
  # sStimFilterHz = 296; (* LONGREAL *)
1607
+ # 2024-11-07 HEKA support says that sStimFilterHz is "antiquated", and this
1608
+ # value (10kHz=off or 100kHz=on) should be ignored.
1609
+ #
1552
1610
  # sRsTau = 304; (* LONGREAL *)
1553
1611
  # sDacToAdcDelay = 312; (* LONGREAL *)
1554
1612
  # sInputFilterTau = 320; (* LONGREAL *)
@@ -1578,13 +1636,6 @@ class AmplifierState:
1578
1636
  """
1579
1637
  Describes the state of an amplifier used by PatchMaster.
1580
1638
  """
1581
- _filter1_options = [
1582
- 'Bessel 100 kHz',
1583
- 'Bessel 30 kHz',
1584
- 'Bessel 10 kHz',
1585
- 'HQ 30 kHz',
1586
- ]
1587
-
1588
1639
  def __init__(self, handle, reader):
1589
1640
 
1590
1641
  # Read properties
@@ -1594,6 +1645,14 @@ class AmplifierState:
1594
1645
  handle.seek(i + 8) # sCurrentGain = 8; (* LONGREAL *)
1595
1646
  self._current_gain = reader.read1('d')
1596
1647
 
1648
+ # Holding potential and offsets
1649
+ handle.seek(i + 112) # sVHold = 112; (* LONGREAL *)
1650
+ self._holding = reader.read1('d')
1651
+ handle.seek(i + 128) # sVpOffset = 128; (* LONGREAL *)
1652
+ self._voff = reader.read1('d')
1653
+ handle.seek(i + 136) # sVLiquidJunction = 136; (* LONGREAL *)
1654
+ self._ljp = reader.read1('d')
1655
+
1597
1656
  # Series resistance and compensation
1598
1657
  handle.seek(i + 40) # sRsFraction = 40; (* LONGREAL *)
1599
1658
  self._rs_fraction = reader.read1('d')
@@ -1610,13 +1669,16 @@ class AmplifierState:
1610
1669
  handle.seek(i + 64) # sCFastAmp2 = 64; (* LONGREAL *)
1611
1670
  self._cf_amp2 = reader.read1('d')
1612
1671
  handle.seek(i + 72) # sCFastTau = 72; (* LONGREAL *)
1613
- self._cf_tau = reader.read1('d')
1614
- handle.seek(i + 285) # sCCCFastOn = 285; (* BYTE *)
1615
- self._cf_enabled = bool(reader.read1('b'))
1672
+ self._cf_tau2 = reader.read1('d')
1673
+ #handle.seek(i + 285) # sCCCFastOn = 285; (* BYTE *)
1674
+ #self._cf_enabled = bool(reader.read1('b'))
1616
1675
 
1617
1676
  # Slow capacitance correction
1618
1677
  handle.seek(i + 80) # sCSlow = 80; (* LONGREAL *)
1619
1678
  self._cs = reader.read1('d')
1679
+ handle.seek(i + 241) # sCSlowRange = 241; (* BYTE *)
1680
+ self._cs_range = CSlowRange(reader.read1('b'))
1681
+
1620
1682
  # Auto CSlow settings. See 4.9 "EPC 10 USB Menu" in the manual.
1621
1683
  handle.seek(i + 152) # sCSlowStimVolts = 152; (* LONGREAL *)
1622
1684
  self._cs_auto_stim = reader.read1('d')
@@ -1625,71 +1687,82 @@ class AmplifierState:
1625
1687
  handle.seek(i + 210) # sCSlowCycles = 210; (* INT16 *)
1626
1688
  self._cs_auto_cycles = reader.read1('h')
1627
1689
 
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
1690
  # Filter 1
1691
+ # Set with a byte that controls type and frequency
1635
1692
  handle.seek(i + 281) # sFilter1 = 281; (* BYTE *)
1636
- self._filter1 = reader.read1('b')
1637
-
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
- # 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
- # handle.seek(i + 296) # sStimFilterHz = 296; (* LONGREAL *)
1658
- # self._temp['sStimFilterHz'] = reader.read1('d')
1659
- # handle.seek(i + 320) # sInputFilterTau = 320; (* LONGREAL *)
1660
- # self._temp['sInputFilterTau'] = reader.read1('d')
1661
- # handle.seek(i + 328) # sOutputFilterTau = 328; (* LONGREAL *)
1662
- # self._temp['sOutputFilterTau'] = reader.read1('d')
1663
-
1664
- # handle.seek(i + 384) # sVmonFiltBandwidth = 384; (* LONGREAL *)
1665
- # self._temp['sVmonFiltBandwidth'] = reader.read1('d')
1666
- # handle.seek(i + 392) # sVmonFiltFrequency = 392; (* LONGREAL *)
1667
- # self._temp['sVmonFiltFrequency'] = reader.read1('d')
1668
-
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')
1693
+ self._filter1 = Filter1Setting(reader.read1('b'))
1694
+
1695
+ # Filter 2
1696
+ # Set with a type and a separate frequency. The user setting for Filter
1697
+ # 2 is the combined bandwidth of Filter 1 and Filter 2.
1698
+ handle.seek(i + 239) # sF2Response = 239; (* BYTE *)
1699
+ self._filter2_type = Filter2Type(reader.read1('b'))
1700
+ handle.seek(i + 24) # sF2Frequency = 24; (* LONGREAL *)
1701
+ self._filter2_freq_solo = reader.read1('d')
1702
+ handle.seek(i + 16) # sF2Bandwidth = 16; (* LONGREAL *)
1703
+ self._filter2_freq_both = reader.read1('d')
1704
+
1705
+ # Suspect this indicates external filter 2
1706
+ #handle.seek(i + 287) # sF2Source = 287; (* BYTE *)
1707
+ #self._temp['sF2Source'] = reader.read1('b')
1708
+ # Don't know what Mode does.
1709
+ #handle.seek(i + 231) # sF2Mode = 231; (* BYTE *)
1710
+ #self._temp['sF2Mode'] = reader.read1('b')
1711
+
1712
+ #self._temp = {}
1713
+ #handle.seek(i + 320) # sInputFilterTau = 320; (* LONGREAL *)
1714
+ #print('InputFilterTau', reader.read1('d'))
1715
+ #self._temp['sInputFilterTau'] = reader.read1('d')
1716
+ #handle.seek(i + 328) # sOutputFilterTau = 328; (* LONGREAL *)
1717
+ #print('OutputFilterTau', reader.read1('d'))
1718
+ #self._temp['sOutputFilterTau'] = reader.read1('d')
1719
+ #handle.seek(i + 384) # sVmonFiltBandwidth = 384; (* LONGREAL *)
1720
+ #self._temp['sVmonFiltBandwidth'] = reader.read1('d')
1721
+ #handle.seek(i + 392) # sVmonFiltFrequency = 392; (* LONGREAL *)
1722
+ #self._temp['sVmonFiltFrequency'] = reader.read1('d')
1723
+ #print(self._temp)
1724
+ #handle.seek(i + 264) # sImon1Bandwidth = 264; (* LONGREAL *)
1725
+ #self._temp['sImon1Bandwidth'] = reader.read1('d')
1726
+
1727
+ # Stimulus filter
1728
+ handle.seek(i + 282) # sStimFilterOn = 282; (* BYTE *)
1729
+ self._stimulus_filter = StimulusFilterSetting(reader.read1('b'))
1673
1730
 
1674
1731
  def c_fast(self):
1675
1732
  """
1676
- Return the capacitance (pF) used in fast capacitance correction
1677
- (CFast2).
1733
+ Return the capacitance (pF) used in fast capacitance correction.
1734
+
1735
+ HEKA amplifiers use a two-component fast capacitance cancellation, with
1736
+ an instantaneous part (component 1) and a delayed part (component 2).
1737
+ This method returns the sum of both values. Individual values can be
1738
+ obtained from :meth:`c_fast_detailed`.
1678
1739
  """
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
1740
+ # The total fast capacitance correction is a sum of two capacitances.
1741
+ # See the EPC-10 manual for details.
1742
+ return (self._cf_amp1 + self._cf_amp2) * 1e12
1683
1743
 
1684
- def c_fast_tau(self):
1744
+ def c_fast_detailed(self):
1685
1745
  """
1686
- Returns the time constant (us) used in fast capacitance correction.
1746
+ Returns the detailed fast capacitance correction values:
1747
+ ``(c_fast1, c_fast2, tau_fast2)`` in (pF, pF, us).
1748
+
1749
+ Here ``c_fast1`` is the capacitance (pF) of the instantaneous component
1750
+ of cancellation, ``c_fast2`` is the capacitance (pF) of the delayed
1751
+ cancellation, and ``tau_fast2`` is a "tau value" (in microseconds)
1752
+ pertaining to the delayed component (the equivalent value for the
1753
+ instantaneous component is 0).
1687
1754
  """
1688
- return self._cf_tau * 1e6
1755
+ # The total fast capacitance correction is a sum of two capacitances.
1756
+ # See the EPC-10 manual for details.
1757
+ return (
1758
+ self._cf_amp1 * 1e12, self._cf_amp2 * 1e12, self._cf_tau2 * 1e6)
1689
1759
 
1690
1760
  def c_fast_enabled(self):
1691
- """ Returns ``True`` if fast capacitance compensation was enabled. """
1692
- return self._cf_enabled
1761
+ """
1762
+ Returns ``True`` if the fast capacitance compensation is set to a
1763
+ non-zero capacitance.
1764
+ """
1765
+ return (self._cf_amp1 + self._cf_amp2) > 0
1693
1766
 
1694
1767
  def c_slow(self):
1695
1768
  """
@@ -1707,6 +1780,18 @@ class AmplifierState:
1707
1780
  return (self._cs_auto_stim * 1e3, self._cs_auto_cycles,
1708
1781
  self._cs_auto_timeout)
1709
1782
 
1783
+ def c_slow_enabled(self):
1784
+ """
1785
+ Returns ``True`` if the C-slow range is not set to 'Off'.
1786
+ """
1787
+ return self._cs_range is not CSlowRange.OFF
1788
+
1789
+ def c_slow_range(self):
1790
+ """
1791
+ Returns the :class:`CSlowRange` used for slow capacitance correction.
1792
+ """
1793
+ return self._cs_range
1794
+
1710
1795
  def current_gain(self):
1711
1796
  """
1712
1797
  The gain setting for current measurements, in mV/pA.
@@ -1715,9 +1800,50 @@ class AmplifierState:
1715
1800
 
1716
1801
  def filter1(self):
1717
1802
  """
1718
- Returns a string describing the used (always-on) analog Filter 1.
1803
+ Returns a :class:`Filter1Setting` describing filter 1.
1804
+
1805
+ For more information on Filter 1 and 2, see :class:`Filter1Setting`.
1806
+ """
1807
+ return self._filter1
1808
+
1809
+ def filter1_str(self):
1810
+ """
1811
+ Returns a string representing the Filter 1 settings.
1812
+
1813
+ For more information on Filter 1 and 2, see :class:`Filter1Setting`.
1814
+ """
1815
+ return str(self._filter1)
1816
+
1817
+ def filter2(self):
1818
+ """
1819
+ Returns a tuple ``(type, f_both, f_solo)`` describing the Filter 2
1820
+ settings, where ``type`` is a :class:`Filter2Type`, where ``f_both`` is
1821
+ the frequency in Hz of both filters combined, and when ``f_solo`` is
1822
+ the frequency of Filter 2 alone.
1823
+
1824
+ The frequency shown in PatchMaster is that of both filters combined.
1825
+ If the "bypass" filter is selected, the frequency settings are unused.
1826
+
1827
+ For more information on Filter 1 and 2, see :class:`Filter1Setting`.
1828
+ """
1829
+ return (
1830
+ self._filter2_type,
1831
+ self._filter2_freq_both,
1832
+ self._filter2_freq_solo,
1833
+ )
1834
+
1835
+ def filter2_str(self):
1719
1836
  """
1720
- return self._filter1_options[self._filter1]
1837
+ Returns a string describing Filter 2.
1838
+
1839
+ For more information on Filter 1 and 2, see :class:`Filter1Setting`.
1840
+ """
1841
+ if self._filter2_type is Filter2Type.BYPASS:
1842
+ return str(self._filter2_type)
1843
+ fb = round(self._filter2_freq_both * 1e-3, 2)
1844
+ fs = round(self._filter2_freq_solo * 1e-3, 2)
1845
+ fb = int(fb) if fb == int(fb) else fb
1846
+ return f'{self._filter2_type} {fb} kHz combined, {fs} kHz f2-only'
1721
1847
 
1722
1848
  def ljp(self):
1723
1849
  """
@@ -1763,12 +1889,136 @@ class AmplifierState:
1763
1889
  """
1764
1890
  return self._rs_tau * 1e6 if self._rs_enabled else 0
1765
1891
 
1766
- def v_off(self):
1892
+ def stimulus_filter(self):
1767
1893
  """
1768
- Returns the used voltage offset (in mV), also called V0.
1894
+ Returns a :class:`StimulusFilterSetting` descibing the stimulus filter
1895
+ settings.
1896
+
1897
+ For more information, see :class:`StimulusFilterSetting`.
1898
+ """
1899
+ return self._stimulus_filter
1900
+
1901
+ def stimulus_filter_str(self):
1769
1902
  """
1903
+ Returns a string descibing the stimulus filter settings.
1904
+
1905
+ For more information, see :class:`StimulusFilterSetting`.
1906
+ """
1907
+ return self._stimulus_filter
1908
+
1909
+ def v_off(self):
1910
+ """ Returns the used voltage offset (in mV), also called V0. """
1770
1911
  return self._voff * 1e3
1771
1912
 
1913
+ def v_hold(self):
1914
+ """
1915
+ Returns the holding potential (in mV).
1916
+
1917
+ This is the potential last set in the amplifier window, before any
1918
+ experiments were run.
1919
+ """
1920
+ return self._holding * 1e3
1921
+
1922
+
1923
+ class Filter1Setting(enum.Enum):
1924
+ """
1925
+ Settings for filter 1, which is applied before filter 2.
1926
+
1927
+ Both filter 1 and filter 2 are hardware filters, implemented on the EPC 9
1928
+ and 10. Filter 1 is used in voltage control, while filter 2 is used to
1929
+ perform filtering before digitisation. Filter 1 is always on, but some
1930
+ amplifiers allow filter 2 to be bypassed. The setting for filter 1
1931
+ determines both the type of filter (Bessel etc.) and the bandwidth. The
1932
+ user setting for filter 2 sets the combined bandwidth of both filters.
1933
+
1934
+ Measurements that passed only through filter 1 can be obtained from
1935
+ Imon1, while Imon2 provides output passed through both filters.
1936
+ """
1937
+ BESSEL_100K = 0
1938
+ BESSEL_30K = 1
1939
+ BESSEL_10K = 2
1940
+ HQ_30K = 3
1941
+
1942
+ def __str__(self):
1943
+ if self is Filter1Setting.BESSEL_100K:
1944
+ return 'Bessel 100 kHz'
1945
+ elif self is Filter1Setting.BESSEL_30K:
1946
+ return 'Bessel 30 kHz'
1947
+ elif self is Filter1Setting.BESSEL_10K:
1948
+ return 'Bessel 10 kHz'
1949
+ else:
1950
+ return 'HQ 30 kHz'
1951
+
1952
+
1953
+ class Filter2Type(enum.Enum):
1954
+ """
1955
+ Filter type for filter 2, which is applied after filter 1.
1956
+
1957
+ Unlike Filter 1, this filter can be disabled, and the frequency is set
1958
+ separately.
1959
+
1960
+ For more information on the filters, see :class:`Filter1Setting`.
1961
+ """
1962
+ BESSEL = 0
1963
+ BUTTERWORTH = 1
1964
+ BYPASS = 2
1965
+ #V_BESSEL = 3 Maybe!
1966
+ #V_Butterworth = 4 Maybe!
1967
+
1968
+ def __str__(self):
1969
+ if self is Filter2Type.BESSEL:
1970
+ return 'Bessel'
1971
+ elif self is Filter2Type.BUTTERWORTH:
1972
+ return 'Butterworth'
1973
+ else:
1974
+ return 'Bypass'
1975
+
1976
+
1977
+ class CSlowRange(enum.Enum):
1978
+ """
1979
+ Available options for slow capacitance cancelling range.
1980
+
1981
+ This doubles up as the on/off setting for slow capacitance cancellation.
1982
+ """
1983
+ OFF = 0
1984
+ pF30 = 1
1985
+ pF100 = 2
1986
+ pF1000 = 3
1987
+
1988
+ def __str__(self):
1989
+ if self is CSlowRange.OFF:
1990
+ return 'Off'
1991
+ elif self is CSlowRange.pF30:
1992
+ return '30 pF'
1993
+ elif self is CSlowRange.pF100:
1994
+ return '100 pF'
1995
+ else:
1996
+ return '1000 pF'
1997
+
1998
+
1999
+ class StimulusFilterSetting(enum.Enum):
2000
+ """
2001
+ Setting for the stimulus filter: 20 us (on, default), or 2 us (off).
2002
+
2003
+ The stimulus filter is a 2-pole Bessel filter applied over the stimulus
2004
+ signal to reduce fast capacitative currents. It is applied to voltages, the
2005
+ manual is less clear whether it is applied to currents too.
2006
+
2007
+ The quoted values are stated to be the filter's "rise time", which is the
2008
+ time needed for the signal to go from 10% to 90% of its step response.
2009
+ However, measurements indicate real rise times are longer (approximately
2010
+ 40us in the 20us setting), so these values should be treated as nominal
2011
+ rather than actual results.
2012
+ """
2013
+ BW2 = 0
2014
+ BW20 = 1
2015
+
2016
+ def __str__(self):
2017
+ if self is StimulusFilterSetting.BW2:
2018
+ return 'Bessel 2 us (off)'
2019
+ else:
2020
+ return 'Bessel 20 us (on)'
2021
+
1772
2022
 
1773
2023
  #
1774
2024
  # From StimFile_v9.txt
@@ -1885,6 +2135,8 @@ class Stimulus(TreeNode):
1885
2135
  handle.seek(start + 144) # stNumberSweeps = 144; (* INT32 *)
1886
2136
  self._sweep_count = reader.read1('i')
1887
2137
 
2138
+ # Filterfactor: Determines desired filtering frequency as a function of
2139
+ # sampling rate. Is overruled by autofilter when enabled.
1888
2140
  # handle.seek(start + 136) # stFilterFactor = 136; (* LONGREAL *)
1889
2141
  # self._filter_factor = reader.read1('d')
1890
2142