micro-reader 0.1.0__tar.gz → 0.2.1__tar.gz

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 (65) hide show
  1. {micro_reader-0.1.0 → micro_reader-0.2.1}/PKG-INFO +1 -1
  2. {micro_reader-0.1.0 → micro_reader-0.2.1}/pyproject.toml +1 -1
  3. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/__init__.py +7 -4
  4. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_base.py +73 -0
  5. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_errors.py +11 -2
  6. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_metadata.py +15 -0
  7. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_plate.py +21 -0
  8. micro_reader-0.2.1/src/micro_reader/_spatial.py +517 -0
  9. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_stack.py +1 -0
  10. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_version.py +1 -1
  11. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/_czi_metadata.py +10 -1
  12. micro_reader-0.2.1/src/micro_reader/backends/_lif_metadata.py +307 -0
  13. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/_nd2_metadata.py +60 -23
  14. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/_tiff_metadata.py +89 -15
  15. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/bdv.py +323 -46
  16. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/companion.py +69 -4
  17. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/cv7000.py +116 -42
  18. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/czi.py +26 -7
  19. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/harmony.py +57 -18
  20. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/ims.py +42 -10
  21. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/lif.py +98 -11
  22. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/metamorph.py +19 -6
  23. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/metaxpress.py +77 -39
  24. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/micromanager.py +50 -13
  25. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/nd2.py +7 -0
  26. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/ndpis.py +2 -1
  27. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/oif.py +59 -15
  28. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/omp2info.py +35 -14
  29. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/tiff.py +21 -1
  30. micro_reader-0.1.0/src/micro_reader/backends/_lif_metadata.py +0 -131
  31. {micro_reader-0.1.0 → micro_reader-0.2.1}/.gitignore +0 -0
  32. {micro_reader-0.1.0 → micro_reader-0.2.1}/LICENSE +0 -0
  33. {micro_reader-0.1.0 → micro_reader-0.2.1}/README.md +0 -0
  34. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_axes.py +0 -0
  35. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_cfb.py +0 -0
  36. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_codecs.py +0 -0
  37. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_eubi_guard.py +0 -0
  38. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_fileset.py +0 -0
  39. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_groups.py +0 -0
  40. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_index.py +0 -0
  41. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_io.py +0 -0
  42. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_jpeg_restarts.py +0 -0
  43. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_memory.py +0 -0
  44. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_mosaic.py +0 -0
  45. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_outline.py +0 -0
  46. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_parts.py +0 -0
  47. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_pool.py +0 -0
  48. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_registry.py +0 -0
  49. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/_stream.py +0 -0
  50. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/__init__.py +0 -0
  51. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/_ets.py +0 -0
  52. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/_hdf5.py +0 -0
  53. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/_nd2_legacy.py +0 -0
  54. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/_nd2_structure.py +0 -0
  55. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/_tiff_variants.py +0 -0
  56. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/_vsi_tree.py +0 -0
  57. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/dv.py +0 -0
  58. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/hdf5.py +0 -0
  59. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/ics.py +0 -0
  60. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/ilastik.py +0 -0
  61. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/incell.py +0 -0
  62. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/mrc.py +0 -0
  63. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/oir.py +0 -0
  64. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/raster.py +0 -0
  65. {micro_reader-0.1.0 → micro_reader-0.2.1}/src/micro_reader/backends/vsi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: micro-reader
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Low-level, memory-bounded pixel reading for microscopy formats: read exactly the rectangle you ask for.
5
5
  Project-URL: Homepage, https://github.com/bugraoezdemir/micro-reader
6
6
  Project-URL: Source, https://github.com/bugraoezdemir/micro-reader
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "micro-reader"
9
- version = "0.1.0"
9
+ version = "0.2.1"
10
10
  description = "Low-level, memory-bounded pixel reading for microscopy formats: read exactly the rectangle you ask for."
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.11"
@@ -10,9 +10,10 @@ from ._version import __version__
10
10
  from ._base import INDEX_NAMES, PLATE_INDICES, SAMPLED_INDICES, File, Image, ReadStats
