Skip to content

Commit 0ce0b86

Browse files
committed
Merge branch 'release/2.7.1'
2 parents 81076be + cec9cd6 commit 0ce0b86

File tree

5 files changed

+78
-27
lines changed

5 files changed

+78
-27
lines changed

brainbox/behavior/dlc.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def insert_idx(array, values):
4141
idx[np.where(abs(values - array[idx - 1]) < abs(values - array[idx]))] -= 1
4242
# If 0 index was reduced, revert
4343
idx[idx == -1] = 0
44+
if np.all(idx == 0):
45+
raise ValueError('Something is wrong, all values to insert are outside of the array.')
4446
return idx
4547

4648

@@ -121,7 +123,7 @@ def get_feature_event_times(dlc, dlc_t, features):
121123

122124
def get_licks(dlc, dlc_t):
123125
"""
124-
Compute lick times from the toungue dlc points
126+
Compute lick times from the tongue dlc points
125127
:param dlc: dlc pqt table
126128
:param dlc_t: dlc times
127129
:return:
@@ -216,6 +218,9 @@ def get_smooth_pupil_diameter(diameter_raw, camera, std_thresh=5, nan_thresh=1):
216218
else:
217219
raise NotImplementedError("camera has to be 'left' or 'right")
218220

221+
# Raise error if too many NaN time points, in this case it doesn't make sense to interpolate
222+
if np.mean(np.isnan(diameter_raw)) > 0.9:
223+
raise ValueError(f"Raw pupil diameter for {camera} is too often NaN, cannot smooth.")
219224
# run savitzy-golay filter on non-nan time points to denoise
220225
diameter_smoothed = smooth_interpolate_savgol(diameter_raw, window=window, order=3, interp_kind='linear')
221226

@@ -440,26 +445,37 @@ def plot_motion_energy_hist(camera_dict, trials_df):
440445
'body': '#035382'}
441446

442447
start_window, end_window = plt_window(trials_df['stimOn_times'])
448+
missing_data = []
443449
for cam in camera_dict.keys():
444-
try:
445-
motion_energy = zscore(camera_dict[cam]['motion_energy'], nan_policy='omit')
446-
start_idx = insert_idx(camera_dict[cam]['times'], start_window)
447-
end_idx = np.array(start_idx + int(WINDOW_LEN * SAMPLING[cam]), dtype='int64')
448-
me_all = [motion_energy[start_idx[i]:end_idx[i]] for i in range(len(start_idx))]
449-
times = np.arange(len(me_all[0])) / SAMPLING[cam] + WINDOW_LAG
450-
me_mean = np.mean(me_all, axis=0)
451-
me_std = np.std(me_all, axis=0) / np.sqrt(len(me_all))
452-
plt.plot(times, me_mean, label=f'{cam} cam', color=colors[cam], linewidth=2)
453-
plt.fill_between(times, me_mean + me_std, me_mean - me_std, color=colors[cam], alpha=0.2)
454-
except AttributeError:
455-
logger.warning(f"Cannot load motion energy AND times data for {cam} camera")
450+
if (camera_dict[cam]['motion_energy'] is not None and len(camera_dict[cam]['motion_energy']) > 0
451+
and camera_dict[cam]['times'] is not None and len(camera_dict[cam]['times']) > 0):
452+
try:
453+
motion_energy = zscore(camera_dict[cam]['motion_energy'], nan_policy='omit')
454+
start_idx = insert_idx(camera_dict[cam]['times'], start_window)
455+
end_idx = np.array(start_idx + int(WINDOW_LEN * SAMPLING[cam]), dtype='int64')
456+
me_all = [motion_energy[start_idx[i]:end_idx[i]] for i in range(len(start_idx))]
457+
times = np.arange(len(me_all[0])) / SAMPLING[cam] + WINDOW_LAG
458+
me_mean = np.mean(me_all, axis=0)
459+
me_std = np.std(me_all, axis=0) / np.sqrt(len(me_all))
460+
plt.plot(times, me_mean, label=f'{cam} cam', color=colors[cam], linewidth=2)
461+
plt.fill_between(times, me_mean + me_std, me_mean - me_std, color=colors[cam], alpha=0.2)
462+
except AttributeError:
463+
logger.warning(f"Cannot load motion energy and/or times data for {cam} camera")
464+
missing_data.append(cam)
465+
else:
466+
logger.warning(f"Data missing or empty for motion energy and/or times data for {cam} camera")
467+
missing_data.append(cam)
456468

457469
plt.xticks([-0.5, 0, 0.5, 1, 1.5])
458470
plt.ylabel('z-scored motion energy [a.u.]')
459471
plt.xlabel('time [sec]')
460472
plt.axvline(x=0, label='stimOn', linestyle='--', c='k')
461473
plt.legend(loc='lower right')
462474
plt.title('Motion Energy')
475+
if len(missing_data) > 0:
476+
ax = plt.gca()
477+
ax.text(.95, .35, f"Data incomplete for\n{' and '.join(missing_data)} camera", color='r', fontsize=10,
478+
horizontalalignment='right', verticalalignment='center', transform=ax.transAxes)
463479
return plt.gca()
464480

465481

@@ -477,6 +493,8 @@ def plot_speed_hist(dlc_df, cam_times, trials_df, feature='paw_r', cam='left', l
477493
"""
478494
# Threshold the dlc traces
479495
dlc_df = likelihood_threshold(dlc_df)
496+
# For pre-GPIO sessions, remove the first few timestamps to match the number of frames
497+
cam_times = cam_times[-len(dlc_df):]
480498
# Get speeds
481499
speeds = get_speed(dlc_df, cam_times, camera=cam, feature=feature)
482500
# Windows aligned to align_to
@@ -495,7 +513,7 @@ def plot_speed_hist(dlc_df, cam_times, trials_df, feature='paw_r', cam='left', l
495513
plt.plot(times, pd.DataFrame.from_dict(dict(zip(incorrect.index, incorrect.values))).mean(axis=1),
496514
c='gray', label='incorrect trial')
497515
plt.axvline(x=0, label='stimOn', linestyle='--', c='r')
498-
plt.title(f'{feature.split("_")[0].capitalize()} speed')
516+
plt.title(f'{feature.split("_")[0].capitalize()} speed ({cam} cam)')
499517
plt.xticks([-0.5, 0, 0.5, 1, 1.5])
500518
plt.xlabel('time [sec]')
501519
plt.ylabel('speed [px/sec]')
@@ -531,7 +549,7 @@ def plot_pupil_diameter_hist(pupil_diameter, cam_times, trials_df, cam='left'):
531549
plt.plot(times, pupil_mean, label=align_to.split("_")[0], color=color)
532550
plt.fill_between(times, pupil_mean + pupil_std, pupil_mean - pupil_std, color=color, alpha=0.5)
533551
plt.axvline(x=0, linestyle='--', c='k')
534-
plt.title('Pupil diameter')
552+
plt.title(f'Pupil diameter ({cam} cam)')
535553
plt.xlabel('time [sec]')
536554
plt.xticks([-0.5, 0, 0.5, 1, 1.5])
537555
plt.ylabel('pupil diameter [px]')

ibllib/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "2.7.0"
1+
__version__ = "2.7.1"
22
import warnings
33

44
from ibllib.misc import logger_config

ibllib/pipes/ephys_preprocessing.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,9 @@ def _run(self):
877877
class EphysPostDLC(tasks.Task):
878878
"""
879879
The post_dlc task takes dlc traces as input and computes useful quantities, as well as qc.
880+
881+
For creating the full dlc_qc_plot, several other inputs are required that can be found in the docstring of
882+
:py:func:ibllib.plots.figures.dlc_qc_plot
880883
"""
881884
io_charge = 90
882885
level = 3
@@ -894,6 +897,16 @@ class EphysPostDLC(tasks.Task):
894897
}
895898

