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 +9 -0
- pyxdaq-0.1.1/PKG-INFO +15 -0
- pyxdaq-0.1.1/README.md +46 -0
- pyxdaq-0.1.1/pyproject.toml +35 -0
- pyxdaq-0.1.1/pyxdaq/__init__.py +9 -0
- pyxdaq-0.1.1/pyxdaq/board.py +98 -0
- pyxdaq-0.1.1/pyxdaq/constants.py +309 -0
- pyxdaq-0.1.1/pyxdaq/datablock.py +155 -0
- pyxdaq-0.1.1/pyxdaq/gen2.py +61 -0
- pyxdaq-0.1.1/pyxdaq/impedance.py +206 -0
- pyxdaq-0.1.1/pyxdaq/intan_headstage.py +223 -0
- pyxdaq-0.1.1/pyxdaq/register.py +191 -0
- pyxdaq-0.1.1/pyxdaq/resources/__init__.py +12 -0
- pyxdaq-0.1.1/pyxdaq/resources/config/isa_rhd.json +46 -0
- pyxdaq-0.1.1/pyxdaq/resources/config/isa_rhs.json +84 -0
- pyxdaq-0.1.1/pyxdaq/resources/config/reg_rhd.json +141 -0
- pyxdaq-0.1.1/pyxdaq/resources/config/reg_rhs.json +170 -0
- pyxdaq-0.1.1/pyxdaq/rhd_driver.py +127 -0
- pyxdaq-0.1.1/pyxdaq/rhs_driver.py +155 -0
- pyxdaq-0.1.1/pyxdaq/stim.py +100 -0
- pyxdaq-0.1.1/pyxdaq/tools/self_diagnosis.py +201 -0
- pyxdaq-0.1.1/pyxdaq/utils.py +17 -0
- pyxdaq-0.1.1/pyxdaq/xdaq.py +1246 -0
- pyxdaq-0.1.1/pyxdaq.egg-info/PKG-INFO +15 -0
- pyxdaq-0.1.1/pyxdaq.egg-info/SOURCES.txt +29 -0
- pyxdaq-0.1.1/pyxdaq.egg-info/dependency_links.txt +1 -0
- pyxdaq-0.1.1/pyxdaq.egg-info/entry_points.txt +2 -0
- pyxdaq-0.1.1/pyxdaq.egg-info/requires.txt +7 -0
- pyxdaq-0.1.1/pyxdaq.egg-info/top_level.txt +1 -0
- pyxdaq-0.1.1/setup.cfg +4 -0
- pyxdaq-0.1.1/tests/test_package.py +10 -0
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,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
|