Releases: Softleif/seqair
Release list
seqair 0.4.0
- Breaking: INFO methods now return
Result. - Added: CRAM 3.1 arith (method 6) and fqzcomp (method 7) codecs, BETA-encoded byte series, the shared slice cache, and CRAM
estimate_region_bytes. - Changed: tok3 speedups, CRAI binary search, and
keep_recordseeing TLEN and mate position already filled in. - Fixed: the BCF reserved values, and all the CRAM fixes, including the cap on tok3 token streams.
compair 0.1.0
First release!
seqair 0.3.1
A performance release. Every hand-written core::arch kernel is now one portable fearless_simd
kernel, and the readers, pileup engine, CRAM codecs and writers were profiled and tightened around
it. Nothing public is removed or changed (cargo semver-checks against 0.3.0 is clean), and the
additions are opt-in. In rastair (chr12, byte-identical output) end-to-end cycles dropped 12 % on
a Ryzen 3950X and 1 % on an Apple M4, measured before the last rounds of CRAM, query and pileup
work.
Fixed
- BGZF writing failed on data that does not compress. A block, gzip framing included, must fit
in 64 KiB, and the writer filled blocks with 65,536 bytes of data. Data that does not compress is
stored, a little larger than it went in, so every full block atcompression_level(0)— any
level-0 BAM, BCF or VCF.gz over 64 KiB — and any full block of random-looking bytes at other
levels failed withBgzfError::CorruptHeader. Blocks now hold at most 65,280 bytes, htslib's
BGZF_BLOCK_SIZE, which fits the worst case; a compile-time check keeps it that way. Block
boundaries move, so output is no longer byte-identical to 0.3.0's (still valid, ~0.4 % more
blocks). - Reading a block filled to exactly 64 KiB could read past the query's chunk. Once such a block
was read to its end,RegionBufnamed the position as the block's own first byte — the
within-block offset 65,536 wrapped to 0 — so the chunk-end check compared a position one block
behind and kept going; a debug build panicked. 0.3.0's own writer produces such blocks. The
position is now the next block's(offset, 0), as the writer names it. - Bgzipped SAM queries took a BGZF error inside a chunk for the end of data and returned short
without an error. The error is returned now. They also no longer read one byte past a chunk end
that falls on a block boundary.
Added
BamWriterBuilder::compression_threads(n)andvcf::Writer::compression_threads(n)compress
BGZF blocks onnworker threads (0, the default, compresses on the calling thread), like
htslib'shts_set_threads. The calling thread keeps serializing and writes blocks in order, so
the data and the co-produced BAI/CSI are byte-identical to the single-threaded writer's. Writing
10 Mb of 30x chr12 as BAM at level 6 with 3 threads: 4.0 → 1.34 s wall on the M4.vcf::Writer::compression_level(level)forVcfGzandBcf(default 6), matching
BamWriterBuilder::compression_level. Plain VCF ignores both settings.BgzfError::ThreadSpawn,CompressionWorkerLostandBlockTooLarge.bam::seq::encode_bases_into, andRegionBuf::fill_buf/consumefor reading a region's
decompressed bytes a block at a time.
Changed
- Plain VCF output is buffered (128 KiB) instead of written with one
write_allper record, so
records reach the sink in large writes.finish()flushes; a writer dropped without it flushes
best-effort and logs a failure withwarn!. - A BGZF block that cannot fit is reported as
BgzfError::BlockTooLarge { size }, not
CorruptHeader. - New dependencies
fearless_simdandfearless_simd_macros. The kernels pick the widest level
the CPU has at run time, so AVX-512 machines now get 64-byte vectors, and theunsafeblocks of
the old AVX2/SSSE3/NEON copies are gone.cram::rans_nx16_avx2andcram::rans_nx16_neonare
now empty and hidden; they only ever held crate-private items.
Performance
- SIMD kernels (3950X, time vs 0.3.0): ASCII→Base 0.70–0.78×, 4-bit sequence decode
0.69–0.76×, plain-FASTA fetch 0.80–0.85×, the rANS Nx16 32-state order-0 kernel 0.29×.
OwnedBamRecord::to_bam_bytesencodes the sequence in place (1.9× fewer cycles serialize-only).
MM/ML base-modification resolution walks a SIMD bitmask of the target base: 5.7–7.6× faster on
sparse calls, 1.8–2.4× on dense ones. - CRAM, chr20:10–30 Mb (813k reads) end to end: CRAM 3.1 2.13 → 1.15 s on the 3950X (M4 1.08 →
0.54 s), CRAM 3.0 1.86 → 1.34 s (M4 1.07 → 0.69 s) — before the last round of codec work, which
took off several percent more. The rANS 4x8 and Nx16 decoders, PACK and RLE now use htscodecs'
table layouts and loop shapes (rANS 4x8 order-0 331 → 513 MB/s, order-1 292 → 338; PACK up to
2.1×). Reference runs are copied instead of converted base by base, codec buffers are reused, tag
encodings are resolved once per slice, and external blocks are ordered by first use. - Small region queries: a reader keeps a 64-block cache of decompressed BGZF blocks and its
per-query setup between queries, starts a query where the previous one first found a record,
and grows its reads from one block instead of the whole remaining range. BAI and CSI look up
candidate bins instead of scanning every bin. chr12, M4, per query: 1 bp every 100 bp 277 →
6.7 µs, 1 bp every 1 kb 293 → 43 µs, 1 kb tiles 334 → 53 µs; 1 Mb tiles unchanged. Bgzipped SAM
shares the cache (1 kb tiles 1.30 → 0.50 ms) and scans lines and fields with SIMD (2.7× fewer
cycles per fetch). - Pileup: each read's CIGAR is walked with an incremental cursor (as htslib's
resolve_cigar2),
D and N columns take the fast path, columns where nothing expires skip eviction, and a column
stops atmax_depthinstead of being built and truncated. Spliced synthetic pileup 2.81 →
2.35 s; ~1000-deep columns capped at 50 1.85 → 0.29 s; a rastair-shaped TAPS pileup −16.5 %
cycles on the M4. - Writers: plain VCF of 2.76 M rastair-shaped records 5.2 → 0.83 s on the M4, from the
buffering above and an O(1) FORMAT duplicate check. That is on top of%gfloats printed with
integer math instead ofcore::fmt's Dragon4 fallback, which had already cut VCF text to 3.8×
fewer cycles and VCF.gz to 2.1× fewer on the 3950X. Each BGZF block is assembled in place and
written with one call. - Decoding: BAM read-name and Z/H aux terminators are found with a vector scan (−2.8 % cycles
decode-only), and BAM and CRAM errors are built only on failure instead of on every call (−8 %
per small BAM query, −9 % on CRAM 3.1).
seqair-types 0.3.1
Changed
- New dependencies
fearless_simdandfearless_simd_macros.
Performance
- The ASCII→
Basekernel behindBase::from_ascii_vec,convert_ascii_in_placeand
convert_ascii_in_place_as_sliceis one portablefearless_simdkernel instead of AVX2, SSSE3
and NEON copies with theirunsafeblocks. It picks the widest level the CPU has at run time, so
AVX-512 machines get 64-byte vectors, and handles a ragged tail as one overlapping vector: 0.70–0.78×
the time of 0.3.0 on a Ryzen 3950X.
seqair 0.3.0
Breaking
Every genomic interval in the API is now one closed span, core::range::RangeInclusive<Pos0>,
instead of two loose positions (see r[interval.span_type]). The reference for a region is fetched
with the very value that queried it, so there is no + 1 at any boundary for a caller to get wrong,
and a span ending on Pos0::MAX names its last base where a half-open end could not.
fetch_into(tid, start, end, store)→fetch_into(tid, span, store), onReaders,
IndexedReaderand every format reader; same forfetch_into_customized,
estimate_region_bytes,IndexedBamReader::queryandPileupEngine::new. These were already
inclusive on both ends:(start..=end).into()is the whole migration.IndexedFastaReader::fetch_seq/fetch_seq_intoandReaders::fetch_base_seqwere half-open
and are now closed.fetch_seq(name, p(0), p(4))becomesfetch_seq(name, (p(0)..=p(3)).into()).
fetch_seq_into_u64, the side door for reaching the last representable base, is gone — the
closed span reaches it.FastaError::RegionOutOfBoundsreportslast(inclusive) instead of
end, and fires forstart > lastorlast >= seq_len.- The segment target
(resolver, start, end)is(resolver, span). Segment::end()→Segment::last(): it returned the last covered position under a name that
reads as one past it.Segment::core_range()→core_span(), now acore::range::RangeInclusive
(.start/.last;Copy, 8 bytes, not an iterator). NewSegment::span().Pos0::max_value()→Pos0::MAX.- MSRV 1.92.0 → 1.98.1. It was already effectively 1.98.1 via
seqair-types;core::rangeneeds
1.96. seqair-typesis depended on withdefault-features = falseand a forwardingserdefeature was
added. seqair serializes nothing itself, so its users no longer compile serde for nothing; enable
seqair/serdeto serialize seqair-types values.
Fixed
-
A reversed query returned the reads spanning it.
fetch_intowithstart > endhanded the
overlap test an interval that names no positions, and the test —pos <= end && end_pos >= start
— is satisfied by exactly the records covering the whole gap. Every reader now treats a span with
last < startas what it is, empty, and returns nothing. Found by a property test written for the
new span API; the behaviour predates it. -
A query on a truncated BAM never returned. Not slowly — never.
IndexedBamReader::fetch_into
on a file truncated anywhere past its header, with its index left intact, spun forever with no
panic, no allocation and no error, which is why 26 fuzz targets never saw it: libFuzzer could
only ever have reported it as a timeout.RegionBuf::read_recordreported two different facts as
the sameBgzfError::UnexpectedEof— "the window ran out mid-chunk, refill" and "the file ended
here" — and the query loop read every one as the first, looping back on the assumption that the
chunk step would happen on the next turn. At the end of the planned ranges the cursor stops
advancing, so it never did.read_recordnow returnsOk(None)when the ranges are exhausted at
a record boundary and keepsUnexpectedEoffor running out partway through a record, and the
chunk step is explicit, so every turn of the loop consumes either a record or a chunk. A
truncated file now yields the records before the cut, as htslib does with one. -
CRAM: a multi-reference container could decode reads as
Nwith no error. Such a container
holds one index entry per slice per reference, and the reference window was taken from the first
entry whose container offset matched. When a later slice reached further along the reference, the
window stopped short and the tail of those reads came back asN— silently, since running past
the fetched reference is only a warning. It is the union of every matching entry now. htslib turns
multi-reference slices on by itself once a container would hold few records per reference, so an
ordinary file with short contigs reaches this; two slices one base apart are enough. -
CRAM:
embed_ref=2files could not be opened at all. The slice-header MD5 was checked against
the FASTA whether or not the slice carried its own reference. Underembed_ref=2htslib embeds a
consensus computed from the reads and digests that, so it matches the external reference only
where the reads happen to agree with it — every such file with low coverage or a real difference
failed withReferenceMd5Mismatch. A slice with an embedded reference is now exempt. -
CRAM: a no-reference file mis-decoded any read with an insertion. The
Qandqfeatures
carry quality and nothing else, but both were decoded as an anchoring reference match, adding a
base and anMoperation. In no-reference mode htslib emits oneQper inserted base next to
theIfeature, so those reads came back one base too long and were refused with
QualLenMismatch. It needs no option to reach: htslib drops into no-reference mode by itself when
embed_refmeetsmulti_seq_per_slice. -
A CSI could silently drop records from a region query. A bin's
loffsetwas written as
that bin's own first chunk offset. htslib derives it from the linear index instead — the first
record at or after the start of the bin's leftmost leaf window — and the two differ whenever a
record in another bin begins earlier inside that window, which a record straddling a window
boundary does routinely. The bin's own chunk is then the larger of the two, and that is the
dangerous direction: a reader takesmin_offfromloffsetand discards every chunk ending
before it, sotabixandbcftools view -rboth returned short, with exit status 0. The record
was never lost from the file and a query for its exact position still found it; only a query
whose range started earlier missed it. Smallest case: a record at 0-based 16384 with a second
straddling the next window, queried from position 1. -
The VCF/BCF writer could not index a contig over 512 Mbp. Its index was built with
min_shift=14, depth=5whatever the header said, so bins ran out at 2^29 — a record above that
was written to the file, pushed to the index, and then unreachable, withbcftools view -r
returning nothing while the same query overbcftools index -c's CSI returned it. Nothing
errored. The depth is now derived from the longest reference, as htslib's
hts_adjust_csi_settingsdoes. This is the case CSI exists for; TBI and BAI cannot express it.
Indexes that already fitted the depth-5 scheme are byte-identical, since the search floors at 5. -
OwnedBamRecord::to_bam_bytesaccepted a record whose quality no longer matched its sequence.
set_seqvalidates the new sequence against the CIGAR and deliberately leavesqualalone, so
set_seqwith no followingset_qualwas a reachable state where the two disagreed. It
serialized:l_seqgoverns how many quality bytes a reader consumes, so the record did not
bounce, it was misread from the sequence onwards. It now raises the sameSeqQualLengthMismatch
the builder andset_qualraise. An emptyqualremains legal at any length. -
Non-finite floats in VCF text now match htslib's spelling.
write_float_gsent NaN and the
infinities throughDisplay, writingNaNwhere C's%g— and so htslib — writesnan. VCF 4.3
§1.3 admitsINF/INFINITY/NANcase-insensitively as Float values and §6.3.3 gives quiet NaN
first-class status, distinct from the missing sentinel, so these are values rather than errors and
are deliberately not written as.: that would turn a value into a missing value and put the
text output at odds with the BCF output, which writes the caller's bits through unchanged. -
The SAM reader rejected every negative element of a signed
Barray.B:c,B:sand
B:iare the signed subtypes, but all three were converted through the unsigned type of the
same width, soXX:B:c,-1failed the whole record withInvalidAuxValue. Each subtype is now
converted through the type it names. An unrecognised subtype is also an error rather than a
Btag whose element count promises more bytes than follow it. -
VCF float text now matches
bcftoolsexactly.write_float_gclaimed to write C's%g
with six significant digits — the format htslib emits — but only ever produced the fixed form,
so a value outside1e-4 .. 1e6came out as0.0000610352or1234567where bcftools writes
6.10352e-05and1.23457e+06. It now switches forms on the value's exponent after rounding
to six significant digits (so0.0001stays0.0001and does not become1e-04), writes the
exponent C's way with a sign and at least two digits, and writes negative zero as-0. Values
in1e-4 .. 1e6are unchanged, which is every quality score and most everything else. -
A float below about
1e-25could not be written to VCF at all.write_float_gsized its
decimal places from the value's magnitude and formatted into a 32-byte buffer, so six significant
digits of5.169879e-26overflowed it and the write failed withFormattedFloatLongerThan32Chars,
taking the whole record with it. Those magnitudes do not occur in QUAL, but they do in a p-value
carried in INFO. The writer now falls back to scientific notation there, which is what%gand
htslib emit. Values that already fit are byte-identical to before.
Added
IndexBuilder::csibuilds a CSI whose depth covers a given longest-reference length, and
IndexBuilder::csi_depth_forexposes that calculation on its own.IndexBuilder::BAI_DEPTHnames
the 5 that BAI and TBI are fixed at and that CSI now treats as a floor.seqair::bam::DecodeErroris re-exported.RecordStore::set_alignment, `p...
seqair-types 0.3.0
Breaking
Pos<S>is gone;Pos0andPos1are two structs. The generic form shared code between the
systems without sharing the floor (see Fixed). Both are#[repr(transparent)]overNonZeroU32:
Pos1holds its value, so the 1-based zero is not a check but a value that cannot exist;Pos0
holdsvalue + 1, so aPos0and thePos1of the same base share one bit pattern and the two
conversions are reinterpretations.Option<Pos0>andOption<Pos1>are now 4 bytes. The marker
typesZeroandOneand thePosname are removed; code that spelledPos<Zero>writesPos0.Pos0::max_value()/Pos1::max_value()→ the constantsPos0::MAX/Pos1::MAX, alongside
newMIN(Pos0(0),Pos1(1)).- serde support now lives behind a
serdefeature, on by default.default-features = false
drops serde,smol_str/serdeandsmallvec/serdefor callers who only parse BAM. One caveat,
invisible tocargo semver-checks: 0.2 had nodefaultfeature, sodefault-features = false
written alongsidefeatures = ["hts-compat"]was a no-op. On 0.3 that same line removes every
Serialize/Deserializeimpl; such callers needfeatures = ["hts-compat", "serde"].
Fixed
Pos1::checked_add_offsetcould produce a 1-based zero.Pos1::new(1).checked_add_offset(-1)
returnedSome(Pos1(0)), the valuePos1::newexists to refuse;checked_sub_offsetlikewise.
The bound check was "negative", which is the 0-based floor, and every test of the arithmetic used
Pos0, where the floor and "negative" coincide. Each type now checks against its own floor, and
every property test is stated once and run against both types.
Added
saturating_add_offset/saturating_sub_offseton both types: clamped to the type's range (0
forPos0,1forPos1) instead of returningNone. Padding a window and trimming an overlap
are saturating by nature — two bases before the start of a contig is the start of the contig — and
the checked forms made every such call site invent the sameunwrap_or.Serialize/DeserializeforPos0andPos1. The wire format is the bare value as au32;
deserialization re-checks what the constructor checks, so a document cannot produce aPos1of
0, and the error names the offending value and the range it was expected in.
seqair 0.2.0
A streaming-window BAM region reader, newtyped record and query indices, a unified
pileup API, and VCF/BCF encoder ergonomics.
Breaking
Pileup API
Readers::pileup(segment, depth)returns aPileupplan; finish it with.run().
pileup_with/pileup_with_reference→.mutate(f)/.with_reference(r)on the plan,
combinable in any order.PileupEngine::newtakes aPileupInput, minted only byRecordStore::prepare_for_pileup()
(which also returnsMateLinkStats). The engine used to silently assume position order and
linked mates.PileupEngine::take_store→reclaim_allocation— it returns an empty store that keeps its
slab capacity, not the records.PileupEngine::set_max_depthtakesNonZeroU32;0used to mean "emit nothing".PileupAlignment::strandremoved — useStrand::from(rec.flags).PileupColumn::pair_indelandPairIndelremoved →PileupColumn::mate_of, so your own
filters decide what the fragment says.
Record store
- Record indices are the
RecordIdxnewtype (bam::record_idx), not bareu32.u32::MAXis
unrepresentable, soOption<RecordIdx>is free and theu32::MAX"no mate" sentinel is gone. - Records are read through
RecordStore::record(idx) -> Option<RecordRef<'_>>, which does not panic.
The by-index readers (try_record,qname(idx),cigar(idx),seq(idx),seq_at,qual(idx),
aux(idx),extra(idx),mate_overlap(idx)) are methods on the handle instead —rec.qname(),
rec.cigar(),rec.base_at(qpos), … — with the record's own fields viaDeref. The handle borrows
the store, soclear, pushes,sort_by_posanddedupare rejected while one is alive.
set_alignment/write_store_recordnow reportNoSuchRecord;extra_mutreturnsOption.
UseRecordStore::indices()instead of0..store.len() as u32.
Coordinates
- Query offsets are the
QPosnewtype, deliberately not interconvertible withPos. Affects
AlignedPair,MatchPosition/MatchedBase/MatchedRef,CigarPosInfo,PileupOp,
PileupAlignment::qpos(),CigarMapping::soft_clip_qpos_at, and
BaseModState::mod_at_qpos/is_unmodified. AlignedPair::Insertion.qpos→first_inserted(also onAlignedPairWithRead/WithRef):
it names the first inserted base, whilePileupOp::Insertion.qposnames the matched base
before the run.
Readers and writers
RegionBufstreams a bounded sliding window instead of loading whole regions: it borrows the
reader (soRegionBuf<'a, _>), no longer implementsUnwindSafe, andloadis gone
(ensure_available/advance_range).- Unmapped-read filtering unified on
filter_raw:IndexedBamReader::keep_unmapped/keeps_unmapped
removed,FilterRawFieldsis type-level enums (publicraw_cigar_bytes,cigar_ops,packed_seq,
basesfields gone;end_posisOption<Pos0>), newRejectUnmappedcustomizer. TargetInfoAccessremoved fromseqair::bam::header.ReaderErrorgained a variant separating "region end past contig" from "exceedsi32::MAX".
VCF/BCF
- VCF headers always declare
VCFv4.5:VcfHeaderBuilder::file_format()removed,
VcfHeader::file_format()returns&'static str, version isVcfHeader::FILE_FORMAT.
A cardinality is versioned, so a header free to declare an older version could promise a grammar
it then violates. EncodeInfoandEncodeFormatremoved;FormatEncodergained three required methods
(format_ints,format_floats,format_string).VcfHeaderError::TooManyFieldsnow carries data.
Added
- Reference hooks on the pileup plan:
Pileup::mutate(f)rewrites the fetchedRecordStore
before pileup (local realignment viaset_alignment) and receives the segment'sRefSeq— the
same onePileupColumn::reference_basereports.with_reference(RefSeq)drives the engine from a
reference you already hold;reference_covers_reads()widens the fetch to the span the records
actually cover. - Mate linking:
RecordStore::link_mates()pairs a template's primary alignments once per store.
Exposed asPileupAlignment::mate_idx()/in_mate_overlap(),AlignmentView::qname_hash(),
PileupColumn::find_record()/mate_of(). - Window queries over a prepared store:
records_overlapping(start, end)onPileupInput,
PileupEngineandPileupColumn— a binary search over a runningend_posmaximum, so one long
read only costs the windows it overlaps. PlusPileupInput::store(). PileupColumn::position_of(record_idx)/alignment_at(index);
AlignmentView::inserted_bases/inserted_quals;PileupAlignment::indel_after;
engine.set_soft_clip_overhang(n)to keepnsoft-clipped bases at alignment fringes.- Indexed FASTQ references: the FAI parser accepts the sixth
qual_offsetcolumn;
FaiEntry::is_fastq()/qual_byte_offset(). htslib-validated. - VCF FORMAT parity:
FormatInts,FormatString, array-of-floats, percent-encoded string values,
htslib-compatible duplicate-field overwrite.Writer::finishreturns a self-serializing
CoordinateIndex. - Byte-aware segment planning to bound per-segment memory.
Fixed
BgzfWriter::virtual_offset()could name a byte inside a record when a write filled a block
exactly, corrupting every co-produced CSI/BAI/TBI offset that landed there — htslib rejected such
files outright. ~0.4 % of blocks fill exactly. Compressed bytes are unchanged; only the recorded
offsets differ.- Region queries stop at the query end instead of inflating BGZF blocks far past it: a 1 kb query
ontests/data/test.bamexamined 2529 records to keep 313; now 313. - SAM and CRAM region queries lost records overlapping by the query's last base (SAM tested a
half-open range against an exclusiveend_pos; CRAI compared a 1-based start to a 0-based query).
All readers now share one convention — 0-based, both ends inclusive. RecordStore::dedupsorts first. It collapses consecutive equal records, so an unsorted store
silently kept the duplicates the method exists to remove.- BCF genotypes carry the phase bit on the first allele. Hardcoded unphased, so htslib rendered a
fully phased0|1as/0|1. SlimRecord::end_posis htslib-compatible on unmapped reads (end_pos == pos).- Region queries no longer error on unplaced reads (
pos = -1/AP = 0). - FASTA: an untrusted
.faican no longer drive a multi-PiB allocation, serve bytes from a wrapped
offset, or panic onlinebases == 0; errors carry the path and distinguishPlainGzipUnsupported
fromGziIndexNotFound. - CRAM: missing rANS Nx16 and allocation-size validation.
- Empty-sample BCF missing-value handling; pileup depth cap always set.
Performance
- Region queries decompress about what htslib's iterator does: 45.8 GB → 22.7 GB on chr12 in 10 kb
tiles, from stopping at the query end. - Pileup columns:
Vec::retaineviction, entries written into one reserved block, pooled scratch
buffers, no cached qname hash,Base::known_indexas a table lookup. Column phase 3.80 s → 3.25 s
on NA12878 chr12 (20 Mb); 1.06x end-to-end in rastair, byte-identical output. - Plain-FASTA fetches use one positional
preadper span (~28 % faster on short slices) and forks
share a handle; fetched spans are stripped and uppercased on a vectorized path. CompactOp16 → 12 bytes;CigarSlicecollapsed to&[CigarOp].- New
pileup_tiledbench andexamples/tiled_pileupmeasure the many-small-queries pattern a
variant caller actually has.
seqair-types 0.2.0
Breaking
strand_from_flags(flags)removed →Strand::from(flags).Pos<S>no longer implementsDeref<Target = u32>. The deref defeated the newtype —*posflowed
into anyu32parameter and autoderef exposed everyu32method. UsePos::as_u32().- MSRV raised to 1.98.1 (algebraic float ops in
Probability→PhredandRmsAccumulator).
Added
QPos: arepr(transparent)0-based offset into a read's sequence, deliberately not
interconvertible withPos— confusing the two silently resolves reference bases from the wrong
locus. Carried by seqair'sPileupOp,AlignedPairandCigarPosInfofamilies.SumOfSquares: a bare sum whose count is supplied atfinish(count), for callers keeping several
sums over the same values (per-strand, per-allele) whereRmsAccumulator's own count is
duplication. Bit-for-bit identical toRmsAccumulator. Merging isAddAssign, notAdd.RootMeanSquare::from_sum_of_squares(sum, count)for a sum accumulated elsewhere.Pos::as_u32(), replacing the removed deref.