11
11
  from ._metadata import Channel, Metadata, Quantity
12
12
  from ._plate import Acquisition, Field, Plate, Well
13
- from ._errors import (CompanionWarning, CompositeWarning, CorruptFileError, MicroReaderError,
14
- MissingImagesWarning, PlateLayoutWarning, SkewedGridWarning,
15
- UnsupportedFormatError)
13
+ from ._spatial import Axis, Collection, CoordinateSystem, Transformation
14
+ from ._errors import (CompanionWarning, CompositeWarning, CorruptFileError, InvariantError,
15
+ MicroReaderError, MissingImagesWarning, PlateLayoutWarning,
16
+ SkewedGridWarning, UnsupportedFormatError)
16
17
  from ._memory import MemoryLimitError, memory_limit, set_memory_limit
17
18
  from ._io import cache_limit, cached_bytes, set_cache_limit
18
19
  from ._pool import decode_threads, set_decode_threads
@@ -23,9 +24,11 @@ from .backends.czi import DamagedFileWarning
23
24
  __all__ = [
24
25
  "open", "group_files", "FileGroup", "Grouping", "File", "Image", "INDEX_NAMES", "PLATE_INDICES", "SAMPLED_INDICES", "ReadStats",
25
26
  "BACKENDS", "Metadata", "Channel", "Quantity", "Plate", "Well", "Field", "Acquisition",
27
+ "Axis", "CoordinateSystem", "Transformation", "Collection",
26
28
  "decode_threads", "set_decode_threads", "memory_limit", "set_memory_limit",
27
29
  "cache_limit", "set_cache_limit", "cached_bytes",
28
- "MicroReaderError", "UnsupportedFormatError", "CorruptFileError", "MemoryLimitError", "DamagedFileWarning",
30
+ "MicroReaderError", "UnsupportedFormatError", "CorruptFileError", "InvariantError",
31
+ "MemoryLimitError", "DamagedFileWarning",
29
32
  "MissingImagesWarning", "CompanionWarning", "CompositeWarning", "PlateLayoutWarning",
30
33
  "SkewedGridWarning", "__version__",
31
34
  ]
@@ -367,6 +367,10 @@ class File:
367
367
  backend's own class is an implementation detail."""
368
368
 
369
369
  backend: str = "base"
370
+ #: (coordinate_systems, coordinate_transformations) once declared and checked
371
+ _graph: Optional[tuple] = None
372
+ #: (the images it was built from, collection) once built (``collection``)
373
+ _tree: Optional[tuple] = None
370
374
 
371
375
  def __init__(self, path: str, images: Sequence[Image], plates: Sequence = ()):
372
376
  #: the file read: for a composite format its main file (docs/COMPOSITE.md)
@@ -453,6 +457,75 @@ class File:
453
457
  out.append(image)
454
458
  return out
455
459
 
