77from typing import Tuple , List , Dict
88import argparse
99
10- def load_formamide_probabilities (file_path : str ) -> Tuple [List [float ], List [float ]]:
11- """Load formamide probabilities from TSV file and normalize them."""
12- # Read the file with explicit float conversion
13- df = pd .read_csv (file_path , sep = r'\s+' , header = None , names = ['formamide' , 'probability' ])
14-
15- # Convert to float explicitly and handle any potential NaN values
16- df ['probability' ] = pd .to_numeric (df ['probability' ], errors = 'coerce' )
17- df = df .dropna () # Remove any rows with NaN values
18-
19- if df .empty :
20- raise ValueError ("No valid probability values found in the formamide file" )
21-
22- # Convert probabilities to numpy array and normalize
23- probs = df ['probability' ].values .astype (float )
24-
25- # Check for any invalid values
26- if np .any (np .isnan (probs )) or np .any (np .isinf (probs )):
27- raise ValueError ("Invalid probability values found (NaN or Inf)" )
28-
29- # Normalize probabilities
30- total = np .sum (probs )
31- if total <= 0 :
32- raise ValueError ("Sum of probabilities must be positive" )
33-
34- probs = probs / total
35-
36- # Verify the probabilities sum to 1 (within numerical precision)
37- if not np .isclose (np .sum (probs ), 1.0 , rtol = 1e-5 ):
38- raise ValueError ("Probabilities do not sum to 1 after normalization" )
39-
40- return df ['formamide' ].tolist (), probs .tolist ()
41-
4210def clean_sequence (sequence : str ) -> str :
4311 """Clean sequence by removing non-ACGT characters and converting to uppercase."""
4412 sequence = sequence .strip ().upper ()
@@ -52,34 +20,47 @@ def calculate_gc_content(sequence: str) -> int:
5220 gc_count = sequence .count ('G' ) + sequence .count ('C' )
5321 return round ((gc_count / len (sequence )) * 100 )
5422
55- def apply_mutations (sequence : str ,
23+ def apply_mutations_sequence (sequence : str ,
5624 insertion_rate : float = 0.01 ,
5725 deletion_rate : float = 0.01 ,
58- mutation_rate : float = 0.1 ) -> str :
26+ mutation_rate : float = 0.1 ,
27+ values : list = [None ]) -> str :
5928 """Apply random mutations to the sequence."""
6029 nucleotides = ['A' , 'C' , 'G' , 'T' ]
6130 result = []
6231
63- for base in sequence :
64- # Apply mutations based on probabilities
65- if random .random () < mutation_rate :
66- # SNP mutation
67- new_base = random .choice ([n for n in nucleotides if n != base ])
68- result .append (new_base )
69- else :
70- result .append (base )
71-
72- # Insertion after current position
73- if random .random () < insertion_rate :
74- result .append (random .choice (nucleotides ))
75-
76- # Deletion of current position
77- if random .random () < deletion_rate :
78- if result :
79- result .pop ()
32+ while sequence == '' .join (result ):
33+ for base in sequence :
34+ # Apply mutations based on probabilities
35+ if random .random () < mutation_rate :
36+ # SNP mutation
37+ new_base = random .choice ([n for n in nucleotides if n != base ])
38+ result .append (new_base )
39+ else :
40+ result .append (base )
41+
42+ # Insertion after current position
43+ if random .random () < insertion_rate :
44+ result .append (random .choice (nucleotides ))
45+
46+ # Deletion of current position
47+ if random .random () < deletion_rate :
48+ if result :
49+ result .pop ()
8050
8151 return '' .join (result )
8252
53+ def apply_mutations_experiment (sequence ,
54+ values : list = [None ],
55+ insertion_rate : float = 0.01 ,
56+ deletion_rate : float = 0.01 ,
57+ mutation_rate : float = 0.1 ):
58+ """Apply random mutations to the experiment parameters."""
59+ if random .random () < mutation_rate :
60+ return random .choice (values )
61+ else :
62+ return None
63+
8364def process_probe_data (df : pd .DataFrame ) -> List [Dict ]:
8465 """Process the input DataFrame into a list of probe dictionaries."""
8566 probes = []
@@ -103,18 +84,37 @@ def process_probe_data(df: pd.DataFrame) -> List[Dict]:
10384
10485def generate_noisy_probes (input_file : str ,
10586 output_file : str ,
106- formamide_file : str ,
87+ mutation_number : int = 1 ,
88+ obligate_mutations : list = ["Sequence" ],
89+ facultative_mutations : list = ["Formamide [%]" ,"Modified version(s)" ],
10790 insertion_rate : float = 0.01 ,
10891 deletion_rate : float = 0.01 ,
10992 mutation_rate : float = 0.1 ,
11093 iterations : int = 1 ) -> None :
111- """Generate noisy probe data with mutations and formamide variations."""
112-
113- # Load formamide probabilities
114- formamide_values , formamide_probs = load_formamide_probabilities (formamide_file )
115-
94+ """Generate noisy probe data with sequence mutations and experiment parameter variations.
95+ Parameters:
96+ input_file (str): Path to the input CSV file containing probe data.
97+ output_file (str): Path to the output file where noisy probes will be saved.
98+ insertion_rate (float, optional): Probability of inserting a nucleotide at each position. Default is 0.01.
99+ deletion_rate (float, optional): Probability of deleting a nucleotide at each position. Default is 0.01.
100+ mutation_rate (float, optional): Probability of introducing a SNP at each position. Default is 0.1.
101+ iterations (int, optional): Number of noisy probe sets to generate. Default is 1.
102+ """
103+
116104 # Read input probe data
117105 df = pd .read_csv (input_file , header = None )
106+ # Prepare
107+ mutation_list = [* obligate_mutations , * facultative_mutations ]
108+ mutation_functions = {
109+ "Sequence" :apply_mutations_sequence ,
110+ "Formamide [%]" :apply_mutations_experiment ,
111+ "Modified version(s)" :apply_mutations_experiment
112+ }
113+ mutation_values = {
114+ "Sequence" :None ,
115+ "Formamide [%]" :df .loc [df [0 ] == "Formamide [%]" , 1 ].tolist (),
116+ "Modified version(s)" :df .loc [df [0 ] == "Modified version(s)" , 1 ].tolist ()
117+ }
118118
119119 # Process probes
120120 probes = process_probe_data (df )
@@ -129,34 +129,43 @@ def generate_noisy_probes(input_file: str,
129129
130130 # Clean sequence
131131 clean_seq = clean_sequence (probe ['Sequence' ])
132+ probe ['Sequence' ] = clean_seq
132133
133134 # Apply mutations
134- mutated_seq = apply_mutations (clean_seq ,
135- insertion_rate ,
136- deletion_rate ,
137- mutation_rate )
135+ for OM in obligate_mutations :
136+ tmp = mutation_functions [OM ]
137+ probe [OM ] = tmp (sequence = probe [OM ],
138+ insertion_rate = insertion_rate ,
139+ deletion_rate = deletion_rate ,
140+ mutation_rate = mutation_rate ,
141+ values = mutation_values [OM ])
142+
143+ if mutation_number < len (mutation_list ):
144+ OMs = random .choices (mutation_list , k = mutation_number )
145+ else :
146+ add = ["Sequence" ]* (mutation_number - len (mutation_list ))
147+ OMs = [* mutation_list , * add ]
138148
149+ for OM in OMs :
150+ tmp = mutation_functions [OM ]
151+ probe [OM ] = tmp (sequence = probe [OM ],
152+ insertion_rate = insertion_rate ,
153+ deletion_rate = deletion_rate ,
154+ mutation_rate = mutation_rate ,
155+ values = mutation_values [OM ])
156+
139157 # Calculate new properties
140- new_length = len (mutated_seq )
141- new_gc_content = calculate_gc_content (mutated_seq )
142-
143- # Randomly select new formamide value based on probabilities
144- new_formamide = random .choices (formamide_values ,
145- weights = formamide_probs ,
146- k = 1 )[0 ]
147-
148- # Create new probe with updated values
149- new_probe = probe .copy ()
150- new_probe ['Sequence' ] = mutated_seq
151- new_probe ['Length [nt]' ] = str (new_length )
152- new_probe ['G+C content [%]' ] = str (new_gc_content )
153- new_probe ['Formamide [%]' ] = str (new_formamide )
154-
158+ new_length = len (probe ['Sequence' ])
159+ new_gc_content = calculate_gc_content (probe ['Sequence' ])
160+ probe ['Length [nt]' ] = str (new_length )
161+ probe ['G+C content [%]' ] = str (new_gc_content )
162+
155163 # Generate unique ID for this iteration
156- if 'Accession no.' in new_probe :
157- new_probe ['Accession no.' ] = f"{ new_probe ['Accession no.' ]} _{ iteration + 1 } "
158-
159- noisy_probes .append (new_probe )
164+ if 'Accession no.' in probe :
165+ probe ['Accession no.' ] = f"{ probe ['Accession no.' ]} _{ iteration + 1 } "
166+
167+ probe ['Mutation number' ] = mutation_number
168+ noisy_probes .append (probe )
160169
161170 all_noisy_probes .extend (noisy_probes )
162171
@@ -174,23 +183,27 @@ def main():
174183 parser = argparse .ArgumentParser (description = 'Generate noisy probe data' )
175184 parser .add_argument ('--input' , required = True , help = 'Input probeBase CSV file' )
176185 parser .add_argument ('--output' , type = str , default = 'data/databases/open/probeBase_false.csv' , help = 'Output noisy probeBase CSV file' )
177- parser .add_argument ('--formamide ' , type = str , default = 'data/databases/open/probeBase_formamide.tsv' , help = 'Formamide probabilities TSV file ' )
186+ parser .add_argument ('--mutation-number ' , type = int , default = 5 , help = 'Maximum number of mutations to apply to each probe ' )
178187 parser .add_argument ('--insertion-rate' , type = float , default = 0.01 , help = 'Insertion mutation rate' )
179188 parser .add_argument ('--deletion-rate' , type = float , default = 0.01 , help = 'Deletion mutation rate' )
180189 parser .add_argument ('--mutation-rate' , type = float , default = 0.1 , help = 'SNP mutation rate' )
181190 parser .add_argument ('--iterations' , type = int , default = 10 , help = 'Number of iterations to generate noisy data' )
182191
183192 args = parser .parse_args ()
184193
185- generate_noisy_probes (
186- args .input ,
187- args .output ,
188- args .formamide ,
189- args .insertion_rate ,
190- args .deletion_rate ,
191- args .mutation_rate ,
192- args .iterations
193- )
194+ for i in range (args .mutation_number ):
195+ generate_noisy_probes (
196+ args .input ,
197+ args .output ,
198+ i ,
199+ insertion_rate = args .insertion_rate ,
200+ deletion_rate = args .deletion_rate ,
201+ mutation_rate = args .mutation_rate ,
202+ iterations = args .iterations
203+ )
194204
195205if __name__ == '__main__' :
196- main ()
206+ main ()
207+
208+ # Usage:
209+ # python scripts/databases/generate_noisy_probes.py --input data/databases/open/probeBase.csv --output data/databases/open/probeBase_mutated.csv
0 commit comments