Skip to content

Commit 874dd06

Browse files
authored
Merge branch 'develop' into hotfix/3.3.1
2 parents 05ebd0c + dcb1f18 commit 874dd06

23 files changed

+505
-82
lines changed

brainbox/io/one.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
import re
66
import os
77
from pathlib import Path
8+
from collections import defaultdict
89

910
import numpy as np
1011
import pandas as pd
1112
from scipy.interpolate import interp1d
1213
import matplotlib.pyplot as plt
1314

1415
from one.api import ONE, One
15-
from one.alf.path import get_alf_path, full_path_parts
16+
from one.alf.path import get_alf_path, full_path_parts, filename_parts
1617
from one.alf.exceptions import ALFObjectNotFound, ALFMultipleCollectionsFound
1718
from one.alf import cache
1819
import one.alf.io as alfio
@@ -193,9 +194,9 @@ def _load_spike_sorting(eid, one=None, collection=None, revision=None, return_ch
193194
for pname in pnames:
194195
probe_collection = _get_spike_sorting_collection(collections, pname)
195196
spikes[pname] = one.load_object(eid, collection=probe_collection, obj='spikes',
196-
attribute=spike_attributes)
197+
attribute=spike_attributes, namespace='')
197198
clusters[pname] = one.load_object(eid, collection=probe_collection, obj='clusters',
198-
attribute=cluster_attributes)
199+
attribute=cluster_attributes, namespace='')
199200
if return_channels:
200201
channels = _load_channels_locations_from_disk(
201202
eid, collection=collection, one=one, revision=revision, brain_regions=brain_regions)
@@ -1035,7 +1036,31 @@ def load_channels(self, **kwargs):
10351036
self.histology = 'alf'
10361037
return Bunch(channels)
10371038

