yabplot 0.1.0__py3-none-any.whl
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.
- yabplot/__init__.py +9 -0
- yabplot/data/__init__.py +371 -0
- yabplot/plotting.py +614 -0
- yabplot/scene.py +124 -0
- yabplot/utils.py +170 -0
- yabplot-0.1.0.dist-info/METADATA +97 -0
- yabplot-0.1.0.dist-info/RECORD +10 -0
- yabplot-0.1.0.dist-info/WHEEL +5 -0
- yabplot-0.1.0.dist-info/licenses/LICENSE +19 -0
- yabplot-0.1.0.dist-info/top_level.txt +1 -0
yabplot/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
2
|
+
|
|
3
|
+
from .plotting import plot_cortical, plot_subcortical, plot_tracts, clear_tract_cache
|
|
4
|
+
from .data import get_available_resources, get_atlas_regions
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version("yabplot")
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
__version__ = "unknown"
|
yabplot/data/__init__.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data management module for fetching and caching remote atlases.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import glob
|
|
7
|
+
import shutil
|
|
8
|
+
import pooch
|
|
9
|
+
from importlib.resources import files
|
|
10
|
+
|
|
11
|
+
from ..utils import parse_lut
|
|
12
|
+
|
|
13
|
+
__all__ = ['get_available_resources']
|
|
14
|
+
|
|
15
|
+
# define cache location
|
|
16
|
+
# e.g., ~/.cache/yabplot
|
|
17
|
+
CACHE_DIR = pooch.os_cache("yabplot")
|
|
18
|
+
|
|
19
|
+
# setup registry
|
|
20
|
+
_REGISTRY_PATH = files('yabplot.data').joinpath('registry.txt')
|
|
21
|
+
_FETCHER = pooch.create(
|
|
22
|
+
path=CACHE_DIR,
|
|
23
|
+
base_url="",
|
|
24
|
+
registry=None,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if _REGISTRY_PATH.is_file():
|
|
28
|
+
_FETCHER.load_registry(_REGISTRY_PATH)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_available_resources(category=None):
|
|
32
|
+
"""
|
|
33
|
+
Returns available resources from the registry.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
category : str or None
|
|
38
|
+
If provided (e.g., 'cortical', 'subcortical', 'tracts', 'bmesh'), returns a list of available names
|
|
39
|
+
for that specific category.
|
|
40
|
+
If None, returns a dictionary containing all categories and their options.
|
|
41
|
+
"""
|
|
42
|
+
if not _FETCHER.registry:
|
|
43
|
+
return [] if category else {}
|
|
44
|
+
|
|
45
|
+
# helper to clean names: e.g., "cortical-aparc.zip" -> ("cortical", "aparc")
|
|
46
|
+
def _parse_key(key):
|
|
47
|
+
if "-" not in key: return None, None
|
|
48
|
+
prefix, remainder = key.split("-", 1)
|
|
49
|
+
name = remainder.replace(".zip", "")
|
|
50
|
+
return prefix, name
|
|
51
|
+
|
|
52
|
+
# mode 1: specific category
|
|
53
|
+
if category:
|
|
54
|
+
available = []
|
|
55
|
+
for key in _FETCHER.registry.keys():
|
|
56
|
+
prefix, name = _parse_key(key)
|
|
57
|
+
if prefix == category:
|
|
58
|
+
available.append(name)
|
|
59
|
+
return sorted(available)
|
|
60
|
+
|
|
61
|
+
# mode 2: all categories
|
|
62
|
+
all_resources = {}
|
|
63
|
+
for key in _FETCHER.registry.keys():
|
|
64
|
+
prefix, name = _parse_key(key)
|
|
65
|
+
if prefix and name:
|
|
66
|
+
if prefix not in all_resources:
|
|
67
|
+
all_resources[prefix] = []
|
|
68
|
+
all_resources[prefix].append(name)
|
|
69
|
+
|
|
70
|
+
for k in all_resources:
|
|
71
|
+
all_resources[k].sort()
|
|
72
|
+
|
|
73
|
+
return all_resources
|
|
74
|
+
|
|
75
|
+
def get_atlas_regions(atlas, category, custom_atlas_path=None):
|
|
76
|
+
"""
|
|
77
|
+
Returns the list of region names for a given atlas in the specific order
|
|
78
|
+
used for mapping data arrays.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
atlas : str
|
|
83
|
+
Name of the atlas (e.g., 'aparc', 'aseg').
|
|
84
|
+
category : str
|
|
85
|
+
'cortical', 'subcortical', or 'tracts'.
|
|
86
|
+
custom_atlas_path : str, optional
|
|
87
|
+
Path to custom atlas directory.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
list
|
|
92
|
+
List of strings containing region names.
|
|
93
|
+
- If input data is a LIST, it must match this order.
|
|
94
|
+
- If input data is a DICT, keys must match these names.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
# resolve the directory path
|
|
98
|
+
try:
|
|
99
|
+
atlas_dir = _resolve_resource_path(atlas, category, custom_path=custom_atlas_path)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"Error resolving atlas: {e}")
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
# --- case 1: cortical ---
|
|
105
|
+
if category == 'cortical':
|
|
106
|
+
check_name = None if custom_atlas_path else atlas
|
|
107
|
+
try:
|
|
108
|
+
_, lut_path = _find_cortical_files(atlas_dir, strict_name=check_name)
|
|
109
|
+
|
|
110
|
+
# use parse_lut to get the IDs and the full names list
|
|
111
|
+
ids, _, names_list, _ = parse_lut(lut_path)
|
|
112
|
+
|
|
113
|
+
# return only the names corresponding to the explicit IDs in the file.
|
|
114
|
+
return [names_list[i] for i in ids]
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(f"Error parsing cortical atlas: {e}")
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
# --- case 2: subcortical ---
|
|
121
|
+
elif category == 'subcortical':
|
|
122
|
+
try:
|
|
123
|
+
file_map = _find_subcortical_files(atlas_dir)
|
|
124
|
+
# the plotting function sorts keys alphabetically
|
|
125
|
+
return sorted(list(file_map.keys()))
|
|
126
|
+
except Exception as e:
|
|
127
|
+
print(f"Error listing subcortical regions: {e}")
|
|
128
|
+
return []
|
|
129
|
+
|
|
130
|
+
# --- case 3: tracts ---
|
|
131
|
+
elif category == 'tracts':
|
|
132
|
+
try:
|
|
133
|
+
file_map = _find_tract_files(atlas_dir)
|
|
134
|
+
# the plotting function sorts keys alphabetically
|
|
135
|
+
return sorted(list(file_map.keys()))
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print(f"Error listing tracts: {e}")
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError("Category must be 'cortical', 'subcortical', or 'tracts'")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _fetch_and_unpack(resource_key):
|
|
145
|
+
"""
|
|
146
|
+
Downloads zip, unpacks it, deletes the zip to save space,
|
|
147
|
+
and returns the extraction path.
|
|
148
|
+
"""
|
|
149
|
+
extract_dir_name = resource_key.replace(".zip", "")
|
|
150
|
+
extract_path = os.path.join(_FETCHER.path, extract_dir_name)
|
|
151
|
+
|
|
152
|
+
# optimization: check if unpacked folder already exists
|
|
153
|
+
# if yes, skip pooch check entirely to avoid re-downloading
|
|
154
|
+
if os.path.isdir(extract_path) and os.listdir(extract_path):
|
|
155
|
+
return extract_path
|
|
156
|
+
|
|
157
|
+
# fetch and unzip
|
|
158
|
+
try:
|
|
159
|
+
_FETCHER.fetch(
|
|
160
|
+
resource_key,
|
|
161
|
+
processor=pooch.Unzip(extract_dir=extract_dir_name)
|
|
162
|
+
)
|
|
163
|
+
except ValueError:
|
|
164
|
+
# if key not in registry
|
|
165
|
+
available = list(_FETCHER.registry.keys())
|
|
166
|
+
raise ValueError(f"Resource '{resource_key}' not found in registry.")
|
|
167
|
+
|
|
168
|
+
# cleanup: delete the source zip to save space
|
|
169
|
+
zip_path = os.path.join(_FETCHER.path, resource_key)
|
|
170
|
+
if os.path.exists(zip_path):
|
|
171
|
+
os.remove(zip_path)
|
|
172
|
+
|
|
173
|
+
return extract_path
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _resolve_resource_path(name, category, custom_path=None):
|
|
177
|
+
"""
|
|
178
|
+
Internal: Resolves atlas path via download or custom location.
|
|
179
|
+
"""
|
|
180
|
+
# 1. custom path logic
|
|
181
|
+
if custom_path:
|
|
182
|
+
if os.path.isdir(custom_path):
|
|
183
|
+
return custom_path
|
|
184
|
+
raise FileNotFoundError(f"Custom atlas directory not found: {custom_path}")
|
|
185
|
+
|
|
186
|
+
# 2. standard download logic
|
|
187
|
+
resource_key = f"{category}-{name}.zip"
|
|
188
|
+
|
|
189
|
+
# validate before fetching
|
|
190
|
+
if resource_key not in _FETCHER.registry:
|
|
191
|
+
available = get_available_resources(category)
|
|
192
|
+
human_cat = {
|
|
193
|
+
'cortical': 'Cortical parcellations (vertices)',
|
|
194
|
+
'subcortical': 'Subcortical segmentations (volumes)',
|
|
195
|
+
'tracts': 'White matter bundles (tracts)',
|
|
196
|
+
'bmesh': 'Brain meshes'
|
|
197
|
+
}.get(category, category)
|
|
198
|
+
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"Resource '{name}' is not available in {human_cat}.\n"
|
|
201
|
+
f"Available options: {available}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return _fetch_and_unpack(resource_key)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _find_cortical_files(atlas_dir, strict_name=None):
|
|
208
|
+
"""
|
|
209
|
+
Internal: Locates files, ignoring hidden/system folders.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def _find_file(directory, pattern):
|
|
213
|
+
"""searches root and valid subdirectories."""
|
|
214
|
+
# check root
|
|
215
|
+
candidates = glob.glob(os.path.join(directory, pattern))
|
|
216
|
+
|
|
217
|
+
# check subdirs if empty
|
|
218
|
+
if not candidates:
|
|
219
|
+
try:
|
|
220
|
+
# get all items, filtering out hidden/system ones
|
|
221
|
+
subdirs = [
|
|
222
|
+
os.path.join(directory, d) for d in os.listdir(directory)
|
|
223
|
+
if os.path.isdir(os.path.join(directory, d))
|
|
224
|
+
and not d.startswith(('.', '__'))
|
|
225
|
+
]
|
|
226
|
+
subdirs.sort()
|
|
227
|
+
|
|
228
|
+
for sd in subdirs:
|
|
229
|
+
candidates.extend(glob.glob(os.path.join(sd, pattern)))
|
|
230
|
+
except FileNotFoundError:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
return candidates
|
|
234
|
+
|
|
235
|
+
# --- mode a: strict (standard atlases) ---
|
|
236
|
+
if strict_name:
|
|
237
|
+
csv_name = f'{strict_name}_conte69.csv'
|
|
238
|
+
lut_name = f'{strict_name}_LUT.txt'
|
|
239
|
+
|
|
240
|
+
found_csvs = _find_file(atlas_dir, csv_name)
|
|
241
|
+
if not found_csvs:
|
|
242
|
+
raise FileNotFoundError(f"Corrupt atlas. Missing '{csv_name}' in {atlas_dir}")
|
|
243
|
+
|
|
244
|
+
found_luts = _find_file(atlas_dir, lut_name)
|
|
245
|
+
if not found_luts:
|
|
246
|
+
raise FileNotFoundError(f"Corrupt atlas. Missing '{lut_name}' in {atlas_dir}")
|
|
247
|
+
|
|
248
|
+
return found_csvs[0], found_luts[0]
|
|
249
|
+
|
|
250
|
+
# --- mode b: flexible (custom atlases) ---
|
|
251
|
+
|
|
252
|
+
# find csv
|
|
253
|
+
csv_candidates = _find_file(atlas_dir, "*.csv")
|
|
254
|
+
if len(csv_candidates) == 1:
|
|
255
|
+
csv_path = csv_candidates[0]
|
|
256
|
+
elif len(csv_candidates) > 1:
|
|
257
|
+
# resolve ambiguity
|
|
258
|
+
filtered = [f for f in csv_candidates if 'conte69' in f]
|
|
259
|
+
if len(filtered) == 1:
|
|
260
|
+
csv_path = filtered[0]
|
|
261
|
+
else:
|
|
262
|
+
names = [os.path.basename(c) for c in csv_candidates]
|
|
263
|
+
raise ValueError(f"Ambiguous CSVs found: {names}")
|
|
264
|
+
else:
|
|
265
|
+
raise FileNotFoundError(f"No .csv file found in custom directory: {atlas_dir}")
|
|
266
|
+
|
|
267
|
+
# find lut
|
|
268
|
+
lut_candidates = _find_file(atlas_dir, "*.txt") + _find_file(atlas_dir, "*.lut")
|
|
269
|
+
if len(lut_candidates) == 1:
|
|
270
|
+
lut_path = lut_candidates[0]
|
|
271
|
+
elif len(lut_candidates) > 1:
|
|
272
|
+
# resolve ambiguity
|
|
273
|
+
filtered = [f for f in lut_candidates if 'LUT' in f or 'lut' in f]
|
|
274
|
+
if len(filtered) == 1:
|
|
275
|
+
lut_path = filtered[0]
|
|
276
|
+
else:
|
|
277
|
+
names = [os.path.basename(c) for c in lut_candidates]
|
|
278
|
+
raise ValueError(f"Ambiguous LUTs found: {names}")
|
|
279
|
+
else:
|
|
280
|
+
raise FileNotFoundError(f"No LUT file found in custom directory: {atlas_dir}")
|
|
281
|
+
|
|
282
|
+
return csv_path, lut_path
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _find_subcortical_files(atlas_dir):
|
|
286
|
+
"""
|
|
287
|
+
Internal: Scans directory for mesh files (.vtk preferred, then .gii).
|
|
288
|
+
Returns a dictionary: {region_name: file_path}
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
def _scan_for_ext(directory, extension):
|
|
292
|
+
"""Recursively finds files with extension, ignoring junk folders."""
|
|
293
|
+
candidates = []
|
|
294
|
+
# check root
|
|
295
|
+
candidates.extend(glob.glob(os.path.join(directory, f"*{extension}")))
|
|
296
|
+
|
|
297
|
+
# check valid subdirectories
|
|
298
|
+
try:
|
|
299
|
+
subdirs = [
|
|
300
|
+
os.path.join(directory, d) for d in os.listdir(directory)
|
|
301
|
+
if os.path.isdir(os.path.join(directory, d))
|
|
302
|
+
and not d.startswith(('.', '__'))
|
|
303
|
+
]
|
|
304
|
+
for sd in subdirs:
|
|
305
|
+
candidates.extend(glob.glob(os.path.join(sd, f"*{extension}")))
|
|
306
|
+
except FileNotFoundError:
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
return candidates
|
|
310
|
+
|
|
311
|
+
# try finding VTK files
|
|
312
|
+
vtk_files = _scan_for_ext(atlas_dir, ".vtk")
|
|
313
|
+
if vtk_files:
|
|
314
|
+
# map basename -> full path
|
|
315
|
+
return {
|
|
316
|
+
os.path.splitext(os.path.basename(f))[0]: f
|
|
317
|
+
for f in vtk_files
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# if no VTKs, try GIfTI files
|
|
321
|
+
gii_files = _scan_for_ext(atlas_dir, ".gii")
|
|
322
|
+
if gii_files:
|
|
323
|
+
# filter for '_surface.surf.gii' but fallback to just .gii if typical naming isn't found
|
|
324
|
+
filtered_gii = [f for f in gii_files if '.surf.gii' in f]
|
|
325
|
+
if not filtered_gii:
|
|
326
|
+
filtered_gii = gii_files
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
os.path.basename(f).split('.')[0]: f
|
|
330
|
+
for f in filtered_gii
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
raise FileNotFoundError(f"No .vtk or .gii mesh files found in {atlas_dir}")
|
|
334
|
+
|
|
335
|
+
def _find_tract_files(atlas_dir):
|
|
336
|
+
"""
|
|
337
|
+
Internal: Scans directory for tractography files (.trk).
|
|
338
|
+
Returns a dictionary: {tract_name: file_path}
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
def _scan_for_ext(directory, extension):
|
|
342
|
+
"""Recursively finds files with extension, ignoring junk folders."""
|
|
343
|
+
candidates = []
|
|
344
|
+
# check root
|
|
345
|
+
candidates.extend(glob.glob(os.path.join(directory, f"*{extension}")))
|
|
346
|
+
|
|
347
|
+
# check valid subdirectories
|
|
348
|
+
try:
|
|
349
|
+
subdirs = [
|
|
350
|
+
os.path.join(directory, d) for d in os.listdir(directory)
|
|
351
|
+
if os.path.isdir(os.path.join(directory, d))
|
|
352
|
+
and not d.startswith(('.', '__'))
|
|
353
|
+
]
|
|
354
|
+
for sd in subdirs:
|
|
355
|
+
candidates.extend(glob.glob(os.path.join(sd, f"*{extension}")))
|
|
356
|
+
except FileNotFoundError:
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
return candidates
|
|
360
|
+
|
|
361
|
+
# find .trk files
|
|
362
|
+
trk_files = _scan_for_ext(atlas_dir, ".trk")
|
|
363
|
+
|
|
364
|
+
if not trk_files:
|
|
365
|
+
raise FileNotFoundError(f"No .trk files found in {atlas_dir}")
|
|
366
|
+
|
|
367
|
+
# map basename -> full path
|
|
368
|
+
return {
|
|
369
|
+
os.path.splitext(os.path.basename(f))[0]: f
|
|
370
|
+
for f in trk_files
|
|
371
|
+
}
|