The talk is the highlights reel. This is the whole journey — start to finish, in order: what we set out to do, how each part of the machine works, the side-quests that worked and the ones that didn't, what broke along the way, and how a rough first version turned into the final pipeline. The real commands and scripts are shown, verbatim.
Two ways to read this. Short on time? Read the Prologue, then each chapter's intro paragraph and open only what catches your eye. Want the engineering? Read straight through — the accordions carry the code, the failures, and the fixes.
Where we started
A NUMT is a piece of mitochondrial DNA that was copied into the nucleus long ago and has been quietly decaying there ever since — a frozen molecular fossil of the mitochondrion of that time. Muyao Huang & Prof. Frith had already detected 189 ancient such fossils in the human genome. We were handed that catalogue and a question in two halves.
The two questions the whole project chases
Junk vs function. Is an ancient NUMT inert "junk," or has it been put to use — sitting inside or beside working DNA?
Erosion vs deletion. For the oldest copies, whose human sequence can no longer be recognised as mitochondrial at all, is the signal truly deleted — or merely eroded below detection, and therefore recoverable if we could rewind the clock?
Everything below is the attempt to answer those two, honestly.
What was ours, and what we were handed
Keeping this straight matters — the honesty of the whole story depends on it.
- Handed to us (Huang & Frith): the detection of the 189 ancient NUMTs; the
genancestorreconstruction program; and Frith's pre-built ladder of ancestral mitochondrial genomes. We did not detect the NUMTs and we did not build the ancestor-maker. - A — Annotation. A functional class + a verified age for all 189 (the foundation table).
- B — Origin. The mitochondrial gene each NUMT came from, and its OXPHOS complex.
- C — Reconstruction. The design of the "rewind" experiment, its honest result, and an independent confirmation run.
So the arc is: foundation (theirs) → A → B → two flagship fossils emerge → C rewinds them → a second method checks C. That is the order of the chapters.
Reading the fossils: annotating all 189
The foundation pipeline. It takes Huang's 189 NUMTs and pins two things on each one: a function (what kind of DNA it landed in) and an age (how far back it can be traced). This is the key pipeline everything else stands on — so it is worth seeing how it actually decides, and how it grew from a leaky first cut into the version we trust.
How a NUMT gets its function labelhow it works
Each NUMT is scored against a stack of genome features, then given one class by walking eight
tiers top-down and taking the first that fires — highest applicable wins. A NUMT that is both exonized
and regulatory is called Exonized (checked first), while every feature it overlaps is still
counted separately, so nothing is hidden.
def classify(feat, params):
"""Return the functional class string for one NUMT's feature dict."""
if feat.get("has_cds_exon"): # 1 — inside a real coding exon
return "Coding"
if is_exonized(feat): # 2 — UTR/ncRNA exon or miRNA (ties with Regulatory)
return "Exonized"
if is_regulatory(feat): # 2 — a regulatory element / promoter peak
return "Regulatory"
if is_active_chromatin(feat, params):
return "Active chromatin"
if has_transcription_signal(feat):
return "Transcription signal"
if feat.get("is_intronic"):
return "Intronic"
if feat.get("near_gene"):
return "Near-gene"
return "None" # 7 — gene desert
Eight tiers, one winner. The result across the 189: Intronic 73 · Active chromatin 34 · Regulatory 31 · Exonized 18 · None 17 · Near-gene 15 · Coding 1 (Transcription signal has 0 members in the final run). Most ancient NUMTs sit in or near functional DNA — an expected tilt, because these are the ancient survivors and slow-changing DNA is often slow-changing for a reason.
Fence: a class label records where a NUMT landed, not proven function. Co-location is necessary-but-not-sufficient for exaptation.
How a NUMT gets a trustworthy age (two axes, kept apart)how it works
Age is the number most easily fooled, so the pipeline keeps two separate signals and only ever
trusts one of them for the age itself. The verified axis is the divergence of the oldest species
whose ortholog bridge still aligns at the locus — but only if that alignment is confident. Broad
100-vertebrate conservation (phyloP) is kept as unscored context, never folded in, because it
can reflect the host gene rather than the NUMT.
def ortho_conf_ok(e):
"""Keep an ortholog only if its confidence (1 - mismap) clears the bar,
i.e. mismap <= ORTHO_MAX_MISMAP. Stops one spurious deep alignment
from inflating huang_confirmed_depth."""
m = parse_E(e.get("mismap"))
return True if m is None else (m <= C.ORTHO_MAX_MISMAP) # ORTHO_MAX_MISMAP = 0.5
# AGE axis uses only confident orthologs (mismap gate); phyloP is NOT folded in.
ortho = [e for e in ev if e["type"]=="ortho" and e["cov_bp"] not in ("NO_BRIDGE","")
and ortho_conf_ok(e)]
The first version counted any ≥1 bp overlap as conservation and never even recorded the
alignment's mismap — so a single short, spurious deep hit (a real one had
mismap = 0.64) could silently push a NUMT's age too far back. The fix captured
mismap and added this gate. Re-running the whole set, it dropped 0 of 1,583
orthologs and changed 0 of 189 ages — the axis was already clean, so the gate is a proven
safety net rather than a correction. Separately, the confidence score reported for each origin is the
UCSC-style −10·log10(E).
The discarded first run: when the chimp "ate" the age axisv1 → final
What went wrong. When we generalised the age step to re-anchor on any species, an early run recorded each NUMT's coordinates in a partner species as the full extent of the alignment block. That is fine for distant species, but the chimp genome is ~99% identical to ours, so its alignment blocks are enormous — a median of 92,174 bp where the NUMT is ~110 bp. The age step then counted any bridge species aligning anywhere in that huge window, and 162 of 187 NUMTs falsely reported the same deep age. The run was biologically meaningless and was discarded.
The fix (this is the resolution, in code). A small function, project(),
walks the alignment column-by-column and maps the NUMT's human interval onto the partner's true
footprint — roughly the NUMT's own width, not the whole block.
def project(hs, he, hsd, hseq, ps, pe, psd, pseq, lo, hi):
# Walk alignment columns; map the human sub-interval [lo,hi) onto the
# PARTNER's coords. Returns the partner interval actually orthologous to
# the NUMT (~NUMT width), NOT the whole block. None if nothing aligns.
hc = pc = 0; pmin = pmax = None
for hch, pch in zip(hseq, pseq):
hpos = ppos = None
if hch != "-":
hpos = (hs + hc) if hsd=="+" else (he - 1 - hc); hc += 1
if pch != "-":
ppos = (ps + pc) if psd=="+" else (pe - 1 - pc); pc += 1
if hpos is not None and lo <= hpos < hi and ppos is not None:
if pmin is None or ppos < pmin: pmin = ppos
if pmax is None or ppos > pmax: pmax = ppos
return None if pmin is None else (pmin, pmax + 1)
The chimp anchor window went from a median 92,174 bp → 107 bp; falsely deep-dated NUMTs collapsed 162 → 1; SLAIN1's chimp window became 176 bp, exactly its human footprint. Human was never affected — it uses genome coordinates directly and never takes this path. The two pre-fix runs were deleted.
A tempting dead-end the design caught by itself: CCNJL "to zebrafish"dead end
One NUMT, near the gene CCNJL, showed a spectacular broad-conservation hit all the way to zebrafish
(~430 My) — which would have been a headline-grade ancient NUMT. But it was wrong, and the pipeline's
own two-axis design flagged it: the fish species that sit at the same depth and align even
better were completely silent. Conservation is nested, so a signal that appears in zebrafish but in
none of the intermediate species cannot be true orthology. The verifier tagged it
phyloP_deep_huang_shallow — "inspect, never an established age" — automatically. It is a neat
example of the whole point of keeping two axes: the broad scanner proposes, the verified bridge disposes.
The final turn: "annotate, don't gate"v4 → v5
The last real change to the foundation was a change of philosophy, not a bug-fix. An earlier version dropped any NUMT whose best mitochondrial anchor was weaker than E = 0.05, which left 17 potentials looking like they had "no origin at all." That was a false negative: all 17 always had a real anchor hit, just barely over the bar (E 0.056–0.73). The principle became annotate everything, let detection be Huang's job — report the origin and attach a confidence flag instead of silently gating.
# config_v4.py
ANCHOR_GATE = False # v5: do NOT drop weak anchors. (True => legacy v4 gating.)
CONF_CAP = None # v5: report the raw, uncapped confidence.
# build_evidence.py — the flag is a pure confidence TIER, applied to every NUMT:
if ok is None: origin_flag = "none"
elif ok <= C.ANCHOR_E_CONFIDENT: origin_flag = "confident" # E <= 0.05
else: origin_flag = "low_confidence"
Flipping ANCHOR_GATE off dropped the "no origin" count 17 → 0 — every one of
the 189 now has a resolved origin. The new origin_flag tier splits the whole catalogue
161 confident + 28 low_confidence, without hiding anything. Crucially the age axis is
byte-identical to the previous version — only the origin reporting changed.
Where they came from: the mitochondrial origin
Chapter 1 stored, for each NUMT, the accession number of the mitochondrial gene it best matches. Chapter 2 asks NCBI what that accession actually is, and files it under its OXPHOS complex. The discipline here — never type a gene name by hand, always resolve the accession — turned out to catch a real error that had spread across the whole project.
Asking NCBI what an accession really ishow it works
For a protein accession, the pipeline fetches the real NCBI GenPept record and parses its
/coded_by= line to recover the source mitochondrial coordinates, strand, and gene — tolerating
the messy realities (complement(...), multi-span join(...), partial-boundary
markers). Every fetch is cached so each accession is pulled at most once.
def fetch_genpept(acc):
cache = C.CACHE_GENPEPT / f"{acc}.gp"
if cache.exists() and cache.stat().st_size > 0:
return cache.read_text() # fetched at most once, then cached
txt = efetch(_eutils_url("protein", acc, "gp")) # real efetch.fcgi, rettype=gp
if txt and "CODED_BY" in txt.upper(): cache.write_text(txt)
return txt
_CODED_RE = re.compile(r'/coded_by="([^"]+)"', re.IGNORECASE)
def parse_coded_by(genpept_txt):
"""{mito_accession, mito_start, mito_end, strand} from a GenPept /coded_by."""
joined = re.sub(r"\n\s+", " ", genpept_txt) # /coded_by can wrap lines
m = _CODED_RE.search(joined)
if not m: return None
expr = m.group(1)
strand = "-" if "complement(" in expr else "+"
# ... parse NC_ accession + min(start)..max(end) across any join() parts ...
It resolves, e.g., NP_904331.1 → COX2, NC_005089.1:7013..7696 (SLAIN1's mouse
origin) directly from NCBI — no gene name is ever assumed. Re-fetched live during the audit, both
flagship accessions re-derived their gene, coordinates and length exactly.
From gene to biological role: the OXPHOS lookuphow it works
Once the gene is known, a fixed, network-free textbook table maps it to its role in the mitochondrion's energy machinery (the OXPHOS complexes). It is species-independent because vertebrate mitochondrial gene content is conserved — which, incidentally, is why the human-only UCSC annotation was tested and rejected as the source in favour of NCBI's per-species RefSeq records.
GENE_FUNCTIONAL_GROUP = {
# Complex I (NADH dehydrogenase) — 7 subunits
"ND1":"Complex I", "ND2":"Complex I", "ND3":"Complex I", "ND4":"Complex I",
"ND4L":"Complex I", "ND5":"Complex I", "ND6":"Complex I",
"CYTB":"Complex III", # cytochrome bc1
"COX1":"Complex IV", "COX2":"Complex IV", "COX3":"Complex IV", # cytochrome c oxidase
"ATP6":"Complex V", "ATP8":"Complex V", # ATP synthase
"RNR1":"rRNA (12S)", "RNR2":"rRNA (16S)",
"D-loop":"control region (non-coding)",
} # TRN* -> "tRNA"; Complex II is entirely nuclear-encoded, so absent.
(Labels above trimmed for width; the file spells each complex out in full.) This is where the origin spectrum comes from — see Chapter 2's result below.
The side-quest that paid off: catching COX1 → ND5win
What happened. One flagship's origin gene had been written down, by hand, in an early
report as "COX1," and that label had spread into the reconstruction scripts, the reports, even a sub-test.
But the stored accession (YP_220690.1) was always correct. Because Deliverable B
resolves the accession from NCBI rather than trusting any hand-typed name, it quietly surfaced the
contradiction:
if C.is_protein_ref(g): # g = the STORED accession, e.g. "YP_220690.1"
res = F.resolve_protein(g) # asks NCBI; gene name is NEVER hand-typed here
if res:
out["gene_symbol"] = res.get("gene_symbol","") # -> "ND5", from /gene
out["resolve_status"] = "resolved"
efetch YP_220690.1 returns /gene="ND5", 607 aa (the sloth's real COX1
is a different accession, YP_220682.1); the alignment lands at ND5 protein
positions ~218–266 of a ~608-aa reference (consistent with ND5 ~600 aa, not COX1 ~513 aa);
and the loci differ. The label was corrected everywhere. This is a genuine provenance win — the kind
of error that only surfaces if you refuse to trust names.
Honest residual: one PTOV1 sub-test ("D12") had been built against COX1, so it is invalid and left open — but the main recovery test uses the full protein database (which already covers ND5) and gives the same answer, so no headline conclusion changed. Never quote the old "COX1" label or the D12 number.
The result: an origin spectrum shaped mostly by sizeverified
Running B over all 189 (0 unresolved; 72 via protein accession + 117 via genome region), grouped by OXPHOS complex:
Complex I + Complex IV = 104/189 = 55%; the four longest blocks = 164/189 = 87%.
The pattern is essentially length-driven — the longest mitochondrial genes are captured most, the
short tRNAs and non-coding region least — the expected null for passive, unselected capture. As a sanity
check, the indirectly-inferred potential profile mirrors the directly-detected
assembled one.
Fences: it's a length-driven baseline with a real biological non-random overlay — the control region is under-represented as a source (Tsuji, Frith, Tomii & Horton 2012, NAR). Any length↔count correlation coefficient is an external, illustrative reference, not a pipeline measurement, so we use the group totals, not shaky single-gene tallies.
Two fossils worth chasing
With function and origin on all 189, two NUMTs stood out — and both share the same tantalising
property: their human copy is potential, meaning it is too eroded to be recognised as
mitochondrial on its own. Their origin is known only because the same fossil is still legible in another
species. That is exactly the "gone, or just eroded?" question made concrete — and the reason for everything in
Chapter 4.
SLAIN1 — the one we end up rewindingverified
Locus chr13:77,741,138–77,741,314 (176 bp) · label
potential · class Exonized · origin mouse COX2
(Complex IV), NP_904331.1, genome-scale E = 0.012 · phyloP +1.11.
Detected directly in mouse, not human, and only projected onto the human genome by orthology. It becomes the star of Chapter 4 — the fossil we manage to partly rewind.
PTOV1 — the 1-in-189, and a deliberate contrastverified
Locus chr19:49,854,479–49,854,646 (167 bp) · label
potential · class the only Coding NUMT of the 189 · origin
sloth ND5 (Complex I), YP_220690.1, genome-scale E = 0.00017 ·
age eutherian-only, ~99 My.
Fences: origin is ND5, not COX1 (Chapter 2). PTOV1 is a contrast case, not a co-success — its rewind goals come back negative, so it is never presented alongside SLAIN1 as a shared win. Any PTOV1 cancer link in the literature is about the host gene, not the fossil.
Rewinding the clock: ancestral reconstruction
The heart of the project. If a fossil's signal is eroded rather than deleted, then winding a sequence back toward its ancestor should make the lost signal legible again. This chapter is the real arc: how the rewind works, how the first attempt was rooted backwards, the genuinely hard problem we had to solve by hand, the one side-quest that worked, and the two that came back negative — reported as negatives.
How you "reconstruct an ancestor" (and the exit-code trap)how it works
Reconstruction is one program, genancestor genome1 og1 og2: it reverts
genome1 by majority vote wherever both outgroups agree against it — undoing that species'
recent, private mutations and winding it back toward the shared ancestral state. Because the run only
substitutes (never inserts/deletes), a valid output must be exactly as long as genome1 — which
is how the wrapper judges success, since the program's exit code is unreliable.
# genancestor's EXIT CODE is unreliable: its temp cleanup does `rm genancestor.<pid>.*`, which
# returns non-zero (harmless) when a run created no temp files. Validate by the OUTPUT instead —
# a 3-arg substitutions-only run MUST produce length == genome1 length (coords preserved).
genancestor -P8 "$OUT/$g1.fa" "$OUT/$og1.fa" "$OUT/$og2.fa" > "$OUT/$name.fa" 2>"$OUT/$name.genancestor.err" || true
g1len=$(bp "$OUT/$g1.fa"); outlen=$(bp "$OUT/$name.fa")
if [ -s "$OUT/$name.fa" ] && [ "$outlen" = "$g1len" ]; then
printf " -> %s bp (reverted ~%s bp vs %s)\n" "$outlen" ...
This one call, run per row of a manifest, is the engine of the whole chapter. The length-equals-length success test replaced trusting the exit code, which had been false-failing 2 of 8 perfectly good reconstructions.
The first attempt was rooted backwardsv1 → v2
What went wrong. Version 1 extracted every species window through the
human bridges and reconstructed rooted on human, then tested for the mitochondrial signal with
a DNA matrix. But both flagships are potential — the human copy carries no detectable
signal to begin with. Rooting the rewind on a degraded copy that never had the signal, and using a DNA test
for a protein-level origin, made v1 a structural false-negative: it was guaranteed to find nothing.
The fix. The rule became: to confirm an origin, reconstruct the origin species; to attempt recovery, reconstruct the degraded species. The fossil lives in the anchor — mouse for SLAIN1, sloth for PTOV1 — so that is where the rewind is rooted. This is stated directly in the manifest's own header:
# THE v2 FIX: SLAIN1 and PTOV1 are `potential` (orthology-inferred) NUMTs — NEITHER is in
# the human<->mito alignment. The mito fossil is directly detected only in the ANCHOR species:
# SLAIN1 -> mouse (protein NP_904331.1, E 0.012)
# PTOV1 -> sloth (ND5 YP_220690.1, E 0.00017)
# So we ROOT each reconstruction on the species whose question it answers:
# SLAIN1 genome1 = mouse (confidence + depth recovery)
# PTOV1 genome1 = human (recover the hit in ancestral human)
# outname genome1 og1 og2
anc_mouse_rabbit_elephant_slain1 mouse_extant_slain1 rabbit_slain1 elephant_slain1
anc_human_chimp_lemur_ptov1 human_extant_ptov1 chimp_ptov1 lemur_ptov1
anc_humanREC_sloth_elephant_ptov1 anc_human_chimp_lemur_ptov1 sloth_ptov1 elephant_ptov1
The last two rows are a recursive refinement: build a near-ancestor from close relatives (chimp+lemur) first, then feed that back in to push to the placental root. It wound back 51 bp vs the one-step 39 bp — a genuine improvement, though (honestly) it did not change the mitochondrial outcome.
The hard part: the ancestor wasn't in any protein database, so we rebuilt it by handthe exemplar
Why this was hard. The mitochondrial origin signal is protein-level (COX2, ND5), and the detection pipeline compares against NCBI's curated mitochondrial proteins. But a reconstructed ancestral mitochondrion is raw DNA with no annotation at all — no gene boundaries, no translations — and the vertebrate mitochondrial genetic code is non-standard. There was simply nothing to feed the protein search. So we rebuilt the 13 ancestral proteins ourselves.
Step 1 — pin the genes and patch the genetic code.
# rCRS NC_012920.1 protein-coding genes: 1-based inclusive, strand, expected aa length (no stop)
CDS = [
("ND1",3307,4262,"+",318),("ND2",4470,5511,"+",347),("COX1",5904,7445,"+",513),
("COX2",7586,8269,"+",227),("ATP8",8366,8572,"+",68),("ATP6",8527,9207,"+",226),
# ... COX3, ND3, ND4L, ND4 ...
("ND5",12337,14148,"+",603),("ND6",14149,14673,"-",174),("CYTB",14747,15887,"+",380),
]
# Vertebrate mitochondrial code (transl_table 2)
B="TCAG"; std="FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIMMTTTTNNKKSSRRVVVVAAAADDEEGGGG"
CODON={}; i=0
for a in B:
for b in B:
for c in B: CODON[a+b+c]=std[i]; i+=1
CODON.update({"AGA":"*","AGG":"*","ATA":"M","TGA":"W"}) # vertebrate-mito overrides
The CDS table pins each gene to its exact coordinates, strand, and the length we expect
back (COX2 227 aa, ND5 603 aa — a built-in sanity check). Then the standard 64-codon table is
patched at the four positions that make it the mitochondrial code. Get these four wrong and every
ancestral protein is silently mistranslated.
Step 2 — borrow the reading frame from human, indel-safely (the "frame-lift").
# Align ancestor (ref) vs human; map every human position -> the ancestor base in the SAME column
hpos=hum_start; hum2boreo={}
for hb,bb in zip(hum_aln,boreo_aln):
if hb!="-": # walk human coordinates; skip human-gap columns
hum2boreo[hpos]=bb; hpos+=1
def anc_cds(start1,end1,strand):
anc=[hum2boreo.get(p,"N") for p in range(start1-1,end1)]
anc="".join(anc).replace("-","") # drop ancestor deletions -> keep frame in human coords
if strand=="-": anc=rc(anc)
return anc
The frame comes from the curated human annotation, inherited column-by-column across the alignment, and survives the handful of indels between the two sequences. The result: 13 ancestral proteins, one-to-one with the human genes, ready to search.
Step 3 — prove the recipe is right. Re-translate modern human mtDNA with the exact same code, coordinates and strand logic, and diff residue-by-residue against NCBI's curated human proteins. Result: 12 of 13 exact; the only difference is ND2 at position 1 (a benign initiator-methionine convention). That is why the ancestral results are trusted — and why any ancestral null is a real biological negative, not a translation bug.
def mytr(s,e,strand):
dna=hum[s-1:e]
if strand=="-": dna=rc(dna)
p="".join(tr(dna[i:i+3]) for i in range(0,len(dna)-len(dna)%3,3))
return p[:-1] if p.endswith("*") else p
# fetch NCBI's curated proteins and diff, residue by residue
# ... efetch -db protein ... ; compare ...
print("\nALL 13 EXACT MATCH" if allok else "\nsome differ — see notes ...")
If our home-grown translation matches NCBI's official human proteins everywhere, the recipe (code + frame + strand) is provably correct. The two residual differences were checked and both land outside the PTOV1 region, so they do not affect the result.
Making a "negative" trustworthy: the controls (and a control that was a bug)fixed
Positive control. Extant mouse SLAIN1 → human COX2 fires at
E = 0.00015 — proof the assay works, so any human negative is real, not a broken
database.
The control that was itself a bug. The negative control originally reverse-complemented the query — but the aligner searches both strands, so that "control" was just the forward run's other strand and proved nothing (forward and control scored identically). The fix: a plain reverse (composition-preserving, but neither original strand).
revseq () { # REVERSE (not complement) — composition-preserving, homology-destroying null.
# NB: lastal searches BOTH strands, so a reverse-COMPLEMENT control is just the forward
# run's minus strand -> fwd_E == rev_E and the control is useless. A plain reverse is
# neither strand of the original, so it is a valid null.
awk 'NR==1{print ">"substr($0,2)"_REV"; next}{s=s$0}
END{ n=split(toupper(s),a,""); o=""; for(i=n;i>=1;i--) o=o a[i]; print o }' "$1"
}
# forward run vs the reversed control (E= preserved: no last-split)
lastal -p "$tr" "$db" "$OUT/$q.fa" > "$OUT/mito/$q.$sp.pro.maf"
lastal -p "$tr" "$db" "$OUT/mito/${q}_REV.fa" > "$OUT/mito/$q.$sp.pro_REV.maf"
Without this fix the entire mitochondrial read-out would have been silently uninterpretable. With
it, every real forward hit has rev_E = none — so the negatives below are real.
The side-quest that worked: G3, reviving a lost cross-species matchthe one win
The setup. At the opossum and platypus SLAIN1 loci, extant mouse gives 0 alignment — the fast rodent mutation rate has eroded the mouse copy past recognition at that distance. So: can rewinding the mouse copy bring the match back?
The trap we had to dodge. A conserved host exon sits ~3–4 kb from the NUMT and is hit even by extant mouse — so a naive "best hit anywhere" would look like recovery. The fix pins the expected NUMT interval and classifies each hit on-locus vs off-locus.
# whole-genome: build db, train, align at genome-scale significance, split to unique best
lastdb -P8 -uNEAR "${sp}_db" "$fa"
lastal -P8 $trainopt -D1e9 "$db" "$OUT/$q.fa" 2>/dev/null | last-split > "$OUT/depth/$q.$sp.maf"
# classify each block: ON-LOCUS = recovery signal; OFF-LOCUS = the conserved exon (NOT recovery)
awk ... 'onloc=(ts==xs && st<xt && en>xf); ...'
Extant mouse at the NUMT locus: nothing. The reconstructed ancestral mouse (~48 bp wound back) re-aligns at both the opossum and platypus SLAIN1 loci at mismap 1e-10, reverse-clean, replicated across two reconstructions × two species, and cleanly separated from the off-locus exon. This is G3 — the one recovery that worked.
The mandatory fence: G3 revives a lost cross-species nuclear match — erosion, not deletion. It is not proof the copy is still mitochondrial. That is a separate test, and it comes back negative — next.
The side-quest that didn't: the mitochondrial signal stays lostnegative
The real prize would be reviving the mitochondrial identity — showing an ancestral human copy that matches an ancestral mitochondrion. It does not happen. Every ancestral human reconstruction returns 0 against the mitochondrial-protein database, while the positive control fires — so the negative is real, not a broken assay.
Why (as far as we can tell). The deepest ancestor we can reach
(boreoeutheria, ~85–96 My, ~79.8% identical to modern human mtDNA) is simply too young
and/or the wrong lineage for these particular fossils. Even a deliberately permissive probe finds no
NUMT-scale match beating a scrambled control.
This is reported as a diagnosed limitation whose cause is not settled (the ancestor's depth vs our species choice) — and explicitly not "saturation," and never "we resurrected a mitochondrial NUMT." A deeper, lineage-matched ancestor is the real next test.
The side-quest we had to take back: "SLAIN1 gone at reptiles"retracted
Along the way, an earlier analysis concluded SLAIN1 was "gone / saturated" at the reptile distance (~319 My). That was wrong, and we retracted it. It had conflated two different things: the conserved host exon (which really does reach reptiles — chicken, alligator, turtle — and is hit even by extant mouse) with the NUMT (whose mitochondrial core returns 0 blocks in those reptiles). The NUMT insertion reaches ~160–180 My (opossum, platypus), not the reptiles.
We keep this in the story deliberately: it is the one agent conclusion that slipped through the first review and was caught only by a later independent audit — the reason the whole project now runs adversarial re-checks on every claim (see the Epilogue).
Standing rule, from this episode: SLAIN1 has two ages — the host exon (~319 My) and the NUMT insertion (~160 My) — and they must never be conflated. Never "SLAIN1 reaches reptiles," never "SLAIN1 is gone at reptiles."
Checking our own work: an independent confirmation
A negative is only as trustworthy as the method behind it. So we asked: was the mitochondrial negative real, or an artefact of our home-built rewind? To find out, we changed the method almost entirely and kept only the question — and got the same answer.
The clean re-test: Frith's ancestor + Huang's own scriptshow it works
Keep the nuclear side as the extant whole genome (the best-preserved copy); swap the mitochondrial side to Frith's pre-built ancestral mtDNA (byte-identical to his original — no home-grown reconstruction anywhere); and run Huang's own, unmodified detection scripts, changing only the mitochondrial input. The signal survives only at protein level, so protein is the decisive test; DNA nulls at the flagships are expected.
# DNA scan — Huang's mt2nu recipe, ancestral mito as the DB:
lastal -j7 -D"$D" -S0 -P8 --split-n --split-m=0.01 -p train MITODB GENOME > mt2nu.maf
# PROTEIN scan (THE decisive test) — translate the DNA query in-frame vs the ancestral protein DB:
lastal -j7 -K1 -D"$D" -P8 -p PROTMAT PROTDB GENOME > mt2nu_pro.maf
Both scans return 0 at all four flagship loci, while recovering hundreds of recent NUMTs genome-wide — so the machinery works, it just finds no ancient mitochondrial signal at the flagships. And the ancestral detector reproduces 94% of Huang's human calls (82–88% concordant overall), so it is a working detector, only slightly less sensitive at borderline loci. The negative is confirmed by a second, independent method.
A documentation bug that made the result look weaker than it wascorrected
An earlier write-up claimed the DNA training had produced a "degenerate 188-byte train file → no substitution matrix → default scoring," which would have undermined the DNA negative. On checking the server, that was simply false: all three DNA trains are full trained matrices (16k / 14k / 22k bytes; the result headers carry the trained-model parameters), and no 188-byte stub exists anywhere. The correction actually strengthens the negative — the DNA scan trained fine and still found nothing; protein is just more sensitive. A clean example of the audit catching a documentation error that would have weakened a real result.
Two zeros we do NOT trust (disclosed, not hidden)artifact
The chr19 "0" that was really "never measured." An unquoted-argument shell bug split a two-chromosome input so the human region file was chr13-only — PTOV1 (chr19) was never scanned. That particular "0" is an absence, not a null; the full-genome scan (which does cover chr19) supersedes it. A reminder that a zero is worthless until you know it was measured.
The sloth "DNA hit" that isn't ND5. A low-scoring forced-DNA hit near the sloth PTOV1 maps to CYTB, is low-complexity, and is sub-threshold genome-wide — so it must not be cited as a DNA analogue of the ND5 origin.
The road ahead
The journey ends with the most interesting loose thread — and an honest label on it.
Could reconstruction find new NUMTs? (a lead, not a result)unverified
The confirmation run, besides reproducing 94% of Huang's human calls, turned up 165 "our-only" human hits: 91 on unplaced scaffolds (an assembly artefact) and 74 on primary chromosomes — the latter being genuine "the ancestor found it, the modern search missed it" cases (20 strong at E < 1e-20).
The hypothesis: these could be older, more-diverged NUMTs that an age-matched mitochondrion catches but the modern one misses — i.e. reconstruction as a NUMT-discovery tool.
The fence, mandatory: these are candidate leads, not confirmed NUMTs. No reverse control, rRNA filter, conservation check or cross-species corroboration has been run, and some may be split pieces of NUMTs already called nearby. A lever worth testing — never a result.
Also deferred as future work: a deeper, lineage-matched ancestral mitochondrion (the real next test of the negative), and a formal saturation gate — parked on purpose, so the confirmation experiment could come first.
What the journey taught us
Two things worth carrying away: a single place to audit every claim, and the working habit that made the whole thing trustworthy.
Every claim and its limit, in one place
- The mitochondrial recovery is NEGATIVE. G3 revived a cross-species nuclear match only — never mitochondrial identity.
- That negative is a diagnosed limitation, cause not settled (ancestor depth vs species choice) — about these ~85-My ancestors, not proof the fossil is unrecoverable in principle, and not saturation.
- A
potentialhuman null is definitional (the copy is too eroded to read), not evidence of loss. - SLAIN1's ~319 My is the host exon, not the NUMT (~160 My).
- Functional-class labels record where a NUMT landed, not proven function.
- Capture is length-driven with a non-random overlay; the length↔count coefficient is external, illustrative — use the group totals.
- The ~74 ancestral-only loci are UNVERIFIED candidates, not calls.
Disclosed as history — named, not buried:
- The reptile "gone at reptiles" claim — RETRACTED (host exon vs NUMT, Ch. 4).
- The PTOV1 D12 sub-test — OPEN (built against COX1; the full-DB test already answers it, negative).
- The chr19 forced-DNA "0" — ARTIFACT (chr19 was never scanned, Ch. 5).
- The sloth score-122 DNA hit — CYTB, not ND5 (Ch. 5).
The habit that made it trustworthy
Across the project, every wrong conclusion was caught by an independent check — never by the person or agent that produced it. Two were caught before they were ever believed; one (the reptile claim) slipped through and was retracted later. That track record is exactly why the project mandates: reproduce every claim from the primary data with a second pass, fire a positive and a reverse control alongside every negative, and match method and stringency to the question. The honesty in this appendix — keeping the retractions and the open items visible — is the point, not an apology.
Glossary, sources, and who did what
Glossary. Every term used here — cell, gene, exon, mitochondrion, alignment, ancestral reconstruction — is defined from scratch in the companion's 📖 Basics tab. ← open the companion
Further reading (foundational, peer-reviewed):
- Bensasson, Zhang, Hartl & Hewitt (2001). Mitochondrial pseudogenes: evolution's misplaced witnesses. Trends in Ecology & Evolution.
- Hazkani-Covo, Zeller & Martin (2010). Molecular poltergeists: mitochondrial DNA copies (numts) in sequenced nuclear genomes. PLoS Genetics.
- Tsuji, Frith, Tomii & Horton (2012). Mammalian NUMT insertion is non-random. Nucleic Acids Research 40(18):9073–9088.
- Huang & Frith (2026). Sensitive, probability-based detection of ancient (pre-eutherian) NUMTs — the upstream foundation this project builds on.
Attribution. Detection of the 189 NUMTs, the genancestor program, and
the pre-built ancestral-mtDNA ladder are Huang & Frith's. Deliverables A (annotation), B (origin) and C
(reconstruction design + interpretation), plus the confirmation experiment, are this project's.