@@ -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+
46824719def _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