Skip to content

PP-6: RunInputs and testing.make_clean_run - #112

Merged
guijacquemet merged 6 commits into
mainfrom
claude/pp-6-run-inputs
Nov 1, 2026
Merged

guijacquemet merged 6 commits into
mainfrom
claude/pp-6-run-inputs

Conversation

@guijacquemet

@guijacquemet guijacquemet commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

PP-6 of the v0.4.0 plan (design v0.4 §6). Based on main: PP-4 (#110) and PP-5 (#111) are merged, and main was merged in (de835c5).

Commits

  1. c44798c merges PP-4's latest commits into the old PP-5 tip (history from when this PR was stacked).
  2. 21e44bc and 1023830 add RunInputs, testing.make_clean_run, the contract tests and contract §9.1.
  3. de835c5 merges main (PP-4 and PP-5 with their review fixes). The only conflict was the error row of docs/contract.md; main's row is a superset and was kept.
  4. 973c4ca (review): read_mask reads through the mask's recorded stored axes and roles; open checks every mask's fingerprint too; make_clean_run(design=True); more contract tests.
  5. 41b0d73 (review): check_sources refuses a version 1 manifest (manifest_before_v040).

What it adds

  • RunInputs.open(output, experiment_root=None, masks_root=None) in the new module run_inputs.py (io layer). It refuses with RunInputsError when:

    • the manifest is version 1 ("written before v0.4.0, without the axis check");
    • the SHA-256 of plane_histograms.csv or experiment_design.csv is not the manifest's;
    • any image or mask changed or is gone (fingerprint check). The message names experiment_root= or masks_root= for files that were only moved.

    It checks the fingerprint again on every read. The table is read once, when the run is opened, and kept as a snapshot.

  • It gives:

    • fovs(): Fov records with the FOV key, plane_ids, axes, roles, axes_from, channel names, dtype, significant_bits, and the Z/T/C counts. They come from the manifest and the table; nothing is parsed from names.
    • pixel_sizes(fov): value, unit and source for each role.
    • read_plane(fov, z, t): a CYX array of the stored values.
    • read_image(fov): a lazy TZCYX LazyImage.
    • Both readers return canonical order. Following the owner rule in the change log, a file whose own header names X before Y (for example ZCXY) is returned Y before X. Nothing else is reordered.
    • mask_types().
    • read_mask(fov, type, z, t): uses the recorded pairing only. It reads the mask as stored, through the manifest's axes (stored, for example IYX) and roles (for example TYX), with preprocessing's reader, which runs check_labels. It refuses a mask that changed since the run, or whose header no longer gives the recorded axes or shape. A stated shared 2-D mask is returned for any plane.
    • histograms(), design() and image_type().

    A RunInputs can be pickled and used in worker processes; a test uses multiprocessing spawn.

  • morphobricks_preprocessing.testing.make_clean_run(folder, *, axes, shape, mask_types, pixel_size, design, seed) writes a two-condition experiment of named-axis OME-TIFFs, with optional masks and an optional confirmed design, runs preprocessing on it, and returns a CleanRun (experiment, output, masks, result).

  • check_sources now fails every image with the single error manifest_before_v040 when the manifest is version 1 (design §6: every v1 run is redone). It does not check masks; RunInputs checks them on open and on every read.

  • RunInputs, RunInputsError, Fov and LazyImage are exported. docs/contract.md gets a new §9.1, and the user guide lists the new code.

Tests

  • tests/test_run_inputs.py has 23 contract tests, and tests/test_check_sources.py has one new test.

  • None of the RunInputs tests can pass on main, because RunInputs doesn't exist there; collection fails with an ImportError.

  • Against the PR before review (de835c5), 5 of the review's new tests fail:

    • the design tests (2);
    • a mask stored otherwise than recorded;
    • a mask changed or moved before opening;
    • the v1 refusal in check_sources.

    The others pass there; they add coverage of behaviour that was already right:

    • spawned worker processes;
    • a header that names X before Y;
    • TZCYX read from a ZTCYX file;
    • a stated plain-stack mask;
    • OME-Zarr label images.

Gate (at 41b0d73)

  • ruff check and ruff format --check pass, and so does tools/check.py --rules.
  • The baseline only shrank: the C901 noqa count went from 2 to 1.
  • Python 3.13 with zarr 3: 1415 passed, 1 skipped. The skip is the file-mode test, which can't run as root.
  • Python 3.11 with zarr 2.18: 1397 passed, 19 skipped. That is the same skip plus 18 OME-NGFF 0.5 tests that need zarr 3.
  • CI is green on all seven jobs.

After this PR the owner tags v0.4.0rc1 (design §8).

https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a


Generated by Claude Code

PP-5 plus PP-4's latest, so PP-6 builds on both; PP-5's branch itself is left
to its reviewer. Conflict in docs/contract.md: the error row gains both
axes_x_before_y and image_named_as_mask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a
RunInputs refuses v1 manifests, a changed table or design, and changed images
(at open and at every read); reads planes, lazy images and masks by the
recorded pairing with the label check; pickles. testing.make_clean_run
builds a tiny clean run for the bricks' tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a
The only conflict was the error row of docs/contract.md: main's row is a
superset of this branch's (it lists image_named_as_mask with PP-5's mask
errors), so main's row is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a
…ks on open

