ngio 0.2.1__py3-none-any.whl → 0.2.3__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 (38) hide show
  1. ngio/__init__.py +20 -2
  2. ngio/common/_pyramid.py +5 -1
  3. ngio/common/_roi.py +2 -2
  4. ngio/hcs/__init__.py +16 -2
  5. ngio/hcs/plate.py +496 -18
  6. ngio/images/abstract_image.py +11 -0
  7. ngio/images/create.py +25 -36
  8. ngio/images/image.py +80 -6
  9. ngio/images/label.py +38 -9
  10. ngio/images/ome_zarr_container.py +70 -33
  11. ngio/ome_zarr_meta/__init__.py +5 -3
  12. ngio/ome_zarr_meta/ngio_specs/__init__.py +10 -2
  13. ngio/ome_zarr_meta/ngio_specs/_axes.py +90 -65
  14. ngio/ome_zarr_meta/ngio_specs/_dataset.py +46 -8
  15. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +242 -70
  16. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +49 -11
  17. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +28 -11
  18. ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
  19. ngio/ome_zarr_meta/v04/_v04_spec_utils.py +2 -2
  20. ngio/tables/_validators.py +1 -83
  21. ngio/tables/backends/__init__.py +27 -1
  22. ngio/tables/backends/_abstract_backend.py +207 -22
  23. ngio/tables/backends/_anndata_utils.py +3 -109
  24. ngio/tables/backends/_anndata_v1.py +43 -46
  25. ngio/tables/backends/_csv_v1.py +162 -0
  26. ngio/tables/backends/_json_v1.py +54 -18
  27. ngio/tables/backends/_table_backends.py +98 -18
  28. ngio/tables/backends/_utils.py +458 -0
  29. ngio/tables/tables_container.py +3 -1
  30. ngio/tables/v1/_feature_table.py +20 -11
  31. ngio/tables/v1/_generic_table.py +20 -15
  32. ngio/tables/v1/_roi_table.py +7 -9
  33. ngio/utils/_zarr_utils.py +46 -32
  34. {ngio-0.2.1.dist-info → ngio-0.2.3.dist-info}/METADATA +3 -1
  35. ngio-0.2.3.dist-info/RECORD +57 -0
  36. ngio-0.2.1.dist-info/RECORD +0 -54
  37. {ngio-0.2.1.dist-info → ngio-0.2.3.dist-info}/WHEEL +0 -0
  38. {ngio-0.2.1.dist-info → ngio-0.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,7 @@
1
1
  """HCS (High Content Screening) specific metadata classes for NGIO."""
2
2
 
3
+ from typing import Annotated
4
+
3
5
  from ome_zarr_models.v04.hcs import HCSAttrs
4
6
  from ome_zarr_models.v04.plate import (
5
7
  Acquisition,
@@ -8,12 +10,34 @@ from ome_zarr_models.v04.plate import (
8
10
  Row,
9
11
  WellInPlate,
10
12
  )
11
- from ome_zarr_models.v04.well import WellAttrs
12
- from ome_zarr_models.v04.well_types import WellImage, WellMeta
13
- from pydantic import BaseModel
14
-
15
- from ngio.ome_zarr_meta.ngio_specs._ngio_image import NgffVersion
16
- from ngio.utils import NgioValueError
13
+ from ome_zarr_models.v04.well import WellAttrs as WellAttrs04
14
+ from ome_zarr_models.v04.well_types import WellImage as WellImage04
15
+ from ome_zarr_models.v04.well_types import WellMeta as WellMeta04
16
+ from pydantic import BaseModel, SkipValidation, field_serializer
17
+
18
+ from ngio.ome_zarr_meta.ngio_specs._ngio_image import DefaultNgffVersion, NgffVersions
19
+ from ngio.utils import NgioValueError, ngio_logger
20
+
21
+
22
+ def path_in_well_validation(path: str) -> str:
23
+ """Validate the path in the well."""
24
+ if path.find("_") != -1:
25
+ # Remove underscores from the path
26
+ # This is a custom serialization step
27
+ old_value = path
28
+ path = path.replace("_", "")
29
+ ngio_logger.warning(
30
+ f"Underscores in well-paths are not allowed. "
31
+ f"Path '{old_value}' was changed to '{path}'"
32
+ f" to comply with the specification."
33
+ )
34
+ # Check if the value contains only alphanumeric characters
35
+ if not path.isalnum():
36
+ raise NgioValueError(
37
+ f"Path '{path}' contains non-alphanumeric characters. "
38
+ f"Only alphanumeric characters are allowed."
39
+ )
40
+ return path
17
41
 
18
42
 
19
43
  class ImageInWellPath(BaseModel):
@@ -26,25 +50,55 @@ class ImageInWellPath(BaseModel):
26
50
  acquisition_name: str | None = None
27
51
 
28
52
 
29
- class NgioWellMeta(WellAttrs):
53
+ class CustomWellImage(WellImage04):
54
+ path: Annotated[str, SkipValidation]
55
+
56
+ @field_serializer("path")
57
+ def serialize_path(self, value: str) -> str:
58
+ """Custom serialization for the path."""
59
+ return path_in_well_validation(value)
60
+
61
+
62
+ class CustomWellMeta(WellMeta04):
63
+ images: list[CustomWellImage] # type: ignore[valid-type]
64
+
65
+
66
+ class CustomWellAttrs(WellAttrs04):
67
+ well: CustomWellMeta # type: ignore[valid-type]
68
+
69
+
70
+ class NgioWellMeta(CustomWellAttrs):
30
71
  """HCS well metadata."""
31
72
 
32
73
  @classmethod
33
74
  def default_init(
34
75
  cls,
35
- images: list[ImageInWellPath] | None = None,
36
- version: NgffVersion | None = None,
76
+ version: NgffVersions | None = None,
37
77
  ) -> "NgioWellMeta":
38
78
  if version is None:
39
- version = "0.4"
40
- well = cls(well=WellMeta(images=[], version=version))
41
- if images is None:
42
- return well
43
-
44
- for image in images:
45
- well = well.add_image(path=image.path, acquisition=image.acquisition_id)
79
+ version = DefaultNgffVersion
80
+ well = cls(well=CustomWellMeta(images=[], version=version))
46
81
  return well
47
82
 
83
+ @property
84
+ def acquisition_ids(self) -> list[int]:
85
+ """Return the acquisition ids in the well."""
86
+ acquisitions = []
87
+ for images in self.well.images:
88
+ if (
89
+ images.acquisition is not None
90
+ and images.acquisition not in acquisitions
91
+ ):
92
+ acquisitions.append(images.acquisition)
93
+ return acquisitions
94
+
95
+ def get_image_acquisition_id(self, image_path: str) -> int | None:
96
+ """Return the acquisition id for the given image path."""
97
+ for images in self.well.images:
98
+ if images.path == image_path:
99
+ return images.acquisition
100
+ raise NgioValueError(f"Image at path {image_path} not found in the well.")
101
+
48
102
  def paths(self, acquisition: int | None = None) -> list[str]:
49
103
  """Return the images paths in the well.
50
104
 
@@ -62,12 +116,16 @@ class NgioWellMeta(WellAttrs):
62
116
  if images.acquisition == acquisition
63
117
  ]
64
118
 
65
- def add_image(self, path: str, acquisition: int | None = None) -> "NgioWellMeta":
119
+ def add_image(
120
+ self, path: str, acquisition: int | None = None, strict: bool = True
121
+ ) -> "NgioWellMeta":
66
122
  """Add an image to the well.
67
123
 
68
124
  Args:
69
125
  path (str): The path of the image.
70
126
  acquisition (int | None): The acquisition id of the image.
127
+ strict (bool): If True, check if the image already exists in the well.
128
+ If False, do not check if the image already exists in the well.
71
129
  """
72
130
  list_of_images = self.well.images
73
131
  for image in list_of_images:
@@ -76,10 +134,20 @@ class NgioWellMeta(WellAttrs):
76
134
  f"Image at path {path} already exists in the well."
77
135
  )
78
136
 
79
- new_image = WellImage(path=path, acquisition=acquisition)
137
+ if (
138
+ strict
139
+ and (acquisition is not None)
140
+ and (acquisition not in self.acquisition_ids)
141
+ ):
142
+ raise NgioValueError(
143
+ f"Acquisition ID {acquisition} not found in well. "
144
+ "Please add it to the plate metadata first."
145
+ )
146
+
147
+ new_image = CustomWellImage(path=path, acquisition=acquisition)
80
148
  list_of_images.append(new_image)
81
149
  return NgioWellMeta(
82
- well=WellMeta(images=list_of_images, version=self.well.version)
150
+ well=CustomWellMeta(images=list_of_images, version=self.well.version)
83
151
  )
84
152
 
85
153
  def remove_image(self, path: str) -> "NgioWellMeta":
@@ -93,7 +161,9 @@ class NgioWellMeta(WellAttrs):
93
161
  if image.path == path:
94
162
  list_of_images.remove(image)
95
163
  return NgioWellMeta(
96
- well=WellMeta(images=list_of_images, version=self.well.version)
164
+ well=CustomWellMeta(
165
+ images=list_of_images, version=self.well.version
166
+ )
97
167
  )
98
168
  raise NgioValueError(f"Image at path {path} not found in the well.")
99
169
 
@@ -165,7 +235,7 @@ class NgioPlateMeta(HCSAttrs):
165
235
  cls,
166
236
  images: list[ImageInWellPath] | None = None,
167
237
  name: str | None = None,
168
- version: NgffVersion | None = None,
238
+ version: NgffVersions | None = None,
169
239
  ) -> "NgioPlateMeta":
170
240
  plate = cls(
171
241
  plate=Plate(
@@ -186,9 +256,12 @@ class NgioPlateMeta(HCSAttrs):
186
256
  plate = plate.add_well(
187
257
  row=image.row,
188
258
  column=image.column,
189
- acquisition_id=image.acquisition_id,
190
- acquisition_name=image.acquisition_name,
191
259
  )
260
+ if image.acquisition_id is not None:
261
+ plate = plate.add_acquisition(
262
+ acquisition_id=image.acquisition_id,
263
+ acquisition_name=image.acquisition_name,
264
+ )
192
265
  return plate
193
266
 
194
267
  @property
@@ -209,7 +282,7 @@ class NgioPlateMeta(HCSAttrs):
209
282
  return [acquisitions.name for acquisitions in self.plate.acquisitions]
210
283
 
211
284
  @property
212
- def acquisitions_ids(self) -> list[int]:
285
+ def acquisition_ids(self) -> list[int]:
213
286
  """Return the acquisitions ids in the plate."""
214
287
  if self.plate.acquisitions is None:
215
288
  return []
@@ -256,50 +329,94 @@ class NgioPlateMeta(HCSAttrs):
256
329
  f"Well at row {row} and column {column} not found in the plate."
257
330
  )
258
331
 
259
- def add_well(
260
- self,
261
- row: str,
262
- column: str | int,
263
- acquisition_id: int | None = None,
264
- acquisition_name: str | None = None,
265
- **acquisition_kwargs,
266
- ) -> "NgioPlateMeta":
267
- """Add an image to the well.
332
+ def add_row(self, row: str) -> "tuple[NgioPlateMeta, int]":
333
+ """Add a row to the plate.
268
334
 
269
335
  Args:
270
- row (str): The row of the well.
271
- column (str | int): The column of the well.
272
- acquisition_id (int | None): The acquisition id of the well.
273
- acquisition_name (str | None): The acquisition name of the well.
274
- **acquisition_kwargs: Additional acquisition metadata.
336
+ row (str): The row to add.
275
337
  """
276
338
  relabel_wells = False
277
339
 
278
340
  row_names = self.rows
279
341
  row_idx = _find_row_index(row_names, row)
280
- if row_idx is None:
281
- row_names.append(row)
282
- row_names.sort()
283
- row_idx = row_names.index(row)
284
- relabel_wells = True
342
+ if row_idx is not None:
343
+ # Nothing to do
344
+ return self, row_idx
345
+
346
+ row_names.append(row)
347
+ row_names.sort()
348
+ row_idx = row_names.index(row)
349
+ relabel_wells = True
285
350
 
286
351
  rows = [Row(name=row) for row in row_names]
287
352
 
353
+ if relabel_wells:
354
+ wells = _relabel_wells(self.plate.wells, rows, self.plate.columns)
355
+ else:
356
+ wells = self.plate.wells
357
+
358
+ new_plate = Plate(
359
+ rows=rows,
360
+ columns=self.plate.columns,
361
+ acquisitions=self.plate.acquisitions,
362
+ wells=wells,
363
+ field_count=self.plate.field_count,
364
+ version=self.plate.version,
365
+ )
366
+ return NgioPlateMeta(plate=new_plate), row_idx
367
+
368
+ def add_column(self, column: str | int) -> "tuple[NgioPlateMeta, int]":
369
+ """Add a column to the plate.
370
+
371
+ Args:
372
+ column (str | int): The column to add.
373
+ """
374
+ relabel_wells = False
375
+
288
376
  columns_names = self.columns
289
377
  column_idx = _find_column_index(columns_names, column)
290
- if column_idx is None:
291
- columns_names.append(_stringify_column(column))
292
- # sort as numbers
293
- columns_names.sort(key=lambda x: int(x))
294
- column_idx = columns_names.index(_stringify_column(column))
295
- relabel_wells = True
378
+ if column_idx is not None:
379
+ # Nothing to do
380
+ return self, column_idx
381
+
382
+ columns_names.append(_stringify_column(column))
383
+ # sort as numbers
384
+ columns_names.sort(key=lambda x: int(x))
385
+ column_idx = columns_names.index(_stringify_column(column))
386
+ relabel_wells = True
296
387
 
297
388
  columns = [Column(name=column) for column in columns_names]
298
389
 
299
- wells = self.plate.wells
300
390
  if relabel_wells:
301
- wells = _relabel_wells(wells, rows, columns)
391
+ wells = _relabel_wells(self.plate.wells, self.plate.rows, columns)
392
+ else:
393
+ wells = self.plate.wells
302
394
 
395
+ new_plate = Plate(
396
+ rows=self.plate.rows,
397
+ columns=columns,
398
+ acquisitions=self.plate.acquisitions,
399
+ wells=wells,
400
+ field_count=self.plate.field_count,
401
+ version=self.plate.version,
402
+ )
403
+ return NgioPlateMeta(plate=new_plate), column_idx
404
+
405
+ def add_well(
406
+ self,
407
+ row: str,
408
+ column: str | int,
409
+ ) -> "NgioPlateMeta":
410
+ """Add an image to the well.
411
+
412
+ Args:
413
+ row (str): The row of the well.
414
+ column (str | int): The column of the well.
415
+ """
416
+ plate, row_idx = self.add_row(row=row)
417
+ plate, column_idx = plate.add_column(column=column)
418
+
419
+ wells = plate.plate.wells
303
420
  for well_obj in wells:
304
421
  if well_obj.rowIndex == row_idx and well_obj.columnIndex == column_idx:
305
422
  break
@@ -312,28 +429,49 @@ class NgioPlateMeta(HCSAttrs):
312
429
  )
313
430
  )
