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
ngio/hcs/plate.py CHANGED
@@ -1,23 +1,44 @@
1
1
  """A module for handling the Plate Collection in an OME-Zarr file."""
2
2
 
3
+ from typing import Literal, overload
4
+
3
5
  from ngio.images import OmeZarrContainer
4
6
  from ngio.ome_zarr_meta import (
5
7
  ImageInWellPath,
6
- NgffVersion,
8
+ NgffVersions,
7
9
  NgioPlateMeta,
8
10
  NgioWellMeta,
9
11
  find_plate_meta_handler,
10
12
  find_well_meta_handler,
11
13
  get_plate_meta_handler,
12
14
  get_well_meta_handler,
15
+ path_in_well_validation,
16
+ )
17
+ from ngio.tables import (
18
+ FeatureTable,
19
+ GenericRoiTable,
20
+ MaskingRoiTable,
21
+ RoiTable,
22
+ Table,
23
+ TablesContainer,
24
+ TypedTable,
13
25
  )
14
26
  from ngio.utils import (
15
27
  AccessModeLiteral,
28
+ NgioValidationError,
29
+ NgioValueError,
16
30
  StoreOrGroup,
17
31
  ZarrGroupHandler,
18
32
  )
19
33
 
20
34
 
35
+ def _default_table_container(handler: ZarrGroupHandler) -> TablesContainer | None:
36
+ """Return a default table container."""
37
+ success, table_handler = handler.safe_derive_handler("tables")
38
+ if success and isinstance(table_handler, ZarrGroupHandler):
39
+ return TablesContainer(table_handler)
40
+
41
+
21
42
  # Mock lock class that does nothing
22
43
  class MockLock:
23
44
  """A mock lock class that does nothing."""
@@ -43,6 +64,10 @@ class OmeZarrWell:
43
64
  self._group_handler = group_handler
44
65
  self._meta_handler = find_well_meta_handler(group_handler)
45
66
 
67
+ def __repr__(self) -> str:
68
+ """Return a string representation of the well."""
69
+ return f"Well(#images: {len(self.paths())})"
70
+
46
71
  @property
47
72
  def meta_handler(self):
48
73
  """Return the metadata handler."""
@@ -53,6 +78,11 @@ class OmeZarrWell:
53
78
  """Return the metadata."""
54
79
  return self._meta_handler.meta
55
80
 
81
+ @property
82
+ def acquisition_ids(self) -> list[int]:
83
+ """Return the acquisitions ids in the well."""
84
+ return self.meta.acquisition_ids
85
+
56
86
  def paths(self, acquisition: int | None = None) -> list[str]:
57
87
  """Return the images paths in the well.
58
88
 
@@ -64,18 +94,115 @@ class OmeZarrWell:
64
94
  """
65
95
  return self.meta.paths(acquisition)
66
96
 
