Skip to content

Commit 598b577

Browse files
committed
.
1 parent bef795b commit 598b577

1 file changed

Lines changed: 56 additions & 7 deletions

File tree

virasign/virasign.py

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4414,7 +4414,11 @@ def cigar_aligned_bases(cigar: str) -> tuple:
44144414
total += n
44154415
return total, aligned_m
44164416

4417-
sam_header_lines = []
4417+
# Store SAM header lines, but keep @SQ lines per-contig so we can write
4418+
# per-accession BAMs with a *single* @SQ in the header (important for
4419+
# downstream tools using `mpileup -aa` which otherwise may traverse all contigs).
4420+
sam_header_common = [] # all header lines except @SQ
4421+
sam_sq_by_accession = {} # canonical accession -> list[@SQ line]
44184422
bam_pipes = {} # accession -> dict(view_p, sort_p, stdin, ref_bam, ref_bai_path)
44194423
samtools_threads = max(1, min(int(threads or 1), 16))
44204424
read_ids_by_reference = {} # accession -> set(read_id)
@@ -4522,8 +4526,22 @@ def _start_bam_pipe(accession: str):
45224526
view_p.stdout.close()
45234527

45244528
assert view_p.stdin is not None
4525-
for h in sam_header_lines:
4529+
# Write a single-contig header for this accession.
4530+
# Keep common header lines (@HD, @PG, @RG, etc.) and include only the matching @SQ.
4531+
sq_lines = sam_sq_by_accession.get(accession) or []
4532+
if not sq_lines:
4533+
# Fallback: if we can't find a matching @SQ (shouldn't happen), write all common lines
4534+
# without any @SQ. samtools may fail later; log for debugging.
4535+
logger.warning(f"Could not find @SQ header line for accession {accession}; writing header without @SQ")
4536+
# Keep @HD first (if present), then @SQ, then the remaining common headers.
4537+
for h in sam_header_common:
4538+
if h.startswith("@HD"):
4539+
view_p.stdin.write(h)
4540+
for h in sq_lines:
45264541
view_p.stdin.write(h)
4542+
for h in sam_header_common:
4543+
if not h.startswith("@HD"):
4544+
view_p.stdin.write(h)
45274545

45284546
bam_pipes[accession] = {"view_p": view_p, "sort_p": sort_p, "stdin": view_p.stdin, "ref_bam": ref_bam}
45294547

@@ -4538,7 +4556,6 @@ def _start_bam_pipe(accession: str):
45384556
if not line:
45394557
continue
45404558
if line.startswith("@SQ"):
4541-
sam_header_lines.append(line)
45424559
parts = line.strip().split("\t")
45434560
header = None
45444561
length = None
@@ -4549,9 +4566,14 @@ def _start_bam_pipe(accession: str):
45494566
length = int(part[3:])
45504567
if header and length:
45514568
ref_lengths[header] = length
4569+
# Index @SQ lines by canonical accession (so we can write per-accession BAMs with a single @SQ).
4570+
acc_raw = extract_accession_from_header(header) or ""
4571+
if _sam_accession_in_curated(curated_acc_expanded, acc_raw):
4572+
acc_canon = _canonical_curated_accession(acc_to_canon, acc_raw)
4573+
sam_sq_by_accession.setdefault(acc_canon, []).append(line)
45524574
continue
45534575
if line.startswith("@"):
4554-
sam_header_lines.append(line)
4576+
sam_header_common.append(line)
45554577
continue
45564578
if not line.strip():
45574579
continue
@@ -4961,7 +4983,11 @@ def create_per_reference_outputs(sample_name: str, curated_descriptions: list, s
49614983
read_ids_by_reference = {} # accession -> set of read IDs
49624984
curated_accessions = [stat.get("accession", "") for stat in curated_descriptions if stat.get("accession", "")]
49634985
curated_acc_expanded, acc_to_canon = _curated_accession_expansion_and_canon(curated_accessions)
4964-
sam_header_lines = []
4986+
# Keep common header lines and store @SQ per contig so per-accession BAMs
4987+
# can be written with a single @SQ (prevents downstream tools from traversing
4988+
# unrelated contigs when using `mpileup -aa`).
4989+
sam_header_common = [] # all header lines except @SQ
4990+
sam_sq_by_accession = {} # canonical accession -> list[@SQ line]
49654991
bam_pipes = {} # accession -> dict(view_p, sort_p, stdin, ref_bam)
49664992
samtools_threads = max(1, min(int(threads or 1), 16))
49674993

@@ -4988,8 +5014,18 @@ def _start_bam_pipe(accession: str):
49885014
view_p.stdout.close()
49895015

49905016
assert view_p.stdin is not None
4991-
for h in sam_header_lines:
5017+
sq_lines = sam_sq_by_accession.get(accession) or []
5018+
if not sq_lines:
5019+
logger.warning(f"Could not find @SQ header line for accession {accession}; writing header without @SQ")
5020+
# @HD first, then @SQ, then remaining common header lines
5021+
for h in sam_header_common:
5022+
if h.startswith("@HD"):
5023+
view_p.stdin.write(h)
5024+
for h in sq_lines:
49925025
view_p.stdin.write(h)
5026+
for h in sam_header_common:
5027+
if not h.startswith("@HD"):
5028+
view_p.stdin.write(h)
49935029

49945030
bam_pipes[accession] = {
49955031
"view_p": view_p,
@@ -5119,7 +5155,20 @@ def _non_overlapping_reads_and_bases_from_bam(bam_path: Path) -> tuple:
51195155
if not line:
51205156
continue
51215157
if line.startswith("@"):
5122-
sam_header_lines.append(line)
5158+
if line.startswith("@SQ"):
5159+
# Index @SQ by canonical accession if it's a curated hit
5160+
header = None
5161+
for part in line.strip().split("\t"):
5162+
if part.startswith("SN:"):
5163+
header = part[3:]
5164+
break
5165+
if header:
5166+
acc_raw = extract_accession_from_header(header) or ""
5167+
if _sam_accession_in_curated(curated_acc_expanded, acc_raw):
5168+
acc_canon = _canonical_curated_accession(acc_to_canon, acc_raw)
5169+
sam_sq_by_accession.setdefault(acc_canon, []).append(line)
5170+
else:
5171+
sam_header_common.append(line)
51235172
continue
51245173
if not line.strip():
51255174
continue

0 commit comments

Comments
 (0)