Skip to content

Commit 5ca5764

Browse files
committed
Merge branch 'release/2.3.0'
2 parents 5dee8e4 + a7ce057 commit 5ca5764

File tree

19 files changed

+1337
-221
lines changed

19 files changed

+1337
-221
lines changed

brainbox/ephys_plots.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import numpy as np
22
from matplotlib import cm
3-
3+
import matplotlib.pyplot as plt
44
from brainbox.plot_base import (ImagePlot, ScatterPlot, ProbePlot, LinePlot, plot_line,
55
plot_image, plot_probe, plot_scatter, arrange_channels2banks)
66
from brainbox.processing import bincount2D, compute_cluster_average
7+
from ibllib.atlas.regions import BrainRegions
78

89

910
def image_lfp_spectrum_plot(lfp_power, lfp_freq, chn_coords, chn_inds, freq_range=(0, 300),
@@ -372,3 +373,100 @@ def line_amp_plot(spike_amps, spike_depths, spike_times, chn_coords, d_bin=10, d
372373
fig, ax = plot_line(data.convert2dict())
373374
return data.convert2dict(), fig, ax
374375
return data
376+
377+
378+
def plot_brain_regions(channel_ids, channel_depths=None, brain_regions=None, display=True, ax=None):
379+
"""
380+
Plot brain regions along probe, if channel depths is provided will plot along depth otherwise along channel idx
381+
:param channel_ids: atlas ids for each channel
382+
:param channel_depths: depth along probe for each channel
383+
:param brain_regions: BrainRegions object
384+
:param display: whether to output plot
385+
:param ax: axis to plot on
386+
:return:
387+
"""
388+
389+
if channel_depths is not None:
390+
assert channel_ids.shape[0] == channel_depths.shape[0]
391+
392+
br = brain_regions or BrainRegions()
393+
394+
region_info = br.get(channel_ids)
395+
boundaries = np.where(np.diff(region_info.id) != 0)[0]
396+
boundaries = np.r_[0, boundaries, region_info.id.shape[0] - 1]
397+
398+
regions = np.c_[boundaries[0:-1], boundaries[1:]]
399+
if channel_depths is not None:
400+
regions = channel_depths[regions]
401+
region_labels = np.c_[np.mean(regions, axis=1), region_info.acronym[boundaries[1:]]]
402+
region_colours = region_info.rgb[boundaries[1:]]
403+
404+
if display:
405+
if ax is None:
406+
fig, ax = plt.subplots()
407+
408+
for reg, col in zip(regions, region_colours):
409+
height = np.abs(reg[1] - reg[0])
410+
color = col / 255
411+
ax.bar(x=0.5, height=height, width=1, color=color, bottom=reg[0], edgecolor='w')
412+
ax.set_yticks(region_labels[:, 0].astype(int))
413+
ax.yaxis.set_tick_params(labelsize=8)
414+
ax.get_xaxis().set_visible(False)
415+
ax.set_yticklabels(region_labels[:, 1])
416+
ax.spines['right'].set_visible(False)
417+
ax.spines['top'].set_visible(False)
418+
ax.spines['bottom'].set_visible(False)
419+
420+
return fig, ax
421+
else:
422+
return regions, region_labels, region_colours
423+
424+
425+
def plot_cdf(spike_amps, spike_depths, spike_times, n_amp_bins=10, d_bin=40, amp_range=None, d_range=None,
426+
display=False, cmap='hot'):
427+
"""
428+
Plot cumulative amplitude of spikes across depth
429+
:param spike_amps:
430+
:param spike_depths:
431+
:param spike_times:
432+
:param n_amp_bins: number of amplitude bins to use
433+
:param d_bin: the value of the depth bins in um (default is 40 um)
434+
:param amp_range: amp range to use [amp_min, amp_max], if not given automatically computed from spike_amps
435+
:param d_range: depth range to use, by default [0, 3840]
436+
:param display: whether or not to display plot
437+
:param cmap:
438+
:return:
439+
"""
440+
441+
amp_range = amp_range or np.quantile(spike_amps, (0, 0.9))
442+
amp_bins = np.linspace(amp_range[0], amp_range[1], n_amp_bins)
443+
d_range = d_range or [0, 3840]
444+
depth_bins = np.arange(d_range[0], d_range[1] + d_bin, d_bin)
445+
t_bin = np.max(spike_times)
446+
447+
def histc(x, bins):
448+
map_to_bins = np.digitize(x, bins) # Get indices of the bins to which each value in input array belongs.
449+
res = np.zeros(bins.shape)
450+
451+
for el in map_to_bins:
452+
res[el - 1] += 1 # Increment appropriate bin.
453+
return res
454+
455+
cdfs = np.empty((len(depth_bins) - 1, n_amp_bins))
456+
for d in range(len(depth_bins) - 1):
457+
spikes = np.bitwise_and(spike_depths > depth_bins[d], spike_depths <= depth_bins[d + 1])
458+
h = histc(spike_amps[spikes], amp_bins) / t_bin
459+
hcsum = np.cumsum(h[::-1])
460+
cdfs[d, :] = hcsum[::-1]
461+
462+
cdfs[cdfs == 0] = np.nan
463+
464+
data = ImagePlot(cdfs.T, x=amp_bins * 1e6, y=depth_bins[:-1], cmap=cmap)
465+
data.set_labels(title='Cumulative Amplitude', xlabel='Spike amplitude (uV)',
466+
ylabel='Distance from probe tip (um)', clabel='Firing Rate (Hz)')
467+
468+
if display:
469+
fig, ax = plot_image(data.convert2dict(), fig_kwargs={'figsize': [3, 7]})
470+
return data.convert2dict(), fig, ax
471+
472+
return data

brainbox/io/one.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,24 @@ def _channels_traj2bunch(xyz_chans, brain_atlas):
9595
return channels
9696

9797

98+
def _channels_bunch2alf(channels):
99+
channels_ = {
100+
'mlapdv': np.c_[channels['x'], channels['y'], channels['z']] * 1e6,
101+
'brainLocationIds_ccf_2017': channels['atlas_id'],
102+
'localCoordinates': np.c_[channels['lateral_um'], channels['axial_um']]}
103+
return channels_
104+
105+
98106
def _channels_alf2bunch(channels, brain_regions=None):
99107
# reformat the dictionary according to the standard that comes out of Alyx
100108
channels_ = {
101109
'x': channels['mlapdv'][:, 0].astype(np.float64) / 1e6,
102110
'y': channels['mlapdv'][:, 1].astype(np.float64) / 1e6,
103111
'z': channels['mlapdv'][:, 2].astype(np.float64) / 1e6,
104112
'acronym': None,
105-
'atlas_id': channels['brainLocationIds_ccf_2017']
113+
'atlas_id': channels['brainLocationIds_ccf_2017'],
114+
'axial_um': channels['localCoordinates'][:, 1],
115+
'lateral_um': channels['localCoordinates'][:, 0],
106116
}
107117
if brain_regions:
108118
channels_['acronym'] = brain_regions.get(channels_['atlas_id'])['acronym']
@@ -207,24 +217,33 @@ def _load_channels_locations_from_disk(eid, collection=None, one=None, revision=
207217
channels_aligned = one.load_object(eid, 'channels', collection=ac_collection)
208218
channels[probe] = channel_locations_interpolation(channels_aligned, channels[probe])
209219
# only have to reformat channels if we were able to load coordinates from disk
210-
channels[probe] = _channels_alf2bunch(channels[probe], brain_regions=brain_regions)
220+
channels[probe] = _channels_alf2bunch(channels[probe], brain_regions=brain_regions)
211221
return channels
212222

213223

214-
def channel_locations_interpolation(channels_aligned, channels):
224+
def channel_locations_interpolation(channels_aligned, channels, brain_regions=None):
215225
"""
216226
oftentimes the channel map for different spike sorters may be different so interpolate the alignment onto
217227
if there is no spike sorting in the base folder, the alignment doesn't have the localCoordinates field
218228
so we reconstruct from the Neuropixel map. This only happens for early pykilosort sorts
219229
:param channels_aligned: Bunch or dictionary of aligned channels containing at least keys
220-
'mlapdv' and 'brainLocationIds_ccf_2017' - those are the guide for the interpolation
230+
'localCoordinates', 'mlapdv' and 'brainLocationIds_ccf_2017'
231+
OR
232+
'x', 'y', 'z', 'acronym', 'axial_um'
233+
those are the guide for the interpolation
221234
:param channels: Bunch or dictionary of aligned channels containing at least keys 'localCoordinates'
222-
:return: Bunch or dictionary of channels with extra keys 'mlapdv' and 'brainLocationIds_ccf_2017'
235+
:param brain_regions: None (default) or ibllib.atlas.BrainRegions object
236+
if None will return a dict with keys 'localCoordinates', 'mlapdv', 'brainLocationIds_ccf_2017
237+
if a brain region object is provided, outputts a dict with keys
238+
'x', 'y', 'z', 'acronym', 'atlas_id', 'axial_um', 'lateral_um'
239+
:return: Bunch or dictionary of channels with brain coordinates keys
223240
"""
224241
nch = channels['localCoordinates'].shape[0]
242+
if set(['x', 'y', 'z']).issubset(set(channels_aligned.keys())):
243+
channels_aligned = _channels_bunch2alf(channels_aligned)
225244
if 'localCoordinates' in channels_aligned.keys():
226245
aligned_depths = channels_aligned['localCoordinates'][:, 1]
227-
else:
246+
else: # this is a edge case for a few spike sorting sessions
228247
assert channels_aligned['mlapdv'].shape[0] == 384
229248
NEUROPIXEL_VERSION = 1
230249
from ibllib.ephys.neuropixel import trace_header
@@ -238,7 +257,10 @@ def channel_locations_interpolation(channels_aligned, channels):
238257
# the brain locations have to be interpolated by nearest neighbour
239258
fcn_interp = interp1d(depth_aligned, channels_aligned['brainLocationIds_ccf_2017'][ind_aligned], kind='nearest')
240259
channels['brainLocationIds_ccf_2017'] = fcn_interp(depths)[iinv].astype(np.int32)
241-
return channels
260+
if brain_regions is not None:
261+
return _channels_alf2bunch(channels, brain_regions=brain_regions)
262+
else:
263+
return channels
242264

243265

244266
def _load_channel_locations_traj(eid, probe=None, one=None, revision=None, aligned=False,
@@ -531,7 +553,7 @@ def merge_clusters_channels(dic_clus, channels, keys_to_add_extra=None):
531553
dic_clus : dict of one.alf.io.AlfBunch
532554
1 bunch per probe, containing cluster information
533555
channels : dict of one.alf.io.AlfBunch
534-
1 bunch per probe, containing channels bunch with keys ('acronym', 'atlas_id')
556+
1 bunch per probe, containing channels bunch with keys ('acronym', 'atlas_id', 'x', 'y', z', 'localCoordinates')
535557
keys_to_add_extra : list of str
536558
Any extra keys to load into channels bunches
537559
@@ -541,7 +563,7 @@ def merge_clusters_channels(dic_clus, channels, keys_to_add_extra=None):
541563
clusters (1 bunch per probe) with new keys values.
542564
"""
543565
probe_labels = list(channels.keys()) # Convert dict_keys into list
544-
keys_to_add_default = ['acronym', 'atlas_id', 'x', 'y', 'z']
566+
keys_to_add_default = ['acronym', 'atlas_id', 'x', 'y', 'z', 'axial_um', 'lateral_um']
545567

546568
if keys_to_add_extra is None:
547569
keys_to_add = keys_to_add_default
@@ -550,10 +572,9 @@ def merge_clusters_channels(dic_clus, channels, keys_to_add_extra=None):
550572
keys_to_add = list(set(keys_to_add_extra + keys_to_add_default))
551573

552574
for label in probe_labels:
553-
try:
554-
clu_ch = dic_clus[label]['channels']
555-
556-
for key in keys_to_add:
575+
clu_ch = dic_clus[label]['channels']
576+
for key in keys_to_add:
577+
try:
557578
assert key in channels[label].keys() # Check key is in channels
558579
ch_key = channels[label][key]
559580
nch_key = len(ch_key) if ch_key is not None else 0
@@ -564,11 +585,9 @@ def merge_clusters_channels(dic_clus, channels, keys_to_add_extra=None):
564585
f'Probe {label}: merging channels and clusters for key "{key}" has {nch_key} on channels'
565586
f' but expected {max(clu_ch)}. Data in new cluster key "{key}" is returned empty.')
566587
dic_clus[label][key] = []
567-
except AssertionError:
568-
_logger.warning(
569-
f'Either clusters or channels does not have key {label}, could not'
570-
f' merge')
571-
continue
588+
except AssertionError:
589+
_logger.warning(f'Either clusters or channels does not have key {key}, could not merge')
590+
continue
572591

573592
return dic_clus
574593

brainbox/io/spikeglx.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,9 @@ def stream(pid, t0, nsecs=1, one=None, cache_folder=None, remove_cached=False, t
128128
samples_folder = Path(one.alyx._par.CACHE_DIR).joinpath('cache', typ)
129129

130130
eid, pname = one.pid2eid(pid)
131-
cbin_rec = one.list_datasets(eid, collection=f"*{pname}", filename='*ap.*bin', details=True)
132-
ch_rec = one.list_datasets(eid, collection=f"*{pname}", filename='*ap.ch', details=True)
133-
meta_rec = one.list_datasets(eid, collection=f"*{pname}", filename='*ap.meta', details=True)
131+
cbin_rec = one.list_datasets(eid, collection=f"*{pname}", filename=f'*{typ}.*bin', details=True)
132+
ch_rec = one.list_datasets(eid, collection=f"*{pname}", filename=f'*{typ}.ch', details=True)
133+
meta_rec = one.list_datasets(eid, collection=f"*{pname}", filename=f'*{typ}.meta', details=True)
134134
ch_file = one._download_datasets(ch_rec)[0]
135135
one._download_datasets(meta_rec)[0]
136136

brainbox/task/passive.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,14 @@ def get_stim_aligned_activity(stim_events, spike_times, spike_depths, z_score_fl
200200
stim_activity = {}
201201
for stim_type, stim_times in stim_events.items():
202202

203+
# Get rid of any nan values
204+
stim_times = stim_times[~np.isnan(stim_times)]
203205
stim_intervals = np.c_[stim_times - pre_stim, stim_times + post_stim]
204206
base_intervals = np.c_[stim_times - base_stim, stim_times - pre_stim]
205207
out_intervals = stim_intervals[:, 1] > times[-1]
206208

207-
idx_stim = np.searchsorted(times, stim_intervals)[np.invert(out_intervals)]
208-
idx_base = np.searchsorted(times, base_intervals)[np.invert(out_intervals)]
209+
idx_stim = np.searchsorted(times, stim_intervals, side='right')[np.invert(out_intervals)]
210+
idx_base = np.searchsorted(times, base_intervals, side='right')[np.invert(out_intervals)]
209211

210212
stim_trials = np.zeros((depths.shape[0], n_bins, idx_stim.shape[0]))
211213
noise_trials = np.zeros((depths.shape[0], n_bins_base, idx_stim.shape[0]))

ibllib/atlas/regions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,17 @@ def _mapping_from_regions_list(self, new_map, lateralize=False):
149149
mapind = mapind[iregion]
150150
return mapind
151151

152+
def remap(self, region_ids, source_map='Allen', target_map='Beryl'):
153+
"""
154+
Remap atlas regions ids from source map to target map
155+
:param region_ids: atlas ids to map
156+
:param source_map: map name which original region_ids are in
157+
:param target_map: map name onto which to map
158+
:return:
159+
"""
160+
_, inds = ismember(region_ids, self.id[self.mappings[source_map]])
161+
return self.id[self.mappings[target_map][inds]]
162+
152163

153164
def regions_from_allen_csv():
154165
"""

ibllib/dsp/voltage.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ def destripe(x, fs, tr_sel=None, neuropixel_version=1, butter_kwargs=None, k_kwa
206206
x = scipy.signal.sosfiltfilt(sos, x)
207207
# apply ADC shift
208208
if neuropixel_version is not None:
209-
x = fshift(x, h['sample_shift'], axis=1)
209+
sample_shift = h['sample_shift'] if (30000 / fs) < 10 else h['sample_shift'] * fs / 30000
210+
x = fshift(x, sample_shift, axis=1)
210211
# apply spatial filter on good channel selection only
211212
x_ = kfilt(x, **k_kwargs)
212213
return x_

0 commit comments

Comments
 (0)