ModDotPlot 0.9.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.
@@ -0,0 +1,1127 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+ from moddotplot.parse_fasta import (
4
+ readKmersFromFile,
5
+ getInputHeaders,
6
+ isValidFasta,
7
+ extractFiles,
8
+ )
9
+
10
+ from moddotplot.estimate_identity import (
11
+ convertToModimizers,
12
+ selfContainmentMatrix,
13
+ pairwiseContainmentMatrix,
14
+ convertMatrixToBed,
15
+ createSelfMatrix,
16
+ createPairwiseMatrix,
17
+ partitionOverlaps,
18
+ )
19
+ from moddotplot.interactive import run_dash
20
+ from moddotplot.const import ASCII_ART, VERSION
21
+
22
+ import argparse
23
+ import math
24
+ from moddotplot.static_plots import read_df_from_file, create_plots, create_grid
25
+ import json
26
+ import numpy as np
27
+ import pickle
28
+ import os
29
+
30
+
31
+ def get_parser():
32
+ """
33
+ Argument parsing for stand-alone runs.
34
+
35
+ """
36
+ parser = argparse.ArgumentParser(
37
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
38
+ description="ModDotPlot: Visualization of Complex Repeat Structures",
39
+ )
40
+ subparsers = parser.add_subparsers(
41
+ dest="command", help="Choose mode: interactive or static"
42
+ )
43
+ interactive_parser = subparsers.add_parser(
44
+ "interactive", help="Interactive mode commands"
45
+ )
46
+ static_parser = subparsers.add_parser("static", help="Static mode commands")
47
+
48
+ # -----------INTERACTIVE MODE SUBCOMMANDS-----------
49
+ interactive_input_group = interactive_parser.add_mutually_exclusive_group(
50
+ required=True
51
+ )
52
+
53
+ interactive_input_group.add_argument(
54
+ "-f",
55
+ "--fasta",
56
+ default=argparse.SUPPRESS,
57
+ help="Path to input fasta file(s).",
58
+ nargs="+",
59
+ )
60
+
61
+ interactive_input_group.add_argument(
62
+ "-l",
63
+ "--load",
64
+ default=None,
65
+ type=str,
66
+ help="Load previously computed hierarchical matrices.",
67
+ )
68
+
69
+ # Add a mutually exclusive group for compare and compare only.
70
+ compare_group = interactive_parser.add_mutually_exclusive_group(required=False)
71
+
72
+ interactive_parser.add_argument(
73
+ "-k", "--kmer", default=21, type=int, help="k-mer length."
74
+ )
75
+
76
+ interactive_parser.add_argument(
77
+ "-m",
78
+ "--modimizer",
79
+ help="Modimizer sketch size. A lower value will reduce the number of modimizers, but will increase performance. Must be less than window length `-w`. ",
80
+ default=1000,
81
+ type=int,
82
+ )
83
+
84
+ interactive_parser.add_argument(
85
+ "-r",
86
+ "--resolution",
87
+ default=1000,
88
+ type=int,
89
+ help="Dotplot resolution, or the number of intervals to compare against.",
90
+ )
91
+
92
+ interactive_parser.add_argument(
93
+ "-w",
94
+ "--window",
95
+ default=None,
96
+ type=int,
97
+ help="Window size, or the length in genomic coordinates of each interval. Default is set to (genome length)/(resolution)",
98
+ )
99
+
100
+ interactive_parser.add_argument(
101
+ "-id",
102
+ "--identity",
103
+ default=86.0,
104
+ type=float,
105
+ help="Identity cutoff threshold.",
106
+ )
107
+
108
+ interactive_parser.add_argument(
109
+ "-d",
110
+ "--delta",
111
+ default=0.5,
112
+ type=float,
113
+ help="Fraction of neighboring partition to include in identity estimation. Must be between 0 and 1, use > 0.5 is not recommended.",
114
+ )
115
+
116
+ interactive_parser.add_argument(
117
+ "-o",
118
+ "--output-dir",
119
+ default=None,
120
+ help="Directory name for saving matrices and coordinate logs. Defaults to working directory.",
121
+ )
122
+
123
+ compare_group.add_argument(
124
+ "--compare",
125
+ action="store_true",
126
+ help="Create a dotplot for each pairwise combination of input sequences (in addition to self-identity plots).",
127
+ )
128
+
129
+ compare_group.add_argument(
130
+ "--compare-only",
131
+ action="store_true",
132
+ help="Create a dotplot for each pairwise combination of input sequences (skips self-identity plots).",
133
+ )
134
+
135
+ interactive_parser.add_argument(
136
+ "-s",
137
+ "--save",
138
+ action="store_true",
139
+ help="Save hierarchical matrices to file.",
140
+ )
141
+
142
+ interactive_parser.add_argument(
143
+ "--port",
144
+ default="8050",
145
+ type=int,
146
+ help="Port number for launching interactive mode on localhost. Only used in interactive mode.",
147
+ )
148
+
149
+ interactive_parser.add_argument(
150
+ "--ambiguous",
151
+ action="store_true",
152
+ help="Preserve diagonal when handling strings of ambiguous homopolymers (eg. long runs of N's).",
153
+ )
154
+
155
+ interactive_parser.add_argument(
156
+ "-q",
157
+ "--quick",
158
+ action="store_true",
159
+ help="Launch a quick, non-interactive version of interactive mode.",
160
+ )
161
+
162
+ interactive_parser.add_argument(
163
+ "--no-plot",
164
+ action="store_true",
165
+ help="Prevent launching dash after saving. Must be used in combination with --save.",
166
+ )
167
+
168
+ # -----------STATIC MODE SUBCOMMANDS-----------
169
+ static_input_group = static_parser.add_mutually_exclusive_group(required=True)
170
+ static_input_group.add_argument(
171
+ "-c",
172
+ "--config",
173
+ default=None,
174
+ type=str,
175
+ help="Config file to use. Takes precedence over any other competing command line arguments.",
176
+ )
177
+
178
+ static_input_group.add_argument(
179
+ "-b",
180
+ "--bed",
181
+ default=argparse.SUPPRESS,
182
+ help="Path to input bed file(s). Exclusively used in static mode.",
183
+ nargs="+",
184
+ )
185
+
186
+ static_input_group.add_argument(
187
+ "-f",
188
+ "--fasta",
189
+ default=argparse.SUPPRESS,
190
+ help="Path to input fasta file(s).",
191
+ nargs="+",
192
+ )
193
+
194
+ # Add a mutually exclusive group for compare and compare only.
195
+ static_compare_group = static_parser.add_mutually_exclusive_group(required=False)
196
+ static_window_size_group = static_parser.add_mutually_exclusive_group(
197
+ required=False
198
+ )
199
+
200
+ static_parser.add_argument(
201
+ "-k", "--kmer", default=21, type=int, help="k-mer length."
202
+ )
203
+
204
+ static_parser.add_argument(
205
+ "-m",
206
+ "--modimizer",
207
+ help="Modimizer sketch size. A lower value will reduce the number of modimizers, but will increase performance. Must be less than window length `-w`. ",
208
+ default=1000,
209
+ type=int,
210
+ )
211
+
212
+ static_window_size_group.add_argument(
213
+ "-r",
214
+ "--resolution",
215
+ default=1000,
216
+ type=int,
217
+ help="Dotplot resolution, or the number of intervals to compare against.",
218
+ )
219
+
220
+ static_window_size_group.add_argument(
221
+ "-w",
222
+ "--window",
223
+ default=None,
224
+ type=int,
225
+ help="Window size, or the length in genomic coordinates of each interval. Default is set to (genome length)/(resolution)",
226
+ )
227
+
228
+ static_parser.add_argument(
229
+ "-id",
230
+ "--identity",
231
+ default=86.0,
232
+ type=float,
233
+ help="Identity cutoff threshold.",
234
+ )
235
+
236
+ static_parser.add_argument(
237
+ "-d",
238
+ "--delta",
239
+ default=0.5,
240
+ type=float,
241
+ help="Fraction of neighboring partition to include in identity estimation. Must be between 0 and 1, use > 0.5 is not recommended.",
242
+ )
243
+
244
+ static_parser.add_argument(
245
+ "-o",
246
+ "--output-dir",
247
+ default=None,
248
+ help="Directory name for saving bed files and plots. Defaults to working directory.",
249
+ )
250
+
251
+ static_compare_group.add_argument(
252
+ "--compare",
253
+ action="store_true",
254
+ help="Create a dotplot with two different sequences (in addition to self-identity plots).",
255
+ )
256
+
257
+ static_compare_group.add_argument(
258
+ "--compare-only",
259
+ action="store_true",
260
+ help="Create a dotplot with two different sequences (skips self-identity plots).",
261
+ )
262
+
263
+ static_parser.add_argument(
264
+ "--no-bed", action="store_true", help="Skip output of bed file."
265
+ )
266
+
267
+ static_parser.add_argument(
268
+ "--no-plot", action="store_true", help="Skip output of plots."
269
+ )
270
+
271
+ static_parser.add_argument(
272
+ "--no-hist", action="store_true", help="Skip output of histogram color legend."
273
+ )
274
+
275
+ static_parser.add_argument(
276
+ "--width", default=9, type=float, nargs="+", help="Plot width."
277
+ )
278
+
279
+ static_parser.add_argument("--dpi", default=600, type=int, help="Plot dpi.")
280
+
281
+ # TODO: Create list of accepted colors.
282
+ static_parser.add_argument(
283
+ "--palette",
284
+ default="Spectral_11",
285
+ help="Select color palette. See RColorBrewer for list of accepted palettes. Will default to Spectral_11 if not used.",
286
+ type=str,
287
+ )
288
+
289
+ static_parser.add_argument(
290
+ "--palette-orientation",
291
+ default="+",
292
+ choices=["+", "-"],
293
+ help="Color palette orientation. + for forward, - for reverse.",
294
+ type=str,
295
+ )
296
+
297
+ static_parser.add_argument(
298
+ "--colors",
299
+ default=None,
300
+ nargs="+",
301
+ help="Use a custom color palette, entered in either hexcode or rgb format.",
302
+ )
303
+
304
+ static_parser.add_argument(
305
+ "--breakpoints",
306
+ default=None,
307
+ nargs="+",
308
+ help="Introduce custom color thresholds. Must be between identity threshold and 100.",
309
+ )
310
+
311
+ static_parser.add_argument(
312
+ "-a",
313
+ "--axes-limits",
314
+ default=None,
315
+ type=float,
316
+ help="Change x and y axis limits for self identity plots. Default is length of the sequence. Can't be shorter than length of sequence.",
317
+ )
318
+
319
+ static_parser.add_argument(
320
+ "-t",
321
+ "--axes-ticks",
322
+ default=None,
323
+ nargs="+",
324
+ help="Tick labels to include in x and y axis for custom plots.",
325
+ )
326
+
327
+ static_parser.add_argument(
328
+ "--bin-freq",
329
+ action="store_true",
330
+ help="By default, histograms are evenly spaced based on the number of colors and the identity threshold. Select this argument to bin based on the frequency of observed identity values.",
331
+ )
332
+
333
+ static_parser.add_argument(
334
+ "--ambiguous",
335
+ action="store_true",
336
+ help="Preserve diagonal when handling strings of ambiguous homopolymers (eg. long runs of N's).",
337
+ )
338
+
339
+ static_parser.add_argument(
340
+ "--grid",
341
+ action="store_true",
342
+ help="Plot comparative plots in an NxN grid like format.",
343
+ )
344
+
345
+ static_parser.add_argument(
346
+ "--grid-only",
347
+ action="store_true",
348
+ help="Plot comparative plots in an NxN grid like format, skipping individual plots",
349
+ )
350
+
351
+ # TODO: Implement static mode logging options
352
+
353
+ return parser
354
+
355
+
356
+ def main():
357
+ print(ASCII_ART)
358
+ print(f"v{VERSION} \n")
359
+ args = get_parser().parse_args()
360
+ # -----------MUTUALLY EXCLUSIVE: INTERACTIVE OR STATIC MODE-----------
361
+ if args.command == "interactive":
362
+ print(f"Running ModDotPlot in interactive mode\n")
363
+ # -----------LOAD MATRICES FOR INTERACTIVE MODE-----------
364
+ if hasattr(args, "load") and args.load:
365
+ print(f"Loading matrix hierarchy from {args.load}... \n")
366
+ matrices, metadata = extractFiles(args.load)
367
+ sparsity = math.ceil(
368
+ (metadata[0]["max_window_size"] + 1) / metadata[0]["resolution"]
369
+ )
370
+ sparsity = 2 ** math.floor(math.log2(sparsity))
371
+ axes = []
372
+ for i in range(len(matrices)):
373
+ matrix_axes = []
374
+ counter = 0
375
+ for matrix in matrices[i]:
376
+ x_axis = [
377
+ j * round(metadata[i]["x_size"] / matrix.shape[0])
378
+ for j in range(matrix.shape[0])
379
+ ]
380
+ y_axis = [
381
+ j * round(metadata[i]["y_size"] / matrix.shape[1])
382
+ for j in range(matrix.shape[1])
383
+ ]
384
+ x_axis.append(metadata[i]["x_size"])
385
+ y_axis.append(metadata[i]["y_size"])
386
+ matrix_axes.append(x_axis)
387
+ matrix_axes.append(y_axis)
388
+ axes.append(matrix_axes)
389
+ run_dash(
390
+ matrices,
391
+ metadata,
392
+ axes,
393
+ sparsity,
394
+ args.identity,
395
+ args.port,
396
+ args.output_dir,
397
+ )
398
+ sys.exit(0)
399
+ elif args.command == "static":
400
+ print(f"Running ModDotPlot in static mode\n")
401
+ # -----------CONFIG PARSING-----------
402
+ # TODO: Change to yml file, add readme to config folder
403
+ if args.config:
404
+ with open(args.config, "r") as f:
405
+ config = json.load(f)
406
+ # TODO: Remove args that are interactive only
407
+ args.fasta = config.get("fasta")
408
+ args.bed = config.get("bed")
409
+
410
+ # Distance matrix commands
411
+ args.kmer = config.get("kmer", args.kmer)
412
+ args.modimizer = config.get("modimizer", args.modimizer)
413
+ args.resolution = config.get("resolution", args.resolution)
414
+ args.window = config.get("window", args.window)
415
+ args.identity = config.get("identity", args.identity)
416
+ args.delta = config.get("delta", args.delta)
417
+ args.output_dir = config.get("output_dir", args.output_dir)
418
+ args.compare = config.get("compare", args.compare)
419
+ args.compare_only = config.get("compare_only", args.compare_only)
420
+
421
+ args.no_bed = config.get("no_bed", args.no_bed)
422
+ args.no_plot = config.get("no_plot", args.no_plot)
423
+ args.no_hist = config.get("no_hist", args.no_hist)
424
+ args.width = config.get("width", args.width)
425
+ args.axes_limits = config.get("axes_limits", args.axes_limits)
426
+ args.dpi = config.get("dpi", args.dpi)
427
+ args.palette = config.get("palette", args.palette)
428
+ args.palette_orientation = config.get(
429
+ "palette_orientation", args.palette_orientation
430
+ )
431
+ args.colors = config.get("color", args.colors)
432
+ args.axes_ticks = config.get("axes_ticks", args.axes_ticks)
433
+ args.breakpoints = config.get("breakpoints", args.breakpoints)
434
+ args.bin_freq = config.get("bin_freq", args.bin_freq)
435
+
436
+ # TODO: Include logging options here
437
+ # Check if conflicitng command line args
438
+ if args.grid or args.grid_only:
439
+ if not (args.compare or args.compare_only) and not args.bed:
440
+ print(
441
+ f"Option --grid was selected, but no comparative plots will be produced. Please rerun ModDotPlot with the `--compare` or `--compare-only` option.\n"
442
+ )
443
+ sys.exit(10)
444
+
445
+ # -----------INPUT COMMAND VALIDATION-----------
446
+ # TODO: More tests!
447
+ # Validation for breakpoints command
448
+ if args.breakpoints:
449
+ # Check that start value for breakpoints = identity threshold value
450
+ if float(args.breakpoints[0]) != float(args.identity):
451
+ print(
452
+ f"Identity threshold is {args.identity}, but starting breakpoint is {args.breakpoints[0]}! \n"
453
+ )
454
+ print(
455
+ f"Please modify identity threshold using --identity {args.breakpoints[0]}.\n"
456
+ )
457
+ # TODO: Chronicle exit codes
458
+ # Exit code 2: breakpoint value != identity threshold
459
+ sys.exit(2)
460
+
461
+ # -----------BEDFILE INPUT FOR STATIC MODE-----------
462
+ if hasattr(args, "bed") and args.bed:
463
+ if args.grid or args.grid_only:
464
+ single_vals = []
465
+ double_vals = []
466
+ single_val_name = []
467
+ double_val_name = []
468
+ try:
469
+ for bed in args.bed:
470
+ # If args.bed is provided as input, run static mode directly from the bed file. Skip counting input k-mers.
471
+ df = read_df_from_file(bed)
472
+
473
+ unique_query_names = df["#query_name"].unique()
474
+ unique_reference_names = df["reference_name"].unique()
475
+ assert len(unique_query_names) == len(unique_reference_names)
476
+ # TODO: Change this to allow for multiple seqs in bed file
477
+ assert len(unique_reference_names) == 1
478
+ self_id_scores = df[df["#query_name"] == df["reference_name"]]
479
+ pairwise_id_scores = df[df["#query_name"] != df["reference_name"]]
480
+ if not args.grid_only:
481
+ print(
482
+ f"Input bed file {bed} read successfully! Creating plots... \n"
483
+ )
484
+ # Create directory
485
+ if not args.output_dir:
486
+ args.output_dir = os.getcwd()
487
+ if not os.path.exists(args.output_dir):
488
+ os.makedirs(args.output_dir)
489
+ if len(self_id_scores) > 1:
490
+ if not args.grid_only:
491
+ create_plots(
492
+ sdf=None,
493
+ directory=args.output_dir if args.output_dir else ".",
494
+ name_x=unique_query_names[0],
495
+ name_y=unique_query_names[0],
496
+ palette=args.palette,
497
+ palette_orientation=args.palette_orientation,
498
+ no_hist=args.no_hist,
499
+ width=args.width,
500
+ dpi=args.dpi,
501
+ is_freq=args.bin_freq,
502
+ xlim=args.axes_limits,
503
+ custom_colors=args.colors,
504
+ custom_breakpoints=args.breakpoints,
505
+ from_file=df,
506
+ is_pairwise=False,
507
+ axes_labels=args.axes_ticks,
508
+ )
509
+ if args.grid or args.grid_only:
510
+ single_vals.append(df)
511
+ single_val_name.append(unique_query_names[0])
512
+ # Case 2: Pairwise bed file
513
+ if len(pairwise_id_scores) > 1:
514
+ if not args.grid_only:
515
+ create_plots(
516
+ sdf=None,
517
+ directory=args.output_dir if args.output_dir else ".",
518
+ name_x=unique_query_names[0],
519
+ name_y=unique_reference_names[0],
520
+ palette=args.palette,
521
+ palette_orientation=args.palette_orientation,
522
+ no_hist=args.no_hist,
523
+ width=args.width,
524
+ dpi=args.dpi,
525
+ is_freq=args.bin_freq,
526
+ xlim=args.axes_limits,
527
+ custom_colors=args.colors,
528
+ custom_breakpoints=args.axes_ticks,
529
+ from_file=df,
530
+ is_pairwise=True,
531
+ axes_labels=args.axes_ticks,
532
+ )
533
+ if args.grid or args.grid_only:
534
+ double_vals.append(df)
535
+ double_val_name.append(
536
+ [unique_query_names[0], unique_reference_names[0]]
537
+ )
538
+ # Exit once all bed files have been iterated through
539
+ if args.grid or args.grid_only:
540
+ print(f"Creating a {len(sequences)}x{len(sequences)} grid.\n")
541
+ create_grid(
542
+ singles=single_vals,
543
+ doubles=double_vals,
544
+ directory=args.output_dir if args.output_dir else ".",
545
+ palette=args.palette,
546
+ palette_orientation=args.palette_orientation,
547
+ no_hist=args.no_hist,
548
+ single_names=single_val_name,
549
+ double_names=double_val_name,
550
+ is_freq=args.bin_freq,
551
+ xlim=args.axes_limits,
552
+ custom_colors=args.colors,
553
+ custom_breakpoints=args.axes_ticks,
554
+ axes_label=args.axes_ticks,
555
+ is_bed=True,
556
+ )
557
+ sys.exit(0)
558
+ except Exception as e:
559
+ # Exit code 7: Error getting info from bed file:
560
+ # TODO: Change to logs
561
+ print(f"Error in bed file: {e}")
562
+ sys.exit(7)
563
+
564
+ # -----------INPUT SEQUENCE VALIDATION-----------
565
+ seq_list = []
566
+ for i in args.fasta:
567
+ isValidFasta(i)
568
+ headers = getInputHeaders(i)
569
+ if len(headers) > 1:
570
+ print(f"File {i} contains multiple fasta entries. \n")
571
+ counter = 1
572
+ for j in headers:
573
+ counter += 1
574
+ for j in headers:
575
+ seq_list.append(j)
576
+
577
+ # -----------LOAD SEQUENCES INTO MEMORY-----------
578
+ kmer_list = []
579
+ for i in args.fasta:
580
+ kmer_list.append(readKmersFromFile(i, args.kmer, False))
581
+ k_list = [item for sublist in kmer_list for item in sublist]
582
+ # Throw error if compare only selected with one sequence.
583
+ if len(k_list) < 2 and args.compare_only:
584
+ print(
585
+ f"Error: Can't create a comparative plot with only one sequence. Please re-run without --compare-only."
586
+ )
587
+ sys.exit(2)
588
+
589
+ # -----------LAUNCH INTERACTIVE MODE-----------
590
+ if args.command == "interactive":
591
+ # Single sequence, can set window length immediately.
592
+ hgi = len(max(k_list))
593
+ hgi = hgi + args.kmer - 1
594
+ min_window_size = 0
595
+ window_lengths = []
596
+ if not args.window:
597
+ if args.quick:
598
+ min_window_size = round((hgi / args.resolution))
599
+ args.window = min_window_size
600
+ else:
601
+ min_window_size = round((hgi / args.resolution) / 2)
602
+ args.window = min_window_size
603
+ else:
604
+ min_window_size = args.window
605
+ if args.window and args.quick:
606
+ print(f"Conflict with `--quick` argument.")
607
+ max_window_size = math.ceil(hgi / args.resolution)
608
+ # If only sequence is too small, throw an error.
609
+ if max_window_size < 10:
610
+ print(f"Error: sequence too small for analysis.\n")
611
+ print(
612
+ f"ModDotPlot requires a minimum window size of 10. Sequences less than 10Kbp will not work with ModDotPlot under normal resolution. We recommend rerunning ModDotPlot with --r {math.ceil(hgi / 10)}.\n"
613
+ )
614
+ sys.exit(0)
615
+ while min_window_size <= max_window_size:
616
+ window_lengths.append(min_window_size)
617
+ min_window_size = min_window_size * 2
618
+
619
+ # Set warning if >2 sequences detected
620
+ if len(seq_list) > 2:
621
+ print(
622
+ f"{len(seq_list)} sequences were detected, however interactive mode can only load two sequences at a time.\n"
623
+ )
624
+ print(
625
+ f"Interactive mode will proceed with {seq_list[0]} and {seq_list[1]}\n"
626
+ )
627
+ # Set warning if <1 sequence detected
628
+ elif len(seq_list) < 1:
629
+ print(f"Error: No sequences detected!")
630
+ sys.exit(5)
631
+
632
+ # Set sparsity to be the closest power of 2
633
+ sparsities = []
634
+ if window_lengths[0] < 1000:
635
+ sparsities.append(1)
636
+ else:
637
+ sparsities.append(round(window_lengths[0] / args.modimizer))
638
+ if sparsities[0] <= args.modimizer:
639
+ sparsities[0] = 2 ** int(math.log2(sparsities[0]))
640
+ else:
641
+ sparsities[0] = 2 ** (int(math.log2(sparsities[0] - 1)) + 1)
642
+ # expectation = round(win/seq_sparsity)
643
+ for i in range(1, len(window_lengths)):
644
+ if window_lengths[i] > 1000:
645
+ sparsities.append(sparsities[-1] * 2)
646
+ else:
647
+ sparsities.append(1)
648
+ expectation = round(window_lengths[-1] / sparsities[-1])
649
+ matrices = []
650
+ metadata = []
651
+ # -----------BUILD IMAGE PYRAMID FOR SELF MATRICES-----------
652
+ if not args.compare_only:
653
+ for j in range(min(len(seq_list), 2)):
654
+ image_pyramid = []
655
+ if args.quick or len(window_lengths) == 1:
656
+ print(
657
+ f"Building 1 self-identity matrix for {seq_list[j]}, using a window size of {window_lengths[0]}.... \n"
658
+ )
659
+ else:
660
+ print(
661
+ f"Building {len(window_lengths)} self-identity matrices for {seq_list[j]}, using a minimum window size of {window_lengths[0]}.... \n"
662
+ )
663
+ if not args.quick:
664
+ print(
665
+ f"Creating base layer using window length {window_lengths[0]}...\n"
666
+ )
667
+
668
+ for i in range(len(window_lengths)):
669
+ layer_sparsity = sparsities[i]
670
+ layer_window_size = window_lengths[i]
671
+ layer_neigh = partitionOverlaps(
672
+ k_list[j],
673
+ layer_window_size,
674
+ args.delta,
675
+ len(k_list[j]),
676
+ args.kmer,
677
+ )
678
+ layer_sing = partitionOverlaps(
679
+ k_list[j], layer_window_size, 0, len(k_list[j]), args.kmer
680
+ )
681
+
682
+ mods_neigh = convertToModimizers(
683
+ layer_neigh,
684
+ layer_sparsity,
685
+ args.ambiguous,
686
+ args.kmer,
687
+ expectation,
688
+ )
689
+ mods_sing = convertToModimizers(
690
+ layer_sing,
691
+ layer_sparsity,
692
+ args.ambiguous,
693
+ args.kmer,
694
+ expectation,
695
+ )
696
+ if not args.quick and i > 0:
697
+ print(f"Layer {i+1} using window length {layer_window_size}\n")
698
+ matrix_layer = selfContainmentMatrix(
699
+ mods_sing, mods_neigh, args.kmer, args.identity, args.ambiguous
700
+ )
701
+ image_pyramid.insert(0, matrix_layer)
702
+ matrices.append(image_pyramid)
703
+ metadata.append(
704
+ {
705
+ "x_name": seq_list[j],
706
+ "y_name": seq_list[j],
707
+ "x_size": len(k_list[j]) + args.kmer - 1,
708
+ "y_size": len(k_list[j]) + args.kmer - 1,
709
+ "self": True,
710
+ "min_window_size": window_lengths[0],
711
+ "max_window_size": window_lengths[-1],
712
+ "resolution": args.resolution,
713
+ "kmer_length": args.kmer,
714
+ "title": f"{seq_list[j]}",
715
+ "sparsities": sparsities,
716
+ }
717
+ )
718
+ # -----------BUILD IMAGE PYRAMID FOR COMPARATIVE MATRICES-----------
719
+ if (args.compare or args.compare_only) and len(seq_list) > 1:
720
+ # Determine which is smaller, which is larger
721
+ larger_name = ""
722
+ smaller_name = ""
723
+ larger_seq = []
724
+ smaller_seq = []
725
+ if len(k_list[0]) > len(k_list[1]):
726
+ larger_name = seq_list[0]
727
+ larger_seq = k_list[0]
728
+ smaller_name = seq_list[1]
729
+ smaller_seq = k_list[1]
730
+ else:
731
+ larger_name = seq_list[1]
732
+ larger_seq = k_list[1]
733
+ smaller_name = seq_list[0]
734
+ smaller_seq = k_list[0]
735
+ if args.quick:
736
+ print(
737
+ f"Quickly building pairwise matrices for {seq_list[0]} and {seq_list[1]}, using a window size of {window_lengths[0]}.... \n"
738
+ )
739
+ else:
740
+ print(
741
+ f"Building pairwise matrices for {seq_list[0]} and {seq_list[1]}, using a minimum window size of {window_lengths[0]}.... \n"
742
+ )
743
+ image_pyramid = []
744
+ for i in range(len(window_lengths)):
745
+ layer_sparsity = sparsities[i]
746
+ layer_window_size = window_lengths[i]
747
+ larger_neigh = partitionOverlaps(
748
+ larger_seq,
749
+ layer_window_size,
750
+ args.delta,
751
+ len(larger_seq),
752
+ args.kmer,
753
+ )
754
+ larger_sing = partitionOverlaps(
755
+ larger_seq, layer_window_size, 0, len(larger_seq), args.kmer
756
+ )
757
+ smaller_neigh = partitionOverlaps(
758
+ smaller_seq,
759
+ layer_window_size,
760
+ args.delta,
761
+ len(smaller_seq),
762
+ args.kmer,
763
+ )
764
+ smaller_sing = partitionOverlaps(
765
+ smaller_seq, layer_window_size, 0, len(smaller_seq), args.kmer
766
+ )
767
+
768
+ larger_mods_neigh = convertToModimizers(
769
+ larger_neigh, layer_sparsity, args.ambiguous, args.kmer, expectation
770
+ )
771
+ larger_mods_sing = convertToModimizers(
772
+ larger_sing, layer_sparsity, args.ambiguous, args.kmer, expectation
773
+ )
774
+ smaller_mods_neigh = convertToModimizers(
775
+ smaller_neigh,
776
+ layer_sparsity,
777
+ args.ambiguous,
778
+ args.kmer,
779
+ expectation,
780
+ )
781
+ smaller_mods_sing = convertToModimizers(
782
+ smaller_sing, layer_sparsity, args.ambiguous, args.kmer, expectation
783
+ )
784
+ if not args.quick:
785
+ print(f"Layer {i+1} using window length {layer_window_size}\n")
786
+ matrix_layer = pairwiseContainmentMatrix(
787
+ larger_mods_sing,
788
+ smaller_mods_sing,
789
+ larger_mods_neigh,
790
+ smaller_mods_neigh,
791
+ args.identity,
792
+ args.kmer,
793
+ False,
794
+ )
795
+ image_pyramid.insert(0, matrix_layer)
796
+ matrices.append(image_pyramid)
797
+ metadata.append(
798
+ {
799
+ "x_name": larger_name,
800
+ "y_name": smaller_name,
801
+ "x_size": len(larger_seq) + args.kmer - 1,
802
+ "y_size": len(smaller_seq) + args.kmer - 1,
803
+ "self": False,
804
+ "min_window_size": window_lengths[0],
805
+ "max_window_size": window_lengths[-1],
806
+ "resolution": args.resolution,
807
+ "kmer_length": args.kmer,
808
+ "title": f"{larger_name}-{smaller_name}",
809
+ "sparsities": sparsities,
810
+ }
811
+ )
812
+
813
+ if args.save:
814
+ # Check if this value already exists
815
+ if not args.output_dir:
816
+ args.output_dir = os.getcwd()
817
+ folder_path = os.path.join(args.output_dir, "interactive_matrices")
818
+ if not os.path.exists(folder_path):
819
+ print(f"Saving interactive matrices in {folder_path}\n")
820
+ os.makedirs(folder_path)
821
+ else:
822
+ print(f"Saving interactive matrices in {folder_path}\n")
823
+ print(f"{folder_path} already exists, overwriting its contents.\n")
824
+
825
+ for i in range(len(metadata)):
826
+ for j in range(len(matrices[i])):
827
+ if metadata[i]["self"]:
828
+ tmp = f"{metadata[i]['x_name']}_{j}.npz"
829
+ else:
830
+ tmp = f"{metadata[i]['x_name']}-{metadata[i]['y_name']}_{j}.npz"
831
+ saved_path = os.path.join(folder_path, tmp)
832
+ np.savez_compressed(saved_path, data=matrices[i][j])
833
+ pickle_path = os.path.join(folder_path, "metadata.pkl")
834
+ # Save the dictionary as a pickle file
835
+ with open(pickle_path, "wb") as f:
836
+ pickle.dump(metadata, f)
837
+ # Check if no plot arg is used
838
+ if args.no_plot:
839
+ print(
840
+ f"Saved matrices to {folder_path}. Thank you for using ModDotPlot!\n"
841
+ )
842
+ sys.exit(0)
843
+
844
+ # Before running dash, change into intervals...
845
+ axes = []
846
+ for matrices_set, meta in zip(matrices, metadata):
847
+ matrix_axes = []
848
+ for matrix in matrices_set:
849
+ x_axis = np.linspace(0, meta["x_size"], matrix.shape[0] + 1)
850
+ y_axis = np.linspace(0, meta["y_size"], matrix.shape[1] + 1)
851
+ matrix_axes.append(x_axis)
852
+ matrix_axes.append(y_axis)
853
+ axes.append(matrix_axes)
854
+ run_dash(
855
+ matrices,
856
+ metadata,
857
+ axes,
858
+ sparsities[0],
859
+ args.identity,
860
+ args.port,
861
+ args.output_dir,
862
+ )
863
+
864
+ # -----------SETUP STATIC MODE-----------
865
+ elif args.command == "static":
866
+ # -----------SET SPARSITY VALUE-----------
867
+ if args.grid or args.grid_only:
868
+ grid_val_singles = []
869
+ grid_val_single_names = []
870
+ sequences = list(zip(seq_list, k_list))
871
+ if len(sequences) > 6 and (args.grid or args.grid_only):
872
+ print("Too many sequences to create a grid. Skipping. \n")
873
+
874
+ # Create output directory, if doesn't exist:
875
+ if (args.output_dir) and not os.path.exists(args.output_dir):
876
+ os.makedirs(args.output_dir)
877
+ # -----------COMPUTE SELF-IDENTITY PLOTS-----------
878
+ if not args.compare_only:
879
+ for i in range(len(sequences)):
880
+ seq_length = len(sequences[i][1])
881
+ win = args.window
882
+ res = args.resolution
883
+ if args.window:
884
+ # Change the resolution of each plot
885
+ res = math.ceil(seq_length / args.window)
886
+ else:
887
+ win = math.ceil(seq_length / args.resolution)
888
+
889
+ if win < args.modimizer:
890
+ args.modimizer = win
891
+ if win < 10:
892
+ print(f"Error: sequence too small for analysis.\n")
893
+ print(
894
+ f"ModDotPlot requires a minimum window size of 10. Sequences less than 10Kbp will not work with ModDotPlot under normal resolution. We recommend rerunning ModDotPlot with --r {math.ceil(seq_length / 10)}.\n"
895
+ )
896
+ sys.exit(0)
897
+
898
+ seq_sparsity = round(win / args.modimizer)
899
+ if seq_sparsity <= args.modimizer:
900
+ seq_sparsity = 2 ** int(math.log2(seq_sparsity))
901
+ else:
902
+ seq_sparsity = 2 ** (int(math.log2(seq_sparsity - 1)) + 1)
903
+ expectation = round(win / seq_sparsity)
904
+ xaxis = 0
905
+ width = 0
906
+ """if isinstance(args.xaxis, int) or args.xaxis == None:
907
+ xaxis = args.xaxis
908
+ else:
909
+ assert len(args.xaxis) == len(sequences)
910
+ xaxis = args.xaxis[i]"""
911
+ if isinstance(args.width, int):
912
+ width = args.width
913
+ else:
914
+ assert len(args.width) == len(sequences)
915
+ width = args.width[i]
916
+
917
+ print(f"Computing self identity matrix for {sequences[i][0]}... \n")
918
+ # TODO: Logging here
919
+ # print(f"\tSparsity value s: {seq_sparsity}\n")
920
+ print(f"\tSequence length n: {len(k_list[i]) + args.kmer - 1}\n")
921
+ print(f"\tWindow size w: {win}\n")
922
+ print(f"\tModimizer sketch size: {expectation}\n")
923
+ print(f"\tPlot Resolution r: {res}\n")
924
+ self_mat = createSelfMatrix(
925
+ seq_length,
926
+ sequences[i][1],
927
+ win,
928
+ seq_sparsity,
929
+ args.delta,
930
+ args.kmer,
931
+ args.identity,
932
+ args.ambiguous,
933
+ expectation,
934
+ )
935
+ bed = convertMatrixToBed(
936
+ self_mat, win, args.identity, seq_list[i], seq_list[i], True
937
+ )
938
+ if args.grid or args.grid_only:
939
+ grid_val_singles.append(bed)
940
+ grid_val_single_names.append(sequences[i][0])
941
+
942
+ if not args.no_bed:
943
+ # Log saving bed file
944
+ if not args.output_dir:
945
+ bedfile_output = sequences[i][0] + ".bed"
946
+ else:
947
+ bedfile_output = os.path.join(
948
+ args.output_dir, sequences[i][0] + ".bed"
949
+ )
950
+ with open(bedfile_output, "w") as bedfile:
951
+ for row in bed:
952
+ bedfile.write("\t".join(map(str, row)) + "\n")
953
+ print(f"Saved bed file to {bedfile_output}\n")
954
+
955
+ if not args.no_plot:
956
+ create_plots(
957
+ sdf=[bed],
958
+ directory=args.output_dir if args.output_dir else ".",
959
+ name_x=sequences[i][0],
960
+ name_y=sequences[i][0],
961
+ palette=args.palette,
962
+ palette_orientation=args.palette_orientation,
963
+ no_hist=args.no_hist,
964
+ width=width,
965
+ dpi=args.dpi,
966
+ is_freq=args.bin_freq,
967
+ xlim=args.axes_limits,
968
+ custom_colors=args.colors,
969
+ custom_breakpoints=args.breakpoints,
970
+ from_file=None,
971
+ is_pairwise=False,
972
+ axes_labels=args.axes_ticks,
973
+ )
974
+
975
+ # -----------COMPUTE COMPARATIVE PLOTS-----------
976
+ # TODO: Optimize computations so that largest sequence doesn't need to be redone all the time
977
+ if (args.compare or args.compare_only) and len(sequences) > 1:
978
+ # Set window size to args.window. Otherwise, set it to n/resolution
979
+
980
+ if args.grid or args.grid_only:
981
+ grid_val_doubles = []
982
+ grid_val_double_names = []
983
+
984
+ for i in range(len(sequences)):
985
+ for j in range(i + 1, len(sequences)):
986
+ larger_seq = sequences[i][1]
987
+ smaller_seq = sequences[j][1]
988
+ larger_length = len(larger_seq)
989
+ smaller_length = len(smaller_seq)
990
+ larger_seq_name = sequences[i][0]
991
+ smaller_seq_name = sequences[j][0]
992
+
993
+ if larger_length < smaller_length:
994
+ smaller_seq = sequences[i][1]
995
+ larger_seq = sequences[j][1]
996
+ larger_length = len(larger_seq)
997
+ smaller_length = len(smaller_seq)
998
+ larger_seq_name = sequences[j][0]
999
+ smaller_seq_name = sequences[i][0]
1000
+
1001
+ win = args.window
1002
+ res = args.resolution
1003
+ if args.window:
1004
+ res = math.ceil(smaller_length / args.window)
1005
+ else:
1006
+ win = math.ceil(smaller_length / args.resolution)
1007
+ if win < args.modimizer:
1008
+ args.modimizer = win
1009
+
1010
+ seq_sparsity = round(win / args.modimizer)
1011
+ if seq_sparsity <= args.modimizer:
1012
+ seq_sparsity = 2 ** int(math.log2(seq_sparsity))
1013
+ else:
1014
+ seq_sparsity = 2 ** (int(math.log2(seq_sparsity - 1)) + 1)
1015
+ expectation = round(win / seq_sparsity)
1016
+ print(
1017
+ f"Computing pairwise identity matrix for {larger_seq_name} and {smaller_seq_name}... \n"
1018
+ )
1019
+ # TODO: Logging here
1020
+ # print(f"\tSparsity value s: {seq_sparsity}\n")
1021
+ print(
1022
+ f"\tSequence length {larger_seq_name}: {larger_length + args.kmer - 1}\n"
1023
+ )
1024
+ print(
1025
+ f"\tSequence length {smaller_seq_name}: {smaller_length + args.kmer - 1}\n"
1026
+ )
1027
+ print(f"\tWindow size w: {win}\n")
1028
+ print(f"\tModimizer sketch size: {expectation}\n")
1029
+ print(f"\tPlot Resolution r: {res}\n")
1030
+
1031
+ pair_mat = createPairwiseMatrix(
1032
+ larger_length,
1033
+ smaller_length,
1034
+ larger_seq,
1035
+ smaller_seq,
1036
+ win,
1037
+ seq_sparsity,
1038
+ args.delta,
1039
+ args.kmer,
1040
+ args.identity,
1041
+ args.ambiguous,
1042
+ expectation,
1043
+ )
1044
+ # Throw error if the matrix is empty
1045
+ if np.all(pair_mat == 0) and (not (args.grid or args.grid_only)):
1046
+ print(
1047
+ f"The pairwise identity matrix for {sequences[i][0]} and {sequences[j][0]} is empty. Skipping.\n"
1048
+ )
1049
+ else:
1050
+ bed = convertMatrixToBed(
1051
+ pair_mat,
1052
+ win,
1053
+ args.identity,
1054
+ seq_list[i],
1055
+ seq_list[j],
1056
+ False,
1057
+ )
1058
+ if args.grid or args.grid_only:
1059
+ grid_val_doubles.append(bed)
1060
+ grid_val_double_names.append(
1061
+ [larger_seq_name, smaller_seq_name]
1062
+ )
1063
+
1064
+ if not args.no_bed:
1065
+ # Log saving bed file
1066
+ if not args.output_dir:
1067
+ bedfile_output = (
1068
+ smaller_seq_name
1069
+ + "_"
1070
+ + larger_seq_name
1071
+ + "_COMPARE.bed"
1072
+ )
1073
+ else:
1074
+ bedfile_output = os.path.join(
1075
+ args.output_dir,
1076
+ smaller_seq_name
1077
+ + "_"
1078
+ + larger_seq_name
1079
+ + "_COMPARE.bed",
1080
+ )
1081
+ with open(bedfile_output, "w") as bedfile:
1082
+ for row in bed:
1083
+ bedfile.write("\t".join(map(str, row)) + "\n")
1084
+ print(f"Saved bed file to {bedfile_output}\n")
1085
+
1086
+ if not args.no_plot:
1087
+ create_plots(
1088
+ sdf=[bed],
1089
+ directory=args.output_dir if args.output_dir else ".",
1090
+ name_x=smaller_seq_name,
1091
+ name_y=larger_seq_name,
1092
+ palette=args.palette,
1093
+ palette_orientation=args.palette_orientation,
1094
+ no_hist=args.no_hist,
1095
+ width=None,
1096
+ dpi=args.dpi,
1097
+ is_freq=args.bin_freq,
1098
+ xlim=args.axes_limits,
1099
+ custom_colors=args.colors,
1100
+ custom_breakpoints=args.breakpoints,
1101
+ from_file=None,
1102
+ is_pairwise=True,
1103
+ axes_labels=args.axes_ticks,
1104
+ )
1105
+
1106
+ if args.grid or args.grid_only:
1107
+ print(f"Creating a {len(sequences)}x{len(sequences)} grid.\n")
1108
+ create_grid(
1109
+ singles=grid_val_singles,
1110
+ doubles=grid_val_doubles,
1111
+ directory=args.output_dir if args.output_dir else ".",
1112
+ palette=args.palette,
1113
+ palette_orientation=args.palette_orientation,
1114
+ no_hist=args.no_hist,
1115
+ single_names=grid_val_single_names,
1116
+ double_names=grid_val_double_names,
1117
+ is_freq=args.bin_freq,
1118
+ xlim=args.axes_limits,
1119
+ custom_colors=args.colors,
1120
+ custom_breakpoints=args.axes_ticks,
1121
+ axes_label=args.axes_ticks,
1122
+ is_bed=False,
1123
+ )
1124
+
1125
+
1126
+ if __name__ == "__main__":
1127
+ main()