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.

Files changed (84) hide show
  1. siibra/VERSION +1 -0
  2. siibra/__init__.py +164 -0
  3. siibra/commons.py +823 -0
  4. siibra/configuration/__init__.py +17 -0
  5. siibra/configuration/configuration.py +189 -0
  6. siibra/configuration/factory.py +589 -0
  7. siibra/core/__init__.py +16 -0
  8. siibra/core/assignment.py +110 -0
  9. siibra/core/atlas.py +239 -0
  10. siibra/core/concept.py +308 -0
  11. siibra/core/parcellation.py +387 -0
  12. siibra/core/region.py +1223 -0
  13. siibra/core/space.py +131 -0
  14. siibra/core/structure.py +111 -0
  15. siibra/exceptions.py +63 -0
  16. siibra/experimental/__init__.py +19 -0
  17. siibra/experimental/contour.py +61 -0
  18. siibra/experimental/cortical_profile_sampler.py +57 -0
  19. siibra/experimental/patch.py +98 -0
  20. siibra/experimental/plane3d.py +256 -0
  21. siibra/explorer/__init__.py +17 -0
  22. siibra/explorer/url.py +222 -0
  23. siibra/explorer/util.py +87 -0
  24. siibra/features/__init__.py +117 -0
  25. siibra/features/anchor.py +224 -0
  26. siibra/features/connectivity/__init__.py +33 -0
  27. siibra/features/connectivity/functional_connectivity.py +57 -0
  28. siibra/features/connectivity/regional_connectivity.py +494 -0
  29. siibra/features/connectivity/streamline_counts.py +27 -0
  30. siibra/features/connectivity/streamline_lengths.py +27 -0
  31. siibra/features/connectivity/tracing_connectivity.py +30 -0
  32. siibra/features/dataset/__init__.py +17 -0
  33. siibra/features/dataset/ebrains.py +90 -0
  34. siibra/features/feature.py +970 -0
  35. siibra/features/image/__init__.py +27 -0
  36. siibra/features/image/image.py +115 -0
  37. siibra/features/image/sections.py +26 -0
  38. siibra/features/image/volume_of_interest.py +88 -0
  39. siibra/features/tabular/__init__.py +24 -0
  40. siibra/features/tabular/bigbrain_intensity_profile.py +77 -0
  41. siibra/features/tabular/cell_density_profile.py +298 -0
  42. siibra/features/tabular/cortical_profile.py +322 -0
  43. siibra/features/tabular/gene_expression.py +257 -0
  44. siibra/features/tabular/layerwise_bigbrain_intensities.py +62 -0
  45. siibra/features/tabular/layerwise_cell_density.py +95 -0
  46. siibra/features/tabular/receptor_density_fingerprint.py +192 -0
  47. siibra/features/tabular/receptor_density_profile.py +110 -0
  48. siibra/features/tabular/regional_timeseries_activity.py +294 -0
  49. siibra/features/tabular/tabular.py +139 -0
  50. siibra/livequeries/__init__.py +19 -0
  51. siibra/livequeries/allen.py +352 -0
  52. siibra/livequeries/bigbrain.py +197 -0
  53. siibra/livequeries/ebrains.py +145 -0
  54. siibra/livequeries/query.py +49 -0
  55. siibra/locations/__init__.py +91 -0
  56. siibra/locations/boundingbox.py +454 -0
  57. siibra/locations/location.py +115 -0
  58. siibra/locations/point.py +344 -0
  59. siibra/locations/pointcloud.py +349 -0
  60. siibra/retrieval/__init__.py +27 -0
  61. siibra/retrieval/cache.py +233 -0
  62. siibra/retrieval/datasets.py +389 -0
  63. siibra/retrieval/exceptions/__init__.py +27 -0
  64. siibra/retrieval/repositories.py +769 -0
  65. siibra/retrieval/requests.py +659 -0
  66. siibra/vocabularies/__init__.py +45 -0
  67. siibra/vocabularies/gene_names.json +29176 -0
  68. siibra/vocabularies/receptor_symbols.json +210 -0
  69. siibra/vocabularies/region_aliases.json +460 -0
  70. siibra/volumes/__init__.py +23 -0
  71. siibra/volumes/parcellationmap.py +1279 -0
  72. siibra/volumes/providers/__init__.py +20 -0
  73. siibra/volumes/providers/freesurfer.py +113 -0
  74. siibra/volumes/providers/gifti.py +165 -0
  75. siibra/volumes/providers/neuroglancer.py +736 -0
  76. siibra/volumes/providers/nifti.py +266 -0
  77. siibra/volumes/providers/provider.py +107 -0
  78. siibra/volumes/sparsemap.py +468 -0
  79. siibra/volumes/volume.py +892 -0
  80. siibra-1.0.0a1.dist-info/LICENSE +201 -0
  81. siibra-1.0.0a1.dist-info/METADATA +160 -0
  82. siibra-1.0.0a1.dist-info/RECORD +84 -0
  83. siibra-1.0.0a1.dist-info/WHEEL +5 -0
  84. siibra-1.0.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,454 @@
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
+ """A box defined by two farthest corner coordinates on a specific space."""
16
+
17
+ from . import point, pointcloud, location
18
+
19
+ from ..commons import logger
20
+ from ..exceptions import SpaceWarpingFailedError
21
+
22
+ from itertools import product
23
+ import hashlib
24
+ import numpy as np
25
+ from typing import TYPE_CHECKING, Union
26
+ if TYPE_CHECKING:
27
+ from ..core.structure import BrainStructure
28
+ from nibabel import Nifti1Image
29
+ from ..core.space import Space
30
+
31
+
32
+ class BoundingBox(location.Location):
33
+ """
34
+ A 3D axis-aligned bounding box spanned by two 3D corner points.
35
+ The box does not necessarily store the given points,
36
+ instead it computes the real minimum and maximum points
37
+ from the two corner points.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ point1,
43
+ point2,
44
+ space: Union[str, 'Space'] = None,
45
+ minsize: float = None,
46
+ sigma_mm=None
47
+ ):
48
+ """
49
+ Construct a new bounding box spanned by two 3D coordinates
50
+ in the given reference space.
51
+
52
+ Parameters
53
+ ----------
54
+ point1 : Point or 3-tuple
55
+ Startpoint given in mm of the given space
56
+ point2 : Point or 3-tuple
57
+ Endpoint given in mm of the given space
58
+ space : reference space (id, name, or Space)
59
+ The reference space
60
+ minsize : float
61
+ Minimum size along each dimension. If not None, the maxpoint will
62
+ be adjusted to match the minimum size, if needed.
63
+ sigma_mm : float, or list of float
64
+ Optional standard deviation of the spanning point locations.
65
+ """
66
+ # TODO: allow to pass sigma for the points, if tuples
67
+ location.Location.__init__(self, space)
68
+ xyz1 = point.Point.parse(point1)
69
+ xyz2 = point.Point.parse(point2)
70
+ if sigma_mm is None:
71
+ s1, s2 = 0, 0
72
+ elif isinstance(sigma_mm, float):
73
+ s1, s2 = sigma_mm, sigma_mm
74
+ elif isinstance(sigma_mm, list):
75
+ assert len(sigma_mm) == 2
76
+ s1, s2 = sigma_mm
77
+ else:
78
+ raise ValueError(f"Cannot interpret sigma_mm parameter value {sigma_mm} for bounding box")
79
+ self.sigma_mm = [s1, s2]
80
+ self.minpoint = point.Point([min(xyz1[i], xyz2[i]) for i in range(3)], space, sigma_mm=s1)
81
+ self.maxpoint = point.Point([max(xyz1[i], xyz2[i]) for i in range(3)], space, sigma_mm=s2)
82
+ if minsize is not None:
83
+ for d in range(3):
84
+ if self.shape[d] < minsize:
85
+ self.maxpoint[d] = self.minpoint[d] + minsize
86
+
87
+ if self.volume == 0:
88
+ logger.warning(f"Zero-volume bounding box from points {point1} and {point2} in {self.space} space.")
89
+
90
+ @property
91
+ def id(self) -> str:
92
+ return hashlib.md5(str(self).encode("utf-8")).hexdigest()
93
+
94
+ @property
95
+ def volume(self) -> float:
96
+ """The volume of the boundingbox in mm^3"""
97
+ return np.prod(self.shape)
98
+
99
+ @property
100
+ def center(self) -> 'point.Point':
101
+ return self.minpoint + (self.maxpoint - self.minpoint) / 2
102
+
103
+ @property
104
+ def shape(self) -> float:
105
+ """The distances of the diagonal points in each axis. (Accounts for sigma)."""
106
+ return tuple(
107
+ (self.maxpoint + self.maxpoint.sigma)
108
+ - (self.minpoint - self.minpoint.sigma)
109
+ )
110
+
111
+ @property
112
+ def is_planar(self) -> bool:
113
+ return any(d == 0 for d in self.shape)
114
+
115
+ def __str__(self):
116
+ if self.space is None:
117
+ return (
118
+ f"Bounding box from ({','.join(f'{v:.2f}' for v in self.minpoint)})mm "
119
+ f"to ({','.join(f'{v:.2f}' for v in self.maxpoint)})mm"
120
+ )
121
+ else:
122
+ return (
123
+ f"Bounding box from ({','.join(f'{v:.2f}' for v in self.minpoint)})mm "
124
+ f"to ({','.join(f'{v:.2f}' for v in self.maxpoint)})mm in {self.space.name} space"
125
+ )
126
+
127
+ def intersection(self, other: 'BrainStructure', dims=[0, 1, 2]):
128
+ """Computes the intersection of this bounding box with another one.
129
+
130
+ Parameters
131
+ ----------
132
+ other: BrainStructure
133
+ dims: List[int], default: all three
134
+ Dimensions where the intersection should be computed
135
+ (applies only to bounding boxes). Along dimensions not listed,
136
+ the union is applied instead.
137
+ """
138
+ # TODO: process the sigma values o the points
139
+ if isinstance(other, BoundingBox):
140
+ try:
141
+ return self._intersect_bbox(other, dims)
142
+ except SpaceWarpingFailedError:
143
+ return other._intersect_bbox(self, dims) # TODO: check this mechanism carefully
144
+ if isinstance(other, point.Point):
145
+ warped = other.warp(self.space)
146
+ return other if self.minpoint <= warped <= self.maxpoint else None
147
+ if isinstance(other, pointcloud.PointCloud):
148
+ points_inside = [p for p in other if self.intersects(p)]
149
+ if len(points_inside) == 0:
150
+ return None
151
+ result = pointcloud.PointCloud(
152
+ points_inside,
153
+ space=other.space,
154
+ sigma_mm=[p.sigma for p in points_inside]
155
+ )
156
+ return result[0] if len(result) == 1 else result # if PointCloud has single point return as a Point
157
+
158
+ return other.intersection(self)
159
+
160
+ def _intersect_bbox(self, other: 'BoundingBox', dims=[0, 1, 2]):
161
+ warped = other.warp(self.space)
162
+
163
+ # Determine the intersecting bounsding box by sorting
164
+ # the coordinates of both bounding boxes for each dimension,
165
+ # and fetching the second and third coordinate after sorting.
166
+ # If those belong to a minimum and maximum point,
167
+ # no matter of which bounding box,
168
+ # we have a nonzero intersection in that dimension.
169
+ minpoints = [b.minpoint for b in (self, warped)]
170
+ maxpoints = [b.maxpoint for b in (self, warped)]
171
+ allpoints = minpoints + maxpoints
172
+ result_minpt = []
173
+ result_maxpt = []
174
+
175
+ for dim in range(3):
176
+
177
+ if dim not in dims:
178
+ # do not intersect in this dimension, so take the union instead
179
+ result_minpt.append(min(p[dim] for p in allpoints))
180
+ result_maxpt.append(max(p[dim] for p in allpoints))
181
+ continue
182
+
183
+ A, B = sorted(allpoints, key=lambda P: P[dim])[1:3]
184
+ if (A in maxpoints) or (B in minpoints):
185
+ # no intersection in this dimension
186
+ return None
187
+ else:
188
+ result_minpt.append(A[dim])
189
+ result_maxpt.append(B[dim])
190
+
191
+ if result_minpt == result_maxpt:
192
+ return result_minpt
193
+
194
+ bbox = BoundingBox(
195
+ point1=point.Point(result_minpt, self.space),
196
+ point2=point.Point(result_maxpt, self.space),
197
+ space=self.space,
198
+ )
199
+
200
+ if bbox.volume == 0 and sum(cmin == cmax for cmin, cmax in zip(result_minpt, result_maxpt)) == 2:
201
+ return None
202
+ return bbox
203
+
204
+ def _intersect_mask(self, mask: 'Nifti1Image', threshold=0):
205
+ """Intersect this bounding box with an image mask. Returns None if they do not intersect.
206
+
207
+ TODO process the sigma values o the points
208
+
209
+ NOTE: The affine matrix of the image must be set to warp voxels
210
+ coordinates into the reference space of this Bounding Box.
211
+ """
212
+ # nonzero voxel coordinates
213
+ X, Y, Z = np.where(mask.get_fdata() > threshold)
214
+ h = np.ones(len(X))
215
+
216
+ # array of homogenous physical nonzero voxel coordinates
217
+ coords = np.dot(mask.affine, np.vstack((X, Y, Z, h)))[:3, :].T
218
+ minpoint = [min(self.minpoint[i], self.maxpoint[i]) for i in range(3)]
219
+ maxpoint = [max(self.minpoint[i], self.maxpoint[i]) for i in range(3)]
220
+ minpt_voxel = np.dot(np.linalg.inv(mask.affine), np.r_[minpoint, 1])[:3]
221
+ voxel_size = np.diff(
222
+ np.dot(
223
+ mask.affine,
224
+ np.vstack((np.r_[minpt_voxel, 1], np.r_[minpt_voxel + 1, 1])).T
225
+ )[:3, :]
226
+ ).squeeze()
227
+ # use voxel size for the test to intersect voxels properly with verythin bounding boxes
228
+ inside = np.logical_and.reduce([
229
+ coords >= minpoint - voxel_size / 2,
230
+ coords <= maxpoint + voxel_size / 2
231
+ ]).min(1)
232
+ XYZ = coords[inside, :3]
233
+ if XYZ.shape[0] == 0:
234
+ return None
235
+ elif XYZ.shape[0] == 1: # reflect voxel size in resulting point set
236
+ return self._intersect_bbox(
237
+ point
238
+ .Point(XYZ.flatten(), space=self.space, sigma_mm=voxel_size.max())
239
+ .boundingbox
240
+ )
241
+ else:
242
+ return self._intersect_bbox(
243
+ pointcloud
244
+ .PointCloud(XYZ, space=self.space, sigma_mm=voxel_size.max())
245
+ .boundingbox
246
+ )
247
+
248
+ def clip(self, xyzmax, xyzmin=(0, 0, 0)):
249
+ """
250
+ Returns a new bounding box obtained by clipping at the given maximum coordinate.
251
+ """
252
+ return self.intersection(
253
+ BoundingBox(
254
+ point.Point(xyzmin, self.space), point.Point(xyzmax, self.space), self.space
255
+ ),
256
+ sigma_mm=[self.minpoint.sigma, self.maxpoint.sigma]
257
+ )
258
+
259
+ @property
260
+ def corners(self):
261
+ """
262
+ Returns all 8 corners of the box as a pointcloud.
263
+
264
+ Note
265
+ ----
266
+ x0, y0, z0 = self.minpoint
267
+ x1, y1, z1 = self.maxpoint
268
+ all_corners = [
269
+ (x0, y0, z0),
270
+ (x1, y0, z0),
271
+ (x0, y1, z0),
272
+ (x1, y1, z0),
273
+ (x0, y0, z1),
274
+ (x1, y0, z1),
275
+ (x0, y1, z1),
276
+ (x1, y1, z1)
277
+ ]
278
+
279
+ TODO: deal with sigma. Currently, returns the mean of min and max point.
280
+ """
281
+ xs, ys, zs = zip(self.minpoint, self.maxpoint)
282
+ return pointcloud.PointCloud(
283
+ coordinates=[[x, y, z] for x, y, z in product(xs, ys, zs)],
284
+ space=self.space,
285
+ sigma_mm=np.mean([self.minpoint.sigma, self.maxpoint.sigma])
286
+ )
287
+
288
+ def warp(self, space):
289
+ """Returns a new bounding box obtained by warping the
290
+ min- and maxpoint of this one into the new target space.
291
+
292
+ TODO process the sigma values o the points
293
+ """
294
+ from ..core.space import Space
295
+ spaceobj = Space.get_instance(space)
296
+ if spaceobj == self.space:
297
+ return self
298
+ else:
299
+ try:
300
+ warped_corners = self.corners.warp(spaceobj)
301
+ except SpaceWarpingFailedError:
302
+ raise SpaceWarpingFailedError(f"Warping {str(self)} to {spaceobj.name} not successful.")
303
+ return warped_corners.boundingbox
304
+
305
+ def transform(self, affine: np.ndarray, space=None):
306
+ """Returns a new bounding box obtained by transforming the
307
+ min- and maxpoint of this one with the given affine matrix.
308
+
309
+ TODO process the sigma values o the points
310
+
311
+ Parameters
312
+ ----------
313
+ affine : numpy 4x4 ndarray
314
+ affine matrix
315
+ space : reference space (str, Space, or None)
316
+ Target reference space which is reached after
317
+ applying the transform. Note that the consistency
318
+ of this cannot be checked and is up to the user.
319
+ """
320
+ from ..core.space import Space
321
+ spaceobj = Space.get_instance(space)
322
+ result = self.corners.transform(affine, spaceobj).boundingbox
323
+ result.sigma_mm = [self.minpoint.sigma, self.maxpoint.sigma] # TODO: error propagation
324
+ return result
325
+
326
+ def shift(self, offset):
327
+ return self.__class__(
328
+ point1=self.minpoint + offset,
329
+ point2=self.maxpoint + offset,
330
+ space=self.space,
331
+ sigma_mm=[self.minpoint.sigma, self.maxpoint.sigma]
332
+ )
333
+
334
+ def zoom(self, ratio: float):
335
+ """
336
+ Create a new bounding box by zooming this one around its center.
337
+ """
338
+ c = self.center
339
+ self.maxpoint - c
340
+ return self.__class__(
341
+ point1=(self.minpoint - c) * ratio + c,
342
+ point2=(self.maxpoint - c) * ratio + c,
343
+ space=self.space,
344
+ sigma_mm=[self.minpoint.sigma * ratio, self.maxpoint.sigma * ratio]
345
+ )
346
+
347
+ def estimate_affine(self, space):
348
+ """
349
+ Computes an affine transform which approximates
350
+ the nonlinear warping of the eight corner points
351
+ to the desired target space.
352
+ The transform is estimated using a least squares
353
+ solution to A*x = b, where A is the matrix of
354
+ point coefficients in the space of this bounding box,
355
+ and b are the target coefficients in the given space
356
+ after calling the nonlinear warping.
357
+ """
358
+
359
+ x0, y0, z0 = self.minpoint
360
+ x1, y1, z1 = self.maxpoint
361
+
362
+ # set of 8 corner points in source space
363
+ corners1 = pointcloud.PointCloud(
364
+ [
365
+ (x0, y0, z0),
366
+ (x0, y0, z1),
367
+ (x0, y1, z0),
368
+ (x0, y1, z1),
369
+ (x1, y0, z0),
370
+ (x1, y0, z1),
371
+ (x1, y1, z0),
372
+ (x1, y1, z1)
373
+ ], self.space
374
+ )
375
+
376
+ # coefficient matrix from original points
377
+ A = np.hstack(
378
+ [
379
+ [np.kron(np.eye(3), np.r_[tuple(c), 1])]
380
+ for c in corners1
381
+ ]).squeeze()
382
+
383
+ # righthand side from warped points
384
+ corners2 = corners1.warp(space)
385
+ b = np.hstack(corners2.as_list())
386
+
387
+ # least squares solution
388
+ x, res, rank, s = np.linalg.lstsq(A, b, rcond=None)
389
+ affine = np.vstack([x.reshape((3, 4)), np.array([0, 0, 0, 1])])
390
+
391
+ # test
392
+ errors = []
393
+ for c1, c2 in zip(list(corners1), list(corners2)):
394
+ errors.append(
395
+ np.linalg.norm(
396
+ np.dot(affine, np.r_[tuple(c1), 1])
397
+ - np.r_[tuple(c2), 1]
398
+ )
399
+ )
400
+ logger.debug(
401
+ f"Average projection error under linear approximation "
402
+ f"was {np.mean(errors):.2f} pixel"
403
+ )
404
+
405
+ return affine
406
+
407
+ def __iter__(self):
408
+ """Iterate the min- and maxpoint of this bounding box."""
409
+ return iter((self.minpoint, self.maxpoint))
410
+
411
+ def __eq__(self, other: 'BoundingBox'):
412
+ if not isinstance(other, BoundingBox):
413
+ return False
414
+ return self.minpoint == other.minpoint and self.maxpoint == other.maxpoint
415
+
416
+ def __hash__(self):
417
+ return super().__hash__()
418
+
419
+
420
+ def _determine_bounds(masked_array: np.ndarray, background: float = 0.0):
421
+ """
422
+ Bounding box of nonzero (background) values in a 3D array.
423
+
424
+ https://stackoverflow.com/questions/31400769/bounding-box-of-numpy-array
425
+ """
426
+ x = np.any(masked_array != background, axis=(1, 2))
427
+ y = np.any(masked_array != background, axis=(0, 2))
428
+ z = np.any(masked_array != background, axis=(0, 1))
429
+ nzx, nzy, nzz = [np.where(v) for v in (x, y, z)]
430
+ if any(len(nz[0]) == 0 for nz in [nzx, nzy, nzz]):
431
+ # empty array
432
+ return None
433
+ xmin, xmax = nzx[0][[0, -1]]
434
+ ymin, ymax = nzy[0][[0, -1]]
435
+ zmin, zmax = nzz[0][[0, -1]]
436
+ return np.array([[xmin, xmax + 1], [ymin, ymax + 1], [zmin, zmax + 1], [1, 1]])
437
+
438
+
439
+ def from_array(
440
+ array: np.ndarray,
441
+ background: Union[int, float] = 0.0,
442
+ space: "Space" = None
443
+ ) -> BoundingBox:
444
+ """
445
+ Find the bounding box of non-background values for any 3D array.
446
+
447
+ Parameters
448
+ ----------
449
+ array: np.ndarray
450
+ background: int or float, default: 0.0
451
+ space: Space, default: None
452
+ """
453
+ bounds = _determine_bounds(array, background)
454
+ return BoundingBox(point1=bounds[:3, 0], point2=bounds[:3, 1], space=space)
@@ -0,0 +1,115 @@
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
+ """Concepts that have primarily spatial meaning."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from ..core.structure import BrainStructure
20
+
21
+ import numpy as np
22
+ from abc import abstractmethod
23
+
24
+ from typing import TYPE_CHECKING, Union, Dict
25
+ if TYPE_CHECKING:
26
+ from siibra.core.space import Space
27
+
28
+
29
+ class Location(BrainStructure):
30
+ """
31
+ Abstract base class for locations in a given reference space.
32
+ """
33
+
34
+ # backend for transforming coordinates between spaces
35
+ SPACEWARP_SERVER = "https://hbp-spatial-backend.apps.hbp.eu/v1"
36
+
37
+ # lookup of space identifiers to be used by SPACEWARP_SERVER
38
+ SPACEWARP_IDS = {
39
+ "minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2": "MNI 152 ICBM 2009c Nonlinear Asymmetric",
40
+ "minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992": "MNI Colin 27",
41
+ "minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588": "Big Brain (Histology)",
42
+ }
43
+
44
+ # The id of BigBrain reference space
45
+ BIGBRAIN_ID = "minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588"
46
+ _MASK_MEMO = {} # cache region masks for Location._assign_region()
47
+ _ASSIGNMENT_CACHE = {} # caches assignment results, see Region.assign()
48
+
49
+ def __init__(self, spacespec: Union[str, Dict[str, str], "Space"]):
50
+ self._space_spec = spacespec
51
+ self._space_cached = None
52
+
53
+ @property
54
+ def space(self):
55
+ if self._space_cached is None:
56
+ from ..core.space import Space
57
+ if isinstance(self._space_spec, dict):
58
+ spec = self._space_spec.get("@id") or self._space_spec.get("name")
59
+ self._space_cached = Space.get_instance(spec)
60
+ else:
61
+ self._space_cached = Space.get_instance(self._space_spec)
62
+ return self._space_cached
63
+
64
+ @abstractmethod
65
+ def warp(self, space):
66
+ """Generates a new location by warping the
67
+ current one into another reference space."""
68
+ pass
69
+
70
+ @abstractmethod
71
+ def transform(self, affine: np.ndarray, space=None):
72
+ """Returns a new location obtained by transforming the
73
+ reference coordinates of this one with the given affine matrix.
74
+
75
+ Parameters
76
+ ----------
77
+ affine : numpy 4x4 ndarray
78
+ affine matrix
79
+ space : reference space (id, name, or Space)
80
+ Target reference space which is reached after
81
+ applying the transform. Note that the consistency
82
+ of this cannot be checked and is up to the user.
83
+ """
84
+ pass
85
+
86
+ @property
87
+ def species(self):
88
+ return None if self.space is None else self.space.species
89
+
90
+ def __str__(self):
91
+ space_str = "" if self.space is None else f" in {self.space.name}"
92
+ coord_str = "" if len(self) == 0 else f" [{','.join(str(pt) for pt in iter(self))}]"
93
+ return f"{self.__class__.__name__}{space_str}{coord_str}"
94
+
95
+ def __repr__(self):
96
+ spacespec = f"'{self.space.id}'" if self.space else None
97
+ return f"<{self.__class__.__name__}({[point.__repr__() for point in self]}), space={spacespec}>"
98
+
99
+ def __hash__(self) -> int:
100
+ return hash(self.__repr__())
101
+
102
+ @abstractmethod
103
+ def __eq__(self):
104
+ """Required to provide comparison and making the object hashable"""
105
+ raise NotImplementedError
106
+
107
+ @staticmethod
108
+ def union(loc0: 'Location', loc1: 'Location') -> 'Location':
109
+ """
110
+ Reassigned at the locations module level for static typing and to avoid
111
+ circular imports. See siibra.locations.__init__.reassign_union()
112
+ """
113
+ raise NotImplementedError(
114
+ "This method is designed to be reassigned at the module level"
115
+ )