77
88import pandas as pd
99from Bio import SeqIO
10- from Bio .SeqRecord import SeqRecord
10+ from Bio .SeqRecord import SeqRecord , SeqFeature
1111from Bio .Seq import Seq
1212
1313
1414NTS = ("A" , "C" , "G" , "T" )
15+ MASK_CHR = "N"
1516
1617
1718def read_monofasta (path : str ) -> SeqRecord :
1819 return SeqIO .read (path , format = "fasta" )
1920
2021
22+ def read_masked_positions (bed_path : str ) -> set :
23+ masked = set ()
24+ with open (bed_path ) as fh :
25+ for line in fh :
26+ if line .startswith ("#" ) or not line .strip ():
27+ continue
28+ parts = line .strip ().split ("\t " )
29+ start , end = int (parts [1 ]), int (parts [2 ])
30+ masked .update (range (start , end ))
31+ return masked
32+
33+
34+ def mask_feature (feature : SeqFeature , record : SeqRecord , masked_positions : set ) -> SeqRecord :
35+ extracted = feature .extract (record )
36+ seq_chars = list (str (extracted .seq ).upper ())
37+ # Handle reverse strand and compound locations
38+ genomic_positions = []
39+ for part in feature .location .parts :
40+ positions = list (range (int (part .start ), int (part .end )))
41+ if part .strand == - 1 :
42+ positions .reverse ()
43+ genomic_positions .extend (positions )
44+ # Mask sites on sequence
45+ for i , pos in enumerate (genomic_positions ):
46+ if seq_chars [i ] not in NTS or pos in masked_positions :
47+ seq_chars [i ] = MASK_CHR
48+ extracted .seq = Seq ("" .join (seq_chars ))
49+ return extracted
50+
51+
2152def split_into_codons (seq : Seq ) -> list :
22- """Split the complete CDS feature in to a list of codons"""
53+ """Split the complete CDS feature in to a list of codons (if ACGT only) """
2354 return [
2455 seq [i :i + 3 ] for i in range (0 , len (seq )- 2 , 3 ) if all (char in NTS for char in seq [i :i + 3 ])
2556 ]
@@ -79,6 +110,9 @@ def main():
79110 with open (snakemake .input .genetic_code ) as f :
80111 genetic_code = json .load (f )
81112
113+ logging .info ("Reading masked sites BED" )
114+ masked = read_masked_positions (snakemake .input .masked )
115+
82116 logging .info ("Reading GenBank file" )
83117 gb = SeqIO .read (snakemake .input .gb , format = "gb" )
84118
@@ -95,7 +129,7 @@ def main():
95129 logging .warning (f"Identifier '{ identifier } ' is already among coding records and will not be replaced by feature at { feature .location } " )
96130 else :
97131 logging .debug (f"Adding feature" )
98- coding_records [identifier ] = feature . extract ( record )
132+ coding_records [identifier ] = mask_feature ( feature , record , masked )
99133
100134 logging .info (f"Splitting { len (coding_records )} records into codons" )
101135 codons = get_feature_codons (coding_records )
0 commit comments