Validation¶
docs/milestones.md signs M6 off when "resistance calls match published
results ... with discrepancies explained". This is what was compared, what
agreed, and what did not.
Everything here is reproduced by tests/test_validation.py, which runs the
real pipeline over real reads on every CI run of the demo job.
Two things are validated, separately¶
The scoring engine is checked against Stanford's own answers.
crates/ligase-core/tests/fixtures/hivdb/ holds the Stanford HIVdb reports
published as supplementary data by the HIVGenoPipe study — thirty clinical
samples at four ambiguity thresholds, 3,348 expectations — produced by
Sierra 3.5.3 against HIVDB 9.8.
All 3,348 are reproduced exactly: every drug, every score, every level.
That oracle matters because the obvious alternative is circular. Checking the scorer against expectations derived from the same XML it reads would test the parser against itself. Sierra is a different implementation, in a different language, by the algorithm's own authors.
The pipeline end to end is checked by running it on reads whose published answers are known, which is the rest of this page.
The samples¶
PRJNA1218827 is the HIVGenoPipe study's MiSeq data. Four runs are fetched, and the SRA metadata maps each to the sample identifier the paper uses — so nothing rests on reading a figure.
| Run | Sample | What it is |
|---|---|---|
| SRR32217726 | Sample 3 | a mixture: K219KE, and L100LI below it |
| SRR32217715 | Sample 4 | multi-class positive: NRTI, INSTI and capsid |
| SRR32217716 | Sample 30 | fully susceptible — a clean negative |
| SRR32217714 | — | the study's no-template control |
A mixture, a strong positive, a true negative and a blank. Between them they exercise every judgement the interpreter makes.
What agrees¶
The pipeline's mixture parameter answers the same question as the paper's
Amb2/Amb5/Amb15 reports, so one set of alignments validates all
three thresholds.
| Threshold | Drug calls compared | Agree |
|---|---|---|
| 15% | 81 | 81 |
| 5% | 81 | 81 |
| 2% | 81 | 72 |
At 15% and 5% the agreement is exact — every score, every level, every mutation string, for all twenty-seven drugs on all three samples.
That includes the cases worth naming:
- Sample 3 at 15%:
K219KE, potential low-level resistance to zidovudine and stavudine. A mixture, scored as both amino acids. - Sample 3 at 5%: adds
L100LI— high-level resistance to four NNRTIs. Same reads, same alignment; a 5% minority virus changes the regimen. - Sample 4:
M184V,T97A + E138T + G140S + Q148Hgiving high-level resistance to all five INSTIs, andM66I + Q67H + N74D + A105Tin capsid giving high-level lenacapavir resistance at a score of 160. - Sample 30: susceptible on all twenty-seven, with no false positives.
- The no-template control: all twenty-seven drugs
Not assessed, with no score. Sixty of its thirty-two thousand reads map to HXB2.
What disagrees, and why¶
Nine of the 243 drug calls differ, all of them at the 2% threshold. Each
is listed with its cause in KNOWN_AT_TWO_PERCENT in
tests/test_validation.py, and the test fails if one of them changes
shape or a new one appears — which is what keeps "discrepancies explained"
from decaying into "discrepancies tolerated".
Six are cosmetic: phasing a VCF cannot carry¶
Sample 3's five NRTI rows read K219KEGR where Stanford reads K219KE,
and Sample 4's lenacapavir row reads A105AT where Stanford reads
A105T. Every score and every level is identical.
The cause is the real limitation of reading codons from a VCF. At HXB2
3204 the reads are 44.65% G; at 3205 they are 2.75% G. At a 2% threshold
both pass, and enumerating the combinations of the two ambiguous bases
gives AAA, AGA, GAA, GGA — K, R, E and G. No single read need
carry both substitutions. A VCF says what is at each position, not what
travels together.
Neither extra amino acid matches a rule, so nothing clinical follows. The fix, if it is wanted, is to count codon haplotypes from reads spanning the codon, which the alignment already makes available.
Three are one missed mutation, and it is a real one¶
Stanford reports V75VA for Sample 30 at 2%, reading potential low-level
resistance to zidovudine, intermediate to stavudine and low-level to
didanosine. Ligase reports susceptible to all three.
This alignment supports the V75A base in 3 of 392 reads at HXB2 2773 — 0.77%, below any threshold that would report it. HIVGenoPipe realigns against a sample-specific reference, which is that paper's whole method, and places reads that an alignment to HXB2 does not.
This is a sensitivity limit, not a disagreement about what the reads mean, and it is the honest cost of the demo's approach. It is also the only one of the nine that would change what anyone did.
What this exercise changed about the pipeline¶
Validating is what found the biggest problem, and it was not in the interpreter.
The demo's call step used to run bcftools call -mv --ploidy 1. On
Sample 3, the reads at HXB2 3204 are A 238, G 192 — a 45% K219E — and
bcftools call emitted no record for it at all. The majority allele is
the reference, so there was no variant to call. The report came back
susceptible to all twenty-seven drugs.
It was right to, given that input: it matches Stanford's own
majority-consensus report for the same sample exactly, which is itself
useful evidence that the interpreter is sound. But Stanford's 5% report
for that sample finds L100I as well and reads high-level resistance to
four NNRTIs. A pipeline whose answer flips from "fully susceptible" to
"resistant to most of the NNRTI class" on a threshold it never exposed is
not doing what it says it does.
So the caller is gone, the pileup's own allele depths are the evidence,
and min_freq now means what its name says.
PRJNA517147 is not used for this¶
docs/milestones.md also names PRJNA517147, the Washington DC cohort
study (Jair et al., PLoS ONE 2019;14(4):e0214820). Its per-sample
resistance calls are published only as colour in a heatmap — Fig 4,
reverse transcriptase only, with protease and integrase "not shown" and no
supporting tables.
Of the four runs fetched from it, three are blank rows in that figure and
the fourth cannot be separated from its neighbour by eye. The mutations
the paper quantifies in text are attached to PL- identifiers that are
never mapped to the specimen numbers the figure uses.
That project remains the demo's default dataset, and M3, M4 and M5 are still signed off on it. It is not used as a source of expected resistance answers, because a number transcribed from a figure is not a citable value. PRJNA1218827 publishes machine-readable per-sample reports with the algorithm version stamped in every row, and that is what M6 is checked against.