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
myokit/formats/wcp/_wcp.py
CHANGED
|
@@ -14,163 +14,191 @@ import myokit
|
|
|
14
14
|
_ENC = 'ascii'
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class WcpFile:
|
|
17
|
+
class WcpFile(myokit.formats.SweepSource):
|
|
18
18
|
"""
|
|
19
|
-
Represents a read-only WinWCP file (``.wcp``), stored at
|
|
20
|
-
pointed to by ``filepath``.
|
|
19
|
+
Represents a read-only WinWCP file (``.wcp``), stored at ``path``.
|
|
21
20
|
|
|
22
21
|
Only files in the newer file format version 9 can be read. This version of
|
|
23
22
|
the format was introduced in 2010. New versions of WinWCP can read older
|
|
24
23
|
files and will convert them to the new format automatically when opened.
|
|
25
24
|
|
|
26
25
|
WinWCP is a tool for recording electrophysiological data written by John
|
|
27
|
-
Dempster of Strathclyde University.
|
|
26
|
+
Dempster of Strathclyde University. For more information, see
|
|
27
|
+
https://documentation.help/WinWCP-V5.3.8/IDH_Topic750.htm
|
|
28
28
|
|
|
29
29
|
WinWCP files contain a number of records ``NR``, each containing data from
|
|
30
30
|
``NC`` channels. Every channel has the same length, ``NP`` samples.
|
|
31
31
|
Sampling happens at a fixed sampling rate.
|
|
32
32
|
"""
|
|
33
|
-
def __init__(self,
|
|
33
|
+
def __init__(self, path):
|
|
34
34
|
# The path to the file and its basename
|
|
35
|
-
|
|
36
|
-
self.
|
|
37
|
-
self._filename = os.path.basename(
|
|
35
|
+
path = str(path)
|
|
36
|
+
self._path = os.path.abspath(path)
|
|
37
|
+
self._filename = os.path.basename(path)
|
|
38
|
+
|
|
39
|
+
# Header info, per file, per record, and per channel
|
|
40
|
+
self._version_str = None
|
|
41
|
+
self._header = {}
|
|
42
|
+
self._header_raw = {}
|
|
43
|
+
self._record_headers = []
|
|
44
|
+
self._channel_headers = []
|
|
38
45
|
|
|
39
46
|
# Records
|
|
40
|
-
self._records =
|
|
41
|
-
self._channel_names = None
|
|
47
|
+
self._records = []
|
|
42
48
|
self._nr = None # Records in file
|
|
43
49
|
self._nc = None # Channels per record
|
|
44
50
|
self._np = None # Samples per channel
|
|
45
|
-
|
|
51
|
+
self._dt = None # Sampling interval (s)
|
|
52
|
+
|
|
53
|
+
# Channels
|
|
54
|
+
self._channel_names = []
|
|
55
|
+
self._channel_name_map = {}
|
|
46
56
|
|
|
47
57
|
# Time signal
|
|
48
58
|
self._time = None
|
|
49
59
|
|
|
60
|
+
# Units
|
|
61
|
+
self._time_unit = None
|
|
62
|
+
self._channel_units = []
|
|
63
|
+
self._unit_cache = {}
|
|
64
|
+
|
|
50
65
|
# Read the file
|
|
51
|
-
with open(
|
|
66
|
+
with open(path, 'rb') as f:
|
|
52
67
|
self._read(f)
|
|
53
68
|
|
|
54
69
|
def _read(self, f):
|
|
55
|
-
"""
|
|
56
|
-
Reads the file header & data.
|
|
57
|
-
"""
|
|
70
|
+
""" Reads the file header & data. """
|
|
58
71
|
# Header size is between 1024 and 16380, depending on number of
|
|
59
72
|
# channels in the file following:
|
|
60
73
|
# n = (int((n_channels - 1)/8) + 1) * 1024
|
|
61
74
|
# Read first part of header, determine version and number of channels
|
|
62
75
|
# in the file
|
|
63
|
-
data = f.read(1024)
|
|
64
|
-
h = [x.strip().split(
|
|
76
|
+
data = f.read(1024).decode(_ENC)
|
|
77
|
+
h = [x.strip().split('=') for x in data.split('\n')]
|
|
65
78
|
h = dict([(x[0].lower(), x[1]) for x in h if len(x) == 2])
|
|
66
|
-
if int(h[
|
|
79
|
+
if int(h['ver']) != 9:
|
|
67
80
|
raise NotImplementedError(
|
|
68
81
|
'Only able to read format version 9. Given file is in format'
|
|
69
|
-
' version ' +
|
|
82
|
+
' version ' + h['ver'])
|
|
83
|
+
self._version_str = h['ver']
|
|
70
84
|
|
|
71
85
|
# Get header size
|
|
72
86
|
try:
|
|
73
87
|
# Get number of 512 byte sectors in header
|
|
74
|
-
#header_size = 512 * int(h[
|
|
88
|
+
#header_size = 512 * int(h['nbh'])
|
|
75
89
|
# Seems to be size in bytes!
|
|
76
|
-
header_size = int(h[
|
|
90
|
+
header_size = int(h['nbh'])
|
|
77
91
|
except KeyError: # pragma: no cover
|
|
78
92
|
# Calculate header size based on number of channels
|
|
79
|
-
header_size = (int((int(h[
|
|
93
|
+
header_size = (int((int(h['nc']) - 1) / 8) + 1) * 1024
|
|
80
94
|
|
|
81
95
|
# Read remaining header data
|
|
82
96
|
if header_size > 1024: # pragma: no cover
|
|
83
|
-
data += f.read(header_size - 1024)
|
|
84
|
-
h = [x.strip().split(
|
|
97
|
+
data += f.read(header_size - 1024).decode(_ENC)
|
|
98
|
+
h = [x.strip().split('=') for x in data.split('\n')]
|
|
85
99
|
h = dict([(x[0].lower(), x[1]) for x in h if len(x) == 2])
|
|
86
100
|
|
|
87
101
|
# Tidy up read data
|
|
88
|
-
header = {}
|
|
89
|
-
header_raw = {}
|
|
90
102
|
for k, v in h.items():
|
|
91
103
|
# Convert to appropriate data type
|
|
92
104
|
try:
|
|
93
105
|
t = HEADER_FIELDS[k]
|
|
94
106
|
if t == float:
|
|
95
107
|
# Allow for windows locale stuff
|
|
96
|
-
v = v.replace(
|
|
97
|
-
|
|
108
|
+
v = v.replace(',', '.')
|
|
109
|
+
self._header[k] = t(v)
|
|
98
110
|
except KeyError:
|
|
99
|
-
|
|
111
|
+
self._header_raw[k] = v
|
|
100
112
|
|
|
101
113
|
# Convert time
|
|
102
|
-
# No, don't. It's in different formats depending on...
|
|
114
|
+
# No, don't. It's in different formats depending on... user locale?
|
|
103
115
|
# if 'ctime' in header:
|
|
104
|
-
# print(header[
|
|
105
|
-
# ctime = time.strptime(header[
|
|
106
|
-
# header[
|
|
116
|
+
# print(header['ctime'])
|
|
117
|
+
# ctime = time.strptime(header['ctime'], "%d/%m/%Y %H:%M:%S")
|
|
118
|
+
# header['ctime'] = time.strftime('%Y-%m-%d %H:%M:%S', ctime)
|
|
107
119
|
|
|
108
120
|
# Get vital fields from header
|
|
109
|
-
#
|
|
110
|
-
self._nr =
|
|
111
|
-
|
|
112
|
-
# Channels per record
|
|
113
|
-
self._nc = header[b'nc']
|
|
121
|
+
self._dt = self._header['dt'] # Sampling interval
|
|
122
|
+
self._nr = self._header['nr'] # Records in file
|
|
123
|
+
self._nc = self._header['nc'] # Channels per record
|
|
114
124
|
try:
|
|
115
|
-
# Samples per channel
|
|
116
|
-
self._np = header[b'np']
|
|
125
|
+
self._np = self._header['np'] # Samples per channel
|
|
117
126
|
except KeyError:
|
|
118
|
-
self._np = (
|
|
127
|
+
self._np = (self._header['nbd'] * 512) // (2 * self._nc)
|
|
119
128
|
|
|
120
|
-
# Get
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
# Get time units
|
|
130
|
+
self._time_unit = myokit.units.s
|
|
131
|
+
# Time as set by dt (which is what we need) is _always_ in seconds.
|
|
132
|
+
# John Dempster says: "The TU= value referred to the time units which
|
|
133
|
+
# were displayed in early versions of WinWCP and no longer exists in
|
|
134
|
+
# recent WCP data files." (Email to michael, 2023-07-12).
|
|
135
|
+
|
|
136
|
+
# Get channel-specific fields
|
|
123
137
|
for i in range(self._nc):
|
|
124
|
-
j = str(i).encode(_ENC)
|
|
125
138
|
c = {}
|
|
126
139
|
for k, t in HEADER_CHANNEL_FIELDS.items():
|
|
127
|
-
c[k] = t(h[k +
|
|
128
|
-
|
|
129
|
-
self._channel_names.append(c[
|
|
140
|
+
c[k] = t(h[k + str(i)])
|
|
141
|
+
self._channel_headers.append(c)
|
|
142
|
+
self._channel_names.append(c['yn'])
|
|
143
|
+
self._channel_name_map[c['yn']] = i
|
|
144
|
+
self._channel_units.append(self._unit(c['yu']))
|
|
130
145
|
|
|
131
146
|
# Analysis block size and data block size
|
|
132
147
|
# Data is stored as 16 bit integers (little-endian)
|
|
133
148
|
try:
|
|
134
|
-
rab_size = 512 *
|
|
149
|
+
rab_size = 512 * self._header['nba']
|
|
135
150
|
except KeyError: # pragma: no cover
|
|
136
151
|
rab_size = header_size
|
|
137
152
|
try:
|
|
138
|
-
rdb_size = 512 *
|
|
153
|
+
rdb_size = 512 * self._header['nbd']
|
|
139
154
|
except KeyError: # pragma: no cover
|
|
140
155
|
rdb_size = 2 * self._nc * self._np
|
|
141
156
|
|
|
142
157
|
# Maximum A/D sample value at vmax
|
|
143
|
-
adcmax =
|
|
158
|
+
adcmax = self._header['adcmax']
|
|
144
159
|
|
|
145
160
|
# Read data records
|
|
146
|
-
records = []
|
|
147
161
|
offset = header_size
|
|
148
162
|
for i in range(self._nr):
|
|
149
163
|
# Read analysis block
|
|
150
164
|
f.seek(offset)
|
|
151
165
|
|
|
152
166
|
# Status of signal (Accepted or rejected, as string)
|
|
153
|
-
rstatus = f.read(8)
|
|
167
|
+
rstatus = f.read(8).decode(_ENC)
|
|
154
168
|
|
|
155
169
|
# Type of recording, as string
|
|
156
|
-
rtype = f.read(4)
|
|
170
|
+
rtype = f.read(4).decode(_ENC)
|
|
157
171
|
|
|
158
|
-
#
|
|
172
|
+
# Leak subtraction group number (float set by the user)
|
|
159
173
|
group_number = struct.unpack(str('<f'), f.read(4))[0]
|
|
160
174
|
|
|
161
175
|
# Time of recording, as float, not sure how to interpret
|
|
162
176
|
rtime = struct.unpack(str('<f'), f.read(4))[0]
|
|
163
177
|
|
|
164
|
-
# Sampling interval
|
|
165
|
-
#
|
|
166
|
-
|
|
178
|
+
# Sampling interval
|
|
179
|
+
# It is technically possible to have different dts for different
|
|
180
|
+
# records (see email J.D. 2023-07-12), but rare.
|
|
181
|
+
# Not supported here!
|
|
182
|
+
rint = round(struct.unpack(str('<f'), f.read(4))[0], 6)
|
|
183
|
+
if rint != self._dt: # pragma: no cover
|
|
184
|
+
raise ValueError(
|
|
185
|
+
'Unsupported feature: WCP file contains more than one'
|
|
186
|
+
' sampling rate.')
|
|
167
187
|
|
|
168
188
|
# Maximum positive limit of A/D converter voltage range
|
|
169
189
|
vmax = struct.unpack(
|
|
170
190
|
str('<' + 'f' * self._nc), f.read(4 * self._nc))
|
|
171
191
|
|
|
172
192
|
# String marker set by user
|
|
173
|
-
marker = f.read(16)
|
|
193
|
+
marker = f.read(16).decode(_ENC).strip('\x00')
|
|
194
|
+
|
|
195
|
+
# Store
|
|
196
|
+
self._record_headers.append({
|
|
197
|
+
'status': rstatus,
|
|
198
|
+
'type': rtype,
|
|
199
|
+
'rtime': rtime,
|
|
200
|
+
'marker': marker,
|
|
201
|
+
})
|
|
174
202
|
|
|
175
203
|
# Delete unused
|
|
176
204
|
del rstatus, rtype, group_number, rtime, rint, marker
|
|
@@ -180,99 +208,252 @@ class WcpFile:
|
|
|
180
208
|
|
|
181
209
|
# Get data from data block
|
|
182
210
|
data = np.memmap(
|
|
183
|
-
self.
|
|
211
|
+
self._path, np.dtype('<i2'), 'r',
|
|
184
212
|
shape=(self._np, self._nc),
|
|
185
213
|
offset=offset,
|
|
186
214
|
)
|
|
187
215
|
|
|
188
216
|
# Separate channels and apply scaling
|
|
189
|
-
record = [
|
|
190
|
-
|
|
191
|
-
h
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
record.append(d)
|
|
195
|
-
records.append(record)
|
|
217
|
+
record = [
|
|
218
|
+
vmx / (adcmax * h['yg']) * data[:, h['yo']].astype('f4')
|
|
219
|
+
for h, vmx in zip(self._channel_headers, vmax)]
|
|
220
|
+
|
|
221
|
+
self._records.append(record)
|
|
196
222
|
|
|
197
223
|
# Increase offset beyong data block
|
|
198
224
|
offset += rdb_size
|
|
199
225
|
|
|
200
|
-
self._records = records
|
|
201
|
-
|
|
202
226
|
# Create time signal
|
|
203
|
-
self._time = np.arange(self._np) *
|
|
227
|
+
self._time = np.arange(self._np) * self._dt
|
|
228
|
+
|
|
229
|
+
def __getitem__(self, key):
|
|
230
|
+
return self._records[key]
|
|
231
|
+
|
|
232
|
+
def __iter__(self):
|
|
233
|
+
return iter(self._records)
|
|
234
|
+
|
|
235
|
+
def __len__(self):
|
|
236
|
+
return self._nr
|
|
237
|
+
|
|
238
|
+
def _channel_id(self, channel_id):
|
|
239
|
+
""" Checks an int or str channel id and returns a valid int. """
|
|
240
|
+
if self._nr == 0: # pragma: no cover
|
|
241
|
+
raise KeyError(f'Channel {channel_id} not found (empty file).')
|
|
242
|
+
|
|
243
|
+
# Handle string
|
|
244
|
+
if isinstance(channel_id, str):
|
|
245
|
+
return self._channel_name_map[channel_id] # Pass KeyError to user
|
|
246
|
+
|
|
247
|
+
int_id = int(channel_id) # TypeError for user
|
|
248
|
+
if int_id < 0 or int_id >= self._nc:
|
|
249
|
+
raise IndexError(f'channel_id out of range: {channel_id}')
|
|
250
|
+
return int_id
|
|
251
|
+
|
|
252
|
+
def channel(self, channel_id, join_sweeps=False):
|
|
253
|
+
# Docstring in SweepSource
|
|
254
|
+
channel_id = self._channel_id(channel_id)
|
|
255
|
+
time, data = [], []
|
|
256
|
+
for r, h in zip(self._records, self._record_headers):
|
|
257
|
+
time.append(self._time + h['rtime'])
|
|
258
|
+
data.append(r[channel_id])
|
|
259
|
+
if join_sweeps:
|
|
260
|
+
return (np.concatenate(time), np.concatenate(data))
|
|
261
|
+
return time, data
|
|
204
262
|
|
|
205
263
|
def channels(self):
|
|
206
|
-
"""
|
|
207
|
-
|
|
208
|
-
|
|
264
|
+
""" Deprecated alias of :meth:`channel_count`. """
|
|
265
|
+
# Deprecated since 2023-06-22
|
|
266
|
+
import warnings
|
|
267
|
+
warnings.warn(
|
|
268
|
+
'The method `channels` is deprecated. Please use'
|
|
269
|
+
' WcpFile.channel_count() instead.')
|
|
209
270
|
return self._nc
|
|
210
271
|
|
|
211
|
-
def
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
272
|
+
def channel_count(self):
|
|
273
|
+
# Docstring in SweepSource
|
|
274
|
+
return self._nc
|
|
275
|
+
|
|
276
|
+
def channel_names(self, index=None):
|
|
277
|
+
# Docstring in SweepSource
|
|
278
|
+
if index is None:
|
|
279
|
+
return list(self._channel_names)
|
|
280
|
+
return self._channel_names[index]
|
|
281
|
+
|
|
282
|
+
def channel_units(self, index=None):
|
|
283
|
+
# Docstring in SweepSource
|
|
284
|
+
if index is None:
|
|
285
|
+
return list(self._channel_units)
|
|
286
|
+
return self._channel_units[index]
|
|
287
|
+
|
|
288
|
+
def da_count(self):
|
|
289
|
+
# Docstring in SweepSource. Rest is allowed raise NotImplementedError
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
def equal_length_sweeps(self):
|
|
293
|
+
# Docstring in SweepSource
|
|
294
|
+
return True
|
|
216
295
|
|
|
217
296
|
def filename(self):
|
|
218
|
-
"""
|
|
219
|
-
Returns the current file's name.
|
|
220
|
-
"""
|
|
297
|
+
""" Returns this file's name. """
|
|
221
298
|
return self._filename
|
|
222
299
|
|
|
223
|
-
def
|
|
224
|
-
"""
|
|
225
|
-
|
|
226
|
-
|
|
300
|
+
def info(self):
|
|
301
|
+
""" Deprecated alias of :meth:`meta_str` """
|
|
302
|
+
# Deprecated since 2023-07-12
|
|
303
|
+
import warnings
|
|
304
|
+
warnings.warn(
|
|
305
|
+
'The method `info` is deprecated. Please use `meta_str` instead.')
|
|
306
|
+
return self.meta_str(False)
|
|
227
307
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
308
|
+
def log(self, join_sweeps=False, use_names=False, include_da=True):
|
|
309
|
+
# Docstring in SweepSource
|
|
310
|
+
|
|
311
|
+
# Create log
|
|
231
312
|
log = myokit.DataLog()
|
|
313
|
+
if self._nr == 0: # pragma: no cover
|
|
314
|
+
return log
|
|
315
|
+
|
|
316
|
+
# Get channel names
|
|
317
|
+
channel_names = self._channel_names
|
|
318
|
+
if not use_names:
|
|
319
|
+
channel_names = [f'{i}.channel' for i in range(self._nc)]
|
|
320
|
+
|
|
321
|
+
# Gather data and return
|
|
322
|
+
if join_sweeps:
|
|
323
|
+
log['time'] = np.concatenate(
|
|
324
|
+
[self._time + h['rtime'] for h in self._record_headers])
|
|
325
|
+
for c, name in enumerate(channel_names):
|
|
326
|
+
log[name] = np.concatenate([r[c] for r in self._records])
|
|
327
|
+
else:
|
|
328
|
+
# Return individual sweeps
|
|
329
|
+
log['time'] = self._time
|
|
330
|
+
for r, record in enumerate(self._records):
|
|
331
|
+
for c, name in enumerate(channel_names):
|
|
332
|
+
log[name, r] = record[c]
|
|
333
|
+
|
|
232
334
|
log.set_time_key('time')
|
|
233
|
-
log['time'] = np.array(self._time)
|
|
234
|
-
for i, record in enumerate(self._records):
|
|
235
|
-
for j, data in enumerate(record):
|
|
236
|
-
name = self._channel_names[j]
|
|
237
|
-
log[name, i] = np.array(data)
|
|
238
335
|
return log
|
|
239
336
|
|
|
240
|
-
def
|
|
337
|
+
def matplotlib_figure(self):
|
|
338
|
+
""" Creates and returns a matplotlib figure with this file's data. """
|
|
339
|
+
import matplotlib.pyplot as plt
|
|
340
|
+
f = plt.figure()
|
|
341
|
+
axes = [f.add_subplot(self._nc, 1, 1 + i) for i in range(self._nc)]
|
|
342
|
+
for record in self._records:
|
|
343
|
+
for ax, channel in zip(axes, record):
|
|
344
|
+
ax.plot(self._time, channel)
|
|
345
|
+
return f
|
|
346
|
+
|
|
347
|
+
def meta_str(self, verbose=False):
|
|
348
|
+
# Docstring in SweepSource
|
|
349
|
+
h = self._header
|
|
350
|
+
out = []
|
|
351
|
+
|
|
352
|
+
# Add file info
|
|
353
|
+
out.append(f'WinWCP file: {self._filename}')
|
|
354
|
+
out.append(f'WinWCP Format version {self._version_str}')
|
|
355
|
+
out.append(f'Recorded on: {h["rtime"]}')
|
|
356
|
+
|
|
357
|
+
# Basic records info
|
|
358
|
+
out.append(f' Number of records: {self._nr}')
|
|
359
|
+
out.append(f' Channels per record: {self._nc}')
|
|
360
|
+
out.append(f' Samples per channel: {self._np}')
|
|
361
|
+
out.append(f' Sampling interval: {self._dt} s')
|
|
362
|
+
|
|
363
|
+
# Channel info
|
|
364
|
+
for c in self._channel_headers:
|
|
365
|
+
out.append(f'A/D channel: {c["yn"]}')
|
|
366
|
+
out.append(f' Unit: {c["yu"]}')
|
|
367
|
+
|
|
368
|
+
# Record info
|
|
369
|
+
out.append(
|
|
370
|
+
'Records: Type, Status, Sampling Interval, Start, Marker')
|
|
371
|
+
for i, r in enumerate(self._record_headers):
|
|
372
|
+
out.append(f'Record {i}: {r["type"]}, {r["status"]},'
|
|
373
|
+
f' {r["rtime"]}, "{r["marker"]}"')
|
|
374
|
+
|
|
375
|
+
# Parsed and unparsed header
|
|
376
|
+
if verbose:
|
|
377
|
+
out.append(f'{"-" * 35} header {"-" * 35}')
|
|
378
|
+
for k, v in self._header.items():
|
|
379
|
+
out.append(f'{k}: {v}')
|
|
380
|
+
|
|
381
|
+
out.append(f'{"-" * 33} raw header {"-" * 33}')
|
|
382
|
+
for k, v in self._header_raw.items():
|
|
383
|
+
out.append(f'{k}: {v}')
|
|
384
|
+
|
|
385
|
+
return '\n'.join(out)
|
|
386
|
+
|
|
387
|
+
def myokit_log(self):
|
|
241
388
|
"""
|
|
242
|
-
|
|
389
|
+
Deprecated method. Please use ``WcpFile.log(use_names=True)`` instead.
|
|
243
390
|
"""
|
|
244
|
-
|
|
391
|
+
# Deprecated since 2023-06-22
|
|
392
|
+
import warnings
|
|
393
|
+
warnings.warn(
|
|
394
|
+
'The method `myokit_log` is deprecated. Please use'
|
|
395
|
+
' WcpFile.log(use_names=True) instead.')
|
|
396
|
+
return self.log(self)
|
|
397
|
+
|
|
398
|
+
def path(self):
|
|
399
|
+
""" Returns the path to this file. """
|
|
400
|
+
return self._path
|
|
245
401
|
|
|
246
402
|
def plot(self):
|
|
247
403
|
"""
|
|
248
|
-
|
|
404
|
+
Deprecated method, please use :meth:`matplotlib_figure()` instead.
|
|
405
|
+
|
|
406
|
+
Creates and shows a matplotlib figure of all data in this file.
|
|
249
407
|
"""
|
|
408
|
+
# Deprecated since 2023-06-22
|
|
409
|
+
import warnings
|
|
410
|
+
warnings.warn(
|
|
411
|
+
'The method `plot` is deprecated. Please use'
|
|
412
|
+
' WcpFile.matplotlib_figure() instead.')
|
|
413
|
+
|
|
250
414
|
import matplotlib.pyplot as plt
|
|
251
|
-
|
|
252
|
-
plt.figure()
|
|
253
|
-
for k, channel in enumerate(record):
|
|
254
|
-
plt.subplot(self._nc, 1, 1 + k)
|
|
255
|
-
plt.plot(self._time, channel)
|
|
415
|
+
self.matplotlib_figure()
|
|
256
416
|
plt.show()
|
|
257
417
|
|
|
418
|
+
def record_count(self):
|
|
419
|
+
""" Alias of :meth:`sweep_count`. """
|
|
420
|
+
return self._nr
|
|
421
|
+
|
|
258
422
|
def records(self):
|
|
259
|
-
"""
|
|
260
|
-
|
|
261
|
-
|
|
423
|
+
""" Deprecated alias of :meth:`sweep_count`. """
|
|
424
|
+
# Deprecated since 2023-06-22
|
|
425
|
+
import warnings
|
|
426
|
+
warnings.warn(
|
|
427
|
+
'The method `records` is deprecated. Please use'
|
|
428
|
+
' WcpFile.record_count() instead.')
|
|
429
|
+
|
|
430
|
+
return self._nr
|
|
431
|
+
|
|
432
|
+
def sample_count(self):
|
|
433
|
+
""" Returns the number of samples in each channel. """
|
|
434
|
+
return self._np
|
|
435
|
+
|
|
436
|
+
def sweep_count(self):
|
|
437
|
+
# Docstring in SweepSource
|
|
262
438
|
return self._nr
|
|
263
439
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
# """
|
|
268
|
-
# return self._dt
|
|
440
|
+
def time_unit(self):
|
|
441
|
+
# Docstring in SweepSource
|
|
442
|
+
return self._time_unit
|
|
269
443
|
|
|
270
444
|
def times(self):
|
|
271
|
-
"""
|
|
272
|
-
Returns the time points sampled at.
|
|
273
|
-
"""
|
|
445
|
+
""" Returns the time points sampled at. """
|
|
274
446
|
return np.array(self._time)
|
|
275
447
|
|
|
448
|
+
def _unit(self, unit_string):
|
|
449
|
+
""" Parses a unit string and returns a :class:`myokit.Unit`. """
|
|
450
|
+
try:
|
|
451
|
+
return self._unit_cache[unit_string]
|
|
452
|
+
except KeyError:
|
|
453
|
+
unit = myokit.parse_unit(unit_string)
|
|
454
|
+
self._unit_cache[unit_string] = unit
|
|
455
|
+
return unit
|
|
456
|
+
|
|
276
457
|
def values(self, record, channel):
|
|
277
458
|
"""
|
|
278
459
|
Returns the values of channel ``channel``, recorded in record
|
|
@@ -280,31 +461,35 @@ class WcpFile:
|
|
|
280
461
|
"""
|
|
281
462
|
return self._records[record][channel]
|
|
282
463
|
|
|
464
|
+
def version(self):
|
|
465
|
+
""" Returns this file's version, as a string. """
|
|
466
|
+
return self._version_str
|
|
467
|
+
|
|
283
468
|
|
|
284
469
|
HEADER_FIELDS = {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
470
|
+
'ver': int, # WCP data file format version number
|
|
471
|
+
'ctime': str, # Create date/time
|
|
472
|
+
'rtime': str, # Start of recording time
|
|
473
|
+
'nc': int, # No. of channels per record
|
|
474
|
+
'nr': int, # No. of records in the file.
|
|
475
|
+
'nbh': int, # No. of 512 byte sectors in file header block
|
|
476
|
+
'nba': int, # No. of 512 byte sectors in a record analysis block
|
|
477
|
+
'nbd': int, # No. of 512 byte sectors in a record data block
|
|
478
|
+
'ad': float, # A/D converter input voltage range (V)
|
|
479
|
+
'adcmax': int, # Maximum A/D sample value
|
|
480
|
+
'np': int, # No. of A/D samples per channel
|
|
481
|
+
'dt': float, # A/D sampling interval (s)
|
|
482
|
+
'nz': int, # No. of samples averaged to calculate a zero level.
|
|
483
|
+
'id': str, # Experiment identification line
|
|
299
484
|
}
|
|
300
485
|
|
|
301
486
|
|
|
302
487
|
HEADER_CHANNEL_FIELDS = {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
488
|
+
'yn': str, # Channel name
|
|
489
|
+
'yu': str, # Channel units
|
|
490
|
+
'yg': float, # Channel gain factor mV/units
|
|
491
|
+
'yz': int, # Channel zero level (A/D bits)
|
|
492
|
+
'yo': int, # Channel offset into sample group in data block
|
|
493
|
+
'yr': int, # ADCZeroAt, probably for old files
|
|
309
494
|
}
|
|
310
495
|
#TODO: Find out if we need to do something with yz and yg
|