siibra 1.0a14__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 (80) hide show
  1. siibra/VERSION +1 -1
  2. siibra/__init__.py +15 -5
  3. siibra/commons.py +3 -48
  4. siibra/configuration/__init__.py +1 -1
  5. siibra/configuration/configuration.py +1 -1
  6. siibra/configuration/factory.py +164 -127
  7. siibra/core/__init__.py +1 -1
  8. siibra/core/assignment.py +1 -1
  9. siibra/core/atlas.py +24 -17
  10. siibra/core/concept.py +18 -9
  11. siibra/core/parcellation.py +76 -55
  12. siibra/core/region.py +163 -183
  13. siibra/core/space.py +3 -1
  14. siibra/core/structure.py +1 -2
  15. siibra/exceptions.py +17 -1
  16. siibra/experimental/contour.py +6 -6
  17. siibra/experimental/patch.py +2 -2
  18. siibra/experimental/plane3d.py +8 -8
  19. siibra/explorer/__init__.py +1 -1
  20. siibra/explorer/url.py +15 -0
  21. siibra/explorer/util.py +1 -1
  22. siibra/features/__init__.py +1 -1
  23. siibra/features/anchor.py +13 -14
  24. siibra/features/connectivity/__init__.py +1 -1
  25. siibra/features/connectivity/functional_connectivity.py +1 -1
  26. siibra/features/connectivity/regional_connectivity.py +7 -5
  27. siibra/features/connectivity/streamline_counts.py +1 -1
  28. siibra/features/connectivity/streamline_lengths.py +1 -1
  29. siibra/features/connectivity/tracing_connectivity.py +1 -1
  30. siibra/features/dataset/__init__.py +1 -1
  31. siibra/features/dataset/ebrains.py +1 -1
  32. siibra/features/feature.py +50 -28
  33. siibra/features/image/__init__.py +1 -1
  34. siibra/features/image/image.py +18 -13
  35. siibra/features/image/sections.py +1 -1
  36. siibra/features/image/volume_of_interest.py +1 -1
  37. siibra/features/tabular/__init__.py +1 -1
  38. siibra/features/tabular/bigbrain_intensity_profile.py +2 -2
  39. siibra/features/tabular/cell_density_profile.py +102 -66
  40. siibra/features/tabular/cortical_profile.py +5 -3
  41. siibra/features/tabular/gene_expression.py +1 -1
  42. siibra/features/tabular/layerwise_bigbrain_intensities.py +1 -1
  43. siibra/features/tabular/layerwise_cell_density.py +8 -25
  44. siibra/features/tabular/receptor_density_fingerprint.py +5 -3
  45. siibra/features/tabular/receptor_density_profile.py +5 -3
  46. siibra/features/tabular/regional_timeseries_activity.py +7 -5
  47. siibra/features/tabular/tabular.py +5 -3
  48. siibra/livequeries/__init__.py +1 -1
  49. siibra/livequeries/allen.py +46 -20
  50. siibra/livequeries/bigbrain.py +9 -9
  51. siibra/livequeries/ebrains.py +1 -1
  52. siibra/livequeries/query.py +1 -2
  53. siibra/locations/__init__.py +10 -10
  54. siibra/locations/boundingbox.py +77 -38
  55. siibra/locations/location.py +12 -4
  56. siibra/locations/point.py +14 -9
  57. siibra/locations/{pointset.py → pointcloud.py} +69 -27
  58. siibra/retrieval/__init__.py +1 -1
  59. siibra/retrieval/cache.py +1 -1
  60. siibra/retrieval/datasets.py +1 -1
  61. siibra/retrieval/exceptions/__init__.py +1 -1
  62. siibra/retrieval/repositories.py +10 -27
  63. siibra/retrieval/requests.py +20 -3
  64. siibra/vocabularies/__init__.py +1 -1
  65. siibra/volumes/__init__.py +2 -2
  66. siibra/volumes/parcellationmap.py +121 -94
  67. siibra/volumes/providers/__init__.py +1 -1
  68. siibra/volumes/providers/freesurfer.py +1 -1
  69. siibra/volumes/providers/gifti.py +1 -1
  70. siibra/volumes/providers/neuroglancer.py +68 -42
  71. siibra/volumes/providers/nifti.py +18 -28
  72. siibra/volumes/providers/provider.py +2 -2
  73. siibra/volumes/sparsemap.py +128 -247
  74. siibra/volumes/volume.py +252 -65
  75. {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/METADATA +17 -4
  76. siibra-1.0.1a0.dist-info/RECORD +84 -0
  77. {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/WHEEL +1 -1
  78. siibra-1.0a14.dist-info/RECORD +0 -84
  79. {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/LICENSE +0 -0
  80. {siibra-1.0a14.dist-info → siibra-1.0.1a0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2021
1
+ # Copyright 2018-2024
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,6 +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, EmptyPointCloudError
21
22
 
22
23
  from typing import List, Union, Tuple
23
24
  import numbers
@@ -31,13 +32,13 @@ except ImportError:
31
32
  _HAS_HDBSCAN = False
32
33
  logger.warning(
33
34
  f"HDBSCAN is not available with your version {sklearn.__version__} of sckit-learn."
34
- "`PointSet.find_clusters()` will not be avaiable."
35
+ "`PointCloud.find_clusters()` will not be avaiable."
35
36
  )
36
37
 
37
38
 
38
- 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":
39
40
  """
40
- Create a PointSet from an iterable of Points.
41
+ Create a PointCloud from an iterable of Points.
41
42
 
42
43
  Parameters
43
44
  ----------
@@ -47,16 +48,17 @@ def from_points(points: List["point.Point"], newlabels: List[Union[int, float, t
47
48
 
48
49
  Returns
49
50
  -------
50
- PointSet
51
+ PointCloud
51
52
  """
52
53
  if len(points) == 0:
53
- return PointSet([])
54
+ raise EmptyPointCloudError("Cannot create a PointCloud without any points.")
55
+
54
56
  spaces = {p.space for p in points}
55
- 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}"
56
58
  coords, sigmas, labels = zip(*((p.coordinate, p.sigma, p.label) for p in points))
57
59
  if all(lb is None for lb in set(labels)):
58
60
  labels = None
59
- return PointSet(
61
+ return PointCloud(
60
62
  coordinates=coords,
61
63
  space=next(iter(spaces)),
62
64
  sigma_mm=sigmas,
@@ -64,7 +66,7 @@ def from_points(points: List["point.Point"], newlabels: List[Union[int, float, t
64
66
  )
65
67
 
66
68
 
67
- class PointSet(location.Location):
69
+ class PointCloud(location.Location):
68
70
  """A set of 3D points in the same reference space,
69
71
  defined by a list of coordinates."""
70
72
 
@@ -90,6 +92,9 @@ class PointSet(location.Location):
90
92
  """
91
93
  location.Location.__init__(self, space)
92
94
 
95
+ if len(coordinates) == 0:
96
+ raise EmptyPointCloudError(f"Cannot create a {self.__class__.__name__} without any coordinates.")
97
+
93
98
  self._coordinates = coordinates
94
99
  if not isinstance(coordinates, np.ndarray):
95
100
  self._coordinates = np.array(self._coordinates).reshape((-1, 3))
@@ -112,7 +117,7 @@ class PointSet(location.Location):
112
117
  NOTE: The affine matrix of the image must be set to warp voxels
113
118
  coordinates into the reference space of this Bounding Box.
114
119
  """
115
- if not isinstance(other, (point.Point, PointSet, _boundingbox.BoundingBox)):
120
+ if not isinstance(other, (point.Point, PointCloud, _boundingbox.BoundingBox)):
116
121
  return other.intersection(self)
117
122
 
118
123
  intersections = [(i, p) for i, p in enumerate(self) if p.intersects(other)]
@@ -121,7 +126,7 @@ class PointSet(location.Location):
121
126
  ids, points = zip(*intersections)
122
127
  labels = None if self.labels is None else [self.labels[i] for i in ids]
123
128
  sigma = [p.sigma for p in points]
124
- intersection = PointSet(
129
+ intersection = PointCloud(
125
130
  points,
126
131
  space=self.space,
127
132
  sigma_mm=sigma,
@@ -149,7 +154,7 @@ class PointSet(location.Location):
149
154
  if spaceobj == self.space:
150
155
  return self
151
156
  if any(_ not in location.Location.SPACEWARP_IDS for _ in [self.space.id, spaceobj.id]):
152
- raise ValueError(
157
+ raise SpaceWarpingFailedError(
153
158
  f"Cannot convert coordinates between {self.space.id} and {spaceobj.id}"
154
159
  )
155
160
 
@@ -178,10 +183,14 @@ class PointSet(location.Location):
178
183
  ).data
179
184
  tgt_points.extend(list(response["target_points"]))
180
185
 
186
+ # TODO: consider using np.isnan(np.dot(arr, arr)). see https://stackoverflow.com/a/45011547
187
+ if np.any(np.isnan(response['target_points'])):
188
+ raise SpaceWarpingFailedError(f'Warping {str(self)} to {spaceobj.name} resulted in NaN')
189
+
181
190
  return self.__class__(coordinates=tuple(tgt_points), space=spaceobj, labels=self.labels)
182
191
 
183
192
  def transform(self, affine: np.ndarray, space=None):
184
- """Returns a new PointSet obtained by transforming the
193
+ """Returns a new PointCloud obtained by transforming the
185
194
  coordinates of this one with the given affine matrix.
186
195
 
187
196
  Parameters
@@ -202,7 +211,7 @@ class PointSet(location.Location):
202
211
  def __getitem__(self, index: int):
203
212
  if (abs(index) >= self.__len__()):
204
213
  raise IndexError(
205
- f"Pointset with {self.__len__()} points "
214
+ f"pointcloud with {self.__len__()} points "
206
215
  f"cannot be accessed with index {index}."
207
216
  )
208
217
  return point.Point(
@@ -224,10 +233,10 @@ class PointSet(location.Location):
224
233
  for i in range(len(self))
225
234
  )
226
235
 
227
- def __eq__(self, other: 'PointSet'):
236
+ def __eq__(self, other: 'PointCloud'):
228
237
  if isinstance(other, point.Point):
229
238
  return len(self) == 1 and self[0] == other
230
- if not isinstance(other, PointSet):
239
+ if not isinstance(other, PointCloud):
231
240
  return False
232
241
  return list(self) == list(other)
233
242
 
@@ -235,7 +244,7 @@ class PointSet(location.Location):
235
244
  return super().__hash__()
236
245
 
237
246
  def __len__(self):
238
- """The number of points in this PointSet."""
247
+ """The number of points in this PointCloud."""
239
248
  return self.coordinates.shape[0]
240
249
 
241
250
  def __str__(self):
@@ -243,15 +252,19 @@ class PointSet(location.Location):
243
252
 
244
253
  @property
245
254
  def boundingbox(self):
246
- """Return the bounding box of these points.
247
- TODO revisit the numerical margin of 1e-6, should not be necessary.
248
255
  """
249
- XYZ = self.coordinates
250
- sigma_min = max(self.sigma[i] for i in XYZ.argmin(0))
251
- sigma_max = max(self.sigma[i] for i in XYZ.argmax(0))
256
+ Return the bounding box of these points, or None in the
257
+ special case of an empty PointCloud.
258
+ """
259
+ if len(self.coordinates) == 0:
260
+ return None
261
+ coords = self.coordinates
262
+ # TODO this needs a more precise treatment of the sigmas
263
+ sigma_min = max(self.sigma[i] for i in coords.argmin(0))
264
+ sigma_max = max(self.sigma[i] for i in coords.argmax(0))
252
265
  return _boundingbox.BoundingBox(
253
- point1=XYZ.min(0) - max(sigma_min, 1e-6),
254
- point2=XYZ.max(0) + max(sigma_max, 1e-6),
266
+ point1=coords.min(0),
267
+ point2=coords.max(0),
255
268
  space=self.space,
256
269
  sigma_mm=[sigma_min, sigma_max]
257
270
  )
@@ -276,11 +289,38 @@ class PointSet(location.Location):
276
289
  """Access the list of 3D point as an Nx4 array of homogeneous coordinates."""
277
290
  return np.c_[self.coordinates, np.ones(len(self))]
278
291
 
279
- def find_clusters(self, min_fraction=1 / 200, max_fraction=1 / 8):
292
+ def find_clusters(
293
+ self,
294
+ min_fraction: float = 1 / 200,
295
+ max_fraction: float = 1 / 8
296
+ ) -> List[int]:
297
+ """
298
+ Find clusters using HDBSCAN (https://dl.acm.org/doi/10.1145/2733381)
299
+ implementation of scikit-learn (https://dl.acm.org/doi/10.5555/1953048.2078195).
300
+
301
+ Parameters
302
+ ----------
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
305
+
306
+ Returns
307
+ -------
308
+ List[int]
309
+ Returns the cluster labels found by skilearn.cluster.HDBSCAN.
310
+
311
+ Note
312
+ ----
313
+ Replaces the labels of the PointCloud instance with these labels.
314
+
315
+ Raises
316
+ ------
317
+ RuntimeError
318
+ If a sklearn version without HDBSCAN is installed.
319
+ """
280
320
  if not _HAS_HDBSCAN:
281
321
  raise RuntimeError(
282
322
  f"HDBSCAN is not available with your version {sklearn.__version__} "
283
- "of sckit-learn. `PointSet.find_clusters()` will not be avaiable."
323
+ "of sckit-learn. `PointCloud.find_clusters()` will not be avaiable."
284
324
  )
285
325
  points = np.array(self.as_list())
286
326
  N = points.shape[0]
@@ -289,7 +329,9 @@ class PointSet(location.Location):
289
329
  max_cluster_size=int(N * max_fraction),
290
330
  )
291
331
  if self.labels is not None:
292
- logger.warn("Existing labels of PointSet will be overwritten with cluster labels.")
332
+ logger.warning(
333
+ "Existing labels of PointCloud will be overwritten with cluster labels."
334
+ )
293
335
  self.labels = clustering.fit_predict(points)
294
336
  return self.labels
295
337
 
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2021
1
+ # Copyright 2018-2024
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-2021
1
+ # Copyright 2018-2024
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-2021
1
+ # Copyright 2018-2024
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-2021
1
+ # Copyright 2018-2024
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-2021
1
+ # Copyright 2018-2024
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");
@@ -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
 
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2021
1
+ # Copyright 2018-2024
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");
@@ -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__(
@@ -1,4 +1,4 @@
1
- # Copyright 2018-2021
1
+ # Copyright 2018-2024
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-2021
1
+ # Copyright 2018-2024
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,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