Skip to content

Declared axes: a plain stack's positional Y and X are open (v0.3.4) - #58

Merged
guijacquemet merged 4 commits into
mainfrom
claude/declared-axes-plain-stacks
Oct 31, 2026
Merged

guijacquemet merged 4 commits into
mainfrom
claude/declared-axes-plain-stacks

Conversation

@guijacquemet

@guijacquemet guijacquemet commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Core fix for v0.3.4 (owner decisions of 31 October 2026): check_declared_axes on plain stacks, and named Y/X declared swapped. No version bump here.

Summary

  • metadata: tifffile labels the last two axes of any plain page Y and X (IYX, QYX, QQYX), even when the file has no axis metadata. New named_axes(meta) is now the one rule behind both header_axes and check_declared_axes. It gives the header's axes, with ? (OPEN_AXIS) for each dimension the metadata does not name. That covers the placeholders (I, Q). In a plain stack of pages it also covers the positional Y and X.
    • New field TiffMetadata.axes_from_metadata, set by inspect_tiff from the public series_axes_from_metadata(series). It is False for tifffile generic/uniform series and for a shaped series without "axes". It defaults to True, the strict reading, for a header built by hand.
  • axes: check_declared_axes binds only named positions; an open position takes any role.
  • Swapped Y and X (owner decision): a declaration that differs from the named axes only by swapping Y and X is accepted: the image is read transposed. New declared_axes_transposed(meta, declared) -> bool tells callers when that happened, so they can record it. It raises ValueError for a declaration that does not fit. Any other disagreement stays refused.
  • Length, repeated roles, non-role letters, and the S -> C rule are unchanged. header_axes gives exactly the same results as before. When it gives axes, the check accepts those axes and their Y/X swap, and nothing else (tested).

Behaviour changes

File (tifffile axes) Declared Before After
plain QYX / IYX (no metadata) YXC, XYZ refused accepted (not reported as a swap: Y/X are not named)
plain QQYX YXZC refused accepted
ImageJ / OME / shaped CYX CXY refused accepted, declared_axes_transposed True
ImageJ ZYX ZXY refused accepted, transposed
plain 2D YX XY refused accepted, transposed
CYX YXC refused refused
ImageJ ZYX CYX, CXY, XZY refused refused
plain RGB IYXS TYXZ refused refused (S comes from a tag)

Callers (GitHub main, read only, not edited)

