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.
Files changed (47) hide show
  1. myokit/__init__.py +11 -14
  2. myokit/__main__.py +0 -3
  3. myokit/_config.py +1 -3
  4. myokit/_datablock.py +914 -12
  5. myokit/_model_api.py +1 -3
  6. myokit/_myokit_version.py +1 -1
  7. myokit/_protocol.py +14 -28
  8. myokit/_sim/cable.c +1 -1
  9. myokit/_sim/cable.py +3 -2
  10. myokit/_sim/cmodel.h +1 -0
  11. myokit/_sim/cvodessim.c +79 -42
  12. myokit/_sim/cvodessim.py +20 -8
  13. myokit/_sim/fiber_tissue.c +1 -1
  14. myokit/_sim/fiber_tissue.py +3 -2
  15. myokit/_sim/openclsim.c +1 -1
  16. myokit/_sim/openclsim.py +8 -11
  17. myokit/_sim/pacing.h +121 -106
  18. myokit/_unit.py +1 -1
  19. myokit/formats/__init__.py +178 -0
  20. myokit/formats/axon/_abf.py +911 -841
  21. myokit/formats/axon/_atf.py +62 -59
  22. myokit/formats/axon/_importer.py +2 -2
  23. myokit/formats/heka/__init__.py +38 -0
  24. myokit/formats/heka/_importer.py +39 -0
  25. myokit/formats/heka/_patchmaster.py +2512 -0
  26. myokit/formats/wcp/_wcp.py +318 -133
  27. myokit/gui/datablock_viewer.py +144 -77
  28. myokit/gui/datalog_viewer.py +212 -231
  29. myokit/tests/ansic_event_based_pacing.py +3 -3
  30. myokit/tests/{ansic_fixed_form_pacing.py → ansic_time_series_pacing.py} +6 -6
  31. myokit/tests/data/formats/abf-v2.abf +0 -0
  32. myokit/tests/test_datablock.py +84 -0
  33. myokit/tests/test_datalog.py +2 -1
  34. myokit/tests/test_formats_axon.py +589 -136
  35. myokit/tests/test_formats_wcp.py +191 -22
  36. myokit/tests/test_pacing_system_c.py +51 -23
  37. myokit/tests/test_pacing_system_py.py +18 -0
  38. myokit/tests/test_simulation_1d.py +62 -22
  39. myokit/tests/test_simulation_cvodes.py +52 -3
  40. myokit/tests/test_simulation_fiber_tissue.py +35 -4
  41. myokit/tests/test_simulation_opencl.py +28 -4
  42. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/LICENSE.txt +1 -1
  43. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/METADATA +1 -1
  44. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/RECORD +47 -44
  45. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/WHEEL +0 -0
  46. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/entry_points.txt +0 -0
  47. {myokit-1.35.0.dist-info → myokit-1.35.2.dist-info}/top_level.txt +0 -0
@@ -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 the location
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, filepath):
33
+ def __init__(self, path):
34
34
  # The path to the file and its basename
35
- filepath = str(filepath)
36
- self._filepath = os.path.abspath(filepath)
37
- self._filename = os.path.basename(filepath)
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 = None
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
- #self._dt = None # Sampling interval
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(filepath, 'rb') as f:
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(b'=') for x in data.split(b'\n')]
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[b'ver']) != 9:
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 ' + str(h[b'ver']))
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[b'nbh'])
88
+ #header_size = 512 * int(h['nbh'])
75
89
  # Seems to be size in bytes!
76
- header_size = int(h[b'nbh'])
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[b'nc']) - 1) / 8) + 1) * 1024
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(b'=') for x in data.split(b'\n')]
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(b',', b'.')
97
- header[k] = t(v)
108
+ v = v.replace(',', '.')
109
+ self._header[k] = t(v)
98
110
  except KeyError:
99
- header_raw[k] = v
111
+ self._header_raw[k] = v
100
112
 
101
113
  # Convert time
102
- # No, don't. It's in different formats depending on... the version?
114
+ # No, don't. It's in different formats depending on... user locale?
103
115
  # if 'ctime' in header:
104
- # print(header[b'ctime'])
105
- # ctime = time.strptime(header[b'ctime'], "%d/%m/%Y %H:%M:%S")
106
- # header[b'ctime'] = time.strftime('%Y-%m-%d %H:%M:%S', ctime)
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
- # Records in file
110
- self._nr = header[b'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 = (header[b'nbd'] * 512) // (2 * self._nc)
127
+ self._np = (self._header['nbd'] * 512) // (2 * self._nc)
119
128
 
120
- # Get channel specific fields
121
- channel_headers = []
122
- self._channel_names = []
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 + j])
128
- channel_headers.append(c)
129
- self._channel_names.append(c[b'yn'].decode(_ENC))
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 * header[b'nba']
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 * header[b'nbd']
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 = header[b'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
- # Group number (float set by the user)
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: pretty sure this should be the same as the
165
- # file wide one in header[b'dt']
166
- rint = struct.unpack(str('<f'), f.read(4))[0]
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._filepath, np.dtype('<i2'), 'r',
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
- for j in range(self._nc):
191
- h = channel_headers[j]
192
- s = float(vmax[j]) / float(adcmax) / float(h[b'yg'])
193
- d = np.array(data[:, h[b'yo']].astype('f4') * s)
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) * header[b'dt']
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
- Returns the number of channels in this file.
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 channel_names(self):
212
- """
213
- Returns the names of the channels in this file.
214
- """
215
- return list(self._channel_names)
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 myokit_log(self):
224
- """
225
- Creates and returns a:class:`myokit.DataLog` containing all the
226
- data from this file.
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
- Each channel is stored under its own name, with an index indicating
229
- the record it was from. Time is stored under ``time``.
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 path(self):
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
- Returns the path to the currently opened file.
389
+ Deprecated method. Please use ``WcpFile.log(use_names=True)`` instead.
243
390
  """
244
- return self._filepath
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
- Creates matplotlib plots of all data in this file.
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
- for record in self._records:
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
- Returns the number of records in this file.
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
- #def sampling_interval(self):
265
- # """
266
- # Returns the sampling interval used in this file.
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
- b'ver': int, # WCP data file format version number
286
- b'ctime': bytes, # Create date/time
287
- b'nc': int, # No. of channels per record
288
- b'nr': int, # No. of records in the file.
289
- b'nbh': int, # No. of 512 byte sectors in file header block
290
- b'nba': int, # No. of 512 byte sectors in a record analysis block
291
- b'nbd': int, # No. of 512 byte sectors in a record data block
292
- b'ad': float, # A/D converter input voltage range (V)
293
- b'adcmax': int, # Maximum A/D sample value
294
- b'np': int, # No. of A/D samples per channel
295
- b'dt': float, # A/D sampling interval (s)
296
- b'nz': int, # No. of samples averaged to calculate a zero level.
297
- b'tu': bytes, # Time units
298
- b'id': bytes, # Experiment identification line
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
- b'yn': bytes, # Channel name
304
- b'yu': bytes, # Channel units
305
- b'yg': float, # Channel gain factor mV/units
306
- b'yz': int, # Channel zero level (A/D bits)
307
- b'yo': int, # Channel offset into sample group in data block
308
- b'yr': int, # ADCZeroAt, probably for old files
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