"""Module that produces figures, usually for the extraction pipeline"""importloggingimporttimefrompathlibimportPathimporttracebackfromstringimportascii_uppercaseimportnumpyasnpimportpandasaspdimportscipy.signalimportmatplotlib.pyplotaspltfromibldspimportvoltagefromibllib.plots.snapshotimportReportSnapshotProbe,ReportSnapshotfromone.apiimportONEimportone.alf.ioasalfiofromone.alf.exceptionsimportALFObjectNotFoundfromibllib.io.videoimportget_video_frame,url_from_eidfromibllib.oneibl.data_handlersimportExpectedDatasetimportspikeglximportneuropixelfrombrainbox.plotimportdriftmapfrombrainbox.io.spikeglximportStreamerfrombrainbox.behavior.dlcimportSAMPLING,plot_trace_on_frame,plot_wheel_position,plot_lick_hist, \
plot_lick_raster,plot_motion_energy_hist,plot_speed_hist,plot_pupil_diameter_histfrombrainbox.ephys_plotsimportimage_lfp_spectrum_plot,image_rms_plot,plot_brain_regionsfrombrainbox.io.oneimportload_spike_sorting_fastfrombrainbox.behaviorimporttrainingfromiblutil.numericalimportismemberfromibllib.plots.miscimportDensitylogger=logging.getLogger(__name__)
[docs]defset_axis_label_size(ax,labels=14,ticklabels=12,title=14,cmap=False):""" Function to normalise size of all axis labels :param ax: :param labels: :param ticklabels: :param title: :param cmap: :return: """ax.xaxis.get_label().set_fontsize(labels)ax.yaxis.get_label().set_fontsize(labels)ax.tick_params(labelsize=ticklabels)ax.title.set_fontsize(title)ifcmap:cbar=ax.images[-1].colorbarcbar.ax.tick_params(labelsize=ticklabels)cbar.ax.yaxis.get_label().set_fontsize(labels)
[docs]defremove_axis_outline(ax):""" Function to remove outline of empty axis :param ax: :return: """ax.get_xaxis().set_visible(False)ax.get_yaxis().set_visible(False)ax.spines['right'].set_visible(False)ax.spines['top'].set_visible(False)ax.spines['bottom'].set_visible(False)ax.spines['left'].set_visible(False)
[docs]classBehaviourPlots(ReportSnapshot):"""Behavioural plots."""@propertydefsignature(self):signature={'input_files':[('*trials.table.pqt',self.trials_collection,True),],'output_files':[('psychometric_curve.png','snapshot/behaviour',True),('chronometric_curve.png','snapshot/behaviour',True),('reaction_time_with_trials.png','snapshot/behaviour',True)]}returnsignaturedef__init__(self,eid,session_path=None,one=None,**kwargs):""" Generate and upload behaviour plots. Parameters ---------- eid : str, uuid.UUID An experiment UUID. session_path : pathlib.Path A session path. one : one.api.One An instance of ONE for registration to Alyx. trials_collection : str The location of the trials data (default: 'alf'). kwargs Arguments for ReportSnapshot constructor. """self.one=oneself.eid=eidself.session_path=session_pathorself.one.eid2path(self.eid)self.trials_collection=kwargs.pop('task_collection','alf')super(BehaviourPlots,self).__init__(self.session_path,self.eid,one=self.one,**kwargs)# Output directory should mirror trials collection, sans 'alf' partself.output_directory=self.session_path.joinpath('snapshot','behaviour',self.trials_collection.removeprefix('alf').strip('/'))self.output_directory.mkdir(exist_ok=True,parents=True)def_run(self):output_files=[]trials=alfio.load_object(self.session_path.joinpath(self.trials_collection),'trials')ifself.one:title=self.one.path2ref(self.session_path,as_dict=False)else:title='_'.join(list(self.session_path.parts[-3:]))fig,ax=training.plot_psychometric(trials,title=title,figsize=(8,6))set_axis_label_size(ax)save_path=Path(self.output_directory).joinpath("psychometric_curve.png")output_files.append(save_path)fig.savefig(save_path)plt.close(fig)fig,ax=training.plot_reaction_time(trials,title=title,figsize=(8,6))set_axis_label_size(ax)save_path=Path(self.output_directory).joinpath("chronometric_curve.png")output_files.append(save_path)fig.savefig(save_path)plt.close(fig)fig,ax=training.plot_reaction_time_over_trials(trials,title=title,figsize=(8,6))set_axis_label_size(ax)save_path=Path(self.output_directory).joinpath("reaction_time_with_trials.png")output_files.append(save_path)fig.savefig(save_path)plt.close(fig)returnoutput_files
# TODO put into histology and alignment pipeline
[docs]classHistologySlices(ReportSnapshotProbe):"""Plots coronal and sagittal slice showing electrode locations."""def_run(self):assertself.pidassertself.brain_atlasoutput_files=[]self.histology_status=self.get_histology_status()electrodes=self.get_channels('electrodeSites',f'alf/{self.pname}')ifself.hist_lookup[self.histology_status]>0:fig=plt.figure(figsize=(12,9))gs=fig.add_gridspec(2,2,width_ratios=[.95,.05])ax1=fig.add_subplot(gs[0,0])self.brain_atlas.plot_tilted_slice(electrodes['mlapdv'],1,ax=ax1)ax1.scatter(electrodes['mlapdv'][:,0]*1e6,electrodes['mlapdv'][:,2]*1e6,s=8,c='r')ax1.set_title(f"{self.pid_label}")ax2=fig.add_subplot(gs[1,0])self.brain_atlas.plot_tilted_slice(electrodes['mlapdv'],0,ax=ax2)ax2.scatter(electrodes['mlapdv'][:,1]*1e6,electrodes['mlapdv'][:,2]*1e6,s=8,c='r')ax3=fig.add_subplot(gs[:,1])plot_brain_regions(electrodes['atlas_id'],brain_regions=self.brain_regions,display=True,ax=ax3,title=self.histology_status)save_path=Path(self.output_directory).joinpath("histology_slices.png")output_files.append(save_path)fig.savefig(save_path)plt.close(fig)returnoutput_files
[docs]classLfpPlots(ReportSnapshotProbe):""" Plots LFP spectrum and LFP RMS plots """def_run(self):assertself.pidoutput_files=[]ifself.location!='server':self.histology_status=self.get_histology_status()electrodes=self.get_channels('electrodeSites',f'alf/{self.pname}')# lfp spectrumfig,axs=plt.subplots(1,2,gridspec_kw={'width_ratios':[.95,.05]},figsize=(16,9))lfp=alfio.load_object(self.session_path.joinpath(f'raw_ephys_data/{self.pname}'),'ephysSpectralDensityLF',namespace='iblqc')_,_,_=image_lfp_spectrum_plot(lfp.power,lfp.freqs,clim=[-65,-95],fig_kwargs={'figsize':(8,6)},ax=axs[0],display=True,title=f"{self.pid_label}")set_axis_label_size(axs[0],cmap=True)ifself.histology_status:plot_brain_regions(electrodes['atlas_id'],brain_regions=self.brain_regions,display=True,ax=axs[1],title=self.histology_status)set_axis_label_size(axs[1])else:remove_axis_outline(axs[1])save_path=Path(self.output_directory).joinpath("lfp_spectrum.png")output_files.append(save_path)fig.savefig(save_path)plt.close(fig)# lfp rms# TODO need to figure out the clim rangefig,axs=plt.subplots(1,2,gridspec_kw={'width_ratios':[.95,.05]},figsize=(16,9))lfp=alfio.load_object(self.session_path.joinpath(f'raw_ephys_data/{self.pname}'),'ephysTimeRmsLF',namespace='iblqc')_,_,_=image_rms_plot(lfp.rms,lfp.timestamps,median_subtract=False,band='LFP',clim=[-35,-45],ax=axs[0],cmap='inferno',fig_kwargs={'figsize':(8,6)},display=True,title=f"{self.pid_label}")set_axis_label_size(axs[0],cmap=True)ifself.histology_status:plot_brain_regions(electrodes['atlas_id'],brain_regions=self.brain_regions,display=True,ax=axs[1],title=self.histology_status)set_axis_label_size(axs[1])else:remove_axis_outline(axs[1])save_path=Path(self.output_directory).joinpath("lfp_rms.png")output_files.append(save_path)fig.savefig(save_path)plt.close(fig)returnoutput_files
[docs]classApPlots(ReportSnapshotProbe):""" Plots AP RMS plots """def_run(self):assertself.pidoutput_files=[]ifself.location!='server':self.histology_status=self.get_histology_status()electrodes=self.get_channels('electrodeSites',f'alf/{self.pname}')# TODO need to figure out the clim rangefig,axs=plt.subplots(1,2,gridspec_kw={'width_ratios':[.95,.05]},figsize=(16,9))ap=alfio.load_object(self.session_path.joinpath(f'raw_ephys_data/{self.pname}'),'ephysTimeRmsAP',namespace='iblqc')_,_,_=image_rms_plot(ap.rms,ap.timestamps,median_subtract=False,band='AP',ax=axs[0],fig_kwargs={'figsize':(8,6)},display=True,title=f"{self.pid_label}")set_axis_label_size(axs[0],cmap=True)ifself.histology_status:plot_brain_regions(electrodes['atlas_id'],brain_regions=self.brain_regions,display=True,ax=axs[1],title=self.histology_status)set_axis_label_size(axs[1])else:remove_axis_outline(axs[1])save_path=Path(self.output_directory).joinpath("ap_rms.png")output_files.append(save_path)fig.savefig(save_path)plt.close(fig)returnoutput_files
[docs]classSpikeSorting(ReportSnapshotProbe):""" Plots raw electrophysiology AP band :param session_path: session path :param probe_id: str, UUID of the probe insertion for which to create the plot :param **kwargs: keyword arguments passed to tasks.Task """def_run(self,collection=None):"""runs for initiated PID, streams data, destripe and check bad channels"""defplot_driftmap(self,spikes,clusters,channels,collection):fig,axs=plt.subplots(1,2,gridspec_kw={'width_ratios':[.95,.05]},figsize=(16,9))driftmap(spikes.times,spikes.depths,t_bin=0.007,d_bin=10,vmax=0.5,ax=axs[0])title_str=f"{self.pid_label}, {collection}, {self.pid}\n " \
f"{spikes.clusters.size:_} spikes, {clusters.depths.size:_} clusters"ylim=(0,np.max(channels['axial_um']))axs[0].set(ylim=ylim,title=title_str)run_label=str(Path(collection).relative_to(f'alf/{self.pname}'))run_label="ks2matlab"ifrun_label=='.'elserun_labeloutfile=self.output_directory.joinpath(f"spike_sorting_raster_{run_label}.png")set_axis_label_size(axs[0])ifself.histology_status:plot_brain_regions(channels['atlas_id'],channel_depths=channels['axial_um'],brain_regions=self.brain_regions,display=True,ax=axs[1],title=self.histology_status)axs[1].set(ylim=ylim)set_axis_label_size(axs[1])else:remove_axis_outline(axs[1])fig.savefig(outfile)plt.close(fig)returnoutfile,fig,axsoutput_files=[]ifself.location=='server':assertcollectionspikes=alfio.load_object(self.session_path.joinpath(collection),'spikes')clusters=alfio.load_object(self.session_path.joinpath(collection),'clusters')channels=alfio.load_object(self.session_path.joinpath(collection),'channels')channels['axial_um']=channels['localCoordinates'][:,1]out,fig,axs=plot_driftmap(self,spikes,clusters,channels,collection)output_files.append(out)else:self.histology_status=self.get_histology_status()all_here,output_files=self.assert_expected(self.output_files,silent=True)spike_sorting_runs=self.one.list_datasets(self.eid,filename='spikes.times.npy',collection=f'alf/{self.pname}*')ifall_hereandlen(output_files)==len(spike_sorting_runs):returnoutput_fileslogger.info(self.output_directory)forruninspike_sorting_runs:collection=str(Path(run).parent.as_posix())spikes,clusters,channels=load_spike_sorting_fast(eid=self.eid,probe=self.pname,one=self.one,nested=False,collection=collection,dataset_types=['spikes.depths'],brain_regions=self.brain_regions)if'atlas_id'notinchannels.keys():channels=self.get_channels('channels',collection)out,fig,axs=plot_driftmap(self,spikes,clusters,channels,collection)output_files.append(out)returnoutput_files
[docs]classBadChannelsAp(ReportSnapshotProbe):""" Plots raw electrophysiology AP band task = BadChannelsAp(pid, one=one=one) :param session_path: session path :param probe_id: str, UUID of the probe insertion for which to create the plot :param **kwargs: keyword arguments passed to tasks.Task """
def_run(self):"""runs for initiated PID, streams data, destripe and check bad channels"""assertself.pidself.eqcs=[]T0=60*30SNAPSHOT_LABEL="raw_ephys_bad_channels"output_files=list(self.output_directory.glob(f'{SNAPSHOT_LABEL}*'))iflen(output_files)==4:returnoutput_filesself.output_directory.mkdir(exist_ok=True,parents=True)ifself.location!='server':self.histology_status=self.get_histology_status()electrodes=self.get_channels('electrodeSites',f'alf/{self.pname}')if'atlas_id'inelectrodes.keys():electrodes['ibr']=ismember(electrodes['atlas_id'],self.brain_regions.id)[1]electrodes['acronym']=self.brain_regions.acronym[electrodes['ibr']]electrodes['name']=self.brain_regions.name[electrodes['ibr']]electrodes['title']=self.histology_statuselse:electrodes=Nonensecs=1sr=Streamer(pid=self.pid,one=self.one,remove_cached=False,typ='ap')s0=T0*sr.fstsel=slice(int(s0),int(s0)+int(nsecs*sr.fs))# Important: remove sync channel from raw data, and transposeraw=sr[tsel,:-sr.nsync].Telse:electrodes=Noneap_file=next(self.session_path.joinpath('raw_ephys_data',self.pname).glob('*ap.*bin'),None)ifap_fileisnotNone:sr=spikeglx.Reader(ap_file)# If T0 is greater than recording length, take 500 sec before endifsr.rl<T0:T0=int(sr.rl-500)raw=sr[int((sr.fs*T0)):int((sr.fs*(T0+1))),:-sr.nsync].Telse:return[]ifsr.meta.get('NP2.4_shank',None)isnotNone:h=neuropixel.trace_header(sr.major_version,nshank=4)h=neuropixel.split_trace_header(h,shank=int(sr.meta.get('NP2.4_shank')))else:h=neuropixel.trace_header(sr.major_version,nshank=np.unique(sr.geometry['shank']).size)channel_labels,channel_features=voltage.detect_bad_channels(raw,sr.fs)_,eqcs,output_files=ephys_bad_channels(raw=raw,fs=sr.fs,channel_labels=channel_labels,channel_features=channel_features,h=h,channels=electrodes,title=SNAPSHOT_LABEL,destripe=True,save_dir=self.output_directory,br=self.brain_regions,pid_info=self.pid_label)self.eqcs=eqcsreturnoutput_files
[docs]defephys_bad_channels(raw,fs,channel_labels,channel_features,h=None,channels=None,title="ephys_bad_channels",save_dir=None,destripe=False,eqcs=None,br=None,pid_info=None,plot_backend='matplotlib'):nc,ns=raw.shaperl=ns/fsdefgain2level(gain):return10**(gain/20)*4*np.array([-1,1])iffs>=2600:# AP bandylim_rms=[0,100]ylim_psd_hf=[0,0.1]eqc_xrange=[450,500]butter_kwargs={'N':3,'Wn':300/fs*2,'btype':'highpass'}eqc_gain=-90eqc_levels=gain2level(eqc_gain)else:# we are working with the LFPylim_rms=[0,1000]ylim_psd_hf=[0,1]eqc_xrange=[450,950]butter_kwargs={'N':3,'Wn':np.array([2,125])/fs*2,'btype':'bandpass'}eqc_gain=-78eqc_levels=gain2level(eqc_gain)inoisy=np.where(channel_labels==2)[0]idead=np.where(channel_labels==1)[0]ioutside=np.where(channel_labels==3)[0]# display voltage traceseqcs=[]ifeqcsisNoneelseeqcs# butterworth, for display onlysos=scipy.signal.butter(**butter_kwargs,output='sos')butt=scipy.signal.sosfiltfilt(sos,raw)ifplot_backend=='matplotlib':_,axs=plt.subplots(1,2,gridspec_kw={'width_ratios':[.95,.05]},figsize=(16,9))eqcs.append(Density(butt,fs=fs,taxis=1,ax=axs[0],title='highpass',vmin=eqc_levels[0],vmax=eqc_levels[1]))ifdestripe:dest=voltage.destripe(raw,fs=fs,h=h,channel_labels=channel_labels)_,axs=plt.subplots(1,2,gridspec_kw={'width_ratios':[.95,.05]},figsize=(16,9))eqcs.append(Density(dest,fs=fs,taxis=1,ax=axs[0],title='destripe',vmin=eqc_levels[0],vmax=eqc_levels[1]))_,axs=plt.subplots(1,2,gridspec_kw={'width_ratios':[.95,.05]},figsize=(16,9))eqcs.append(Density((butt-dest),fs=fs,taxis=1,ax=axs[0],title='difference',vmin=eqc_levels[0],vmax=eqc_levels[1]))foreqcineqcs:y,x=np.meshgrid(ioutside,np.linspace(0,rl*1e3,500))eqc.ax.scatter(x.flatten(),y.flatten(),c='goldenrod',s=4)y,x=np.meshgrid(inoisy,np.linspace(0,rl*1e3,500))eqc.ax.scatter(x.flatten(),y.flatten(),c='r',s=4)y,x=np.meshgrid(idead,np.linspace(0,rl*1e3,500))eqc.ax.scatter(x.flatten(),y.flatten(),c='b',s=4)eqc.ax.set_xlim(*eqc_xrange)eqc.ax.set_ylim(0,nc)eqc.ax.set_ylabel('Channel index')eqc.ax.set_title(f'{pid_info}_{eqc.title}')set_axis_label_size(eqc.ax)ax=eqc.figure.axes[1]ifchannelsisnotNone:chn_title=channels.get('title',None)plot_brain_regions(channels['atlas_id'],brain_regions=br,display=True,ax=ax,title=chn_title)set_axis_label_size(ax)else:remove_axis_outline(ax)else:fromviewspikes.guiimportviewephys# noqaeqcs.append(viewephys(butt,fs=fs,channels=channels,title='highpass',br=br))ifdestripe:dest=voltage.destripe(raw,fs=fs,h=h,channel_labels=channel_labels)eqcs.append(viewephys(dest,fs=fs,channels=channels,title='destripe',br=br))eqcs.append(viewephys((butt-dest),fs=fs,channels=channels,title='difference',br=br))foreqcineqcs:y,x=np.meshgrid(ioutside,np.linspace(0,rl*1e3,500))eqc.ctrl.add_scatter(x.flatten(),y.flatten(),rgb=(164,142,35),label='outside')y,x=np.meshgrid(inoisy,np.linspace(0,rl*1e3,500))eqc.ctrl.add_scatter(x.flatten(),y.flatten(),rgb=(255,0,0),label='noisy')y,x=np.meshgrid(idead,np.linspace(0,rl*1e3,500))eqc.ctrl.add_scatter(x.flatten(),y.flatten(),rgb=(0,0,255),label='dead')eqcs[0].ctrl.set_gain(eqc_gain)eqcs[0].resize(1960,1200)eqcs[0].viewBox_seismic.setXRange(*eqc_xrange)eqcs[0].viewBox_seismic.setYRange(0,nc)eqcs[0].ctrl.propagate()# display featuresfig,axs=plt.subplots(2,2,sharex=True,figsize=[16,9],tight_layout=True)fig.suptitle(title)axs[0,0].plot(channel_features['rms_raw']*1e6)axs[0,0].set(title='rms',xlabel='channel number',ylabel='rms (uV)',ylim=ylim_rms)axs[1,0].plot(channel_features['psd_hf'])axs[1,0].plot(inoisy,np.minimum(channel_features['psd_hf'][inoisy],0.0999),'xr')axs[1,0].set(title='PSD above 80% Nyquist',xlabel='channel number',ylabel='PSD (uV ** 2 / Hz)',ylim=ylim_psd_hf)axs[1,0].legend=['psd','noisy']axs[0,1].plot(channel_features['xcor_hf'])axs[0,1].plot(channel_features['xcor_lf'])axs[0,1].plot(idead,channel_features['xcor_hf'][idead],'xb')axs[0,1].plot(ioutside,channel_features['xcor_lf'][ioutside],'xy')axs[0,1].set(title='Similarity',xlabel='channel number',ylabel='',ylim=[-1.5,0.5])axs[0,1].legend(['detrend','trend','dead','outside'])fscale,psd=scipy.signal.welch(raw*1e6,fs=fs)# units; uV ** 2 / Hzaxs[1,1].imshow(20*np.log10(psd).T,extent=[0,nc-1,fscale[0],fscale[-1]],origin='lower',aspect='auto',vmin=-50,vmax=-20)axs[1,1].set(title='PSD',xlabel='channel number',ylabel="Frequency (Hz)")axs[1,1].plot(idead,idead*0+fs/4,'xb')axs[1,1].plot(inoisy,inoisy*0+fs/4,'xr')axs[1,1].plot(ioutside,ioutside*0+fs/4,'xy')ifsave_dirisnotNone:output_files=[Path(save_dir).joinpath(f"{title}.png")]fig.savefig(output_files[0])foreqcineqcs:ifplot_backend=='matplotlib':output_files.append(Path(save_dir).joinpath(f"{title}_{eqc.title}.png"))eqc.figure.savefig(str(output_files[-1]))else:output_files.append(Path(save_dir).joinpath(f"{title}_{eqc.windowTitle()}.png"))eqc.grab().save(str(output_files[-1]))returnfig,eqcs,output_fileselse:returnfig,eqcs
[docs]defraw_destripe(raw,fs,t0,i_plt,n_plt,fig=None,axs=None,savedir=None,detect_badch=True,SAMPLE_SKIP=200,DISPLAY_TIME=0.05,N_CHAN=384,MIN_X=-0.00011,MAX_X=0.00011):''' :param raw: raw ephys data, Ns x Nc, x-axis: time (s), y-axis: channel :param fs: sampling freq (Hz) of the raw ephys data :param t0: time (s) of ephys sample beginning from session start :param i_plt: increment of plot to display image one (start from 0, has to be < n_plt) :param n_plt: total number of subplot on figure :param fig: figure handle :param axs: axis handle :param savedir: filename, including directory, to save figure to :param detect_badch: boolean, to detect or not bad channels :param SAMPLE_SKIP: number of samples to skip at origin of ephsy sample for display :param DISPLAY_TIME: time (s) to display :param N_CHAN: number of expected channels on the probe :param MIN_X: max voltage for color range :param MAX_X: min voltage for color range :return: fig, axs '''# Importfromibldspimportvoltagefromibllib.plotsimportDensity# Init figiffigisNoneoraxsisNone:fig,axs=plt.subplots(nrows=1,ncols=n_plt,figsize=(14,5),gridspec_kw={'width_ratios':4*n_plt})ifi_plt>len(axs)-1:# ErrorraiseValueError(f'The given increment of subplot ({i_plt+1}) 'f'is larger than the total number of subplots ({len(axs)})')[nc,ns]=raw.shapeifnc==N_CHAN:destripe=voltage.destripe(raw,fs=fs)X=destripe[:,:int(DISPLAY_TIME*fs)].TXs=X[SAMPLE_SKIP:].T# Remove artifact at beginningTplot=Xs.shape[1]/fs# PLOT RAW DATAd=Density(-Xs,fs=fs,taxis=1,ax=axs[i_plt],vmin=MIN_X,vmax=MAX_X)# noqaaxs[i_plt].set_ylabel('')axs[i_plt].set_xlim((0,Tplot*1e3))axs[i_plt].set_ylim((0,nc))# Init titletitle_plt=f't0 = {int(t0/60)} min'ifdetect_badch:# Detect and remove bad channels prior to spike detectionlabels,xfeats=voltage.detect_bad_channels(raw,fs)idx_badchan=np.where(labels!=0)[0]# Plot bad channels on raw datax,y=np.meshgrid(idx_badchan,np.linspace(0,Tplot*1e3,20))axs[i_plt].plot(y.flatten(),x.flatten(),'.k',markersize=1)# Append titletitle_plt+=f', n={len(idx_badchan)} bad ch'# Set titleaxs[i_plt].title.set_text(title_plt)else:axs[i_plt].title.set_text(f'CANNOT DESTRIPE, N CHAN = {nc}')# Amend some axis styleifi_plt>0:axs[i_plt].set_yticklabels('')# Fig layoutfig.tight_layout()ifsavedirisnotNone:fig.savefig(fname=savedir)returnfig,axs
[docs]defdlc_qc_plot(session_path,one=None,device_collection='raw_video_data',cameras=('left','right','body'),trials_collection='alf'):""" Creates DLC QC plot. Data is searched first locally, then on Alyx. Panels that lack required data are skipped. Required data to create all panels 'raw_video_data/_iblrig_bodyCamera.raw.mp4', 'raw_video_data/_iblrig_leftCamera.raw.mp4', 'raw_video_data/_iblrig_rightCamera.raw.mp4', 'alf/_ibl_bodyCamera.dlc.pqt', 'alf/_ibl_leftCamera.dlc.pqt', 'alf/_ibl_rightCamera.dlc.pqt', 'alf/_ibl_bodyCamera.times.npy', 'alf/_ibl_leftCamera.times.npy', 'alf/_ibl_rightCamera.times.npy', 'alf/_ibl_leftCamera.features.pqt', 'alf/_ibl_rightCamera.features.pqt', 'alf/rightROIMotionEnergy.position.npy', 'alf/leftROIMotionEnergy.position.npy', 'alf/bodyROIMotionEnergy.position.npy', 'alf/_ibl_trials.choice.npy', 'alf/_ibl_trials.feedbackType.npy', 'alf/_ibl_trials.feedback_times.npy', 'alf/_ibl_trials.stimOn_times.npy', 'alf/_ibl_wheel.position.npy', 'alf/_ibl_wheel.timestamps.npy', 'alf/licks.times.npy', :params session_path: Path to session data on disk :params one: ONE instance, if None is given, default ONE is instantiated :returns: Matplotlib figure """one=oneorONE()# hack for running on cortexlab local serverifone.alyx.base_url=='https://alyx.cortexlab.net':one=ONE(base_url='https://alyx.internationalbrainlab.org')data={}session_path=Path(session_path)# Load data for each cameraforcamincameras:# Load a single frame for each video# Check if video data is available locally,if yes, load a single framevideo_path=session_path.joinpath(device_collection,f'_iblrig_{cam}Camera.raw.mp4')ifvideo_path.exists():data[f'{cam}_frame']=get_video_frame(video_path,frame_number=5*60*SAMPLING[cam])[:,:,0]# If not, try to stream a frame (try three times)else:try:video_url=url_from_eid(one.path2eid(session_path),one=one)[cam]fortriesinrange(3):try:data[f'{cam}_frame']=get_video_frame(video_url,frame_number=5*60*SAMPLING[cam])[:,:,0]breakexceptException:iftries<2:tries+=1logger.info(f"Streaming {cam} video failed, retrying x{tries}")time.sleep(30)else:logger.warning(f"Could not load video frame for {cam} cam. Skipping trace on frame.")data[f'{cam}_frame']=NoneexceptKeyError:logger.warning(f"Could not load video frame for {cam} cam. Skipping trace on frame.")data[f'{cam}_frame']=None# Other camera associated dataforfeatin['dlc','times','features','ROIMotionEnergy']:# Check locally first, then try to load from alyx, if nothing works, set to Noneiffeat=='features'andcam=='body':# this doesn't exist for body camcontinuelocal_file=list(session_path.joinpath('alf').glob(f'*{cam}Camera.{feat}*'))iflen(local_file)>0:data[f'{cam}_{feat}']=alfio.load_file_content(local_file[0])else:alyx_ds=[dsfordsinone.list_datasets(one.path2eid(session_path))iff'{cam}Camera.{feat}'inds]iflen(alyx_ds)>0:data[f'{cam}_{feat}']=one.load_dataset(one.path2eid(session_path),alyx_ds[0])else:logger.warning(f"Could not load _ibl_{cam}Camera.{feat} some plots have to be skipped.")data[f'{cam}_{feat}']=None# Sometimes there is a file but the object is empty, set to Noneifdata[f'{cam}_{feat}']isnotNoneandlen(data[f'{cam}_{feat}'])==0:logger.warning(f"Object loaded from _ibl_{cam}Camera.{feat} is empty, some plots have to be skipped.")data[f'{cam}_{feat}']=None# If we have no frame and/or no DLC and/or no times for all cams, raise an error, something is really wrongassertany(data[f'{cam}_frame']isnotNoneforcamincameras),"No camera data could be loaded, aborting."assertany(data[f'{cam}_dlc']isnotNoneforcamincameras),"No DLC data could be loaded, aborting."assertany(data[f'{cam}_times']isnotNoneforcamincameras),"No camera times data could be loaded, aborting."# Load session level dataforalf_object,collectioninzip(['trials','wheel','licks'],[trials_collection,trials_collection,'alf']):try:data[f'{alf_object}']=alfio.load_object(session_path.joinpath(collection),alf_object)# load locallycontinueexceptALFObjectNotFound:passtry:# then try from alyxdata[f'{alf_object}']=one.load_object(one.path2eid(session_path),alf_object,collection=collection)exceptALFObjectNotFound:logger.warning(f"Could not load {alf_object} object, some plots have to be skipped.")data[f'{alf_object}']=None# Simplify and clean up trials dataifdata['trials']:data['trials']=pd.DataFrame({k:data['trials'][k]forkin['stimOn_times','feedback_times','choice','feedbackType']})# Discard nan events and too long trialsdata['trials']=data['trials'].dropna()data['trials']=data['trials'].drop(data['trials'][(data['trials']['feedback_times']-data['trials']['stimOn_times'])>10].index)# Make a list of panels, if inputs are missing, instead input a text to displaypanels=[]# Panel A, B, C: Trace on frameforcamincameras:ifdata[f'{cam}_frame']isnotNoneanddata[f'{cam}_dlc']isnotNone:panels.append((plot_trace_on_frame,{'frame':data[f'{cam}_frame'],'dlc_df':data[f'{cam}_dlc'],'cam':cam}))else:panels.append((None,f'Data missing\n{cam.capitalize()} cam trace on frame'))# If trials data is not there, we cannot plot any of the trial average plots, skip all remaining panelsifdata['trials']isNone:panels.extend([(None,'No trial data,\ncannot compute trial avgs')]*7)else:# Panel D: Motion energycamera_dict={}forcamincameras:# Remove cameras where we don't have motion energy AND camera timesd={'motion_energy':data.get(f'{cam}_ROIMotionEnergy'),'times':data.get(f'{cam}_times')}ifnotany(xisNoneforxind.values()):camera_dict[cam]=diflen(camera_dict)>0:panels.append((plot_motion_energy_hist,{'camera_dict':camera_dict,'trials_df':data['trials']}))else:panels.append((None,'Data missing\nMotion energy'))# Panel E: Wheel positionifdata['wheel']:panels.append((plot_wheel_position,{'wheel_position':data['wheel'].position,'wheel_time':data['wheel'].timestamps,'trials_df':data['trials']}))else:panels.append((None,'Data missing\nWheel position'))# Panel F, G: Paw speed and nose speed# Try if all data is there for left cam first, otherwise rightforcamin['left','right']:fail=Falseif(data[f'{cam}_dlc']isnotNoneanddata[f'{cam}_times']isnotNoneandlen(data[f'{cam}_times'])>=len(data[f'{cam}_dlc'])):breakfail=Trueifnotfail:paw='r'ifcam=='left'else'l'panels.append((plot_speed_hist,{'dlc_df':data[f'{cam}_dlc'],'cam_times':data[f'{cam}_times'],'trials_df':data['trials'],'feature':f'paw_{paw}','cam':cam}))panels.append((plot_speed_hist,{'dlc_df':data[f'{cam}_dlc'],'cam_times':data[f'{cam}_times'],'trials_df':data['trials'],'feature':'nose_tip','legend':False,'cam':cam}))else:panels.extend([(None,'Data missing or corrupt\nSpeed histograms')]*2)# Panel H and I: Lick plotsifdata['licks']anddata['licks'].times.shape[0]>0:panels.append((plot_lick_hist,{'lick_times':data['licks'].times,'trials_df':data['trials']}))panels.append((plot_lick_raster,{'lick_times':data['licks'].times,'trials_df':data['trials']}))else:panels.extend([(None,'Data missing\nLicks plots')foriinrange(2)])# Panel J: pupil plot# Try if all data is there for left cam first, otherwise rightforcamin['left','right']:fail=Falseif(data.get(f'{cam}_times')isnotNoneanddata.get(f'{cam}_features')isnotNoneandlen(data[f'{cam}_times'])>=len(data[f'{cam}_features'])andnotnp.all(np.isnan(data[f'{cam}_features'].pupilDiameter_smooth))):breakfail=Trueifnotfail:panels.append((plot_pupil_diameter_hist,{'pupil_diameter':data[f'{cam}_features'].pupilDiameter_smooth,'cam_times':data[f'{cam}_times'],'trials_df':data['trials'],'cam':cam}))else:panels.append((None,'Data missing or corrupt\nPupil diameter'))# Plottingplt.rcParams.update({'font.size':10})fig=plt.figure(figsize=(17,10))fori,panelinenumerate(panels):ax=plt.subplot(2,5,i+1)ax.text(-0.1,1.15,ascii_uppercase[i],transform=ax.transAxes,fontsize=16,fontweight='bold')# Check if there was in issue with inputs, if yes, print the respective textifpanel[0]isNone:ax.text(.5,.5,panel[1],color='r',fontweight='bold',fontsize=12,horizontalalignment='center',verticalalignment='center',transform=ax.transAxes)plt.axis('off')else:try:panel[0](**panel[1])exceptException:logger.error(f'Error in {panel[0].__name__}\n'+traceback.format_exc())ax.text(.5,.5,f'Error while plotting\n{panel[0].__name__}',color='r',fontweight='bold',fontsize=12,horizontalalignment='center',verticalalignment='center',transform=ax.transAxes)plt.axis('off')plt.tight_layout(rect=[0,0.03,1,0.95])returnfig