1038-
def load_spike_sorting(self, spike_sorter='iblsorter', revision=None, enforce_version=False, good_units=False, **kwargs):
1039+
@staticmethod
1040+
def filter_files_by_namespace(all_files, namespace):
1041+
1042+
# Create dict for each file with available namespaces, no namespce is stored under the key None
1043+
namespace_files = defaultdict(dict)
1044+
available_namespaces = []
1045+
for file in all_files:
1046+
fparts = filename_parts(file.name, as_dict=True)
1047+
fname = f"{fparts['object']}.{fparts['attribute']}"
1048+
nspace = fparts['namespace']
1049+
available_namespaces.append(nspace)
1050+
namespace_files[fname][nspace] = file
1051+
1052+
if namespace not in set(available_namespaces):
1053+
_logger.info(f'Could not find manual curation results for {namespace}, returning default'
1054+
f' non manually curated spikesorting data')
1055+
1056+
# Return the files with the chosen namespace.
1057+
files = [f.get(namespace, f.get(None, None)) for f in namespace_files.values()]
1058+
# remove any None files
1059+
files = [f for f in files if f]
1060+
return files
1061+
1062+
def load_spike_sorting(self, spike_sorter='iblsorter', revision=None, enforce_version=False, good_units=False,
1063+
namespace=None, **kwargs):
10391064
"""
10401065
Loads spikes, clusters and channels
10411066
@@ -1053,6 +1078,8 @@ def load_spike_sorting(self, spike_sorter='iblsorter', revision=None, enforce_ve
10531078
:param enforce_version: if True, will raise an error if the spike sorting version and revision is not the expected one
10541079
:param dataset_types: list of extra dataset types, for example: ['spikes.samples', 'spikes.templates']
10551080
:param good_units: False, if True will load only the good units, possibly by downloading a smaller spikes table
1081+
:param namespace: None, if given will load the manually curated spikesorting with the given namespace,
1082+
e.g to load '_av_.clusters.depths use namespace='av'
10561083
:param kwargs: additional arguments to be passed to one.api.One.load_object
10571084
:return:
10581085
"""
@@ -1061,13 +1088,21 @@ def load_spike_sorting(self, spike_sorter='iblsorter', revision=None, enforce_ve
10611088
self.files = {}
10621089
self.spike_sorter = spike_sorter
10631090
self.revision = revision
1091+
1092+
if good_units and namespace is not None:
1093+
_logger.info('Good units table does not exist for manually curated spike sorting. Pass in namespace with'
1094+
'good_units=False and filter the spikes post hoc by the good clusters.')
1095+
return [None] * 3
10641096
objects = ['passingSpikes', 'clusters', 'channels'] if good_units else None
10651097
self.download_spike_sorting(spike_sorter=spike_sorter, revision=revision, objects=objects, **kwargs)
10661098
channels = self.load_channels(spike_sorter=spike_sorter, revision=revision, **kwargs)
1099+
self.files['clusters'] = self.filter_files_by_namespace(self.files['clusters'], namespace)
10671100
clusters = self._load_object(self.files['clusters'], wildcards=self.one.wildcards)
1101+
10681102
if good_units:
10691103
spikes = self._load_object(self.files['passingSpikes'], wildcards=self.one.wildcards)
10701104
else:
1105+
self.files['spikes'] = self.filter_files_by_namespace(self.files['spikes'], namespace)
10711106
spikes = self._load_object(self.files['spikes'], wildcards=self.one.wildcards)
10721107
if enforce_version:
10731108
self._assert_version_consistency()

examples/exploring_data/data_download.ipynb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,16 +142,18 @@
142142
]
143143
},
144144
{
145-
"metadata": {},
146145
"cell_type": "markdown",
146+
"metadata": {},
147147
"source": [
148148
"### Find recordings of a specific brain region\n",
149149
"If we are interested in a given brain region, we can use the `search_insertions` method to find all recordings associated with that region. For example, to find all recordings associated with the **Rhomboid Nucleus (RH)** region of the thalamus."
150150
]
151151
},
152152
{
153-
"metadata": {},
154153
"cell_type": "code",
154+
"execution_count": null,
155+
"metadata": {},
156+
"outputs": [],
155157
"source": [
156158
"# this is the query that yields the few recordings for the Rhomboid Nucleus (RH) region\n",
157159
"insertions_rh = one.search_insertions(atlas_acronym='RH', datasets='spikes.times.npy', project='brainwide')\n",
@@ -161,9 +163,7 @@
161163
"\n",
162164
"# the Allen brain regions parcellation is hierarchical, and searching for Thalamus will return all child Rhomboid Nucleus (RH) regions\n",
163165
"assert set(insertions_rh).issubset(set(insertions_th))\n"
164-
],
165-
"outputs": [],
166-
"execution_count": null
166+
]
167167
},
168168
{
169169
"cell_type": "markdown",
@@ -183,7 +183,7 @@
183183
"outputs": [],
184184
"source": [
185185
"# Find sessions that have spikes.times datasets\n",
186-
"sessions_with_spikes = one.search(project='brainwide', dataset='spikes.times')"
186+
"sessions_with_spikes = one.search(project='brainwide', datasets='spikes.times.npy')"
187187
]
188188
},
189189
{
@@ -253,7 +253,7 @@
253253
"outputs": [],
254254
"source": [
255255
"# Find an example session with trials data\n",
256-
"eid, *_ = one.search(project='brainwide', dataset='_ibl_trials.table.pqt')\n",
256+
"eid, *_ = one.search(project='brainwide', datasets='_ibl_trials.table.pqt')\n",
257257
"# List datasets associated with a session, in the alf collection\n",
258258
"datasets = one.list_datasets(eid, collection='alf*')\n",
259259
"\n",
@@ -279,7 +279,7 @@
279279
"source": [
280280
"# Find an example session with spike data\n",
281281
"# Note: Restricting by task and project makes searching for data much quicker\n",
282-
"eid, *_ = one.search(project='brainwide', dataset='spikes', task='ephys')\n",
282+
"eid, *_ = one.search(project='brainwide', datasets='spikes.times.npy', task='ephys')\n",
283283
"\n",
284284
"# Data for each probe insertion are stored in the alf/probeXX folder.\n",
285285
"datasets = one.list_datasets(eid, collection='alf/probe*')\n",
@@ -375,7 +375,7 @@
375375
"lab_name = list(labs)[0]\n",
376376
"\n",
377377
"# Searching for RS sessions with specific lab name\n",
378-
"sessions_lab = one.search(dataset='spikes', lab=lab_name)"
378+
"sessions_lab = one.search(datasets='spikes', lab=lab_name)"
379379
]
380380
},
381381
{

examples/loading_data/loading_photometry_data.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"source": [
6262
"from one.api import ONE\n",
6363
"one = ONE()\n",
64-
"sessions = one.search(dataset='photometry.signal.pqt')\n",
64+
"sessions = one.search(datasets='photometry.signal.pqt')\n",
6565
"print(f'{len(sessions)} sessions with photometry data found')"
6666
]
6767
},
@@ -271,7 +271,7 @@
271271
"name": "python",
272272
"nbconvert_exporter": "python",
273273
"pygments_lexer": "ipython3",
274-
"version": "3.9.16"
274+
"version": "3.11.9"
275275
}
276276
},
277277
"nbformat": 4,

