feat(variant): subset Number=A/R/G fields to the emitted allele (#105) - #210
feat(variant): subset Number=A/R/G fields to the emitted allele (#105)#210TimD1-bot wants to merge 1 commit into
Conversation
2c69277 to
b71be9b
Compare
2fadee3 to
b9367b5
Compare
|
Claude Opus 5 🤖: Planned rework, filed as #229. htslib's So Ordering constraint: the synthesized GT must be written with No action needed until #229 lands. |
00d763f to
d68e417
Compare
Number=A/R/G values are positionally tied to the source record's ALT list, while the
summary VCF emits normalized, split alleles, so the previous step retained no such
field rather than emit one incorrectly. Subset each to the one allele its output record
carries instead: Number=A takes element alt_idx-1, Number=R elements 0 and alt_idx, and
Number=G the genotypes over alleles {0, alt_idx} via index k(k+1)/2 + j, or one entry
per allele when the record is haploid.
The ALT ordinal belongs to the variant rather than to the record, so retention keeps
each value whole and the subset is taken at write time; the two entries of a het-alt
record therefore share one retained copy and come out holding different elements of it.
Each field's length class is read from the input header once and carried alongside the
columns, since the writer subsets without a header to look it up in.
Every declaration propagates verbatim. The writer builds each record with exactly two
alleles, so Number=A already means one value, Number=R two, and Number=G that record's
own genotype count -- rewriting any of them to a literal would only be wrong at some
ploidy, and htslib rejects PL declared as anything but Number=G outright.
b9367b5 to
6504773
Compare
|
Claude Opus 5 🤖: #229 has landed and this branch is rebased onto it. Taking the two halves of the comment separately. Adopted — the cardinality rewriting is gone. Not adopted — Driving it as a pure value-subsetting engine on a scratch If the Verification on the rebased branch: |
Note
Authorship: the content below was drafted by Claude Opus 5 (an AI coding agent) and
filed via
ghunder @TimD1-bot, a bot account operated by @TimD1. It reflects theagent's analysis, not a statement authored by @TimD1.
Closes #105. Step 5 of 8 toward #48, stacked on #209 (
104_td_keep-id-qual-filter-info-format).Rebased onto the post-#229 writer. The previous step retained every source field whose values are
independent of the
ALTlist and deliberately omitted theNumber=A/R/Gones, since theirvalues are positionally tied to the original
ALTlist while every output record carries anormalized, split allele. This completes field retention by subsetting them instead of dropping
them, and removes the
WARNthat named what was dropped.Subsetting
For a variant whose original
ALTordinal isalt_idx:Number=ABCF_VL_Aalt_idx - 1Number=RBCF_VL_R0andalt_idxNumber=GBCF_VL_G{0, alt_idx}, via indexk(k+1)/2 + jNumber=1, fixed,., FlagBCF_VL_FIXED,BCF_VL_VARThe
ALTordinal belongs to the variant, not to the record, so retention keeps each value wholeand the subset is taken at write time in
src_info/src_fmt_vals(src/variant.cpp:566), aheadof the re-typing
set_info_field/set_source_formatsalready do. That is what makes the het-altcase work: its two entries share one retained copy of the source columns and come out holding
different elements. Each field's length class is read from the input header once by
retain_header_linesintosrcRecords::info_lens/fmt_lens, since the writer subsets without aheader to look it up in.
A value that cannot be tied to the emitted allele — an unknown
ALTordinal, or a list shorterthan the ordinal indexing it — is reported as
.inFORMATand omitted fromINFO, which is howeach column expresses "missing".
Header cardinality: nothing is rewritten
An earlier revision of this PR rewrote
Number=A→1andNumber=R→2on the propagateddeclarations. That is unnecessary and is now gone:
set_var_recordbuilds every record withbcf_update_alleles(..., 2), so the records really are biallelic and the input's own declarationalready states the resulting cardinality —
Number=Ais one value,Number=Rtwo,Number=Gthatrecord's own genotype count. Rewriting to a literal is also actively wrong at some ploidy; htslib
rejects
PLdeclared as anything else outright:All three declarations now propagate verbatim, and
output_number()plus thebcf_hrec_duprewriting are deleted.
On
bcf_remove_allele_set(the open comment on this PR)The comment above proposed replacing the hand-rolled subsetting with htslib's
bcf_remove_allele_set()once #229 landed. #229 has landed, and I did not adopt it. Reason:bcf_remove_allele_setselects among the alleles a record already holds, and no record in thiswriter ever holds the source's
ALTlist.#229's plan had #209 switch retention to
bcf_dup()per source record, which is what would makethe call natural. #209 landed with the text-based
srcRecordsstore instead —grep -c 'bcf_dup\|bcf_hdr_merge\|bcf_translate\|bcf_remove_allele' src/*.cppon the base is0.set_var_recordsynthesizes each record from vcfdist's internal arrays, and those alleles are notgenerally a subset of the source's:
CAAGA>TTat 210 becomesG>GTTandGCAAGA>Gat 209 — neither allele nor theposition exists in the source record
srcRecordsretains noREF/ALTat all, so there is nothing to reconstruct one fromUsing it as a pure value-subsetting engine on a scratch
bcf1_tis possible but comes out largerthan the comma-split it replaces: it needs a separate one-sample header, typed values in and out, a
valid synthesized
GT(per the ordering invariant the comment itself notes), and a placeholderallele list. The part of the comment that does apply — that the cardinality rewriting is
unnecessary — is adopted above.
If the
bcf_dupretention model from #229's table is still wanted, it belongs in a follow-up thatrevisits #209's store, not here; I have not opened one.
Tests
tests/unit/src/test_variant.cpp: 9SubsetAltIndexedtests over the two entries of a het-altrecord —
A,R, andGeach taking different elements per entry, the haploidGshape,pass-through of
ALT-independent fields, and the ways a value can fail to be subset. Retentiontests reworked from "dropped" to "kept whole", plus the recorded length classes and
PropagatesAltIndexedHeaderCardinalityVerbatim.tests/unit/src/test_phase.cpp:HetAltEntriesReceiveDifferentAltIndexedValuespins the samething end to end through the htslib writer, reading the two co-located records out of the file.
tests/integration/test-integration.yml: thepreserved_fieldsfixture's het-alt record gainedAF=0.4,0.6and distinctPLvalues so thealt_idx == 2case (Gelements 0, 3, 5) isobservably different from
alt_idx == 1. Both records are pinned in full, the three verbatimdeclarations are asserted, and
must_not_containguards that no whole unsubset list reaches theoutput.
Verification
pytest: 134 passed (includes 779 gtest unit tests).makeat-Wall -Wextra: clean, no warnings.doxygen: 0 warnings.bcftools 1.21on the fixture's output:view,stats, andview -Ob(typed encoding, where acardinality mismatch would surface) each with zero bytes on stderr.
bcftools queryreadsback
AF=0.4/0.6,AD=0,15/0,16, andPL=255,60,0/255,44,7on the two het-alt records.