myokit 1.35.0__py3-none-any.whl → 1.35.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- myokit/__init__.py +11 -14
- myokit/__main__.py +0 -3
- myokit/_config.py +1 -3
- myokit/_datablock.py +914 -12
- myokit/_model_api.py +1 -3
- myokit/_myokit_version.py +1 -1
- myokit/_protocol.py +14 -28
- myokit/_sim/cable.c +1 -1
- myokit/_sim/cable.py +3 -2
- myokit/_sim/cmodel.h +1 -0
- myokit/_sim/cvodessim.c +79 -42
- myokit/_sim/cvodessim.py +20 -8
- myokit/_sim/fiber_tissue.c +1 -1
- myokit/_sim/fiber_tissue.py +3 -2
- myokit/_sim/openclsim.c +1 -1
- myokit/_sim/openclsim.py +8 -11
- myokit/_sim/pacing.h +121 -106
- myokit/_unit.py +1 -1
- myokit/formats/__init__.py +178 -0
- myokit/formats/axon/_abf.py +911 -841
- myokit/formats/axon/_atf.py +62 -59
- myokit/formats/axon/_importer.py +2 -2
- myokit/formats/heka/__init__.py +38 -0
- myokit/formats/heka/_importer.py +39 -0
- myokit/formats/heka/_patchmaster.py +2512 -0
- myokit/formats/wcp/_wcp.py +318 -133
- myokit/gui/datablock_viewer.py +144 -77
- myokit/gui/datalog_viewer.py +212 -231
- myokit/tests/ansic_event_based_pacing.py +3 -3
- myokit/tests/{ansic_fixed_form_pacing.py → ansic_time_series_pacing.py} +6 -6
- myokit/tests/data/formats/abf-v2.abf +0 -0
- myokit/tests/test_datablock.py +84 -0
- myokit/tests/test_datalog.py +2 -1
- myokit/tests/test_formats_axon.py +589 -136
- myokit/tests/test_formats_wcp.py +191 -22
- myokit/tests/test_pacing_system_c.py +51 -23
- myokit/tests/test_pacing_system_py.py +18 -0
- myokit/tests/test_simulation_1d.py +62 -22
- myokit/tests/test_simulation_cvodes.py +52 -3
- myokit/tests/test_simulation_fiber_tissue.py +35 -4
- myokit/tests/test_simulation_opencl.py +28 -4
- {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/LICENSE.txt +1 -1
- {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/METADATA +1 -1
- {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/RECORD +47 -44
- {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/WHEEL +0 -0
- {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/entry_points.txt +0 -0
- {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,2512 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This module reads files in HEKA Patchmaster format.
|
|
3
|
+
#
|
|
4
|
+
# Specifically, it targets the 2x90.2 format.
|
|
5
|
+
#
|
|
6
|
+
# It has not been extensively tested.
|
|
7
|
+
#
|
|
8
|
+
# Notes:
|
|
9
|
+
# - HEKA publishes a lot of information about its file format on its FTP
|
|
10
|
+
# server: server.hekahome.de
|
|
11
|
+
# This can be accessed using name and password "anonymous".
|
|
12
|
+
# Unfortunately, it describes all of the fields and gives their names, but
|
|
13
|
+
# gives very little guidance on how to interpret them.
|
|
14
|
+
# - However, PatchMaster is quite "close to the bone", so a lot can be learned
|
|
15
|
+
# from the manual. In particular:
|
|
16
|
+
# - Section 10.9. Channel Settings for DA Output and AD Input
|
|
17
|
+
# - Section 10.10. Segments
|
|
18
|
+
# - Chapter 14. Parameters Window
|
|
19
|
+
# - At the moment, the code interprets "BYTE" in HEKA terms as a "signed char"
|
|
20
|
+
# or 'b': https://docs.python.org/3/library/struct.html#format-characters
|
|
21
|
+
# However, for most fields unsigned would have been more sensible, so maybe
|
|
22
|
+
# it's that?
|
|
23
|
+
# - Stimulus support is minimal: even with the manual it's not very clear what
|
|
24
|
+
# all the increment/decrement types should do. E.g. no examples with odd
|
|
25
|
+
# numbers of steps are given; the manual mentions two types of log
|
|
26
|
+
# interpretation but there is no obvious field to select one; a "toggle"
|
|
27
|
+
# mode is mentioned in the manual but again is not easy to find in the file.
|
|
28
|
+
#
|
|
29
|
+
# This file is part of Myokit.
|
|
30
|
+
# See http://myokit.org for copyright, sharing, and licensing details.
|
|
31
|
+
#
|
|
32
|
+
import datetime
|
|
33
|
+
import enum
|
|
34
|
+
import os
|
|
35
|
+
import struct
|
|
36
|
+
import warnings
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
|
|
40
|
+
import myokit
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#
|
|
44
|
+
# From DataFile_v9.txt
|
|
45
|
+
# (* BundleHeader = RECORD *)
|
|
46
|
+
# oSignature = 0; (* ARRAY[0..7] OF CHAR *)
|
|
47
|
+
# oVersion = 8; (* ARRAY[0..31] OF CHAR *)
|
|
48
|
+
# oTime = 40; (* LONGREAL *)
|
|
49
|
+
# oItems = 48; (* INT32 *)
|
|
50
|
+
# oIsLittleEndian = 52; (* BOOLEAN *)
|
|
51
|
+
# oReserved = 53; (* ARRAY[0..10] OF CHAR *)
|
|
52
|
+
# oBundleItems = 64; (* ARRAY[0..11] OF BundleItem *)
|
|
53
|
+
# BundleHeaderSize = 256; (* = 32 * 8 *)
|
|
54
|
+
#
|
|
55
|
+
# (* BundleItem = RECORD *)
|
|
56
|
+
# oStart = 0; (* INT32 *)
|
|
57
|
+
# oLength = 4; (* INT32 *)
|
|
58
|
+
# oExtension = 8; (* ARRAY[0..7] OF CHAR *)
|
|
59
|
+
# BundleItemSize = 16; (* = 2 * 8 *)
|
|
60
|
+
#
|
|
61
|
+
# See also Manual 14.1.1 Root
|
|
62
|
+
#
|
|
63
|
+
class PatchMasterFile:
|
|
64
|
+
"""
|
|
65
|
+
Provides read-only access to data stored in HEKA PatchMaster ("bundle")
|
|
66
|
+
files (``.dat``), stored at ``filepath``.
|
|
67
|
+
|
|
68
|
+
Data loading is "lazy", meaning that data is only read when requested. This
|
|
69
|
+
means the file stays open, and so :meth:`close()` must be called after
|
|
70
|
+
using a `PatchMasterFile`. To ensure this happens, use ``with``::
|
|
71
|
+
|
|
72
|
+
with PatchMasterFile('my-file.dat') as f:
|
|
73
|
+
for group in f:
|
|
74
|
+
print(group.label)
|
|
75
|
+
|
|
76
|
+
Each file contains a hierarchy of :class:`Group`, :class:`Series`,
|
|
77
|
+
:class:`Sweep` and :class:`Trace` objects. For example, each "group" might
|
|
78
|
+
represent a single cell, and each "series" will be a protocol (called a
|
|
79
|
+
"stimulus") run on that cell. Groups are named by the user. Series are
|
|
80
|
+
named after the "stimulus" they run. Sweeps are usually unnamed (although
|
|
81
|
+
they do have a ``label`` property), and channels are named by the user.
|
|
82
|
+
|
|
83
|
+
To access groups, index them by integer, or use the :meth:`group` method to
|
|
84
|
+
find the first group with a given label::
|
|
85
|
+
|
|
86
|
+
with PatchMasterFile('my-file.dat') as f:
|
|
87
|
+
group_0 = f[0]
|
|
88
|
+
group_x = f.group('Experiment X')
|
|
89
|
+
|
|
90
|
+
Series, sweeps, and traces are accessed with integers::
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
with PatchMasterFile('my-file.dat') as f:
|
|
94
|
+
group = f.group('Experiment X')
|
|
95
|
+
series = group[0]
|
|
96
|
+
sweep = series[0]
|
|
97
|
+
trace = sweep[0]
|
|
98
|
+
|
|
99
|
+
Each object in the hierarchy can be iterated over::
|
|
100
|
+
|
|
101
|
+
with PatchMasterFile('my-file.dat') as f:
|
|
102
|
+
for group in f:
|
|
103
|
+
for series in group:
|
|
104
|
+
for sweep in series:
|
|
105
|
+
for trace in sweep:
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
To see only completed series (where all sweeps were run to finish), use::
|
|
109
|
+
|
|
110
|
+
with PatchMasterFile('my-file.dat') as f:
|
|
111
|
+
for group in f:
|
|
112
|
+
for series in group.complete_series():
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
The :class:`Series` class implements the
|
|
116
|
+
:class:`myokit.formats.SweepSource` interface::
|
|
117
|
+
|
|
118
|
+
with PatchMasterFile('my-file.dat') as f:
|
|
119
|
+
for group in f:
|
|
120
|
+
for series in group.complete_series():
|
|
121
|
+
log = series.log()
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
def __init__(self, filepath):
|
|
125
|
+
warnings.warn(
|
|
126
|
+
'PatchMaster file reading is new: There are no unit tests yet.')
|
|
127
|
+
|
|
128
|
+
# The path to the file and its basename
|
|
129
|
+
self._filepath = os.path.abspath(filepath)
|
|
130
|
+
self._filename = os.path.basename(filepath)
|
|
131
|
+
|
|
132
|
+
# File format version
|
|
133
|
+
self._version = None
|
|
134
|
+
|
|
135
|
+
# File handle
|
|
136
|
+
self._handle = f = open(self._filepath, 'rb')
|
|
137
|
+
|
|
138
|
+
# Check that this is a "bundle" file.
|
|
139
|
+
sig = f.read(8).decode(_ENC)
|
|
140
|
+
if sig[:4] == 'DATA ':
|
|
141
|
+
raise NotImplementedError(
|
|
142
|
+
'Older version not supported.')
|
|
143
|
+
elif sig[:4] == 'DAT1':
|
|
144
|
+
raise NotImplementedError(
|
|
145
|
+
'Only bundle files are supported.')
|
|
146
|
+
elif sig[:4] != 'DAT2':
|
|
147
|
+
raise NotImplementedError(
|
|
148
|
+
'Older version or not recognised as HEKA PatchMaster'
|
|
149
|
+
' format.')
|
|
150
|
+
|
|
151
|
+
# Read remaining bundle header
|
|
152
|
+
|
|
153
|
+
# Version number and software time
|
|
154
|
+
self._version = f.read(32).decode(_ENC)
|
|
155
|
+
try:
|
|
156
|
+
self._version = self._version[:self._version.index('\x00')]
|
|
157
|
+
except ValueError:
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
if not self._version.startswith('v2x90.2'):
|
|
161
|
+
warnings.warn('Only PatchMaster version v2x90.2 is supported.')
|
|
162
|
+
|
|
163
|
+
# Endianness
|
|
164
|
+
f.seek(52)
|
|
165
|
+
is_little_endian = struct.unpack('?', f.read(1))
|
|
166
|
+
r = EndianAwareReader(f, is_little_endian)
|
|
167
|
+
|
|
168
|
+
# Last time this file was opened for modification
|
|
169
|
+
f.seek(40)
|
|
170
|
+
self._time = r.time()
|
|
171
|
+
|
|
172
|
+
# Number of valid bundle items
|
|
173
|
+
n_items = r.read('i')[0] # Note: Unsigned would make more sense?
|
|
174
|
+
|
|
175
|
+
# Skip endianness and "reserved" bits
|
|
176
|
+
f.seek(12, 1) # 1 = offset from current position
|
|
177
|
+
|
|
178
|
+
# Read bundle items
|
|
179
|
+
self._items = {
|
|
180
|
+
'.dat': None, # Raw data file
|
|
181
|
+
'.pul': None, # Pulsed Tree file --> Acquisition parameters
|
|
182
|
+
# and pointer to raw data.
|
|
183
|
+
'.pgf': None, # Stimulus template Tree file --> Information on the
|
|
184
|
+
# stimulus protocols.
|
|
185
|
+
'.sol': None, # Solutions Tree file
|
|
186
|
+
'.onl': None, # Analysis Tree file
|
|
187
|
+
'.mth': None, # Method file
|
|
188
|
+
'.mrk': None, # Marker file
|
|
189
|
+
'.amp': None, # Amplifier (when multiple amplifiers are used)
|
|
190
|
+
'.txt': None, # Notebook
|
|
191
|
+
}
|
|
192
|
+
self._f_onl = None # Analysis file
|
|
193
|
+
for i in range(12):
|
|
194
|
+
start = r.read('i')[0] # Again, unsigned would make sense
|
|
195
|
+
size = r.read('i')[0]
|
|
196
|
+
ext = r.str(8)
|
|
197
|
+
if size == 0:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
item = self._items.get(ext)
|
|
202
|
+
except KeyError: # pragma: no-cover
|
|
203
|
+
raise NotImplementedError(
|
|
204
|
+
'Unsupported bundle item: ' + ext)
|
|
205
|
+
if item is not None: # pragma: no-cover
|
|
206
|
+
raise ValueError(
|
|
207
|
+
'Invalid or unsupported file. Bundle item appears'
|
|
208
|
+
' more than once: ' + ext)
|
|
209
|
+
self._items[ext] = (start, size)
|
|
210
|
+
|
|
211
|
+
# Read stimulus template
|
|
212
|
+
f.seek(self._items['.pgf'][0])
|
|
213
|
+
self._stimulus_tree = TreeNode.read(
|
|
214
|
+
self, f, (StimulusFile, Stimulus, StimulusChannel, Segment))
|
|
215
|
+
|
|
216
|
+
# Read pulsed tree
|
|
217
|
+
f.seek(self._items['.pul'][0])
|
|
218
|
+
self._pulsed_tree = TreeNode.read(
|
|
219
|
+
self, f, (PulsedFile, Group, Series, Sweep, Trace))
|
|
220
|
+
|
|
221
|
+
def __enter__(self):
|
|
222
|
+
return self
|
|
223
|
+
|
|
224
|
+
def __exit__(self, type, value, tb):
|
|
225
|
+
self._handle.close()
|
|
226
|
+
|
|
227
|
+
def __iter__(self):
|
|
228
|
+
return iter(self._pulsed_tree)
|
|
229
|
+
|
|
230
|
+
def close(self):
|
|
231
|
+
"""
|
|
232
|
+
Closes this file: no new data can be read once this has been called.
|
|
233
|
+
"""
|
|
234
|
+
self._handle.close()
|
|
235
|
+
|
|
236
|
+
def filename(self):
|
|
237
|
+
""" Returns this file's filename. """
|
|
238
|
+
return self._filename
|
|
239
|
+
|
|
240
|
+
def group(self, label):
|
|
241
|
+
""" Returns the first :class`Group` matching the given ``label``. """
|
|
242
|
+
for g in self:
|
|
243
|
+
if g.label() == label:
|
|
244
|
+
return g
|
|
245
|
+
raise KeyError(f'Group not found: {label}')
|
|
246
|
+
|
|
247
|
+
def path(self):
|
|
248
|
+
""" Returns the path to this PatchMaster file. """
|
|
249
|
+
return self._filepath
|
|
250
|
+
|
|
251
|
+
def stimulus_tree(self):
|
|
252
|
+
""" Returns this file's stimulus tree. """
|
|
253
|
+
return self._stimulus_tree
|
|
254
|
+
|
|
255
|
+
def version(self):
|
|
256
|
+
""" Returns this file's version number. """
|
|
257
|
+
return self._version
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class EndianAwareReader:
|
|
261
|
+
"""
|
|
262
|
+
Used by :class:`PatchmasterFile` and its supporting classes when reading
|
|
263
|
+
from an open file handle that may be either big or little endian.
|
|
264
|
+
|
|
265
|
+
This class may be useful when extending the patchmaster file reading, or to
|
|
266
|
+
read other HEKA formats. To read a patchmaster file, use the
|
|
267
|
+
``PatchMasterFile`` class.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def __init__(self, handle, is_little_endian):
|
|
271
|
+
self._f = handle
|
|
272
|
+
self._e = '<' if is_little_endian else '>'
|
|
273
|
+
|
|
274
|
+
def read(self, form):
|
|
275
|
+
""" Read and unpack using the struct format ``form``. """
|
|
276
|
+
#if offset is not None:
|
|
277
|
+
# f.seek(offset)
|
|
278
|
+
return struct.unpack(
|
|
279
|
+
self._e + form, self._f.read(struct.calcsize(form)))
|
|
280
|
+
|
|
281
|
+
def read1(self, form):
|
|
282
|
+
"""
|
|
283
|
+
Returns the first item from a call to ``read(form)``.
|
|
284
|
+
|
|
285
|
+
This is useful for the many cases where a single number is read.
|
|
286
|
+
"""
|
|
287
|
+
return self.read(form)[0]
|
|
288
|
+
|
|
289
|
+
def str(self, size):
|
|
290
|
+
""" Read and unpack a string of at most length ``size``. """
|
|
291
|
+
b = struct.unpack(f'{size}s', self._f.read(size))[0]
|
|
292
|
+
b = b.decode(_ENC)
|
|
293
|
+
try:
|
|
294
|
+
return b[:b.index('\x00')]
|
|
295
|
+
except ValueError:
|
|
296
|
+
return b
|
|
297
|
+
|
|
298
|
+
def time(self):
|
|
299
|
+
"""
|
|
300
|
+
Reads a time in HEKA's seconds-since-1990 format, converts it, and
|
|
301
|
+
returns a ``datetime`` object.
|
|
302
|
+
"""
|
|
303
|
+
t = struct.unpack(f'{self._e}d', self._f.read(8))[0]
|
|
304
|
+
return datetime.datetime.fromtimestamp(t + _ts_1990, tz=_tz)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class TreeNode:
|
|
308
|
+
"""
|
|
309
|
+
Base class for objects within a PatchMaster file that form a tree.
|
|
310
|
+
|
|
311
|
+
This class may be useful when extending the patchmaster file reading, or to
|
|
312
|
+
read other HEKA formats. To read a patchmaster file, use the
|
|
313
|
+
``PatchMasterFile`` class.
|
|
314
|
+
|
|
315
|
+
For subclasses:
|
|
316
|
+
|
|
317
|
+
When reading a file, :class:`TreeNode` objects will be created by (1)
|
|
318
|
+
calling the constructor with a ``parent`` but no children. (2) Calling the
|
|
319
|
+
method :meth:`_read_properties` which should read record properties from
|
|
320
|
+
the open file handle and update the ``TreeNode`` accordingly. (3) Calling
|
|
321
|
+
the method :meth:`_read_finalize`, which can handl any actions that require
|
|
322
|
+
children to have been added.
|
|
323
|
+
"""
|
|
324
|
+
def __init__(self, parent):
|
|
325
|
+
self._parent = parent
|
|
326
|
+
self._children = []
|
|
327
|
+
try:
|
|
328
|
+
self._file = parent._file
|
|
329
|
+
except AttributeError:
|
|
330
|
+
self._file = parent
|
|
331
|
+
|
|
332
|
+
def __len__(self):
|
|
333
|
+
return len(self._children)
|
|
334
|
+
|
|
335
|
+
def __getitem__(self, key):
|
|
336
|
+
return self._children[key]
|
|
337
|
+
|
|
338
|
+
def __iter__(self):
|
|
339
|
+
return iter(self._children)
|
|
340
|
+
|
|
341
|
+
def parent(self):
|
|
342
|
+
""" Returns this tree TreeNode's parent. """
|
|
343
|
+
return self._parent
|
|
344
|
+
|
|
345
|
+
@staticmethod
|
|
346
|
+
def read(pfile, handle, levels):
|
|
347
|
+
"""
|
|
348
|
+
Reads a full HEKA "Tree" structure.
|
|
349
|
+
|
|
350
|
+
Arguments:
|
|
351
|
+
|
|
352
|
+
``pfile``
|
|
353
|
+
A :class:`PatchMasterFile`.
|
|
354
|
+
``handle``
|
|
355
|
+
An file handle, open at the tree root.
|
|
356
|
+
``levels``
|
|
357
|
+
The classes to use for each tree level.
|
|
358
|
+
|
|
359
|
+
Returns a TreeNode representing the tree's root.
|
|
360
|
+
"""
|
|
361
|
+
# Get endianness
|
|
362
|
+
m = handle.read(4).decode(_ENC)
|
|
363
|
+
if m == 'Tree':
|
|
364
|
+
is_little_endian = False
|
|
365
|
+
elif m == 'eerT':
|
|
366
|
+
is_little_endian = True
|
|
367
|
+
else:
|
|
368
|
+
raise ValueError( # pragma: no-cover
|
|
369
|
+
'Invalid or unsupported file: Unable to read tree.')
|
|
370
|
+
reader = EndianAwareReader(handle, is_little_endian)
|
|
371
|
+
|
|
372
|
+
# Number of levels in this tree
|
|
373
|
+
n = reader.read1('i')
|
|
374
|
+
sizes = reader.read(n * 'i')
|
|
375
|
+
if n != len(levels):
|
|
376
|
+
raise ValueError(
|
|
377
|
+
'Unexpected number of levels found in tree: expected'
|
|
378
|
+
f' ({len(levels)}), found ({n}).')
|
|
379
|
+
|
|
380
|
+
# Go!
|
|
381
|
+
return TreeNode._read(pfile, handle, reader, levels, sizes)
|
|
382
|
+
|
|
383
|
+
@staticmethod
|
|
384
|
+
def _read(parent, handle, reader, levels, sizes, depth=0):
|
|
385
|
+
"""
|
|
386
|
+
Recursively reads a node and its children.
|
|
387
|
+
|
|
388
|
+
Arguments:
|
|
389
|
+
|
|
390
|
+
``parent``
|
|
391
|
+
The parent node to the one being read. For the root object,
|
|
392
|
+
``parent`` is the :class:`PatchMasterFile`.
|
|
393
|
+
``handle``
|
|
394
|
+
A file handle, open at the record contents.
|
|
395
|
+
``reader``
|
|
396
|
+
A :class:`Reader` object using the handle.
|
|
397
|
+
``levels``
|
|
398
|
+
A list of classes to use for each level. The current level is
|
|
399
|
+
``levels[depth]``.
|
|
400
|
+
``sizes``
|
|
401
|
+
A list of record contents sizes. The current record size is
|
|
402
|
+
``sizes[depth]``.
|
|
403
|
+
``depth``
|
|
404
|
+
The current level being read.
|
|
405
|
+
|
|
406
|
+
"""
|
|
407
|
+
# Create node
|
|
408
|
+
node = levels[depth](parent)
|
|
409
|
+
|
|
410
|
+
# Read properties
|
|
411
|
+
pos = handle.tell()
|
|
412
|
+
node._read_properties(handle, reader)
|
|
413
|
+
|
|
414
|
+
# Add kids
|
|
415
|
+
handle.seek(pos + sizes[depth])
|
|
416
|
+
n = reader.read1('i')
|
|
417
|
+
for i in range(n):
|
|
418
|
+
node._children.append(TreeNode._read(
|
|
419
|
+
node, handle, reader, levels, sizes, depth + 1))
|
|
420
|
+
|
|
421
|
+
# Finalize and return
|
|
422
|
+
node._read_finalize()
|
|
423
|
+
return node
|
|
424
|
+
|
|
425
|
+
def _read_properties(self, handle, reader):
|
|
426
|
+
"""
|
|
427
|
+
Reads information from the file ``handle``, open at this node's record
|
|
428
|
+
start, and sets any properties not relating to the node's children.
|
|
429
|
+
|
|
430
|
+
For arguments meanings, see :meth:`_read`.
|
|
431
|
+
"""
|
|
432
|
+
pass
|
|
433
|
+
|
|
434
|
+
def _read_finalize(self):
|
|
435
|
+
"""
|
|
436
|
+
Performs any initialization actions that require children to have
|
|
437
|
+
already been set.
|
|
438
|
+
"""
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
#
|
|
443
|
+
# From PulsedFile_v9.txt
|
|
444
|
+
# RoVersion = 0; (* INT32 *)
|
|
445
|
+
# RoMark = 4; (* INT32 *)
|
|
446
|
+
# RoVersionName = 8; (* String32Type *)
|
|
447
|
+
# RoAuxFileName = 40; (* String80Type *)
|
|
448
|
+
# RoRootText = 120; (* String400Type *)
|
|
449
|
+
# RoStartTime = 520; (* LONGREAL *)
|
|
450
|
+
# RoMaxSamples = 528; (* INT32 *)
|
|
451
|
+
# RoCRC = 532; (* CARD32 *)
|
|
452
|
+
# RoFeatures = 536; (* SET16 *)
|
|
453
|
+
# RoFiller1 = 538; (* INT16 *)
|
|
454
|
+
# RoFiller2 = 540; (* INT32 *)
|
|
455
|
+
# RoTcEnumerator = 544; (* ARRAY[0..Max_TcKind_M1] OF INT16 *)
|
|
456
|
+
# RoTcKind = 608; (* ARRAY[0..Max_TcKind_M1] OF INT8 *)
|
|
457
|
+
# RootRecSize = 640; (* = 80 * 8 *)
|
|
458
|
+
#
|
|
459
|
+
# See also manual 14.1.1 Root
|
|
460
|
+
#
|
|
461
|
+
class PulsedFile(TreeNode):
|
|
462
|
+
"""
|
|
463
|
+
Represents the "pulsed file" section of a PatchMaster bundle (.pul).
|
|
464
|
+
|
|
465
|
+
Each ``PulsedFile`` contains zero or more :class:`Group` objects.
|
|
466
|
+
"""
|
|
467
|
+
def __init__(self, parent):
|
|
468
|
+
super().__init__(parent)
|
|
469
|
+
self._version = None
|
|
470
|
+
self._time = None
|
|
471
|
+
|
|
472
|
+
def _read_properties(self, handle, reader):
|
|
473
|
+
# See TreeNode._read_properties
|
|
474
|
+
start = handle.tell()
|
|
475
|
+
handle.seek(start + 8)
|
|
476
|
+
self._version = reader.str(32)
|
|
477
|
+
handle.seek(start + 520)
|
|
478
|
+
self._time = reader.time()
|
|
479
|
+
|
|
480
|
+
def time(self):
|
|
481
|
+
""" Returns the time this file was created as a ``datetime``. """
|
|
482
|
+
return self._time
|
|
483
|
+
|
|
484
|
+
def version(self):
|
|
485
|
+
"""
|
|
486
|
+
Returns a (hard-to-parse) string representation of this file's
|
|
487
|
+
PatchMaster format version.
|
|
488
|
+
"""
|
|
489
|
+
return self._version
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
#
|
|
493
|
+
# From PulsedFile_v9.txt
|
|
494
|
+
# GrMark = 0; (* INT32 *)
|
|
495
|
+
# GrLabel = 4; (* String32Size *)
|
|
496
|
+
# GrText = 36; (* String80Size *)
|
|
497
|
+
# GrExperimentNumber = 116; (* INT32 *)
|
|
498
|
+
# GrGroupCount = 120; (* INT32 *)
|
|
499
|
+
# GrCRC = 124; (* CARD32 *)
|
|
500
|
+
# GrMatrixWidth = 128; (* LONGREAL *) For imaging
|
|
501
|
+
# GrMatrixHeight = 136; (* LONGREAL *) For imaging
|
|
502
|
+
# GroupRecSize = 144; (* = 18 * 8 *)
|
|
503
|
+
#
|
|
504
|
+
# See also manual 14.1.2 Group
|
|
505
|
+
#
|
|
506
|
+
class Group(TreeNode):
|
|
507
|
+
"""
|
|
508
|
+
A PatchMaster group containing zero or more :class:`Series` objects.
|
|
509
|
+
"""
|
|
510
|
+
def __init__(self, parent):
|
|
511
|
+
super().__init__(parent)
|
|
512
|
+
self._label = None
|
|
513
|
+
self._number = None
|
|
514
|
+
|
|
515
|
+
def _read_properties(self, handle, reader):
|
|
516
|
+
# See TreeNode._read_properties
|
|
517
|
+
mark = reader.read1('i')
|
|
518
|
+
self._label = reader.str(32)
|
|
519
|
+
text = reader.str(80)
|
|
520
|
+
self._number = reader.read1('i')
|
|
521
|
+
|
|
522
|
+
def __str__(self):
|
|
523
|
+
return f'{self._label} ({len(self)} series)'
|
|
524
|
+
|
|
525
|
+
def complete_series(self):
|
|
526
|
+
"""
|
|
527
|
+
Returns an iterator over this group's complete series.
|
|
528
|
+
|
|
529
|
+
See :meth:`Series.is_complete()`.
|
|
530
|
+
"""
|
|
531
|
+
return filter(lambda series: series.is_complete(), self)
|
|
532
|
+
|
|
533
|
+
def label(self):
|
|
534
|
+
""" Returns this group's string label. """
|
|
535
|
+
return self._label
|
|
536
|
+
|
|
537
|
+
def number(self):
|
|
538
|
+
""" Returns this group's number (an integer index). """
|
|
539
|
+
return self._number
|
|
540
|
+
|
|
541
|
+
def series(self, label, n=0, complete_only=False):
|
|
542
|
+
"""
|
|
543
|
+
Returns the first series with the given ``label``.
|
|
544
|
+
|
|
545
|
+
If ``n`` is specified it returns the n+1th such series (i.e. the first
|
|
546
|
+
for n=0, the second for n=1, etc.).
|
|
547
|
+
"""
|
|
548
|
+
i = 0
|
|
549
|
+
for series in (self.complete_series() if complete_only else self):
|
|
550
|
+
if series.label() == label:
|
|
551
|
+
if i == n:
|
|
552
|
+
return series
|
|
553
|
+
i += 1
|
|
554
|
+
raise ValueError('Unable to find the requested series.')
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
#
|
|
558
|
+
# From PulsedFile_v9.txt
|
|
559
|
+
# (* SeriesRecord = RECORD *)
|
|
560
|
+
# SeMark = 0; (* INT32 *)
|
|
561
|
+
# SeLabel = 4; (* String32Type *)
|
|
562
|
+
# SeComment = 36; (* String80Type *)
|
|
563
|
+
# SeSeriesCount = 116; (* INT32 *)
|
|
564
|
+
# SeNumberSweeps = 120; (* INT32 *)
|
|
565
|
+
# SeAmplStateOffset = 124; (* INT32 *)
|
|
566
|
+
# SeAmplStateSeries = 128; (* INT32 *)
|
|
567
|
+
# SeMethodTag = 132; (* INT32 *)
|
|
568
|
+
# SeTime = 136; (* LONGREAL *)
|
|
569
|
+
# SePageWidth = 144; (* LONGREAL *)
|
|
570
|
+
# SeSwUserParamDescr = 152; (* ARRAY[0..3] OF UserParamDescrType = 4*40 *)
|
|
571
|
+
# SeMethodName = 312; (* String32Type *)
|
|
572
|
+
# SeSeUserParams1 = 344; (* ARRAY[0..3] OF LONGREAL *)
|
|
573
|
+
# SeLockInParams = 376; (* SeLockInSize = 96, see "Pulsed.de" *)
|
|
574
|
+
# SeAmplifierState = 472; (* AmplifierStateSize = 400 *)
|
|
575
|
+
# SeUsername = 872; (* String80Type *)
|
|
576
|
+
# SeSeUserParamDescr1 = 952; (* ARRAY[0..3] OF UserParamDescrType = 4*40 *)
|
|
577
|
+
# SeFiller1 = 1112; (* INT32 *)
|
|
578
|
+
# SeCRC = 1116; (* CARD32 *)
|
|
579
|
+
# SeSeUserParams2 = 1120; (* ARRAY[0..3] OF LONGREAL *)
|
|
580
|
+
# SeSeUserParamDescr2 = 1152; (* ARRAY[0..3] OF UserParamDescrType = 4*40 *)
|
|
581
|
+
# SeScanParams = 1312; (* ScanParamsSize = 96 *)
|
|
582
|
+
# SeriesRecSize = 1408; (* = 176 * 8 *)
|
|
583
|
+
#
|
|
584
|
+
# See also manual 14.1.3 Series
|
|
585
|
+
#
|
|
586
|
+
class Series(TreeNode, myokit.formats.SweepSource):
|
|
587
|
+
"""
|
|
588
|
+
A PatchMaster "series", containing zero or more :class:`Sweep` objects.
|
|
589
|
+
|
|
590
|
+
The data in a :class:`Series` can be obtained from its child :class:`Sweep`
|
|
591
|
+
and :class:`Trace` objects, or via the unified
|
|
592
|
+
:class:`myokit.formats.SweepSource` interface.
|
|
593
|
+
|
|
594
|
+
An example of native access::
|
|
595
|
+
|
|
596
|
+
for sweep in series:
|
|
597
|
+
for trace in sweep:
|
|
598
|
+
time, data = trace.times(), trace.values()
|
|
599
|
+
|
|
600
|
+
An example using the ``SweepSource`` interface::
|
|
601
|
+
|
|
602
|
+
log = trace.log(join_sweeps=False, use_names=False)
|
|
603
|
+
time = log.time()
|
|
604
|
+
data = log['0.0.channel']
|
|
605
|
+
|
|
606
|
+
Meta data is stored in two places in each series:
|
|
607
|
+
|
|
608
|
+
1. In the individual :class:`Trace` objects. This is somewhat
|
|
609
|
+
counter-intuitive as some of these properties (e.g.
|
|
610
|
+
:meth:Trace.pipette_resistance()`) were set before a series was
|
|
611
|
+
acquired and do no change between channels or sweeps.
|
|
612
|
+
2. In the series' :class:`AmplifierState`, which can be accessed via
|
|
613
|
+
the :meth:`amplifier_state()` method.
|
|
614
|
+
|
|
615
|
+
A summary of the meta data can be obtained using the ``SweepSource``
|
|
616
|
+
method :meth:`meta_str()`.
|
|
617
|
+
|
|
618
|
+
"""
|
|
619
|
+
def __init__(self, parent):
|
|
620
|
+
super().__init__(parent)
|
|
621
|
+
|
|
622
|
+
self._label = None
|
|
623
|
+
self._time = None
|
|
624
|
+
|
|
625
|
+
self._channel_names = None
|
|
626
|
+
self._channel_units = None
|
|
627
|
+
|
|
628
|
+
# Info from stimulus file
|
|
629
|
+
self._stimulus = None
|
|
630
|
+
self._intended_sweep_count = None
|
|
631
|
+
self._sweep_interval = None
|
|
632
|
+
self._sweep_durations = None
|
|
633
|
+
self._sweep_starts_s = None
|
|
634
|
+
|
|
635
|
+
# Info from sweeps
|
|
636
|
+
self._sweep_starts_r = None
|
|
637
|
+
|
|
638
|
+
# Info from amplifier
|
|
639
|
+
self._amplifier_state = None
|
|
640
|
+
|
|
641
|
+
def _read_properties(self, handle, reader):
|
|
642
|
+
# See TreeNode._read_properties
|
|
643
|
+
i = handle.tell()
|
|
644
|
+
handle.seek(i + 4) # SeLabel
|
|
645
|
+
self._label = reader.str(32)
|
|
646
|
+
handle.seek(i + 136) # SeTime
|
|
647
|
+
self._time = reader.time()
|
|
648
|
+
handle.seek(i + 472) # SeAmplifierState = 472; (* Size = 400 *)
|
|
649
|
+
self._amplifier_state = AmplifierState(handle, reader)
|
|
650
|
+
|
|
651
|
+
def _read_finalize(self):
|
|
652
|
+
# See TreeNode._read_finalize
|
|
653
|
+
|
|
654
|
+
# Recorded channels
|
|
655
|
+
if len(self) == 0 or len(self[0]) == 0:
|
|
656
|
+
self._channel_names = []
|
|
657
|
+
self._channel_units = []
|
|
658
|
+
else:
|
|
659
|
+
self._channel_names = [c.label() for c in self[0]]
|
|
660
|
+
self._channel_units = [c.value_unit() for c in self[0]]
|
|
661
|
+
|
|
662
|
+
# Get intended sweep count
|
|
663
|
+
tree = self._file.stimulus_tree()
|
|
664
|
+
self._stimulus = tree[self[0]._stimulus_id]
|
|
665
|
+
self._intended_sweep_count = self._stimulus.sweep_count()
|
|
666
|
+
|
|
667
|
+
# Get sweep interval (delay between sweeps)
|
|
668
|
+
self._sweep_interval = self._stimulus.sweep_interval()
|
|
669
|
+
|
|
670
|
+
# Get intended sweep durations
|
|
671
|
+
self._sweep_durations = self._stimulus.sweep_durations()
|
|
672
|
+
|
|
673
|
+
# Derive sweep starts from stimulus info
|
|
674
|
+
self._sweep_starts_s = np.zeros(self._sweep_durations.shape)
|
|
675
|
+
self._sweep_starts_s[1:] = (
|
|
676
|
+
np.cumsum(self._sweep_durations[:-1]) + self._sweep_interval)
|
|
677
|
+
|
|
678
|
+
# Get sweep starts based on time
|
|
679
|
+
t0 = self[0].time()
|
|
680
|
+
self._sweep_starts_r = [
|
|
681
|
+
(s.time() - t0).total_seconds() for s in self]
|
|
682
|
+
|
|
683
|
+
# Either 0 or 1 supported D/A output
|
|
684
|
+
c = self._stimulus.supported_channel()
|
|
685
|
+
if c is None:
|
|
686
|
+
self._da_names = []
|
|
687
|
+
self._da_units = []
|
|
688
|
+
else:
|
|
689
|
+
self._da_names = ['Stim-DA']
|
|
690
|
+
self._da_units = [c.unit()]
|
|
691
|
+
|
|
692
|
+
def __str__(self):
|
|
693
|
+
if len(self) == self._intended_sweep_count:
|
|
694
|
+
return f'{self._label} ({len(self)} sweeps)'
|
|
695
|
+
return (f'{self._label} (partial: {len(self)} out of'
|
|
696
|
+
f' {self._intended_sweep_count} sweeps)')
|
|
697
|
+
|
|
698
|
+
def amplifier_state(self):
|
|
699
|
+
"""
|
|
700
|
+
Returns this series's :class:`AmplifierState`, containing meta data
|
|
701
|
+
about the recording.
|
|
702
|
+
"""
|
|
703
|
+
return self._amplifier_state
|
|
704
|
+
|
|
705
|
+
def channel(self, channel_id, join_sweeps=False,
|
|
706
|
+
use_real_start_times=False):
|
|
707
|
+
"""
|
|
708
|
+
Implementation of :meth:`myokit.formats.SweepSource.channel`.
|
|
709
|
+
|
|
710
|
+
Sweep starts in the Patchmaster format can be derived from the stimulus
|
|
711
|
+
information (the intended start) or from the logged system time at the
|
|
712
|
+
start of each sweep. Ideally these should be the same. By default the
|
|
713
|
+
intended start times are used, but this can be changed to the system
|
|
714
|
+
clock derived times by setting ``use_real_start_times=True``.
|
|
715
|
+
"""
|
|
716
|
+
# Check channel id
|
|
717
|
+
if isinstance(channel_id, str):
|
|
718
|
+
channel_id = self._channel_names.index(channel_id)
|
|
719
|
+
else:
|
|
720
|
+
self._channel_names[channel_id] # IndexError/TypeError to user
|
|
721
|
+
|
|
722
|
+
# Get sweep starts
|
|
723
|
+
offsets = self._sweep_starts_s
|
|
724
|
+
if use_real_start_times:
|
|
725
|
+
offset = self._sweep_starts_r
|
|
726
|
+
|
|
727
|
+
# Gather data and return
|
|
728
|
+
time, data = [], []
|
|
729
|
+
for sweep, offset in zip(self, offsets):
|
|
730
|
+
time.append(offset + sweep[channel_id].times())
|
|
731
|
+
data.append(sweep[channel_id].values())
|
|
732
|
+
if join_sweeps:
|
|
733
|
+
return (np.concatenate(time), np.concatenate(data))
|
|
734
|
+
return time, data
|
|
735
|
+
|
|
736
|
+
def channel_count(self):
|
|
737
|
+
# Docstring in SweepSource
|
|
738
|
+
return len(self._channel_names)
|
|
739
|
+
|
|
740
|
+
def channel_names(self, index=None):
|
|
741
|
+
# Docstring in SweepSource
|
|
742
|
+
if index is None:
|
|
743
|
+
return list(self._channel_names)
|
|
744
|
+
return self._channel_names[index]
|
|
745
|
+
|
|
746
|
+
def channel_units(self, index=None):
|
|
747
|
+
# Docstring in SweepSource
|
|
748
|
+
if index is None:
|
|
749
|
+
return list(self._channel_units)
|
|
750
|
+
return self._channel_units[index]
|
|
751
|
+
|
|
752
|
+
def _da_id(self, output_id=None):
|
|
753
|
+
""" Checks an output_id is OK - just to conform to the interface. """
|
|
754
|
+
if output_id is None:
|
|
755
|
+
output_id = 0
|
|
756
|
+
|
|
757
|
+
if isinstance(output_id, str):
|
|
758
|
+
output_id = self._da_names.index(output_id)
|
|
759
|
+
else:
|
|
760
|
+
self._da_names[output_id] # IndexError/TypeError to user
|
|
761
|
+
|
|
762
|
+
def da(self, output_id=None, join_sweeps=False):
|
|
763
|
+
# Docstring in SweepSource
|
|
764
|
+
self._da_id(output_id)
|
|
765
|
+
return self._stimulus.reconstruction(join_sweeps)
|
|
766
|
+
|
|
767
|
+
def da_count(self):
|
|
768
|
+
# Docstring in SweepSource
|
|
769
|
+
return len(self._da_names)
|
|
770
|
+
|
|
771
|
+
def da_names(self, index=None):
|
|
772
|
+
# Docstring in SweepSource
|
|
773
|
+
if index is None:
|
|
774
|
+
return list(self._da_names)
|
|
775
|
+
return self._da_names[index]
|
|
776
|
+
|
|
777
|
+
def da_protocol(self, output_id=None, tu='ms', vu='mV', cu='pA',
|
|
778
|
+
n_digits=9):
|
|
779
|
+
# Docstring in SweepSource
|
|
780
|
+
self._da_id(output_id)
|
|
781
|
+
return self._stimulus.protocol(tu='ms', vu='mV', cu='pA', n_digits=9)
|
|
782
|
+
|
|
783
|
+
def da_units(self, index=None):
|
|
784
|
+
# Docstring in SweepSource
|
|
785
|
+
if index is None:
|
|
786
|
+
return list(self._da_units)
|
|
787
|
+
return self._da_units[index]
|
|
788
|
+
|
|
789
|
+
def equal_length_sweeps(self):
|
|
790
|
+
# Docstring in SweepSource
|
|
791
|
+
|
|
792
|
+
# Get length of any trace
|
|
793
|
+
n = 0
|
|
794
|
+
for sweep in self:
|
|
795
|
+
for trace in sweep:
|
|
796
|
+
if len(trace):
|
|
797
|
+
n = len(trace)
|
|
798
|
+
break
|
|
799
|
+
if n == 0:
|
|
800
|
+
return True
|
|
801
|
+
|
|
802
|
+
# Check all traces
|
|
803
|
+
for sweep in self:
|
|
804
|
+
for trace in sweep:
|
|
805
|
+
if len(trace) != n:
|
|
806
|
+
return False
|
|
807
|
+
|
|
808
|
+
return True
|
|
809
|
+
|
|
810
|
+
def is_complete(self):
|
|
811
|
+
"""
|
|
812
|
+
Returns ``False`` if this series was aborted before the recording was
|
|
813
|
+
completed.
|
|
814
|
+
|
|
815
|
+
The full check can only be run if this channel's Stimulus can be
|
|
816
|
+
analysed to determine the intended number of samples in each sweep. If
|
|
817
|
+
the stimulus contains unsupported features, this part of the check will
|
|
818
|
+
not be run.
|
|
819
|
+
"""
|
|
820
|
+
if len(self) != self._intended_sweep_count:
|
|
821
|
+
return False
|
|
822
|
+
|
|
823
|
+
try:
|
|
824
|
+
n_samples = self._stimulus.sweep_samples()
|
|
825
|
+
for sweep, expected in zip(self, n_samples):
|
|
826
|
+
for ch in sweep:
|
|
827
|
+
if len(ch) != expected:
|
|
828
|
+
return False
|
|
829
|
+
except NoSupportedDAChannelError:
|
|
830
|
+
return True
|
|
831
|
+
|
|
832
|
+
return True
|
|
833
|
+
|
|
834
|
+
def label(self):
|
|
835
|
+
""" Returns this series's label. """
|
|
836
|
+
return self._label
|
|
837
|
+
|
|
838
|
+
def log(self, join_sweeps=False, use_names=False, include_da=True,
|
|
839
|
+
use_real_start_times=False):
|
|
840
|
+
"""
|
|
841
|
+
See :meth:`myokit.formats.SweepSource.log`.
|
|
842
|
+
|
|
843
|
+
A D/A reconstruction will only be included if the stimulus type is
|
|
844
|
+
supported, and if all segments are stored.
|
|
845
|
+
|
|
846
|
+
Sweep starts in the Patchmaster format can be derived from the stimulus
|
|
847
|
+
information (the intended start) or from the logged system time at the
|
|
848
|
+
start of each sweep. This method uses the intended start times, but
|
|
849
|
+
this can be changed by setting ``use_real_start_times=True``.
|
|
850
|
+
"""
|
|
851
|
+
|
|
852
|
+
# Create log
|
|
853
|
+
log = myokit.DataLog()
|
|
854
|
+
ns = len(self)
|
|
855
|
+
if ns == 0: # pragma: no cover
|
|
856
|
+
return log
|
|
857
|
+
|
|
858
|
+
# Get channel names
|
|
859
|
+
channel_names = self._channel_names
|
|
860
|
+
da_names = self._da_names
|
|
861
|
+
if not use_names:
|
|
862
|
+
channel_names = [f'{i}.channel' for i in range(len(channel_names))]
|
|
863
|
+
da_names = [f'{i}.da' for i in range(len(da_names))]
|
|
864
|
+
|
|
865
|
+
# Don't include D/A if it has a different length than the data
|
|
866
|
+
if include_da and len(da_names) > 0: # This means it's supported
|
|
867
|
+
if not self._stimulus.all_segments_stored():
|
|
868
|
+
include_da = False
|
|
869
|
+
|
|
870
|
+
# Populate log
|
|
871
|
+
if join_sweeps:
|
|
872
|
+
# Join sweeps
|
|
873
|
+
offsets = (self._sweep_starts_r if use_real_start_times
|
|
874
|
+
else self._sweep_starts_s)
|
|
875
|
+
log['time'] = np.concatenate(
|
|
876
|
+
[offset + s[0].times() for s, offset in zip(self, offsets)])
|
|
877
|
+
for c, name in enumerate(channel_names):
|
|
878
|
+
log[name] = np.concatenate(
|
|
879
|
+
[sweep[c].values() for sweep in self])
|
|
880
|
+
if include_da and len(da_names) == 1:
|
|
881
|
+
log[da_names[0]] = self.da(join_sweeps=True)[1]
|
|
882
|
+
|
|
883
|
+
else:
|
|
884
|
+
# Individual sweeps
|
|
885
|
+
log['time'] = self[0][0].times()
|
|
886
|
+
for i, sweep in enumerate(self):
|
|
887
|
+
for j, name in enumerate(channel_names):
|
|
888
|
+
log[name, i] = sweep[j].values()
|
|
889
|
+
if include_da and len(da_names) == 1:
|
|
890
|
+
_, vs = self.da(join_sweeps=False)
|
|
891
|
+
for i, v in enumerate(vs):
|
|
892
|
+
log[da_names[0], i] = v
|
|
893
|
+
|
|
894
|
+
log.set_time_key('time')
|
|
895
|
+
return log
|
|
896
|
+
|
|
897
|
+
def meta_str(self, verbose=False):
|
|
898
|
+
# Docstring in SweepSource
|
|
899
|
+
out = []
|
|
900
|
+
|
|
901
|
+
# Basic info
|
|
902
|
+
out.append(f'Series {self._label}')
|
|
903
|
+
out.append(f' in {self._parent.label()}')
|
|
904
|
+
out.append(f' {self._file.path()}')
|
|
905
|
+
out.append(f' version {self._file.version()}')
|
|
906
|
+
out.append(f'Recorded on {self._time}')
|
|
907
|
+
out.append(f'{len(self)} sweeps,'
|
|
908
|
+
f' {len(self._channel_names)} channels.')
|
|
909
|
+
|
|
910
|
+
# Completion status
|
|
911
|
+
c = self._stimulus.supported_channel()
|
|
912
|
+
if c is None:
|
|
913
|
+
out.append('Unable to determine if recording was completed.')
|
|
914
|
+
else:
|
|
915
|
+
if self.is_complete():
|
|
916
|
+
out.append('Complete recording: all sweeps ran and completed.')
|
|
917
|
+
elif self._intended_sweep_count == len(self):
|
|
918
|
+
out.append('Incomplete recording: final sweep not completed.')
|
|
919
|
+
else:
|
|
920
|
+
out.append(f'Incomplete recording: {len(self)} out of'
|
|
921
|
+
f' {self._intended_sweep_count} ran.')
|
|
922
|
+
|
|
923
|
+
# Resistance, capacitance, etc.
|
|
924
|
+
a = self.amplifier_state()
|
|
925
|
+
out.append('Information from amplifier state:')
|
|
926
|
+
if a.ljp():
|
|
927
|
+
out.append(' LJP correction applied using'
|
|
928
|
+
f' LJP={round(a.ljp(), 4)} mV.')
|
|
929
|
+
if a.c_fast_enabled():
|
|
930
|
+
out.append(f' C fast compensation: {a.c_fast()} pF,'
|
|
931
|
+
f' {round(a.c_fast_tau(), 4)} us.')
|
|
932
|
+
else:
|
|
933
|
+
out.append(' C fast compensation: not enabled.')
|
|
934
|
+
out.append(f' C slow compensation: {a.c_slow()} pF.')
|
|
935
|
+
out.append(f' R series: {a.r_series()} MOhm.')
|
|
936
|
+
if a.r_series_enabled():
|
|
937
|
+
p = round(a.r_series_fraction() * 100, 1)
|
|
938
|
+
out.append(f' R series compensation: {p} %.')
|
|
939
|
+
else:
|
|
940
|
+
out.append(' R series compensation: not enabled')
|
|
941
|
+
if len(self) and len(self[0]):
|
|
942
|
+
t = self[0][0]
|
|
943
|
+
out.append('Information from first trace:')
|
|
944
|
+
|
|
945
|
+
out.append(f' Pipette resistance: {t.r_pipette()} MOhm.')
|
|
946
|
+
out.append(f' Seal resistance: {t.r_seal()} MOhm.')
|
|
947
|
+
out.append(f' Series resistance: {t.r_series()} MOhm.')
|
|
948
|
+
out.append(f' after compensation: {t.r_series_remaining()}'
|
|
949
|
+
f' MOhm.')
|
|
950
|
+
out.append(f' C slow: {t.c_slow()} pF.')
|
|
951
|
+
|
|
952
|
+
# Sweeps and channels
|
|
953
|
+
if verbose:
|
|
954
|
+
out.append('-' * 60)
|
|
955
|
+
for i, sweep in enumerate(self):
|
|
956
|
+
out.append(f'Sweep {i}, label: "{sweep.label()}", recorded on'
|
|
957
|
+
f' {sweep.time()}.')
|
|
958
|
+
if i == 0:
|
|
959
|
+
for j, trace in enumerate(self[0]):
|
|
960
|
+
out.append(f' Trace {j}, label: "{trace.label()}",'
|
|
961
|
+
f' in {trace.time_unit()} and'
|
|
962
|
+
f' {trace.value_unit()}.')
|
|
963
|
+
|
|
964
|
+
# Stimulus
|
|
965
|
+
if verbose:
|
|
966
|
+
stim = self._stimulus
|
|
967
|
+
out.append('-' * 60)
|
|
968
|
+
out.append(f'Stimulus "{stim.label()}".')
|
|
969
|
+
out.append(f' {stim.sweep_count()} sweeps.')
|
|
970
|
+
out.append(f' Delay between sweeps: {stim.sweep_interval()} s.')
|
|
971
|
+
out.append(f' Sampling interval: {stim.sampling_interval()} s.')
|
|
972
|
+
for i, ch in enumerate(stim):
|
|
973
|
+
out.append(f' Channel {i}, in {ch.unit()}, amplifier in'
|
|
974
|
+
f' {ch.amplifier_mode()} mode.')
|
|
975
|
+
out.append(f' Stimulus reconstruction: {ch.support_str()}.')
|
|
976
|
+
for j, seg in enumerate(ch):
|
|
977
|
+
out.append(f' Segment {j}, {seg.storage()}')
|
|
978
|
+
out.append(f' {seg.segment_class()}, {seg}')
|
|
979
|
+
|
|
980
|
+
return '\n'.join(out)
|
|
981
|
+
|
|
982
|
+
def stimulus(self):
|
|
983
|
+
""" Returns the :class:`Stimulus` linked to this series. """
|
|
984
|
+
return self._stimulus
|
|
985
|
+
|
|
986
|
+
def sweep_count(self):
|
|
987
|
+
# Docstring in SweepSource
|
|
988
|
+
return len(self)
|
|
989
|
+
|
|
990
|
+
def time(self):
|
|
991
|
+
""" Returns the time this series was created as a ``datetime``. """
|
|
992
|
+
return self._time
|
|
993
|
+
|
|
994
|
+
def time_unit(self):
|
|
995
|
+
# Docstring in SweepSource
|
|
996
|
+
return myokit.units.s
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
#
|
|
1000
|
+
# From PulsedFile_v9.txt
|
|
1001
|
+
# (* SweepRecord = RECORD *)
|
|
1002
|
+
# SwMark = 0; (* INT32 *)
|
|
1003
|
+
# SwLabel = 4; (* String32Type *)
|
|
1004
|
+
# SwAuxDataFileOffset = 36; (* INT32 *)
|
|
1005
|
+
# SwStimCount = 40; (* INT32 *)
|
|
1006
|
+
# SwSweepCount = 44; (* INT32 *)
|
|
1007
|
+
# SwTime = 48; (* LONGREAL *)
|
|
1008
|
+
# SwTimer = 56; (* LONGREAL *)
|
|
1009
|
+
# SwSwUserParams = 64; (* ARRAY[0..3] OF LONGREAL *)
|
|
1010
|
+
# SwTemperature = 96; (* LONGREAL *) From an external device
|
|
1011
|
+
# SwOldIntSol = 104; (* INT32 *)
|
|
1012
|
+
# SwOldExtSol = 108; (* INT32 *)
|
|
1013
|
+
# SwDigitalIn = 112; (* SET16 *)
|
|
1014
|
+
# SwSweepKind = 114; (* SET16 *)
|
|
1015
|
+
# SwDigitalOut = 116; (* SET16 *)
|
|
1016
|
+
# SwFiller1 = 118; (* INT16 *)
|
|
1017
|
+
# SwSwMarkers = 120; (* ARRAY[0..3] OF LONGREAL, see SwMarkersNo *)
|
|
1018
|
+
# SwFiller2 = 152; (* INT32 *)
|
|
1019
|
+
# SwCRC = 156; (* CARD32 *)
|
|
1020
|
+
# SwSwHolding = 160; (* ARRAY[0..15] OF LONGREAL, see SwHoldingNo *)
|
|
1021
|
+
# SweepRecSize = 288; (* = 36 * 8 *)
|
|
1022
|
+
#
|
|
1023
|
+
# See also manual 14.1.4 Sweep
|
|
1024
|
+
#
|
|
1025
|
+
class Sweep(TreeNode):
|
|
1026
|
+
"""
|
|
1027
|
+
A sweep, containing zero or more :class:`Trace` objects.
|
|
1028
|
+
"""
|
|
1029
|
+
def __init__(self, parent):
|
|
1030
|
+
super().__init__(parent)
|
|
1031
|
+
self._label = None
|
|
1032
|
+
self._time = None
|
|
1033
|
+
|
|
1034
|
+
# Seconds since first sweep, based on self._time, set by parent series
|
|
1035
|
+
self._time_since_first = None
|
|
1036
|
+
|
|
1037
|
+
#self._data_offset = None
|
|
1038
|
+
self._stimulus_id = None
|
|
1039
|
+
|
|
1040
|
+
def _read_properties(self, handle, reader):
|
|
1041
|
+
# See TreeNode._read_properties
|
|
1042
|
+
i = handle.tell()
|
|
1043
|
+
handle.seek(i + 4) # SwLabel = 4; (* String32Type *)
|
|
1044
|
+
self._label = reader.str(32)
|
|
1045
|
+
#self._data_offset = reader.read1('i')
|
|
1046
|
+
handle.seek(i + 40) # SwStimCount = 40; (* INT32 *)
|
|
1047
|
+
self._stimulus_id = reader.read1('i') - 1
|
|
1048
|
+
handle.seek(i + 48) # SwTime = 48; (* LONGREAL *)
|
|
1049
|
+
self._time = reader.time()
|
|
1050
|
+
|
|
1051
|
+
def duration(self):
|
|
1052
|
+
"""
|
|
1053
|
+
Returns the maximum :meth:`duration()` of all this sweep's channels.
|
|
1054
|
+
"""
|
|
1055
|
+
return max(channel.duration() for channel in self)
|
|
1056
|
+
|
|
1057
|
+
def label(self):
|
|
1058
|
+
""" Returns this sweep's string label. """
|
|
1059
|
+
return self._label
|
|
1060
|
+
|
|
1061
|
+
def time(self):
|
|
1062
|
+
""" Returns the time this sweep was recorded as a ``datetime``. """
|
|
1063
|
+
return self._time
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
#
|
|
1067
|
+
# From PulsedFile_v9.txt
|
|
1068
|
+
# (* TraceRecord = RECORD *)
|
|
1069
|
+
# TrMark = 0; (* INT32 *)
|
|
1070
|
+
# TrLabel = 4; (* String32Type *)
|
|
1071
|
+
# TrTraceID = 36; (* INT32 *)
|
|
1072
|
+
# TrData = 40; (* INT32 *)
|
|
1073
|
+
# TrDataPoints = 44; (* INT32 *)
|
|
1074
|
+
# TrInternalSolution = 48; (* INT32 *)
|
|
1075
|
+
# TrAverageCount = 52; (* INT32 *)
|
|
1076
|
+
# TrLeakID = 56; (* INT32 *)
|
|
1077
|
+
# TrLeakTraces = 60; (* INT32 *)
|
|
1078
|
+
# TrDataKind = 64; (* SET16 *)
|
|
1079
|
+
# TrUseXStart = 66; (* BOOLEAN *)
|
|
1080
|
+
# TrTcKind = 67; (* BYTE *)
|
|
1081
|
+
# TrRecordingMode = 68; (* BYTE *)
|
|
1082
|
+
# TrAmplIndex = 69; (* CHAR *)
|
|
1083
|
+
# TrDataFormat = 70; (* BYTE *)
|
|
1084
|
+
# TrDataAbscissa = 71; (* BYTE *)
|
|
1085
|
+
# TrDataScaler = 72; (* LONGREAL *)
|
|
1086
|
+
# TrTimeOffset = 80; (* LONGREAL *)
|
|
1087
|
+
# TrZeroData = 88; (* LONGREAL *)
|
|
1088
|
+
# TrYUnit = 96; (* String8Type *)
|
|
1089
|
+
# TrXInterval = 104; (* LONGREAL *)
|
|
1090
|
+
# TrXStart = 112; (* LONGREAL *)
|
|
1091
|
+
# TrXUnit = 120; (* String8Type *)
|
|
1092
|
+
# TrYRange = 128; (* LONGREAL *)
|
|
1093
|
+
# TrYOffset = 136; (* LONGREAL *)
|
|
1094
|
+
# TrBandwidth = 144; (* LONGREAL *)
|
|
1095
|
+
# TrPipetteResistance = 152; (* LONGREAL *)
|
|
1096
|
+
# TrCellPotential = 160; (* LONGREAL *)
|
|
1097
|
+
# TrSealResistance = 168; (* LONGREAL *)
|
|
1098
|
+
# TrCSlow = 176; (* LONGREAL *)
|
|
1099
|
+
# TrGSeries = 184; (* LONGREAL *)
|
|
1100
|
+
# TrRsValue = 192; (* LONGREAL *)
|
|
1101
|
+
# TrGLeak = 200; (* LONGREAL *)
|
|
1102
|
+
# TrMConductance = 208; (* LONGREAL *)
|
|
1103
|
+
# TrLinkDAChannel = 216; (* INT32 *)
|
|
1104
|
+
# TrValidYrange = 220; (* BOOLEAN *)
|
|
1105
|
+
# TrAdcMode = 221; (* CHAR *)
|
|
1106
|
+
# TrAdcChannel = 222; (* INT16 *)
|
|
1107
|
+
# TrYmin = 224; (* LONGREAL *)
|
|
1108
|
+
# TrYmax = 232; (* LONGREAL *)
|
|
1109
|
+
# TrSourceChannel = 240; (* INT32 *)
|
|
1110
|
+
# TrExternalSolution = 244; (* INT32 *)
|
|
1111
|
+
# TrCM = 248; (* LONGREAL *)
|
|
1112
|
+
# TrGM = 256; (* LONGREAL *)
|
|
1113
|
+
# TrPhase = 264; (* LONGREAL *)
|
|
1114
|
+
# TrDataCRC = 272; (* CARD32 *)
|
|
1115
|
+
# TrCRC = 276; (* CARD32 *)
|
|
1116
|
+
# TrGS = 280; (* LONGREAL *)
|
|
1117
|
+
# TrSelfChannel = 288; (* INT32 *)
|
|
1118
|
+
# TrInterleaveSize = 292; (* INT32 *)
|
|
1119
|
+
# TrInterleaveSkip = 296; (* INT32 *)
|
|
1120
|
+
# TrImageIndex = 300; (* INT32 *)
|
|
1121
|
+
# TrTrMarkers = 304; (* ARRAY[0..9] OF LONGREAL *)
|
|
1122
|
+
# TrSECM_X = 384; (* LONGREAL *)
|
|
1123
|
+
# TrSECM_Y = 392; (* LONGREAL *)
|
|
1124
|
+
# TrSECM_Z = 400; (* LONGREAL *)
|
|
1125
|
+
# TrTrHolding = 408; (* LONGREAL *)
|
|
1126
|
+
# TrTcEnumerator = 416; (* INT32 *)
|
|
1127
|
+
# TrXTrace = 420; (* INT32 *)
|
|
1128
|
+
# TrIntSolValue = 424; (* LONGREAL *)
|
|
1129
|
+
# TrExtSolValue = 432; (* LONGREAL *)
|
|
1130
|
+
# TrIntSolName = 440; (* String32Size *)
|
|
1131
|
+
# TrExtSolName = 472; (* String32Size *)
|
|
1132
|
+
# TrDataPedestal = 504; (* LONGREAL *)
|
|
1133
|
+
# TraceRecSize = 512; (* = 64 * 8 *)
|
|
1134
|
+
#
|
|
1135
|
+
# See also manual 14.1.5 Trace
|
|
1136
|
+
#
|
|
1137
|
+
class Trace(TreeNode):
|
|
1138
|
+
"""
|
|
1139
|
+
A trace from a :class:`Sweep`.
|
|
1140
|
+
|
|
1141
|
+
Data can be accessed via :meth:`values` (causing it to be read from disk).
|
|
1142
|
+
|
|
1143
|
+
The number of data points can be accessed with :meth:`count_samples()` or
|
|
1144
|
+
``len(trace)`` (this does not require reading anything from disk).
|
|
1145
|
+
"""
|
|
1146
|
+
def __init__(self, parent):
|
|
1147
|
+
super().__init__(parent)
|
|
1148
|
+
|
|
1149
|
+
# Handle, for lazy reading
|
|
1150
|
+
self._handle = None
|
|
1151
|
+
|
|
1152
|
+
# Label
|
|
1153
|
+
self._label = None
|
|
1154
|
+
|
|
1155
|
+
# Raw data
|
|
1156
|
+
self._n = None # Number of points
|
|
1157
|
+
self._data_pos = None # Data offset (in bytes)
|
|
1158
|
+
self._data_type = None # Data type (struct code)
|
|
1159
|
+
self._data_size = None # Size of a point
|
|
1160
|
+
self._data_scale = None # Scaling from raw to with-unit
|
|
1161
|
+
self._data_interleave_size = None # 0 or more if interleaved
|
|
1162
|
+
self._data_interleave_skip = None # distance to next block
|
|
1163
|
+
|
|
1164
|
+
# Time
|
|
1165
|
+
self._t0 = None # Trace start
|
|
1166
|
+
self._dt = None # Sampling interval
|
|
1167
|
+
|
|
1168
|
+
# Units
|
|
1169
|
+
self._data_unit = None
|
|
1170
|
+
|
|
1171
|
+
# Meta data
|
|
1172
|
+
self._r_pipette = None
|
|
1173
|
+
self._r_seal = None
|
|
1174
|
+
self._r_series_comp = None
|
|
1175
|
+
self._g_series = None
|
|
1176
|
+
self._c_slow = None
|
|
1177
|
+
|
|
1178
|
+
def _read_properties(self, handle, reader):
|
|
1179
|
+
# See TreeNode._read_properties
|
|
1180
|
+
self._handle = handle
|
|
1181
|
+
i = handle.tell()
|
|
1182
|
+
|
|
1183
|
+
# Label
|
|
1184
|
+
handle.seek(i + 4)
|
|
1185
|
+
self._label = reader.str(32)
|
|
1186
|
+
|
|
1187
|
+
# Raw data
|
|
1188
|
+
handle.seek(i + 40) # TrData
|
|
1189
|
+
self._data_pos = reader.read1('i')
|
|
1190
|
+
self._n = reader.read1('i')
|
|
1191
|
+
dtype = int(reader.read1('b'))
|
|
1192
|
+
self._data_type = _data_types[dtype]
|
|
1193
|
+
self._data_size = _data_sizes[dtype]
|
|
1194
|
+
handle.seek(i + 72) # TrDataScaler
|
|
1195
|
+
self._data_scale = reader.read1('d')
|
|
1196
|
+
handle.seek(i + 88) # TrZeroData
|
|
1197
|
+
self._data_zero = reader.read1('d')
|
|
1198
|
+
handle.seek(i + 292) # TrInterleaveSize
|
|
1199
|
+
self._data_interleave_size = reader.read1('i')
|
|
1200
|
+
self._data_interleave_skip = reader.read1('i')
|
|
1201
|
+
|
|
1202
|
+
# Time
|
|
1203
|
+
# Note: There is a boolean field TrUseXStart, but scripts online all
|
|
1204
|
+
# seem to be ignoring this and the HEKA demo data sets non-zero
|
|
1205
|
+
# TrXStart without ever setting TrUseXStart to true.
|
|
1206
|
+
handle.seek(i + 80) # TrTimeOffset
|
|
1207
|
+
self._t0 = reader.read1('d')
|
|
1208
|
+
handle.seek(i + 104) # TrXInterval
|
|
1209
|
+
self._dt = reader.read1('d')
|
|
1210
|
+
handle.seek(i + 112) # TrXStart
|
|
1211
|
+
self._t0 += reader.read1('d')
|
|
1212
|
+
|
|
1213
|
+
# Units
|
|
1214
|
+
handle.seek(i + 96) # TrYUnit
|
|
1215
|
+
self._data_unit = reader.str(8)
|
|
1216
|
+
handle.seek(i + 120) # TrXUnit
|
|
1217
|
+
time_unit = reader.str(8)
|
|
1218
|
+
assert time_unit == 's'
|
|
1219
|
+
|
|
1220
|
+
# Meta data
|
|
1221
|
+
handle.seek(i + 152) # TrPipetteResistance = 152; (* LONGREAL *)
|
|
1222
|
+
self._r_pipette = reader.read1('d')
|
|
1223
|
+
handle.seek(i + 168) # TrSealResistance = 168; (* LONGREAL *)
|
|
1224
|
+
self._r_seal = reader.read1('d')
|
|
1225
|
+
handle.seek(i + 176) # TrCSlow = 176; (* LONGREAL *)
|
|
1226
|
+
self._c_slow = reader.read1('d')
|
|
1227
|
+
handle.seek(i + 184) # TrGSeries = 184; (* LONGREAL *)
|
|
1228
|
+
self._g_series = reader.read1('d')
|
|
1229
|
+
handle.seek(i + 192) # TrRsValue = 192; (* LONGREAL *)
|
|
1230
|
+
self._r_series_comp = reader.read1('d')
|
|
1231
|
+
|
|
1232
|
+
# Convert unit
|
|
1233
|
+
self._data_unit = myokit.parse_unit(self._data_unit)
|
|
1234
|
+
|
|
1235
|
+
def __len__(self):
|
|
1236
|
+
return self._n
|
|
1237
|
+
|
|
1238
|
+
def count_samples(self):
|
|
1239
|
+
""" Returns the number of samples in this trace. """
|
|
1240
|
+
return self._n
|
|
1241
|
+
|
|
1242
|
+
def c_slow(self):
|
|
1243
|
+
"""
|
|
1244
|
+
Returns the capacitance (pF) compensated by the slow capacitance
|
|
1245
|
+
compensation (i.e. the membrane capacitance).
|
|
1246
|
+
"""
|
|
1247
|
+
return self._c_slow * 1e12
|
|
1248
|
+
|
|
1249
|
+
def duration(self):
|
|
1250
|
+
""" Returns the total duration of this sweep's recorded data. """
|
|
1251
|
+
return self._n * self._dt
|
|
1252
|
+
|
|
1253
|
+
def label(self):
|
|
1254
|
+
""" Returns this trace's label. """
|
|
1255
|
+
return self._label
|
|
1256
|
+
|
|
1257
|
+
def r_seal(self):
|
|
1258
|
+
"""
|
|
1259
|
+
Returns the seal resistance (MOhm) determined from the test pulse
|
|
1260
|
+
before the trace was acquired.
|
|
1261
|
+
"""
|
|
1262
|
+
return self._r_seal * 1e-6
|
|
1263
|
+
|
|
1264
|
+
def r_series(self):
|
|
1265
|
+
"""
|
|
1266
|
+
Returns the last (uncompensated) series resistance (MOhm) before
|
|
1267
|
+
acquiring the trace.
|
|
1268
|
+
"""
|
|
1269
|
+
return 1e-6 / self._g_series
|
|
1270
|
+
|
|
1271
|
+
def r_series_remaining(self):
|
|
1272
|
+
"""
|
|
1273
|
+
Returns the series resistance (MOhm) remaining after compensation.
|
|
1274
|
+
"""
|
|
1275
|
+
# "Absolute fraction of the compensated R-series value. The value
|
|
1276
|
+
# depends on the % of R-series compensation."
|
|
1277
|
+
return (1 / self._g_series - self._r_series_comp) * 1e-6
|
|
1278
|
+
|
|
1279
|
+
def r_pipette(self):
|
|
1280
|
+
"""
|
|
1281
|
+
Returns the pipette resistance (MOhm) determined from the test pulse
|
|
1282
|
+
before breaking the seal.
|
|
1283
|
+
|
|
1284
|
+
This was manually logged when the "R-memb to R-pip" button was pressed
|
|
1285
|
+
before acquiring the data.
|
|
1286
|
+
"""
|
|
1287
|
+
return self._r_pipette * 1e-6
|
|
1288
|
+
|
|
1289
|
+
def times(self):
|
|
1290
|
+
""" Recreates and returns a time vector for this trace. """
|
|
1291
|
+
return self._t0 + np.arange(self._n) * self._dt
|
|
1292
|
+
|
|
1293
|
+
def time_unit(self):
|
|
1294
|
+
""" Returns a string version of the units on the time axis. """
|
|
1295
|
+
return myokit.units.s
|
|
1296
|
+
|
|
1297
|
+
def values(self):
|
|
1298
|
+
""" Reads and returns this trace's data. """
|
|
1299
|
+
|
|
1300
|
+
if self._data_interleave_size == 0 or self._data_interleave_skip == 0:
|
|
1301
|
+
# Read continuous data
|
|
1302
|
+
d = np.memmap(self._handle, self._data_type, 'r',
|
|
1303
|
+
offset=self._data_pos, shape=(self._n,))
|
|
1304
|
+
else:
|
|
1305
|
+
# Read interleaved data
|
|
1306
|
+
# points_per_block = self._data_interleave_size / self._data_size
|
|
1307
|
+
# n_blocks = np.ceil(self._n / points_per_block)
|
|
1308
|
+
raise NotImplementedError('Interleaved data is not supported.')
|
|
1309
|
+
|
|
1310
|
+
return d * self._data_scale - self._data_zero
|
|
1311
|
+
|
|
1312
|
+
def value_unit(self):
|
|
1313
|
+
""" Returns a string version of the units on the data axis. """
|
|
1314
|
+
return self._data_unit
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
# (* AmplifierState = RECORD *)
|
|
1318
|
+
# sStateVersion = 0; (* 8 = SizeStateVersion *)
|
|
1319
|
+
# sCurrentGain = 8; (* LONGREAL *)
|
|
1320
|
+
# sF2Bandwidth = 16; (* LONGREAL *)
|
|
1321
|
+
# sF2Frequency = 24; (* LONGREAL *)
|
|
1322
|
+
# sRsValue = 32; (* LONGREAL *)
|
|
1323
|
+
# sRsFraction = 40; (* LONGREAL *)
|
|
1324
|
+
# sGLeak = 48; (* LONGREAL *)
|
|
1325
|
+
# sCFastAmp1 = 56; (* LONGREAL *)
|
|
1326
|
+
# sCFastAmp2 = 64; (* LONGREAL *)
|
|
1327
|
+
# sCFastTau = 72; (* LONGREAL *)
|
|
1328
|
+
# sCSlow = 80; (* LONGREAL *)
|
|
1329
|
+
# sGSeries = 88; (* LONGREAL *)
|
|
1330
|
+
# sVCStimDacScale = 96; (* LONGREAL *)
|
|
1331
|
+
# sCCStimScale = 104; (* LONGREAL *)
|
|
1332
|
+
# sVHold = 112; (* LONGREAL *)
|
|
1333
|
+
# sLastVHold = 120; (* LONGREAL *)
|
|
1334
|
+
# sVpOffset = 128; (* LONGREAL *)
|
|
1335
|
+
# sVLiquidJunction = 136; (* LONGREAL *)
|
|
1336
|
+
# sCCIHold = 144; (* LONGREAL *)
|
|
1337
|
+
# sCSlowStimVolts = 152; (* LONGREAL *)
|
|
1338
|
+
# sCCTrackVHold = 160; (* LONGREAL *)
|
|
1339
|
+
# sTimeoutCSlow = 168; (* LONGREAL *)
|
|
1340
|
+
# sSearchDelay = 176; (* LONGREAL *)
|
|
1341
|
+
# sMConductance = 184; (* LONGREAL *)
|
|
1342
|
+
# sMCapacitance = 192; (* LONGREAL *)
|
|
1343
|
+
# sSerialNumber = 200; (* 8 = SizeSerialNumber *)
|
|
1344
|
+
#
|
|
1345
|
+
# sE9Boards = 208; (* INT16 *)
|
|
1346
|
+
# sCSlowCycles = 210; (* INT16 *)
|
|
1347
|
+
# sIMonAdc = 212; (* INT16 *)
|
|
1348
|
+
# sVMonAdc = 214; (* INT16 *)
|
|
1349
|
+
#
|
|
1350
|
+
# sMuxAdc = 216; (* INT16 *)
|
|
1351
|
+
# sTestDac = 218; (* INT16 *)
|
|
1352
|
+
# sStimDac = 220; (* INT16 *)
|
|
1353
|
+
# sStimDacOffset = 222; (* INT16 *)
|
|
1354
|
+
#
|
|
1355
|
+
# sMaxDigitalBit = 224; (* INT16 *)
|
|
1356
|
+
# sHasCFastHigh = 226; (* BYTE *)
|
|
1357
|
+
# sCFastHigh = 227; (* BYTE *)
|
|
1358
|
+
# sHasBathSense = 228; (* BYTE *)
|
|
1359
|
+
# sBathSense = 229; (* BYTE *)
|
|
1360
|
+
# sHasF2Bypass = 230; (* BYTE *)
|
|
1361
|
+
# sF2Mode = 231; (* BYTE *)
|
|
1362
|
+
#
|
|
1363
|
+
# sAmplKind = 232; (* BYTE *)
|
|
1364
|
+
# sIsEpc9N = 233; (* BYTE *)
|
|
1365
|
+
# sADBoard = 234; (* BYTE *)
|
|
1366
|
+
# sBoardVersion = 235; (* BYTE *)
|
|
1367
|
+
# sActiveE9Board = 236; (* BYTE *)
|
|
1368
|
+
# sMode = 237; (* BYTE *)
|
|
1369
|
+
# sRange = 238; (* BYTE *)
|
|
1370
|
+
# sF2Response = 239; (* BYTE *)
|
|
1371
|
+
#
|
|
1372
|
+
# sRsOn = 240; (* BYTE *)
|
|
1373
|
+
# sCSlowRange = 241; (* BYTE *)
|
|
1374
|
+
# sCCRange = 242; (* BYTE *)
|
|
1375
|
+
# sCCGain = 243; (* BYTE *)
|
|
1376
|
+
# sCSlowToTestDac = 244; (* BYTE *)
|
|
1377
|
+
# sStimPath = 245; (* BYTE *)
|
|
1378
|
+
# sCCTrackTau = 246; (* BYTE *)
|
|
1379
|
+
# sWasClipping = 247; (* BYTE *)
|
|
1380
|
+
#
|
|
1381
|
+
# sRepetitiveCSlow = 248; (* BYTE *)
|
|
1382
|
+
# sLastCSlowRange = 249; (* BYTE *)
|
|
1383
|
+
# sOld1 = 250; (* BYTE *)
|
|
1384
|
+
# sCanCCFast = 251; (* BYTE *)
|
|
1385
|
+
# sCanLowCCRange = 252; (* BYTE *)
|
|
1386
|
+
# sCanHighCCRange = 253; (* BYTE *)
|
|
1387
|
+
# sCanCCTracking = 254; (* BYTE *)
|
|
1388
|
+
# sHasVmonPath = 255; (* BYTE *)
|
|
1389
|
+
#
|
|
1390
|
+
# sHasNewCCMode = 256; (* BYTE *)
|
|
1391
|
+
# sSelector = 257; (* CHAR *)
|
|
1392
|
+
# sHoldInverted = 258; (* BYTE *)
|
|
1393
|
+
# sAutoCFast = 259; (* BYTE *)
|
|
1394
|
+
# sAutoCSlow = 260; (* BYTE *)
|
|
1395
|
+
# sHasVmonX100 = 261; (* BYTE *)
|
|
1396
|
+
# sTestDacOn = 262; (* BYTE *)
|
|
1397
|
+
# sQMuxAdcOn = 263; (* BYTE *)
|
|
1398
|
+
#
|
|
1399
|
+
# sImon1Bandwidth = 264; (* LONGREAL *)
|
|
1400
|
+
# sStimScale = 272; (* LONGREAL *)
|
|
1401
|
+
#
|
|
1402
|
+
# sGain = 280; (* BYTE *)
|
|
1403
|
+
# sFilter1 = 281; (* BYTE *)
|
|
1404
|
+
# sStimFilterOn = 282; (* BYTE *)
|
|
1405
|
+
# sRsSlow = 283; (* BYTE *)
|
|
1406
|
+
# sOld2 = 284; (* BYTE *)
|
|
1407
|
+
# sCCCFastOn = 285; (* BYTE *)
|
|
1408
|
+
# sCCFastSpeed = 286; (* BYTE *)
|
|
1409
|
+
# sF2Source = 287; (* BYTE *)
|
|
1410
|
+
#
|
|
1411
|
+
# sTestRange = 288; (* BYTE *)
|
|
1412
|
+
# sTestDacPath = 289; (* BYTE *)
|
|
1413
|
+
# sMuxChannel = 290; (* BYTE *)
|
|
1414
|
+
# sMuxGain64 = 291; (* BYTE *)
|
|
1415
|
+
# sVmonX100 = 292; (* BYTE *)
|
|
1416
|
+
# sIsQuadro = 293; (* BYTE *)
|
|
1417
|
+
# sF1Mode = 294; (* BYTE *)
|
|
1418
|
+
# sOld3 = 295; (* BYTE *)
|
|
1419
|
+
#
|
|
1420
|
+
# sStimFilterHz = 296; (* LONGREAL *)
|
|
1421
|
+
# sRsTau = 304; (* LONGREAL *)
|
|
1422
|
+
# sDacToAdcDelay = 312; (* LONGREAL *)
|
|
1423
|
+
# sInputFilterTau = 320; (* LONGREAL *)
|
|
1424
|
+
# sOutputFilterTau = 328; (* LONGREAL *)
|
|
1425
|
+
# sVmonFactor = 336; (* LONGREAL *)
|
|
1426
|
+
# sCalibDate = 344; (* 16 = SizeCalibDate *)
|
|
1427
|
+
# sVmonOffset = 360; (* LONGREAL *)
|
|
1428
|
+
#
|
|
1429
|
+
# sEEPROMKind = 368; (* BYTE *)
|
|
1430
|
+
# sVrefX2 = 369; (* BYTE *)
|
|
1431
|
+
# sHasVrefX2AndF2Vmon = 370; (* BYTE *)
|
|
1432
|
+
# sSpare1 = 371; (* BYTE *)
|
|
1433
|
+
# sSpare2 = 372; (* BYTE *)
|
|
1434
|
+
# sSpare3 = 373; (* BYTE *)
|
|
1435
|
+
# sSpare4 = 374; (* BYTE *)
|
|
1436
|
+
# sSpare5 = 375; (* BYTE *)
|
|
1437
|
+
#
|
|
1438
|
+
# sCCStimDacScale = 376; (* LONGREAL *)
|
|
1439
|
+
# sVmonFiltBandwidth = 384; (* LONGREAL *)
|
|
1440
|
+
# sVmonFiltFrequency = 392; (* LONGREAL *)
|
|
1441
|
+
# AmplifierStateSize = 400; (* = 50 * 8 *)
|
|
1442
|
+
#
|
|
1443
|
+
# NOTE: Not to be confused with AmplStateRecord, which can contain an
|
|
1444
|
+
# AmplifierState.
|
|
1445
|
+
#
|
|
1446
|
+
class AmplifierState:
|
|
1447
|
+
"""
|
|
1448
|
+
Describes the state of an amplifier used by PatchMaster.
|
|
1449
|
+
"""
|
|
1450
|
+
def __init__(self, handle, reader):
|
|
1451
|
+
|
|
1452
|
+
# Read properties
|
|
1453
|
+
i = handle.tell()
|
|
1454
|
+
|
|
1455
|
+
# Series resistance compensation
|
|
1456
|
+
handle.seek(i + 40) # sRsFraction = 40; (* LONGREAL *)
|
|
1457
|
+
self._rs_fraction = reader.read1('d')
|
|
1458
|
+
handle.seek(i + 240) # sRsOn = 240; (* BYTE *)
|
|
1459
|
+
self._rs_enabled = bool(reader.read1('b'))
|
|
1460
|
+
|
|
1461
|
+
handle.seek(i + 88) # sGSeries = 88; (* LONGREAL *)
|
|
1462
|
+
self._g_series = reader.read1('d')
|
|
1463
|
+
|
|
1464
|
+
handle.seek(i + 56) # sCFastAmp1 = 56; (* LONGREAL *)
|
|
1465
|
+
self._cf_amp1 = reader.read1('d')
|
|
1466
|
+
handle.seek(i + 64) # sCFastAmp2 = 64; (* LONGREAL *)
|
|
1467
|
+
self._cf_amp2 = reader.read1('d')
|
|
1468
|
+
handle.seek(i + 72) # sCFastTau = 72; (* LONGREAL *)
|
|
1469
|
+
self._cf_tau = reader.read1('d')
|
|
1470
|
+
handle.seek(i + 285) # sCCCFastOn = 285; (* BYTE *)
|
|
1471
|
+
self._cf_enabled = bool(reader.read1('b'))
|
|
1472
|
+
|
|
1473
|
+
handle.seek(i + 80) # sCSlow = 80; (* LONGREAL *)
|
|
1474
|
+
self._cs = reader.read1('d')
|
|
1475
|
+
|
|
1476
|
+
handle.seek(i + 136) # sVLiquidJunction = 136; (* LONGREAL *)
|
|
1477
|
+
self._ljp = reader.read1('d')
|
|
1478
|
+
|
|
1479
|
+
def c_fast(self):
|
|
1480
|
+
"""
|
|
1481
|
+
Return the capacitance (pF) used in fast capacitance correction
|
|
1482
|
+
(CFast2).
|
|
1483
|
+
"""
|
|
1484
|
+
# Not sure why there are two. They are almost identical. Older EPC9
|
|
1485
|
+
# manual has a Fast1 and Fast2 as well, but only for GET, for SET there
|
|
1486
|
+
# is only 2. So... going with that for now
|
|
1487
|
+
return self._cf_amp2 * 1e12
|
|
1488
|
+
|
|
1489
|
+
def c_fast_tau(self):
|
|
1490
|
+
"""
|
|
1491
|
+
Returns the time constant (us) used in fast capacitance correction.
|
|
1492
|
+
"""
|
|
1493
|
+
return self._cf_tau * 1e6
|
|
1494
|
+
|
|
1495
|
+
def c_fast_enabled(self):
|
|
1496
|
+
""" Returns ``True`` if fast capacitance compensation was enabled. """
|
|
1497
|
+
return self._cf_enabled
|
|
1498
|
+
|
|
1499
|
+
def c_slow(self):
|
|
1500
|
+
"""
|
|
1501
|
+
Returns the capacitance (cF) used in slow capacitance correction.
|
|
1502
|
+
"""
|
|
1503
|
+
return self._cs * 1e12
|
|
1504
|
+
|
|
1505
|
+
def ljp(self):
|
|
1506
|
+
"""
|
|
1507
|
+
Returns the liquid junction potential (LJP, in mV) used in the LJP
|
|
1508
|
+
correction.
|
|
1509
|
+
|
|
1510
|
+
The LJP is defined as the potential of the bath with respect to the
|
|
1511
|
+
pipette (V_bath - V_pipette), and so will typically be a positive
|
|
1512
|
+
number. This will be subtracted or added from the measured or applied
|
|
1513
|
+
voltage, depending on the selected clamping mode.
|
|
1514
|
+
|
|
1515
|
+
If this is non-zero, then PatchMaster will have corrected recorded Vm's
|
|
1516
|
+
before storing, and will have corrected output command potentials
|
|
1517
|
+
before applying. No further a posteriori correction is necessary.
|
|
1518
|
+
"""
|
|
1519
|
+
return self._ljp * 1e3
|
|
1520
|
+
|
|
1521
|
+
def r_series_enabled(self):
|
|
1522
|
+
"""
|
|
1523
|
+
Returns ``True`` if series resistance compensation was enabled and set
|
|
1524
|
+
to a non-zero value.
|
|
1525
|
+
"""
|
|
1526
|
+
return self._rs_enabled and self._rs_fraction > 0
|
|
1527
|
+
|
|
1528
|
+
def r_series_fraction(self):
|
|
1529
|
+
"""
|
|
1530
|
+
Returns the fraction of series resistance that was compensated, or 0 if
|
|
1531
|
+
series resistance compensation was not enabled.
|
|
1532
|
+
"""
|
|
1533
|
+
return self._rs_fraction if self._rs_enabled else 0
|
|
1534
|
+
|
|
1535
|
+
def r_series(self):
|
|
1536
|
+
"""
|
|
1537
|
+
Returns the last (uncompensated) series resistance (MOhm) before
|
|
1538
|
+
acquiring the trace.
|
|
1539
|
+
"""
|
|
1540
|
+
return 1e-6 / self._g_series
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
#
|
|
1544
|
+
# From StimFile_v9.txt
|
|
1545
|
+
# (* RootRecord = RECORD *)
|
|
1546
|
+
# roVersion = 0; (* INT32 *)
|
|
1547
|
+
# roMark = 4; (* INT32 *)
|
|
1548
|
+
# roVersionName = 8; (* String32Type *)
|
|
1549
|
+
# roMaxSamples = 40; (* INT32 *)
|
|
1550
|
+
# roFiller1 = 44; (* INT32 *)
|
|
1551
|
+
# (* StimParams = 10 *)
|
|
1552
|
+
# (* StimParamChars = 320 *)
|
|
1553
|
+
# roParams = 48; (* ARRAY[0..9] OF LONGREAL *)
|
|
1554
|
+
# roParamText = 128; (* ARRAY[0..9],[0..31]OF CHAR *)
|
|
1555
|
+
# roReserved = 448; (* String128Type *)
|
|
1556
|
+
# roFiller2 = 576; (* INT32 *)
|
|
1557
|
+
# roCRC = 580; (* CARD32 *)
|
|
1558
|
+
# RootRecSize = 584; (* = 73 * 8 *)
|
|
1559
|
+
#
|
|
1560
|
+
class StimulusFile(TreeNode):
|
|
1561
|
+
"""
|
|
1562
|
+
Represents the "stimulus file" section of a PatchMaster bundle (.pgf).
|
|
1563
|
+
|
|
1564
|
+
Each ``PulsedFile`` contains zero or more :class:`Stimulus` objects.
|
|
1565
|
+
"""
|
|
1566
|
+
def __init__(self, parent):
|
|
1567
|
+
super().__init__(parent)
|
|
1568
|
+
self._version = None
|
|
1569
|
+
self._time = None
|
|
1570
|
+
|
|
1571
|
+
def _read_properties(self, handle, reader):
|
|
1572
|
+
# See TreeNode._read_properties
|
|
1573
|
+
start = handle.tell()
|
|
1574
|
+
handle.seek(start + 8)
|
|
1575
|
+
self._version = reader.str(32)
|
|
1576
|
+
|
|
1577
|
+
def version(self):
|
|
1578
|
+
"""
|
|
1579
|
+
Returns a (hard-to-parse) string representation of this file's
|
|
1580
|
+
PatchMaster format version.
|
|
1581
|
+
"""
|
|
1582
|
+
return self._version
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
#
|
|
1586
|
+
# From StimFile_v9.txt
|
|
1587
|
+
# (* StimulationRecord = RECORD *)
|
|
1588
|
+
# stMark = 0; (* INT32 *)
|
|
1589
|
+
# stEntryName = 4; (* String32Type *)
|
|
1590
|
+
# stFileName = 36; (* String32Type *)
|
|
1591
|
+
# stAnalName = 68; (* String32Type *)
|
|
1592
|
+
# stDataStartSegment = 100; (* INT32 *)
|
|
1593
|
+
# stDataStartTime = 104; (* LONGREAL *)
|
|
1594
|
+
# stSampleInterval = 112; (* LONGREAL *)
|
|
1595
|
+
# stSweepInterval = 120; (* LONGREAL *)
|
|
1596
|
+
# stLeakDelay = 128; (* LONGREAL *)
|
|
1597
|
+
# stFilterFactor = 136; (* LONGREAL *)
|
|
1598
|
+
# stNumberSweeps = 144; (* INT32 *)
|
|
1599
|
+
# stNumberLeaks = 148; (* INT32 *)
|
|
1600
|
+
# stNumberAverages = 152; (* INT32 *)
|
|
1601
|
+
# stActualAdcChannels = 156; (* INT32 *)
|
|
1602
|
+
# stActualDacChannels = 160; (* INT32 *)
|
|
1603
|
+
# stExtTrigger = 164; (* BYTE *)
|
|
1604
|
+
# stNoStartWait = 165; (* BOOLEAN *)
|
|
1605
|
+
# stUseScanRates = 166; (* BOOLEAN *)
|
|
1606
|
+
# stNoContAq = 167; (* BOOLEAN *)
|
|
1607
|
+
# stHasLockIn = 168; (* BOOLEAN *)
|
|
1608
|
+
# stOldStartMacKind = 169; (* CHAR *)
|
|
1609
|
+
# stOldEndMacKind = 170; (* BOOLEAN *)
|
|
1610
|
+
# stAutoRange = 171; (* BYTE *)
|
|
1611
|
+
# stBreakNext = 172; (* BOOLEAN *)
|
|
1612
|
+
# stIsExpanded = 173; (* BOOLEAN *)
|
|
1613
|
+
# stLeakCompMode = 174; (* BOOLEAN *)
|
|
1614
|
+
# stHasChirp = 175; (* BOOLEAN *)
|
|
1615
|
+
# stOldStartMacro = 176; (* String32Type *)
|
|
1616
|
+
# stOldEndMacro = 208; (* String32Type *)
|
|
1617
|
+
# sIsGapFree = 240; (* BOOLEAN *)
|
|
1618
|
+
# sHandledExternally = 241; (* BOOLEAN *)
|
|
1619
|
+
# stFiller1 = 242; (* BOOLEAN *)
|
|
1620
|
+
# stFiller2 = 243; (* BOOLEAN *)
|
|
1621
|
+
# stCRC = 244; (* CARD32 *)
|
|
1622
|
+
# StimulationRecSize = 248; (* = 31 * 8 *)
|
|
1623
|
+
#
|
|
1624
|
+
class Stimulus(TreeNode):
|
|
1625
|
+
"""
|
|
1626
|
+
Represents a "stimulation" record.
|
|
1627
|
+
|
|
1628
|
+
Each ``Stimulus`` contains zero or more :class:`StimulusChannel` objects,
|
|
1629
|
+
and each channel is made up out of :class:`Segment` objects.
|
|
1630
|
+
|
|
1631
|
+
For selected stimuli, a reconstruction can be made using :meth:`protocol`
|
|
1632
|
+
or :meth:`reconstruction`.
|
|
1633
|
+
"""
|
|
1634
|
+
def __init__(self, parent):
|
|
1635
|
+
super().__init__(parent)
|
|
1636
|
+
|
|
1637
|
+
self._entry_name = None
|
|
1638
|
+
self._sweep_count = None
|
|
1639
|
+
self._sweep_interval = None # Waiting time between sweeps (s)
|
|
1640
|
+
self._dt = None
|
|
1641
|
+
|
|
1642
|
+
# Supported channel: there should be either 0 or 1 supported DAC
|
|
1643
|
+
self._supported_channel = None
|
|
1644
|
+
|
|
1645
|
+
def _read_properties(self, handle, reader):
|
|
1646
|
+
# See TreeNode._read_properties
|
|
1647
|
+
start = handle.tell()
|
|
1648
|
+
|
|
1649
|
+
handle.seek(start + 4)
|
|
1650
|
+
self._entry_name = reader.str(32)
|
|
1651
|
+
handle.seek(start + 112) # stSampleInterval = 112; (* LONGREAL *)
|
|
1652
|
+
self._dt = reader.read1('d')
|
|
1653
|
+
handle.seek(start + 120) # stSweepInterval = 120; (* LONGREAL *)
|
|
1654
|
+
self._sweep_interval = reader.read1('d')
|
|
1655
|
+
handle.seek(start + 144) # stNumberSweeps = 144; (* INT32 *)
|
|
1656
|
+
self._sweep_count = reader.read1('i')
|
|
1657
|
+
|
|
1658
|
+
def _read_finalize(self):
|
|
1659
|
+
# See TreeNode._read_finalize
|
|
1660
|
+
|
|
1661
|
+
# Find supported channel, if any
|
|
1662
|
+
for c in self:
|
|
1663
|
+
if c.supported():
|
|
1664
|
+
m, u = c.amplifier_mode(), c.unit()
|
|
1665
|
+
vc = m is AmplifierMode.VC and u is myokit.units.V
|
|
1666
|
+
cc = m is AmplifierMode.CC and u is myokit.units.nA
|
|
1667
|
+
if vc or cc:
|
|
1668
|
+
self._supported_channel = c
|
|
1669
|
+
break
|
|
1670
|
+
|
|
1671
|
+
def all_segments_stored(self):
|
|
1672
|
+
"""
|
|
1673
|
+
Returns ``True`` if all of this stimulus's segments should be stored.
|
|
1674
|
+
|
|
1675
|
+
The channel to check is chosen automatically. If no supported channel
|
|
1676
|
+
can be found, a :class:`NoSupportedDAChannelError` is raised.
|
|
1677
|
+
"""
|
|
1678
|
+
if self._supported_channel is None:
|
|
1679
|
+
raise NoSupportedDAChannelError()
|
|
1680
|
+
return self._supported_channel.all_segments_stored()
|
|
1681
|
+
|
|
1682
|
+
def label(self):
|
|
1683
|
+
""" Returns this stimulus's name. """
|
|
1684
|
+
return self._entry_name
|
|
1685
|
+
|
|
1686
|
+
def protocol(self, tu='ms', vu='mV', cu='pA', n_digits=9):
|
|
1687
|
+
"""
|
|
1688
|
+
Generates a :class:`myokit.Protocol` corresponding to this stimulus, or
|
|
1689
|
+
raises a ``ValueError`` if unsupported features are required.
|
|
1690
|
+
|
|
1691
|
+
The channel to use is chosen automatically. If no supported channel can
|
|
1692
|
+
be found, a :class:`NoSupportedDAChannelError` is raised.
|
|
1693
|
+
|
|
1694
|
+
Support notes:
|
|
1695
|
+
|
|
1696
|
+
- Only stimuli with constant segments are supported (so no ramps,
|
|
1697
|
+
continuous, etc.)
|
|
1698
|
+
- Deriving values from parameters is not supported.
|
|
1699
|
+
- File templates are not supported.
|
|
1700
|
+
- Constant segments with a zero duration are ignored.
|
|
1701
|
+
|
|
1702
|
+
"""
|
|
1703
|
+
if self._supported_channel is None:
|
|
1704
|
+
raise NoSupportedDAChannelError()
|
|
1705
|
+
return self._supported_channel.protocol(
|
|
1706
|
+
self._sweep_count, tu, vu, cu, n_digits)
|
|
1707
|
+
|
|
1708
|
+
def reconstruction(self, join_sweeps=False):
|
|
1709
|
+
"""
|
|
1710
|
+
Returns a reconstructed D/A signal corresponding to this stimulus.
|
|
1711
|
+
|
|
1712
|
+
The channel to use is chosen automatically. If no supported channel can
|
|
1713
|
+
be found, a :class:`NoSupportedDAChannelError` is raised.
|
|
1714
|
+
"""
|
|
1715
|
+
if self._supported_channel is None:
|
|
1716
|
+
raise NoSupportedDAChannelError()
|
|
1717
|
+
return self._supported_channel.reconstruction(
|
|
1718
|
+
self._sweep_count, self._sweep_interval, self._dt, join_sweeps)
|
|
1719
|
+
|
|
1720
|
+
def sampling_interval(self):
|
|
1721
|
+
""" Returns this stimulus's sampling interval (in seconds). """
|
|
1722
|
+
return self._dt
|
|
1723
|
+
|
|
1724
|
+
def supported_channel(self):
|
|
1725
|
+
"""
|
|
1726
|
+
Returns the channel in this stimulus for which a D/A signal can be
|
|
1727
|
+
reconstructed, or returns ``None`` if no such channel is available.
|
|
1728
|
+
"""
|
|
1729
|
+
return self._supported_channel
|
|
1730
|
+
|
|
1731
|
+
def sweep_count(self):
|
|
1732
|
+
""" Returns this stimulus's sweep count. """
|
|
1733
|
+
return self._sweep_count
|
|
1734
|
+
|
|
1735
|
+
def sweep_durations(self):
|
|
1736
|
+
"""
|
|
1737
|
+
Returns the (intended) durations of this stimulus's sweeps, as a list
|
|
1738
|
+
of times in seconds.
|
|
1739
|
+
|
|
1740
|
+
Note that this returns the actual duration of a sweep, regardless of
|
|
1741
|
+
storage.
|
|
1742
|
+
"""
|
|
1743
|
+
d = np.array(
|
|
1744
|
+
[channel.sweep_durations(self._sweep_count) for channel in self])
|
|
1745
|
+
# Not sure if all durations should be the same for all channels
|
|
1746
|
+
# So returning maximum per sweep
|
|
1747
|
+
return np.max(d, axis=0)
|
|
1748
|
+
|
|
1749
|
+
def sweep_interval(self):
|
|
1750
|
+
"""
|
|
1751
|
+
Returns this stimulus's sweep interval (the intentional delay between
|
|
1752
|
+
one sweep end and the next sweep start, in seconds).
|
|
1753
|
+
"""
|
|
1754
|
+
return self._sweep_interval
|
|
1755
|
+
|
|
1756
|
+
def sweep_samples(self):
|
|
1757
|
+
"""
|
|
1758
|
+
Returns the (intended) number of samples stored during each of this
|
|
1759
|
+
stimulus's sweeps.
|
|
1760
|
+
|
|
1761
|
+
Unlike :meth:`sweep_durations`, this method takes storage into account.
|
|
1762
|
+
"""
|
|
1763
|
+
if self._supported_channel is None:
|
|
1764
|
+
raise NoSupportedDAChannelError()
|
|
1765
|
+
return self._supported_channel.sweep_samples(
|
|
1766
|
+
self._sweep_count, self._dt)
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
#
|
|
1770
|
+
# From StimFile_v9.txt
|
|
1771
|
+
# (* ChannelRecord = RECORD *)
|
|
1772
|
+
# chMark = 0; (* INT32 *)
|
|
1773
|
+
# chLinkedChannel = 4; (* INT32 *)
|
|
1774
|
+
# chCompressionFactor = 8; (* INT32 *)
|
|
1775
|
+
# chYUnit = 12; (* String8Type *)
|
|
1776
|
+
# chAdcChannel = 20; (* INT16 *)
|
|
1777
|
+
# chAdcMode = 22; (* BYTE *)
|
|
1778
|
+
# chDoWrite = 23; (* BOOLEAN *)
|
|
1779
|
+
# stLeakStore = 24; (* BYTE *)
|
|
1780
|
+
# chAmplMode = 25; (* BYTE *)
|
|
1781
|
+
# chOwnSegTime = 26; (* BOOLEAN *)
|
|
1782
|
+
# chSetLastSegVmemb = 27; (* BOOLEAN *)
|
|
1783
|
+
# chDacChannel = 28; (* INT16 *)
|
|
1784
|
+
# chDacMode = 30; (* BYTE *)
|
|
1785
|
+
# chHasLockInSquare = 31; (* BYTE *)
|
|
1786
|
+
# chRelevantXSegment = 32; (* INT32 *)
|
|
1787
|
+
# chRelevantYSegment = 36; (* INT32 *)
|
|
1788
|
+
# chDacUnit = 40; (* String8Type *)
|
|
1789
|
+
# chHolding = 48; (* LONGREAL *)
|
|
1790
|
+
# chLeakHolding = 56; (* LONGREAL *)
|
|
1791
|
+
# chLeakSize = 64; (* LONGREAL *)
|
|
1792
|
+
# chLeakHoldMode = 72; (* BYTE *)
|
|
1793
|
+
# chLeakAlternate = 73; (* BOOLEAN *)
|
|
1794
|
+
# chAltLeakAveraging = 74; (* BOOLEAN *)
|
|
1795
|
+
# chLeakPulseOn = 75; (* BOOLEAN *)
|
|
1796
|
+
# chStimToDacID = 76; (* SET16 *)
|
|
1797
|
+
# chCompressionMode = 78; (* SET16 *)
|
|
1798
|
+
# chCompressionSkip = 80; (* INT32 *)
|
|
1799
|
+
# chDacBit = 84; (* INT16 *)
|
|
1800
|
+
# chHasLockInSine = 86; (* BOOLEAN *)
|
|
1801
|
+
# chBreakMode = 87; (* BYTE *)
|
|
1802
|
+
# chZeroSeg = 88; (* INT32 *)
|
|
1803
|
+
# chStimSweep = 92; (* INT32 *)
|
|
1804
|
+
# chSine_Cycle = 96; (* LONGREAL *)
|
|
1805
|
+
# chSine_Amplitude = 104; (* LONGREAL *)
|
|
1806
|
+
# chLockIn_VReversal = 112; (* LONGREAL *)
|
|
1807
|
+
# chChirp_StartFreq = 120; (* LONGREAL *)
|
|
1808
|
+
# chChirp_EndFreq = 128; (* LONGREAL *)
|
|
1809
|
+
# chChirp_MinPoints = 136; (* LONGREAL *)
|
|
1810
|
+
# chSquare_NegAmpl = 144; (* LONGREAL *)
|
|
1811
|
+
# chSquare_DurFactor = 152; (* LONGREAL *)
|
|
1812
|
+
# chLockIn_Skip = 160; (* INT32 *)
|
|
1813
|
+
# chPhoto_MaxCycles = 164; (* INT32 *)
|
|
1814
|
+
# chPhoto_SegmentNo = 168; (* INT32 *)
|
|
1815
|
+
# chLockIn_AvgCycles = 172; (* INT32 *)
|
|
1816
|
+
# chImaging_RoiNo = 176; (* INT32 *)
|
|
1817
|
+
# chChirp_Skip = 180; (* INT32 *)
|
|
1818
|
+
# chChirp_Amplitude = 184; (* LONGREAL *)
|
|
1819
|
+
# chPhoto_Adapt = 192; (* BYTE *)
|
|
1820
|
+
# chSine_Kind = 193; (* BYTE *)
|
|
1821
|
+
# chChirp_PreChirp = 194; (* BYTE *)
|
|
1822
|
+
# chSine_Source = 195; (* BYTE *)
|
|
1823
|
+
# chSquare_NegSource = 196; (* BYTE *)
|
|
1824
|
+
# chSquare_PosSource = 197; (* BYTE *)
|
|
1825
|
+
# chChirp_Kind = 198; (* BYTE *)
|
|
1826
|
+
# chChirp_Source = 199; (* BYTE *)
|
|
1827
|
+
# chDacOffset = 200; (* LONGREAL *)
|
|
1828
|
+
# chAdcOffset = 208; (* LONGREAL *)
|
|
1829
|
+
# chTraceMathFormat = 216; (* BYTE *)
|
|
1830
|
+
# chHasChirp = 217; (* BOOLEAN *)
|
|
1831
|
+
# chSquare_Kind = 218; (* BYTE *)
|
|
1832
|
+
# chFiller1 = 219; (* ARRAY[0..5] OF CHAR *)
|
|
1833
|
+
# chSquare_BaseIncr = 224; (* LONGREAL *)
|
|
1834
|
+
# chSquare_Cycle = 232; (* LONGREAL *)
|
|
1835
|
+
# chSquare_PosAmpl = 240; (* LONGREAL *)
|
|
1836
|
+
# chCompressionOffset = 248; (* INT32 *)
|
|
1837
|
+
# chPhotoMode = 252; (* INT32 *)
|
|
1838
|
+
# chBreakLevel = 256; (* LONGREAL *)
|
|
1839
|
+
# chTraceMath = 264; (* String128Type *)
|
|
1840
|
+
# chFiller2 = 392; (* INT32 *)
|
|
1841
|
+
# chCRC = 396; (* CARD32 *)
|
|
1842
|
+
# ChannelRecSize = 400; (* = 50 * 8 *)
|
|
1843
|
+
#
|
|
1844
|
+
class StimulusChannel(TreeNode):
|
|
1845
|
+
"""
|
|
1846
|
+
Represents a channel inside a :class:`Stimulus`.
|
|
1847
|
+
|
|
1848
|
+
Each ``StimulusChannel`` contains zero or more :class:`StimulusSegment`
|
|
1849
|
+
objects.
|
|
1850
|
+
"""
|
|
1851
|
+
def __init__(self, parent):
|
|
1852
|
+
super().__init__(parent)
|
|
1853
|
+
|
|
1854
|
+
self._unit = None
|
|
1855
|
+
self._amplifier_mode = None
|
|
1856
|
+
|
|
1857
|
+
# Interpretation of "voltage" in segments
|
|
1858
|
+
self._holding = None
|
|
1859
|
+
self._use_stim_scale = None
|
|
1860
|
+
self._use_relative = None
|
|
1861
|
+
self._use_file_template = None
|
|
1862
|
+
|
|
1863
|
+
def _read_properties(self, handle, reader):
|
|
1864
|
+
# See TreeNode._read_properties
|
|
1865
|
+
start = handle.tell()
|
|
1866
|
+
|
|
1867
|
+
handle.seek(start + 25) # chAmplMode = 25; (* BYTE *)
|
|
1868
|
+
self._amplifier_mode = AmplifierMode(reader.read1('b'))
|
|
1869
|
+
|
|
1870
|
+
handle.seek(start + 40) # chDacUnit = 40; (* String8Type *)
|
|
1871
|
+
self._unit = reader.str(8)
|
|
1872
|
+
self._holding = reader.read1('d')
|
|
1873
|
+
|
|
1874
|
+
handle.seek(start + 76) # chStimToDacID = 76; (* SET16 *)
|
|
1875
|
+
flags = StimulusChannelDACFlags(reader.read('?' * 16))
|
|
1876
|
+
self._use_stim_scale = flags.use_stim_scale
|
|
1877
|
+
self._use_relative = flags.use_relative
|
|
1878
|
+
self._use_file_template = flags.use_file_template
|
|
1879
|
+
|
|
1880
|
+
# Convert unit
|
|
1881
|
+
if self._unit == 'A':
|
|
1882
|
+
# It appears that segments stored as 'A' are in nA
|
|
1883
|
+
self._unit = myokit.units.nA
|
|
1884
|
+
elif self._unit == 'V':
|
|
1885
|
+
self._unit = myokit.units.V
|
|
1886
|
+
else:
|
|
1887
|
+
self._unit = f'Unsupported units {self._unit}'
|
|
1888
|
+
|
|
1889
|
+
def __str__(self):
|
|
1890
|
+
return (f'StimulusChannel in {self._amplifier_mode} mode and units'
|
|
1891
|
+
f' {self._unit}')
|
|
1892
|
+
|
|
1893
|
+
def all_segments_stored(self):
|
|
1894
|
+
"""
|
|
1895
|
+
Returns ``True`` if all this channel's segments should be stored.
|
|
1896
|
+
"""
|
|
1897
|
+
return all(s.storage() is SegmentStorage.Stored for s in self)
|
|
1898
|
+
|
|
1899
|
+
def amplifier_mode(self):
|
|
1900
|
+
"""
|
|
1901
|
+
Returns the :class:`AmplifierMode` that this channel was recorded in.
|
|
1902
|
+
"""
|
|
1903
|
+
return self._amplifier_mode
|
|
1904
|
+
|
|
1905
|
+
def protocol(self, sweep_count, tu='ms', vu='mV', cu='pA', n_digits=9):
|
|
1906
|
+
"""
|
|
1907
|
+
Generates a :class:`myokit.Protocol` corresponding to this channel, or
|
|
1908
|
+
raises a ``NotImplementedError`` if unsupported features are required.
|
|
1909
|
+
|
|
1910
|
+
See :meth:`Stimulus.protocol()` for details.
|
|
1911
|
+
"""
|
|
1912
|
+
self._supported()
|
|
1913
|
+
|
|
1914
|
+
# Get durations and values
|
|
1915
|
+
durations = []
|
|
1916
|
+
values = []
|
|
1917
|
+
for seg in self: # Guaranteed to be constant by _supported() call
|
|
1918
|
+
# These can throw further NotImplementedErrors:
|
|
1919
|
+
durations.append(seg.durations(sweep_count))
|
|
1920
|
+
values.append(seg.values(
|
|
1921
|
+
sweep_count, self._holding, self._use_relative))
|
|
1922
|
+
|
|
1923
|
+
# Get unit conversion factors
|
|
1924
|
+
tf = float(myokit.Unit.conversion_factor(myokit.units.s, tu))
|
|
1925
|
+
if self._unit is myokit.units.V:
|
|
1926
|
+
df = float(myokit.Unit.conversion_factor(self._unit, vu))
|
|
1927
|
+
else: # Guaranteed to be nA by _supported() call
|
|
1928
|
+
df = float(myokit.Unit.conversion_factor(self._unit, cu))
|
|
1929
|
+
|
|
1930
|
+
# Generate and return
|
|
1931
|
+
t = 0
|
|
1932
|
+
p = myokit.Protocol()
|
|
1933
|
+
for sweep in range(sweep_count):
|
|
1934
|
+
for segment in range(len(self)):
|
|
1935
|
+
d = durations[segment][sweep]
|
|
1936
|
+
v = values[segment][sweep]
|
|
1937
|
+
|
|
1938
|
+
if d != 0: # Silently ignore duration=0 steps
|
|
1939
|
+
p.schedule(
|
|
1940
|
+
level=round(v * df, n_digits),
|
|
1941
|
+
start=round(t * tf, n_digits),
|
|
1942
|
+
duration=round(d * tf, n_digits),
|
|
1943
|
+
)
|
|
1944
|
+
t += d
|
|
1945
|
+
return p
|
|
1946
|
+
|
|
1947
|
+
def reconstruction(self, sweep_count, sweep_interval, dt,
|
|
1948
|
+
join_sweeps=False):
|
|
1949
|
+
"""
|
|
1950
|
+
Returns a reconstructed D/A signal corresponding to this stimulus
|
|
1951
|
+
channel, with the given number of sweeps.
|
|
1952
|
+
|
|
1953
|
+
Note: The full signal is reconstructed, regardless of whether segments
|
|
1954
|
+
were set to be recorded or not.
|
|
1955
|
+
|
|
1956
|
+
Arguments:
|
|
1957
|
+
|
|
1958
|
+
``sweep_count``
|
|
1959
|
+
The number of sweeps in the reconstruction.
|
|
1960
|
+
``sweep_interval``
|
|
1961
|
+
The delay between one sweep's end and the next sweep's start.
|
|
1962
|
+
``dt``
|
|
1963
|
+
The sampling interval.
|
|
1964
|
+
``join_sweeps``
|
|
1965
|
+
Set to ``True`` to return a tuple (``time``, ``values``) where
|
|
1966
|
+
``time`` and ``values`` are 1d arrays.
|
|
1967
|
+
|
|
1968
|
+
"""
|
|
1969
|
+
self._supported()
|
|
1970
|
+
|
|
1971
|
+
# Get durations and values
|
|
1972
|
+
durations = []
|
|
1973
|
+
values = []
|
|
1974
|
+
for seg in self: # Guaranteed to be constant by _supported() call
|
|
1975
|
+
# These can throw further NotImplementedErrors:
|
|
1976
|
+
durations.append(seg.durations(sweep_count))
|
|
1977
|
+
values.append(seg.values(
|
|
1978
|
+
sweep_count, self._holding, self._use_relative))
|
|
1979
|
+
|
|
1980
|
+
# Generate
|
|
1981
|
+
t = 0
|
|
1982
|
+
nseg = len(self)
|
|
1983
|
+
time, data = [], []
|
|
1984
|
+
for sweep in range(sweep_count):
|
|
1985
|
+
# Sweep duration and sample count
|
|
1986
|
+
sw_duration = np.sum([durations[s][sweep] for s in range(nseg)])
|
|
1987
|
+
n_samples = int(round(sw_duration / dt))
|
|
1988
|
+
|
|
1989
|
+
# Time
|
|
1990
|
+
time.append(t + np.arange(n_samples) * dt)
|
|
1991
|
+
t += sw_duration + sweep_interval
|
|
1992
|
+
|
|
1993
|
+
# Values
|
|
1994
|
+
vs = np.zeros(n_samples)
|
|
1995
|
+
i = 0
|
|
1996
|
+
for segment in range(nseg):
|
|
1997
|
+
d = durations[segment][sweep]
|
|
1998
|
+
v = values[segment][sweep]
|
|
1999
|
+
n = int(round(d / dt))
|
|
2000
|
+
vs[i: i + n] = v
|
|
2001
|
+
i += n
|
|
2002
|
+
data.append(vs)
|
|
2003
|
+
|
|
2004
|
+
# Return
|
|
2005
|
+
if join_sweeps:
|
|
2006
|
+
return np.concatenate(time), np.concatenate(data)
|
|
2007
|
+
return time, data
|
|
2008
|
+
|
|
2009
|
+
def _supported(self):
|
|
2010
|
+
"""
|
|
2011
|
+
Check for unsupported features and raises a NotImplementedError if they
|
|
2012
|
+
are encountered.
|
|
2013
|
+
"""
|
|
2014
|
+
# Scaling has to use the "standard conversion" method
|
|
2015
|
+
if not self._use_stim_scale:
|
|
2016
|
+
# See "10.9.1 DA output channel settings" in the manual
|
|
2017
|
+
raise NotImplementedError(
|
|
2018
|
+
'Unsupported feature: non-standard D/A conversion.')
|
|
2019
|
+
|
|
2020
|
+
# File templates are not supported
|
|
2021
|
+
if self._use_file_template:
|
|
2022
|
+
raise NotImplementedError(
|
|
2023
|
+
'Unsupported feature: channel uses a stimulus file.')
|
|
2024
|
+
|
|
2025
|
+
# All segments are constant
|
|
2026
|
+
for seg in self:
|
|
2027
|
+
if seg.segment_class() is not SegmentClass.Constant:
|
|
2028
|
+
raise NotImplementedError(
|
|
2029
|
+
'Unsupported segment type: {seg.segment_class()}')
|
|
2030
|
+
|
|
2031
|
+
# Units are of a supported type
|
|
2032
|
+
if self._unit not in (myokit.units.V, myokit.units.nA):
|
|
2033
|
+
raise NotImplementedError(self._unit)
|
|
2034
|
+
|
|
2035
|
+
def supported(self):
|
|
2036
|
+
"""
|
|
2037
|
+
Returns ``True`` if this channel's D/A signal can be reconstructed.
|
|
2038
|
+
"""
|
|
2039
|
+
try:
|
|
2040
|
+
self._supported()
|
|
2041
|
+
except NotImplementedError:
|
|
2042
|
+
return False
|
|
2043
|
+
return True
|
|
2044
|
+
|
|
2045
|
+
def support_str(self):
|
|
2046
|
+
"""
|
|
2047
|
+
Returns a string indicating support for this channel.
|
|
2048
|
+
|
|
2049
|
+
Supported channels return ``"Supported"``, unsupported channels return
|
|
2050
|
+
a string detailing the reason this channel is not supported.
|
|
2051
|
+
"""
|
|
2052
|
+
try:
|
|
2053
|
+
self._supported()
|
|
2054
|
+
except NotImplementedError as e:
|
|
2055
|
+
return str(e)
|
|
2056
|
+
return 'Supported'
|
|
2057
|
+
|
|
2058
|
+
def sweep_durations(self, sweep_count):
|
|
2059
|
+
"""
|
|
2060
|
+
Returns the (intended) durations of this channel's sweeps, as a list
|
|
2061
|
+
of times in seconds.
|
|
2062
|
+
"""
|
|
2063
|
+
d = np.array([seg.durations(sweep_count) for seg in self])
|
|
2064
|
+
return d.sum(axis=0)
|
|
2065
|
+
|
|
2066
|
+
def sweep_samples(self, sweep_count, dt):
|
|
2067
|
+
"""
|
|
2068
|
+
Returns the (intended) number of samples in each of this channel's
|
|
2069
|
+
sweeps.
|
|
2070
|
+
|
|
2071
|
+
Note that, unlike :meth:`sweep_durations`, this method takes storage
|
|
2072
|
+
into account.
|
|
2073
|
+
"""
|
|
2074
|
+
return np.sum(
|
|
2075
|
+
np.array([s.samples(sweep_count, dt) for s in self]),
|
|
2076
|
+
axis=0)
|
|
2077
|
+
|
|
2078
|
+
def unit(self):
|
|
2079
|
+
""" Returns the units that this channel's output is in. """
|
|
2080
|
+
return self._unit
|
|
2081
|
+
|
|
2082
|
+
|
|
2083
|
+
#
|
|
2084
|
+
# From StimFile_v9.txt
|
|
2085
|
+
# (* StimToDacID : Specifies how to convert the Segment "Voltage" to the actual
|
|
2086
|
+
# voltage sent to the DAC
|
|
2087
|
+
# -> meaning of bits:
|
|
2088
|
+
# bit 0 (UseStimScale) -> use StimScale
|
|
2089
|
+
# bit 1 (UseRelative) -> relative to Vmemb
|
|
2090
|
+
# bit 2 (UseFileTemplate) -> use file template
|
|
2091
|
+
# bit 3 (UseForLockIn) -> use for LockIn computation
|
|
2092
|
+
# bit 4 (UseForWavelength)
|
|
2093
|
+
# bit 5 (UseScaling)
|
|
2094
|
+
# bit 6 (UseForChirp)
|
|
2095
|
+
# bit 7 (UseForImaging)
|
|
2096
|
+
# bit 14 (UseReserved)
|
|
2097
|
+
# bit 15 (UseReserved)
|
|
2098
|
+
# *)
|
|
2099
|
+
#
|
|
2100
|
+
class StimulusChannelDACFlags:
|
|
2101
|
+
"""
|
|
2102
|
+
Takes a ``StimToDacID`` list of bools and sets them as named properties.
|
|
2103
|
+
|
|
2104
|
+
See "10.9.1 DA output channel settings" in the manual.
|
|
2105
|
+
"""
|
|
2106
|
+
def __init__(self, bools):
|
|
2107
|
+
self.use_stim_scale = bools[0] # Apply "standard conversion"
|
|
2108
|
+
self.use_relative = bools[1] # Relative to holding
|
|
2109
|
+
self.use_file_template = bools[2] # Use recorded wave form
|
|
2110
|
+
self.use_for_lock_in = bools[3]
|
|
2111
|
+
self.use_for_wavelength = bools[4]
|
|
2112
|
+
self.use_scaling = bools[5]
|
|
2113
|
+
self.use_for_chirp = bools[6]
|
|
2114
|
+
self.use_for_imaging = bools[7]
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
class AmplifierMode(enum.Enum):
|
|
2118
|
+
""" Amplifier mode """
|
|
2119
|
+
Any = 0
|
|
2120
|
+
VC = 1
|
|
2121
|
+
CC = 2
|
|
2122
|
+
IDensity = 3
|
|
2123
|
+
|
|
2124
|
+
def __str__(self):
|
|
2125
|
+
if self is AmplifierMode.Any:
|
|
2126
|
+
return 'any'
|
|
2127
|
+
elif self is AmplifierMode.VC:
|
|
2128
|
+
return 'voltage clamp'
|
|
2129
|
+
elif self is AmplifierMode.CC:
|
|
2130
|
+
return 'current clamp'
|
|
2131
|
+
else:
|
|
2132
|
+
return 'I density' # Can't find in manual
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
#
|
|
2136
|
+
# From StimFile_v9.txt
|
|
2137
|
+
# (* StimSegmentRecord = RECORD *)
|
|
2138
|
+
# seMark = 0; (* INT32 *)
|
|
2139
|
+
# seClass = 4; (* BYTE *)
|
|
2140
|
+
# seStoreKind = 5; (* BYTE *)
|
|
2141
|
+
# seVoltageIncMode = 6; (* BYTE *)
|
|
2142
|
+
# seDurationIncMode = 7; (* BYTE *)
|
|
2143
|
+
# seVoltage = 8; (* LONGREAL *)
|
|
2144
|
+
# seVoltageSource = 16; (* INT32 *)
|
|
2145
|
+
# seDeltaVFactor = 20; (* LONGREAL *)
|
|
2146
|
+
# seDeltaVIncrement = 28; (* LONGREAL *)
|
|
2147
|
+
# seDuration = 36; (* LONGREAL *)
|
|
2148
|
+
# seDurationSource = 44; (* INT32 *)
|
|
2149
|
+
# seDeltaTFactor = 48; (* LONGREAL *)
|
|
2150
|
+
# seDeltaTIncrement = 56; (* LONGREAL *)
|
|
2151
|
+
# seFiller1 = 64; (* INT32 *)
|
|
2152
|
+
# seCRC = 68; (* CARD32 *)
|
|
2153
|
+
# seScanRate = 72; (* LONGREAL *)
|
|
2154
|
+
# StimSegmentRecSize = 80; (* = 10 * 8 *)
|
|
2155
|
+
#
|
|
2156
|
+
class Segment(TreeNode):
|
|
2157
|
+
"""
|
|
2158
|
+
Represents a segment of a stimulus.
|
|
2159
|
+
"""
|
|
2160
|
+
def __init__(self, parent):
|
|
2161
|
+
super().__init__(parent)
|
|
2162
|
+
|
|
2163
|
+
self._class = None
|
|
2164
|
+
self._storage = None
|
|
2165
|
+
|
|
2166
|
+
# Value, usually voltage in V
|
|
2167
|
+
self._value = None
|
|
2168
|
+
self._value_increment = None
|
|
2169
|
+
self._value_delta = None
|
|
2170
|
+
self._value_factor = None
|
|
2171
|
+
self._value_from_holding = False
|
|
2172
|
+
self._value_from_parameter = None
|
|
2173
|
+
|
|
2174
|
+
# Duration, in s
|
|
2175
|
+
self._duration = None
|
|
2176
|
+
self._duration_increment = None
|
|
2177
|
+
self._duration_delta = None
|
|
2178
|
+
self._duration_factor = None
|
|
2179
|
+
self._duration_from_holding = False
|
|
2180
|
+
self._duration_from_parameter = None
|
|
2181
|
+
|
|
2182
|
+
def _read_properties(self, handle, reader):
|
|
2183
|
+
# See TreeNode._read_properties
|
|
2184
|
+
|
|
2185
|
+
start = handle.tell()
|
|
2186
|
+
handle.seek(start + 4)
|
|
2187
|
+
self._class = SegmentClass(reader.read1('b'))
|
|
2188
|
+
self._storage = SegmentStorage(reader.read1('b'))
|
|
2189
|
+
self._value_increment = SegmentIncrement(reader.read1('b'))
|
|
2190
|
+
self._duration_increment = SegmentIncrement(reader.read1('b'))
|
|
2191
|
+
|
|
2192
|
+
self._value = reader.read1('d')
|
|
2193
|
+
value_source = reader.read1('i')
|
|
2194
|
+
self._value_from_holding = (value_source == 1)
|
|
2195
|
+
self._value_from_parameter = (value_source > 1)
|
|
2196
|
+
self._value_factor = reader.read1('d')
|
|
2197
|
+
self._value_delta = reader.read1('d')
|
|
2198
|
+
|
|
2199
|
+
self._duration = reader.read1('d')
|
|
2200
|
+
duration_source = reader.read1('i')
|
|
2201
|
+
self._duration_from_holding = (duration_source == 1) # Can't happen ?
|
|
2202
|
+
self._duration_from_parameter = (duration_source > 1)
|
|
2203
|
+
self._duration_factor = reader.read1('d')
|
|
2204
|
+
self._duration_delta = reader.read1('d')
|
|
2205
|
+
|
|
2206
|
+
def __str__(self):
|
|
2207
|
+
if self._value_from_holding:
|
|
2208
|
+
v = 'Value from holding'
|
|
2209
|
+
elif self._value_from_parameter:
|
|
2210
|
+
v = 'Value from parameter'
|
|
2211
|
+
else:
|
|
2212
|
+
v = self._value_increment.format(
|
|
2213
|
+
self._value, self._value_delta, self._value_factor)
|
|
2214
|
+
v = f'Value {v}'
|
|
2215
|
+
|
|
2216
|
+
if self._duration_from_parameter:
|
|
2217
|
+
t = 'Duration from parameter'
|
|
2218
|
+
else:
|
|
2219
|
+
t = self._duration_increment.format(
|
|
2220
|
+
self._duration, self._duration_delta, self._duration_factor)
|
|
2221
|
+
t = f'Duration {t}'
|
|
2222
|
+
|
|
2223
|
+
return f'{t}; {v}'
|
|
2224
|
+
|
|
2225
|
+
def durations(self, sweep_count):
|
|
2226
|
+
"""
|
|
2227
|
+
Returns the (intended) durations of this segment, for the given number
|
|
2228
|
+
of sweeps.
|
|
2229
|
+
|
|
2230
|
+
For example, if this segment is used in a stimulus with 3 sweeps, and
|
|
2231
|
+
its duration increases with 1s per sweep from a base duration of 2s,
|
|
2232
|
+
the returned values would be ``[2, 3, 4]``.
|
|
2233
|
+
|
|
2234
|
+
Durations are returned regardless of whether data is stored or not
|
|
2235
|
+
(although the "First sweep" and "Last sweep" storage modes can affect
|
|
2236
|
+
the durations and so are considered by this method).
|
|
2237
|
+
|
|
2238
|
+
If unsupported features are encountered, a ``NotImplementedError`` is
|
|
2239
|
+
raised.
|
|
2240
|
+
"""
|
|
2241
|
+
if self._duration_from_parameter:
|
|
2242
|
+
# Note: Could get these from the Series?
|
|
2243
|
+
raise NotImplementedError('Segment duration set as parameter.')
|
|
2244
|
+
if self._duration_from_holding:
|
|
2245
|
+
raise NotImplementedError('Segment duration set as holding.')
|
|
2246
|
+
|
|
2247
|
+
durations = self._duration_increment.sweep_values(
|
|
2248
|
+
sweep_count,
|
|
2249
|
+
self._duration,
|
|
2250
|
+
self._duration_delta,
|
|
2251
|
+
self._duration_factor,
|
|
2252
|
+
)
|
|
2253
|
+
if self._storage is SegmentStorage.First:
|
|
2254
|
+
durations[1:] = 0
|
|
2255
|
+
elif self._storage is SegmentStorage.Last:
|
|
2256
|
+
durations[:-1] = 0
|
|
2257
|
+
return durations
|
|
2258
|
+
|
|
2259
|
+
def segment_class(self):
|
|
2260
|
+
""" Returns this segment's :class:`SegmentClass`. """
|
|
2261
|
+
return self._class
|
|
2262
|
+
|
|
2263
|
+
def samples(self, sweep_count, dt):
|
|
2264
|
+
"""
|
|
2265
|
+
Returns the number of samples that should be stored during this
|
|
2266
|
+
segment in each sweep.
|
|
2267
|
+
"""
|
|
2268
|
+
if self._storage is SegmentStorage.NotStored:
|
|
2269
|
+
return [0] * sweep_count
|
|
2270
|
+
|
|
2271
|
+
# Get sweep durations. This also handles SegmentStorage.First and Last
|
|
2272
|
+
durations = self.durations(sweep_count)
|
|
2273
|
+
return [int(round(d / dt)) for d in durations]
|
|
2274
|
+
|
|
2275
|
+
def storage(self):
|
|
2276
|
+
""" Returns this segment's :class:`SegmentStorage`. """
|
|
2277
|
+
return self._storage
|
|
2278
|
+
|
|
2279
|
+
def values(self, sweep_count, holding, relative):
|
|
2280
|
+
"""
|
|
2281
|
+
Returns the target values of this segment, for the given number of
|
|
2282
|
+
sweeps.
|
|
2283
|
+
|
|
2284
|
+
For example, if this is a ``Constant`` segment in a stimulus with 4
|
|
2285
|
+
sweeps, and its value increases with 0.02 V per sweep from a base value
|
|
2286
|
+
of -0.08 V, the returned values are ``[-0.08, -0.06, -0.04, -0.02]``.
|
|
2287
|
+
|
|
2288
|
+
Note: These are the values for ``Constant`` segments. The
|
|
2289
|
+
interpretation for other segment classes is unclear.
|
|
2290
|
+
|
|
2291
|
+
If unsupported features are encountered, a ``NotImplementedError`` is
|
|
2292
|
+
raised.
|
|
2293
|
+
|
|
2294
|
+
Arguments:
|
|
2295
|
+
|
|
2296
|
+
``sweep_count``
|
|
2297
|
+
The number of sweeps
|
|
2298
|
+
``holding``
|
|
2299
|
+
The holding potential; used if the segment is defined as "at
|
|
2300
|
+
holding potential" or if ``relative==True``.
|
|
2301
|
+
``relative``
|
|
2302
|
+
Set to ``True`` if this segment's value is set relative to the
|
|
2303
|
+
holding potential.
|
|
2304
|
+
|
|
2305
|
+
"""
|
|
2306
|
+
if self._value_from_parameter:
|
|
2307
|
+
# Note: Could get these from the Series?
|
|
2308
|
+
raise NotImplementedError('Segment value set as parameter.')
|
|
2309
|
+
|
|
2310
|
+
# Always at holding potential (can ignore ``relative`` here).
|
|
2311
|
+
if self._value_from_holding:
|
|
2312
|
+
return [holding] * sweep_count
|
|
2313
|
+
|
|
2314
|
+
# Get values, add holding if necessary
|
|
2315
|
+
values = self._value_increment.sweep_values(
|
|
2316
|
+
sweep_count,
|
|
2317
|
+
self._value,
|
|
2318
|
+
self._value_delta,
|
|
2319
|
+
self._value_factor,
|
|
2320
|
+
)
|
|
2321
|
+
if relative:
|
|
2322
|
+
return holding + values
|
|
2323
|
+
return values
|
|
2324
|
+
|
|
2325
|
+
|
|
2326
|
+
class SegmentClass(enum.Enum):
|
|
2327
|
+
""" Type of segment """
|
|
2328
|
+
Constant = 0
|
|
2329
|
+
Ramp = 1
|
|
2330
|
+
Continuous = 2
|
|
2331
|
+
ConstSine = 3
|
|
2332
|
+
Squarewave = 4
|
|
2333
|
+
Chirpwave = 5
|
|
2334
|
+
|
|
2335
|
+
def __str__(self):
|
|
2336
|
+
if self is SegmentClass.Constant:
|
|
2337
|
+
return 'Constant'
|
|
2338
|
+
elif self is SegmentClass.Ramp:
|
|
2339
|
+
return 'Ramp'
|
|
2340
|
+
elif self is SegmentClass.Continuous:
|
|
2341
|
+
return 'Continuous'
|
|
2342
|
+
elif self is SegmentClass.ConstSine:
|
|
2343
|
+
return 'Sine wave'
|
|
2344
|
+
elif self is SegmentClass.Squarewave:
|
|
2345
|
+
return 'Square wave'
|
|
2346
|
+
else:
|
|
2347
|
+
return 'Chirp wave'
|
|
2348
|
+
|
|
2349
|
+
|
|
2350
|
+
class SegmentStorage(enum.Enum):
|
|
2351
|
+
""" Segment storage mode """
|
|
2352
|
+
NotStored = 0
|
|
2353
|
+
Stored = 1
|
|
2354
|
+
|
|
2355
|
+
# The next two determine more than just storage:
|
|
2356
|
+
# "First Sweep: These segments are output only with the first Sweep of the
|
|
2357
|
+
# Series but are not stored."
|
|
2358
|
+
# "Last Sweep: These segments are output only with the last Sweep of the
|
|
2359
|
+
# Series but are not stored."
|
|
2360
|
+
First = 2
|
|
2361
|
+
Last = 3
|
|
2362
|
+
|
|
2363
|
+
def __str__(self):
|
|
2364
|
+
if self is SegmentStorage.Stored:
|
|
2365
|
+
return 'Stored'
|
|
2366
|
+
elif self is SegmentStorage.NotStored:
|
|
2367
|
+
return 'Not stored'
|
|
2368
|
+
elif self is SegmentStorage.First:
|
|
2369
|
+
return 'Not stored, output on first sweep only'
|
|
2370
|
+
else:
|
|
2371
|
+
return 'Not stored, output on last sweep only'
|
|
2372
|
+
|
|
2373
|
+
|
|
2374
|
+
class SegmentIncrement(enum.Enum):
|
|
2375
|
+
""" Segment increment mode (for time or voltage) """
|
|
2376
|
+
Increase = 0
|
|
2377
|
+
Decrease = 1
|
|
2378
|
+
IncreaseInterleaved = 2
|
|
2379
|
+
DecreaseInterleaved = 3
|
|
2380
|
+
Alternate = 4
|
|
2381
|
+
LogIncrease = 5
|
|
2382
|
+
LogDecrease = 6
|
|
2383
|
+
LogIncreaseInterleaved = 7
|
|
2384
|
+
LogDecreaseInterleaved = 8
|
|
2385
|
+
LogAlternate = 9
|
|
2386
|
+
# Note: The manual mentions a "toggle" mode, for V only
|
|
2387
|
+
|
|
2388
|
+
def __str__(self):
|
|
2389
|
+
if self is SegmentIncrement.Increase:
|
|
2390
|
+
return 'Increase'
|
|
2391
|
+
elif self is SegmentIncrement.Decrease:
|
|
2392
|
+
return 'Decrease'
|
|
2393
|
+
elif self is SegmentIncrement.IncreaseInterleaved:
|
|
2394
|
+
return 'Interleave+'
|
|
2395
|
+
elif self is SegmentIncrement.DecreaseInterleaved:
|
|
2396
|
+
return 'Interleave-'
|
|
2397
|
+
elif self is SegmentIncrement.Alternate:
|
|
2398
|
+
return 'Alternate'
|
|
2399
|
+
elif self is SegmentIncrement.LogIncrease:
|
|
2400
|
+
return 'Log increase'
|
|
2401
|
+
elif self is SegmentIncrement.LogDecrease:
|
|
2402
|
+
return 'Log decrease'
|
|
2403
|
+
elif self is SegmentIncrement.LogIncreaseInterleaved:
|
|
2404
|
+
return 'Log interleave+'
|
|
2405
|
+
elif self is SegmentIncrement.LogDecreaseInterleaved:
|
|
2406
|
+
return 'Log interleave-'
|
|
2407
|
+
else:
|
|
2408
|
+
return 'Log alternate'
|
|
2409
|
+
|
|
2410
|
+
def format(self, base, delta, factor):
|
|
2411
|
+
"""
|
|
2412
|
+
Formats this increment.
|
|
2413
|
+
|
|
2414
|
+
Note: The manual explains about a "t * factor" and a "dt * factor"
|
|
2415
|
+
mode, but it's not clear where this is stored in the file.
|
|
2416
|
+
"""
|
|
2417
|
+
if self.value < 5:
|
|
2418
|
+
if delta == 0:
|
|
2419
|
+
return f'constant at {base}'
|
|
2420
|
+
return f'{self.name} from {base} with step {delta}'
|
|
2421
|
+
|
|
2422
|
+
if factor == 1:
|
|
2423
|
+
return f'constant at {base}'
|
|
2424
|
+
|
|
2425
|
+
return f'{self.name} from {base} with dt {delta} and factor {factor}'
|
|
2426
|
+
|
|
2427
|
+
def sweep_order(self, n_sweeps):
|
|
2428
|
+
"""
|
|
2429
|
+
Returns a list of array indices in the order specified by this segment
|
|
2430
|
+
increment mode.
|
|
2431
|
+
|
|
2432
|
+
For example, for 6 sweeps and mode ``Increase``, it returns
|
|
2433
|
+
``[0, 1, 2, 3, 4, 5]``. For 5 sweeps and mode ``Alternate`` it returns
|
|
2434
|
+
``[0, 4, 1, 3, 2]``.
|
|
2435
|
+
|
|
2436
|
+
If unsupported features are encountered, a ``NotImplementedError`` is
|
|
2437
|
+
raised.
|
|
2438
|
+
"""
|
|
2439
|
+
S = SegmentIncrement
|
|
2440
|
+
if self in (S.Increase, S.LogIncrease):
|
|
2441
|
+
return np.arange(n_sweeps)
|
|
2442
|
+
elif self in (S.Decrease, S.LogDecrease):
|
|
2443
|
+
return np.arange(n_sweeps - 1, -1, -1)
|
|
2444
|
+
elif self in (S.IncreaseInterleaved, S.LogIncreaseInterleaved):
|
|
2445
|
+
# Interleaved
|
|
2446
|
+
# The pattern given in the manual is not explained and only
|
|
2447
|
+
# length-6 examples are given. Easy to come up with different
|
|
2448
|
+
# interpretations of what an odd-numbered sequence looks like...
|
|
2449
|
+
#indices = np.arange(n_sweeps)
|
|
2450
|
+
#indices[1:-1:2] = range(2, n_sweeps, 2)
|
|
2451
|
+
#indices[2::2] = range(1, n_sweeps - 1, 2)
|
|
2452
|
+
#return indices
|
|
2453
|
+
raise NotImplementedError('Segment with interleaved increase')
|
|
2454
|
+
elif self in (S.DecreaseInterleaved, S.LogDecreaseInterleaved):
|
|
2455
|
+
# Interleaved
|
|
2456
|
+
# The pattern given in the manual is not explained and only
|
|
2457
|
+
# length-6 examples are given. Easy to come up with different
|
|
2458
|
+
# interpretations of what an odd-numbered sequence looks like...
|
|
2459
|
+
#indices = np.zeros(n_sweeps) - 1#, -1, -1) - 1
|
|
2460
|
+
#indices[1:-1:2] = range(n_sweeps - 3, -1, -2)
|
|
2461
|
+
#indices[2:-2:2] = range(n_sweeps - 2, 2, -2)
|
|
2462
|
+
#return indices
|
|
2463
|
+
raise NotImplementedError('Segment with interleaved decrease')
|
|
2464
|
+
elif self is S.Alternate:
|
|
2465
|
+
indices = np.zeros(n_sweeps) * np.nan
|
|
2466
|
+
nh = (1 + n_sweeps) // 2
|
|
2467
|
+
indices[::2] = range(0, nh, 1)
|
|
2468
|
+
indices[1::2] = range(n_sweeps - 1, nh - 1, -1)
|
|
2469
|
+
return indices
|
|
2470
|
+
|
|
2471
|
+
def sweep_values(self, n_sweeps, base, delta, factor):
|
|
2472
|
+
"""
|
|
2473
|
+
Returns the sequence specified by this order for the given number of
|
|
2474
|
+
sweeps, base level, and "delta" or "factor" increments.
|
|
2475
|
+
|
|
2476
|
+
For example, for mode "Increasing" with a base of -80, a delta of 10,
|
|
2477
|
+
and 4 sweeps, it returns [-80, -70, -60, -50].
|
|
2478
|
+
"""
|
|
2479
|
+
# Logarithmic increments are not supported (because I don't understand
|
|
2480
|
+
# how to select between the two logarithmic equations given in the
|
|
2481
|
+
# manual).
|
|
2482
|
+
if self.value > 4:
|
|
2483
|
+
if factor == 1:
|
|
2484
|
+
return np.ones(n_sweeps) * base
|
|
2485
|
+
raise NotImplementedError(
|
|
2486
|
+
'Segment with logarithmic increments or decrements.')
|
|
2487
|
+
elif delta == 0:
|
|
2488
|
+
return np.ones(n_sweeps) * base
|
|
2489
|
+
|
|
2490
|
+
values = base + delta * np.arange(n_sweeps)
|
|
2491
|
+
return values[self.sweep_order(n_sweeps)]
|
|
2492
|
+
|
|
2493
|
+
|
|
2494
|
+
class NoSupportedDAChannelError(myokit.MyokitError):
|
|
2495
|
+
"""
|
|
2496
|
+
Raised if no channel can be found in a stimulus to convert to a D/A
|
|
2497
|
+
signal or protocol.
|
|
2498
|
+
"""
|
|
2499
|
+
def __init__(self):
|
|
2500
|
+
super().__init__('No supported DAC Channel found')
|
|
2501
|
+
|
|
2502
|
+
|
|
2503
|
+
# Encoding for text parts of files
|
|
2504
|
+
_ENC = 'latin-1'
|
|
2505
|
+
|
|
2506
|
+
# Time offset since 1990, utc
|
|
2507
|
+
_tz = datetime.timezone.utc
|
|
2508
|
+
_ts_1990 = datetime.datetime(1990, 1, 1, tzinfo=_tz).timestamp()
|
|
2509
|
+
|
|
2510
|
+
_data_types = (np.int16, np.int32, np.float32, np.float64)
|
|
2511
|
+
_data_sizes = (2, 4, 4, 8)
|
|
2512
|
+
|