97
+ def get_image_store(self, image_path: str) -> StoreOrGroup:
98
+ """Get the image store from the well.
99
+
100
+ Args:
101
+ image_path (str): The path of the image.
102
+ """
103
+ return self._group_handler.get_group(image_path, create_mode=True)
104
+
105
+ def get_image_acquisition_id(self, image_path: str) -> int | None:
106
+ """Get the acquisition id of an image in the well.
107
+
108
+ Args:
109
+ image_path (str): The path of the image.
110
+
111
+ Returns:
112
+ int | None: The acquisition id of the image.
113
+ """
114
+ return self.meta.get_image_acquisition_id(image_path=image_path)
115
+
116
+ def get_image(self, image_path: str) -> OmeZarrContainer:
117
+ """Get an image from the well.
118
+
119
+ Args:
120
+ image_path (str): The path of the image.
121
+
122
+ Returns:
123
+ OmeZarrContainer: The image.
124
+ """
125
+ handler = self._group_handler.derive_handler(image_path)
126
+ return OmeZarrContainer(handler)
127
+
128
+ def _add_image(
129
+ self,
130
+ image_path: str,
131
+ acquisition_id: int | None = None,
132
+ strict: bool = True,
133
+ atomic: bool = False,
134
+ ) -> StoreOrGroup:
135
+ """Add an image to an ome-zarr well."""
136
+ image_path = path_in_well_validation(path=image_path)
137
+
138
+ if atomic:
139
+ well_lock = self._group_handler.lock
140
+ else:
141
+ well_lock = MockLock()
142
+
143
+ with well_lock:
144
+ meta = self.meta.add_image(
145
+ path=image_path, acquisition=acquisition_id, strict=strict
146
+ )
147
+ self.meta_handler.write_meta(meta)
148
+ self.meta_handler._group_handler.clean_cache()
149
+
150
+ return self._group_handler.get_group(image_path, create_mode=True)
151
+
152
+ def atomic_add_image(
153
+ self,
154
+ image_path: str,
155
+ acquisition_id: int | None = None,
156
+ strict: bool = True,
157
+ ) -> StoreOrGroup:
158
+ """Parallel safe version of add_image."""
159
+ return self._add_image(
160
+ image_path=image_path,
161
+ acquisition_id=acquisition_id,
162
+ atomic=True,
163
+ strict=strict,
164
+ )
165
+
166
+ def add_image(
167
+ self,
168
+ image_path: str,
169
+ acquisition_id: int | None = None,
170
+ strict: bool = True,
171
+ ) -> StoreOrGroup:
172
+ """Add an image to an ome-zarr well.
173
+
174
+ Args:
175
+ image_path (str): The path of the image.
176
+ acquisition_id (int | None): The acquisition id to filter the images.
177
+ strict (bool): Whether to check if the acquisition id is already exists
178
+ in the well. Defaults to True. If False this might lead to
179
+ acquision in a well that does not exist at the plate level.
180
+ """
181
+ return self._add_image(
182
+ image_path=image_path,
183
+ acquisition_id=acquisition_id,
184
+ atomic=False,
185
+ strict=strict,
186
+ )
187
+
67
188
 
68
189
  class OmeZarrPlate:
69
190
  """A class to handle the Plate Collection in an OME-Zarr file."""
70
191
 
71
- def __init__(self, group_handler: ZarrGroupHandler) -> None:
192
+ def __init__(
193
+ self,
194
+ group_handler: ZarrGroupHandler,
195
+ table_container: TablesContainer | None = None,
196
+ ) -> None:
72
197
  """Initialize the LabelGroupHandler.
73
198
 
74
199
  Args:
75
200
  group_handler: The Zarr group handler that contains the Plate.
201
+ table_container: The tables container that contains plate level tables.
76
202
  """
77
203
  self._group_handler = group_handler
78
204
  self._meta_handler = find_plate_meta_handler(group_handler)
205
+ self._tables_container = table_container
79
206
 
80
207
  def __repr__(self) -> str:
81
208
  """Return a string representation of the plate."""
@@ -107,9 +234,9 @@ class OmeZarrPlate:
107
234
  return self.meta.acquisitions_names
108
235
 
109
236
  @property
110
- def acquisitions_ids(self) -> list[int]:
237
+ def acquisition_ids(self) -> list[int]:
111
238
  """Return the acquisitions ids in the plate."""
112
- return self.meta.acquisitions_ids
239
+ return self.meta.acquisition_ids
113
240
 
114
241
  def _well_path(self, row: str, column: int | str) -> str:
115
242
  """Return the well path in the plate."""
@@ -160,6 +287,22 @@ class OmeZarrPlate:
160
287
  images.append(self._image_path(row=row, column=column, path=path))
161
288
  return images
162
289
 
