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");
@@ -16,15 +16,77 @@
16
16
  from . import cortical_profile
17
17
 
18
18
  from .. import anchor as _anchor
19
- from ...commons import PolyLine, logger, create_key
19
+ from ...commons import logger
20
20
  from ...retrieval import requests
21
21
 
22
22
  from skimage.draw import polygon
23
23
  from skimage.transform import resize
24
- from io import BytesIO
25
24
  import numpy as np
26
25
  import pandas as pd
27
26
 
27
+ from io import BytesIO
28
+ from typing import Union, Tuple, Iterable
29
+
30
+
31
+ def cell_reader(bytes_buffer: bytes):
32
+ return pd.read_csv(BytesIO(bytes_buffer[2:]), delimiter=" ", header=0).astype(
33
+ {"layer": int, "label": int}
34
+ )
35
+
36
+
37
+ def layer_reader(bytes_buffer: bytes):
38
+ return pd.read_csv(BytesIO(bytes_buffer[2:]), delimiter=" ", header=0, index_col=0)
39
+
40
+
41
+ def poly_srt(poly):
42
+ return poly[poly[:, 0].argsort(), :]
43
+
44
+
45
+ def poly_rev(poly):
46
+ return poly[poly[:, 0].argsort()[::-1], :]
47
+
48
+
49
+ class PolyLine:
50
+ """Simple polyline representation which allows equidistant sampling."""
51
+
52
+ def __init__(self, pts):
53
+ self.pts = pts
54
+ self.lengths = [
55
+ np.sqrt(np.sum((pts[i, :] - pts[i - 1, :]) ** 2))
56
+ for i in range(1, pts.shape[0])
57
+ ]
58
+
59
+ def length(self):
60
+ return sum(self.lengths)
61
+
62
+ def sample(self, d: Union[Iterable[float], np.ndarray, float]):
63
+ # if d is iterable, we assume a list of sample positions
64
+ try:
65
+ iter(d)
66
+ except TypeError:
67
+ positions = [d]
68
+ else:
69
+ positions = d
70
+
71
+ samples = []
72
+ for s_ in positions:
73
+ s = min(max(s_, 0), 1)
74
+ target_distance = s * self.length()
75
+ current_distance = 0
76
+ for i, length in enumerate(self.lengths):
77
+ current_distance += length
78
+ if current_distance >= target_distance:
79
+ p1 = self.pts[i, :]
80
+ p2 = self.pts[i + 1, :]
81
+ r = (target_distance - current_distance + length) / length
82
+ samples.append(p1 + (p2 - p1) * r)
83
+ break
84
+
85
+ if len(samples) == 1:
86
+ return samples[0]
87
+ else:
88
+ return np.array(samples)
89
+
28
90
 
