Skip to content

Commit f05d8bb

Browse files
committed
Merge branch 'release/2.14.0'
2 parents 9f2d8a1 + 705534c commit f05d8bb

File tree

15 files changed

+504
-73
lines changed

15 files changed

+504
-73
lines changed

brainbox/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
"""IBL shared data processing methods."""
12
import logging
23
try:
34
import one

brainbox/io/one.py

Lines changed: 334 additions & 1 deletion
Large diffs are not rendered by default.

ibllib/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
__version__ = "2.13.6"
1+
"""Library implementing the International Brain Laboratory data pipeline."""
2+
__version__ = "2.14.0"
23
import warnings
34

45
from ibllib.misc import logger_config

ibllib/atlas/atlas.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,16 +300,34 @@ def _lookup(self, xyz):
300300
"""
301301
return self._lookup_inds(self.bc.xyz2i(xyz))
302302

303-
def get_labels(self, xyz, mapping='Allen'):
303+
def get_labels(self, xyz, mapping='Allen', radius_um=None):
304304
"""
305305
Performs a 3D lookup from real world coordinates to the volume labels
306306
and return the regions ids according to the mapping
307307
:param xyz: [n, 3] array of coordinates
308308
:param mapping: brain region mapping (defaults to original Allen mapping)
309+
:param radius_um: if not null, returns a regions ids array and an array of proportion
310+
of regions in a sphere of size radius around the coordinates.
309311
:return: n array of region ids
310312
"""
311-
regions_indices = self._get_mapping(mapping=mapping)[self.label.flat[self._lookup(xyz)]]
312-
return self.regions.id[regions_indices]
313+
if radius_um:
314+
nrx = int(np.ceil(radius_um / abs(self.bc.dx) / 1e6))
315+
nry = int(np.ceil(radius_um / abs(self.bc.dy) / 1e6))
316+
nrz = int(np.ceil(radius_um / abs(self.bc.dz) / 1e6))
317+
nr = [nrx, nry, nrz]
318+
iii = self.bc.xyz2i(xyz)
319+
# computing the cube radius and indices is more complicated as volume indices are not
320+
# necessariy in ml, ap, dv order so the indices order is dynamic
321+
rcube = np.meshgrid(*tuple((np.arange(
322+
-nr[i], nr[i] + 1) * self.bc.dxyz[i]) ** 2 for i in self.xyz2dims))
323+
rcube = np.sqrt(rcube[0] + rcube[1], rcube[2]) * 1e6
324+
icube = tuple(slice(-nr[i] + iii[i], nr[i] + iii[i] + 1) for i in self.xyz2dims)
325+
cube = self.regions.mappings[mapping][self.label[icube]]
326+
ilabs, counts = np.unique(cube[rcube <= radius_um], return_counts=True)
327+
return self.regions.id[ilabs], counts / np.sum(counts)
328+
else:
329+
regions_indices = self._get_mapping(mapping=mapping)[self.label.flat[self._lookup(xyz)]]
330+
return self.regions.id[regions_indices]
313331

314332
def _get_mapping(self, mapping='Allen'):
315333
"""

ibllib/atlas/beryl.npy

-56 Bytes
Binary file not shown.

ibllib/io/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
import spikeglx
1+
import spikeglx # TODO Remove November 2022

ibllib/io/extractors/training_audio.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
import logging
55

66
import numpy as np
7-
from scipy import signal
7+
import scipy.signal
8+
import scipy.ndimage
89
from scipy.io import wavfile
910

11+
1012
from neurodsp.utils import WindowGenerator
1113
from neurodsp import fourier
1214
import ibllib.io.raw_data_loaders as ioraw
1315
from ibllib.io.extractors.training_trials import GoCueTimes
1416

17+
1518
logger_ = logging.getLogger('ibllib')
1619

1720
NS_WIN = 2 ** 18 # 2 ** np.ceil(np.log2(1 * fs))
@@ -22,16 +25,19 @@
2225
READY_TONE_SPL = 85
2326

2427

