Repository navigation
Stage 3 S3.2: plane_histograms v5 wide, fits by channel ID - #90
Merged
Merged
Conversation
This was referenced Nov 11, 2026
…ID (ADR 0003) Wide table (HV1): one row per plane, a 'channels' presence column, per-channel columns named by canonical channel ID for every ID of the experiment, sorted; absent channels are blank and a value there is refused. IDs are read from Histogram_ValidPixelCount_<ID> (HV2). A failed channel measurement writes nothing (HV3, ChannelMeasurementFailure / ChannelMeasurementError). Version 4 is refused naming version 5 and the fix (HV4); no converter. PlaneRecord.channels (required), fit/select/apply, Parameters columns, summaries, intensity checks and range checks address channels by ID; a channel is pooled over the rows that have it, reported when only some do. Parameters and its columns move to parameters.py (fitting.py over 600 lines). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…v04, v05, v06, v13, v17, v22 Vendors the Spec's standard test set at cc9820f (tools/vendor.py) with the pin test and a CI check; the vendored folder is excluded from Ruff and the code rules, as in morphobricks-preprocessing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
… N61-N64; CHANGELOG Unreleased Adds tools/evidence_v4_parity.py, draft-only evidence that fully present planes give the version-4 numbers bit for bit (delete before merge). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
The #91 tests for 64-bit integers and overflowing tail squares and the #92 test of exact percentile names build PlaneRecords and Parameters; they now pass channel IDs (PlaneRecord channels=, write_plane_histograms channel_ids=, Parameters channels by ID). The code-rules baseline drops the two entries the merge left behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…t is chosen
The Spec (experiment-v1 §8, A_SCOPE_INCOMPLETE; ADR 0001 SI1) refuses a channel that
is absent from part of the requested scope unless the user selects and records an
eligible subset. fit no longer pools such a channel over the planes that have it by
default: it raises ChannelScopeIncompleteError, naming every such channel and the
planes that lack it ("present in k of n planes"). The new setting
NormalizationSettings.partial_channels = "planes_with_channel" is the explicit
opt-in: the channel is pooled over the planes that have it, the others are named in
the channel_partially_present warning, and the choice is stored by to_dict (left out
at the default, so existing settings dicts are unchanged). RGB resets it: RGB rows
always hold R, G and B.
Tests: the owner's extra-stain case (global fit refused, fitted with the opt-in, bit
for bit like repeat 2 alone), v17 (R002 refused; PHASE from acquisition_1 with the
opt-in), v05/v06 and the discrepant FOV refused then fitted with the opt-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Open question 4 of #90: the version-4 parameter column Normalization_Offset_C0 is also a valid channel-ID column (C0 is a legal Token), so from_columns would read an old row as channel C0. Parameters.to_columns now writes normalization_parameters_version = 2 (appended to COLUMNS), and from_columns refuses a row without it (normalization 0.4 and earlier, C-index columns) or of another version, with a message saying to fit again; nothing is converted (ADR 0002 AR10). The version is accepted as text or as the number a table reader gives (2, or 2.0 in a column with blanks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Open question 5 of #90. The writer left an existing table in place when a run had a failed channel measurement, so an earlier run's table stayed beside this run's other outputs and read as current. write_plane_histograms now removes a file already at the path before raising ChannelMeasurementError; the error names it (new attribute removed, and one sentence in the message). Nothing else in the folder is touched, and other errors still leave the file as it was (the atomic write is unchanged). _check_universe moves to records.check_universe (pure record validation) to keep csv_io.py within the 600-line rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…draft evidence tool Contract: header IDs are the declared channels, never used_ids (§6.1); code point order and the place of the channels column (§6.1, §6.2); a failed measurement removes an earlier table (§6.2); normalization_parameters_version and the one channel-order rule, RGB included (§8.1, §8.2); partial_channels (§1.5, §2, §8.3); the new refusals (§9.1) and the opt-in-only channel_partially_present warning (§9.2). Plan §12 lists the implementation choices for the owner's review. CHANGELOG Unreleased describes them and the evidence; the #91 entries name Histogram_Exact_<ID>. tools/evidence_v4_parity.py was draft-only (it compares two installed releases, so it is not a test); it is removed after its last run (3600 fitted channels, identical to main 0b35530 and to v0.4.0). The test helper takes the declared channels, with a test that a retired ID stays out of the header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
guijacquemet
force-pushed
the
claude/stage3-v5-histograms
branch
from
November 11, 2026 08:17
c9d9120 to
5a22848
Compare
guijacquemet
marked this pull request as ready for review
November 11, 2026 08:20
Owner decision of 11 November 2026 (plan N65; #91's question). The underflow and overflow of a robust histogram are stored as Histogram_{Underflow,Overflow}Count, ...Mean and ...Std per channel ID, replacing the raw float64 Sum and SumOfSquares, which cancel for tails of large values with a small spread and overflow beyond about 1.3e154. HistogramTail is now (count, mean, std). The new pure module tail_moments.py takes every tail pixel at its exact value (a float as its 53-bit significand times a power of two, an integer as a Python int), keeps the sum and sum of squares as exact rationals over blocks, and rounds the mean and the population standard deviation (integer square root with a sticky bit) once each to the nearest float64. Both are finite for finite pixels (the std is at most half the tail's range), so no scale column is needed, and a plane gives the same bits whole or in blocks of any order. Pooling adds count * std**2 and count * (mean - pooled mean)**2 per tail (centred, no cancellation) with the existing power-of-two rescaling on overflow; the pooled mean of tails whose products overflow is rescaled the same way. This supersedes #91's named refusals of infinite tail sums and sums of squares: those tails now give their finite statistics. integrity.check_tail checks a finite mean in the tail's range and a std of at most half of it. Tests: tests/test_tail_moments.py against an exact Fraction oracle (large values with a small spread where the raw formula is wrong by more than the spread, squares beyond float64, subnormals, both float64 limits, 64-bit integers beyond 2**53, blocks in any order, pooling exact given the stored moments). Tests of the old sums were rewritten for the moments; blocks now equal whole planes bit for bit. Numbers without tails do not change: the 400 golden fits and the fully-present-plane comparison are identical. tools/dump_outputs.py dumps (count, mean, std), or the old fields on older commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Contributor
Author
Independent science review of head 1a0109d: approve and mergeI reviewed against ADR 0003 HV1–HV4 (Spec b4b1008), plan §12 N61–N66, the contract, and the owner decisions of 11 November. I found no defects, so this review adds no commits. Tail moments (N65): checked with my own exact oracle. I wrote the oracle independently of
Results:
v5 table (HV1, HV2, HV4)
Fits by ID (N66)
Parameter columns (version 2) and HV3
Evidence: re-run here, all digests match the PR
Gate and CI
🤖 Generated with Claude Code Generated by Claude Code |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ready for review. Do not merge until the owner has reviewed it. On 11 November 2026 the owner decided to start Stage 3 now. Different channel counts across repeats are to be enabled as soon as possible, and nobody uses production yet.
This PR is rebased on
main0b35530, which includes #91 (numerics) and #92 (validation). The owner decided two points the same day:The remaining open questions of the draft are resolved below, each on the side the Spec states or the safer one; please confirm or overrule them. The version is not bumped and nothing is tagged. Everything is under CHANGELOG "Unreleased".
I read MorphoBricks-Spec
mainb4b1008. It differs from the vendored pin cc9820f only indocs/pins.md, so the pin stays at cc9820f (vendor.py --check: identical). Documents read: ADR 0003 HV1–HV4, roadmap Stage 3, conformance §4 "Histogram table v5", experiment-v1 §8 (A_SCOPE_INCOMPLETE) and ADR 0001 SI1.Commits
aacebecplane_histograms.csv schema version 5: channels by canonical channel ID (ADR 0003). This is the draft, rebased.e4df1c2Tests on described trees: the owner's extra-stain case and Spec fixtures v04, v05, v06, v13, v17, v22. Rebased.faea256Docs: contract §1.5, §6, §7–§9; plan N61–N64; CHANGELOG. Rebased.5df10aaRebase fixes: the tests from Fixes #28, fixes #33, fixes #41 #91 and Fixes #35, fixes #39, fixes #23 #92 use channel IDs.6c4243aQ3 / N66: refuse partial presence inside a fitted group unless the subset is chosen.e0b7c1dQ4: the parameter columns get their own version.91f4e75Q5: a failed measurement removes an earlier table.5a22848Docs for the resolutions;tools/evidence_v4_parity.pyis deleted.1a0109dN65: schema v5 tail columns hold centred moments (count, mean, std), computed exactly.Commit 9: centred tail moments (owner decision N65)
What is stored. Each tail of a robust histogram is written as
Histogram_{Underflow,Overflow}Count_<ID>,…Mean_<ID>and…Std_<ID>. These replace the raw float64…Sum_<ID>and…SumOfSquares_<ID>. In code,HistogramTail(count, mean, std)replacesHistogramTail(count, sum, sum_of_squares).Why this minimal set. The mean and the population standard deviation are each the float64 nearest their exact value.
count·std². That is exact up to one rounding, with the same relative accuracy as a stored M2.How it is computed. The new pure module
tail_moments.pytakes every tail pixel at its exact value:It keeps the sum and the sum of squares as exact rationals across blocks. The mean is rounded once. The std is taken by integer square root with a sticky bit, then rounded once. As a result:
Pooling. Each tail adds
count·std²pluscount·(mean − pooled mean)². Both terms are centred, and the existing power-of-two rescaling handles overflow (#33). When the tail products in a pooled mean overflow, that mean is rescaled the same way.What this replaces from #91. #91 had named refusals for an infinite tail sum of squares and for opposite infinite tail sums. Those tails now give their finite mean and std, and
zscoreworks on them.Validation.
integrity.check_tailchecks that the mean is finite and inside the tail's range, and that the std is at most half the range.Tests (
tests/test_tail_moments.py), all against an exactFractionoracle:Tests of the old sums in
test_robust_range,test_integrity,test_poolingandtest_csv_iowere rewritten for the moments.assert_same_histogramnow requires blocks to equal whole planes bit for bit.Cost. The exact accumulation runs at Python speed over the tail pixels only. On a 2048² float32 plane, robust (0.01, 99.99) took 0.194 s against 0.189 s before. With wide quantiles (5, 95), which put 400k pixels in the tails, it took 0.214 s against 0.167 s. This is one local run, not a benchmark.
Contract and docs. Contract §1.2.2, §1.2.3 table, §1.4, §6.2 and §9.1 are updated. Plan §12 records N65 and N66. CHANGELOG and AGENTS are updated.
tools/dump_outputs.pydumps (count, mean, std), or the old fields on older commits.Rebase conflicts and how they were resolved
Both sides' science is kept.
rgb.py: Fixes #35, fixes #39, fixes #23 #92'sRGB_DTYPESand this PR'sRGB_CHANNELS/check_rgb_channelsare both kept.pooling.py: merged automatically. Fixes #28, fixes #33, fixes #41 #91's std rescaling stays; its tail refusals are superseded by N65 as described above.from_columns: it moved toparameters.py. It still compares against Fixes #35, fixes #39, fixes #23 #92'spercentile_name.histogram.pygoing from 861 to 835 lines.Resolutions (for the owner)
[channels]table), neverused_ids.channels, which also holds retired IDs. A declared but unused channel (v22) is blank everywhere. The calling brick passes the list. A test checks that a retired ID stays out of the header.channelschannelsis the last plane column, before every per-channel column.fitraisesChannelScopeIncompleteErrorwith the message "A_SCOPE_INCOMPLETE: in group 'R002', channel PHASE is present in k of n planes (absent from …)";.channelsmaps each such ID to the planes that lack it. The opt-in isNormalizationSettings(partial_channels="planes_with_channel").to_dictstores it when it is chosen, and each fit reports thechannel_partially_presentwarning. RGB resets the setting.Normalization_Offset_C0normalization_parameters_version=2.from_columnsrefuses a row without it or with another version ("fit again").ChannelMeasurementError.removednames it. Other errors leave an existing file untouched.B,G,R), one rule for every per-channel table. Source orderR,G,Bapplies to a plane'schannelscell, pixel arrays andapply.pins.mdrowEvidence (final head 1a0109d)
Numbers without tails do not change.
float.hex. The output is byte-identical onmain0b35530, on the pre-N65 head 5a22848 and on 1a0109d:sha256 d89c140f…c24c99. v0.4.0 gave the same digest earlier.tools/dump_outputs.py --corpus(4764 lines):mainand 5a22848:8bb7c3c1…72256c.0111f8bd…6009e.tools/dump_outputs.py(1788 lines):main:1723cafb….0b6f6feb…. Againstmain, that is 600 lines: 60 table digests, 464 lines differing only by the addednormalization_parameters_version, 60 lines whereIssue.channelis'C0', and 16 messages.032b395b…. Against 5a22848, 192 lines differ:max_representative_errorof the int64 and uint64 tight robust planes (whole and blocks now agree bit for bit; before, they differed);Gate and CI.
python tools/check.py: exit 0, 1754 passed, 0 skipped (CSBDeep 0.8.2 installed, so the two CSBDeep tests ran). Without CSBDeep, those two skip, as onmain.What the consumers need (S3.3 and later; not done here)
preprocessing
PlaneRecord(..., channels=)from the unit's confirmed list.write_plane_histograms(records, path, channel_ids=<declared [channels] IDs>, failures=…), giving every shard the same IDs.ChannelMeasurementError, report.failuresand.removed.RunInputs, read withread_plane_histogram_table.range_check_issues.ChannelScopeIncompleteErrorasA_SCOPE_INCOMPLETE, and offerpartial_channelsonly as an explicit user choice.CellPatcher
record.channel_histogram(id),fit(..., channels=), andapply/selectby ID.Normalization_Offset_<ID>/Normalization_Scale_<ID>plusnormalization_parameters_version. Old tables are refused.ChannelScopeIncompleteError.MorphoFeature (
histogram_fit)record.channels.ChannelScopeIncompleteErrorunless the settings opt in.All bricks
Issue.channelis now an ID.summary_columns(channel_ids).channel_partially_presentappears only with the opt-in.ChannelScopeIncompleteError,NormalizationSettings.partial_channels, thenormalization_parameters_versioncolumn (COLUMNShas 5 entries),ChannelMeasurementError.removed,records.check_universe.HistogramTail(count, mean, std);.sumand.sum_of_squaresare gone.tail_moments.Open questions
mainafter the merge (for example Release v0.4.0 #89 for v0.4.0).partial_channelsis one choice per run. A per-channel or per-group choice would be a small follow-up if the notebook needs it.pins.mdneeds a normalization row at cc9820f.infornan, which the methods refuse by name, unchanged frommain. The rescaling that this PR adds for tails could be extended to it as a separate fix; it is not done here to keep untailed numbers and behaviour unchanged.🤖 Generated with Claude Code
https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc