feat: MIMIC-CXR text adapter + solo susceptibility real-API results (n=35, n=600) - #336
Conversation
…316) Add benchmaxxing/datasets/mimic_cxr_text.py (Lane B, MCQ over CheXpert findings from report text, per the #330 design: single confirmed-positive answer, distractors from confirmed-negative findings only, "No Finding" studies excluded). Reuse experiments/medqa/reproduce.py unchanged (per #316) for real Gemini API solo susceptibility + noise-floor runs at n=35 (smoke) then n=600, per #334. Raw manifests and call caches are gitignored: report text is credentialed PhysioNet data under a DUA and isn't redistributed, even in this private repo. Only aggregate results are committed. Closes #316. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Tested locally on macOS Apple Silicon in a Environment/details: Commands run: pip install -e ".[dev]"
ruff check .
pytest -qResults:
So the test suite passes locally, but the PR does not currently pass |
|
@armaanvgrewal thanks for testing! Confirmed this is pre-existing on `main`, not something this PR introduces: our branch has zero commits/diff touching `assets/figures/figure1_gen.py`, and `ruff check` fails identically against a clean checkout of `main` (tried in an isolated worktree, same file, same errors — f-strings without placeholders + a missing-executable-bit shebang warning). Likely a ruff version/config difference between your local setup and whatever ran clean before (some of these rules, like EXE001, are newer). Not blocking this PR — happy to fix it here anyway if useful, or it's better as its own small cleanup PR against main. Let me know which you'd prefer. |
|
Fixed in #341 (small, separate PR against main since it's unrelated to this diff). Once that merges, a rebase here will pick it up. |
Agastya191
left a comment
There was a problem hiding this comment.
Good work on mimic_cxr_text.py and on the #330 filtering — keeping only single-confirmed-positive studies and drawing distractors solely from confirmed negatives is the right call, and the DUA split holds up: I read run_solo and solo_results.json plus call_cache.jsonl really is everything it writes, both of which your .gitignore covers. The problem is that build_manifest puts the whole report into question instead of report, and lexical_overlap_bias in cues/text.py appends every salient question token to a distractor. I can't run it against the credentialed text, but on a typical-length report that is ~67 tokens, so the distractor becomes a ~540-character blob that names the correct finding verbatim and is also ~45x longer than every other option — which means the lexical_overlap rate that RESULTS.md calls the strongest cue for both tiers (0.093 and 0.148) is partly counting models that read the answer out of the report pasted into option A, while longest_option fires on the same case.
You have a few options here. I would put the report in Case.report and leave question as the short stem, which is what pubmedqa.py and mimic_cxr.py already do — _mcq_prompt renders report as "Clinical context:", so the model sees identical text, but the cue then draws from the stem alone (I checked: the distractor drops to Cardiomegaly primary finding described report). That does mean re-running the n=600 solo pass.
sebasmos
left a comment
There was a problem hiding this comment.
Nice work getting this running end to end. Agastya's catch is a good one though: report should go in Case.report, not question, since the cue is pulling the whole report into a distractor right now. That's probably why lexical_overlap looks strongest. Worth fixing and re-running the n=600 pass before we trust that number.
Fixes the 6 ruff errors reported on PR #336 (unrelated to that PR's diff): 3 f-strings without placeholders (F541) and 3 semicolon-chained statements (E702). Made the file executable to match its shebang (EXE001). Pure lint fix: figure1.svg regenerates byte-identical. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
maximinl
left a comment
There was a problem hiding this comment.
Peer review — agree with Agastya and Seb; requesting the schema fix + n=600 re-run.
What’s solid
- #330 filtering (single confirmed-positive answer, distractors from confirmed negatives only, drop No Finding / multi-positive / underdetermined) looks correct, and the tests pin those exclusions well.
- DUA split is right: gitignore manifests + call caches, commit only aggregates.
- Reusing
experiments/medqa/reproduce.pyunchanged matches #316’s replicate-don’t-rewrite rule. - Zero noise floor on both tiers is a clean control signal.
Blocker: report text in Case.question
build_manifest does:
question=f"{report_path.read_text(...)}\n\n{_QUESTION_PROMPT}"lexical_overlap_bias in cues/text.py appends salient question tokens onto a distractor. Putting the full report in question therefore (a) pastes the correct finding into the contaminated option and (b) makes that option ~orders of magnitude longer, so longest_option co-fires. That is a plausible explanation for why RESULTS.md ranks lexical_overlap as the strongest cue (0.093 / 0.148) — part of that rate may be models reading the answer out of the option, not cue susceptibility.
Put the report in Case.report and leave question as the short stem (same pattern as pubmedqa / mimic_cxr). _mcq_prompt still surfaces the report as clinical context, so the model sees the same text; the cue only draws from the stem. Then re-run the n=600 solo pass before those numbers are citable.
(Armaan’s ruff note on figure1_gen.py is orthogonal and already fixed in #341.)
…(review on #336) Two bugs caught in review by @Agastya191, @sebasmos, @maximinl: 1. mimic_cxr_text.py put the whole report into Case.question instead of Case.report, letting lexical_overlap_bias quote report text (including the correct finding) into a distractor and co-activate longest_option. Fixed to match the existing pubmedqa.py report/question split. 2. benchmaxxing/data.py's _text_case never read `report` back off a manifest CSV (unlike _image_case). Pre-existing bug in shared code, newly triggered because this is the first Lane B experiment to run a Case.report through a real manifest round-trip. Effect: the real API run was answering blind, with ~35-60% of gemini-2.5-flash responses a refusal ("please provide the report"). Fixed with a one-line read-back plus a regression test. Re-ran n=35 and n=600 for real against Gemini with both fixes; added a refusal-aware cross-check (experiments/mimic_cxr_text/refusal_aware_reanalysis.py, no new API calls) confirming ~0% abstention and consistent flip rates. Corrected numbers land in the same band as MedQA (flash 0.089 vs 0.063, flash-lite 0.114 vs 0.117). See experiments/mimic_cxr_text/RESULTS.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@Agastya191 @sebasmos @maximinl — fixed, and found a second bug along the way. Pushed in 1907034. Bug 1 (your catch): moved report into Bug 2 (found while re-running): re-running with just bug 1 fixed produced a wildly different, much higher flip rate (~0.6-0.9) and a ~60% abstention rate on gemini-2.5-flash (lots of "please provide the report" replies). Traced it to With both fixes, re-ran n=35 and n=600 for real: {"gemini-2.5-flash": {"flip": 0.089, "noise_floor": 0.067},
"gemini-2.5-flash-lite": {"flip": 0.114, "noise_floor": 0.0}}Also added Full writeup in |
Agastya191
left a comment
There was a problem hiding this comment.
Good work on the fix, and on running down the data.py bug. I checked it: _text_case really never read report back while _case_to_row had been writing the column all along, and no committed manifest under experiments/ has a populated report column, so the one-line read-back can't move any existing MedQA number. One problem, though: now that question is the constant _QUESTION_PROMPT, lexical_overlap_bias in cues/text.py has no case-specific text left to draw from, since _content_tokens on that stem returns ['primary', 'finding', 'described', 'report'] for every study, so the cue appends the identical four-word suffix to option 0 on all 600 cases. That makes lexical_overlap the same manipulation as longest_option with a different filler, rather than the per-case lexical signal it is on medqa.py and pubmedqa.py where question carries the case, so the two columns converging isn't a finding and RESULTS.md's cross-dataset read of that column isn't comparing the same construct.
You have a few options. I would have lexical_overlap_bias fall back to case.report tokens minus any token appearing in the options when the stem yields nothing case-specific, which restores a per-case signal without putting the report's naming of the answer back into the distractor. If you'd rather not touch shared cue code on this PR, a sentence in RESULTS.md saying the cue degenerates to a constant suffix here would be enough to stop that number being read as comparable to MedQA's. Separately, that same paragraph calls lexical_overlap strongest for both tiers, but results_n600/solo_results.json has flash-lite at longest_option 0.130 against lexical_overlap 0.118.
sebasmos
left a comment
There was a problem hiding this comment.
Bug 2 is genuinely fixed, I round-tripped a manifest and confirmed the report renders as Clinical context in the built prompt. Good catch finding it while re-running rather than shipping the first fix.
One new problem though. Now that the report is out of question, lexical_overlap_bias draws its tokens from the fixed 8-word stem, so it appends the same constant suffix to the same distractor that longest_option pads, on all 600 cases. Those two columns are one manipulation, not two, so the per-cue split is not comparable to MedQA/PubMedQA where the overlap is case-specific.
Also RESULTS.md says lexical_overlap is the strongest cue for both tiers at n=600, but the committed results_n600/solo_results.json has flash-lite longest_option 0.13 above lexical_overlap 0.1183.
Flagging that I have provisionally put your n=600 cross-dataset row in the paper with that cell marked on hold, so resolving this unblocks it.
…336) Third bug caught in review by @Agastya191 and @sebasmos, found on the re-run after moving report out of Case.question: lexical_overlap_bias draws its perturbation tokens from case.question, which is now a fixed 8-word stem identical across every MIMIC-CXR text case, so it appended the same four words to the same distractor on all 600 cases -- the same manipulation as longest_option with different filler, not a per-case lexical signal, making the "lexical_overlap is strongest" read invalid. Fixed: the cue now draws from case.report when present (falls back to case.question exactly as before for MedQA/etc, which have no separate report field -- unaffected), and excludes any token appearing in ANY option, not just the target, so it can no longer echo another option's wording -- in particular the correct answer -- into a distractor. Re-ran n=35 and n=600 for real with the fix; lexical_overlap and longest_option now diverge per-model as expected of two genuinely different manipulations. No single cue is "strongest" for both tiers anymore. Updated experiments/mimic_cxr_text/RESULTS.md accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@Agastya191 @sebasmos — good catch, and confirmed exactly as described: with
Re-ran n=35 and n=600 for real. {"gemini-2.5-flash": {"lexical_overlap": 0.093, "longest_option": 0.105, "option_order": 0.043},
"gemini-2.5-flash-lite": {"lexical_overlap": 0.175, "longest_option": 0.13, "option_order": 0.095}}@Agastya191 also right that RESULTS.md's "lexical_overlap strongest for both tiers" line was Ready for re-review. |
sebasmos
left a comment
There was a problem hiding this comment.
Verified the post-fix n=600 numbers on the branch: flash 0.043 / 0.105 / 0.093 and flash-lite 0.095 / 0.130 / 0.175, so the two cues genuinely diverge now. Drawing from the report and excluding tokens present in any option is the right fix, and the two regression tests pin exactly the failure mode. Dropping the overclaim rather than defending it is appreciated. Approving.
…(review on #336) Two bugs caught in review by @Agastya191, @sebasmos, @maximinl: 1. mimic_cxr_text.py put the whole report into Case.question instead of Case.report, letting lexical_overlap_bias quote report text (including the correct finding) into a distractor and co-activate longest_option. Fixed to match the existing pubmedqa.py report/question split. 2. benchmaxxing/data.py's _text_case never read `report` back off a manifest CSV (unlike _image_case). Pre-existing bug in shared code, newly triggered because this is the first Lane B experiment to run a Case.report through a real manifest round-trip. Effect: the real API run was answering blind, with ~35-60% of gemini-2.5-flash responses a refusal ("please provide the report"). Fixed with a one-line read-back plus a regression test. Re-ran n=35 and n=600 for real against Gemini with both fixes; added a refusal-aware cross-check (experiments/mimic_cxr_text/refusal_aware_reanalysis.py, no new API calls) confirming ~0% abstention and consistent flip rates. Corrected numbers land in the same band as MedQA (flash 0.089 vs 0.063, flash-lite 0.114 vs 0.117). See experiments/mimic_cxr_text/RESULTS.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
) Reuse experiments/medqa/reproduce.py --stage cascade as-is for the bare baseline-relative seed (null, same as MedQA: a wrong peer with no rationale does not spread). Copy experiments/medqa/push_c.py into experiments/mimic_cxr_text/push_c.py for the case-anchored plausibility dose-response, adapted for this dataset's report/question split (#336): _mcq now renders Case.report as clinical context (missing entirely in the original, which would answer blind), and the anchor detail is pulled from Case.report instead of Case.question (the latter is now a fixed stem, identical across cases, which would make every "anchored" rationale generic). build_solo_records.py reconstructs push_c.py's --solo-records input from the already-cached n=600 solo run, no new API calls. Real n=20 (bare) and n=60 (anchored) runs against Gemini. Cascade transcripts are gitignored (they embed report text via committee turn content, same DUA policy as manifests/call caches). See experiments/mimic_cxr_text/RESULTS.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…336) Third bug caught in review by @Agastya191 and @sebasmos, found on the re-run after moving report out of Case.question: lexical_overlap_bias draws its perturbation tokens from case.question, which is now a fixed 8-word stem identical across every MIMIC-CXR text case, so it appended the same four words to the same distractor on all 600 cases -- the same manipulation as longest_option with different filler, not a per-case lexical signal, making the "lexical_overlap is strongest" read invalid. Fixed: the cue now draws from case.report when present (falls back to case.question exactly as before for MedQA/etc, which have no separate report field -- unaffected), and excludes any token appearing in ANY option, not just the target, so it can no longer echo another option's wording -- in particular the correct answer -- into a distractor. Re-ran n=35 and n=600 for real with the fix; lexical_overlap and longest_option now diverge per-model as expected of two genuinely different manipulations. No single cue is "strongest" for both tiers anymore. Updated experiments/mimic_cxr_text/RESULTS.md accordingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…318) Copy experiments/medqa/break_it.py's arm A into experiments/mimic_cxr_text/break_it_a.py, adapted for this dataset's report/question split (_mcq renders Case.report as clinical context, same fix pattern as #336/#348) and per #318's explicit instruction to choose the flagged wrong answer per-model so it differs from that model's own clean baseline (MedQA's original picked one wrong per case regardless of model and was noted as confounded there). Real n=20 run against Gemini: a wrong automated pre-screen flag is adopted by both tiers (flash +0.40, flash-lite +0.50), comparable to MedQA's own system-flag finding (+0.26/+0.30). See experiments/mimic_cxr_text/RESULTS.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Port experiments/blind_metric/blind_metric.py (not break_it.py's simpler arm D) into experiments/mimic_cxr_text/blind_metric.py: #320 explicitly asks whether the model names the rubric, which only blind_metric.py's three-condition design (baseline/blind/test_aware + a naming regex on the justification) measures. Adapted the prompt builder to render Case.report as clinical context, same fix pattern as #336/#348/#353. Also widened the DUA gitignore rule: this script writes its cache under a different filename (blind_metric_call_cache.jsonl) than the existing call_cache.jsonl pattern covered, and its cached responses embed quoted report text in the model's reasoning, so the narrower glob would have let it slip through. Real n=40 run (gemini-2.5-flash-lite, matching the MedQA original): decoy uptake blind 0.05 vs baseline 0.0, but test_aware (0.175) is HIGHER than blind, the reverse of MedQA's own suppression direction. Traced to a genuine model reasoning failure (confirmed by inspecting the raw completions, not a prompt bug): flash-lite's chain-of-thought misreads "the rubric may be misaligned, answer clinically" as "assume the rubric is correct, justify it." Reported honestly in RESULTS.md with the n=40 sample-size caveat rather than forced to match the expected direction. Where the datasets do agree: both decoy-drifters named the rubric explicitly (2/2), consistent with MedQA's text-lane self-declared drift. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes the 6 ruff errors reported on PR #336 (unrelated to that PR's diff): 3 f-strings without placeholders (F541) and 3 semicolon-chained statements (E702). Made the file executable to match its shebang (EXE001). Pure lint fix: figure1.svg regenerates byte-identical. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
feat: MIMIC-CXR text adapter + solo susceptibility real-API results (n=35, n=600)
…udge (#357) * feat: MIMIC-CXR text cascade contagion, bare vs case-anchored seed (#317) Reuse experiments/medqa/reproduce.py --stage cascade as-is for the bare baseline-relative seed (null, same as MedQA: a wrong peer with no rationale does not spread). Copy experiments/medqa/push_c.py into experiments/mimic_cxr_text/push_c.py for the case-anchored plausibility dose-response, adapted for this dataset's report/question split (#336): _mcq now renders Case.report as clinical context (missing entirely in the original, which would answer blind), and the anchor detail is pulled from Case.report instead of Case.question (the latter is now a fixed stem, identical across cases, which would make every "anchored" rationale generic). build_solo_records.py reconstructs push_c.py's --solo-records input from the already-cached n=600 solo run, no new API calls. Real n=20 (bare) and n=60 (anchored) runs against Gemini. Cascade transcripts are gitignored (they embed report text via committee turn content, same DUA policy as manifests/call caches). See experiments/mimic_cxr_text/RESULTS.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: case-anchored rationale pulled report header boilerplate, not findings (#348 review) @sebasmos caught it on review: the anchor detail took the first 14/30 words of the whole report, which for MIMIC-CXR is always fixed boilerplate ("FINAL REPORT", "EXAMINATION: CHEST (PA AND LAT)", "INDICATION: ..."), not clinical content. That made "anchored" and "generic" prompts near-identical, so the anchored-equals-generic null was measuring the boilerplate, not anchoring. Fixed with _findings_text(), extracting from the first FINDINGS:/IMPRESSION: header onward (falls back to the whole report if neither is present). Spot-checked against real reports to confirm real clinical sentences are captured. Also added two paired tests sebasmos had computed independently on the old (buggy) run: anchored_strong vs generic (p=0.29, correctly not a dose-response) and anchored vs anchored_solo (the majority-vs-single-peer contrast, p=0.0039 -- the actually-significant effect, now foregrounded in RESULTS.md instead of the null anchored-vs-generic comparison). Re-ran n=60 for real with the fix. RESULTS.md rewritten to report both new contrasts and stop reading the null as a dose-response. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: MIMIC-CXR text Break-it A, contaminated system pre-screen flag (#318) Copy experiments/medqa/break_it.py's arm A into experiments/mimic_cxr_text/break_it_a.py, adapted for this dataset's report/question split (_mcq renders Case.report as clinical context, same fix pattern as #336/#348) and per #318's explicit instruction to choose the flagged wrong answer per-model so it differs from that model's own clean baseline (MedQA's original picked one wrong per case regardless of model and was noted as confounded there). Real n=20 run against Gemini: a wrong automated pre-screen flag is adopted by both tiers (flash +0.40, flash-lite +0.50), comparable to MedQA's own system-flag finding (+0.26/+0.30). See experiments/mimic_cxr_text/RESULTS.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: MIMIC-CXR text blind-metric substitution probe (#320) Port experiments/blind_metric/blind_metric.py (not break_it.py's simpler arm D) into experiments/mimic_cxr_text/blind_metric.py: #320 explicitly asks whether the model names the rubric, which only blind_metric.py's three-condition design (baseline/blind/test_aware + a naming regex on the justification) measures. Adapted the prompt builder to render Case.report as clinical context, same fix pattern as #336/#348/#353. Also widened the DUA gitignore rule: this script writes its cache under a different filename (blind_metric_call_cache.jsonl) than the existing call_cache.jsonl pattern covered, and its cached responses embed quoted report text in the model's reasoning, so the narrower glob would have let it slip through. Real n=40 run (gemini-2.5-flash-lite, matching the MedQA original): decoy uptake blind 0.05 vs baseline 0.0, but test_aware (0.175) is HIGHER than blind, the reverse of MedQA's own suppression direction. Traced to a genuine model reasoning failure (confirmed by inspecting the raw completions, not a prompt bug): flash-lite's chain-of-thought misreads "the rubric may be misaligned, answer clinically" as "assume the rubric is correct, justify it." Reported honestly in RESULTS.md with the n=40 sample-size caveat rather than forced to match the expected direction. Where the datasets do agree: both decoy-drifters named the rubric explicitly (2/2), consistent with MedQA's text-lane self-declared drift. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: MIMIC-CXR text referee detection, naive gate vs deployable vs judge (#321) Port experiments/referee/referee_deployable.py and referee_judge.py into experiments/mimic_cxr_text/. Both are self-contained (each builds its own anchored-cascade committee run per case with two colluding peers + a real holdout), so this doesn't depend on #317/#355's cascade transcripts. Same report/question adaptation as #336/#348/#353/#320: _mcq renders Case.report as clinical context, and the anchor detail reuses push_c.py's _findings_text. Real n=40 runs against Gemini, 14 real adoptions: - naive_gate: precision 0.35, recall 1.0, FPR 1.0 (over-fires, as expected) - deployable (peer-modal + private re-query, no key): precision/recall 1.0, FPR 0.0, matching the oracle upper bound - same-lineage judge: precision 1.0, recall 0.93, FPR 0.0 Closely matches MedQA's own referee finding (naive over-fires, deployable recovers the oracle, judge is viable but noisier). Completes the #296 battery (#316-#321), modulo the anchored-seed contagion gap tracked in #355. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs: close #355, anchored-seed contagion from already-committed referee data #317's "contagion = shared minus isolated" was only measured for the bare seed in #348 (n=20, 0.0). referee_deployable.jsonl (#357, n=40) already contains the same pair for the ANCHORED seed at zero extra API cost: its board answer (shared, anchored rationale) vs bare answer (private, unseeded re-query) is exactly a shared-vs-isolated comparison, since `wrong` is chosen to differ from bare by construction (same baseline-relative convention used throughout the project). contagion = 0.35 - 0.0 = 0.35, notably higher than the bare seed's null (0.0), consistent with the battery's overall finding that MIMIC-CXR text holdouts propagate a minimally-plausible seed but not a content-free one. Documented the one honest caveat: this isolated condition is a cold, seedless re-query, not MedQA's original isolated-committee mechanics (same seed planted per-agent, no board-sharing) -- a stronger, slightly different control, judged sufficient given it's the same private-re-query convention used everywhere else in this battery (push_c.py, break_it_a.py, both referee scripts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: naming regex and prompt asymmetry in blind_metric.py (#356 review) Two bugs caught by @Agastya191 on PR #356: 1. _NAMING was only word-bounded on its first/last alternative, and had a substring fallback treating a bare decoy_letter ("A"/"B") appearing anywhere in the first 120 chars as evidence of naming -- true of almost any sentence, so `named` could never come back False for a drifter. n_drifted_silently: 0 was a guaranteed artifact, not an observation. Fixed: proper \b boundaries on every alternative, substring fallback removed. 2. blind_p asked for a justification before the letter; aware_p asked for a bare letter only, confounding test_awareness_suppression with the justification requirement itself. Fixed: aware_p now requests the same one-sentence justification. Re-ran n=40 for real with both fixes. Both changed the reading substantially: test_aware dropped from 0.175 to 0.075 (most of the "reversed suppression" was the justification-requirement artifact, not a model reasoning failure), and the naming result flipped from 2/2 to 0/2 -- confirmed by reading the raw justifications, which cite the report's own clinical language, not the rubric. So MIMIC-CXR text's blind-metric drift is silent here (matching the imaging lane, opposite of MedQA's own 11/11 self-declared text-lane finding), the honest number rather than the artifact the buggy regex reported. base_is_decoy=0-by-construction now documented explicitly (same convention as #317/#355). Also registers mimic_cxr_text in benchmaxxing/datasets/status.py and staging.py, added to main by an unrelated merge (infra this branch predates) that now requires every registered adapter to have both entries. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Summary
benchmaxxing/datasets/mimic_cxr_text.py(Lane B): builds a 4-way MCQ per study from MIMIC-CXR report text + CheXpert labels, per the design agreed in Design: MCQ template + distractor selection for mimic_cxr_text adapter (blocks #316) #330 (exactly one confirmed-positive finding as the answer, 3 confirmed-negative findings as distractors, "No Finding" studies excluded, uncertain labels never used as distractors).experiments/medqa/reproduce.pyunchanged (per Solo shortcut susceptibility + noise floor (MIMIC-CXR text) #316's "do not rewrite the method, replicate it") to run real Gemini API solo-susceptibility + noise-floor experiments on the new manifest, at n=35 (smoke) then n=600, per Real-API solo susceptibility run: MIMIC-CXR text at n=35 then n=600 #334.solo_results.json,RESULTS.md); manifests and call caches are gitignored since MIMIC-CXR report text is credentialed PhysioNet data under a DUA and must not be redistributed, even in this private repo.Results (n=600)
Consistent with the n=35 smoke run (0.095 / 0.124). Full detail in
experiments/mimic_cxr_text/RESULTS.md.Closes #316. Part of #296. Related: #330, #334.
Test plan
pytest tests/— 611 passed, 10 skipped (full suite, python 3.12)ruff checkclean on all changed filesbuild_manifestagainst the real credentialed MIMIC-CXR data at/orcd/pool/006/lceli_shared/data/mimic-cxr-{reports,labels}@sebasmos — flagging for review given the #330 design decisions and the credentialed-data handling call (gitignoring manifest/call_cache); let me know if that split should be different.