siibra 1.0a1__1-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.
Potentially problematic release.
This version of siibra might be problematic. Click here for more details.
- siibra/VERSION +1 -0
- siibra/__init__.py +164 -0
- siibra/commons.py +823 -0
- siibra/configuration/__init__.py +17 -0
- siibra/configuration/configuration.py +189 -0
- siibra/configuration/factory.py +589 -0
- siibra/core/__init__.py +16 -0
- siibra/core/assignment.py +110 -0
- siibra/core/atlas.py +239 -0
- siibra/core/concept.py +308 -0
- siibra/core/parcellation.py +387 -0
- siibra/core/region.py +1223 -0
- siibra/core/space.py +131 -0
- siibra/core/structure.py +111 -0
- siibra/exceptions.py +63 -0
- siibra/experimental/__init__.py +19 -0
- siibra/experimental/contour.py +61 -0
- siibra/experimental/cortical_profile_sampler.py +57 -0
- siibra/experimental/patch.py +98 -0
- siibra/experimental/plane3d.py +256 -0
- siibra/explorer/__init__.py +17 -0
- siibra/explorer/url.py +222 -0
- siibra/explorer/util.py +87 -0
- siibra/features/__init__.py +117 -0
- siibra/features/anchor.py +224 -0
- siibra/features/connectivity/__init__.py +33 -0
- siibra/features/connectivity/functional_connectivity.py +57 -0
- siibra/features/connectivity/regional_connectivity.py +494 -0
- siibra/features/connectivity/streamline_counts.py +27 -0
- siibra/features/connectivity/streamline_lengths.py +27 -0
- siibra/features/connectivity/tracing_connectivity.py +30 -0
- siibra/features/dataset/__init__.py +17 -0
- siibra/features/dataset/ebrains.py +90 -0
- siibra/features/feature.py +970 -0
- siibra/features/image/__init__.py +27 -0
- siibra/features/image/image.py +115 -0
- siibra/features/image/sections.py +26 -0
- siibra/features/image/volume_of_interest.py +88 -0
- siibra/features/tabular/__init__.py +24 -0
- siibra/features/tabular/bigbrain_intensity_profile.py +77 -0
- siibra/features/tabular/cell_density_profile.py +298 -0
- siibra/features/tabular/cortical_profile.py +322 -0
- siibra/features/tabular/gene_expression.py +257 -0
- siibra/features/tabular/layerwise_bigbrain_intensities.py +62 -0
- siibra/features/tabular/layerwise_cell_density.py +95 -0
- siibra/features/tabular/receptor_density_fingerprint.py +192 -0
- siibra/features/tabular/receptor_density_profile.py +110 -0
- siibra/features/tabular/regional_timeseries_activity.py +294 -0
- siibra/features/tabular/tabular.py +139 -0
- siibra/livequeries/__init__.py +19 -0
- siibra/livequeries/allen.py +352 -0
- siibra/livequeries/bigbrain.py +197 -0
- siibra/livequeries/ebrains.py +145 -0
- siibra/livequeries/query.py +49 -0
- siibra/locations/__init__.py +91 -0
- siibra/locations/boundingbox.py +454 -0
- siibra/locations/location.py +115 -0
- siibra/locations/point.py +344 -0
- siibra/locations/pointcloud.py +349 -0
- siibra/retrieval/__init__.py +27 -0
- siibra/retrieval/cache.py +233 -0
- siibra/retrieval/datasets.py +389 -0
- siibra/retrieval/exceptions/__init__.py +27 -0
- siibra/retrieval/repositories.py +769 -0
- siibra/retrieval/requests.py +659 -0
- siibra/vocabularies/__init__.py +45 -0
- siibra/vocabularies/gene_names.json +29176 -0
- siibra/vocabularies/receptor_symbols.json +210 -0
- siibra/vocabularies/region_aliases.json +460 -0
- siibra/volumes/__init__.py +23 -0
- siibra/volumes/parcellationmap.py +1279 -0
- siibra/volumes/providers/__init__.py +20 -0
- siibra/volumes/providers/freesurfer.py +113 -0
- siibra/volumes/providers/gifti.py +165 -0
- siibra/volumes/providers/neuroglancer.py +736 -0
- siibra/volumes/providers/nifti.py +266 -0
- siibra/volumes/providers/provider.py +107 -0
- siibra/volumes/sparsemap.py +468 -0
- siibra/volumes/volume.py +892 -0
- siibra-1.0.0a1.dist-info/LICENSE +201 -0
- siibra-1.0.0a1.dist-info/METADATA +160 -0
- siibra-1.0.0a1.dist-info/RECORD +84 -0
- siibra-1.0.0a1.dist-info/WHEEL +5 -0
- siibra-1.0.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# Copyright 2018-2024
|
|
2
|
+
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from . import provider as _provider
|
|
17
|
+
|
|
18
|
+
from ...commons import logger, resample_img_to_img
|
|
19
|
+
from ...retrieval import requests
|
|
20
|
+
from ...locations import pointcloud, boundingbox as _boundingbox
|
|
21
|
+
|
|
22
|
+
from typing import Union, Dict, Tuple
|
|
23
|
+
import nibabel as nib
|
|
24
|
+
import os
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NiftiProvider(_provider.VolumeProvider, srctype="nii"):
|
|
29
|
+
|
|
30
|
+
def __init__(self, src: Union[str, Dict[str, str], nib.Nifti1Image, Tuple[np.ndarray, np.ndarray]]):
|
|
31
|
+
"""
|
|
32
|
+
Construct a new NIfTI volume source, from url, local file, or Nift1Image object.
|
|
33
|
+
"""
|
|
34
|
+
_provider.VolumeProvider.__init__(self)
|
|
35
|
+
|
|
36
|
+
self._init_url: Union[str, Dict[str, str]] = None
|
|
37
|
+
|
|
38
|
+
def loader(url):
|
|
39
|
+
if os.path.isfile(url):
|
|
40
|
+
return lambda fn=url: nib.load(fn)
|
|
41
|
+
else:
|
|
42
|
+
req = requests.HttpRequest(url)
|
|
43
|
+
return lambda req=req: req.data
|
|
44
|
+
|
|
45
|
+
if isinstance(src, nib.Nifti1Image):
|
|
46
|
+
self._img_loaders = {None: lambda img=src: img}
|
|
47
|
+
elif isinstance(src, str): # one single image to load
|
|
48
|
+
self._img_loaders = {None: loader(src)}
|
|
49
|
+
elif isinstance(src, dict): # assuming multiple for fragment images
|
|
50
|
+
self._img_loaders = {lbl: loader(url) for lbl, url in src.items()}
|
|
51
|
+
elif isinstance(src, tuple):
|
|
52
|
+
assert len(src) == 2
|
|
53
|
+
assert all(isinstance(_, np.ndarray) for _ in src)
|
|
54
|
+
self._img_loaders = {None: lambda data=src[0], affine=src[1]: nib.Nifti1Image(data, affine)}
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError(f"Invalid source specification for {self.__class__}: {src}")
|
|
57
|
+
if not isinstance(src, (nib.Nifti1Image, tuple)):
|
|
58
|
+
self._init_url = src
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def _url(self) -> Union[str, Dict[str, str]]:
|
|
62
|
+
return self._init_url
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def fragments(self):
|
|
66
|
+
return [k for k in self._img_loaders if k is not None]
|
|
67
|
+
|
|
68
|
+
def get_boundingbox(self, **fetch_kwargs) -> "_boundingbox.BoundingBox":
|
|
69
|
+
"""
|
|
70
|
+
Return the bounding box in physical coordinates of the union of
|
|
71
|
+
fragments in this nifti volume.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
fetch_kwargs:
|
|
76
|
+
Not used
|
|
77
|
+
"""
|
|
78
|
+
bbox = None
|
|
79
|
+
for loader in self._img_loaders.values():
|
|
80
|
+
img = loader()
|
|
81
|
+
if len(img.shape) > 3:
|
|
82
|
+
logger.warning(
|
|
83
|
+
f"N-D NIfTI volume has shape {img.shape}, but "
|
|
84
|
+
f"bounding box considers only {img.shape[:3]}"
|
|
85
|
+
)
|
|
86
|
+
shape = img.shape[:3]
|
|
87
|
+
next_bbox = _boundingbox.BoundingBox(
|
|
88
|
+
(0, 0, 0), shape, space=None
|
|
89
|
+
).transform(img.affine)
|
|
90
|
+
bbox = next_bbox if bbox is None else bbox.union(next_bbox)
|
|
91
|
+
return bbox
|
|
92
|
+
|
|
93
|
+
def _merge_fragments(self) -> nib.Nifti1Image:
|
|
94
|
+
"""
|
|
95
|
+
Merge all fragments this volume contains into one Nifti1Image.
|
|
96
|
+
"""
|
|
97
|
+
bbox = self.get_boundingbox(clip=False, background=0.0)
|
|
98
|
+
num_conflicts = 0
|
|
99
|
+
result = None
|
|
100
|
+
for loader in self._img_loaders.values():
|
|
101
|
+
img = loader()
|
|
102
|
+
if result is None:
|
|
103
|
+
# build the empty result image with its own affine and voxel space
|
|
104
|
+
s0 = np.identity(4)
|
|
105
|
+
s0[:3, -1] = list(bbox.minpoint.transform(np.linalg.inv(img.affine)))
|
|
106
|
+
result_affine = np.dot(img.affine, s0) # adjust global bounding box offset to get global affine
|
|
107
|
+
voxdims = np.asanyarray(np.ceil(
|
|
108
|
+
bbox.transform(np.linalg.inv(result_affine)).shape
|
|
109
|
+
), dtype="int")
|
|
110
|
+
result_arr = np.zeros(voxdims, dtype=img.dataobj.dtype)
|
|
111
|
+
result = nib.Nifti1Image(dataobj=result_arr, affine=result_affine)
|
|
112
|
+
|
|
113
|
+
# resample to merge template and update it
|
|
114
|
+
resampled_img = resample_img_to_img(source_img=img, target_img=result)
|
|
115
|
+
arr = np.asanyarray(resampled_img.dataobj)
|
|
116
|
+
nonzero_voxels = arr != 0
|
|
117
|
+
num_conflicts += np.count_nonzero(result_arr[nonzero_voxels])
|
|
118
|
+
result_arr[nonzero_voxels] = arr[nonzero_voxels]
|
|
119
|
+
|
|
120
|
+
if num_conflicts > 0:
|
|
121
|
+
num_voxels = np.count_nonzero(result_arr)
|
|
122
|
+
logger.warning(
|
|
123
|
+
f"Merging fragments required to overwrite {num_conflicts} "
|
|
124
|
+
f"conflicting voxels ({num_conflicts / num_voxels * 100.:2.1f}%)."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
def fetch(
|
|
130
|
+
self,
|
|
131
|
+
fragment: str = None,
|
|
132
|
+
voi: _boundingbox.BoundingBox = None,
|
|
133
|
+
label: int = None
|
|
134
|
+
):
|
|
135
|
+
"""
|
|
136
|
+
Loads and returns a Nifti1Image object
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
fragment: str
|
|
141
|
+
Optional name of a fragment volume to fetch, if any.
|
|
142
|
+
For example, some volumes are split into left and right hemisphere fragments.
|
|
143
|
+
see :func:`~siibra.volumes.Volume.fragments`
|
|
144
|
+
voi : BoundingBox
|
|
145
|
+
optional specification of a volume of interest to fetch.
|
|
146
|
+
label: int, default: None
|
|
147
|
+
Optional: a label index can be provided. Then the mask of the
|
|
148
|
+
3D volume will be returned, where voxels matching this label
|
|
149
|
+
are marked as "1".
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
result = None
|
|
153
|
+
if len(self._img_loaders) > 1:
|
|
154
|
+
if fragment is None:
|
|
155
|
+
logger.info(
|
|
156
|
+
f"Merging fragments [{', '.join(self._img_loaders.keys())}]. "
|
|
157
|
+
f"You can select one using {self.__class__.__name__}.fetch(fragment=<name>)."
|
|
158
|
+
)
|
|
159
|
+
result = self._merge_fragments()
|
|
160
|
+
else:
|
|
161
|
+
matched_names = [n for n in self._img_loaders if fragment.lower() in n.lower()]
|
|
162
|
+
if len(matched_names) != 1:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"Requested fragment '{fragment}' could not be matched uniquely "
|
|
165
|
+
f"to [{', '.join(self._img_loaders)}]"
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
result = self._img_loaders[matched_names[0]]()
|
|
169
|
+
else:
|
|
170
|
+
assert len(self._img_loaders) > 0
|
|
171
|
+
fragment_name, loader = next(iter(self._img_loaders.items()))
|
|
172
|
+
if (fragment_name is not None) and (fragment is not None):
|
|
173
|
+
assert fragment.lower() in fragment_name.lower()
|
|
174
|
+
result = loader()
|
|
175
|
+
|
|
176
|
+
if voi is not None:
|
|
177
|
+
zoom_xyz = np.array(result.header.get_zooms()) # voxel dimensions in xyzt_units
|
|
178
|
+
bb_vox = voi.transform(np.linalg.inv(result.affine))
|
|
179
|
+
x0, y0, z0 = np.floor(np.array(bb_vox.minpoint.coordinate) / zoom_xyz).astype(int)
|
|
180
|
+
x1, y1, z1 = np.ceil(np.array(bb_vox.maxpoint.coordinate) / zoom_xyz).astype(int)
|
|
181
|
+
shift = np.identity(4)
|
|
182
|
+
shift[:3, -1] = bb_vox.minpoint
|
|
183
|
+
result = nib.Nifti1Image(
|
|
184
|
+
dataobj=result.dataobj[x0:x1, y0:y1, z0:z1],
|
|
185
|
+
affine=np.dot(result.affine, shift),
|
|
186
|
+
dtype=result.header.get_data_dtype(),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if label is not None:
|
|
190
|
+
result = nib.Nifti1Image(
|
|
191
|
+
(result.get_fdata() == label).astype('uint8'),
|
|
192
|
+
result.affine,
|
|
193
|
+
dtype='uint8'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
def get_shape(self, resolution_mm=None):
|
|
199
|
+
if resolution_mm is not None:
|
|
200
|
+
raise NotImplementedError(
|
|
201
|
+
"NiftiVolume does not support to specify different image resolutions"
|
|
202
|
+
)
|
|
203
|
+
try:
|
|
204
|
+
loader_shapes = {loader().shape for loader in self._img_loaders.values()}
|
|
205
|
+
if len(loader_shapes) == 1:
|
|
206
|
+
return next(iter(loader_shapes))
|
|
207
|
+
else:
|
|
208
|
+
raise RuntimeError(f"Fragments have different shapes: {loader_shapes}")
|
|
209
|
+
except AttributeError as e:
|
|
210
|
+
logger.error(
|
|
211
|
+
f"Invalid object type/s {[type(loader()) for loader in self._img_loaders.values()]} of image for {self}."
|
|
212
|
+
)
|
|
213
|
+
raise (e)
|
|
214
|
+
|
|
215
|
+
def is_float(self):
|
|
216
|
+
return all(
|
|
217
|
+
loader().dataobj.dtype.kind == "f"
|
|
218
|
+
for loader in self._img_loaders.values()
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def find_peaks(self, min_distance_mm=5):
|
|
222
|
+
"""
|
|
223
|
+
Find peaks in the image data.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
min_distance_mm : float
|
|
228
|
+
Minimum distance between peaks in mm
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
--------
|
|
232
|
+
PointCloud
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
from skimage.feature.peak import peak_local_max
|
|
236
|
+
from ...commons import affine_scaling
|
|
237
|
+
|
|
238
|
+
img = self.fetch()
|
|
239
|
+
dist = int(min_distance_mm / affine_scaling(img.affine) + 0.5)
|
|
240
|
+
voxels = peak_local_max(
|
|
241
|
+
img.get_fdata(),
|
|
242
|
+
exclude_border=False,
|
|
243
|
+
min_distance=dist,
|
|
244
|
+
)
|
|
245
|
+
return (
|
|
246
|
+
pointcloud.PointCloud(
|
|
247
|
+
[np.dot(img.affine, [x, y, z, 1])[:3] for x, y, z in voxels],
|
|
248
|
+
space=self.space,
|
|
249
|
+
),
|
|
250
|
+
img,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class ZipContainedNiftiProvider(NiftiProvider, srctype="zip/nii"):
|
|
255
|
+
|
|
256
|
+
def __init__(self, src: str):
|
|
257
|
+
"""
|
|
258
|
+
Construct a new NIfTI volume source, from url, local file, or Nift1Image object.
|
|
259
|
+
"""
|
|
260
|
+
_provider.VolumeProvider.__init__(self)
|
|
261
|
+
zipurl, zipped_file = src.split(" ")
|
|
262
|
+
req = requests.ZipfileRequest(zipurl, zipped_file)
|
|
263
|
+
self._img_loaders = {None: lambda req=req: req.data}
|
|
264
|
+
|
|
265
|
+
# required for self._url property
|
|
266
|
+
self._init_url = src
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Copyright 2018-2024
|
|
2
|
+
# Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
|
|
3
|
+
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from typing import TYPE_CHECKING, Union, Dict, List
|
|
20
|
+
from nibabel import Nifti1Image
|
|
21
|
+
import json
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ...locations.boundingbox import BoundingBox
|
|
24
|
+
|
|
25
|
+
# TODO add mesh primitive. Check nibabel implementation? Use trimesh? Do we want to add yet another dependency?
|
|
26
|
+
VolumeData = Union[Nifti1Image, Dict]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VolumeProvider(ABC):
|
|
30
|
+
|
|
31
|
+
_SUBCLASSES: List[VolumeProvider] = []
|
|
32
|
+
|
|
33
|
+
def __init_subclass__(cls, srctype: str) -> None:
|
|
34
|
+
cls.srctype = srctype
|
|
35
|
+
VolumeProvider._SUBCLASSES.append(cls)
|
|
36
|
+
return super().__init_subclass__()
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def get_boundingbox(self, clip=True, background=0.0) -> "BoundingBox":
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def fragments(self) -> List[str]:
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def fetch(self, *args, **kwargs) -> VolumeData:
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def _url(self) -> Union[str, Dict[str, str]]:
|
|
53
|
+
"""
|
|
54
|
+
This is needed to provide urls to applications that can utilise such resources directly.
|
|
55
|
+
e.g. siibra-api
|
|
56
|
+
"""
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SubvolumeProvider(VolumeProvider, srctype="subvolume"):
|
|
61
|
+
"""
|
|
62
|
+
This provider wraps around an existing volume provider,
|
|
63
|
+
but is preconfigured to always fetch a fixed subvolume.
|
|
64
|
+
The primary use is to provide a fixed z coordinate
|
|
65
|
+
of a 4D volume provider as a 3D volume under the
|
|
66
|
+
interface of a normal volume provider.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
_USE_CACHING = False
|
|
70
|
+
_FETCHED_VOLUMES = {}
|
|
71
|
+
|
|
72
|
+
class UseCaching:
|
|
73
|
+
def __enter__(self):
|
|
74
|
+
SubvolumeProvider._USE_CACHING = True
|
|
75
|
+
|
|
76
|
+
def __exit__(self, et, ev, tb):
|
|
77
|
+
SubvolumeProvider._USE_CACHING = False
|
|
78
|
+
SubvolumeProvider._FETCHED_VOLUMES = {}
|
|
79
|
+
|
|
80
|
+
def __init__(self, parent_provider: VolumeProvider, z: int):
|
|
81
|
+
VolumeProvider.__init__(self)
|
|
82
|
+
self.provider = parent_provider
|
|
83
|
+
self.srctype = parent_provider.srctype
|
|
84
|
+
self.z = z
|
|
85
|
+
|
|
86
|
+
def get_boundingbox(self, clip=True, background=0.0, **fetch_kwargs) -> "BoundingBox":
|
|
87
|
+
return self.provider.get_boundingbox(clip=clip, background=background, **fetch_kwargs)
|
|
88
|
+
|
|
89
|
+
def fetch(self, **kwargs):
|
|
90
|
+
# activate caching at the caller using "with SubvolumeProvider.UseCaching():""
|
|
91
|
+
if self.__class__._USE_CACHING:
|
|
92
|
+
data_key = json.dumps(self.provider._url, sort_keys=True) \
|
|
93
|
+
+ json.dumps(kwargs, sort_keys=True)
|
|
94
|
+
if data_key not in self.__class__._FETCHED_VOLUMES:
|
|
95
|
+
vol = self.provider.fetch(**kwargs)
|
|
96
|
+
self.__class__._FETCHED_VOLUMES[data_key] = vol
|
|
97
|
+
vol = self.__class__._FETCHED_VOLUMES[data_key]
|
|
98
|
+
else:
|
|
99
|
+
vol = self.provider.fetch(**kwargs)
|
|
100
|
+
return vol.slicer[:, :, :, self.z]
|
|
101
|
+
|
|
102
|
+
def __getattr__(self, attr):
|
|
103
|
+
return self.provider.__getattribute__(attr)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def _url(self) -> Union[str, Dict[str, str]]:
|
|
107
|
+
return super()._url
|