460
+ # The spatial model (plan section 3; _spatial.py): the shared coordinate
461
+ # systems the file's positions are in, the other edges between them, and
462
+ # the tree of what contains what. A backend declares the systems from
463
+ # what opening the file already read; an image names its system beside
464
+ # its position (``metadata.coordinate_system``).
465
+
466
+ def _declare_coordinate_systems(self) -> Sequence:
467
+ """The shared coordinate systems this file's positions are in (plan
468
+ 4.2), declared from what opening the file already read -- the
469
+ indices, the plates, the file's own header -- never from an image's
470
+ ``metadata`` (no ``_read_metadata``: ``coordinate_systems`` must stay
471
+ as cheap as opening). Backends override it; the default declares
472
+ none. ``(CoordinateSystem(system_id("stage")),)`` is the usual
473
+ answer; one per well (``system_id("well", well.name)``, 4.9) or per
474
+ series (``system_id("stage", f"s{n}")``, 4.15) where the plan says so;
475
+ ``axes=None`` (the writer fills them, 3.12). Called once, on the first
476
+ access of ``coordinate_systems``; the result is checked (I1)."""
477
+ return ()
478
+
479
+ def _declare_coordinate_transformations(self) -> Sequence:
480
+ """The edges of the graph other than positions (plan 3.3): only
481
+ BigDataViewer has some (affine views and registrations, 9.16). Each a
482
+ ``Transformation`` between declared systems and image keys (I5); the
483
+ same rules as ``_declare_coordinate_systems``."""
484
+ return ()
485
+
486
+ def _declared(self) -> tuple:
487
+ if self._graph is None:
488
+ from ._spatial import check_declared_systems, check_edges, image_key
489
+ systems = check_declared_systems(self._declare_coordinate_systems(), where=self.path)
490
+ edges = self._declare_coordinate_transformations()
491
+ keys = {image_key(image) for image in self.images} if edges else ()
492
+ self._graph = (systems, check_edges(edges, systems, keys, where=self.path))
493
+ return self._graph
494
+
495
+ @property
496
+ def coordinate_systems(self) -> tuple:
497
+ """The shared coordinate systems the backend declares for this file
498
+ (a tuple of ``CoordinateSystem``, plan 3.3, 4.2): the systems its
499
+ images' positions are in, named by ``metadata.coordinate_system``.
500
+ Empty for a backend that declares none. Declared when first read,
501
+ without reading any image's metadata."""
502
+ return self._declared()[0]
503
+
504
+ @property
505
+ def coordinate_transformations(self) -> tuple:
506
+ """The edges of the file's transformation graph other than positions
507
+ (a tuple of ``Transformation``, plan 3.3): empty for every backend
508
+ but BigDataViewer, whose affine views and registrations are edges
509
+ from an image into ``global`` and ``global-registered`` (9.16)."""
510
+ return self._declared()[1]
511
+
512
+ @property
513
+ def collection(self):
514
+ """The tree of what contains what (a ``Collection``, plan 3.1, 3.17):
515
+ a root named after the file's stem holding plate > well > field, or
516
+ series, each holding its full-resolution images. A field or series
517
+ of one image collapses into it; tiles and the orthogonal indices
518
+ (view, illumination, ...) make no collection. The leaves are the
519
+ objects of ``images`` (after ``open(axes=...)`` too), and the tree
520
+ is built from the indices and ``plates`` alone: no metadata is read.
521
+ Built on first access, again when ``images`` has changed
522
+ (``_spatial.file_collection``)."""
523
+ images = tuple(self.images)
524
+ if self._tree is None or self._tree[0] != images: # identity: Image has no __eq__
525
+ from ._spatial import file_collection
526
+ self._tree = (images, file_collection(self))
527
+ return self._tree[1]
528
+
456
529
  def _member_files(self) -> set:
457
530
  """The other files this file's images are read from -- a file set's
458
531
  members -- for ``group_files``. Empty for a file that reads only itself."""
@@ -14,6 +14,14 @@ class CorruptFileError(MicroReaderError):
14
14
  """The file is recognised but inconsistent, e.g. truncated."""
15
15
 
16
16
 
17
+ class InvariantError(MicroReaderError):
18
+ """The spatial model's invariants (plan 3.22) do not hold for a file: a
19
+ coordinate system id that is not one micro-reader makes or not unique
20
+ (I1), a position in a system the file does not declare or an estimated
21
+ one (I2), a position and a coordinate system given without each other
22
+ (I3), or an edge whose end is neither (I5). A backend bug."""
23
+
24
+
17
25
  class OpenInstead(Exception):
18
26
  """Internal, not an error: a backend found that *path* is a member of a set
19
27
  and that the set's metadata file (``.path``) is what opens it (``open``'s
@@ -37,8 +45,9 @@ class CompositeWarning(UserWarning):
37
45
 
38
46
  class CompanionWarning(UserWarning):
39
47
  """An OME companion set's metadata came from somewhere other than the file
