ScreenPro2 0.5.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.
- pyproject.toml +30 -0
- screenpro/__init__.py +31 -0
- screenpro/__main__.py +8 -0
- screenpro/assays/__init__.py +465 -0
- screenpro/dashboard/__init__.py +293 -0
- screenpro/load.py +239 -0
- screenpro/main.py +227 -0
- screenpro/ngs/__init__.py +390 -0
- screenpro/ngs/cas12.py +206 -0
- screenpro/ngs/cas9.py +276 -0
- screenpro/phenoscore/__init__.py +148 -0
- screenpro/phenoscore/_annotate.py +122 -0
- screenpro/phenoscore/delta.py +375 -0
- screenpro/phenoscore/deseq.py +57 -0
- screenpro/phenoscore/evaluate.py +66 -0
- screenpro/phenoscore/phenostat.py +84 -0
- screenpro/plotting/__init__.py +9 -0
- screenpro/plotting/_rank.py +88 -0
- screenpro/plotting/_utils.py +81 -0
- screenpro/plotting/pheno_plots.py +196 -0
- screenpro/plotting/qc_plots.py +45 -0
- screenpro/preprocessing.py +98 -0
- screenpro2-0.5.0.dist-info/LICENSE +25 -0
- screenpro2-0.5.0.dist-info/METADATA +366 -0
- screenpro2-0.5.0.dist-info/RECORD +27 -0
- screenpro2-0.5.0.dist-info/WHEEL +4 -0
- screenpro2-0.5.0.dist-info/entry_points.txt +3 -0
screenpro/main.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import polars as pl
|
|
6
|
+
from glob import glob
|
|
7
|
+
from simple_colors import green
|
|
8
|
+
from .__init__ import __version__
|
|
9
|
+
from . import ngs
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_counter_parser(parent_subparsers, parent):
|
|
13
|
+
name = "guidecounter"
|
|
14
|
+
desc = "Process FASTQ files to count sgRNA sequences."
|
|
15
|
+
help = """
|
|
16
|
+
Example usage:
|
|
17
|
+
|
|
18
|
+
`screenpro guidecounter --single-guide-design -l <path-to-library> -s <path-to-sample-sheet>`
|
|
19
|
+
`screenpro guidecounter --dual-guide-design -l <path-to-library> -s <path-to-sample-sheet>`
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
sub_parser = parent_subparsers.add_parser(
|
|
24
|
+
name, parents=[parent], description=desc, help=help, add_help=True
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
sub_parser.add_argument(
|
|
28
|
+
'-c',
|
|
29
|
+
'--cas-type',
|
|
30
|
+
type=str,
|
|
31
|
+
default='cas9',
|
|
32
|
+
help='Type of Cas protein used for the screen. Default: cas9'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
sub_parser.add_argument(
|
|
36
|
+
"--single-guide-design",
|
|
37
|
+
action="store_true",
|
|
38
|
+
help="Single guide design library.",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
sub_parser.add_argument(
|
|
42
|
+
"--dual-guide-design",
|
|
43
|
+
action="store_true",
|
|
44
|
+
help="Dual guide design library.",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
sub_parser.add_argument(
|
|
48
|
+
"-l",
|
|
49
|
+
"--library",
|
|
50
|
+
type=str,
|
|
51
|
+
required=True,
|
|
52
|
+
help="Path to library file.",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
sub_parser.add_argument(
|
|
56
|
+
"-p",
|
|
57
|
+
"--path",
|
|
58
|
+
type=str,
|
|
59
|
+
required=True,
|
|
60
|
+
help="Path to directory containing FASTQ files."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
sub_parser.add_argument(
|
|
64
|
+
"-s",
|
|
65
|
+
"--samples",
|
|
66
|
+
required=True,
|
|
67
|
+
type=str,
|
|
68
|
+
help="Sample ID(s) to process. Regex can be used to match multiple samples."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
sub_parser.add_argument(
|
|
72
|
+
"--write-count-matrix",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Write count matrix to file."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
sub_parser.add_argument(
|
|
78
|
+
'-o',
|
|
79
|
+
'--output',
|
|
80
|
+
type=str,
|
|
81
|
+
help='Path to output directory.'
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return sub_parser
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main():
|
|
88
|
+
"""
|
|
89
|
+
Function containing argparse parsers and arguments to allow the use of screenpro from the terminal.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
# Define parent parser
|
|
93
|
+
parent_parser = argparse.ArgumentParser(
|
|
94
|
+
description=f"screenpro v{__version__}", add_help=False
|
|
95
|
+
)
|
|
96
|
+
# Initiate subparsers and define parent
|
|
97
|
+
parent_subparsers = parent_parser.add_subparsers(dest="command")
|
|
98
|
+
parent = argparse.ArgumentParser(add_help=False)
|
|
99
|
+
|
|
100
|
+
# Add custom help argument to parent parser
|
|
101
|
+
parent_parser.add_argument(
|
|
102
|
+
"-h", "--help", action="store_true", help="Print manual."
|
|
103
|
+
)
|
|
104
|
+
# Add custom version argument to parent parser
|
|
105
|
+
parent_parser.add_argument(
|
|
106
|
+
"-v", "--version", action="store_true", help="Print version."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
## screenpro commands
|
|
110
|
+
|
|
111
|
+
# counter subcommand
|
|
112
|
+
counter_parser = add_counter_parser(parent_subparsers, parent)
|
|
113
|
+
|
|
114
|
+
## Define return values
|
|
115
|
+
args = parent_parser.parse_args()
|
|
116
|
+
|
|
117
|
+
# Help return
|
|
118
|
+
if args.help:
|
|
119
|
+
# Retrieve all subparsers from the parent parser
|
|
120
|
+
subparsers_actions = [
|
|
121
|
+
action
|
|
122
|
+
for action in parent_parser._actions
|
|
123
|
+
if isinstance(action, argparse._SubParsersAction)
|
|
124
|
+
]
|
|
125
|
+
for subparsers_action in subparsers_actions:
|
|
126
|
+
# Get all subparsers and print help
|
|
127
|
+
for choice, subparser in subparsers_action.choices.items():
|
|
128
|
+
print("Subparser '{}'".format(choice))
|
|
129
|
+
print(subparser.format_help())
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
# Version return
|
|
133
|
+
if args.version:
|
|
134
|
+
print(f"screenpro version: {__version__}")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
# Show help when no arguments are given
|
|
138
|
+
if len(sys.argv) == 1:
|
|
139
|
+
parent_parser.print_help(sys.stderr)
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
# Show module specific help if only module but no further arguments are given
|
|
143
|
+
command_to_parser = {
|
|
144
|
+
"guidecounter": counter_parser,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if len(sys.argv) == 2:
|
|
148
|
+
if sys.argv[1] in command_to_parser:
|
|
149
|
+
command_to_parser[sys.argv[1]].print_help(sys.stderr)
|
|
150
|
+
else:
|
|
151
|
+
parent_parser.print_help(sys.stderr)
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
## counter return
|
|
155
|
+
if args.command == "guidecounter":
|
|
156
|
+
### Perform checks on input arguments
|
|
157
|
+
## Check if library platform is provided by user
|
|
158
|
+
if args.single_guide_design:
|
|
159
|
+
args.library_type = "single_guide_design"
|
|
160
|
+
elif args.dual_guide_design:
|
|
161
|
+
args.library_type = "dual_guide_design"
|
|
162
|
+
else:
|
|
163
|
+
print("Library type not provided. Available options are '--single-guide-design' or '--dual-guide-design'. Exiting...")
|
|
164
|
+
sys.exit(1)
|
|
165
|
+
|
|
166
|
+
if args.output:
|
|
167
|
+
pass
|
|
168
|
+
else:
|
|
169
|
+
print("No output directory provided. Exiting...")
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
|
|
172
|
+
# Create saving directory
|
|
173
|
+
directory = "/".join(args.output.split("/")[:-1])
|
|
174
|
+
if directory != "":
|
|
175
|
+
os.makedirs(directory, exist_ok=True)
|
|
176
|
+
|
|
177
|
+
### Parse input files: library, samples, and fastq files
|
|
178
|
+
counter = ngs.GuideCounter(args.cas_type, args.library_type)
|
|
179
|
+
|
|
180
|
+
## 1. Load library table and check if required columns are present
|
|
181
|
+
counter.load_library(
|
|
182
|
+
args.library, sep = '\t', index_col=False,
|
|
183
|
+
verbose=True
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
counter.library.to_pandas().to_csv(
|
|
187
|
+
f"{args.output}/library.reformatted.tsv", sep = '\t',
|
|
188
|
+
)
|
|
189
|
+
print(f"Library table saved to {args.output}/library.reformatted.tsv")
|
|
190
|
+
|
|
191
|
+
## 2. Get list of samples to process
|
|
192
|
+
#TODO: Implement regex to match multiple samples
|
|
193
|
+
samples = args.samples.split(",")
|
|
194
|
+
|
|
195
|
+
if len(samples) == 0:
|
|
196
|
+
print(f"No samples found in {args.path}/{args.samples}. Exiting...")
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
|
|
199
|
+
print(f"Samples to process: {','.join(samples)}")
|
|
200
|
+
|
|
201
|
+
## 3. Load FASTQ files and count sgRNA sequences
|
|
202
|
+
counter.get_counts_matrix(
|
|
203
|
+
fastq_dir = args.path,
|
|
204
|
+
samples = samples,
|
|
205
|
+
# get_recombinant=True,
|
|
206
|
+
# write='force',
|
|
207
|
+
verbose = True
|
|
208
|
+
)
|
|
209
|
+
print("Finished FASTQ processing.")
|
|
210
|
+
|
|
211
|
+
# Save counts for each sample
|
|
212
|
+
for sample in samples:
|
|
213
|
+
cnt = counter.counts_mat[[sample]].rename(columns={sample: 'count'}).astype({'count': int})
|
|
214
|
+
cnt.to_csv(f"{args.output}/{sample}.counts.tsv", sep='\t')
|
|
215
|
+
print(f"Counts for sample {sample} saved to {args.output}/{sample}.counts.tsv")
|
|
216
|
+
|
|
217
|
+
# Save count matrix
|
|
218
|
+
if args.write_count_matrix:
|
|
219
|
+
counter.counts_mat.to_csv(
|
|
220
|
+
f"{args.output}/count_matrix.tsv",
|
|
221
|
+
sep='\t'
|
|
222
|
+
)
|
|
223
|
+
print(f"Count matrix saved to {args.output}/count_matrix.tsv")
|
|
224
|
+
|
|
225
|
+
# Pipeline finished
|
|
226
|
+
print(green("Your run is finished successfully."))
|
|
227
|
+
sys.exit(0)
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
## Copyright (c) 2022-2025 ScreenPro2 Development Team.
|
|
2
|
+
## All rights reserved.
|
|
3
|
+
## Gilbart Lab, UCSF / Arc Institute.
|
|
4
|
+
## Multi-Omics Tech Center, Arc Insititue.
|
|
5
|
+
|
|
6
|
+
## This part of the software is conceptualized and developed by Abolfazl Arab (@abearab)
|
|
7
|
+
## with support from the Nick Youngblut (@nick-youngblut).
|
|
8
|
+
|
|
9
|
+
'''Scripts to work with NGS data
|
|
10
|
+
|
|
11
|
+
This module provides functions to process FASTQ files from screens with single or dual guide
|
|
12
|
+
libraries. In general, the algorithm is fairly simple:
|
|
13
|
+
|
|
14
|
+
1. Read the FASTQ file and extract the proper sequences
|
|
15
|
+
2. Count the exact number of occurrences for each unique sequence
|
|
16
|
+
3. Map the counted sequences to the reference sequence library
|
|
17
|
+
4. Return the counted mapped or unmapped events as a dataframe(s)
|
|
18
|
+
|
|
19
|
+
For single-guide screens, the sequences are counted as single protospacer
|
|
20
|
+
from a single-end read file (R1). Then, these sequences are mapped to the reference
|
|
21
|
+
library of protospacer sequences.
|
|
22
|
+
|
|
23
|
+
For dual-guide screens, the sequences are counted as pairs of protospacer A and B
|
|
24
|
+
from paired-end read files (R1 and R2). Then, sequences are mapped to the reference
|
|
25
|
+
library of protospacer A and B pairs.
|
|
26
|
+
|
|
27
|
+
Theoretically, the algorithm is able to detect any observed sequence since it is counting first
|
|
28
|
+
and then mapping. Therefore, the recombination events can be detected. In dual-guide design
|
|
29
|
+
protospacer A and B are not the same pairs as in the reference library. These events include:
|
|
30
|
+
|
|
31
|
+
- Protospacer A and B pairs are present in the reference library but paired differently
|
|
32
|
+
- Only one of the protospacer A and B is present in the reference library
|
|
33
|
+
- None of the protospacer A and B is present in the reference library
|
|
34
|
+
'''
|
|
35
|
+
|
|
36
|
+
import pandas as pd
|
|
37
|
+
import polars as pl
|
|
38
|
+
import anndata as ad
|
|
39
|
+
import os
|
|
40
|
+
|
|
41
|
+
from . import cas9
|
|
42
|
+
from . import cas12
|
|
43
|
+
from ..load import load_cas9_sgRNA_library
|
|
44
|
+
from simple_colors import green
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GuideCounter:
|
|
48
|
+
'''Class to count sequences from FASTQ files
|
|
49
|
+
'''
|
|
50
|
+
|
|
51
|
+
def __init__(self, cas_type, library_type):
|
|
52
|
+
self.cas_type = cas_type
|
|
53
|
+
self.library_type = library_type
|
|
54
|
+
self.counts_dict = None
|
|
55
|
+
self.counts_mat = None
|
|
56
|
+
self.recombinants = None
|
|
57
|
+
|
|
58
|
+
def load_library(self, library_path, sep='\t', index_col=0, protospacer_length=19, verbose=False, **args):
|
|
59
|
+
'''Load library file
|
|
60
|
+
'''
|
|
61
|
+
if self.cas_type == 'cas9':
|
|
62
|
+
|
|
63
|
+
library = load_cas9_sgRNA_library(library_path, library_type=self.library_type, sep=sep, index_col=index_col, protospacer_length=protospacer_length, verbose=verbose, **args)
|
|
64
|
+
|
|
65
|
+
# Check if the library has duplicate sequences and remove them
|
|
66
|
+
if library.duplicated('sequence').any():
|
|
67
|
+
shape_before_dedup = library.shape[0]
|
|
68
|
+
library = library.drop_duplicates(subset='sequence', keep='first')
|
|
69
|
+
shape_after_dedup = library.shape[0]
|
|
70
|
+
if verbose:
|
|
71
|
+
print(f"Warning: {shape_before_dedup - shape_after_dedup} duplicate sgRNA sequences found and removed.")
|
|
72
|
+
|
|
73
|
+
if self.library_type == "single_guide_design":
|
|
74
|
+
sgRNA_table = library[['target','sgID','protospacer']].set_index('sgID')
|
|
75
|
+
|
|
76
|
+
elif self.library_type == "dual_guide_design":
|
|
77
|
+
sgRNA_table = pd.concat([
|
|
78
|
+
library[['target','sgID_A','protospacer_A']].rename(columns={'sgID_A':'sgID','protospacer_A':'protospacer'}),
|
|
79
|
+
library[['target','sgID_B', 'protospacer_B']].rename(columns={'sgID_B':'sgID','protospacer_B':'protospacer'})
|
|
80
|
+
])
|
|
81
|
+
# drop duplicates and set index
|
|
82
|
+
sgRNA_table = sgRNA_table.drop_duplicates(keep='first')
|
|
83
|
+
|
|
84
|
+
if verbose: print('total # of cas9 sgRNAs:', sgRNA_table.shape[0])
|
|
85
|
+
|
|
86
|
+
elif self.cas_type == 'cas12':
|
|
87
|
+
raise NotImplementedError("Cas12 library is not yet implemented.")
|
|
88
|
+
|
|
89
|
+
# covert to polar DataFrame
|
|
90
|
+
library = pl.from_pandas(library)
|
|
91
|
+
sgRNA_table = pl.from_pandas(sgRNA_table)
|
|
92
|
+
|
|
93
|
+
self.library = library
|
|
94
|
+
self.sgRNA_table = sgRNA_table
|
|
95
|
+
|
|
96
|
+
def _process_cas9_single_guide_sample(self, fastq_dir, sample_id, trim_first_g, protospacer_length, write, verbose=False):
|
|
97
|
+
if verbose: print(green(sample_id, ['bold']))
|
|
98
|
+
get_counts = True
|
|
99
|
+
|
|
100
|
+
# check if df_count is already available
|
|
101
|
+
if os.path.exists(f'{fastq_dir}/{sample_id}_count.arrow'):
|
|
102
|
+
if verbose: print('count file exists ...')
|
|
103
|
+
if write != "force":
|
|
104
|
+
df_count = pl.read_ipc_stream(f'{fastq_dir}/{sample_id}_count.arrow')
|
|
105
|
+
get_counts = False
|
|
106
|
+
else:
|
|
107
|
+
if verbose: print('skip loading count file, force write is set ...')
|
|
108
|
+
|
|
109
|
+
if get_counts:
|
|
110
|
+
if trim_first_g:
|
|
111
|
+
trim5p_start = 2
|
|
112
|
+
else:
|
|
113
|
+
trim5p_start = 1
|
|
114
|
+
df_count = cas9.fastq_to_count_single_guide(
|
|
115
|
+
fastq_file_path=f'{fastq_dir}/{sample_id}.fastq.gz',
|
|
116
|
+
trim5p_start=trim5p_start,
|
|
117
|
+
trim5p_length=protospacer_length,
|
|
118
|
+
verbose=verbose
|
|
119
|
+
)
|
|
120
|
+
if write == "force" or write == True:
|
|
121
|
+
# write df_count to file
|
|
122
|
+
df_count.write_ipc_stream(f'{fastq_dir}/{sample_id}_count.arrow', compression='lz4')
|
|
123
|
+
if verbose: print('count file written ...')
|
|
124
|
+
|
|
125
|
+
out = cas9.map_to_library_single_guide(
|
|
126
|
+
df_count=df_count,
|
|
127
|
+
library=self.library,
|
|
128
|
+
return_type='all',
|
|
129
|
+
verbose=verbose
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
def _process_cas9_dual_guide_sample(self, fastq_dir, sample_id, get_recombinant, trim_first_g, protospacer_A_length, protospacer_B_length, write, verbose=False):
|
|
135
|
+
if verbose: print(green(sample_id, ['bold']))
|
|
136
|
+
get_counts = True
|
|
137
|
+
|
|
138
|
+
# check if df_count is already available
|
|
139
|
+
if os.path.exists(f'{fastq_dir}/{sample_id}_count.arrow'):
|
|
140
|
+
if verbose: print('count file exists ...')
|
|
141
|
+
if write != "force":
|
|
142
|
+
df_count = pl.read_ipc_stream(f'{fastq_dir}/{sample_id}_count.arrow')
|
|
143
|
+
get_counts = False
|
|
144
|
+
else:
|
|
145
|
+
if verbose: print('skip loading count file, force write is set ...')
|
|
146
|
+
|
|
147
|
+
if get_counts:
|
|
148
|
+
if get_counts:
|
|
149
|
+
if trim_first_g == True or trim_first_g == {'A':True, 'B':True}:
|
|
150
|
+
trim5p_pos1_start = 2
|
|
151
|
+
trim5p_pos2_start = 2
|
|
152
|
+
elif trim_first_g == False or trim_first_g == {'A':False, 'B':False}:
|
|
153
|
+
trim5p_pos1_start = 1
|
|
154
|
+
trim5p_pos2_start = 1
|
|
155
|
+
elif trim_first_g == {'A':True, 'B':False}:
|
|
156
|
+
trim5p_pos1_start = 2
|
|
157
|
+
trim5p_pos2_start = 1
|
|
158
|
+
elif trim_first_g == {'A':False, 'B':True}:
|
|
159
|
+
trim5p_pos1_start = 1
|
|
160
|
+
trim5p_pos2_start = 2
|
|
161
|
+
else:
|
|
162
|
+
raise ValueError("Invalid trim_first_g argument. Please provide a boolean or a dictionary with 'A' and 'B' keys.")
|
|
163
|
+
|
|
164
|
+
df_count = cas9.fastq_to_count_dual_guide(
|
|
165
|
+
R1_fastq_file_path=f'{fastq_dir}/{sample_id}_R1.fastq.gz',
|
|
166
|
+
R2_fastq_file_path=f'{fastq_dir}/{sample_id}_R2.fastq.gz',
|
|
167
|
+
trim5p_pos1_start=trim5p_pos1_start,
|
|
168
|
+
trim5p_pos1_length=protospacer_A_length,
|
|
169
|
+
trim5p_pos2_start=trim5p_pos2_start,
|
|
170
|
+
trim5p_pos2_length=protospacer_B_length,
|
|
171
|
+
verbose=verbose
|
|
172
|
+
)
|
|
173
|
+
if write == "force" or write == True:
|
|
174
|
+
# write df_count to file
|
|
175
|
+
df_count.write_ipc_stream(f'{fastq_dir}/{sample_id}_count.arrow', compression='lz4')
|
|
176
|
+
if verbose: print('count file written ...')
|
|
177
|
+
|
|
178
|
+
out = cas9.map_to_library_dual_guide(
|
|
179
|
+
df_count=df_count,
|
|
180
|
+
library=self.library,
|
|
181
|
+
get_recombinant=get_recombinant,
|
|
182
|
+
return_type='all',
|
|
183
|
+
verbose=verbose
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
def get_counts_matrix(self, fastq_dir, samples, get_recombinant=False, cas_type='cas9', protospacer_length='auto', trim_first_g=False, write=True, verbose=False):
|
|
189
|
+
'''Get count matrix for given samples
|
|
190
|
+
'''
|
|
191
|
+
if self.cas_type == 'cas9':
|
|
192
|
+
counts = {}
|
|
193
|
+
|
|
194
|
+
if self.library_type == "single_guide_design":
|
|
195
|
+
if get_recombinant:
|
|
196
|
+
raise ValueError("Recombinants are not applicable for single guide design!")
|
|
197
|
+
if protospacer_length == 'auto':
|
|
198
|
+
protospacer_length = self.library['protospacer'].str.len_bytes().unique().to_list()[0]
|
|
199
|
+
|
|
200
|
+
for sample_id in samples:
|
|
201
|
+
cnt = self._process_cas9_single_guide_sample(
|
|
202
|
+
fastq_dir=fastq_dir,
|
|
203
|
+
sample_id=sample_id,
|
|
204
|
+
trim_first_g=trim_first_g,
|
|
205
|
+
protospacer_length=protospacer_length,
|
|
206
|
+
write=write,
|
|
207
|
+
verbose=verbose
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
counts[sample_id] = cnt['mapped']
|
|
211
|
+
|
|
212
|
+
counts_mat = pd.concat([
|
|
213
|
+
counts[sample_id].to_pandas().set_index('sgID')['count'].rename(sample_id)
|
|
214
|
+
for sample_id in counts.keys()
|
|
215
|
+
],axis=1).fillna(0)
|
|
216
|
+
|
|
217
|
+
elif self.library_type == "dual_guide_design":
|
|
218
|
+
if get_recombinant: recombinants = {}
|
|
219
|
+
|
|
220
|
+
if protospacer_length == 'auto':
|
|
221
|
+
protospacer_A_length = self.library['protospacer_A'].str.len_bytes().unique().to_list()[0]
|
|
222
|
+
protospacer_B_length = self.library['protospacer_B'].str.len_bytes().unique().to_list()[0]
|
|
223
|
+
elif isinstance(protospacer_length, dict):
|
|
224
|
+
protospacer_A_length = protospacer_length['protospacer_A']
|
|
225
|
+
protospacer_B_length = protospacer_length['protospacer_B']
|
|
226
|
+
elif isinstance(protospacer_length, int):
|
|
227
|
+
protospacer_A_length = protospacer_length
|
|
228
|
+
protospacer_B_length = protospacer_length
|
|
229
|
+
else:
|
|
230
|
+
raise ValueError("Invalid protospacer_length argument. If not 'auto', please provide an integer or a dictionary with 'protospacer_A' and 'protospacer_B' keys.")
|
|
231
|
+
|
|
232
|
+
for sample_id in samples:
|
|
233
|
+
cnt = self._process_cas9_dual_guide_sample(
|
|
234
|
+
fastq_dir=fastq_dir,
|
|
235
|
+
sample_id=sample_id,
|
|
236
|
+
get_recombinant=get_recombinant,
|
|
237
|
+
trim_first_g=trim_first_g,
|
|
238
|
+
protospacer_A_length=protospacer_A_length,
|
|
239
|
+
protospacer_B_length=protospacer_B_length,
|
|
240
|
+
write=write,
|
|
241
|
+
verbose=verbose
|
|
242
|
+
)
|
|
243
|
+
counts[sample_id] = cnt['mapped']
|
|
244
|
+
if get_recombinant:
|
|
245
|
+
recombinants[sample_id] = cnt['recombinant']
|
|
246
|
+
|
|
247
|
+
counts_mat = pd.concat([
|
|
248
|
+
counts[sample_id].to_pandas().set_index('sgID_AB')['count'].rename(sample_id)
|
|
249
|
+
for sample_id in counts.keys()
|
|
250
|
+
],axis=1).fillna(0)
|
|
251
|
+
|
|
252
|
+
else:
|
|
253
|
+
raise ValueError("Invalid library type. Please choose from 'single_guide_design' or 'dual_guide_design'.")
|
|
254
|
+
|
|
255
|
+
if cas_type == 'cas12':
|
|
256
|
+
# TODO: Implement codes to build count matrix for given samples
|
|
257
|
+
raise NotImplementedError("Cas12 count matrix is not yet implemented.")
|
|
258
|
+
|
|
259
|
+
self.counts_dict = counts
|
|
260
|
+
self.counts_mat = counts_mat
|
|
261
|
+
if get_recombinant:
|
|
262
|
+
self.recombinants = recombinants
|
|
263
|
+
|
|
264
|
+
def load_counts_matrix(self, counts_mat_path, **kwargs):
|
|
265
|
+
'''Load count matrix from file
|
|
266
|
+
'''
|
|
267
|
+
self.counts_mat = pd.read_csv(counts_mat_path, **kwargs)
|
|
268
|
+
|
|
269
|
+
def _build_cas9_dual_guide_var_table(self, counts_table, source, ctrl_label='negative_control'):
|
|
270
|
+
'''Build variant table for dual guide design
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
counts_table (pd.DataFrame): count table for dual guide design (e.g. main library mapped counts or recombinant counts)
|
|
274
|
+
'''
|
|
275
|
+
if source=='library':
|
|
276
|
+
var_table = pd.DataFrame(
|
|
277
|
+
counts_table.index.str.split('|').to_list(),
|
|
278
|
+
index = counts_table.index.to_list(),
|
|
279
|
+
columns=['sgID_A','sgID_B']
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
elif source=='recombinant':
|
|
283
|
+
var_table = pd.DataFrame(
|
|
284
|
+
counts_table.index.to_list(),
|
|
285
|
+
index = ['|'.join(i) for i in counts_table.index.to_list()],
|
|
286
|
+
columns=['sgID_A','sgID_B']
|
|
287
|
+
)
|
|
288
|
+
var_table.index.name = 'sgID_AB'
|
|
289
|
+
|
|
290
|
+
sgRNA_table = self.sgRNA_table.to_pandas().set_index('sgID')
|
|
291
|
+
|
|
292
|
+
#TODO: extract "target" values from protospacer IDs
|
|
293
|
+
var_table = pd.concat([
|
|
294
|
+
var_table.reset_index().reset_index(drop=True),
|
|
295
|
+
sgRNA_table.loc[var_table['sgID_A']].rename(columns={'target':'target_A', 'protospacer':'protospacer_A'}).reset_index(drop=True),
|
|
296
|
+
sgRNA_table.loc[var_table['sgID_B']].rename(columns={'target':'target_B', 'protospacer':'protospacer_B'}).reset_index(drop=True),
|
|
297
|
+
], axis=1).set_index('sgID_AB')
|
|
298
|
+
|
|
299
|
+
var_table['targetType'] = ''
|
|
300
|
+
var_table['target'] = ''
|
|
301
|
+
|
|
302
|
+
### assign target types: negative_control
|
|
303
|
+
control_targets = (var_table.target_A.eq(ctrl_label)) & (var_table.target_B.eq(ctrl_label))
|
|
304
|
+
var_table.loc[control_targets,'targetType'] = 'negative_control'
|
|
305
|
+
var_table.loc[control_targets,'target'] = ctrl_label
|
|
306
|
+
|
|
307
|
+
### assign target types: gene
|
|
308
|
+
same_gene_targets = (var_table.target_A == var_table.target_B) & ~(var_table.target_A.eq(ctrl_label)) & ~(var_table.target_B.eq(ctrl_label))
|
|
309
|
+
var_table.loc[same_gene_targets,'targetType'] = 'gene'
|
|
310
|
+
var_table.loc[same_gene_targets,'target'] = var_table.target_A # or target_B
|
|
311
|
+
|
|
312
|
+
### assign target types: gene-negative_control
|
|
313
|
+
gene_control_targets = ~(var_table.target_A.eq(ctrl_label)) & (var_table.target_B.eq(ctrl_label))
|
|
314
|
+
var_table.loc[gene_control_targets,'targetType'] = 'gene--negative_control'
|
|
315
|
+
var_table.loc[gene_control_targets,'target'] = var_table.target_A + '|' + var_table.target_B
|
|
316
|
+
|
|
317
|
+
### assign target types: negative_control-gene
|
|
318
|
+
control_gene_targets = (var_table.target_A.eq(ctrl_label)) & ~(var_table.target_B.eq(ctrl_label))
|
|
319
|
+
var_table.loc[control_gene_targets,'targetType'] = 'negative_control--negative_control'
|
|
320
|
+
var_table.loc[control_gene_targets,'target'] = var_table.target_A + '|' + var_table.target_B
|
|
321
|
+
|
|
322
|
+
### assign target types: gene-gene
|
|
323
|
+
gene_gene_targets = (var_table.target_A != var_table.target_B) & ~(var_table.target_A.eq(ctrl_label)) & ~(var_table.target_B.eq(ctrl_label))
|
|
324
|
+
var_table.loc[gene_gene_targets,'targetType'] = 'gene--gene'
|
|
325
|
+
var_table.loc[gene_gene_targets,'target'] = var_table.target_A + '|' + var_table.target_B
|
|
326
|
+
|
|
327
|
+
var_table.index.name = None
|
|
328
|
+
var_table.targetType = pd.Categorical(
|
|
329
|
+
var_table.targetType, categories=[
|
|
330
|
+
'gene','gene--gene',
|
|
331
|
+
'gene--negative_control','negative_control--gene',
|
|
332
|
+
'negative_control'
|
|
333
|
+
]
|
|
334
|
+
).remove_unused_categories()
|
|
335
|
+
|
|
336
|
+
var_table['sequence'] = var_table['protospacer_A'] + ';' + var_table['protospacer_B']
|
|
337
|
+
|
|
338
|
+
return var_table
|
|
339
|
+
|
|
340
|
+
def build_counts_anndata(self, source='library', verbose=False):
|
|
341
|
+
'''Build AnnData object from count matrix
|
|
342
|
+
'''
|
|
343
|
+
if source == 'recombinant' and self.library_type == "single_guide_design":
|
|
344
|
+
raise ValueError("Recombinants are not applicable for single guide design!")
|
|
345
|
+
if source == 'recombinant' and self.recombinants is None:
|
|
346
|
+
raise ValueError("Recombinants are not available. If applicable, please set get_recombinant=True in get_counts_matrix method.")
|
|
347
|
+
|
|
348
|
+
if self.library_type == "single_guide_design":
|
|
349
|
+
adata = ad.AnnData(
|
|
350
|
+
X = self.counts_mat.T,
|
|
351
|
+
var = self.library.to_pandas().set_index('sgID')
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
elif self.library_type == "dual_guide_design":
|
|
355
|
+
|
|
356
|
+
adata = ad.AnnData(
|
|
357
|
+
X = self.counts_mat.T,
|
|
358
|
+
var = self._build_cas9_dual_guide_var_table(self.counts_mat, source='library')
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if source == 'recombinant':
|
|
362
|
+
counts_recombinants = {}
|
|
363
|
+
|
|
364
|
+
for sample in self.recombinants.keys():
|
|
365
|
+
if verbose: print(green(sample, ['bold']))
|
|
366
|
+
d = self.recombinants[sample].drop_nulls()
|
|
367
|
+
d = d.to_pandas()
|
|
368
|
+
counts_recombinants[sample] = d.set_index(['sgID_A','sgID_B'])['count']
|
|
369
|
+
if verbose: print('recombinant count added ...')
|
|
370
|
+
|
|
371
|
+
counts_recombinants = pd.concat(counts_recombinants,axis=1).fillna(0)
|
|
372
|
+
|
|
373
|
+
if verbose: print('recombinant count matrix built ...')
|
|
374
|
+
|
|
375
|
+
var_table = self._build_cas9_dual_guide_var_table(counts_recombinants, source='recombinant')
|
|
376
|
+
|
|
377
|
+
rdata = ad.AnnData(
|
|
378
|
+
X = counts_recombinants.T.to_numpy(),
|
|
379
|
+
var = var_table,
|
|
380
|
+
obs = adata.obs
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
if verbose: print('recombinant AnnData created.')
|
|
384
|
+
|
|
385
|
+
if source == 'mapped' or source == 'library':
|
|
386
|
+
return adata
|
|
387
|
+
elif source == 'recombinant':
|
|
388
|
+
return rdata
|
|
389
|
+
else:
|
|
390
|
+
raise ValueError("Invalid source argument. Please choose from 'mapped', 'recombinant' or 'library'. Note: 'mapped' and 'library' act the same way.")
|