Source code for ibllib.tests.qc.test_alignment_qc

import unittest
from pathlib import Path
import re
from inspect import getmembers, ismethod

import numpy as np
import copy
import random
import string
from datetime import date

from one.api import ONE
from neuropixel import trace_header

from ibllib.tests import TEST_DB
from ibllib.tests.fixtures.utils import register_new_session
from iblatlas.atlas import AllenAtlas
from ibllib.pipes.misc import create_alyx_probe_insertions
from ibllib.qc.alignment_qc import AlignmentQC
from ibllib.pipes.histology import register_track, register_chronic_track

EPHYS_SESSION = 'b1c968ad-4874-468d-b2e4-5ffa9b9964e9'
one = ONE(**TEST_DB)
brain_atlas = AllenAtlas(25)

refch_3a = np.array([36, 75, 112, 151, 188, 227, 264, 303, 340, 379])
th = trace_header(version=1)
SITES_COORDINATES = np.delete(np.c_[th['x'], th['y']], refch_3a, axis=0)


[docs] class TestTracingQc(unittest.TestCase): probe01_id = None probe00_id = None
[docs] @classmethod def setUpClass(cls) -> None: probe = [''.join(random.choices(string.ascii_letters, k=5)), ''.join(random.choices(string.ascii_letters, k=5))] _, eid = register_new_session(one, subject='ZM_1150') cls.eid = str(eid) # Currently the task protocol of a session must contain 'ephys' in order to create an insertion! one.alyx.rest('sessions', 'partial_update', id=cls.eid, data={'task_protocol': 'ephys'}) ins = create_alyx_probe_insertions(session_path=cls.eid, model='3B2', labels=probe, one=one, force=True) cls.probe00_id, cls.probe01_id = (x['id'] for x in ins) data = np.load(Path(Path(__file__).parent.parent. joinpath('fixtures', 'qc', 'data_alignmentqc_existing.npz')), allow_pickle=True) cls.xyz_picks = np.array(data['xyz_picks']) / 1e6
[docs] def test_tracing_exists(self): register_track(self.probe00_id, picks=self.xyz_picks, one=one, overwrite=True, channels=False, brain_atlas=brain_atlas) insertion = one.alyx.get('/insertions/' + self.probe00_id, clobber=True) self.assertEqual(insertion['json']['qc'], 'NOT_SET') self.assertEqual(insertion['json']['extended_qc']['tracing_exists'], 1)
[docs] def test_tracing_not_exists(self): register_track(self.probe01_id, picks=None, one=one, overwrite=True, channels=False, brain_atlas=brain_atlas) insertion = one.alyx.get('/insertions/' + self.probe01_id, clobber=True) self.assertEqual(insertion['json']['qc'], 'CRITICAL') self.assertEqual(insertion['json']['extended_qc']['tracing_exists'], 0)
[docs] @classmethod def tearDownClass(cls) -> None: one.alyx.rest('insertions', 'delete', id=cls.probe01_id) one.alyx.rest('insertions', 'delete', id=cls.probe00_id) one.alyx.rest('sessions', 'delete', id=cls.eid)
[docs] class TestChronicTracingQC(unittest.TestCase):
[docs] @classmethod def setUpClass(cls) -> None: probe = ''.join(random.choices(string.ascii_letters, k=5)) serial = ''.join(random.choices(string.ascii_letters, k=10)) # Make a chronic insertions _, eid = register_new_session(one, subject='ZM_1150') cls.eid = str(eid) # Currently the task protocol of a session must contain 'ephys' in order to create an insertion! one.alyx.rest('sessions', 'partial_update', id=cls.eid, data={'task_protocol': 'ephys'}) insdict = {"subject": 'ZM_1150', "name": probe, "model": '3B2', "serial": serial} ins = one.alyx.rest('chronic-insertions', 'create', data=insdict) cls.chronic_id = ins['id'] # Make a probe insertions insdict = {"session": cls.eid, "name": probe, "model": '3B2', "serial": serial, "chronic_insertion": cls.chronic_id} ins = one.alyx.rest('insertions', 'create', data=insdict) cls.probe_id = ins['id'] # Load in the tracing data data = np.load(Path(Path(__file__).parent.parent. joinpath('fixtures', 'qc', 'data_alignmentqc_existing.npz')), allow_pickle=True) cls.xyz_picks = np.array(data['xyz_picks']) / 1e6
[docs] def test_tracing_exists(self): register_chronic_track(self.chronic_id, picks=self.xyz_picks, one=one, overwrite=True, channels=False, brain_atlas=brain_atlas) insertion = one.alyx.get('/insertions/' + self.probe_id, clobber=True) self.assertEqual(insertion['json']['qc'], 'NOT_SET') self.assertEqual(insertion['json']['extended_qc']['tracing_exists'], 1) insertion = one.alyx.get('/chronic-insertions/' + self.chronic_id, clobber=True) self.assertEqual(insertion['json']['qc'], 'NOT_SET') self.assertEqual(insertion['json']['extended_qc']['tracing_exists'], 1)
[docs] def test_tracing_not_exists(self): register_chronic_track(self.chronic_id, picks=None, one=one, overwrite=True, channels=False, brain_atlas=brain_atlas) insertion = one.alyx.get('/insertions/' + self.probe_id, clobber=True) self.assertEqual(insertion['json']['qc'], 'CRITICAL') self.assertEqual(insertion['json']['extended_qc']['tracing_exists'], 0) insertion = one.alyx.get('/chronic-insertions/' + self.chronic_id, clobber=True) self.assertEqual(insertion['json']['qc'], 'CRITICAL') self.assertEqual(insertion['json']['extended_qc']['tracing_exists'], 0)
[docs] @classmethod def tearDownClass(cls) -> None: one.alyx.rest('insertions', 'delete', id=cls.probe_id) one.alyx.rest('chronic-insertions', 'delete', id=cls.chronic_id) one.alyx.rest('sessions', 'delete', id=cls.eid)
[docs] class TestAlignmentQcExisting(unittest.TestCase): probe_id = None prev_traj_id = None eid = None alignments = None xyz_picks = None trajectory = None
[docs] @classmethod def setUpClass(cls) -> None: data = np.load(Path(Path(__file__).parent.parent. joinpath('fixtures', 'qc', 'data_alignmentqc_existing.npz')), allow_pickle=True) cls.xyz_picks = data['xyz_picks'].tolist() cls.alignments = data['alignments'].tolist() # Manipulate so one alignment disagrees cls.alignments['2020-06-26T16:40:14_Karolina_Socha'][1] = \ list(np.array(cls.alignments['2020-06-26T16:40:14_Karolina_Socha'][1]) + 0.0001) cls.cluster_chns = data['cluster_chns'] insertion = data['insertion'].tolist() insertion['name'] = ''.join(random.choices(string.ascii_letters, k=5)) insertion['json'] = {'xyz_picks': cls.xyz_picks} _, eid = register_new_session(one, subject='ZM_1150') cls.eid = str(eid) # Currently the task protocol of a session must contain 'ephys' in order to create an insertion! one.alyx.rest('sessions', 'partial_update', id=cls.eid, data={'task_protocol': 'ephys'}) insertion['session'] = cls.eid probe_insertion = one.alyx.rest('insertions', 'create', data=insertion) cls.probe_id = probe_insertion['id'] cls.trajectory = data['trajectory'].tolist() cls.trajectory.update({'probe_insertion': cls.probe_id})
[docs] def test_alignments(self): checks = getmembers(self, lambda x: ismethod(x) and re.match(r'^_\d{2}_.*', x.__name__)) # Run numbered functions in order for _, fn in sorted(checks, key=lambda x: x[0]): self._get_prev_traj_id() fn()
def _get_prev_traj_id(self): traj = one.alyx.get('/trajectories?' f'&probe_id={self.probe_id}' '&provenance=Ephys aligned histology track', clobber=True) if traj: self.prev_traj_id = traj[0]['id'] def _01_no_alignments(self): align_qc = AlignmentQC(self.probe_id, one=one, brain_atlas=brain_atlas, channels=False) align_qc.run(update=True, upload_alyx=True, upload_flatiron=False) insertion = one.alyx.get(f'/insertions/{self.probe_id}', clobber=True) self.assertEqual('NOT_SET', insertion['json']['qc']) self.assertTrue(len(insertion['json']['extended_qc']) == 0) def _02_one_alignment(self): alignments = {'2020-06-26T16:40:14_Karolina_Socha': self.alignments['2020-06-26T16:40:14_Karolina_Socha']} trajectory = copy.deepcopy(self.trajectory) trajectory.update({'json': alignments}) trajectory.update({'chronic_insertion': None}) _ = one.alyx.rest('trajectories', 'create', data=trajectory) align_qc = AlignmentQC(self.probe_id, one=one, brain_atlas=brain_atlas, channels=False) align_qc.run(update=True, upload_alyx=True, upload_flatiron=False) _verify(self, alignment_count=1, alignment_stored='2020-06-26T16:40:14_Karolina_Socha', alignment_resolved=False) def _03_alignments_disagree(self): alignments = {'2020-06-26T16:40:14_Karolina_Socha': self.alignments['2020-06-26T16:40:14_Karolina_Socha'], '2020-06-12T00:39:15_nate': self.alignments['2020-06-12T00:39:15_nate']} trajectory = copy.deepcopy(self.trajectory) trajectory.update({'probe_insertion': self.probe_id, 'json': alignments}) traj = one.alyx.rest('trajectories', 'partial_update', id=self.prev_traj_id, data=trajectory) align_qc = AlignmentQC(self.probe_id, one=one, brain_atlas=brain_atlas, channels=False) align_qc.load_data(prev_alignments=traj['json'], xyz_picks=np.array(self.xyz_picks) / 1e6, cluster_chns=self.cluster_chns, depths=SITES_COORDINATES[:, 1], chn_coords=SITES_COORDINATES) align_qc.run(update=True, upload_alyx=True, upload_flatiron=False) _verify(self, alignment_qc=0.782216, alignment_resolved=False, alignment_count=2, alignment_stored='2020-06-26T16:40:14_Karolina_Socha', trajectory_created=False) def _04_alignments_agree(self): alignments = {'2020-06-19T10:52:36_noam.roth': self.alignments['2020-06-19T10:52:36_noam.roth'], '2020-06-12T00:39:15_nate': self.alignments['2020-06-12T00:39:15_nate']} trajectory = copy.deepcopy(self.trajectory) trajectory.update({'probe_insertion': self.probe_id, 'json': alignments}) traj = one.alyx.rest('trajectories', 'partial_update', id=self.prev_traj_id, data=trajectory) self.assertEqual(self.prev_traj_id, traj['id']) align_qc = AlignmentQC(self.probe_id, one=one, brain_atlas=brain_atlas, channels=False) align_qc.load_data(cluster_chns=self.cluster_chns, depths=SITES_COORDINATES[:, 1], chn_coords=SITES_COORDINATES) align_qc.run(update=True, upload_alyx=True, upload_flatiron=False) _verify(self, alignment_resolved='qc', alignment_qc=0.952319, trajectory_created=False, alignment_count=2, alignment_stored='2020-06-19T10:52:36_noam.roth') def _05_not_latest_alignments_agree(self): alignments = copy.deepcopy(self.alignments) trajectory = copy.deepcopy(self.trajectory) trajectory.update({'probe_insertion': self.probe_id, 'json': alignments}) traj = one.alyx.rest('trajectories', 'partial_update', id=self.prev_traj_id, data=trajectory) self.assertEqual(self.prev_traj_id, traj['id']) align_qc = AlignmentQC(self.probe_id, one=one, brain_atlas=brain_atlas, channels=False) align_qc.load_data(prev_alignments=traj['json'], xyz_picks=np.array(self.xyz_picks) / 1e6, cluster_chns=self.cluster_chns, depths=SITES_COORDINATES[:, 1], chn_coords=SITES_COORDINATES) align_qc.resolved = 0 align_qc.run(update=True, upload_alyx=True, upload_flatiron=False) _verify(self, alignment_resolved='qc', alignment_qc=0.952319, alignment_count=4, alignment_stored='2020-06-19T10:52:36_noam.roth', trajectory_created=True)
[docs] @classmethod def tearDownClass(cls) -> None: one.alyx.rest('insertions', 'delete', id=cls.probe_id) one.alyx.rest('sessions', 'delete', id=cls.eid)
[docs] class TestAlignmentQcManual(unittest.TestCase): probe_id = None prev_traj_id = None eid = None alignments = None xyz_picks = None trajectory = None
[docs] @classmethod def setUpClass(cls) -> None: fixture_path = Path(__file__).parent.parent.joinpath('fixtures', 'qc') data = np.load(fixture_path / 'data_alignmentqc_manual.npz', allow_pickle=True) cls.xyz_picks = (data['xyz_picks'] * 1e6).tolist() cls.alignments = data['alignments'].tolist() cls.cluster_chns = data['cluster_chns'] data = np.load(fixture_path / 'data_alignmentqc_existing.npz', allow_pickle=True) insertion = data['insertion'].tolist() insertion['name'] = ''.join(random.choices(string.ascii_letters, k=5)) insertion['json'] = {'xyz_picks': cls.xyz_picks} _, eid = register_new_session(one, subject='ZM_1150') cls.eid = str(eid) insertion['session'] = cls.eid probe_insertion = one.alyx.rest('insertions', 'create', data=insertion) cls.probe_id = probe_insertion['id'] cls.trajectory = data['trajectory'].tolist() cls.trajectory.update({'probe_insertion': cls.probe_id}) cls.trajectory.update({'chronic_insertion': None}) cls.trajectory.update({'json': cls.alignments}) cls.traj = one.alyx.rest('trajectories', 'create', data=cls.trajectory)
[docs] def test_alignments(self): checks = getmembers(self, lambda x: ismethod(x) and re.match(r'^_\d{2}_.*', x.__name__)) # Run numbered functions in order for _, fn in sorted(checks, key=lambda x: x[0]): self._get_prev_traj_id() fn()
def _get_prev_traj_id(self): traj = one.alyx.get('/trajectories?' f'&probe_id={self.probe_id}' '&provenance=Ephys aligned histology track', clobber=True) if traj: self.prev_traj_id = traj[0]['id'] def _01_normal_computation(self): align_qc = AlignmentQC(self.probe_id, one=one, brain_atlas=brain_atlas, channels=False) align_qc.load_data(prev_alignments=self.traj['json'], xyz_picks=np.array(self.xyz_picks) / 1e6, cluster_chns=self.cluster_chns, depths=SITES_COORDINATES[:, 1], chn_coords=SITES_COORDINATES) align_qc.run(update=True, upload_alyx=True, upload_flatiron=False) _verify(self, alignment_resolved=False, alignment_stored='2020-09-28T15:57:25_mayo', alignment_count=3, trajectory_created=False, alignment_qc=0.604081) def _02_manual_resolution_latest(self): align_qc = AlignmentQC(self.probe_id, one=one, brain_atlas=brain_atlas, channels=False) align_qc.load_data(prev_alignments=self.traj['json'], xyz_picks=np.array(self.xyz_picks) / 1e6, cluster_chns=self.cluster_chns, depths=SITES_COORDINATES[:, 1], chn_coords=SITES_COORDINATES) align_qc.resolve_manual('2020-09-28T15:57:25_mayo', update=True, upload_alyx=True, upload_flatiron=False) _verify(self, alignment_resolved='experimenter', alignment_stored='2020-09-28T15:57:25_mayo', alignment_count=3, trajectory_created=False, alignment_qc=0.604081, alignment_date=date.today().isoformat()) def _03_manual_resolution_not_latest(self): align_qc = AlignmentQC(self.probe_id, one=one, brain_atlas=brain_atlas, channels=False) align_qc.load_data(prev_alignments=self.traj['json'], xyz_picks=np.array(self.xyz_picks) / 1e6, cluster_chns=self.cluster_chns, depths=SITES_COORDINATES[:, 1], chn_coords=SITES_COORDINATES) align_qc.resolve_manual('2020-09-28T10:03:06_alejandro', update=True, upload_alyx=True, upload_flatiron=False, force=True) _verify(self, alignment_resolved='experimenter', alignment_stored='2020-09-28T10:03:06_alejandro', alignment_count=3, trajectory_created=True, alignment_qc=0.604081, alignment_date=date.today().isoformat())
[docs] @classmethod def tearDownClass(cls) -> None: one.alyx.rest('insertions', 'delete', id=cls.probe_id) one.alyx.rest('sessions', 'delete', id=cls.eid)
def _verify(tc, alignment_resolved=None, alignment_count=None, alignment_stored=None, trajectory_created=False, alignment_qc=None, alignment_date=None): """ For a given test case with a `probe_id` attribute, check that Alyx returns insertion records that match the provided parameters. :param tc: An instance of TestAlignmentQcManual or TestAlignmentQcExisting :param alignment_resolved: Check the alignment_resolved is true or false :param alignment_count: Check the alignment count matches the one given :param alignment_stored: Check the alignment stored key matches the one given :param trajectory_created: Check whether a new trajectory exists on Alyx :param alignment_qc: Check the stored QC value is close to the provided one :return: """ QC_THRESH = 0.8 # Expected alignment QC threshold insertion = one.alyx.get(f'/insertions/{tc.probe_id}', clobber=True) tc.assertEqual('NOT_SET', insertion['json']['qc']) if alignment_count is not None: tc.assertEqual(alignment_count, insertion['json']['extended_qc']['alignment_count']) if alignment_stored is not None: tc.assertEqual(alignment_stored, insertion['json']['extended_qc']['alignment_stored']) if alignment_resolved: tc.assertEqual(alignment_resolved, insertion['json']['extended_qc']['alignment_resolved_by']) tc.assertEqual(1, insertion['json']['extended_qc']['alignment_resolved']) elif alignment_resolved is False: tc.assertEqual(0, insertion['json']['extended_qc']['alignment_resolved']) if alignment_qc: tc.assertEqual(insertion['json']['extended_qc']['alignment_qc'] < QC_THRESH, alignment_qc < QC_THRESH) tc.assertTrue(np.isclose(insertion['json']['extended_qc']['alignment_qc'], alignment_qc)) if tc.prev_traj_id: traj = one.alyx.get('/trajectories?' f'&probe_id={tc.probe_id}' '&provenance=Ephys aligned histology track', clobber=True) tc.assertNotEqual(tc.prev_traj_id == traj[0]['id'], trajectory_created) if alignment_date: tc.assertEqual(insertion['json']['extended_qc']['alignment_resolved_date'], alignment_date)
[docs] class TestUploadToFlatIron(unittest.TestCase): probe_id = None alignments = None xyz_picks = None trajectory = None
[docs] @unittest.skip("Skip FTP upload test") @classmethod def setUpClass(cls) -> None: data = np.load(Path(Path(__file__).parent.parent. joinpath('fixtures', 'qc', 'data_alignmentqc_manual.npz')), allow_pickle=True) cls.xyz_picks = (data['xyz_picks'] * 1e6).tolist() cls.alignments = data['alignments'].tolist() cls.cluster_chns = data['cluster_chns'] data = np.load(Path(Path(__file__).parent.parent. joinpath('fixtures', 'qc', 'data_alignmentqc_existing.npz')), allow_pickle=True) insertion = data['insertion'].tolist() insertion['json'] = {'xyz_picks': cls.xyz_picks} probe_insertion = one.alyx.rest('insertions', 'create', data=insertion) cls.probe_id = probe_insertion['id'] cls.probe_name = probe_insertion['name'] cls.trajectory = data['trajectory'].tolist() cls.trajectory.update({'probe_insertion': cls.probe_id}) cls.trajectory.update({'chronic_insertion': None}) cls.trajectory.update({'json': cls.alignments}) cls.traj = one.alyx.rest('trajectories', 'create', data=cls.trajectory) align_qc = AlignmentQC(cls.probe_id, one=one, brain_atlas=brain_atlas, channels=False) align_qc.load_data(prev_alignments=cls.traj['json'], xyz_picks=np.array(cls.xyz_picks) / 1e6, cluster_chns=cls.cluster_chns, depths=SITES_COORDINATES[:, 1], chn_coords=SITES_COORDINATES) cls.file_paths = align_qc.resolve_manual('2020-09-28T15:57:25_mayo', update=True, upload_alyx=True, upload_flatiron=True) print(cls.file_paths)
[docs] def test_data_content(self): alf_path = one.eid2path(EPHYS_SESSION).joinpath('alf', self.probe_name) channels_mlapdv = np.load(alf_path.joinpath('channels.mlapdv.npy')) self.assertTrue(np.all(np.abs(channels_mlapdv) > 0)) channels_id = np.load(alf_path.joinpath('channels.brainLocationIds_ccf_2017.npy')) self.assertEqual(channels_mlapdv.shape[0], channels_id.shape[0])
[docs] def test_upload_to_flatiron(self): for file in self.file_paths: file_registered = one.alyx.get(f'/datasets?&session={EPHYS_SESSION}' f'&dataset_type={file.stem}') data_id = file_registered[0]['url'][-36:] self.assertEqual(len(file_registered), 1) one.alyx.rest('datasets', 'delete', id=data_id)
[docs] @classmethod def tearDownClass(cls) -> None: one.alyx.rest('insertions', 'delete', id=cls.probe_id)
if __name__ == '__main__': unittest.main(exit=False, verbosity=2)