Skip to content

Commit ce861a2

Browse files
committed
Merge branch 'release/2.9.0'
2 parents 8adae0a + bc6ce56 commit ce861a2

File tree

5 files changed

+203
-19
lines changed

5 files changed

+203
-19
lines changed

brainbox/behavior/dlc.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ def plot_wheel_position(wheel_position, wheel_time, trials_df):
341341
trials_df['wheel_position'] = [wheel_position[start_idx[w]: end_idx[w]] - wheel_position[start_idx[w]]
342342
for w in range(len(start_idx))]
343343
# Plotting
344-
times = np.arange(len(trials_df['wheel_position'][0])) * T_BIN + WINDOW_LAG
344+
times = np.arange(len(trials_df['wheel_position'].iloc[0])) * T_BIN + WINDOW_LAG
345345
for side, label, color in zip([-1, 1], ['right', 'left'], ['darkred', '#1f77b4']):
346346
side_df = trials_df[trials_df['choice'] == side]
347347
for idx in side_df.index:
@@ -373,7 +373,11 @@ def _bin_window_licks(lick_times, trials_df):
373373
lick_bins = np.squeeze(lick_bins)
374374
start_window, end_window = plt_window(trials_df['feedback_times'])
375375
# Translating the time window into an index window
376-
start_idx = insert_idx(bin_times, start_window)
376+
try:
377+
start_idx = insert_idx(bin_times, start_window)
378+
except ValueError:
379+
logger.error('Lick time stamps are outside of the trials windows')
380+
raise
377381
end_idx = np.array(start_idx + int(WINDOW_LEN / T_BIN), dtype='int64')
378382
# Get the binned licks for each window
379383
trials_df['lick_bins'] = [lick_bins[start_idx[i]:end_idx[i]] for i in range(len(start_idx))]
@@ -394,7 +398,7 @@ def plot_lick_hist(lick_times, trials_df):
394398
"""
395399
licks_df = _bin_window_licks(lick_times, trials_df)
396400
# Plot
397-
times = np.arange(len(licks_df['lick_bins'][0])) * T_BIN + WINDOW_LAG
401+
times = np.arange(len(licks_df['lick_bins'].iloc[0])) * T_BIN + WINDOW_LAG
398402
correct = licks_df[licks_df['feedbackType'] == 1]['lick_bins']
399403
incorrect = licks_df[licks_df['feedbackType'] == -1]['lick_bins']
400404
plt.plot(times, pd.DataFrame.from_dict(dict(zip(correct.index, correct.values))).mean(axis=1),
@@ -420,7 +424,7 @@ def plot_lick_raster(lick_times, trials_df):
420424
"""
421425
licks_df = _bin_window_licks(lick_times, trials_df)
422426
plt.imshow(list(licks_df[licks_df['feedbackType'] == 1]['lick_bins']), aspect='auto',
423-
extent=[-0.5, 1.5, len(licks_df['lick_bins'][0]), 0], cmap='gray_r')
427+
extent=[-0.5, 1.5, len(licks_df['lick_bins'].iloc[0]), 0], cmap='gray_r')
424428
plt.xticks([-0.5, 0, 0.5, 1, 1.5])
425429
plt.ylabel('trials')
426430
plt.xlabel('time [sec]')
@@ -451,7 +455,11 @@ def plot_motion_energy_hist(camera_dict, trials_df):
451455
and camera_dict[cam]['times'] is not None and len(camera_dict[cam]['times']) > 0):
452456
try:
453457
motion_energy = zscore(camera_dict[cam]['motion_energy'], nan_policy='omit')
454-
start_idx = insert_idx(camera_dict[cam]['times'], start_window)
458+
try:
459+
start_idx = insert_idx(camera_dict[cam]['times'], start_window)
460+
except ValueError:
461+
logger.error("Camera.times are outside of the trial windows")
462+
raise
455463
end_idx = np.array(start_idx + int(WINDOW_LEN * SAMPLING[cam]), dtype='int64')
456464
me_all = [motion_energy[start_idx[i]:end_idx[i]] for i in range(len(start_idx))]
457465
times = np.arange(len(me_all[0])) / SAMPLING[cam] + WINDOW_LAG
@@ -495,6 +503,8 @@ def plot_speed_hist(dlc_df, cam_times, trials_df, feature='paw_r', cam='left', l
495503
dlc_df = likelihood_threshold(dlc_df)
496504
# For pre-GPIO sessions, remove the first few timestamps to match the number of frames
497505
cam_times = cam_times[-len(dlc_df):]
506+
if len(cam_times) != len(dlc_df):
507+
raise ValueError("Camera times length and DLC length are inconsistent")
498508
# Get speeds
499509
speeds = get_speed(dlc_df, cam_times, camera=cam, feature=feature)
500510
# Windows aligned to align_to
@@ -504,7 +514,7 @@ def plot_speed_hist(dlc_df, cam_times, trials_df, feature='paw_r', cam='left', l
504514
# Add speeds to trials_df
505515
trials_df[f'speed_{feature}'] = [speeds[start_idx[i]:end_idx[i]] for i in range(len(start_idx))]
506516
# Plot
507-
times = np.arange(len(trials_df[f'speed_{feature}'][0])) / SAMPLING[cam] + WINDOW_LAG
517+
times = np.arange(len(trials_df[f'speed_{feature}'].iloc[0])) / SAMPLING[cam] + WINDOW_LAG
508518
# Need to expand the series of lists into a dataframe first, for the nan skipping to work
509519
correct = trials_df[trials_df['feedbackType'] == 1][f'speed_{feature}']
510520
incorrect = trials_df[trials_df['feedbackType'] == -1][f'speed_{feature}']

ibllib/__init__.py

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

44
from ibllib.misc import logger_config

ibllib/pipes/ephys_preprocessing.py

Lines changed: 163 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pathlib import Path
88
import packaging.version
99

10+
import cv2
1011
import numpy as np
1112
import pandas as pd
1213

@@ -15,7 +16,7 @@
1516
from ibllib.misc import check_nvidia_driver
1617
from ibllib.ephys import ephysqc, spikes, sync_probes
1718
from ibllib.io import ffmpeg, spikeglx
18-
from ibllib.io.video import label_from_path
19+
from ibllib.io.video import label_from_path, assert_valid_label
1920
from ibllib.io.extractors import ephys_fpga, ephys_passive, camera
2021
from ibllib.pipes import tasks
2122
from ibllib.pipes.training_preprocessing import TrainingRegisterRaw as EphysRegisterRaw
@@ -893,14 +894,159 @@ def get_signatures(self, **kwargs):
893894

894895

895896
class EphysDLC(tasks.Task):
897+
"""
898+
This task relies on a correctly installed dlc environment as per
899+
https://docs.google.com/document/d/1g0scP6_3EmaXCU4SsDNZWwDTaD9MG0es_grLA-d0gh0/edit#
900+
901+
If your environment is set up otherwise, make sure that you set the respective attributes:
902+
t = EphysDLC(session_path)
903+
t.dlcenv = Path('/path/to/your/dlcenv/bin/activate')
904+
t.scripts = Path('/path/to/your/iblscripts/deploy/serverpc/dlc')
905+
"""
896906
gpu = 1
897907
cpu = 4
898908
io_charge = 90
899909
level = 2
910+
force = True
900911

901-
def _run(self):
902-
"""empty placeholder for job creation only"""
903-
pass
912+
dlcenv = Path.home().joinpath('Documents', 'PYTHON', 'envs', 'dlcenv', 'bin', 'activate')
913+
scripts = Path.home().joinpath('Documents', 'PYTHON', 'iblscripts', 'deploy', 'serverpc', 'dlc')
914+
signature = {
915+
'input_files': [
916+
('_iblrig_leftCamera.raw.mp4', 'raw_video_data', True),
917+
('_iblrig_rightCamera.raw.mp4', 'raw_video_data', True),
918+
('_iblrig_bodyCamera.raw.mp4', 'raw_video_data', True),
919+
],
920+
'output_files': [
921+
('_ibl_leftCamera.dlc.pqt', 'alf', True),
922+
('_ibl_rightCamera.dlc.pqt', 'alf', True),
923+
('_ibl_bodyCamera.dlc.pqt', 'alf', True),
924+
('leftCamera.ROIMotionEnergy.npy', 'alf', True),
925+
('rightCamera.ROIMotionEnergy.npy', 'alf', True),
926+
('bodyCamera.ROIMotionEnergy.npy', 'alf', True),
927+
('leftROIMotionEnergy.position.npy', 'alf', True),
928+
('rightROIMotionEnergy.position.npy', 'alf', True),
929+
('bodyROIMotionEnergy.position.npy', 'alf', True),
930+
],
931+
}
932+
933+
def _check_dlcenv(self):
934+
"""Check that scripts are present, dlcenv can be activated and get iblvideo version"""
935+
assert len(list(self.scripts.rglob('run_dlc.*'))) == 2, \
936+
f'Scripts run_dlc.sh and run_dlc.py do not exist in {self.scripts}'
937+
assert len(list(self.scripts.rglob('run_motion.*'))) == 2, \
938+
f'Scripts run_motion.sh and run_motion.py do not exist in {self.scripts}'
939+
assert self.dlcenv.exists(), f"DLC environment does not exist in assumed location {self.dlcenv}"
940+
command2run = f"source {self.dlcenv}; python -c 'import iblvideo; print(iblvideo.__version__)'"
941+
process = subprocess.Popen(
942+
command2run,
943+
shell=True,
944+
stdout=subprocess.PIPE,
945+
stderr=subprocess.PIPE,
946+
executable="/bin/bash"
947+
)
948+
info, error = process.communicate()
949+
if process.returncode != 0:
950+
raise AssertionError(f"DLC environment check failed\n{error.decode('utf-8')}")
951+
version = info.decode("utf-8").strip().split('\n')[-1]
952+
return version
953+
954+
@staticmethod
955+
def _video_intact(file_mp4):
956+
"""Checks that the downloaded video can be opened and is not empty"""
957+
cap = cv2.VideoCapture(str(file_mp4))
958+
frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
959+
intact = True if frame_count > 0 else False
960+
cap.release()
961+
return intact
962+
963+
def _run(self, cams=None, overwrite=False):
964+
# Default to all three cams
965+
cams = cams or ['left', 'right', 'body']
966+
cams = assert_valid_label(cams)
967+
# Set up
968+
self.session_id = self.one.path2eid(self.session_path)
969+
actual_outputs = []
970+
971+
# Loop through cams
972+
for cam in cams:
973+
# Catch exceptions so that following cameras can still run
974+
try:
975+
# If all results exist and overwrite is False, skip computation
976+
expected_outputs_present, expected_outputs = self.assert_expected(self.output_files, silent=True)
977+
if overwrite is False and expected_outputs_present is True:
978+
actual_outputs.extend(expected_outputs)
979+
continue
980+
else:
981+
file_mp4 = next(self.session_path.joinpath('raw_video_data').glob(f'_iblrig_{cam}Camera.raw*.mp4'))
982+
if not file_mp4.exists():
983+
# In this case we set the status to Incomplete.
984+
_logger.error(f"No raw video file available for {cam}, skipping.")
985+
self.status = -3
986+
continue
987+
if not self._video_intact(file_mp4):
988+
_logger.error(f"Corrupt raw video file {file_mp4}")
989+
self.status = -1
990+
continue
991+
# Check that dlc environment is ok, shell scripts exists, and get iblvideo version, GPU addressable
992+
self.version = self._check_dlcenv()
993+
_logger.info(f'iblvideo version {self.version}')
994+
check_nvidia_driver()
995+
996+
_logger.info(f'Running DLC on {cam}Camera.')
997+
command2run = f"{self.scripts.joinpath('run_dlc.sh')} {str(self.dlcenv)} {file_mp4} {overwrite}"
998+
_logger.info(command2run)
999+
process = subprocess.Popen(
1000+
command2run,
1001+
shell=True,
1002+
stdout=subprocess.PIPE,
1003+
stderr=subprocess.PIPE,
1004+
executable="/bin/bash",
1005+
)
1006+
info, error = process.communicate()
1007+
info_str = info.decode("utf-8").strip()
1008+
_logger.info(info_str)
1009+
if process.returncode != 0:
1010+
error_str = error.decode("utf-8").strip()
1011+
_logger.error(f'DLC failed for {cam}Camera\n {error_str}')
1012+
self.status = -1
1013+
# We dont' run motion energy, or add any files if dlc failed to run
1014+
continue
1015+
dlc_result = next(self.session_path.joinpath('alf').glob(f'_ibl_{cam}Camera.dlc*.pqt'))
1016+
actual_outputs.append(dlc_result)
1017+
1018+
_logger.info(f'Computing motion energy for {cam}Camera')
1019+
command2run = f"{self.scripts.joinpath('run_motion.sh')} {str(self.dlcenv)} {file_mp4} {dlc_result}"
1020+
_logger.info(command2run)
1021+
process = subprocess.Popen(
1022+
command2run,
1023+
shell=True,
1024+
stdout=subprocess.PIPE,
1025+
stderr=subprocess.PIPE,
1026+
executable="/bin/bash",
1027+
)
1028+
info, error = process.communicate()
1029+
info_str = info.decode("utf-8").strip()
1030+
_logger.info(info_str)
1031+
if process.returncode != 0:
1032+
error_str = error.decode("utf-8").strip()
1033+
_logger.error(f'Motion energy failed for {cam}Camera \n {error_str}')
1034+
self.status = -1
1035+
continue
1036+
actual_outputs.append(next(self.session_path.joinpath('alf').glob(
1037+
f'{cam}Camera.ROIMotionEnergy*.npy')))
1038+
actual_outputs.append(next(self.session_path.joinpath('alf').glob(
1039+
f'{cam}ROIMotionEnergy.position*.npy')))
1040+
except BaseException:
1041+
_logger.error(traceback.format_exc())
1042+
self.status = -1
1043+
continue
1044+
# If status is Incomplete, check that there is at least one output.
1045+
# # Otherwise make sure it gets set to Empty (outputs = None), and set status to -1 to make sure it doesn't slip
1046+
if self.status == -3 and len(actual_outputs) == 0:
1047+
actual_outputs = None
1048+
self.status = -1
1049+
return actual_outputs
9041050

9051051

9061052
class EphysPostDLC(tasks.Task):
@@ -965,15 +1111,22 @@ def _run(self, overwrite=False, run_qc=True, plot_qc=True):
9651111
f'Computations using camera.times will be skipped')
9661112
self.status = -1
9671113
times = False
1114+
elif dlc_t.shape[0] < len(dlc_thresh):
1115+
_logger.error(f'Camera times shorter than DLC traces for {cam} camera. '
1116+
f'Computations using camera.times will be skipped')
1117+
self.status = -1
1118+
times = 'short'
9681119
# These features are only computed from left and right cam
9691120
if cam in ('left', 'right'):
9701121
features = pd.DataFrame()
9711122
# If camera times are available, get the lick time stamps for combined array
972-
if times:
1123+
if times is True:
9731124
_logger.info(f"Computing lick times for {cam} camera.")
9741125
combined_licks.append(get_licks(dlc_thresh, dlc_t))
975-
else:
976-
_logger.warning(f"Skipping lick times for {cam} camera as no camera.times available.")
1126+
elif times is False:
1127+
_logger.warning(f"Skipping lick times for {cam} camera as no camera.times available")
1128+
elif times == 'short':
1129+
_logger.warning(f"Skipping lick times for {cam} camera as camera.times are too short")
9771130
# Compute pupil diameter, raw and smoothed
9781131
_logger.info(f"Computing raw pupil diameter for {cam} camera.")
9791132
features['pupilDiameter_raw'] = get_pupil_diameter(dlc_thresh)
@@ -983,19 +1136,20 @@ def _run(self, overwrite=False, run_qc=True, plot_qc=True):
9831136
cam)
9841137
except BaseException:
9851138
_logger.error(f"Computing smooth pupil diameter for {cam} camera failed, saving all NaNs.")
1139+
_logger.error(traceback.format_exc())
9861140
features['pupilDiameter_smooth'] = np.nan
9871141
# Safe to pqt
9881142
features_file = Path(self.session_path).joinpath('alf', f'_ibl_{cam}Camera.features.pqt')
9891143
features.to_parquet(features_file)
9901144
output_files.append(features_file)
9911145