25-
def _running_mean(x, N):
26-
cumsum = np.cumsum(np.insert(x, 0, 0))
27-
return (cumsum[N:] - cumsum[:-N]) / N
28-
29-
30-
def _detect_ready_tone(w, fs):
28+
def detect_ready_tone(w, fs, ftone=FTONE, threshold=0.8):
29+
"""
30+
Detects a transient sinusoid signal in a time-serie
31+
:param w: audio time seried
32+
:param fs: sampling frequency (Hz)
33+
:param ftone: frequency of the tone to detect
34+
:param threshold: ratio of the Hilbert to the signal, between 0 and 1 (set to 0.8)
35+
:return:
36+
"""
3137
# get envelope of DC free signal and envelope of BP signal around freq of interest
32-
h = np.abs(signal.hilbert(w - np.median(w)))
33-
fh = np.abs(signal.hilbert(fourier.bp(w, si=1 / fs, b=FTONE * np.array([0.9, 0.95, 1.15, 1.1]))))
34-
dtect = _running_mean(fh / (h + 1e-3), int(fs * 0.1)) > 0.8
38+
h = np.abs(scipy.signal.hilbert(w - np.median(w)))
39+
fh = np.abs(scipy.signal.hilbert(fourier.bp(w, si=1 / fs, b=ftone * np.array([0.9, 0.95, 1.15, 1.1]))))
40+
dtect = scipy.ndimage.uniform_filter1d(fh / (h + 1e-3), int(fs * 0.1)) > threshold
3541
return np.where(np.diff(dtect.astype(int)) == 1)[0]
3642
# tone = np.sin(2 * np.pi * FTONE * np.arange(0, fs * 0.1) / fs)
3743
# tone = tone / np.sum(tone ** 2)
@@ -56,7 +62,7 @@ def _get_conversion_factor(unit=UNIT, ready_tone_spl=READY_TONE_SPL):
5662
return fac
5763

5864

59-
def welchogram(fs, wav, nswin=NS_WIN, overlap=OVERLAP, nperseg=NS_WELCH):
65+
def welchogram(fs, wav, nswin=NS_WIN, overlap=OVERLAP, nperseg=NS_WELCH, detect_kwargs=None):
6066
"""
6167
Computes a spectrogram on a very large audio file.
6268
@@ -65,6 +71,7 @@ def welchogram(fs, wav, nswin=NS_WIN, overlap=OVERLAP, nperseg=NS_WELCH):
6571
:param nswin: n samples of the sliding window
6672
:param overlap: n samples of the overlap between windows
6773
:param nperseg: n samples for the computation of the spectrogram
74+
:param detect_kwargs: specified paramaters for detection
6875
:return: tscale, fscale, downsampled_spectrogram
6976
"""
7077
ns = wav.shape[0]
@@ -78,16 +85,17 @@ def welchogram(fs, wav, nswin=NS_WIN, overlap=OVERLAP, nperseg=NS_WELCH):
7885
# load the current window into memory
7986
w = np.float64(wav[first:last]) * _get_conversion_factor()
8087
# detection of ready tones
81-
a = [d + first for d in _detect_ready_tone(w, fs)]
88+
detect_kwargs = detect_kwargs or {}
89+
a = [d + first for d in detect_ready_tone(w, fs, **detect_kwargs)]
8290
if len(a):
8391
detect += a
8492
# the last window may not allow a pwelch
8593
if (last - first) < nperseg:
8694
continue
8795
# compute PSD estimate for the current window
8896
iw = window_generator.iw
89-
_, W[iw, :] = signal.welch(w, fs=fs, window='hann', nperseg=nperseg, axis=-1,
90-
detrend='constant', return_onesided=True, scaling='density')
97+
_, W[iw, :] = scipy.signal.welch(w, fs=fs, window='hann', nperseg=nperseg, axis=-1,
98+
detrend='constant', return_onesided=True, scaling='density')
9199
# the onset detection may have duplicates with sliding window, average them and remove
92100
detect = np.sort(np.array(detect)) / fs
93101
ind = np.where(np.diff(detect) < 0.1)[0]

