cerelink 8.0.1__cp311-cp311-macosx_11_0_arm64.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.
- cerelink/__init__.py +5 -0
- cerelink/__version__.py +34 -0
- cerelink/cbpy.cpython-311-darwin.so +0 -0
- cerelink/cbpy.pyx +1157 -0
- cerelink/cbsdk_cython.pxd +458 -0
- cerelink/cbsdk_helper.cpp +112 -0
- cerelink/cbsdk_helper.h +54 -0
- cerelink-8.0.1.dist-info/METADATA +59 -0
- cerelink-8.0.1.dist-info/RECORD +11 -0
- cerelink-8.0.1.dist-info/WHEEL +5 -0
- cerelink-8.0.1.dist-info/top_level.txt +1 -0
cerelink/cbpy.pyx
ADDED
@@ -0,0 +1,1157 @@
|
|
1
|
+
# distutils: language = c++
|
2
|
+
# cython: language_level=2
|
3
|
+
from cbsdk_cython cimport *
|
4
|
+
from libcpp cimport bool
|
5
|
+
from libc.stdlib cimport malloc, free
|
6
|
+
from libc.string cimport strncpy
|
7
|
+
cimport numpy as cnp
|
8
|
+
cimport cython
|
9
|
+
|
10
|
+
import sys
|
11
|
+
import locale
|
12
|
+
import typing
|
13
|
+
|
14
|
+
import numpy as np
|
15
|
+
|
16
|
+
# Initialize numpy C API
|
17
|
+
cnp.import_array()
|
18
|
+
|
19
|
+
# NumPy array flags for ownership
|
20
|
+
cdef extern from "numpy/arrayobject.h":
|
21
|
+
cdef int NPY_ARRAY_OWNDATA
|
22
|
+
|
23
|
+
|
24
|
+
# Determine the correct numpy dtype for PROCTIME based on its size
|
25
|
+
cdef object _proctime_dtype():
|
26
|
+
if sizeof(PROCTIME) == 4:
|
27
|
+
return np.uint32
|
28
|
+
elif sizeof(PROCTIME) == 8:
|
29
|
+
return np.uint64
|
30
|
+
else:
|
31
|
+
raise RuntimeError("Unexpected PROCTIME size: %d" % sizeof(PROCTIME))
|
32
|
+
|
33
|
+
# Cache the dtype for performance
|
34
|
+
_PROCTIME_DTYPE = _proctime_dtype()
|
35
|
+
|
36
|
+
|
37
|
+
def version(int instance=0):
|
38
|
+
"""Get library version
|
39
|
+
Inputs:
|
40
|
+
instance - (optional) library instance number
|
41
|
+
Outputs:"
|
42
|
+
dictionary with following keys
|
43
|
+
major - major API version
|
44
|
+
minor - minor API version
|
45
|
+
release - release API version
|
46
|
+
beta - beta API version (0 if a non-beta)
|
47
|
+
protocol_major - major protocol version
|
48
|
+
protocol_minor - minor protocol version
|
49
|
+
nsp_major - major NSP firmware version
|
50
|
+
nsp_minor - minor NSP firmware version
|
51
|
+
nsp_release - release NSP firmware version
|
52
|
+
nsp_beta - beta NSP firmware version (0 if non-beta))
|
53
|
+
nsp_protocol_major - major NSP protocol version
|
54
|
+
nsp_protocol_minor - minor NSP protocol version
|
55
|
+
"""
|
56
|
+
|
57
|
+
cdef cbSdkResult res
|
58
|
+
cdef cbSdkVersion ver
|
59
|
+
|
60
|
+
res = cbSdkGetVersion(<uint32_t>instance, &ver)
|
61
|
+
handle_result(res)
|
62
|
+
|
63
|
+
ver_dict = {'major':ver.major, 'minor':ver.minor, 'release':ver.release, 'beta':ver.beta,
|
64
|
+
'protocol_major':ver.majorp, 'protocol_minor':ver.majorp,
|
65
|
+
'nsp_major':ver.nspmajor, 'nsp_minor':ver.nspminor, 'nsp_release':ver.nsprelease, 'nsp_beta':ver.nspbeta,
|
66
|
+
'nsp_protocol_major':ver.nspmajorp, 'nsp_protocol_minor':ver.nspmajorp
|
67
|
+
}
|
68
|
+
return <int>res, ver_dict
|
69
|
+
|
70
|
+
|
71
|
+
def defaultConParams():
|
72
|
+
#Note: Defaulting to 255.255.255.255 assumes the client is connected to the NSP via a switch.
|
73
|
+
#A direct connection might require the client-addr to be "192.168.137.1"
|
74
|
+
con_parms = {
|
75
|
+
'client-addr': str(cbNET_UDP_ADDR_BCAST.decode("utf-8"))\
|
76
|
+
if ('linux' in sys.platform or 'linux2' in sys.platform) else '255.255.255.255',
|
77
|
+
'client-port': cbNET_UDP_PORT_BCAST,
|
78
|
+
'inst-addr': cbNET_UDP_ADDR_CNT.decode("utf-8"),
|
79
|
+
'inst-port': cbNET_UDP_PORT_CNT,
|
80
|
+
'receive-buffer-size': (8 * 1024 * 1024) if sys.platform == 'win32' else (6 * 1024 * 1024)
|
81
|
+
}
|
82
|
+
return con_parms
|
83
|
+
|
84
|
+
|
85
|
+
def open(int instance=0, connection: str = 'default', parameter: dict[str, str | int] | None = None) -> dict[str, str]:
|
86
|
+
"""Open library.
|
87
|
+
Inputs:
|
88
|
+
connection - connection type, string can be one of the following
|
89
|
+
'default': tries slave then master connection
|
90
|
+
'master': tries master connection (UDP)
|
91
|
+
'slave': tries slave connection (needs another master already open)
|
92
|
+
parameter - dictionary with following keys (all optional)
|
93
|
+
'inst-addr': instrument IPv4 address.
|
94
|
+
'inst-port': instrument port number.
|
95
|
+
'client-addr': client IPv4 address.
|
96
|
+
'client-port': client port number.
|
97
|
+
'receive-buffer-size': override default network buffer size (low value may result in drops).
|
98
|
+
instance - (optional) library instance number
|
99
|
+
Outputs:
|
100
|
+
Same as "get_connection_type" command output
|
101
|
+
|
102
|
+
Test with:
|
103
|
+
from cerelink import cbpy
|
104
|
+
cbpy.open(parameter=cbpy.defaultConParams())
|
105
|
+
"""
|
106
|
+
cdef cbSdkResult res
|
107
|
+
|
108
|
+
wconType = {'default': CBSDKCONNECTION_DEFAULT, 'slave': CBSDKCONNECTION_CENTRAL, 'master': CBSDKCONNECTION_UDP}
|
109
|
+
if not connection in wconType.keys():
|
110
|
+
raise RuntimeError("invalid connection %s" % connection)
|
111
|
+
|
112
|
+
cdef cbSdkConnectionType conType = wconType[connection]
|
113
|
+
cdef cbSdkConnection con
|
114
|
+
|
115
|
+
parameter = parameter if parameter is not None else {}
|
116
|
+
cdef bytes szOutIP = parameter.get('inst-addr', cbNET_UDP_ADDR_CNT.decode("utf-8")).encode()
|
117
|
+
cdef bytes szInIP = parameter.get('client-addr', '').encode()
|
118
|
+
|
119
|
+
con.szOutIP = szOutIP
|
120
|
+
con.nOutPort = parameter.get('inst-port', cbNET_UDP_PORT_CNT)
|
121
|
+
con.szInIP = szInIP
|
122
|
+
con.nInPort = parameter.get('client-port', cbNET_UDP_PORT_BCAST)
|
123
|
+
con.nRecBufSize = parameter.get('receive-buffer-size', 0)
|
124
|
+
|
125
|
+
res = cbSdkOpen(<uint32_t>instance, conType, con)
|
126
|
+
|
127
|
+
handle_result(res)
|
128
|
+
|
129
|
+
return get_connection_type(instance=instance)
|
130
|
+
|
131
|
+
|
132
|
+
def close(int instance=0):
|
133
|
+
"""Close library.
|
134
|
+
Inputs:
|
135
|
+
instance - (optional) library instance number
|
136
|
+
"""
|
137
|
+
return handle_result(cbSdkClose(<uint32_t>instance))
|
138
|
+
|
139
|
+
|
140
|
+
def get_connection_type(int instance=0) -> dict[str, str]:
|
141
|
+
""" Get connection type
|
142
|
+
Inputs:
|
143
|
+
instance - (optional) library instance number
|
144
|
+
Outputs:
|
145
|
+
dictionary with following keys
|
146
|
+
'connection': Final established connection; can be any of:
|
147
|
+
'Default', 'Slave', 'Master', 'Closed', 'Unknown'
|
148
|
+
'instrument': Instrument connected to; can be any of:
|
149
|
+
'NSP', 'nPlay', 'Local NSP', 'Remote nPlay', 'Unknown')
|
150
|
+
"""
|
151
|
+
|
152
|
+
cdef cbSdkResult res
|
153
|
+
cdef cbSdkConnectionType conType
|
154
|
+
cdef cbSdkInstrumentType instType
|
155
|
+
|
156
|
+
res = cbSdkGetType(<uint32_t>instance, &conType, &instType)
|
157
|
+
|
158
|
+
handle_result(res)
|
159
|
+
|
160
|
+
connections = ["Default", "Slave", "Master", "Closed", "Unknown"]
|
161
|
+
instruments = ["NSP", "nPlay", "Local NSP", "Remote nPlay", "Unknown"]
|
162
|
+
|
163
|
+
con_idx = conType
|
164
|
+
if con_idx < 0 or con_idx >= len(connections):
|
165
|
+
con_idx = len(connections) - 1
|
166
|
+
inst_idx = instType
|
167
|
+
if inst_idx < 0 or inst_idx >= len(instruments):
|
168
|
+
inst_idx = len(instruments) - 1
|
169
|
+
|
170
|
+
return {"connection": connections[con_idx], "instrument": instruments[inst_idx]}
|
171
|
+
|
172
|
+
|
173
|
+
def trial_config(
|
174
|
+
int instance=0,
|
175
|
+
activate=True,
|
176
|
+
n_continuous: int = 0,
|
177
|
+
n_event: int = 0,
|
178
|
+
n_comment: int = 0,
|
179
|
+
n_tracking: int = 0,
|
180
|
+
begin_channel: int = 0,
|
181
|
+
begin_mask: int = 0,
|
182
|
+
begin_value: int = 0,
|
183
|
+
end_channel: int = 0,
|
184
|
+
end_mask: int = 0,
|
185
|
+
end_value: int = 0,
|
186
|
+
) -> None:
|
187
|
+
"""
|
188
|
+
Configure trial settings. See cbSdkSetTrialConfig in cbsdk.h for details.
|
189
|
+
|
190
|
+
Inputs:
|
191
|
+
activate - boolean, set True to flush data cache and start collecting data immediately,
|
192
|
+
set False to stop collecting data immediately or, if not already in a trial,
|
193
|
+
to rely on the begin/end channel mask|value to start/stop collecting data.
|
194
|
+
n_continuous - integer, number of continuous data samples to be cached.
|
195
|
+
0 means no continuous data is cached. -1 will use cbSdk_CONTINUOUS_DATA_SAMPLES.
|
196
|
+
n_event - integer, number of events to be cached.
|
197
|
+
0 means no events are captured. -1 will use cbSdk_EVENT_DATA_SAMPLES.
|
198
|
+
n_comment - integer, number of comments to be cached.
|
199
|
+
n_tracking - integer, number of video tracking events to be cached. Deprecated.
|
200
|
+
begin_channel - integer, channel to monitor to trigger trial start
|
201
|
+
Ignored if activate is True.
|
202
|
+
begin_mask - integer, mask to bitwise-and with data on begin_channel to look for trigger
|
203
|
+
Ignored if activate is True.
|
204
|
+
begin_value - value to trigger trial start
|
205
|
+
Ignored if activate is True.
|
206
|
+
end_channel - integer, channel to monitor to trigger trial stop
|
207
|
+
Ignored if activate is True.
|
208
|
+
end_mask - mask to bitwise-and with data on end_channel to evaluate trigger
|
209
|
+
Ignored if activate is True.
|
210
|
+
end_value - value to trigger trial stop
|
211
|
+
Ignored if activate is True.
|
212
|
+
instance - (optional) library instance number
|
213
|
+
"""
|
214
|
+
|
215
|
+
cdef cbSdkResult res
|
216
|
+
cdef cbSdkConfigParam cfg_param
|
217
|
+
|
218
|
+
# retrieve old values
|
219
|
+
res = cbsdk_get_trial_config(<uint32_t>instance, &cfg_param)
|
220
|
+
handle_result(res)
|
221
|
+
|
222
|
+
cfg_param.bActive = <uint32_t>activate
|
223
|
+
|
224
|
+
# Fill cfg_param with provided buffer_parameter values or default.
|
225
|
+
cfg_param.uWaveforms = 0 # waveforms do not work
|
226
|
+
cfg_param.uConts = n_continuous if n_continuous >= 0 else cbSdk_CONTINUOUS_DATA_SAMPLES
|
227
|
+
cfg_param.uEvents = n_event if n_event >= 0 else cbSdk_EVENT_DATA_SAMPLES
|
228
|
+
cfg_param.uComments = n_comment or 0
|
229
|
+
cfg_param.uTrackings = n_tracking or 0
|
230
|
+
|
231
|
+
# Fill cfg_param mask-related parameters with provided range_parameter or default.
|
232
|
+
cfg_param.Begchan = begin_channel
|
233
|
+
cfg_param.Begmask = begin_mask
|
234
|
+
cfg_param.Begval = begin_value
|
235
|
+
cfg_param.Endchan = end_channel
|
236
|
+
cfg_param.Endmask = end_mask
|
237
|
+
cfg_param.Endval = end_value
|
238
|
+
|
239
|
+
res = cbsdk_set_trial_config(<int>instance, &cfg_param)
|
240
|
+
|
241
|
+
handle_result(res)
|
242
|
+
|
243
|
+
|
244
|
+
def trial_event(int instance=0, bool seek=False, bool reset_clock=False):
|
245
|
+
""" Trial spike and event data.
|
246
|
+
Inputs:
|
247
|
+
instance - (optional) library instance number
|
248
|
+
seek - (optional) boolean
|
249
|
+
set False (default) to leave buffer intact -- peek only.
|
250
|
+
set True to clear all the data after it has been retrieved.
|
251
|
+
reset_clock - (optional) boolean
|
252
|
+
set False (default) to leave the trial clock alone.
|
253
|
+
set True to update the _next_ trial time to the current time.
|
254
|
+
This is overly complicated. Leave this as `False` unless you really know what you are doing.
|
255
|
+
Note: All timestamps are now in absolute device time.
|
256
|
+
Outputs:
|
257
|
+
list of arrays [channel, {'timestamps':[unit0_ts, ..., unitN_ts], 'events':digital_events}]
|
258
|
+
channel: integer, channel number (1-based)
|
259
|
+
digital_events: array, digital event values for channel (if a digital or serial channel)
|
260
|
+
unitN_ts: array, spike timestamps of unit N for channel (if an electrode channel));
|
261
|
+
"""
|
262
|
+
|
263
|
+
cdef cbSdkResult res
|
264
|
+
cdef cbSdkTrialEvent trialevent
|
265
|
+
|
266
|
+
# get how many samples are available
|
267
|
+
res = cbsdk_init_trial_event(<uint32_t>instance, <int>reset_clock, &trialevent)
|
268
|
+
handle_result(res)
|
269
|
+
|
270
|
+
if trialevent.num_events == 0:
|
271
|
+
return res, {'timestamps': np.array([], dtype=_PROCTIME_DTYPE),
|
272
|
+
'channels': np.array([], dtype=np.uint16),
|
273
|
+
'units': np.array([], dtype=np.uint16)}
|
274
|
+
|
275
|
+
# Allocate flat arrays for event data
|
276
|
+
cdef cnp.ndarray timestamps = np.zeros(trialevent.num_events, dtype=_PROCTIME_DTYPE)
|
277
|
+
cdef cnp.ndarray channels = np.zeros(trialevent.num_events, dtype=np.uint16)
|
278
|
+
cdef cnp.ndarray units = np.zeros(trialevent.num_events, dtype=np.uint16)
|
279
|
+
|
280
|
+
# Point C structure to our numpy arrays
|
281
|
+
trialevent.timestamps = <PROCTIME *>cnp.PyArray_DATA(timestamps)
|
282
|
+
trialevent.channels = <uint16_t *>cnp.PyArray_DATA(channels)
|
283
|
+
trialevent.units = <uint16_t *>cnp.PyArray_DATA(units)
|
284
|
+
trialevent.waveforms = NULL # TODO: Add waveform support if needed
|
285
|
+
|
286
|
+
# get the trial
|
287
|
+
res = cbsdk_get_trial_event(<uint32_t>instance, <int>seek, &trialevent)
|
288
|
+
handle_result(res)
|
289
|
+
|
290
|
+
trial = {
|
291
|
+
'timestamps': timestamps,
|
292
|
+
'channels': channels,
|
293
|
+
'units': units
|
294
|
+
}
|
295
|
+
|
296
|
+
return <int>res, trial
|
297
|
+
|
298
|
+
|
299
|
+
def trial_continuous(
|
300
|
+
int instance=0,
|
301
|
+
uint8_t group=0,
|
302
|
+
bool seek=True,
|
303
|
+
bool reset_clock=False,
|
304
|
+
timestamps=None,
|
305
|
+
samples=None,
|
306
|
+
uint32_t num_samples=0
|
307
|
+
) -> dict[str, typing.Any]:
|
308
|
+
"""
|
309
|
+
Trial continuous data for a specific sample group.
|
310
|
+
|
311
|
+
Inputs:
|
312
|
+
instance - (optional) library instance number
|
313
|
+
group - (optional) sample group to retrieve (0-6), default=0 will always return an empty result.
|
314
|
+
seek - (optional) boolean
|
315
|
+
set True (default) to advance read pointer after retrieval.
|
316
|
+
set False to peek -- data remains in buffer for next call.
|
317
|
+
reset_clock - (optional) boolean
|
318
|
+
Keep False (default) unless you really know what you are doing.
|
319
|
+
timestamps - (optional) pre-allocated numpy array for timestamps, shape=[num_samples], dtype=uint32 or uint64
|
320
|
+
If None, function will allocate
|
321
|
+
samples - (optional) pre-allocated numpy array for samples, shape=[num_samples, num_channels], dtype=int16
|
322
|
+
If None, function will allocate
|
323
|
+
num_samples - (optional) maximum samples to read. If 0 and arrays provided, inferred from array shape.
|
324
|
+
If arrays provided and this is specified, reads min(num_samples, array_size)
|
325
|
+
|
326
|
+
Outputs:
|
327
|
+
data - dictionary with keys:
|
328
|
+
'group': group number (0-6)
|
329
|
+
'count': number of channels in this group
|
330
|
+
'chan': array of channel IDs (1-based)
|
331
|
+
'num_samples': actual number of samples read
|
332
|
+
'trial_start_time': PROCTIME timestamp when the current **trial** started
|
333
|
+
'timestamps': numpy array [num_samples] of PROCTIME timestamps (absolute device time)
|
334
|
+
'samples': numpy array [num_samples, count] of int16 sample data
|
335
|
+
"""
|
336
|
+
cdef cbSdkResult res
|
337
|
+
cdef cbSdkTrialCont trialcont
|
338
|
+
cdef cnp.ndarray timestamps_array
|
339
|
+
cdef cnp.ndarray samples_array
|
340
|
+
cdef bool user_provided_arrays = timestamps is not None and samples is not None
|
341
|
+
|
342
|
+
# Set the group we want to retrieve
|
343
|
+
trialcont.group = group
|
344
|
+
|
345
|
+
# Initialize - get channel count and buffer info for this group
|
346
|
+
res = cbSdkInitTrialData(<uint32_t>instance, reset_clock, NULL, &trialcont, NULL, NULL, 0)
|
347
|
+
handle_result(res)
|
348
|
+
|
349
|
+
if trialcont.count == 0:
|
350
|
+
# No channels in this group
|
351
|
+
return {
|
352
|
+
"group": group,
|
353
|
+
"count": 0,
|
354
|
+
"chan": np.array([], dtype=np.uint16),
|
355
|
+
"num_samples": 0,
|
356
|
+
"trial_start_time": trialcont.trial_start_time,
|
357
|
+
"timestamps": np.array([], dtype=_PROCTIME_DTYPE),
|
358
|
+
"samples": np.array([], dtype=np.int16).reshape(0, 0)
|
359
|
+
}
|
360
|
+
|
361
|
+
# Determine num_samples to request
|
362
|
+
if user_provided_arrays:
|
363
|
+
# Validate and use user arrays
|
364
|
+
if not isinstance(timestamps, np.ndarray) or not isinstance(samples, np.ndarray):
|
365
|
+
raise ValueError("timestamps and samples must both be numpy arrays")
|
366
|
+
|
367
|
+
# Check dtypes
|
368
|
+
if timestamps.dtype not in [np.uint32, np.uint64]:
|
369
|
+
raise ValueError(f"timestamps must be dtype uint32 or uint64, got {timestamps.dtype}")
|
370
|
+
if samples.dtype != np.int16:
|
371
|
+
raise ValueError(f"samples must be dtype int16, got {samples.dtype}")
|
372
|
+
|
373
|
+
# Check contiguity
|
374
|
+
if not timestamps.flags['C_CONTIGUOUS']:
|
375
|
+
raise ValueError("timestamps array must be C-contiguous")
|
376
|
+
if not samples.flags['C_CONTIGUOUS']:
|
377
|
+
raise ValueError("samples array must be C-contiguous")
|
378
|
+
|
379
|
+
# Check shapes
|
380
|
+
if timestamps.ndim != 1:
|
381
|
+
raise ValueError(f"timestamps must be 1D array, got shape {timestamps.shape}")
|
382
|
+
if samples.ndim != 2:
|
383
|
+
raise ValueError(f"samples must be 2D array, got shape {samples.shape}")
|
384
|
+
|
385
|
+
if timestamps.shape[0] != samples.shape[0]:
|
386
|
+
raise ValueError(f"timestamps and samples must have same length: {timestamps.shape[0]} != {samples.shape[0]}")
|
387
|
+
|
388
|
+
if samples.shape[1] != trialcont.count:
|
389
|
+
raise ValueError(f"samples shape[1] must match channel count: {samples.shape[1]} != {trialcont.count} expected")
|
390
|
+
|
391
|
+
# Determine num_samples from arrays if not specified
|
392
|
+
if num_samples == 0:
|
393
|
+
num_samples = timestamps.shape[0]
|
394
|
+
else:
|
395
|
+
# Use minimum of requested and available array size
|
396
|
+
num_samples = min(num_samples, <uint32_t>timestamps.shape[0])
|
397
|
+
|
398
|
+
timestamps_array = timestamps
|
399
|
+
samples_array = samples
|
400
|
+
else:
|
401
|
+
# Allocate arrays
|
402
|
+
if num_samples == 0:
|
403
|
+
num_samples = trialcont.num_samples # Use what Init reported as available
|
404
|
+
|
405
|
+
timestamps_array = np.empty(num_samples, dtype=_PROCTIME_DTYPE)
|
406
|
+
samples_array = np.empty((num_samples, trialcont.count), dtype=np.int16)
|
407
|
+
|
408
|
+
# Set num_samples and point to array data
|
409
|
+
trialcont.num_samples = num_samples
|
410
|
+
trialcont.timestamps = <PROCTIME*>cnp.PyArray_DATA(timestamps_array)
|
411
|
+
trialcont.samples = <int16_t*>cnp.PyArray_DATA(samples_array)
|
412
|
+
|
413
|
+
# Get the data
|
414
|
+
res = cbSdkGetTrialData(<uint32_t>instance, <uint32_t>seek, NULL, &trialcont, NULL, NULL)
|
415
|
+
handle_result(res)
|
416
|
+
|
417
|
+
# Build channel array
|
418
|
+
cdef cnp.ndarray chan_array = np.empty(trialcont.count, dtype=np.uint16)
|
419
|
+
cdef uint16_t[::1] chan_view = chan_array
|
420
|
+
cdef int i
|
421
|
+
for i in range(trialcont.count):
|
422
|
+
chan_view[i] = trialcont.chan[i]
|
423
|
+
|
424
|
+
# Prepare output - actual num_samples may be less than requested
|
425
|
+
cdef uint32_t actual_samples = trialcont.num_samples
|
426
|
+
|
427
|
+
return {
|
428
|
+
'group': group,
|
429
|
+
'count': trialcont.count,
|
430
|
+
'chan': chan_array,
|
431
|
+
'num_samples': actual_samples,
|
432
|
+
'trial_start_time': trialcont.trial_start_time,
|
433
|
+
'timestamps': timestamps_array[:actual_samples],
|
434
|
+
'samples': samples_array[:actual_samples, :]
|
435
|
+
}
|
436
|
+
|
437
|
+
|
438
|
+
def trial_data(int instance=0, bool seek=False, bool reset_clock=False,
|
439
|
+
bool do_event=True, bool do_cont=True, bool do_comment=False, unsigned long wait_for_comment_msec=250):
|
440
|
+
"""
|
441
|
+
|
442
|
+
:param instance: (optional) library instance number
|
443
|
+
:param seek: (optional) boolean
|
444
|
+
set False (default) to peek only and leave buffer intact.
|
445
|
+
set True to advance the data pointer after retrieval.
|
446
|
+
:param reset_clock - (optional) boolean
|
447
|
+
set False (default) to leave the trial clock alone.
|
448
|
+
set True to update the _next_ trial time to the current time.
|
449
|
+
This is overly complicated. Leave this as `False` unless you really know what you are doing.
|
450
|
+
Note: All timestamps are now in absolute device time.
|
451
|
+
:param do_event: (optional) boolean. Set False to skip fetching events.
|
452
|
+
:param do_cont: (optional) boolean. Set False to skip fetching continuous data.
|
453
|
+
:param do_comment: (optional) boolean. Set to True to fetch comments.
|
454
|
+
:param wait_for_comment_msec: (optional) unsigned long. How long we should wait for new comments.
|
455
|
+
Default (0) will not wait and will only return comments that existed prior to calling this.
|
456
|
+
:return: (result, event_data, continuous_data, t_zero, comment_data)
|
457
|
+
res: (int) returned by cbsdk
|
458
|
+
event data: list of arrays [channel, {'timestamps':[unit0_ts, ..., unitN_ts], 'events':digital_events}]
|
459
|
+
channel: integer, channel number (1-based)
|
460
|
+
digital_events: array, digital event values for channel (if a digital or serial channel)
|
461
|
+
unitN_ts: array, spike timestamps of unit N for channel (if an electrode channel)
|
462
|
+
Note: timestamps are PROCTIME (uint32 or uint64), events are uint16
|
463
|
+
continuous data: list of dictionaries, one per sample group (0-7) that has channels:
|
464
|
+
{
|
465
|
+
'group': group number (0-7),
|
466
|
+
'count': number of channels,
|
467
|
+
'chan': numpy array of channel IDs (1-based),
|
468
|
+
'sample_rate': sample rate (Hz),
|
469
|
+
'num_samples': number of samples,
|
470
|
+
'timestamps': numpy array [num_samples] of PROCTIME,
|
471
|
+
'samples': numpy array [num_samples, count] of int16
|
472
|
+
}
|
473
|
+
t_zero: deprecated, always 0
|
474
|
+
comment_data: list of lists the form [timestamp, comment_str, charset, rgba]
|
475
|
+
timestamp: PROCTIME
|
476
|
+
comment_str: the comment in binary.
|
477
|
+
Use comment_str.decode('utf-16' if charset==1 else locale.getpreferredencoding())
|
478
|
+
rgba: integer; the comment colour. 8 bits each for r, g, b, a
|
479
|
+
"""
|
480
|
+
|
481
|
+
cdef cbSdkResult res
|
482
|
+
cdef cbSdkTrialEvent trialevent
|
483
|
+
cdef cbSdkTrialComment trialcomm
|
484
|
+
cdef cbSdkTrialCont trialcont_group
|
485
|
+
# cdef uint8_t ch_type
|
486
|
+
cdef uint32_t b_dig_in
|
487
|
+
cdef uint32_t b_serial
|
488
|
+
|
489
|
+
cdef uint32_t tzero = 0
|
490
|
+
cdef int comm_ix
|
491
|
+
cdef uint32_t num_samples
|
492
|
+
cdef int channel
|
493
|
+
cdef uint16_t ch
|
494
|
+
cdef int u
|
495
|
+
cdef int g, j
|
496
|
+
cdef uint32_t actual_samples_grp
|
497
|
+
|
498
|
+
cdef cnp.ndarray mxa_proctime
|
499
|
+
cdef cnp.uint16_t[:] mxa_u16
|
500
|
+
cdef cnp.uint8_t[:] mxa_u8
|
501
|
+
cdef cnp.uint32_t[:] mxa_u32
|
502
|
+
cdef cnp.ndarray timestamps_grp
|
503
|
+
cdef cnp.ndarray samples_grp
|
504
|
+
cdef cnp.ndarray chan_array_grp
|
505
|
+
cdef uint16_t[::1] chan_view_grp
|
506
|
+
|
507
|
+
trial_event = []
|
508
|
+
trial_cont = []
|
509
|
+
trial_comment = []
|
510
|
+
|
511
|
+
# get how many samples are available
|
512
|
+
# Note: continuous data is now handled per-group below, so we don't init it here
|
513
|
+
res = cbsdk_init_trial_data(<uint32_t>instance, <int>reset_clock, &trialevent if do_event else NULL,
|
514
|
+
NULL, &trialcomm if do_comment else NULL,
|
515
|
+
wait_for_comment_msec)
|
516
|
+
handle_result(res)
|
517
|
+
|
518
|
+
# Early return if none of the requested data are available.
|
519
|
+
# For continuous, we'll check per-group below
|
520
|
+
if (not do_event or (trialevent.num_events == 0)) \
|
521
|
+
and (not do_comment or (trialcomm.num_samples == 0)) \
|
522
|
+
and (not do_cont):
|
523
|
+
return res, trial_event, trial_cont, tzero, trial_comment
|
524
|
+
|
525
|
+
# Events #
|
526
|
+
# ------ #
|
527
|
+
if do_event and trialevent.num_events > 0:
|
528
|
+
# Allocate flat arrays for event data
|
529
|
+
timestamps = np.zeros(trialevent.num_events, dtype=_PROCTIME_DTYPE)
|
530
|
+
channels = np.zeros(trialevent.num_events, dtype=np.uint16)
|
531
|
+
units = np.zeros(trialevent.num_events, dtype=np.uint16)
|
532
|
+
|
533
|
+
# Point C structure to our numpy arrays
|
534
|
+
trialevent.timestamps = <PROCTIME *>cnp.PyArray_DATA(timestamps)
|
535
|
+
trialevent.channels = <uint16_t *>cnp.PyArray_DATA(channels)
|
536
|
+
trialevent.units = <uint16_t *>cnp.PyArray_DATA(units)
|
537
|
+
trialevent.waveforms = NULL # TODO: Add waveform support if needed
|
538
|
+
|
539
|
+
trial_event = {
|
540
|
+
'timestamps': timestamps,
|
541
|
+
'channels': channels,
|
542
|
+
'units': units
|
543
|
+
}
|
544
|
+
|
545
|
+
# Continuous #
|
546
|
+
# ---------- #
|
547
|
+
# With the new group-based API, we fetch each group (0-7) separately
|
548
|
+
if do_cont:
|
549
|
+
# Fetch all 8 groups
|
550
|
+
for g in range(8): # Groups 0-7 (cbMAXGROUPS)
|
551
|
+
trialcont_group.group = g
|
552
|
+
|
553
|
+
# Initialize this group
|
554
|
+
res = cbSdkInitTrialData(<uint32_t>instance, reset_clock, NULL, &trialcont_group, NULL, NULL, 0)
|
555
|
+
if res != CBSDKRESULT_SUCCESS or trialcont_group.count == 0:
|
556
|
+
continue # Skip groups with no channels
|
557
|
+
|
558
|
+
# Allocate arrays for this group
|
559
|
+
num_samples = trialcont_group.num_samples
|
560
|
+
timestamps_grp = np.empty(num_samples, dtype=_PROCTIME_DTYPE)
|
561
|
+
samples_grp = np.empty((num_samples, trialcont_group.count), dtype=np.int16)
|
562
|
+
|
563
|
+
# Point trialcont to arrays
|
564
|
+
trialcont_group.num_samples = num_samples
|
565
|
+
trialcont_group.timestamps = <PROCTIME*>cnp.PyArray_DATA(timestamps_grp)
|
566
|
+
trialcont_group.samples = <int16_t*>cnp.PyArray_DATA(samples_grp)
|
567
|
+
|
568
|
+
# Get data for this group
|
569
|
+
res = cbSdkGetTrialData(<uint32_t>instance, 0, NULL, &trialcont_group, NULL, NULL)
|
570
|
+
if res != CBSDKRESULT_SUCCESS:
|
571
|
+
continue
|
572
|
+
|
573
|
+
# Build channel array
|
574
|
+
chan_array_grp = np.empty(trialcont_group.count, dtype=np.uint16)
|
575
|
+
chan_view_grp = chan_array_grp
|
576
|
+
for j in range(trialcont_group.count):
|
577
|
+
chan_view_grp[j] = trialcont_group.chan[j]
|
578
|
+
|
579
|
+
actual_samples_grp = trialcont_group.num_samples
|
580
|
+
|
581
|
+
# Append group data as dictionary
|
582
|
+
trial_cont.append({
|
583
|
+
'group': g,
|
584
|
+
'count': trialcont_group.count,
|
585
|
+
'chan': chan_array_grp,
|
586
|
+
'sample_rate': trialcont_group.sample_rate,
|
587
|
+
'num_samples': actual_samples_grp,
|
588
|
+
'timestamps': timestamps_grp[:actual_samples_grp],
|
589
|
+
'samples': samples_grp[:actual_samples_grp, :]
|
590
|
+
})
|
591
|
+
|
592
|
+
# Comments #
|
593
|
+
# -------- #
|
594
|
+
if do_comment and (trialcomm.num_samples > 0):
|
595
|
+
# Allocate memory
|
596
|
+
# For charsets;
|
597
|
+
mxa_u8 = np.zeros(trialcomm.num_samples, dtype=np.uint8)
|
598
|
+
trialcomm.charsets = <uint8_t *>&mxa_u8[0]
|
599
|
+
my_charsets = np.asarray(mxa_u8)
|
600
|
+
# For rgbas
|
601
|
+
mxa_u32 = np.zeros(trialcomm.num_samples, dtype=np.uint32)
|
602
|
+
trialcomm.rgbas = <uint32_t *>&mxa_u32[0]
|
603
|
+
my_rgbas = np.asarray(mxa_u32)
|
604
|
+
# For timestamps
|
605
|
+
mxa_proctime = np.zeros(trialcomm.num_samples, dtype=_PROCTIME_DTYPE)
|
606
|
+
trialcomm.timestamps = <PROCTIME *>cnp.PyArray_DATA(mxa_proctime)
|
607
|
+
my_timestamps = mxa_proctime
|
608
|
+
# For comments
|
609
|
+
trialcomm.comments = <uint8_t **>malloc(trialcomm.num_samples * sizeof(uint8_t*))
|
610
|
+
for comm_ix in range(trialcomm.num_samples):
|
611
|
+
trialcomm.comments[comm_ix] = <uint8_t *>malloc(256 * sizeof(uint8_t))
|
612
|
+
trial_comment.append([my_timestamps[comm_ix], trialcomm.comments[comm_ix],
|
613
|
+
my_charsets[comm_ix], my_rgbas[comm_ix]])
|
614
|
+
|
615
|
+
# cbsdk get trial data
|
616
|
+
# Note: continuous data was already fetched per-group above
|
617
|
+
try:
|
618
|
+
if do_event or do_comment:
|
619
|
+
res = cbsdk_get_trial_data(<uint32_t>instance, <int>seek,
|
620
|
+
&trialevent if do_event else NULL,
|
621
|
+
NULL, # continuous handled per-group above
|
622
|
+
&trialcomm if do_comment else NULL)
|
623
|
+
handle_result(res)
|
624
|
+
finally:
|
625
|
+
if do_comment:
|
626
|
+
free(trialcomm.comments)
|
627
|
+
|
628
|
+
# Note: tzero is no longer meaningful with group-based continuous data
|
629
|
+
return <int>res, trial_event, trial_cont, tzero, trial_comment
|
630
|
+
|
631
|
+
|
632
|
+
def trial_comment(int instance=0, bool seek=False, bool clock_reset=False, unsigned long wait_for_comment_msec=250):
|
633
|
+
""" Trial comment data.
|
634
|
+
Inputs:
|
635
|
+
seek - (optional) boolean
|
636
|
+
set False (default) to peek only and leave buffer intact.
|
637
|
+
set True to clear all the data and advance the data pointer.
|
638
|
+
clock_reset - (optional) boolean
|
639
|
+
set False (default) to leave the trial clock alone.
|
640
|
+
set True to update the _next_ trial time to the current time.
|
641
|
+
instance - (optional) library instance number
|
642
|
+
Outputs:
|
643
|
+
list of lists the form [timestamp, comment_str, rgba]
|
644
|
+
timestamp: PROCTIME
|
645
|
+
comment_str: the comment as a py string
|
646
|
+
rgba: integer; the comment colour. 8 bits each for r, g, b, a
|
647
|
+
"""
|
648
|
+
|
649
|
+
cdef cbSdkResult res
|
650
|
+
cdef cbSdkTrialComment trialcomm
|
651
|
+
|
652
|
+
# get how many comments are available
|
653
|
+
res = cbsdk_init_trial_comment(<uint32_t>instance, <int>clock_reset, &trialcomm, wait_for_comment_msec)
|
654
|
+
handle_result(res)
|
655
|
+
|
656
|
+
if trialcomm.num_samples == 0:
|
657
|
+
return <int>res, []
|
658
|
+
|
659
|
+
# allocate memory
|
660
|
+
|
661
|
+
# types
|
662
|
+
cdef cnp.uint8_t[:] mxa_u8_cs # charsets
|
663
|
+
cdef cnp.uint32_t[:] mxa_u32_rgbas
|
664
|
+
cdef cnp.ndarray mxa_proctime
|
665
|
+
|
666
|
+
# For charsets;
|
667
|
+
mxa_u8_cs = np.zeros(trialcomm.num_samples, dtype=np.uint8)
|
668
|
+
trialcomm.charsets = <uint8_t *>&mxa_u8_cs[0]
|
669
|
+
my_charsets = np.asarray(mxa_u8_cs)
|
670
|
+
|
671
|
+
# For rgbas
|
672
|
+
mxa_u32_rgbas = np.zeros(trialcomm.num_samples, dtype=np.uint32)
|
673
|
+
trialcomm.rgbas = <uint32_t *>&mxa_u32_rgbas[0]
|
674
|
+
my_rgbas = np.asarray(mxa_u32_rgbas)
|
675
|
+
|
676
|
+
# For comments
|
677
|
+
trialcomm.comments = <uint8_t **>malloc(trialcomm.num_samples * sizeof(uint8_t*))
|
678
|
+
cdef int comm_ix
|
679
|
+
for comm_ix in range(trialcomm.num_samples):
|
680
|
+
trialcomm.comments[comm_ix] = <uint8_t *>malloc(256 * sizeof(uint8_t))
|
681
|
+
|
682
|
+
# For timestamps
|
683
|
+
mxa_proctime = np.zeros(trialcomm.num_samples, dtype=_PROCTIME_DTYPE)
|
684
|
+
trialcomm.timestamps = <PROCTIME *>cnp.PyArray_DATA(mxa_proctime)
|
685
|
+
my_timestamps = mxa_proctime
|
686
|
+
|
687
|
+
trial = []
|
688
|
+
try:
|
689
|
+
res = cbsdk_get_trial_comment(<int>instance, <int>seek, &trialcomm)
|
690
|
+
handle_result(res)
|
691
|
+
for comm_ix in range(trialcomm.num_samples):
|
692
|
+
# this_enc = 'utf-16' if my_charsets[comm_ix]==1 else locale.getpreferredencoding()
|
693
|
+
row = [my_timestamps[comm_ix], trialcomm.comments[comm_ix], my_rgbas[comm_ix]] # .decode(this_enc)
|
694
|
+
trial.append(row)
|
695
|
+
finally:
|
696
|
+
free(trialcomm.comments)
|
697
|
+
|
698
|
+
return <int>res, trial
|
699
|
+
|
700
|
+
|
701
|
+
def file_config(int instance=0, command='info', comment='', filename='', patient_info=None):
|
702
|
+
""" Configure remote file recording or get status of recording.
|
703
|
+
Inputs:
|
704
|
+
command - string, File configuration command, can be of of the following
|
705
|
+
'info': (default) get File recording information
|
706
|
+
'open': opens the File dialog if closed, ignoring other parameters
|
707
|
+
'close': closes the File dialog if open
|
708
|
+
'start': starts recording, opens dialog if closed
|
709
|
+
'stop': stops recording
|
710
|
+
filename - (optional) string, file name to use for recording
|
711
|
+
comment - (optional) string, file comment to use for file recording
|
712
|
+
instance - (optional) library instance number
|
713
|
+
patient_info - (optional) dict carrying patient info. If provided then valid keys and value types:
|
714
|
+
'ID': str - required, 'firstname': str - defaults to 'J', 'lastname': str - defaults to 'Doe',
|
715
|
+
'DOBMonth': int - defaults to 01, 'DOBDay': int - defaults to 01, 'DOBYear': int - defaults to 1970
|
716
|
+
Outputs:
|
717
|
+
Only if command is 'info' output is returned
|
718
|
+
A dictionary with following keys:
|
719
|
+
'Recording': boolean, if recording is in progress
|
720
|
+
'FileName': string, file name being recorded
|
721
|
+
'UserName': Computer that is recording
|
722
|
+
"""
|
723
|
+
|
724
|
+
|
725
|
+
cdef cbSdkResult res
|
726
|
+
cdef char fname[256]
|
727
|
+
cdef char username[256]
|
728
|
+
cdef bool bRecording = False
|
729
|
+
|
730
|
+
if command == 'info':
|
731
|
+
|
732
|
+
res = cbSdkGetFileConfig(<uint32_t>instance, fname, username, &bRecording)
|
733
|
+
handle_result(res)
|
734
|
+
info = {'Recording':bRecording, 'FileName':<bytes>fname, 'UserName':<bytes>username}
|
735
|
+
return res, info
|
736
|
+
|
737
|
+
cdef int start = 0
|
738
|
+
cdef unsigned int options = cbFILECFG_OPT_NONE
|
739
|
+
if command == 'open':
|
740
|
+
if filename or comment:
|
741
|
+
raise RuntimeError('filename and comment must not be specified for open')
|
742
|
+
options = cbFILECFG_OPT_OPEN
|
743
|
+
elif command == 'close':
|
744
|
+
options = cbFILECFG_OPT_CLOSE
|
745
|
+
elif command == 'start':
|
746
|
+
if not filename:
|
747
|
+
raise RuntimeError('filename must be specified for start')
|
748
|
+
start = 1
|
749
|
+
elif command == 'stop':
|
750
|
+
if not filename:
|
751
|
+
raise RuntimeError('filename must be specified for stop')
|
752
|
+
start = 0
|
753
|
+
else:
|
754
|
+
raise RuntimeError("invalid file config command %s" % command)
|
755
|
+
|
756
|
+
cdef cbSdkResult patient_res
|
757
|
+
if start and patient_info is not None and 'ID' in patient_info:
|
758
|
+
default_patient_info = {'firstname': 'J', 'lastname': 'Doe', 'DOBMonth': 1, 'DOBDay': 1, 'DOBYear': 1970}
|
759
|
+
patient_info = {**default_patient_info, **patient_info}
|
760
|
+
for k,v in patient_info.items():
|
761
|
+
if type(v) is str:
|
762
|
+
patient_info[k] = v.encode('UTF-8')
|
763
|
+
|
764
|
+
patient_res = cbSdkSetPatientInfo(<uint32_t>instance,
|
765
|
+
<const char *>patient_info['ID'],
|
766
|
+
<const char *>patient_info['firstname'],
|
767
|
+
<const char *>patient_info['lastname'],
|
768
|
+
<uint32_t>patient_info['DOBMonth'],
|
769
|
+
<uint32_t>patient_info['DOBDay'],
|
770
|
+
<uint32_t>patient_info['DOBYear'])
|
771
|
+
handle_result(patient_res)
|
772
|
+
|
773
|
+
cdef int set_res
|
774
|
+
filename_string = filename.encode('UTF-8')
|
775
|
+
comment_string = comment.encode('UTF-8')
|
776
|
+
set_res = cbsdk_file_config(<uint32_t>instance, <const char *>filename_string, <char *>comment_string, start, options)
|
777
|
+
|
778
|
+
|
779
|
+
return set_res
|
780
|
+
|
781
|
+
|
782
|
+
def time(int instance=0, unit='samples'):
|
783
|
+
"""Instrument time.
|
784
|
+
Inputs:
|
785
|
+
unit - time unit, string can be one the following
|
786
|
+
'samples': (default) sample number integer
|
787
|
+
'seconds' or 's': seconds calculated from samples
|
788
|
+
'milliseconds' or 'ms': milliseconds calculated from samples
|
789
|
+
instance - (optional) library instance number
|
790
|
+
Outputs:
|
791
|
+
time - time passed since last reset
|
792
|
+
"""
|
793
|
+
|
794
|
+
|
795
|
+
cdef cbSdkResult res
|
796
|
+
|
797
|
+
if unit == 'samples':
|
798
|
+
factor = 1
|
799
|
+
elif unit in ['seconds', 's']:
|
800
|
+
raise NotImplementedError("Use time unit of samples for now")
|
801
|
+
elif unit in ['milliseconds', 'ms']:
|
802
|
+
raise NotImplementedError("Use time unit of samples for now")
|
803
|
+
else:
|
804
|
+
raise RuntimeError("Invalid time unit %s" % unit)
|
805
|
+
|
806
|
+
cdef PROCTIME cbtime
|
807
|
+
res = cbSdkGetTime(<uint32_t>instance, &cbtime)
|
808
|
+
handle_result(res)
|
809
|
+
|
810
|
+
time = float(cbtime) / factor
|
811
|
+
|
812
|
+
return <int>res, time
|
813
|
+
|
814
|
+
|
815
|
+
def analog_out(channel_out, channel_mon, track_last=True, spike_only=False, int instance=0):
|
816
|
+
"""
|
817
|
+
Monitor a channel.
|
818
|
+
Inputs:
|
819
|
+
channel_out - integer, analog output channel number (1-based)
|
820
|
+
On NSP, should be >= MIN_CHANS_ANALOG_OUT (273) && <= MAX_CHANS_AUDIO (278)
|
821
|
+
channel_mon - integer, channel to monitor (1-based)
|
822
|
+
track_last - (optional) If True, track last channel clicked on in raster plot or hardware config window.
|
823
|
+
spike_only - (optional) If True, only play spikes. If False, play continuous.
|
824
|
+
"""
|
825
|
+
cdef cbSdkResult res
|
826
|
+
cdef cbSdkAoutMon mon
|
827
|
+
if channel_out < 273:
|
828
|
+
channel_out += 128 # Recent NSP firmware upgrade added 128 more analog channels.
|
829
|
+
if channel_mon is None:
|
830
|
+
res = cbSdkSetAnalogOutput(<uint32_t>instance, <uint16_t>channel_out, NULL, NULL)
|
831
|
+
else:
|
832
|
+
mon.chan = <uint16_t>channel_mon
|
833
|
+
mon.bTrack = track_last
|
834
|
+
mon.bSpike = spike_only
|
835
|
+
res = cbSdkSetAnalogOutput(<uint32_t>instance, <uint16_t>channel_out, NULL, &mon)
|
836
|
+
handle_result(res)
|
837
|
+
return <int>res
|
838
|
+
|
839
|
+
|
840
|
+
def digital_out(int channel, int instance=0, value='low'):
|
841
|
+
"""Digital output command.
|
842
|
+
Inputs:
|
843
|
+
channel - integer, digital output channel number (1-based)
|
844
|
+
On NSP, 153 (dout1), 154 (dout2), 155 (dout3), 156 (dout4)
|
845
|
+
value - (optional), depends on the command
|
846
|
+
for command of 'set_value':
|
847
|
+
string, can be 'high' or 'low' (default)
|
848
|
+
instance - (optional) library instance number
|
849
|
+
"""
|
850
|
+
|
851
|
+
values = ['low', 'high']
|
852
|
+
if value not in values:
|
853
|
+
raise RuntimeError("Invalid value %s" % value)
|
854
|
+
|
855
|
+
cdef cbSdkResult res
|
856
|
+
cdef uint16_t int_val = values.index(value)
|
857
|
+
res = cbSdkSetDigitalOutput(<uint32_t>instance, <uint16_t>channel, int_val)
|
858
|
+
handle_result(res)
|
859
|
+
return <int>res
|
860
|
+
|
861
|
+
|
862
|
+
def get_channel_config(int channel, int instance=0, encoding="utf-8") -> dict[str, typing.Any]:
|
863
|
+
""" Get channel configuration.
|
864
|
+
Inputs:
|
865
|
+
- channel: 1-based channel number
|
866
|
+
- instance: (optional) library instance number
|
867
|
+
- encoding: (optional) encoding for string fields, default = 'utf-8'
|
868
|
+
Outputs:
|
869
|
+
-chaninfo = A Python dictionary with the following fields:
|
870
|
+
'time': system clock timestamp,
|
871
|
+
'chid': 0x8000,
|
872
|
+
'type': cbPKTTYPE_AINP*,
|
873
|
+
'dlen': cbPKT_DLENCHANINFO,
|
874
|
+
'chan': actual channel id of the channel being configured,
|
875
|
+
'proc': the address of the processor on which the channel resides,
|
876
|
+
'bank': the address of the bank on which the channel resides,
|
877
|
+
'term': the terminal number of the channel within it's bank,
|
878
|
+
'chancaps': general channel capablities (given by cbCHAN_* flags),
|
879
|
+
'doutcaps': digital output capablities (composed of cbDOUT_* flags),
|
880
|
+
'dinpcaps': digital input capablities (composed of cbDINP_* flags),
|
881
|
+
'aoutcaps': analog output capablities (composed of cbAOUT_* flags),
|
882
|
+
'ainpcaps': analog input capablities (composed of cbAINP_* flags),
|
883
|
+
'spkcaps': spike processing capabilities,
|
884
|
+
'label': Label of the channel (null terminated if <16 characters),
|
885
|
+
'userflags': User flags for the channel state,
|
886
|
+
'doutopts': digital output options (composed of cbDOUT_* flags),
|
887
|
+
'dinpopts': digital input options (composed of cbDINP_* flags),
|
888
|
+
'aoutopts': analog output options,
|
889
|
+
'eopchar': digital input capablities (given by cbDINP_* flags),
|
890
|
+
'ainpopts': analog input options (composed of cbAINP* flags),
|
891
|
+
'smpfilter': continuous-time pathway filter id,
|
892
|
+
'smpgroup': continuous-time pathway sample group,
|
893
|
+
'smpdispmin': continuous-time pathway display factor,
|
894
|
+
'smpdispmax': continuous-time pathway display factor,
|
895
|
+
'trigtype': trigger type (see cbDOUT_TRIGGER_*),
|
896
|
+
'trigchan': trigger channel,
|
897
|
+
'trigval': trigger value,
|
898
|
+
'lncrate': line noise cancellation filter adaptation rate,
|
899
|
+
'spkfilter': spike pathway filter id,
|
900
|
+
'spkdispmax': spike pathway display factor,
|
901
|
+
'lncdispmax': Line Noise pathway display factor,
|
902
|
+
'spkopts': spike processing options,
|
903
|
+
'spkthrlevel': spike threshold level,
|
904
|
+
'spkthrlimit': ,
|
905
|
+
'spkgroup': NTrodeGroup this electrode belongs to - 0 is single unit, non-0 indicates a multi-trode grouping,
|
906
|
+
'amplrejpos': Amplitude rejection positive value,
|
907
|
+
'amplrejneg': Amplitude rejection negative value,
|
908
|
+
'refelecchan': Software reference electrode channel,
|
909
|
+
"""
|
910
|
+
cdef cbSdkResult res
|
911
|
+
cdef cbPKT_CHANINFO cb_chaninfo
|
912
|
+
res = cbSdkGetChannelConfig(<uint32_t>instance, <uint16_t>channel, &cb_chaninfo)
|
913
|
+
handle_result(res)
|
914
|
+
if res != 0:
|
915
|
+
return {}
|
916
|
+
|
917
|
+
chaninfo = {
|
918
|
+
'time': cb_chaninfo.cbpkt_header.time,
|
919
|
+
'chid': cb_chaninfo.cbpkt_header.chid,
|
920
|
+
'type': cb_chaninfo.cbpkt_header.type, # cbPKTTYPE_AINP*
|
921
|
+
'dlen': cb_chaninfo.cbpkt_header.dlen, # cbPKT_DLENCHANINFO
|
922
|
+
'chan': cb_chaninfo.chan,
|
923
|
+
'proc': cb_chaninfo.proc,
|
924
|
+
'bank': cb_chaninfo.bank,
|
925
|
+
'term': cb_chaninfo.term,
|
926
|
+
'chancaps': cb_chaninfo.chancaps,
|
927
|
+
'doutcaps': cb_chaninfo.doutcaps,
|
928
|
+
'dinpcaps': cb_chaninfo.dinpcaps,
|
929
|
+
'aoutcaps': cb_chaninfo.aoutcaps,
|
930
|
+
'ainpcaps': cb_chaninfo.ainpcaps,
|
931
|
+
'spkcaps': cb_chaninfo.spkcaps,
|
932
|
+
'label': cb_chaninfo.label.decode(encoding),
|
933
|
+
'userflags': cb_chaninfo.userflags,
|
934
|
+
'doutopts': cb_chaninfo.doutopts,
|
935
|
+
'dinpopts': cb_chaninfo.dinpopts,
|
936
|
+
'moninst': cb_chaninfo.moninst,
|
937
|
+
'monchan': cb_chaninfo.monchan,
|
938
|
+
'outvalue': cb_chaninfo.outvalue,
|
939
|
+
'aoutopts': cb_chaninfo.aoutopts,
|
940
|
+
'eopchar': cb_chaninfo.eopchar,
|
941
|
+
'ainpopts': cb_chaninfo.ainpopts,
|
942
|
+
'smpfilter': cb_chaninfo.smpfilter,
|
943
|
+
'smpgroup': cb_chaninfo.smpgroup,
|
944
|
+
'smpdispmin': cb_chaninfo.smpdispmin,
|
945
|
+
'smpdispmax': cb_chaninfo.smpdispmax,
|
946
|
+
'trigtype': cb_chaninfo.trigtype,
|
947
|
+
'trigchan': cb_chaninfo.trigchan,
|
948
|
+
'lncrate': cb_chaninfo.lncrate,
|
949
|
+
'spkfilter': cb_chaninfo.spkfilter,
|
950
|
+
'spkdispmax': cb_chaninfo.spkdispmax,
|
951
|
+
'lncdispmax': cb_chaninfo.lncdispmax,
|
952
|
+
'spkopts': cb_chaninfo.spkopts,
|
953
|
+
'spkthrlevel': cb_chaninfo.spkthrlevel,
|
954
|
+
'spkthrlimit': cb_chaninfo.spkthrlimit,
|
955
|
+
'spkgroup': cb_chaninfo.spkgroup,
|
956
|
+
'amplrejpos': cb_chaninfo.amplrejpos,
|
957
|
+
'amplrejneg': cb_chaninfo.amplrejneg,
|
958
|
+
'refelecchan': cb_chaninfo.refelecchan
|
959
|
+
}
|
960
|
+
|
961
|
+
# TODO:
|
962
|
+
#cbSCALING physcalin # physical channel scaling information
|
963
|
+
#cbFILTDESC phyfiltin # physical channel filter definition
|
964
|
+
#cbSCALING physcalout # physical channel scaling information
|
965
|
+
#cbFILTDESC phyfiltout # physical channel filter definition
|
966
|
+
#int32_t position[4] # reserved for future position information
|
967
|
+
#cbSCALING scalin # user-defined scaling information for AINP
|
968
|
+
#cbSCALING scalout # user-defined scaling information for AOUT
|
969
|
+
#uint16_t moninst
|
970
|
+
#uint16_t monchan
|
971
|
+
#int32_t outvalue # output value
|
972
|
+
#uint16_t lowsamples # address of channel to monitor
|
973
|
+
#uint16_t highsamples # address of channel to monitor
|
974
|
+
#int32_t offset
|
975
|
+
#cbMANUALUNITMAPPING unitmapping[cbMAXUNITS+0] # manual unit mapping
|
976
|
+
#cbHOOP spkhoops[cbMAXUNITS+0][cbMAXHOOPS+0] # spike hoop sorting set
|
977
|
+
|
978
|
+
return chaninfo
|
979
|
+
|
980
|
+
|
981
|
+
def set_channel_config(int channel, chaninfo: dict[str, typing.Any] | None = None, int instance=0, encoding="utf-8") -> None:
|
982
|
+
""" Set channel configuration.
|
983
|
+
Inputs:
|
984
|
+
- channel: 1-based channel number
|
985
|
+
- chaninfo: A Python dict. See fields descriptions in get_channel_config. All fields are optional.
|
986
|
+
- instance: (optional) library instance number
|
987
|
+
- encoding: (optional) encoding for string fields, default = 'utf-8'
|
988
|
+
"""
|
989
|
+
cdef cbSdkResult res
|
990
|
+
cdef cbPKT_CHANINFO cb_chaninfo
|
991
|
+
res = cbSdkGetChannelConfig(<uint32_t>instance, <uint16_t>channel, &cb_chaninfo)
|
992
|
+
handle_result(res)
|
993
|
+
|
994
|
+
if "label" in chaninfo:
|
995
|
+
new_label = chaninfo["label"]
|
996
|
+
if not isinstance(new_label, bytes):
|
997
|
+
new_label = new_label.encode(encoding)
|
998
|
+
strncpy(cb_chaninfo.label, new_label, sizeof(new_label))
|
999
|
+
|
1000
|
+
if "smpgroup" in chaninfo:
|
1001
|
+
cb_chaninfo.smpgroup = chaninfo["smpgroup"]
|
1002
|
+
|
1003
|
+
if "spkthrlevel" in chaninfo:
|
1004
|
+
cb_chaninfo.spkthrlevel = chaninfo["spkthrlevel"]
|
1005
|
+
|
1006
|
+
if "ainpopts" in chaninfo:
|
1007
|
+
cb_chaninfo.ainpopts = chaninfo["ainpopts"]
|
1008
|
+
|
1009
|
+
res = cbSdkSetChannelConfig(<uint32_t>instance, <uint16_t>channel, &cb_chaninfo)
|
1010
|
+
handle_result(res)
|
1011
|
+
|
1012
|
+
|
1013
|
+
def get_sample_group(int group_ix, int instance=0, encoding="utf-8", int proc=1) -> list[dict[str, typing.Any]]:
|
1014
|
+
"""
|
1015
|
+
Get the channel infos for all channels in a sample group.
|
1016
|
+
Inputs:
|
1017
|
+
- group_ix: sample group index (1-6)
|
1018
|
+
- instance: (optional) library instance number
|
1019
|
+
- encoding: (optional) encoding for string fields, default = 'utf-8'
|
1020
|
+
Outputs:
|
1021
|
+
- channels_info: A list of dictionaries, one per channel in the group. Each dictionary has the following fields:
|
1022
|
+
'chid': 0x8000,
|
1023
|
+
'chan': actual channel id of the channel being configured,
|
1024
|
+
'proc': the address of the processor on which the channel resides,
|
1025
|
+
'bank': the address of the bank on which the channel resides,
|
1026
|
+
'term': the terminal number of the channel within its bank,
|
1027
|
+
'gain': the analog input gain (calculated from physical scaling),
|
1028
|
+
'label': Label of the channel (null terminated if <16 characters),
|
1029
|
+
'unit': physical unit of the channel,
|
1030
|
+
"""
|
1031
|
+
cdef cbSdkResult res
|
1032
|
+
cdef uint32_t nChansInGroup
|
1033
|
+
cdef uint16_t pGroupList[cbNUM_ANALOG_CHANS+0]
|
1034
|
+
|
1035
|
+
res = cbSdkGetSampleGroupList(<uint32_t>instance, <uint32_t>proc, <uint32_t>group_ix, &nChansInGroup, pGroupList)
|
1036
|
+
if res:
|
1037
|
+
handle_result(res)
|
1038
|
+
return []
|
1039
|
+
|
1040
|
+
cdef cbPKT_CHANINFO chanInfo
|
1041
|
+
channels_info = []
|
1042
|
+
for chan_ix in range(nChansInGroup):
|
1043
|
+
chan_info = {}
|
1044
|
+
res = cbSdkGetChannelConfig(<uint32_t>instance, pGroupList[chan_ix], &chanInfo)
|
1045
|
+
handle_result(<cbSdkResult>res)
|
1046
|
+
ana_range = chanInfo.physcalin.anamax - chanInfo.physcalin.anamin
|
1047
|
+
dig_range = chanInfo.physcalin.digmax - chanInfo.physcalin.digmin
|
1048
|
+
chan_info["chan"] = chanInfo.chan
|
1049
|
+
chan_info["proc"] = chanInfo.proc
|
1050
|
+
chan_info["bank"] = chanInfo.bank
|
1051
|
+
chan_info["term"] = chanInfo.term
|
1052
|
+
chan_info["gain"] = ana_range / dig_range
|
1053
|
+
chan_info["label"] = chanInfo.label.decode(encoding)
|
1054
|
+
chan_info["unit"] = chanInfo.physcalin.anaunit.decode(encoding)
|
1055
|
+
channels_info.append(chan_info)
|
1056
|
+
|
1057
|
+
return channels_info
|
1058
|
+
|
1059
|
+
|
1060
|
+
def set_comment(comment_string, rgba_tuple=(0, 0, 0, 255), int instance=0):
|
1061
|
+
"""rgba_tuple is actually t_bgr: transparency, blue, green, red. Default: no-transparency & red."""
|
1062
|
+
cdef cbSdkResult res
|
1063
|
+
cdef uint32_t t_bgr = (rgba_tuple[0] << 24) + (rgba_tuple[1] << 16) + (rgba_tuple[2] << 8) + rgba_tuple[3]
|
1064
|
+
cdef uint8_t charset = 0 # Character set (0 - ANSI, 1 - UTF16, 255 - NeuroMotive ANSI)
|
1065
|
+
cdef bytes py_bytes = comment_string.encode()
|
1066
|
+
cdef const char* comment = py_bytes
|
1067
|
+
res = cbSdkSetComment(<uint32_t> instance, t_bgr, charset, comment)
|
1068
|
+
|
1069
|
+
|
1070
|
+
def get_sys_config(int instance=0):
|
1071
|
+
cdef cbSdkResult res
|
1072
|
+
cdef uint32_t spklength
|
1073
|
+
cdef uint32_t spkpretrig
|
1074
|
+
cdef uint32_t sysfreq
|
1075
|
+
res = cbSdkGetSysConfig(<uint32_t> instance, &spklength, &spkpretrig, &sysfreq)
|
1076
|
+
handle_result(res)
|
1077
|
+
return {'spklength': spklength, 'spkpretrig': spkpretrig, 'sysfreq': sysfreq}
|
1078
|
+
|
1079
|
+
|
1080
|
+
def set_spike_config(int spklength=48, int spkpretrig=10, int instance=0):
|
1081
|
+
cdef cbSdkResult res
|
1082
|
+
res = cbSdkSetSpikeConfig(<uint32_t> instance, <uint32_t> spklength, <uint32_t> spkpretrig)
|
1083
|
+
handle_result(res)
|
1084
|
+
|
1085
|
+
|
1086
|
+
cdef extern from "numpy/arrayobject.h":
|
1087
|
+
void PyArray_ENABLEFLAGS(cnp.ndarray arr, int flags)
|
1088
|
+
|
1089
|
+
|
1090
|
+
cdef class SpikeCache:
|
1091
|
+
cdef readonly int inst, chan, n_samples, n_pretrig
|
1092
|
+
cdef cbSPKCACHE *p_cache
|
1093
|
+
cdef int last_valid
|
1094
|
+
# cache
|
1095
|
+
# .chid: ID of the Channel
|
1096
|
+
# .pktcnt: number of packets that can be saved
|
1097
|
+
# .pktsize: size of an individual packet
|
1098
|
+
# .head: Where (0 based index) in the circular buffer to place the NEXT packet
|
1099
|
+
# .valid: How many packets have come in since the last configuration
|
1100
|
+
# .spkpkt: Circular buffer of the cached spikes
|
1101
|
+
|
1102
|
+
def __cinit__(self, int channel=1, int instance=0):
|
1103
|
+
self.inst = instance
|
1104
|
+
self.chan = channel
|
1105
|
+
cdef cbSPKCACHE ignoreme # Just so self.p_cache is not NULL... but this won't be used by anything
|
1106
|
+
self.p_cache = &ignoreme # because cbSdkGetSpkCache changes what self.p_cache is pointing to.
|
1107
|
+
self.reset_cache()
|
1108
|
+
sys_config_dict = get_sys_config(instance)
|
1109
|
+
self.n_samples = sys_config_dict['spklength']
|
1110
|
+
self.n_pretrig = sys_config_dict['spkpretrig']
|
1111
|
+
|
1112
|
+
def reset_cache(self):
|
1113
|
+
cdef cbSdkResult res = cbSdkGetSpkCache(self.inst, self.chan, &self.p_cache)
|
1114
|
+
handle_result(res)
|
1115
|
+
self.last_valid = self.p_cache.valid
|
1116
|
+
|
1117
|
+
# This function needs to be FAST!
|
1118
|
+
@cython.boundscheck(False) # turn off bounds-checking for entire function
|
1119
|
+
def get_new_waveforms(self):
|
1120
|
+
# cdef everything!
|
1121
|
+
cdef int new_valid = self.p_cache.valid
|
1122
|
+
cdef int new_head = self.p_cache.head
|
1123
|
+
cdef int n_new = min(max(new_valid - self.last_valid, 0), 400)
|
1124
|
+
cdef cnp.ndarray[cnp.int16_t, ndim=2, mode="c"] np_waveforms = np.empty((n_new, self.n_samples), dtype=np.int16)
|
1125
|
+
cdef cnp.ndarray[cnp.uint16_t, ndim=1] np_unit_ids = np.empty(n_new, dtype=np.uint16)
|
1126
|
+
cdef int wf_ix, pkt_ix, samp_ix
|
1127
|
+
for wf_ix in range(n_new):
|
1128
|
+
pkt_ix = (new_head - 2 - n_new + wf_ix) % 400
|
1129
|
+
np_unit_ids[wf_ix] = self.p_cache.spkpkt[pkt_ix].cbpkt_header.type
|
1130
|
+
# Instead of per-sample copy, we could copy the pointer for the whole wave to the buffer of a 1-d np array,
|
1131
|
+
# then use memory view copying from 1-d array into our 2d matrix. But below is pure-C so should be fast too.
|
1132
|
+
for samp_ix in range(self.n_samples):
|
1133
|
+
np_waveforms[wf_ix, samp_ix] = self.p_cache.spkpkt[pkt_ix].wave[samp_ix]
|
1134
|
+
#unit_ids_out = [<int>unit_ids[wf_ix] for wf_ix in range(n_new)]
|
1135
|
+
PyArray_ENABLEFLAGS(np_waveforms, NPY_ARRAY_OWNDATA)
|
1136
|
+
self.last_valid = new_valid
|
1137
|
+
return np_waveforms, np_unit_ids
|
1138
|
+
|
1139
|
+
|
1140
|
+
cdef cbSdkResult handle_result(cbSdkResult res):
|
1141
|
+
if res == CBSDKRESULT_WARNCLOSED:
|
1142
|
+
print("Library is already closed.")
|
1143
|
+
if res < 0:
|
1144
|
+
errtext = "No associated error string. See cbsdk.h"
|
1145
|
+
if res == CBSDKRESULT_ERROFFLINE:
|
1146
|
+
errtext = "Instrument is offline."
|
1147
|
+
elif res == CBSDKRESULT_CLOSED:
|
1148
|
+
errtext = "Interface is closed; cannot do this operation."
|
1149
|
+
elif res == CBSDKRESULT_ERRCONFIG:
|
1150
|
+
errtext = "Trying to run an unconfigured method."
|
1151
|
+
elif res == CBSDKRESULT_NULLPTR:
|
1152
|
+
errtext = "Null pointer."
|
1153
|
+
elif res == CBSDKRESULT_INVALIDCHANNEL:
|
1154
|
+
errtext = "Invalid channel number."
|
1155
|
+
|
1156
|
+
raise RuntimeError(("%d, " + errtext) % res)
|
1157
|
+
return res
|