Skip to content

Z-slice segment-counting line (negative result) - #5

Open
trisha-ant wants to merge 7 commits into
trisha/gently-remove-confidencefrom
trisha/gently-zslice-multi
Open

Z-slice segment-counting line (negative result)#5
trisha-ant wants to merge 7 commits into
trisha/gently-remove-confidencefrom
trisha/gently-zslice-multi

Conversation

@trisha-ant

@trisha-ant trisha-ant commented Apr 22, 2026

Copy link
Copy Markdown
Owner

Stacked on #1. This PR contains the full z-slice segment-counting line of inquiry (harness + three variants, previously split across #2#4, now consolidated here). All results in this PR are on claude-opus-4-6; the Opus 4.7 re-test of zslice_multi is in #6. Retrospective on the full arc below.


Retrospective: z-slice segment-counting (PRs #3#5)

Hypothesis

Max-intensity projections collapse depth, fusing overlapping body segments into one bright band and destroying the discriminative feature for fold stages — how many times the body folds back on itself. A biologist resolves this by scrolling z-slices and counting body-segment cross-sections at a single plane: ≤1 → 1.5fold, 2 → 2fold, ≥3 → pretzel. The hypothesis was that a VLM subagent, given a non-projected z-slice, could perform this count and use it to override the projection-based classifier on fold stages.

What was tried

Three variants, each refining the previous after a negative result:

PR Variant Approach
#3 zslice Single midplane slice (z=Z/2). Symmetric override: count → stage, replaces primary on all fold stages.
#4 zslice_asym Same single midplane. Upgrade-only override (1.5fold→2fold→pretzel, never backward) to cap the pretzel regression from #3.
#5 zslice_multi Five z-planes at z={0.25, 0.40, 0.50, 0.60, 0.75}. Count each concurrently, take the max. Symmetric override to test the hypothesis cleanly.

All three run perceive_scientific first and only invoke the subagent when the primary lands on a fold stage, so the subagent never moves predictions into or out of the fold region.

Results

n=233 hard-stage frames (1.5fold + 2fold + pretzel), 4 embryos. Baseline is scientific re-run on the same subset.

Stage scientific zslice zslice_asym zslice_multi
1.5fold (41) 65.9% 51.2% 19.5% 2.4%
2fold (59) 61.0% 59.3% 79.7% 44.1%
pretzel (133) 88.0% 6.8% 85.0% 54.1%
Exact 77.3% 27.9% 72.1% 42.5%
Adjacent 99.6% 89.3% 100% 95.3%

Override attribution (isolating the subagent's contribution from primary-classifier variance):

Variant Override fired Helpful Harmful Net
zslice_asym 7 / 88 triggers 0 7 −3.0%
zslice_multi 111 / 227 triggers 26 85 −25.3%

Why it did not work

The subagent cannot count. The core assumption — that a VLM can reliably distinguish 2 vs 3 fluorescent-nuclei blobs in an optical section — is false at this image resolution and contrast.

  1. Single midplane returns ≤2 almost always. Across 88 zslice_asym triggers, the count distribution was {1:40, 2:46, 3:2} — 98% ≤2 regardless of true stage. A pretzel embryo's folds sit at different z-depths, so one plane rarely intersects 3+ segments at separable locations; what does intersect tends to merge into one blob. The symmetric zslice variant therefore systematically downgraded correct pretzel calls to 2fold/1.5fold (88%→7%).

  2. Asymmetric gating works as damage control but reveals zero signal. zslice_asym restored pretzel to 85% by refusing to downgrade, but the override only fired 7 times in 88 triggers and was wrong all 7. The large per-stage swings vs scientific (1.5fold −46pp, 2fold +19pp) are run-to-run variance in the primary classifier, not the override.

  3. Multi-plane max amplifies noise instead of recovering signal. Taking the max over 5 noisy estimators with a ~20% per-slice false-3 rate yields a ~67% any-slice-says-3 rate. The decisive measurement: max≥3 fires at 56% on GT=2fold vs 55% on GT=pretzel — statistically identical. No depth reliably hits 3 (per-depth mean count on pretzel: 1.83–2.19 across the five planes).

Root cause: in fluorescent-nuclei light-sheet data, body segments at fold stages are dense clusters of bright dots without clear membrane boundaries. The VLM perceives a single z-slice as 1–2 diffuse bright regions; it cannot resolve the gap structure that a human counts after contrast adjustment and scrolling. The count task needs either a different imaging modality (membrane marker) or classical image processing (threshold + connected components) rather than a VLM.

(We later re-tested zslice_multi on claude-opus-4-7 to see if its improved vision rescues segment-counting — it does not; max≥3 still has no 2fold/pretzel separation. See #6 for that result alongside the rest of the 4.7 work.)

What's salvageable


What's in this PR

Harness (benchmark/testset.py, run.py):

  • _create_slice_image(volume, z, max_dim) — render one XY z-slice as base64 JPEG, same crop as the projection
  • TestCase gains midplane_b64: str (z=Z//2) and zslices_b64: list[str] (z = {0.25, 0.40, 0.50, 0.60, 0.75})
  • run.py: _accepted_optional_kwargs() introspects each variant's signature and passes optional z-slice kwargs only if declared — zero churn to existing variants

Variants (perception/):

  • zslice.py — scientific + midplane segment-count subagent, symmetric override
  • zslice_asym.py — upgrade-only override on 1.5fold/2fold
  • zslice_multi.py — max segment-count across 5 z-planes, symmetric override

Docs: program.md × 2 — document optional z-slice kwargs.

Verification

  • 18 variants registered; _accepted_optional_kwargs returns {'midplane_b64'} for zslice/zslice_asym, {'zslices_b64'} for zslice_multi, set() for all others
  • All three variants run end-to-end on 233 hard-stage frames; result JSONs + chart committed
  • Count distributions and override attribution extracted from reasoning field in saved predictions

…l kwarg

Max-intensity projections collapse depth, so overlapping body segments
fuse into one band — the key discriminative feature for fold stages is
lost. This is the harness half of the z-slice subagent work: expose a
single non-projected XY slice (z=Z//2) so a perception variant can
count discrete body-segment profiles at one plane.

- testset.py: new _create_slice_image() helper (same crop as the
  three-view projection); TestCase gains midplane_b64; iter_embryo
  populates it. Also collapses the duplicated load-volumes-or-not
  branch into one path.
- run.py: introspect perceive_fn signature once and only pass
  midplane_b64 when the function declares it (or **kwargs), so all
  existing variants are called unchanged.
- program.md: document the new optional kwarg.
…old stages

Max-intensity projections fuse overlapping body segments, which is the
main reason 1.5fold/2fold/pretzel are hard to tell apart. This variant
runs the scientific classifier, and when that lands on a fold stage it
sends the midplane z-slice to a subagent that counts discrete
body-segment cross-sections (≤1 → 1.5fold, 2 → 2fold, ≥3 → pretzel)
and overrides.

Trigger is deliberately the simplest baseline (primary ∈ fold stages)
so the subagent only ever moves between fold stages, never into or out
of them — count=1 is ambiguous between comma and 1.5fold, so triggering
on comma would systematically push comma frames forward.

Override is recorded in reasoning ("(overrode 2fold)") and in
verification_triggered/phase_count for auditability.
…stages

Midplane slice rarely shows >=3 separable cross-sections on pretzel embryos
(body folds sit at different z-depths), so the segment-counting subagent
systematically downgrades correct pretzel calls to 2fold/1.5fold.

Per-stage vs scientific (n=233):
  1.5fold: 51.2% (-14.7)
  2fold:   59.3% (-1.7)
  pretzel:  6.8% (-81.2)
  adjacent: 89.3% (-10.3)
Same segment-counting subagent as zslice, but only applies the override when
the count maps to a *later* stage than the primary (1.5fold->2fold->pretzel).
Trigger excludes pretzel since there is nothing to upgrade to, so the worst
case on pretzel is parity with scientific. Targets the 1.5fold/2fold gap
without the catastrophic pretzel downgrade observed in the symmetric variant.
…gent has no signal

Overall 72.1% vs scientific 77.3% (n=233). Asymmetric design successfully
caps the pretzel catastrophe (85% vs 7% for symmetric zslice), but
attribution shows the override itself only fired 7 times and was wrong every
time (5x 1.5fold->2fold on GT=1.5fold, 2x 2fold->pretzel on GT=2fold).

Segment-count distribution across 88 triggers: {1:40, 2:46, 3:2} -- the
subagent returns <=2 in 98% of cases regardless of true stage, because a
single midplane slice genuinely doesn't show >2 separable cross-sections.
The large per-stage swings (1.5fold -46, 2fold +19) are run-to-run variance
in the primary classifier, not the override.

Conclusion: midplane segment-counting carries no signal; multi-plane
sampling or a different subagent task is the remaining path.
Single-midplane segment-counting (zslice, zslice_asym) returns <=2 in 98% of
cases because body folds sit at different depths and one plane rarely
intersects 3+ at separable locations. This variant samples slices at z =
{0.25, 0.40, 0.50, 0.60, 0.75} of stack depth, runs the count subagent on
each concurrently, and overrides with the *maximum*. Symmetric override on
the full FOLD_STAGES trigger so the hypothesis is tested cleanly.

Harness: TestCase gains zslices_b64 (list of 5 base64 JPEGs via
_create_zslice_stack), and run.py whitelists it as an optional kwarg.
Existing variants unaffected (kwarg routing is per-signature).
…ration

Overall 42.5% vs scientific 77.3% (n=233). Max-count >=3 fires at 56% on
GT=2fold and 55% on GT=pretzel -- zero discriminative power between the two
stages we need to separate. The max over 5 planes amplifies per-slice noise
(~20% false-3 rate -> ~67% any-slice-says-3).

Override attribution: 111 fired, 26 helpful / 85 harmful (net -25.3%).
Per-depth mean count on pretzel: z=0.25:2.19, 0.40:1.99, 0.50:1.83,
0.60:1.87, 0.75:2.12 -- quarter-planes show marginally more structure than
midplane but no depth reliably hits 3.

Closes the segment-counting line: neither single-plane (zslice/zslice_asym)
nor multi-plane works because the subagent cannot reliably distinguish 2 vs
3 fluorescent-nuclei blobs in any optical section.
@trisha-ant
trisha-ant marked this pull request as ready for review April 23, 2026 15:53
@trisha-ant trisha-ant changed the title Add zslice_multi variant: max-count over 5 z-planes (negative — no 2fold/pretzel separation) Z-slice segment-counting line (negative) + Opus 4.7 bump Apr 28, 2026
@trisha-ant
trisha-ant changed the base branch from trisha/gently-zslice-asym to trisha/gently-remove-confidence April 28, 2026 13:35
@trisha-ant
trisha-ant force-pushed the trisha/gently-zslice-multi branch from 79a881e to 9bfdba6 Compare April 28, 2026 14:55
@trisha-ant trisha-ant changed the title Z-slice segment-counting line (negative) + Opus 4.7 bump Z-slice segment-counting line (negative result) Apr 28, 2026
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.

1 participant