siibra 1.0a19__py3-none-any.whl → 1.0.1a1__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 (82) hide show
  1. siibra/VERSION +1 -1
  2. siibra/__init__.py +7 -7
  3. siibra/commons.py +8 -53
  4. siibra/configuration/__init__.py +1 -1
  5. siibra/configuration/configuration.py +1 -1
  6. siibra/configuration/factory.py +11 -21
  7. siibra/core/__init__.py +1 -1
  8. siibra/core/assignment.py +1 -1
  9. siibra/core/atlas.py +21 -15
  10. siibra/core/concept.py +3 -3
  11. siibra/core/parcellation.py +69 -54
  12. siibra/core/region.py +178 -158
  13. siibra/core/space.py +1 -1
  14. siibra/core/structure.py +2 -2
  15. siibra/exceptions.py +13 -1
  16. siibra/experimental/__init__.py +1 -1
  17. siibra/experimental/contour.py +8 -8
  18. siibra/experimental/cortical_profile_sampler.py +1 -1
  19. siibra/experimental/patch.py +3 -3
  20. siibra/experimental/plane3d.py +12 -12
  21. siibra/explorer/__init__.py +1 -1
  22. siibra/explorer/url.py +2 -2
  23. siibra/explorer/util.py +1 -1
  24. siibra/features/__init__.py +1 -1
  25. siibra/features/anchor.py +14 -15
  26. siibra/features/connectivity/__init__.py +1 -1
  27. siibra/features/connectivity/functional_connectivity.py +1 -1
  28. siibra/features/connectivity/regional_connectivity.py +4 -4
  29. siibra/features/connectivity/streamline_counts.py +1 -1
  30. siibra/features/connectivity/streamline_lengths.py +1 -1
  31. siibra/features/connectivity/tracing_connectivity.py +1 -1
  32. siibra/features/dataset/__init__.py +1 -1
  33. siibra/features/dataset/ebrains.py +1 -1
  34. siibra/features/feature.py +24 -26
  35. siibra/features/image/__init__.py +1 -1
  36. siibra/features/image/image.py +2 -2
  37. siibra/features/image/sections.py +1 -1
  38. siibra/features/image/volume_of_interest.py +1 -1
  39. siibra/features/tabular/__init__.py +1 -1
  40. siibra/features/tabular/bigbrain_intensity_profile.py +2 -2
  41. siibra/features/tabular/cell_density_profile.py +98 -64
  42. siibra/features/tabular/cortical_profile.py +3 -3
  43. siibra/features/tabular/gene_expression.py +1 -1
  44. siibra/features/tabular/layerwise_bigbrain_intensities.py +1 -1
  45. siibra/features/tabular/layerwise_cell_density.py +4 -23
  46. siibra/features/tabular/receptor_density_fingerprint.py +13 -10
  47. siibra/features/tabular/receptor_density_profile.py +1 -1
  48. siibra/features/tabular/regional_timeseries_activity.py +4 -4
  49. siibra/features/tabular/tabular.py +7 -5
  50. siibra/livequeries/__init__.py +1 -1
  51. siibra/livequeries/allen.py +42 -19
  52. siibra/livequeries/bigbrain.py +21 -12
  53. siibra/livequeries/ebrains.py +1 -1
  54. siibra/livequeries/query.py +2 -3
  55. siibra/locations/__init__.py +11 -11
  56. siibra/locations/boundingbox.py +30 -29
  57. siibra/locations/location.py +1 -1
  58. siibra/locations/point.py +7 -7
  59. siibra/locations/{pointset.py → pointcloud.py} +36 -33
  60. siibra/retrieval/__init__.py +1 -1
  61. siibra/retrieval/cache.py +1 -1
  62. siibra/retrieval/datasets.py +4 -4
  63. siibra/retrieval/exceptions/__init__.py +1 -1
  64. siibra/retrieval/repositories.py +13 -30
  65. siibra/retrieval/requests.py +25 -8
  66. siibra/vocabularies/__init__.py +1 -1
  67. siibra/volumes/__init__.py +2 -2
  68. siibra/volumes/parcellationmap.py +119 -91
  69. siibra/volumes/providers/__init__.py +1 -1
  70. siibra/volumes/providers/freesurfer.py +3 -3
  71. siibra/volumes/providers/gifti.py +1 -1
  72. siibra/volumes/providers/neuroglancer.py +67 -41
  73. siibra/volumes/providers/nifti.py +12 -26
  74. siibra/volumes/providers/provider.py +1 -1
  75. siibra/volumes/sparsemap.py +125 -246
  76. siibra/volumes/volume.py +150 -61
  77. {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/METADATA +26 -4
  78. siibra-1.0.1a1.dist-info/RECORD +84 -0
  79. {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/WHEEL +1 -1
  80. siibra-1.0a19.dist-info/RECORD +0 -84
  81. {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/LICENSE +0 -0
  82. {siibra-1.0a19.dist-info → siibra-1.0.1a1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,14 +12,14 @@
12
12
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
- """Matches BigBrain intesity profiles extracted by Wagstyl et al. to volumes."""
15
+ """Matches BigBrain intensity profiles extracted by Wagstyl et al. to volumes."""
16
16
 
17
17
  from . import query
18
18
 
19
19
  from ..features.tabular import bigbrain_intensity_profile, layerwise_bigbrain_intensities
20
20
  from ..features import anchor as _anchor
21
21
  from ..commons import logger
22
- from ..locations import point, pointset
22
+ from ..locations import point, pointcloud, location
23
23
  from ..core import structure
24
24
  from ..retrieval import requests, cache
25
25
  from ..retrieval.datasets import GenericDataset
@@ -119,15 +119,20 @@ class BigBrainProfileQuery(query.LiveQuery, args=[], FeatureType=bigbrain_intens
119
119
 
120
120
  def query(self, concept: structure.BrainStructure, **kwargs) -> List[bigbrain_intensity_profile.BigBrainIntensityProfile]:
121
121
  loader = WagstylProfileLoader()
122
- mesh_vertices = pointset.PointSet(loader._vertices, space='bigbrain')
123
- matched = concept.intersection(mesh_vertices) # returns a reduced PointSet with og indices as labels
122
+ mesh_vertices = pointcloud.PointCloud(loader._vertices, space='bigbrain')
123
+ matched = concept.intersection(mesh_vertices) # returns a reduced PointCloud with og indices as labels
124
124
  if matched is None:
125
125
  return []
126
- assert isinstance(matched, pointset.PointSet)
126
+ if isinstance(matched, point.Point):
127
+ matched = pointcloud.from_points([matched])
128
+ assert isinstance(matched, pointcloud.PointCloud)
129
+ if isinstance(concept, location.Location):
130
+ mesh_as_list = mesh_vertices.as_list()
131
+ matched.labels = [mesh_as_list.index(v.coordinate) for v in matched]
127
132
  indices = matched.labels
128
133
  assert indices is not None
129
134
  features = []
130
- for i in matched.labels:
135
+ for i in indices:
131
136
  anchor = _anchor.AnatomicalAnchor(
132
137
  location=point.Point(loader._vertices[i], space='bigbrain'),
133
138
  region=str(concept),
@@ -159,13 +164,17 @@ class LayerwiseBigBrainIntensityQuery(query.LiveQuery, args=[], FeatureType=laye
159
164
  def query(self, concept: structure.BrainStructure, **kwargs) -> List[layerwise_bigbrain_intensities.LayerwiseBigBrainIntensities]:
160
165
 
161
166
  loader = WagstylProfileLoader()
162
- mesh_vertices = pointset.PointSet(loader._vertices, space='bigbrain')
163
- matched = concept.intersection(mesh_vertices) # returns a reduced PointSet with og indices as labels
167
+ mesh_vertices = pointcloud.PointCloud(loader._vertices, space='bigbrain')
168
+ matched = concept.intersection(mesh_vertices) # returns a reduced PointCloud with og indices as labels if the concept is a region
164
169
  if matched is None:
165
170
  return []
166
- assert isinstance(matched, pointset.PointSet)
171
+ if isinstance(matched, point.Point):
172
+ matched = pointcloud.from_points([matched])
173
+ assert isinstance(matched, pointcloud.PointCloud)
174
+ if isinstance(concept, location.Location):
175
+ mesh_as_list = mesh_vertices.as_list()
176
+ matched.labels = [mesh_as_list.index(v.coordinate) for v in matched]
167
177
  indices = matched.labels
168
- assert indices is not None
169
178
  matched_profiles = loader._profiles[indices, :]
170
179
  boundary_depths = loader._boundary_depths[indices, :]
171
180
  # compute array of layer labels for all coefficients in profiles_left
@@ -177,7 +186,7 @@ class LayerwiseBigBrainIntensityQuery(query.LiveQuery, args=[], FeatureType=laye
177
186
  ]).reshape((-1, 200))
178
187
 
179
188
  anchor = _anchor.AnatomicalAnchor(
180
- location=pointset.PointSet(loader._vertices[indices, :], space='bigbrain'),
189
+ location=pointcloud.PointCloud(loader._vertices[indices, :], space='bigbrain'),
181
190
  region=str(concept),
182
191
  species='Homo sapiens'
183
192
  )
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -46,5 +46,4 @@ class LiveQuery(ABC):
46
46
 
47
47
  @abstractmethod
48
48
  def query(self, concept: AtlasConcept, **kwargs) -> List[Feature]:
49
- raise NotImplementedError(f"Dervied class {self.__class__} needs to implement query()")
50
- pass
49
+ raise NotImplementedError(f"Derived class {self.__class__} needs to implement query()")
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,13 +16,13 @@
16
16
 
17
17
  from .location import Location
18
18
  from .point import Point
19
- from .pointset import PointSet, from_points
19
+ from .pointcloud import PointCloud, from_points
20
20
  from .boundingbox import BoundingBox
21
21
 
22
22
 
23
23
  def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
24
24
  """
25
- Add two locations of same or diffrent type to find their union as a
25
+ Add two locations of same or different type to find their union as a
26
26
  Location object.
27
27
  Note
28
28
  ----
@@ -36,11 +36,11 @@ def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
36
36
  Returns
37
37
  -------
38
38
  Location
39
- - Point U Point = PointSet
40
- - Point U PointSet = PointSet
41
- - PointSet U PointSet = PointSet
39
+ - Point U Point = PointCloud
40
+ - Point U PointCloud = PointCloud
41
+ - PointCloud U PointCloud = PointCloud
42
42
  - BoundingBox U BoundingBox = BoundingBox
43
- - BoundingBox U PointSet = BoundingBox
43
+ - BoundingBox U PointCloud = BoundingBox
44
44
  - BoundingBox U Point = BoundingBox
45
45
  - WholeBrain U Location = NotImplementedError
46
46
  (all operations are commutative)
@@ -53,14 +53,14 @@ def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
53
53
  # location that has its own union method since it is not a part of locations
54
54
  # module and to avoid importing Volume here.
55
55
  if not all(
56
- isinstance(loc, (Point, PointSet, BoundingBox)) for loc in [loc0, loc1]
56
+ isinstance(loc, (Point, PointCloud, BoundingBox)) for loc in [loc0, loc1]
57
57
  ):
58
58
  try:
59
59
  return loc1.union(loc0)
60
60
  except Exception:
61
61
  raise NotImplementedError(f"There are no union method for {(loc0.__class__.__name__, loc1.__class__.__name__)}")
62
62
 
63
- # convert Points to PointSets
63
+ # convert Points to PointClouds
64
64
  loc0, loc1 = [
65
65
  from_points([loc]) if isinstance(loc, Point) else loc
66
66
  for loc in [loc0, loc1]
@@ -69,8 +69,8 @@ def reassign_union(loc0: 'Location', loc1: 'Location') -> 'Location':
69
69
  # adopt the space of the first location
70
70
  loc1_w = loc1.warp(loc0.space)
71
71
 
72
- if isinstance(loc0, PointSet):
73
- if isinstance(loc1_w, PointSet):
72
+ if isinstance(loc0, PointCloud):
73
+ if isinstance(loc1_w, PointCloud):
74
74
  points = list(dict.fromkeys([*loc0, *loc1_w]))
75
75
  return from_points(points)
76
76
  if isinstance(loc1_w, BoundingBox):
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +14,7 @@
14
14
  # limitations under the License.
15
15
  """A box defined by two farthest corner coordinates on a specific space."""
16
16
 
17
- from . import point, pointset, location
17
+ from . import point, pointcloud, location
18
18
 
19
19
  from ..commons import logger
20
20
  from ..exceptions import SpaceWarpingFailedError
@@ -85,7 +85,7 @@ class BoundingBox(location.Location):
85
85
  self.maxpoint[d] = self.minpoint[d] + minsize
86
86
 
87
87
  if self.volume == 0:
88
- logger.warning("Created BoundedBox's have zero volume.")
88
+ logger.warning(f"Zero-volume bounding box from points {point1} and {point2} in {self.space} space.")
89
89
 
90
90
  @property
91
91
  def id(self) -> str:
@@ -144,16 +144,12 @@ class BoundingBox(location.Location):
144
144
  if isinstance(other, point.Point):
145
145
  warped = other.warp(self.space)
146
146
  return other if self.minpoint <= warped <= self.maxpoint else None
147
- if isinstance(other, pointset.PointSet):
147
+ if isinstance(other, pointcloud.PointCloud):
148
148
  points_inside = [p for p in other if self.intersects(p)]
149
- result = pointset.PointSet(
150
- points_inside,
151
- space=other.space,
152
- sigma_mm=[p.sigma for p in points_inside]
153
- )
154
- if len(result) == 0:
149
+ if len(points_inside) == 0:
155
150
  return None
156
- return result[0] if len(result) == 1 else result # if PointSet has single point return as a Point
151
+ result = pointcloud.from_points(points_inside)
152
+ return result[0] if len(result) == 1 else result # if PointCloud has single point return as a Point
157
153
 
158
154
  return other.intersection(self)
159
155
 
@@ -213,7 +209,7 @@ class BoundingBox(location.Location):
213
209
  X, Y, Z = np.where(mask.get_fdata() > threshold)
214
210
  h = np.ones(len(X))
215
211
 
216
- # array of homogenous physical nonzero voxel coordinates
212
+ # array of homogeneous physical nonzero voxel coordinates
217
213
  coords = np.dot(mask.affine, np.vstack((X, Y, Z, h)))[:3, :].T
218
214
  minpoint = [min(self.minpoint[i], self.maxpoint[i]) for i in range(3)]
219
215
  maxpoint = [max(self.minpoint[i], self.maxpoint[i]) for i in range(3)]
@@ -240,8 +236,8 @@ class BoundingBox(location.Location):
240
236
  )
241
237
  else:
242
238
  return self._intersect_bbox(
243
- pointset
244
- .PointSet(XYZ, space=self.space, sigma_mm=voxel_size.max())
239
+ pointcloud
240
+ .PointCloud(XYZ, space=self.space, sigma_mm=voxel_size.max())
245
241
  .boundingbox
246
242
  )
247
243
 
@@ -259,7 +255,7 @@ class BoundingBox(location.Location):
259
255
  @property
260
256
  def corners(self):
261
257
  """
262
- Returns all 8 corners of the box as a pointset.
258
+ Returns all 8 corners of the box as a pointcloud.
263
259
 
264
260
  Note
265
261
  ----
@@ -279,7 +275,7 @@ class BoundingBox(location.Location):
279
275
  TODO: deal with sigma. Currently, returns the mean of min and max point.
280
276
  """
281
277
  xs, ys, zs = zip(self.minpoint, self.maxpoint)
282
- return pointset.PointSet(
278
+ return pointcloud.PointCloud(
283
279
  coordinates=[[x, y, z] for x, y, z in product(xs, ys, zs)],
284
280
  space=self.space,
285
281
  sigma_mm=np.mean([self.minpoint.sigma, self.maxpoint.sigma])
@@ -360,7 +356,7 @@ class BoundingBox(location.Location):
360
356
  x1, y1, z1 = self.maxpoint
361
357
 
362
358
  # set of 8 corner points in source space
363
- corners1 = pointset.PointSet(
359
+ corners1 = pointcloud.PointCloud(
364
360
  [
365
361
  (x0, y0, z0),
366
362
  (x0, y0, z1),
@@ -417,14 +413,15 @@ class BoundingBox(location.Location):
417
413
  return super().__hash__()
418
414
 
419
415
 
420
- def _determine_bounds(array: np.ndarray, threshold=0):
416
+ def _determine_bounds(masked_array: np.ndarray, background: float = 0.0):
421
417
  """
422
- Bounding box of nonzero values in a 3D array.
418
+ Bounding box of nonzero (background) values in a 3D array.
419
+
423
420
  https://stackoverflow.com/questions/31400769/bounding-box-of-numpy-array
424
421
  """
425
- x = np.any(array > threshold, axis=(1, 2))
426
- y = np.any(array > threshold, axis=(0, 2))
427
- z = np.any(array > threshold, axis=(0, 1))
422
+ x = np.any(masked_array != background, axis=(1, 2))
423
+ y = np.any(masked_array != background, axis=(0, 2))
424
+ z = np.any(masked_array != background, axis=(0, 1))
428
425
  nzx, nzy, nzz = [np.where(v) for v in (x, y, z)]
429
426
  if any(len(nz[0]) == 0 for nz in [nzx, nzy, nzz]):
430
427
  # empty array
@@ -435,15 +432,19 @@ def _determine_bounds(array: np.ndarray, threshold=0):
435
432
  return np.array([[xmin, xmax + 1], [ymin, ymax + 1], [zmin, zmax + 1], [1, 1]])
436
433
 
437
434
 
438
- def from_array(array: np.ndarray, threshold=0, space: "Space" = None) -> "BoundingBox":
435
+ def from_array(
436
+ array: np.ndarray,
437
+ background: Union[int, float] = 0.0,
438
+ space: "Space" = None
439
+ ) -> BoundingBox:
439
440
  """
440
- Find the bounding box of an array.
441
+ Find the bounding box of non-background values for any 3D array.
441
442
 
442
443
  Parameters
443
444
  ----------
444
- array : np.ndarray
445
- threshold : int, default: 0
446
- space : Space, default: None
445
+ array: np.ndarray
446
+ background: int or float, default: 0.0
447
+ space: Space, default: None
447
448
  """
448
- bounds = _determine_bounds(array, threshold)
449
- return BoundingBox(bounds[:3, 0], bounds[:3, 1], space=space)
449
+ bounds = _determine_bounds(array, background)
450
+ return BoundingBox(point1=bounds[:3, 0], point2=bounds[:3, 1], space=space)
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
siibra/locations/point.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,7 +14,7 @@
14
14
  # limitations under the License.
15
15
  """Singular coordinate defined on a space, possibly with an uncertainty."""
16
16
 
17
- from . import location, boundingbox, pointset
17
+ from . import location, boundingbox, pointcloud
18
18
 
19
19
  from ..commons import logger
20
20
  from ..retrieval.requests import HttpRequest
@@ -108,14 +108,14 @@ class Point(location.Location):
108
108
 
109
109
  @property
110
110
  def homogeneous(self):
111
- """The homogenous coordinate of this point as a 4-tuple,
111
+ """The homogeneous coordinate of this point as a 4-tuple,
112
112
  obtained by appending '1' to the original 3-tuple."""
113
113
  return np.atleast_2d(self.coordinate + (1,))
114
114
 
115
115
  def intersection(self, other: location.Location) -> "Point":
116
116
  if isinstance(other, Point):
117
117
  return self if self == other else None
118
- elif isinstance(other, pointset.PointSet):
118
+ elif isinstance(other, pointcloud.PointCloud):
119
119
  return self if self in other else None
120
120
  else:
121
121
  return self if other.intersection(self) else None
@@ -157,7 +157,7 @@ class Point(location.Location):
157
157
  return self.sigma**3 * np.pi * 4. / 3.
158
158
 
159
159
  def __sub__(self, other):
160
- """Substract the coordinates of two points to get
160
+ """Subtract the coordinates of two points to get
161
161
  a new point representing the offset vector. Alternatively,
162
162
  subtract an integer from the all coordinates of this point
163
163
  to create a new one.
@@ -190,8 +190,8 @@ class Point(location.Location):
190
190
  return super().__hash__()
191
191
 
192
192
  def __eq__(self, other: 'Point'):
193
- if isinstance(other, pointset.PointSet):
194
- return other == self # implemented at pointset
193
+ if isinstance(other, pointcloud.PointCloud):
194
+ return other == self # implemented at pointcloud
195
195
  if not isinstance(other, Point):
196
196
  return False
197
197
  o = other if self.space is None else other.warp(self.space)
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,7 @@ from . import location, point, boundingbox as _boundingbox
18
18
 
19
19
  from ..retrieval.requests import HttpRequest
20
20
  from ..commons import logger
21
- from ..exceptions import SpaceWarpingFailedError
21
+ from ..exceptions import SpaceWarpingFailedError, EmptyPointCloudError
22
22
 
23
23
  from typing import List, Union, Tuple
24
24
  import numbers
@@ -32,13 +32,13 @@ except ImportError:
32
32
  _HAS_HDBSCAN = False
33
33
  logger.warning(
34
34
  f"HDBSCAN is not available with your version {sklearn.__version__} of sckit-learn."
35
- "`PointSet.find_clusters()` will not be avaiable."
35
+ "`PointCloud.find_clusters()` will not be available."
36
36
  )
37
37
 
38
38
 
39
- def from_points(points: List["point.Point"], newlabels: List[Union[int, float, tuple]] = None) -> "PointSet":
39
+ def from_points(points: List["point.Point"], newlabels: List[Union[int, float, tuple]] = None) -> "PointCloud":
40
40
  """
41
- Create a PointSet from an iterable of Points.
41
+ Create a PointCloud from an iterable of Points.
42
42
 
43
43
  Parameters
44
44
  ----------
@@ -48,16 +48,17 @@ def from_points(points: List["point.Point"], newlabels: List[Union[int, float, t
48
48
 
49
49
  Returns
50
50
  -------
51
- PointSet
51
+ PointCloud
52
52
  """
53
53
  if len(points) == 0:
54
- return PointSet([])
54
+ raise EmptyPointCloudError("Cannot create a PointCloud without any points.")
55
+
55
56
  spaces = {p.space for p in points}
56
- assert len(spaces) == 1, f"PointSet can only be constructed with points from the same space.\n{spaces}"
57
+ assert len(spaces) == 1, f"PointCloud can only be constructed with points from the same space.\n{spaces}"
57
58
  coords, sigmas, labels = zip(*((p.coordinate, p.sigma, p.label) for p in points))
58
59
  if all(lb is None for lb in set(labels)):
59
60
  labels = None
60
- return PointSet(
61
+ return PointCloud(
61
62
  coordinates=coords,
62
63
  space=next(iter(spaces)),
63
64
  sigma_mm=sigmas,
@@ -65,7 +66,7 @@ def from_points(points: List["point.Point"], newlabels: List[Union[int, float, t
65
66
  )
66
67
 
67
68
 
68
- class PointSet(location.Location):
69
+ class PointCloud(location.Location):
69
70
  """A set of 3D points in the same reference space,
70
71
  defined by a list of coordinates."""
71
72
 
@@ -91,6 +92,9 @@ class PointSet(location.Location):
91
92
  """
92
93
  location.Location.__init__(self, space)
93
94
 
95
+ if len(coordinates) == 0:
96
+ raise EmptyPointCloudError(f"Cannot create a {self.__class__.__name__} without any coordinates.")
97
+
94
98
  self._coordinates = coordinates
95
99
  if not isinstance(coordinates, np.ndarray):
96
100
  self._coordinates = np.array(self._coordinates).reshape((-1, 3))
@@ -113,21 +117,16 @@ class PointSet(location.Location):
113
117
  NOTE: The affine matrix of the image must be set to warp voxels
114
118
  coordinates into the reference space of this Bounding Box.
115
119
  """
116
- if not isinstance(other, (point.Point, PointSet, _boundingbox.BoundingBox)):
120
+ if not isinstance(other, (point.Point, PointCloud, _boundingbox.BoundingBox)):
117
121
  return other.intersection(self)
118
122
 
119
- intersections = [(i, p) for i, p in enumerate(self) if p.intersects(other)]
120
- if len(intersections) == 0:
123
+ if isinstance(other, PointCloud):
124
+ intersecting_points = [p for p in self if p.coordinate in other.coordinates]
125
+ else:
126
+ intersecting_points = [p for p in self if p.intersects(other)]
127
+ if len(intersecting_points) == 0:
121
128
  return None
122
- ids, points = zip(*intersections)
123
- labels = None if self.labels is None else [self.labels[i] for i in ids]
124
- sigma = [p.sigma for p in points]
125
- intersection = PointSet(
126
- points,
127
- space=self.space,
128
- sigma_mm=sigma,
129
- labels=labels
130
- )
129
+ intersection = from_points(intersecting_points)
131
130
  return intersection[0] if len(intersection) == 1 else intersection
132
131
 
133
132
  @property
@@ -186,7 +185,7 @@ class PointSet(location.Location):
186
185
  return self.__class__(coordinates=tuple(tgt_points), space=spaceobj, labels=self.labels)
187
186
 
188
187
  def transform(self, affine: np.ndarray, space=None):
189
- """Returns a new PointSet obtained by transforming the
188
+ """Returns a new PointCloud obtained by transforming the
190
189
  coordinates of this one with the given affine matrix.
191
190
 
192
191
  Parameters
@@ -207,7 +206,7 @@ class PointSet(location.Location):
207
206
  def __getitem__(self, index: int):
208
207
  if (abs(index) >= self.__len__()):
209
208
  raise IndexError(
210
- f"Pointset with {self.__len__()} points "
209
+ f"pointcloud with {self.__len__()} points "
211
210
  f"cannot be accessed with index {index}."
212
211
  )
213
212
  return point.Point(
@@ -229,10 +228,10 @@ class PointSet(location.Location):
229
228
  for i in range(len(self))
230
229
  )
231
230
 
232
- def __eq__(self, other: 'PointSet'):
231
+ def __eq__(self, other: 'PointCloud'):
233
232
  if isinstance(other, point.Point):
234
233
  return len(self) == 1 and self[0] == other
235
- if not isinstance(other, PointSet):
234
+ if not isinstance(other, PointCloud):
236
235
  return False
237
236
  return list(self) == list(other)
238
237
 
@@ -240,7 +239,7 @@ class PointSet(location.Location):
240
239
  return super().__hash__()
241
240
 
242
241
  def __len__(self):
243
- """The number of points in this PointSet."""
242
+ """The number of points in this PointCloud."""
244
243
  return self.coordinates.shape[0]
245
244
 
246
245
  def __str__(self):
@@ -249,9 +248,13 @@ class PointSet(location.Location):
249
248
  @property
250
249
  def boundingbox(self):
251
250
  """
252
- Return the bounding box of these points.
251
+ Return the bounding box of these points, or None in the
252
+ special case of an empty PointCloud.
253
253
  """
254
+ if len(self.coordinates) == 0:
255
+ return None
254
256
  coords = self.coordinates
257
+ # TODO this needs a more precise treatment of the sigmas
255
258
  sigma_min = max(self.sigma[i] for i in coords.argmin(0))
256
259
  sigma_max = max(self.sigma[i] for i in coords.argmax(0))
257
260
  return _boundingbox.BoundingBox(
@@ -292,8 +295,8 @@ class PointSet(location.Location):
292
295
 
293
296
  Parameters
294
297
  ----------
295
- min_fraction: min cluster size as a fraction of total points in the PointSet
296
- max_fraction: max cluster size as a fraction of total points in the PointSet
298
+ min_fraction: min cluster size as a fraction of total points in the PointCloud
299
+ max_fraction: max cluster size as a fraction of total points in the PointCloud
297
300
 
298
301
  Returns
299
302
  -------
@@ -302,7 +305,7 @@ class PointSet(location.Location):
302
305
 
303
306
  Note
304
307
  ----
305
- Replaces the labels of the PointSet instance with these labels.
308
+ Replaces the labels of the PointCloud instance with these labels.
306
309
 
307
310
  Raises
308
311
  ------
@@ -312,7 +315,7 @@ class PointSet(location.Location):
312
315
  if not _HAS_HDBSCAN:
313
316
  raise RuntimeError(
314
317
  f"HDBSCAN is not available with your version {sklearn.__version__} "
315
- "of sckit-learn. `PointSet.find_clusters()` will not be avaiable."
318
+ "of sckit-learn. `PointCloud.find_clusters()` will not be available."
316
319
  )
317
320
  points = np.array(self.as_list())
318
321
  N = points.shape[0]
@@ -322,7 +325,7 @@ class PointSet(location.Location):
322
325
  )
323
326
  if self.labels is not None:
324
327
  logger.warning(
325
- "Existing labels of PointSet will be overwritten with cluster labels."
328
+ "Existing labels of PointCloud will be overwritten with cluster labels."
326
329
  )
327
330
  self.labels = clustering.fit_predict(points)
328
331
  return self.labels
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
siibra/retrieval/cache.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -319,12 +319,12 @@ class EbrainsV3Dataset(EbrainsBaseDataset):
319
319
  @property
320
320
  def contributors(self):
321
321
  if self._contributers is None:
322
- contributers = {}
322
+ contributors = {}
323
323
  for version_id in self.version_ids:
324
- contributers.update(
324
+ contributors.update(
325
325
  {c['@id']: c for c in EbrainsV3DatasetVersion(version_id).contributors}
326
326
  )
327
- self._contributers = list(contributers.values())
327
+ self._contributers = list(contributors.values())
328
328
  return self._contributers
329
329
 
330
330
  @property
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2024
1
+ # Copyright 2018-2025
2
2
  # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");