Skip to content

base-depth aborts with "CSV error: found record with N fields" when --keep-zeros and --ref-fasta are combined #109

Description

@CristEin

Environment

  • perbase 1.4.0 (bioconda, build h15397dd_0) — the latest available on bioconda, so upgrading is not an option
  • Linux x86_64
  • Input is BAM (not CRAM), coordinate-sorted with a .bai index

Summary

base-depth fails with a CSV error when --ref-fasta and --keep-zeros are used together, and aborts partway through writing the output file. Either flag alone works fine.

Steps to reproduce

perbase base-depth --threads 16 --keep-zeros \
    --bed-file probeCov.hg38.bed \
    --ref-fasta hg38.fa \
    WU-cfDNA.dna.sscs.sorted.bam --output out2.tsv

probeCov.hg38.bed is a targeted panel BED. The BAM is indexed. Any BED interval that contains both covered and uncovered positions is enough to trigger it — for example a 100 bp interval where the first 48 bp have no reads:

printf 'chr1\t1053200\t1053300\n' > span.bed

# fails
perbase base-depth --bed-file span.bed --ref-fasta hg38.fa --keep-zeros in.bam > out.tsv   # exit 1

Both flags are individually fine:

perbase base-depth --bed-file span.bed --ref-fasta hg38.fa               in.bam > out.tsv  # exit 0, 21 fields/row
perbase base-depth --bed-file span.bed --keep-zeros                      in.bam > out.tsv  # exit 0, 20 fields/row
perbase base-depth --bed-file span.bed --ref-fasta hg38.fa --keep-zeros  in.bam > out.tsv  # exit 1

Observed

[2026-09-21T08:41:34Z ERROR perbase] CSV error: found record with 21 fields, but the previous record has 20 fields

followed by a secondary panic:

thread '<unnamed>' panicked at /opt/conda/conda-bld/perbase_.../src/lib/par_granges.rs:265:49:
Sent a serializable to writer: "SendError(..)"

Process exits non-zero.

Analysis

With --ref-fasta, the output table gains a REF_BASE column (field 3):

REF  POS  REF_BASE  DEPTH  A  C  G  T  N  R  Y  S  W  K  M  INS  DEL  REF_SKIP  FAIL  COUNT_OF_MATE_RESOLUTIONS  NEAR_MAX_DEPTH

But that column is only populated for positions that produced a pileup record. Positions emitted purely because of --keep-zeros have no reference base, and serialize without that field at all — dropping from 21 fields to 20:

chr1  1053201          0  0  0  0  0 ...   false    <- zero depth, 20 fields
chr1  1053248  C   9  0  9  0  0  0  0 ...   false    <- covered,    21 fields

The CSV writer fixes the field count from the first record and rejects any record that differs (UnequalLengths), so as soon as a file contains both kinds of rows it aborts. The panic above is a knock-on effect: the writer has already exited, and the
worker thread panics when its channel send fails.

This also explains why the failure looks sample-dependent: a BAM whose BED regions are uniformly covered (all 21-field rows) or uniformly uncovered (all 20-field rows) completes normally. Only samples with mixed coverage fail — which, for a
targeted panel, is most of them.

Impact

On a 165-BAM panel run, the majority of samples aborted. More concerning, the abort leaves a truncated output file behind that looks complete. One sample's output contained 4008 correct 21-field rows followed by a single 20-field row with no
trailing newline — the file was written right up to the point of failure and then abandoned, with no marker in the file itself. Any pipeline that globs these .tsv files will silently consume partial results.

Expected

Any of the following would resolve it:

  1. Always emit REF_BASE, writing an empty value (or N) for zero-depth positions, so the field count is constant; or
  2. Never emit REF_BASE when --keep-zeros is in effect, so the shape is constant; or
  3. Configure the CSV writer to tolerate variable field counts / not infer the schema from the first record.

Also happy to know whether --keep-zeros + --ref-fasta is intended to be a supported combination at all.

Workaround

Run base-depth with --keep-zeros but without --ref-fasta (constant 20 fields, no abort), and populate REF_BASE afterwards from the reference FASTA. In my testing over 4008 positions, the column perbase emits is exactly the reference base
at that position, so the column can be reconstructed losslessly — the output is byte-identical apart from that one column.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions