yabplot 0.3.1__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yabplot
3
- Version: 0.3.1
3
+ Version: 0.5.0
4
4
  Summary: yet another brain plot
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
8
  Requires-Dist: cmocean>=4.0.3
9
9
  Requires-Dist: ipywidgets>=8.1.8
10
+ Requires-Dist: matplotlib>=3.10.7
10
11
  Requires-Dist: nibabel>=5.3.2
11
12
  Requires-Dist: pandas>=2.3.3
12
13
  Requires-Dist: pooch>=1.8.2
@@ -31,17 +32,18 @@ Dynamic: license-file
31
32
  [![Tests](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml/badge.svg)](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml)
32
33
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.18237144.svg)](https://doi.org/10.5281/zenodo.18237144)
33
34
 
34
- **yabplot** is a Python library for creating beautiful, publication-quality 3D brain visualizations. it supports plotting cortical regions (+ vertexwise), subcortical structures, and white matter bundles.
35
+ **yabplot** is a Python library for creating publication-quality 3D brain visualizations. it provides a unified interface for cortical regions, subcortical structures, white matter bundles, connectomes, voxel-wise maps, and vertex-wise maps.
35
36
 
36
- the idea is simple. while there are already amazing visualization tools available, they often focus on specific domains—using one tool for white matter tracts and another for cortical surfaces inevitably leads to inconsistent styles. i wanted a unified, simple-to-use tool that enables me (and hopefully others) to perform most brain visualizations in a single place. recognizing that neuroscience evolves daily, i designed **yabplot** to be modular: it supports standard pre-packaged atlases out of the box, but easily accepts any custom parcellation or tractography dataset you might need.
37
+ the idea is simple. while there are already amazing visualization tools available, they often focus on specific domains—using one tool for white matter tracts and another for cortical surfaces inevitably leads to inconsistent styles. i wanted a unified, simple-to-use tool that enables me (and hopefully others) to perform most brain visualizations in a single place. recognizing that neuroscience evolves daily, i designed **yabplot** to be modular: it supports standard pre-packaged atlases out of the box, but easily accepts any custom parcellation or tractography dataset you might need. moreover, it enables to plot volumetric data either voxel-wise or by projecting the data to cortical surface or white matter tracts.
37
38
 
38
39
  ## features
39
40
 
40
- * **pre-existing atlases:** access many commonly used atlases (schaefer2018, brainnetome, aparc, aseg, musus100, xtract, etc) on demand.
41
- * [new!] **vertexwise plotting:** project volume (.nii) to cortical surface and plot.
42
- * **simple to use:** plug-n-play functions for cortex, subcortex, and tracts with a unified API.
43
- * **custom atlases:** easily use your own parcellations, segmentations (.nii/.gii), or tractograms (.trk).
44
- * **flexible inputs:** accepts data as dictionaries (for partial mapping) or arrays (for strict mapping).
41
+ * **unified plotting API:** plot cortical regions, vertex-wise maps, subcortical structures, voxel-wise maps, white-matter tracts, and connectomes with a consistent interface.
42
+ * **pre-packaged resources:** access commonly used atlases and meshes on demand, including schaefer, brainnetome, aparc, aseg, musus100, and xtract atlases.
43
+ * **flexible data mapping:** pass data as arrays for strict ordering or dictionaries for partial/name-based mapping.
44
+ * **custom atlases:** build and use custom cortical parcellations, subcortical segmentations, and tractography datasets.
45
+ * **volume projection:** project nifti images to cortical surfaces or tractograms for vertex-wise and tractometry visualizations, or plot the volumes voxel-wise.
46
+ * **publication-oriented output:** generate static figures, saved images, and interactive 3D views.
45
47
 
46
48
  ## installation
47
49
 
@@ -55,7 +57,7 @@ pip install yabplot # to install
55
57
  pip install yabplot --upgrade # to update
56
58
  ```
57
59
 
58
- dependencies: python 3.11 with ipywidgets, nibabel, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
60
+ dependencies: python 3.11 with ipywidgets, nibabel, matplotlib, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
59
61
 
60
62
  (Connectome Workbench (`wb_command`) is a requirement to create custom cortical atlases unless you plan to only use pre-loaded atlases; see more in docs)
61
63
 
@@ -77,36 +79,53 @@ print(yab.get_available_resources())
77
79
  # see the region names for a specific atlas
78
80
  print(yab.get_atlas_regions(atlas='aseg', category='subcortical'))
79
81
 
80
- # cortical surfaces
82
+ # plotting cortical surface regions
81
83
  atlas = 'aparc'
82
- dmap1 = {'L_lateraloccipital': 0.265, 'L_postcentral': 0.086, ...}
83
- dmap2 = {'L_fusiform': 0.218, 'L_supramarginal': 0.119, ...}
84
- yab.plot_cortical(data=dmap1, atlas=atlas, vminmax=[-0.1, 0.3],
85
- bmesh_type='midthickness', views=['left_lateral', 'left_medial'],
86
- figsize=(600, 300), cmap='viridis', proc_vertices='sharp')
87
- yab.plot_cortical(data=dmap2, atlas=atlas, vminmax=[-0.1, 0.3],
88
- bmesh_type='swm', views=['left_lateral', 'left_medial'],
89
- figsize=(1200, 600), cmap='viridis', proc_vertices='sharp')
90
-
91
- # subcortical structures
84
+ dmap1 = {'L_lateraloccipital': 0.265, 'L_postcentral': 0.086}
85
+ ax = yab.plot_cortical(data=dmap1, atlas=atlas, vminmax=[0.0, 0.3], cmap='viridis',
86
+ bmesh='midthickness', views=['left_lateral', 'left_medial'])
87
+
88
+ # plotting subcortical regions
92
89
  atlas = 'aseg'
93
90
  regs = yab.get_atlas_regions(atlas=atlas, category='subcortical')
94
91
  data = np.arange(1, len(regs)+1)
95
- yab.plot_subcortical(data=data, atlas=atlas, vminmax=[2, 14],
96
- views=['left_lateral', 'superior', 'right_lateral'],
97
- bmesh_alpha=0.1, figsize=(600, 300), cmap='viridis')
92
+ ax = yab.plot_subcortical(data=data, atlas=atlas, vminmax=[2, 14],
93
+ views=['left_lateral', 'superior', 'right_lateral'],
94
+ bmesh_alpha=0.1, cmap='plasma')
98
95
 
99
- # white matter bundles
100
- atlas = 'xtract_tiny'
96
+ # plotting white matter tracts
97
+ atlas = 'xtract_medium'
101
98
  regs = yab.get_atlas_regions(atlas=atlas, category='tracts')
102
99
  data = {reg: np.sin(i) for i, reg in enumerate(regs)}
103
- yab.plot_tracts(data=data, atlas=atlas, style='matte',
104
- views=['left_lateral', 'anterior', 'superior'], bmesh_type='pial',
105
- bmesh_alpha=0.1, figsize=(1600, 800), cmap='viridis')
100
+ ax = yab.plot_tracts(data=data, atlas=atlas, style='matte', cmap='coolwarm',
101
+ views=['left_lateral', 'anterior', 'superior'], bmesh='pial')
102
+
103
+ # plotting connectome
104
+ data = np.random.rand(400, 400)
105
+ ax = yab.plot_connectome(matrix=data, atlas='schaefer400',
106
+ edge_cmap='dense', node_cmap='binary', edge_threshold='95%',
107
+ views=['left_lateral', 'superior', 'posterior']
108
+ )
109
+
110
+ # volume projection to cortical surface and vertexwise plotting
111
+ threshold = 4
112
+ b_lh_path, b_rh_path = yab.data.get_surface_paths('midthickness', 'bmesh')
113
+ lh_data, rh_data = yab.project_vol2surf('path/to/yourdata.nii.gz', bmesh='midthickness')
114
+ lh_data = np.where(lh_data > threshold, lh_data, np.nan)
115
+ rh_data = np.where(rh_data > threshold, rh_data, np.nan)
116
+ lh_mesh, rh_mesh = yab.load_vertexwise_mesh(b_lh_path, b_rh_path, lh_data, rh_data)
117
+ ax = yab.plot_vertexwise(lh_mesh, rh_mesh, cmap='viridis', vminmax=[-10, 10],
118
+ views=['left_lateral', 'left_medial'])
119
+
120
+ # plotting volume voxel-wise
121
+ threshold = '99.5%'
122
+ nii_path = 'path/to/yourdata.nii.gz'
123
+ ax = yab.plot_voxelwise(nii_path, threshold='99%', cmap='Reds',
124
+ views=['left_lateral', 'superior', 'anterior'])
106
125
 
107
126
  ```
108
127
 
109
- ![examples](docs/assets/examples.png)
128
+ ![examples](docs/assets/overview.png)
110
129
 
111
130
  ## acknowledgements
112
131
 
@@ -0,0 +1,107 @@
1
+ # yabplot: yet another brain plot
2
+
3
+ ![logo](docs/assets/yabplot_logo.png)
4
+
5
+ [![PyPI version](https://img.shields.io/pypi/v/yabplot.svg)](https://pypi.org/project/yabplot/)
6
+ [![Docs](https://github.com/teanijarv/yabplot/actions/workflows/docs.yml/badge.svg)](https://teanijarv.github.io/yabplot/)
7
+ [![Tests](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml/badge.svg)](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml)
8
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.18237144.svg)](https://doi.org/10.5281/zenodo.18237144)
9
+
10
+ **yabplot** is a Python library for creating publication-quality 3D brain visualizations. it provides a unified interface for cortical regions, subcortical structures, white matter bundles, connectomes, voxel-wise maps, and vertex-wise maps.
11
+
12
+ the idea is simple. while there are already amazing visualization tools available, they often focus on specific domains—using one tool for white matter tracts and another for cortical surfaces inevitably leads to inconsistent styles. i wanted a unified, simple-to-use tool that enables me (and hopefully others) to perform most brain visualizations in a single place. recognizing that neuroscience evolves daily, i designed **yabplot** to be modular: it supports standard pre-packaged atlases out of the box, but easily accepts any custom parcellation or tractography dataset you might need. moreover, it enables to plot volumetric data either voxel-wise or by projecting the data to cortical surface or white matter tracts.
13
+
14
+ ## features
15
+
16
+ * **unified plotting API:** plot cortical regions, vertex-wise maps, subcortical structures, voxel-wise maps, white-matter tracts, and connectomes with a consistent interface.
17
+ * **pre-packaged resources:** access commonly used atlases and meshes on demand, including schaefer, brainnetome, aparc, aseg, musus100, and xtract atlases.
18
+ * **flexible data mapping:** pass data as arrays for strict ordering or dictionaries for partial/name-based mapping.
19
+ * **custom atlases:** build and use custom cortical parcellations, subcortical segmentations, and tractography datasets.
20
+ * **volume projection:** project nifti images to cortical surfaces or tractograms for vertex-wise and tractometry visualizations, or plot the volumes voxel-wise.
21
+ * **publication-oriented output:** generate static figures, saved images, and interactive 3D views.
22
+
23
+ ## installation
24
+
25
+ ```bash
26
+ uv add yabplot # to install
27
+ uv sync --upgrade-package yabplot # to update
28
+ ```
29
+ or
30
+ ```bash
31
+ pip install yabplot # to install
32
+ pip install yabplot --upgrade # to update
33
+ ```
34
+
35
+ dependencies: python 3.11 with ipywidgets, nibabel, matplotlib, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
36
+
37
+ (Connectome Workbench (`wb_command`) is a requirement to create custom cortical atlases unless you plan to only use pre-loaded atlases; see more in docs)
38
+
39
+
40
+ ## quick start
41
+
42
+ please refer to the [documentation](https://teanijarv.github.io/yabplot/) for more comprehensive guides.
43
+
44
+ ```python
45
+ import yabplot as yab
46
+ import numpy as np
47
+
48
+ # check that you have the latest version
49
+ print(yab.__version__)
50
+
51
+ # see available atlases and brain meshes
52
+ print(yab.get_available_resources())
53
+
54
+ # see the region names for a specific atlas
55
+ print(yab.get_atlas_regions(atlas='aseg', category='subcortical'))
56
+
57
+ # plotting cortical surface regions
58
+ atlas = 'aparc'
59
+ dmap1 = {'L_lateraloccipital': 0.265, 'L_postcentral': 0.086}
60
+ ax = yab.plot_cortical(data=dmap1, atlas=atlas, vminmax=[0.0, 0.3], cmap='viridis',
61
+ bmesh='midthickness', views=['left_lateral', 'left_medial'])
62
+
63
+ # plotting subcortical regions
64
+ atlas = 'aseg'
65
+ regs = yab.get_atlas_regions(atlas=atlas, category='subcortical')
66
+ data = np.arange(1, len(regs)+1)
67
+ ax = yab.plot_subcortical(data=data, atlas=atlas, vminmax=[2, 14],
68
+ views=['left_lateral', 'superior', 'right_lateral'],
69
+ bmesh_alpha=0.1, cmap='plasma')
70
+
71
+ # plotting white matter tracts
72
+ atlas = 'xtract_medium'
73
+ regs = yab.get_atlas_regions(atlas=atlas, category='tracts')
74
+ data = {reg: np.sin(i) for i, reg in enumerate(regs)}
75
+ ax = yab.plot_tracts(data=data, atlas=atlas, style='matte', cmap='coolwarm',
76
+ views=['left_lateral', 'anterior', 'superior'], bmesh='pial')
77
+
78
+ # plotting connectome
79
+ data = np.random.rand(400, 400)
80
+ ax = yab.plot_connectome(matrix=data, atlas='schaefer400',
81
+ edge_cmap='dense', node_cmap='binary', edge_threshold='95%',
82
+ views=['left_lateral', 'superior', 'posterior']
83
+ )
84
+
85
+ # volume projection to cortical surface and vertexwise plotting
86
+ threshold = 4
87
+ b_lh_path, b_rh_path = yab.data.get_surface_paths('midthickness', 'bmesh')
88
+ lh_data, rh_data = yab.project_vol2surf('path/to/yourdata.nii.gz', bmesh='midthickness')
89
+ lh_data = np.where(lh_data > threshold, lh_data, np.nan)
90
+ rh_data = np.where(rh_data > threshold, rh_data, np.nan)
91
+ lh_mesh, rh_mesh = yab.load_vertexwise_mesh(b_lh_path, b_rh_path, lh_data, rh_data)
92
+ ax = yab.plot_vertexwise(lh_mesh, rh_mesh, cmap='viridis', vminmax=[-10, 10],
93
+ views=['left_lateral', 'left_medial'])
94
+
95
+ # plotting volume voxel-wise
96
+ threshold = '99.5%'
97
+ nii_path = 'path/to/yourdata.nii.gz'
98
+ ax = yab.plot_voxelwise(nii_path, threshold='99%', cmap='Reds',
99
+ views=['left_lateral', 'superior', 'anterior'])
100
+
101
+ ```
102
+
103
+ ![examples](docs/assets/overview.png)
104
+
105
+ ## acknowledgements
106
+
107
+ yabplot relies on the extensive work of the neuroimaging community. if you use these atlases in your work, please cite the original authors. if you use this package for any scientific work, please cite the DOI (see more info on [Zenodo](https://doi.org/10.5281/zenodo.18237144)).
@@ -1,12 +1,13 @@
1
1
  [project]
2
2
  name = "yabplot"
3
- version = "0.3.1"
3
+ version = "0.5.0"
4
4
  description = "yet another brain plot"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
8
  "cmocean>=4.0.3",
9
9
  "ipywidgets>=8.1.8",
10
+ "matplotlib>=3.10.7",
10
11
  "nibabel>=5.3.2",
11
12
  "pandas>=2.3.3",
12
13
  "pooch>=1.8.2",
@@ -10,6 +10,37 @@ def test_version():
10
10
  """Check that the package has a version string."""
11
11
  assert yab.__version__ is not None
12
12
 
13
+ def test_none_returns_empty_dict():
14
+ """
15
+ Unit test: Verify that passing None disables the background mesh
16
+ by correctly returning an empty dictionary.
17
+ """
18
+ result = yab.mesh.load_bmesh(None)
19
+ assert result == {}
20
+
21
+ def test_dict_passthrough():
22
+ """
23
+ Unit test: Verify that custom dictionary keys for hemispheres
24
+ are properly sanitized to strict 'L' and 'R' keys.
25
+ """
26
+ mesh_l = pv.Sphere()
27
+ mesh_r = pv.Cube()
28
+ mesh_other = pv.Cone()
29
+ d = {'left': mesh_l, 'RIGHT': mesh_r, 'other': mesh_other}
30
+ result = yab.mesh.load_bmesh(d)
31
+ expected = {'L': mesh_l, 'R': mesh_r, 'other': mesh_other}
32
+ assert result == expected
33
+
34
+ def test_polydata_wrapped_in_both():
35
+ """
36
+ Unit test: Verify that passing a single PyVista mesh (whole brain)
37
+ safely wraps it in a dictionary with the 'both' key.
38
+ """
39
+ mesh = pv.Sphere()
40
+ result = yab.mesh.load_bmesh(mesh)
41
+ assert 'both' in result
42
+ assert result['both'] is mesh
43
+
13
44
  def test_plotter_instantiation():
14
45
  """
15
46
  Smoke test: Can we create a Plotter without crashing?
@@ -46,4 +77,4 @@ def test_plot_vertexwise():
46
77
  rh = pv.Sphere()
47
78
  lh['Data'] = np.random.rand(lh.n_points)
48
79
  rh['Data'] = np.random.rand(rh.n_points)
49
- yab.plot_vertexwise(lh, rh, display_type=None)
80
+ yab.plot_vertexwise(lh, rh, display_type=None)
@@ -2,7 +2,8 @@ from importlib.metadata import version, PackageNotFoundError
2
2
 
3
3
  from .plotting import (
4
4
  plot_cortical, plot_subcortical, plot_tracts,
5
- clear_tract_cache, plot_vertexwise
5
+ clear_cache, plot_vertexwise, plot_connectome,
6
+ plot_voxelwise
6
7
  )
7
8
  from .data import (
8
9
  get_available_resources, get_atlas_regions
@@ -11,7 +12,10 @@ from .atlas_builder import (
11
12
  build_cortical_atlas, build_subcortical_atlas
12
13
  )
13
14
  from .mesh import (
14
- load_vertexwise_mesh, project_vol2surf
15
+ load_vertexwise_mesh, make_cortical_mesh, load_nii_as_mesh
16
+ )
17
+ from .projection import (
18
+ project_vol2surf, project_vol2tract, project_vol2tract_atlas
15
19
  )
16
20
 
17
21
  try:
@@ -224,10 +224,10 @@ def qc_custom_cortical_atlas(atlas_dir, atlasname='atlas'):
224
224
  plot_file = os.path.join(qc_dir, f"{rid:03d}_{name}.png")
225
225
 
226
226
  try:
227
- plot_cortical(
227
+ ax = plot_cortical(
228
228
  data={name: 1},
229
229
  custom_atlas_path=atlas_dir,
230
- cmap='binary',vminmax=[0, 1],
230
+ cmap='binary', vminmax=[0, 1],
231
231
  export_path=plot_file
232
232
  )
233
233
  except Exception as e:
@@ -328,6 +328,12 @@ def build_subcortical_atlas(nii_path, labels_dict, out_dir, include_list=None, e
328
328
  # save as a vtk file
329
329
  out_file = os.path.join(out_dir, f"{name}.vtk")
330
330
  mesh.save(out_file)
331
+
332
+ # file for ordering the labels
333
+ lut_path = os.path.join(out_dir, "atlas_LUT.txt")
334
+ with open(lut_path, 'w') as f:
335
+ for rid, name in targets.items():
336
+ f.write(f"{rid} {name}\n")
331
337
 
332
338
  print(f"\nsubcortical atlas successfully saved to: {out_dir}")
333
339
 
@@ -391,7 +397,7 @@ def qc_custom_subcortical_atlas(atlas_dir):
391
397
  plot_file = os.path.join(qc_dir, f"{region_name}.png")
392
398
 
393
399
  try:
394
- plot_subcortical(
400
+ ax = plot_subcortical(
395
401
  data={region_name: 1},
396
402
  custom_atlas_path=atlas_dir,
397
403
  cmap='binary', vminmax=[0, 1],
@@ -6,6 +6,7 @@ import os
6
6
  import glob
7
7
  from pathlib import Path
8
8
  import pooch
9
+ import shutil
9
10
 
10
11
  from ..utils import parse_lut
11
12
 
@@ -165,8 +166,7 @@ def get_atlas_regions(atlas, category, custom_atlas_path=None):
165
166
  elif category == 'subcortical':
166
167
  try:
167
168
  file_map = _find_subcortical_files(atlas_dir)
168
- # the plotting function sorts keys alphabetically
169
- return sorted(list(file_map.keys()))
169
+ return _get_ordered_names(atlas_dir, file_map)
170
170
  except Exception as e:
171
171
  print(f"Error listing subcortical regions: {e}")
172
172
  return []
@@ -175,8 +175,7 @@ def get_atlas_regions(atlas, category, custom_atlas_path=None):
175
175
  elif category == 'tracts':
176
176
  try:
177
177
  file_map = _find_tract_files(atlas_dir)
178
- # the plotting function sorts keys alphabetically
179
- return sorted(list(file_map.keys()))
178
+ return _get_ordered_names(atlas_dir, file_map)
180
179
  except Exception as e:
181
180
  print(f"Error listing tracts: {e}")
182
181
  return []
@@ -188,26 +187,44 @@ def get_atlas_regions(atlas, category, custom_atlas_path=None):
188
187
  def _fetch_and_unpack(resource_key):
189
188
  """
190
189
  Downloads zip, unpacks it, deletes the zip to save space,
191
- and returns the extraction path.
190
+ and returns the extraction path. Forces a redownload if the
191
+ registry hash changes (indicating an update).
192
192
  """
193
193
  extract_dir_name = resource_key.replace(".zip", "")
194
194
  extract_path = os.path.join(GOODBOY.path, extract_dir_name)
195
+ hash_file = os.path.join(extract_path, ".registry_hash")
195
196
 
196
- # optimization: check if unpacked folder already exists
197
- # if yes, skip pooch check entirely to avoid re-downloading
198
- if os.path.isdir(extract_path) and os.listdir(extract_path):
199
- return extract_path
197
+ # get the expected hash from the registry
198
+ expected_hash = GOODBOY.registry.get(resource_key)
199
+ if not expected_hash:
200
+ raise ValueError(f"Resource '{resource_key}' not found in registry.")
200
201
 
201
- # fetch and unzip
202
+ # check if unpacked folder already exists and is up-to-date
203
+ is_up_to_date = False
204
+ if os.path.isdir(extract_path) and os.path.exists(hash_file):
205
+ with open(hash_file, 'r') as f:
206
+ local_hash = f.read().strip()
207
+ if local_hash == expected_hash:
208
+ is_up_to_date = True
209
+ if is_up_to_date:
210
+ return extract_path
211
+ # if folder exists but hash is wrong (outdated), wipe it clean
212
+ elif os.path.exists(extract_path):
213
+ print(f"Update found for '{extract_dir_name}'. Removing legacy data...")
214
+ shutil.rmtree(extract_path)
215
+
216
+ # fetch and unzip new data
202
217
  try:
203
218
  GOODBOY.fetch(
204
219
  resource_key,
205
220
  processor=pooch.Unzip(extract_dir=extract_dir_name)
206
221
  )
207
- except ValueError:
208
- # if key not in registry
209
- available = list(GOODBOY.registry.keys())
210
- raise ValueError(f"Resource '{resource_key}' not found in registry.")
222
+ except Exception as e:
223
+ raise RuntimeError(f"Failed to fetch '{resource_key}': {e}")
224
+
225
+ # stamp the new folder with the updated hash
226
+ with open(hash_file, 'w') as f:
227
+ f.write(expected_hash)
211
228
 
212
229
  # cleanup: delete the source zip to save space
213
230
  zip_path = os.path.join(GOODBOY.path, resource_key)
@@ -225,7 +242,9 @@ def _resolve_resource_path(name, category, custom_path=None):
225
242
  if custom_path:
226
243
  if os.path.isdir(custom_path):
227
244
  return custom_path
228
- raise FileNotFoundError(f"Custom atlas directory not found: {custom_path}")
245
+ if os.path.isfile(custom_path):
246
+ return custom_path
247
+ raise FileNotFoundError(f"Custom atlas directory/file not found: {custom_path}")
229
248
 
230
249
  # 2. standard download logic
231
250
  resource_key = f"{category}-{name}.zip"
@@ -326,6 +345,46 @@ def _find_cortical_files(atlas_dir, strict_name=None):
326
345
 
327
346
  return csv_path, lut_path
328
347
 
348
+ def _get_ordered_names(atlas_dir, file_map):
349
+ """
350
+ Attempts to read the strict region order from a LUT or order text file.
351
+ Falls back to alphabetical sorting if no file exists.
352
+ """
353
+ txt_files = []
354
+
355
+ # look for a LUT or order file, ignoring qc reports
356
+ for root, dirs, files in os.walk(atlas_dir):
357
+ dirs[:] = [d for d in dirs if not d.startswith(('.', '__')) and 'qc_report' not in d]
358
+ for file in files:
359
+ if file.endswith('.txt') and 'registry' not in file:
360
+ txt_files.append(os.path.join(root, file))
361
+ def file_priority(filepath):
362
+ name = filepath.lower()
363
+ if 'lut' in name: return 0
364
+ if 'order' in name: return 1
365
+ return 2
366
+ txt_files.sort(key=file_priority)
367
+
368
+ if txt_files:
369
+ ordered_names = []
370
+ with open(txt_files[0], 'r') as f:
371
+ for line in f:
372
+ parts = line.strip().split()
373
+ # assuming standard LUT format (ID Name ...) or simple list (ID Name)
374
+ if len(parts) >= 2:
375
+ name = parts[1]
376
+ if name in file_map and name not in ordered_names:
377
+ ordered_names.append(name)
378
+
379
+ # append any stray files that exist in the directory but weren't in the text file
380
+ for name in sorted(file_map.keys()):
381
+ if name not in ordered_names:
382
+ ordered_names.append(name)
383
+
384
+ return ordered_names
385
+
386
+ # legacy fallback: alphabetical
387
+ return sorted(list(file_map.keys()))
329
388
 
330
389
  def _find_subcortical_files(atlas_dir):
331
390
  """
@@ -403,8 +462,15 @@ def _find_tract_files(atlas_dir):
403
462
 
404
463
  return candidates
405
464
 
406
- # scan for both .trk and .tck
407
- found_files = _scan_for_ext(atlas_dir, ".trk") + _scan_for_ext(atlas_dir, ".tck")
465
+ if os.path.isdir(atlas_dir):
466
+ # scan for both .trk and .tck
467
+ found_files = _scan_for_ext(atlas_dir, ".trk") + _scan_for_ext(atlas_dir, ".tck")
468
+ elif os.path.isfile(atlas_dir):
469
+ if atlas_dir.endswith((".trk", ".tck")):
470
+ found_files = [atlas_dir]
471
+ else:
472
+ raise ValueError(f"Invalid atlas directory/file path: {atlas_dir}, no valid tck or trk file found.")
473
+
408
474
 
409
475
  if not found_files:
410
476
  raise FileNotFoundError(f"No .trk or .tck files found in {atlas_dir}")
@@ -0,0 +1,30 @@
1
+ cortical-aparc.zip sha256:9e3e4853c580b6ed7c70a2b398b8ddacfc30e05600f04f722d3b5e8ee28ba119 https://osf.io/q5a2v/download
2
+ cortical-aal3.zip sha256:da90797ab768cb9b0ad1acf1a9035d62debdb823d0bcc2b96405b7e86c75a7dc https://osf.io/rcjqk/download
3
+ cortical-brainnetome.zip sha256:42b828d5dd5734a34fb61282b1ee92c1961c4c3e3cc35fa73d3f48f6c12c0fcd https://osf.io/5nr4x/download
4
+ cortical-schaefer100.zip sha256:79358c491ee2d9400e8166552a7e4d3f8ad7332e65192f0edf215d657f317896 https://osf.io/782gu/download
5
+ cortical-schaefer200.zip sha256:e24ab0fe4acec59604ce2a98ef9f4c14cc3c3d6dea73fb31c6900bd3879ea138 https://osf.io/9uzc8/download
6
+ cortical-schaefer300.zip sha256:ef096c2b96b0a8e1998417c23162f0e69f0693852911e55cb03f9681cc398669 https://osf.io/jcu8a/download
7
+ cortical-schaefer400.zip sha256:000c1a3b71926d83ea8c73fed63c8688d34c3cd8f0b8f91bd5c7f4c80edf7554 https://osf.io/k8dvw/download
8
+ cortical-schaefer1000.zip sha256:369827006e724e5ac74ef6a3761e2f77b5b30f2825e09d9a02c169dc5d3fbe90 https://osf.io/m3ze7/download
9
+ subcortical-aal3.zip sha256:7d0965016a393e20f86bb4b20761854e5fc74731a83aff51e56eddec9f2d6f21 https://osf.io/yvheg/download
10
+ subcortical-aal3_nocer.zip sha256:742e88052056dae851916dfaaac87173eeda5e74ec17bd44639140a34d70be04 https://osf.io/a59sj/download
11
+ subcortical-aseg.zip sha256:083ba39d80bd42e92344ac5bb46cf520e60ddb31a4cce6f5bcb9dc54892d4e27 https://osf.io/8akre/download
12
+ subcortical-brainnetome_sc.zip sha256:e008a3310bd8b80477af3685097800aa923487519274214eb2e0488109730051 https://osf.io/f9u3m/download
13
+ subcortical-musus100.zip sha256:61ed78eafc9f407e625a0288c0ed2cf9273b833a5341e9712d30a26974976140 https://osf.io/ak3hb/download
14
+ subcortical-musus100_dbn.zip sha256:8c79864c0cf37b2bc23752931742061be8a583a9064974bd52368ca657320555 https://osf.io/9g5z7/download
15
+ subcortical-musus100_tha.zip sha256:14f44f5b7b7ac24eb3870600f87fa66b862d3b5a7403039b3b9c219f2bf28cdf https://osf.io/xrd9j/download
16
+ subcortical-tian2020_s1.zip sha256:90c5c78a3d4636aea94944d54b24a3c0510580f6ca62fdb9192a316fd8e1a9cf https://osf.io/9z7wm/download
17
+ tracts-hcp1065_medium.zip sha256:366bb100074cf7b1e55586e5468653f0c342e42cfdd91dca6d99b6fde82f06ff https://osf.io/kjf8e/download
18
+ tracts-hcp1065_small.zip sha256:020f23059c3a20ee0dda8ad12cd3df99b9fe5d3b37e7a6b9ae34b8a52b4b4346 https://osf.io/ynpa5/download
19
+ tracts-hcp1065_tiny.zip sha256:54380d82f5cd234029c6fc011910e00f0ad859fdb6c22c6aaa6452d055449ae9 https://osf.io/jzk7p/download
20
+ tracts-xtract_large.zip sha256:e2d59e9f90b024018788af87e389f93b2fa9ac213604601966a44d7f81c9d89f https://osf.io/pktq6/download
21
+ tracts-xtract_medium.zip sha256:f095028d3dfc0f974ebef1feffe4006b5bac1366325a784ea2301c56f583b1e7 https://osf.io/7tbjg/download
22
+ tracts-xtract_small.zip sha256:9d3fe2c57acb87e8d13eb40a49a6c2c354dbe1a020122b693e9bf713e57cad5b https://osf.io/bmn3a/download
23
+ tracts-xtract_tiny.zip sha256:469f9ed8ed5ceb7f8e17c9a0a92f1491d540814f832ef56441a7539532111f41 https://osf.io/a73x2/download
24
+ bmesh-inflated.zip sha256:ba72f9e75f16fe767f6bbcec5c98381f6a20b2f5e2092505b4692844a1686461 https://osf.io/kfzjg/download
25
+ bmesh-midthickness.zip sha256:ef8209853e1dac8804a60b5cde0b653ef2f6c77b6c7536f7acaa69a46dbb06c0 https://osf.io/xaktf/download
26
+ bmesh-pial.zip sha256:5e36ba7883dd2a88485fc45c9166ed48e22db607d45655242961242b51f9f126 https://osf.io/knpg8/download
27
+ bmesh-swm.zip sha256:1a516c070cb63557751d841d5cf75772660872b89ec749ee19c23298d77fa807 https://osf.io/hpbc9/download
28
+ bmesh-very_inflated.zip sha256:8c33a00c718a47f0af118ce2c5804a2adf13d79be05c4665ca26582e3e8dd977 https://osf.io/xp9jr/download
29
+ bmesh-white.zip sha256:b885ac1a4dcdf9e241a97e8d3ca59d7b1c86d8a3ebe17df512993bc5c1c0354a https://osf.io/wfc5t/download
30
+ label-nomedialwall.zip sha256:03bd589b151814a1fc5e48236a78decf1a51791f04c3f4a521d2ed4fb214317b https://osf.io/2rgmc/download