9921146
# For all cams, compute DLC qc if times available
993-
if times and run_qc:
1147+
if times is True or times == 'short' and run_qc:
9941148
# Setting download_data to False because at this point the data should be there
9951149
qc = DlcQC(self.session_path, side=cam, one=self.one, download_data=False)
9961150
qc.run(update=True)
9971151
else:
998-
if not times:
1152+
if times is False:
9991153
_logger.warning(f"Skipping QC for {cam} camera as no camera.times available")
10001154
if not run_qc:
10011155
_logger.warning(f"Skipping QC for {cam} camera as run_qc=False")

ibllib/plots/figures.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ def dlc_qc_plot(eid, one=None):
652652
logger.warning(f"Could not load {alf_object} object for session {eid}, some plots have to be skipped.")
653653
data[f'{alf_object}'] = None
654654
# Simplify to what we actually need
655-
data['licks'] = data['licks'].times if data['licks'] else None
655+
data['licks'] = data['licks'].times if data['licks'] and data['licks'].times.shape[0] > 0 else None
656656
data['left_pupil'] = data['left_features'].pupilDiameter_smooth if (
657657
data['left_features'] is not None and not np.all(np.isnan(data['left_features'].pupilDiameter_smooth))
658658
) else None
@@ -686,15 +686,28 @@ def dlc_qc_plot(eid, one=None):
686686
(plot_pupil_diameter_hist,
687687
{'pupil_diameter': data['left_pupil'], 'cam_times': data['left_times'], 'trials_df': data['trials']})
688688
]
689+
690+
# If camera times is shorter than dlc traces, many plots will fail. Don't even try those and give informative error
691+
if data['left_dlc'] is not None and data['left_times'] is not None:
692+
if len(data['left_times']) < len(data['left_dlc']):
693+
for p in range(5, 10):
694+
panels[p] = (panels[p][0], 'cam_times')
695+
689696
# Plotting
690697
plt.rcParams.update({'font.size': 10})
691698
fig = plt.figure(figsize=(17, 10))
692699
for i, panel in enumerate(panels):
693700
ax = plt.subplot(2, 5, i + 1)
694701
ax.text(-0.1, 1.15, ascii_uppercase[i], transform=ax.transAxes, fontsize=16, fontweight='bold')
702+
# Check if we have the cam times issue:
703+
if panel[1] == 'cam_times':
704+
ax.text(.5, .5, "Issue with camera.times\nsee task logs", color='r', fontweight='bold',
705+
fontsize=12, horizontalalignment='center', verticalalignment='center', transform=ax.transAxes)
706+
plt.axis('off')
707+
695708
# Check if any of the inputs is None
696-
if any([v is None for v in panel[1].values()]) or any([v.values() is None for v in panel[1].values()
697-
if isinstance(v, dict)]):
709+
elif any([v is None for v in panel[1].values()]) or any([v.values() is None for v in panel[1].values()
710+
if isinstance(v, dict)]):
698711
ax.text(.5, .5, f"Data incomplete\n{panel[0].__name__}", color='r', fontweight='bold',
699712
fontsize=12, horizontalalignment='center', verticalalignment='center', transform=ax.transAxes)
700713
plt.axis('off')

release_notes.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## Release Note 2.9
2+
3+
### Release Note 2.9.0 2022-01-24
4+
- Adding EphysDLC task in ephys_preprocessing pipeline
5+
- NOTE: requires DLC environment to be set up on local servers!
6+
- Fixes to EphysPostDLC dlc_qc_plot
7+
18
## Release Note 2.8
29

310
### Release Notes 2.8.0 2022-01-19

0 commit comments

Comments
 (0)