Package: npyx
NeuroPyxels: loading, processing and plotting Neuropixels data in PythonNeuroPyxels(npyx) is a python library built for electrophysiologists using Neuropixels electrodes. This package results from the needs of a pythonist who really did not want to transition to MATLAB to work with Neuropixels: it features a suite of core utility functions for loading, processing and plotting Neuropixels data.❓Any questions or issues?:Create a github issueto get support, or create apull request. Alternatively, you can emailus: maximebeaujeanroch047[at]gmail[dot]com. You can also use theNeuropixels slack workgroup.⬇️ Installation🤗 Support and citing🔍️ Documentation💡 Design philosophy📁 Directory structure👉 Common use casesLoad recording metadataLoad synchronization channelGet good units from datasetLoad spike times from unit uLoad waveforms from unit uCompute auto/crosscorrelogram between 2 unitsPlot waveform and crosscorrelograms of unit uPreprocess your waveforms and spike trainsPlot chunk of raw data with overlaid unitsPlot peri-stimulus time histograms across neurons and conditionsMerge datasets acquired on two probes simultaneously⭐ Bonus: matplotlib plot prettifier (mplp)⬇️ Installation:We recommend using a conda environment. Pre-existing packages on a python installation might be incompatible with npyx and break your installation. You can find instructions on setting up a conda environmenthere.condacreate-nmy_envpython=3.10condaactivatemy_envpipinstallnpyx# optionally (see 'Dealing with cupy' section below):condainstall-cconda-forgecupycudatoolkit=11.0# test installation:python-c'import npyx'# should not return any errorAdvanced installationif you want the very latest version:condacreate-nmy_envpython=3.10
condaactivatemy_env
pipinstallgit+https://github.com/m-beau/NeuroPyxels@master# optionally (see 'Dealing with cupy' section below):condainstall-cconda-forgecupycudatoolkit=11.0# test installation:python-c'import npyx'# should not return any errorIf you want to edit npyx locally and eventually contribute:💡 Tip: in an ipython/jupyter session, use%load_ext autoreloadthen%autoreload 2to make your local edits active in your session without having to restart your kernel. Amazing for development.condacreate-nmy_envpython=3.10
condaactivatemy_envcdpath/to/save_dir# any directory where your code will be accessible by your editor and safe. NOT downloads folder.gitclonehttps://github.com/m-beau/NeuroPyxelscdNeuroPyxels
pipinstall.# this will create an egg link to save_dir, which means that you do not need to reinstall the package each time you edit it (e.g. after pulling from github).# optionally (see 'Dealing with cupy' section below):condainstall-cconda-forgecupycudatoolkit=11.0# test installation:python-c'import npyx'# should not return any errorand pull every now and then:cdpath/to/save_dir/NeuroPyxels
gitpull# And that's it, thanks to the egg link no need to reinstall the package!Npyx supports Python >=3.7.Dealing with cupy (GPU shenanigans)To run some preprocessing functions, you will need NVIDIA drivers and cuda-toolkit installed on your computer. It is a notorious source of bugs. To test your CUDA installation do the following:nvidia-smi# Should show how much your GPU is being used right nownvcc# This is the CUDA compilerIf it doesn't work, try up/downgrading the version of cudatoolkit installed:# check the current versioncondaactivatemy_env
condalistcudatoolkit# E.g. install version 10.0condaactivatemy_env
condaremovecupy,cudatoolkit
condainstall-cconda-forgecupycudatoolkit=10.0Test installationYou can use the built-in unit testing function 'test_npyx' to make sure that npyx core functions run smoothly, all at once.fromnpyx.testingimporttest_npyx# any spike sorted recording compatible with phy# (e.g. kilosort output)dp='datapath/to/myrecording'test_npyx(dp)# if any test fails, re-run them with the following to print the error log, and try to fix it or post an issue on github:test_npyx(dp,raise_error=True)--- npyx version 2.3.4 unit testing initiated, on directory /media/maxime/AnalysisSSD/test_dataset_artefact...--- Successfully ran 'read_metadata' from npyx.inout.--- Successfully ran 'get_npix_sync' from npyx.inout.--- Successfully ran 'get_units' from npyx.gl.--- Successfully ran 'ids' from npyx.spk_t.--- Successfully ran 'trn' from npyx.spk_t.--- Successfully ran 'trn_filtered' from npyx.spk_t.--- Successfully ran 'wvf' from npyx.spk_wvf.--- Successfully ran 'wvf_dsmatch' from npyx.spk_wvf.--- Successfully ran 'get_peak_chan' from npyx.spk_wvf.--- Successfully ran 'templates' from npyx.spk_wvf.--- Successfully ran 'ccg' from npyx.corr.--- Successfully ran 'plot_wvf' from npyx.plot.--- Successfully ran 'plot_ccg' from npyx.plot.--- Successfully ran 'plot_raw' from npyx.plot.(bunch of plots...):warning: Known installation issuescannot import numba.core hence cannot import npyxOlder versions of numba did not feature the .core submodule. If you get this error, you are probably running a too old version of numba. Make sure that you have installed npyx in a fresh conda environment if that happens to you. If you still get an error, check that numba is not installed in your root directory.pip uninstall numba
conda activate my_env
pip uninstall numba
pip install numbacore dumped when importingThis seems to be an issue related to PyQt5 required by opencv (opencv-python).
Solution:# activate npyx environment first
pip uninstall opencv-python
pip install opencv-python
# pip install other missing dependenciesFull log:In [1]: from npyx import *
In [2]: QObject::moveToThread: Current thread (0x5622e1ea6800) is not the object's thread (0x5622e30e86f0).
Cannot move to target thread (0x5622e1ea6800)

qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "/home/maxime/miniconda3/envs/npyx/lib/python3.7/site-packages/cv2/qt/plugins" even though it was found.
This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem.

Available platform plugins are: xcb, eglfs, linuxfb, minimal, minimalegl, offscreen, vnc, wayland-egl, wayland, wayland-xcomposite-egl, wayland-xcomposite-glx, webgl.

Aborted (core dumped)I think I installed everything properly, but npyx is not found if I run 'python -c "import npyx" '!Typically:Traceback(mostrecentcalllast):File"<stdin>",line1,in<module>
ModuleNotFoundError:Nomodulenamed'npyx'Make sure that the python installation that you are using is indeed the version of your new environment.To do so, in your terminal, run "which python" on linux/mac or "where python" on windows: the output should be the path to the right environment e.g. "/home/.../anaconda/envs/npyx/bin/python". If it isn't, try to deactivate/reactivate your conda environment, or make sure you do not have conflicting python installations on your machine.🤗 Support and citingIf you find Neuropyxels useful in your work, we kindly request that you cite:Maxime Beau, Federico D'Agostino, Ago Lajko, Gabriela Martínez, Michael Häusser & Dimitar Kostadinov. (2021). NeuroPyxels: loading, processing and plotting Neuropixels data in python. Zenodo.https://doi.org/10.5281/zenodo.5509733You can additionally star this repo using the top-right star button to help it gain more visibility.Cheers!🔍️ Documentation:Npyx works with the data formatting employed bySpikeGLXandOpenEphys(binary data and meta data) used in combination withPhy-compatible spike-sorters (Kilosort,SpyKING CIRCUS...).Any dataset compatible with phy can also be analyzed with npyx, in essence.💡 Design philosophyMemoizationNpyx is fast because it rarely computes the same thing twice by relying heavily on caching (memoization as purists like to call it) - in the background, it saves most relevant outputs (spike trains, waveforms, correlograms...) atnpix_dataset/npyxMemory, from where they are simply reloaded if called again.An important argument controlling this behaviour isagain(boolean), by default set to False: if True, most npyx functions will recompute their output rather than loading it from npyxMemory. This is important to be aware of this behaviour, as it can lead to mind boggling bugs. For instance, if you load a spike train then re-curate your dataset, e.g. by splitting unit 56 into 504 and 505, the train of the old 'unit 56' will still exist at kilosort_dataset/npyxMemory and you will remain able to load it even though the unit is gone!Ubiquitous argumentsMost npyx functions take at least one input:dp, which is the path to your Neuropixels-phy dataset. You can find afull description of the structure of such datasetson the phy documentation.Other typical parameters are:verbose(whether to print a bunch of informative messages, useful when debugging),saveFig(boolean) andsaveDir(whether to save the figure in saveDir for plotting functions).Importantly,dpcan also be the path to amerged dataset, generated withnpyx.merge_datasets()- every function will run as smoothly on merged datasets as on any regular dataset. See below for more details.Minimal and modular reliance of spike-sorter outputEvery function requires the filesmyrecording.ap.meta/myrecording.oebin(metadata from SpikeGLX/OpenEphys),params.py,spike_times.npyandspike_clusters.npy.If you have started spike sorting,cluster_groups.tsvwill also be required obviously (will be created filled with 'unsorted' groups if none is found).Then, specific functions will require specific files: loading waveforms withnpyx.spk_wvf.wvfor extracting your sync channel withnpyx.io.get_npix_syncrequire the raw datamyrecording.ap.bin,npyx.spk_wvf.templatesthe filestemplates.npyandspike_templates.npy, and so on. This allows you to only transfer the strictly necassary files for your use case from a machine to the next: for instance, if you only want to make behavioural analysis of spike trains but do not care about the waveforms, you can runget_npix_syncon a first machine (which will generate async_chanfolder containing extracted onsets/offsets from the sync channel(s)), then exclusively transfer thedataset/sync_chan/folder along withspike_times.npyandspike_clusters.npy(all very light files) on another computer and analyze your data there seemlessly.📁 Directory structureThedpparameter of all npyx functions must be theabsolute path tomyrecordingbelow.For SpikeGLX recordings:myrecording/
  myrecording.ap.meta
  params.py
  spike_times.npy
  spike_clusters.npy
  cluster_groups.tsv # optional, if manually curated with phy
  myrecording.ap.bin # optional, if wanna plot waveforms

  # other kilosort/spyking circus outputs hereFor Open-Ephys recordings:myrecording/
  myrecording.oebin
  params.py
  spike_times.npy
  spike_clusters.npy
  cluster_groups.tsv # if manually curated with phy

  # other spikesorter outputs here

  continuous/
    Neuropix-PXI-100.somethingsomething (1, AP...)/
      continuous.dat # optional, if wanna plot waveforms
    Neuropix-PXI-100.somethingsomething (2, LFP...)/
      continuous.dat # optional, if want to plot LFP with plot_raw

  events/
    Neuropix-PXI-100.somethingsomething (1, AP...)/
      TTL somethingelse/
        timestamps.npy # optional, if need to get synchronyzation channel to load with get_npix_sync e.g. to merge datasets
    Neuropix-PXI-100.somethingsomething (2, LFP...)/
      TTL somethingelse/
        timestamps.npy # same timestamps for LFP channel👉 Common use casesLoad recording metadatafromnpyximport*dp='datapath/to/myrecording'# load contents of .lf.meta and .ap.meta or .oebin files as python dictionnary.# The metadata of the high and lowpass filtered files are in meta['highpass'] and meta['lowpass']# Quite handy to get probe version, sampling frequency, recording length etcmeta=read_metadata(dp)# works for spikeGLX (contents of .meta files) and open-ephys (contents of .oebin file)Load synchronization channelfromnpyx.inoutimportget_npix_sync# star import is sufficient, but I like explicit imports!# If SpikeGLX: slow the first time, then super fastonsets,offsets=get_npix_sync(dp,filt_key='highpass')# works for spikeGLX (extracted from .ap.bin file) and open-ephys (/events/..AP/TTL/timestamps.npy)# onsets/offsets are dictionnaries# keys: ids of sync channel where a TTL was detected (0,1,2... for spikeGLX, name of TTL folders in events/..AP for openephys),# values: times of up (onsets) or down (offsets) threshold crosses, in seconds.Preprocess binary dataMakes a preprocessed copy of the binary file in dp, moves original binary file at dp/original_data
