Skip to content

Archaic Tier B: diagnose the failure as the wrong observable, and bank the instruments - #41

Merged
JamesKane merged 1 commit into
mainfrom
docs/archaic-observable-diagnosis
Jul 31, 2026
Merged

Archaic Tier B: diagnose the failure as the wrong observable, and bank the instruments#41
JamesKane merged 1 commit into
mainfrom
docs/archaic-observable-diagnosis

Conversation

@JamesKane

Copy link
Copy Markdown
Owner

Tier B was gated off in #40 for carrying no per-individual signal. This records why — the caller wasn't mis-tuned, it was built on an observable that leaves about one bit of evidence per tract, and no amount of fitting could have rescued it.

Documentation and dev-only examples. No shipped behaviour changes; Tier B stays gated.

The number that settles it

Detectability of one 36 kb tract (the measured median):

observable evidence per tract sensitivity @ ≤5% FP @ ≤1% FP
private-variant density (what we built) ~1 variant 14.3% 4.3%
archaic-allele matching at diagnostic sites ~30 sites 95.1% 80.9%

Reaching 80% sensitivity at 5% false positives takes 36 kb by allele matching, and is not reached by 500 kb with density. hmmix's p10 tract is 7 kb.

Both observables carry the same ~3× contrast (2.89× vs 3.04×) — so contrast was never the problem. Evidence per tract was.

The wrong turn

§3 chose Skov 2018 (hmmix), whose premise is detecting introgression without archaic reference genomes — it infers them from private-mutation density precisely because it assumes you don't have them. We do: all four archaic genomes, and archaic_classify already ships 2,031,406 diagnostic sites derived from them.

§3 dismissed the reference-based alternative in one clause — "IBDmix is reference-based and heavier" — and that clause is the root of every failure below. An introgressed tract is a haplotype inherited intact; the question is whether a stretch matches an archaic genome, not whether it's slightly more mutated. Runs, not counts.

What was ruled out first — each looked right at the time

  • The hard-coded 6.0× multiple is wrong (real enrichment 2.89×), but correcting it changes little
  • Per-individual Baum-Welch EM — the thing hmmix does and we don't — makes it worse: unconstrained it learns 22.4× and ~9 kb tracts, calling 7× the truth
  • Oracle parameters taken from the truth still sit at their random-placement null, precision pinned at 4–5% → the model class is wrong, not its parameters
  • The background is 14.6× overdispersed against the Poisson it's modelled with (5.3× p10–p90 spread) — noise larger than signal
  • A mutation-rate map wouldn't have sufficed: the best proxy explains 38% of that variance, leaving 7.4×
  • Quality filtering doesn't help — it lowers dispersion only by discarding variants proportionally
  • Not our variant calling: 1000G's own calls for the same person give the same contrast (1.98× vs 2.08×), though ours are 6× noisier
  • Not the truth set or my lift: in native hg38 with no lifting, hmmix's tracts are enriched 1.84× for their own archaic SNPs against a 1.04× null

What the reframe also fixes

  • The mutation-rate map becomes unnecessary — diagnostic sites are the denominator, so their uneven density cancels
  • Background dispersion stops applying — nothing is modelled as a density any more
  • The assets already exist — no new offline pipeline to start
  • Attribution may come back with it — per-site matching split by lineage class is Skov's post-hoc annotation, the thing attribute_lineage was gated for

Still open

  • Background carrying rate measures 13.0% here against the 4.3% recorded earlier in the design. Unreconciled, and it sets the contrast.
  • ~30 sites per tract assumes adequate coverage at diagnostic sites — needs checking against real call rates.
  • The reframe is n=1 so far. The cohort harness runs cheaply now that CRAM region queries decoded the whole chromosome; skip non-overlapping containers #39 landed.
  • Separately: our caller emits 2.12× the SNVs 1000G does on the same genome, with 97% of private calls unique to us. Worth its own investigation, well beyond archaic work.

Banked

Three diagnostic examples (archaic_private_dump, archaic_outgroup_density, archaic_classify_dump) and eight analysis scripts under scripts/archaic-validation/observable/, with a README mapping each to the question it answered.