314
431
 
432
+ new_plate = Plate(
433
+ rows=plate.plate.rows,
434
+ columns=plate.plate.columns,
435
+ acquisitions=plate.plate.acquisitions,
436
+ wells=wells,
437
+ field_count=plate.plate.field_count,
438
+ version=plate.plate.version,
439
+ )
440
+ return NgioPlateMeta(plate=new_plate)
441
+
442
+ def add_acquisition(
443
+ self,
444
+ acquisition_id: int,
445
+ acquisition_name: str | None = None,
446
+ **acquisition_kwargs,
447
+ ) -> "NgioPlateMeta":
448
+ """Add an acquisition to the plate.
449
+
450
+ Args:
451
+ acquisition_id (int): The acquisition id of the well.
452
+ acquisition_name (str | None): The acquisition name of the well.
453
+ **acquisition_kwargs: Additional acquisition metadata.
454
+ """
315
455
  acquisitions = self.plate.acquisitions
316
- if acquisition_id is not None:
317
- if acquisitions is None and len(wells) > 0:
318
- acquisitions = [Acquisition(id=0, name=acquisition_name)]
319
- elif acquisitions is None:
320
- acquisitions = []
321
-
322
- for acquisition_obj in acquisitions:
323
- if acquisition_obj.id == acquisition_id:
324
- break
325
- else:
326
- acquisitions.append(
327
- Acquisition(
328
- id=acquisition_id, name=acquisition_name, **acquisition_kwargs
329
- )
330
- )
456
+ if acquisitions is None:
457
+ acquisitions = []
458
+
459
+ for acquisition_obj in acquisitions:
460
+ if acquisition_obj.id == acquisition_id:
461
+ # If the acquisition already exists
462
+ # Nothing to do
463
+ # Maybe we should update the acquisition name and kwargs
464
+ return self
465
+
466
+ acquisitions.append(
467
+ Acquisition(id=acquisition_id, name=acquisition_name, **acquisition_kwargs)
468
+ )
331
469
 