290
+ def get_image_acquisition_id(
291
+ self, row: str, column: int | str, image_path: str
292
+ ) -> int | None:
293
+ """Get the acquisition id of an image in a well.
294
+
295
+ Args:
296
+ row (str): The row of the well.
297
+ column (int | str): The column of the well.
298
+ image_path (str): The path of the image.
299
+
300
+ Returns:
301
+ int | None: The acquisition id of the image.
302
+ """
303
+ well = self.get_well(row=row, column=column)
304
+ return well.get_image_acquisition_id(image_path=image_path)
305
+
163
306
  def get_well(self, row: str, column: int | str) -> OmeZarrWell:
164
307
  """Get a well from the plate.
165
308
 
@@ -217,6 +360,19 @@ class OmeZarrPlate:
217
360
  group_handler = self._group_handler.derive_handler(image_path)
218
361
  return OmeZarrContainer(group_handler)
219
362
 
363
+ def get_image_store(
364
+ self, row: str, column: int | str, image_path: str
365
+ ) -> StoreOrGroup:
366
+ """Get the image store from the plate.
367
+
368
+ Args:
369
+ row (str): The row of the well.
370
+ column (int | str): The column of the well.
371
+ image_path (str): The path of the image.
372
+ """
373
+ well = self.get_well(row=row, column=column)
374
+ return well.get_image_store(image_path=image_path)
375
+
220
376
  def get_well_images(
221
377
  self, row: str, column: str | int, acquisition: int | None = None
222
378
  ) -> dict[str, OmeZarrContainer]:
@@ -239,12 +395,15 @@ class OmeZarrPlate:
239
395
  self,
240
396
  row: str,
241
397
  column: int | str,
242
- image_path: str,
398
+ image_path: str | None = None,
243
399
  acquisition_id: int | None = None,
244
400
  acquisition_name: str | None = None,
245
401
  atomic: bool = False,
246
- ) -> StoreOrGroup:
402
+ ) -> StoreOrGroup | None:
247
403
  """Add an image to an ome-zarr plate."""
404
+ if image_path is not None:
405
+ image_path = path_in_well_validation(path=image_path)
406
+
248
407
  if atomic:
249
408
  plate_lock = self._group_handler.lock
250
409
  else:
@@ -252,7 +411,11 @@ class OmeZarrPlate:
252
411
 
253
412
  with plate_lock:
254
413
  meta = self.meta
255
- meta = meta.add_well(row, column, acquisition_id, acquisition_name)
414
+ meta = meta.add_well(row=row, column=column)
415
+ if acquisition_id is not None:
416
+ meta = meta.add_acquisition(
417
+ acquisition_id=acquisition_id, acquisition_name=acquisition_name
418
+ )
256
419
  self.meta_handler.write_meta(meta)
257
420
  self.meta_handler._group_handler.clean_cache()
258
421
 
@@ -279,11 +442,16 @@ class OmeZarrPlate:
279
442
 
280
443
  group_handler = self._group_handler.derive_handler(well_path)
281
444
 
282
- well_meta = well_meta.add_image(path=image_path, acquisition=acquisition_id)
445
+ if image_path is not None:
446
+ well_meta = well_meta.add_image(
447
+ path=image_path, acquisition=acquisition_id, strict=False
448
+ )
283
449
  meta_handler.write_meta(well_meta)
284
450
  meta_handler._group_handler.clean_cache()
285
451
 
286
- return group_handler.get_group(image_path, create_mode=True)
452
+ if image_path is not None:
453
+ return group_handler.get_group(image_path, create_mode=True)
454
+ return None
287
455
 
288
456
  def atomic_add_image(
289
457
  self,
@@ -294,7 +462,12 @@ class OmeZarrPlate:
294
462
  acquisition_name: str | None = None,
295
463
  ) -> StoreOrGroup:
296
464
  """Parallel safe version of add_image."""
297
- return self._add_image(
465
+ if image_path is None:
466
+ raise ValueError(
467
+ "Image path cannot be None for atomic add_image. "
468
+ "If your intent is to add a well, use add_well instead."
469
+ )
470
+ group = self._add_image(
298
471
  row=row,
299
472
  column=column,
300
473
  image_path=image_path,
@@ -302,6 +475,12 @@ class OmeZarrPlate:
302
475
  acquisition_name=acquisition_name,
303
476
  atomic=True,
304
477
  )
478
+ if group is None:
479
+ raise ValueError(
480
+ f"Some error occurred while adding image {image_path} "
481
+ f"to well {row}{column}."
482
+ )
483
+ return group
305
484
 
306
485
  def add_image(
307
486
  self,
@@ -312,7 +491,12 @@ class OmeZarrPlate:
312
491
  acquisition_name: str | None = None,
313
492
  ) -> StoreOrGroup:
314
493
  """Add an image to an ome-zarr plate."""
315
- return self._add_image(
494
+ if image_path is None:
495
+ raise ValueError(
496
+ "Image path cannot be None for atomic add_image. "
497
+ "If your intent is to add a well, use add_well instead."
498
+ )
499
+ group = self._add_image(
316
500
  row=row,
317
501
  column=column,
318
502
  image_path=image_path,
@@ -320,6 +504,68 @@ class OmeZarrPlate:
320
504
  acquisition_name=acquisition_name,
321
505
  atomic=False,
322
506
  )
507
+ if group is None:
508
+ raise ValueError(
509
+ f"Some error occurred while adding image {image_path} "
510
+ f"to well {row}{column}."
511
+ )
512
+ return group
513
+
514
+ def add_well(
515
+ self,
516
+ row: str,
517
+ column: int | str,
518
+ ) -> OmeZarrWell:
519
+ """Add a well to an ome-zarr plate."""
520
+ _ = self._add_image(
521
+ row=row,
522
+ column=column,
523
+ image_path=None,
524
+ acquisition_id=None,
525
+ acquisition_name=None,
526
+ atomic=False,
527
+ )
528
+ return self.get_well(row=row, column=column)
529
+
530
+ def add_column(
531
+ self,
532
+ column: int | str,
533
+ ) -> "OmeZarrPlate":
534
+ """Add a column to an ome-zarr plate."""
535
+ meta, _ = self.meta.add_column(column)
536
+ self.meta_handler.write_meta(meta)
537
+ self.meta_handler._group_handler.clean_cache()
538
+ return self
539
+
540
+ def add_row(
541
+ self,
542
+ row: str,
543
+ ) -> "OmeZarrPlate":
544
+ """Add a row to an ome-zarr plate."""
545
+ meta, _ = self.meta.add_row(row)
546
+ self.meta_handler.write_meta(meta)
547
+ self.meta_handler._group_handler.clean_cache()
548
+ return self
549
+
550
+ def add_acquisition(
551
+ self,
552
+ acquisition_id: int,
553
+ acquisition_name: str,
554
+ ) -> "OmeZarrPlate":
555
+ """Add an acquisition to an ome-zarr plate.
556
+
557
+ Be aware that this is not a parallel safe operation.
558
+
559
+ Args:
560
+ acquisition_id (int): The acquisition id.
561
+ acquisition_name (str): The acquisition name.
562
+ """
563
+ meta = self.meta.add_acquisition(
564
+ acquisition_id=acquisition_id, acquisition_name=acquisition_name
565
+ )
566
+ self.meta_handler.write_meta(meta)
567
+ self.meta_handler._group_handler.clean_cache()
568
+ return self
323
569
 
324
570
  def _remove_well(
325
571
  self,
@@ -390,6 +636,127 @@ class OmeZarrPlate:
390
636
  atomic=False,
391
637
  )
392
638
 
639
+ def derive_plate(
640
+ self,
641
+ store: StoreOrGroup,
642
+ plate_name: str | None = None,
643
+ version: NgffVersions = "0.4",
644
+ keep_acquisitions: bool = False,
645
+ cache: bool = False,
646
+ overwrite: bool = False,
647
+ parallel_safe: bool = True,
648
+ ) -> "OmeZarrPlate":
649
+ """Derive a new OME-Zarr plate from an existing one.
650
+
651
+ Args:
652
+ store (StoreOrGroup): The Zarr store or group that stores the plate.
653
+ plate_name (str | None): The name of the new plate.
654
+ version (NgffVersion): The version of the new plate.
655
+ keep_acquisitions (bool): Whether to keep the acquisitions in the new plate.
656
+ cache (bool): Whether to use a cache for the zarr group metadata.
657
+ overwrite (bool): Whether to overwrite the existing plate.
658
+ parallel_safe (bool): Whether the group handler is parallel safe.
659
+ """
660
+ return derive_ome_zarr_plate(
661
+ ome_zarr_plate=self,
662
+ store=store,
663
+ plate_name=plate_name,
664
+ version=version,
665
+ keep_acquisitions=keep_acquisitions,
666
+ cache=cache,
667
+ overwrite=overwrite,
668
+ parallel_safe=parallel_safe,
669
+ )
670
+
671
+ @property
672
+ def tables_container(self) -> TablesContainer:
673
+ """Return the tables container."""
674
+ if self._tables_container is None:
675
+ self._tables_container = _default_table_container(self._group_handler)
676
+ if self._tables_container is None:
677
+ raise NgioValidationError("No tables found in the image.")
678
+ return self._tables_container
679
+
680
+ @property
681
+ def list_tables(self) -> list[str]:
682
+ """List all tables in the image."""
683
+ return self.tables_container.list()
684
+
685
+ def list_roi_tables(self) -> list[str]:
686
+ """List all ROI tables in the image."""
687
+ return self.tables_container.list_roi_tables()
688
+
689
+ @overload
690
+ def get_table(self, name: str, check_type: None) -> Table: ...
691
+
692
+ @overload
693
+ def get_table(self, name: str, check_type: Literal["roi_table"]) -> RoiTable: ...
694
+
695
+ @overload
696
+ def get_table(
697
+ self, name: str, check_type: Literal["masking_roi_table"]
698
+ ) -> MaskingRoiTable: ...
699
+
700
+ @overload
701
+ def get_table(
702
+ self, name: str, check_type: Literal["feature_table"]
703
+ ) -> FeatureTable: ...
704
+
705
+ @overload
706
+ def get_table(
707
+ self, name: str, check_type: Literal["generic_roi_table"]
708
+ ) -> GenericRoiTable: ...
709
+
710
+ def get_table(self, name: str, check_type: TypedTable | None = None) -> Table:
711
+ """Get a table from the image."""
712
+ table = self.tables_container.get(name)
713
+ match check_type:
714
+ case "roi_table":
715
+ if not isinstance(table, RoiTable):
716
+ raise NgioValueError(
717
+ f"Table '{name}' is not a ROI table. Found type: {table.type()}"
718
+ )
719
+ return table
720
+ case "masking_roi_table":
721
+ if not isinstance(table, MaskingRoiTable):
722
+ raise NgioValueError(
723
+ f"Table '{name}' is not a masking ROI table. "
724
+ f"Found type: {table.type()}"
725
+ )
726
+ return table
727
+
728
+ case "generic_roi_table":
729
+ if not isinstance(table, GenericRoiTable):
730
+ raise NgioValueError(
731
+ f"Table '{name}' is not a generic ROI table. "
732
+ f"Found type: {table.type()}"
733
+ )
734
+ return table
735
+
736
+ case "feature_table":
737
+ if not isinstance(table, FeatureTable):
738
+ raise NgioValueError(
739
+ f"Table '{name}' is not a feature table. "
740
+ f"Found type: {table.type()}"
741
+ )
742
+ return table
743
+ case None:
744
+ return table
745
+ case _:
746
+ raise NgioValueError(f"Unknown check_type: {check_type}")
747
+
748
+ def add_table(
749
+ self,
750
+ name: str,
751
+ table: Table,
752
+ backend: str | None = None,
753
+ overwrite: bool = False,
754
+ ) -> None:
755
+ """Add a table to the image."""
756
+ self.tables_container.add(
757
+ name=name, table=table, backend=backend, overwrite=overwrite
758
+ )
759
+
393
760
 
394
761
  def open_ome_zarr_plate(
395
762
  store: StoreOrGroup,
@@ -412,26 +779,42 @@ def open_ome_zarr_plate(
412
779
  return OmeZarrPlate(group_handler)
413
780
 
414
781
 
782
+ def _create_empty_plate_from_meta(
783
+ store: StoreOrGroup,
784
+ meta: NgioPlateMeta,
785
+ version: NgffVersions = "0.4",
786
+ overwrite: bool = False,
787
+ ) -> ZarrGroupHandler:
788
+ """Create an empty OME-Zarr plate from metadata."""
789
+ mode = "w" if overwrite else "w-"
790
+ group_handler = ZarrGroupHandler(
791
+ store=store, cache=True, mode=mode, parallel_safe=False
792
+ )
793
+ meta_handler = get_plate_meta_handler(group_handler, version=version)
794
+ meta_handler.write_meta(meta)
795
+ return group_handler
796
+
797
+
415
798
  def create_empty_plate(
416
799
  store: StoreOrGroup,
417
800
  name: str,
418
801
  images: list[ImageInWellPath] | None = None,
419
- version: NgffVersion = "0.4",
802
+ version: NgffVersions = "0.4",
420
803
  cache: bool = False,
421
804
  overwrite: bool = False,
422
805
  parallel_safe: bool = True,
423
806
  ) -> OmeZarrPlate:
424
807
  """Initialize and create an empty OME-Zarr plate."""
425
- mode = "w" if overwrite else "w-"
426
- group_handler = ZarrGroupHandler(
427
- store=store, cache=True, mode=mode, parallel_safe=False
428
- )
429
- meta_handler = get_plate_meta_handler(group_handler, version=version)
430
808
  plate_meta = NgioPlateMeta.default_init(
431
809
  name=name,
432
810
  version=version,
433
811
  )
434
- meta_handler.write_meta(plate_meta)
812
+ group_handler = _create_empty_plate_from_meta(
813
+ store=store,
814
+ meta=plate_meta,
815
+ version=version,
816
+ overwrite=overwrite,
817
+ )
435
818
 
436
819
  if images is not None:
437
820
  plate = OmeZarrPlate(group_handler)
@@ -449,3 +832,98 @@ def create_empty_plate(
449
832
  mode="r+",
450
833
  parallel_safe=parallel_safe,
451
834
  )
835
+
836
+
837
+ def derive_ome_zarr_plate(
838
+ ome_zarr_plate: OmeZarrPlate,
839
+ store: StoreOrGroup,
840
+ plate_name: str | None = None,
841
+ version: NgffVersions = "0.4",
842
+ keep_acquisitions: bool = False,
843
+ cache: bool = False,
844
+ overwrite: bool = False,
845
+ parallel_safe: bool = True,
846
+ ) -> OmeZarrPlate:
847
+ """Derive a new OME-Zarr plate from an existing one.
848
+
849
+ Args:
850
+ ome_zarr_plate (OmeZarrPlate): The existing OME-Zarr plate.
851
+ store (StoreOrGroup): The Zarr store or group that stores the plate.
852
+ plate_name (str | None): The name of the new plate.
853
+ version (NgffVersion): The version of the new plate.
854
+ keep_acquisitions (bool): Whether to keep the acquisitions in the new plate.
855
+ cache (bool): Whether to use a cache for the zarr group metadata.
856
+ overwrite (bool): Whether to overwrite the existing plate.
857
+ parallel_safe (bool): Whether the group handler is parallel safe.
858
+ """
859
+ if plate_name is None:
860
+ plate_name = ome_zarr_plate.meta.plate.name
861
+
862
+ new_meta = ome_zarr_plate.meta.derive(
863
+ name=plate_name,
864
+ version=version,
865
+ keep_acquisitions=keep_acquisitions,
866
+ )
867
+ _ = _create_empty_plate_from_meta(
868
+ store=store,
869
+ meta=new_meta,
870
+ overwrite=overwrite,
871
+ version=version,
872
+ )
873
+ return open_ome_zarr_plate(
874
+ store=store,
875
+ cache=cache,
876
+ mode="r+",
877
+ parallel_safe=parallel_safe,
878
+ )
879
+
880
+
881
+ def open_ome_zarr_well(
882
+ store: StoreOrGroup,
883
+ cache: bool = False,
884
+ mode: AccessModeLiteral = "r+",
885
+ parallel_safe: bool = True,
886
+ ) -> OmeZarrWell:
887
+ """Open an OME-Zarr well.
888
+
889
+ Args:
890
+ store (StoreOrGroup): The Zarr store or group that stores the plate.
891
+ cache (bool): Whether to use a cache for the zarr group metadata.
892
+ mode (AccessModeLiteral): The access mode for the image. Defaults to "r+".
893
+ parallel_safe (bool): Whether the group handler is parallel safe.
894
+ """
895
+ group_handler = ZarrGroupHandler(
896
+ store=store, cache=cache, mode=mode, parallel_safe=parallel_safe
897
+ )
898
+ return OmeZarrWell(group_handler)
899
+
900
+
901
+ def create_empty_well(
902
+ store: StoreOrGroup,
903
+ version: NgffVersions = "0.4",
904
+ cache: bool = False,
905
+ overwrite: bool = False,
906
+ parallel_safe: bool = True,
907
+ ) -> OmeZarrWell:
908
+ """Create an empty OME-Zarr well.
909
+
910
+ Args:
911
+ store (StoreOrGroup): The Zarr store or group that stores the well.
912
+ version (NgffVersion): The version of the new well.
913
+ cache (bool): Whether to use a cache for the zarr group metadata.
914
+ overwrite (bool): Whether to overwrite the existing well.
915
+ parallel_safe (bool): Whether the group handler is parallel safe.
916
+ """
917
+ group_handler = ZarrGroupHandler(
918
+ store=store, cache=True, mode="w" if overwrite else "w-", parallel_safe=False
919
+ )
920
+ meta_handler = get_well_meta_handler(group_handler, version=version)
921
+ meta = NgioWellMeta.default_init()
922
+ meta_handler.write_meta(meta)
923
+
924
+ return open_ome_zarr_well(
925
+ store=store,
926
+ cache=cache,
927
+ mode="r+",
928
+ parallel_safe=parallel_safe,
929
+ )
@@ -135,6 +135,16 @@ class AbstractImage(Generic[_image_handler]):
135
135
  """Return True if the image is multichannel."""
136
136
  return self.dimensions.is_multi_channels
137
137
 
138
+ @property
139
+ def space_unit(self) -> str | None:
140
+ """Return the space unit of the image."""
141
+ return self.meta_handler.meta.space_unit
142
+
143
+ @property
144
+ def time_unit(self) -> str | None:
145
+ """Return the time unit of the image."""
146
+ return self.meta_handler.meta.time_unit
147
+
138
148
  @property
139
149
  def pixel_size(self) -> PixelSize:
140
150
  """Return the pixel size of the image."""
@@ -152,6 +162,7 @@ class AbstractImage(Generic[_image_handler]):
152
162
 
153
163
  def has_axis(self, axis: str) -> bool:
154
164
  """Return True if the image has the given axis."""
165
+ self.axes_mapper.get_index("x")
155
166
  return self.dimensions.has_axis(axis)
156
167
 
157
168
  def get_array(