Skip to content

Commit cc6ae8d

Browse files
committed
add reading of sparse event based recordings
Code extracted from 3Brain documentation https://gin.g-node.org/NeuralEnsemble/ephy_testing_data/src/master/biocam/documentation_brw_4.x_bxr_3.x_bcmp_1.x_in_brainwave_5.x_v1.1.3.pdf with kind permission by Inna Forsiuk <[email protected]>
1 parent f401627 commit cc6ae8d

File tree

1 file changed

+126
-7
lines changed

1 file changed

+126
-7
lines changed

neo/rawio/biocamrawio.py

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,25 +197,32 @@ def open_biocam_file_header(filename):
197197
min_digital = experiment_settings["ValueConverter"]["MinDigitalValue"]
198198
scale_factor = experiment_settings["ValueConverter"]["ScaleFactor"]
199199
sampling_rate = experiment_settings["TimeConverter"]["FrameRate"]
200+
num_frames = rf['TOC'][-1,-1]
200201

202+
wellID = None
201203
for key in rf:
202204
if key[:5] == "Well_":
205+
wellID = key
203206
num_channels = len(rf[key]["StoredChIdxs"])
204-
if len(rf[key]["Raw"]) % num_channels:
205-
raise RuntimeError(f"Length of raw data array is not multiple of channel number in {key}")
206-
num_frames = len(rf[key]["Raw"]) // num_channels
207+
if "Raw" in rf[key]:
208+
if len(rf[key]["Raw"]) % num_channels:
209+
raise RuntimeError(f"Length of raw data array is not multiple of channel number in {key}")
210+
if num_frames != len(rf[key]["Raw"]) // num_channels:
211+
raise RuntimeError(f"Estimated number of frames from TOC does not match length of raw data array in {key}")
207212
break
208-
try:
209-
num_channels_x = num_channels_y = int(np.sqrt(num_channels))
210-
except NameError:
213+
if not wellID:
211214
raise RuntimeError("No Well found in the file")
215+
num_channels_x = num_channels_y = int(np.sqrt(num_channels))
212216
if num_channels_x * num_channels_y != num_channels:
213217
raise RuntimeError(f"Cannot determine structure of the MEA plate with {num_channels} channels")
214218
channels = 1 + np.concatenate(np.transpose(np.meshgrid(range(num_channels_x), range(num_channels_y))))
215219

216220
gain = scale_factor * (max_uv - min_uv) / (max_digital - min_digital)
217221
offset = min_uv
218-
read_function = readHDF5t_brw4
222+
if "Raw" in rf[wellID]:
223+
read_function = readHDF5t_brw4
224+
elif "EventsBasedSparseRaw" in rf[wellID]:
225+
read_function = readHDF5t_brw4_sparse
219226