332
470
  new_plate = Plate(
333
- rows=rows,
334
- columns=columns,
471
+ rows=self.plate.rows,
472
+ columns=self.plate.columns,
335
473
  acquisitions=acquisitions,
336
- wells=wells,
474
+ wells=self.plate.wells,
337
475
  field_count=self.plate.field_count,
338
476
  version=self.plate.version,
339
477
  )
@@ -374,5 +512,39 @@ class NgioPlateMeta(HCSAttrs):
374
512
  )
375
513
  return NgioPlateMeta(plate=new_plate)
376
514
 
515
+ def derive(
516
+ self,
517
+ name: str | None = None,
518
+ version: NgffVersions | None = None,
519
+ keep_acquisitions: bool = False,
520
+ ) -> "NgioPlateMeta":
521
+ """Derive the plate metadata.
522
+
523
+ Args:
524
+ name (str): The name of the derived plate.
525
+ version (NgffVersion | None): The version of the derived plate.
526
+ If None, use the version of the original plate.
527
+ keep_acquisitions (bool): If True, keep the acquisitions in the plate.
528
+ """
529
+ columns = self.plate.columns
530
+ rows = self.plate.rows
531
+
532
+ if keep_acquisitions:
533
+ acquisitions = self.plate.acquisitions
534
+ else:
535
+ acquisitions = None
536
+
537
+ if version is None:
538
+ version = self.plate.version # type: ignore[assignment]
377
539
 