None of the three calls check_declared_axes on main; each has its own prefill_mapping copy. The D86 adoption branches do call it.

  • MorphoRelate (claude/d86-core, Re-pin the vendored Spec to b197ac1; reserved names apply to used_ids.channels #208): the header_axes-gated workaround in _series_axes can go. _series_header builds TiffMetadata by hand for any series, so it must pass axes_from_metadata=series_axes_from_metadata(selected).
  • MorphoFeature (claude/d86-core, PP-4: manifest version 2, row keys, per-file plain-stack confirmation #110): check_header_axes's retry with X and Y swapped is now redundant. Use declared_axes_transposed to record the transposition. Plain stacks declared YXC become accepted, matching main's copy.
  • CellPatcher (main, io.header_axes_disagreement): it already allows swapped Y/X, so adopting the shared check now changes only these:
    • other named axes beyond Y/X (R, A, ...) cannot be declared;
    • partly named files are checked;
    • the length must match.

Tests

  • tests/test_header_axes.py, plain stacks:
    • plain QYX/IYX declared YXC, CYX, ZYX, TYX, XYZ and yxc: accepted;
    • QQYX: accepted;
    • malformed declarations on a plain stack: still refused.
  • tests/test_header_axes.py, named axes:
    • named Y/X beside a placeholder;
    • RGB samples of a plain stack;
    • OME CYX and shaped CYX declared YXC: refused.
  • tests/test_header_axes.py, swaps:
    • CYX declared CXY (ImageJ, OME, shaped), ZYX declared ZXY, and 2D YX declared XY: all accepted and reported transposed;
    • CYX→YXC, ZYX→CYX, ZYX→CXY and ZYX→XZY: refused;
    • a plain stack is never reported transposed.
  • tests/test_header_axes.py, rule and helpers:
    • named_axes for each writer;
    • the per-series flag;
    • a header built by hand;
    • the consistency test between header_axes and check_declared_axes.
  • On the old code: 7 of the plain-stack cases fail. The swap cases CYX→CXY and ZYX→ZXY are refused by the previous code (probe), and the new test file does not import there (declared_axes_transposed is missing).
  • Gate (Python 3.13, pp2 venv, worktree with PYTHONPATH=src): ruff check . passed, ruff format --check . passed, pytest 1394 passed, 1 skipped (the root file-mode test test_discovery.py:665).

🤖 Generated with Claude Code

https://claude.ai/code/session_01VbytvBRB9CmKQ5AAkhEFcv

tifffile reports the last two axes of any plain page as Y and X (QYX,
QQYX, IYX) even when the file has no axis metadata. check_declared_axes
treated them as named, so it refused a correct declaration such as YXC
on a plain QYX stack (a (Y, X, C) array written without metadata).

Only what the metadata names now binds a declaration. New named_axes
gives the rule position by position (? for open), and both header_axes
and check_declared_axes use it. TiffMetadata.axes_from_metadata records
whether the axes came from axis metadata (OME, ImageJ, shaped with axes,
other vendor formats, OME-NGFF). Declarations the header contradicts
(ImageJ ZYX as CYX, OME or shaped CYX as YXC) are still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbytvBRB9CmKQ5AAkhEFcv
A brick that builds a TiffMetadata for a series other than the first
(MorphoRelate's series reader) needs the same flag inspect_tiff records.
A header built by hand keeps the strict default: a placeholder stack's
Y and X stay binding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbytvBRB9CmKQ5AAkhEFcv
Owner decision of 31 October 2026: a declaration that differs from the
header's named axes only by swapping Y and X (CYX declared CXY, ZYX
declared ZXY) means the image is read transposed and is accepted. The new
declared_axes_transposed(meta, declared) tells the caller so it can record
it; it raises ValueError for a declaration that does not fit. Any other
disagreement stays refused (CYX as YXC, ZYX as CYX or CXY).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbytvBRB9CmKQ5AAkhEFcv
Leaving a plain stack's positional Y and X open let a declaration with no
spatial axes at all (QYX declared CTZ) pass. Refuse it by name, and correct
the header_axes/contract wording: check_declared_axes accepts the header's
axes, them with Y and X swapped, and S declared C, not "no others".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbytvBRB9CmKQ5AAkhEFcv

Copy link
Copy Markdown
Contributor Author

Review (independent, from GitHub's current state at 9f321df): no changes needed.

Checked:

  • named_axes opens only placeholders, plus Y and X in a plain stack (a placeholder and no axis metadata). On real files from tifffile 2026.3.3: uniform YX/YXS stay named; generic IYX → ???; shaped without axes QYX → ???; shaped with axes, OME and ImageJ stay named. series.kind and shaped_description give the same answers on the minimum pin, tifffile 2023.7.10.
  • header_axes returns the same as before (open only when a placeholder exists), so masks and other callers are unchanged.
  • Swap rule: named Y and X may only be declared Y or X, and each role appears once, so the only change accepted is a swap of the two. ?YX declared YXC/XYC/YCX is refused; ZYX declared XZY/CXY is refused. declared_axes_transposed is False for a plain stack (nothing named) and raises when the declaration does not fit.
  • A plain stack must still declare Y and X.
  • Tests against old main (the new imports shimmed): the behaviour tests fail there, i.e. plain QYX/IYX declared YXC/XYZ, and CYX→CXY, ZYX→ZXY, YX→XY.
  • Local gate (Python 3.11, zarr 3): ruff check and format clean; 1397 passed, 1 skipped (test_discovery.py:665, file modes ignored as root). CI is green on all 7 jobs.
  • Merges cleanly with Experiment design: one strict reader for other bricks (v0.3.4) #59.

Merging with a merge commit.


Generated by Claude Code

@guijacquemet
guijacquemet marked this pull request as ready for review October 31, 2026 14:38
@guijacquemet
guijacquemet merged commit 3daad9e into main Oct 31, 2026
7 checks passed
guijacquemet pushed a commit that referenced this pull request Oct 31, 2026
guijacquemet added a commit that referenced this pull request Oct 31, 2026
Version 0.3.4: declared axes and one design reader (PRs #58, #59)
@guijacquemet
guijacquemet deleted the claude/declared-axes-plain-stacks branch November 1, 2026 18:05
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