ngio 0.2.0a2__py3-none-any.whl → 0.5.0b4__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.
Files changed (106) hide show
  1. ngio/__init__.py +40 -12
  2. ngio/common/__init__.py +16 -32
  3. ngio/common/_dimensions.py +270 -48
  4. ngio/common/_masking_roi.py +153 -0
  5. ngio/common/_pyramid.py +267 -73
  6. ngio/common/_roi.py +290 -66
  7. ngio/common/_synt_images_utils.py +101 -0
  8. ngio/common/_zoom.py +54 -22
  9. ngio/experimental/__init__.py +5 -0
  10. ngio/experimental/iterators/__init__.py +15 -0
  11. ngio/experimental/iterators/_abstract_iterator.py +390 -0
  12. ngio/experimental/iterators/_feature.py +189 -0
  13. ngio/experimental/iterators/_image_processing.py +130 -0
  14. ngio/experimental/iterators/_mappers.py +48 -0
  15. ngio/experimental/iterators/_rois_utils.py +126 -0
  16. ngio/experimental/iterators/_segmentation.py +235 -0
  17. ngio/hcs/__init__.py +17 -58
  18. ngio/hcs/_plate.py +1354 -0
  19. ngio/images/__init__.py +30 -9
  20. ngio/images/_abstract_image.py +968 -0
  21. ngio/images/_create_synt_container.py +132 -0
  22. ngio/images/_create_utils.py +423 -0
  23. ngio/images/_image.py +926 -0
  24. ngio/images/_label.py +417 -0
  25. ngio/images/_masked_image.py +531 -0
  26. ngio/images/_ome_zarr_container.py +1235 -0
  27. ngio/images/_table_ops.py +471 -0
  28. ngio/io_pipes/__init__.py +75 -0
  29. ngio/io_pipes/_io_pipes.py +361 -0
  30. ngio/io_pipes/_io_pipes_masked.py +488 -0
  31. ngio/io_pipes/_io_pipes_roi.py +146 -0
  32. ngio/io_pipes/_io_pipes_types.py +56 -0
  33. ngio/io_pipes/_match_shape.py +377 -0
  34. ngio/io_pipes/_ops_axes.py +344 -0
  35. ngio/io_pipes/_ops_slices.py +411 -0
  36. ngio/io_pipes/_ops_slices_utils.py +199 -0
  37. ngio/io_pipes/_ops_transforms.py +104 -0
  38. ngio/io_pipes/_zoom_transform.py +180 -0
  39. ngio/ome_zarr_meta/__init__.py +39 -15
  40. ngio/ome_zarr_meta/_meta_handlers.py +490 -96
  41. ngio/ome_zarr_meta/ngio_specs/__init__.py +24 -10
  42. ngio/ome_zarr_meta/ngio_specs/_axes.py +268 -234
  43. ngio/ome_zarr_meta/ngio_specs/_channels.py +125 -41
  44. ngio/ome_zarr_meta/ngio_specs/_dataset.py +42 -87
  45. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +536 -2
  46. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +202 -198
  47. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +72 -34
  48. ngio/ome_zarr_meta/v04/__init__.py +21 -5
  49. ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
  50. ngio/ome_zarr_meta/v04/{_v04_spec_utils.py → _v04_spec.py} +151 -90
  51. ngio/ome_zarr_meta/v05/__init__.py +27 -0
  52. ngio/ome_zarr_meta/v05/_custom_models.py +18 -0
  53. ngio/ome_zarr_meta/v05/_v05_spec.py +511 -0
  54. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/mask.png +0 -0
  55. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
  56. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/raw.jpg +0 -0
  57. ngio/resources/__init__.py +55 -0
  58. ngio/resources/resource_model.py +36 -0
  59. ngio/tables/__init__.py +20 -4
  60. ngio/tables/_abstract_table.py +270 -0
  61. ngio/tables/_tables_container.py +449 -0
  62. ngio/tables/backends/__init__.py +50 -1
  63. ngio/tables/backends/_abstract_backend.py +200 -31
  64. ngio/tables/backends/_anndata.py +139 -0
  65. ngio/tables/backends/_anndata_utils.py +10 -114
  66. ngio/tables/backends/_csv.py +19 -0
  67. ngio/tables/backends/_json.py +92 -0
  68. ngio/tables/backends/_parquet.py +19 -0
  69. ngio/tables/backends/_py_arrow_backends.py +222 -0
  70. ngio/tables/backends/_table_backends.py +162 -38
  71. ngio/tables/backends/_utils.py +608 -0
  72. ngio/tables/v1/__init__.py +19 -4
  73. ngio/tables/v1/_condition_table.py +71 -0
  74. ngio/tables/v1/_feature_table.py +79 -115
  75. ngio/tables/v1/_generic_table.py +21 -90
  76. ngio/tables/v1/_roi_table.py +486 -137
  77. ngio/transforms/__init__.py +5 -0
  78. ngio/transforms/_zoom.py +19 -0
  79. ngio/utils/__init__.py +16 -14
  80. ngio/utils/_cache.py +48 -0
  81. ngio/utils/_datasets.py +121 -13
  82. ngio/utils/_fractal_fsspec_store.py +42 -0
  83. ngio/utils/_zarr_utils.py +374 -218
  84. ngio-0.5.0b4.dist-info/METADATA +147 -0
  85. ngio-0.5.0b4.dist-info/RECORD +88 -0
  86. {ngio-0.2.0a2.dist-info → ngio-0.5.0b4.dist-info}/WHEEL +1 -1
  87. ngio/common/_array_pipe.py +0 -160
  88. ngio/common/_axes_transforms.py +0 -63
  89. ngio/common/_common_types.py +0 -5
  90. ngio/common/_slicer.py +0 -97
  91. ngio/images/abstract_image.py +0 -240
  92. ngio/images/create.py +0 -251
  93. ngio/images/image.py +0 -389
  94. ngio/images/label.py +0 -236
  95. ngio/images/omezarr_container.py +0 -535
  96. ngio/ome_zarr_meta/_generic_handlers.py +0 -320
  97. ngio/ome_zarr_meta/v04/_meta_handlers.py +0 -54
  98. ngio/tables/_validators.py +0 -192
  99. ngio/tables/backends/_anndata_v1.py +0 -75
  100. ngio/tables/backends/_json_v1.py +0 -56
  101. ngio/tables/tables_container.py +0 -300
  102. ngio/tables/v1/_masking_roi_table.py +0 -175
  103. ngio/utils/_logger.py +0 -29
  104. ngio-0.2.0a2.dist-info/METADATA +0 -95
  105. ngio-0.2.0a2.dist-info/RECORD +0 -53
  106. {ngio-0.2.0a2.dist-info → ngio-0.5.0b4.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,15 @@
1
1
  """Fractal internal module for axes handling."""
2
2
 
3
- from collections.abc import Collection
3
+ from collections.abc import Sequence
4
4
  from enum import Enum
5
- from logging import Logger
6
- from typing import TypeVar
5
+ from typing import Literal, TypeAlias, TypeVar
7
6
 
8
- import numpy as np
9
7
  from pydantic import BaseModel, ConfigDict, Field
10
8
 
11
9
  from ngio.utils import NgioValidationError, NgioValueError
12
10
 
13
- logger = Logger(__name__)
14
-
15
11
  T = TypeVar("T")
12
+ SlicingType: TypeAlias = slice | tuple[int, ...] | int
16
13
 
17
14
  ################################################################################################
18
15
  #
@@ -32,98 +29,94 @@ class AxisType(str, Enum):
32
29
  space = "space"
33
30
 
34
31
 
35
- class SpaceUnits(str, Enum):
36
- """Allowed space units."""
37
-
38
- nanometer = "nanometer"
39
- nm = "nm"
40
- micrometer = "micrometer"
41
- um = "um"
42
- millimeter = "millimeter"
43
- mm = "mm"
44
- centimeter = "centimeter"
45
- cm = "cm"
46
-
47
- @classmethod
48
- def default(cls) -> "SpaceUnits":
49
- return SpaceUnits.um
50
-
51
-
52
- class TimeUnits(str, Enum):
53
- """Allowed time units."""
54
-
55
- seconds = "seconds"
56
- s = "s"
57
-
58
- @classmethod
59
- def default(cls) -> "TimeUnits":
60
- return TimeUnits.s
32
+ SpaceUnits = Literal[
33
+ "micrometer",
34
+ "nanometer",
35
+ "angstrom",
36
+ "picometer",
37
+ "millimeter",
38
+ "centimeter",
39
+ "decimeter",
40
+ "meter",
41
+ "inch",
42
+ "foot",
43
+ "yard",
44
+ "mile",
45
+ "kilometer",
46
+ "hectometer",
47
+ "megameter",
48
+ "gigameter",
49
+ "terameter",
50
+ "petameter",
51
+ "exameter",
52
+ "parsec",
53
+ "femtometer",
54
+ "attometer",
55
+ "zeptometer",
56
+ "yoctometer",
57
+ "zettameter",
58
+ "yottameter",
59
+ ]
60
+ DefaultSpaceUnit = "micrometer"
61
+
62
+ TimeUnits = Literal[
63
+ "attosecond",
64
+ "centisecond",
65
+ "day",
66
+ "decisecond",
67
+ "exasecond",
68
+ "femtosecond",
69
+ "gigasecond",
70
+ "hectosecond",
71
+ "hour",
72
+ "kilosecond",
73
+ "megasecond",
74
+ "microsecond",
75
+ "millisecond",
76
+ "minute",
77
+ "nanosecond",
78
+ "petasecond",
79
+ "picosecond",
80
+ "second",
81
+ "terasecond",
82
+ "yoctosecond",
83
+ "yottasecond",
84
+ "zeptosecond",
85
+ "zettasecond",
86
+ ]
87
+ DefaultTimeUnit = "second"
61
88
 
62
89
 
63
90
  class Axis(BaseModel):
64
91
  """Axis infos model."""
65
92
 
66
- on_disk_name: str
67
- unit: SpaceUnits | TimeUnits | None = None
93
+ name: str
94
+ unit: str | None = None
68
95
  axis_type: AxisType | None = None
69
96
 
70
97
  model_config = ConfigDict(extra="forbid", frozen=True)
71
98
 
72
99
  def implicit_type_cast(self, cast_type: AxisType) -> "Axis":
73
- if self.axis_type != cast_type:
74
- logger.warning(
75
- f"Axis {self.on_disk_name} has type {self.axis_type}. "
76
- f"Casting to {cast_type}."
77
- )
78
- new_axis = Axis(
79
- on_disk_name=self.on_disk_name, axis_type=cast_type, unit=self.unit
80
- )
81
- if cast_type == AxisType.time and not isinstance(self.unit, TimeUnits):
82
- logger.warning(
83
- f"Time axis {self.on_disk_name} has unit {self.unit}. "
84
- f"Casting to {TimeUnits.default()}."
85
- )
86
- new_axis = Axis(
87
- on_disk_name=self.on_disk_name,
88
- axis_type=AxisType.time,
89
- unit=TimeUnits.default(),
90
- )
91
- elif cast_type == AxisType.space and not isinstance(self.unit, SpaceUnits):
92
- logger.warning(
93
- f"Space axis {self.on_disk_name} has unit {self.unit}. "
94
- f"Casting to {SpaceUnits.default()}."
95
- )
96
- new_axis = Axis(
97
- on_disk_name=self.on_disk_name,
98
- axis_type=AxisType.space,
99
- unit=SpaceUnits.default(),
100
- )
101
- elif cast_type == AxisType.channel and self.unit is not None:
102
- logger.warning(
103
- f"Channel axis {self.on_disk_name} has unit {self.unit}. Removing unit."
104
- )
105
- new_axis = Axis(
106
- on_disk_name=self.on_disk_name,
107
- axis_type=AxisType.channel,
108
- unit=None,
109
- )
110
- return new_axis
100
+ unit = self.unit
101
+ if cast_type == AxisType.time and unit is None:
102
+ unit = DefaultTimeUnit
103
+
104
+ if cast_type == AxisType.space and unit is None:
105
+ unit = DefaultSpaceUnit
106
+
107
+ return Axis(name=self.name, axis_type=cast_type, unit=unit)
111
108
 
112
109
  def canonical_axis_cast(self, canonical_name: str) -> "Axis":
113
110
  """Cast the implicit axis to the correct type."""
114
111
  match canonical_name:
115
112
  case "t":
116
- if self.axis_type != AxisType.time or not isinstance(
117
- self.unit, TimeUnits
118
- ):
113
+ if self.axis_type != AxisType.time or self.unit is None:
119
114
  return self.implicit_type_cast(AxisType.time)
120
115
  case "c":
121
- if self.axis_type != AxisType.channel or self.unit is not None:
116
+ if self.axis_type != AxisType.channel:
122
117
  return self.implicit_type_cast(AxisType.channel)
123
118
  case "z" | "y" | "x":
124
- if self.axis_type != AxisType.space or not isinstance(
125
- self.unit, SpaceUnits
126
- ):
119
+ if self.axis_type != AxisType.space or self.unit is None:
127
120
  return self.implicit_type_cast(AxisType.space)
128
121
  return self
129
122
 
@@ -163,10 +156,40 @@ class AxesSetup(BaseModel):
163
156
 
164
157
  model_config = ConfigDict(extra="forbid", frozen=True)
165
158
 
166
-
167
- def _check_unique_names(axes: Collection[Axis]):
159
+ def canonical_map(self) -> dict[str, str]:
160
+ """Get the canonical map of axes."""
161
+ return {
162
+ "t": self.t,
163
+ "c": self.c,
164
+ "z": self.z,
165
+ "y": self.y,
166
+ "x": self.x,
167
+ }
168
+
169
+ def get_on_disk_name(self, canonical_name: str) -> str | None:
170
+ """Get the on disk name of the axis by its canonical name."""
171
+ canonical_map = self.canonical_map()
172
+ return canonical_map.get(canonical_name, None)
173
+
174
+ def inverse_canonical_map(self) -> dict[str, str]:
175
+ """Get the on disk map of axes."""
176
+ return {
177
+ self.t: "t",
178
+ self.c: "c",
179
+ self.z: "z",
180
+ self.y: "y",
181
+ self.x: "x",
182
+ }
183
+
184
+ def get_canonical_name(self, on_disk_name: str) -> str | None:
185
+ """Get the canonical name of the axis by its on disk name."""
186
+ inv_map = self.inverse_canonical_map()
187
+ return inv_map.get(on_disk_name, None)
188
+
189
+
190
+ def _check_unique_names(axes: Sequence[Axis]):
168
191
  """Check if all axes on disk have unique names."""
169
- names = [ax.on_disk_name for ax in axes]
192
+ names = [ax.name for ax in axes]
170
193
  if len(set(names)) != len(names):
171
194
  duplicates = {item for item in names if names.count(item) > 1}
172
195
  raise NgioValidationError(
@@ -183,41 +206,41 @@ def _check_non_canonical_axes(axes_setup: AxesSetup, allow_non_canonical_axes: b
183
206
  )
184
207
 
185
208
 
186
- def _check_axes_validity(axes: Collection[Axis], axes_setup: AxesSetup):
209
+ def _check_axes_validity(axes: Sequence[Axis], axes_setup: AxesSetup):
187
210
  """Check if all axes are valid."""
188
211
  _axes_setup = axes_setup.model_dump(exclude={"others"})
189
212
  _all_known_axes = [*_axes_setup.values(), *axes_setup.others]
190
213
  for ax in axes:
191
- if ax.on_disk_name not in _all_known_axes:
214
+ if ax.name not in _all_known_axes:
192
215
  raise NgioValidationError(
193
- f"Invalid axis name '{ax.on_disk_name}'. "
194
- f"Please correct map `{ax.on_disk_name}` "
216
+ f"Invalid axis name '{ax.name}'. "
217
+ f"Please correct map `{ax.name}` "
195
218
  f"using the AxesSetup model {axes_setup}"
196
219
  )
197
220
 
198
221
 
199
222
  def _check_canonical_order(
200
- axes: Collection[Axis], axes_setup: AxesSetup, strict_canonical_order: bool
223
+ axes: Sequence[Axis], axes_setup: AxesSetup, strict_canonical_order: bool
201
224
  ):
202
225
  """Check if the axes are in the canonical order."""
203
226
  if not strict_canonical_order:
204
227
  return
205
- _on_disk_names = [ax.on_disk_name for ax in axes]
228
+ _names = [ax.name for ax in axes]
206
229
  _canonical_order = []
207
230
  for name in canonical_axes_order():
208
231
  mapped_name = getattr(axes_setup, name)
209
- if mapped_name in _on_disk_names:
232
+ if mapped_name in _names:
210
233
  _canonical_order.append(mapped_name)
211
234
 
212
- if _on_disk_names != _canonical_order:
235
+ if _names != _canonical_order:
213
236
  raise NgioValidationError(
214
237
  f"Invalid axes order. The axes must be in the canonical order. "
215
- f"Expected {_canonical_order}, but found {_on_disk_names}"
238
+ f"Expected {_canonical_order}, but found {_names}"
216
239
  )
217
240
 
218
241
 
219
242
  def validate_axes(
220
- axes: Collection[Axis],
243
+ axes: Sequence[Axis],
221
244
  axes_setup: AxesSetup,
222
245
  allow_non_canonical_axes: bool = False,
223
246
  strict_canonical_order: bool = False,
@@ -239,33 +262,20 @@ def validate_axes(
239
262
  )
240
263
 
241
264
 
242
- class AxesTransformation(BaseModel):
243
- model_config = ConfigDict(extra="forbid", frozen=True, arbitrary_types_allowed=True)
244
-
245
-
246
- class AxesTranspose(AxesTransformation):
247
- axes: tuple[int, ...]
248
-
249
-
250
- class AxesExpand(AxesTransformation):
251
- axes: tuple[int, ...]
252
-
253
-
254
- class AxesSqueeze(AxesTransformation):
255
- axes: tuple[int, ...]
256
-
257
-
258
- class AxesMapper:
259
- """Map on disk axes to canonical axes.
260
-
261
- This class is used to map the on disk axes to the canonical axes.
265
+ class AxesHandler:
266
+ """This class is used to handle and operate on OME-Zarr axes.
262
267
 
268
+ The class also provides:
269
+ - methods to reorder, squeeze and expand axes.
270
+ - methods to validate the axes.
271
+ - methods to get axis by name or index.
272
+ - methods to operate on the axes.
263
273
  """
264
274
 
265
275
  def __init__(
266
276
  self,
267
277
  # spec dictated args
268
- on_disk_axes: Collection[Axis],
278
+ axes: Sequence[Axis],
269
279
  # user defined args
270
280
  axes_setup: AxesSetup | None = None,
271
281
  allow_non_canonical_axes: bool = False,
@@ -274,7 +284,7 @@ class AxesMapper:
274
284
  """Create a new AxesMapper object.
275
285
 
276
286
  Args:
277
- on_disk_axes (list[Axis]): The axes on disk.
287
+ axes (list[Axis]): The axes on disk.
278
288
  axes_setup (AxesSetup, optional): The axis setup. Defaults to None.
279
289
  allow_non_canonical_axes (bool, optional): Allow non canonical axes.
280
290
  strict_canonical_order (bool, optional): Check if the axes are in the
@@ -283,7 +293,7 @@ class AxesMapper:
283
293
  axes_setup = axes_setup if axes_setup is not None else AxesSetup()
284
294
 
285
295
  validate_axes(
286
- axes=on_disk_axes,
296
+ axes=axes,
287
297
  axes_setup=axes_setup,
288
298
  allow_non_canonical_axes=allow_non_canonical_axes,
289
299
  strict_canonical_order=strict_canonical_order,
@@ -293,163 +303,181 @@ class AxesMapper:
293
303
  self._strict_canonical_order = strict_canonical_order
294
304
 
295
305
  self._canonical_order = canonical_axes_order()
296
- self._extended_canonical_order = [*axes_setup.others, *self._canonical_order]
297
306
 
298
- self._on_disk_axes = on_disk_axes
307
+ self._axes = axes
299
308
  self._axes_setup = axes_setup
300
309
 
301
- self._name_mapping = self._compute_name_mapping()
302
310
  self._index_mapping = self._compute_index_mapping()
303
311
 
304
312
  # Validate the axes type and cast them if necessary
305
313
  # This needs to be done after the name mapping is computed
306
- self.validate_axex_type()
307
-
308
- def _compute_name_mapping(self):
309
- """Compute the name mapping.
310
-
311
- The name mapping is a dictionary with keys the canonical axes names
312
- and values the on disk axes names.
313
- """
314
- _name_mapping = {}
315
- axis_setup_dict = self._axes_setup.model_dump(exclude={"others"})
316
- _on_disk_names = self.on_disk_axes_names
317
- for canonical_key, on_disk_value in axis_setup_dict.items():
318
- if on_disk_value in _on_disk_names:
319
- _name_mapping[canonical_key] = on_disk_value
320
- else:
321
- _name_mapping[canonical_key] = None
322
-
323
- for on_disk_name in _on_disk_names:
324
- if on_disk_name not in _name_mapping.keys():
325
- _name_mapping[on_disk_name] = on_disk_name
326
-
327
- for other in self._axes_setup.others:
328
- if other not in _name_mapping.keys():
329
- _name_mapping[other] = None
330
- return _name_mapping
314
+ self.validate_axes_type()
331
315
 
332
316
  def _compute_index_mapping(self):
333
317
  """Compute the index mapping.
334
318
 
335
319
  The index mapping is a dictionary with keys the canonical axes names
336
320
  and values the on disk axes index.
321
+
322
+ Example:
323
+ If the on disk axes are ['channel', 't', 'z', 'y', 'x'],
324
+ the index mapping will be:
325
+ {
326
+ 'c': 0,
327
+ 'channel': 0,
328
+ 't': 1,
329
+ 'z': 2,
330
+ 'y': 3,
331
+ 'x': 4,
332
+ }
337
333
  """
338
334
  _index_mapping = {}
339
- for canonical_key, on_disk_value in self._name_mapping.items():
340
- if on_disk_value is not None:
341
- _index_mapping[canonical_key] = self.on_disk_axes_names.index(
342
- on_disk_value
343
- )
344
- else:
345
- _index_mapping[canonical_key] = None
335
+ for i, ax in enumerate(self.axes_names):
336
+ _index_mapping[ax] = i
337
+ # If the axis is not in the canonical order we also set it.
338
+ canonical_map = self._axes_setup.canonical_map()
339
+ for canonical_name, on_disk_name in canonical_map.items():
340
+ if on_disk_name in _index_mapping.keys():
341
+ _index_mapping[canonical_name] = _index_mapping[on_disk_name]
346
342
  return _index_mapping
347
343
 
348
344
  @property
349
- def on_disk_axes(self) -> list[Axis]:
350
- return list(self._on_disk_axes)
345
+ def axes_setup(self) -> AxesSetup:
346
+ """Return the axes setup."""
347
+ return self._axes_setup
348
+
349
+ @property
350
+ def axes(self) -> tuple[Axis, ...]:
351
+ return tuple(self._axes)
352
+
353
+ @property
354
+ def axes_names(self) -> tuple[str, ...]:
355
+ """On disk axes names."""
356
+ return tuple(ax.name for ax in self._axes)
351
357
 
352
358
  @property
353
- def on_disk_axes_names(self) -> list[str]:
354
- return [ax.on_disk_name for ax in self._on_disk_axes]
359
+ def allow_non_canonical_axes(self) -> bool:
360
+ """Return if non canonical axes are allowed."""
361
+ return self._allow_non_canonical_axes
362
+
363
+ @property
364
+ def strict_canonical_order(self) -> bool:
365
+ """Return if strict canonical order is enforced."""
366
+ return self._strict_canonical_order
367
+
368
+ @property
369
+ def space_unit(self) -> str | None:
370
+ """Return the space unit for a given axis."""
371
+ x_axis = self.get_axis("x")
372
+ y_axis = self.get_axis("y")
373
+
374
+ if x_axis is None or y_axis is None:
375
+ raise NgioValidationError(
376
+ "The dataset must have x and y axes to determine the space unit."
377
+ )
378
+
379
+ if x_axis.unit == y_axis.unit:
380
+ return x_axis.unit
381
+ else:
382
+ raise NgioValidationError(
383
+ "Inconsistent space units. "
384
+ f"x={x_axis.unit} and y={y_axis.unit} should have the same unit."
385
+ )
386
+
387
+ @property
388
+ def time_unit(self) -> str | None:
389
+ """Return the time unit for a given axis."""
390
+ t_axis = self.get_axis("t")
391
+ if t_axis is None:
392
+ return None
393
+ return t_axis.unit
394
+
395
+ def to_units(
396
+ self,
397
+ *,
398
+ space_unit: SpaceUnits = DefaultSpaceUnit,
399
+ time_unit: TimeUnits = DefaultTimeUnit,
400
+ ) -> "AxesHandler":
401
+ """Convert the pixel size to the given units.
402
+
403
+ Args:
404
+ space_unit(str): The space unit to convert to.
405
+ time_unit(str): The time unit to convert to.
406
+ """
407
+ new_axes = []
408
+ for ax in self.axes:
409
+ if ax.axis_type == AxisType.space:
410
+ new_ax = Axis(
411
+ name=ax.name,
412
+ axis_type=ax.axis_type,
413
+ unit=space_unit,
414
+ )
415
+ new_axes.append(new_ax)
416
+ elif ax.axis_type == AxisType.time:
417
+ new_ax = Axis(name=ax.name, axis_type=ax.axis_type, unit=time_unit)
418
+ new_axes.append(new_ax)
419
+ else:
420
+ new_axes.append(ax)
421
+
422
+ return AxesHandler(
423
+ axes=new_axes,
424
+ axes_setup=self.axes_setup,
425
+ allow_non_canonical_axes=self.allow_non_canonical_axes,
426
+ strict_canonical_order=self.strict_canonical_order,
427
+ )
355
428
 
356
429
  def get_index(self, name: str) -> int | None:
357
430
  """Get the index of the axis by name."""
358
- if name not in self._index_mapping.keys():
359
- raise NgioValueError(
360
- f"Invalid axis name '{name}'. "
361
- f"Possible values are {self._index_mapping.keys()}"
362
- )
363
- return self._index_mapping[name]
431
+ return self._index_mapping.get(name, None)
432
+
433
+ def has_axis(self, axis_name: str) -> bool:
434
+ """Return whether the axis exists."""
435
+ index = self.get_index(axis_name)
436
+ if index is None:
437
+ return False
438
+ return True
439
+
440
+ def get_canonical_name(self, name: str) -> str | None:
441
+ """Get the canonical name of the axis by name."""
442
+ return self._axes_setup.get_canonical_name(name)
364
443
 
365
444
  def get_axis(self, name: str) -> Axis | None:
366
445
  """Get the axis object by name."""
367
446
  index = self.get_index(name)
368
447
  if index is None:
369
448
  return None
370
- return self.on_disk_axes[index]
449
+ return self.axes[index]
371
450
 
372
- def validate_axex_type(self):
451
+ def validate_axes_type(self):
373
452
  """Validate the axes type.
374
453
 
375
454
  If the axes type is not correct, a warning is issued.
376
455
  and the axis is implicitly cast to the correct type.
377
456
  """
378
457
  new_axes = []
379
- for axes in self.on_disk_axes:
458
+ for axes in self.axes:
380
459
  for name in self._canonical_order:
381
460
  if axes == self.get_axis(name):
382
461
  new_axes.append(axes.canonical_axis_cast(name))
383
462
  break
384
463
  else:
385
464
  new_axes.append(axes)
386
- self._on_disk_axes = new_axes
387
-
388
- def _change_order(
389
- self, names: Collection[str]
390
- ) -> tuple[tuple[int, ...], tuple[int, ...]]:
391
- unique_names = set()
392
- for name in names:
393
- if name not in self._index_mapping.keys():
394
- raise NgioValueError(
395
- f"Invalid axis name '{name}'. "
396
- f"Possible values are {self._index_mapping.keys()}"
397
- )
398
- _unique_name = self._name_mapping.get(name)
399
- if _unique_name is None:
400
- continue
401
- if _unique_name in unique_names:
402
- raise NgioValueError(
403
- f"Duplicate axis name, two or more '{_unique_name}' were found. "
404
- f"Please provide unique names."
405
- )
406
- unique_names.add(_unique_name)
465
+ self._axes = new_axes
407
466
 
408
- if len(self.on_disk_axes_names) > len(unique_names):
409
- missing_names = set(self.on_disk_axes_names) - unique_names
410
- raise NgioValueError(
411
- f"Some axes where not queried. "
412
- f"Please provide the following missing axes {missing_names}"
413
- )
414
- _indices, _insert = [], []
415
- for i, name in enumerate(names):
416
- _index = self._index_mapping[name]
417
- if _index is None:
418
- _insert.append(i)
419
- else:
420
- _indices.append(self._index_mapping[name])
421
- return tuple(_indices), tuple(_insert)
422
-
423
- def to_order(self, names: Collection[str]) -> tuple[AxesTransformation, ...]:
424
- """Get the new order of the axes."""
425
- _indices, _insert = self._change_order(names)
426
- return AxesTranspose(axes=_indices), AxesExpand(axes=_insert)
427
-
428
- def from_order(self, names: Collection[str]) -> tuple[AxesTransformation, ...]:
429
- """Get the new order of the axes."""
430
- _indices, _insert = self._change_order(names)
431
- # Inverse transpose is just the transpose with the inverse indices
432
- _reverse_indices = tuple(np.argsort(_indices))
433
- return AxesSqueeze(axes=_insert), AxesTranspose(axes=_reverse_indices)
434
-
435
- def to_canonical(self) -> tuple[AxesTransformation, ...]:
436
- """Get the new order of the axes."""
437
- return self.to_order(self._extended_canonical_order)
438
-
439
- def from_canonical(self) -> tuple[AxesTransformation, ...]:
440
- """Get the new order of the axes."""
441
- return self.from_order(self._extended_canonical_order)
442
-
443
-
444
- def canonical_axes(
445
- axes_names: Collection[str],
446
- space_units: SpaceUnits | None = None,
447
- time_units: TimeUnits | None = None,
448
- ) -> list[Axis]:
467
+
468
+ def build_canonical_axes_handler(
469
+ axes_names: Sequence[str],
470
+ space_units: SpaceUnits | str | None = DefaultSpaceUnit,
471
+ time_units: TimeUnits | str | None = DefaultTimeUnit,
472
+ # user defined args
473
+ axes_setup: AxesSetup | None = None,
474
+ allow_non_canonical_axes: bool = False,
475
+ strict_canonical_order: bool = False,
476
+ ) -> AxesHandler:
449
477
  """Create a new canonical axes mapper.
450
478
 
451
479
  Args:
452
- axes_names (Collection[str] | int): The axes names on disk.
480
+ axes_names (Sequence[str] | int): The axes names on disk.
453
481
  - The axes should be in ['t', 'c', 'z', 'y', 'x']
454
482
  - The axes should be in strict canonical order.
455
483
  - If an integer is provided, the axes are created from the last axis
@@ -457,25 +485,31 @@ def canonical_axes(
457
485
  e.g. 3 -> ["z", "y", "x"]
458
486
  space_units (SpaceUnits, optional): The space units. Defaults to None.
459
487
  time_units (TimeUnits, optional): The time units. Defaults to None.
488
+ axes_setup (AxesSetup, optional): The axis setup. Defaults to None.
489
+ allow_non_canonical_axes (bool, optional): Allow non canonical axes.
490
+ Defaults to False.
491
+ strict_canonical_order (bool, optional): Check if the axes are in the
492
+ canonical order. Defaults to False.
460
493
 
461
494
  """
462
495
  axes = []
463
496
  for name in axes_names:
464
497
  match name:
465
498
  case "t":
466
- axes.append(
467
- Axis(on_disk_name=name, axis_type=AxisType.time, unit=time_units)
468
- )
499
+ axes.append(Axis(name=name, axis_type=AxisType.time, unit=time_units))
469
500
  case "c":
470
- axes.append(Axis(on_disk_name=name, axis_type=AxisType.channel))
501
+ axes.append(Axis(name=name, axis_type=AxisType.channel))
471
502
  case "z" | "y" | "x":
472
- axes.append(
473
- Axis(on_disk_name=name, axis_type=AxisType.space, unit=space_units)
474
- )
503
+ axes.append(Axis(name=name, axis_type=AxisType.space, unit=space_units))
475
504
  case _:
476
505
  raise NgioValueError(
477
506
  f"Invalid axis name '{name}'. "
478
507
  "Only 't', 'c', 'z', 'y', 'x' are allowed."
479
508
  )
480
509
 
481
- return axes
510
+ return AxesHandler(
511
+ axes=axes,
512
+ axes_setup=axes_setup,
513
+ allow_non_canonical_axes=allow_non_canonical_axes,
514
+ strict_canonical_order=strict_canonical_order,
515
+ )