levseq 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- levseq/IO_processor.py +565 -0
- levseq/__init__.py +34 -0
- levseq/barcoding/__init__.py +1 -0
- levseq/barcoding/demultiplex +0 -0
- levseq/barcoding/demultiplex-arm64 +0 -0
- levseq/barcoding/demultiplex-x86 +0 -0
- levseq/barcoding/minion_barcodes.fasta +386 -0
- levseq/basecaller.py +80 -0
- levseq/cmd.py +23 -0
- levseq/globals.py +66 -0
- levseq/interface.py +85 -0
- levseq/parser.py +82 -0
- levseq/run_levseq.py +558 -0
- levseq/screen.py +38 -0
- levseq/simulation.py +311 -0
- levseq/user.py +157 -0
- levseq/utils.py +474 -0
- levseq/variantcaller.py +252 -0
- levseq/visualization.py +1130 -0
- levseq-1.0.0.data/data/LICENSE +674 -0
- levseq-1.0.0.dist-info/LICENSE +674 -0
- levseq-1.0.0.dist-info/METADATA +180 -0
- levseq-1.0.0.dist-info/RECORD +26 -0
- levseq-1.0.0.dist-info/WHEEL +5 -0
- levseq-1.0.0.dist-info/entry_points.txt +2 -0
- levseq-1.0.0.dist-info/top_level.txt +1 -0
levseq/IO_processor.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# #
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify #
|
|
4
|
+
# it under the terms of the GNU General Public License as published by #
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or #
|
|
6
|
+
# (at your option) any later version. #
|
|
7
|
+
# #
|
|
8
|
+
# This program is distributed in the hope that it will be useful, #
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
|
11
|
+
# GNU General Public License for more details. #
|
|
12
|
+
# #
|
|
13
|
+
# You should have received a copy of the GNU General Public License #
|
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
|
15
|
+
# #
|
|
16
|
+
###############################################################################
|
|
17
|
+
|
|
18
|
+
# This script contains function to check for input data and process it for downstream analysis.
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import glob
|
|
22
|
+
from Bio import SeqIO
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
import gzip
|
|
25
|
+
import subprocess
|
|
26
|
+
import re
|
|
27
|
+
import numpy as np
|
|
28
|
+
import pandas as pd
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SequenceGenerator:
|
|
32
|
+
"""
|
|
33
|
+
A class for processing Sequence data. The class uses variant data frame to generate sequences.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, variant_df, reference_path, padding_start=50, padding_end=50):
|
|
37
|
+
self.variant_df = variant_df
|
|
38
|
+
self.reference = get_template_sequence(reference_path)
|
|
39
|
+
self.padding_start = padding_start
|
|
40
|
+
self.padding_end = padding_end
|
|
41
|
+
|
|
42
|
+
def get_sequences(self):
|
|
43
|
+
self.variant_df["Sequence"] = self.variant_df.apply(self.get_sequence, axis=1)
|
|
44
|
+
return self.variant_df
|
|
45
|
+
|
|
46
|
+
def get_sequence(self, row):
|
|
47
|
+
variant = row["Variant"]
|
|
48
|
+
if pd.isnull(variant):
|
|
49
|
+
return np.nan
|
|
50
|
+
else:
|
|
51
|
+
# Process the variant and generate the sequence
|
|
52
|
+
seq = self.generate_sequence(str(variant))
|
|
53
|
+
return seq
|
|
54
|
+
|
|
55
|
+
def generate_sequence(self, variant: str, ref_only=True):
|
|
56
|
+
"""
|
|
57
|
+
Generate sequence from variant string. E.g "A123T
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
new_seq = self.reference
|
|
61
|
+
|
|
62
|
+
if isinstance(variant, float):
|
|
63
|
+
return float('nan')
|
|
64
|
+
|
|
65
|
+
elif variant != "#PARENT#":
|
|
66
|
+
|
|
67
|
+
variants = variant.split("_")
|
|
68
|
+
|
|
69
|
+
for var in variants:
|
|
70
|
+
match = re.match(r'[A-Za-z]+(\d+)([A-Za-z]+)', var)
|
|
71
|
+
if match:
|
|
72
|
+
position = int(match.group(1))
|
|
73
|
+
adj_pos = position - 1 + self.padding_start
|
|
74
|
+
new_nucleotide = match.group(2)
|
|
75
|
+
else:
|
|
76
|
+
raise ValueError(f"Invalid variant format: {var}")
|
|
77
|
+
if new_nucleotide == "DEL":
|
|
78
|
+
new_seq = new_seq[:adj_pos] + "-" + new_seq[adj_pos + 1:]
|
|
79
|
+
|
|
80
|
+
else:
|
|
81
|
+
new_seq = new_seq[:adj_pos] + new_nucleotide + new_seq[adj_pos + 1:]
|
|
82
|
+
|
|
83
|
+
# Remove "-" from sequence
|
|
84
|
+
new_seq = new_seq.replace("-", "")
|
|
85
|
+
|
|
86
|
+
if ref_only:
|
|
87
|
+
return new_seq[self.padding_start: -self.padding_end]
|
|
88
|
+
|
|
89
|
+
return new_seq
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class BarcodeProcessor:
|
|
93
|
+
"""
|
|
94
|
+
A class for processing barcode fasta file.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, barcode_path, front_prefix="NB", reverse_prefix="RB"):
|
|
98
|
+
self.barcode_path = barcode_path
|
|
99
|
+
self.front_prefix = front_prefix
|
|
100
|
+
self.reverse_prefix = reverse_prefix
|
|
101
|
+
|
|
102
|
+
def filter_barcodes(self, filtered_fasta_file, front_range, reverse_number):
|
|
103
|
+
"""
|
|
104
|
+
Filters barcodes in the given ranges and writes them to a new fasta file.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
- filtered_fasta_file (str): The path to the filtered fasta file.
|
|
108
|
+
- front_range (tuple): A tuple of two integers representing the range for front barcodes.
|
|
109
|
+
- reverse_number (int): An integer representing the specific reverse barcode number.
|
|
110
|
+
"""
|
|
111
|
+
records = list(SeqIO.parse(self.barcode_path, "fasta"))
|
|
112
|
+
min_front, max_front = front_range
|
|
113
|
+
|
|
114
|
+
filtered_records = [
|
|
115
|
+
record for record in records
|
|
116
|
+
if (record.id.startswith(self.front_prefix) and min_front <= int(
|
|
117
|
+
record.id[len(self.front_prefix):]) <= max_front) or
|
|
118
|
+
(record.id.startswith(self.reverse_prefix) and int(
|
|
119
|
+
record.id[len(self.reverse_prefix):]) == reverse_number)
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
with open(filtered_fasta_file, "w") as output_handle:
|
|
123
|
+
SeqIO.write(filtered_records, output_handle, "fasta")
|
|
124
|
+
|
|
125
|
+
def get_barcode_dict(self, demultiplex_folder: Path) -> dict:
|
|
126
|
+
"""
|
|
127
|
+
Get a dictionary of folder paths, where reverse is the key and forward is stored in a list
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
- demultiplex_folder, where the demultiplex folder is located
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
- barcode_dict, dictionary of reverse and front barcode paths
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
rbc_folders = get_rbc_barcode_folders(demultiplex_folder, prefix=self.reverse_prefix)
|
|
137
|
+
fbc_folders = get_fbc_barcode_folders(rbc_folders, prefix=self.front_prefix)
|
|
138
|
+
|
|
139
|
+
return fbc_folders
|
|
140
|
+
|
|
141
|
+
def get_rbc_barcode_folders(self, demultiplex_folder: Path) -> list:
|
|
142
|
+
"""
|
|
143
|
+
Extract the reverse barcode folders (rbc) from the demultiplex folder
|
|
144
|
+
Args:
|
|
145
|
+
- demultiplex_folder (Path): Where the demultiplex folder is located.
|
|
146
|
+
Returns:
|
|
147
|
+
- reverse_barcodes (list): Where the barcode folders are located.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
if not demultiplex_folder.exists():
|
|
151
|
+
raise FileNotFoundError(
|
|
152
|
+
f"Demultiplex folder '{demultiplex_folder}' does not exist. Run levseq to get the demultiplex folder.")
|
|
153
|
+
|
|
154
|
+
reverse_barcodes = list(demultiplex_folder.glob(f"{self.reverse_prefix}*"))
|
|
155
|
+
|
|
156
|
+
if not reverse_barcodes:
|
|
157
|
+
# Optionally, use logging here instead of raising an exception
|
|
158
|
+
raise FileNotFoundError(
|
|
159
|
+
f"No reverse barcodes found in {demultiplex_folder}. Either no barcodes were found or the barcode score is too high. Rerun the experiment or adapt the barcode score in the TOML file.")
|
|
160
|
+
|
|
161
|
+
return reverse_barcodes
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def find_folder(start_path, target_folder_name):
|
|
165
|
+
"""
|
|
166
|
+
Find a specific folder within the start_path. Access the name from Meta File.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
- start_path (str): The path to begin the search from.
|
|
170
|
+
- target_folder_name (str): The name of the folder to search for.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
- str: The full path to the found folder.
|
|
174
|
+
- Exception: If the target folder is not found.
|
|
175
|
+
"""
|
|
176
|
+
for (path, dirs, _) in os.walk(start_path, topdown=True):
|
|
177
|
+
if target_folder_name in dirs:
|
|
178
|
+
return os.path.join(path, target_folder_name)
|
|
179
|
+
raise Exception(f"{target_folder_name} folder does not exist. Please check if you have chosen the right name.")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def check_data_folder(path: Path) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Check if minknown data folder exists. If not, assert an error.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
- path (str): The path to the minknow data folder.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
- FileNotFoundError: If the minknow data folder does not exist.
|
|
191
|
+
"""
|
|
192
|
+
if not Path(path).exists():
|
|
193
|
+
raise FileNotFoundError(
|
|
194
|
+
f"MinKNOW data folder '{path}' does not exist. Please check if the path is correct.")
|
|
195
|
+
else:
|
|
196
|
+
return path
|
|
197
|
+
|
|
198
|
+
def find_experiment_folder(experiment_name: str, minknow_path) -> None:
|
|
199
|
+
"""Find the experiment folder in the minknow data folder. Access the name from Meta File.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
- experiment_name (str): The name of the experiment to search for.
|
|
203
|
+
- minknow_path (str): The path to the minknow data folder.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
- str: The full path to the found experiment folder.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
check_data_folder(Path(minknow_path))
|
|
210
|
+
|
|
211
|
+
for (path, dirs, files) in os.walk(minknow_path, topdown=True):
|
|
212
|
+
if experiment_name in dirs:
|
|
213
|
+
return os.path.join(path, experiment_name)
|
|
214
|
+
|
|
215
|
+
raise Exception("Experiment folder does not exist. Please check if you have chosen the right experiment name.")
|
|
216
|
+
|
|
217
|
+
def find_folder(start_path, target_folder_name):
|
|
218
|
+
"""
|
|
219
|
+
Find a specific folder within the start_path. Access the name from Meta File.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
- start_path (str): The path to begin the search from.
|
|
223
|
+
- target_folder_name (str): The name of the folder to search for.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
- str: The full path to the found folder.
|
|
227
|
+
- Exception: If the target folder is not found.
|
|
228
|
+
"""
|
|
229
|
+
for (path, dirs, _) in os.walk(start_path, topdown=True):
|
|
230
|
+
if target_folder_name in dirs:
|
|
231
|
+
return os.path.join(path, target_folder_name)
|
|
232
|
+
raise Exception(f"{target_folder_name} folder does not exist. Please check if you have chosen the right name.")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def find_experiment_files(start_path: Path, target_folder_name: list) -> Path or None:
|
|
236
|
+
"""
|
|
237
|
+
Check if the experimenter has already basecalled before. If pod5_pass or fastq_pass folder exists, the function returns True.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
- start_path (Path): The path to begin the search from.
|
|
241
|
+
- target_folder_name (str): The name of the folder to search for.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
- Target folder (Path): The full path to the found folder.
|
|
245
|
+
- False: If the target folder is not found.
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
for value in target_folder_name:
|
|
250
|
+
try:
|
|
251
|
+
return find_folder(start_path, value)
|
|
252
|
+
except:
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def extract_files_from_folder(path: Path) -> list:
|
|
259
|
+
"""Extract all files from a folder"""
|
|
260
|
+
return [file for file in path.iterdir() if file.is_file()]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def filter_fastq_by_length(input_fastq: Path, output_fastq: Path, min_length: int, max_length: int):
|
|
264
|
+
"""
|
|
265
|
+
Filter a FASTQ file based on read length and write to a new FASTQ file.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
- input_fastq: Path to the input FASTQ gzip file.
|
|
269
|
+
- output_fastq: Path to the output FASTQ gzip file.
|
|
270
|
+
- min_length: Minimum length of reads to retain.
|
|
271
|
+
- max_length: Maximum length of reads to retain.
|
|
272
|
+
Returns:
|
|
273
|
+
- Fastq file with reads between min_length and max_length.
|
|
274
|
+
- N_reads
|
|
275
|
+
"""
|
|
276
|
+
with gzip.open(input_fastq, 'r') as infile, open(output_fastq, 'w') as outfile:
|
|
277
|
+
|
|
278
|
+
while True:
|
|
279
|
+
|
|
280
|
+
header = infile.readline().strip()
|
|
281
|
+
sequence = infile.readline().strip()
|
|
282
|
+
plus_sign = infile.readline().strip()
|
|
283
|
+
quality_scores = infile.readline().strip()
|
|
284
|
+
|
|
285
|
+
if not header:
|
|
286
|
+
break
|
|
287
|
+
N_reads = 0
|
|
288
|
+
if min_length <= len(sequence) <= max_length:
|
|
289
|
+
outfile.write(f"{header}\n")
|
|
290
|
+
outfile.write(f"{sequence}\n")
|
|
291
|
+
outfile.write(f"{plus_sign}\n")
|
|
292
|
+
outfile.write(f"{quality_scores}\n")
|
|
293
|
+
N_reads += 1
|
|
294
|
+
|
|
295
|
+
return N_reads
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def find_file(start_path: Path, prefix: str, extension: str) -> Path:
|
|
299
|
+
"""
|
|
300
|
+
Find a file in the given start_path that matches the specified prefix and extension.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
- start_path (str): The directory to begin the search from.
|
|
304
|
+
- prefix (str): The prefix of the filename to search for.
|
|
305
|
+
- extension (str): The extension of the file to search for (e.g., '.txt').
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
- str: The full path to the found file.
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
- Exception: If no matching file is found.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
for dirpath, _, filenames in os.walk(start_path):
|
|
315
|
+
for filename in filenames:
|
|
316
|
+
if filename.startswith(prefix) and filename.endswith(extension):
|
|
317
|
+
return os.path.join(dirpath, filename)
|
|
318
|
+
|
|
319
|
+
raise Exception(f"No file with prefix '{prefix}' and extension '{extension}' was not found in {start_path}.")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def concatenate_fastq_files(path: Path, filename: str = "concatenated", prefix: str = "*", delete: bool = True) -> str:
|
|
323
|
+
"""
|
|
324
|
+
Concatenate all fastq files in a directory.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
- path (Path): Directory where fastq files are located.
|
|
328
|
+
- filename (str): Name of the concatenated file. Default is 'concatenated'.
|
|
329
|
+
- prefix (str): Prefix of files to be concatenated. Default is '*' (all files).
|
|
330
|
+
- delete (bool): Whether to delete the original files after concatenation.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
- str: Message indicating the result of the operation.
|
|
334
|
+
"""
|
|
335
|
+
search_pattern = path / f"{prefix}.fastq"
|
|
336
|
+
fastq_files = [Path(p) for p in glob.glob(str(search_pattern))]
|
|
337
|
+
|
|
338
|
+
if not fastq_files:
|
|
339
|
+
return "No fastq files found."
|
|
340
|
+
|
|
341
|
+
if len(fastq_files) == 1:
|
|
342
|
+
single_file = fastq_files[0]
|
|
343
|
+
|
|
344
|
+
if single_file.name != f"{filename}.fastq":
|
|
345
|
+
target_path = path / f"{filename}.fastq"
|
|
346
|
+
|
|
347
|
+
if target_path.exists():
|
|
348
|
+
return f"Target file {target_path} already exists. Rename operation aborted."
|
|
349
|
+
|
|
350
|
+
single_file.rename(target_path)
|
|
351
|
+
return f"Single fastq file in {path} found and renamed."
|
|
352
|
+
|
|
353
|
+
else:
|
|
354
|
+
# Concatenate multiple files
|
|
355
|
+
target_path = path / f"{filename}.fastq"
|
|
356
|
+
|
|
357
|
+
if target_path.exists():
|
|
358
|
+
return f"Target file {target_path} already exists. Concatenation aborted."
|
|
359
|
+
|
|
360
|
+
with target_path.open("w") as outfile:
|
|
361
|
+
for fastq_file in fastq_files:
|
|
362
|
+
with fastq_file.open() as infile:
|
|
363
|
+
outfile.write(infile.read())
|
|
364
|
+
|
|
365
|
+
if delete:
|
|
366
|
+
for fastq_file in fastq_files:
|
|
367
|
+
fastq_file.unlink()
|
|
368
|
+
|
|
369
|
+
return "Concatenation successful."
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def read_fasta_file(path: Path, score=False) -> dict:
|
|
373
|
+
"""
|
|
374
|
+
Read fasta file from an input path
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
- path, where the fasta file is located
|
|
378
|
+
- score, if True, return sequence and quality scores
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
- Dictionary with sequence only or sequence and quality scores
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
if score:
|
|
385
|
+
sequences_and_scores = {"Sequence": [], "Quality-Score": []}
|
|
386
|
+
|
|
387
|
+
for record in SeqIO.parse(path, "fastq"):
|
|
388
|
+
sequences_and_scores["Sequence"].append(str(record.seq))
|
|
389
|
+
sequences_and_scores["Quality-Score"].append(record.letter_annotations["phred_quality"])
|
|
390
|
+
|
|
391
|
+
return sequences_and_scores
|
|
392
|
+
|
|
393
|
+
else:
|
|
394
|
+
sequences = {"Sequence": []}
|
|
395
|
+
|
|
396
|
+
file_extension = os.path.splitext(path)[1][1:] # Get file extension
|
|
397
|
+
|
|
398
|
+
for record in SeqIO.parse(path, "fasta"):
|
|
399
|
+
sequences["Sequence"].append(str(record.seq))
|
|
400
|
+
|
|
401
|
+
return sequences
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def create_folder(experiment_name: str, target_path: Path = None, output_name: str = None) -> Path:
|
|
405
|
+
"""
|
|
406
|
+
When Starting levseq, the function checks if a levseq result folder exists. If not, it creates one.
|
|
407
|
+
It also checks for the subfolder of the experiment.
|
|
408
|
+
If no information is given about the folder, it creates a default folder in the current directory.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
|
|
412
|
+
- target_path, path of the folder to be created
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
|
|
416
|
+
- Path object representing the experiment folder
|
|
417
|
+
- Raises Exception if the folder could not be created or path is invalid
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
if target_path is None:
|
|
421
|
+
# Get current working directory
|
|
422
|
+
curr_dir = Path.cwd()
|
|
423
|
+
else:
|
|
424
|
+
if not target_path.exists():
|
|
425
|
+
raise Exception("Target path does not exist. Please check if you have chosen the right path.")
|
|
426
|
+
curr_dir = target_path
|
|
427
|
+
|
|
428
|
+
# Create minION_results folder if it doesn't exist
|
|
429
|
+
minION_results_dir = curr_dir / "EVSeqL_Output"
|
|
430
|
+
minION_results_dir.mkdir(exist_ok=True)
|
|
431
|
+
|
|
432
|
+
# Create experiment folder
|
|
433
|
+
|
|
434
|
+
experiment_name = f"{experiment_name}"
|
|
435
|
+
|
|
436
|
+
if output_name is None:
|
|
437
|
+
result_folder = minION_results_dir / experiment_name
|
|
438
|
+
else:
|
|
439
|
+
output_name = f"{output_name}"
|
|
440
|
+
result_folder = minION_results_dir / output_name
|
|
441
|
+
|
|
442
|
+
result_folder.mkdir(exist_ok=True)
|
|
443
|
+
|
|
444
|
+
return result_folder
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def get_rbc_barcode_folders(demultiplex_folder: Path, prefix="barcode") -> list:
|
|
448
|
+
"""Extract the reverse barcode folders (rbc) from the demultiplex folder
|
|
449
|
+
Args:
|
|
450
|
+
- demultiplex_folder (Path): Where the demultiplex folder is located.
|
|
451
|
+
Returns:
|
|
452
|
+
- reverse_barcodes (list): Where the barcode folders are located.
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
if not demultiplex_folder.exists():
|
|
456
|
+
raise FileNotFoundError(
|
|
457
|
+
f"Demultiplex folder '{demultiplex_folder}' does not exist. Run levseq to get the demultiplex folder.")
|
|
458
|
+
|
|
459
|
+
reverse_barcodes = list(demultiplex_folder.glob(f"{prefix}*"))
|
|
460
|
+
|
|
461
|
+
if not reverse_barcodes:
|
|
462
|
+
# Optionally, use logging here instead of raising an exception
|
|
463
|
+
raise FileNotFoundError(
|
|
464
|
+
f"No reverse barcodes found in {demultiplex_folder}. Either no barcodes were found or the barcode score is too high. Rerun the experiment or adapt the barcode score in the TOML file.")
|
|
465
|
+
|
|
466
|
+
return reverse_barcodes
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def get_fbc_barcode_folders(rbc_barcode_folders: list, prefix="barcode") -> dict:
|
|
470
|
+
"""Extract the forward barcode folders (fbc) within the reverse barcode folders
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
- result_folder, where the demultiplex folder is located
|
|
474
|
+
- rbc_barcode_folders, name of reverse barcode folders
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
- Barcode folders (dict), where the forward barcode folders are located
|
|
478
|
+
"""
|
|
479
|
+
|
|
480
|
+
fbc_folders = {}
|
|
481
|
+
|
|
482
|
+
if rbc_barcode_folders is None:
|
|
483
|
+
raise Exception("Reverse barcode folders are not given. Please check if you have chosen the right path.")
|
|
484
|
+
|
|
485
|
+
for folder in rbc_barcode_folders:
|
|
486
|
+
fbc_folders[folder] = list(folder.glob(f"{prefix}*"))
|
|
487
|
+
|
|
488
|
+
if not fbc_folders:
|
|
489
|
+
raise Exception(
|
|
490
|
+
f"Forward barcodes in {rbc_barcode_folders} do not exist. Either no barcodes were found or the barcode score is too high. Rerun the experiment or adapt the barcode score in the TOML file. ")
|
|
491
|
+
|
|
492
|
+
if not any(fbc_folders.values()):
|
|
493
|
+
raise Exception(f"Forward barcodes in {rbc_barcode_folders} do not exist. Please check the folders.")
|
|
494
|
+
|
|
495
|
+
return fbc_folders
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def get_barcode_dict(demultiplex_folder: Path, front_prefix="barcode", reverse_prefix="barcode") -> dict:
|
|
499
|
+
"""
|
|
500
|
+
Get a dictionary of folder paths, where reverse is the key and forward is stored in a list
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
- demultiplex_folder, where the demultiplex folder is located
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
- barcode_dict, dictionary of reverse and front barcode paths
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
rbc_folders = get_rbc_barcode_folders(demultiplex_folder, prefix=reverse_prefix)
|
|
510
|
+
fbc_folders = get_fbc_barcode_folders(rbc_folders, prefix=front_prefix)
|
|
511
|
+
|
|
512
|
+
return fbc_folders
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def trim_fasta(input_file, output_file, trim_length=12):
|
|
516
|
+
with open(input_file, 'r') as in_fasta, open(output_file, 'w') as out_fasta:
|
|
517
|
+
for record in SeqIO.parse(in_fasta, 'fasta'):
|
|
518
|
+
trimmed_seq = record.seq[trim_length:]
|
|
519
|
+
record.seq = trimmed_seq
|
|
520
|
+
SeqIO.write(record, out_fasta, 'fasta')
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def filter_fastq_by_length(result_folder, input_fastq: Path, min_length: int, max_length: int, ind: int):
|
|
524
|
+
"""
|
|
525
|
+
Filter a FASTQ file based on read length and write to a new FASTQ file. Currently we use fastq-filter.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
- input_fastq: Path to the input FASTQ file or folder.
|
|
529
|
+
- output_fastq: Path to the output FASTQ file.
|
|
530
|
+
- min_length: Minimum length of reads to retain.
|
|
531
|
+
- max_length: Maximum length of reads to retain.
|
|
532
|
+
Returns:
|
|
533
|
+
- Fastq file with reads between min_length and max_length.
|
|
534
|
+
- N_reads
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
if input_fastq.is_dir():
|
|
538
|
+
input_files = f'{input_fastq}/*.fastq'
|
|
539
|
+
|
|
540
|
+
elif input_fastq.is_file():
|
|
541
|
+
input_files = input_fastq
|
|
542
|
+
|
|
543
|
+
else:
|
|
544
|
+
raise Exception("Input file is not a file or a folder. Please check if you have chosen the right path.")
|
|
545
|
+
|
|
546
|
+
basecall_folder = os.path.join(result_folder, "basecalled_filtered")
|
|
547
|
+
Path(basecall_folder).mkdir(parents=True, exist_ok=True)
|
|
548
|
+
|
|
549
|
+
prompt = f'fastq-filter -o {basecall_folder}/basecalled_filtered{ind}.fastq.gz -l {min_length} -L {max_length} {input_files} --quiet'
|
|
550
|
+
|
|
551
|
+
subprocess.run(prompt, shell=True)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def get_template_sequence(path: Path) -> str:
|
|
555
|
+
"""
|
|
556
|
+
Read template sequence fasta file
|
|
557
|
+
Args:
|
|
558
|
+
- path, where the fasta file is located
|
|
559
|
+
Returns:
|
|
560
|
+
- Template sequence
|
|
561
|
+
"""
|
|
562
|
+
|
|
563
|
+
template = read_fasta_file(path)
|
|
564
|
+
|
|
565
|
+
return template["Sequence"][0]
|
levseq/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# #
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify #
|
|
4
|
+
# it under the terms of the GNU General Public License as published by #
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or #
|
|
6
|
+
# (at your option) any later version. #
|
|
7
|
+
# #
|
|
8
|
+
# This program is distributed in the hope that it will be useful, #
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
|
11
|
+
# GNU General Public License for more details. #
|
|
12
|
+
# #
|
|
13
|
+
# You should have received a copy of the GNU General Public License #
|
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
|
15
|
+
# #
|
|
16
|
+
###############################################################################
|
|
17
|
+
|
|
18
|
+
__title__ = 'levseq'
|
|
19
|
+
__description__ = 'LevSeq nanopore sequencing'
|
|
20
|
+
__url__ = 'https://github.com/fhalab/levseq/'
|
|
21
|
+
__version__ = '1.0.0'
|
|
22
|
+
__author__ = 'Yueming Long, Emreay Gursoy, Ariane Mora'
|
|
23
|
+
__author_email__ = 'ylong@caltech.edu'
|
|
24
|
+
__license__ = 'GPL3'
|
|
25
|
+
|
|
26
|
+
from levseq.globals import *
|
|
27
|
+
from levseq.variantcaller import *
|
|
28
|
+
from levseq.visualization import *
|
|
29
|
+
from levseq.interface import *
|
|
30
|
+
from levseq.cmd import *
|
|
31
|
+
from levseq.utils import *
|
|
32
|
+
from levseq.simulation import *
|
|
33
|
+
from levseq.user import *
|
|
34
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Add for resources to locate file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|