pyxdaq 0.1.1__tar.gz

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.
pyxdaq-0.1.1/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KonteX Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
pyxdaq-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyxdaq
3
+ Version: 0.1.1
4
+ License-Expression: MIT
5
+ Project-URL: Homepage, https://github.com/kontex-neuro/pyxdaq
6
+ Requires-Python: >=3.11
7
+ License-File: LICENSE
8
+ Requires-Dist: colorlog
9
+ Requires-Dist: dataclass_wizard
10
+ Requires-Dist: pylibxdaq
11
+ Requires-Dist: numpy
12
+ Requires-Dist: rich
13
+ Requires-Dist: tqdm
14
+ Requires-Dist: matplotlib
15
+ Dynamic: license-file
pyxdaq-0.1.1/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # pyxdaq: Python Interface and Tools for XDAQ
2
+
3
+ ## Installation
4
+
5
+ ### Requirements
6
+ - **Python** ≥ 3.11
7
+ - **XDAQ** drivers installed and your device/headstage connected
8
+
9
+ ### From PyPI
10
+ ```shell
11
+ python3 -m pip install --upgrade pip
12
+ pip install pyxdaq
13
+ ```
14
+
15
+ ### From Source
16
+ ```shell
17
+ git clone https://github.com/kontex-neuro/pyxdaq.git
18
+ cd pyxdaq
19
+ python3 -m venv .venv
20
+ source .venv/bin/activate # on Windows: .venv\Scripts\Activate.ps1
21
+ pip install --upgrade pip
22
+ pip install .
23
+ ```
24
+
25
+ ## Running the Self‑Diagnosis
26
+
27
+ A built‑in CLI script verifies:
28
+ - package installation
29
+ - XDAQ dynamic libraries
30
+ - device enumeration
31
+ - headstage detection
32
+
33
+ Run:
34
+ ```shell
35
+ xdaq-diagnosis
36
+ ```
37
+
38
+ Results are saved to `diagnostic_report.json` in your current directory.
39
+
40
+ ## Quick Start
41
+
42
+ See the `examples/` folder.
43
+
44
+ ## License
45
+
46
+ MIT — see [LICENSE](LICENSE)
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "pyxdaq"
3
+ version = "0.1.1"
4
+ license = "MIT"
5
+ license-files = ["LICENSE"]
6
+
7
+ requires-python = ">=3.11"
8
+ dependencies = [
9
+ "colorlog",
10
+ "dataclass_wizard",
11
+ "pylibxdaq",
12
+ "numpy",
13
+ "rich",
14
+ "tqdm",
15
+ "matplotlib",
16
+ ]
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/kontex-neuro/pyxdaq"
20
+
21
+ [tool.setuptools.packages.find]
22
+ include = ["pyxdaq", "pyxdaq.*"]
23
+
24
+ [build-system]
25
+ requires = ["setuptools>=42.0", "wheel"]
26
+ build-backend = "setuptools.build_meta"
27
+
28
+ [tool.setuptools.package-data]
29
+ "pyxdaq.resources.config" = ["*.json"]
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = "tests"
33
+
34
+ [project.scripts]
35
+ xdaq-diagnosis = "pyxdaq.tools.self_diagnosis:main"
@@ -0,0 +1,9 @@
1
+ import logging
2
+
3
+ import colorlog
4
+
5
+ handler = colorlog.StreamHandler()
6
+ handler.setFormatter(
7
+ colorlog.ColoredFormatter('%(log_color)s%(asctime)s [%(levelname)s] %(name)s %(message)s')
8
+ )
9
+ logging.basicConfig(handlers=[handler], level=logging.INFO)
@@ -0,0 +1,98 @@
1
+ import json
2
+ import logging
3
+ from copy import deepcopy
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Callable, List
7
+
8
+ from pylibxdaq import pyxdaq_device
9
+ from pylibxdaq.managers import manager_paths
10
+
11
+ from .constants import EndPoints
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass
17
+ class DeviceSetup:
18
+ manager_path: Path
19
+ manager_info: dict
20
+ options: dict
21
+
22
+ def with_mode(self, mode: str):
23
+ info = deepcopy(self)
24
+ info.options['mode'] = mode
25
+ return info
26
+
27
+
28
+ class Board:
29
+
30
+ @classmethod
31
+ def list_devices(cls) -> List[DeviceSetup]:
32
+ devices = []
33
+ for manager_path in manager_paths:
34
+ manager = pyxdaq_device.get_device_manager(str(manager_path))
35
+ info = manager.info()
36
+ for device_options in json.loads(manager.list_devices()):
37
+ devices.append(DeviceSetup(manager_path, info, device_options))
38
+ return sorted(devices, key=lambda x: str(x.options))
39
+
40
+ def __init__(self, device_info: DeviceSetup):
41
+ manager: pyxdaq_device.DeviceManager = pyxdaq_device.get_device_manager(
42
+ str(device_info.manager_path)
43
+ )
44
+ self.dev = manager.create_device(json.dumps(device_info.options))
45
+ status = json.loads(self.dev.get_status())
46
+ if status['Mode'] == 'rhd':
47
+ self.rhs = False
48
+ elif status['Mode'] == 'rhs':
49
+ self.rhs = True
50
+
51
+ def __enter__(self):
52
+ return self
53
+
54
+ def __exit__(self, exc_type, exc_val, exc_tb):
55
+ del self.dev
56
+
57
+ def __getattr__(self, name: str):
58
+ return getattr(self.dev, name)
59
+
60
+ def GetWireOutValue(self, addr: EndPoints, update: bool = True) -> int:
61
+ if update:
62
+ return self.dev.get_register_sync(addr.value)
63
+ else:
64
+ return self.dev.get_register(addr.value)
65
+
66
+ def SetWireInValue(
67
+ self, addr: EndPoints, value: int, mask: int = 0xFFFFFFFF, update: bool = True
68
+ ):
69
+ if update:
70
+ self.dev.set_register_sync(addr.value, value, mask)
71
+ else:
72
+ self.dev.set_register(addr.value, value, mask)
73
+
74
+ def ActivateTriggerIn(self, addr: EndPoints, value: int):
75
+ self.dev.trigger(addr.value, value)
76
+
77
+ def WriteToBlockPipeIn(self, epAddr: EndPoints, data: bytearray):
78
+ return self.dev.write(epAddr.value, data)
79
+
80
+ def ReadFromBlockPipeOut(self, epAddr: EndPoints, data: bytearray):
81
+ return self.dev.read(epAddr.value, data)
82
+
83
+ def start_receiving_aligned_buffer(
84
+ self,
85
+ epAddr: EndPoints,
86
+ alignment: int,
87
+ callback: Callable[[pyxdaq_device.ManagedBuffer], None],
88
+ chunk_size: int = 0
89
+ ):
90
+ return self.dev.start_aligned_read_stream(
91
+ epAddr.value, alignment, callback, chunk_size=chunk_size
92
+ )
93
+
94
+ def SendTrig(
95
+ self, trig: EndPoints, bit: int, epAddr: EndPoints, value: int, mask: int = 0xFFFFFFFF
96
+ ):
97
+ self.dev.set_register_sync(epAddr.value, value, mask)
98
+ self.dev.trigger(trig.value, bit)
@@ -0,0 +1,309 @@
1
+ from enum import Enum
2
+
3
+
4
+ class EndPoints(Enum):
5
+ pass
6
+
7
+
8
+ class XDAQWireOut(EndPoints):
9
+ Serial = 0x32
10
+ Hdmi = 0x31
11
+ Fpga = 0x3f
12
+ Daio = 0x30
13
+ Oled = 0x33
14
+ Vido = 0x34
15
+ Expr = 0x35
16
+ Xprt = 0x36
17
+
18
+
19
+ class RHD(EndPoints):
20
+ TTL_override = 0x1400
21
+ WireInResetRun = 0x00
22
+ WireInMaxTimeStep = 0x01
23
+ WireInSerialDigitalInCntl = 0x02
24
+ WireInDataFreqPll = 0x03
25
+ WireInMisoDelay = 0x04
26
+ WireInCmdRamAddr = 0x05
27
+ WireInCmdRamBank = 0x06
28
+ WireInCmdRamData = 0x07
29
+ WireInAuxCmdBank1 = 0x08
30
+ WireInAuxCmdBank2 = 0x09
31
+ WireInAuxCmdBank3 = 0x0a
32
+ WireInAuxCmdLength = 0x0b
33
+ WireInAuxCmdLoop = 0x0c
34
+ WireInLedDisplay = 0x0d
35
+ WireInDacReref = 0x0e
36
+ WireInDataStreamEn = 0x14
37
+ WireInTtlOut = 0x15
38
+ WireInTtlOut32 = 0x10
39
+ WireInDacSource1 = 0x16
40
+ WireInDacSource2 = 0x17
41
+ WireInDacSource3 = 0x18
42
+ WireInDacSource4 = 0x19
43
+ WireInDacSource5 = 0x1a
44
+ WireInDacSource6 = 0x1b
45
+ WireInDacSource7 = 0x1c
46
+ WireInDacSource8 = 0x1d
47
+ WireInDacSource9 = 0x48
48
+ WireInDacSource10 = 0x49
49
+ WireInDacSource11 = 0x4A
50
+ WireInDacSource12 = 0x4B
51
+ WireInDacManual = 0x1e
52
+ WireInMultiUse = 0x1f
53
+ TrigInConfig = 0x40
54
+ TrigInSpiStart = 0x41
55
+ TrigInDacConfig = 0x42
56
+ WireOutNumWords = 0x20
57
+ WireOutSerialDigitalIn = 0x21
58
+ WireOutSpiRunning = 0x22
59
+ WireOutTtlIn = 0x23
60
+ WireOutDataClkLocked = 0x24
61
+ WireOutBoardMode = 0x25
62
+ WireOutBoardId = 0x3e
63
+ WireOutBoardVersion = 0x3f
64
+ PipeOutData = 0xa0
65
+ TrigVStim = 0x40
66
+ TrigMCU = 0x48
67
+ WireOutXDAQStatus = 0x22
68
+ WireInMCUControl = 0x02
69
+ PipeInFirmware = 0x88
70
+ PipeOutFirmware = 0xb0
71
+ WireInSetMode = 0x00
72
+ Enable32bitDIO = 0x12
73
+ PipeInDAC1 = 0x90
74
+ PipeInDAC2 = 0x91
75
+ PipeInDAC3 = 0x92
76
+ PipeInDAC4 = 0x93
77
+ PipeInDAC5 = 0x94
78
+ PipeInDAC6 = 0x95
79
+ PipeInDAC7 = 0x96
80
+ PipeInDAC8 = 0x97
81
+ PipeInDAC9 = 0x98
82
+ PipeInDAC10 = 0x99
83
+ PipeInDAC11 = 0x9A
84
+ PipeInDAC12 = 0x9B
85
+ ExpanderInfo = 0x35
86
+
87
+
88
+ class RHS(EndPoints):
89
+ TTL_override = 0x1400
90
+ WireInResetRun = 0x00
91
+ WireInMaxTimeStep = 0x01
92
+ WireInMaxTimeStepLsb = 0x01
93
+ WireInMaxTimeStepMsb = 0x02
94
+ WireInDataFreqPll = 0x03
95
+ WireInMisoDelay = 0x04
96
+ WireInStimCmdMode = 0x05
97
+ WireInStimRegAddr = 0x06
98
+ WireInStimRegWord = 0x07
99
+ WireInDcAmpConvert = 0x08
100
+ WireInExtraStates = 0x09
101
+ WireInDacReref = 0x0a
102
+ WireInAuxEnable = 0x0c
103
+ WireInGlobalSettleSelect = 0x0d
104
+ WireInAdcThreshold = 0x0f
105
+ WireInSerialDigitalInCntl = 0x10
106
+ WireInTtlOut32 = 0x10
107
+ WireInLedDisplay = 0x11
108
+ WireInManualTriggers = 0x12
109
+ WireInTtlOutMode = 0x13
110
+ WireInDataStreamEn = 0x14
111
+ WireInTtlOut = 0x15
112
+ WireInDacSource1 = 0x16
113
+ WireInDacSource2 = 0x17
114
+ WireInDacSource3 = 0x18
115
+ WireInDacSource4 = 0x19
116
+ WireInDacSource5 = 0x1a
117
+ WireInDacSource6 = 0x1b
118
+ WireInDacSource7 = 0x1c
119
+ WireInDacSource8 = 0x1d
120
+ WireInDacSource9 = 0x48
121
+ WireInDacSource10 = 0x49
122
+ WireInDacSource11 = 0x4A
123
+ WireInDacSource12 = 0x4B
124
+ WireInDacManual = 0x1e
125
+ WireInMultiUse = 0x1f
126
+ TrigInConfig = 0x40
127
+ TrigInDcmProg = 0x40
128
+ TrigInSpiStart = 0x41
129
+ TrigInRamAddrReset = 0x42
130
+ TrigInDacThresh = 0x43
131
+ TrigInDacHpf = 0x44
132
+ TrigInAuxCmdLength = 0x45
133
+ WireOutNumWords = 0x20
134
+ WireOutNumWordsLsb = 0x20
135
+ WireOutNumWordsMsb = 0x21
136
+ WireOutSpiRunning = 0x22
137
+ WireOutTtlIn = 0x23
138
+ WireOutDataClkLocked = 0x24
139
+ WireOutBoardMode = 0x25
140
+ WireOutSerialDigitalIn = 0x26
141
+ WireOutBoardId = 0x3e
142
+ WireOutBoardVersion = 0x3f
143
+ PipeInAuxCmd1Msw = 0x80
144
+ PipeInAuxCmd1 = 0x81
145
+ PipeInAuxCmd1Lsw = 0x81
146
+ PipeInAuxCmd2Msw = 0x82
147
+ PipeInAuxCmd2 = 0x83
148
+ PipeInAuxCmd2Lsw = 0x83
149
+ PipeInAuxCmd3Msw = 0x84
150
+ PipeInAuxCmd3 = 0x85
151
+ PipeInAuxCmd3Lsw = 0x85
152
+ PipeInAuxCmd4Msw = 0x86
153
+ PipeInAuxCmd4 = 0x87
154
+ PipeInAuxCmd4Lsw = 0x87
155
+ PipeOutData = 0xa0
156
+ TrigVStim = 0x40
157
+ TrigMCU = 0x48
158
+ WireOutXDAQStatus = 0x22
159
+ WireInMCUControl = 0x02
160
+ PipeInFirmware = 0x88
161
+ PipeOutFirmware = 0xb0
162
+ WireInSetMode = 0x00
163
+ Enable32bitDIO = 0x0B
164
+ PipeInDAC1 = 0x90
165
+ PipeInDAC2 = 0x91
166
+ PipeInDAC3 = 0x92
167
+ PipeInDAC4 = 0x93
168
+ PipeInDAC5 = 0x94
169
+ PipeInDAC6 = 0x95
170
+ PipeInDAC7 = 0x96
171
+ PipeInDAC8 = 0x97
172
+ PipeInDAC9 = 0x98
173
+ PipeInDAC10 = 0x99
174
+ PipeInDAC11 = 0x9A
175
+ PipeInDAC12 = 0x9B
176
+ ExpanderInfo = 0x35
177
+
178
+
179
+ class SampleRate(Enum):
180
+ SampleRate1000Hz = (7, 125, 1000)
181
+ SampleRate1250Hz = (7, 100, 1250)
182
+ SampleRate1500Hz = (21, 250, 1500)
183
+ SampleRate2000Hz = (14, 125, 2000)
184
+ SampleRate2500Hz = (35, 250, 2500)
185
+ SampleRate3000Hz = (21, 125, 3000)
186
+ SampleRate3333Hz = (14, 75, 3333)
187
+ SampleRate4000Hz = (28, 125, 4000)
188
+ SampleRate5000Hz = (7, 25, 5000)
189
+ SampleRate6250Hz = (7, 20, 6250)
190
+ SampleRate8000Hz = (112, 250, 8000)
191
+ SampleRate10000Hz = (14, 25, 10000)
192
+ SampleRate12500Hz = (7, 10, 12500)
193
+ SampleRate15000Hz = (21, 25, 15000)
194
+ SampleRate20000Hz = (28, 25, 20000)
195
+ SampleRate25000Hz = (35, 25, 25000)
196
+ SampleRate30000Hz = (42, 25, 30000)
197
+
198
+ @classmethod
199
+ def fromRate(cls, rate: int):
200
+ for sr in SampleRate:
201
+ if sr.value[2] == rate:
202
+ return sr
203
+ raise ValueError('Invalid sample rate')
204
+
205
+ @property
206
+ def rate(self):
207
+ return self.value[2]
208
+
209
+
210
+ class HeadstageChipID(Enum):
211
+ NA = 0
212
+ RHD2132 = 1
213
+ RHD2216 = 2
214
+ RHD2164 = 4
215
+ RHS2116 = 32
216
+
217
+ def num_channels(self):
218
+ if self == HeadstageChipID.RHD2164:
219
+ return 64
220
+ elif self == HeadstageChipID.RHD2132:
221
+ return 32
222
+ elif self == HeadstageChipID.RHD2216:
223
+ return 16
224
+ elif self == HeadstageChipID.RHS2116:
225
+ return 16
226
+ else:
227
+ return 0
228
+
229
+ def num_channels_per_stream(self):
230
+ if self == HeadstageChipID.RHD2164:
231
+ return self.num_channels() // 2
232
+ return self.num_channels()
233
+
234
+
235
+ class HeadstageChipMISOID(Enum):
236
+ NA = 0
237
+ MISO_A = 53
238
+ MISO_B = 58
239
+
240
+
241
+ class TriggerEvent(Enum):
242
+ Level = 0
243
+ Edge = 1
244
+
245
+
246
+ class TriggerPolarity(Enum):
247
+ Low = 0
248
+ High = 1
249
+
250
+
251
+ class StimShape(Enum):
252
+ Biphasic = 0
253
+ BiphasicWithInterphaseDelay = 1
254
+ Triphasic = 2
255
+ Monophasic = 3
256
+
257
+
258
+ class StartPolarity(Enum):
259
+ anodic = 0
260
+ cathodic = 1
261
+
262
+
263
+ class StimRegister(Enum):
264
+ Trigger = 0
265
+ Param = 1
266
+ EventAmpSettleOn = 2
267
+ EventAmpSettleOff = 3
268
+ EventStartStim = 4
269
+ EventStimPhase2 = 5
270
+ EventStimPhase3 = 6
271
+ EventEndStim = 7
272
+ EventRepeatStim = 8
273
+ EventChargeRecovOn = 9
274
+ EventChargeRecovOff = 10
275
+ EventAmpSettleOnRepeat = 11
276
+ EventAmpSettleOffRepeat = 12
277
+ EventEnd = 13
278
+
279
+
280
+ class StimStepSize(Enum):
281
+ StimStepSizeMin = 0
282
+ StimStepSize10nA = 1
283
+ StimStepSize20nA = 2
284
+ StimStepSize50nA = 3
285
+ StimStepSize100nA = 4
286
+ StimStepSize200nA = 5
287
+ StimStepSize500nA = 6
288
+ StimStepSize1uA = 7
289
+ StimStepSize2uA = 8
290
+ StimStepSize5uA = 9
291
+ StimStepSize10uA = 10
292
+ StimStepSizeMax = 11
293
+
294
+ @property
295
+ def nA(self):
296
+ return {
297
+ StimStepSize.StimStepSizeMin: float('nan'),
298
+ StimStepSize.StimStepSize10nA: 10,
299
+ StimStepSize.StimStepSize20nA: 20,
300
+ StimStepSize.StimStepSize50nA: 50,
301
+ StimStepSize.StimStepSize100nA: 100,
302
+ StimStepSize.StimStepSize200nA: 200,
303
+ StimStepSize.StimStepSize500nA: 500,
304
+ StimStepSize.StimStepSize1uA: 1000,
305
+ StimStepSize.StimStepSize2uA: 2000,
306
+ StimStepSize.StimStepSize5uA: 5000,
307
+ StimStepSize.StimStepSize10uA: 10000,
308
+ StimStepSize.StimStepSizeMax: float('nan')
309
+ }[self]
@@ -0,0 +1,155 @@
1
+ import struct
2
+ from dataclasses import dataclass
3
+ from typing import List, Union
4
+
5
+ import numpy as np
6
+
7
+ _uint16le = np.dtype('u2').newbyteorder('<')
8
+ _uint32le = np.dtype('u4').newbyteorder('<')
9
+ _RHD_HEADER_MAGIC = 0xD7A22AAA38132A53
10
+ _RHS_HEADER_MAGIC = 0x8D542C8A49712F0B
11
+
12
+
13
+ @dataclass
14
+ class Sample:
15
+ """
16
+ Represents a single sample at time `ts` from the XDAQ data stream.
17
+ """
18
+ ts: int
19
+ aux: np.ndarray
20
+ amp: np.ndarray
21
+ adc: np.ndarray
22
+ ttlin: np.ndarray
23
+ ttlout: np.ndarray
24
+ dac: Union[None, np.ndarray]
25
+ stim: Union[None, np.ndarray]
26
+
27
+ @classmethod
28
+ def from_buffer(
29
+ cls, rhs: bool, buffer: Union[bytearray, memoryview], datastreams: int
30
+ ) -> 'Sample':
31
+ """
32
+ Deserialize a single sample from a buffer.
33
+ Keeps the same memory layout as the original data for further optimization.
34
+ """
35
+ idx = 0
36
+ magic, ts = struct.unpack("<QI", buffer[idx:12])
37
+ if magic != (_RHS_HEADER_MAGIC if rhs else _RHD_HEADER_MAGIC):
38
+ raise ValueError(f"Invalid magic: {magic:016X}")
39
+ idx += 12
40
+
41
+ aux = np.frombuffer(
42
+ buffer[idx:], dtype=_uint16le, count=3 * datastreams * (2 if rhs else 1)
43
+ ).reshape([3, datastreams] + ([2] if rhs else []))
44
+ idx += 3 * datastreams * 2 * (2 if rhs else 1)
45
+
46
+ amp = np.frombuffer(
47
+ buffer[idx:],
48
+ dtype=_uint16le,
49
+ count=(16 if rhs else 32) * datastreams * (2 if rhs else 1)
50
+ ).reshape([16 if rhs else 32, datastreams] + ([2] if rhs else []))
51
+ idx += (16 if rhs else 32) * datastreams * 2 * (2 if rhs else 1)
52
+
53
+ if rhs:
54
+ aux0 = np.frombuffer(
55
+ buffer[idx:], dtype=_uint16le, count=1 * datastreams * 2
56
+ ).reshape((1, datastreams, 2))
57
+ aux = np.concatenate((aux0, aux), axis=0)
58
+ idx += 1 * datastreams * 2 * 2
59
+
60
+ stim = np.frombuffer(
61
+ buffer[idx:], dtype=_uint16le, count=4 * datastreams
62
+ ).reshape(4, datastreams)
63
+ idx += 4 * datastreams * 2
64
+ idx += 4
65
+
66
+ dac = np.frombuffer(buffer[idx:], dtype=_uint16le, count=8)
67
+ idx += 16
68
+ else:
69
+ stim = None
70
+ dac = None
71
+ idx += 2 * ((datastreams + 2) % 4) # padding
72
+
73
+ adc = np.frombuffer(buffer[idx:], dtype=_uint16le, count=8)
74
+ idx += 16
75
+
76
+ ttlin = np.frombuffer(buffer[idx:], dtype=_uint32le, count=1)
77
+ idx += 4
78
+
79
+ ttlout = np.frombuffer(buffer[idx:], dtype=_uint32le, count=1)
80
+ idx += 4
81
+ return cls(ts, aux, amp, adc, ttlin, ttlout, dac, stim)
82
+
83
+
84
+ @dataclass
85
+ class Samples(Sample):
86
+ """
87
+ A collection of samples, the first dimension represents the sample index.
88
+ """
89
+ n: int
90
+
91
+ def device_name(self):
92
+ if self.n != 128:
93
+ raise ValueError("Unable to determine device name for non-128 sample data block")
94
+ if self.stim is None:
95
+ return self.aux[[32, 33, 34, 35, 36, 24, 25, 26], 2, :]
96
+ else:
97
+ rom = self.aux[:, 0, :, :][58:61, :, 0]
98
+ aux = np.array(rom).view(np.uint8).reshape(
99
+ (rom.shape[0], rom.shape[1], 2)
100
+ ).transpose(1, 0, 2).reshape((rom.shape[1], -1))
101
+ return aux[:, :0:-1].T
102
+
103
+ def device_id(self):
104
+ if self.n != 128:
105
+ raise ValueError("Unable to determine device ID for non-128 sample data block")
106
+ if self.stim is None:
107
+ return self.aux[19, 2, :], self.aux[23, 2, :]
108
+ else:
109
+ rom = self.aux[:, 0, :, :][56:58, :, 0]
110
+ aux = np.array(rom).view(np.uint8).reshape(
111
+ (rom.shape[0], rom.shape[1], 2)
112
+ ).transpose(1, 0, 2).reshape((rom.shape[1], -1))
113
+ return aux[:, 0].T, np.zeros_like(aux[:, 0].T)
114
+
115
+
116
+ @dataclass
117
+ class DataBlock:
118
+ """
119
+ Raw data block which keeps the original memory layout.
120
+ """
121
+ samples: List[Sample]
122
+ # Samples x C x Datastreams : [C x Datastreams] [C x Datastreams] ... [C x Datastreams]
123
+
124
+ @classmethod
125
+ def from_buffer(
126
+ cls, rhs, sample_size, buffer: Union[bytearray, memoryview], datastreams: int
127
+ ) -> 'DataBlock':
128
+ return cls(
129
+ [
130
+ Sample.from_buffer(rhs, buffer[i:i + sample_size], datastreams)
131
+ for i in range(0, len(buffer), sample_size)
132
+ ]
133
+ )
134
+
135
+ def to_samples(self) -> Samples:
136
+ """
137
+ Concatenate all samples into a single Samples object.
138
+ This method breaks the original memory layout.
139
+ """
140
+ return Samples(
141
+ np.array([s.ts for s in self.samples]), np.stack([s.aux for s in self.samples]),
142
+ np.stack([s.amp for s in self.samples]), np.stack([s.adc for s in self.samples]),
143
+ np.stack([s.ttlin for s in self.samples]), np.stack([s.ttlout for s in self.samples]),
144
+ None if self.samples[0].dac is None else np.stack([s.dac for s in self.samples]),
145
+ None if self.samples[0].dac is None else np.stack([s.stim for s in self.samples]),
146
+ len(self.samples)
147
+ )
148
+
149
+
150
+ def amplifier2mv(amp: np.array):
151
+ return (amp.astype(np.float32) - 32768) * 0.195
152
+
153
+
154
+ def adc2v(adc: np.array):
155
+ return (adc.astype(np.float32) - 32768) * 0.0003125