Skip to content

Commit af3956c

Browse files
committed
.
1 parent b7df8b7 commit af3956c

1 file changed

Lines changed: 90 additions & 2 deletions

File tree

virasign/virasign.py

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4161,8 +4161,10 @@ def _cigar_aligned_bases(cigar: str) -> tuple:
41614161

41624162
# 1. Extract reference FASTA
41634163
ref_fasta = acc_dir / f"{accession}.fasta"
4164-
if not extract_fasta_record(selected_refs_fasta, description, ref_fasta):
4165-
logger.warning(f" Could not extract reference FASTA for {accession}")
4164+
if not extract_fasta_record_by_accession(selected_refs_fasta, accession, ref_fasta):
4165+
# Fallback: older logic (exact header match), in case accession parsing fails for a rare header
4166+
if not extract_fasta_record(selected_refs_fasta, description, ref_fasta):
4167+
logger.warning(f" Could not extract reference FASTA for {accession}")
41664168

41674169
# 2/3. Finalize streamed BAM for this accession (+ index)
41684170
if accession in bam_pipes:
@@ -4679,6 +4681,41 @@ def extract_fasta_record(database_fasta: Path, target_header: str, out_fasta: Pa
46794681
out.write(line)
46804682
return found
46814683

4684+
4685+
def extract_fasta_record_by_accession(database_fasta: Path, accession: str, out_fasta: Path) -> bool:
4686+
"""
4687+
Stream-scan a FASTA and write the record whose header contains `accession` (as parsed by
4688+
extract_accession_from_header) to `out_fasta`.
4689+
Returns True if found.
4690+
"""
4691+
accession = (accession or "").strip()
4692+
if not accession:
4693+
return False
4694+
4695+
database_fasta = Path(database_fasta)
4696+
if str(database_fasta).endswith(".gz"):
4697+
import gzip
4698+
fh = gzip.open(database_fasta, "rt")
4699+
else:
4700+
fh = open(database_fasta, "r")
4701+
4702+
found = False
4703+
write = False
4704+
with fh:
4705+
with open(out_fasta, "w") as out:
4706+
for line in fh:
4707+
if line.startswith(">"):
4708+
header = line[1:].strip()
4709+
header_acc = extract_accession_from_header(header)
4710+
write = (header_acc == accession)
4711+
if write:
4712+
out.write(line)
4713+
found = True
4714+
else:
4715+
if write:
4716+
out.write(line)
4717+
return found
4718+
46824719
def _process_sample_task(task_args):
46834720
"""
46844721
Wrapper function for parallel processing of samples.
@@ -5078,6 +5115,57 @@ def save_results(sample_name, best_ref, best_stats, all_stats, filtered_stats, c
50785115
logger.warning("Could not extract final selected references. Skipping re-mapping.")
50795116
else:
50805117
logger.warning("No curated references meeting thresholds after deduplication. Skipping re-mapping.")
5118+
# Fallback: still generate per-reference outputs for the best reference (if available).
5119+
# This is especially useful for custom databases where coverage thresholds may be too strict
5120+
# but users still want BAM/FASTQ/FASTA for the best hit.
5121+
try:
5122+
if best_ref and isinstance(best_ref, dict) and best_ref.get("accession") and best_ref.get("description"):
5123+
fallback_accession = best_ref["accession"]
5124+
fallback_desc = best_ref["description"]
5125+
logger.info(f"Fallback output: remapping to best reference only: {fallback_accession}")
5126+
5127+
selected_refs_fasta = sample_dir / f"{sample_name}_selected_references.fasta"
5128+
# Extract best reference sequence from the original database FASTA.
5129+
best_ref_fasta = sample_dir / f"{fallback_accession}.fasta"
5130+
if not extract_fasta_record_by_accession(database_fasta_path, fallback_accession, best_ref_fasta):
5131+
# Last resort: try matching exact header if accession parsing fails
5132+
extract_fasta_record(database_fasta_path, fallback_desc, best_ref_fasta)
5133+
if best_ref_fasta.exists() and best_ref_fasta.stat().st_size > 0:
5134+
merge_fasta_files([best_ref_fasta], selected_refs_fasta)
5135+
try:
5136+
best_ref_fasta.unlink()
5137+
except Exception:
5138+
pass
5139+
5140+
# Build minimap2 index for the small selected reference set and remap reads.
5141+
ensure_minimap2_index(selected_refs_fasta)
5142+
remap_sam = sample_dir / f"{sample_name}_remapped.sam"
5143+
cmd = [
5144+
"minimap2",
5145+
"-a",
5146+
"-t",
5147+
str(max(1, int(threads or 1))),
5148+
"-I",
5149+
str(minimap2_I),
5150+
str(selected_refs_fasta),
5151+
str(sample_fastq),
5152+
]
5153+
with open(remap_sam, "w") as out_sam:
5154+
subprocess.run(cmd, check=True, stdout=out_sam, stderr=subprocess.PIPE, text=True)
5155+
5156+
create_per_reference_outputs(
5157+
sample_name,
5158+
[{"accession": fallback_accession, "description": fallback_desc}],
5159+
selected_refs_fasta,
5160+
remap_sam,
5161+
sample_fastq,
5162+
sample_dir,
5163+
threads=threads,
5164+
gzip_fastq=gzip_fastq,
5165+
min_identity=min_identity,
5166+
)
5167+
except Exception as e:
5168+
logger.warning(f"Fallback output failed: {e}")
50815169
# Clean up initial SAM file even if no curated references
50825170
initial_sam = sample_dir / f"{sample_name}.sam"
50835171
if initial_sam.exists():

0 commit comments

Comments
 (0)