896899
def _run(self, overwrite=False, run_qc=True, plot_qc=True):
900+
"""
901+
Run the EphysPostDLC task. Returns a list of file locations for the output files in signature. The created plot
902+
(dlc_qc_plot.png) is not returned, but saved in session_path/snapshots and uploaded to Alyx as a note.
903+
904+
:param overwrite: bool, whether to recompute existing output files (default is False).
905+
Note that the dlc_qc_plot will be computed even if overwrite = False
906+
:param run_qc: bool, whether to run the DLC QC (default is True)
907+
:param plot_qc: book, whether to create the dlc_qc_plot (default is True)
908+
909+
"""
897910
# Check if output files exist locally
898911
exist, output_files = self.assert_expected(self.signature['output_files'], silent=True)
899912
if exist and not overwrite:
@@ -916,15 +929,13 @@ def _run(self, overwrite=False, run_qc=True, plot_qc=True):
916929
dlc = pd.read_parquet(dlc_file)
917930
dlc_thresh = likelihood_threshold(dlc, 0.9)
918931
# try to load respective camera times
919-
try:
920-
dlc_t = np.load(next(Path(self.session_path).joinpath('alf').glob(f'_ibl_{cam}Camera.times.*npy')))
921-
times = True
922-
except StopIteration:
923-
_logger.error(f'No camera.times found for {cam} camera. '
932+
dlc_t = np.load(next(Path(self.session_path).joinpath('alf').glob(f'_ibl_{cam}Camera.times.*npy')))
933+
times = True
934+
if dlc_t.shape[0] == 0:
935+
_logger.error(f'camera.times empty for {cam} camera. '
924936
f'Computations using camera.times will be skipped')
925937
self.status = -1
926938
times = False
927-
928939
# These features are only computed from left and right cam
929940
if cam in ('left', 'right'):
930941
features = pd.DataFrame()
@@ -937,8 +948,13 @@ def _run(self, overwrite=False, run_qc=True, plot_qc=True):
937948
# Compute pupil diameter, raw and smoothed
938949
_logger.info(f"Computing raw pupil diameter for {cam} camera.")
939950
features['pupilDiameter_raw'] = get_pupil_diameter(dlc_thresh)
940-
_logger.info(f"Computing smooth pupil diameter for {cam} camera.")
941-
features['pupilDiameter_smooth'] = get_smooth_pupil_diameter(features['pupilDiameter_raw'], cam)
951+
try:
952+
_logger.info(f"Computing smooth pupil diameter for {cam} camera.")
953+
features['pupilDiameter_smooth'] = get_smooth_pupil_diameter(features['pupilDiameter_raw'],
954+
cam)
955+
except BaseException:
956+
_logger.error(f"Computing smooth pupil diameter for {cam} camera failed, saving all NaNs.")
957+
features['pupilDiameter_smooth'] = np.nan
942958
# Safe to pqt
943959
features_file = Path(self.session_path).joinpath('alf', f'_ibl_{cam}Camera.features.pqt')
944960
features.to_parquet(features_file)
@@ -977,6 +993,7 @@ def _run(self, overwrite=False, run_qc=True, plot_qc=True):
977993
fig_path.parent.mkdir(parents=True, exist_ok=True)
978994
fig = dlc_qc_plot(self.one.path2eid(self.session_path), one=self.one)
979995
fig.savefig(fig_path)
996+
fig.clf()
980997
snp = ReportSnapshot(self.session_path, session_id, one=self.one)
981998
snp.outputs = [fig_path]
982999
snp.register_images(widths=['orig'],
@@ -1060,5 +1077,6 @@ def __init__(self, session_path=None, **kwargs):
10601077
tasks["EphysCellsQc"] = EphysCellsQc(self.session_path, parents=[tasks["SpikeSorting"]])
10611078
tasks["EphysDLC"] = EphysDLC(self.session_path, parents=[tasks["EphysVideoCompress"]])
10621079
# level 3
1063-
tasks["EphysPostDLC"] = EphysPostDLC(self.session_path, parents=[tasks["EphysDLC"]])
1080+
tasks["EphysPostDLC"] = EphysPostDLC(self.session_path, parents=[tasks["EphysDLC"], tasks["EphysTrials"],
1081+
tasks["EphysVideoSyncQc"]])
10641082
self.tasks = tasks

ibllib/plots/figures.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
import logging
55
from pathlib import Path
6+
import traceback
67
from string import ascii_uppercase
78

89
import numpy as np
@@ -341,6 +342,11 @@ def dlc_qc_plot(eid, one=None):
341342
else:
342343
logger.warning(f"Could not load _ibl_{cam}Camera.{feat} some DLC QC plots have to be skipped.")
343344
data[f'{cam}_{feat}'] = None
345+
# Sometimes there is a file but the object is empty
346+
if data[f'{cam}_{feat}'] is not None and len(data[f'{cam}_{feat}']) == 0:
347+
logger.warning(f"Object loaded from _ibl_{cam}Camera.{feat} is empty, some plots have to be skipped.")
348+
data[f'{cam}_{feat}'] = None
349+
344350
# Session data
345351
for alf_object in ['trials', 'wheel', 'licks']:
346352
try:
@@ -355,7 +361,9 @@ def dlc_qc_plot(eid, one=None):
355361
data[f'{alf_object}'] = None
356362
# Simplify to what we actually need
357363
data['licks'] = data['licks'].times if data['licks'] else None
358-
data['left_pupil'] = data['left_features'].pupilDiameter_smooth if data['left_features'] is not None else None
364+
data['left_pupil'] = data['left_features'].pupilDiameter_smooth if (
365+
data['left_features'] is not None and not np.all(np.isnan(data['left_features'].pupilDiameter_smooth))
366+
) else None
359367
data['wheel_time'] = data['wheel'].timestamps if data['wheel'] is not None else None
360368
data['wheel_position'] = data['wheel'].position if data['wheel'] is not None else None
361369
if data['trials']:
@@ -393,14 +401,16 @@ def dlc_qc_plot(eid, one=None):
393401
ax = plt.subplot(2, 5, i + 1)
394402
ax.text(-0.1, 1.15, ascii_uppercase[i], transform=ax.transAxes, fontsize=16, fontweight='bold')
395403
# Check if any of the inputs is None
396-
if any([v is None for v in panel[1].values()]):
404+
if any([v is None for v in panel[1].values()]) or any([v.values() is None for v in panel[1].values()
405+
if isinstance(v, dict)]):
397406
ax.text(.5, .5, f"Data incomplete\n{panel[0].__name__}", color='r', fontweight='bold',
398407
fontsize=12, horizontalalignment='center', verticalalignment='center', transform=ax.transAxes)
399408
plt.axis('off')
400409
else:
401410
try:
402411
panel[0](**panel[1])
403412
except BaseException:
413+
logger.error(f'Error in {panel[0].__name__}\n' + traceback.format_exc())
404414
ax.text(.5, .5, f'Error in \n{panel[0].__name__}', color='r', fontweight='bold',
405415
fontsize=12, horizontalalignment='center', verticalalignment='center', transform=ax.transAxes)
406416
plt.axis('off')

release_notes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
## Release Note 2.7
2+
3+
### Release Notes 2.7.1 2022-01-05
4+
5+
- Fixes and better logging for EphysPostDLC task
6+
27
### Release Notes 2.7.0 2021-12-20
38

49
- Remove atlas instantiation from import of histology module

0 commit comments

Comments
 (0)