ibllib/qc/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Data quality control calculation and aggregation."""

ibllib/qc/camera.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
1-
"""Camera QC
1+
"""Video quality control.
2+
23
This module runs a list of quality control metrics on the camera and extracted video data.
34
4-
Example - Run right camera QC, downloading all but video file
5-
qc = CameraQC(eid, 'right', download_data=True, stream=True)
6-
qc.run()
5+
Examples
6+
--------
7+
Run right camera QC, downloading all but video file
8+
9+
>>> qc = CameraQC(eid, 'right', download_data=True, stream=True)
10+
>>> qc.run()
11+
12+
Run left camera QC with session path, update QC field in Alyx
13+
14+
>>> qc = CameraQC(session_path, 'left')
15+
>>> outcome, extended = qc.run(update=True) # Returns outcome of videoQC only
16+
>>> print(f'video QC = {outcome}; overall session QC = {qc.outcome}') # NB difference outcomes
17+
18+
Run only video QC (no timestamp/alignment checks) on 20 frames for the body camera
19+
20+
>>> qc = CameraQC(eid, 'body', n_samples=20)
21+
>>> qc.load_video_data() # Quicker than loading all data
22+
>>> qc.run()
723
8-
Example - Run left camera QC with session path, update QC field in Alyx
9-
qc = CameraQC(session_path, 'left')
10-
outcome, extended = qc.run(update=True) # Returns outcome of videoQC only
11-
print(f'video QC = {outcome}; overall session QC = {qc.outcome}') # NB difference outcomes
24+
Run specific video QC check and display the plots
1225
13-
Example - Run only video QC (no timestamp/alignment checks) on 20 frames for the body camera
14-
qc = CameraQC(eid, 'body', n_samples=20)
15-
qc.load_video_data() # Quicker than loading all data
16-
qc.run()
26+
>>> qc = CameraQC(eid, 'left')
27+
>>> qc.load_data(download_data=True)
28+
>>> qc.check_position(display=True) # NB: Not all checks make plots
1729
18-
Example - Run specific video QC check and display the plots
19-
qc = CameraQC(eid, 'left;)
20-
qc.load_data(download_data=True)
21-
qc.check_position(display=True) # NB: Not all checks make plots
30+
Run the QC for all cameras
2231
23-
Example - Run the QC for all cameras
24-
qcs = run_all_qc(eid)
25-
qcs['left'].metrics # Dict of checks and outcomes for left camera
32+
>>> qcs = run_all_qc(eid)
33+
>>> qcs['left'].metrics # Dict of checks and outcomes for left camera
2634
"""
2735
import logging
2836
from inspect import getmembers, isfunction
@@ -586,7 +594,7 @@ def check_wheel_alignment(self, tolerance=(1, 2), display=False):
586594

587595
# Determine the outcome. If there are two values for the tolerance, one is taken to be
588596
# a warning threshold, the other a failure threshold.
589-
out_map = {0: 'FAIL', 1: 'WARNING', 2: 'PASS'}
597+
out_map = {0: 'WARNING', 1: 'WARNING', 2: 'PASS'} # 0: FAIL -> WARNING Aug 2022
590598
passed = np.abs(offset) <= np.sort(np.array(tolerance))
591599
return out_map[sum(passed)], int(offset)
592600

ibllib/qc/dlc.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,11 @@ class DlcQC(base.QC):
4646
'body': ['_ibl_bodyCamera.dlc.*', '_ibl_bodyCamera.times.*'],
4747
}
4848

49-
def __init__(self, session_path_or_eid, side, **kwargs):
49+
def __init__(self, session_path_or_eid, side, ignore_checks=['check_pupil_diameter_snr'], **kwargs):
5050
"""
5151
:param session_path_or_eid: A session eid or path
5252
:param side: The camera to run QC on
53+
:param ignore_checks: Checks that won't count towards aggregate QC, but will be run and added to extended QC
5354
:param log: A logging.Logger instance, if None the 'ibllib' logger is used
5455
:param one: An ONE instance for fetching and setting the QC on Alyx
5556
"""
@@ -61,6 +62,8 @@ def __init__(self, session_path_or_eid, side, **kwargs):
6162
super().__init__(session_path_or_eid, **kwargs)
6263
self.data = Bunch()
6364

65+
# checks to be added to extended QC but not taken into account for aggregate QC
66+
self.ignore_checks = ignore_checks
6467
# QC outcomes map
6568
self.metrics = None
6669

@@ -139,9 +142,9 @@ def is_metric(x):
139142
checks = getmembers(DlcQC, is_metric)
140143
self.metrics = {f'_{namespace}_' + k[6:]: fn(self) for k, fn in checks}
141144

142-
values = [x if isinstance(x, str) else x[0] for x in self.metrics.values()]
143-
code = max(base.CRITERIA[x] for x in values)
144-
outcome = next(k for k, v in base.CRITERIA.items() if v == code)
145+
ignore_metrics = [f'_{namespace}_' + i[6:] for i in self.ignore_checks]
146+
metrics_to_aggregate = {k: v for k, v in self.metrics.items() if k not in ignore_metrics}
147+
outcome = self.overall_outcome(metrics_to_aggregate.values())
145148

146149
if update:
147150
extended = {

0 commit comments

Comments
 (0)