Muhamed-Kheir commited on
Commit
4e81d43
·
verified ·
1 Parent(s): 8a70bd0

Upload kmer_predict.py

Browse files
Files changed (1) hide show
  1. kmer_predict.py +563 -0
kmer_predict.py ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ K-mer-based group prediction for unknown sequences.
4
+
5
+ Inputs:
6
+ - Unknown sequences: a FASTA file or a directory of FASTA files
7
+ DNA unknowns are automatically tested in both forward and reverse-complement orientations.
8
+ - Unique k-mers: either
9
+ * a directory containing unique_k{k}_{group}.tsv/.txt files (from script #1), OR
10
+ * a ZIP file containing those files
11
+
12
+ Modes:
13
+ - fast: exact substring matching only (very fast)
14
+ - full: alignment-based matching (slower, more tolerant) + Fisher + FDR
15
+
16
+ Outputs:
17
+ - predictions_by_alignment.csv
18
+ - predicted_results_summary.png
19
+
20
+ Example:
21
+ python kmer_predict.py \
22
+ --unknown unknown_fastas/ \
23
+ --kmer-input kmer_results.zip \
24
+ --outdir pred_out \
25
+ --seqtype dna \
26
+ --mode fast
27
+
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import os
34
+ import re
35
+ import shutil
36
+ import tempfile
37
+ import zipfile
38
+ from dataclasses import dataclass
39
+ from typing import Dict, Iterable, List, Optional, Sequence, Tuple
40
+
41
+ import pandas as pd
42
+ import matplotlib
43
+ matplotlib.use("Agg")
44
+ import matplotlib.pyplot as plt
45
+
46
+ from scipy.stats import fisher_exact
47
+ from statsmodels.stats.multitest import multipletests
48
+
49
+ from Bio import Align
50
+ from Bio.Align import substitution_matrices
51
+
52
+
53
+ FASTA_EXTS = (".fasta", ".fa", ".fas", ".fna")
54
+ KMER_FILE_EXTS = (".tsv", ".txt")
55
+ DEFAULT_GROUP_REGEX = r"unique_k\d+_(.+)\.(tsv|txt)$"
56
+
57
+ BLOSUM62 = substitution_matrices.load("BLOSUM62")
58
+
59
+
60
+ # ----------------------------
61
+ # FASTA utilities
62
+ # ----------------------------
63
+
64
+ def read_fasta(filepath: str) -> Tuple[List[str], List[str]]:
65
+ headers, seqs, seq = [], [], []
66
+ with open(filepath, "r", encoding="utf-8") as fh:
67
+ for line in fh:
68
+ line = line.rstrip("\n")
69
+ if not line:
70
+ continue
71
+ if line.startswith(">"):
72
+ if seq:
73
+ seqs.append("".join(seq))
74
+ seq = []
75
+ headers.append(line[1:].strip())
76
+ else:
77
+ seq.append(line.strip().upper())
78
+ if seq:
79
+ seqs.append("".join(seq))
80
+ return headers, seqs
81
+
82
+
83
+ def clean_protein(seq: str) -> str:
84
+ return re.sub(r"[^ACDEFGHIKLMNPQRSTVWY]", "", seq.upper())
85
+
86
+
87
+ def clean_dna(seq: str) -> str:
88
+ # allow U and N like your original
89
+ return re.sub(r"[^ACGTUN]", "", seq.upper())
90
+
91
+
92
+ def reverse_complement_dna(seq: str) -> str:
93
+ """
94
+ Return the reverse-complement of a DNA sequence.
95
+
96
+ U is treated as T so RNA-like input is handled as DNA.
97
+ Ambiguous N is preserved.
98
+ """
99
+ seq = seq.upper().replace("U", "T")
100
+ table = str.maketrans("ACGTN", "TGCAN")
101
+ return seq.translate(table)[::-1]
102
+
103
+
104
+ def iter_unknown_sequences(unknown: str, is_protein: bool) -> List[Tuple[str, str, str]]:
105
+ """
106
+ Returns list of (source_file, header, cleaned_seq).
107
+ unknown can be a fasta file or a directory with fasta files.
108
+ """
109
+ seq_index: List[Tuple[str, str, str]] = []
110
+
111
+ if os.path.isdir(unknown):
112
+ files = [
113
+ os.path.join(unknown, f)
114
+ for f in os.listdir(unknown)
115
+ if f.lower().endswith(FASTA_EXTS)
116
+ ]
117
+ else:
118
+ files = [unknown]
119
+
120
+ files = [f for f in files if os.path.isfile(f)]
121
+ for fp in sorted(files):
122
+ headers, seqs = read_fasta(fp)
123
+ if is_protein:
124
+ seqs = [clean_protein(s) for s in seqs]
125
+ else:
126
+ seqs = [clean_dna(s) for s in seqs]
127
+
128
+ for h, s in zip(headers, seqs):
129
+ if s: # drop empty after cleaning
130
+ seq_index.append((fp, h, s))
131
+
132
+ return seq_index
133
+
134
+
135
+ # ----------------------------
136
+ # ZIP utilities (safe extract)
137
+ # ----------------------------
138
+
139
+ def safe_extract_zip(zip_path: str, dst_dir: str) -> None:
140
+ """Extract ZIP safely (prevents zip-slip)."""
141
+ with zipfile.ZipFile(zip_path, "r") as z:
142
+ for member in z.infolist():
143
+ if member.is_dir():
144
+ continue
145
+ target = os.path.normpath(os.path.join(dst_dir, member.filename))
146
+ if not target.startswith(os.path.abspath(dst_dir) + os.sep):
147
+ continue # skip suspicious paths
148
+ os.makedirs(os.path.dirname(target), exist_ok=True)
149
+ with z.open(member) as src, open(target, "wb") as out:
150
+ shutil.copyfileobj(src, out)
151
+
152
+
153
+ # ----------------------------
154
+ # Load unique kmers
155
+ # ----------------------------
156
+
157
+ @dataclass
158
+ class KmerDB:
159
+ group_kmers: Dict[str, List[str]]
160
+ source_dir: str
161
+
162
+
163
+ def parse_group_from_filename(fname: str, group_regex: str) -> str:
164
+ m = re.search(group_regex, fname, re.IGNORECASE)
165
+ if m:
166
+ return m.group(1)
167
+ # fallback: remove extension
168
+ return os.path.splitext(fname)[0]
169
+
170
+
171
+ def load_unique_kmers_from_dir(
172
+ kmer_dir: str,
173
+ is_protein: bool,
174
+ group_regex: str = DEFAULT_GROUP_REGEX,
175
+ ) -> KmerDB:
176
+ """
177
+ Loads k-mers from files like:
178
+ unique_k15_group1.tsv
179
+ unique_k20_groupA.txt
180
+ Accepts TSV or TXT; ignores comment/header lines.
181
+ """
182
+ group_kmers: Dict[str, List[str]] = {}
183
+
184
+ for fname in sorted(os.listdir(kmer_dir)):
185
+ if not fname.lower().endswith(KMER_FILE_EXTS):
186
+ continue
187
+
188
+ fpath = os.path.join(kmer_dir, fname)
189
+ if not os.path.isfile(fpath):
190
+ continue
191
+
192
+ group = parse_group_from_filename(fname, group_regex)
193
+ group = group.strip()
194
+
195
+ group_kmers.setdefault(group, [])
196
+
197
+ with open(fpath, "r", encoding="utf-8") as fh:
198
+ for line in fh:
199
+ line = line.strip()
200
+ if (not line) or line.startswith("#"):
201
+ continue
202
+ if line.lower().startswith(("kmer", "total")):
203
+ continue
204
+
205
+ token = line.split()[0].upper()
206
+ token = clean_protein(token) if is_protein else clean_dna(token)
207
+ if token:
208
+ group_kmers[group].append(token)
209
+
210
+ # Deduplicate while preserving order
211
+ for g in list(group_kmers.keys()):
212
+ group_kmers[g] = list(dict.fromkeys(group_kmers[g]))
213
+ if len(group_kmers[g]) == 0:
214
+ # drop empty groups
215
+ del group_kmers[g]
216
+
217
+ if not group_kmers:
218
+ raise FileNotFoundError(f"No k-mer files found in: {kmer_dir}")
219
+
220
+ return KmerDB(group_kmers=group_kmers, source_dir=kmer_dir)
221
+
222
+
223
+ def load_unique_kmers(kmer_input: str, is_protein: bool, group_regex: str) -> KmerDB:
224
+ """
225
+ kmer_input can be a directory OR a .zip file containing k-mer output files.
226
+ """
227
+ if os.path.isdir(kmer_input):
228
+ return load_unique_kmers_from_dir(kmer_input, is_protein, group_regex=group_regex)
229
+
230
+ if os.path.isfile(kmer_input) and kmer_input.lower().endswith(".zip"):
231
+ tmp = tempfile.mkdtemp(prefix="kmerdb_")
232
+ safe_extract_zip(kmer_input, tmp)
233
+ # find a directory inside that actually contains kmer files; simplest: use tmp itself
234
+ return load_unique_kmers_from_dir(tmp, is_protein, group_regex=group_regex)
235
+
236
+ raise FileNotFoundError(f"--kmer-input must be a directory or a .zip file: {kmer_input}")
237
+
238
+
239
+ # ----------------------------
240
+ # Matching
241
+ # ----------------------------
242
+
243
+ def align_kmer_to_seq(
244
+ kmer: str,
245
+ seq: str,
246
+ is_protein: bool,
247
+ identity_threshold: float = 0.9,
248
+ min_coverage: float = 0.8,
249
+ gap_open: float = -10,
250
+ gap_extend: float = -0.5,
251
+ nuc_match: float = 2,
252
+ nuc_mismatch: float = -1,
253
+ nuc_gap_open: float = -5,
254
+ nuc_gap_extend: float = -1,
255
+ ) -> bool:
256
+ if not kmer or not seq:
257
+ return False
258
+
259
+ # Fast exact substring path
260
+ if identity_threshold == 1.0 and min_coverage == 1.0:
261
+ return kmer in seq
262
+ if len(kmer) <= 3:
263
+ return kmer in seq
264
+
265
+ try:
266
+ aligner = Align.PairwiseAligner()
267
+ if is_protein:
268
+ aligner.substitution_matrix = BLOSUM62
269
+ aligner.open_gap_score = gap_open
270
+ aligner.extend_gap_score = gap_extend
271
+ else:
272
+ aligner.match_score = nuc_match
273
+ aligner.mismatch_score = nuc_mismatch
274
+ aligner.open_gap_score = nuc_gap_open
275
+ aligner.extend_gap_score = nuc_gap_extend
276
+
277
+ alns = aligner.align(kmer, seq)
278
+ if not alns:
279
+ return False
280
+
281
+ aln = alns[0]
282
+ aligned_query = aln.aligned[0]
283
+ aligned_target = aln.aligned[1]
284
+
285
+ aligned_len = sum(e - s for s, e in aligned_query)
286
+ if aligned_len == 0:
287
+ return False
288
+
289
+ matches = 0
290
+ for (qs, qe), (ts, te) in zip(aligned_query, aligned_target):
291
+ subseq_q = kmer[qs:qe]
292
+ subseq_t = seq[ts:te]
293
+ matches += sum(1 for a, b in zip(subseq_q, subseq_t) if a == b)
294
+
295
+ identity = matches / aligned_len
296
+ coverage = aligned_len / len(kmer)
297
+ return (identity >= identity_threshold) and (coverage >= min_coverage)
298
+
299
+ except Exception:
300
+ return False
301
+
302
+
303
+ def count_group_matches_for_sequence(
304
+ seq: str,
305
+ group_kmers: Dict[str, List[str]],
306
+ is_protein: bool,
307
+ identity_threshold: float,
308
+ min_coverage: float,
309
+ ) -> Tuple[Dict[str, int], int]:
310
+ """
311
+ Count how many group-specific k-mers match one sequence.
312
+ Returns (counts_by_group, total_matches).
313
+ """
314
+ group_found_counts = {g: 0 for g in group_kmers}
315
+ total_found = 0
316
+
317
+ for g, kmers in group_kmers.items():
318
+ for kmer in kmers:
319
+ if align_kmer_to_seq(
320
+ kmer,
321
+ seq,
322
+ is_protein=is_protein,
323
+ identity_threshold=identity_threshold,
324
+ min_coverage=min_coverage,
325
+ ):
326
+ group_found_counts[g] += 1
327
+ total_found += 1
328
+
329
+ return group_found_counts, total_found
330
+
331
+
332
+ def best_orientation_counts(
333
+ seq: str,
334
+ group_kmers: Dict[str, List[str]],
335
+ is_protein: bool,
336
+ identity_threshold: float,
337
+ min_coverage: float,
338
+ ) -> Tuple[str, str, Dict[str, int], int]:
339
+ """
340
+ For DNA, test both forward and reverse-complement orientations and keep
341
+ the orientation with the strongest signal.
342
+
343
+ Selection rule:
344
+ 1. highest total k-mer matches
345
+ 2. if tied, highest best-group matches
346
+ 3. if still tied, keep forward
347
+
348
+ For protein, only the input orientation is used.
349
+ Returns (orientation, oriented_sequence, counts_by_group, total_matches).
350
+ """
351
+ f_counts, f_total = count_group_matches_for_sequence(
352
+ seq, group_kmers, is_protein, identity_threshold, min_coverage
353
+ )
354
+
355
+ if is_protein:
356
+ return "input", seq, f_counts, f_total
357
+
358
+ rc_seq = reverse_complement_dna(seq)
359
+ r_counts, r_total = count_group_matches_for_sequence(
360
+ rc_seq, group_kmers, is_protein, identity_threshold, min_coverage
361
+ )
362
+
363
+ f_best = max(f_counts.values()) if f_counts else 0
364
+ r_best = max(r_counts.values()) if r_counts else 0
365
+
366
+ if (r_total > f_total) or (r_total == f_total and r_best > f_best):
367
+ return "reverse_complement", rc_seq, r_counts, r_total
368
+
369
+ return "forward", seq, f_counts, f_total
370
+
371
+
372
+ # ----------------------------
373
+ # Prediction core
374
+ # ----------------------------
375
+
376
+ def predict(
377
+ unknown: str,
378
+ kmer_input: str,
379
+ output_dir: str,
380
+ seqtype: str,
381
+ mode: str,
382
+ identity_threshold: float,
383
+ min_coverage: float,
384
+ fdr_alpha: float,
385
+ group_regex: str,
386
+ ) -> pd.DataFrame:
387
+ is_protein = (seqtype.lower() == "protein")
388
+ mode = mode.lower().strip()
389
+ if mode not in {"fast", "full"}:
390
+ raise ValueError("--mode must be 'fast' or 'full'")
391
+
392
+ # Load kmers (dir or zip)
393
+ db = load_unique_kmers(kmer_input, is_protein=is_protein, group_regex=group_regex)
394
+ group_kmers = db.group_kmers
395
+
396
+ print(f"Loaded k-mer counts: { {g: len(group_kmers[g]) for g in group_kmers} }")
397
+
398
+ # Unknown sequences
399
+ seq_index = iter_unknown_sequences(unknown, is_protein=is_protein)
400
+ if not seq_index:
401
+ raise FileNotFoundError("No sequences found in --unknown (file/dir).")
402
+
403
+ # Mode parameters
404
+ if mode == "fast":
405
+ eff_identity = 1.0
406
+ eff_coverage = 1.0
407
+ compute_stats = False
408
+ else:
409
+ eff_identity = float(identity_threshold)
410
+ eff_coverage = float(min_coverage)
411
+ compute_stats = True
412
+
413
+ results: List[dict] = []
414
+
415
+ total_seqs = len(seq_index)
416
+ for i, (srcfile, header, seq) in enumerate(seq_index, start=1):
417
+ print(f"Processing sequence {i}/{total_seqs} ({os.path.basename(srcfile)})")
418
+
419
+ orientation, oriented_seq, group_found_counts, total_found = best_orientation_counts(
420
+ seq=seq,
421
+ group_kmers=group_kmers,
422
+ is_protein=is_protein,
423
+ identity_threshold=eff_identity,
424
+ min_coverage=eff_coverage,
425
+ )
426
+
427
+ predicted = max(group_found_counts, key=group_found_counts.get)
428
+ conf_present = (group_found_counts[predicted] / total_found) if total_found else 0.0
429
+ conf_vocab = group_found_counts[predicted] / max(1, len(group_kmers[predicted]))
430
+
431
+ row = {
432
+ "Source_file": os.path.basename(srcfile),
433
+ "Sequence": header,
434
+ "Orientation_used": orientation,
435
+ "Predicted_group": predicted,
436
+ "Matches_total": total_found,
437
+ **{f"Matches_{g}": group_found_counts[g] for g in group_kmers},
438
+ "Confidence_by_present": conf_present,
439
+ "Confidence_by_group_vocab": conf_vocab,
440
+ }
441
+
442
+ if compute_stats:
443
+ fisher_p = {}
444
+ # total vocabulary size of "other groups" for contingency table
445
+ other_vocab_total = {g: sum(len(group_kmers[og]) for og in group_kmers if og != g) for g in group_kmers}
446
+
447
+ for g in group_kmers:
448
+ a = group_found_counts[g]
449
+ b = len(group_kmers[g]) - a
450
+ c = total_found - a
451
+ d = other_vocab_total[g] - c
452
+ if d < 0:
453
+ d = 0
454
+ table = [[a, b], [c, d]]
455
+ _, p = fisher_exact(table, alternative="greater")
456
+ fisher_p[g] = p
457
+ row.update({f"FisherP_{g}": fisher_p[g] for g in group_kmers})
458
+
459
+ results.append(row)
460
+
461
+ df = pd.DataFrame(results)
462
+
463
+ # FDR correction (full mode)
464
+ if mode == "full":
465
+ fisher_cols = [c for c in df.columns if c.startswith("FisherP_")]
466
+ if fisher_cols:
467
+ all_pvals = df[fisher_cols].values.flatten()
468
+ _, qvals, _, _ = multipletests(all_pvals, alpha=float(fdr_alpha), method="fdr_bh")
469
+ qval_matrix = qvals.reshape(df[fisher_cols].shape)
470
+ for j, col in enumerate(fisher_cols):
471
+ grp = col.split("_", 1)[1]
472
+ df[f"FDR_{grp}"] = qval_matrix[:, j]
473
+
474
+ # Save
475
+ os.makedirs(output_dir, exist_ok=True)
476
+ out_csv = os.path.join(output_dir, "predictions_by_alignment.csv")
477
+ df.to_csv(out_csv, index=False)
478
+ print(f"Saved predictions to {out_csv}")
479
+
480
+ # Plot
481
+ save_summary_plot(df, output_dir)
482
+
483
+ return df
484
+
485
+
486
+ def save_summary_plot(df: pd.DataFrame, output_dir: str) -> None:
487
+ """
488
+ Matplotlib-only summary figure:
489
+ - Left: predicted group counts
490
+ - Right: confidence distribution (boxplot)
491
+ """
492
+ fig, axes = plt.subplots(1, 2, figsize=(12, 5))
493
+
494
+ # Left: bar counts
495
+ counts = df["Predicted_group"].value_counts()
496
+ axes[0].bar(counts.index.astype(str), counts.values)
497
+ axes[0].set_xlabel("Predicted Group")
498
+ axes[0].set_ylabel("Number of Sequences")
499
+ axes[0].set_title("Predicted Group Counts")
500
+ axes[0].tick_params(axis="x", rotation=45)
501
+
502
+ # Right: boxplot confidence_by_present by group
503
+ groups = sorted(df["Predicted_group"].unique().tolist())
504
+ data = [df.loc[df["Predicted_group"] == g, "Confidence_by_present"].values for g in groups]
505
+ axes[1].boxplot(data, labels=groups, showfliers=False)
506
+ axes[1].set_title("Prediction Confidence (by Present)")
507
+ axes[1].set_xlabel("Predicted Group")
508
+ axes[1].set_ylabel("Confidence")
509
+ axes[1].tick_params(axis="x", rotation=45)
510
+
511
+ fig.tight_layout()
512
+ fig.savefig(os.path.join(output_dir, "predicted_results_summary.png"), dpi=300)
513
+ plt.close(fig)
514
+
515
+
516
+ # ----------------------------
517
+ # CLI
518
+ # ----------------------------
519
+
520
+ def build_parser() -> argparse.ArgumentParser:
521
+ p = argparse.ArgumentParser(
522
+ description="Predict group membership of unknown sequences using unique k-mers.",
523
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
524
+ )
525
+ p.add_argument("--unknown", required=True, help="Unknown FASTA file OR directory of FASTA files.")
526
+ p.add_argument("--kmer-input", required=True, help="Directory of unique_k*.tsv/txt OR a ZIP containing them.")
527
+ p.add_argument("--outdir", default="prediction_results", help="Output directory.")
528
+ p.add_argument("--seqtype", choices=["dna", "protein"], default="dna", help="Sequence type.")
529
+ p.add_argument("--mode", choices=["fast", "full"], default="fast", help="fast=substring only; full=alignment+Fisher+FDR.")
530
+ p.add_argument("--identity", type=float, default=0.9, help="Alignment identity threshold (full mode only).")
531
+ p.add_argument("--coverage", type=float, default=0.8, help="Alignment coverage threshold (full mode only).")
532
+ p.add_argument("--fdr", type=float, default=0.05, help="FDR alpha (full mode only).")
533
+ p.add_argument(
534
+ "--group-regex",
535
+ default=DEFAULT_GROUP_REGEX,
536
+ help="Regex to extract group name from k-mer filenames (1st capture group = group).",
537
+ )
538
+ return p
539
+
540
+
541
+ def main() -> None:
542
+ args = build_parser().parse_args()
543
+
544
+ # Validate unknown
545
+ if not os.path.exists(args.unknown):
546
+ raise FileNotFoundError(f"--unknown not found: {args.unknown}")
547
+
548
+ # Run
549
+ predict(
550
+ unknown=args.unknown,
551
+ kmer_input=args.kmer_input,
552
+ output_dir=args.outdir,
553
+ seqtype=args.seqtype,
554
+ mode=args.mode,
555
+ identity_threshold=args.identity,
556
+ min_coverage=args.coverage,
557
+ fdr_alpha=args.fdr,
558
+ group_regex=args.group_regex,
559
+ )
560
+
561
+
562
+ if __name__ == "__main__":
563
+ main()