Two harness bugs that produced confident wrong answers are documented there too, since both are easy to repeat: conditioning on "has a call" (reported ~80% carrying against a known 4.3% background), and measuring tract contrast in 100 kb bins (reported 1.14× where the correct figure is 1.98×).

discriminability.py needs no inputs and is the whole argument in twelve lines of output.

🤖 Generated with Claude Code

…k the instruments

Tier B was gated off (#40) for carrying no per-individual signal. This records WHY,
because the caller was not mis-tuned -- it was built on an observable that leaves
about one bit of evidence per tract, and no amount of fitting could have rescued it.

THE NUMBER THAT SETTLES IT. Detectability of one 36 kb tract (the measured median):

                                        evidence      sens @5% FP   @1% FP
    private-variant density (built)     ~1 variant       14.3%        4.3%
    archaic-allele matching             ~30 sites        95.1%       80.9%

Reaching 80% sensitivity at 5% false positives takes 36 kb by allele matching and is
not reached by 500 kb with density. Both observables carry the SAME ~3x contrast
(2.89x vs 3.04x), so contrast was never the problem: evidence per tract was.

THE WRONG TURN. §3 chose Skov 2018 (hmmix), whose premise is detecting introgression
WITHOUT archaic reference genomes -- it infers them from private-mutation density
precisely because it assumes you do not have them. We do: all four archaic genomes,
and archaic_classify already ships 2,031,406 diagnostic sites derived from them. §3
dismissed the reference-based alternative in one clause ("IBDmix is reference-based
and heavier"), and that clause is the root of every failure below. An introgressed
tract is a haplotype inherited intact; the question is whether a stretch MATCHES an
archaic genome, not whether it is slightly more mutated. Runs, not counts.

WHAT WAS RULED OUT FIRST, each of which looked right at the time:

- the 6.0x hard-coded multiple IS wrong (real enrichment 2.89x), but correcting it
  changes little;
- per-individual Baum-Welch EM -- the thing hmmix does and we do not -- makes it
  WORSE: unconstrained it learns 22.4x and ~9 kb tracts, calling 7x the truth;
- oracle parameters taken from the truth still sit at their random-placement null,
  precision pinned at 4-5%, so the model CLASS is wrong, not its parameters;
- the background is 14.6x overdispersed against the Poisson it is modelled with, with
  a 5.3x p10-p90 spread -- noise larger than the signal;
- a mutation-rate map would not have sufficed: the best proxy available explains 38%
  of that variance, leaving 7.4x;
- quality filtering does not help (it lowers dispersion only by discarding variants);
- it is NOT our variant calling: 1000G's own calls for the same person give the same
  contrast (1.98x vs 2.08x), though ours are 6x noisier -- 2.12x the carried SNVs on
  chr21 and 97% of private calls unique to us, which is worth its own investigation;
- it is NOT the truth set or my lift: in native hg38 with no lifting, hmmix's tracts
  are enriched 1.84x for their own archaic SNPs against a 1.04x null.

WHAT THE REFRAME ALSO FIXES: diagnostic sites become the denominator, so their uneven
density cancels and the mutation-rate map is unnecessary; the background's dispersion
stops applying because nothing is modelled as a density; the assets already exist; and
per-site matching split by lineage class IS Skov's post-hoc annotation, so the gated
attribute_lineage may be recoverable with it.

STILL OPEN: the background carrying rate measures 13.0% here against the 4.3% recorded
earlier in the design -- unreconciled, and it sets the contrast. ~30 sites per tract
assumes adequate coverage at diagnostic sites. And the reframe is n=1 so far.

Banked: three diagnostic examples (archaic_private_dump, archaic_outgroup_density,
archaic_classify_dump) and the eight analysis scripts, with a README mapping each to
the question it answered. Two harness bugs that produced confident wrong answers are
documented there too -- conditioning on "has a call" (reported ~80% carrying against a
known 4.3% background) and measuring tract contrast in 100 kb bins (reported 1.14x
where the correct figure is 1.98x).

Documentation and dev-only examples; no shipped behaviour changes. Tier B stays gated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JamesKane
JamesKane merged commit 016cd98 into main Jul 31, 2026
3 checks passed
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