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.
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.")