siibra 1.0a19__py3-none-any.whl → 1.0.1a0__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 (38) hide show
  1. siibra/VERSION +1 -1
  2. siibra/__init__.py +3 -3
  3. siibra/commons.py +0 -46
  4. siibra/configuration/factory.py +10 -20
  5. siibra/core/atlas.py +20 -14
  6. siibra/core/parcellation.py +67 -52
  7. siibra/core/region.py +133 -123
  8. siibra/exceptions.py +8 -0
  9. siibra/experimental/contour.py +6 -6
  10. siibra/experimental/patch.py +2 -2
  11. siibra/experimental/plane3d.py +8 -8
  12. siibra/features/anchor.py +12 -13
  13. siibra/features/connectivity/regional_connectivity.py +2 -2
  14. siibra/features/feature.py +14 -16
  15. siibra/features/tabular/bigbrain_intensity_profile.py +1 -1
  16. siibra/features/tabular/cell_density_profile.py +97 -63
  17. siibra/features/tabular/layerwise_cell_density.py +3 -22
  18. siibra/features/tabular/regional_timeseries_activity.py +2 -2
  19. siibra/livequeries/allen.py +39 -16
  20. siibra/livequeries/bigbrain.py +8 -8
  21. siibra/livequeries/query.py +0 -1
  22. siibra/locations/__init__.py +9 -9
  23. siibra/locations/boundingbox.py +29 -24
  24. siibra/locations/point.py +4 -4
  25. siibra/locations/{pointset.py → pointcloud.py} +30 -22
  26. siibra/retrieval/repositories.py +9 -26
  27. siibra/retrieval/requests.py +19 -2
  28. siibra/volumes/__init__.py +1 -1
  29. siibra/volumes/parcellationmap.py +88 -81
  30. siibra/volumes/providers/neuroglancer.py +62 -36
  31. siibra/volumes/providers/nifti.py +11 -25
  32. siibra/volumes/sparsemap.py +124 -245
  33. siibra/volumes/volume.py +141 -52
  34. {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/METADATA +16 -3
  35. {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/RECORD +38 -38
  36. {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/WHEEL +1 -1
  37. {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/LICENSE +0 -0
  38. {siibra-1.0a19.dist-info → siibra-1.0.1a0.dist-info}/top_level.txt +0 -0
@@ -16,7 +16,7 @@
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
 
@@ -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):
@@ -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,16 @@ 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(
149
+ if len(points_inside) == 0:
150
+ return None
151
+ result = pointcloud.PointCloud(
150
152
  points_inside,
151
153
  space=other.space,
152
154
  sigma_mm=[p.sigma for p in points_inside]
153
155
  )
154
- if len(result) == 0:
155
- return None
156
- return result[0] if len(result) == 1 else result # if PointSet has single point return as a Point
156
+ return result[0] if len(result) == 1 else result # if PointCloud has single point return as a Point
157
157
 
158
158
  return other.intersection(self)
159
159
 
@@ -240,8 +240,8 @@ class BoundingBox(location.Location):
240
240
  )
241
241
  else:
242
242
  return self._intersect_bbox(
243
- pointset
244
- .PointSet(XYZ, space=self.space, sigma_mm=voxel_size.max())
243
+ pointcloud
244
+ .PointCloud(XYZ, space=self.space, sigma_mm=voxel_size.max())
245
245
  .boundingbox
246
246
  )
247
247
 
@@ -259,7 +259,7 @@ class BoundingBox(location.Location):
259
259
  @property
260
260
  def corners(self):
261
261
  """
262
- Returns all 8 corners of the box as a pointset.
262
+ Returns all 8 corners of the box as a pointcloud.
263
263
 
264
264
  Note
265
265
  ----
@@ -279,7 +279,7 @@ class BoundingBox(location.Location):
279
279
  TODO: deal with sigma. Currently, returns the mean of min and max point.
280
280
  """
281
281
  xs, ys, zs = zip(self.minpoint, self.maxpoint)
282
- return pointset.PointSet(
282
+ return pointcloud.PointCloud(
283
283
  coordinates=[[x, y, z] for x, y, z in product(xs, ys, zs)],
284
284
  space=self.space,
285
285
  sigma_mm=np.mean([self.minpoint.sigma, self.maxpoint.sigma])
@@ -360,7 +360,7 @@ class BoundingBox(location.Location):
360
360
  x1, y1, z1 = self.maxpoint
361
361
 
362
362
  # set of 8 corner points in source space
363
- corners1 = pointset.PointSet(
363
+ corners1 = pointcloud.PointCloud(
364
364
  [
365
365
  (x0, y0, z0),
366
366
  (x0, y0, z1),
@@ -417,14 +417,15 @@ class BoundingBox(location.Location):
417
417
  return super().__hash__()
418
418
 
419
419
 
420
- def _determine_bounds(array: np.ndarray, threshold=0):
420
+ def _determine_bounds(masked_array: np.ndarray, background: float = 0.0):
421
421
  """
422
- Bounding box of nonzero values in a 3D array.
422
+ Bounding box of nonzero (background) values in a 3D array.
423
+
423
424
  https://stackoverflow.com/questions/31400769/bounding-box-of-numpy-array
424
425
  """
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))
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))
428
429
  nzx, nzy, nzz = [np.where(v) for v in (x, y, z)]
429
430
  if any(len(nz[0]) == 0 for nz in [nzx, nzy, nzz]):
430
431
  # empty array
@@ -435,15 +436,19 @@ def _determine_bounds(array: np.ndarray, threshold=0):
435
436
  return np.array([[xmin, xmax + 1], [ymin, ymax + 1], [zmin, zmax + 1], [1, 1]])
436
437
 
437
438
 
438
- def from_array(array: np.ndarray, threshold=0, space: "Space" = None) -> "BoundingBox":
439
+ def from_array(
440
+ array: np.ndarray,
441
+ background: Union[int, float] = 0.0,
442
+ space: "Space" = None
443
+ ) -> BoundingBox:
439
444
  """
440
- Find the bounding box of an array.
445
+ Find the bounding box of non-background values for any 3D array.
441
446
 
442
447
  Parameters
443
448
  ----------
444
- array : np.ndarray
445
- threshold : int, default: 0
446
- space : Space, default: None
449
+ array: np.ndarray
450
+ background: int or float, default: 0.0
451
+ space: Space, default: None
447
452
  """
448
- bounds = _determine_bounds(array, threshold)
449
- return BoundingBox(bounds[:3, 0], bounds[:3, 1], space=space)
453
+ bounds = _determine_bounds(array, background)
454
+ return BoundingBox(point1=bounds[:3, 0], point2=bounds[:3, 1], space=space)
siibra/locations/point.py CHANGED
@@ -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
@@ -115,7 +115,7 @@ class Point(location.Location):
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
@@ -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)
@@ -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 avaiable."
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,7 +117,7 @@ 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
123
  intersections = [(i, p) for i, p in enumerate(self) if p.intersects(other)]
@@ -122,7 +126,7 @@ class PointSet(location.Location):
122
126
  ids, points = zip(*intersections)
123
127
  labels = None if self.labels is None else [self.labels[i] for i in ids]
124
128
  sigma = [p.sigma for p in points]
125
- intersection = PointSet(
129
+ intersection = PointCloud(
126
130
  points,
127
131
  space=self.space,
128
132
  sigma_mm=sigma,
@@ -186,7 +190,7 @@ class PointSet(location.Location):
186
190
  return self.__class__(coordinates=tuple(tgt_points), space=spaceobj, labels=self.labels)
187
191
 
188
192
  def transform(self, affine: np.ndarray, space=None):
189
- """Returns a new PointSet obtained by transforming the
193
+ """Returns a new PointCloud obtained by transforming the
190
194
  coordinates of this one with the given affine matrix.
191
195
 
192
196
  Parameters
@@ -207,7 +211,7 @@ class PointSet(location.Location):
207
211
  def __getitem__(self, index: int):
208
212
  if (abs(index) >= self.__len__()):
209
213
  raise IndexError(
210
- f"Pointset with {self.__len__()} points "
214
+ f"pointcloud with {self.__len__()} points "
211
215
  f"cannot be accessed with index {index}."
212
216
  )
213
217
  return point.Point(
@@ -229,10 +233,10 @@ class PointSet(location.Location):
229
233
  for i in range(len(self))
230
234
  )
231
235
 
232
- def __eq__(self, other: 'PointSet'):
236
+ def __eq__(self, other: 'PointCloud'):
233
237
  if isinstance(other, point.Point):
234
238
  return len(self) == 1 and self[0] == other
235
- if not isinstance(other, PointSet):
239
+ if not isinstance(other, PointCloud):
236
240
  return False
237
241
  return list(self) == list(other)
238
242
 
@@ -240,7 +244,7 @@ class PointSet(location.Location):
240
244
  return super().__hash__()
241
245
 
242
246
  def __len__(self):
243
- """The number of points in this PointSet."""
247
+ """The number of points in this PointCloud."""
244
248
  return self.coordinates.shape[0]
245
249
 
246
250
  def __str__(self):
@@ -249,9 +253,13 @@ class PointSet(location.Location):
249
253
  @property
250
254
  def boundingbox(self):
251
255
  """
252
- Return the bounding box of these points.
256
+ Return the bounding box of these points, or None in the
257
+ special case of an empty PointCloud.
253
258
  """
259
+ if len(self.coordinates) == 0:
260
+ return None
254
261
  coords = self.coordinates
262
+ # TODO this needs a more precise treatment of the sigmas
255
263
  sigma_min = max(self.sigma[i] for i in coords.argmin(0))
256
264
  sigma_max = max(self.sigma[i] for i in coords.argmax(0))
257
265
  return _boundingbox.BoundingBox(
@@ -292,8 +300,8 @@ class PointSet(location.Location):
292
300
 
293
301
  Parameters
294
302
  ----------
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
303
+ min_fraction: min cluster size as a fraction of total points in the PointCloud
304
+ max_fraction: max cluster size as a fraction of total points in the PointCloud
297
305
 
298
306
  Returns
299
307
  -------
@@ -302,7 +310,7 @@ class PointSet(location.Location):
302
310
 
303
311
  Note
304
312
  ----
305
- Replaces the labels of the PointSet instance with these labels.
313
+ Replaces the labels of the PointCloud instance with these labels.
306
314
 
307
315
  Raises
308
316
  ------
@@ -312,7 +320,7 @@ class PointSet(location.Location):
312
320
  if not _HAS_HDBSCAN:
313
321
  raise RuntimeError(
314
322
  f"HDBSCAN is not available with your version {sklearn.__version__} "
315
- "of sckit-learn. `PointSet.find_clusters()` will not be avaiable."
323
+ "of sckit-learn. `PointCloud.find_clusters()` will not be avaiable."
316
324
  )
317
325
  points = np.array(self.as_list())
318
326
  N = points.shape[0]
@@ -322,7 +330,7 @@ class PointSet(location.Location):
322
330
  )
323
331
  if self.labels is not None:
324
332
  logger.warning(
325
- "Existing labels of PointSet will be overwritten with cluster labels."
333
+ "Existing labels of PointCloud will be overwritten with cluster labels."
326
334
  )
327
335
  self.labels = clustering.fit_predict(points)
328
336
  return self.labels
@@ -19,7 +19,8 @@ from .requests import (
19
19
  EbrainsRequest,
20
20
  SiibraHttpRequestError,
21
21
  find_suitiable_decoder,
22
- DECODERS
22
+ DECODERS,
23
+ FileLoader
23
24
  )
24
25
  from .cache import CACHE
25
26
 
@@ -124,34 +125,16 @@ class LocalFileRepository(RepositoryConnector):
124
125
  self._folder = pathlib.Path(folder)
125
126
  assert pathlib.Path.is_dir(self._folder)
126
127
 
127
- def _build_url(self, folder: str, filename: str):
128
- return pathlib.Path.joinpath(self._folder, folder, filename)
129
-
130
- class FileLoader:
131
- """
132
- Just a loads a local file, but mimics the behaviour
133
- of cached http requests used in other connectors.
134
- """
135
- def __init__(self, file_url, decode_func):
136
- self.url = file_url
137
- self.func = decode_func
138
- self.cached = True
139
-
140
- @property
141
- def data(self):
142
- with open(self.url, 'rb') as f:
143
- return self.func(f.read())
128
+ def _build_url(self, folder: str, filename: str) -> str:
129
+ return pathlib.Path.joinpath(self._folder, folder, filename).as_posix()
144
130
 
145
131
  def get_loader(self, filename, folder="", decode_func=None):
146
132
  """Get a lazy loader for a file, for loading data
147
133
  only once loader.data is accessed."""
148
- url = self._build_url(folder, filename)
149
- if url is None:
150
- raise RuntimeError(f"Cannot build url for ({folder}, {filename})")
151
- if decode_func is None:
152
- return self.FileLoader(url, lambda b: self._decode_response(b, filename))
153
- else:
154
- return self.FileLoader(url, decode_func)
134
+ filepath = self._build_url(folder, filename)
135
+ if not pathlib.Path(filepath).is_file():
136
+ raise RuntimeError(f"No file is found in {filepath}")
137
+ return FileLoader(filepath, decode_func)
155
138
 
156
139
  def search_files(self, folder="", suffix=None, recursive=False):
157
140
  results = []
@@ -165,7 +148,7 @@ class LocalFileRepository(RepositoryConnector):
165
148
  def __str__(self):
166
149
  return f"{self.__class__.__name__} at {self._folder}"
167
150
 
168
- def __eq__(self, other):
151
+ def __eq__(self, other: "LocalFileRepository"):
169
152
  return self._folder == other._folder
170
153
 
171
154
 
@@ -117,8 +117,7 @@ def find_suitiable_decoder(url: str) -> Callable:
117
117
  suitable_decoders = [
118
118
  dec for sfx, dec in DECODERS.items() if urlpath.endswith(sfx)
119
119
  ]
120
- if len(suitable_decoders) > 0:
121
- assert len(suitable_decoders) == 1
120
+ if len(suitable_decoders) == 1:
122
121
  return suitable_decoders[0]
123
122
  else:
124
123
  return None
@@ -270,6 +269,24 @@ class HttpRequest:
270
269
  return self.get()
271
270
 
272
271
 
272
+ class FileLoader(HttpRequest):
273
+ """
274
+ Just a loads a local file, but mimics the behaviour
275
+ of cached http requests used in other connectors.
276
+ """
277
+ def __init__(self, filepath, func=None):
278
+ HttpRequest.__init__(
279
+ self, filepath, refresh=False,
280
+ func=func or find_suitiable_decoder(filepath)
281
+ )
282
+ self.cachefile = filepath
283
+
284
+ def _retrieve(self, **kwargs):
285
+ if kwargs:
286
+ logger.info(f"Keywords {list(kwargs.keys())} are supplied but won't be used.")
287
+ assert os.path.isfile(self.cachefile)
288
+
289
+
273
290
  class ZipfileRequest(HttpRequest):
274
291
  def __init__(self, url, filename, func=None, refresh=False):
275
292
  HttpRequest.__init__(
@@ -16,7 +16,7 @@
16
16
 
17
17
  from .parcellationmap import Map
18
18
  from .providers import provider
19
- from .volume import from_array, from_file, from_pointset, from_nifti, Volume
19
+ from .volume import from_array, from_file, from_pointcloud, from_nifti, Volume
20
20
 
21
21
  from ..commons import logger
22
22
  from typing import List, Union