From e4defad233ad6825806ad1ad68dc23ed261bd63c Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Fri, 27 Feb 2026 21:32:33 +0530 Subject: [PATCH 01/16] fix: replace dead neuroinformatics.nl URL with Allen mesoscale GCS via cloud-volume (#266) --- .../allen_brain_atlas/streamlines.py | 171 ++++++++++++++---- pyproject.toml | 1 + 2 files changed, 136 insertions(+), 36 deletions(-) diff --git a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py index 29fc42f1..07f522d1 100644 --- a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py +++ b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py @@ -1,4 +1,6 @@ import pandas as pd +import numpy as np +import requests as http_requests from loguru import logger from myterial import orange from rich import print @@ -14,28 +16,37 @@ except ModuleNotFoundError: # pragma: no cover allen_sdk_installed = False # pragma: no cover +try: + import cloudvolume + + cloudvolume_installed = True +except ModuleNotFoundError: # pragma: no cover + cloudvolume_installed = False # pragma: no cover + from brainrender import base_dir -from brainrender._io import request from brainrender._utils import listify streamlines_folder = base_dir / "streamlines" streamlines_folder.mkdir(exist_ok=True) +ALLEN_MESOSCALE_URL = "precomputed://gs://allen_neuroglancer_ccf/allen_mesoscale" +ALLEN_API_URL = "https://api.brain-map.org/api/v2/data/query.json" +VOXEL_SIZE_NM = 1000 # skeleton vertices are in nanometers +DV_EXTENT_UM = 8000.0 # 320 voxels * 25um = full DV extent of Allen CCF + def experiments_source_search(SOI): """ Returns data about experiments whose injection was in the SOI, structure of interest :param SOI: str, structure of interest. Acronym of structure to use as seed for the search - :param source: (Default value = True) """ - transgenic_id = 0 # id = 0 means use only wild type primary_structure_only = True if not allen_sdk_installed: print( - f"[{orange}]Streamlines cannot be download because the AllenSDK package is not installed. " + f"[{orange}]Streamlines cannot be downloaded because the AllenSDK package is not installed. " "Please install `allensdk` with `pip install allensdk`" ) return None @@ -50,57 +61,145 @@ def experiments_source_search(SOI): ) -def get_streamlines_data(eids, force_download=False): +def _get_injection_site_um(eid): """ - Given a list of expeirmental IDs, it downloads the streamline data - from the https://neuroinformatics.nl cache and saves them as - json files. + Fetches the injection site coordinates for an experiment from the Allen + Brain Atlas API. Coordinates are returned in the same um space as the + streamline vertices (x, y_flipped, z). + + Uses the ProjectionStructureUnionize endpoint which provides max_voxel + coordinates in Allen CCF um space for the injection site voxel. - :param eids: list of integers with experiments IDs + :param eid: int, experiment ID + :return: dict with x, y, z keys or None if not found """ - data = [] - for eid in track(eids, total=len(eids), description="downloading"): - url = "https://neuroinformatics.nl/HBP/allen-connectivity-viewer/json/streamlines_{}.json.gz".format( - eid + try: + url = ( + f"{ALLEN_API_URL}?q=model::ProjectionStructureUnionize," + f"rma::criteria,section_data_set[id$eq{eid}]," + f"rma::criteria,[is_injection$eqtrue]," + f"rma::options[num_rows$eq1][order$eq'projection_volume desc']" ) + response = http_requests.get(url, timeout=10) + data = response.json() + if data["success"] and data["num_rows"] > 0: + voxel = data["msg"][0] + x = float(voxel["max_voxel_x"]) + y = float(DV_EXTENT_UM - voxel["max_voxel_y"]) # flip DV axis + z = float(voxel["max_voxel_z"]) + return {"x": x, "y": y, "z": z} + except Exception as e: + logger.warning(f"Could not fetch injection site for experiment {eid}: {e}") + return None + + +def _skeleton_to_dataframe(skeleton, eid): + """ + Converts a cloudvolume Skeleton object to the pd.DataFrame format + expected by brainrender's Streamlines actor. - jsonpath = streamlines_folder / f"{eid}.json" + Vertices are in nanometers and in Allen CCF PIR space. We: + 1. Convert nm -> um (divide by VOXEL_SIZE_NM) + 2. Flip the y (DV) axis to match brainrender's ASR orientation - if not jsonpath.exists() or force_download: - response = request(url) + The injection site is fetched from the Allen API using the experiment ID. + If unavailable, falls back to the centroid of all skeleton vertices. + + :param skeleton: cloudvolume Skeleton object + :param eid: int, experiment ID used to fetch real injection coordinates + :return: pd.DataFrame with 'lines' and 'injection_sites' columns + """ + components = skeleton.components() + + lines = [] + for component in components: + verts_um = component.vertices / VOXEL_SIZE_NM + points = [ + { + "x": float(v[0]), + "y": float(DV_EXTENT_UM - v[1]), # flip DV axis + "z": float(v[2]), + } + for v in verts_um + ] + lines.append(points) + + # get real injection site from Allen API, fall back to centroid + injection_site = _get_injection_site_um(eid) + if injection_site is None: + logger.warning( + f"Falling back to centroid for injection site of experiment {eid}" + ) + all_verts_um = skeleton.vertices / VOXEL_SIZE_NM + centroid = all_verts_um.mean(axis=0) + injection_site = { + "x": float(centroid[0]), + "y": float(DV_EXTENT_UM - centroid[1]), + "z": float(centroid[2]), + } - # Write the response content as a temporary compressed file - temp_path = streamlines_folder / "temp.gz" - with open(str(temp_path), "wb") as temp: - temp.write(response.content) + return pd.DataFrame({ + "lines": [lines], + "injection_sites": [[injection_site]], + }) - # Open in pandas and delete temp - url_data = pd.read_json( - str(temp_path), lines=True, compression="gzip" - ) - temp_path.unlink() - # save json - url_data.to_json(str(jsonpath)) +def get_streamlines_data(eids, force_download=False): + """ + Given a list of experiment IDs, downloads streamline data from the + Allen mesoscale connectivity dataset hosted on Google Cloud Storage + via cloud-volume, and saves them as JSON files. + + :param eids: list of integers with experiment IDs + :param force_download: bool, if True re-download even if cached + """ + if not cloudvolume_installed: + print( + f"[{orange}]Streamlines cannot be downloaded because the cloud-volume package is not installed. " + "Please install it with `pip install cloud-volume`" + ) + return [] + + cv = cloudvolume.CloudVolume( + ALLEN_MESOSCALE_URL, + use_https=True, + progress=False, + ) - # append to lists and return - data.append(url_data) + data = [] + for eid in track(eids, total=len(eids), description="downloading"): + jsonpath = streamlines_folder / f"{eid}.json" + + if not jsonpath.exists() or force_download: + try: + skeleton = cv.skeleton.get(int(eid)) + except Exception as e: + logger.warning( + f"Could not fetch streamlines for experiment {eid}: {e}" + ) + continue + + df = _skeleton_to_dataframe(skeleton, int(eid)) + df.to_json(str(jsonpath)) + data.append(df) else: data.append(pd.read_json(str(jsonpath))) + return data def get_streamlines_for_region(region, force_download=False): """ - Using the Allen Mouse Connectivity data and corresponding API, this function finds experiments whose injections - were targeted to the region of interest and downloads the corresponding streamlines data. By default, experiments - are selected for only WT mice and only when the region was the primary injection target. - - :param region: str with region to use for research - + Using the Allen Mouse Connectivity data and corresponding API, this function finds experiments + whose injections were targeted to the region of interest and downloads the corresponding + streamlines data from the Allen mesoscale connectivity dataset on Google Cloud Storage. + By default, experiments are selected for only WT mice and only when the region was + the primary injection target. + + :param region: str with region to use for search + :param force_download: bool, if True re-download even if cached """ logger.debug(f"Getting streamlines data for region: {region}") - # Get experiments whose injections were targeted to the region region_experiments = experiments_source_search(region) if region_experiments is None or region_experiments.empty: logger.debug("No experiments found from allen data") diff --git a/pyproject.toml b/pyproject.toml index 2a22595f..bded2ee9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dynamic = ["version"] dependencies = [ "brainglobe-atlasapi>=2.0.1", + "cloud-volume", "brainglobe-space>=1.0.0", "brainglobe-utils>=0.5.0", "h5py", From d35bf72f6dfcf1207491b578ced6d1dcfc5b6531 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Fri, 27 Feb 2026 21:36:37 +0530 Subject: [PATCH 02/16] fix: derive DV extent dynamically from atlas instead of hardcoding --- .../allen_brain_atlas/streamlines.py | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py index 07f522d1..7584ebc7 100644 --- a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py +++ b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py @@ -23,6 +23,7 @@ except ModuleNotFoundError: # pragma: no cover cloudvolume_installed = False # pragma: no cover +from brainglobe_atlasapi import BrainGlobeAtlas from brainrender import base_dir from brainrender._utils import listify @@ -33,7 +34,16 @@ ALLEN_MESOSCALE_URL = "precomputed://gs://allen_neuroglancer_ccf/allen_mesoscale" ALLEN_API_URL = "https://api.brain-map.org/api/v2/data/query.json" VOXEL_SIZE_NM = 1000 # skeleton vertices are in nanometers -DV_EXTENT_UM = 8000.0 # 320 voxels * 25um = full DV extent of Allen CCF + + +def _get_dv_extent_um(): + """ + Derives the full dorsal-ventral extent of the Allen CCF atlas in microns + dynamically from the brainglobe atlas API. Used to flip the DV axis when + converting from Allen CCF PIR space to brainrender's ASR orientation. + """ + atlas = BrainGlobeAtlas("allen_mouse_25um", check_latest=False) + return float(atlas.shape[1] * atlas.resolution[1]) def experiments_source_search(SOI): @@ -61,16 +71,17 @@ def experiments_source_search(SOI): ) -def _get_injection_site_um(eid): +def _get_injection_site_um(eid, dv_extent_um): """ Fetches the injection site coordinates for an experiment from the Allen Brain Atlas API. Coordinates are returned in the same um space as the streamline vertices (x, y_flipped, z). Uses the ProjectionStructureUnionize endpoint which provides max_voxel - coordinates in Allen CCF um space for the injection site voxel. + coordinates in Allen CCF um space for the highest-density injection voxel. :param eid: int, experiment ID + :param dv_extent_um: float, full DV extent of the atlas in um for axis flip :return: dict with x, y, z keys or None if not found """ try: @@ -85,7 +96,7 @@ def _get_injection_site_um(eid): if data["success"] and data["num_rows"] > 0: voxel = data["msg"][0] x = float(voxel["max_voxel_x"]) - y = float(DV_EXTENT_UM - voxel["max_voxel_y"]) # flip DV axis + y = float(dv_extent_um - voxel["max_voxel_y"]) # flip DV axis z = float(voxel["max_voxel_z"]) return {"x": x, "y": y, "z": z} except Exception as e: @@ -93,7 +104,7 @@ def _get_injection_site_um(eid): return None -def _skeleton_to_dataframe(skeleton, eid): +def _skeleton_to_dataframe(skeleton, eid, dv_extent_um): """ Converts a cloudvolume Skeleton object to the pd.DataFrame format expected by brainrender's Streamlines actor. @@ -107,6 +118,7 @@ def _skeleton_to_dataframe(skeleton, eid): :param skeleton: cloudvolume Skeleton object :param eid: int, experiment ID used to fetch real injection coordinates + :param dv_extent_um: float, full DV extent of the atlas in um for axis flip :return: pd.DataFrame with 'lines' and 'injection_sites' columns """ components = skeleton.components() @@ -117,7 +129,7 @@ def _skeleton_to_dataframe(skeleton, eid): points = [ { "x": float(v[0]), - "y": float(DV_EXTENT_UM - v[1]), # flip DV axis + "y": float(dv_extent_um - v[1]), # flip DV axis "z": float(v[2]), } for v in verts_um @@ -125,7 +137,7 @@ def _skeleton_to_dataframe(skeleton, eid): lines.append(points) # get real injection site from Allen API, fall back to centroid - injection_site = _get_injection_site_um(eid) + injection_site = _get_injection_site_um(eid, dv_extent_um) if injection_site is None: logger.warning( f"Falling back to centroid for injection site of experiment {eid}" @@ -134,7 +146,7 @@ def _skeleton_to_dataframe(skeleton, eid): centroid = all_verts_um.mean(axis=0) injection_site = { "x": float(centroid[0]), - "y": float(DV_EXTENT_UM - centroid[1]), + "y": float(dv_extent_um - centroid[1]), "z": float(centroid[2]), } @@ -160,6 +172,8 @@ def get_streamlines_data(eids, force_download=False): ) return [] + dv_extent_um = _get_dv_extent_um() + cv = cloudvolume.CloudVolume( ALLEN_MESOSCALE_URL, use_https=True, @@ -179,7 +193,7 @@ def get_streamlines_data(eids, force_download=False): ) continue - df = _skeleton_to_dataframe(skeleton, int(eid)) + df = _skeleton_to_dataframe(skeleton, int(eid), dv_extent_um) df.to_json(str(jsonpath)) data.append(df) else: From 0f7facc5614c9dc90932c3075f12f51fc210b31c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:09:25 +0000 Subject: [PATCH 03/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../allen_brain_atlas/streamlines.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py index 7584ebc7..a474164d 100644 --- a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py +++ b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py @@ -1,5 +1,4 @@ import pandas as pd -import numpy as np import requests as http_requests from loguru import logger from myterial import orange @@ -31,7 +30,9 @@ streamlines_folder = base_dir / "streamlines" streamlines_folder.mkdir(exist_ok=True) -ALLEN_MESOSCALE_URL = "precomputed://gs://allen_neuroglancer_ccf/allen_mesoscale" +ALLEN_MESOSCALE_URL = ( + "precomputed://gs://allen_neuroglancer_ccf/allen_mesoscale" +) ALLEN_API_URL = "https://api.brain-map.org/api/v2/data/query.json" VOXEL_SIZE_NM = 1000 # skeleton vertices are in nanometers @@ -100,7 +101,9 @@ def _get_injection_site_um(eid, dv_extent_um): z = float(voxel["max_voxel_z"]) return {"x": x, "y": y, "z": z} except Exception as e: - logger.warning(f"Could not fetch injection site for experiment {eid}: {e}") + logger.warning( + f"Could not fetch injection site for experiment {eid}: {e}" + ) return None @@ -150,10 +153,12 @@ def _skeleton_to_dataframe(skeleton, eid, dv_extent_um): "z": float(centroid[2]), } - return pd.DataFrame({ - "lines": [lines], - "injection_sites": [[injection_site]], - }) + return pd.DataFrame( + { + "lines": [lines], + "injection_sites": [[injection_site]], + } + ) def get_streamlines_data(eids, force_download=False): From 75faeebb7bafa9c9e0b91ad5014b69c15a23ea18 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Tue, 3 Mar 2026 01:30:12 +0530 Subject: [PATCH 04/16] test: add unit tests for streamlines rewrite with mocked I/O --- tests/test_streamlines.py | 180 +++++++++++++++++++++++++++++++++----- 1 file changed, 156 insertions(+), 24 deletions(-) diff --git a/tests/test_streamlines.py b/tests/test_streamlines.py index 47efadbb..85767cfb 100644 --- a/tests/test_streamlines.py +++ b/tests/test_streamlines.py @@ -1,36 +1,168 @@ +import json +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import numpy as np import pandas as pd import pytest -from brainrender import Scene -from brainrender.actors.streamlines import ( - Streamlines, - make_streamlines, -) from brainrender.atlas_specific import get_streamlines_for_region +from brainrender.atlas_specific.allen_brain_atlas.streamlines import ( + _get_dv_extent_um, + _get_injection_site_um, + _skeleton_to_dataframe, + get_streamlines_data, +) + +DV_EXTENT = 5200.0 + + +def _make_fake_skeleton(): + verts = np.array([[1000.0, 2000.0, 3000.0]] * 4, dtype=float) + comp1 = MagicMock() + comp1.vertices = verts[:2] + comp2 = MagicMock() + comp2.vertices = verts[2:] + skeleton = MagicMock() + skeleton.vertices = verts + skeleton.components.return_value = [comp1, comp2] + return skeleton + + +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.BrainGlobeAtlas") +def test_get_dv_extent_um(mock_atlas_cls): + mock_atlas = MagicMock() + mock_atlas.shape = (528, 320, 456) + mock_atlas.resolution = (25, 25, 25) + mock_atlas_cls.return_value = mock_atlas + result = _get_dv_extent_um() + assert result == pytest.approx(320 * 25) + + +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get") +def test_get_injection_site_success(mock_get): + mock_resp = MagicMock() + mock_resp.json.return_value = { + "success": True, "num_rows": 1, + "msg": [{"max_voxel_x": 100.0, "max_voxel_y": 200.0, "max_voxel_z": 300.0}], + } + mock_get.return_value = mock_resp + result = _get_injection_site_um(12345, DV_EXTENT) + assert result is not None + assert result["x"] == pytest.approx(100.0) + assert result["y"] == pytest.approx(DV_EXTENT - 200.0) + assert result["z"] == pytest.approx(300.0) + + +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get") +def test_get_injection_site_empty_response(mock_get): + mock_resp = MagicMock() + mock_resp.json.return_value = {"success": True, "num_rows": 0, "msg": []} + mock_get.return_value = mock_resp + assert _get_injection_site_um(12345, DV_EXTENT) is None + + +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get") +def test_get_injection_site_network_error(mock_get): + mock_get.side_effect = Exception("timeout") + assert _get_injection_site_um(12345, DV_EXTENT) is None + + +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._get_injection_site_um") +def test_skeleton_to_dataframe_with_injection(mock_inj): + mock_inj.return_value = {"x": 10.0, "y": 20.0, "z": 30.0} + skeleton = _make_fake_skeleton() + df = _skeleton_to_dataframe(skeleton, 99, DV_EXTENT) + assert isinstance(df, pd.DataFrame) + assert "lines" in df.columns + assert "injection_sites" in df.columns + lines = df["lines"].iloc[0] + assert len(lines) == 2 + pt = lines[0][0] + assert set(pt.keys()) == {"x", "y", "z"} + assert pt["x"] == pytest.approx(1.0) + assert pt["y"] == pytest.approx(DV_EXTENT - 2.0) + assert pt["z"] == pytest.approx(3.0) + assert df["injection_sites"].iloc[0] == [{"x": 10.0, "y": 20.0, "z": 30.0}] + + +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._get_injection_site_um") +def test_skeleton_to_dataframe_fallback_centroid(mock_inj): + mock_inj.return_value = None + skeleton = _make_fake_skeleton() + df = _skeleton_to_dataframe(skeleton, 99, DV_EXTENT) + injection = df["injection_sites"].iloc[0][0] + assert set(injection.keys()) == {"x", "y", "z"} + assert injection["x"] == pytest.approx(1.0) + + +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um") +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True) +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._skeleton_to_dataframe") +def test_get_streamlines_data_downloads(mock_s2df, mock_dv): + mock_dv.return_value = DV_EXTENT + fake_df = pd.DataFrame({"lines": [[]], "injection_sites": [[]]}) + mock_s2df.return_value = fake_df + mock_cv_module = MagicMock() + mock_cv_instance = MagicMock() + mock_cv_instance.skeleton.get.return_value = _make_fake_skeleton() + mock_cv_module.CloudVolume.return_value = mock_cv_instance + with tempfile.TemporaryDirectory() as tmpdir: + with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.streamlines_folder", Path(tmpdir)): + with patch.dict("sys.modules", {"cloudvolume": mock_cv_module}): + with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume", mock_cv_module, create=True): + result = get_streamlines_data([111, 222], force_download=True) + assert len(result) == 2 + assert all(isinstance(r, pd.DataFrame) for r in result) + + +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um") +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True) +def test_get_streamlines_data_uses_cache(mock_dv): + mock_dv.return_value = DV_EXTENT + fake_df = pd.DataFrame({"lines": [[[]]], "injection_sites": [[{"x": 1, "y": 2, "z": 3}]]}) + mock_cv_module = MagicMock() + with tempfile.TemporaryDirectory() as tmpdir: + fake_df.to_json(str(Path(tmpdir) / "111.json")) + with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.streamlines_folder", Path(tmpdir)): + with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume", mock_cv_module, create=True): + result = get_streamlines_data([111], force_download=False) + mock_cv_module.CloudVolume.return_value.skeleton.get.assert_not_called() + assert len(result) == 1 -@pytest.mark.xfail(reason="Likely due to fail due to neuromorpho") -def test_download(): - streams = get_streamlines_for_region("TH", force_download=False) - assert len(streams) == 54 - assert isinstance(streams[0], pd.DataFrame) +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", False) +def test_get_streamlines_data_no_cloudvolume(): + assert get_streamlines_data([111]) == [] -@pytest.mark.xfail(reason="Likely due to fail due to neuromorpho") -def test_download_slow(): - streams = get_streamlines_for_region("TH", force_download=True) - assert len(streams) == 54 - assert isinstance(streams[0], pd.DataFrame) +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um") +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True) +def test_get_streamlines_data_skips_failed_experiment(mock_dv): + mock_dv.return_value = DV_EXTENT + mock_cv_module = MagicMock() + mock_cv_instance = MagicMock() + mock_cv_instance.skeleton.get.side_effect = Exception("not found") + mock_cv_module.CloudVolume.return_value = mock_cv_instance + with tempfile.TemporaryDirectory() as tmpdir: + with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.streamlines_folder", Path(tmpdir)): + with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume", mock_cv_module, create=True): + result = get_streamlines_data([999], force_download=True) + assert result == [] -@pytest.mark.xfail(reason="Likely due to fail due to neuromorpho") -def test_streamlines(): - s = Scene(title="BR") - streams = get_streamlines_for_region("TH", force_download=False) - s.add(Streamlines(streams[0])) - s.add(*make_streamlines(*streams[1:3])) +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.experiments_source_search") +def test_get_streamlines_for_region_no_experiments(mock_search): + mock_search.return_value = pd.DataFrame() + assert get_streamlines_for_region("XYZ") is None - with pytest.raises(TypeError): - Streamlines([1, 2, 3]) - del s +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.get_streamlines_data") +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.experiments_source_search") +def test_get_streamlines_for_region_calls_download(mock_search, mock_dl): + mock_search.return_value = pd.DataFrame({"id": [111, 222]}) + mock_dl.return_value = ["df1", "df2"] + result = get_streamlines_for_region("TH") + assert result == ["df1", "df2"] + mock_dl.assert_called_once() From 5eae27645d96ad99d00527928b953c3b5b4eae62 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:07:44 +0000 Subject: [PATCH 05/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_streamlines.py | 121 +++++++++++++++++++++++++++++--------- 1 file changed, 93 insertions(+), 28 deletions(-) diff --git a/tests/test_streamlines.py b/tests/test_streamlines.py index 85767cfb..d90f3961 100644 --- a/tests/test_streamlines.py +++ b/tests/test_streamlines.py @@ -1,4 +1,3 @@ -import json import tempfile from pathlib import Path from unittest.mock import MagicMock, patch @@ -30,7 +29,9 @@ def _make_fake_skeleton(): return skeleton -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.BrainGlobeAtlas") +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.BrainGlobeAtlas" +) def test_get_dv_extent_um(mock_atlas_cls): mock_atlas = MagicMock() mock_atlas.shape = (528, 320, 456) @@ -40,12 +41,17 @@ def test_get_dv_extent_um(mock_atlas_cls): assert result == pytest.approx(320 * 25) -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get") +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get" +) def test_get_injection_site_success(mock_get): mock_resp = MagicMock() mock_resp.json.return_value = { - "success": True, "num_rows": 1, - "msg": [{"max_voxel_x": 100.0, "max_voxel_y": 200.0, "max_voxel_z": 300.0}], + "success": True, + "num_rows": 1, + "msg": [ + {"max_voxel_x": 100.0, "max_voxel_y": 200.0, "max_voxel_z": 300.0} + ], } mock_get.return_value = mock_resp result = _get_injection_site_um(12345, DV_EXTENT) @@ -55,7 +61,9 @@ def test_get_injection_site_success(mock_get): assert result["z"] == pytest.approx(300.0) -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get") +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get" +) def test_get_injection_site_empty_response(mock_get): mock_resp = MagicMock() mock_resp.json.return_value = {"success": True, "num_rows": 0, "msg": []} @@ -63,13 +71,17 @@ def test_get_injection_site_empty_response(mock_get): assert _get_injection_site_um(12345, DV_EXTENT) is None -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get") +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get" +) def test_get_injection_site_network_error(mock_get): mock_get.side_effect = Exception("timeout") assert _get_injection_site_um(12345, DV_EXTENT) is None -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._get_injection_site_um") +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines._get_injection_site_um" +) def test_skeleton_to_dataframe_with_injection(mock_inj): mock_inj.return_value = {"x": 10.0, "y": 20.0, "z": 30.0} skeleton = _make_fake_skeleton() @@ -87,7 +99,9 @@ def test_skeleton_to_dataframe_with_injection(mock_inj): assert df["injection_sites"].iloc[0] == [{"x": 10.0, "y": 20.0, "z": 30.0}] -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._get_injection_site_um") +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines._get_injection_site_um" +) def test_skeleton_to_dataframe_fallback_centroid(mock_inj): mock_inj.return_value = None skeleton = _make_fake_skeleton() @@ -97,9 +111,16 @@ def test_skeleton_to_dataframe_fallback_centroid(mock_inj): assert injection["x"] == pytest.approx(1.0) -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um") -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True) -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._skeleton_to_dataframe") +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um" +) +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", + True, +) +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines._skeleton_to_dataframe" +) def test_get_streamlines_data_downloads(mock_s2df, mock_dv): mock_dv.return_value = DV_EXTENT fake_df = pd.DataFrame({"lines": [[]], "injection_sites": [[]]}) @@ -109,36 +130,67 @@ def test_get_streamlines_data_downloads(mock_s2df, mock_dv): mock_cv_instance.skeleton.get.return_value = _make_fake_skeleton() mock_cv_module.CloudVolume.return_value = mock_cv_instance with tempfile.TemporaryDirectory() as tmpdir: - with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.streamlines_folder", Path(tmpdir)): + with patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.streamlines_folder", + Path(tmpdir), + ): with patch.dict("sys.modules", {"cloudvolume": mock_cv_module}): - with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume", mock_cv_module, create=True): - result = get_streamlines_data([111, 222], force_download=True) + with patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume", + mock_cv_module, + create=True, + ): + result = get_streamlines_data( + [111, 222], force_download=True + ) assert len(result) == 2 assert all(isinstance(r, pd.DataFrame) for r in result) -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um") -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True) +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um" +) +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", + True, +) def test_get_streamlines_data_uses_cache(mock_dv): mock_dv.return_value = DV_EXTENT - fake_df = pd.DataFrame({"lines": [[[]]], "injection_sites": [[{"x": 1, "y": 2, "z": 3}]]}) + fake_df = pd.DataFrame( + {"lines": [[[]]], "injection_sites": [[{"x": 1, "y": 2, "z": 3}]]} + ) mock_cv_module = MagicMock() with tempfile.TemporaryDirectory() as tmpdir: fake_df.to_json(str(Path(tmpdir) / "111.json")) - with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.streamlines_folder", Path(tmpdir)): - with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume", mock_cv_module, create=True): + with patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.streamlines_folder", + Path(tmpdir), + ): + with patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume", + mock_cv_module, + create=True, + ): result = get_streamlines_data([111], force_download=False) mock_cv_module.CloudVolume.return_value.skeleton.get.assert_not_called() assert len(result) == 1 -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", False) +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", + False, +) def test_get_streamlines_data_no_cloudvolume(): assert get_streamlines_data([111]) == [] -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um") -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True) +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um" +) +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", + True, +) def test_get_streamlines_data_skips_failed_experiment(mock_dv): mock_dv.return_value = DV_EXTENT mock_cv_module = MagicMock() @@ -146,20 +198,33 @@ def test_get_streamlines_data_skips_failed_experiment(mock_dv): mock_cv_instance.skeleton.get.side_effect = Exception("not found") mock_cv_module.CloudVolume.return_value = mock_cv_instance with tempfile.TemporaryDirectory() as tmpdir: - with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.streamlines_folder", Path(tmpdir)): - with patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume", mock_cv_module, create=True): + with patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.streamlines_folder", + Path(tmpdir), + ): + with patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume", + mock_cv_module, + create=True, + ): result = get_streamlines_data([999], force_download=True) assert result == [] -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.experiments_source_search") +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.experiments_source_search" +) def test_get_streamlines_for_region_no_experiments(mock_search): mock_search.return_value = pd.DataFrame() assert get_streamlines_for_region("XYZ") is None -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.get_streamlines_data") -@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.experiments_source_search") +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.get_streamlines_data" +) +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.experiments_source_search" +) def test_get_streamlines_for_region_calls_download(mock_search, mock_dl): mock_search.return_value = pd.DataFrame({"id": [111, 222]}) mock_dl.return_value = ["df1", "df2"] From 47b8ad8720acdccdfbc91b1c2d4287b5747d6fb5 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Wed, 11 Mar 2026 01:49:20 +0530 Subject: [PATCH 06/16] fix: pin cloud-volume>=3.11.0, cache DV extent, clarify test fixture value --- .../atlas_specific/allen_brain_atlas/streamlines.py | 13 +++++++++++-- pyproject.toml | 2 +- tests/test_streamlines.py | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py index a474164d..c4e04c48 100644 --- a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py +++ b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py @@ -37,14 +37,23 @@ VOXEL_SIZE_NM = 1000 # skeleton vertices are in nanometers +_dv_extent_um_cache = None + + def _get_dv_extent_um(): """ Derives the full dorsal-ventral extent of the Allen CCF atlas in microns dynamically from the brainglobe atlas API. Used to flip the DV axis when converting from Allen CCF PIR space to brainrender's ASR orientation. + + Result is cached after the first call to avoid reinstantiating the atlas + on every experiment download. """ - atlas = BrainGlobeAtlas("allen_mouse_25um", check_latest=False) - return float(atlas.shape[1] * atlas.resolution[1]) + global _dv_extent_um_cache + if _dv_extent_um_cache is None: + atlas = BrainGlobeAtlas("allen_mouse_25um", check_latest=False) + _dv_extent_um_cache = float(atlas.shape[1] * atlas.resolution[1]) + return _dv_extent_um_cache def experiments_source_search(SOI): diff --git a/pyproject.toml b/pyproject.toml index bded2ee9..6c520e4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dynamic = ["version"] dependencies = [ "brainglobe-atlasapi>=2.0.1", - "cloud-volume", + "cloud-volume>=3.11.0", "brainglobe-space>=1.0.0", "brainglobe-utils>=0.5.0", "h5py", diff --git a/tests/test_streamlines.py b/tests/test_streamlines.py index d90f3961..048804b3 100644 --- a/tests/test_streamlines.py +++ b/tests/test_streamlines.py @@ -14,6 +14,7 @@ get_streamlines_data, ) +# Arbitrary test fixture value only - real Allen CCF 25um atlas DV extent is 8000um DV_EXTENT = 5200.0 From 1b8a851bbe05a655f10d7404addd24a9bc00e673 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sun, 5 Apr 2026 20:35:28 +0530 Subject: [PATCH 07/16] =?UTF-8?q?fix:=20remove=20axis=20flips=20=E2=80=94?= =?UTF-8?q?=20brain=20mesh=20already=20in=20PIR=20space?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Numerical verification showed brainrender's atlas mesh vertices use raw Allen CCF (PIR) coordinates, so no PIR→ASR conversion is needed. Removed _get_atlas_extents_um, _get_dv_extent_um, and all axis flip logic. Skeleton vertices are now passed through as-is after nm→um conversion. Co-developed-by: RobertoDF (identified the flip issue in brainglobe#438) --- .../allen_brain_atlas/streamlines.py | 71 +++++-------------- tests/test_streamlines.py | 54 ++++---------- 2 files changed, 31 insertions(+), 94 deletions(-) diff --git a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py index c4e04c48..f0c26d7a 100644 --- a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py +++ b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py @@ -22,8 +22,6 @@ except ModuleNotFoundError: # pragma: no cover cloudvolume_installed = False # pragma: no cover -from brainglobe_atlasapi import BrainGlobeAtlas - from brainrender import base_dir from brainrender._utils import listify @@ -37,24 +35,6 @@ VOXEL_SIZE_NM = 1000 # skeleton vertices are in nanometers -_dv_extent_um_cache = None - - -def _get_dv_extent_um(): - """ - Derives the full dorsal-ventral extent of the Allen CCF atlas in microns - dynamically from the brainglobe atlas API. Used to flip the DV axis when - converting from Allen CCF PIR space to brainrender's ASR orientation. - - Result is cached after the first call to avoid reinstantiating the atlas - on every experiment download. - """ - global _dv_extent_um_cache - if _dv_extent_um_cache is None: - atlas = BrainGlobeAtlas("allen_mouse_25um", check_latest=False) - _dv_extent_um_cache = float(atlas.shape[1] * atlas.resolution[1]) - return _dv_extent_um_cache - def experiments_source_search(SOI): """ @@ -81,17 +61,13 @@ def experiments_source_search(SOI): ) -def _get_injection_site_um(eid, dv_extent_um): +def _get_injection_site_um(eid): """ Fetches the injection site coordinates for an experiment from the Allen - Brain Atlas API. Coordinates are returned in the same um space as the - streamline vertices (x, y_flipped, z). - - Uses the ProjectionStructureUnionize endpoint which provides max_voxel - coordinates in Allen CCF um space for the highest-density injection voxel. + Brain Atlas API. Coordinates are in Allen CCF um space (PIR), matching + brainrender's brain mesh coordinate system. :param eid: int, experiment ID - :param dv_extent_um: float, full DV extent of the atlas in um for axis flip :return: dict with x, y, z keys or None if not found """ try: @@ -105,10 +81,11 @@ def _get_injection_site_um(eid, dv_extent_um): data = response.json() if data["success"] and data["num_rows"] > 0: voxel = data["msg"][0] - x = float(voxel["max_voxel_x"]) - y = float(dv_extent_um - voxel["max_voxel_y"]) # flip DV axis - z = float(voxel["max_voxel_z"]) - return {"x": x, "y": y, "z": z} + return { + "x": float(voxel["max_voxel_x"]), + "y": float(voxel["max_voxel_y"]), + "z": float(voxel["max_voxel_z"]), + } except Exception as e: logger.warning( f"Could not fetch injection site for experiment {eid}: {e}" @@ -116,21 +93,17 @@ def _get_injection_site_um(eid, dv_extent_um): return None -def _skeleton_to_dataframe(skeleton, eid, dv_extent_um): +def _skeleton_to_dataframe(skeleton, eid): """ Converts a cloudvolume Skeleton object to the pd.DataFrame format expected by brainrender's Streamlines actor. - Vertices are in nanometers and in Allen CCF PIR space. We: - 1. Convert nm -> um (divide by VOXEL_SIZE_NM) - 2. Flip the y (DV) axis to match brainrender's ASR orientation - - The injection site is fetched from the Allen API using the experiment ID. - If unavailable, falls back to the centroid of all skeleton vertices. + Vertices are in nanometers in Allen CCF space. We convert nm -> um + (divide by VOXEL_SIZE_NM). No axis flips are needed because + brainrender's brain mesh uses the same PIR coordinate system. :param skeleton: cloudvolume Skeleton object :param eid: int, experiment ID used to fetch real injection coordinates - :param dv_extent_um: float, full DV extent of the atlas in um for axis flip :return: pd.DataFrame with 'lines' and 'injection_sites' columns """ components = skeleton.components() @@ -139,17 +112,12 @@ def _skeleton_to_dataframe(skeleton, eid, dv_extent_um): for component in components: verts_um = component.vertices / VOXEL_SIZE_NM points = [ - { - "x": float(v[0]), - "y": float(dv_extent_um - v[1]), # flip DV axis - "z": float(v[2]), - } + {"x": float(v[0]), "y": float(v[1]), "z": float(v[2])} for v in verts_um ] lines.append(points) - # get real injection site from Allen API, fall back to centroid - injection_site = _get_injection_site_um(eid, dv_extent_um) + injection_site = _get_injection_site_um(eid) if injection_site is None: logger.warning( f"Falling back to centroid for injection site of experiment {eid}" @@ -158,15 +126,12 @@ def _skeleton_to_dataframe(skeleton, eid, dv_extent_um): centroid = all_verts_um.mean(axis=0) injection_site = { "x": float(centroid[0]), - "y": float(dv_extent_um - centroid[1]), + "y": float(centroid[1]), "z": float(centroid[2]), } return pd.DataFrame( - { - "lines": [lines], - "injection_sites": [[injection_site]], - } + {"lines": [lines], "injection_sites": [[injection_site]]} ) @@ -186,8 +151,6 @@ def get_streamlines_data(eids, force_download=False): ) return [] - dv_extent_um = _get_dv_extent_um() - cv = cloudvolume.CloudVolume( ALLEN_MESOSCALE_URL, use_https=True, @@ -207,7 +170,7 @@ def get_streamlines_data(eids, force_download=False): ) continue - df = _skeleton_to_dataframe(skeleton, int(eid), dv_extent_um) + df = _skeleton_to_dataframe(skeleton, int(eid)) df.to_json(str(jsonpath)) data.append(df) else: diff --git a/tests/test_streamlines.py b/tests/test_streamlines.py index 048804b3..72e2f01c 100644 --- a/tests/test_streamlines.py +++ b/tests/test_streamlines.py @@ -8,15 +8,11 @@ from brainrender.atlas_specific import get_streamlines_for_region from brainrender.atlas_specific.allen_brain_atlas.streamlines import ( - _get_dv_extent_um, _get_injection_site_um, _skeleton_to_dataframe, get_streamlines_data, ) -# Arbitrary test fixture value only - real Allen CCF 25um atlas DV extent is 8000um -DV_EXTENT = 5200.0 - def _make_fake_skeleton(): verts = np.array([[1000.0, 2000.0, 3000.0]] * 4, dtype=float) @@ -30,18 +26,6 @@ def _make_fake_skeleton(): return skeleton -@patch( - "brainrender.atlas_specific.allen_brain_atlas.streamlines.BrainGlobeAtlas" -) -def test_get_dv_extent_um(mock_atlas_cls): - mock_atlas = MagicMock() - mock_atlas.shape = (528, 320, 456) - mock_atlas.resolution = (25, 25, 25) - mock_atlas_cls.return_value = mock_atlas - result = _get_dv_extent_um() - assert result == pytest.approx(320 * 25) - - @patch( "brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get" ) @@ -55,10 +39,10 @@ def test_get_injection_site_success(mock_get): ], } mock_get.return_value = mock_resp - result = _get_injection_site_um(12345, DV_EXTENT) + result = _get_injection_site_um(12345) assert result is not None assert result["x"] == pytest.approx(100.0) - assert result["y"] == pytest.approx(DV_EXTENT - 200.0) + assert result["y"] == pytest.approx(200.0) assert result["z"] == pytest.approx(300.0) @@ -69,7 +53,7 @@ def test_get_injection_site_empty_response(mock_get): mock_resp = MagicMock() mock_resp.json.return_value = {"success": True, "num_rows": 0, "msg": []} mock_get.return_value = mock_resp - assert _get_injection_site_um(12345, DV_EXTENT) is None + assert _get_injection_site_um(12345) is None @patch( @@ -77,7 +61,7 @@ def test_get_injection_site_empty_response(mock_get): ) def test_get_injection_site_network_error(mock_get): mock_get.side_effect = Exception("timeout") - assert _get_injection_site_um(12345, DV_EXTENT) is None + assert _get_injection_site_um(12345) is None @patch( @@ -86,7 +70,7 @@ def test_get_injection_site_network_error(mock_get): def test_skeleton_to_dataframe_with_injection(mock_inj): mock_inj.return_value = {"x": 10.0, "y": 20.0, "z": 30.0} skeleton = _make_fake_skeleton() - df = _skeleton_to_dataframe(skeleton, 99, DV_EXTENT) + df = _skeleton_to_dataframe(skeleton, 99) assert isinstance(df, pd.DataFrame) assert "lines" in df.columns assert "injection_sites" in df.columns @@ -94,9 +78,9 @@ def test_skeleton_to_dataframe_with_injection(mock_inj): assert len(lines) == 2 pt = lines[0][0] assert set(pt.keys()) == {"x", "y", "z"} - assert pt["x"] == pytest.approx(1.0) - assert pt["y"] == pytest.approx(DV_EXTENT - 2.0) - assert pt["z"] == pytest.approx(3.0) + assert pt["x"] == pytest.approx(1.0) # 1000 / 1000 + assert pt["y"] == pytest.approx(2.0) # 2000 / 1000 + assert pt["z"] == pytest.approx(3.0) # 3000 / 1000 assert df["injection_sites"].iloc[0] == [{"x": 10.0, "y": 20.0, "z": 30.0}] @@ -106,15 +90,14 @@ def test_skeleton_to_dataframe_with_injection(mock_inj): def test_skeleton_to_dataframe_fallback_centroid(mock_inj): mock_inj.return_value = None skeleton = _make_fake_skeleton() - df = _skeleton_to_dataframe(skeleton, 99, DV_EXTENT) + df = _skeleton_to_dataframe(skeleton, 99) injection = df["injection_sites"].iloc[0][0] assert set(injection.keys()) == {"x", "y", "z"} assert injection["x"] == pytest.approx(1.0) + assert injection["y"] == pytest.approx(2.0) + assert injection["z"] == pytest.approx(3.0) -@patch( - "brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um" -) @patch( "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True, @@ -122,8 +105,7 @@ def test_skeleton_to_dataframe_fallback_centroid(mock_inj): @patch( "brainrender.atlas_specific.allen_brain_atlas.streamlines._skeleton_to_dataframe" ) -def test_get_streamlines_data_downloads(mock_s2df, mock_dv): - mock_dv.return_value = DV_EXTENT +def test_get_streamlines_data_downloads(mock_s2df): fake_df = pd.DataFrame({"lines": [[]], "injection_sites": [[]]}) mock_s2df.return_value = fake_df mock_cv_module = MagicMock() @@ -148,15 +130,11 @@ def test_get_streamlines_data_downloads(mock_s2df, mock_dv): assert all(isinstance(r, pd.DataFrame) for r in result) -@patch( - "brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um" -) @patch( "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True, ) -def test_get_streamlines_data_uses_cache(mock_dv): - mock_dv.return_value = DV_EXTENT +def test_get_streamlines_data_uses_cache(): fake_df = pd.DataFrame( {"lines": [[[]]], "injection_sites": [[{"x": 1, "y": 2, "z": 3}]]} ) @@ -185,15 +163,11 @@ def test_get_streamlines_data_no_cloudvolume(): assert get_streamlines_data([111]) == [] -@patch( - "brainrender.atlas_specific.allen_brain_atlas.streamlines._get_dv_extent_um" -) @patch( "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True, ) -def test_get_streamlines_data_skips_failed_experiment(mock_dv): - mock_dv.return_value = DV_EXTENT +def test_get_streamlines_data_skips_failed_experiment(): mock_cv_module = MagicMock() mock_cv_instance = MagicMock() mock_cv_instance.skeleton.get.side_effect = Exception("not found") From 2217400df9558527a14dd12c0d8adc805a09e819 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:06:06 +0000 Subject: [PATCH 08/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- brainrender/atlas_specific/allen_brain_atlas/streamlines.py | 1 - tests/test_streamlines.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py index f0c26d7a..218635f6 100644 --- a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py +++ b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py @@ -35,7 +35,6 @@ VOXEL_SIZE_NM = 1000 # skeleton vertices are in nanometers - def experiments_source_search(SOI): """ Returns data about experiments whose injection was in the SOI, structure of interest diff --git a/tests/test_streamlines.py b/tests/test_streamlines.py index 72e2f01c..ad5d5a79 100644 --- a/tests/test_streamlines.py +++ b/tests/test_streamlines.py @@ -78,9 +78,9 @@ def test_skeleton_to_dataframe_with_injection(mock_inj): assert len(lines) == 2 pt = lines[0][0] assert set(pt.keys()) == {"x", "y", "z"} - assert pt["x"] == pytest.approx(1.0) # 1000 / 1000 - assert pt["y"] == pytest.approx(2.0) # 2000 / 1000 - assert pt["z"] == pytest.approx(3.0) # 3000 / 1000 + assert pt["x"] == pytest.approx(1.0) # 1000 / 1000 + assert pt["y"] == pytest.approx(2.0) # 2000 / 1000 + assert pt["z"] == pytest.approx(3.0) # 3000 / 1000 assert df["injection_sites"].iloc[0] == [{"x": 10.0, "y": 20.0, "z": 30.0}] From c2814b64d356ea2aa87359f2880032bfad759f89 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Wed, 8 Apr 2026 23:30:04 +0530 Subject: [PATCH 09/16] fix: flip Z (ML) axis for correct hemisphere alignment --- .../allen_brain_atlas/streamlines.py | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py index 218635f6..be919646 100644 --- a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py +++ b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py @@ -1,5 +1,5 @@ import pandas as pd -import requests as http_requests +import requests from loguru import logger from myterial import orange from rich import print @@ -22,6 +22,8 @@ except ModuleNotFoundError: # pragma: no cover cloudvolume_installed = False # pragma: no cover +from brainglobe_atlasapi import BrainGlobeAtlas + from brainrender import base_dir from brainrender._utils import listify @@ -34,6 +36,24 @@ ALLEN_API_URL = "https://api.brain-map.org/api/v2/data/query.json" VOXEL_SIZE_NM = 1000 # skeleton vertices are in nanometers +_ml_extent_um_cache = None + + +def _get_ml_extent_um(): + """ + Derives the full medial-lateral extent of the Allen CCF atlas in microns + dynamically from the brainglobe atlas API. Used to flip the Z (ML) axis + when converting from Allen CCF space to brainrender's coordinate system, + where left and right hemispheres are mirrored relative to the Allen CCF. + + Result is cached after the first call to avoid reinstantiating the atlas + on every experiment download. + """ + global _ml_extent_um_cache + if _ml_extent_um_cache is None: + atlas = BrainGlobeAtlas("allen_mouse_25um", check_latest=False) + _ml_extent_um_cache = float(atlas.shape[2] * atlas.resolution[2]) + return _ml_extent_um_cache def experiments_source_search(SOI): """ @@ -60,13 +80,14 @@ def experiments_source_search(SOI): ) -def _get_injection_site_um(eid): +def _get_injection_site_um(eid, ml_extent_um): """ Fetches the injection site coordinates for an experiment from the Allen - Brain Atlas API. Coordinates are in Allen CCF um space (PIR), matching - brainrender's brain mesh coordinate system. + Brain Atlas API. Coordinates are in Allen CCF um space with the Z (ML) + axis flipped to match brainrender's hemisphere convention. :param eid: int, experiment ID + :param ml_extent_um: float, full ML extent of the atlas in um for LR flip :return: dict with x, y, z keys or None if not found """ try: @@ -76,14 +97,14 @@ def _get_injection_site_um(eid): f"rma::criteria,[is_injection$eqtrue]," f"rma::options[num_rows$eq1][order$eq'projection_volume desc']" ) - response = http_requests.get(url, timeout=10) + response = requests.get(url, timeout=10) data = response.json() if data["success"] and data["num_rows"] > 0: voxel = data["msg"][0] return { "x": float(voxel["max_voxel_x"]), "y": float(voxel["max_voxel_y"]), - "z": float(voxel["max_voxel_z"]), + "z": float(ml_extent_um - voxel["max_voxel_z"]), } except Exception as e: logger.warning( @@ -92,17 +113,21 @@ def _get_injection_site_um(eid): return None -def _skeleton_to_dataframe(skeleton, eid): +def _skeleton_to_dataframe(skeleton, eid, ml_extent_um): """ Converts a cloudvolume Skeleton object to the pd.DataFrame format expected by brainrender's Streamlines actor. - Vertices are in nanometers in Allen CCF space. We convert nm -> um - (divide by VOXEL_SIZE_NM). No axis flips are needed because - brainrender's brain mesh uses the same PIR coordinate system. + Vertices are in nanometers in Allen CCF space. We: + 1. Convert nm -> um (divide by VOXEL_SIZE_NM) + 2. Flip Z (ML) axis to match brainrender's hemisphere convention + + X (AP) and Y (DV) are passed through as-is because brainrender's + brain mesh uses the same orientation as the Allen CCF for those axes. :param skeleton: cloudvolume Skeleton object :param eid: int, experiment ID used to fetch real injection coordinates + :param ml_extent_um: float, full ML extent of the atlas in um for LR flip :return: pd.DataFrame with 'lines' and 'injection_sites' columns """ components = skeleton.components() @@ -111,12 +136,16 @@ def _skeleton_to_dataframe(skeleton, eid): for component in components: verts_um = component.vertices / VOXEL_SIZE_NM points = [ - {"x": float(v[0]), "y": float(v[1]), "z": float(v[2])} + { + "x": float(v[0]), + "y": float(v[1]), + "z": float(ml_extent_um - v[2]), + } for v in verts_um ] lines.append(points) - injection_site = _get_injection_site_um(eid) + injection_site = _get_injection_site_um(eid, ml_extent_um) if injection_site is None: logger.warning( f"Falling back to centroid for injection site of experiment {eid}" @@ -126,7 +155,7 @@ def _skeleton_to_dataframe(skeleton, eid): injection_site = { "x": float(centroid[0]), "y": float(centroid[1]), - "z": float(centroid[2]), + "z": float(ml_extent_um - centroid[2]), } return pd.DataFrame( @@ -150,6 +179,8 @@ def get_streamlines_data(eids, force_download=False): ) return [] + ml_extent_um = _get_ml_extent_um() + cv = cloudvolume.CloudVolume( ALLEN_MESOSCALE_URL, use_https=True, @@ -169,7 +200,7 @@ def get_streamlines_data(eids, force_download=False): ) continue - df = _skeleton_to_dataframe(skeleton, int(eid)) + df = _skeleton_to_dataframe(skeleton, int(eid), ml_extent_um) df.to_json(str(jsonpath)) data.append(df) else: From 91ac22fac13aac040b49e5f24bfc2a4587f975ec Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Wed, 8 Apr 2026 23:30:04 +0530 Subject: [PATCH 10/16] test: update tests for ML flip, add GCS download smoke test --- tests/test_streamlines.py | 94 +++++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 18 deletions(-) diff --git a/tests/test_streamlines.py b/tests/test_streamlines.py index ad5d5a79..c5ef5d58 100644 --- a/tests/test_streamlines.py +++ b/tests/test_streamlines.py @@ -9,10 +9,22 @@ from brainrender.atlas_specific import get_streamlines_for_region from brainrender.atlas_specific.allen_brain_atlas.streamlines import ( _get_injection_site_um, + _get_ml_extent_um, _skeleton_to_dataframe, get_streamlines_data, ) +ML_EXTENT = 11400.0 + + +@pytest.fixture(autouse=True) +def _reset_ml_cache(): + import brainrender.atlas_specific.allen_brain_atlas.streamlines as _mod + + _mod._ml_extent_um_cache = None + yield + _mod._ml_extent_um_cache = None + def _make_fake_skeleton(): verts = np.array([[1000.0, 2000.0, 3000.0]] * 4, dtype=float) @@ -27,7 +39,19 @@ def _make_fake_skeleton(): @patch( - "brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get" + "brainrender.atlas_specific.allen_brain_atlas.streamlines.BrainGlobeAtlas" +) +def test_get_ml_extent_um(mock_atlas_cls): + mock_atlas = MagicMock() + mock_atlas.shape = (528, 320, 456) + mock_atlas.resolution = (25, 25, 25) + mock_atlas_cls.return_value = mock_atlas + result = _get_ml_extent_um() + assert result == pytest.approx(456 * 25) + + +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines.requests.get" ) def test_get_injection_site_success(mock_get): mock_resp = MagicMock() @@ -35,33 +59,37 @@ def test_get_injection_site_success(mock_get): "success": True, "num_rows": 1, "msg": [ - {"max_voxel_x": 100.0, "max_voxel_y": 200.0, "max_voxel_z": 300.0} + { + "max_voxel_x": 100.0, + "max_voxel_y": 200.0, + "max_voxel_z": 300.0, + } ], } mock_get.return_value = mock_resp - result = _get_injection_site_um(12345) + result = _get_injection_site_um(12345, ML_EXTENT) assert result is not None assert result["x"] == pytest.approx(100.0) assert result["y"] == pytest.approx(200.0) - assert result["z"] == pytest.approx(300.0) + assert result["z"] == pytest.approx(ML_EXTENT - 300.0) @patch( - "brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get" + "brainrender.atlas_specific.allen_brain_atlas.streamlines.requests.get" ) def test_get_injection_site_empty_response(mock_get): mock_resp = MagicMock() mock_resp.json.return_value = {"success": True, "num_rows": 0, "msg": []} mock_get.return_value = mock_resp - assert _get_injection_site_um(12345) is None + assert _get_injection_site_um(12345, ML_EXTENT) is None @patch( - "brainrender.atlas_specific.allen_brain_atlas.streamlines.http_requests.get" + "brainrender.atlas_specific.allen_brain_atlas.streamlines.requests.get" ) def test_get_injection_site_network_error(mock_get): mock_get.side_effect = Exception("timeout") - assert _get_injection_site_um(12345) is None + assert _get_injection_site_um(12345, ML_EXTENT) is None @patch( @@ -70,7 +98,7 @@ def test_get_injection_site_network_error(mock_get): def test_skeleton_to_dataframe_with_injection(mock_inj): mock_inj.return_value = {"x": 10.0, "y": 20.0, "z": 30.0} skeleton = _make_fake_skeleton() - df = _skeleton_to_dataframe(skeleton, 99) + df = _skeleton_to_dataframe(skeleton, 99, ML_EXTENT) assert isinstance(df, pd.DataFrame) assert "lines" in df.columns assert "injection_sites" in df.columns @@ -78,10 +106,12 @@ def test_skeleton_to_dataframe_with_injection(mock_inj): assert len(lines) == 2 pt = lines[0][0] assert set(pt.keys()) == {"x", "y", "z"} - assert pt["x"] == pytest.approx(1.0) # 1000 / 1000 - assert pt["y"] == pytest.approx(2.0) # 2000 / 1000 - assert pt["z"] == pytest.approx(3.0) # 3000 / 1000 - assert df["injection_sites"].iloc[0] == [{"x": 10.0, "y": 20.0, "z": 30.0}] + assert pt["x"] == pytest.approx(1.0) + assert pt["y"] == pytest.approx(2.0) + assert pt["z"] == pytest.approx(ML_EXTENT - 3.0) + assert df["injection_sites"].iloc[0] == [ + {"x": 10.0, "y": 20.0, "z": 30.0} + ] @patch( @@ -90,14 +120,17 @@ def test_skeleton_to_dataframe_with_injection(mock_inj): def test_skeleton_to_dataframe_fallback_centroid(mock_inj): mock_inj.return_value = None skeleton = _make_fake_skeleton() - df = _skeleton_to_dataframe(skeleton, 99) + df = _skeleton_to_dataframe(skeleton, 99, ML_EXTENT) injection = df["injection_sites"].iloc[0][0] assert set(injection.keys()) == {"x", "y", "z"} assert injection["x"] == pytest.approx(1.0) assert injection["y"] == pytest.approx(2.0) - assert injection["z"] == pytest.approx(3.0) + assert injection["z"] == pytest.approx(ML_EXTENT - 3.0) +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines._get_ml_extent_um" +) @patch( "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True, @@ -105,7 +138,8 @@ def test_skeleton_to_dataframe_fallback_centroid(mock_inj): @patch( "brainrender.atlas_specific.allen_brain_atlas.streamlines._skeleton_to_dataframe" ) -def test_get_streamlines_data_downloads(mock_s2df): +def test_get_streamlines_data_downloads(mock_s2df, mock_ml): + mock_ml.return_value = ML_EXTENT fake_df = pd.DataFrame({"lines": [[]], "injection_sites": [[]]}) mock_s2df.return_value = fake_df mock_cv_module = MagicMock() @@ -130,11 +164,15 @@ def test_get_streamlines_data_downloads(mock_s2df): assert all(isinstance(r, pd.DataFrame) for r in result) +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines._get_ml_extent_um" +) @patch( "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True, ) -def test_get_streamlines_data_uses_cache(): +def test_get_streamlines_data_uses_cache(mock_ml): + mock_ml.return_value = ML_EXTENT fake_df = pd.DataFrame( {"lines": [[[]]], "injection_sites": [[{"x": 1, "y": 2, "z": 3}]]} ) @@ -163,11 +201,15 @@ def test_get_streamlines_data_no_cloudvolume(): assert get_streamlines_data([111]) == [] +@patch( + "brainrender.atlas_specific.allen_brain_atlas.streamlines._get_ml_extent_um" +) @patch( "brainrender.atlas_specific.allen_brain_atlas.streamlines.cloudvolume_installed", True, ) -def test_get_streamlines_data_skips_failed_experiment(): +def test_get_streamlines_data_skips_failed_experiment(mock_ml): + mock_ml.return_value = ML_EXTENT mock_cv_module = MagicMock() mock_cv_instance = MagicMock() mock_cv_instance.skeleton.get.side_effect = Exception("not found") @@ -206,3 +248,19 @@ def test_get_streamlines_for_region_calls_download(mock_search, mock_dl): result = get_streamlines_for_region("TH") assert result == ["df1", "df2"] mock_dl.assert_called_once() + + +@pytest.mark.parametrize( + "eid", + [479983421], +) +def test_download_streamlines_from_gcs(eid): + """Smoke test: download one small experiment to verify the GCS source is live.""" + data = get_streamlines_data([eid], force_download=True) + assert len(data) == 1 + df = data[0] + assert "lines" in df.columns + assert "injection_sites" in df.columns + lines = df["lines"].iloc[0] + assert len(lines) > 0 + assert set(lines[0][0].keys()) == {"x", "y", "z"} From baa5ade6838b168d96b10cfebded168dc2ab88a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:01:38 +0000 Subject: [PATCH 11/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../allen_brain_atlas/streamlines.py | 1 + tests/test_streamlines.py | 16 ++++------------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py index be919646..30c4d690 100644 --- a/brainrender/atlas_specific/allen_brain_atlas/streamlines.py +++ b/brainrender/atlas_specific/allen_brain_atlas/streamlines.py @@ -55,6 +55,7 @@ def _get_ml_extent_um(): _ml_extent_um_cache = float(atlas.shape[2] * atlas.resolution[2]) return _ml_extent_um_cache + def experiments_source_search(SOI): """ Returns data about experiments whose injection was in the SOI, structure of interest diff --git a/tests/test_streamlines.py b/tests/test_streamlines.py index c5ef5d58..2bf8d82d 100644 --- a/tests/test_streamlines.py +++ b/tests/test_streamlines.py @@ -50,9 +50,7 @@ def test_get_ml_extent_um(mock_atlas_cls): assert result == pytest.approx(456 * 25) -@patch( - "brainrender.atlas_specific.allen_brain_atlas.streamlines.requests.get" -) +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.requests.get") def test_get_injection_site_success(mock_get): mock_resp = MagicMock() mock_resp.json.return_value = { @@ -74,9 +72,7 @@ def test_get_injection_site_success(mock_get): assert result["z"] == pytest.approx(ML_EXTENT - 300.0) -@patch( - "brainrender.atlas_specific.allen_brain_atlas.streamlines.requests.get" -) +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.requests.get") def test_get_injection_site_empty_response(mock_get): mock_resp = MagicMock() mock_resp.json.return_value = {"success": True, "num_rows": 0, "msg": []} @@ -84,9 +80,7 @@ def test_get_injection_site_empty_response(mock_get): assert _get_injection_site_um(12345, ML_EXTENT) is None -@patch( - "brainrender.atlas_specific.allen_brain_atlas.streamlines.requests.get" -) +@patch("brainrender.atlas_specific.allen_brain_atlas.streamlines.requests.get") def test_get_injection_site_network_error(mock_get): mock_get.side_effect = Exception("timeout") assert _get_injection_site_um(12345, ML_EXTENT) is None @@ -109,9 +103,7 @@ def test_skeleton_to_dataframe_with_injection(mock_inj): assert pt["x"] == pytest.approx(1.0) assert pt["y"] == pytest.approx(2.0) assert pt["z"] == pytest.approx(ML_EXTENT - 3.0) - assert df["injection_sites"].iloc[0] == [ - {"x": 10.0, "y": 20.0, "z": 30.0} - ] + assert df["injection_sites"].iloc[0] == [{"x": 10.0, "y": 20.0, "z": 30.0}] @patch( From b68010ea765172309914858e679f5e285fb974e1 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Wed, 8 Apr 2026 23:35:40 +0530 Subject: [PATCH 12/16] deps: make cloud-volume optional instead of required --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6c520e4b..2a22595f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ dynamic = ["version"] dependencies = [ "brainglobe-atlasapi>=2.0.1", - "cloud-volume>=3.11.0", "brainglobe-space>=1.0.0", "brainglobe-utils>=0.5.0", "h5py", From c1c1938e7e32cc4e5ab10e95adba5a61e4b71b80 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Wed, 8 Apr 2026 23:47:01 +0530 Subject: [PATCH 13/16] test: skip GCS smoke test when cloud-volume not installed --- tests/test_streamlines.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_streamlines.py b/tests/test_streamlines.py index 2bf8d82d..6d94aaf9 100644 --- a/tests/test_streamlines.py +++ b/tests/test_streamlines.py @@ -248,6 +248,7 @@ def test_get_streamlines_for_region_calls_download(mock_search, mock_dl): ) def test_download_streamlines_from_gcs(eid): """Smoke test: download one small experiment to verify the GCS source is live.""" + pytest.importorskip("cloudvolume") data = get_streamlines_data([eid], force_download=True) assert len(data) == 1 df = data[0] From df43676580d7b8461f05e68182bfc0b7edcd4be7 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Mon, 13 Apr 2026 23:50:07 +0530 Subject: [PATCH 14/16] test: add hemisphere orientation check for right-hemisphere experiment --- tests/test_streamlines.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_streamlines.py b/tests/test_streamlines.py index 6d94aaf9..60c0293d 100644 --- a/tests/test_streamlines.py +++ b/tests/test_streamlines.py @@ -257,3 +257,31 @@ def test_download_streamlines_from_gcs(eid): lines = df["lines"].iloc[0] assert len(lines) > 0 assert set(lines[0][0].keys()) == {"x", "y", "z"} + + +def test_streamlines_hemisphere_orientation(): + """Verify that a right-hemisphere experiment renders in the correct hemisphere. + + Experiment 298004028 has injection at ML=2.15mm (right hemisphere) with + near-zero left hemisphere projection volume per the Allen connectivity viewer. + After the Z (ML) axis flip, all streamline Z coordinates should be below + the atlas midline (~5700um), matching brainrender's right hemisphere convention. + """ + pytest.importorskip("cloudvolume") + data = get_streamlines_data([298004028], force_download=True) + assert len(data) == 1 + lines = data[0]["lines"].iloc[0] + assert len(lines) > 0 + + # Collect all Z coordinates from all streamline components + all_z = [pt["z"] for component in lines for pt in component] + + # Allen CCF ML extent is 11400um, midline is ~5700um + # Right hemisphere in brainrender = Z < midline after flip + midline = 5700.0 + right_side = [z for z in all_z if z < midline] + assert len(right_side) / len(all_z) > 0.95, ( + f"Expected >95% of streamline points in right hemisphere (Z < {midline}), " + f"got {len(right_side)}/{len(all_z)} ({100*len(right_side)/len(all_z):.1f}%)" + ) + From 10523fec0741b41aa763715cccb1bf2930e381a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:20:16 +0000 Subject: [PATCH 15/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_streamlines.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_streamlines.py b/tests/test_streamlines.py index 60c0293d..1eeb3738 100644 --- a/tests/test_streamlines.py +++ b/tests/test_streamlines.py @@ -284,4 +284,3 @@ def test_streamlines_hemisphere_orientation(): f"Expected >95% of streamline points in right hemisphere (Z < {midline}), " f"got {len(right_side)}/{len(all_z)} ({100*len(right_side)/len(all_z):.1f}%)" ) - From e0f7716cef228829e425940c0dc7377759fe32e9 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Tue, 14 Apr 2026 23:13:49 +0530 Subject: [PATCH 16/16] deps: add cloud-volume to dependencies, remove importorskip from tests --- pyproject.toml | 1 + tests/test_streamlines.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2a22595f..6c520e4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dynamic = ["version"] dependencies = [ "brainglobe-atlasapi>=2.0.1", + "cloud-volume>=3.11.0", "brainglobe-space>=1.0.0", "brainglobe-utils>=0.5.0", "h5py", diff --git a/tests/test_streamlines.py b/tests/test_streamlines.py index 1eeb3738..74bcef5b 100644 --- a/tests/test_streamlines.py +++ b/tests/test_streamlines.py @@ -248,7 +248,6 @@ def test_get_streamlines_for_region_calls_download(mock_search, mock_dl): ) def test_download_streamlines_from_gcs(eid): """Smoke test: download one small experiment to verify the GCS source is live.""" - pytest.importorskip("cloudvolume") data = get_streamlines_data([eid], force_download=True) assert len(data) == 1 df = data[0] @@ -267,7 +266,6 @@ def test_streamlines_hemisphere_orientation(): After the Z (ML) axis flip, all streamline Z coordinates should be below the atlas midline (~5700um), matching brainrender's right hemisphere convention. """ - pytest.importorskip("cloudvolume") data = get_streamlines_data([298004028], force_download=True) assert len(data) == 1 lines = data[0]["lines"].iloc[0]