Skip to content

Commit 9869d8a

Browse files
committed
Merge branch 'hotfix/2.0.7' into develop
2 parents a0a4296 + a0282c8 commit 9869d8a

File tree

12 files changed

+259
-53
lines changed

12 files changed

+259
-53
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[flake8]
2-
max-line-length = 99
2+
max-line-length = 130
33
ignore = W504, W503, E266
44
exclude =
55
.git,

brainbox/plot.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ def peri_event_time_histogram(
618618

619619

620620
def driftmap(ts, feat, ax=None, plot_style='bincount',
621-
t_bin=0.01, d_bin=20, weights=None, **kwargs):
621+
t_bin=0.01, d_bin=20, weights=None, vmax=None, **kwargs):
622622
"""
623623
Plots the values of a spike feature array (y-axis) over time (x-axis).
624624
Two arguments can be given for the plot_style of the drift map:
@@ -636,6 +636,7 @@ def driftmap(ts, feat, ax=None, plot_style='bincount',
636636
t_bin: time bin used when plot_style='bincount'
637637
d_bin: depth bin used when plot_style='bincount'
638638
plot_style: 'scatter', 'bincount'
639+
**kwargs: matplotlib.imshow arguments
639640
640641
Returns
641642
-------
@@ -674,7 +675,7 @@ def driftmap(ts, feat, ax=None, plot_style='bincount',
674675
R, times, depths = bincount2D(
675676
ts[iok], feat[iok], t_bin, d_bin, weights=weights)
676677
# plot raster map
677-
ax.imshow(R, aspect='auto', cmap='binary', vmin=0, vmax=np.std(R) * 4,
678+
ax.imshow(R, aspect='auto', cmap='binary', vmin=0, vmax=vmax or np.std(R) * 4,
678679
extent=np.r_[times[[0, -1]], depths[[0, -1]]], origin='lower', **kwargs)
679680
ax.set_xlabel('time (secs)')
680681
ax.set_ylabel('depth (um)')

ibllib/io/extractors/camera.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ def _extract(self, sync=None, chmap=None, video_path=None,
9191
"""
9292
fpga_times = extract_camera_sync(sync=sync, chmap=chmap)
9393
count, (*_, gpio) = raw.load_embedded_frame_data(self.session_path, self.label)
94+
raw_ts = fpga_times[self.label]
95+
96+
if video_path is None:
97+
filename = f'_iblrig_{self.label}Camera.raw.mp4'
98+
video_path = self.session_path.joinpath('raw_video_data', filename)
99+
# Permit the video path to be the length for development and debugging purposes
100+
length = (video_path if isinstance(video_path, int) else get_video_length(video_path))
101+
_logger.debug(f'Number of video frames = {length}')
94102

