Skip to content

Multiome passthrough

--passthrough <FILE> adds a third FASTQ input to paired-end trimming that is not trimmed — instead, its records are carried through 1:1 to a parallel output file, kept in lockstep with R1/R2 such that any pair dropped by quality, adapter, length, or N-content filtering also drops the matching record from the passthrough output.

The headline use case is 10X Multiome ATAC-seq, where the modified ATAC adapter is opaque to Cellranger Arc's auto-detection. Trim R1/R2 against your own -a/-a2 while the cell-barcode read (I1 / I2 / R3 — whichever your library design names it) stays aligned to the surviving fragments. One invocation, no awk pipelines on the side.

Terminal window
trim_galore --paired \
--passthrough sample_I1.fastq.gz \
sample_R1.fastq.gz \
sample_R2.fastq.gz

Output files:

FileDescription
sample_R1_val_1.fq.gzTrimmed R1 (standard --paired output)
sample_R2_val_2.fq.gzTrimmed R2 (standard --paired output)
sample_I1_passthrough.fq.gzPassthrough output — same record count and order as R1/R2 outputs
sample_R{1,2}.fastq.gz_trimming_report.{txt,json}Trimming reports — R2's text report contains a new === Passthrough file === block, both JSON reports gain a "passthrough" object

With --basename foo set, all three outputs use that basename uniformly: foo_val_1.fq.gz, foo_val_2.fq.gz, foo_passthrough.fq.gz.

All of them, the passthrough output included, are written to --output_dir if given and otherwise to Read 1's directory — so a carrier file kept apart from the read pair still produces its output alongside them. See Output files.

Every record, Trim Galore extracts an ID prefix from R1, R2, and the passthrough read and compares them three-way. The prefix is everything after the leading @, before the first whitespace, with a trailing /1 / /2 / /3 stripped. That covers both modern Illumina (@HEADER 1:N:0:CGATCG → prefix HEADER) and legacy SRA/ENA (@read/1 @read/2 @read/3 → prefix read).

If any pair-wise comparison disagrees, the run fails loudly with a row-numbered error:

Read ID mismatch at record 4137: R1='SRR123.4137', R2='SRR123.4137',
passthrough='SRR123.4138'. Files are out of sync.

Mid-stream truncation in any of the three streams is caught the same way:

--passthrough file is truncated — R1/R2 have more reads than passthrough
at record 5001

The opposite case (passthrough longer than R1/R2) fails with a matching message. Files of unequal length never produce silently misaligned barcodes — that was the load-bearing concern that drove the per-record sync check.

--passthrough is rejected at startup in combination with:

  • --retain_unpaired — passthrough requires strict pair semantics in v1; a half-rescued mate has no clean interpretation for the index file.
  • --clumpify — the clumpy reordering shuffles records by minimizer, which breaks the lockstep contract.
  • Multi-pair input — exactly one R1/R2 pair per invocation; for multiple samples, run Trim Galore once per pair.
  • Specialty modes--clock, --implicon, --hardtrim5/3, --demux each own their own input arity and output naming.

--passthrough is also rejected without --paired. Each rejection comes with a precise error message.

When --fastqc is set, FastQC runs on all three outputs, including the passthrough. The passthrough report will look noisier than R1/R2: cell-barcode reads are 16–28 bp of uniformly-structured sequence by design (positional base bias from the barcode whitelist + natural duplication from multi-fragment cells), so FastQC's Per base sequence content and Sequence Duplication Levels modules typically FAIL even on perfectly good data. That's the data type, not a defect. The remaining modules (per-base quality, adapter content, N-content) carry real signal.

  • Untouched bytes. The passthrough record's sequence and quality are written verbatim. The third line (+... description) and line endings are canonicalised to bare + and \n — the standard canonicalisation R1/R2 already go through. The id / seq / qual fields are byte-identical to the input.
  • Single-pass, in-memory lockstep. The third stream is read in parallel with R1/R2 in the same reader thread (parallel path) or the same loop iteration (serial path); the writes happen in the same flush iteration as R1/R2 within each batch. No temp files.
  • Mid-stream errors leave no output on disk. If a sync error fires at record 5,000 of 10,000, none of the three outputs is published — re-run the input after fixing the source. Trim Galore prints a clear error and exits with non-zero status.
  • Output gzip is uniform across all three outputs and follows R1's input compression (plain R1 in → plain _passthrough.fq out; gzipped R1 in → gzipped _passthrough.fq.gz out), regardless of the passthrough input's own extension.

Generate a synthetic I1 file from one of the bundled BS-seq fixtures and trim everything in one pass:

Terminal window
# Mint a synthetic 16 bp barcode read aligned to BS-seq_10K_R1's headers.
gunzip -c test_files/BS-seq_10K_R1.fastq.gz | awk 'NR % 4 == 1 {
print $1
print "AAAACCCCGGGGTTTT"
print "+"
print "IIIIIIIIIIIIIIII"
}' | gzip > /tmp/BS-seq_10K_I1.fastq.gz
trim_galore --paired \
--passthrough /tmp/BS-seq_10K_I1.fastq.gz \
--output_dir /tmp \
test_files/BS-seq_10K_R1.fastq.gz \
test_files/BS-seq_10K_R2.fastq.gz
# All three outputs should report the same record count.
for f in /tmp/BS-seq_10K_R1_val_1.fq.gz \
/tmp/BS-seq_10K_R2_val_2.fq.gz \
/tmp/BS-seq_10K_I1_passthrough.fq.gz; do
printf " %-50s %s records\n" "$(basename $f)" \
"$(gunzip -c "$f" | awk 'NR%4==1' | wc -l | tr -d ' ')"
done
# The new block in the R2 text report:
grep -A6 "Passthrough file" /tmp/BS-seq_10K_R2.fastq.gz_trimming_report.txt

Out of 10,000 input pairs you should see 9,996 surviving (4 dropped at default settings), and the same count in all three outputs.