This will be as fast as literally copying your file, with a decent GPU!fromnpyx.inoutimportpreprocess_binary_file# star import is sufficient, but I like explicit imports!# can perform bandpass filtering (butterworth 3 nodes) and median subtraction (aka common average referenceing, CAR)# in the future: ADC realignment (like CatGT), whitening, spatial filtering (experimental).filtered_fname=preprocess_binary_file(dp,filt_key='ap',median_subtract=True,f_low=None,f_high=300,order=3,verbose=True)Get good units from datasetfromnpyx.glimportget_unitsunits=get_units(dp,quality='good')Load spike times from unit ufromnpyx.spk_timporttrnu=234t=trn(dp,u)# gets all spikes from unit 234, in samplesLoad waveforms from unit ufromnpyx.inoutimportread_spikeglx_metafromnpyx.spk_timportids,trnfromnpyx.spk_wvfimportget_peak_chan,wvf,templates# returns a random sample of 100 waveforms from unit 234, in uV, across 384 channelswaveforms=wvf(dp,u)# return array of shape (n_waves, n_samples, n_channels)=(100, 82, 384) by defaultwaveforms=wvf(dp,u,n_waveforms=1000,t_waveforms=90)# now 1000 random waveforms, 90 samples=3ms long# Get the unit peak channel (channel with the biggest amplitude)peak_chan=get_peak_chan(dp,u)# extract the waveforms located on peak channelw=waves[:,:,peak_chan]# Extract waveforms of spikes occurring between# 0-100s and 300-400s in the recording,# because that's when your mouse sneezedwaveforms=wvf(dp,u,periods=[(0,100),(300,400)])# alternatively, longer but more flexible:fs=meta['highpass']['sampling_rate']t=trn(dp,u)/fs# convert in s# get ids of unit u: all spikes have a unique index in the dataset,# which is their rank sorted by time (as in spike_times.npy)u_ids=ids(dp,u)ids=ids(dp,u)[(t>900)&(t<1000)]mask=(t<100)|((t>300)&(t<400))waves=wvf(dp,u,spike_ids=u_ids[mask])# If you want to load the templates instead (faster and does not require binary file):temp=templates(dp,u)# return array of shape (n_templates, 82, n_channels)Compute auto/crosscorrelogram between 2 unitsfromnpyx.corrimportccg,ccg_stack# returns ccg between 234 and 92 with a binsize of 0.2 and a window of 80c=ccg(dp,[234,92],cbin=0.2,cwin=80)# Only using spikes from the first and third minutes of recordingc=ccg(dp,[234,92],cbin=0.2,cwin=80,periods=[(0,60),(120,180)])# better, compute a big stack of crosscorrelograms with a given name# The first time, CCGs will be computed in parallel using all the available CPU cores# and it will be saved in the background and, reloadable instantaneously in the futuresource_units=[1,2,3,4,5]target_units=[6,7,8,9,10]c_stack=ccg_stack(dp,source_units,target_units,0.2,80,name='my_relevant_ccg_stack')c_stack=ccg_stack(dp,name='my_relevant_ccg_stack')# will work to reaload in the futurePlot waveform and crosscorrelogram of unit u# all plotting functions return matplotlib figuresfromnpyx.plotimportplot_wvf,get_peak_chanu=234# plot waveform, 2.8ms around templates center, on 16 channels around peak channel# (the peak channel is found automatically, no need to worry about finding it)fig=plot_wvf(dp,u,Nchannels=16,t_waveforms=2.8)# But if you wished to get it, simply runpeakchannel=get_peak_chan(dp,u)# plot ccg between 234 and 92# as_grid also plot the autocorrelogramsfig=plot_ccg(dp,[u,92],cbin=0.2,cwin=80,as_grid=True)Preprocess your waveforms (drift-shift-matching) and spike trains (detect periods with few false positive/negative)# all plotting functions return matplotlib figuresfromnpyx.spk_wvfimportwvf_dsmatchfromnpyx.spk_timporttrn_filtered# wvf_dsmatch subselect 'best looking' waveforms# by first matching them by drift state (Z, peak channel and XY, amplitude on peak channel)# then shifting them around to realign them (using the crosscorr of its whole spatial footprint)# on the plot, black is the original waveform as it would be plotted in phy,# green is drift-matched, red is drift-shift matchedw_preprocessed=wvf_dsmatch(dp,u,plot_debug=True)# trn_filtered clips the recording in 10s (default) chunks# and estimates the false positive/false negative spike sporting rates on such chunks# before masking out spikes occurring inside 'bad chunks',# defined as chunks with too high FP OR FN rates (5% and 5% by default)t_preprocessed=trn_filtered(dp,u,plot_debug=True)Plot chunk of raw data with overlaid unitsunits=[1,2,3,4,5,6]channels=np.arange(70,250)# raw data are whitened, high-pass filtered and median-subtracted by default - parameters are explicit belowplot_raw_units(dp,times=[0,0.130],units=units,channels=channels,colors=['orange','red','limegreen','darkgreen','cyan','navy'],lw=1.5,offset=450,figsize=(6,16),Nchan_plot=10,med_sub=1,whiten=1,hpfilt=1)Plot peri-stimulus time histograms across neurons and conditions# Explore responses of 3 neurons to 4 categories of events:fs=30000# Hzunits=[1,2,3]trains=[trn(dp,u)/fsforuinunits]# make list of trains of 3 unitstrains_str=units# can give specific names to units here, show on the left of each rowevents=[licks,sneezes,visual_stimuli,auditory_stimuli]# get events corresponding to 4 conditionsevents_str=['licking','sneezing','visual_stim','auditory_stim']# can give specific names to events here, show above each columnevents_col='batlow'# colormap from which the event colors will be drawnfig=summary_psth(trains,trains_str,events,events_str,psthb=10,psthw=[-750,750],zscore=0,bsl_subtract=False,bsl_window=[-3000,-750],convolve=True,gsd=2,events_toplot=[0],events_col=events_col,trains_col_groups=trains_col_groups,title=None,saveFig=0,saveDir='~/Downloads',_format='pdf',figh=None,figratio=None,transpose=1,as_heatmap=False,vmin=None,center=None,vmax=None,cmap_str=None)Merge datasets acquired on two probes simultaneously# The three recordings need to include the same sync channel.fromnpyx.mergerimportmerge_datasetsdps=['same_folder/lateralprobe_dataset','same_folder/medialprobe_dataset','same_folder/anteriorprobe_dataset']probenames=['lateral','medial','anterior']dp_dict={p:dpforp,dpinzip(dps,probenames)}# This will merge the 3 datasets (only relevant information, not the raw data) in a new folder at# dp_merged: same_folder/merged_lateralprobe_dataset_medialprobe_dataset_anteriorprobe_dataset# where all npyx functions can smoothly run.# The only difference is that units now need to be called as floats,# of format u.x (u=unit id, x=dataset id [0-2]).# lateralprobe, medial probe and anteriorprobe x will be respectively 0,1 and 2.dp_merged,datasets_table=merge_datasets(dp_dic)---Mergeddata(from2dataset(s))willbesavedhere:/same_folder/merged_lateralprobe_dataset_medialprobe_dataset_anteriorprobe_dataset.---Loadingspiketrainsof2datasets...syncchannelextractiondirectoryfound:/same_folder/lateralprobe_dataset/sync_chanDatafoundonsyncchannels:chan2(201events).chan4(16events).chan5(175events).chan6(28447events).chan7(93609events).Whichchannelshallbeusedtosynchronizeprobes?>>>7syncchannelextractiondirectoryfound:/same_folder/medialprobe_dataset/sync_chanDatafoundonsyncchannels:chan2(201events).chan4(16events).chan5(175events).chan6(28447events).chan7(93609events).Whichchannelshallbeusedtosynchronizeprobes?>>>7syncchannelextractiondirectoryfound:/same_folder/anteriorprobe_dataset/sync_chanDatafoundonsyncchannels:chan2(201events).chan4(16events).chan5(175events).chan6(28194events).chan7(93609events).Whichchannelshallbeusedtosynchronizeprobes?>>>7---Aligningspiketrainsof2datasets...Morethan50syncsignalsfound-forperformancereasons,sub-samplingto50homogenoeouslyspacedsyncsignalstoaligndata.50synceventsusedforalignement-start-enddriftof-3080.633ms---Mergedspike_timesandspike_clusterssavedat/same_folder/merged_lateralprobe_dataset_medialprobe_dataset_anteriorprobe_dataset.-->Mergesuccessful!Useafloatu.xinanynpyxfunctiontocallunitufromdatasetx:-u.0fordatasetlateralprobe_dataset,-u.1fordatasetmedialprobe_dataset,-u.2fordatasetanteriorprobe_dataset.Now any npyx function runs on the merged dataset!
Under the hood, it will create amerged_dataset_dataset1_dataset2/npyxMemoryfolder to save any data computed across dataframes, but will use the originaldataset1/npyxMemoryfolder to save data related to this dataset exclusively (e.g. waveforms). Hence, there is no redundancy: space and time are saved.This is also why it is primordial that you do not move your datatasets from their original paths after merging them - else, functions ran on merged_dataset1_dataset2 will not know where to go fetch the data! They refer to the paths inmerged_dataset_dataset1_dataset2/datasets_table.csv. If you really need to, you can move your datasets but do not forget to edit this file accordingly.# These will work!t=trn(dp_merged,92.1)# get spikes of unit 92 in dataset 1 i.e. medialprobefig=plot_ccg(dp_merged,[10.0,92.1,cbin=0.2,cwin=80])# compute CCG between 2 units across datasetsPS - The spike times are aligned across datasets by modelling the drift between the clocks of the neuropixels headstages linearly: TTL probe 1 = a * TTL probe 1 + b (if a!=1, there is drift between the clocks), so spiketimes_probe2_aligned_to_probe1  = a * spiketimes_probe2 + b⭐ Bonus: matplotlib plot prettifierfromnpyx.plotimportget_ncolors_cmap# allows you to easily extract the (r,g,b) tuples from a matplotlib or crameri colormap# to use them in other plots!colors=get_ncolors_cmap('coolwarm',10,plot=1)colors=get_ncolors_cmap('viridis',10,plot=1)# in a jupyter notebook, will also plot he HTML colormap:fromnpyx.plotimportmplpimportmatplotlib.pyplotasplt# mplp() will turn any matplotlib plot into something you can work with.# fed up googling around and landing on stack overflow to tweak your figures?# just read mplp parameters, they are self-explanatory!df1=pd.load("my_dataframe.csv")# Seaborn figure (seaborn is simply a wrapper for matplotlib):fig=plt.figure()sns.scatterplot(data=df1,x='popsync',y='depth',hue='mean_popsync',palette='plasma',alpha=1,linewidth=1,edgecolor='black')# Same figure, tweaked with mplp():fig=plt.figure()sns.scatterplot(data=df1,x='popsync',y='depth',hue='mean_popsync',palette='plasma',alpha=1,linewidth=1,edgecolor='black')mplp(figsize=(3,3),title="My title",ylim=[-10,-2],xlim=[-40,60],xlabel="My x label (rotated ticks)",ylabel="My y label",xtickrot=45,hide_legend=True,colorbar=True,vmin=df['mean_popsync'].min(),vmax=df['mean_popsync'].max(),cbar_w=0.03,cbar_h=0.4,clabel="My colorbar label\n(no more ugly legend!)",cmap="plasma",clabel_s=16,cticks_s=14,ticklab_s=16,saveFig=saveFig,saveDir=saveDir,figname=f"popsync_{pair}")