29
91
  class CellDensityProfile(
30
92
  cortical_profile.CorticalProfile,
@@ -45,24 +107,6 @@ class CellDensityProfile(
45
107
 
46
108
  _filter_attrs = cortical_profile.CorticalProfile._filter_attrs + ["location"]
47
109
 
48
- @classmethod
49
- def CELL_READER(cls, b):
50
- return pd.read_csv(BytesIO(b[2:]), delimiter=" ", header=0).astype(
51
- {"layer": int, "label": int}
52
- )
53
-
54
- @classmethod
55
- def LAYER_READER(cls, b):
56
- return pd.read_csv(BytesIO(b[2:]), delimiter=" ", header=0, index_col=0)
57
-
58
- @staticmethod
59
- def poly_srt(poly):
60
- return poly[poly[:, 0].argsort(), :]
61
-
62
- @staticmethod
63
- def poly_rev(poly):
64
- return poly[poly[:, 0].argsort()[::-1], :]
65
-
66
110
  def __init__(
67
111
  self,
68
112
  section: int,
@@ -89,9 +133,9 @@ class CellDensityProfile(
89
133
  )
90
134
  self._step = 0.01
91
135
  self._url = url
92
- self._cell_loader = requests.HttpRequest(url, self.CELL_READER)
136
+ self._cell_loader = requests.HttpRequest(url, cell_reader)
93
137
  self._layer_loader = requests.HttpRequest(
94
- url.replace("segments", "layerinfo"), self.LAYER_READER
138
+ url.replace("segments", "layerinfo"), layer_reader
95
139
  )
96
140
  self._density_image = None
97
141
  self._layer_mask = None
@@ -105,47 +149,49 @@ class CellDensityProfile(
105
149
 
106
150
  @property
107
151
  def shape(self):
108
- return tuple(self.cells[["y", "x"]].max().astype("int") + 1)
152
+ """(y,x)"""
153
+ return tuple(np.ceil(self.cells[["y", "x"]].max()).astype("int"))
109
154
 
110
- def boundary_annotation(self, boundary):
155
+ def boundary_annotation(self, boundary: Tuple[int, int]) -> np.ndarray:
111
156
  """Returns the annotation of a specific layer boundary."""
112
- y1, x1 = self.shape
157
+ shape_y, shape_x = self.shape
113
158
 
114
159
  # start of image patch
115
160
  if boundary == (-1, 0):
116
- return np.array([[0, 0], [x1, 0]])
161
+ return np.array([[0, 0], [shape_x, 0]])
117
162
 
118
163
  # end of image patch
119
164
  if boundary == (7, 8):
120
- return np.array([[0, y1], [x1, y1]])
165
+ return np.array([[0, shape_y], [shape_x, shape_y]])
121
166
 
122
167
  # retrieve polygon
123
168
  basename = "{}_{}.json".format(
124
169
  *(self.LAYERS[layer] for layer in boundary)
125
170
  ).replace("0_I", "0")
126
- url = self._url.replace("segments.txt", basename)
127
- poly = self.poly_srt(np.array(requests.HttpRequest(url).get()["segments"]))
171
+ poly_url = self._url.replace("segments.txt", basename)
172
+ poly = poly_srt(np.array(requests.HttpRequest(poly_url).get()["segments"]))
128
173
 
129
- # ensure full width
174
+ # ensure full width and trim to the image shape
130
175
  poly[0, 0] = 0
131
- poly[-1, 0] = x1
176
+ poly[poly[:, 0] > shape_x, 0] = shape_x
177
+ poly[poly[:, 1] > shape_y, 1] = shape_y
132
178
 
133
179
  return poly
134
180
 
135
- def layer_annotation(self, layer):
181
+ def layer_annotation(self, layer: int) -> np.ndarray:
136
182
  return np.vstack(
137
183
  (
138
184
  self.boundary_annotation((layer - 1, layer)),
139
- self.poly_rev(self.boundary_annotation((layer, layer + 1))),
185
+ poly_rev(self.boundary_annotation((layer, layer + 1))),
140
186
  self.boundary_annotation((layer - 1, layer))[0, :],
141
187
  )
142
188
  )
143
189
 
144
190
  @property
145
- def layer_mask(self):
191
+ def layer_mask(self) -> np.ndarray:
146
192
  """Generates a layer mask from boundary annotations."""
147
193
  if self._layer_mask is None:
148
- self._layer_mask = np.zeros(np.array(self.shape).astype("int") + 1)
194
+ self._layer_mask = np.zeros(np.array(self.shape, dtype=int) + 1, dtype="int")
149
195
  for layer in range(1, 8):
150
196
  pl = self.layer_annotation(layer)
151
197
  X, Y = polygon(pl[:, 0], pl[:, 1])
@@ -153,20 +199,20 @@ class CellDensityProfile(
153
199
  return self._layer_mask
154
200
 
155
201
  @property
156
- def depth_image(self):
202
+ def depth_image(self) -> np.ndarray:
157
203
  """Cortical depth image from layer boundary polygons by equidistant sampling."""
158
204
 
159
205
  if self._depth_image is None:
160
-
206
+ logger.info("Calculating cell densities from cell and layer data...")
161
207
  # compute equidistant cortical depth image from inner and outer contour
162
208
  scale = 0.1
163
- D = np.zeros((np.array(self.density_image.shape) * scale).astype("int") + 1)
209
+ depth_arr = np.zeros(np.ceil(np.array(self.shape) * scale).astype("int") + 1)
164
210
 
165
211
  # determine sufficient stepwidth for profile sampling
166
212
  # to match downscaled image resolution
167
- vstep, hstep = 1.0 / np.array(D.shape) / 2.0
213
+ vstep, hstep = 1.0 / np.array(depth_arr.shape) / 2.0
168
214
  vsteps = np.arange(0, 1 + vstep, vstep)
169
- hsteps = np.arange(0, 1 + vstep, hstep)
215
+ hsteps = np.arange(0, 1 + hstep, hstep)
170
216
 
171
217
  # build straight profiles between outer and inner cortical boundary
172
218
  s0 = PolyLine(self.boundary_annotation((0, 1)) * scale).sample(hsteps)
@@ -175,16 +221,16 @@ class CellDensityProfile(
175
221
 
176
222
  # write sample depths to their location in the depth image
177
223
  for prof in profiles:
178
- XY = prof.sample(vsteps).astype("int")
179
- D[XY[:, 1], XY[:, 0]] = vsteps
224
+ prof_samples_as_index = prof.sample(vsteps).astype("int")
225
+ depth_arr[prof_samples_as_index[:, 1], prof_samples_as_index[:, 0]] = vsteps
180
226
 
181
227
  # fix wm region, account for rounding error
182
228
  XY = self.layer_annotation(7) * scale
183
- D[polygon(XY[:, 1] - 1, XY[:, 0])] = 1
184
- D[-1, :] = 1
229
+ depth_arr[polygon(XY[:, 1] - 1, XY[:, 0])] = 1
230
+ depth_arr[-1, :] = 1
185
231
 
186
232
  # rescale depth image to original patch size
187
- self._depth_image = resize(D, self.density_image.shape)
233
+ self._depth_image = resize(depth_arr, self.density_image.shape)
188
234
 
189
235
  return self._depth_image
190
236
 
@@ -200,7 +246,7 @@ class CellDensityProfile(
200
246
  return self._boundary_positions
201
247
 
202
248
  @property
203
- def density_image(self):
249
+ def density_image(self) -> np.ndarray:
204
250
  if self._density_image is None:
205
251
  logger.debug("Computing density image for", self._url)
206
252
  # we integrate cell counts into 2D bins
@@ -209,9 +255,7 @@ class CellDensityProfile(
209
255
  counts, xedges, yedges = np.histogram2d(
210
256
  self.cells.y,
211
257
  self.cells.x,
212
- bins=(np.array(self.layer_mask.shape) / pixel_size_micron + 0.5).astype(
213
- "int"
214
- ),
258
+ bins=np.round(np.array(self.shape) / pixel_size_micron).astype("int"),
215
259
  )
216
260
 
217
261
  # rescale the counts from count / pixel_size**2 to count / 0.1mm^3,
@@ -229,11 +273,11 @@ class CellDensityProfile(
229
273
  return self._density_image
230
274
 
231
275
  @property
232
- def cells(self):
276
+ def cells(self) -> pd.DataFrame:
233
277
  return self._cell_loader.get()
234
278
 
235
279
  @property
236
- def layers(self):
280
+ def layers(self) -> pd.DataFrame:
237
281
  return self._layer_loader.get()
238
282
 
239
283
  @property
@@ -242,6 +286,7 @@ class CellDensityProfile(
242
286
 
243
287
  @property
244
288
  def _values(self):
289
+ # TODO: release a dataset update instead of on the fly computation
245
290
  densities = []
246
291
  delta = self._step / 2.0
247
292
  for d in self._depths:
@@ -249,16 +294,5 @@ class CellDensityProfile(
249
294
  if np.sum(mask) > 0:
250
295
  densities.append(self.density_image[mask].mean())
251
296
  else:
252
- densities.append(np.NaN)
297
+ densities.append(np.nan)
253
298
  return np.asanyarray(densities)
254
-
255
- @property
256
- def key(self):
257
- assert len(self.species) == 1
258
- return create_key("{}_{}_{}_{}_{}".format(
259
- self.id,
260
- self.species[0]['name'],
261
- self.regionspec,
262
- self.section,
263
- self.patch
264
- ))
@@ -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");
@@ -37,7 +37,7 @@ class CorticalProfile(tabular.Tabular, Compoundable):
37
37
  Optionally, the depth coordinates of layer boundaries can be specified.
38
38
 
39
39
  Most attributes are modelled as properties, so dervide classes are able
40
- to implement lazy loading instead of direct initialiation.
40
+ to implement lazy loading instead of direct initialization.
41
41
 
42
42
  """
43
43
 
@@ -151,7 +151,7 @@ class CorticalProfile(tabular.Tabular, Compoundable):
151
151
 
152
152
  @property
153
153
  def _layers(self):
154
- """List of layers assigned to each measurments,
154
+ """List of layers assigned to each measurements,
155
155
  if layer boundaries are available for this features.
156
156
  """
157
157
  if self.boundaries_mapped:
@@ -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");
@@ -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
  from . import cortical_profile
17
17
  from .. import anchor as _anchor
18
18
  from . import tabular
19
+ from ..tabular.cell_density_profile import cell_reader, layer_reader
19
20
 
20
21
  from ... import commons
21
22
  from ...retrieval import requests
22
23
 
23
24
  import pandas as pd
24
25
  import numpy as np
25
- from io import BytesIO
26
26
 
27
27
 
28
28
  class LayerwiseCellDensity(
@@ -40,16 +40,6 @@ class LayerwiseCellDensity(
40
40
  "The cortical depth is estimated from the measured layer thicknesses."
41
41
  )
42
42
 
43
- @classmethod
44
- def CELL_READER(cls, b):
45
- return pd.read_csv(BytesIO(b[2:]), delimiter=" ", header=0).astype(
46
- {"layer": int, "label": int}
47
- )
48
-
49
- @classmethod
50
- def LAYER_READER(cls, b):
51
- return pd.read_csv(BytesIO(b[2:]), delimiter=" ", header=0, index_col=0)
52
-
53
43
  def __init__(
54
44
  self,
55
45
  segmentfiles: list,
@@ -77,8 +67,8 @@ class LayerwiseCellDensity(
77
67
  density_dict = {}
78
68
  for i, (cellfile, layerfile) in enumerate(self._filepairs):
79
69
  try:
80
- cells = requests.HttpRequest(cellfile, func=self.CELL_READER).data
81
- layers = requests.HttpRequest(layerfile, func=self.LAYER_READER).data
70
+ cells = requests.HttpRequest(cellfile, func=cell_reader).data
71
+ layers = requests.HttpRequest(layerfile, func=layer_reader).data
82
72
  except requests.SiibraHttpRequestError as e:
83
73
  print(str(e))
84
74
  commons.logger.error(f"Skipping to bootstrap a {self.__class__.__name__} feature, cannot access file resource.")
@@ -103,12 +93,3 @@ class LayerwiseCellDensity(
103
93
  )
104
94
  self._data_cached.index.name = 'layer'
105
95
  return self._data_cached
106
-
107
- @property
108
- def key(self):
109
- assert len(self.species) == 1
110
- return commons.create_key("{}_{}_{}".format(
111
- self.dataset_id,
112
- self.species[0]['name'],
113
- self.regionspec
114
- ))
@@ -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,7 +16,8 @@
16
16
  from .. import anchor as _anchor
17
17
  from . import tabular
18
18
 
19
- from ... import commons, vocabularies
19
+ from ...commons import logger
20
+ from ...vocabularies import RECEPTOR_SYMBOLS
20
21
  from ...retrieval import requests
21
22
 
22
23
  import pandas as pd
@@ -34,7 +35,7 @@ class ReceptorDensityFingerprint(
34
35
  DESCRIPTION = (
35
36
  "Fingerprint of densities (in fmol/mg protein) of receptors for classical neurotransmitters "
36
37
  "obtained by means of quantitative in vitro autoradiography. The fingerprint provides average "
37
- "density measurments for different receptors measured in tissue samples from different subjects "
38
+ "density measurements for different receptors measured in tissue samples from different subjects "
38
39
  "together with the corresponding standard deviations. "
39
40
  )
40
41
 
@@ -75,9 +76,9 @@ class ReceptorDensityFingerprint(
75
76
  # Likely ill-formed tsv's
76
77
  return [
77
78
  "{} ({})".format(
78
- vocabularies.RECEPTOR_SYMBOLS[t]['neurotransmitter']['label'],
79
- vocabularies.RECEPTOR_SYMBOLS[t]['neurotransmitter']['name'],
80
- ) if t in vocabularies.RECEPTOR_SYMBOLS else
79
+ RECEPTOR_SYMBOLS[t]['neurotransmitter']['label'],
80
+ RECEPTOR_SYMBOLS[t]['neurotransmitter']['name'],
81
+ ) if t in RECEPTOR_SYMBOLS else
81
82
  f"{t} (undeciphered)"
82
83
  for t in self.receptors
83
84
  ]
@@ -107,7 +108,7 @@ class ReceptorDensityFingerprint(
107
108
  std = [data[_]["density (sd)"] for _ in labels]
108
109
  except KeyError as e:
109
110
  print(str(e))
110
- commons.logger.error("Could not parse fingerprint from this dictionary")
111
+ logger.error("Could not parse fingerprint from this dictionary")
111
112
  return {
112
113
  'unit': next(iter(units)),
113
114
  'labels': labels,
@@ -124,9 +125,11 @@ class ReceptorDensityFingerprint(
124
125
  if backend == "matplotlib":
125
126
  try:
126
127
  import matplotlib.pyplot as plt
127
- except ImportError:
128
- commons.logger.error("matplotlib not available. Plotting of fingerprints disabled.")
129
- return None
128
+ except ImportError as e:
129
+ logger.error(
130
+ "matplotlib not available. Please install matplotlib or use or another backend such as plotly."
131
+ )
132
+ raise e
130
133
  from collections import deque
131
134
 
132
135
  # default args
@@ -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");
@@ -19,7 +19,7 @@ from ..feature import Compoundable
19
19
  from ...core import region as _region
20
20
  from .. import anchor as _anchor
21
21
  from ...commons import QUIET, siibra_tqdm
22
- from ...locations import pointset
22
+ from ...locations import pointcloud
23
23
  from ...retrieval.repositories import RepositoryConnector
24
24
  from ...retrieval.requests import HttpRequest
25
25
 
@@ -203,7 +203,7 @@ class RegionalTimeseriesActivity(tabular.Tabular, Compoundable):
203
203
  found = [r for r in region if r.name in all_centroids]
204
204
  assert len(found) > 0
205
205
  result.append(
206
- tuple(pointset.PointSet(
206
+ tuple(pointcloud.PointCloud(
207
207
  [all_centroids[r.name] for r in found], space=space
208
208
  ).centroid)
209
209
  )
@@ -239,7 +239,7 @@ class RegionalTimeseriesActivity(tabular.Tabular, Compoundable):
239
239
  backend="plotly", **kwargs
240
240
  ):
241
241
  """
242
- Create a carpet plot ofthe timeseries data per region.
242
+ Create a carpet plot of the timeseries data per region.
243
243
 
244
244
  Parameters
245
245
  ----------
@@ -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");
@@ -19,7 +19,7 @@ from .. import feature
19
19
 
20
20
  from .. import anchor as _anchor
21
21
 
22
- from ... import commons
22
+ from ...commons import logger
23
23
 
24
24
  import pandas as pd
25
25
  from textwrap import wrap
@@ -91,9 +91,11 @@ class Tabular(feature.Feature):
91
91
  if backend == "matplotlib":
92
92
  try:
93
93
  import matplotlib.pyplot as plt
94
- except ImportError:
95
- commons.logger.error("matplotlib not available. Plotting of fingerprints disabled.")
96
- return None
94
+ except ImportError as e:
95
+ logger.error(
96
+ "matplotlib not available. Please install matplotlib or use or another backend such as plotly."
97
+ )
98
+ raise e
97
99
  # default kwargs
98
100
  if kwargs.get("error_y") is None:
99
101
  kwargs["yerr"] = kwargs.get("yerr", 'std' if 'std' in self.data.columns else None)
@@ -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");
@@ -20,11 +20,11 @@ from ..core import space as _space, structure
20
20
  from ..features import anchor as _anchor
21
21
  from ..features.tabular.gene_expression import GeneExpressions
22
22
  from ..commons import logger, Species
23
- from ..locations import point, pointset
23
+ from ..locations import point, pointcloud
24
24
  from ..retrieval import HttpRequest
25
25
  from ..vocabularies import GENE_NAMES
26
26
 
27
- from typing import Iterable
27
+ from typing import List
28
28
  from xml.etree import ElementTree
29
29
  import numpy as np
30
30
  import json
@@ -32,6 +32,24 @@ import json
32
32
 
33
33
  BASE_URL = "http://api.brain-map.org/api/v2/data"
34
34
 
35
+ LOCATION_PRECISION_MM = 2. # the assumed spatial precision of the probe locations in MNI space
36
+
37
+
38
+ def is_allen_api_microarray_service_available():
39
+ import requests
40
+
41
+ # see https://community.brain-map.org/t/human-brain-atlas-api/2876
42
+ microarray_test_url = "http://api.brain-map.org/api/v2/data/query.json?criteria= service::human_microarray_expression[probes$eq1023146,1023147][donors$eq15496][structures$eq9148]"
43
+ try:
44
+ response = requests.get(microarray_test_url).json()
45
+ except requests.RequestException:
46
+ return False
47
+ return response["success"]
48
+
49
+
50
+ class InvalidAllenAPIResponseException(Exception):
51
+ pass
52
+
35
53
 
36
54
  class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions):
37
55
  """
@@ -48,7 +66,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
48
66
  - Each sample was subject to multiple (in fact 4) different probes.
49
67
  - The probe data structures contain the list of gene expression of a
50
68
  particular gene measured in each sample. Therefore the length of the gene
51
- expression list in a probe coresponds to the number of samples taken in
69
+ expression list in a probe corresponds to the number of samples taken in
52
70
  the corresponding donor for the given gene.
53
71
  """
54
72
 
@@ -96,7 +114,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
96
114
  Each sample is linked to a donor, brain structure, and
97
115
  ICBM coordinate.
98
116
  When querying with a brain structure, the ICBM coordinates
99
- will be tested agains the region mask in ICBM space
117
+ will be tested against the region mask in ICBM space
100
118
  to produce a table of outputs.
101
119
  """
102
120
  LiveQuery.__init__(self, **kwargs)
@@ -117,7 +135,14 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
117
135
 
118
136
  self.genes = parse_gene(gene)
119
137
 
120
- def query(self, concept: structure.BrainStructure) -> Iterable[GeneExpressions]:
138
+ def query(self, concept: structure.BrainStructure) -> List[GeneExpressions]:
139
+ if not is_allen_api_microarray_service_available():
140
+ raise InvalidAllenAPIResponseException(
141
+ 'The service "web API of the Allen Brain Atlas for the human microarray expression" '
142
+ 'is not available at the moment, therefore siibra is not able to fetch '
143
+ 'gene expression features. This is a known issue which we are investigating: '
144
+ 'https://github.com/FZJ-INM1-BDA/siibra-python/issues/636.'
145
+ )
121
146
 
122
147
  mnispace = _space.Space.registry().get('mni152')
123
148
 
@@ -125,23 +150,21 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
125
150
  # Record matched instances and their locations.
126
151
  measurements = []
127
152
  coordinates = []
128
- points_inside = dict()
129
153
  for measurement in self:
130
- pt = point.Point(measurement['mni_xyz'], space=mnispace)
131
- if pt not in points_inside: # cache redundant intersection tests
132
- points_inside[pt] = pt in concept
133
- if points_inside[pt]:
154
+ pt = point.Point(measurement['mni_xyz'], space=mnispace, sigma_mm=LOCATION_PRECISION_MM)
155
+ if pt in concept:
134
156
  measurements.append(measurement)
135
157
  coordinates.append(pt)
136
158
 
137
- if len(points_inside) == 0:
138
- raise StopIteration
159
+ if len(coordinates) == 0:
160
+ logger.info(f"No probes found that lie within {concept}")
161
+ return []
139
162
 
140
163
  # Build the anatomical anchor and assignment to the query concept.
141
164
  # It will be attached to the returned feature, with the set of matched
142
165
  # MNI coordinates as anchor's location.
143
166
  anchor = _anchor.AnatomicalAnchor(
144
- location=pointset.from_points(coordinates),
167
+ location=pointcloud.from_points(coordinates),
145
168
  species=self.species
146
169
  )
147
170
  explanation = f"MNI coordinates of tissue samples were filtered using {concept}"
@@ -153,7 +176,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
153
176
  )]
154
177
  anchor._last_matched_concept = concept
155
178
 
156
- yield GeneExpressions(
179
+ return [GeneExpressions(
157
180
  anchor=anchor,
158
181
  genes=[m['gene'] for m in measurements],
159
182
  levels=[m['expression_level'] for m in measurements],
@@ -167,7 +190,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
167
190
  "probe_id": [m['probe_id'] for m in measurements],
168
191
  "donor_name": [m['donor_name'] for m in measurements],
169
192
  }
170
- )
193
+ )]
171
194
 
172
195
  def __iter__(self):
173
196
 
@@ -261,7 +284,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
261
284
  url = AllenBrainAtlasQuery._QUERY["specimen"].format(specimen_id=specimen_id)
262
285
  response = HttpRequest(url).get()
263
286
  if not response["success"]:
264
- raise Exception(
287
+ raise InvalidAllenAPIResponseException(
265
288
  "Invalid response when retrieving specimen information: {}".format(url)
266
289
  )
267
290
  # we ask for 1 specimen, so list should have length 1
@@ -278,7 +301,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
278
301
  return specimen
279
302
 
280
303
  @classmethod
281
- def _retrieve_microarray(cls, donor_id: str, probe_ids: str) -> Iterable[GeneExpressions]:
304
+ def _retrieve_microarray(cls, donor_id: str, probe_ids: str):
282
305
  """
283
306
  Retrieve microarray data for several probes of a given donor, and
284
307
  compute the MRI position of the corresponding tissue block in the ICBM
@@ -297,7 +320,7 @@ class AllenBrainAtlasQuery(LiveQuery, args=['gene'], FeatureType=GeneExpressions
297
320
  except json.JSONDecodeError as e:
298
321
  raise RuntimeError(f"Allen institute site produced an empty response - please try again later.\n{e}")
299
322
  if not response["success"]:
300
- raise Exception(
323
+ raise InvalidAllenAPIResponseException(
301
324
  "Invalid response when retrieving microarray data: {}".format(url)
302
325
  )
303
326