examples/loading_data/loading_trials_data.ipynb

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@
5050
"cell_type": "markdown",
5151
"id": "a5d358e035a91310",
5252
"metadata": {
53-
"collapsed": false
53+
"collapsed": false,
54+
"jupyter": {
55+
"outputs_hidden": false
56+
}
5457
},
5558
"source": [
5659
"## Loading a single session's trials\n"
@@ -77,7 +80,10 @@
7780
"cell_type": "markdown",
7881
"id": "d6c98a81f5426445",
7982
"metadata": {
80-
"collapsed": false
83+
"collapsed": false,
84+
"jupyter": {
85+
"outputs_hidden": false
86+
}
8187
},
8288
"source": [
8389
"For combining trials data with various recording modalities for a given session, the `SessionLoader` class is more convenient:"
@@ -130,8 +136,12 @@
130136
"from one.api import ONE\n",
131137
"one = ONE()\n",
132138
"subject = 'SWC_043'\n",
139+
"# Load in subject trials table\n",
133140
"trials = one.load_aggregate('subjects', subject, '_ibl_subjectTrials.table')\n",
134141
"\n",
142+
"# Load in subject sessions table\n",
143+
"sessions = one.load_aggregate('subjects', subject, '_ibl_subjectSessions.table')\n",
144+
"\n",
135145
"# Load training status and join to trials table\n",
136146
"training = one.load_aggregate('subjects', subject, '_ibl_subjectTraining.table')\n",
137147
"trials = (trials\n",
@@ -141,10 +151,9 @@
141151
"trials['training_status'] = trials.training_status.fillna(method='ffill')\n",
142152
"\n",
143153
"# Join sessions table for number, task_protocol, etc.\n",
144-
"trials = one.load_aggregate('subjects', subject, '_ibl_subjectTrials.table')\n",
145154
"if 'task_protocol' in trials:\n",
146-
" trials.drop('task_protocol', axis=1)\n",
147-
"trials = trials.set_index('session').join(one._cache.sessions.drop('date', axis=1))"
155+
" trials = trials.drop('task_protocol', axis=1)\n",
156+
"trials = trials.join(sessions.drop('date', axis=1))"
148157
]
149158
},
150159
{
@@ -302,7 +311,10 @@
302311
"cell_type": "markdown",
303312
"id": "55ad2e5d71ac301",
304313
"metadata": {
305-
"collapsed": false
314+
"collapsed": false,
315+
"jupyter": {
316+
"outputs_hidden": false
317+
}
306318
},
307319
"source": [
308320
"### Example 5: Computing the inter-trial interval (ITI)\n",
@@ -345,9 +357,9 @@
345357
"metadata": {
346358
"celltoolbar": "Edit Metadata",
347359
"kernelspec": {
348-
"display_name": "Python [conda env:iblenv] *",
360+
"display_name": "Python 3 (ipykernel)",
349361
"language": "python",
350-
"name": "conda-env-iblenv-py"
362+
"name": "python3"
351363
},
352364
"language_info": {
353365
"codemirror_mode": {
@@ -359,7 +371,7 @@
359371
"name": "python",
360372
"nbconvert_exporter": "python",
361373
"pygments_lexer": "ipython3",
362-
"version": "3.11.6"
374+
"version": "3.11.9"
363375
}
364376
},
365377
"nbformat": 4,

examples/loading_data/loading_widefield_data.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"source": [
8787
"from one.api import ONE\n",
8888
"one = ONE()\n",
89-
"sessions = one.search(dataset='widefieldU.images.npy')\n",
89+
"sessions = one.search(datasets='widefieldU.images.npy')\n",
9090
"print(f'{len(sessions)} sessions with widefield data found')"
9191
]
9292
},
@@ -224,7 +224,7 @@
224224
"name": "python",
225225
"nbconvert_exporter": "python",
226226
"pygments_lexer": "ipython3",
227-
"version": "3.9.16"
227+
"version": "3.11.9"
228228
}
229229
},
230230
"nbformat": 4,

ibllib/io/extractors/default_channel_maps.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@
6262
'audio': 4,
6363
'bpod': 5,
6464
'rotary_encoder': 6,
65-
'neural_frames': 7}
65+
'neural_frames': 7,
66+
'volume_counter': 8}
6667
}
6768
}
6869

ibllib/io/extractors/ephys_fpga.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
from pathlib import Path
3636
import uuid
3737
import re
38-
from functools import partial
3938