RunInputs.read_mask took a mask's stored axes from the file's header as
read now and ignored the manifest's "axes" (the stored axes recorded since
3b572ca). It now reads through the recorded axes and roles, and refuses a
mask whose header no longer gives the recorded axes or shape.

RunInputs.open checked only the images' fingerprints; design §6 says each
file is checked when the run is opened. It now checks every recorded mask
too, and the message names masks_root= (experiment_root= for images) for
files that were only moved.

make_clean_run gets design=True (a confirmed design, ctrl the control), so
bricks can test runs with experiment_design.csv.

New tests: design() of a run with a design, a changed or missing design,
spawned worker processes reading planes and masks, a header naming X before
Y read Y before X (owner rule), TZCYX from a ZTCYX file, a stated
plain-stack mask read through its stored axes, a mask stored otherwise than
recorded, a mask changed or moved before opening, and OME-Zarr label images.
Contract §9.1 lists these and the canonical-order rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WX6rdMV1r5jUPUR94kxV5a
Design v0.4 §6: every v1 run must be redone, and RunInputs refuses v1
manifests; check_sources still accepted them. It now fails every image with
the single error manifest_before_v040. Extracting the issue builder brings
check_sources under the complexity limit, so its C901 noqa goes (the noqa
baseline shrinks from 2 to 1).

Masks are not added to check_sources: RunInputs checks them on open and on
every read, and file runs read through RunInputs from v0.4.0.

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-5-masks to main November 1, 2026 18:10
@guijacquemet
guijacquemet marked this pull request as ready for review November 1, 2026 18:21

Copy link
Copy Markdown
Contributor Author

Independent review of PP-6 (design v0.4 §6, §8, §9, and the change log).

Brought up to date: merged main after PP-4 and PP-5 (de835c5, a merge with no rebase) and retargeted to main. The only conflict was the error row of docs/contract.md. Main's row is a superset of this branch's, so it was kept.

Findings, fixed:

  • 973c4ca:
    • read_mask took a mask's stored axes from its header as it reads now, and ignored the manifest's axes. Since 3b572ca, the manifest records those stored axes. read_mask now reads through the recorded axes and roles, and refuses a mask whose header no longer gives them or the recorded shape.
    • open checked only the images, but §6 says each file is checked when the run is opened. It now checks every mask too, and the message names masks_root= for masks that were moved.
    • make_clean_run(design=True) is new. I added the missing test of design() on a run that has a design, and a test that a changed or missing design is refused.
  • 41b0d73: check_sources accepted version 1 manifests, but §6 says every v1 run must be redone. It now fails every image with manifest_before_v040. It still does not check masks: §6 puts mask checks in RunInputs, which makes them on open and on every read. The C901 noqa went away, so the noqa baseline went from 2 to 1.
  • 5 of the new tests fail on the PR before review (de835c5).

Verified:

  • Opening a run: it refuses a v1 manifest, a table or design whose SHA-256 is not the manifest's, and an image or mask that changed or is gone. It checks the fingerprint again on every read.
  • Pickling: an unpickled RunInputs works, and so does one used under multiprocessing spawn, reading planes and masks.
  • Canonical order: a ZCXY header gives Y-before-X planes and images (owner rule). ZTCYX storage gives TZCYX. Nothing else is reordered.
  • Masks: a stated plain-stack mask (IYX stored, TYX roles) is read as stored. A shared 2-D mask serves every plane. Labels are checked on every read. OME-Zarr label images are read by their recorded pairing.
  • fovs(): keys come from the manifest's labels and position_id, never parsed. Z/T counts come from the table, which keeps every plane.
  • §9: tools/check.py --rules passes. I checked the baseline diff against main by hand: the only change is the noqa count going down. run_inputs.py is 421 lines.
  • Gate:
    • Python 3.13 with zarr 3: 1415 passed, 1 skipped (the file-mode test, which can't run as root).
    • Python 3.11 with zarr 2.18: 1397 passed, 19 skipped (the same skip plus 18 OME-NGFF 0.5 tests that need zarr 3).
    • All seven CI jobs are green.

Generated by Claude Code

@guijacquemet
guijacquemet merged commit bd4a5dc into main Nov 1, 2026
7 checks passed
@guijacquemet
guijacquemet deleted the claude/pp-6-run-inputs branch November 1, 2026 18:21
@guijacquemet
guijacquemet restored the claude/pp-6-run-inputs branch November 1, 2026 18:21
@guijacquemet
guijacquemet deleted the claude/pp-6-run-inputs 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