Skip to content

PP-5: masks (stated axes, never converted, labels always used) - #111

Merged
guijacquemet merged 4 commits into
mainfrom
claude/pp-5-masks
Nov 1, 2026
Merged

guijacquemet merged 4 commits into
mainfrom
claude/pp-5-masks

Conversation

@guijacquemet

@guijacquemet guijacquemet commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

PP-5 of the v0.4.0 plan (design v0.4 §2, §3). Based on main: PP-4 (#110) is merged, and main was merged in (fdf16fb).

Commits

  1. 54a68d8 moves code only: masks.py becomes the masks/ package (model, files, header, layout, read, checks, labels).
  2. 88a14b8 holds the feature work below.
  3. fdf16fb merges main (PP-4 with its review fix).
  4. 3b572ca is a review fix: a mask's manifest axes are its stored axes (IYX), beside its roles (TYX when stated); the contract's masks entry now describes stated and shared.

Behaviour changes

  • Stated mask axes. A plain-stack mask gets its axes stated once per mask type, with --mask-axes TYPE=ROLES on check, run and masks, or mask_axes= in preflight and run_experiment. The statement also covers masks of that type added later.
    • A statement that does not fit the mask gives invalid_mask_axes. So does a statement for a mask type that isn't found.
    • A statement that contradicts an axis the mask's metadata names gives mask_axes_contradict_header.
    • The manifest records axes_from: "stated".
  • Shared 2-D masks. A 2-D mask on an image with several Z/T planes is still mask_planes_mismatch, unless its type is stated YX. Then one plane is shared by every plane of the image, recorded as shared: true, and read_mask returns it for any z/t.
  • Masks are never transposed. A mask stored X before Y, or with its image's height and width swapped, gives the error mask_layout with a numpy.swapaxes hint. Before, the second case was mask_size_mismatch.
  • image_named_as_mask is now an error. It was a warning. To clear it, rename the file or exclude it; an excluded file is no longer reported. _masks and _cp_masks are added to MASK_NAME_SUFFIXES.
  • Masks of excluded images now get their own warning, mask_of_excluded_image, instead of mask_without_image. Unpaired masks stay a warning, because §3 keeps the severity of every existing check (see the question below).
  • New error output_inside_masks.
  • OME-Zarr labels/ are always used. The ome_zarr_labels parameter is removed from preflight, run_experiment and find_masks. Each label image has its own fingerprint. The labels group is left out of the image's source_fingerprint, so adding a label image doesn't recompute the image's rows.
    • One-time effect: OME-Zarr images that have labels get a new fingerprint, so their rows are recomputed once.
  • Code moves:
    • position_id, IMAGE_SUFFIXES and NAME_SUFFIXES move to image_names.py.
    • The mask-name screen moves to mask_names.py.
    • The axis-letter constants move to axis_names.py.
    • Public exports are unchanged.

Tests

  • tests/test_masks_v040.py has 26 tests. On the PP-4 code, 23 of them fail. The other 3 pass there on purpose, because they guard behaviour that stays the same: a single-plane 2-D mask needs no statement, a named TYX mask works, and unpaired masks stay a warning.
  • Existing tests were adapted where behaviour changed on purpose:
    • mask_of_excluded_image instead of mask_without_image;
    • mask_layout for a swapped mask;
    • the image_named_as_mask warning test moved into the new file as an error test.

Gate (at 3b572ca)

  • ruff check and ruff format --check pass, and so does tools/check.py --rules. The baseline only went down: entries were removed, lowered, or renamed for the moved code.
  • Python 3.13 with zarr 3: 1391 passed, 1 skipped. The skip is the file-mode test, which can't run as root.
  • Python 3.11 with zarr 2.18: 1373 passed, 19 skipped: the same skip plus 18 OME-NGFF 0.5 tests that need zarr 3.
  • CI is green on all six jobs.

Notes

discovery.py is 610 lines, still over the 600 limit but below its baseline entry, which went down.

Owner answer

mask_without_image stays a warning (owner, 1 November 2026; design change log).

https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a


Generated by Claude Code

Plan-structure audit §5, to make room for the mask work. No behaviour
change.
- masks/model.py: MaskFile, MaskInventory and the constants.
- masks/files.py: find_masks and the pairing.
- masks/header.py: the header without pixels.
- masks/layout.py: the stored axes and plane index.
- masks/read.py: read_mask, read_mask_file.
- masks/checks.py: header checks, mask_plane_issues.
- masks/labels.py: check_labels, LARGEST_LABEL.

morphobricks_preprocessing.masks re-exports the public names. The modules
are layered so that there is no import cycle. Baseline entries are re-keyed
with --rename.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a
- Plain-stack masks get their axes stated once per mask type (--mask-axes
  TYPE=ROLES, mask_axes=); a 2-D mask on a multi-plane image is shared only
  when stated YX. Recorded as axes_from "stated" and shared in the manifest.
- A mask stored with Y and X the other way round from its image is the error
  mask_layout with a numpy.swapaxes hint; masks are never transposed.
- image_named_as_mask becomes an error (rename or exclude); _masks and
  _cp_masks join the mask name suffixes. Excluding the file clears it.
- Masks of excluded images are reported apart (mask_of_excluded_image);
  unpaired masks stay a warning, as every existing check keeps its severity.
- output_inside_masks error.
- OME-Zarr label images are always used (ome_zarr_labels removed) and are
  fingerprinted per label, outside the image's fingerprint.
- Name helpers move from discovery.py to image_names.py and mask_names.py;
  axis-name constants to axis_names.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a
Conflicts resolved hunk by hunk:
- run/pipeline.py: PP-5's mask_axes with main's unchanged_confirmation and
  asdict counts.
- docs/contract.md: main's error row (axes_x_before_y) plus the errors PP-5
  added but had not listed there (image_named_as_mask, output_inside_masks,
  mask_layout, invalid_mask_axes, mask_axes_contradict_header); PP-5's
  warning row.
- tools/code_rules_baseline.json: the lower value of each entry, PP-5's
  renames, main's noqa counts; run_experiment 159 lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a
@guijacquemet
guijacquemet changed the base branch from claude/pp-4-manifest-v2 to main November 1, 2026 17:42
A stated plain-stack mask was recorded with axes and roles both "TYX", so
the manifest lost how the mask is stored ("IYX"): a brick reading it from
the manifest could not read it as stored. The manifest's mask "axes" are
now the header's letters, "roles" what they are read as.

Contract §9: the masks entry describes axes_from "stated" and shared true
(it still said header/false and "the manifest stays version 1"), and that
label images give a masks entry without a masks folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a
@guijacquemet
guijacquemet marked this pull request as ready for review November 1, 2026 17:48

Copy link
Copy Markdown
Contributor Author

Independent review of PP-5 (design v0.4 rev 4 §2, §3, §6, §9, and the change log).

Brought up to date: merged main after PP-4 (fdf16fb, a merge with no rebase) and retargeted to main. I resolved the conflicts hunk by hunk:

  • run/pipeline.py: mask_axes together with PP-4's unchanged_confirmation.
  • Contract error row: PP-4's axes_x_before_y, plus PP-5's new errors, which were missing from it (image_named_as_mask, output_inside_masks, mask_layout, invalid_mask_axes, mask_axes_contradict_header).
  • Baseline: the lower value of each entry.

Finding, fixed in 3b572ca: for a stated plain-stack mask, the manifest recorded axes and roles as the same value (TYX). The stored layout (IYX) was lost, so a brick could not read the mask as stored from the manifest. axes now records the header's letters. I added a regression test, which fails without the fix. The contract's masks entry also still said axes_from: header, shared: false and "manifest stays version 1"; it is now updated.

Verified:

  • 54a68d8 only moves code (--color-moved). The moves in 88a14b8 (image_names, mask_names, axis_names) are also moves; their only changes are the intended _masks/_cp_masks suffixes, the error severity and the exclusion handling.
  • Stated mask axes are per type, and a statement covers masks added later. Bad statements are refused: invalid_mask_axes for the wrong count or role, or an unknown type; mask_axes_contradict_header for a statement that contradicts a named axis. Reading follows the stated roles as stored. Probed: TXY gives mask_layout, and YXT gives a size or plane error, never a reorder.
  • A shared 2-D mask needs Cells=YX. Without it, a 2-D mask on a stack is still mask_planes_mismatch. A shared mask is recorded shared: true, and read_mask returns it for any t.
  • mask_layout: X stored before Y, or the image's height and width swapped. It is an error with a swapaxes hint, and no masks code transposes.
  • image_named_as_mask is an error, and excluding the file clears it. _masks/_cp_masks are in MASK_NAME_SUFFIXES.
  • labels/ are always used (the flag was removed). Each label image is fingerprinted, and the image's own fingerprint leaves out labels. Adding a label image reuses the image's rows.
  • mask_without_image stays a warning, as the owner decided. Masks of excluded images are reported apart, as mask_of_excluded_image.
  • §9: I checked the baseline diff against main by hand. Entries are only removed or lowered; the two moved private functions keep their entries under their new paths. discovery.py is 610 lines (it was 847).
  • Gate: ruff, format and --rules pass. Python 3.13 with zarr 3: 1391 passed, 1 skipped. Python 3.11 with zarr 2.18: 1373 passed, 19 skipped. All six CI jobs are green.

Generated by Claude Code

@guijacquemet
guijacquemet merged commit 885e35d into main Nov 1, 2026
7 checks passed
@guijacquemet
guijacquemet deleted the claude/pp-5-masks branch November 14, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants