Skip to content

stain.check_options: check a conversion request without pixels (#97) - #98

Open
guijacquemet wants to merge 4 commits into
mainfrom
ccr-52991b66-5vnbgy
Open

guijacquemet wants to merge 4 commits into
mainfrom
ccr-52991b66-5vnbgy

Conversation

@guijacquemet

Copy link
Copy Markdown
Contributor

Closes #97. Adds a public, pixel-free morphobricks_normalization.stain.check_options(preset, *, white=None, stain_vectors=None, polarizer=None). It runs every check to_scalar makes about these arguments, so consumers can refuse an unusable request before reading an image. This closes the one known limitation of fiberfeatures v0.6.0's RGB input (review J/K item R1).

What it checks, in to_scalar's order:

  1. Unknown preset, refused with the known presets listed.
  2. Presence rules: a missing needed option is refused, an unused option is refused (never ignored), and a polarizer must be in POLARIZERS.
  3. white: three finite values in (0, 255]. A masked array (an all-false mask included) or a sequence holding np.ma.masked is refused before np.asarray.
  4. Stain vectors (custom_od): names, three finite values each, norm ≥ 1e-6, masked values refused, collinear vectors (< 5°, sign-insensitive) refused, condition number > 50 refused. The default vectors go through the same checks.

One implementation:

  • A private _checked_options() does the checks and returns the checked white and matrix. check_options wraps it, and to_scalar calls it directly to reuse the values, so each check runs once per call.
  • _custom_od and _white_rescaled now receive checked values.
  • Behaviour change: when both an option and the pixels are invalid, the option error is now reported first. Noted in the CHANGELOG.

Parity with main:

  • 33 conversions (every preset, YXC/CYX, with and without white, default and user vectors, both polarizers, passthrough float and uint16) give identical images, exclusions and provenance (same SHA-256).
  • For 40 refusal cases, the exception type and message text are identical.

Tests (tests/test_stain_check_options.py, NumPy only):

  • 40 refusals called with no image, plus the same 40 through to_scalar, asserting identical messages;
  • 17 accepted cases, at least one per preset;
  • masked values refused before np.asarray;
  • options refused before pixels;
  • arguments not modified;
  • check_options runs with scikit-image blocked.

Checks (tools/check.py, Python 3.12):

  • With [dev,histology]: 2138 passed, 2 skipped (CSBDeep is installed only in its own CI job).
  • Histology job files: 386 passed.
  • Without the extra: 2077 passed, 63 skipped (scikit-image missing, as before).
  • Not run locally: the CSBDeep parity job, the macOS and Windows matrix, Python 3.11 and 3.13, and the spec-pin job. CI covers them.

No version change. It ships in the next normalization release, with the coordinated pin move; fiberfeatures and MorphoFeature then call it at planning.


Generated by Claude Code

check_options(preset, *, white=None, stain_vectors=None, polarizer=None)
makes, without pixels, every check to_scalar makes of the preset and these
arguments: unknown preset, the presence rules (needed, unused, polarizer),
white (range, shape, masked values before np.asarray) and the stain matrix
(names, shape, finiteness, masked values, collinearity, conditioning; the
preset's default vectors when none are given). to_scalar runs the same code
first and uses its checked white and matrix, so there is one implementation;
exception types, messages and outputs are unchanged.

Tests call check_options with no image for every refusal, accept a request
per preset, compare to_scalar's and check_options' refusals message for
message, and show masked inputs are refused before np.asarray. Contract
§3.4, CHANGELOG (Unreleased) and the AGENTS.md module map are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbytvF3Gsa85ix8Ne2yMDW
…eeze v0.6.0 parity (#97)

Fixes the three review blockers of PR #98.

B1. _check_arguments now requires a str polarizer before the membership
test. np.array(["linear"]) passed `in POLARIZERS` element-wise and made
orientation_valid a NumPy array; it is now refused with a ValueError naming
the type (numpy.ndarray, bytes, int, list). None still gets the "needs
polarizer" message.

B2. optical_density._refuse_masked searches nested lists, tuples and object
arrays (np.ma.masked or a masked array at any depth), so white and every
stain vector (residual included) with masked content are refused as masked
before np.asarray. v0.6.0 refused these later as "not three numbers" or an
inhomogeneous sequence; the CHANGELOG lists this as a safety fix to the
refusal-parity claim.

B3. tests/data/stain_v060_parity.json, recorded by tests/stain_parity_cases.py
on 95e1d6d (v0.6.0) in a temporary worktree, freezes the full exception type
and message of the 40 refusals and the SHA-256 of image, exclusion and
canonical provenance of 33 conversions. check_options and to_scalar must
match the messages exactly; the 11 intentionally changed inputs (B1, B2) are
listed with their v0.6.0 outcome and their new one. Default stain vectors are
proven to go through the user-vector checks by replacing he_custom's defaults
in a copy of PRESETS. The stale test_messages_are_unchanged comment is gone.
test_stain_check_options.py joins the CI histology job so the scikit-image
conversions run there without skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbytvF3Gsa85ix8Ne2yMDW
CI failed the six custom_od conversions of test_outputs_are_those_of_v060 on every OS with the
same digests, while they passed on the machine that recorded the table: custom_od solves a stain
matrix through LAPACK, and OpenBLAS selects its kernel by CPU, so the last bits of the image and
of the provenance (stain matrix, condition number) depend on the CPU, not on this code.

The parity table now freezes, for custom_od only, the image values and the plain provenance,
compared with a relative tolerance of 1e-9 (floats; everything else exactly), and the exclusion's
digest exactly; the 27 other conversions keep their SHA-256 digests. Re-recorded on 95e1d6d in a
temporary worktree: the refusals, the changed cases and the 27 digests are byte-identical to the
previous table, and the six exclusion digests are unchanged. A 1e-6 relative change of a frozen
image is caught.

tools/check.py: 2222 passed, 2 skipped (csbdeep not installed), code rules and Ruff clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbytvF3Gsa85ix8Ne2yMDW
…tests (review M)

B1 (review M): an accepted str subclass such as np.str_ was stored as is in the provenance's
polarizer. orientation_valid came from comparing it, and the test only compared it with another
comparison result. The provenance now holds str(polarizer), and orientation_valid is
bool(...). The test asserts type(...) is str and type(...) is bool for "circular", "linear",
np.str_("circular") and np.str_("linear"). The numerical conversion is unchanged.

FILE (review M):
- custom_od parity now freezes the exclusion values as well as the image values. Both are
  float32, compared with rtol 1e-6 and atol 1e-7 (a few float32 ULPs; rtol 1e-9 was effectively
  bitwise for float32). Float64 provenance stays at 1e-9. Re-recorded on 95e1d6d: the refusals,
  the changed cases, the 27 digests, the six images and the six provenances are identical to the
  previous table.
- Tests: the masked search ends on a list or an object array that holds itself, with and without
  masked content. stain_vectors_from_regions refuses a masked white before np.asarray.

tools/check.py: 2230 passed, 2 skipped (csbdeep not installed), code rules and Ruff clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbytvF3Gsa85ix8Ne2yMDW
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.

Public pixel-free check of to_scalar options (preset, white, stain vectors, polarizer)

2 participants