378
- # %%
540
+ return NgioPlateMeta(
541
+ plate=Plate(
542
+ rows=rows,
543
+ columns=columns,
544
+ acquisitions=acquisitions,
545
+ wells=[],
546
+ field_count=self.plate.field_count,
547
+ version=version,
548
+ name=name,
549
+ )
550
+ )
@@ -13,6 +13,10 @@ import numpy as np
13
13
  from pydantic import BaseModel
14
14
 
15
15
  from ngio.ome_zarr_meta.ngio_specs._axes import (
16
+ DefaultSpaceUnit,
17
+ DefaultTimeUnit,
18
+ SpaceUnits,
19
+ TimeUnits,
16
20
  canonical_axes,
17
21
  )
18
22
  from ngio.ome_zarr_meta.ngio_specs._channels import Channel, ChannelsMeta
@@ -21,17 +25,18 @@ from ngio.ome_zarr_meta.ngio_specs._pixel_size import PixelSize
21
25
  from ngio.utils import NgioValidationError, NgioValueError
22
26
 
23
27
  T = TypeVar("T")
24
- NgffVersion = Literal["0.4"]
28
+ NgffVersions = Literal["0.4"]
29
+ DefaultNgffVersion: Literal["0.4"] = "0.4"
25
30
 
26
31
 