220227
return dict(
221228
file_handle=rf,
@@ -249,3 +256,115 @@ def readHDF5t_brw4(rf, t0, t1, nch):
249256
for key in rf:
250257
if key[:5] == "Well_":
251258
return rf[key]["Raw"][nch * t0 : nch * t1].reshape((t1 - t0, nch), order="C")
259+
260+
261+
def readHDF5t_brw4_sparse(rf, t0, t1, nch):
262+
useSyntheticNoise = True
263+
noiseStdDev = None
264+
startFrame = t0
265+
numFrames = t1 - t0
266+
for key in rf:
267+
if key[:5] == "Well_":
268+
wellID = key
269+
break
270+
# initialize an empty (fill with zeros) data collection
271+
data = np.zeros((nch, numFrames), dtype=np.int16)
272+
# fill the data collection with Gaussian noise if requested
273+
if useSyntheticNoise:
274+
generateSyntheticNoise(rf, data, wellID, startFrame, numFrames, stdDev=noiseStdDev)
275+
# fill the data collection with the decoded event based sparse raw data
276+
decodeEventBasedRawData(rf, data, wellID, startFrame, numFrames)
277+
return data.T
278+
279+
280+
def decodeEventBasedRawData(rf, data, wellID, startFrame, numFrames):
281+
# Source: Documentation by 3Brain
282+
# https://gin.g-node.org/NeuralEnsemble/ephy_testing_data/src/master/biocam/documentation_brw_4.x_bxr_3.x_bcmp_1.x_in_brainwave_5.x_v1.1.3.pdf
283+
# collect the TOCs
284+
toc = np.array(rf["TOC"])
285+
eventsToc = np.array(rf[wellID]["EventsBasedSparseRawTOC"])
286+
# from the given start position and duration in frames, localize the corresponding event positions
287+
# using the TOC
288+
tocStartIdx = np.searchsorted(toc[:, 1], startFrame)
289+
tocEndIdx = min(
290+
np.searchsorted(toc[:, 1], startFrame + numFrames, side="right") + 1,
291+
len(toc) - 1)
292+
eventsStartPosition = eventsToc[tocStartIdx]
293+
eventsEndPosition = eventsToc[tocEndIdx]
294+
# decode all data for the given well ID and time interval
295+
binaryData = rf[wellID]["EventsBasedSparseRaw"][eventsStartPosition:eventsEndPosition]
296+
binaryDataLength = len(binaryData)
297+
pos = 0
298+
while pos < binaryDataLength:
299+
chIdx = int.from_bytes(binaryData[pos:pos + 4], byteorder="little", signed=True)
300+
pos += 4
301+
chDataLength = int.from_bytes(binaryData[pos:pos + 4], byteorder="little", signed=True)
302+
pos += 4
303+
chDataPos = pos
304+
while pos < chDataPos + chDataLength:
305+
fromInclusive = int.from_bytes(binaryData[pos:pos + 8], byteorder="little", signed=True)
306+
pos += 8
307+
toExclusive = int.from_bytes(binaryData[pos:pos + 8], byteorder="little", signed=True)
308+
pos += 8
309+
rangeDataPos = pos
310+
for j in range(fromInclusive, toExclusive):
311+
if j >= startFrame + numFrames:
312+
break
313+
if j >= startFrame:
314+
data[chIdx][j - startFrame] = int.from_bytes(
315+
binaryData[rangeDataPos:rangeDataPos + 2], byteorder="little", signed=True)
316+
rangeDataPos += 2
317+
pos += (toExclusive - fromInclusive) * 2
318+
319+
320+
def generateSyntheticNoise(rf, data, wellID, startFrame, numFrames, stdDev=None):
321+
# Source: Documentation by 3Brain
322+
# https://gin.g-node.org/NeuralEnsemble/ephy_testing_data/src/master/biocam/documentation_brw_4.x_bxr_3.x_bcmp_1.x_in_brainwave_5.x_v1.1.3.pdf
323+
# collect the TOCs
324+
toc = np.array(rf["TOC"])
325+
noiseToc = np.array(rf[wellID]["NoiseTOC"])
326+
# from the given start position in frames, localize the corresponding noise positions
327+
# using the TOC
328+
tocStartIdx = np.searchsorted(toc[:, 1], startFrame)
329+
noiseStartPosition = noiseToc[tocStartIdx]
330+
noiseEndPosition = noiseStartPosition
331+
for i in range(tocStartIdx + 1, len(noiseToc)):
332+
nextPosition = noiseToc[i]
333+
if nextPosition > noiseStartPosition:
334+
noiseEndPosition = nextPosition
335+
break
336+
if noiseEndPosition == noiseStartPosition:
337+
for i in range(tocStartIdx - 1, 0, -1):
338+
previousPosition = noiseToc[i]
339+
if previousPosition < noiseStartPosition:
340+
noiseEndPosition = noiseStartPosition
341+
noiseStartPosition = previousPosition
342+
break
343+
# obtain the noise info at the start position
344+
noiseChIdxs = rf[wellID]["NoiseChIdxs"][noiseStartPosition:noiseEndPosition]
345+
noiseMean = rf[wellID]["NoiseMean"][noiseStartPosition:noiseEndPosition]
346+
if stdDev is None:
347+
noiseStdDev = rf[wellID]["NoiseStdDev"][noiseStartPosition:noiseEndPosition]
348+
else:
349+
noiseStdDev = np.repeat(stdDev, noiseEndPosition - noiseStartPosition)
350+
noiseLength = noiseEndPosition - noiseStartPosition
351+
noiseInfo = {}
352+
meanCollection = []
353+
stdDevCollection = []
354+
for i in range(1, noiseLength):
355+
noiseInfo[noiseChIdxs[i]] = [noiseMean[i], noiseStdDev[i]]
356+
meanCollection.append(noiseMean[i])
357+
stdDevCollection.append(noiseStdDev[i])
358+
# calculate the median mean and standard deviation of all channels to be used for
359+
# invalid channels
360+
dataMean = np.median(meanCollection)
361+
dataStdDev = np.median(stdDevCollection)
362+
# fill with Gaussian noise
363+
for chIdx in range(len(data)):
364+
if chIdx in noiseInfo:
365+
data[chIdx] = np.array(np.random.normal(noiseInfo[chIdx][0], noiseInfo[chIdx][1],
366+
numFrames), dtype=np.int16)
367+
else:
368+
data[chIdx] = np.array(np.random.normal(dataMean, dataStdDev, numFrames),
369+
dtype=np.int16)
370+

0 commit comments

Comments
 (0)