Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ src/morphobricks_preprocessing/
for comparing), space_unit/to_micrometers (the shared unit rule, contract §16),
read_physical_sizes (OME / ImageJ TIFF metadata; also importable
) and ngff_physical_sizes (OME-NGFF scale and axis units)
ngff_scale.py read_scale: an OME-NGFF coordinateTransformations scale, checked; what
is wrong with a malformed one (PhysicalSizes.invalid, issue #94)
dataset.py dataset consistency from the folders: biological repeats per condition, balance
table, unbalanced_images, repeat_missing_conditions, CONSISTENCY_CODES (P2)
design.py experiment design (P3): ExperimentDesign/DesignRow, default pairing, reconcile with
Expand Down Expand Up @@ -313,7 +315,7 @@ functions the tests use.
"confirmed"`).
`AxisReview.axis_error_files` lists them; mask sizes and planes are checked by the
confirmed roles and skip those images (`run.mask_issues`). OME-Zarr images never have
these errors.
these errors: their roles are always those their OME-NGFF axes declare (next bullets).
- **Position-like axes** (`R` tile, `M` mosaic, `J` column, `K` row) cannot get a role. If
they hold more than one plane: `unsupported_axis` error.
- **Missing axes:** a file lacking some of the mapping's axes (e.g. `ZYX` next to `ZCYX`) is
Expand All @@ -333,10 +335,18 @@ functions the tests use.
(`axes._SUPERSEDED_METADATA_CODES`). Shared builder `metadata.channel_count_issues`.
- **OME-Zarr axes:** translated from OME-NGFF metadata (`zarr_input.ngff_axes`: time -> T,
channel -> C, space z/y/x -> Z/Y/X, anything else -> Q). The experiment mapping is
defined on the TIFF arrangement if there are TIFFs; OME-Zarr images with another order
are matched by role (`axes.declared_mapping`, `file_mapping_for`); a misfit is
`ome_zarr_axes_mismatch`. Use `file_mapping_for`, never `mapping.for_axes`, to get an
image's mapping.
defined on the TIFF arrangement if there are TIFFs; every OME-Zarr image, also one with
the mapping's letters, is matched by role (`axes.ome_zarr_mapping`, `declared_mapping`,
`file_mapping_for`; issue #72): a misfit, or confirmed roles that contradict the declared
axes (`ZYX` confirmed `TYX`), is `ome_zarr_axes_mismatch`. Use `file_mapping_for`, never
`mapping.for_axes`, to get an image's mapping. Missing axes are never guessed (issue
#73): the version is read first, and OME-NGFF 0.1/0.2 stores are refused whatever they
contain (re-save as 0.4 or later; owner, 2 November 2026); any other store without axes
is `unreadable_ome_zarr` too, with no confirmation path.
- **OME-Zarr calibration (issue #94):** a malformed `coordinateTransformations`
(`ngff_scale.read_scale`) gives unknown sizes with the reason in
`PhysicalSizes.invalid`, named by the warning `invalid_physical_calibration`; never a
silent fallback, never a substituted size.

### Pixel types and binning (normalization contract §1.2.1–§1.2.2)

Expand Down
11 changes: 6 additions & 5 deletions docs/contract.md

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions docs/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -1514,4 +1514,38 @@ Raised during review; to be decided before the milestones that need them:
`--memory-limit` is parsed without floating point and must be a whole number of bytes
above 0 (`1.5GiB` is accepted, `1.5` and `0.5B` are refused before anything is
read).
60. How are OME-Zarr axes and calibrations checked (issues #72, #73, #94, from the audit)?
*Decided (2 November 2026, design v0.4 §3: nothing guessed, nothing silent):*
(1) **Same letters, contradictory roles (#72)**: every OME-Zarr image is matched by the
roles its OME-NGFF axes declare, also when its letters are the mapping's
(`axes.ome_zarr_mapping`, in `review_axes` and `file_mapping_for`). A confirmed role
that is not the declared letter (`ZYX` confirmed `TYX` or `ZXY`, `QYX` confirmed `CYX`)
is the error `ome_zarr_axes_mismatch`, naming each image with its declared axes and
the confirmed roles. It is not one of the axis-check errors (`axes_contradict_header`
and others) because those offer conversion, and an OME-Zarr image is never converted;
the fix is to correct the roles. An axis of no known type is therefore never given a
role, in any arrangement.
(2) **Missing axes (#73)**, by the version the metadata states, read before the axes:
an OME-NGFF 0.1 or 0.2 store is `unreadable_ome_zarr` whatever it contains, with the
message to re-save it as OME-NGFF 0.4 or later: no spec-default `t, c, z, y, x` is
applied (owner, 2 November 2026; the PR had kept it for 5-D arrays). 0.3, 0.4 and 0.5
require `axes`, so a store without them is `unreadable_ome_zarr`; a store without
`axes` that states no version or another one is `unreadable_ome_zarr` too. Such an
image is refused, with no confirmation path, rather than offered for confirmation as a
plain stack (owner, 2 November 2026): an OME-Zarr image cannot be converted to a
named-axis copy, and the declared-role match refuses an axis of no known type. No new
`axes_from` value.
(3) **Malformed calibration (#94)**: a calibration given but unusable (transformations
not a list or of another type than scale, translation and identity, two scales, a
scale in a file or not one number per axis, a scale value of X, Y, Z or T that is not a
positive finite number) gives unknown sizes on those axes, with what is wrong in
`PhysicalSizes.invalid`, and the warning `invalid_physical_calibration` naming every
image (also when every image has it). A warning, not an error: the pixels are readable
and the image is uncalibrated, as design v0.4 §3 treats an image without a value
(owner, 2 November 2026: stays a warning). It disappears once every size the malformed
calibration leaves unknown is stated in `calibration.csv` (owner, 2 November 2026); a
time step cannot be stated there, so a lost time step of a time series stays named
(review finding: the PR dropped the warning when only X and Y were stated). Only
these metadata problems are caught: any other exception while reading the scale makes
the image unreadable instead of being hidden by a debug log.

10 changes: 7 additions & 3 deletions docs/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,10 @@ notebook's step 3 (§4), also tells how they relate.
axes you know better than its header is saved as a named-axis copy first. A plain stack
of pages without axis metadata is read with the roles you confirm (a warning).
- **OME-Zarr images:** OME-NGFF 0.4 and 0.5 (needs the `zarr` extra; 0.5 needs Python 3.11
or newer). Their axes (time, channel, z, y, x) come from their metadata. Only the
or newer). Their axes (time, channel, z, y, x) come from their metadata, and they are
read only with those roles: roles that give an axis another role (its depth as time,
say) are an error, and an image whose metadata do not name its axes is not read (the
axes are never guessed). Only the
full-resolution level of a single image is read; a folder holding several images (a
plate, or a bioformats2raw export with several series) is an error: export each image
separately or exclude it.
Expand Down Expand Up @@ -902,8 +905,8 @@ continues.
| `output_inside_experiment` | Error | The output folder is inside the experiment. Choose another. |
| `no_images` | Error | No images to analyse (or all excluded). Check the folder. |
| `unreadable_tiff` | Error | A TIFF file cannot be opened. Export it again, or exclude it. |
| `unreadable_ome_zarr` | Error | A `.zarr` folder is not an OME-Zarr image that can be read. Export it again as OME-Zarr (OME-NGFF 0.4 or 0.5), or exclude it. |
| `ome_zarr_axes_mismatch` | Error | An OME-Zarr image declares an axis with several planes that the experiment has no role for (or has no X/Y). Check the axis roles, or process it separately. |
| `unreadable_ome_zarr` | Error | A `.zarr` folder is not an OME-Zarr image that can be read; the message says why. That includes an image whose metadata do not name its axes (OME-NGFF 0.3 to 0.5 require them), and every OME-NGFF 0.1 or 0.2 store (their metadata never name the axes): the axes are never guessed. Re-save an 0.1 or 0.2 store as OME-NGFF 0.4 or later. Export it again as OME-Zarr (OME-NGFF 0.4 or 0.5) with its axes, or exclude it. |
| `ome_zarr_axes_mismatch` | Error | An OME-Zarr image declares an axis with several planes that the experiment has no role for (or has no X/Y), or the confirmed roles give one of its axes another role than its metadata declares (for example its depth `z` confirmed as time `T`); the message names each image with its declared axes and the confirmed roles. An OME-Zarr image is read only with the roles it declares. Check the axis roles, or process it separately. |
| `multiple_series` | Error | A file or OME-Zarr folder holds several images (positions, scenes, series, wells or fields). Export them separately, or exclude it. |
| `unsupported_dtype` | Error | Unsupported pixel type (e.g. float16). Convert, or exclude it. |
| `mixed_dtypes` | Error | Images have different pixel types. Process them separately or convert. |
Expand Down Expand Up @@ -949,6 +952,7 @@ continues.
| `different_pixel_sizes` | Warning | The physical pixel size or z-step (from the metadata or `calibration.csv`) differs between images by more than 0.1 % (for example different objectives). Images outside the largest group of matching sizes are listed; when sizes only change by small steps from image to image, none is listed. Check the microscope settings if this is not intended. |
| `missing_pixel_size` | Warning | Some images have a pixel size (from the metadata or `calibration.csv`) and others do not, so it cannot be checked; images stated as measured in pixels count as without one, and the message says how many. Export them again with metadata, or state their sizes with `calibrate`, if needed. |
| `unsupported_pixel_size_unit` | Warning | An image gives its pixel size or z-step in a unit that is not converted (for example inches); only pm, Å, nm, µm, mm, cm and m are. The message names each image, axis and unit. Those sizes are not used: the image is treated as uncalibrated on those axes. Export it again with the pixel size in a supported unit, or state it with `calibrate`, if the physical size matters. Not reported for an axis whose size is stated. |
| `invalid_physical_calibration` | Warning | An image's metadata give a pixel size, z-step or time step that cannot be used (for OME-Zarr: `coordinateTransformations` that are malformed, of an unsupported type, stored in a separate file, or a scale that is zero, negative or not a number); the message names each image and what is wrong. The image is treated as uncalibrated on those axes; no size is made up. Correct the metadata, or state the sizes with the `calibrate` command: the warning goes once every lost pixel size and z-step is stated. A lost time step cannot be stated, so its warning stays until the metadata are corrected. |
| `calibration_axis_absent` | Warning | `calibration.csv` states a size for an axis some images do not have (for example a Z size for 2D images or projections); the message names them. No size is recorded for an axis an image does not have. Give the statement a narrower target, or no action if it is meant for the other images. |
| `calibration_differs_from_header` | Warning | `calibration.csv` states a pixel size (or `pixels`) that differs from an image's metadata; the message names each image and axis with both sizes. The stated size is used. Correct or remove the statement if the metadata are right. |
| `calibration_target_not_found` | Error | `calibration.csv` names a folder or image that is not in the experiment (names are compared exactly, case included). Correct the target, or remove the line. |
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ baseline = "tools/code_rules_baseline.json"
"morphobricks_preprocessing.notebook_ui" = "ui"
"morphobricks_preprocessing.physical_sizes" = "io"
"morphobricks_preprocessing.pixel_size_checks" = "pure"
"morphobricks_preprocessing.ngff_scale" = "pure"
"morphobricks_preprocessing.calibration" = "pure"
"morphobricks_preprocessing.calibration_io" = "io"
"morphobricks_preprocessing.run.calibrate" = "io"
Expand Down
46 changes: 38 additions & 8 deletions src/morphobricks_preprocessing/axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
contradict the named axes are an error, and such an image is never read (the user corrects
the roles, or saves named-axis copies); an image whose metadata does not name its axes (a
plain stack) is read with the confirmed roles, with a warning, and recorded as confirmed.
An OME-Zarr image is always matched by the roles its OME-NGFF metadata declares
(:func:`ome_zarr_mapping`), also when its axis letters are the mapping's (issue #72).

Nothing here reads pixels; the preview plane is read by
:func:`~morphobricks_preprocessing.planes.read_sample_plane`.
Expand Down Expand Up @@ -322,16 +324,17 @@ def file_mapping_for(
) -> AxisMapping | None:
"""Resolve the experiment mapping for one image, or None if it does not fit.

An image with the mapping's arrangement gets the mapping itself. A TIFF with
some of its dimensions left out gets :meth:`AxisMapping.for_axes`. An OME-Zarr
image with another arrangement is matched by the roles its metadata declares
(see :func:`declared_mapping`). Experiment roles an image lacks are read as
length 1 only if ``missing_as_singleton``.
A TIFF with the mapping's arrangement gets the mapping itself, and one with some
of its dimensions left out gets :meth:`AxisMapping.for_axes`. An OME-Zarr image,
whatever its arrangement, is matched by the roles its metadata declares
(:func:`ome_zarr_mapping`): with the mapping's arrangement but other roles it
does not fit (issue #72). Experiment roles an image lacks are read as length 1
only if ``missing_as_singleton``.
"""
if f.axes == mapping.axes:
return mapping
if f.is_ome_zarr:
resolved, problem = declared_mapping(f, mapping)
resolved, problem = ome_zarr_mapping(f, mapping)
elif f.axes == mapping.axes:
return mapping
else:
resolved = mapping.for_axes(f.axes)
problem = "" if resolved is not None else "mismatch"
Expand All @@ -342,6 +345,33 @@ def file_mapping_for(
return resolved


def ome_zarr_mapping(f: TiffMetadata, mapping: AxisMapping) -> tuple[AxisMapping | None, str]:
"""Resolve the experiment mapping for an OME-Zarr image by the roles it declares.

Every OME-Zarr image is matched by its OME-NGFF axes (:func:`declared_mapping`),
also when its axis letters are the mapping's (issue #72): confirmed roles that
give a declared axis another role (``ZYX`` confirmed ``TYX``, ``ZYX`` confirmed
``ZXY``) or give an axis of no known type a role (``QYX`` confirmed ``CYX``)
contradict the metadata, and the image is not read under them.

Returns
-------
tuple
As :func:`declared_mapping`, and ``(None, "contradicted")`` for an image
with the mapping's arrangement when a confirmed role is not the letter its
metadata declares for that axis.
"""
if f.axes == mapping.axes and any(
role is not None and role != axis for axis, role in zip(f.axes, mapping.roles, strict=True)
):
return None, CONTRADICTED
return declared_mapping(f, mapping)


CONTRADICTED = "contradicted"
""":func:`ome_zarr_mapping`'s problem for roles that contradict an image's OME-NGFF axes."""


def declared_mapping(f: TiffMetadata, mapping: AxisMapping) -> tuple[AxisMapping | None, str]:
"""Match an OME-Zarr image to the experiment mapping by the roles it declares.

Expand Down
6 changes: 4 additions & 2 deletions src/morphobricks_preprocessing/axis_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ def header_axis_issue(meta: TiffMetadata, mapping: AxisMapping) -> str | None:
``unassigned_axis``, and a position axis (``R``, ``M``, ``J``, ``K``) its
``unsupported_axis``, so they are left to those.

OME-Zarr images name their axes in their OME-NGFF metadata, which gives their roles
(:func:`declared_mapping`): they never have one of these problems.
OME-Zarr images name their axes in their OME-NGFF metadata, which gives their roles:
confirmed roles that contradict them are :func:`.axes.ome_zarr_mapping`'s
``ome_zarr_axes_mismatch`` (issue #72), whatever the arrangement, so they never have
one of these problems (an OME-Zarr image cannot be converted to a named-axis copy).

Parameters
----------
Expand Down
Loading
Loading