4039
import matplotlib.pyplot as plt
4140
from matplotlib.colors import TABLEAU_COLORS
@@ -794,6 +793,7 @@ def _extract(self, sync=None, chmap=None, sync_collection='raw_ephys_data',
794793
elif (protocol_number := kwargs.get('protocol_number')) is not None: # look for spacer
795794
# The spacers are TTLs generated by Bpod at the start of each protocol
796795
tmin, tmax = get_protocol_period(self.session_path, protocol_number, bpod)
796+
tmin += (Spacer().times[-1] + Spacer().tup + 0.05) # exclude spacer itself
797797
else:
798798
# Older sessions don't have protocol spacers so we sync the Bpod intervals here to
799799
# find the approximate end time of the protocol (this will exclude the passive signals
@@ -1453,16 +1453,23 @@ def get_bpod_event_times(self, sync, chmap, bpod_event_ttls=None, display=False,
14531453
# lengths are defined by the state machine of the task protocol and therefore vary.
14541454
if bpod_event_ttls is None:
14551455
# Currently (at least v8.12 and below) there is no trial start or end TTL, only an ITI pulse
1456-
bpod_event_ttls = {'trial_iti': (1, 1.1), 'valve_open': (0, 0.4)}
1456+
bpod_event_ttls = {'trial_iti': (.999, 1.1), 'valve_open': (0, 0.4)}
14571457
bpod_event_intervals = self._assign_events(
14581458
bpod['times'], bpod['polarities'], bpod_event_ttls, display=display)
14591459

14601460
# The first trial pulse is shorter and assigned to valve_open. Here we remove the first
14611461
# valve event, prepend a 0 to the trial_start events, and drop the last trial if it was
14621462
# incomplete in Bpod.
1463-
bpod_event_intervals['trial_iti'] = np.r_[bpod_event_intervals['valve_open'][0:1, :],
1464-
bpod_event_intervals['trial_iti']]
1465-
bpod_event_intervals['valve_open'] = bpod_event_intervals['valve_open'][1:, :]
1463+
t0 = bpod_event_intervals['trial_iti'][0, 0] # expect 1st event to be trial_start
1464+
pretrial = [(k, v[0, 0]) for k, v in bpod_event_intervals.items() if v.size and v[0, 0] < t0]
1465+
if pretrial:
1466+
(pretrial, _) = sorted(pretrial, key=lambda x: x[1])[0] # take the earliest event
1467+
dt = np.diff(bpod_event_intervals[pretrial][0, :]) * 1e3 # record TTL length to log
1468+
_logger.debug('Reassigning first %s to trial_start. TTL length = %.3g ms', pretrial, dt)
1469+
bpod_event_intervals['trial_iti'] = np.r_[
1470+
bpod_event_intervals[pretrial][0:1, :], bpod_event_intervals['trial_iti']
1471+
]
1472+
bpod_event_intervals[pretrial] = bpod_event_intervals[pretrial][1:, :]
14661473

14671474
return bpod, bpod_event_intervals
14681475

@@ -1514,13 +1521,16 @@ def build_trials(self, sync, chmap, display=False, **kwargs):
15141521
out.update({k: self.bpod2fpga(self.bpod_trials[k][ibpod]) for k in self.bpod_rsync_fields})
15151522

15161523
# Assigning each event to a trial ensures exactly one event per trial (missing events are NaN)
1517-
assign_to_trial = partial(_assign_events_to_trial, fpga_events['intervals_0'])
15181524
trials = alfio.AlfBunch({
1519-
'goCue_times': assign_to_trial(fpga_events['goCue_times'], take='first'),
1520-
'feedback_times': assign_to_trial(fpga_events['feedback_times']),
1521-
'stimCenter_times': assign_to_trial(self.frame2ttl['times'], take=-2),
1522-
'stimOn_times': assign_to_trial(self.frame2ttl['times'], take='first'),
1523-
'stimOff_times': assign_to_trial(self.frame2ttl['times']),
1525+
'goCue_times': _assign_events_to_trial(out['goCueTrigger_times'], fpga_events['goCue_times'], take='first'),
1526+
'feedback_times': _assign_events_to_trial(fpga_events['intervals_0'], fpga_events['feedback_times']),
1527+
'stimCenter_times': _assign_events_to_trial(
1528+
out['stimCenterTrigger_times'], self.frame2ttl['times'], take='first', t_trial_end=out['stimOffTrigger_times']),
1529+
'stimOn_times': _assign_events_to_trial(
1530+
out['stimOnTrigger_times'], self.frame2ttl['times'], take='first', t_trial_end=out['stimCenterTrigger_times']),
1531+
'stimOff_times': _assign_events_to_trial(
1532+
out['stimOffTrigger_times'], self.frame2ttl['times'],
1533+
take='first', t_trial_end=np.r_[out['intervals'][1:, 0], np.inf])
15241534
})
15251535
out.update({k: trials[k][ifpga] for k in trials.keys()})
15261536

0 commit comments

Comments
 (0)