Read Reordering¶
fqxv-reorder clusters similar reads together so a downstream sequence coder
sees runs of near-identical reads — the cross-read redundancy lever that
per-read context modeling can't reach on its own (the idea behind SPRING and
PgRC).
How it clusters¶
Each read is reduced to its minimum canonical k-mer: over every k-mer in the read, take the smaller of the k-mer and its reverse complement, and keep the minimum. Reads are then sorted by that key (and by oriented sequence as a tie-break). Exact duplicates, reverse-complement duplicates, and near-duplicates that share a minimizer all land next to each other. A per-read flip flag marks reads stored reverse-complemented so a read and its RC-duplicate become byte-identical after orientation.
plan() returns the emission order and the flip flags; the caller reorders the
name/sequence/quality streams accordingly and stores a permutation to restore
the original order.
The pairing matters¶
Feeding reordered reads straight into the order-k context model barely helps — the model already captures much of the redundancy, so clustering double-counts it (a naïve reorder gave only ~9% on RNA-seq, negated by the permutation cost).
The win comes from explicit differential coding on the clustered reads — SPRING's actual mechanism. Each read is coded relative to the previous read in the reordered stream:
- MATCH — identical to the previous read (nearly free),
- DELTA — same length, a few mismatches: store the mismatch positions + bases,
- LITERAL — everything else, coded with the
fqxv-seqcontext model.
Duplicates collapse to a single op; the unique reads still get the context model. Each block records which block-local codec it used in a leading method byte (single-contig, or the literal-rescue variant that adds block-local rescue), and the container keeps whichever is smaller per block.
Global reference. Beyond the block-local codecs, the container can also
assemble one frozen whole-file reference (SPRING-style) over every clustered
read and code reads as (contig, offset, mismatches) positions on it, so the
cross-block overlaps the block-local codecs strand as literals collapse to a cheap
back-reference. The assembly runs the greedy fold over a fixed set of windows in
parallel (a fixed count, never derived from --threads, so the reference is
byte-identical regardless of thread count); the reference is then overlap-merged
— contigs whose suffix overlaps another's prefix are chained into fewer, longer
super-contigs, reclaiming the cross-window deduplication the split gave up — and
stored once. It is coded with the same clean-room order-k fqxv-seq model as
the reads, split into a fixed number of contig blocks compressed in parallel.
There is no external/C compressor in this path — the earlier xz (liblzma)
reference coder was removed; the whole codec stack is pure-Rust clean-room. This is
the adaptive rescue path (default under --order any; --no-rescue turns it
off). It is adopted only when the reference frame plus the v4 blocks beat the
block-local total, so it can only ever shrink the archive. See Container Format →
Reordered archives for the on-disk frame layout
and the reference-frame method byte.
Measured on the sequence stream:
| dataset | fqxv-seq order-11 |
reorder + delta + ctx-literals | gain |
|---|---|---|---|
| E. coli, ~119× coverage | 1.344 bits/base | 0.737 | −45% |
| RNA-seq, shallow | 1.247 bits/base | 0.949 | −24% |
These are idealized numbers: fixed read length, sequence stream only, order not preserved.
End to end, and the real-world caveats¶
fqxv compress --order any (or --max) turns on read reordering. On single-end
input the reads emerge clustered (order not preserved). On grouped (paired /
single-cell) input a permutation is stored so the mate interleaving is
reconstructed on decompress and --split — grouped reorder is therefore always
order-preserving. For single-end input, whether the original order is kept is
picked adaptively (a stored permutation wins for counter-style names that
delta-code to almost nothing in original order); the Advanced --keep-order flag
forces it on, and --order shuffle opts into discarding order entirely by
regenerating purely positional names from a template (reorder-lossy — reads are
renumbered, sequence and quality preserved exactly). All modes round-trip
exactly (or exactly as a set for single-end any). On a full, real deep
dataset (E. coli, 2.19 M variable-length reads) the whole-archive gain is modest:
| mode | size | vs plain |
|---|---|---|
plain (--order preserve) |
255.8 MB | — |
--order any (order not kept) |
247.4 MB | −3.3% |
| reorder + stored permutation | 253.9 MB | −0.7% |
Three things erode the idealized gain on real data:
- Variable read lengths.
MATCH/DELTArequire equal length, so trimmed reads (249/250/251 bp) mostly fall toLITERAL. The 45% was measured on a fixed-251bp subset. - Reordering scrambles read names, which destroys the tokenizer's match/delta structure — a cost that partly offsets the sequence gain.
- The permutation (order-preserving / grouped reorder) is expensive at scale.
So the honest state: the mechanism is real and validated, but realizing it on everyday data needs variable-length-aware differential coding (align, or allow length-changing deltas) and a way to keep names in original order while the sequence is reordered. That's the remaining work toward the SPRING/PgRC tier.