zipstrain 0.2.4__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.
- zipstrain/__init__.py +7 -0
- zipstrain/cli.py +808 -0
- zipstrain/compare.py +377 -0
- zipstrain/database.py +871 -0
- zipstrain/profile.py +221 -0
- zipstrain/task_manager.py +1978 -0
- zipstrain/utils.py +451 -0
- zipstrain/visualize.py +586 -0
- zipstrain-0.2.4.dist-info/METADATA +27 -0
- zipstrain-0.2.4.dist-info/RECORD +12 -0
- zipstrain-0.2.4.dist-info/WHEEL +4 -0
- zipstrain-0.2.4.dist-info/entry_points.txt +3 -0
zipstrain/cli.py
ADDED
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
"""
|
|
2
|
+
zipstrain.utils
|
|
3
|
+
========================
|
|
4
|
+
This module contains the command-line interface (CLI) implementation for the zipstrain application.
|
|
5
|
+
"""
|
|
6
|
+
import click as click
|
|
7
|
+
import zipstrain.utils as ut
|
|
8
|
+
import zipstrain.compare as cp
|
|
9
|
+
import zipstrain.profile as pf
|
|
10
|
+
import zipstrain.task_manager as tm
|
|
11
|
+
import zipstrain.database as db
|
|
12
|
+
import polars as pl
|
|
13
|
+
import pathlib
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def cli():
|
|
18
|
+
"""ZipStrain CLI"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@cli.group()
|
|
22
|
+
def utilities():
|
|
23
|
+
"""The commands in this group are related to various utility functions that mainly prepare input files for profiling and comparison."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@utilities.command("build-null-model")
|
|
27
|
+
@click.option('--error-rate', '-e', default=0.001, help="Error rate for the sequencing technology.")
|
|
28
|
+
@click.option('--max-total-reads', '-m', default=10000, help="Maximum coverage to consider for a base")
|
|
29
|
+
@click.option('--p-threshold', '-p', default=0.05, help="Significance threshold for the Poisson distribution.")
|
|
30
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the output Parquet file.")
|
|
31
|
+
@click.option('--model-type', '-t', default="poisson", type=click.Choice(['poisson']), help="Type of null model to build.")
|
|
32
|
+
def build_null_model(error_rate, max_total_reads, p_threshold, output_file, model_type):
|
|
33
|
+
"""
|
|
34
|
+
Build a null model for sequencing errors based on the Poisson distribution.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
error_rate (float): Error rate for the sequencing technology.
|
|
38
|
+
max_total_reads (int): Maximum total reads to consider.
|
|
39
|
+
p_threshold (float): Significance threshold for the Poisson distribution.
|
|
40
|
+
"""
|
|
41
|
+
if model_type == "poisson":
|
|
42
|
+
df_thresh = ut.build_null_poisson(error_rate, max_total_reads, p_threshold)
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f"Unsupported model type: {model_type}")
|
|
45
|
+
df_thresh = pl.DataFrame(df_thresh, schema=["cov", "max_error_count"])
|
|
46
|
+
df_thresh.write_parquet(output_file)
|
|
47
|
+
|
|
48
|
+
@utilities.command("merge_parquet")
|
|
49
|
+
@click.option('--input-dir', '-i', required=True, help="Directory containing Parquet files to merge.")
|
|
50
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the merged Parquet file.")
|
|
51
|
+
def merge_parquet(input_dir, output_file):
|
|
52
|
+
"""
|
|
53
|
+
Merge multiple Parquet files in a directory into a single Parquet file, adding gene information.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
input_dir (str): Directory containing Parquet files to merge.
|
|
57
|
+
output_file (str): Path to save the merged Parquet file.
|
|
58
|
+
"""
|
|
59
|
+
input_path = pathlib.Path(input_dir)
|
|
60
|
+
parquet_files = list(input_path.glob("*.parquet"))
|
|
61
|
+
if not parquet_files:
|
|
62
|
+
raise ValueError(f"No Parquet files found in directory: {input_dir}")
|
|
63
|
+
|
|
64
|
+
mpileup_df = pl.concat([pl.scan_parquet(pf) for pf in parquet_files])
|
|
65
|
+
mpileup_df.sink_parquet(pathlib.Path(output_file), compression='zstd')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@utilities.command("process_mpileup")
|
|
69
|
+
@click.option('--gene-range-table-loc', '-g', required=True, help="Location of the gene range table in TSV format.")
|
|
70
|
+
@click.option('--batch-bed', '-b', required=True, help="Location of the batch BED file.")
|
|
71
|
+
@click.option('--batch-size', '-s', default=10000, help="Buffer size for processing stdin from samtools.")
|
|
72
|
+
@click.option('--output-file', '-o', required=True, help="Location to save the output Parquet file.")
|
|
73
|
+
def process_mpileup(gene_range_table_loc, batch_bed, batch_size, output_file):
|
|
74
|
+
"""
|
|
75
|
+
Process mpileup files and save the results in a Parquet file.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
gene_range_table_loc (str): Path to the gene range table in TSV format.
|
|
79
|
+
batch_bed (str): Path to the batch BED file.
|
|
80
|
+
output_file (str): Path to save the output Parquet file.
|
|
81
|
+
"""
|
|
82
|
+
ut.process_mpileup_function(gene_range_table_loc, batch_bed, batch_size, output_file)
|
|
83
|
+
|
|
84
|
+
@utilities.command("make_bed")
|
|
85
|
+
@click.option('--db-fasta-dir', '-d', required=True, help="Path to the database in fasta format.")
|
|
86
|
+
@click.option('--max-scaffold-length', '-m', default=500000, help="Maximum scaffold length to split into multiple entries.")
|
|
87
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the output BED file.")
|
|
88
|
+
def make_bed(db_fasta_dir, max_scaffold_length, output_file):
|
|
89
|
+
"""
|
|
90
|
+
Create a BED file from the database in fasta format.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
db_fasta_dir (str): Path to the fasta file.
|
|
94
|
+
max_scaffold_length (int): Splits scaffolds longer than this into multiple entries of length <= max_scaffold_length.
|
|
95
|
+
output_file (str): Path to save the output BED file.
|
|
96
|
+
"""
|
|
97
|
+
bed_df = ut.make_the_bed(db_fasta_dir, max_scaffold_length)
|
|
98
|
+
bed_df.write_csv(output_file, separator='\t', include_header=False)
|
|
99
|
+
|
|
100
|
+
@utilities.command("get_genome_lengths")
|
|
101
|
+
@click.option('--stb-file', '-s', required=True, help="Path to the scaffold-to-genome mapping file.")
|
|
102
|
+
@click.option('--bed-file', '-b', required=True, help="Path to the BED file.")
|
|
103
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the output Parquet file.")
|
|
104
|
+
def get_genome_lengths(stb_file, bed_file, output_file):
|
|
105
|
+
"""
|
|
106
|
+
Extract the genome length information from the scaffold-to-genome mapping table.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
stb_file (str): Path to the scaffold-to-genome mapping file.
|
|
110
|
+
bed_file (str): Path to the BED file containing genomic regions.
|
|
111
|
+
output_file (str): Path to save the output Parquet file.
|
|
112
|
+
"""
|
|
113
|
+
stb = pl.scan_csv(stb_file, separator='\t',has_header=False).with_columns(
|
|
114
|
+
pl.col("column_1").alias("scaffold"),
|
|
115
|
+
pl.col("column_2").alias("genome")
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
bed_table = pl.scan_csv(bed_file, separator='\t',has_header=False).with_columns(
|
|
119
|
+
pl.col("column_1").alias("scaffold"),
|
|
120
|
+
pl.col("column_2").cast(pl.Int64).alias("start"),
|
|
121
|
+
pl.col("column_3").cast(pl.Int64).alias("end")
|
|
122
|
+
).select(["scaffold", "start", "end"])
|
|
123
|
+
genome_length = ut.extract_genome_length(stb, bed_table)
|
|
124
|
+
genome_length.sink_parquet(output_file, compression='zstd')
|
|
125
|
+
|
|
126
|
+
@utilities.command("genome_breadth_matrix")
|
|
127
|
+
@click.option('--profile', '-p', type=str, required=True, help="Path to the profile Parquet file.")
|
|
128
|
+
@click.option('--genome-length', '-g', type=str, required=True, help="Path to the genome length Parquet file.")
|
|
129
|
+
@click.option('--stb', '-s', type=str, required=True, help="Path to the scaffold-to-genome mapping file.")
|
|
130
|
+
@click.option('--min-cov', '-c', default=1, help="Minimum coverage to consider a position.")
|
|
131
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the output Parquet file.")
|
|
132
|
+
def genome_breadth_matrix(profile, genome_length, stb, min_cov, output_file):
|
|
133
|
+
"""
|
|
134
|
+
Generate a genome breadth matrix from the given profiles and scaffold-to-genome mapping.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
profiles (list): List of profiles in the format 'name:path_to_profile'.
|
|
138
|
+
genome_length (str): Path to the genome length Parquet file.
|
|
139
|
+
stb_file (str): Path to the scaffold-to-genome mapping file.
|
|
140
|
+
min_cov (int): Minimum coverage to consider a position.
|
|
141
|
+
output_file (str): Path to save the output Parquet file.
|
|
142
|
+
"""
|
|
143
|
+
genome_length = pl.scan_parquet(genome_length)
|
|
144
|
+
stb = pl.scan_csv(stb, separator='\t', has_header=False).select(
|
|
145
|
+
pl.col("column_1").alias("scaffold"),
|
|
146
|
+
pl.col("column_2").alias("genome")
|
|
147
|
+
)
|
|
148
|
+
profile_dir= pathlib.Path(profile)
|
|
149
|
+
profile = pl.scan_parquet(profile)
|
|
150
|
+
lf=ut.get_genome_breadth_matrix(profile,profile_dir.name, genome_length,stb, min_cov)
|
|
151
|
+
lf.sink_parquet(output_file, compression='zstd')
|
|
152
|
+
|
|
153
|
+
@utilities.command("collect_breadth_tables")
|
|
154
|
+
@click.option('--breadth-tables-dir', '-d', required=True, help="Directory containing breadth tables in Parquet format.")
|
|
155
|
+
@click.option('--extension', '-e', default='parquet', help="File extension of the breadth tables.")
|
|
156
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the collected breadth tables.")
|
|
157
|
+
def collect_breadth(breadth_tables_dir, extension, output_file):
|
|
158
|
+
"""
|
|
159
|
+
Collect multiple genome breadth tables into a single LazyFrame.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
breadth_tables_dir (str): Directory containing breadth tables in Parquet format.
|
|
163
|
+
extension (str): File extension of the breadth tables.
|
|
164
|
+
output_file (str): Path to save the collected breadth tables.
|
|
165
|
+
"""
|
|
166
|
+
breadth_tables = list(pathlib.Path(breadth_tables_dir).glob(f"*.{extension}"))
|
|
167
|
+
if not breadth_tables:
|
|
168
|
+
raise ValueError(f"No breadth tables found in directory: {breadth_tables_dir}")
|
|
169
|
+
|
|
170
|
+
lazy_frames = [pl.scan_parquet(str(pf)) for pf in breadth_tables]
|
|
171
|
+
combined_lf = ut.collect_breadth_tables(lazy_frames)
|
|
172
|
+
combined_lf.sink_parquet(output_file, compression='zstd')
|
|
173
|
+
|
|
174
|
+
@utilities.command("strain_heterogeneity")
|
|
175
|
+
@click.option('--profile-file', '-p', required=True, help="Path to the profile Parquet file.")
|
|
176
|
+
@click.option('--stb-file', '-s', required=True, help="Path to the scaffold-to-genome mapping file.")
|
|
177
|
+
@click.option('--min-cov', '-c', default=5, help="Minimum coverage to consider a position.")
|
|
178
|
+
@click.option('--freq-threshold', '-f', default=0.8, help="Frequency threshold to define dominant nucleotide.")
|
|
179
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the output Parquet file.")
|
|
180
|
+
def strain_heterogeneity(profile_file, stb_file, min_cov, freq_threshold, output_file):
|
|
181
|
+
"""
|
|
182
|
+
Calculate strain heterogeneity for each genome based on nucleotide frequencies.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
profile_file (str): Path to the profile Parquet file.
|
|
186
|
+
stb_file (str): Path to the scaffold-to-genome mapping file.
|
|
187
|
+
min_cov (int): Minimum coverage to consider a position.
|
|
188
|
+
freq_threshold (float): Frequency threshold to define dominant nucleotide.
|
|
189
|
+
output_file (str): Path to save the output Parquet file.
|
|
190
|
+
"""
|
|
191
|
+
profile = pl.scan_parquet(profile_file)
|
|
192
|
+
stb = pl.scan_csv(stb_file, separator="\t", has_header=False).with_columns(
|
|
193
|
+
pl.col("column_1").alias("scaffold"),
|
|
194
|
+
pl.col("column_2").alias("genome")
|
|
195
|
+
).select(["scaffold", "genome"])
|
|
196
|
+
|
|
197
|
+
het_profile = pf.get_strain_hetrogeneity(profile, stb, min_cov=min_cov, freq_threshold=freq_threshold)
|
|
198
|
+
het_profile.sink_parquet(output_file, compression='zstd')
|
|
199
|
+
|
|
200
|
+
@utilities.command("build-profile-db")
|
|
201
|
+
@click.option('--profile-db-csv', '-p', required=True, help="Path to the profile database CSV file.")
|
|
202
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the output Parquet file.")
|
|
203
|
+
def build_profile_db(profile_db_csv, output_file):
|
|
204
|
+
"""
|
|
205
|
+
Build a profile database from the given CSV file.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
profile_db_csv (str): Path to the profile database CSV file.
|
|
209
|
+
"""
|
|
210
|
+
profile_db = db.ProfileDatabase.from_csv(pathlib.Path(profile_db_csv))
|
|
211
|
+
profile_db.save_as_new_database(pathlib.Path(output_file))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@utilities.command("build-comparison-config")
|
|
215
|
+
@click.option('--profile-db', '-p', required=True, help="Path to the profile database Parquet file.")
|
|
216
|
+
@click.option('--gene-db-id', '-g', required=True, help="Gene database ID.")
|
|
217
|
+
@click.option('--reference-db-id', '-r', required=True, help="Reference fasta ID.")
|
|
218
|
+
@click.option('--scope', '-s', default="all", help="Genome scope for comparison.")
|
|
219
|
+
@click.option('--min-cov', '-c', default=5, help="Minimum coverage to consider a position.")
|
|
220
|
+
@click.option('--min-gene-compare-len', '-l', default=200, help="Minimum gene length to consider for comparison.")
|
|
221
|
+
@click.option('--null-model-p-value', '-n', default=0.05, help="P-value threshold for the null model to detect sequencing error.")
|
|
222
|
+
@click.option('--stb-file-loc', '-t', required=True, help="Path to the scaffold-to-genome mapping file.")
|
|
223
|
+
@click.option('--null-model-loc', '-m', required=True, help="Path to the null model Parquet file.")
|
|
224
|
+
@click.option('--current-comp-table', '-a', default=None, help="Path to the current comparison table in Parquet format.")
|
|
225
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the output configuration JSON file.")
|
|
226
|
+
def build_comparison_config(profile_db, gene_db_id, reference_genome_id, scope, min_cov, min_gene_compare_len, null_model_p_value, stb_file_loc, null_model_loc, current_comp_table, output_file):
|
|
227
|
+
"""
|
|
228
|
+
Build a comparison configuration JSON file from the given parameters.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
profile_db (str): Path to the profile database Parquet file.
|
|
232
|
+
gene_db_id (str): Gene database ID.
|
|
233
|
+
reference_genome_id (str): Reference genome ID.
|
|
234
|
+
scope (str): Genome scope for comparison.
|
|
235
|
+
min_cov (int): Minimum coverage to consider a position.
|
|
236
|
+
min_gene_compare_len (int): Minimum gene length to consider for comparison.
|
|
237
|
+
null_model_p_value (float): P-value threshold for the null model to detect sequencing error.
|
|
238
|
+
stb_file_loc (str): Path to the scaffold-to-genome mapping file.
|
|
239
|
+
null_model_loc (str): Path to the null model Parquet file.
|
|
240
|
+
current_comp_table (str): Path to the current comparison table in Parquet format.
|
|
241
|
+
output_file (str): Path to save the output configuration JSON file.
|
|
242
|
+
"""
|
|
243
|
+
conf_obj=db.GenomeComparisonConfig(
|
|
244
|
+
gene_db_id=gene_db_id,
|
|
245
|
+
reference_id=reference_genome_id,
|
|
246
|
+
scope=scope,
|
|
247
|
+
min_cov=min_cov,
|
|
248
|
+
min_gene_compare_len=min_gene_compare_len,
|
|
249
|
+
null_model_p_value=null_model_p_value,
|
|
250
|
+
stb_file_loc=stb_file_loc,
|
|
251
|
+
null_model_loc=null_model_loc,
|
|
252
|
+
)
|
|
253
|
+
comp_obj=db.GenomeComparisonDatabase(
|
|
254
|
+
profile_db=profile_db,
|
|
255
|
+
config=conf_obj,
|
|
256
|
+
comp_db_loc=current_comp_table
|
|
257
|
+
)
|
|
258
|
+
comp_obj.dump_obj(pathlib.Path(output_file))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@utilities.command("to-complete-table")
|
|
262
|
+
@click.option("--genome-comparison-object", "-g", required=True, help="Path to the genome comparison object in json format.")
|
|
263
|
+
@click.option("--output-file", "-o", required=True, help="Path to save the completed pairs csv file.")
|
|
264
|
+
def to_complete_table(genome_comparison_object, output_file):
|
|
265
|
+
"""
|
|
266
|
+
Generate a table of completed genome comparison pairs and save it to a csv file.
|
|
267
|
+
|
|
268
|
+
Parameters:
|
|
269
|
+
genome_comparison_object (str): Path to the genome comparison object in json format.
|
|
270
|
+
output_file (str): Path to save the completed pairs Parquet file.
|
|
271
|
+
"""
|
|
272
|
+
genome_comp_db=db.GenomeComparisonDatabase.load_obj(pathlib.Path(genome_comparison_object))
|
|
273
|
+
completed_pairs=genome_comp_db.to_complete_input_table()
|
|
274
|
+
completed_pairs.sink_csv(pathlib.Path(output_file), compression='zstd', engine="streaming")
|
|
275
|
+
|
|
276
|
+
@utilities.command("presence-profile")
|
|
277
|
+
@click.option('--profile-file', '-p', required=True, help="Path to the profile Parquet file.")
|
|
278
|
+
@click.option('--stb-file', '-s', required=True, help="Path to the scaffold-to-genome mapping file.")
|
|
279
|
+
@click.option('--bed-file', '-b', required=True, help="Path to the BED file.")
|
|
280
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the output Parquet file.")
|
|
281
|
+
def presence_profile(profile_file, stb_file, bed_file, output_file):
|
|
282
|
+
"""
|
|
283
|
+
Generate a presence profile from the given files.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
profile_file (str): Path to the profile Parquet file.
|
|
287
|
+
stb_file (str): Path to the scaffold-to-genome mapping file.
|
|
288
|
+
bed_file (str): Path to the BED file.
|
|
289
|
+
"""
|
|
290
|
+
profile=pl.scan_parquet(profile_file)
|
|
291
|
+
stb = pl.scan_csv(stb_file, separator="\t", has_header=False).with_columns(
|
|
292
|
+
pl.col("column_1").alias("scaffold"),
|
|
293
|
+
pl.col("column_2").alias("genome")
|
|
294
|
+
).select(["scaffold", "genome"])
|
|
295
|
+
bed = pl.scan_csv(bed_file, separator="\t", has_header=False).with_columns(
|
|
296
|
+
pl.col("column_1").alias("scaffold"),
|
|
297
|
+
pl.col("column_2").alias("start"),
|
|
298
|
+
pl.col("column_3").alias("end")
|
|
299
|
+
).select(["scaffold", "start", "end"])
|
|
300
|
+
ut.estimate_genome_presence(
|
|
301
|
+
profile=profile,
|
|
302
|
+
stb=stb,
|
|
303
|
+
bed=bed
|
|
304
|
+
).sink_parquet(output_file, compression='zstd',engine="streaming")
|
|
305
|
+
|
|
306
|
+
@cli.group()
|
|
307
|
+
def gene_tools():
|
|
308
|
+
"""Holds anything related to gene analysis."""
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@gene_tools.command("gene-range-table")
|
|
313
|
+
@click.option('--gene-file', '-g', required=True, help="location of gene file. Prodigal's nucleotide fasta output")
|
|
314
|
+
@click.option('--output-file', '-o', required=True, help="location to save output tsv file")
|
|
315
|
+
def get_gene_range_table(gene_file, output_file):
|
|
316
|
+
"""
|
|
317
|
+
Main function to build and save the gene location table.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
gene_file (str): Path to the gene FASTA file.
|
|
321
|
+
output_file (str): Path to save the output TSV file.
|
|
322
|
+
"""
|
|
323
|
+
gene_locs=pf.build_gene_range_table(pathlib.Path(gene_file))
|
|
324
|
+
gene_locs.sink_csv(pathlib.Path(output_file), separator="\t", include_header=False)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@gene_tools.command("gene-loc-table")
|
|
328
|
+
@click.option('--gene-file', '-g', required=True, help="location of gene file. Prodigal's nucleotide fasta output")
|
|
329
|
+
@click.option('--scaffold-list', '-s', required=True, help="location of scaffold list. A text file with each line containing a scaffold name.")
|
|
330
|
+
@click.option('--output-file', '-o', required=True, help="location to save output parquet file")
|
|
331
|
+
def get_gene_loc_table(gene_file, scaffold_list, output_file):
|
|
332
|
+
"""
|
|
333
|
+
Main function to build and save the gene location table.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
gene_file (str): Path to the gene FASTA file.
|
|
337
|
+
scaffold_list (str): Path to the scaffold list file.
|
|
338
|
+
output_file (str): Path to save the output Parquet file.
|
|
339
|
+
"""
|
|
340
|
+
scaffolds=set(pl.read_csv(pathlib.Path(scaffold_list), has_header=False,separator="\t").select(pl.col("column_1")).to_series().to_list())
|
|
341
|
+
gene_locs=pf.build_gene_loc_table(pathlib.Path(gene_file), scaffolds)
|
|
342
|
+
gene_locs.write_parquet(pathlib.Path(output_file))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@cli.group()
|
|
347
|
+
def compare():
|
|
348
|
+
"""The commands in this group are related to comparing profiled samples."""
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
@compare.command("single_compare_genome")
|
|
352
|
+
@click.option('--mpileup-contig-1', '-m1', required=True, help="Path to the first mpileup file.")
|
|
353
|
+
@click.option('--mpileup-contig-2', '-m2', required=True, help="Path to the second mpileup file.")
|
|
354
|
+
@click.option('--scaffolds-1', '-s1', required=True, help="Path to the list of scaffolds for the first mpileup file.")
|
|
355
|
+
@click.option('--scaffolds-2', '-s2', required=True, help="Path to the list of scaffolds for the second mpileup file.")
|
|
356
|
+
@click.option('--null-model', '-n', required=True, help="Path to the null model Parquet file.")
|
|
357
|
+
@click.option('--stb-file', '-s', required=True, help="Path to the scaffold to genome mapping file.")
|
|
358
|
+
@click.option('--min-cov', '-c', default=5, help="Minimum coverage to consider a position.")
|
|
359
|
+
@click.option('--min-gene-compare-len', '-l', default=100, help="Minimum gene length to consider for comparison.")
|
|
360
|
+
@click.option('--memory-mode', '-m', default="heavy", type=click.Choice(['heavy', 'light'], case_sensitive=False), help="Memory mode for processing: 'heavy' or 'light'.")
|
|
361
|
+
@click.option('--chrom-batch-size', '-b', default=10000, help="Batch size for processing chromosomes. Only used in light memory mode.")
|
|
362
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the parquet file.")
|
|
363
|
+
@click.option('--genome', '-g', default="all", help="If provided, do the comparison only for the specified genome.")
|
|
364
|
+
@click.option('--engine', '-e', default="streaming", type=click.Choice(['streaming', 'gpu',"auto"], case_sensitive=False), help="Engine to use for processing: 'streaming', 'gpu' or 'auto'.")
|
|
365
|
+
@click.option('--ani-method', '-a', default="popani", help="ANI calculation method to use (e.g., 'popani', 'conani', 'cosani_0.4').")
|
|
366
|
+
def single_compare_genome(mpileup_contig_1, mpileup_contig_2, scaffolds_1, scaffolds_2, null_model, stb_file, min_cov, min_gene_compare_len, memory_mode, chrom_batch_size, output_file, genome, engine, ani_method):
|
|
367
|
+
"""
|
|
368
|
+
Main function to compare two mpileup files and calculate genome and gene statistics.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
mpileup_contig_1 (str): Path to the first mpileup file.
|
|
372
|
+
mpileup_contig_2 (str): Path to the second mpileup file.
|
|
373
|
+
scaffolds_1 (str): Path to the list of scaffolds for the first mpileup file.
|
|
374
|
+
scaffolds_2 (str): Path to the list of scaffolds for the second mpileup file.
|
|
375
|
+
null_model (str): Path to the null model Parquet file.
|
|
376
|
+
gene_locs (str): Path to the gene locations Parquet file.
|
|
377
|
+
min_cov (int): Minimum coverage to consider a position.
|
|
378
|
+
min_gene_compare_len (int): Minimum gene length to consider for comparison.
|
|
379
|
+
memory_mode (str): Memory mode for processing: 'heavy' or 'light'.
|
|
380
|
+
chrom_batch_size (int): Batch size for processing chromosomes. Only used in light memory mode.
|
|
381
|
+
output_file (str): Path to save the parquet file.
|
|
382
|
+
genome (str): If provided, do the comparison only for the specified genome.
|
|
383
|
+
stb_file (str): Path to the scaffold to genome mapping file.
|
|
384
|
+
"""
|
|
385
|
+
with pl.StringCache():
|
|
386
|
+
mpile_contig_1 = pl.scan_parquet(mpileup_contig_1).with_columns(
|
|
387
|
+
(pl.col("chrom").cast(pl.Categorical).alias("chrom"),
|
|
388
|
+
pl.col("gene").cast(pl.Categorical).alias("gene"))
|
|
389
|
+
)
|
|
390
|
+
mpile_contig_2 = pl.scan_parquet(mpileup_contig_2).with_columns(
|
|
391
|
+
(pl.col("chrom").cast(pl.Categorical).alias("chrom"),
|
|
392
|
+
pl.col("gene").cast(pl.Categorical).alias("gene"))
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
stb = pl.scan_csv(stb_file, separator="\t", has_header=False).with_columns(
|
|
396
|
+
pl.col("column_1").alias("scaffold").cast(pl.Categorical),
|
|
397
|
+
pl.col("column_2").alias("genome").cast(pl.Categorical)
|
|
398
|
+
).select(["scaffold", "genome"])
|
|
399
|
+
if genome != "all":
|
|
400
|
+
stb = stb.filter(pl.col("genome") == genome)
|
|
401
|
+
|
|
402
|
+
null_model = pl.scan_parquet(null_model)
|
|
403
|
+
mpile_contig_1_name = pathlib.Path(mpileup_contig_1).name
|
|
404
|
+
mpile_contig_2_name = pathlib.Path(mpileup_contig_2).name
|
|
405
|
+
if genome != "all":
|
|
406
|
+
scaffold_scope = stb.filter(pl.col("genome") == genome).collect()["scaffold"].to_list()
|
|
407
|
+
else:
|
|
408
|
+
scaffold_scope = None
|
|
409
|
+
|
|
410
|
+
if memory_mode == "light":
|
|
411
|
+
scaffolds_1 = pl.scan_csv(scaffolds_1, separator="\t", has_header=False).select(pl.col("column_1").alias("scaffold"))
|
|
412
|
+
scaffolds_2 = pl.scan_csv(scaffolds_2, separator="\t", has_header=False).select(pl.col("column_1").alias("scaffold"))
|
|
413
|
+
shared_scaffolds = list(set(scaffolds_1["scaffold"].to_list()).intersection(set(scaffolds_2["scaffold"].to_list())))
|
|
414
|
+
mpile_contig_1 = mpile_contig_1.filter(pl.col("chrom").is_in(shared_scaffolds))
|
|
415
|
+
mpile_contig_2 = mpile_contig_2.filter(pl.col("chrom").is_in(shared_scaffolds))
|
|
416
|
+
else:
|
|
417
|
+
shared_scaffolds=None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
comp = cp.compare_genomes(mpile_contig_1=mpile_contig_1,
|
|
421
|
+
mpile_contig_2=mpile_contig_2,
|
|
422
|
+
null_model=null_model,
|
|
423
|
+
scaffold_to_genome=stb,
|
|
424
|
+
min_cov=min_cov,
|
|
425
|
+
min_gene_compare_len=min_gene_compare_len,
|
|
426
|
+
memory_mode=memory_mode,
|
|
427
|
+
chrom_batch_size=chrom_batch_size,
|
|
428
|
+
shared_scaffolds=shared_scaffolds,
|
|
429
|
+
scaffold_scope=scaffold_scope,
|
|
430
|
+
engine=engine,
|
|
431
|
+
ani_method=ani_method)
|
|
432
|
+
comp=comp.join(
|
|
433
|
+
stb.select("genome").unique(),
|
|
434
|
+
left_on=pl.col("genome"),
|
|
435
|
+
right_on=pl.col("genome"),
|
|
436
|
+
how="full",
|
|
437
|
+
coalesce=True
|
|
438
|
+
).fill_null(0)
|
|
439
|
+
|
|
440
|
+
comp=comp.with_columns(pl.lit(mpile_contig_1_name).alias("sample_1"), pl.lit(mpile_contig_2_name).alias("sample_2")).fill_null(0)
|
|
441
|
+
|
|
442
|
+
comp.sink_parquet(output_file,engine=engine)
|
|
443
|
+
|
|
444
|
+
@compare.command("single_compare_gene")
|
|
445
|
+
@click.option('--mpileup-contig-1', '-m1', required=True, help="Path to the first mpileup file.")
|
|
446
|
+
@click.option('--mpileup-contig-2', '-m2', required=True, help="Path to the second mpileup file.")
|
|
447
|
+
@click.option('--null-model', '-n', required=True, help="Path to the null model Parquet file.")
|
|
448
|
+
@click.option('--stb-file', '-s', required=True, help="Path to the scaffold to genome mapping file.")
|
|
449
|
+
@click.option('--min-cov', '-c', default=5, help="Minimum coverage to consider a position.")
|
|
450
|
+
@click.option('--min-gene-compare-len', '-l', default=100, help="Minimum gene length to consider for comparison.")
|
|
451
|
+
@click.option('--output-file', '-o', required=True, help="Path to save the parquet file.")
|
|
452
|
+
@click.option('--scope', '-g', default="all:all", help="If provided, do the comparison only for the specified genome-gene pair.")
|
|
453
|
+
@click.option('--engine', '-e', default="streaming", type=click.Choice(['streaming', 'gpu',"auto"], case_sensitive=False), help="Engine to use for processing: 'streaming', 'gpu' or 'auto'.")
|
|
454
|
+
@click.option('--ani-method', '-a', default="popani", help="ANI calculation method to use (e.g., 'popani', 'conani', 'cosani_0.4').")
|
|
455
|
+
def single_compare_gene(mpileup_contig_1, mpileup_contig_2, null_model, stb_file, min_cov, min_gene_compare_len, output_file, scope, engine, ani_method):
|
|
456
|
+
"""
|
|
457
|
+
Compare two mpileup files and calculate gene-level comparison statistics.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
mpileup_contig_1 (str): Path to the first mpileup file.
|
|
461
|
+
mpileup_contig_2 (str): Path to the second mpileup file.
|
|
462
|
+
null_model (str): Path to the null model Parquet file.
|
|
463
|
+
stb_file (str): Path to the scaffold to genome mapping file.
|
|
464
|
+
min_cov (int): Minimum coverage to consider a position.
|
|
465
|
+
min_gene_compare_len (int): Minimum gene length to consider for comparison.
|
|
466
|
+
output_file (str): Path to save the parquet file.
|
|
467
|
+
scope (str): If provided, do the comparison only for the specified genome-gene pair.
|
|
468
|
+
engine (str): Engine to use for processing: 'streaming', 'gpu' or 'auto'.
|
|
469
|
+
ani_method (str): ANI calculation method to use.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
with pl.StringCache():
|
|
473
|
+
mpile_contig_1 = pl.scan_parquet(mpileup_contig_1).with_columns(
|
|
474
|
+
(pl.col("chrom").cast(pl.Categorical).alias("chrom"),
|
|
475
|
+
pl.col("gene").cast(pl.Categorical).alias("gene"))
|
|
476
|
+
)
|
|
477
|
+
mpile_contig_2 = pl.scan_parquet(mpileup_contig_2).with_columns(
|
|
478
|
+
(pl.col("chrom").cast(pl.Categorical).alias("chrom"),
|
|
479
|
+
pl.col("gene").cast(pl.Categorical).alias("gene"))
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
stb = pl.scan_csv(stb_file, separator="\t", has_header=False).with_columns(
|
|
483
|
+
pl.col("column_1").alias("scaffold").cast(pl.Categorical),
|
|
484
|
+
pl.col("column_2").alias("genome").cast(pl.Categorical)
|
|
485
|
+
).select(["scaffold", "genome"])
|
|
486
|
+
genome_scope, gene_scope = scope.split(":")
|
|
487
|
+
|
|
488
|
+
if genome_scope != "all":
|
|
489
|
+
mpile_contig_1 = mpile_contig_1.filter(pl.col("genome") == genome_scope)
|
|
490
|
+
mpile_contig_2 = mpile_contig_2.filter(pl.col("genome") == genome_scope)
|
|
491
|
+
|
|
492
|
+
if gene_scope != "all":
|
|
493
|
+
mpile_contig_1 = mpile_contig_1.filter(pl.col("gene") == gene_scope)
|
|
494
|
+
mpile_contig_2 = mpile_contig_2.filter(pl.col("gene") == gene_scope)
|
|
495
|
+
|
|
496
|
+
null_model = pl.scan_parquet(null_model)
|
|
497
|
+
mpile_contig_1_name = pathlib.Path(mpileup_contig_1).name
|
|
498
|
+
mpile_contig_2_name = pathlib.Path(mpileup_contig_2).name
|
|
499
|
+
|
|
500
|
+
comp = cp.compare_genes(
|
|
501
|
+
mpile_contig_1=mpile_contig_1,
|
|
502
|
+
mpile_contig_2=mpile_contig_2,
|
|
503
|
+
null_model=null_model,
|
|
504
|
+
scaffold_to_genome=stb,
|
|
505
|
+
min_cov=min_cov,
|
|
506
|
+
min_gene_compare_len=min_gene_compare_len,
|
|
507
|
+
engine=engine,
|
|
508
|
+
ani_method=ani_method
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
comp = comp.with_columns(
|
|
512
|
+
pl.lit(mpile_contig_1_name).alias("sample_1"),
|
|
513
|
+
pl.lit(mpile_contig_2_name).alias("sample_2")
|
|
514
|
+
).fill_null(0)
|
|
515
|
+
|
|
516
|
+
comp.sink_parquet(output_file, engine=engine)
|
|
517
|
+
|
|
518
|
+
@cli.group()
|
|
519
|
+
def profile():
|
|
520
|
+
"""The commands in this group are related to profiling bam files."""
|
|
521
|
+
pass
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@profile.command("prepare_profiling",help="Prepare the files needed for profiling bam files and save them in the specified output directory.")
|
|
525
|
+
@click.option('--reference-fasta', '-r', required=True, help="Path to the reference genome in FASTA format.")
|
|
526
|
+
@click.option('--gene-fasta', '-g', required=True, help="Path to the gene annotations in FASTA format.")
|
|
527
|
+
@click.option('--stb-file', '-s', required=True, help="Path to the scaffold-to-genome mapping file.")
|
|
528
|
+
@click.option('--output-dir', '-o', required=True, help="Directory to save the profiling database.")
|
|
529
|
+
def prepare_profiling(reference_fasta, gene_fasta, stb_file, output_dir):
|
|
530
|
+
"""
|
|
531
|
+
Prepare the files needed for profiling bam files and save them in the specified output directory.
|
|
532
|
+
"""
|
|
533
|
+
output_dir=pathlib.Path(output_dir)
|
|
534
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
535
|
+
bed_df = ut.make_the_bed(reference_fasta)
|
|
536
|
+
bed_df.write_csv(output_dir / "genomes_bed_file.bed", separator='\t', include_header=False)
|
|
537
|
+
gene_range_table = pf.build_gene_range_table(pathlib.Path(gene_fasta))
|
|
538
|
+
gene_range_table.write_csv(output_dir / "gene_range_table.tsv", separator='\t', include_header=False)
|
|
539
|
+
|
|
540
|
+
stb = pl.scan_csv(stb_file, separator='\t',has_header=False).with_columns(
|
|
541
|
+
pl.col("column_1").alias("scaffold"),
|
|
542
|
+
pl.col("column_2").alias("genome")
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
bed_df = bed_df.lazy()
|
|
546
|
+
genome_length = ut.extract_genome_length(stb, bed_df)
|
|
547
|
+
genome_length.sink_parquet(output_dir / "genome_lengths.parquet", compression='zstd')
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@profile.command("profile-single")
|
|
551
|
+
@click.option('--bed-file', '-b', required=True, help="Path to the BED file describing regions to be profiled.")
|
|
552
|
+
@click.option('--bam-file', '-a', required=True, help="Path to the BAM file to be profiled.")
|
|
553
|
+
@click.option('--gene-range-table', '-g', required=True, help="Path to the gene range table.")
|
|
554
|
+
@click.option('--num-workers', '-n', default=1, help="Number of workers to use for profiling.")
|
|
555
|
+
@click.option('--output-dir', '-o', required=True, help="Directory to save the profiling output.")
|
|
556
|
+
def profile_single(bed_file, bam_file, gene_range_table, num_workers, output_dir):
|
|
557
|
+
"""
|
|
558
|
+
Profile a single BAM file using the provided BED file and gene range table.
|
|
559
|
+
|
|
560
|
+
"""
|
|
561
|
+
output_dir=pathlib.Path(output_dir)
|
|
562
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
563
|
+
pf.profile_bam(
|
|
564
|
+
bed_file=bed_file,
|
|
565
|
+
bam_file=bam_file,
|
|
566
|
+
gene_range_table=gene_range_table,
|
|
567
|
+
output_dir=output_dir,
|
|
568
|
+
num_workers=num_workers
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
@cli.group()
|
|
572
|
+
def run():
|
|
573
|
+
"""The commands in this group are related to running zipstrain workflows."""
|
|
574
|
+
pass
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
@run.command("profile")
|
|
578
|
+
@click.option('--input-table', '-i', required=True, help="Path to the input table in TSV format containing sample names and paths to bam files.")
|
|
579
|
+
@click.option('--stb-file', '-s', required=True, help="Path to the scaffold-to-genome mapping file.")
|
|
580
|
+
@click.option('--gene-range-table', '-g', required=True, help="Path to the gene range table file.")
|
|
581
|
+
@click.option('--bed-file', '-b', required=True, help="Path to the BED file for profiling regions.")
|
|
582
|
+
@click.option('--genome-length-file', '-l', required=True, help="Path to the genome length file.")
|
|
583
|
+
@click.option('--run-dir', '-r', required=True, help="Directory to save the run data.")
|
|
584
|
+
@click.option('--num-procs', '-n', default=8, help="Number of processors to use for each profiling task.")
|
|
585
|
+
@click.option('--max-concurrent-batches', '-m', default=5, help="Maximum number of concurrent batches to run.")
|
|
586
|
+
@click.option('--poll-interval', '-p', default=1, help="Polling interval in seconds to check the status of batches.")
|
|
587
|
+
@click.option('--execution-mode', '-e', default="local", help="Execution mode: 'local' or 'slurm'.")
|
|
588
|
+
@click.option('--slurm-config', '-c', default=None, help="Path to the SLURM configuration file in json format. Required if execution mode is 'slurm'.")
|
|
589
|
+
@click.option('--container-engine', '-o', default="local", help="Container engine to use: 'local', 'docker' or 'apptainer'.")
|
|
590
|
+
@click.option('--task-per-batch', '-t', default=10, help="Number of tasks to include in each batch.")
|
|
591
|
+
def profile(input_table, stb_file, gene_range_table, bed_file, genome_length_file, run_dir, num_procs, max_concurrent_batches, poll_interval, execution_mode, slurm_config, container_engine, task_per_batch):
|
|
592
|
+
"""
|
|
593
|
+
Run BAM file profiling in batches using the specified execution mode and container engine.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
input_table (str): Path to the input table in TSV format containing sample names and BAM file paths.
|
|
597
|
+
stb_file (str): Path to the scaffold-to-genome mapping file.
|
|
598
|
+
gene_range_table (str): Path to the gene range table file.
|
|
599
|
+
bed_file (str): Path to the BED file for profiling regions.
|
|
600
|
+
genome_length_file (str): Path to the genome length file.
|
|
601
|
+
run_dir (str): Directory to save the run data.
|
|
602
|
+
num_procs (int): Number of processors to use for each profiling task.
|
|
603
|
+
max_concurrent_batches (int): Maximum number of concurrent batches to run.
|
|
604
|
+
poll_interval (int): Polling interval in seconds to check the status of batches.
|
|
605
|
+
execution_mode (str): Execution mode: 'local' or 'slurm'.
|
|
606
|
+
slurm_config (str): Path to the SLURM configuration file in json format. Required if execution mode is 'slurm'.
|
|
607
|
+
container_engine (str): Container engine to use: 'local', 'docker' or 'apptainer'.
|
|
608
|
+
task_per_batch (int): Number of tasks to include in each batch.
|
|
609
|
+
"""
|
|
610
|
+
# Load the BAM files table
|
|
611
|
+
bams_lf = pl.scan_csv(input_table)
|
|
612
|
+
|
|
613
|
+
# Validate required columns exist
|
|
614
|
+
required_columns = {"sample_name", "bamfile"}
|
|
615
|
+
actual_columns = set(bams_lf.collect_schema().names())
|
|
616
|
+
if not required_columns.issubset(actual_columns):
|
|
617
|
+
missing = required_columns - actual_columns
|
|
618
|
+
raise ValueError(f"Input table missing required columns: {missing}")
|
|
619
|
+
|
|
620
|
+
run_dir = pathlib.Path(run_dir)
|
|
621
|
+
slurm_conf = None
|
|
622
|
+
if execution_mode == "slurm":
|
|
623
|
+
if slurm_config is None:
|
|
624
|
+
raise ValueError("SLURM configuration file must be provided when execution mode is 'slurm'.")
|
|
625
|
+
slurm_conf = tm.SlurmConfig.from_json(slurm_config)
|
|
626
|
+
|
|
627
|
+
if container_engine == "local":
|
|
628
|
+
container_engine_obj = tm.LocalEngine(address="")
|
|
629
|
+
elif container_engine == "docker":
|
|
630
|
+
container_engine_obj = tm.DockerEngine(address="parsaghadermazi/zipstrain:amd64")
|
|
631
|
+
elif container_engine == "apptainer":
|
|
632
|
+
container_engine_obj = tm.ApptainerEngine(address="docker://parsaghadermazi/zipstrain:amd64")
|
|
633
|
+
else:
|
|
634
|
+
raise ValueError("Invalid container engine. Choose from 'local', 'docker', or 'apptainer'.")
|
|
635
|
+
|
|
636
|
+
tm.lazy_run_profile(
|
|
637
|
+
run_dir=run_dir,
|
|
638
|
+
container_engine=container_engine_obj,
|
|
639
|
+
bams_lf=bams_lf,
|
|
640
|
+
stb_file=pathlib.Path(stb_file),
|
|
641
|
+
gene_range_table=pathlib.Path(gene_range_table),
|
|
642
|
+
bed_file=pathlib.Path(bed_file),
|
|
643
|
+
genome_length_file=pathlib.Path(genome_length_file),
|
|
644
|
+
num_procs=num_procs,
|
|
645
|
+
tasks_per_batch=task_per_batch,
|
|
646
|
+
max_concurrent_batches=max_concurrent_batches,
|
|
647
|
+
poll_interval=poll_interval,
|
|
648
|
+
execution_mode=execution_mode,
|
|
649
|
+
slurm_config=slurm_conf,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@run.command("compare_genomes")
|
|
654
|
+
@click.option("--genome-comparison-object", "-g", required=True, help="Path to the genome comparison object in json format.")
|
|
655
|
+
@click.option("--run-dir", "-r", required=True, help="Directory to save the run data.")
|
|
656
|
+
@click.option("--max-concurrent-batches", "-m", default=5, help="Maximum number of concurrent batches to run.")
|
|
657
|
+
@click.option("--poll-interval", "-p", default=1, help="Polling interval in seconds to check the status of batches.")
|
|
658
|
+
@click.option("--execution-mode", "-e", default="local", help="Execution mode: 'local' or 'slurm'.")
|
|
659
|
+
@click.option("--slurm-config", "-s", default=None, help="Path to the SLURM configuration file in json format. Required if execution mode is 'slurm'.")
|
|
660
|
+
@click.option("--container-engine", "-c", default="local", help="Container engine to use: 'local', 'docker' or 'apptainer'.")
|
|
661
|
+
@click.option("--task-per-batch", "-t", default=10, help="Number of tasks to include in each batch.")
|
|
662
|
+
@click.option("--polars-engine", "-a", default="streaming", type=click.Choice(['streaming', 'gpu', 'auto'], case_sensitive=False), help="Polars engine to use: 'streaming', 'gpu' or 'auto'.")
|
|
663
|
+
@click.option("--chrom-batch-size", "-b", default=10000, help="Batch size for processing chromosomes. Only used in light memory mode.")
|
|
664
|
+
@click.option("--memory-mode", "-h", default="heavy", type=click.Choice(['heavy', 'light'], case_sensitive=False), help="Memory mode for processing: 'heavy' or 'light'.")
|
|
665
|
+
def compare_genomes(genome_comparison_object, run_dir, max_concurrent_batches, poll_interval, execution_mode, slurm_config, container_engine, task_per_batch, polars_engine, chrom_batch_size, memory_mode):
|
|
666
|
+
"""
|
|
667
|
+
Run genome comparisons in batches using the specified execution mode and container engine.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
genome_comparison_object (str): Path to the genome comparison object in json format.
|
|
671
|
+
run_dir (str): Directory to save the run data.
|
|
672
|
+
max_concurrent_batches (int): Maximum number of concurrent batches to run.
|
|
673
|
+
poll_interval (int): Polling interval in seconds to check the status of batches.
|
|
674
|
+
execution_mode (str): Execution mode: 'local' or 'slurm'.
|
|
675
|
+
slurm_config (str): Path to the SLURM configuration file in json format. Required if execution mode is 'slurm'.
|
|
676
|
+
container_engine (str): Container engine to use: 'local', 'docker' or 'apptainer'.
|
|
677
|
+
task_per_batch (int): Number of tasks to include in each batch.
|
|
678
|
+
"""
|
|
679
|
+
genome_comp_db=db.GenomeComparisonDatabase.load_obj(pathlib.Path(genome_comparison_object))
|
|
680
|
+
run_dir=pathlib.Path(run_dir)
|
|
681
|
+
slurm_conf=None
|
|
682
|
+
if execution_mode == "slurm":
|
|
683
|
+
if slurm_config is None:
|
|
684
|
+
raise ValueError("SLURM configuration file must be provided when execution mode is 'slurm'.")
|
|
685
|
+
slurm_conf = tm.SlurmConfig.from_json(slurm_config)
|
|
686
|
+
|
|
687
|
+
if container_engine == "local":
|
|
688
|
+
container_engine_obj = tm.LocalEngine(address="")
|
|
689
|
+
elif container_engine == "docker":
|
|
690
|
+
container_engine_obj = tm.DockerEngine(address="parsaghadermazi/zipstrain:amd64") #could go to a toml or json config file
|
|
691
|
+
elif container_engine == "apptainer":
|
|
692
|
+
container_engine_obj = tm.ApptainerEngine(address="docker://parsaghadermazi/zipstrain:amd64") #could go to a toml or json config file
|
|
693
|
+
else:
|
|
694
|
+
raise ValueError("Invalid container engine. Choose from 'local', 'docker', or 'apptainer'.")
|
|
695
|
+
tm.lazy_run_compares(
|
|
696
|
+
comps_db=genome_comp_db,
|
|
697
|
+
container_engine=container_engine_obj,
|
|
698
|
+
run_dir=run_dir,
|
|
699
|
+
max_concurrent_batches=max_concurrent_batches,
|
|
700
|
+
polars_engine=polars_engine,
|
|
701
|
+
execution_mode=execution_mode,
|
|
702
|
+
slurm_config=slurm_conf,
|
|
703
|
+
memory_mode=memory_mode,
|
|
704
|
+
chrom_batch_size=chrom_batch_size,
|
|
705
|
+
tasks_per_batch=task_per_batch,
|
|
706
|
+
poll_interval=poll_interval,
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
@run.command("build-comp-database")
|
|
712
|
+
@click.option("--profile-db-dir", "-p", required=True, help="Directory containing profile either in parquet format.")
|
|
713
|
+
@click.option("--config-file", "-c", required=True, help="Path to the genome comparsion database config file in json format.")
|
|
714
|
+
@click.option("--output-dir", "-o", required=True, help="Directory to genome comparison database object.")
|
|
715
|
+
@click.option("--comp-db-file", "-f", required=False, help="The initial database file. If provided only additional comparisons will be added to this database.")
|
|
716
|
+
def build_comp_database(profile_db_dir, config_file, output_dir, comp_db_file):
|
|
717
|
+
"""
|
|
718
|
+
Build a genome comparison database from the given profiles and configuration.
|
|
719
|
+
|
|
720
|
+
Parameters:
|
|
721
|
+
profile_db_dir (str): Directory containing profile either in parquet format.
|
|
722
|
+
config_file (str): Path to the genome comparison database config file in json format.
|
|
723
|
+
"""
|
|
724
|
+
profile_db_dir=pathlib.Path(profile_db_dir)
|
|
725
|
+
profile_db=db.ProfileDatabase(
|
|
726
|
+
db_loc=profile_db_dir,
|
|
727
|
+
)
|
|
728
|
+
existing_db_loc=pathlib.Path(comp_db_file) if comp_db_file is not None else None
|
|
729
|
+
if existing_db_loc is not None and not existing_db_loc.exists():
|
|
730
|
+
raise FileNotFoundError(f"{existing_db_loc} does not exist.")
|
|
731
|
+
obj=db.GenomeComparisonDatabase(
|
|
732
|
+
profile_db=profile_db,
|
|
733
|
+
config=db.GenomeComparisonConfig.from_json(pathlib.Path(config_file)),
|
|
734
|
+
comp_db_loc=existing_db_loc,
|
|
735
|
+
)
|
|
736
|
+
obj.dump_obj(pathlib.Path(output_dir))
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
@run.command("compare_genes")
|
|
740
|
+
@click.option("--genome-comparison-object", "-g", required=True, help="Path to the genome comparison object in json format.")
|
|
741
|
+
@click.option("--run-dir", "-r", required=True, help="Directory to save the run data.")
|
|
742
|
+
@click.option("--max-concurrent-batches", "-m", default=5, help="Maximum number of concurrent batches to run.")
|
|
743
|
+
@click.option("--poll-interval", "-p", default=1, help="Polling interval in seconds to check the status of batches.")
|
|
744
|
+
@click.option("--execution-mode", "-e", default="local", help="Execution mode: 'local' or 'slurm'.")
|
|
745
|
+
@click.option("--slurm-config", "-s", default=None, help="Path to the SLURM configuration file in json format. Required if execution mode is 'slurm'.")
|
|
746
|
+
@click.option("--container-engine", "-c", default="local", help="Container engine to use: 'local', 'docker' or 'apptainer'.")
|
|
747
|
+
@click.option("--task-per-batch", "-t", default=10, help="Number of tasks to include in each batch.")
|
|
748
|
+
@click.option("--polars-engine", "-a", default="streaming", type=click.Choice(['streaming', 'gpu', 'auto'], case_sensitive=False), help="Polars engine to use: 'streaming', 'gpu' or 'auto'.")
|
|
749
|
+
@click.option("--ani-method", "-n", default="popani", help="ANI calculation method to use (e.g., 'popani', 'conani', 'cosani_0.4').")
|
|
750
|
+
def compare_genes(genome_comparison_object, run_dir, max_concurrent_batches, poll_interval, execution_mode, slurm_config, container_engine, task_per_batch, polars_engine, ani_method):
|
|
751
|
+
"""
|
|
752
|
+
Run gene comparisons in batches using the specified execution mode and container engine.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
genome_comparison_object (str): Path to the genome comparison object in json format.
|
|
756
|
+
run_dir (str): Directory to save the run data.
|
|
757
|
+
max_concurrent_batches (int): Maximum number of concurrent batches to run.
|
|
758
|
+
poll_interval (int): Polling interval in seconds to check the status of batches.
|
|
759
|
+
execution_mode (str): Execution mode: 'local' or 'slurm'.
|
|
760
|
+
slurm_config (str): Path to the SLURM configuration file in json format. Required if execution mode is 'slurm'.
|
|
761
|
+
container_engine (str): Container engine to use: 'local', 'docker' or 'apptainer'.
|
|
762
|
+
task_per_batch (int): Number of tasks to include in each batch.
|
|
763
|
+
polars_engine (str): Polars engine to use: 'streaming', 'gpu' or 'auto'.
|
|
764
|
+
ani_method (str): ANI calculation method to use.
|
|
765
|
+
"""
|
|
766
|
+
genome_comp_db=db.GenomeComparisonDatabase.load_obj(pathlib.Path(genome_comparison_object))
|
|
767
|
+
run_dir=pathlib.Path(run_dir)
|
|
768
|
+
slurm_conf=None
|
|
769
|
+
if execution_mode == "slurm":
|
|
770
|
+
if slurm_config is None:
|
|
771
|
+
raise ValueError("SLURM configuration file must be provided when execution mode is 'slurm'.")
|
|
772
|
+
slurm_conf = tm.SlurmConfig.from_json(slurm_config)
|
|
773
|
+
|
|
774
|
+
if container_engine == "local":
|
|
775
|
+
container_engine_obj = tm.LocalEngine(address="")
|
|
776
|
+
elif container_engine == "docker":
|
|
777
|
+
container_engine_obj = tm.DockerEngine(address="parsaghadermazi/zipstrain:amd64")
|
|
778
|
+
elif container_engine == "apptainer":
|
|
779
|
+
container_engine_obj = tm.ApptainerEngine(address="docker://parsaghadermazi/zipstrain:amd64")
|
|
780
|
+
else:
|
|
781
|
+
raise ValueError("Invalid container engine. Choose from 'local', 'docker', or 'apptainer'.")
|
|
782
|
+
|
|
783
|
+
tm.lazy_run_gene_compares(
|
|
784
|
+
comps_db=genome_comp_db,
|
|
785
|
+
container_engine=container_engine_obj,
|
|
786
|
+
run_dir=run_dir,
|
|
787
|
+
max_concurrent_batches=max_concurrent_batches,
|
|
788
|
+
polars_engine=polars_engine,
|
|
789
|
+
execution_mode=execution_mode,
|
|
790
|
+
slurm_config=slurm_conf,
|
|
791
|
+
tasks_per_batch=task_per_batch,
|
|
792
|
+
poll_interval=poll_interval,
|
|
793
|
+
ani_method=ani_method,
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# ...existing code...
|
|
797
|
+
|
|
798
|
+
@cli.command("test")
|
|
799
|
+
def test():
|
|
800
|
+
"""Run basic tests to ensure ZipStrain is setup correctly."""
|
|
801
|
+
### Check samtools installation
|
|
802
|
+
if all([ut.check_samtools()]):
|
|
803
|
+
click.echo("ZipStrain setup looks good!")
|
|
804
|
+
else:
|
|
805
|
+
click.echo("There are issues with the ZipStrain setup. Please check the above messages.")
|
|
806
|
+
|
|
807
|
+
if __name__ == "__main__":
|
|
808
|
+
cli()
|