27
32
  class ImageLabelSource(BaseModel):
28
33
  """Image label source model."""
29
34
 
30
- version: NgffVersion
35
+ version: NgffVersions
31
36
  source: dict[str, str | None]
32
37
 
33
38
  @classmethod
34
- def default_init(cls, version: NgffVersion) -> "ImageLabelSource":
39
+ def default_init(cls, version: NgffVersions) -> "ImageLabelSource":
35
40
  """Initialize the ImageLabelSource object."""
36
41
  return cls(version=version, source={"image": "../../"})
37
42
 
@@ -40,7 +45,7 @@ class AbstractNgioImageMeta:
40
45
  """Base class for ImageMeta and LabelMeta."""
41
46
 
42
47
  def __init__(
43
- self, version: NgffVersion, name: str | None, datasets: list[Dataset]
48
+ self, version: NgffVersions, name: str | None, datasets: list[Dataset]
44
49
  ) -> None:
45
50
  """Initialize the ImageMeta object."""
46
51
  self._version = version
@@ -66,13 +71,13 @@ class AbstractNgioImageMeta:
66
71
  pixel_size: PixelSize,
67
72
  scaling_factors: Collection[float] | None = None,
68
73
  name: str | None = None,
69
- version: NgffVersion = "0.4",
74
+ version: NgffVersions = DefaultNgffVersion,
70
75
  ):
