feat(archaic): ghost lineages, Neanderthal tracts in named living haplotypes, and where the method breaks - #3
Draft
ruvnet wants to merge 6 commits into
Draft
Conversation
… flywheel Adds `examples/dna/src/archaic.rs`, a first-principles implementation of the logic behind TRACE (Science, 30 July 2026), which recovered two extinct hominin lineages from present-day genomes alone. Validated against a simulated cohort where the truth is known. - structured (multispecies) coalescent per window on a hominin population tree calibrated to the published divergence times; introgression planted by reassigning a lineage to the donor deme - detector embeds each window as an rvDNA k-mer profile vector, indexes it in a RuVector HNSW graph for candidate retrieval, and decides on exact Jukes-Cantor-corrected coalescent depth against those candidates only (3.1x fewer exact comparisons than an all-pairs scan) - Darwin mode hill-climbs the detector's own hyperparameters behind a margin gate; held-out F1 0.821 -> 0.867, with the held-out gain exceeding the training gain - flywheel promotes confirmed ghost segments to references and runs until dry The flywheel needs two rules or it eats itself: only segments that cleared the depth test on their own evidence may become references, and a rescue requires the segment to be closer to a known ghost than to anyone living. Without them false positives validate more false positives and precision falls 0.84 -> 0.19 over three rounds. `flywheel_does_not_bootstrap_on_its_own_errors` guards it. Two binaries drive it: `trace-rv` (the full study) and `depth-hist` (the brute-force reference measurement the indexed detector is judged against). `artifacts/` holds the illustrated report: four animated SVG diagrams, the generated data, and a self-contained HTML story built by `artifacts/build.mjs`. The cohort is simulated, not real 1000 Genomes data. It validates the method and makes no claim about any living person's ancestry. See ADR-016. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Dhba1gV3hjg4tX9F3Q4C7
… sweeps Adds `discover`, a third binary that asks where the method stops working, and rebuilds the report on the Cognitum design system. New findings: - Resolution limit. A ghost lineage stops being recoverable as a distinct lineage below roughly 9 recovered segments (0.043% of the panel). Above that it separates into its own mode, keeps ~100% of its segments and shows the carrier-geography enrichment that identifies its route; below it the deep tail of the more common ghost swallows it. - Information limit. Detection saturates at ~500 bp per window (~54 kb of real-sequence information, ~30 expected variable sites at a 650 ka split). Longer windows buy almost nothing; shorter ones fail from the bottom up as the depth estimate turns to noise before the threshold does. - Index fidelity. Approximate retrieval reproduces 100% of the calls an exhaustive scan makes, zero missed, for 3.3x less comparison work, with the two detectors within 0.0003 F1. The index chooses what to compare, not what to conclude — now measured rather than asserted. Experiments 1 and 2 use an exhaustive detector on purpose, so the limits belong to the biology and the method rather than to the index. Report changes: - Cognitum design system: tokens, 4px spacing base, 12px radius, its four motion layers, and Outfit + JetBrains Mono embedded as data URIs under the SIL OFL (the Artifact CSP blocks font CDNs and a silent fallback would lose the typography). Dark-committed, as the system is. - Lineage colours are Cognitum hues stepped into the readable band for chart marks, validated for colour-vision separation and contrast; brand cyan stays reserved for active intelligence and green for verified/positive. - Seven-pass scroll narrative over the real measurement: depth-hist now emits all 20,880 per-segment depths so each pass paints data rather than an illustration. Population structure is visible in it — African haplotypes run deeper. - New section explaining the platform in plain language: what rvDNA, RuVector, metaharness Darwin and Cognitum One each do, how a question moves through them, and what every reported metric actually means. - Mobile: sticky bar, drawer menu with 44px targets, responsive scrollytelling, chart type scaled for small viewports, horizontally scrollable tables. - The resolution sweep is drawn as small multiples sharing an x axis rather than two series on one y axis. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Dhba1gV3hjg4tX9F3Q4C7
…45 KB The published artifact was failing to load. It was 811 KB in a single document, with ~500 KB of JSON parsed synchronously on the main thread. This fixes the weight and rebuilds the opening as a 3D narrative. Payload: - per-segment coalescent depths ship as a base64 Uint16Array plus class bytes instead of a JSON number array (253 KB -> 111 KB, and it now carries the call and truth classes too) - archaic calls carry the six fields the page reads, not all twenty (171 -> 38 KB) - the mono webfont is no longer inlined into each SVG diagram. Three copies cost 126 KB to restyle the labels on three static illustrations. Opening sequence — six acts, scroll-driven, one GPU point per segment: now -> dispersal -> the ledger -> depth becomes height -> the outliers -> two ghosts All 20,880 points morph between six analytic layouts computed in the vertex shader from the real measurement. Height is coalescent depth throughout, so the closing shot separates the two lineages by their actual divergence times. Fixes a degenerate look-at by moving the whole scene to a consistent Y-up world. Not three.js: the CSP blocks external scripts, so a library has to be inlined, and minified three.js is ~600 KB — which would have put the page back near a megabyte, i.e. straight back into the bug being fixed here. Hand-written WebGL 2 costs a few KB. Falls back to the written account when WebGL 2 is unavailable. Mobile: act cards sit low so the animation stays visible above them. Verified headless at 1440px and 390px: no console errors, WebGL context acquired, all six acts fire, no horizontal overflow, load ~450 ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Dhba1gV3hjg4tX9F3Q4C7
Four additions, all of which change what the study can claim. Real archaic genomes (`real-dna`). The 1 Mb chr22 region now has the Altai Neanderthal, Vindija 33.19 and Denisova 3 pulled over the same coordinates from the MPI-EVA snpAD call set, by the same .tbi -> HTTP Range -> BGZF route used for the modern panel. The introgression claim rests on Patterson's D and not on divergence, because GRCh37 is a European-weighted reference and African haplotypes accumulate apparent mismatches to any archaic for reasons unrelated to admixture. ABBA and BABA both condition on the archaic carrying the derived allele, so that bias cancels. D(AFR, non-AFR; Altai, ref) = +0.1146 +/- 0.1047 Z = +1.1 D(AFR, non-AFR; Vindija, ref) = +0.1281 +/- 0.1007 Z = +1.3 D(AFR, non-AFR; Denisova3, ref) = +0.0040 +/- 0.1243 Z = +0.0 Both Neanderthals lean positive, the Denisovan is flat at 3% of that — the pattern introgression predicts and a reference-bias artefact does not, since all three were called against the same reference by the same pipeline. It is also not significant, and the report says so: at this effect size the comparison needs ~7 Mb of chromosome to reach Z = 3, emitted as `mb_for_z3`. That is a power limit, not a null result, and it is the real-data twin of the resolution floor the simulation reports. Tract-aware detection (`linkage-study`). Introgression arrives in tracts, so a rule that reads a window's neighbours before thresholding should beat one that does not. It does: F1 0.591 -> 0.624, and all 19 rescued windows were genuinely introgressed. The control is what makes it a result — on an unlinked cohort the same rule moves F1 by exactly 0.0000, so the gain is linkage rather than a relaxed threshold. Neither detector recovers tracts at their planted length (4.86 windows planted, 1.81 and 2.01 recovered); both fragment rather than fuse, and since tract length is how admixture is dated, a fragmenting detector systematically over-ages the pulse it finds (95 vs 229 generations). Panel design (`panel-study`). Size buys precision and costs recall, with the noise floor falling 140 -> 32 ka. Composition beats count: 24 African-only haplotypes (F1 0.761) beat 36 balanced ones (0.744). But the obvious corollary fails — the African panel is strictly worse for every non-African query and wins only because African queries are where the false positives are. Archaic reference value is steeply diminishing (three Neanderthal genomes to one costs 0.008 ghost precision; one to zero costs 0.216) and ghost recall is 1.000 in every arm, so the failure mode of an under-referenced study is inventing lineages, never missing them. The page is branded rUv with a masthead, carries all of the above, and is 425 KB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Dhba1gV3hjg4tX9F3Q4C7
…nguage Cold open. The masthead is now a title sequence rather than a hero image: letterbox bars retract, 34,000 points start scattered in a sphere and assemble into a DNA double helix staggered from the middle outwards, the camera dollies from 46 units to 21, and the wordmark resolves out of 26px of blur. Scrolling flies the helix past the lens instead of cutting. Depth fade on w does the work a depth-of-field pass would. Still hand-written WebGL 2 and not three.js, for the reason the README already gives: the page has to parse everything before it can draw anything, minified three.js is ~600 KB, and the 811 KB build of this page failed to load outright. The shader is ~4 KB and drives the same GPU pipeline a library would drive for you. Parallax. One rAF-throttled scroll handler writes a `--py` custom property on registered layers — a soft light behind each section drifting at its own rate, plus figures, pull quotes and section eyebrows. Off-screen layers are skipped and nothing reads layout outside the rAF callback. Two overflow bugs fixed on the way. `.sect-glow` was centred with `translateX(-50%)` and also carried `.plx`, whose transform overwrote the centring — the glow sat 340px right of where it belonged and pushed the document 40px wide on desktop, 273px on mobile. It now centres with `margin-inline:auto` so `transform` belongs to the parallax alone. Verified 0 horizontal overflow at 1440px and 390px across a full scroll pass. Plainer language. The archaic-genome section was written for someone who already knows what an ABBA/BABA count is. It now leads with the test in one sentence — take one African genome and one non-African genome, find a spot where they disagree, look up what the Neanderthal has there, count — and names Patterson's D afterwards as the label for what was just described. The reference-bias problem is explained as "the template was assembled mostly from people of European descent" rather than as GRCh37 ascertainment, and the block jackknife as "chop the region into 19 pieces and see how much the answer wobbles." Page is 438 KB. No console errors at either viewport. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Dhba1gV3hjg4tX9F3Q4C7
…named haplotypes A third real-data stage, and the first whose subject is the people rather than the detector. `human-story` asks three questions of the same megabase, each with its own control. Where the Neanderthal DNA is. The scan does not measure similarity to a Neanderthal — it defines a Neanderthal-informative site: a position where Altai or Vindija carries the non-reference allele and none of the 160 African haplotypes does. Conditioning on absence in Africa is what makes it immune to the European weighting of GRCh37, because the test never references distance from the reference. non-African (test) 847 stretches in 254 / 640 haplotypes 1.83% of sequence African (control) 0 stretches in 0 / 160 haplotypes 0% The control returns zero. And 1.83% lands on the published ~2% figure for Neanderthal ancestry in non-Africans, from one megabase and a rule about what is missing from Africa. Informative alleles per haplotype: EAS 24.8, SAS 9.0, AMR 5.8, EUR 2.5, AFR 0.0. East Asians carrying more Neanderthal ancestry than Europeans is the published ordering, recovered here. The 10x magnitude is not — genome-wide the excess is ~20%, and the report says so: one megabase is one locus. When it arrived, and a mistake the simulation predicted. Mean surviving tract is 13,825 bp, which inverts to 7,233 generations — 210 ka against a published 50-60 ka. That 4x over-estimate is reported as a headline rather than buried, because the linkage sweep predicted it on simulated data where truth was known: a detector that fragments tracts instead of fusing them always dates the pulse too old, and there the error was 2.4x on a planted pulse. A simulation that predicts the direction and mechanism of an error later seen in real human DNA is worth more than one that only scores well. The family, with no tree assumed. Two haplotypes drawn from the same population must go back 864 ka to meet in MSL and 641 ka in PEL, and the seven most internally diverse populations are all African. Two people from MSL are further apart than any European-East Asian pair in the panel (widest 847 ka). Illustration. Two new animated figures: a tract map with one row per living haplotype, continents banded and Africa first, revealed left to right along the chromosome — the empty African band is the control made visible — and a 26-population diversity chart that grows on scroll. Refactor. `real_panel.rs` extracted from `real_dna.rs` so both binaries share one loader, one archaic aligner, and the two bit-parallel primitives. Page weight held at 457 KB by packing the tract map as index triples (16 KB, not 150) and folding detector call and ground truth into two nibbles of one byte (-28 KB). 194 tests pass, clippy and fmt clean, 0 horizontal overflow at 1440px and 390px, no console errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Dhba1gV3hjg4tX9F3Q4C7
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.
Recovers two extinct hominin "ghost" lineages from present-day genomes on the rvDNA engine, sweeps the method until it fails to find its real limits, and then turns the same machinery on real 1000 Genomes and real archaic genomes to say something about people rather than about detectors. Ships an illustrated, scroll-driven report under
artifacts/, titled rUv.Responds to Zhang, Biddanda, Johnson, O'Dushlaine & Moorjani, Science, 30 July 2026, whose TRACE method found a ~800 ka lineage in every living human and a super-archaic lineage that reached us via Denisovans. Neither has ever been sequenced.
Two figures from the press coverage do not survive a read of the paper, and this PR does not repeat them: "Denisovans carry 3–5% super-archaic DNA" is a press-release number (TRACE's estimate is 0.3% of the Denisovan ancestry in Oceanians), and 1.8 Ma is a coalescence time — an upper bound on divergence — where the authors' conservative claim is "diverged over 500,000 years ago". Those figures are used here only as the simulation's planted splits, where truth is known by construction.
Detection is validated in simulation; everything claimed about real people is measured on real data, at the population level, and attributes no ancestry to any named person.
Headline results on real genomes
800 phased haplotypes (400 people, 80 per superpopulation) over chr22:20–21 Mb, 21,418 biallelic SNPs, plus Altai Neanderthal (~52×), Vindija 33.19 (~30×) and Denisova 3 (~30×) from the MPI-EVA snpAD call set over the same coordinates. Acquired without
tabix,bcftoolsorpysam: parse the.tbilinear index in pure Python, HTTPRangeinto the bgzipped VCF, decode BGZF withzlib.1 · African haplotypes coalesce exactly 2.00× deeper than every other superpopulation, which all land on the same median (46.4 ka vs 23.2 ka). All ten deepest segments in the region are African. 6,072,400 exact popcount comparisons in 0.4 s.
2 · Where the Neanderthal DNA actually is. The scan defines a Neanderthal-informative site: a position where a Neanderthal carries the non-reference allele and not one of the 160 African haplotypes does. Conditioning on absence in Africa makes it immune to the European weighting of GRCh37 — the test never references distance from the reference.
The control returns zero, and 1.83% lands on the published ~2% figure for Neanderthal ancestry in non-Africans — from one megabase and a rule about what is missing from Africa.
Informative alleles per haplotype: EAS 24.8, SAS 9.0, AMR 5.8, EUR 2.5, AFR 0.0. East Asians carrying more Neanderthal ancestry than Europeans is the published (and counter-intuitive) ordering, recovered here. The 10× magnitude is not — genome-wide the excess is ~20% — and the report says so: one megabase is one locus.
3 · Dating the pulse, and a mistake the simulation saw coming. Mean surviving tract is 13,825 bp → 7,233 generations → 210 ka, against a published 50–60 ka. 4× too old. That failure was predicted: sweep 4 below reports on simulated data with known truth that a detector which fragments tracts rather than fusing them always dates the pulse too old, because a shorter measured tract inverts to more generations — there the error was 2.4× on a planted pulse. It is reported as a headline rather than buried, because a simulation that predicts the direction and the mechanism of an error later observed in real human DNA is doing more than one that merely scores well.
4 · The family, with no tree assumed. Two haplotypes drawn from the same population must go back 864 ka to meet in MSL and 641 ka in PEL, and the seven most internally diverse populations are all African. Two people from MSL are further apart than any European–East Asian pair in the panel (widest 847 ka).
5 · Patterson's D. Over 160 African × 640 non-African haplotype pairs, with a leave-one-window-out jackknife:
Both Neanderthals lean, the Denisovan is flat at 3% of that. Reference bias would push all three the same way. It is also not significant, and that is the more useful number: at this effect size the comparison needs ~7 Mb to reach Z = 3, emitted as
mb_for_z3so the limit is a number in the output rather than a hedge in the prose.Engine
examples/dna/src/archaic.rsandreal_panel.rs— no new dependencies.simulate_cohortTraceEnginecluster_ghostsdarwinflywheelreal_panelSeven binaries:
trace-rv,depth-hist,discover,linkage-study,panel-study,real-dna,human-story.Simulation results
Five limit sweeps
1 · A ghost lineage vanishes below ~9 recovered segments (0.043% of the panel).
2 · Detection saturates at ~30 variable sites per window; below that the depth estimate turns to noise before the threshold does.
3 · HNSW reproduces 100% of the exhaustive scan's calls for 3.3× less work.
4 · Reading a window's neighbours beats reading it alone (F1 0.591 → 0.624; all 19 rescued windows genuinely introgressed; +0.089 held out). The control is the result: on an unlinked cohort the same rule moves F1 by exactly 0.0000.
5 · Size buys precision and costs recall (noise floor 140 → 32 ka). Composition beats count — 24 African-only haplotypes (F1 0.761) beat 36 balanced (0.744) — but the obvious corollary fails: that panel is strictly worse for every non-African query, and wins only because African queries are where the false positives are. Archaic reference value is steeply diminishing (three Neanderthal genomes → one costs 0.008 ghost precision; one → zero costs 0.216), and ghost recall is 1.000 in every arm: an under-referenced study invents lineages, it never misses them.
The flywheel bug, and why the fix is in the model not the tuning
The first implementation promoted any GHOST call to a reference and rescued anything within
ghost_link_kaof one. False positives became references and validated more false positives: precision fell 0.84 → 0.22 → 0.19 over three rounds.Two rules fix it, both from the population genetics: only primary calls may be promoted, and the rescue rule is relative — closer to a known ghost than to anyone alive, not merely close to a known ghost.
flywheel_does_not_bootstrap_on_its_own_errorsfails if either is removed.Report
artifacts/index.html, titled rUv, self-contained at 457 KB.Opens on a title sequence: letterbox bars retract, 34,000 points assemble out of a scattered sphere into a DNA double helix staggered from the middle outwards, the camera dollies from far to close, and the wordmark resolves out of blur — then scrolling flies the helix past the lens. Behind it, a nine-act scroll-driven sequence in raw WebGL 2, one GPU point per segment, 20,880 of them, morphing between analytic layouts computed in the vertex shader. The camera flies up one axis — coalescent time, 1.8 Ma to the present — then turns around and looks back down it, so the closing shot separates the two ghosts at their actual divergence times rather than by decoration. Section-level parallax throughout.
Two new animated figures for the real-genome section: a tract map with one row per living haplotype, continents banded and Africa first, revealed left to right along the chromosome — the empty African band is the control made visible — and a 26-population diversity chart that grows on scroll.
Why not three.js, which was asked for: the CSP blocks external scripts, so a library has to be inlined, and minified three.js is ~600 KB. An earlier 811 KB build of this page failed to load outright. Hand-written WebGL is ~4 KB and drives the same GPU pipeline.
Styled with the Cognitum design system — its tokens, 4px spacing base, 12px radius, four motion layers, Outfit + JetBrains Mono embedded as data URIs under the SIL OFL. Lineage colours stepped into the readable band and validated for colour-vision separation. Mobile-first: sticky bar, drawer menu with 44px targets, scrollable tables.
Verification
cargo test --release— 194 passed, 0 failedcargo clippy --release --all-targets— clean;cargo fmt --check— cleandata/real/is checked in (~35 MB) so the real-genome stages reproduce without network access.See ADR-016 and artifacts/README.md.
Sources
🤖 Generated with Claude Code
https://claude.ai/code/session_011Dhba1gV3hjg4tX9F3Q4C7