fqxv compress¶
Compress one or more FASTQ files into a single .fqxv archive.

Usage¶
fqxv compress <INPUTS>... [-o <OUTPUT>] [OPTIONS]
Give one input for single-end, two for paired-end, or three/four for single-cell
(R1/R2/I1[/I2]). Each input's slot label (the R1/I1/2 token before its
extension) is recorded in the archive when every input has a distinct one, so
decompress --split can restore the original per-slot names rather than
renumbering positionally. Multiple inputs are interleaved per spot into one archive and
can be split back out with decompress --split. Input order is
preserved for the split.
Arguments¶
| Argument | Description |
|---|---|
<INPUTS>... |
Input FASTQ file(s), plain or gzipped (gzip auto-detected). |
Options¶
| Option | Description |
|---|---|
-o, --output <PATH> |
Output .fqxv path. Defaults to the first input's name with the FASTQ/gzip extension replaced by .fqxv (reads.fastq.gz → reads.fqxv), alongside the input. Required when reading from stdin (-). |
-f, --force |
Overwrite the output archive if it already exists. By default compress refuses to clobber an existing file and errors before doing any work. |
--verify |
After writing the archive, re-read and fully decode it to confirm it round-trips before reporting success — recommended before deleting the source FASTQ. See Verifying on write. |
--max |
Maximum-compression preset: deepest sequence context plus read reordering where it helps (applied to short reads, auto-skipped for long reads). Overrides --level/--order. |
--estimate [<FORMAT>] |
Predict the archive size and compression ratio from a sample of the input, then exit without writing anything. Bare --estimate prints the human report; --estimate tsv prints a two-line table (file, input_bytes, est_fqxv_bytes, ratio). Conflicts with --verify. See Estimating compression. |
--threads <N> |
Worker threads (0 = all cores). |
Advanced options¶
| Option | Description |
|---|---|
-l, --level <N> |
Effort 1–9; higher raises the sequence context order (up to order 11, reached at level 5) and the block size, and enables a hashed high-order tier at level 8+. Default: 5. |
--block-reads <N> |
Reads per row group, overriding the size --level would pick. Decouples random-access granularity from effort: smaller groups give finer remote/parallel access and more parallelism at some ratio cost; larger groups the reverse. On long-read input, where a byte budget normally cuts blocks, an explicit N makes the read count alone govern the cut. Sequence order still follows --level. Ignored by the reorder path (--order any/--max). See Row-group sizing. |
--order <MODE> |
Read-order guarantee: preserve (default, restores original order), any (allows reordering for a better ratio; single-end order may change), or shuffle (like any, but discards order and regenerates purely positional names — reorder-lossy, single-end only; grouped input and --keep-order fall back to any). |
--interleaved <N> |
Interleaving of a single input, in members per spot (1 = single-end, 2 = paired as from sracha get -Z). Auto-detected from read names by default. Ignored with multiple inputs. |
--keep-order |
With --order any, force original read order to be restored (store a permutation, code names/quality in original order). Chosen automatically when it makes the archive smaller. |
--no-rescue |
With --order any, disable the adaptive assembly codecs (block-local literal-rescue and the whole-file global reference) and use the faster single-contig sequence codec only. |
--quality-bin <MODE> |
lossless (default), or a lossy table: bin8, bin4, bin2 (Illumina), ont, hifi (long read). See Lossy quality binning. |
--platform <NAME> |
Sequencing platform to record: illumina, nanopore, pacbio, mgi. Auto-detected from read names by default; pass to override. |
Examples¶
# single-end, gzipped input (-o defaults to reads.fqxv)
fqxv compress reads.fastq.gz
# paired-end at higher effort
fqxv compress sample_R1.fq.gz sample_R2.fq.gz -o sample.fqxv --level 7 --threads 16
# 10x single-cell (R1 + R2 + I1 + I2)
fqxv compress R1.fq R2.fq I1.fq I2.fq -o sample.fqxv
# lossy quality (Illumina 8-level binning)
fqxv compress reads.fastq -o reads.fqxv --quality-bin bin8
# lossy quality on long reads (match the table to the platform)
fqxv compress ont_reads.fastq -o ont_reads.fqxv --quality-bin ont
fqxv compress hifi_reads.fastq -o hifi_reads.fqxv --quality-bin hifi
# finer row groups for remote/parallel access (smaller range fetches)
fqxv compress reads.fastq.gz -o reads.fqxv --block-reads 65536
# maximum compression (deepest context + read reordering where it helps)
fqxv compress reads.fastq.gz --max
# verify the archive round-trips before trusting (or deleting) the source
fqxv compress reads.fastq.gz --verify
# estimate the ratio and archive size from a sample, writing nothing
fqxv compress reads.fastq.gz --estimate
# ...the same estimate as a two-line table for scripts
fqxv compress reads.fastq.gz --estimate tsv
Verifying on write¶
--verify closes the window between writing an archive and trusting it. After the
archive is written, it is reopened and fully decoded — exercising every block
CRC-32C and content digest — and the decoded read count is checked against what was
written, all before compress reports success:
fqxv compress reads.fastq.gz --verify
This catches a codec or in-flight memory error that produced a CRC-valid but wrong archive, which the archive's own on-disk checksums cannot detect on their own. It is the check to run before deleting the source FASTQ.
- On any failure — a decode error or a read-count mismatch — the archive is left in place for inspection and the command exits non-zero. A bad archive is never reported as done.
- It adds a full decode pass, so expect roughly double the wall time (the
--max/ reorder path is the heaviest). - It validates the bytes the encoder emitted, not long-term on-disk durability;
for the latter, re-run
fqxv verifyagainst the stored file later.
Estimating compression¶
--estimate tells you how well an input will compress before committing to a
full run. It takes a bounded sample of the leading reads (up to 300,000 reads, or
one block's worth of bases — whichever binds first, which on long reads is the
bases) and predicts the archive without coding it: the real codecs are
entropy coders, so a histogram pass measures directly what they converge to —
static order-k sequence entropy (blended with a k-mer duplication sketch on the
long-read path), order-1 quality entropy, and the real tokenizer for names, each
scaled by a small per-platform calibration factor. It then projects the whole-file
archive at the chosen --level/--quality-bin and prints the result — no archive
is written:
reads.fastq.gz (59.99 MB) → estimated fqxv ~37.58 MB (37% smaller, ~1.60x)
Estimated from a 300,000-read sample (102.26 MB uncompressed FASTQ):
stream compressed share rate
names 11.27 KB 0.0% 0.31 bits/read
sequence 8.92 MB 31.6% 1.663 bits/base
quality 19.26 MB 68.3% 3.590 bits/base
vs uncompressed FASTQ (~136.32 MB): ~3.63x
The top line leads with the outcome: input size(s) → estimated archive size, as a percent reduction and ratio against the input on disk. The per-stream table below shows where the bytes go (names / sequence / quality) for the sample, and the final line gives the ratio against the whole file's uncompressed FASTQ bytes. Multiple inputs are listed individually under a combined headline; a streaming stdin input has no on-disk size, so it reports the sample's own reduction and omits the whole-file projection (pass a file to get one).
For scripting, --estimate tsv prints the same projection as a header line plus
one data row:
file input_bytes est_fqxv_bytes ratio
reads.fastq.gz 62898962 39405273 1.5962
Accuracy. Because blocks are coded independently and the models are per-block
stationary, the sample's ratio is a faithful proxy for the whole file — in
practice within ~1–2% of a real run (every projected figure is marked ~). The
projection scales the sample by the fraction of on-disk bytes it consumed, so it
works for plain and gzipped inputs alike.
Reordering is a lower bound. --order any / --max reordering is not
modeled — its cross-read redundancy grows with read count, so a small sample
can't capture it. With those flags the estimate is a conservative lower bound
(the real archive comes out that size or smaller), and the report says so.
Row-group sizing¶
The archive is a run of independently-coded row groups (blocks); the row
group is the unit of parallelism, of coarse random access, and of remote column
projection. By default its size comes from --level — higher effort uses larger
groups, which train the sequence model on more reads for a better ratio. Pass
--block-reads <N> to set it directly and decouple granularity from effort.
The trade-off is real. Smaller groups mean finer random access and more
parallelism, but a worse ratio (the order-k sequence model has fewer reads to
train on per group) and a slightly larger footer index. Larger groups compress
better but make the smallest independently-fetchable unit coarser. When archiving
to object storage where clients issue small Range reads — say, fetching just
the read names, or just one row group — a smaller --block-reads makes those
fetches cheaper; for a write-once/read-sequentially archive the --level default
is the right call.
Long reads are sized in bytes, not reads. A long-read file holds so few
reads that the read count never binds; blocks are cut by a raw-sequence byte
budget instead. For Nanopore input that budget defaults to 64 MiB (other
platforms use the 256 MiB cap): on a 576 MB MinION file that is 5 blocks
instead of 2, which took full decode from flat-at-any-thread-count to 3.2×
faster at 8 threads, and --fasta from 4.6 s to 1.9 s, for +1.08% archive
size. --max keeps maximal 256 MiB blocks — its contract is the smallest
archive, and the last ~1% lives in the sequence codec's per-block coverage.
To trade further in either direction pass --block-reads: with it the read
count alone governs the cut (e.g. --block-reads 2000 on ~14 kb reads is
~28 MB groups — more parallelism at ~+3% size; a large N restores maximal
blocks at any level).
The per-group and per-stream byte offsets recorded in the footer are what make
this projection possible without reading whole blocks — see
Container format → Column projection.
The reorder path (--order any/--max) clusters globally and does not use this
sizing, so --block-reads is ignored there.
Lossy quality binning¶
Quality is lossless by default. --quality-bin maps each quality byte through a
fixed table before coding — an explicit, opt-in lossy transform that shrinks the
quality stream (usually the largest part of the archive). Sequence and read names
are never touched, and a binned read still aligns exactly where the original did.
| Mode | Levels | Calibrated for |
|---|---|---|
lossless |
all | default; nothing is discarded |
bin8 |
8 | Illumina standard binning |
bin4 |
4 | Illumina documented 4-level (NovaSeq X / RTA4) |
bin2 |
2 | custom, most aggressive |
ont |
4 | Oxford Nanopore (CoLoRd ONT cutpoints) |
hifi |
5 | PacBio HiFi (CoLoRd HiFi cutpoints; Q93 kept exact) |
Match the table to the platform. The tables are not interchangeable: the
Illumina bins are absolute-Phred cutpoints that collapse HiFi's narrow high-Q
band into a single level, and ont applied to HiFi data folds the Q93
max-quality symbol into the 26+ bin, destroying its application meaning (measured
mean |Δ| 42.84, 99.4% of bases changed). hifi keeps Q93 as its own level. On
ONT data the ont and hifi tables are byte-identical, since ONT never reaches
Q93.
On the ecoli_ont benchmark, --quality-bin ont cuts the quality stream from
165.2 MB to 47.0 MB (3.5×) — the whole archive from 2.96× to 7.05× — at mean
|Δ| 3.35. Cutpoints should ultimately be
judged by downstream fidelity, not raw ratio — see
Long-read support.
Notes¶
- Losslessness: read name + description, sequence, and quality are preserved
exactly. The
+separator line is normalized to a bare+. - Grouping: interleaving mates shrinks the archive — near-identical mate names collapse to matches, and a spot's related reads sit together for the sequence model.