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
@@ -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
+