71
76
  """Initialize the ImageMeta object."""
72
77
  axes = canonical_axes(
73
78
  axes_names,
74
- space_units=pixel_size.space_unit,
75
- time_units=pixel_size.time_unit,
79
+ space_units=pixel_size.space_unit, # type: ignore[arg-type]
80
+ time_units=pixel_size.time_unit, # type: ignore[arg-type]
76
81
  )
77
82
 
78
83
  px_size_dict = pixel_size.as_dict()
@@ -105,10 +110,33 @@ class AbstractNgioImageMeta:
105
110
  datasets=datasets,
106
111
  )
107
112
 
113
+ def to_units(
114
+ self,
115
+ *,
116
+ space_unit: SpaceUnits = DefaultSpaceUnit,
117
+ time_unit: TimeUnits = DefaultTimeUnit,
118
+ ):
119
+ """Convert the pixel size to the given units.
120
+
121
+ Args:
122
+ space_unit(str): The space unit to convert to.
123
+ time_unit(str): The time unit to convert to.
124
+ """
125
+ new_datasets = []
126
+ for dataset in self.datasets:
127
+ new_dataset = dataset.to_units(space_unit=space_unit, time_unit=time_unit)
128
+ new_datasets.append(new_dataset)
129
+
130
+ return type(self)(
131
+ version=self.version,
132
+ name=self.name,
133
+ datasets=new_datasets,
134
+ )
135
+
108
136
  @property
109
- def version(self) -> NgffVersion:
137
+ def version(self) -> NgffVersions:
110
138
  """Version of the OME-NFF metadata used to build the object."""
111
- return self._version
139
+ return self._version # type: ignore[return-value]
112
140
 
113
141
  @property
114
142
  def name(self) -> str | None:
@@ -135,6 +163,16 @@ class AbstractNgioImageMeta:
135
163
  """List of paths of the datasets."""
136
164
  return [dataset.path for dataset in self.datasets]
137
165
 
166
+ @property
167
+ def space_unit(self) -> str | None:
168
+ """Get the space unit of the pixel size."""
169
+ return self.datasets[0].pixel_size.space_unit
170
+
171
+ @property
172
+ def time_unit(self) -> str | None:
173
+ """Get the time unit of the pixel size."""
174
+ return self.datasets[0].pixel_size.time_unit
175
+
138
176
  def _get_dataset_by_path(self, path: str) -> Dataset:
139
177
  """Get a dataset by its path."""
140
178
  for dataset in self.datasets:
@@ -331,7 +369,7 @@ class NgioLabelMeta(AbstractNgioImageMeta):
331
369
 