95103
if gpio is not None and gpio['indices'].size > 1:
96104
_logger.info('Aligning to audio TTLs')
@@ -111,19 +119,10 @@ def _extract(self, sync=None, chmap=None, video_path=None,
111119
right at the end of the video. We therefore simply shorten the arrays to match
112120
the length of the video.
113121
"""
114-
if video_path is None:
115-
filename = f'_iblrig_{self.label}Camera.raw.mp4'
116-
video_path = self.session_path.joinpath('raw_video_data', filename)
117-
# Permit the video path to be the length for development and debugging purposes
118-
length = (video_path
119-
if isinstance(video_path, int)
120-
else get_video_length(video_path))
121-
_logger.debug(f'Number of video frames = {length}')
122122
if count.size > length:
123123
count = count[:length]
124124
else:
125125
assert length == count.size, 'fewer counts than frames'
126-
raw_ts = fpga_times[self.label]
127126
return align_with_audio(raw_ts, audio, gpio, count,
128127
display=display,
129128
extrapolate_missing=extrapolate_missing)
@@ -132,7 +131,11 @@ def _extract(self, sync=None, chmap=None, video_path=None,
132131

133132
# If you reach here extracting using audio TTLs was not possible
134133
_logger.warning('Alignment by wheel data not yet implemented')
135-
return fpga_times[self.label]
134+
if length < raw_ts.size:
135+
df = raw_ts.size - length
136+
_logger.info(f'Discarding first {df} pulses')
137+
raw_ts = raw_ts[df:]
138+
return raw_ts
136139

137140

138141
class CameraTimestampsBpod(BaseBpodTrialsExtractor):
@@ -219,6 +222,8 @@ def _extract(self, video_path=None, display=False, extrapolate_missing=True):
219222
raw_ts = np.r_[raw_ts, to_app] # Append the missing times
220223
elif n_missing < 0:
221224
_logger.warning(f'{abs(n_missing)} fewer frames than Bpod timestamps')
225+
_logger.info(f'Discarding first {abs(n_missing)} pulses')
226+
raw_ts = raw_ts[abs(n_missing):]
222227

223228
return raw_ts
224229

ibllib/io/extractors/ephys_fpga.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -245,28 +245,34 @@ def _rotary_encoder_positions_from_fronts(ta, pa, tb, pb, ticks=WHEEL_TICKS, rad
245245
return t, p
246246

247247

248-
def _assign_events_audio(audio_t, audio_polarities, return_indices=False):
248+
def _assign_events_audio(audio_t, audio_polarities, return_indices=False, display=False):
249249
"""
250250
From detected fronts on the audio sync traces, outputs the synchronisation events
251251
related to tone in
252252
253253
:param audio_t: numpy vector containing times of fronts
254254
:param audio_fronts: numpy vector containing polarity of fronts (1 rise, -1 fall)
255255
:param return_indices (False): returns indices of tones
256+
:param display (False): for debug mode, displays the raw fronts overlaid with detections
256257
:return: numpy arrays t_ready_tone_in, t_error_tone_in
257258
:return: numpy arrays ind_ready_tone_in, ind_error_tone_in if return_indices=True
258259
"""
259260
# make sure that there are no 2 consecutive fall or consecutive rise events
260261
assert(np.all(np.abs(np.diff(audio_polarities)) == 2))
261262
# take only even time differences: ie. from rising to falling fronts
262-
i0 = 0 if audio_polarities[0] == 1 else 1
263-
dt = np.diff(audio_t)[i0::2]
263+
dt = np.diff(audio_t)
264264
# detect ready tone by length below 110 ms
265-
i_ready_tone_in = np.r_[np.where(dt <= 0.11)[0] * 2]
265+
i_ready_tone_in = np.where(np.logical_and(dt <= 0.11, audio_polarities[:-1] == 1))[0]
266266
t_ready_tone_in = audio_t[i_ready_tone_in]
267-
# error tones are events lasting from 400ms to 600ms
268-
i_error_tone_in = np.where(np.logical_and(0.4 < dt, dt < 1.2))[0] * 2
267+
# error tones are events lasting from 400ms to 1200ms
268+
i_error_tone_in = np.where(np.logical_and(np.logical_and(0.4 < dt, dt < 1.2), audio_polarities[:-1] == 1))[0]
269269
t_error_tone_in = audio_t[i_error_tone_in]
270+
if display: # pragma: no cover
271+
from ibllib.plots import squares, vertical_lines
272+
squares(audio_t, audio_polarities, yrange=[-1, 1],)
273+
vertical_lines(t_ready_tone_in, ymin=-.8, ymax=.8)
274+
vertical_lines(t_error_tone_in, ymin=-.8, ymax=.8)
275+
270276
if return_indices:
271277
return t_ready_tone_in, t_error_tone_in, i_ready_tone_in, i_error_tone_in
272278
else:
@@ -280,7 +286,6 @@ def _assign_events_to_trial(t_trial_start, t_event, take='last'):
280286
Trials without an event
281287
result in nan value in output time vector.
282288
The output has a consistent size with t_trial_start and ready to output to alf.
283-
284289
:param t_trial_start: numpy vector of trial start times
285290
:param t_event: numpy vector of event times to assign to trials
286291
:param take: 'last' or 'first' (optional, default 'last'): index to take in case of duplicates
@@ -325,6 +330,37 @@ def get_sync_fronts(sync, channel_nb, tmin=None, tmax=None):
325330
'polarities': sync['polarities'][selection]})
326331

327332

333+
def _clean_audio(audio, display=False):
334+
"""
335+
one guy wired the 150 Hz camera output onto the soundcard. The effect is to get 150 Hz periodic
336+
square pulses, 2ms up and 4.666 ms down. When this happens we remove all of the intermediate
337+
pulses to repair the audio trace
338+
Here is some helper code
339+
dd = np.diff(audio['times'])
340+
1 / np.median(dd[::2]) # 2ms up
341+
1 / np.median(dd[1::2]) # 4.666 ms down
342+
1 / (np.median(dd[::2]) + np.median(dd[1::2])) # both sum to 150 Hx
343+
This only runs on sessions when the bug is detected and leaves others untouched
344+
"""
345+
DISCARD_THRESHOLD = 0.01
346+
average_150_hz = np.mean(1 / np.diff(audio['times'][audio['polarities'] == 1]) > 140)
347+
naudio = audio['times'].size
348+
if average_150_hz > 0.7 and naudio > 100:
349+
_logger.warning("Soundcard signal on FPGA seems to have been mixed with 150Hz camera")
350+
keep_ind = np.r_[np.diff(audio['times']) > DISCARD_THRESHOLD, False]
351+
keep_ind = np.logical_and(keep_ind, audio['polarities'] == -1)
352+
keep_ind = np.where(keep_ind)[0]
353+
keep_ind = np.sort(np.r_[0, keep_ind, keep_ind + 1, naudio - 1])
354+
355+
if display: # pragma: no cover
356+
from ibllib.plots import squares
357+
squares(audio['times'], audio['polarities'], ax=None, yrange=[-1, 1])
358+
squares(audio['times'][keep_ind], audio['polarities'][keep_ind], yrange=[-1, 1])
359+
audio = {'times': audio['times'][keep_ind],
360+
'polarities': audio['polarities'][keep_ind]}
361+
return audio
362+
363+
328364
def _clean_frame2ttl(frame2ttl, display=False):
329365
"""
330366
Frame 2ttl calibration can be unstable and the fronts may be flickering at an unrealistic
@@ -387,6 +423,7 @@ def extract_behaviour_sync(sync, chmap=None, display=False, bpod_trials=None):
387423
frame2ttl = get_sync_fronts(sync, chmap['frame2ttl'])
388424
frame2ttl = _clean_frame2ttl(frame2ttl)
389425
audio = get_sync_fronts(sync, chmap['audio'])
426+
audio = _clean_audio(audio)
390427
# extract events from the fronts for each trace
391428
t_trial_start, t_valve_open, t_iti_in = _assign_events_bpod(bpod['times'], bpod['polarities'])
392429
# one issue is that sometimes bpod pulses may not have been detected, in this case

ibllib/io/extractors/ephys_passive.py

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ def _load_passive_session_fixtures(session_path: str) -> dict:
7979
return fixture
8080

8181

82+
def _load_task_protocol(session_path: str) -> str:
83+
"""Find the IBL rig version used for the session
84+
85+
:param session_path: the path to a session
86+
:type session_path: str
87+
:return: ibl rig task protocol version
88+
:rtype: str
89+
"""
90+
settings = rawio.load_settings(session_path)
91+
ses_ver = settings["IBLRIG_VERSION_TAG"]
92+
93+
return ses_ver
94+
95+
8296
def _load_passive_stim_meta() -> dict:
8397
"""load_passive_stim_meta Loads the passive protocol metadata
8498
@@ -321,38 +335,72 @@ def _extract_passiveValve_intervals(bpod: dict) -> np.array:
321335

322336
# check all values are within bpod tolerance of 100µs
323337
assert np.allclose(
324-
valveOff_times - valveOn_times, valveOff_times[0] - valveOn_times[0], atol=0.0001
338+
valveOff_times - valveOn_times, valveOff_times[1] - valveOn_times[1], atol=0.0001
325339
), "Some valve outputs are longer or shorter than others"
326340

327341
return np.array([(x, y) for x, y in zip(valveOn_times, valveOff_times)])
328342

329343

330-
def _extract_passiveAudio_intervals(audio: dict) -> Tuple[np.array, np.array]:
331-
# Get Tone and Noise cue intervals
344+
def _extract_passiveAudio_intervals(audio: dict, rig_version: str) -> Tuple[np.array, np.array]:
345+
346+
# make an exception for task version = 6.2.5 where things are strange but data is recoverable
347+
if rig_version == '6.2.5':
348+
# Get all sound onsets and offsets
349+
soundOn_times = audio["times"][audio["polarities"] > 0]
350+
soundOff_times = audio["times"][audio["polarities"] < 0]
332351

333-
# Get all sound onsets and offsets
334-
soundOn_times = audio["times"][audio["polarities"] > 0]
335-
soundOff_times = audio["times"][audio["polarities"] < 0]
336-
# Check they are the correct number
337-
assert len(soundOn_times) == NTONES + NNOISES, "Wrong number of sound ONSETS"
338-
assert len(soundOff_times) == NTONES + NNOISES, "Wrong number of sound OFFSETS"
352+
# Have a couple that are wayyy too long!
353+
time_threshold = 10
354+
diff = soundOff_times - soundOn_times
355+
stupid = np.where(diff > time_threshold)[0]
356+
NREMOVE = len(stupid)
357+
not_stupid = np.where(diff < time_threshold)[0]
339358

340-
diff = soundOff_times - soundOn_times
341-
# Tone is ~100ms so check if diff < 0.3
342-
toneOn_times = soundOn_times[diff <= 0.3]
343-
toneOff_times = soundOff_times[diff <= 0.3]
344-
# Noise is ~500ms so check if diff > 0.3
345-
noiseOn_times = soundOn_times[diff > 0.3]
346-
noiseOff_times = soundOff_times[diff > 0.3]
359+
assert len(soundOn_times) == NTONES + NNOISES - NREMOVE, "Wrong number of sound ONSETS"
360+
assert len(soundOff_times) == NTONES + NNOISES - NREMOVE, "Wrong number of sound OFFSETS"
347361

348-
assert len(toneOn_times) == NTONES
349-
assert len(toneOff_times) == NTONES
350-
assert len(noiseOn_times) == NNOISES
351-
assert len(noiseOff_times) == NNOISES
362+
soundOn_times = soundOn_times[not_stupid]
363+
soundOff_times = soundOff_times[not_stupid]
352364

353-
# Fixed delays from soundcard ~500µs
354-
np.allclose(toneOff_times - toneOn_times, 0.1, atol=0.0006)
355-
np.allclose(noiseOff_times - noiseOn_times, 0.5, atol=0.0006)
365+
diff = soundOff_times - soundOn_times
366+
# Tone is ~100ms so check if diff < 0.3
367+
toneOn_times = soundOn_times[diff <= 0.3]
368+
toneOff_times = soundOff_times[diff <= 0.3]
369+
# Noise is ~500ms so check if diff > 0.3
370+
noiseOn_times = soundOn_times[diff > 0.3]
371+
noiseOff_times = soundOff_times[diff > 0.3]
372+
373+
# append with nans
374+
toneOn_times = np.r_[toneOn_times, np.full((NTONES - len(toneOn_times)), np.NAN)]
375+
toneOff_times = np.r_[toneOff_times, np.full((NTONES - len(toneOff_times)), np.NAN)]
376+
noiseOn_times = np.r_[noiseOn_times, np.full((NNOISES - len(noiseOn_times)), np.NAN)]
377+
noiseOff_times = np.r_[noiseOff_times, np.full((NNOISES - len(noiseOff_times)), np.NAN)]
378+
379+
else:
380+
# Get all sound onsets and offsets
381+
soundOn_times = audio["times"][audio["polarities"] > 0]
382+
soundOff_times = audio["times"][audio["polarities"] < 0]
383+
384+
# Check they are the correct number
385+
assert len(soundOn_times) == NTONES + NNOISES, "Wrong number of sound ONSETS"
386+
assert len(soundOff_times) == NTONES + NNOISES, "Wrong number of sound OFFSETS"
387+
388+
diff = soundOff_times - soundOn_times
389+
# Tone is ~100ms so check if diff < 0.3
390+
toneOn_times = soundOn_times[diff <= 0.3]
391+
toneOff_times = soundOff_times[diff <= 0.3]
392+
# Noise is ~500ms so check if diff > 0.3
393+
noiseOn_times = soundOn_times[diff > 0.3]
394+
noiseOff_times = soundOff_times[diff > 0.3]
395+
396+
assert len(toneOn_times) == NTONES
397+
assert len(toneOff_times) == NTONES
398+
assert len(noiseOn_times) == NNOISES
399+
assert len(noiseOff_times) == NNOISES
400+
401+
# Fixed delays from soundcard ~500µs
402+
np.allclose(toneOff_times - toneOn_times, 0.1, atol=0.0006)
403+
np.allclose(noiseOff_times - noiseOn_times, 0.5, atol=0.0006)
356404

357405
passiveTone_intervals = np.append(
358406
toneOn_times.reshape((len(toneOn_times), 1)),
@@ -444,8 +492,10 @@ def extract_task_replay(
444492
bpod = ephys_fpga.get_sync_fronts(sync, sync_map["bpod"], tmin=treplay[0])
445493
passiveValve_intervals = _extract_passiveValve_intervals(bpod)
446494

495+
task_version = _load_task_protocol(session_path)
447496
audio = ephys_fpga.get_sync_fronts(sync, sync_map["audio"], tmin=treplay[0])
448-
passiveTone_intervals, passiveNoise_intervals = _extract_passiveAudio_intervals(audio)
497+
passiveTone_intervals, passiveNoise_intervals = _extract_passiveAudio_intervals(audio,
498+
task_version)
449499

450500
passiveStims_df = np.concatenate(
451501
[passiveValve_intervals, passiveTone_intervals, passiveNoise_intervals], axis=1
@@ -493,8 +543,10 @@ def extract_replay_debug(
493543
passiveValve_intervals = _extract_passiveValve_intervals(bpod)
494544
plot_valve_times(passiveValve_intervals, ax=ax)
495545

546+
task_version = _load_task_protocol(session_path)
496547
audio = ephys_fpga.get_sync_fronts(sync, sync_map["audio"], tmin=treplay[0])
497-
passiveTone_intervals, passiveNoise_intervals = _extract_passiveAudio_intervals(audio)
548+
passiveTone_intervals, passiveNoise_intervals = _extract_passiveAudio_intervals(audio,
549+
task_version)
498550
plot_audio_times(passiveTone_intervals, passiveNoise_intervals, ax=ax)
499551

500552
passiveStims_df = np.concatenate(

ibllib/io/extractors/video_motion.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def align_motion(self, period=(-np.inf, np.inf), side='left', sd_thresh=10, disp
154154
# TODO Add function arg to make grayscale
155155
self.alignment.frames = \
156156
vidio.get_video_frames_preload(camera_path, frame_numbers, mask=roi)
157+
assert self.alignment.frames.size != 0
157158
except AssertionError:
158159
self.log.error('Failed to open video')
159160
return None, None, None

ibllib/pipes/ephys_preprocessing.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,11 @@ def _label_probe_qc(self, folder_probe, df_units, drift):
344344
:return:
345345
"""
346346
eid = self.one.path2eid(self.session_path, query_type='remote')
347-
pdict = self.one.alyx.rest('insertions', 'list',
348-
session=eid, name=folder_probe.parts[-1], no_cache=True)
347+
# the probe name is the first folder after alf: {session_path}/alf/{probe_name}/{spike_sorter_name}
348+
probe_name = Path(folder_probe).relative_to(self.session_path.joinpath('alf')).parts[0]
349+
pdict = self.one.alyx.rest('insertions', 'list', session=eid, name=probe_name, no_cache=True)
349350
if len(pdict) != 1:
351+
_logger.warning(f'No probe found for probe name: {probe_name}')
350352
return
351353
isok = df_units['label'] == 1
352354
qcdict = {'n_units': int(df_units.shape[0]),

ibllib/qc/base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,11 @@ def update_extended_qc(self, data):
188188

189189
# Ensure None instead of NaNs
190190
for k, v in data.items():
191-
if (v is not None and not isinstance(v, str)) and np.isnan(v).all():
192-
data[k] = None
191+
if v is not None and not isinstance(v, str):
192+
if isinstance(v, tuple):
193+
data[k] = tuple(None if np.isnan(i) else i for i in v)
194+
else:
195+
data[k] = None if np.isnan(v).all() else v
193196

194197
details = self.one.alyx.get(f'/{self.endpoint}/{self.eid}', clobber=True)
195198
if self.json:

ibllib/qc/task_extractors.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ def channel_events(name):
120120
mask = sync['channels'] == chmap[name]
121121
return dict(zip(keys, (sync[k][mask] for k in keys)))
122122

123-
ttls = [channel_events(ch) for ch in ('frame2ttl', 'audio', 'bpod')]
123+
ttls = [ephys_fpga._clean_frame2ttl(channel_events('frame2ttl')),
124+
ephys_fpga._clean_audio(channel_events('audio')),
125+
channel_events('bpod')]
124126
self.frame_ttls, self.audio_ttls, self.bpod_ttls = ttls
125127

126128
def extract_data(self):

0 commit comments

Comments
 (0)