40
- opened, or a member path had to be resolved differently than written
41
- (docs/COMPANION.md)."""
48
+ opened, a member path had to be resolved differently than written, or the
49
+ companion's bytes had to be read in another encoding than its XML
50
+ declares (docs/COMPANION.md)."""
42
51
 
43
52
 
44
53
  class SkewedGridWarning(UserWarning):
@@ -146,6 +146,10 @@ class Metadata:
146
146
  is absent / None."""
147
147
  pixel_sizes: dict = field(default_factory=dict) # axis -> Quantity (x y z t)
148
148
  position: dict = field(default_factory=dict) # axis -> Quantity
149
+ #: the id of the shared coordinate system ``position`` is in: one of
150
+ #: ``File.coordinate_systems`` (nominal, ``estimated=False``), set exactly
151
+ #: when the position is not empty (plan I3); None for an unplaced image
152
+ coordinate_system: Optional[str] = None
149
153
  channels: tuple = () # Channel per index of c
150
154
  #: this image's value of its indices that have one (D5): {"emission":
151
155
  #: Quantity(510.0, "nanometer"), "lifetime": Quantity(2.5, "nanosecond")}
@@ -156,6 +160,17 @@ class Metadata:
156
160
  #: a ``position``: axis -> Quantity (Bugra, 2026-10-19; MetaMorph)
157
161
  stage_position: dict = field(default_factory=dict)
158
162
 
163
+ def __post_init__(self):
164
+ if self.coordinate_system is not None and not self.position:
165
+ raise ValueError(f"Metadata: coordinate_system={self.coordinate_system!r} with an "
166
+ "empty position: an image is placed exactly when it has both "
167
+ "(plan I3); give the position, or coordinate_system=None")
168
+
169
+ def unplaced(self) -> "Metadata":
170
+ """This metadata without a position: ``position`` empty and
171
+ ``coordinate_system`` None, the rest kept."""
172
+ return replace(self, position={}, coordinate_system=None)
173
+
159
174
  def scaled(self, factors: dict) -> "Metadata":
160
175
  """This metadata for a resolution level whose pixels are *factors*
161
176
  (axis -> float) times larger than level 0's."""
@@ -57,6 +57,27 @@ def layout(rows: int, columns: int, wells, where: str) -> tuple:
57
57
  return tuple(row_label(r) for r in range(rows)), tuple(str(c + 1) for c in range(columns))
58
58
 
59
59
 
60
+ def well_system(plates, plate: int, well: int) -> str:
61
+ """The id of the coordinate system of well *well* of plate *plate*
62
+ (indices into *plates* and its ``wells``), for backends whose positions
63
+ are relative to the well's centre (plan 4.9; ids 3.23): ``well-B2``, or
64
+ ``well-p1-B2`` when *plates* holds several (``p`` + the plate index)."""
65
+ from ._spatial import system_id
66
+ name = plates[plate].wells[well].name
67
+ return system_id("well", *((f"p{plate}", name) if len(plates) > 1 else (name,)))
68
+
69
+
70
+ def well_systems(plates) -> tuple:
71
+ """One ``CoordinateSystem`` per well with images of *plates*, plate by
72
+ plate in well order (``well_system``'s ids; names "well B2", or "plate 1
73
+ well B2" with several plates): what a backend of plan 4.9 declares."""
74
+ from ._spatial import CoordinateSystem
75
+ several = len(plates) > 1
76
+ return tuple(CoordinateSystem(well_system(plates, p, w),
77
+ f"{f'plate {p} ' if several else ''}well {well.name}")
78
+ for p, plate in enumerate(plates) for w, well in enumerate(plate.wells))
79
+
80
+
60
81
  @dataclass(frozen=True, eq=False)
61
82
  class Field:
62
83
  """One field of a well: its field index and its images (full resolution,