332
370
  def __init__(
333
371
  self,
334
- version: NgffVersion,
372
+ version: NgffVersions,
335
373
  name: str | None,
336
374
  datasets: list[Dataset],
337
375
  image_label: ImageLabelSource | None = None,
@@ -370,7 +408,7 @@ class NgioImageMeta(AbstractNgioImageMeta):
370
408
 
371
409
  def __init__(
372
410
  self,
373
- version: NgffVersion,
411
+ version: NgffVersions,
374
412
  name: str | None,
375
413
  datasets: list[Dataset],
376
414
  channels: ChannelsMeta | None = None,
@@ -5,7 +5,12 @@ from functools import total_ordering
5
5
 
6
6
  import numpy as np
7
7
 
8
- from ngio.ome_zarr_meta.ngio_specs import SpaceUnits, TimeUnits
8
+ from ngio.ome_zarr_meta.ngio_specs import (
9
+ DefaultSpaceUnit,
10
+ DefaultTimeUnit,
11
+ SpaceUnits,
12
+ TimeUnits,
13
+ )
9
14
 
10
15
  ################################################################################################
11
16
  #
@@ -33,8 +38,8 @@ class PixelSize:
33
38
  y: float,
34
39
  z: float,
35
40
  t: float = 1,
36
- space_unit: SpaceUnits = SpaceUnits.micrometer,
37
- time_unit: TimeUnits | None = TimeUnits.s,
41
+ space_unit: SpaceUnits | str | None = DefaultSpaceUnit,
42
+ time_unit: TimeUnits | str | None = DefaultTimeUnit,
38
43
  ):
39
44
  """Initialize the pixel size."""
40
45
  self.x = _validate_type(x, "x")
@@ -42,13 +47,8 @@ class PixelSize:
42
47
  self.z = _validate_type(z, "z")
43
48
  self.t = _validate_type(t, "t")
44
49
 
45
- if not isinstance(space_unit, SpaceUnits):
46
- raise TypeError("space_unit must be of type SpaceUnits.")
47
- self.space_unit = space_unit
48
-
49
- if time_unit is not None and not isinstance(time_unit, TimeUnits):
50
- raise TypeError("time_unit must be of type TimeUnits.")
51
- self.time_unit = time_unit
50
+ self._space_unit = space_unit
51
+ self._time_unit = time_unit
52
52
 
53
53
  def __repr__(self) -> str:
54
54
  """Return a string representation of the pixel size."""
@@ -74,13 +74,30 @@ class PixelSize:
74
74
  """Check if one pixel size is less than the other."""
75
75
  if not isinstance(other, PixelSize):
76
76
  raise TypeError("Can only compare PixelSize with PixelSize.")
77
- ref = PixelSize(0, 0, 0, 0, self.space_unit, self.time_unit)
77
+ ref = PixelSize(
78
+ 0,
79
+ 0,
80
+ 0,
81
+ 0,
82
+ space_unit=self.space_unit,
83
+ time_unit=self.time_unit, # type: ignore
84
+ )
78
85
  return self.distance(ref) < other.distance(ref)
79
86
 
80
87
  def as_dict(self) -> dict:
81
88
  """Return the pixel size as a dictionary."""
82
89
  return {"t": self.t, "z": self.z, "y": self.y, "x": self.x}
83
90
 
91
+ @property
92
+ def space_unit(self) -> SpaceUnits | str | None:
93
+ """Return the space unit."""
94
+ return self._space_unit
95
+
96
+ @property
97
+ def time_unit(self) -> TimeUnits | str | None:
98
+ """Return the time unit."""
99
+ return self._time_unit
100
+
84
101
  @property
85
102
  def tzyx(self) -> tuple[float, float, float, float]:
86
103
  """Return the voxel size in t, z, y, x order."""
@@ -0,0 +1,18 @@
1
+ from typing import Annotated
2
+
3
+ from ome_zarr_models.v04.well import WellAttrs as WellAttrs04
4
+ from ome_zarr_models.v04.well_types import WellImage as WellImage04
5
+ from ome_zarr_models.v04.well_types import WellMeta as WellMeta04
6
+ from pydantic import SkipValidation
7
+
8
+
9
+ class CustomWellImage(WellImage04):
10
+ path: Annotated[str, SkipValidation]
11
+
12
+
13
+ class CustomWellMeta(WellMeta04):
14
+ images: list[CustomWellImage] # type: ignore[valid-type]
15
+
16
+
17
+ class CustomWellAttrs(WellAttrs04):
18
+ well: CustomWellMeta # type: ignore[valid-type]
@@ -23,7 +23,6 @@ from ome_zarr_models.v04.multiscales import Multiscale as MultiscaleV04
23
23
  from ome_zarr_models.v04.omero import Channel as ChannelV04
24
24
  from ome_zarr_models.v04.omero import Omero as OmeroV04
25
25
  from ome_zarr_models.v04.omero import Window as WindowV04
26
- from ome_zarr_models.v04.well import WellAttrs as WellAttrsV04
27
26
  from pydantic import ValidationError
28
27
 
29
28
  from ngio.ome_zarr_meta.ngio_specs import (
@@ -41,6 +40,7 @@ from ngio.ome_zarr_meta.ngio_specs import (
41
40
  NgioWellMeta,
42
41
  default_channel_name,
43
42
  )
43
+ from ngio.ome_zarr_meta.v04._custom_models import CustomWellAttrs as WellAttrsV04
44
44
 
45
45
 
46
46
  def _is_v04_image_meta(metadata: dict) -> ImageAttrsV04 | ValidationError:
@@ -321,7 +321,7 @@ def _ngio_to_v04_multiscale(name: str | None, datasets: list[Dataset]) -> Multis
321
321
  AxisV04(
322
322
  name=axis.on_disk_name,
323
323
  type=axis.axis_type.value if axis.axis_type is not None else None,
324
- unit=axis.unit.value if axis.unit is not None else None,
324
+ unit=axis.unit if axis.unit is not None else None,
325
325
  )
326
326
  )
327
327