From 72c2b4354b4aaf06310b0192b992a4fd7e276e04 Mon Sep 17 00:00:00 2001 From: "Jin Seok (Andy) Lee" Date: Mon, 11 May 2026 10:26:30 -0400 Subject: [PATCH 1/4] test v0.4.6a1 --- .github/workflows/ci.yml | 116 + .github/workflows/main.yml | 61 - .gitignore | 7 +- Cargo.toml | 25 +- README.md | 411 +- docs/.gitignore | 2 + docs/_quarto.yml | 43 + docs/cli/index.qmd | 23 + docs/custom.scss | 28 + docs/faq.qmd | 49 + docs/index.qmd | 101 + docs/pipelines/index.qmd | 17 + exacto/Cargo.toml | 62 +- exacto/exacto-annotator/Cargo.toml | 34 +- .../src/algorithms/variant_annotation.rs | 52 +- .../src/structs/position_annotation.rs | 2 +- .../src/structs/variant_call_annotation.rs | 9 +- .../structs/variant_call_annotation_set.rs | 20 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 2 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 2 +- exacto/exacto-caller/Cargo.toml | 76 +- exacto/exacto-caller/src/algorithms/mod.rs | 1 - .../reference_transcript_matching.rs | 162 +- .../src/algorithms/variant_calling.rs | 1007 +- .../src/algorithms/variant_calling_dna.rs | 1242 +- .../src/algorithms/variant_calling_peptide.rs | 486 - .../src/algorithms/variant_calling_rna.rs | 289 +- exacto/exacto-caller/src/common/constants.rs | 46 +- exacto/exacto-caller/src/common/logging.rs | 41 - exacto/exacto-caller/src/common/mod.rs | 1 - exacto/exacto-caller/src/lib.rs | 2 +- exacto/exacto-caller/src/prelude.rs | 2 +- exacto/exacto-caller/src/structs/alignment.rs | 255 +- .../src/structs/alignment_record.rs | 15 +- .../src/structs/alignment_structure.rs | 764 +- .../src/structs/alignment_structure_base.rs | 42 +- .../src/structs/alignment_structure_event.rs | 30 +- .../src/structs/alignment_structure_record.rs | 48 +- .../src/structs/dna_variant_call_set.rs | 41 +- .../src/structs/graph_operation.rs | 18 +- .../src/structs/graph_operation_view.rs | 228 + exacto/exacto-caller/src/structs/mod.rs | 1 + .../src/structs/mutant_peptides_set.rs | 30 +- .../src/structs/reference_base.rs | 12 +- .../src/structs/reference_transcript_match.rs | 12 +- .../structs/reference_transcript_sequence.rs | 26 +- .../src/structs/rna_variant_call_set.rs | 60 +- .../src/structs/transcript_model.rs | 109 +- .../src/structs/transcript_model_exon.rs | 24 +- .../src/structs/transcript_model_intron.rs | 20 +- .../src/structs/transcript_model_set.rs | 24 +- .../exacto-caller/src/structs/variant_call.rs | 118 +- .../src/structs/variant_record.rs | 20 +- .../src/structs/variant_record_cluster.rs | 16 +- ...na-005-normal_minimap2_mdtagged_sorted.bam | Bin 14773 -> 14758 bytes ...05-normal_minimap2_mdtagged_sorted.bam.bai | Bin 3880 -> 3880 bytes ...dna-005-tumor_minimap2_mdtagged_sorted.bam | Bin 38073 -> 38060 bytes ...005-tumor_minimap2_mdtagged_sorted.bam.bai | Bin 3880 -> 3880 bytes ...na-009-normal_minimap2_mdtagged_sorted.bam | Bin 19813 -> 208363 bytes ...09-normal_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3856 bytes ...dna-009-tumor_minimap2_mdtagged_sorted.bam | Bin 118906 -> 641886 bytes ...009-tumor_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3880 bytes ...na-010-normal_minimap2_mdtagged_sorted.bam | Bin 19814 -> 104355 bytes ...10-normal_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3856 bytes ...dna-010-tumor_minimap2_mdtagged_sorted.bam | Bin 153622 -> 829757 bytes ...010-tumor_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3880 bytes ...na-011-normal_minimap2_mdtagged_sorted.bam | Bin 10407 -> 44305 bytes ...11-normal_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3856 bytes ...dna-011-tumor_minimap2_mdtagged_sorted.bam | Bin 16044 -> 83467 bytes ...011-tumor_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3856 bytes ...gh-read-count_minimap2_mdtagged_sorted.bam | Bin 10886 -> 0 bytes ...ead-count_minimap2_mdtagged_sorted.bam.bai | Bin 3880 -> 0 bytes ...gh-read-count_minimap2_mdtagged_sorted.bam | Bin 16837 -> 0 bytes ...ead-count_minimap2_mdtagged_sorted.bam.bai | Bin 3928 -> 0 bytes ...rna-112-tumor_minimap2_mdtagged_sorted.bam | Bin 0 -> 1706 bytes ...112-tumor_minimap2_mdtagged_sorted.bam.bai | Bin 0 -> 3864 bytes .../dna-001-tumor_ground_truth.tsv | 4 +- .../dna-002-tumor_ground_truth.tsv | 4 +- .../dna-003-tumor_ground_truth.tsv | 4 +- .../dna-004-tumor_ground_truth.tsv | 6 +- .../dna-005-tumor_ground_truth.tsv | 4 +- .../dna-006-tumor_ground_truth.tsv | 4 +- .../dna-007-tumor_ground_truth.tsv | 4 +- .../dna-008-tumor_ground_truth.tsv | 4 +- .../dna-009-tumor_ground_truth.tsv | 6 +- .../dna-010-tumor_ground_truth.tsv | 8 +- .../dna-011-tumor_ground_truth.tsv | 6 +- .../rna-100-tumor_ground_truth.tsv | 4 +- .../rna-101-tumor_ground_truth.tsv | 4 +- .../rna-102-tumor_ground_truth.tsv | 4 +- .../rna-103-tumor_ground_truth.tsv | 2 +- .../rna-104-tumor_ground_truth.tsv | 2 +- .../rna-105-tumor_ground_truth.tsv | 2 +- .../rna-106-tumor_ground_truth.tsv | 2 +- .../rna-107-tumor_ground_truth.tsv | 2 +- .../rna-108-tumor_ground_truth.tsv | 2 +- .../rna-109-tumor_ground_truth.tsv | 2 +- ...umor_transcript_structure_ground_truth.tsv | 44 +- .../rna-110-tumor_ground_truth.tsv | 2 +- .../rna-111-tumor_ground_truth.tsv | 2 +- .../rna-112-tumor_ground_truth.tsv | 2 + exacto/exacto-caller/src/tests/mod.rs | 10 +- .../exacto-caller/src/tests/test_alignment.rs | 577 +- .../src/tests/test_alignment_record.rs | 29 +- .../src/tests/test_alignment_structure.rs | 655 +- .../src/tests/test_playground.rs | 62 - .../test_reference_transcript_matching.rs | 13 +- .../src/tests/test_transcript_model.rs | 667 +- .../src/tests/test_transcript_model_set.rs | 76 +- .../src/tests/test_variant_calling.rs | 483 +- .../src/tests/test_variant_calling_dna.rs | 529 +- .../src/tests/test_variant_calling_peptide.rs | 56 - .../src/tests/test_variant_calling_rna.rs | 147 +- exacto/exacto-core/Cargo.toml | 48 +- .../gene}/cds.rs | 14 +- .../gene}/exon.rs | 14 +- .../gene}/gencode.rs | 88 +- .../gene}/gene.rs | 10 +- .../gene}/gene_annotator.rs | 9 +- .../gene}/intron.rs | 14 +- .../gene}/mod.rs | 4 +- .../gene}/start_codon.rs | 14 +- .../gene}/stop_codon.rs | 14 +- .../gene}/transcript.rs | 47 +- .../src/annotation/gene/tsv_gene_annotator.rs | 304 + .../gene}/utr.rs | 14 +- exacto/exacto-core/src/annotation/mod.rs | 1 + exacto/exacto-core/src/common/algorithms.rs | 56 + exacto/exacto-core/src/common/bam.rs | 571 +- exacto/exacto-core/src/common/constants.rs | 4 +- exacto/exacto-core/src/common/fasta.rs | 84 +- exacto/exacto-core/src/common/files.rs | 8 +- exacto/exacto-core/src/common/logging.rs | 15 +- .../src/common/math.rs} | 21 +- exacto/exacto-core/src/common/mod.rs | 5 +- exacto/exacto-core/src/common/sequences.rs | 88 +- exacto/exacto-core/src/common/utilities.rs | 220 +- exacto/exacto-core/src/index/fasta_map.rs | 58 + .../interval_tree.rs | 0 .../{structs/data_structures => index}/mod.rs | 1 + .../data_structures => index}/trie.rs | 2 +- .../data_structures => index}/union_find.rs | 34 +- exacto/exacto-core/src/lib.rs | 7 +- exacto/exacto-core/src/macros.rs | 6 +- exacto/exacto-core/src/prelude.rs | 31 +- exacto/exacto-core/src/structs/mod.rs | 2 - ...gh-read-count_minimap2_mdtagged_sorted.bam | Bin 10886 -> 0 bytes ...ead-count_minimap2_mdtagged_sorted.bam.bai | Bin 3880 -> 0 bytes ...gh-read-count_minimap2_mdtagged_sorted.bam | Bin 16837 -> 0 bytes ...ead-count_minimap2_mdtagged_sorted.bam.bai | Bin 3928 -> 0 bytes .../data/tsv/sample_gene_annotations.tsv | 21 + exacto/exacto-core/src/tests/mod.rs | 3 + .../exacto-core/src/tests/test_algorithms.rs | 13 + exacto/exacto-core/src/tests/test_bam.rs | 134 +- exacto/exacto-core/src/tests/test_fasta.rs | 2 +- .../exacto-core/src/tests/test_fasta_map.rs | 15 + exacto/exacto-core/src/tests/test_gene.rs | 8 +- .../exacto-core/src/tests/test_sequences.rs | 41 +- .../exacto-core/src/tests/test_transcript.rs | 14 +- .../src/tests/test_tsv_gene_annotator.rs | 56 + .../exacto-core/src/tests/test_union_find.rs | 33 +- .../exacto-core/src/tests/test_utilities.rs | 138 +- exacto/exacto-core/src/traits/mod.rs | 1 - exacto/exacto-graph/Cargo.toml | 18 +- .../src/algorithms/variation_graph.rs | 479 +- exacto/exacto-graph/src/common/constants.rs | 89 +- exacto/exacto-graph/src/common/mod.rs | 1 - exacto/exacto-graph/src/common/parsers.rs | 72 - .../src/{structs => graph}/mod.rs | 4 +- .../src/{structs => graph}/multidigraph.rs | 355 +- exacto/exacto-graph/src/graph/vargraph.rs | 2168 + .../exacto-graph/src/graph/vargraph_edge.rs | 85 + .../exacto-graph/src/graph/vargraph_node.rs | 124 + .../src/{structs => graph}/vargraph_path.rs | 62 +- .../src/graph/vargraph_reference_node.rs | 96 + .../src/graph/vargraph_segment.rs | 113 + .../src/graph/vargraph_traversal_state.rs | 63 + .../src/graph/vargraph_variant_node.rs | 89 + exacto/exacto-graph/src/lib.rs | 8 +- exacto/exacto-graph/src/prelude.rs | 19 +- exacto/exacto-graph/src/structs/graph.rs | 272 - exacto/exacto-graph/src/structs/vargraph.rs | 1071 - .../exacto-graph/src/structs/vargraph_edge.rs | 64 - .../exacto-graph/src/structs/vargraph_port.rs | 65 - .../src/structs/vargraph_reference_node.rs | 92 - .../src/structs/vargraph_segment.rs | 69 - .../src/structs/vargraph_variant_node.rs | 121 - .../src/tests/data/fasta/sample.fa.fai | 2 +- .../src/tests/data/fasta/sample2.fa.fai | 2 +- .../src/tests/data/fasta/sample3.fa | 6 + .../src/tests/data/fasta/sample3.fa.fai | 3 + .../data/tsv/sample_dna_variant_callset_1.tsv | 2 + .../tsv/sample_dna_variant_callset_10.tsv | 7 + .../tsv/sample_dna_variant_callset_11.tsv | 2 + .../tsv/sample_dna_variant_callset_12.tsv | 5 + .../tsv/sample_dna_variant_callset_13.tsv | 5 + .../tsv/sample_dna_variant_callset_14.tsv | 3 + .../tsv/sample_dna_variant_callset_15.tsv | 3 + .../data/tsv/sample_dna_variant_callset_2.tsv | 3 + .../data/tsv/sample_dna_variant_callset_3.tsv | 3 + .../data/tsv/sample_dna_variant_callset_4.tsv | 2 + .../data/tsv/sample_dna_variant_callset_5.tsv | 3 + .../data/tsv/sample_dna_variant_callset_6.tsv | 3 + .../data/tsv/sample_dna_variant_callset_7.tsv | 3 + .../data/tsv/sample_dna_variant_callset_8.tsv | 2 + .../data/tsv/sample_dna_variant_callset_9.tsv | 3 + .../data/tsv/sample_gene_annotations.tsv | 21 + .../data/tsv/sample_rna_variant_callset_1.tsv | 8 + .../tsv/sample_rna_variant_callset_10.tsv | 8 + .../tsv/sample_rna_variant_callset_11.tsv | 3 + .../tsv/sample_rna_variant_callset_12.tsv | 7 + .../tsv/sample_rna_variant_callset_13.tsv | 5 + .../tsv/sample_rna_variant_callset_14.tsv | 7 + .../tsv/sample_rna_variant_callset_15.tsv | 7 + .../tsv/sample_rna_variant_callset_16.tsv | 2 + .../tsv/sample_rna_variant_callset_17.tsv | 2 + .../tsv/sample_rna_variant_callset_18.tsv | 6 + .../tsv/sample_rna_variant_callset_19.tsv | 6 + .../data/tsv/sample_rna_variant_callset_2.tsv | 8 + .../tsv/sample_rna_variant_callset_20.tsv | 4 + .../data/tsv/sample_rna_variant_callset_3.tsv | 8 + .../data/tsv/sample_rna_variant_callset_4.tsv | 7 + .../data/tsv/sample_rna_variant_callset_5.tsv | 10 + .../data/tsv/sample_rna_variant_callset_6.tsv | 6 + .../data/tsv/sample_rna_variant_callset_7.tsv | 8 + .../data/tsv/sample_rna_variant_callset_8.tsv | 12 + .../data/tsv/sample_rna_variant_callset_9.tsv | 6 + .../data/tsv/sample_variant_callset_1.tsv | 2 - .../data/tsv/sample_variant_callset_2.tsv | 3 - .../data/tsv/sample_variant_callset_3.tsv | 3 - .../data/tsv/sample_variant_callset_4.tsv | 2 - exacto/exacto-graph/src/tests/mod.rs | 9 +- .../src/tests/test_multidigraph.rs | 161 + .../exacto-graph/src/tests/test_vargraph.rs | 350 + .../src/tests/test_variation_graph.rs | 1472 + .../src/tests/unit_tests_graph.rs | 140 - .../src/tests/unit_tests_multidigraph.rs | 163 - .../src/tests/unit_tests_parsers.rs | 38 - .../src/tests/unit_tests_playground.rs | 22 - .../src/tests/unit_tests_vargraph.rs | 237 - .../src/tests/unit_tests_variation_graph.rs | 109 - exacto/exacto-graph/src/traits/mod.rs | 1 - .../exacto-graph/src/traits/vargraph_node.rs | 31 - exacto/exacto-integrator/Cargo.toml | 24 +- .../src/algorithms/variant_integration.rs | 40 +- exacto/exacto-integrator/src/lib.rs | 2 +- .../src/structs/integrated_variant.rs | 14 +- .../structs/integrated_variant_distance.rs | 8 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...tagged_sorted_exacto_rna_variant_calls.tsv | 2 +- exacto/exacto-qc/Cargo.toml | 25 +- .../src/algorithms/unspliced_rna_filtering.rs | 4 +- exacto/exacto-translator/Cargo.toml | 26 +- .../src/algorithms/translation.rs | 331 +- .../exacto-translator/src/common/constants.rs | 16 + exacto/exacto-translator/src/lib.rs | 3 +- exacto/exacto-translator/src/prelude.rs | 14 +- .../src/primary_structure/mod.rs | 3 + .../primary_structure.rs | 29 +- .../primary_structure_record.rs | 83 +- .../primary_structure_set.rs | 30 +- exacto/exacto-translator/src/structs/mod.rs | 7 - ...tagged_sorted_exacto_rna_variant_calls.tsv | 2 +- ...ed_sorted_exacto_transcript_structures.tsv | 298 +- exacto/exacto-translator/src/tests/mod.rs | 2 +- .../src/tests/test_translation.rs | 920 + .../src/tests/unit_tests_translation.rs | 102 - .../exacto-translator/src/translation/mod.rs | 4 + .../src/{structs => translation}/peptide.rs | 12 +- .../src/{structs => translation}/rna.rs | 2 +- .../{structs => translation}/translation.rs | 0 .../translation_set.rs | 0 exacto/exacto/Cargo.toml | 24 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- .../sample_dna_variant_callset_1.fasta | 6 + .../sample_dna_variant_callset_2.fasta | 6 + .../sample_dna_variant_callset_3.fasta | 6 + .../sample_dna_variant_callset_4.fasta | 4 + .../sample_dna_variant_callset_4_1.fasta | 6 + .../sample_dna_variant_callset_4_2.fasta | 4 + .../sample_transcriptome.fasta | 2 + ...nimap2_mdtagged_sorted_exacto_variants.tsv | 1 + ...agged_sorted_exacto_peptide_variants.fasta | 76 + ...dtagged_sorted_exacto_peptide_variants.tsv | 229 + ..._minimap2_mdtagged_sorted_exacto_exons.tsv | 12 + ...inimap2_mdtagged_sorted_exacto_introns.tsv | 11 + ...agged_sorted_exacto_read_filter_status.tsv | 0 ...ed_exacto_reference_transcript_matches.tsv | 5 +- ...tagged_sorted_exacto_rna_variant_calls.tsv | 8 + ...ed_sorted_exacto_transcript_structures.tsv | 74 + ...ap2_mdtagged_sorted_exacto_transcripts.tsv | 2 +- ...sorted_exacto_transcripts_read_support.tsv | 0 ...dtagged_sorted_exacto_somatic_variants.tsv | 1 + ...dtagged_sorted_exacto_somatic_variants.tsv | 2 - ...nimap2_mdtagged_sorted_exacto_variants.tsv | 2 - ...umor_dna-001-tumor_variants_integrated.tsv | 8 +- ...ead_dna-001-tumor_long-read.filtered.fasta | 0 ...dna-001-tumor_long-read.filtered.fasta.fai | 0 ...umor_minimap2_mdtagged_sorted.filtered.bam | Bin 0 -> 1906 bytes ..._minimap2_mdtagged_sorted.filtered.bam.bai | Bin 3856 -> 3856 bytes ..._minimap2_mdtagged_sorted_exacto_exons.tsv | 12 - ...inimap2_mdtagged_sorted_exacto_introns.tsv | 11 - ...tagged_sorted_exacto_rna_variant_calls.tsv | 19 - ...ed_sorted_exacto_transcript_structures.tsv | 149 - ...umor_minimap2_mdtagged_sorted.filtered.bam | Bin 1902 -> 0 bytes ...agged_sorted_exacto_primary_structures.tsv | 7147 - ..._rna_exacto_translations_all-orfs.fasta.gz | Bin ..._exacto_translations_all-orfs.fasta.gz.fai | 0 ..._exacto_translations_all-orfs.fasta.gz.gzi | Bin ...ad_rna_exacto_translations_all-orfs.tsv.gz | Bin 4511 -> 4511 bytes ...a_exacto_translations_longest-orf.fasta.gz | Bin ...acto_translations_longest-orf.fasta.gz.fai | 0 ...acto_translations_longest-orf.fasta.gz.gzi | Bin ...rna_exacto_translations_longest-orf.tsv.gz | Bin 1480 -> 1480 bytes ...ged_sorted_exacto_primary_structures.fasta | 0 ...sorted_exacto_primary_structures.fasta.fai | 0 ...agged_sorted_exacto_primary_structures.tsv | 7147 + examples/run_exacto_annotate_vars.sh | 4 +- examples/run_exacto_build_genome_var_graph.sh | 44 + ...un_exacto_build_transcriptome_var_graph.sh | 6 + examples/run_exacto_build_var_graph.sh | 4 - examples/run_exacto_call_dna_vars.sh | 13 - examples/run_exacto_call_germline_dna_vars.sh | 7 + examples/run_exacto_call_peptide_vars.sh | 10 + examples/run_exacto_call_rna_vars.sh | 4 +- examples/run_exacto_call_somatic_dna_vars.sh | 9 + examples/run_exacto_diff_kmers.sh | 6 - examples/run_exacto_integrate_vars.sh | 4 +- examples/run_exacto_remove_unspliced_rnas.sh | 8 +- examples/run_exacto_translate_seqs.sh | 10 +- examples/run_exacto_translate_structs.sh | 6 +- pyproject.toml | 26 +- python/exactolib/cli/cli_annotate_vars.py | 1 - .../cli/cli_build_genome_var_graph.py | 137 + .../cli/cli_build_transcriptome_var_graph.py | 118 + python/exactolib/cli/cli_build_var_graph.py | 86 - python/exactolib/cli/cli_call_dna_vars.py | 279 - .../cli/cli_call_germline_dna_vars.py | 371 + python/exactolib/cli/cli_call_peptide_vars.py | 163 +- python/exactolib/cli/cli_call_rna_vars.py | 53 +- .../cli/cli_call_somatic_dna_vars.py | 387 + python/exactolib/cli/cli_diff_kmers.py | 139 - python/exactolib/cli/cli_integrate_vars.py | 2 - python/exactolib/cli/cli_main.py | 51 +- .../cli/cli_remove_unspliced_rnas.py | 1 - python/exactolib/cli/cli_report.py | 178 - python/exactolib/cli/cli_translate_seqs.py | 7 +- python/exactolib/cli/cli_translate_structs.py | 5 +- python/exactolib/constants.py | 15 +- python/exactolib/default.py | 93 +- .../exactolib/{scripts => index}/__init__.py | 0 python/exactolib/index/reference.py | 76 + python/exactolib/main.py | 371 +- python/exactolib/scripts/format_gtf_file.py | 62 - .../exactolib/scripts/format_oarfish_file.py | 130 - .../scripts/format_transigner_file.py | 84 - .../scripts/prepare_excl_bed_file.py | 109 - python/exactolib/utilities.py | 21 +- python/exactolib/variant_calling/__init__.py | 0 python/exactolib/variant_calling/peptide.py | 222 + .../align_hg38_long_read_dna_fastq_files.sh | 15 +- .../bam/samples_long_read_dna_fastq_files.tsv | 4 +- .../bam/samples_long_read_rna_fastq_files.tsv | 2 +- .../data/fastq/create_dna-009_fastq_files.py | 40 +- .../data/fastq/create_dna-010_fastq_files.py | 40 +- .../data/fastq/create_dna-011_fastq_files.py | 40 +- .../data/fastq/create_rna-112_fastq_file.py | 70 + scripts/docs/generate_cli_docs.py | 522 + src/functions/build_genome_variation_graph.rs | 280 + .../build_transcriptome_variation_graph.rs | 137 + src/functions/build_variation_graph.rs | 51 - ...s.rs => identify_germline_dna_variants.rs} | 44 +- src/functions/identify_peptide_variants.rs | 79 - src/functions/identify_rna_variants.rs | 29 +- ...ts.rs => identify_somatic_dna_variants.rs} | 56 +- src/functions/integrate_dna_rna_variants.rs | 6 +- src/functions/mod.rs | 10 +- src/functions/remove_unspliced_rnas.rs | 2 +- src/functions/translate_fasta_file.rs | 39 +- src/functions/translate_sequence.rs | 4 +- src/lib.rs | 58 +- ...na-005-normal_minimap2_mdtagged_sorted.bam | Bin 14773 -> 14758 bytes ...05-normal_minimap2_mdtagged_sorted.bam.bai | Bin 3880 -> 3880 bytes ...dna-005-tumor_minimap2_mdtagged_sorted.bam | Bin 38073 -> 38060 bytes ...005-tumor_minimap2_mdtagged_sorted.bam.bai | Bin 3880 -> 3880 bytes ...na-009-normal_minimap2_mdtagged_sorted.bam | Bin 19813 -> 208363 bytes ...09-normal_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3856 bytes ...dna-009-tumor_minimap2_mdtagged_sorted.bam | Bin 118906 -> 641886 bytes ...009-tumor_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3880 bytes ...na-010-normal_minimap2_mdtagged_sorted.bam | Bin 19814 -> 104355 bytes ...10-normal_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3856 bytes ...dna-010-tumor_minimap2_mdtagged_sorted.bam | Bin 153622 -> 829757 bytes ...010-tumor_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3880 bytes ...na-011-normal_minimap2_mdtagged_sorted.bam | Bin 10407 -> 44305 bytes ...11-normal_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3856 bytes ...dna-011-tumor_minimap2_mdtagged_sorted.bam | Bin 16044 -> 83467 bytes ...011-tumor_minimap2_mdtagged_sorted.bam.bai | Bin 3856 -> 3856 bytes ...gh-read-count_minimap2_mdtagged_sorted.bam | Bin 10886 -> 0 bytes ...ead-count_minimap2_mdtagged_sorted.bam.bai | Bin 3880 -> 0 bytes ...gh-read-count_minimap2_mdtagged_sorted.bam | Bin 16837 -> 0 bytes ...ead-count_minimap2_mdtagged_sorted.bam.bai | Bin 3928 -> 0 bytes ...rna-112-tumor_minimap2_mdtagged_sorted.bam | Bin 0 -> 1706 bytes ...112-tumor_minimap2_mdtagged_sorted.bam.bai | Bin 0 -> 3864 bytes test/data/fasta/hg38_chr17_1-2M.fa | 33335 ---- test/data/fasta/hg38_chr17_1-2M.fa.fai | 1 - test/data/fasta/hg38_chr17_1-3M.fa | 50001 ------ test/data/fasta/hg38_chr17_1-3M.fa.fai | 1 - test/data/fasta/hg38_chr17_1-8M.fa | 133335 --------------- test/data/fasta/hg38_chr17_1-8M.fa.fai | 1 - ...ences.fa.gz => reference_peptides_2.fa.gz} | Bin test/data/fasta/sample2.fa | 6 + test/data/fasta/sample2.fa.fai | 3 + test/data/fasta/sample3.fa | 6 + test/data/fasta/sample3.fa.fai | 3 + .../sample_tumor_long_read_rna.fasta.fai | 2 + .../fastq/dna-009-normal_long-read.fastq.gz | Bin 23047 -> 228300 bytes .../fastq/dna-009-tumor_long-read.fastq.gz | Bin 62640 -> 312025 bytes .../fastq/dna-010-normal_long-read.fastq.gz | Bin 23048 -> 114227 bytes .../fastq/dna-010-tumor_long-read.fastq.gz | Bin 65130 -> 324307 bytes .../fastq/dna-011-normal_long-read.fastq.gz | Bin 17366 -> 85489 bytes .../fastq/dna-011-tumor_long-read.fastq.gz | Bin 34457 -> 170894 bytes ...3-tumor_long-read_high_read_count.fastq.gz | Bin 5283 -> 0 bytes ...9-tumor_long-read_high_read_count.fastq.gz | Bin 6771 -> 0 bytes .../fastq/rna-112-tumor_long-read.fastq.gz | Bin 0 -> 751 bytes .../rna-103-tumor_ground_truth.tsv | 4 +- .../rna-112-tumor_ground_truth.tsv | 2 + ...agged_sorted_exacto_primary_structures.tsv | 7147 + .../sample_transcript_structure.tsv | 8 + ...dtagged_sorted_exacto_somatic_variants.tsv | 4 +- ...rted_exacto_somatic_variants_annotated.tsv | 4 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 4 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 4 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 6 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 4 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 4 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 4 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 6 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 8 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 10 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...dtagged_sorted_exacto_somatic_variants.tsv | 6 +- ...rted_exacto_somatic_variants_annotated.tsv | 2 +- ...tagged_sorted_exacto_rna_variant_calls.tsv | 2 +- ...ed_sorted_exacto_transcript_structures.tsv | 298 +- .../sample_dna_variant_callset_1.tsv | 2 + .../sample_dna_variant_callset_2.tsv | 3 + .../sample_dna_variant_callset_3.tsv | 3 + .../sample_dna_variant_callset_4.tsv | 2 + .../sample_rna_variant_callset_1.tsv | 8 + .../sample_rna_variant_callset_10.tsv | 8 + .../sample_rna_variant_callset_11.tsv | 3 + .../sample_rna_variant_callset_12.tsv | 7 + .../sample_rna_variant_callset_13.tsv | 5 + .../sample_rna_variant_callset_14.tsv | 7 + .../sample_rna_variant_callset_15.tsv | 7 + .../sample_rna_variant_callset_16.tsv | 2 + .../sample_rna_variant_callset_17.tsv | 2 + .../sample_rna_variant_callset_18.tsv | 6 + .../sample_rna_variant_callset_19.tsv | 6 + .../sample_rna_variant_callset_2.tsv | 8 + .../sample_rna_variant_callset_20.tsv | 4 + .../sample_rna_variant_callset_3.tsv | 8 + .../sample_rna_variant_callset_4.tsv | 7 + .../sample_rna_variant_callset_5.tsv | 10 + .../sample_rna_variant_callset_6.tsv | 6 + .../sample_rna_variant_callset_7.tsv | 8 + .../sample_rna_variant_callset_8.tsv | 12 + .../sample_rna_variant_callset_9.tsv | 6 + .../sample_variant_callset_3.tsv | 3 - test/test_build_genome_var_graph.py | 112 + test/test_build_transcriptome_var_graph.py | 427 + test/test_call_germline_dna_variants.py | 200 + test/test_call_peptide_variants.py | 13 + test/test_call_rna_variants.py | 79 +- ...s.py => test_call_somatic_dna_variants.py} | 331 +- test/test_diff_kmers.py | 20 - test/test_translate_structs.py | 6 +- 495 files changed, 33891 insertions(+), 234140 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/main.yml create mode 100644 docs/.gitignore create mode 100644 docs/_quarto.yml create mode 100644 docs/cli/index.qmd create mode 100644 docs/custom.scss create mode 100644 docs/faq.qmd create mode 100644 docs/index.qmd create mode 100644 docs/pipelines/index.qmd delete mode 100644 exacto/exacto-caller/src/algorithms/variant_calling_peptide.rs delete mode 100644 exacto/exacto-caller/src/common/logging.rs create mode 100644 exacto/exacto-caller/src/structs/graph_operation_view.rs delete mode 100644 exacto/exacto-caller/src/tests/data/bam/rna-103-tumor-high-read-count_minimap2_mdtagged_sorted.bam delete mode 100644 exacto/exacto-caller/src/tests/data/bam/rna-103-tumor-high-read-count_minimap2_mdtagged_sorted.bam.bai delete mode 100644 exacto/exacto-caller/src/tests/data/bam/rna-109-tumor-high-read-count_minimap2_mdtagged_sorted.bam delete mode 100644 exacto/exacto-caller/src/tests/data/bam/rna-109-tumor-high-read-count_minimap2_mdtagged_sorted.bam.bai create mode 100644 exacto/exacto-caller/src/tests/data/bam/rna-112-tumor_minimap2_mdtagged_sorted.bam create mode 100644 exacto/exacto-caller/src/tests/data/bam/rna-112-tumor_minimap2_mdtagged_sorted.bam.bai create mode 100644 exacto/exacto-caller/src/tests/data/tsv/ground_truth/rna-112-tumor_ground_truth.tsv delete mode 100644 exacto/exacto-caller/src/tests/test_playground.rs delete mode 100644 exacto/exacto-caller/src/tests/test_variant_calling_peptide.rs rename exacto/exacto-core/src/{structs/gene_annotation => annotation/gene}/cds.rs (94%) rename exacto/exacto-core/src/{structs/gene_annotation => annotation/gene}/exon.rs (94%) rename exacto/exacto-core/src/{structs/gene_annotation => annotation/gene}/gencode.rs (82%) rename exacto/exacto-core/src/{structs/gene_annotation => annotation/gene}/gene.rs (96%) rename exacto/exacto-core/src/{traits => annotation/gene}/gene_annotator.rs (84%) rename exacto/exacto-core/src/{structs/gene_annotation => annotation/gene}/intron.rs (93%) rename exacto/exacto-core/src/{structs/gene_annotation => annotation/gene}/mod.rs (67%) rename exacto/exacto-core/src/{structs/gene_annotation => annotation/gene}/start_codon.rs (94%) rename exacto/exacto-core/src/{structs/gene_annotation => annotation/gene}/stop_codon.rs (94%) rename exacto/exacto-core/src/{structs/gene_annotation => annotation/gene}/transcript.rs (90%) create mode 100644 exacto/exacto-core/src/annotation/gene/tsv_gene_annotator.rs rename exacto/exacto-core/src/{structs/gene_annotation => annotation/gene}/utr.rs (94%) create mode 100644 exacto/exacto-core/src/annotation/mod.rs create mode 100644 exacto/exacto-core/src/common/algorithms.rs rename exacto/{exacto-caller/src/macros.rs => exacto-core/src/common/math.rs} (64%) create mode 100644 exacto/exacto-core/src/index/fasta_map.rs rename exacto/exacto-core/src/{structs/data_structures => index}/interval_tree.rs (100%) rename exacto/exacto-core/src/{structs/data_structures => index}/mod.rs (75%) rename exacto/exacto-core/src/{structs/data_structures => index}/trie.rs (98%) rename exacto/exacto-core/src/{structs/data_structures => index}/union_find.rs (67%) delete mode 100644 exacto/exacto-core/src/structs/mod.rs delete mode 100644 exacto/exacto-core/src/tests/data/bam/rna-103-tumor-high-read-count_minimap2_mdtagged_sorted.bam delete mode 100644 exacto/exacto-core/src/tests/data/bam/rna-103-tumor-high-read-count_minimap2_mdtagged_sorted.bam.bai delete mode 100644 exacto/exacto-core/src/tests/data/bam/rna-109-tumor-high-read-count_minimap2_mdtagged_sorted.bam delete mode 100644 exacto/exacto-core/src/tests/data/bam/rna-109-tumor-high-read-count_minimap2_mdtagged_sorted.bam.bai create mode 100644 exacto/exacto-core/src/tests/data/tsv/sample_gene_annotations.tsv create mode 100644 exacto/exacto-core/src/tests/test_algorithms.rs create mode 100644 exacto/exacto-core/src/tests/test_fasta_map.rs create mode 100644 exacto/exacto-core/src/tests/test_tsv_gene_annotator.rs delete mode 100644 exacto/exacto-core/src/traits/mod.rs delete mode 100644 exacto/exacto-graph/src/common/parsers.rs rename exacto/exacto-graph/src/{structs => graph}/mod.rs (75%) rename exacto/exacto-graph/src/{structs => graph}/multidigraph.rs (57%) create mode 100644 exacto/exacto-graph/src/graph/vargraph.rs create mode 100644 exacto/exacto-graph/src/graph/vargraph_edge.rs create mode 100644 exacto/exacto-graph/src/graph/vargraph_node.rs rename exacto/exacto-graph/src/{structs => graph}/vargraph_path.rs (50%) create mode 100644 exacto/exacto-graph/src/graph/vargraph_reference_node.rs create mode 100644 exacto/exacto-graph/src/graph/vargraph_segment.rs create mode 100644 exacto/exacto-graph/src/graph/vargraph_traversal_state.rs create mode 100644 exacto/exacto-graph/src/graph/vargraph_variant_node.rs delete mode 100644 exacto/exacto-graph/src/structs/graph.rs delete mode 100644 exacto/exacto-graph/src/structs/vargraph.rs delete mode 100644 exacto/exacto-graph/src/structs/vargraph_edge.rs delete mode 100644 exacto/exacto-graph/src/structs/vargraph_port.rs delete mode 100644 exacto/exacto-graph/src/structs/vargraph_reference_node.rs delete mode 100644 exacto/exacto-graph/src/structs/vargraph_segment.rs delete mode 100644 exacto/exacto-graph/src/structs/vargraph_variant_node.rs create mode 100644 exacto/exacto-graph/src/tests/data/fasta/sample3.fa create mode 100644 exacto/exacto-graph/src/tests/data/fasta/sample3.fa.fai create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_1.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_10.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_11.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_12.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_13.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_14.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_15.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_2.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_3.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_4.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_5.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_6.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_7.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_8.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_dna_variant_callset_9.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_gene_annotations.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_1.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_10.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_11.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_12.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_13.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_14.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_15.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_16.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_17.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_18.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_19.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_2.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_20.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_3.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_4.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_5.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_6.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_7.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_8.tsv create mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_rna_variant_callset_9.tsv delete mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_variant_callset_1.tsv delete mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_variant_callset_2.tsv delete mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_variant_callset_3.tsv delete mode 100644 exacto/exacto-graph/src/tests/data/tsv/sample_variant_callset_4.tsv create mode 100644 exacto/exacto-graph/src/tests/test_multidigraph.rs create mode 100644 exacto/exacto-graph/src/tests/test_vargraph.rs create mode 100644 exacto/exacto-graph/src/tests/test_variation_graph.rs delete mode 100644 exacto/exacto-graph/src/tests/unit_tests_graph.rs delete mode 100644 exacto/exacto-graph/src/tests/unit_tests_multidigraph.rs delete mode 100644 exacto/exacto-graph/src/tests/unit_tests_parsers.rs delete mode 100644 exacto/exacto-graph/src/tests/unit_tests_playground.rs delete mode 100644 exacto/exacto-graph/src/tests/unit_tests_vargraph.rs delete mode 100644 exacto/exacto-graph/src/tests/unit_tests_variation_graph.rs delete mode 100644 exacto/exacto-graph/src/traits/mod.rs delete mode 100644 exacto/exacto-graph/src/traits/vargraph_node.rs create mode 100644 exacto/exacto-translator/src/primary_structure/mod.rs rename exacto/exacto-translator/src/{structs => primary_structure}/primary_structure.rs (69%) rename exacto/exacto-translator/src/{structs => primary_structure}/primary_structure_record.rs (63%) rename exacto/exacto-translator/src/{structs => primary_structure}/primary_structure_set.rs (73%) delete mode 100644 exacto/exacto-translator/src/structs/mod.rs create mode 100644 exacto/exacto-translator/src/tests/test_translation.rs delete mode 100644 exacto/exacto-translator/src/tests/unit_tests_translation.rs create mode 100644 exacto/exacto-translator/src/translation/mod.rs rename exacto/exacto-translator/src/{structs => translation}/peptide.rs (87%) rename exacto/exacto-translator/src/{structs => translation}/rna.rs (96%) rename exacto/exacto-translator/src/{structs => translation}/translation.rs (100%) rename exacto/exacto-translator/src/{structs => translation}/translation_set.rs (100%) rename examples/outputs/{ => annotate-vars}/dna-001-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants_annotated.tsv (95%) create mode 100644 examples/outputs/build-genome-var-graph/sample_dna_variant_callset_1.fasta create mode 100644 examples/outputs/build-genome-var-graph/sample_dna_variant_callset_2.fasta create mode 100644 examples/outputs/build-genome-var-graph/sample_dna_variant_callset_3.fasta create mode 100644 examples/outputs/build-genome-var-graph/sample_dna_variant_callset_4.fasta create mode 100644 examples/outputs/build-genome-var-graph/sample_dna_variant_callset_4_1.fasta create mode 100644 examples/outputs/build-genome-var-graph/sample_dna_variant_callset_4_2.fasta create mode 100644 examples/outputs/build-transcriptome-var-graph/sample_transcriptome.fasta create mode 100644 examples/outputs/call-germline-dna-vars/dna-001-tumor_minimap2_mdtagged_sorted_exacto_variants.tsv create mode 100644 examples/outputs/call-peptide-vars/rna-100-tumor_minimap2_mdtagged_sorted_exacto_peptide_variants.fasta create mode 100644 examples/outputs/call-peptide-vars/rna-100-tumor_minimap2_mdtagged_sorted_exacto_peptide_variants.tsv create mode 100644 examples/outputs/call-rna-vars/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_exons.tsv create mode 100644 examples/outputs/call-rna-vars/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_introns.tsv rename examples/outputs/{ => call-rna-vars}/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_read_filter_status.tsv (100%) rename examples/outputs/{ => call-rna-vars}/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_reference_transcript_matches.tsv (63%) create mode 100644 examples/outputs/call-rna-vars/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_rna_variant_calls.tsv create mode 100644 examples/outputs/call-rna-vars/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_transcript_structures.tsv rename examples/outputs/{ => call-rna-vars}/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_transcripts.tsv (70%) rename examples/outputs/{ => call-rna-vars}/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_transcripts_read_support.tsv (100%) create mode 100644 examples/outputs/call-somatic-dna-vars/dna-001-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv delete mode 100644 examples/outputs/dna-001-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv delete mode 100644 examples/outputs/dna-001-tumor_minimap2_mdtagged_sorted_exacto_variants.tsv rename examples/outputs/{ => integrate-vars}/rna-100-tumor_dna-001-tumor_variants_integrated.tsv (100%) rename examples/outputs/{ => remove-unspliced-rnas}/rna-100-tumor_long-read_dna-001-tumor_long-read.filtered.fasta (100%) rename examples/outputs/{ => remove-unspliced-rnas}/rna-100-tumor_long-read_dna-001-tumor_long-read.filtered.fasta.fai (100%) create mode 100644 examples/outputs/remove-unspliced-rnas/rna-100-tumor_minimap2_mdtagged_sorted_dna-001-tumor_minimap2_mdtagged_sorted.filtered.bam rename examples/outputs/{ => remove-unspliced-rnas}/rna-100-tumor_minimap2_mdtagged_sorted_dna-001-tumor_minimap2_mdtagged_sorted.filtered.bam.bai (98%) delete mode 100644 examples/outputs/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_exons.tsv delete mode 100644 examples/outputs/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_introns.tsv delete mode 100644 examples/outputs/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_rna_variant_calls.tsv delete mode 100644 examples/outputs/rna-100-tumor-minimap2_mdtagged_sorted_bam_exacto_call_rna_vars_outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_transcript_structures.tsv delete mode 100644 examples/outputs/rna-100-tumor_minimap2_mdtagged_sorted_dna-001-tumor_minimap2_mdtagged_sorted.filtered.bam delete mode 100644 examples/outputs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_primary_structures.tsv rename examples/outputs/{ => translate-seqs}/sample_tumor_long_read_rna_exacto_translations_all-orfs.fasta.gz (100%) rename examples/outputs/{ => translate-seqs}/sample_tumor_long_read_rna_exacto_translations_all-orfs.fasta.gz.fai (100%) rename examples/outputs/{ => translate-seqs}/sample_tumor_long_read_rna_exacto_translations_all-orfs.fasta.gz.gzi (100%) rename examples/outputs/{ => translate-seqs}/sample_tumor_long_read_rna_exacto_translations_all-orfs.tsv.gz (98%) rename examples/outputs/{ => translate-seqs}/sample_tumor_long_read_rna_exacto_translations_longest-orf.fasta.gz (100%) rename examples/outputs/{ => translate-seqs}/sample_tumor_long_read_rna_exacto_translations_longest-orf.fasta.gz.fai (100%) rename examples/outputs/{ => translate-seqs}/sample_tumor_long_read_rna_exacto_translations_longest-orf.fasta.gz.gzi (100%) rename examples/outputs/{ => translate-seqs}/sample_tumor_long_read_rna_exacto_translations_longest-orf.tsv.gz (95%) rename examples/outputs/{ => translate-structs}/rna-100-tumor_minimap2_mdtagged_sorted_exacto_primary_structures.fasta (100%) rename examples/outputs/{ => translate-structs}/rna-100-tumor_minimap2_mdtagged_sorted_exacto_primary_structures.fasta.fai (100%) create mode 100644 examples/outputs/translate-structs/rna-100-tumor_minimap2_mdtagged_sorted_exacto_primary_structures.tsv create mode 100644 examples/run_exacto_build_genome_var_graph.sh create mode 100644 examples/run_exacto_build_transcriptome_var_graph.sh delete mode 100644 examples/run_exacto_build_var_graph.sh delete mode 100644 examples/run_exacto_call_dna_vars.sh create mode 100644 examples/run_exacto_call_germline_dna_vars.sh create mode 100644 examples/run_exacto_call_peptide_vars.sh create mode 100644 examples/run_exacto_call_somatic_dna_vars.sh delete mode 100644 examples/run_exacto_diff_kmers.sh create mode 100644 python/exactolib/cli/cli_build_genome_var_graph.py create mode 100644 python/exactolib/cli/cli_build_transcriptome_var_graph.py delete mode 100644 python/exactolib/cli/cli_build_var_graph.py delete mode 100644 python/exactolib/cli/cli_call_dna_vars.py create mode 100644 python/exactolib/cli/cli_call_germline_dna_vars.py create mode 100644 python/exactolib/cli/cli_call_somatic_dna_vars.py delete mode 100644 python/exactolib/cli/cli_diff_kmers.py delete mode 100644 python/exactolib/cli/cli_report.py rename python/exactolib/{scripts => index}/__init__.py (100%) create mode 100644 python/exactolib/index/reference.py delete mode 100644 python/exactolib/scripts/format_gtf_file.py delete mode 100644 python/exactolib/scripts/format_oarfish_file.py delete mode 100644 python/exactolib/scripts/format_transigner_file.py delete mode 100644 python/exactolib/scripts/prepare_excl_bed_file.py create mode 100644 python/exactolib/variant_calling/__init__.py create mode 100644 python/exactolib/variant_calling/peptide.py create mode 100644 scripts/data/fastq/create_rna-112_fastq_file.py create mode 100644 scripts/docs/generate_cli_docs.py create mode 100644 src/functions/build_genome_variation_graph.rs create mode 100644 src/functions/build_transcriptome_variation_graph.rs delete mode 100644 src/functions/build_variation_graph.rs rename src/functions/{identify_dna_variants.rs => identify_germline_dna_variants.rs} (64%) delete mode 100644 src/functions/identify_peptide_variants.rs rename src/functions/{identify_case_specific_dna_variants.rs => identify_somatic_dna_variants.rs} (64%) delete mode 100644 test/data/bam/rna-103-tumor-high-read-count_minimap2_mdtagged_sorted.bam delete mode 100644 test/data/bam/rna-103-tumor-high-read-count_minimap2_mdtagged_sorted.bam.bai delete mode 100644 test/data/bam/rna-109-tumor-high-read-count_minimap2_mdtagged_sorted.bam delete mode 100644 test/data/bam/rna-109-tumor-high-read-count_minimap2_mdtagged_sorted.bam.bai create mode 100644 test/data/bam/rna-112-tumor_minimap2_mdtagged_sorted.bam create mode 100644 test/data/bam/rna-112-tumor_minimap2_mdtagged_sorted.bam.bai delete mode 100644 test/data/fasta/hg38_chr17_1-2M.fa delete mode 100644 test/data/fasta/hg38_chr17_1-2M.fa.fai delete mode 100644 test/data/fasta/hg38_chr17_1-3M.fa delete mode 100644 test/data/fasta/hg38_chr17_1-3M.fa.fai delete mode 100644 test/data/fasta/hg38_chr17_1-8M.fa delete mode 100644 test/data/fasta/hg38_chr17_1-8M.fa.fai rename test/data/fasta/{reference_peptide_sequences.fa.gz => reference_peptides_2.fa.gz} (100%) create mode 100644 test/data/fasta/sample2.fa create mode 100644 test/data/fasta/sample2.fa.fai create mode 100644 test/data/fasta/sample3.fa create mode 100644 test/data/fasta/sample3.fa.fai create mode 100644 test/data/fasta/sample_tumor_long_read_rna.fasta.fai delete mode 100644 test/data/fastq/rna-103-tumor_long-read_high_read_count.fastq.gz delete mode 100644 test/data/fastq/rna-109-tumor_long-read_high_read_count.fastq.gz create mode 100644 test/data/fastq/rna-112-tumor_long-read.fastq.gz create mode 100644 test/data/tsv/ground_truth/rna-112-tumor_ground_truth.tsv create mode 100644 test/data/tsv/primary_structure/rna-100-tumor_minimap2_mdtagged_sorted_exacto_primary_structures.tsv create mode 100644 test/data/tsv/transcript_structure/sample_transcript_structure.tsv create mode 100644 test/data/tsv/variant_callset/sample_dna_variant_callset_1.tsv create mode 100644 test/data/tsv/variant_callset/sample_dna_variant_callset_2.tsv create mode 100644 test/data/tsv/variant_callset/sample_dna_variant_callset_3.tsv create mode 100644 test/data/tsv/variant_callset/sample_dna_variant_callset_4.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_1.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_10.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_11.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_12.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_13.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_14.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_15.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_16.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_17.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_18.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_19.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_2.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_20.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_3.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_4.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_5.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_6.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_7.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_8.tsv create mode 100644 test/data/tsv/variant_callset/sample_rna_variant_callset_9.tsv delete mode 100644 test/data/tsv/variant_callset/sample_variant_callset_3.tsv create mode 100644 test/test_build_genome_var_graph.py create mode 100644 test/test_build_transcriptome_var_graph.py create mode 100644 test/test_call_germline_dna_variants.py create mode 100644 test/test_call_peptide_variants.py rename test/{test_call_dna_variants.py => test_call_somatic_dna_variants.py} (58%) delete mode 100644 test/test_diff_kmers.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..788e9ed --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,116 @@ +name: "CI" + +on: + push: + branches: [main, dev] + pull_request: + branches: [main] + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Run linter + run: | + pip install pylint==3.3.5 + chmod +x lint.sh + bash lint.sh + + test-rust: + runs-on: ubuntu-latest + timeout-minutes: 120 + needs: lint + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Free disk space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + - name: Set up Miniconda + uses: conda-incubator/setup-miniconda@v3 + with: + miniconda-version: "latest" + activate-environment: test-env + python-version: "3.10" + channels: conda-forge,bioconda,defaults + channel-priority: strict + + - name: Install Rust toolchain + shell: bash -l {0} + run: | + conda install -y rust=1.88.0 + + - name: Run Rust unit tests + shell: bash -l {0} + run: | + cd exacto/ + cargo test -- --nocapture + + test-python: + runs-on: ubuntu-latest + timeout-minutes: 120 + needs: lint + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Free disk space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + - name: Set up Miniconda + uses: conda-incubator/setup-miniconda@v3 + with: + miniconda-version: "latest" + activate-environment: test-env + python-version: "3.10" + channels: conda-forge,bioconda,defaults + channel-priority: strict + + - name: Install dependencies + shell: bash -l {0} + run: | + conda install -y \ + rust=1.88.0 \ + pandas=2.2.3 \ + polars=1.26.0 \ + pyarrow=19.0.1 \ + coveralls=4.0.1 \ + pytest=8.3.5 \ + pytest-cov=6.1.1 + pip install pysam==0.23.0 + pip install . --verbose + + - name: Run Python unit tests + shell: bash -l {0} + run: | + chmod +x unittest.sh + bash unittest.sh \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index c8ceaf0..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: build -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Set up Miniconda - uses: conda-incubator/setup-miniconda@v2 - with: - miniconda-version: "latest" - activate-environment: test-env - python-version: "3.10" - - - name: Install dependencies - shell: bash -l {0} - run: | - # Remove tmp files - sudo rm -rf /tmp/* - - # Install conda packages - pip install pysam==0.23.0 - conda install -c conda-forge rust==1.88.0 - conda install -c anaconda pandas==2.2.3 - conda install -c conda-forge polars==1.26.0 - conda install -c conda-forge pyarrow==19.0.1 - conda install -c anaconda pylint==3.3.5 - conda install -c conda-forge coveralls==4.0.1 - conda install -c conda-forge pytest==8.3.5 - conda install -c conda-forge pytest-cov==6.1.1 - - # Install exacto - pip install . --verbose - - - name: Run rust unit tests - shell: bash -l {0} - run: | - cd exacto/ - cargo test -- --nocapture - - - name: Run linter - shell: bash -l {0} - run: | - chmod +x lint.sh - bash lint.sh - - - name: Run python unit tests - shell: bash -l {0} - run: | - chmod +x unittest.sh - bash unittest.sh diff --git a/.gitignore b/.gitignore index f92f65d..9c29af0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,12 @@ Exacto.egg-info/ docs/.ipynb_checkpoints/ target/ docs/_site -docs/.quarto +docs/.quarto/ +docs/cli/* +docs/pipelines/* +!docs/cli/index.qmd +!docs/pipelines/index.qmd Cargo.lock .nojekyll *.nextflow.log* **/.nextflow/ - diff --git a/Cargo.toml b/Cargo.toml index 2c17c60..42b1865 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,46 +1,47 @@ [package] name = "exactolib" -version = "0.4.5" +version = "0.4.6" edition = "2021" [package.metadata.maturin] name = "exactolib.exactolibrs" [lib] -name = "exactolib" +name = "exactolibrs" crate-type = ["cdylib"] [dependencies] bstr = "1.9.0" chrono = "0.4" env_logger = "0.9" -exacto = { path = "exacto/exacto", version = "0.4.5" } +exacto = { path = "exacto/exacto", version = "0.4.6" } exitcode = "1.1.2" flate2 = "1.0.35" log = "0.4" -noodles-bgzf = "0.34.0" -noodles-fasta = "0.46.0" +noodles-bgzf = "0.43.0" +noodles-fasta = "0.56.0" noodles-fastq = "0.16.0" polars = { version = "0.45.0", features = ["csv", "ipc", "lazy", "parquet"] } pyo3 = { version = "0.22.0", features = ["extension-module"] } pyo3-polars = "0.19.0" -serde = { version = "1.0", features = ["derive"] } +rayon = "1.10.0" +serde = { version = "1.0.210", features = ["derive", "rc"] } sysinfo = "0.32.0" tempfile = "3.6" [profile.dev] opt-level = 0 -debug = true +debug = 1 overflow-checks = true panic = 'unwind' incremental = true codegen-units = 256 [profile.release] -opt-level = 0 -debug = false -lto = true +opt-level = 3 +debug = 0 +lto = "thin" overflow-checks = false panic = 'unwind' -incremental = true -codegen-units = 1 +incremental = false +codegen-units = 16 diff --git a/README.md b/README.md index d8dc801..174cf44 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,21 @@ # Exacto -Exacto (**EX**acto **A**ccurate **C**haracterization of **T**ranscriptomes and gen**O**mes) performs the following -primary tasks using long-read sequencing data for mutant proteoform prediction: -* Identification of somatic and germline DNA variants. -* Identification of RNA variants. -* Integration of DNA and RNA variants. -* Translation of full-length transcripts with any underlying DNA and RNA variant annotation at the amino-acid level. +**EX**acto **A**ccurate **C**haracterization of **T**ranscriptomes and gen**O**mes + +A long-read toolkit for mutant proteoform prediction. Exacto identifies somatic +and germline DNA variants, RNA variants, integrates them, and translates +full-length transcripts with variant annotation at the amino-acid level. [![build](https://github.com/pirl-unc/exacto/actions/workflows/main.yml/badge.svg)](https://github.com/pirl-unc/exacto/actions/workflows/main.yml) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -## 01. Docker Container -Docker images of Exacto can be found here:
-https://hub.docker.com/r/ajslee/exacto +**Documentation**: [https://pirl-unc.github.io/exacto/](https://pirl-unc.github.io/exacto/) -## 02. Dependencies -- python3 (3.10 tested) -- numpy (>=1.22.3) -- pandas (>=2.0.3) -- polars (>=1.12.0) -- pyarrow (>=18.0.0) -- pysam (>=0.22.0) -- pytz (>=2024.1) -- rust +## 01. Installation -## 03. Installation -``` +Download the latest stable release [here](https://github.com/pirl-unc/exacto/releases). + +```bash conda create -n exacto python=3.10 conda activate exacto pip install pysam==0.23.0 @@ -36,358 +26,53 @@ conda install -c conda-forge pyarrow==19.0.1 pip install exacto-.tar.gz --verbose ``` -## 04. Mutant Proteoform Prediction Pipeline - -Align tumor and normal DNA reads to a reference genome: - -``` -minimap2 \ - -ax map-hifi --cs --eqx -Y -L --secondary=no \ - reference_genome.fasta tumor_dna.fastq.gz \ -| samtools view -b - \ -| samtools sort -o {tumor,normal}_dna.sorted.bam -``` - -Identify tumor-specific (somatic) DNA variants: -``` -exacto call-dna-vars \ - --bam-file tumor_dna.sorted.bam \ - --bam-bai-file tumor_dna.sorted.bam.bai \ - --mode case-specific \ - --control-bam-files normal_dna.sorted.bam \ - --control-bam-bai-files normal_dna.sorted.bam.bai \ - --output-tsv-file tumor_specific_dna_variants.tsv -``` - -Annotate the tumor-specific (somatic) DNA variants: - -``` -exacto annotate-vars \ - --tsv-file tumor_specific_dna_variants.tsv \ - --reference-gene-annotation-file gencode.gtf.gz \ - --reference-gene-annotation-source gencode \ - --reference-gene-annotation-assembly assembly_name \ - --reference-gene-annotation-version assembly_version \ - --output-tsv-file tumor_specific_dna_variants.annotated.tsv -``` - -Assemble tumor transcriptome using [RNAbloom2](https://github.com/bcgsc/RNA-Bloom): - -``` -java -jar RNA-Bloom.jar \ - -long tumor_rna.fastq.gz \ - --outdir \ - --qual 20 --qual-avg 20 --mincov 3 -ntcard -savebf -``` - -Align the assembled tumor transcriptome to a reference genome using [Minimap2](https://github.com/lh3/minimap2): - -``` -minimap2 \ - -ax splice:hq -uf --cs --eqx -Y -L --secondary=no \ - reference_genome.fasta tumor_rna.fasta \ -| samtools view -b - \ -| samtools sort -o tumor_transcriptome_assembly.sorted.bam -``` - -Remove unspliced models from the transcriptome assembly: - -``` -exacto remove-unspliced-rnas \ - --bam-file tumor_transcriptome_assembly.sorted.bam \ - --bam-bai-file tumor_transcriptome_assembly.sorted.bam.bai \ - --fasta-file reference_genome.fasta \ - --reference-gene-annotation-file gencode.gtf.gz \ - --reference-gene-annotation-source gencode \ - --reference-gene-annotation-assembly assembly_name \ - --reference-gene-annotation-version assembly_version \ - --output-bam-file tumor_transcriptome_assembly.sorted.filtered.bam \ - --output-bam-bai-file tumor_transcriptome_assembly.sorted.filtered.bam.bai \ - --output-fasta-file tumor_transcriptome_assembly.sorted.filtered.fasta -``` - -Identify tumor RNA variants: - -``` -exacto call-rna-vars \ - --bam-file tumor_transcriptome_assembly.sorted.filtered.bam \ - --bam-bai-file tumor_transcriptome_assembly.sorted.filtered.bam.bai \ - --reference-genome-fasta-file reference_genome.fasta \ - --reference-gene-annotation-file gencode.gtf.gz \ - --reference-gene-annotation-source gencode \ - --reference-gene-annotation-assembly assembly_name \ - --reference-gene-annotation-version assembly_version \ - --output-dir rna_variants_outputs/ \ - --output-prefix tumor -``` - -Integrate DNA and RNA variants: - -``` -exacto integrate-vars \ - --annotated-dna-vars-tsv-file tumor_specific_dna_variants.annotated.tsv \ - --rna-vars-tsv-file rna_variants_outputs/tumor_exacto_rna_variant_calls.tsv \ - --reference-gene-annotation-file gencode.gtf.gz \ - --reference-gene-annotation-source gencode \ - --reference-gene-annotation-assembly assembly_name \ - --reference-gene-annotation-version assembly_version \ - --output-tsv-file tumor_dna_rna_variants_integrated.tsv -``` +A Docker image is also available on +[Docker Hub](https://hub.docker.com/r/ajslee/exacto). -Identify primary structures: - -``` -exacto translate-structs \ - --transcript-structures-tsv-file rna_variants_outputs/tumor_exacto_transcript_structures.tsv \ - --rna-variant-calls-tsv-file rna_variants_outputs/tumor_exacto_rna_variant_calls.tsv \ - --integrated-variants-tsv-file tumor_dna_rna_variants_integrated.tsv \ - --strategy longest_orf \ - --output-tsv-file tumor_primary_structures.tsv \ - --output-fasta-file tumor_primary_structures.fasta -``` - -## 05. Available Commands - -| Command | Description | -|-------------------------------------------------|-------------------------------------------------| -| [annotate-vars](#annotate-vars) | Annotate variants | -| [call-dna-vars](#call-dna-vars) | Perform somatic or germline DNA variant calling | -| [call-rna-vars](#call-rna-vars) | Perform RNA variant calling | -| [integrate-vars](#integrate-vars) | Integrate DNA and RNA variants | -| [remove-unspliced-rnas](#remove-unspliced-rnas) | Removed unspliced RNAs | -| [translate-seqs](#translate-seqs) | Translate transcript sequences | -| [translate-structs](#translate-structs) | Translate transcript structures | - -Example scripts for running Exacto can be found [here](https://github.com/pirl-unc/exacto/tree/main/examples). - -### annotate-vars - -Annotate DNA or RNA variants. - -``` -exacto annotate-vars \ - --tsv-file TSV_FILE \ - --reference-gene-annotation-file REFERENCE_GENE_ANNOTATION_FILE \ - --reference-gene-annotation-source REFERENCE_GENE_ANNOTATION_SOURCE \ - --reference-gene-annotation-assembly REFERENCE_GENE_ANNOTATION_ASSEMBLY \ - --reference-gene-annotation-version REFERENCE_GENE_ANNOTATION_VERSION \ - --output-tsv-file OUTPUT_TSV_FILE \ - [--num-threads NUM_THREADS] \ - [--gene-types GENE_TYPES [GENE_TYPES ...]] \ - [--gene-levels GENE_LEVELS [GENE_LEVELS ...]] \ - [--transcript-types TRANSCRIPT_TYPES [TRANSCRIPT_TYPES ...]] \ - [--transcript-levels TRANSCRIPT_LEVELS [TRANSCRIPT_LEVELS ...]] -``` - -### call-dna-vars - -#### Case-specific DNA Variant Calling - -Identify case-specific (e.g. somatic) DNA variants in a case (e.g. tumor) long-read DNA BAM file against a set of control (e.g. matched normal) long-read DNA BAM files: - -``` -exacto call-dna-vars [-h] - --bam-file BAM_FILE - --bam-bai-file BAM_BAI_FILE - --mode case-specific - --output-tsv-file OUTPUT_TSV_FILE - --control-bam-files CONTROL_BAM_FILES [CONTROL_BAM_FILES ...] - --control-bam-bai-files CONTROL_BAM_BAI_FILES [CONTROL_BAM_BAI_FILES ...] - [--num-threads NUM_THREADS] - [--gzip GZIP] - [--chromosomes CHROMOSOMES [CHROMOSOMES ...]] - [--min-reads MIN_READS] - [--min-mapping-quality MIN_MAPPING_QUALITY] - [--min-average-base-quality MIN_AVERAGE_BASE_QUALITY] - [--min-size-proportion MIN_SIZE_PROPORTION] - [--max-ins-norm-edit-distance MAX_INS_NORM_EDIT_DISTANCE] - [--max-intrachromosomal-distance MAX_INTRACHROMOSOMAL_DISTANCE] - [--max-intrachromosomal-distance-tau MAX_INTRACHROMOSOMAL_DISTANCE_TAU] - [--max-interchromosomal-distance MAX_INTERCHROMOSOMAL_DISTANCE] - [--apply-infinite-sites-assumption APPLY_INFINITE_SITES_ASSUMPTION] - [--temp-dir TEMP_DIR] -``` - -#### DNA Variant Calling - -Identify all (e.g. germline) DNA variants in a long-read DNA BAM file: - -``` -exacto call-dna-vars [-h] - --bam-file BAM_FILE - --bam-bai-file BAM_BAI_FILE - --mode all - --output-tsv-file OUTPUT_TSV_FILE - [--num-threads NUM_THREADS] - [--gzip GZIP] - [--chromosomes CHROMOSOMES [CHROMOSOMES ...]] - [--min-reads MIN_READS] - [--min-mapping-quality MIN_MAPPING_QUALITY] - [--min-average-base-quality MIN_AVERAGE_BASE_QUALITY] - [--min-size-proportion MIN_SIZE_PROPORTION] - [--max-ins-norm-edit-distance MAX_INS_NORM_EDIT_DISTANCE] - [--max-intrachromosomal-distance MAX_INTRACHROMOSOMAL_DISTANCE] - [--max-intrachromosomal-distance-tau MAX_INTRACHROMOSOMAL_DISTANCE_TAU] - [--max-interchromosomal-distance MAX_INTERCHROMOSOMAL_DISTANCE] - [--apply-infinite-sites-assumption APPLY_INFINITE_SITES_ASSUMPTION] - [--temp-dir TEMP_DIR] -``` - -### call-rna-vars - -Identify RNA variants in a long-read assembled transcripts BAM file: - -``` -exacto call-rna-vars [-h] - --bam-file BAM_FILE - --bam-bai-file BAM_BAI_FILE - --reference-genome-fasta-file REFERENCE_GENOME_FASTA_FILE - --reference-gene-annotation-file REFERENCE_GENE_ANNOTATION_FILE - --reference-gene-annotation-source REFERENCE_GENE_ANNOTATION_SOURCE - --reference-gene-annotation-assembly REFERENCE_GENE_ANNOTATION_ASSEMBLY - --reference-gene-annotation-version REFERENCE_GENE_ANNOTATION_VERSION - --output-dir OUTPUT_DIR - --output-prefix OUTPUT_PREFIX - [--num-threads NUM_THREADS] - [--min-mapping-quality MIN_MAPPING_QUALITY] - [--reference-transcript-scoring-method REFERENCE_TRANSCRIPT_SCORING_METHOD] - [--reference-transcript-selection-strategy REFERENCE_TRANSCRIPT_SELECTION_STRATEGY] - [--reference-transcript-top-k REFERENCE_TRANSCRIPT_TOP_K] - [--reference-transcript-threshold REFERENCE_TRANSCRIPT_THRESHOLD] - [--min-average-base-quality MIN_AVERAGE_BASE_QUALITY] - [--temp-dir TEMP_DIR] - [--gene-types GENE_TYPES [GENE_TYPES ...]] - [--gene-levels GENE_LEVELS [GENE_LEVELS ...]] - [--transcript-types TRANSCRIPT_TYPES [TRANSCRIPT_TYPES ...]] - [--transcript-levels TRANSCRIPT_LEVELS [TRANSCRIPT_LEVELS ...]] -``` - -### integrate-vars - -Integrate DNA and RNA variants based on genomic coordinates. - -``` -exacto integrate-vars [-h] - --annotated-dna-vars-tsv-file ANNOTATED_DNA_VARS_TSV_FILE - --rna-vars-tsv-file RNA_VARS_TSV_FILE - --reference-gene-annotation-file REFERENCE_GENE_ANNOTATION_FILE - --reference-gene-annotation-source REFERENCE_GENE_ANNOTATION_SOURCE - --reference-gene-annotation-assembly REFERENCE_GENE_ANNOTATION_ASSEMBLY - --reference-gene-annotation-version REFERENCE_GENE_ANNOTATION_VERSION - --output-tsv-file OUTPUT_TSV_FILE - [--num-threads NUM_THREADS] - [--max-exon-offset MAX_EXON_OFFSET] - [--max-transcript-boundary-offset MAX_TRANSCRIPT_BOUNDARY_OFFSET] - [--max-intergenic-distance MAX_INTERGENIC_DISTANCE] -``` - -### remove-unspliced-rnas - -Remove unspliced (nascent) RNAs in a transcriptome assembly. -Note that assembled transcripts whose alignments overlap with any single-exon reference transcript will be retained. - -``` -exacto remove-unspliced-rnas [-h] - --bam-file BAM_FILE - --bam-bai-file BAM_BAI_FILE - --fasta-file FASTA_FILE - --reference-gene-annotation-file REFERENCE_GENE_ANNOTATION_FILE - --reference-gene-annotation-source REFERENCE_GENE_ANNOTATION_SOURCE - --reference-gene-annotation-assembly REFERENCE_GENE_ANNOTATION_ASSEMBLY - --reference-gene-annotation-version REFERENCE_GENE_ANNOTATION_VERSION - --output-bam-file OUTPUT_BAM_FILE - --output-bam-bai-file OUTPUT_BAM_BAI_FILE - --output-fasta-file OUTPUT_FASTA_FILE - [--num-threads NUM_THREADS] - [--min-mapping-quality MIN_MAPPING_QUALITY] - [--gene-types GENE_TYPES [GENE_TYPES ...]] - [--gene-levels GENE_LEVELS [GENE_LEVELS ...]] - [--transcript-types TRANSCRIPT_TYPES [TRANSCRIPT_TYPES ...]] - [--transcript-levels TRANSCRIPT_LEVELS [TRANSCRIPT_LEVELS ...]] -``` - -### translate-seqs - -Translate transcript sequences to peptide sequences. - -``` -exacto translate-seqs [-h] - (--fastq-file FASTQ_FILE | --fasta-file FASTA_FILE | --sequence SEQUENCE) - --strategy {longest_orf,all_orfs} - [--output-tsv-file OUTPUT_TSV_FILE] - [--output-fasta-file OUTPUT_FASTA_FILE] - [--num-threads NUM_THREADS] - [--temp-dir TEMP_DIR] - [--gzip GZIP] -``` - -### translate-structs - -Translate transcript structures to primary structures. - -``` -exacto translate-structs [-h] - --transcript-structures-tsv-file TRANSCRIPT_STRUCTURES_TSV_FILE - --rna-variant-calls-tsv-file RNA_VARIANT_CALLS_TSV_FILE - --integrated-variants-tsv-file INTEGRATED_VARIANTS_TSV_FILE - --strategy {longest_orf,all_orfs} - --output-tsv-file OUTPUT_TSV_FILE - --output-fasta-file OUTPUT_FASTA_FILE - [--num-threads NUM_THREADS] -``` - -## 06. Input Preparation - -### DNA Variant Calling +## 02. Dependencies -Exacto performs DNA variant identification using the [cs tag](https://github.com/lh3/minimap2#cs) produced by [minimap2](https://github.com/lh3/minimap2) alignments. +* Python (>=3.10) +* Rust (1.88.0 tested) +* numpy (>=1.22.3) +* pandas (>=2.0.3) +* polars (>=1.12.0) +* pyarrow (>=18.0.0) +* pysam (>=0.22.0) +* pytz (>=2024.1) -For automated long-read DNA alignment, you can use the Nexus workflow manager:
-https://github.com/pirl-unc/nexus/tree/main/src/nexuslib/pipelines/alignment/long_read_alignment_minimap2 +## 03. Usage -If you prefer to run `minimap2` outside of Nexus, use the following parameters: -``` --ax map-hifi --cs --eqx -Y -L --secondary=no +### View all available subcommands +```bash +exacto --help ``` -### RNA Variant Calling - -Exacto identifies RNA variants from assembled transcript models rather than directly from raw reads. -You can generate transcript models from long RNA-seq reads using [RNAbloom2](https://github.com/bcgsc/RNA-Bloom). A corresponding `Nexus` workflow for RNAbloom2 is available here:
-https://github.com/pirl-unc/nexus/tree/main/src/nexuslib/pipelines/assembly/transcriptome_assembly_rnabloom2 - -After assembling the transcriptome, align the assembled transcripts back to the reference genome using `minimap2` with the following parameters: -``` --ax splice:hq -uf --cs --eqx -Y -L --secondary=no +### View a subcommand's parameters +```bash +exacto --help ``` -Then, remove any assembled transcripts that are likely unspliced RNAs by running `exacto remove-unspliced-rnas`. - -## 07. DNA / RNA Variant Types Identified by Exacto - -### DNA (Somatic and Germline) +### Available subcommands -Sequence variant types: -- Breakpoint (duplication, inversion) -- Deletion -- Insertion -- Multi-nucleotide variant -- Single-nucleotide variant -- Translocation +| Subcommand | Description | +|---------------------------------|--------------------------------------------------------------| +| `annotate-vars` | Annotate DNA or RNA variants with gene-level context. | +| `build-genome-var-graph` | Build a personalized genome variation graph. | +| `build-transcriptome-var-graph` | Build a personalized transcriptome variation graph. | +| `call-germline-dna-vars` | Call germline DNA variants from long-read alignments. | +| `call-somatic-dna-vars` | Call somatic DNA variants against matched control samples. | +| `call-rna-vars` | Call RNA variants from assembled transcript alignments. | +| `call-peptide-vars` | Call peptide-level variants from translated proteoforms. | +| `integrate-vars` | Integrate DNA and RNA variants into a unified callset. | +| `remove-unspliced-rnas` | Filter out unspliced (nascent) RNAs from a transcriptome assembly. | +| `translate-seqs` | Translate transcript sequences into peptide sequences. | +| `translate-structs` | Translate transcript structures into mutant proteoforms. | -### RNA +See the [Commands reference](https://pirl-unc.github.io/exacto/cli/) for full +parameter documentation, and the [Pipelines guide](https://pirl-unc.github.io/exacto/pipelines/) +for end-to-end mutant-proteoform-prediction and variation-graph-construction +walkthroughs. -Sequence variant types: -- Breakpoint -- Deletion -- Insertion -- Multi-nucleotide variant -- Single-nucleotide variant +## 04. License -Splice variant types: -- Circular RNA -- Cryptic exon -- Exon truncation -- Fusion gene -- Intron retention -- UTR extension +Licensed under the Apache License, Version 2.0. diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..ad29309 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +/.quarto/ +**/*.quarto_ipynb diff --git a/docs/_quarto.yml b/docs/_quarto.yml new file mode 100644 index 0000000..6fc0041 --- /dev/null +++ b/docs/_quarto.yml @@ -0,0 +1,43 @@ +project: + type: website + output-dir: _site + +website: + title: "Exacto" + navbar: + left: + - href: index.qmd + text: Home + - href: pipelines/index.qmd + text: Pipelines + - href: cli/index.qmd + text: Commands + - href: faq.qmd + text: FAQ + right: + - icon: github + href: https://github.com/pirl-unc/exacto + sidebar: + style: docked + contents: + - text: Home + href: index.qmd + - section: Pipelines + contents: + - pipelines/index.qmd + - pipelines/mutant-proteoform-prediction.qmd + - pipelines/variation-graph-construction.qmd + - section: Commands + contents: + - cli/index.qmd + - auto: cli/*.qmd + - text: FAQ + href: faq.qmd + +format: + html: + theme: [cosmo, custom.scss] + toc: true + toc-depth: 3 + code-copy: true + code-overflow: wrap diff --git a/docs/cli/index.qmd b/docs/cli/index.qmd new file mode 100644 index 0000000..752c08e --- /dev/null +++ b/docs/cli/index.qmd @@ -0,0 +1,23 @@ +--- +title: "Commands" +--- + +Alphabetical reference for every Exacto command line interface. For end-to-end +walkthroughs that compose these into pipelines, see [Pipelines](../pipelines/index.qmd). + +## Available Commands + +| Command | Description | +|:--------|:------------| +| [`annotate-vars`](annotate-vars.qmd) | Annotate variants. | +| [`build-genome-var-graph`](build-genome-var-graph.qmd) | Build a genome variation graph. | +| [`build-transcriptome-var-graph`](build-transcriptome-var-graph.qmd) | Build a transcriptome variation graph. | +| [`call-germline-dna-vars`](call-somatic-dna-vars.qmd) | Call germline DNA variants in a long-read DNA BAM file. | +| [`call-somatic-dna-vars`](call-somatic-dna-vars.qmd) | Call somatic DNA variants in tumor and matched normal long-read DNA BAM files. | +| [`call-rna-vars`](call-rna-vars.qmd) | Call RNA variants in a long-read RNA assembly BAM file. | +| [`integrate-vars`](integrate-vars.qmd) | Integrate DNA and RNA variants. | +| [`remove-unspliced-rnas`](remove-unspliced-rnas.qmd) | Remove unspliced RNAs. | +| [`translate-seqs`](translate-seqs.qmd) | Translate transcript sequences in a FASTA or FASTQ file to peptide sequences. | +| [`translate-structs`](translate-structs.qmd) | Translate transcript structures to primary structures. | + +: {.striped .hover} diff --git a/docs/custom.scss b/docs/custom.scss new file mode 100644 index 0000000..27b42c9 --- /dev/null +++ b/docs/custom.scss @@ -0,0 +1,28 @@ +/*-- scss:defaults --*/ +$sidebar-width: 180px; + +/*-- scss:rules --*/ +/* Force the docked-sidebar grid track to be exactly the sidebar's width */ +@media (min-width: 992px) { + body.nav-sidebar.docked #quarto-content.page-columns { + grid-template-columns: + [screen-start] 1.5em + [screen-start-inset] minmax(0, 1fr) + [page-start page-start-inset] 180px + [body-start-outset] 35px + [body-start] 1.5em + [body-content-start] minmax(500px, calc(1080px - 3em)) + [body-content-end] 1.5em + [body-end] 35px + [body-end-outset] minmax(75px, 145px) + [page-end-inset] 35px + [page-end] 5fr + [screen-end-inset] 1.5em + [screen-end]; + } + + #quarto-sidebar.sidebar-navigation.docked { + /* Let the sidebar fill its grid cell (which is now sized) */ + width: auto; + } +} diff --git a/docs/faq.qmd b/docs/faq.qmd new file mode 100644 index 0000000..3114b41 --- /dev/null +++ b/docs/faq.qmd @@ -0,0 +1,49 @@ +--- +title: "FAQ" +--- + +Frequently asked questions. If your question isn't answered here, please +[open an issue](https://github.com/pirl-unc/exacto/issues). + + + +
+ + +
+ +
+ +**Inputs** + +::: {.callout-note collapse="true" icon="false"} +## For `integrate-vars` can I supply a DNA variant callset from another method? + +Yes, but the variant callset format must be in the Occam's Grammar format (TSV). You can do this using +[VSTOL](https://github.com/pirl-unc/vstol). +::: + +::: {.callout-note collapse="true" icon="false"} +## How do I avoid out-of-memory errors on large BAMs? + +Exacto's DNA variant caller (`call-dna-vars`) holds per-read evidence in memory while +clustering variant candidates. If your Exacto job runs out of memory, you can: + +- Reduce `--num-threads` to lower concurrent peak memory. +- Reduce `--chunk-size` (default: 100,000) for `call-dna-vars`. +::: + +
diff --git a/docs/index.qmd b/docs/index.qmd new file mode 100644 index 0000000..5349300 --- /dev/null +++ b/docs/index.qmd @@ -0,0 +1,101 @@ +--- +title: "Exacto" +subtitle: "EXacto Accurate Characterization of Transcriptomes and genOmes" +--- + +A long-read toolkit for mutant proteoform prediction. Exacto identifies +somatic and germline DNA variants, RNA variants, integrates them, and +translates full-length transcripts with variant annotation at the +amino-acid level. + +## Quick Start + +### 1. Install + +```bash +conda create -n exacto python=3.10 +conda activate exacto +pip install pysam==0.23.0 +conda install -c conda-forge rust==1.88.0 +conda install -c anaconda pandas==2.2.3 +conda install -c conda-forge polars==1.26.0 +conda install -c conda-forge pyarrow==19.0.1 +pip install exacto-.tar.gz --verbose +``` + +Download the latest release from +[GitHub Releases](https://github.com/pirl-unc/exacto/releases). A Docker +image is also available on +[Docker Hub](https://hub.docker.com/r/ajslee/exacto). + +### 2. See What's Available + +```bash +exacto --help +``` + +Or browse the [Commands](cli/index.qmd) reference for every subcommand +and its options. + +### 3. Pipelines + +Exacto supports pipelines for mutant proteoform prediction and individualized graph genome / transcriptome construction. +See [Pipelines](pipelines/index.qmd) for more. + +## How Exacto is Organized + +::: {.grid} + +::: {.g-col-4} +### DNA / RNA variant calling / annotation +`call-dna-vars` and `call-rna-vars` identify variants from long-read +alignments; `annotate-vars` adds gene-level context. +::: + +::: {.g-col-4} +### DNA + RNA variant integration +`integrate-vars` merges DNA and RNA variant evidence into a single +unified callset. +::: + +::: {.g-col-4} +### Peptide translation +`translate-seqs` and `translate-structs` translate transcripts and +transcript structures to mutant proteoform sequences. +::: + +::: {.g-col-4} +### Mutant peptide calling +`call-peptide-vars` identifies peptide-level variants in mutant +proteoforms produced by translation. +::: + +::: {.g-col-4} +### Individualized genome / transcriptome graph construction +`build-genome-var-graph` and `build-transcriptome-var-graph` produce +personalized graphs that encode the sample's variants +alongside the linear reference. +::: + +::: {.g-col-4} +### Quality control +`remove-unspliced-rnas` filters out background unspliced contigs before +RNA variant calling. +::: + +::: + +## Dependencies + +* python (>=3.10) +* rust (1.88.0 tested) +* numpy (>=1.22.3) +* pandas (>=2.0.3) +* polars (>=1.12.0) +* pyarrow (>=18.0.0) +* pysam (>=0.22.0) +* pytz (>=2024.1) + +## License + +Licensed under the [Apache License, Version 2.0](https://opensource.org/licenses/Apache-2.0). diff --git a/docs/pipelines/index.qmd b/docs/pipelines/index.qmd new file mode 100644 index 0000000..3d7a94d --- /dev/null +++ b/docs/pipelines/index.qmd @@ -0,0 +1,17 @@ +--- +title: "Pipelines" +--- + +Exacto exposes its commands as two end-to-end pipelines for long-read +mutant-proteoform analysis. Each page below walks you through inputs, +intermediate artifacts, and final outputs, with cross-links to the +relevant commands. + +## Available Pipelines + +[**Mutant Proteoform Prediction**](mutant-proteoform-prediction.qmd)
+Identify somatic and germline variants from long-read DNA and RNA, integrate them, and translate full-length transcripts to mutant proteoforms. + + +[**Variant Graph Construction**](variant-graph-construction.qmd)
+Build individualized genome and transcriptome variation graphs from variant calls. diff --git a/exacto/Cargo.toml b/exacto/Cargo.toml index de78325..8f27faf 100644 --- a/exacto/Cargo.toml +++ b/exacto/Cargo.toml @@ -12,8 +12,68 @@ members = [ resolver = "2" [workspace.package] -license = "MIT" +version = "0.4.6" +license = "Apache-2.0" edition = "2021" rust-version = "1.79.0" [workspace.dependencies] +exacto-annotator = { path = "exacto-annotator", version = "0.4.6" } +exacto-caller = { path = "exacto-caller", version = "0.4.6" } +exacto-core = { path = "exacto-core", version = "0.4.6" } +exacto-graph = { path = "exacto-graph", version = "0.4.6" } +exacto-integrator = { path = "exacto-integrator", version = "0.4.6" } +exacto-qc = { path = "exacto-qc", version = "0.4.6" } +exacto-translator = { path = "exacto-translator", version = "0.4.6" } + +# Third-party crates +bimap = { version = "0.6.3", features = ["serde"] } +bincode = "1.3" +bio = "2.0.3" +bitvec = "1" +bstr = "1.12.1" +chrono = "0.4.38" +csv = "1.3.0" +edit-distance = "2.1.3" +env_logger = "0.10" +flate2 = "1.0.35" +indicatif = "0.17" +interavl = "0.2.0" +itertools = "0.14.0" +log = "0.4.22" +nalgebra = "0.33.2" +noodles-bam = "0.83.0" +noodles-bgzf = "0.43.0" +noodles-core = "0.18.0" +noodles-cram = "=0.86.0" +noodles-csi = "=0.51.0" +noodles-fasta = "0.56.0" +noodles-fastq = "0.16.0" +noodles-sam = "0.79.0" +noodles-util = { version = "0.70.0", default-features = false, features = ["alignment"] } +once_cell = "1.20.2" +phf = { version = "0.11", features = ["macros"] } +polars = { version = "0.45.0", features = ["csv","decompress","json","lazy","parquet","serde","strings"] } +rayon = "1.10.0" +serde = { version = "1.0.210", features = ["derive", "rc"] } +statrs = "0.18.0" +sysinfo = "0.32.0" +tempfile = "3.22.0" +regex = "1.10.6" + +[profile.dev] +opt-level = 0 +debug = 1 +overflow-checks = true +panic = 'unwind' +incremental = true +codegen-units = 256 + +[profile.release] +opt-level = 3 +debug = 0 +lto = "thin" +overflow-checks = false +panic = 'unwind' +incremental = false +codegen-units = 16 diff --git a/exacto/exacto-annotator/Cargo.toml b/exacto/exacto-annotator/Cargo.toml index c1229e5..f801d63 100644 --- a/exacto/exacto-annotator/Cargo.toml +++ b/exacto/exacto-annotator/Cargo.toml @@ -1,27 +1,21 @@ [package] name = "exacto-annotator" -version = "0.4.5" +version.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true [dependencies] -exacto-caller = { path = "../exacto-caller", version = "0.4.5" } -exacto-core = { path = "../exacto-core", version = "0.4.5" } -bio = "2.0.3" -flate2 = "1.0.35" -indicatif = "0.17" -noodles-bgzf = "0.43.0" -noodles-fasta = "0.56.0" -once_cell = "1.20.2" -phf = { version = "0.11", features = ["macros"] } -polars = { version = "0.45.0", features = ["csv","decompress","json","lazy","parquet","serde"] } -rayon = "1.10.0" -serde = { version = "1.0.210", features = ["derive"] } -tempfile = "3.6" - -[profile.dev] -opt-level = 0 - -[profile.release] -incremental = true \ No newline at end of file +exacto-caller = { workspace = true } +exacto-core = { workspace = true } +bio = { workspace = true } +flate2 = { workspace = true } +indicatif = { workspace = true } +noodles-bgzf = { workspace = true } +noodles-fasta = { workspace = true } +once_cell = { workspace = true } +phf = { workspace = true } +polars = { workspace = true } +rayon = { workspace = true } +serde = { workspace = true } +tempfile = { workspace = true } diff --git a/exacto/exacto-annotator/src/algorithms/variant_annotation.rs b/exacto/exacto-annotator/src/algorithms/variant_annotation.rs index f7bed46..70bc6da 100644 --- a/exacto/exacto-annotator/src/algorithms/variant_annotation.rs +++ b/exacto/exacto-annotator/src/algorithms/variant_annotation.rs @@ -35,39 +35,35 @@ use crate::prelude::*; /// * `PositionAnnotation` object. pub fn annotate_position( chromosome_name: &str, - position: usize, + position: u32, gene_annotator: &(impl GeneAnnotator + Sync) ) -> PositionAnnotation { let gene_ids: Vec> = gene_annotator.get_gene_ids_at_locus(&*chromosome_name, position); let mut transcript_ids: HashMap,HashSet>> = HashMap::new(); let mut exon_ids: HashMap,Box> = HashMap::new(); - if !gene_ids.is_empty() { - let pos_isize: isize = position as isize; - for gene_id in gene_ids.iter() { - if let Some(gene) = gene_annotator.get_gene(&*gene_id) { - for transcript_id in gene.get_transcript_ids() { - if let Some(transcript) = gene_annotator.get_transcript(&*transcript_id) { - if overlaps(pos_isize, pos_isize, transcript.start as isize, transcript.end as isize) { - transcript_ids - .entry(gene_id.clone()) - .or_insert_with(HashSet::new) - .insert(transcript_id.clone()); - } - for exon_id in transcript.get_exon_ids() { - if let Some(exon) = gene_annotator.get_exon(&*transcript_id, &*exon_id) { - if overlaps(pos_isize, pos_isize, exon.start as isize, exon.end as isize) { - transcript_ids - .entry(gene_id.clone()) - .or_insert_with(HashSet::new) - .insert(transcript_id.clone()); - exon_ids.insert(transcript_id.clone(), exon_id.clone()); - } - } - } - } + let pos = position as isize; + for gene_id in &gene_ids { + let Some(gene) = gene_annotator.get_gene(&*gene_id) else { continue }; + for transcript_id in gene.get_transcript_ids() { + let Some(transcript) = gene_annotator.get_transcript(&*transcript_id) else { continue }; + + let mut record_transcript: bool = overlaps(pos, pos, transcript.start as isize, transcript.end as isize); + + for exon_id in transcript.get_exon_ids() { + let Some(exon) = gene_annotator.get_exon(&*transcript_id, &*exon_id) else { continue }; + if overlaps(pos, pos, exon.start as isize, exon.end as isize) { + record_transcript = true; + exon_ids.insert(transcript_id.clone(), exon_id.clone()); } } + + if record_transcript { + transcript_ids + .entry(gene_id.clone()) + .or_default() + .insert(transcript_id.clone()); + } } } @@ -125,7 +121,7 @@ pub fn annotate_variant_calls( let col_chromosome_2 = df_variant_calls.column("chromosome_2").unwrap().str().unwrap(); let col_position_2 = df_variant_calls.column("position_2").unwrap().i64().unwrap(); let col_variant_type = df_variant_calls.column("variant_type").unwrap().str().unwrap(); - let col_variant_sequence = df_variant_calls.column("variant_sequence").unwrap().str().unwrap(); + let col_variant_sequence = df_variant_calls.column("sequence").unwrap().str().unwrap(); let thread_pool = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) @@ -147,7 +143,7 @@ pub fn annotate_variant_calls( let variant_call_id: usize = col_variant_call_id.get(i).unwrap() as usize; // Position 1 - let position_1: usize = col_position_1.get(i).unwrap() as usize; + let position_1: u32 = col_position_1.get(i).unwrap() as u32; let chromosome_1: &str = col_chromosome_1.get(i).unwrap(); let position_annotation_1: PositionAnnotation = annotate_position( chromosome_1, @@ -156,7 +152,7 @@ pub fn annotate_variant_calls( ); // Position 2 - let position_2: usize = col_position_2.get(i).unwrap() as usize; + let position_2: u32 = col_position_2.get(i).unwrap() as u32; let chromosome_2: &str = col_chromosome_2.get(i).unwrap(); let position_annotation_2: PositionAnnotation = annotate_position( chromosome_2, diff --git a/exacto/exacto-annotator/src/structs/position_annotation.rs b/exacto/exacto-annotator/src/structs/position_annotation.rs index 141efd1..e425509 100644 --- a/exacto/exacto-annotator/src/structs/position_annotation.rs +++ b/exacto/exacto-annotator/src/structs/position_annotation.rs @@ -14,7 +14,7 @@ use exacto_core::prelude::GenicRegion; use serde::{Serialize, Deserialize}; use std::collections::{HashMap, HashSet}; -use std::hash::{Hash, Hasher}; +use std::hash::Hasher; #[derive(Debug,Eq,PartialEq,Serialize,Deserialize)] diff --git a/exacto/exacto-annotator/src/structs/variant_call_annotation.rs b/exacto/exacto-annotator/src/structs/variant_call_annotation.rs index cf93779..4c34bc5 100644 --- a/exacto/exacto-annotator/src/structs/variant_call_annotation.rs +++ b/exacto/exacto-annotator/src/structs/variant_call_annotation.rs @@ -13,7 +13,6 @@ use exacto_caller::prelude::*; use serde::{Serialize, Deserialize}; -use std::hash::Hash; use crate::prelude::*; @@ -22,9 +21,9 @@ use crate::prelude::*; pub struct VariantCallAnnotation { pub variant_call_id: usize, pub chromosome_1: Box, - pub position_1: usize, + pub position_1: u32, pub chromosome_2: Box, - pub position_2: usize, + pub position_2: u32, pub variant_type: VariantType, pub variant_sequence: Box, pub position_1_annotation: PositionAnnotation, @@ -53,9 +52,9 @@ impl VariantCallAnnotation { pub fn new( variant_call_id: usize, chromosome_1: &str, - position_1: usize, + position_1: u32, chromosome_2: &str, - position_2: usize, + position_2: u32, variant_type: VariantType, variant_sequence: &str, position_1_annotation: PositionAnnotation, diff --git a/exacto/exacto-annotator/src/structs/variant_call_annotation_set.rs b/exacto/exacto-annotator/src/structs/variant_call_annotation_set.rs index 6f514e0..123bde5 100644 --- a/exacto/exacto-annotator/src/structs/variant_call_annotation_set.rs +++ b/exacto/exacto-annotator/src/structs/variant_call_annotation_set.rs @@ -29,7 +29,7 @@ pub struct VariantCallAnnotationSet { /// Nested structure for position indexing: /// - Outer HashMap: Maps chromosome names to their position index /// - Inner BTreeMap: Maps positions to variant call IDs - position_index: HashMap, BTreeMap>>, + position_index: HashMap, BTreeMap>>, /// A map between reference transcript ID and its associated variant call IDs transcript_index: HashMap, HashSet> @@ -95,7 +95,7 @@ impl VariantCallAnnotationSet { self.annotations.get(&variant_call_id).unwrap() } - pub fn get_by_range(&self, chromosome: &str, start: usize, end: usize) -> Vec<&VariantCallAnnotation> { + pub fn get_by_range(&self, chromosome: &str, start: u32, end: u32) -> Vec<&VariantCallAnnotation> { let mut result_ids = HashSet::new(); if let Some(position_map) = self.position_index.get(chromosome) { @@ -139,7 +139,7 @@ impl VariantCallAnnotationSet { let col_chromosome_2 = df.column("chromosome_2").unwrap().str().unwrap(); let col_position_2 = df.column("position_2").unwrap().i64().unwrap(); let col_variant_type = df.column("variant_type").unwrap().str().unwrap(); - let col_variant_sequence = df.column("variant_sequence").unwrap().str().unwrap(); + let col_variant_sequence = df.column("sequence").unwrap().str().unwrap(); let col_position_1_genic_region = df.column("position_1_genic_region").unwrap().str().unwrap(); let col_position_1_annotation = df.column("position_1_annotation").unwrap().str().unwrap(); let col_position_2_genic_region = df.column("position_2_genic_region").unwrap().str().unwrap(); @@ -149,9 +149,9 @@ impl VariantCallAnnotationSet { for i in 0 ..df.height() { let variant_call_id: usize = col_variant_call_id.get(i).unwrap() as usize; let chromosome_1: Box = col_chromosome_1.get(i).unwrap().into(); - let position_1: usize = col_position_1.get(i).unwrap() as usize; + let position_1: u32 = col_position_1.get(i).unwrap() as u32; let chromosome_2: Box = col_chromosome_2.get(i).unwrap().into(); - let position_2: usize = col_position_2.get(i).unwrap() as usize; + let position_2: u32 = col_position_2.get(i).unwrap() as u32; let variant_type: VariantType = VariantType::from_str(col_variant_type.get(i).unwrap()).unwrap(); let variant_sequence: Box = col_variant_sequence.get(i).unwrap().into(); let position_1_annotation: PositionAnnotation = PositionAnnotation::from_string(col_position_1_annotation.get(i).unwrap()); @@ -178,9 +178,9 @@ impl VariantCallAnnotationSet { pub fn to_dataframe(&self) -> DataFrame { let mut variant_call_id_values: Vec = Vec::new(); let mut chromosome_1_values: Vec<&str> = Vec::new(); - let mut position_1_values: Vec = Vec::new(); + let mut position_1_values: Vec = Vec::new(); let mut chromosome_2_values: Vec<&str> = Vec::new(); - let mut position_2_values: Vec = Vec::new(); + let mut position_2_values: Vec = Vec::new(); let mut variant_type_values: Vec<&str> = Vec::new(); let mut variant_sequence_values: Vec<&str> = Vec::new(); let mut position_1_annotation_genic_region_values: Vec = Vec::new(); @@ -191,9 +191,9 @@ impl VariantCallAnnotationSet { for variant_call_annotation in self.annotations.values() { variant_call_id_values.push(variant_call_annotation.variant_call_id as u64); chromosome_1_values.push(&*variant_call_annotation.chromosome_1); - position_1_values.push(variant_call_annotation.position_1 as u64); + position_1_values.push(variant_call_annotation.position_1); chromosome_2_values.push(&*variant_call_annotation.chromosome_2); - position_2_values.push(variant_call_annotation.position_2 as u64); + position_2_values.push(variant_call_annotation.position_2); variant_type_values.push(variant_call_annotation.variant_type.as_str()); variant_sequence_values.push(&*variant_call_annotation.variant_sequence); position_1_annotation_genic_region_values.push(variant_call_annotation.position_1_annotation.genic_region.as_str().to_string()); @@ -209,7 +209,7 @@ impl VariantCallAnnotationSet { Column::from(Series::new("chromosome_2".into(), chromosome_2_values)), Column::from(Series::new("position_2".into(), position_2_values)), Column::from(Series::new("variant_type".into(), variant_type_values)), - Column::from(Series::new("variant_sequence".into(), variant_sequence_values)), + Column::from(Series::new("sequence".into(), variant_sequence_values)), Column::from(Series::new("position_1_genic_region".into(), position_1_annotation_genic_region_values)), Column::from(Series::new("position_1_annotation".into(), position_1_annotation_values)), Column::from(Series::new("position_2_genic_region".into(), position_2_annotation_genic_region_values)), diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-001-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-001-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv index cdb49b9..dc400f1 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-001-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-001-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv @@ -1,2 +1,2 @@ -variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type variant_sequence consensus_read_names consensus_read_names_count read_names read_names_count +variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type sequence consensus_read_names consensus_read_names_count read_names read_names_count 1 chr17 7674224 + D chr17 7674226 + U 1 SNV A m64012_325382_158010/1/ccs,m64012_665998_318912/3/ccs,m64012_618298_831330/2/ccs 3 m64012_325382_158010/1/ccs,m64012_665998_318912/3/ccs,m64012_618298_831330/2/ccs 3 diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-001-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants_annotated.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-001-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants_annotated.tsv index e6a6bc0..7b4b982 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-001-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants_annotated.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-001-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants_annotated.tsv @@ -1,2 +1,2 @@ -variant_call_id chromosome_1 position_1 chromosome_2 position_2 variant_type variant_sequence position_1_genic_region position_1_annotation position_2_genic_region position_2_annotation +variant_call_id chromosome_1 position_1 chromosome_2 position_2 variant_type sequence position_1_genic_region position_1_annotation position_2_genic_region position_2_annotation 1 chr17 7674224 chr17 7674226 SNV A exonic ENSG00000141510.18;ENSG00000141510.18-ENST00000269305.9,ENSG00000141510.18-ENST00000359597.8,ENSG00000141510.18-ENST00000413465.6,ENSG00000141510.18-ENST00000420246.6,ENSG00000141510.18-ENST00000445888.6,ENSG00000141510.18-ENST00000455263.6,ENSG00000141510.18-ENST00000504290.5,ENSG00000141510.18-ENST00000504937.5,ENSG00000141510.18-ENST00000509690.5,ENSG00000141510.18-ENST00000510385.5,ENSG00000141510.18-ENST00000514944.5,ENSG00000141510.18-ENST00000610292.4,ENSG00000141510.18-ENST00000610538.4,ENSG00000141510.18-ENST00000610623.4,ENSG00000141510.18-ENST00000618944.4,ENSG00000141510.18-ENST00000619186.4,ENSG00000141510.18-ENST00000619485.4,ENSG00000141510.18-ENST00000620739.4,ENSG00000141510.18-ENST00000622645.4;ENST00000269305.9-ENSE00003712342.1,ENST00000359597.8-ENSE00003712342.1,ENST00000413465.6-ENSE00003712342.1,ENST00000420246.6-ENSE00003712342.1,ENST00000445888.6-ENSE00003712342.1,ENST00000455263.6-ENSE00003712342.1,ENST00000504290.5-ENSE00003712342.1,ENST00000504937.5-ENSE00003712342.1,ENST00000509690.5-ENSE00003712342.1,ENST00000510385.5-ENSE00003712342.1,ENST00000514944.5-ENSE00002048269.1,ENST00000610292.4-ENSE00003712342.1,ENST00000610538.4-ENSE00003712342.1,ENST00000610623.4-ENSE00003712342.1,ENST00000618944.4-ENSE00003712342.1,ENST00000619186.4-ENSE00003712342.1,ENST00000619485.4-ENSE00003712342.1,ENST00000620739.4-ENSE00003712342.1,ENST00000622645.4-ENSE00003712342.1 exonic ENSG00000141510.18;ENSG00000141510.18-ENST00000269305.9,ENSG00000141510.18-ENST00000359597.8,ENSG00000141510.18-ENST00000413465.6,ENSG00000141510.18-ENST00000420246.6,ENSG00000141510.18-ENST00000445888.6,ENSG00000141510.18-ENST00000455263.6,ENSG00000141510.18-ENST00000504290.5,ENSG00000141510.18-ENST00000504937.5,ENSG00000141510.18-ENST00000509690.5,ENSG00000141510.18-ENST00000510385.5,ENSG00000141510.18-ENST00000514944.5,ENSG00000141510.18-ENST00000610292.4,ENSG00000141510.18-ENST00000610538.4,ENSG00000141510.18-ENST00000610623.4,ENSG00000141510.18-ENST00000618944.4,ENSG00000141510.18-ENST00000619186.4,ENSG00000141510.18-ENST00000619485.4,ENSG00000141510.18-ENST00000620739.4,ENSG00000141510.18-ENST00000622645.4;ENST00000269305.9-ENSE00003712342.1,ENST00000359597.8-ENSE00003712342.1,ENST00000413465.6-ENSE00003712342.1,ENST00000420246.6-ENSE00003712342.1,ENST00000445888.6-ENSE00003712342.1,ENST00000455263.6-ENSE00003712342.1,ENST00000504290.5-ENSE00003712342.1,ENST00000504937.5-ENSE00003712342.1,ENST00000509690.5-ENSE00003712342.1,ENST00000510385.5-ENSE00003712342.1,ENST00000514944.5-ENSE00002048269.1,ENST00000610292.4-ENSE00003712342.1,ENST00000610538.4-ENSE00003712342.1,ENST00000610623.4-ENSE00003712342.1,ENST00000618944.4-ENSE00003712342.1,ENST00000619186.4-ENSE00003712342.1,ENST00000619485.4-ENSE00003712342.1,ENST00000620739.4-ENSE00003712342.1,ENST00000622645.4-ENSE00003712342.1 diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-002-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-002-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv index e1ab6c5..f1b696f 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-002-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-002-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv @@ -1,2 +1,2 @@ -variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type variant_sequence consensus_read_names consensus_read_names_count read_names read_names_count +variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type sequence consensus_read_names consensus_read_names_count read_names read_names_count 1 chr17 7674225 - D chr17 7674226 - U 30 INS ACGTACGTGGTATGCATGCTGAGACTGAGG m64012_413665_732367/1/ccs,m64012_382982_262550/2/ccs,m64012_202369_785869/3/ccs 3 m64012_413665_732367/1/ccs,m64012_382982_262550/2/ccs,m64012_202369_785869/3/ccs 3 diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-003-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-003-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv index 5a2b4d0..5f2898f 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-003-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-003-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv @@ -1,2 +1,2 @@ -variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type variant_sequence consensus_read_names consensus_read_names_count read_names read_names_count +variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type sequence consensus_read_names consensus_read_names_count read_names read_names_count 1 chr17 7674200 - D chr17 7674231 - U 30 DEL m64012_813570_745374/1/ccs,m64012_478275_464661/2/ccs,m64012_787306_416079/3/ccs 3 m64012_813570_745374/1/ccs,m64012_478275_464661/2/ccs,m64012_787306_416079/3/ccs 3 diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-004-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-004-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv index 6c9b01a..199b494 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-004-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-004-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv @@ -1,3 +1,3 @@ -variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type variant_sequence consensus_read_names consensus_read_names_count read_names read_names_count +variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type sequence consensus_read_names consensus_read_names_count read_names read_names_count 1 chr17 7670308 + D chr17 7680501 - D 10192 BND TTACACAATTATTAGGCCCCTCCTTGAGACCCTCCAGCTCTGGGCTGGGAGTTGCGGAGAATGGCAAAGAAGTATCCACACTCGTCCCTGGGTTTGGATGTTCTGTGGATACACTGAGGCAAGAATGTGGTTATAGGATTCAACCGGAGGAAGACTAAAAAAATGTCTGTGCAGGGCTGGGACCCAATGAG m64012_688467_423419/3/ccs,m64012_781722_218290/2/ccs,m64012_767230_904257/1/ccs 3 m64012_688467_423419/3/ccs,m64012_781722_218290/2/ccs,m64012_767230_904257/1/ccs 3 2 chr17 7670501 - U chr17 7680502 + U 10000 BND A m64012_688467_423419/3/ccs,m64012_767230_904257/1/ccs,m64012_781722_218290/2/ccs 3 m64012_688467_423419/3/ccs,m64012_767230_904257/1/ccs,m64012_781722_218290/2/ccs 3 diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-005-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-005-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv index d48ba3d..598a9e3 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-005-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-005-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv @@ -1,2 +1,2 @@ -variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type variant_sequence consensus_read_names consensus_read_names_count read_names read_names_count +variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type sequence consensus_read_names consensus_read_names_count read_names read_names_count 1 chr17 4637155 + D chr17 7674880 + U 3037724 BND ATATACGAGCGTACGTGACTGGTACGTTA m64012_114794_165957/3/ccs,m64012_283345_480209/1/ccs,m64012_785796_448978/2/ccs 3 m64012_114794_165957/3/ccs,m64012_283345_480209/1/ccs,m64012_785796_448978/2/ccs 3 diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-006-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-006-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv index 64e6add..a8d729d 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-006-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-006-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv @@ -1,2 +1,2 @@ -variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type variant_sequence consensus_read_names consensus_read_names_count read_names read_names_count +variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type sequence consensus_read_names consensus_read_names_count read_names read_names_count 1 chr17 7676155 + D chr18 5170101 + U -1 TRA G m64012_925457_739219/3/ccs,m64012_400530_589417/2/ccs,m64012_825713_352116/1/ccs 3 m64012_925457_739219/3/ccs,m64012_400530_589417/2/ccs,m64012_825713_352116/1/ccs 3 diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-007-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-007-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv index e267441..4dff19f 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-007-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-007-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv @@ -1,2 +1,2 @@ -variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type variant_sequence consensus_read_names consensus_read_names_count read_names read_names_count +variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type sequence consensus_read_names consensus_read_names_count read_names read_names_count 1 chr17 7676155 + D chr18 5170100 - D -1 TRA m64012_459679_277770/3/ccs,m64012_291012_248279/1/ccs,m64012_436917_496758/2/ccs 3 m64012_459679_277770/3/ccs,m64012_291012_248279/1/ccs,m64012_436917_496758/2/ccs 3 diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-008-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-008-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv index ad2b7bc..d20d6a3 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-008-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-008-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv @@ -1,3 +1,3 @@ -variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type variant_sequence consensus_read_names consensus_read_names_count read_names read_names_count +variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type sequence consensus_read_names consensus_read_names_count read_names read_names_count 2 chr17 7668421 + U chr17 7687489 + D 19067 BND G m64012_159437_408623/1/ccs,m64012_827124_453400/3/ccs,m64012_926681_991093/2/ccs 3 m64012_159437_408623/1/ccs,m64012_827124_453400/3/ccs,m64012_926681_991093/2/ccs 3 1 chr17 7668420 - D chr17 7668421 - U 1 INS T m64012_926681_991093/2/ccs,m64012_827124_453400/3/ccs,m64012_159437_408623/1/ccs 3 m64012_926681_991093/2/ccs,m64012_827124_453400/3/ccs,m64012_159437_408623/1/ccs 3 diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-009-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-009-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv index 5fdfbb3..0a8bb21 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-009-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-009-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv @@ -1,4 +1,4 @@ -variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type variant_sequence consensus_read_names consensus_read_names_count read_names read_names_count +variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type sequence consensus_read_names consensus_read_names_count read_names read_names_count 1 chr17 7668421 - U chr17 7687491 + U 19069 BND m64012_253179_426965/3/ccs,m64012_131378_588213/1/ccs,m64012_871250_915061/2/ccs 3 m64012_253179_426965/3/ccs,m64012_131378_588213/1/ccs,m64012_871250_915061/2/ccs 3 3 chr17 7668420 - D chr17 7668421 - U 1 INS T m64012_871250_915061/2/ccs,m64012_253179_426965/3/ccs,m64012_131378_588213/1/ccs 3 m64012_871250_915061/2/ccs,m64012_253179_426965/3/ccs,m64012_131378_588213/1/ccs 3 2 chr17 7687490 - D chr17 7687490 + D -1 BND m64012_871250_915061/2/ccs,m64012_253179_426965/3/ccs,m64012_131378_588213/1/ccs 3 m64012_871250_915061/2/ccs,m64012_253179_426965/3/ccs,m64012_131378_588213/1/ccs 3 diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-010-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-010-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv index 2bb1e2e..54e228c 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-010-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-010-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv @@ -1,4 +1,4 @@ -variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type variant_sequence consensus_read_names consensus_read_names_count read_names read_names_count +variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type sequence consensus_read_names consensus_read_names_count read_names read_names_count 3 chr17 7668420 - D chr17 7668421 - U 1 INS T m64012_633396_571140/2/ccs,m64012_525413_986568/1/ccs,m64012_497519_692299/3/ccs 3 m64012_633396_571140/2/ccs,m64012_525413_986568/1/ccs,m64012_497519_692299/3/ccs 3 4 chr17 7668421 - U chr17 7687491 + U 19069 BND m64012_497519_692299/3/ccs,m64012_633396_571140/2/ccs,m64012_525413_986568/1/ccs 3 m64012_497519_692299/3/ccs,m64012_633396_571140/2/ccs,m64012_525413_986568/1/ccs 3 1 chr17 7668421 - U chr17 7687489 - D 19067 BND G m64012_633396_571140/2/ccs,m64012_525413_986568/1/ccs,m64012_497519_692299/3/ccs 3 m64012_633396_571140/2/ccs,m64012_525413_986568/1/ccs,m64012_497519_692299/3/ccs 3 diff --git a/exacto/exacto-annotator/src/tests/data/tsv/dna-011-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv b/exacto/exacto-annotator/src/tests/data/tsv/dna-011-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv index 5bac01a..f234979 100644 --- a/exacto/exacto-annotator/src/tests/data/tsv/dna-011-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv +++ b/exacto/exacto-annotator/src/tests/data/tsv/dna-011-tumor_minimap2_mdtagged_sorted_exacto_somatic_variants.tsv @@ -1,3 +1,3 @@ -variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type variant_sequence consensus_read_names consensus_read_names_count read_names read_names_count +variant_call_id chromosome_1 position_1 strand_1 operation_1 chromosome_2 position_2 strand_2 operation_2 variant_size variant_type sequence consensus_read_names consensus_read_names_count read_names read_names_count 2 chr17 7668420 - D chr17 7668421 - U 30 INS AGCGGCGAATATCAGCTACCTCTTAAGATC m64012_474878_778231/2/ccs,m64012_503464_377764/3/ccs,m64012_520695_447719/1/ccs 3 m64012_474878_778231/2/ccs,m64012_503464_377764/3/ccs,m64012_520695_447719/1/ccs 3 1 chr17 7687490 + D chr17 7687491 + U 30 INS TATCTCGCGAATTCAGCTACTACTACGGGA m64012_520695_447719/1/ccs,m64012_503464_377764/3/ccs,m64012_474878_778231/2/ccs 3 m64012_520695_447719/1/ccs,m64012_503464_377764/3/ccs,m64012_474878_778231/2/ccs 3 diff --git a/exacto/exacto-caller/Cargo.toml b/exacto/exacto-caller/Cargo.toml index 1ead6ab..df6aec8 100644 --- a/exacto/exacto-caller/Cargo.toml +++ b/exacto/exacto-caller/Cargo.toml @@ -1,49 +1,35 @@ [package] name = "exacto-caller" -version = "0.4.5" -edition = "2021" +version.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true [dependencies] -exacto-core = { path = "../exacto-core", version = "0.4.5" } -bimap = { version = "0.6.3", features = ["serde"] } -bincode = "1.3" -bstr = "1.10.0" -chrono = "0.4.38" -csv = "1.3.0" -edit-distance = "2.1.3" -env_logger = "0.10" -flate2 = "1.0.35" -indicatif = "0.17" -interavl = "0.2.0" -itertools = "0.14.0" -log = "0.4.22" -nalgebra = "0.33.2" -noodles-bam = "0.83.0" -noodles-bgzf = "0.43.0" -noodles-core = "0.18.0" -noodles-fasta = "0.56.0" -noodles-sam = "0.79.0" -once_cell = "1.10" -polars = { version = "0.45.0", features = ["csv","decompress","json","lazy","parquet","serde","strings"] } -rayon = "1.10.0" -serde = { version = "1.0.210", features = ["derive"] } -sysinfo = "0.32.0" -tempfile = "3.6" -regex = "1.10.6" - -[profile.dev] -opt-level = 0 -debug = true -overflow-checks = true -panic = 'unwind' -incremental = true -codegen-units = 256 - -[profile.release] -opt-level = 3 -debug = false -lto = true -overflow-checks = false -panic = 'unwind' -incremental = true -codegen-units = 1 +exacto-core = { workspace = true } +bimap = { workspace = true } +bincode = { workspace = true } +bstr = { workspace = true } +chrono = { workspace = true } +csv= { workspace = true } +edit-distance = { workspace = true } +env_logger = { workspace = true } +flate2 = { workspace = true } +indicatif = { workspace = true } +interavl = { workspace = true } +itertools = { workspace = true } +log = { workspace = true } +nalgebra = { workspace = true } +noodles-bam = { workspace = true } +noodles-bgzf = { workspace = true } +noodles-core = { workspace = true } +noodles-fasta = { workspace = true } +noodles-sam = { workspace = true } +once_cell = { workspace = true } +polars = { workspace = true } +rayon = { workspace = true } +serde = { workspace = true } +statrs = { workspace = true } +sysinfo = { workspace = true } +tempfile = { workspace = true } +regex = { workspace = true } diff --git a/exacto/exacto-caller/src/algorithms/mod.rs b/exacto/exacto-caller/src/algorithms/mod.rs index 8c4efb6..308c801 100644 --- a/exacto/exacto-caller/src/algorithms/mod.rs +++ b/exacto/exacto-caller/src/algorithms/mod.rs @@ -2,4 +2,3 @@ pub mod reference_transcript_matching; pub mod variant_calling; pub mod variant_calling_dna; pub mod variant_calling_rna; -pub mod variant_calling_peptide; diff --git a/exacto/exacto-caller/src/algorithms/reference_transcript_matching.rs b/exacto/exacto-caller/src/algorithms/reference_transcript_matching.rs index 7fb8816..1b7a825 100644 --- a/exacto/exacto-caller/src/algorithms/reference_transcript_matching.rs +++ b/exacto/exacto-caller/src/algorithms/reference_transcript_matching.rs @@ -21,26 +21,28 @@ use crate::prelude::*; pub fn identify_reference_transcript_matches( exons: &Vec, gene_annotator: &impl GeneAnnotator, - chromosome_names_map: &BiMap,u16>, + chromosome_names_map: &BiMap, u16>, scoring_method: ReferenceTranscriptScoringMethod, selection_strategy: ReferenceTranscriptSelectionStrategy, top_k: usize, threshold: f32 ) -> Vec { - // Step 1. Identifying overlapping genes and transcript IDs - let reference_gene_ids: HashSet> = identify_overlapping_gene_ids( - exons, - gene_annotator, - chromosome_names_map - ); + // Step 1. Identifying overlapping transcript IDa let reference_transcript_ids: HashSet> = identify_overlapping_transcript_ids( exons, - &reference_gene_ids, gene_annotator, chromosome_names_map ); - // Step 2. Score each transcript in each gene + // Step 2. Identifying overlapping gene IDs + let mut reference_gene_ids: HashSet> = HashSet::new(); + for reference_transcript_id in reference_transcript_ids.iter() { + let transcript: &Transcript = gene_annotator.get_transcript(reference_transcript_id).unwrap(); + reference_gene_ids.insert(transcript.gene_id.clone()); + } + + // Step 3. Score each overlapping transcript in each gene + // gene_id -> scored transcript matches let mut reference_transcript_scores: HashMap, Vec> = HashMap::new(); for reference_gene_id in reference_gene_ids.iter() { let reference_gene: &Gene = gene_annotator.get_gene(reference_gene_id).unwrap(); @@ -60,38 +62,37 @@ pub fn identify_reference_transcript_matches( reference_transcript_scores.insert(reference_gene.gene_id.clone(), reference_transcript_matches_); } - // Step 3. Select reference transcript matches + // Step 4. Select reference transcript matches let mut reference_transcript_matches: Vec = Vec::new(); for reference_gene_id in reference_gene_ids.iter() { if let Some(matches) = reference_transcript_scores.get(reference_gene_id) { - // Sort by score descending - let mut sorted_matches = matches.clone(); - sorted_matches.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); + let mut sorted_matches: Vec = matches.clone(); - if selection_strategy == ReferenceTranscriptSelectionStrategy::TopK { - // Find the top K score values - let mut prev_score: f32 = f32::NEG_INFINITY; - let mut top_k_scores: Vec = Vec::new(); - for reference_transcript_match in sorted_matches.iter() { - let score = reference_transcript_match.score; - if (score == prev_score) || (top_k_scores.len() >= top_k) { - continue; - } - top_k_scores.push(score); - prev_score = score; - } + // Build canonical ordering via rank_transcripts + let ranked: Vec<&Transcript> = gene_annotator.rank_transcripts( + sorted_matches.iter() + .map(|m| gene_annotator.get_transcript(&*m.reference_transcript_id).unwrap()) + .collect() + ); + let canonical_order: HashMap<&str, usize> = ranked.iter() + .enumerate() + .map(|(i, t)| (t.transcript_id.as_ref(), i)) + .collect(); - // Find reference transcript matches that have the top K score values - let min_score = top_k_scores - .iter() - .copied() - .reduce(|a, b| a.partial_cmp(&b).map(|o| if o == std::cmp::Ordering::Less { a } else { b }).unwrap()) - .expect("top_k_scores is empty"); - for reference_transcript_match in sorted_matches.iter() { - if reference_transcript_match.score >= min_score { - reference_transcript_matches.push(reference_transcript_match.clone()); - } - } + // Sort by score descending, then canonical rank ascending as tiebreaker + sorted_matches.sort_by(|a, b| { + b.score.partial_cmp(&a.score) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| { + let rank_a = canonical_order.get(a.reference_transcript_id.as_ref()).unwrap_or(&usize::MAX); + let rank_b = canonical_order.get(b.reference_transcript_id.as_ref()).unwrap_or(&usize::MAX); + rank_a.cmp(rank_b) + }) + }); + + if selection_strategy == ReferenceTranscriptSelectionStrategy::TopK { + sorted_matches.truncate(top_k); + reference_transcript_matches.extend(sorted_matches); } else if selection_strategy == ReferenceTranscriptSelectionStrategy::Threshold { reference_transcript_matches.extend( sorted_matches.into_iter().filter(|m| m.score >= threshold) @@ -102,7 +103,7 @@ pub fn identify_reference_transcript_matches( } } - // Step 4. Sort by score + // Step 5. Sort by score reference_transcript_matches.sort_by(|a, b| { b.score .partial_cmp(&a.score) @@ -113,63 +114,51 @@ pub fn identify_reference_transcript_matches( reference_transcript_matches } -fn identify_overlapping_gene_ids( - exons: &Vec, +fn identify_overlapping_transcript_ids( + model_exons: &Vec, gene_annotator: &impl GeneAnnotator, chromosome_names_map: &BiMap, u16> ) -> HashSet> { - let mut gene_ids: HashSet> = HashSet::new(); - for exon in exons.iter() { - let chromosome_name: Box = chromosome_names_map.get_by_right(&exon.reference_chromosome_id).unwrap().to_string().into_boxed_str(); - for gene_id in gene_annotator.get_gene_ids_overlapping_region(&*chromosome_name, exon.reference_start, exon.reference_end) { - let gene: &Gene = gene_annotator.get_gene(&*gene_id).unwrap(); - if gene.strand.as_str() == exon.reference_strand.as_str() { - gene_ids.insert(gene_id); + let mut reference_transcript_ids: HashSet> = HashSet::new(); + for model_exon in model_exons.iter() { + let chromosome_name: Box = chromosome_names_map.get_by_right(&model_exon.reference_chromosome_id).unwrap().to_string().into_boxed_str(); + let overlapping_transcript_ids: Vec> = gene_annotator.get_transcript_ids_overlapping_region( + &*chromosome_name, + model_exon.reference_start, + model_exon.reference_end + ); + for transcript_id in overlapping_transcript_ids.iter() { + if reference_transcript_ids.contains(transcript_id) { + continue; } - } - } - gene_ids -} - -fn identify_overlapping_transcript_ids( - exons: &Vec, - reference_gene_ids: &HashSet>, - gene_annotator: &impl GeneAnnotator, - chromosome_names_map: &BiMap,u16> -) -> HashSet> { - let mut transcript_ids: HashSet> = HashSet::new(); - for reference_gene_id in reference_gene_ids { - let reference_gene: &Gene = gene_annotator.get_gene(&reference_gene_id).unwrap(); - for transcript in reference_gene.transcripts.values() { - let transcript: &Transcript = gene_annotator.get_transcript(&*transcript.transcript_id).unwrap(); - for exon in exons.iter() { - let chromosome_name: Box = chromosome_names_map.get_by_right(&exon.reference_chromosome_id).unwrap().to_string().into_boxed_str(); - if transcript.chromosome == chromosome_name && - transcript.strand == exon.reference_strand { + let transcript: &Transcript = gene_annotator.get_transcript(transcript_id).unwrap(); + if transcript.strand == model_exon.reference_strand { + for reference_exon in transcript.get_sorted_exons() { if overlaps( - exon.reference_start as isize, - exon.reference_end as isize, - transcript.start as isize, - transcript.end as isize + model_exon.reference_start as isize, + model_exon.reference_end as isize, + reference_exon.start as isize, + reference_exon.end as isize ) { - transcript_ids.insert(transcript.transcript_id.clone()); + reference_transcript_ids.insert(transcript_id.clone()); } } } } } - transcript_ids + + reference_transcript_ids } fn score_reference_transcript( exons: &Vec, reference_transcript: &Transcript, reference_gene: &Gene, - chromosome_names_map: &BiMap,u16>, + chromosome_names_map: &BiMap, u16>, scoring_method: ReferenceTranscriptScoringMethod ) -> ReferenceTranscriptMatch { // Step 1. Get the transcript model's exonic regions - let model_exon_regions: Vec<(Box, usize, usize)> = exons + let model_exon_regions: Vec<(Box, u32, u32)> = exons .iter() .map(|exon| { let chr = chromosome_names_map @@ -182,7 +171,7 @@ fn score_reference_transcript( .collect(); // Step 2. Get the reference transcript's exonic regions - let reference_exons: Vec<(Box, usize, usize)> = reference_transcript + let reference_exons: Vec<(Box, u32, u32)> = reference_transcript .get_sorted_exons() .iter() .map(|exon| (exon.chromosome.clone(), exon.start, exon.end)) @@ -220,8 +209,8 @@ fn score_reference_transcript( ReferenceTranscriptScoringMethod::CosineSimilarity => { // Identify the portion of the reference gene covered by the transcript model exons let chromosome_id: u16 = *chromosome_names_map.get_by_left(&reference_transcript.chromosome).unwrap(); - let mut exon_reference_start_positions: Vec = Vec::new(); - let mut exon_reference_end_positions: Vec = Vec::new(); + let mut exon_reference_start_positions: Vec = Vec::new(); + let mut exon_reference_end_positions: Vec = Vec::new(); let reference_gene_start: isize = reference_gene.start as isize; let reference_gene_end: isize = reference_gene.end as isize; for exon in exons.iter() { @@ -241,12 +230,8 @@ fn score_reference_transcript( let max_exon_reference_end_position = *exon_reference_end_positions.iter().max().expect("Exon reference end positions vector is empty."); // Tight boundaries - let vectorization_reference_start: usize = reference_transcript.start.max(min_exon_reference_start_position); - let vectorization_reference_end: usize = reference_transcript.end.min(max_exon_reference_end_position); - - // Loose boundaries - // let vectorization_reference_start: usize = reference_transcript.start.min(min_exon_reference_start_position); - // let vectorization_reference_end: usize = reference_transcript.end.max(max_exon_reference_end_position); + let vectorization_reference_start: u32 = reference_transcript.start.max(min_exon_reference_start_position); + let vectorization_reference_end: u32 = reference_transcript.end.min(max_exon_reference_end_position); assert!( vectorization_reference_start <= vectorization_reference_end, @@ -298,14 +283,17 @@ fn score_reference_transcript( } }; - let num_overlap_bases: f32 = count_common_bases(&model_exon_regions, &reference_exons) as f32; + let num_overlap_bases: u32 = count_common_bases(&model_exon_regions, &reference_exons); - let (num_model_only_bases, num_reference_only_bases) = count_non_overlapping_bases(&model_exon_regions, &reference_exons); + let (num_model_only_bases, num_reference_only_bases) = count_non_overlapping_bases( + &model_exon_regions, + &reference_exons + ); let reference_transcript_match: ReferenceTranscriptMatch = ReferenceTranscriptMatch::new( &*reference_transcript.gene_id, &*reference_transcript.transcript_id, - num_overlap_bases as usize, + num_overlap_bases, num_model_only_bases, num_reference_only_bases, scoring_method.clone(), @@ -319,7 +307,7 @@ fn score_reference_transcripts( exons: &Vec, reference_transcripts: Vec<&Transcript>, reference_gene: &Gene, - chromosome_names_map: &BiMap,u16>, + chromosome_names_map: &BiMap, u16>, scoring_method: ReferenceTranscriptScoringMethod ) -> Vec { let reference_transcript_matches: Vec = reference_transcripts diff --git a/exacto/exacto-caller/src/algorithms/variant_calling.rs b/exacto/exacto-caller/src/algorithms/variant_calling.rs index 9a129a0..f432fd4 100644 --- a/exacto/exacto-caller/src/algorithms/variant_calling.rs +++ b/exacto/exacto-caller/src/algorithms/variant_calling.rs @@ -14,19 +14,18 @@ use bimap::BiMap; use edit_distance::edit_distance; use exacto_core::prelude::*; -use interavl::IntervalTree; use rayon::prelude::*; +use statrs::distribution::{Binomial, DiscreteCDF}; +use statrs::distribution::{Discrete, Hypergeometric}; use std::cmp::{min,max}; use std::collections::{BTreeMap,HashMap,HashSet}; use std::sync::Arc; -use sysinfo::System; use crate::prelude::*; -use crate::log_info; -fn calculate_max_distance(size: f32, tau: usize, max_distance: usize) -> usize { - (max_distance as f32 * (1.0 - f32::exp(-size / tau as f32))).ceil() as usize +fn calculate_max_distance(size: f32, tau: u32, max_distance: u32) -> u32 { + (max_distance as f32 * (1.0 - f32::exp(-size / tau as f32))).ceil() as u32 } /// Cluster variant records. @@ -52,17 +51,19 @@ fn calculate_max_distance(size: f32, tau: usize, max_distance: usize) -> usize { /// opposite strands. pub fn cluster_variant_records( variant_records: Vec>, + depths_map: &Arc, Vec>>, + chromosome_names_map: &BiMap, u16>, num_threads: usize, min_size_proportion: f32, max_ins_norm_edit_distance: f32, - max_intrachromosomal_distance_tau: usize, - max_intrachromosomal_distance: usize, - max_interchromosomal_distance: usize, + max_intrachromosomal_distance_tau: u32, + max_intrachromosomal_distance: u32, + max_interchromosomal_distance: u32, stranded: bool ) -> Vec { - // Step 1. Split variant records + // Step 1. Split variant records by chromosome // Key = (chromosome_1 ID, chromosome_2 ID) - let mut variant_records_map: HashMap<(u16, u16), Vec>> = split_variant_records_by_chromosome( + let mut variant_records_map: HashMap<(u16, u16, VariantType, GraphOperationType, GraphOperationType), Vec>> = split_variant_records( variant_records .into_iter() .map(|rc| Arc::new((*rc).clone())) @@ -70,27 +71,12 @@ pub fn cluster_variant_records( num_threads ); - // Step 2. Sort variant records by position_1 - let thread_pool = rayon::ThreadPoolBuilder::new() - .num_threads(num_threads) - .build() - .unwrap(); - thread_pool.install(|| { - variant_records_map - .iter_mut() // Borrow mutable references instead of consuming the map - .for_each(|(_, records)| { - records.sort_by(|variant_record_1, variant_record_2| { - variant_record_1.get_position_1().cmp(&variant_record_2.get_position_1()) - }); - }); - }); - - // Step 3. Identify variant calls + // Step 2. Identify variant calls let mut variant_calls: Vec = Vec::new(); - for ((chromosome_1, chromosome_2), curr_variant_records) in variant_records_map.iter() { + for curr_variant_records in variant_records_map.values() { // Identify local clusters of variant records using sweep line algorithm let clusters: Vec = sweep_clusters( - curr_variant_records.clone(), + curr_variant_records, min_size_proportion, max_ins_norm_edit_distance, max_intrachromosomal_distance_tau, @@ -101,9 +87,13 @@ pub fn cluster_variant_records( ); // Create variant calls + let thread_pool = rayon::ThreadPoolBuilder::new() + .num_threads(num_threads) + .build() + .unwrap(); let curr_variant_calls: Vec = thread_pool.install(|| { clusters - .par_chunks((clusters.len() + num_threads - 1) / num_threads) + .par_chunks(((clusters.len() + num_threads - 1) / num_threads).max(1)) .flat_map(|curr_clusters| { let mut curr_variant_calls: Vec = Vec::new(); for curr_cluster in curr_clusters.iter() { @@ -111,6 +101,18 @@ pub fn cluster_variant_records( for variant_record in curr_cluster.variant_records.iter() { curr_variant_call.add_variant_record((**variant_record).clone()); } + let variant_record: &VariantRecord = curr_variant_call.get_consensus_record().0; + let total_depth: u32 = get_variant_position_total_depth( + &depths_map, + chromosome_names_map.get_by_right(&variant_record.get_chromosome_1()).unwrap(), + variant_record.get_position_1(), + variant_record.get_operation_1(), + chromosome_names_map.get_by_right(&variant_record.get_chromosome_2()).unwrap(), + variant_record.get_position_2(), + variant_record.get_operation_2(), + variant_record.get_standardized_sequence().as_str() + ); + curr_variant_call.set_total_depth(total_depth as i32); curr_variant_calls.push(curr_variant_call); } curr_variant_calls.into_par_iter() @@ -125,6 +127,145 @@ pub fn cluster_variant_records( variant_calls } +pub fn compute_min_read_support( + total_depth: u64, + expected_variant_allele_fraction: f64, // 0.25 for tumor, 0.5 for normal + expected_mutation_rate: f64, // 1e-6 for tumor, 1e-3 for normal + expected_sequencing_error: f64, // 0.01 for PacBio and Illumina, 0.05 for ONT (R9) + max_f1_fraction: f64, // e.g. 0.99 + max_fpr: f64 // e.g. 1e-6 +) -> (u32, f64, f64, f64, f64) { + assert!(total_depth > 0); + assert!((0.0..=1.0).contains(&expected_variant_allele_fraction) && expected_variant_allele_fraction > 0.0); + assert!((0.0..=1.0).contains(&expected_mutation_rate) && expected_mutation_rate > 0.0); + assert!((0.0..=1.0).contains(&expected_sequencing_error)); + assert!((0.0..=1.0).contains(&max_f1_fraction)); + assert!((0.0..=1.0).contains(&max_fpr)); + + let binom_f = Binomial::new(expected_variant_allele_fraction, total_depth).unwrap(); + let binom_e = Binomial::new(expected_sequencing_error, total_depth).unwrap(); + + let mut f1_values: Vec = Vec::with_capacity(total_depth as usize); + let mut recalls: Vec = Vec::with_capacity(total_depth as usize); + let mut fpr_values: Vec = Vec::with_capacity(total_depth as usize); + let mut precisions: Vec = Vec::with_capacity(total_depth as usize); + + for k in 1..=total_depth { + let recall: f64 = 1.0 - binom_f.cdf(k - 1); + let fpr: f64 = 1.0 - binom_e.cdf(k - 1); + let precision: f64 = (recall * expected_mutation_rate) / ((recall * expected_mutation_rate) + (fpr * (1.0 - expected_mutation_rate))); + let f1: f64 = f1_score(precision, recall); + + recalls.push(recall); + fpr_values.push(fpr); + precisions.push(precision); + f1_values.push(f1); + } + + // Eligible k: precision >= min_precision + let eligible: Vec = (0..(total_depth as usize)) + .filter(|&idx| fpr_values[idx] <= max_fpr && f1_values[idx].is_finite()) + .collect(); + + if eligible.is_empty() { + return (u32::MAX, 0.0f64, 0.0f64, 0.0f64, 0.0f64); + } + + // Peak F1 among eligible k + let (peak_idx, f1_peak) = eligible + .iter() + .copied() + .fold((eligible[0], f1_values[eligible[0]]), |best, i| { + let v = f1_values[i]; + if v > best.1 { (i, v) } else { best } + }); + + let threshold: f64 = max_f1_fraction * f1_peak; + + // Among eligible ks, choose the smallest k whose F1 is within threshold of the eligible peak + let best_idx = eligible + .iter() + .copied() + .filter(|&i| f1_values[i] >= threshold) + .min() + .unwrap_or(peak_idx); + + let optimal_read_support = (best_idx + 1) as u32; + ( + optimal_read_support, + f1_values[best_idx], + recalls[best_idx], + fpr_values[best_idx], + precisions[best_idx] + ) +} + +pub fn compute_min_read_support_index( + max_depth: u64, + max_slippage_repeat_length: u32, + expected_variant_allele_fraction: f64, // 0.25 for tumor, 0.5 for normal + expected_mutation_rate: f64, // 1e-6 for tumor, 1e-3 for normal + expected_sequencing_error: f64, // 0.001 for PacBio and Illumina, 0.05 for ONT + expected_slippage_rate: f64, // same as expected_sequencing_error + max_f1_fraction: f64, // typically 0.95 + max_fpr: f64 // typically 1e-6 +) -> Vec> { + assert!(max_depth > 0); + assert!(max_slippage_repeat_length > 1); + assert!(expected_variant_allele_fraction > 0.0); + assert!(expected_variant_allele_fraction < 1.0); + assert!(expected_mutation_rate > 0.0); + assert!(expected_mutation_rate < 1.0); + assert!(expected_sequencing_error >= 0.0); + assert!(expected_sequencing_error < 1.0); + assert!(expected_slippage_rate >= 0.0); + assert!(expected_slippage_rate < 1.0); + assert!(max_f1_fraction > 0.0); + assert!(max_f1_fraction <= 1.0); + assert!(max_fpr > 0.0); + assert!(max_fpr <= 1.0); + + let mut read_support_index: Vec> = Vec::new(); + + // No repeat + let mut read_suports: Vec = Vec::new(); + for c in 1..=max_depth { + let (min_read_support, f1, recall, fpr, precision) = compute_min_read_support( + c, + expected_variant_allele_fraction, + expected_mutation_rate, + expected_sequencing_error, + max_f1_fraction, + max_fpr + ); + read_suports.push(min_read_support); + } + read_support_index.push(read_suports); + + // No 1-bp homopolymer repeat + read_support_index.push(vec![]); + + // Repeats (from 2-bp) + for r in 2..=max_slippage_repeat_length { + let mut read_suports: Vec = Vec::new(); + let p_slippage: f64 = 1.0 - (1.0 - expected_slippage_rate).powf(r as f64 - 1.0); + for c in 1..=max_depth { + let (min_read_support, f1, recall, fpr, precision) = compute_min_read_support( + c, + expected_variant_allele_fraction, + expected_mutation_rate, + p_slippage, + max_f1_fraction, + max_fpr + ); + read_suports.push(min_read_support); + } + read_support_index.push(read_suports); + } + + read_support_index +} + /// Diff b from a. /// /// # Parameters: @@ -155,13 +296,13 @@ pub fn cluster_variant_records( pub fn diff_variant_records( a: Vec>, b: Vec>, - bin_size: usize, + bin_size: u32, num_threads: usize, min_size_proportion: f32, max_ins_norm_edit_distance: f32, - max_intrachromosomal_distance_tau: usize, - max_intrachromosomal_distance: usize, - max_interchromosomal_distance: usize, + max_intrachromosomal_distance_tau: u32, + max_intrachromosomal_distance: u32, + max_interchromosomal_distance: u32, apply_infinite_sites_assumption: bool, stranded: bool ) -> Vec> { @@ -170,10 +311,9 @@ pub fn diff_variant_records( } // Step 1. Create indices - log_info!("\tCreating indices"); - let mut position_snv_map: HashSet<(u16,usize)> = HashSet::new(); - let mut position_1_map: HashMap<(u16, usize, VariantType),BTreeMap>>> = HashMap::new(); - let mut position_2_map: HashMap<(u16, usize, VariantType),BTreeMap>>> = HashMap::new(); + let mut position_snv_map: HashSet<(u16, u32)> = HashSet::new(); + let mut position_1_map: HashMap<(u16, u32, VariantType),BTreeMap>>> = HashMap::new(); + let mut position_2_map: HashMap<(u16, u32, VariantType),BTreeMap>>> = HashMap::new(); for variant_record in b.iter() { let variant_type: VariantType = variant_record.get_variant_type().clone(); if variant_type == VariantType::SingleNucleotideVariant { @@ -181,10 +321,10 @@ pub fn diff_variant_records( } else { let chr1: u16 = variant_record.get_chromosome_1(); let chr2: u16 = variant_record.get_chromosome_2(); - let pos1: usize = variant_record.get_position_1(); - let pos2: usize = variant_record.get_position_2(); - let zipcode1: usize = pos1 / bin_size; - let zipcode2: usize = pos2 / bin_size; + let pos1: u32 = variant_record.get_position_1(); + let pos2: u32 = variant_record.get_position_2(); + let zipcode1: u32 = pos1 / bin_size; + let zipcode2: u32 = pos2 / bin_size; position_1_map .entry((chr1,zipcode1,variant_type.clone())) .or_insert_with(BTreeMap::new) @@ -201,8 +341,7 @@ pub fn diff_variant_records( } // Step 2. Identify differences - log_info!("\tDiffing against interval trees"); - let max_distance: usize = max(max_intrachromosomal_distance, max_interchromosomal_distance); + let max_distance: u32 = max(max_intrachromosomal_distance, max_interchromosomal_distance); let chunk_size = (a.len() + num_threads - 1) / num_threads; let thread_pool = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) @@ -223,13 +362,13 @@ pub fn diff_variant_records( } } else { // Search nearby variant records for position_1 - let chr1 = variant_a.get_chromosome_1(); - let pos1 = variant_a.get_position_1(); - let zipcode1 = pos1 / bin_size; - let min_zipcode1 = if zipcode1 > 0 { zipcode1 - 1 } else { 0 }; + let chr1: u16 = variant_a.get_chromosome_1(); + let pos1: u32 = variant_a.get_position_1(); + let zipcode1: u32 = pos1 / bin_size; + let min_zipcode1: u32 = if zipcode1 > 0 { zipcode1 - 1 } else { 0 }; for zipcode in min_zipcode1..=(zipcode1 + 1) { if let Some(btree) = position_1_map.get(&(chr1,zipcode,variant_type.clone())) { - let results = btree.range(pos1 - max_distance..=pos1 + max_distance); + let results = btree.range(pos1.saturating_sub(max_distance)..=pos1 + max_distance); for (_, sequence_operations) in results { for sequence_operation in sequence_operations.iter() { if !is_different( @@ -251,10 +390,10 @@ pub fn diff_variant_records( } // Search nearby variant records for position_2 - let chr2 = variant_a.get_chromosome_2(); - let pos2 = variant_a.get_position_2(); - let zipcode2 = pos2 / bin_size; - let min_zipcode2 = if zipcode2 > 0 { zipcode2 - 1 } else { 0 }; + let chr2: u16 = variant_a.get_chromosome_2(); + let pos2: u32 = variant_a.get_position_2(); + let zipcode2: u32 = pos2 / bin_size; + let min_zipcode2: u32 = if zipcode2 > 0 { zipcode2 - 1 } else { 0 }; for zipcode in min_zipcode2..=(zipcode2 + 1) { if let Some(btree) = position_2_map.get(&(chr2,zipcode,variant_type.clone())) { let results = btree.range(pos2 - max_distance..=pos2 + max_distance); @@ -286,6 +425,111 @@ pub fn diff_variant_records( }) } +pub fn get_variant_position_total_depth( + depths_map: &HashMap, Vec>, + chromosome_1: &str, + position_1: u32, + operation_1: &GraphOperationType, + chromosome_2: &str, + position_2: u32, + operation_2: &GraphOperationType, + sequence: &str +) -> u32 { + // SNV + if chromosome_1 == chromosome_2 && + *operation_1 == GraphOperationType::Downstream && + *operation_2 == GraphOperationType::Upstream && + (position_2 - position_1 - 1) == 1 && + sequence.len() == 1 { + let index: usize = (position_1 + 1 - 1) as usize; + let depth: u32 = *depths_map.get(chromosome_1).unwrap().get(index).unwrap(); + return depth; + } + + // MNV + if chromosome_1 == chromosome_2 && + *operation_1 == GraphOperationType::Downstream && + *operation_2 == GraphOperationType::Upstream && + (position_2 - position_1 - 1) == sequence.len() as u32 && + sequence.len() >= 2 { + let start: usize = (position_1 + 1 - 1) as usize; + let end: usize = (position_2 - 1 - 1) as usize; + let depths: Vec = depths_map.get(chromosome_1).unwrap()[start..=end].to_vec(); + let max_depth: u32 = *depths.iter().max().unwrap(); + return max_depth; + } + + // INS + if chromosome_1 == chromosome_2 && + *operation_1 == GraphOperationType::Downstream && + *operation_2 == GraphOperationType::Upstream && + (position_2 - position_1 - 1) == 0 && + sequence.len() >= 1 { + let start: usize = (position_1 - 1) as usize; + let end: usize = (position_2 - 1) as usize; + let depths: Vec = depths_map.get(chromosome_1).unwrap()[start..=end].to_vec(); + let max_depth: u32 = *depths.iter().max().unwrap(); + return max_depth; + } + + // DEL + if chromosome_1 == chromosome_2 && + *operation_1 == GraphOperationType::Downstream && + *operation_2 == GraphOperationType::Upstream && + (position_2 - position_1 - 1) >= 1 && + sequence.len() == 0 { + let index_1: usize = (position_1 - 1) as usize; + let index_2: usize = (position_2 - 1) as usize; + let depth_1: u32 = *depths_map.get(chromosome_1).unwrap().get(index_1).unwrap(); + let depth_2: u32 = *depths_map.get(chromosome_1).unwrap().get(index_2).unwrap(); + if depth_1 < depth_2 { + return depth_2; + } else { + return depth_1; + } + } + + // BND + let index_1: usize = (position_1 - 1) as usize; + let index_2: usize = (position_2 - 1) as usize; + let depth_1: u32 = *depths_map.get(chromosome_1).unwrap().get(index_1).unwrap(); + let depth_2: u32 = *depths_map.get(chromosome_2).unwrap().get(index_2).unwrap(); + if depth_1 < depth_2 { + depth_2 + } else { + depth_1 + } +} + +pub fn has_strand_bias( + alt_fwd: u64, + alt_rev: u64, + ref_fwd: u64, + ref_rev: u64, + alpha: f64 +) -> bool { + if alt_fwd == 0 || alt_rev == 0 { + return true; + } + + let alt_total: u64 = alt_fwd + alt_rev; + let ref_total: u64 = ref_fwd + ref_rev; + + // Fisher's exact test needs some REF evidence to compare against. + if ref_total == 0 { + return false; + } + + let p: f64 = perform_fisher_exact_test_two_sided( + alt_fwd, + alt_rev, + ref_fwd, + ref_rev + ); + + p < alpha +} + /// Checks whether two VariantRecord objects can be clustered together. /// /// # Parameters: @@ -316,50 +560,66 @@ pub fn is_clusterable( b: &VariantRecord, min_size_proportion: f32, max_ins_norm_edit_distance: f32, - max_intrachromosomal_distance_tau: usize, - max_intrachromosomal_distance: usize, - max_interchromosomal_distance: usize, + max_intrachromosomal_distance_tau: u32, + max_intrachromosomal_distance: u32, + max_interchromosomal_distance: u32, stranded: bool ) -> bool { // Records from the same read ID cannot be clustered - if a.get_read_id() == b.get_read_id() { + let a_read_id: usize = a.get_read_id(); + let b_read_id: usize = b.get_read_id(); + if a_read_id == b_read_id { return false; } - // Chromosomes 1 and 2 must be the same - if a.get_chromosome_1() != b.get_chromosome_1() || a.get_chromosome_2() != b.get_chromosome_2() { + // Variant types must be the same + let a_variant_type: &VariantType = a.get_variant_type(); + let b_variant_type: &VariantType = b.get_variant_type(); + if a_variant_type != b_variant_type { return false; } - // Variant types must be the same - if a.get_variant_type() != b.get_variant_type() { + // Orientations must be the same + let a_op1: &GraphOperationType = a.get_operation_1(); + let a_op2: &GraphOperationType = a.get_operation_2(); + let b_op1: &GraphOperationType = b.get_operation_1(); + let b_op2: &GraphOperationType = b.get_operation_2(); + if a_op1 != b_op1 || a_op2 != b_op2 { return false; } - // Orientations must be the same - if a.get_operation_1() != b.get_operation_1() || a.get_operation_2() != b.get_operation_2() { + // Chromosomes 1 and 2 must be the same + let a_chr1: u16 = a.get_chromosome_1(); + let a_chr2: u16 = a.get_chromosome_2(); + let b_chr1: u16 = b.get_chromosome_1(); + let b_chr2: u16 = b.get_chromosome_2(); + if a_chr1 != b_chr1 || a_chr2 != b_chr2 { return false; } // Strand pairing must be the same - match (a.get_strand_1(), a.get_strand_2()) { + let a_s1: &Strand = a.get_strand_1(); + let a_s2: &Strand = a.get_strand_2(); + let b_s1: &Strand = b.get_strand_1(); + let b_s2: &Strand = b.get_strand_2(); + match (a_s1, a_s2) { (Strand::Forward, Strand::Forward) => { - if b.get_strand_1() != b.get_strand_2() { + if b_s1 != b_s2 { return false; } }, (Strand::Forward, Strand::Reverse) => { - if b.get_strand_1() == b.get_strand_2() { + if b_s1 == b_s2 { return false; } }, (Strand::Reverse, Strand::Forward) => { - if b.get_strand_1() == b.get_strand_2() { + if b_s1 == b_s2 { return false; } }, (Strand::Reverse, Strand::Reverse) => { - if b.get_strand_1() != b.get_strand_2() { + if b_s1 != b_s2 { return false; } }, @@ -368,46 +628,63 @@ pub fn is_clusterable( } } - // The normalized edit distance must be within the allowed limit if they are both insertions - if a.get_variant_type().clone() == VariantType::Insertion { - let a_sequence: String = a.graph_operation.get_standardized_sequence(); - let b_sequence: String = b.graph_operation.get_standardized_sequence(); - let edit_distance = edit_distance(a_sequence.as_str(), b_sequence.as_str()) as f32; - let max_size = f32::max(a_sequence.len() as f32, b_sequence.len() as f32); - let normalized_edit_distance: f32 = edit_distance / max_size; - if normalized_edit_distance > max_ins_norm_edit_distance { - return false; + // If stranded, strands must match exactly + if stranded && (a_s1 != b_s1 || a_s2 != b_s2) { + return false; + } + + // The size proportion must be within the allowed limit + let a_size: isize = a.get_variant_size(); + let b_size: isize = b.get_variant_size(); + let has_valid_sizes: bool = a_size >= 0 && b_size >= 0; + if has_valid_sizes { + let min_size: f32 = a_size.min(b_size) as f32; + let max_size: f32 = a_size.max(b_size) as f32; + if max_size > 0.0 { + let size_proportion: f32 = min_size / max_size; + if size_proportion < min_size_proportion { + return false; + } } } // The breakpoint distances must be close where proximity is a function of the variant size - let variant_size = f32::max(a.get_variant_size() as f32, b.get_variant_size() as f32); - let max_distance: usize; - if a.get_chromosome_1() == b.get_chromosome_2() { - if variant_size == 1f32 { - max_distance = 0; + let max_distance: u32 = if !has_valid_sizes { + max_interchromosomal_distance + } else if a_chr1 == b_chr2 { + let size_u32: u32 = a_size.max(b_size) as u32; + if size_u32 == 1 { + 0 } else { - max_distance = (max_intrachromosomal_distance as f32 * (1f32 - f32::exp(-1f32 * (variant_size / max_intrachromosomal_distance_tau as f32)))).ceil() as usize; + let tau = max_intrachromosomal_distance_tau as f32; + let size_f = size_u32 as f32; + let factor = 1.0_f32 - (-size_f / tau).exp(); + (max_intrachromosomal_distance as f32 * factor).ceil() as u32 } } else { - max_distance = max_interchromosomal_distance; - } - let distance_1: usize = (a.get_position_1() as f32 - b.get_position_1() as f32).abs() as usize; - let distance_2: usize = (a.get_position_2() as f32 - b.get_position_2() as f32).abs() as usize; - if distance_1 > max_distance || distance_2 > max_distance { - return false; - } + max_interchromosomal_distance + }; - // The size proportion must be within the allowed limit - let min_size = f32::min(a.get_variant_size() as f32, b.get_variant_size() as f32); - let max_size = f32::max(a.get_variant_size() as f32, b.get_variant_size() as f32); - let size_proportion: f32 = min_size / max_size; - if size_proportion < min_size_proportion { + let a_pos1: u32 = a.get_position_1(); + let a_pos2: u32 = a.get_position_2(); + let b_pos1: u32 = b.get_position_1(); + let b_pos2: u32 = b.get_position_2(); + + let distance_1: u32 = a_pos1.abs_diff(b_pos1); + let distance_2: u32 = a_pos2.abs_diff(b_pos2); + + if distance_1 > max_distance || distance_2 > max_distance { return false; } - if stranded { - if a.get_strand_1() != b.get_strand_1() || a.get_strand_2() != b.get_strand_2() { + // The normalized edit distance must be within the allowed limit if they are both insertions + if *a_variant_type == VariantType::Insertion { + let a_sequence: String = a.graph_operation.get_standardized_sequence(); + let b_sequence: String = b.graph_operation.get_standardized_sequence(); + let edit_distance: f32 = edit_distance(a_sequence.as_str(), b_sequence.as_str()) as f32; + let max_len: f32 = a_sequence.len().max(b_sequence.len()) as f32; + let normalized_edit_distance: f32 = edit_distance / max_len; + if normalized_edit_distance > max_ins_norm_edit_distance { return false; } } @@ -448,15 +725,15 @@ pub fn is_different( b: &GraphOperation, min_size_proportion: f32, max_ins_norm_edit_distance: f32, - max_intrachromosomal_distance_tau: usize, - max_intrachromosomal_distance: usize, - max_interchromosomal_distance: usize, + max_intrachromosomal_distance_tau: u32, + max_intrachromosomal_distance: u32, + max_interchromosomal_distance: u32, apply_infinite_sites_assumption: bool, stranded: bool ) -> bool { // Fast early exit for different chromosomes - if a.get_chromosome_1() != b.get_chromosome_1() && - a.get_chromosome_2() != b.get_chromosome_2() { + if (a.get_chromosome_1() != b.get_chromosome_1() && a.get_chromosome_2() != b.get_chromosome_2()) && + (a.get_chromosome_1() != b.get_chromosome_2() && a.get_chromosome_2() != b.get_chromosome_1()) { return true; } @@ -468,36 +745,46 @@ pub fn is_different( } // Precompute reusable properties - let size_a = a.get_variant_size(); - let size_b = b.get_variant_size(); - let max_size = max(size_a, size_b); - let min_size = min(size_a, size_b); - let size_proportion = min_size as f32 / max_size as f32; + let size_a: isize = a.get_variant_size(); + let size_b: isize = b.get_variant_size(); + let max_size: isize = max(size_a, size_b); + let min_size: isize = min(size_a, size_b); + let size_proportion: f32 = if max_size > 0 { + min_size as f32 / max_size as f32 + } else { + 1.0 + }; // Infinite sites assumption if apply_infinite_sites_assumption { - if (a.get_chromosome_1() == b.get_chromosome_1() && - a.get_position_1() == b.get_position_1()) || - (a.get_chromosome_2() == b.get_chromosome_2() && - a.get_position_2() == b.get_position_2()) { + if (a.get_chromosome_1() == b.get_chromosome_1() && a.get_position_1() == b.get_position_1()) || + (a.get_chromosome_2() == b.get_chromosome_2() && a.get_position_2() == b.get_position_2()) { return false; } } - let pos1_distance = a.get_position_1().abs_diff(b.get_position_1()); - let pos2_distance = a.get_position_2().abs_diff(b.get_position_2()); + let pos1_distance: u32 = a.get_position_1().abs_diff(b.get_position_1()); + let pos2_distance: u32 = a.get_position_2().abs_diff(b.get_position_2()); // Translocation if a.get_variant_type().clone() == VariantType::Translocation { + let cross_pos1_distance: u32 = a.get_position_1().abs_diff(b.get_position_2()); + let cross_pos2_distance: u32 = a.get_position_2().abs_diff(b.get_position_1()); if (a.get_chromosome_1() == b.get_chromosome_1() && pos1_distance <= max_interchromosomal_distance) || - (a.get_chromosome_2() == b.get_chromosome_2() && pos2_distance <= max_interchromosomal_distance) { + (a.get_chromosome_2() == b.get_chromosome_2() && pos2_distance <= max_interchromosomal_distance) || + (a.get_chromosome_1() == b.get_chromosome_2() && cross_pos1_distance <= max_interchromosomal_distance) || + (a.get_chromosome_2() == b.get_chromosome_1() && cross_pos2_distance <= max_interchromosomal_distance) { return false; } else { return true; } } - let max_distance: usize = calculate_max_distance(max(size_a, size_b) as f32, max_intrachromosomal_distance_tau, max_intrachromosomal_distance); + let max_distance: u32 = calculate_max_distance( + max(size_a, size_b) as f32, + max_intrachromosomal_distance_tau, + max_intrachromosomal_distance + ); // Insertion if a.get_variant_type().clone() == VariantType::Insertion { @@ -505,7 +792,7 @@ pub fn is_different( let b_sequence: String = b.get_standardized_sequence(); let edit_distance: f32 = edit_distance(a_sequence.as_str(), b_sequence.as_str()) as f32; let max_seq_length: f32 = max(a.get_sequence_length(), b.get_sequence_length()) as f32; - let normalized_edit_distance = edit_distance / max_seq_length; + let normalized_edit_distance: f32 = edit_distance / max_seq_length; if a.get_chromosome_1() == b.get_chromosome_1() && pos1_distance <= max_distance && size_proportion >= min_size_proportion && @@ -517,39 +804,306 @@ pub fn is_different( } // Deletion, breakpoint or MNV - if (a.get_chromosome_1() == b.get_chromosome_1() && - pos1_distance <= max_distance && - size_proportion >= min_size_proportion) || - (a.get_chromosome_2() == b.get_chromosome_2() && - pos2_distance <= max_distance && - size_proportion >= min_size_proportion) { + if (a.get_chromosome_1() == b.get_chromosome_1() && pos1_distance <= max_distance && size_proportion >= min_size_proportion) || + (a.get_chromosome_2() == b.get_chromosome_2() && pos2_distance <= max_distance && size_proportion >= min_size_proportion) { return false; } true } +pub fn is_repeat_indel( + variant_record: &VariantRecord, + chromosome_names_map: &BiMap, u16>, + fasta_map: &FastaMap +) -> (bool, u32) { + let variant_type: &VariantType = variant_record.get_variant_type(); + if variant_type != &VariantType::Insertion && variant_type != &VariantType::Deletion { + return (false, 0); + } + + let chromosome: &str = chromosome_names_map + .get_by_right(&variant_record.get_chromosome_1()) + .unwrap(); + + let position_1: u32 = variant_record.get_position_1(); + let position_2: u32 = variant_record.get_position_2(); + + // Helper functions + let get_fasta_sequence = |start: u32, end: u32| -> Box { + if start >= 1 && end <= fasta_map.get_length(chromosome) as u32 { + fasta_map + .get_sequence(chromosome, start as usize, end as usize) + .to_uppercase() + .into_boxed_str() + } else { + "".into() + } + }; + let count_left_homopolymer = |mut position: u32, base: &str| -> u32 { + let mut n: u32 = 0; + while position >= 1 { + if &*get_fasta_sequence(position, position) == base { + n += 1; + if position == 1 { + break; + } + position -= 1; + } else { + break; + } + } + n + }; + let count_right_homopolymer = |mut position: u32, base: &str| -> u32 { + let mut n: u32 = 0; + loop { + if &*get_fasta_sequence(position, position) == base { + n += 1; + position = position.saturating_add(1); + } else { + break; + } + } + n + }; + let count_left_dinucleotide_repeat = |mut position: u32, motif: &str| -> u32 { + let mut n: u32 = 0; + while position >= 2 { + let s: Box = get_fasta_sequence(position - 1, position); + if s.len() != 2 { + break; + } + if &*s == motif { + n += 2; + position -= 2; + } else { + break; + } + } + n + }; + let count_right_dinucleotide_repeat = |mut position: u32, motif: &str| -> u32 { + let mut n: u32 = 0; + loop { + let s: Box = get_fasta_sequence(position, position + 1); + if s.len() != 2 { + break; + } + if &*s == motif { + n += 2; + position = position.saturating_add(2); + } else { + break; + } + } + n + }; + + // Insertion + if variant_type == &VariantType::Insertion { + let sequence: String = variant_record.graph_operation.get_standardized_sequence(); + assert!(sequence.len() > 0); + + // Homopolymer insertion + if is_homopolymer_sequence(sequence.as_str()) { + let base: &str = sequence.chars().next().map(|c| &sequence[..c.len_utf8()]).unwrap(); + + // Count the length of the left homopolymer sequence, if any + let left_homopolymer_length: u32 = count_left_homopolymer(position_1, base); + + // Count the length of the right homopolymer sequence, if any + let right_homopolymer_length: u32 = count_right_homopolymer(position_2, base); + + if left_homopolymer_length > 0 || right_homopolymer_length > 0 { + return (true, left_homopolymer_length + sequence.len() as u32 + right_homopolymer_length); + } + } + + // Dinucleotide repeat insertion + if sequence.len() == 1 { + // Count the length of the left dinucleotide repeat sequence, if any + let left_sequence: Box = get_fasta_sequence(position_1 - 1, position_1); + let left_repeat_length: u32 = count_left_dinucleotide_repeat(position_1, &*left_sequence); + + // Count the length of the right dinucleotide repeat sequence, if any + let right_sequence: Box = get_fasta_sequence(position_2, position_2 + 1); + let right_repeat_length: u32 = count_right_dinucleotide_repeat(position_2, &*right_sequence); + + if left_repeat_length > 0 || right_repeat_length > 0 { + return (true, left_repeat_length + sequence.len() as u32 + right_repeat_length); + } + } else { + let result: Option = get_dinucleotide_repeat_motif(sequence.as_str()); + if result.is_some() { + let motif: String = result.unwrap(); + let motif_reversed: String = motif.chars().rev().collect(); + + // Count the length of the left dinucleotide repeat sequence, if any + let left_repeat_length_1: u32 = count_left_dinucleotide_repeat(position_1, motif.as_str()); + let left_repeat_length_2: u32 = count_left_dinucleotide_repeat(position_1, motif_reversed.as_str()); + + // Count the length of the right dinucleotide repeat sequence, if any + let right_repeat_length_1: u32 = count_right_dinucleotide_repeat(position_2, motif.as_str()); + let right_repeat_length_2: u32 = count_right_dinucleotide_repeat(position_2, motif_reversed.as_str()); + + let length_1: u32 = if left_repeat_length_1 > 0 || right_repeat_length_1 > 0 { + left_repeat_length_1 + sequence.len() as u32 + right_repeat_length_1 + } else { + 0 + }; + + let length_2: u32 = if left_repeat_length_2 > 0 || right_repeat_length_2 > 0 { + left_repeat_length_2 + sequence.len() as u32 + right_repeat_length_2 + } else { + 0 + }; + + if length_1 > 0 || length_2 > 0 { + return (true, length_1.max(length_2)); + } + } + } + } + + // Deletion + if variant_type == &VariantType::Deletion { + let sequence: Box = get_fasta_sequence(position_1 + 1, position_2 - 1); + assert!(sequence.len() > 0); + + // Homopolymer deletion + if is_homopolymer_sequence(&*sequence) { + let base: &str = sequence.chars().next().map(|c| &sequence[..c.len_utf8()]).unwrap(); + + // Count the length of the left homopolymer sequence, if any + let left_homopolymer_length: u32 = count_left_homopolymer(position_1, base); + + // Count the length of the right homopolymer sequence, if any + let right_homopolymer_length: u32 = count_right_homopolymer(position_2, base); + + if left_homopolymer_length > 0 || right_homopolymer_length > 0 { + return (true, left_homopolymer_length + sequence.len() as u32 + right_homopolymer_length); + } + } + + // Dinucleotide repeat deletion + if sequence.len() == 1 { + // Count the length of the left dinucleotide repeat sequence, if any + let left_sequence: Box = get_fasta_sequence(position_1 - 1, position_1); + let left_repeat_length: u32 = count_left_dinucleotide_repeat(position_1, &*left_sequence); + + // Count the length of the right dinucleotide repeat sequence, if any + let right_sequence: Box = get_fasta_sequence(position_2, position_2 + 1); + let right_repeat_length: u32 = count_right_dinucleotide_repeat(position_2, &*right_sequence); + + if left_repeat_length > 0 || right_repeat_length > 0 { + return (true, left_repeat_length + sequence.len() as u32 + right_repeat_length); + } + } else { + let result: Option = get_dinucleotide_repeat_motif(&*sequence); + if result.is_some() { + let motif: String = result.unwrap(); + let motif_reversed: String = motif.chars().rev().collect(); + + // Count the length of the left dinucleotide repeat sequence, if any + let left_repeat_length_1: u32 = count_left_dinucleotide_repeat(position_1, motif.as_str()); + let left_repeat_length_2: u32 = count_left_dinucleotide_repeat(position_1, motif_reversed.as_str()); + + // Count the length of the right dinucleotide repeat sequence, if any + let right_repeat_length_1: u32 = count_right_dinucleotide_repeat(position_2, motif.as_str()); + let right_repeat_length_2: u32 = count_right_dinucleotide_repeat(position_2, motif_reversed.as_str()); + + let length_1: u32 = if left_repeat_length_1 > 0 || right_repeat_length_1 > 0 { + left_repeat_length_1 + sequence.len() as u32 + right_repeat_length_1 + } else { + 0 + }; + + let length_2: u32 = if left_repeat_length_2 > 0 || right_repeat_length_2 > 0 { + left_repeat_length_2 + sequence.len() as u32 + right_repeat_length_2 + } else { + 0 + }; + + if length_1 > 0 || length_2 > 0 { + return (true, length_1.max(length_2)); + } + } + } + } + + (false, 0) +} + + +/// Two-sided Fisher exact test p-value for a 2x2 table. +/// +/// Table: +/// FWD REV +/// ALT a b +/// REF c d +/// +/// Returns p-value in [0, 1]. +pub fn perform_fisher_exact_test_two_sided( + a: u64, + b: u64, + c: u64, + d: u64 +) -> f64 { + let r1: u64 = a + b; // ALT total + let r2: u64 = c + d; // REF total + let c1: u64 = a + c; // FWD total + let c2: u64 = b + d; // REV total + let n: u64 = r1 + r2; // total + + // Population: n + // Population successes: c1 (FWD reads) + // Draws: r1 (ALT reads) + let hg: Hypergeometric = Hypergeometric::new(n, c1, r1).expect("Invalid hypergeometric parameters for Fisher test"); + let observed_p: f64 = hg.pmf(a); + + // Feasible range: + // max(0, r1 - c2) <= x <= min(r1, c1) + let min_x: u64 = r1.saturating_sub(n - c1); + let max_x: u64 = r1.min(c1); + + // Two-sided definition: sum probabilities <= observed probability + let mut p_two_sided: f64 = 0.0; + for x in min_x..=max_x { + let px: f64 = hg.pmf(x); + if px <= observed_p + 1e-12 { + p_two_sided += px; + } + } + + p_two_sided.min(1.0) +} + /// Split variant records by chromosome. /// /// # Returns: /// /// * A HashMap where the key is (chromosome_1,chromosome_2) and the value is a vector of /// VariantRecord objects. -pub fn split_variant_records_by_chromosome( +pub fn split_variant_records( variant_records: Vec>, - num_threads: usize, -) -> HashMap<(u16, u16),Vec>> { + num_threads: usize +) -> HashMap<(u16, u16, VariantType, GraphOperationType, GraphOperationType), Vec>> { + // Step 1. Split variant records by chromosome let thread_pool = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) .build() .unwrap(); - thread_pool.install(|| { + let mut variant_records_map: HashMap<(u16, u16, VariantType, GraphOperationType, GraphOperationType), Vec>> = thread_pool.install(|| { variant_records .par_iter() .map(|variant_record| { let key = ( variant_record.get_chromosome_1(), variant_record.get_chromosome_2(), + variant_record.get_variant_type().clone(), + variant_record.get_operation_1().clone(), + variant_record.get_operation_2().clone() ); (key, Arc::clone(variant_record)) }) @@ -558,7 +1112,7 @@ pub fn split_variant_records_by_chromosome( |mut acc, (key, variant_record)| { acc.entry(key).or_insert_with(Vec::new).push(variant_record); acc - }, + } ) .reduce( || HashMap::new(), @@ -567,9 +1121,22 @@ pub fn split_variant_records_by_chromosome( map1.entry(key).or_insert_with(Vec::new).append(&mut vec); } map1 - }, + } ) - }) + }); + + // Step 2. Sort variant records by position_1 + thread_pool.install(|| { + variant_records_map + .iter_mut() // Borrow mutable references instead of consuming the map + .for_each(|(_, records)| { + records.sort_by(|variant_record_1, variant_record_2| { + variant_record_1.get_position_1().cmp(&variant_record_2.get_position_1()) + }); + }); + }); + + variant_records_map } /// Sweep clusters. @@ -591,59 +1158,66 @@ pub fn split_variant_records_by_chromosome( /// * `max_interchromosomal_distance` is the maximum distance between two breakpoints for /// interchromosomal translocations. pub fn sweep_clusters( - variant_records: Vec>, + variant_records: &Vec>, min_size_proportion: f32, max_ins_norm_edit_distance: f32, - max_intrachromosomal_distance_tau: usize, - max_intrachromosomal_distance: usize, - max_interchromosomal_distance: usize, + max_intrachromosomal_distance_tau: u32, + max_intrachromosomal_distance: u32, + max_interchromosomal_distance: u32, num_threads: usize, stranded: bool ) -> Vec { - // Step 1. Build a binary search tree - let mut bst: BTreeMap>> = BTreeMap::new(); - for variant_record in variant_records.iter() { - let key = variant_record.get_position_1() as usize; - bst.entry(key) - .or_insert_with(Vec::new) - .push(Arc::clone(variant_record)); + let n: usize = variant_records.len(); + if n == 0 { + return Vec::new(); } - // Step 2. Ensure all records are on the same chromosomes + // Step 1. Ensure all records are on the same chromosomes let chromosome_1: u16 = variant_records[0].get_chromosome_1(); let chromosome_2: u16 = variant_records[0].get_chromosome_2(); - for variant_record in variant_records.iter() { - assert!(variant_record.get_chromosome_1() == chromosome_1, "Supplied variant_records must have the same chromosome_1 value."); - assert!(variant_record.get_chromosome_2() == chromosome_2, "Supplied variant_records must have the same chromosome_2 value."); - } - - // Step 3. Assign a unique ID to each VariantRecord - let mut variant_records_map: BiMap, usize> = BiMap::new(); - for (id, variant_record) in variant_records.iter().enumerate() { - variant_records_map.insert(Arc::clone(variant_record), id); + for vr in variant_records.iter() { + assert!( + vr.get_chromosome_1() == chromosome_1, + "Supplied variant_records must have the same chromosome_1 value." + ); + assert!( + vr.get_chromosome_2() == chromosome_2, + "Supplied variant_records must have the same chromosome_2 value." + ); } - // Step 4. Identify clusterable pairs - let max_distance: usize = max(max_intrachromosomal_distance, max_interchromosomal_distance); + // Step 2. Identify clusterable pairs with a sliding window over sorted positions. let thread_pool = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) .build() .unwrap(); - let pairs: HashSet<(usize, usize)> = thread_pool.install(|| { - variant_records + let max_distance: u32 = max(max_intrachromosomal_distance, max_interchromosomal_distance); + let positions: Vec = (0..n) + .map(|i| variant_records[i].get_position_1()) + .collect(); + let mut j: usize = 0; + let mut j_end: Vec = vec![n; n]; + for i in 0..n { + if j < i + 1 { + j = i + 1; + } + while j < n && positions[j].saturating_sub(positions[i]) <= max_distance { + j += 1; + } + j_end[i] = j; + } + let indices: Vec = (0..variant_records.len()).collect(); + let pairs_list: Vec<(u32, Vec)> = thread_pool.install(|| { + indices .par_iter() - .flat_map(|variant_record_1| { - let min_position: usize = variant_record_1.get_position_1().saturating_sub(max_distance) as usize; - let max_position: usize = (variant_record_1.get_position_1() + max_distance) as usize; - let variant_records_: Vec> = bst - .range(min_position..=max_position) - .flat_map(|(_, variant_records)| variant_records.iter().cloned()) - .collect(); - let mut curr_pairs: HashSet<(usize, usize)> = HashSet::new(); - for variant_record_2 in variant_records_.iter() { + .map(|&i| { + let mut mate_ids: Vec = Vec::new(); + let variant_record_i: &Arc = &variant_records[i]; + for j in (i + 1)..j_end[i] { + let variant_record_j: &Arc = &variant_records[j]; if is_clusterable( - variant_record_1, - variant_record_2, + variant_record_i.as_ref(), + variant_record_j.as_ref(), min_size_proportion, max_ins_norm_edit_distance, max_intrachromosomal_distance_tau, @@ -651,79 +1225,76 @@ pub fn sweep_clusters( max_interchromosomal_distance, stranded ) { - let variant_record_1_id: usize = *variant_records_map.get_by_left(variant_record_1).unwrap(); - let variant_record_2_id: usize = *variant_records_map.get_by_left(variant_record_2).unwrap(); - if variant_record_1_id < variant_record_2_id { - curr_pairs.insert((variant_record_1_id, variant_record_2_id)); - } else { - curr_pairs.insert((variant_record_2_id, variant_record_1_id)); - } + mate_ids.push(j as u32); } } - curr_pairs + (i as u32, mate_ids) }) .collect() }); - // Step 5. Identify clusters + // Step 4. Union-Find over indices let mut uf: UnionFind = UnionFind::new(); - for pair in pairs.iter() { - uf.union(pair.0, pair.1); + for (i, mate_ids) in pairs_list.iter() { + for j in mate_ids.iter() { + uf.union(*i, *j); + } } - // Step 6. Get clusters from UnionFind + // Step 5. Build clusters from Union-Find sets let mut clusters: Vec = Vec::new(); - let mut clustered_variant_records: HashSet> = HashSet::new(); - for variant_record_ids in uf.get_clusters() { - let mut curr_variant_records: HashSet> = HashSet::new(); - let mut min_position_1: usize = usize::MAX; - let mut min_position_2: usize = usize::MAX; - let mut max_position_1: usize = usize::MIN; - let mut max_position_2: usize = usize::MIN; - for variant_record_id in variant_record_ids.iter() { - let variant_record: &Arc = variant_records_map.get_by_right(variant_record_id).unwrap(); - curr_variant_records.insert(Arc::clone(variant_record)); - if (variant_record.get_position_1() as usize) < min_position_1 { - min_position_1 = variant_record.get_position_1() as usize; - } - if (variant_record.get_position_1() as usize) > max_position_1 { - max_position_1 = variant_record.get_position_1() as usize; - } - if (variant_record.get_position_2() as usize) < min_position_2 { - min_position_2 = variant_record.get_position_2() as usize; - } - if (variant_record.get_position_2() as usize) > max_position_2 { - max_position_2 = variant_record.get_position_2() as usize; - } + let mut in_cluster: Vec = vec![false; n]; + for ids in uf.get_clusters().iter() { + if ids.is_empty() { + continue; } - let mut cluster = VariantRecordCluster::new( + let mut cluster: VariantRecordCluster = VariantRecordCluster::new( chromosome_1, chromosome_2, - min_position_1 as usize, - max_position_1 as usize, - min_position_2 as usize, - max_position_2 as usize, + u32::MAX, + u32::MIN, + u32::MAX, + u32::MIN ); - for variant_record in curr_variant_records.iter() { - cluster.add_variant_record(Arc::clone(variant_record)); - clustered_variant_records.insert(Arc::clone(variant_record)); + + for &idx in ids.iter() { + let vr: Arc = Arc::clone(&variant_records[idx as usize]); + + // Update bounds + let p1: u32 = vr.get_position_1(); + let p2: u32 = vr.get_position_2(); + if p1 < cluster.min_position_1 { + cluster.min_position_1 = p1; + } + if p1 > cluster.max_position_1 { + cluster.max_position_1 = p1; + } + if p2 < cluster.min_position_2 { + cluster.min_position_2 = p2; + } + if p2 > cluster.max_position_2 { + cluster.max_position_2 = p2; + } + + cluster.variant_records.push(vr); + in_cluster[idx as usize] = true; } + clusters.push(cluster); } - // Step 7. If a variant record was not included in any cluster, include them - // as an independent VariantRecordCluster - for variant_record in variant_records.iter() { - if clustered_variant_records.contains(variant_record) == false { - let mut cluster: VariantRecordCluster = VariantRecordCluster::new( - variant_record.get_chromosome_1(), - variant_record.get_chromosome_2(), - variant_record.get_position_1(), - variant_record.get_position_1(), - variant_record.get_position_2(), - variant_record.get_position_2() + // Step 6. Add singleton clusters + for (idx, vr) in variant_records.iter().enumerate() { + if !in_cluster[idx] { + let mut cluster = VariantRecordCluster::new( + vr.get_chromosome_1(), + vr.get_chromosome_2(), + vr.get_position_1(), + vr.get_position_1(), + vr.get_position_2(), + vr.get_position_2() ); - cluster.add_variant_record(Arc::clone(variant_record)); + cluster.variant_records.push(Arc::clone(vr)); clusters.push(cluster); } } diff --git a/exacto/exacto-caller/src/algorithms/variant_calling_dna.rs b/exacto/exacto-caller/src/algorithms/variant_calling_dna.rs index 5583274..037de2a 100644 --- a/exacto/exacto-caller/src/algorithms/variant_calling_dna.rs +++ b/exacto/exacto-caller/src/algorithms/variant_calling_dna.rs @@ -14,26 +14,32 @@ use bimap::BiMap; use bincode; use exacto_core::prelude::*; -use indicatif::{ProgressBar, ProgressStyle}; +use exacto_core::log_info; use noodles_bam as bam; +use noodles_bam::bai; +use noodles_bam::bai::Index; +use noodles_bgzf::VirtualPosition; use noodles_fasta::io::indexed_reader::Builder; +use noodles_sam::Header; use rayon::prelude::*; +use rayon::ThreadPool; use std::cmp::max; use std::collections::{BTreeMap, HashMap, HashSet}; use std::env; use std::fs::File; -use std::io::{Read, Write}; +use std::io::{BufReader, BufWriter, Read, Write}; use std::path::{Path, PathBuf}; use std::mem; use std::sync::Arc; +use noodles_bgzf::io::{BufRead, Seek}; +use noodles_sam::alignment::Record; use sysinfo::System; -use tempfile::NamedTempFile; +use tempfile::{NamedTempFile, TempPath}; use crate::prelude::*; -use crate::log_info; -/// Identify DNA variants. +/// Identify germline DNA variants. /// /// # Parameters: /// @@ -49,45 +55,109 @@ use crate::log_info; /// * `max_interchromosomal_distance` is the maximum distance between two breakpoints for /// interchromosomal translocations. /// * `chromosomes` is a vector of chromosomes in which variants should be called. -pub fn identify_dna_variants( +pub fn identify_germline_dna_variants( bam_file: &str, bam_bai_file: &str, + fasta_file: &str, + regions: &Vec<(&str, u32, u32)>, min_reads: usize, - min_mapping_quality: usize, + min_mapping_quality: u16, min_base_quality: u8, + min_total_depth: u32, + min_alt_allele_fraction: f32, min_size_proportion: f32, max_ins_norm_edit_distance: f32, - max_intrachromosomal_distance_tau: usize, - max_intrachromosomal_distance: usize, - max_interchromosomal_distance: usize, + max_intrachromosomal_distance_tau: u32, + max_intrachromosomal_distance: u32, + max_interchromosomal_distance: u32, + max_slippage_repeat_length: u32, num_threads: usize, - chromosomes: Vec<&str>, + chunk_size: u32, + max_records: usize, + expected_variant_allele_fraction: f64, + expected_mutation_rate: f64, + expected_sequencing_error: f64, + expected_slippage_probability: f64, + max_f1_fraction: f64, + max_fpr: f64, temp_dir: &str ) -> DNAVariantCallSet { - // Step 1. Get read IDs map - log_info!("Converting read names to IDs."); - let read_names_map: BiMap,usize> = create_read_names_map( + // Step 1. Get all chromosome IDs and names + log_info!("Getting chromosomes."); + let chromosome_names_map: BiMap, u16> = create_chromosome_names_map(bam_file); + let chromosome_lengths: HashMap, u32> = get_chromosome_lengths(bam_file); + let chromosomes: Vec<&str> = chromosome_lengths + .keys() + .map(|k| k.as_ref()) + .collect(); + + // Step 2. Get sequencing depths + log_info!("Getting sequencing depths and strands."); + let depths_map: Arc, Vec>> = Arc::new(get_bam_depths_map(bam_file, num_threads)); + let strands_map: Arc, Vec<(u32, u32)>>> = Arc::new(get_bam_strands_map(bam_file, num_threads)); + let max_depth: u32 = depths_map + .values() + .flat_map(|v| v.iter()) + .copied() + .max() + .unwrap(); + log_info!("\tMax sequencing depth: {max_depth}."); + + // Step 3. Compute minimum read support index + log_info!("Computing minimum read support index."); + let min_read_support_index: Vec> = compute_min_read_support_index( + max_depth as u64, + max_slippage_repeat_length, + expected_variant_allele_fraction, + expected_mutation_rate, + expected_sequencing_error, + expected_slippage_probability, + max_f1_fraction, + max_fpr + ); + + // Step 4. Index BAM records by read names + log_info!("Indexing BAM records by read names."); + let (record_positions_map, read_names_map) = index_bam_records( bam_file, - bam_bai_file, num_threads ); - // Step 2. Get all chromosome IDs and names - let chromosome_names_map: BiMap,u16> = create_chromosome_names_map(bam_file); - let chromosome_lengths: HashMap,usize> = get_chromosome_lengths(bam_file); + // Step 5. Generate regions + log_info!("Generating genomic regions."); + let ordered_regions: BTreeMap, Vec<(u32, u32)>> = if regions.is_empty() { + let regions: HashMap, Vec<(u32, u32)>> = generate_regions( + &chromosomes, + &chromosome_lengths, + chunk_size + ); + let mut ordered_regions: BTreeMap, Vec<(u32, u32)>> = regions.into_iter().collect(); + for (_, vec) in ordered_regions.iter_mut() { + vec.sort_by(|a, b| a.0.cmp(&b.0)); + } + ordered_regions + } else { + let regions_split: Vec<(Box, u32, u32)> = split_regions(regions, chunk_size); + let mut ordered_regions: BTreeMap, Vec<(u32, u32)>> = regions_split + .iter() + .map(|(contig, start, end)| { + ( + contig.to_string().into_boxed_str(), + vec![(*start, *end)] + ) + }) + .collect(); + for vec in ordered_regions.values_mut() { + vec.sort_by_key(|(start, _)| *start); + } + ordered_regions + }; - // Step 3. Generate regions - let regions: HashMap,Vec<(usize,usize)>> = generate_regions( - bam_file, - &chromosomes, - *chromosome_lengths.values().max().unwrap() - ); - let mut ordered_regions: BTreeMap,Vec<(usize,usize)>> = regions.into_iter().collect(); - for (_, vec) in ordered_regions.iter_mut() { - vec.sort_by(|a, b| a.0.cmp(&b.0)); - } + // Step 6. Load the FASTA file + log_info!("Loading FASTA file."); + let fasta_map: FastaMap = FastaMap::new(fasta_file); - // Step 4. Fetch the temp directory + // Step 7. Fetch the temp directory let dir: String = if temp_dir.is_empty() { // Use TMPDIR environment variable or fallback to system temp directory env::var("TMPDIR").unwrap_or_else(|_| env::temp_dir().to_string_lossy().to_string()) @@ -99,115 +169,113 @@ pub fn identify_dna_variants( panic!("Directory does not exist: {}", dir); } - // Step 5. Identify variant calls - let mut temp_files: Vec = Vec::new(); - let padding: isize = 2 * std::cmp::max(max_interchromosomal_distance, std::cmp::max(max_intrachromosomal_distance,max_intrachromosomal_distance_tau)) as isize; + // Step 8. Identify variant calls + log_info!("Identifying DNA variants."); let thread_pool = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) .build() .unwrap(); - log_info!("Identifying DNA variants."); - let pb = Arc::new(ProgressBar::new(ordered_regions.len() as u64)); - pb.set_style( - ProgressStyle::default_bar() - .template("[{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})") - .unwrap() - .progress_chars("=>-") - ); let mut variant_call_idx: usize = 1; - for (chromosome,curr_regions) in &ordered_regions { - let mut variant_records_btree: BTreeMap> = BTreeMap::new(); - let mut prev_end: isize = 0; - for (start,end) in curr_regions.iter() { - log_info!("Identifying variant calls in {}:{}-{}.", chromosome, start, end); + let padding: u32 = 2 * max(max_interchromosomal_distance, max(max_intrachromosomal_distance, max_intrachromosomal_distance_tau)); + let mut reader = bam::io::reader::Builder::default() + .build_from_path(bam_file) + .unwrap(); + let header: Header = reader.read_header().unwrap(); + let index: Index = bai::fs::read(bam_bai_file).unwrap(); + let mut temp_files: Vec = Vec::new(); + for (chromosome, curr_regions) in &ordered_regions { + let chromosome_id: u16 = chromosome_names_map.get_by_left(chromosome).unwrap().clone(); + let mut chromosome_variant_records: Vec = Vec::new(); + let mut prev_end: u32 = 0; + for (start,end) in curr_regions { + log_info!("\tIdentifying variant calls in {}:{}-{}.", chromosome, start, end); // Fetch BAM records - let mut records_map: HashMap> = fetch_bam_records( - bam_file, - bam_bai_file, + let mut records_map: HashMap> = fetch_bam_records( + &mut reader, + &header, + &index, chromosome, *start, *end, + &record_positions_map, &read_names_map, + max_records, num_threads ); - + log_info!("\t\t{} BAM record(s) fetched.", records_map.len()); + // Identify variant records - log_info!("\tIdentifying variant records."); let mut variant_records: HashSet = thread_pool.install(|| { records_map - .par_iter() - .map(|(read_id, records)| { - let read_sequence: Box = get_fastx_read_sequence(records.iter().collect::>().as_slice()); - let quality_scores: Vec = get_fastx_base_quality_scores(records.iter().collect::>().as_slice()); + .into_par_iter() + .flat_map(|(read_id, records)| { + let read_sequence: Box = get_fastx_read_sequence(&records); + let quality_scores: Vec = get_fastx_base_quality_scores(&records); + let arc_records: Vec> = records + .into_iter() + .map(|r| Arc::new(r)) + .collect(); let alignment: Alignment = Alignment::new( - *read_id, + read_id, &*read_sequence, &quality_scores, - records + &arc_records ); - let variant_records: Vec = alignment + alignment .get_alignment_structure() .identify_variant_records( - min_mapping_quality, - min_base_quality, - AnalyteType::DNA - ); - variant_records + min_mapping_quality, + min_base_quality, + AnalyteType::DNA + ) }) - .flatten() .collect() }); - records_map.clear(); - records_map.shrink_to_fit(); - drop(records_map); - - // Filter by chromosome (allow inter-chromosomal translocations) - let chromosome_id: u16 = chromosome_names_map.get_by_left(chromosome).unwrap().clone(); - variant_records.retain(|vr| { - if vr.get_variant_type().clone() == VariantType::Translocation { - if vr.get_chromosome_1() == chromosome_id || vr.get_chromosome_2() == chromosome_id { - true - } else { - false - } - } else { - if vr.get_chromosome_1() == chromosome_id { - true - } else { - // intra-chromosomal variants in other chromosomes should not be called - false - } - } + // Filter variant records by chromosome (allow inter-chromosomal translocations) + variant_records = thread_pool.install(|| { + variant_records + .into_par_iter() + .filter(|vr| match vr.get_variant_type() { + VariantType::Translocation => { + vr.get_chromosome_1() == chromosome_id || vr.get_chromosome_2() == chromosome_id + }, + _ => vr.get_chromosome_1() == chromosome_id + }) + .collect() }); + log_info!("\t\t{} variant record(s) identified.", variant_records.len()); - // Add the variant records to the binary search tree - for variant_record in variant_records.iter() { - variant_records_btree - .entry(variant_record.get_position_1() as usize) - .or_insert_with(HashSet::new) - .insert(variant_record.clone()); - } + // Move into chromosome variant records + chromosome_variant_records.extend(variant_records.into_iter()); - // Keep VariantRecord objects whose position_1 is equal to or greater than (prev_end - padding) - prev_end = if prev_end - padding < 0 { 0 } else { prev_end - padding }; - variant_records_btree = variant_records_btree.split_off(&(prev_end as usize)); + // Keep sorted by position_1 + chromosome_variant_records.sort_unstable_by_key(|vr| vr.get_position_1()); - // Deduplicate variant records - variant_records_btree.values() - .flat_map(|curr_variant_records| curr_variant_records.iter()) - .for_each(|variant_record| { - variant_records.insert(variant_record.clone()); - }); + // Compute window indices [keep_from, end] + let keep_from: u32 = prev_end.saturating_sub(padding); + let keep_to: u32 = *end; - // Update prev_end - prev_end = *end as isize; + // Slice variant records for clustering + let start_idx: usize = chromosome_variant_records + .binary_search_by_key(&keep_from, |vr| vr.get_position_1()) + .unwrap_or_else(|idx| idx); + let end_idx: usize = chromosome_variant_records + .binary_search_by_key(&keep_to, |vr| vr.get_position_1()) + .map(|idx| idx + 1) + .unwrap_or_else(|idx| idx); + let slice_for_clustering = &chromosome_variant_records[start_idx..end_idx]; - // Identify variant calls - log_info!("\tClustering variant records into variant calls."); - let variant_calls: Vec = cluster_variant_records( - variant_records.into_iter().map(Arc::new).collect(), + // Cluster variant records into variant calls + let mut variant_calls: Vec = cluster_variant_records( + slice_for_clustering + .iter() + .cloned() + .map(Arc::new) + .collect(), + &depths_map, + &chromosome_names_map, num_threads, min_size_proportion, max_ins_norm_edit_distance, @@ -217,69 +285,132 @@ pub fn identify_dna_variants( false ); - // Filter variant calls by the minimum read count and then store - // them into a variant_call_set - log_info!("\tFiltering variant calls by the minimum read count."); - let mut variant_call_set: DNAVariantCallSet = DNAVariantCallSet::new(); + // Filter variant calls for the following: + // 1. Total depth + // 2. Alternate allele fraction + // 3. Variant read count + // 4. Strand bias + variant_calls = thread_pool.install(|| { + variant_calls + .into_par_iter() + .filter(|vc| { + // Total depth + if vc.get_total_depth() < min_total_depth as i32 { + return false; + } + + // Alternate allele fraction + if vc.get_alternate_allele_fraction() < min_alt_allele_fraction { + return false; + } + + // Read count + let curr_read_support: usize = vc.get_read_ids().len(); + if curr_read_support < min_reads { + return false; + } + let (vr, consensus_read_ids) = vc.get_consensus_record(); + let chromosome_1: &Box = chromosome_names_map.get_by_right(&vr.get_chromosome_1()).unwrap(); + let chromosome_2: &Box = chromosome_names_map.get_by_right(&vr.get_chromosome_2()).unwrap(); + let position_1_depth: u32 = depths_map[chromosome_1][(vr.get_position_1() - 1) as usize]; + let position_2_depth: u32 = depths_map[chromosome_2][(vr.get_position_2() - 1) as usize]; + let (is_repeat, repeat_length_) = is_repeat_indel(vr, &chromosome_names_map, &fasta_map); + let repeat_length: usize = repeat_length_.min(max_slippage_repeat_length) as usize; + + let min_read_support_1: usize = if position_1_depth == 0 { + usize::MAX + } else { + let repeat_idx: usize = if is_repeat { repeat_length } else { 0 }; + min_read_support_index[repeat_idx][position_1_depth as usize - 1] as usize + }; + let min_read_support_2: usize = if position_2_depth == 0 { + usize::MAX + } else { + let repeat_idx: usize = if is_repeat { repeat_length } else { 0 }; + min_read_support_index[repeat_idx][position_2_depth as usize - 1] as usize + }; + if curr_read_support < min_read_support_1 || curr_read_support < min_read_support_2 { + return false; + } + + // Strand bias — skip for variants whose alt support is intrinsically + // strand-asymmetric (breakpoints / translocations). For these, all reads + // spanning the breakend share the same strand pair *by construction*, so + // a one-sided strand distribution is the variant signature, not an + // artifact. The Fisher's-exact check is only meaningful for SNVs/indels. + match vr.get_variant_type() { + VariantType::Breakpoint | VariantType::Translocation => {} + _ => { + let mut alt_fwd_1: u64 = 0u64; + let mut alt_rev_1: u64 = 0u64; + let mut alt_fwd_2: u64 = 0u64; + let mut alt_rev_2: u64 = 0u64; + for vr in &vc.variant_records { + if *vr.get_strand_1() == Strand::Forward { + alt_fwd_1 += 1; + } else { + alt_rev_1 += 1; + } + if *vr.get_strand_2() == Strand::Forward { + alt_fwd_2 += 1; + } else { + alt_rev_2 += 1; + } + } + let (ref_fwd_1, ref_rev_1) = strands_map.get(chromosome_1).unwrap().get(vr.get_position_1() as usize - 1).unwrap(); + let (ref_fwd_2, ref_rev_2) = strands_map.get(chromosome_2).unwrap().get(vr.get_position_2() as usize - 1).unwrap(); + if has_strand_bias(alt_fwd_1, alt_rev_1, *ref_fwd_1 as u64, *ref_rev_1 as u64, 0.05f64) || + has_strand_bias(alt_fwd_2, alt_rev_2, *ref_fwd_2 as u64, *ref_rev_2 as u64, 0.05f64) { + return false; + } + } + } + + true + }) + .collect() + }); + log_info!("\t\t{} variant call(s) identified.", variant_calls.len()); + + // Aggregate variant calls into a variant callset + let mut variant_call_set = DNAVariantCallSet::new(); for mut variant_call in variant_calls { - if variant_call.get_consensus_record().1.len() >= min_reads { - // Rename the variant call ID - variant_call.id = variant_call_idx; - variant_call_set.add_variant_call(variant_call); - variant_call_idx += 1; - } + // Rename the variant call ID + variant_call.id = variant_call_idx; + variant_call_set.add_variant_call(variant_call); + variant_call_idx += 1; } // Store variant_call_set in a temp file - log_info!("\tStoring variant calls into a temp file."); - let mut temp_file = NamedTempFile::new_in(dir_path).unwrap(); - let mut encoded: Vec = bincode::serialize(&variant_call_set).expect("Failed to serialize data"); - temp_file.write_all(&encoded).unwrap(); - temp_file.flush().unwrap(); - temp_files.push(temp_file); - - encoded.clear(); - encoded.shrink_to_fit(); - drop(encoded); - variant_call_set.variant_calls.clear(); - variant_call_set.variant_calls.shrink_to_fit(); - drop(variant_call_set); + let temp_path: TempPath = { + let temp_file = NamedTempFile::new_in(dir_path).unwrap(); + let mut writer = BufWriter::new(temp_file); + bincode::serialize_into(&mut writer, &variant_call_set) + .expect("Failed to serialize variant_call_set"); + writer.flush().unwrap(); + let temp_file: NamedTempFile = writer.into_inner().unwrap(); + temp_file.into_temp_path() + }; + temp_files.push(temp_path); + + prev_end = keep_to; } - variant_records_btree.clear(); - drop(variant_records_btree); - pb.inc(1); + chromosome_variant_records.clear(); } - pb.finish_with_message("Completed identifying DNA variants."); - drop(thread_pool); - // Step 6. Load all VariantCallSet objects and merge them + // Step 8. Load all VariantCallSet objects and merge them log_info!("Loading all temp files and merging them into a variant call set."); - let mut variant_calls: HashSet = HashSet::new(); - for temp_file in temp_files.iter() { - let mut file = File::open(temp_file.path().to_str().unwrap()).unwrap(); - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer).unwrap(); - let variant_call_set_: DNAVariantCallSet = bincode::deserialize(&buffer).expect("Failed to deserialize data"); - buffer.clear(); - buffer.shrink_to_fit(); - for (_,variant_call) in variant_call_set_.variant_calls { - variant_calls.insert(variant_call); - } - } - let mut variant_call_set: DNAVariantCallSet = DNAVariantCallSet::new(); - let mut variant_call_id: usize = 1; - for mut variant_call in variant_calls { - variant_call.id = variant_call_id; - variant_call_set.add_variant_call(variant_call); - variant_call_id += 1; - } - variant_call_set.load_read_names(read_names_map); - variant_call_set.load_chromosome_names(chromosome_names_map); + let variant_call_set: DNAVariantCallSet = merge_temp_variant_call_sets( + &temp_files, + &read_names_map, + &chromosome_names_map, + num_threads + ); variant_call_set } -/// Identify case-specific DNA variants. +/// Identify somatic DNA variants. /// /// # Parameters: /// @@ -299,34 +430,50 @@ pub fn identify_dna_variants( /// * `chromosomes` is a vector of chromosomes in which variants should be called. /// * If `apply_infinite_sites_assumption` is true, any `a` variant record that shares a breakpoint /// (either position_1 or position_2) with any of the `b` variant record will be filtered out. -pub fn identify_case_specific_dna_variants( +pub fn identify_somatic_dna_variants( case_bam_file: &str, case_bam_bai_file: &str, control_bam_files: Vec<&str>, control_bam_bai_files: Vec<&str>, + fasta_file: &str, + regions: &Vec<(&str, u32, u32)>, min_reads: usize, - min_mapping_quality: usize, + min_mapping_quality: u16, min_base_quality: u8, + min_total_depth: u32, + min_alt_allele_fraction: f32, min_size_proportion: f32, max_ins_norm_edit_distance: f32, - max_intrachromosomal_distance_tau: usize, - max_intrachromosomal_distance: usize, - max_interchromosomal_distance: usize, - apply_infinite_sites_assumption: bool, + max_intrachromosomal_distance_tau: u32, + max_intrachromosomal_distance: u32, + max_interchromosomal_distance: u32, + max_slippage_repeat_length: u32, num_threads: usize, - chromosomes: Vec<&str>, + chunk_size: u32, + max_records: usize, + expected_variant_allele_fraction: f64, + expected_mutation_rate: f64, + expected_sequencing_error: f64, + expected_slippage_probability: f64, + max_f1_fraction: f64, + max_fpr: f64, + apply_infinite_sites_assumption: bool, temp_dir: &str ) -> DNAVariantCallSet { assert!(control_bam_files.len() == control_bam_bai_files.len()); // Step 1. Get all chromosome IDs and names - let chromosome_names_map: BiMap,u16> = create_chromosome_names_map(case_bam_file); - let chromosome_lengths: HashMap,usize> = get_chromosome_lengths(case_bam_file); + let chromosome_names_map: BiMap, u16> = create_chromosome_names_map(case_bam_file); + let chromosome_lengths: HashMap, u32> = get_chromosome_lengths(case_bam_file); + let chromosomes: Vec<&str> = chromosome_lengths + .keys() + .map(|k| k.as_ref()) + .collect(); - // Step 2. Make sure the chromosome IDs and names are the same + // Step 2. Make sure the chromosome IDs and names are the same in the control BAM files for control_bam_file in control_bam_files.iter() { - let chromosome_names_map_: BiMap,u16> = create_chromosome_names_map(control_bam_file); - let chromosome_lengths_: HashMap,usize> = get_chromosome_lengths(control_bam_file); + let chromosome_names_map_: BiMap, u16> = create_chromosome_names_map(control_bam_file); + let chromosome_lengths_: HashMap, u32> = get_chromosome_lengths(control_bam_file); for (chromosome_name,chromosome_id) in chromosome_names_map.iter() { if chromosome_names_map_.contains_left(chromosome_name) { let chromosome_id_: u16 = *chromosome_names_map_.get_by_left(chromosome_name).unwrap(); @@ -342,290 +489,585 @@ pub fn identify_case_specific_dna_variants( } } - // Step 3. Get read IDs maps - log_info!("Converting read names to IDs."); - let case_read_names_map: BiMap,usize> = create_read_names_map( + // Step 3. Get sequencing depths + log_info!("Getting sequencing depths and strands."); + let depths_map: Arc, Vec>> = Arc::new(get_bam_depths_map(case_bam_file, num_threads)); + let strands_map: Arc, Vec<(u32, u32)>>> = Arc::new(get_bam_strands_map(case_bam_file, num_threads)); + let max_depth: u32 = depths_map + .values() + .flat_map(|v| v.iter()) + .copied() + .max() + .unwrap(); + log_info!("\tMax sequencing depth: {max_depth}."); + + // Step 4. Compute minimum read support index + log_info!("Computing minimum read support index."); + let min_read_support_index: Vec> = compute_min_read_support_index( + max_depth as u64, + max_slippage_repeat_length, + expected_variant_allele_fraction, + expected_mutation_rate, + expected_sequencing_error, + expected_slippage_probability, + max_f1_fraction, + max_fpr + ); + + // Step 5. Index BAM records by read names + log_info!("Indexing BAM records by read names."); + let (case_record_positions_map, case_read_names_map) = index_bam_records( case_bam_file, - case_bam_bai_file, num_threads ); - let mut control_read_names_map: HashMap,BiMap,usize>> = HashMap::new(); + log_info!("{} read names/IDs in case BAM file.", case_read_names_map.len()); + let mut control_record_positions_map: HashMap, HashMap>> = HashMap::new(); + let mut control_read_names_map: HashMap, BiMap, usize>> = HashMap::new(); for (index,control_bam_file) in control_bam_files.iter().enumerate() { - let control_bam_bai_file: &str = control_bam_bai_files[index]; - let read_names_map: BiMap,usize> = create_read_names_map( + let (record_positions_map, read_names_map) = index_bam_records( control_bam_file, - control_bam_bai_file, num_threads ); + log_info!("{} read names/IDs in control BAM file {}/{}.", read_names_map.len(), index + 1, control_bam_files.len()); + control_record_positions_map.insert(control_bam_file.to_string().into_boxed_str(), record_positions_map); control_read_names_map.insert(control_bam_file.to_string().into_boxed_str(), read_names_map); } - // Step 4. Fetch the temp directory + // Step 6. Generate regions + log_info!("Generating genomic regions."); + let ordered_regions: BTreeMap, Vec<(u32, u32)>> = if regions.is_empty() { + let regions: HashMap, Vec<(u32, u32)>> = generate_regions( + &chromosomes, + &chromosome_lengths, + chunk_size + ); + let mut ordered_regions: BTreeMap, Vec<(u32, u32)>> = regions.into_iter().collect(); + for (_, vec) in ordered_regions.iter_mut() { + vec.sort_by(|a, b| a.0.cmp(&b.0)); + } + ordered_regions + } else { + let regions_split: Vec<(Box, u32, u32)> = split_regions(regions, chunk_size); + let mut ordered_regions: BTreeMap, Vec<(u32, u32)>> = regions_split + .iter() + .map(|(contig, start, end)| { + ( + contig.to_string().into_boxed_str(), + vec![(*start, *end)] + ) + }) + .collect(); + for vec in ordered_regions.values_mut() { + vec.sort_by_key(|(start, _)| *start); + } + ordered_regions + }; + + // Step 7. Load the FASTA file + log_info!("Loading FASTA file."); + let fasta_map: FastaMap = FastaMap::new(fasta_file); + + // Step 8. Fetch the temp directory let dir: String = if temp_dir.is_empty() { // Use TMPDIR environment variable or fallback to system temp directory env::var("TMPDIR").unwrap_or_else(|_| env::temp_dir().to_string_lossy().to_string()) } else { temp_dir.to_string() }; - let dir_path = Path::new(&dir); + let dir_path: &Path = Path::new(&dir); if !dir_path.exists() { panic!("Directory does not exist: {}", dir); } - // Step 5. Identify case-specific variant calls + // Step 9. Identify case-specific variant calls log_info!("Identifying case-specific DNA variants."); - let mut temp_files: Vec = Vec::new(); - let thread_pool = rayon::ThreadPoolBuilder::new() + let thread_pool: ThreadPool = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) .build() .unwrap(); - let mut max_distance: usize = max(max_intrachromosomal_distance_tau, max_intrachromosomal_distance); - max_distance = max(max_distance, max_interchromosomal_distance); - let bin_size: usize = 10_usize.pow((max_distance as f32).log10().floor() as u32 + 1); - let pb = Arc::new(ProgressBar::new(chromosomes.len() as u64)); - pb.set_style( - ProgressStyle::default_bar() - .template("[{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})") - .unwrap() - .progress_chars("=>-") - ); let mut variant_call_idx: usize = 1; - for chromosome in chromosomes.iter() { - let start: usize = 1; - let end: usize = *chromosome_lengths.get(&chromosome.to_string().into_boxed_str()).unwrap(); - let chromosome_id: u16 = chromosome_names_map.get_by_left(&chromosome.to_string().into_boxed_str()).unwrap().clone(); - - log_info!("Identifying variant calls in {}:{}-{}.", chromosome, 1, end); - - // Fetch case BAM records - let mut case_records_map: HashMap> = fetch_bam_records( - case_bam_file, - case_bam_bai_file, - chromosome, - start, - end, - &case_read_names_map, - num_threads - ); + let padding: u32 = 2 * max(max_interchromosomal_distance, max(max_intrachromosomal_distance, max_intrachromosomal_distance_tau)); + let mut case_reader = bam::io::reader::Builder::default() + .build_from_path(case_bam_file) + .unwrap(); + let mut max_distance: u32 = max(max_intrachromosomal_distance_tau, max_intrachromosomal_distance); + max_distance = max(max_distance, max_interchromosomal_distance); + let bin_size: u32 = 10_u32.pow((max_distance as f32).log10().floor() as u32 + 1); + let case_header: Header = case_reader.read_header().unwrap(); + let case_index: Index = bai::fs::read(case_bam_bai_file).unwrap(); + let mut control_readers = HashMap::new(); + let mut control_headers = HashMap::new(); + let mut control_indices = HashMap::new(); + for (i, control_bam_file) in control_bam_files.iter().enumerate() { + let control_bam_bai_file: &str = control_bam_bai_files[i]; + let mut reader = bam::io::reader::Builder::default() + .build_from_path(control_bam_file) + .unwrap(); + let header: Header = reader.read_header().unwrap(); + let index = bai::fs::read(control_bam_bai_file).unwrap(); + control_readers.insert(control_bam_file, reader); + control_headers.insert(control_bam_file, header); + control_indices.insert(control_bam_file, index); + } + let mut temp_files: Vec = Vec::new(); + for (chromosome, curr_regions) in &ordered_regions { + let chromosome_id: u16 = chromosome_names_map.get_by_left(chromosome).unwrap().clone(); + let mut chromosome_case_variant_records: Vec = Vec::new(); + let mut prev_end: u32 = 0; + for (start,end) in curr_regions { + log_info!("\tProcessing {}:{}-{}.", chromosome, start, end); - // Identify case variant records - log_info!("\tIdentifying case variant records."); - let mut case_variant_records_: HashSet = thread_pool.install(|| { - case_records_map - .par_iter() - .map(|(read_id, records)| { - let read_sequence: Box = get_fastx_read_sequence(records.iter().collect::>().as_slice()); - let quality_scores: Vec = get_fastx_base_quality_scores(records.iter().collect::>().as_slice()); - let alignment: Alignment = Alignment::new( - *read_id, - &*read_sequence, - &quality_scores, - records - ); - let variant_records: Vec = alignment - .get_alignment_structure() - .identify_variant_records( - min_mapping_quality, - min_base_quality, - AnalyteType::DNA - ); - variant_records - }) - .flatten() - .collect() - }); - case_records_map.clear(); - case_records_map.shrink_to_fit(); - drop(case_records_map); - - // Filter by chromosome (allow inter-chromosomal translocations) - case_variant_records_.retain(|vr| { - if vr.get_variant_type().clone() == VariantType::Translocation { - if vr.get_chromosome_1() == chromosome_id || vr.get_chromosome_2() == chromosome_id { - true - } else { - false - } - } else { - if vr.get_chromosome_1() == chromosome_id { - true - } else { - // intra-chromosomal variants in other chromosomes should not be called - false + // Fetch case BAM records + let mut case_records_map: HashMap> = fetch_bam_records( + &mut case_reader, + &case_header, + &case_index, + chromosome, + *start, + *end, + &case_record_positions_map, + &case_read_names_map, + max_records, + num_threads + ); + log_info!("\t\t{} BAM record(s) fetched.", case_records_map.len()); + + // Identify case variant records + let mut case_variant_records: HashSet = thread_pool.install(|| { + case_records_map + .into_par_iter() + .flat_map(|(read_id, records)| { + let read_sequence: Box = get_fastx_read_sequence(&records); + let quality_scores: Vec = get_fastx_base_quality_scores(&records); + let arc_records: Vec> = records + .into_iter() + .map(|r| Arc::new(r)) // move, not clone + .collect(); + let alignment: Alignment = Alignment::new( + read_id, + &*read_sequence, + &quality_scores, + &arc_records + ); + alignment + .get_alignment_structure() + .identify_variant_records( + min_mapping_quality, + min_base_quality, + AnalyteType::DNA + ) + }) + .collect() + }); + + // Filter case variant records by chromosome (allow inter-chromosomal translocations) + case_variant_records = thread_pool.install(|| { + case_variant_records + .into_par_iter() + .filter(|vr| match vr.get_variant_type() { + VariantType::Translocation => { + vr.get_chromosome_1() == chromosome_id || vr.get_chromosome_2() == chromosome_id + }, + _ => vr.get_chromosome_1() == chromosome_id + }) + .collect() + }); + log_info!("\t\t{} case variant record(s) identified.", case_variant_records.len()); + + // Move into chromosome variant records + chromosome_case_variant_records.extend(case_variant_records.into_iter()); + + // Keep sorted by position_1 + chromosome_case_variant_records.sort_unstable_by_key(|vr| vr.get_position_1()); + + // Compute window indices [keep_from, end] + let keep_from: u32 = prev_end.saturating_sub(padding); + let keep_to: u32 = *end; + + // Slice case variant records for clustering + let start_idx: usize = chromosome_case_variant_records + .binary_search_by_key(&keep_from, |vr| vr.get_position_1()) + .unwrap_or_else(|idx| idx); + let end_idx: usize = chromosome_case_variant_records + .binary_search_by_key(&keep_to, |vr| vr.get_position_1()) + .map(|idx| idx + 1) + .unwrap_or_else(|idx| idx); + let slice_for_clustering: &[VariantRecord] = &chromosome_case_variant_records[start_idx..end_idx]; + + // Cluster case variant records into variant calls + let mut variant_calls: Vec = cluster_variant_records( + slice_for_clustering + .iter() + .cloned() + .map(Arc::new) + .collect(), + &depths_map, + &chromosome_names_map, + num_threads, + min_size_proportion, + max_ins_norm_edit_distance, + max_intrachromosomal_distance_tau, + max_intrachromosomal_distance, + max_interchromosomal_distance, + false + ); + + // Filter variant calls for the following: + // 1. Total depth + // 2. Alternate allele fraction + // 3. Variant read count + // 4. Strand bias + variant_calls = thread_pool.install(|| { + variant_calls + .into_par_iter() + .filter(|vc| { + // Total depth + if vc.get_total_depth() < min_total_depth as i32 { + return false; + } + + // Alternate allele fraction + if vc.get_alternate_allele_fraction() < min_alt_allele_fraction { + return false; + } + + // Read count + let curr_read_support: usize = vc.get_read_ids().len(); + if curr_read_support < min_reads { + return false; + } + let (vr, consensus_read_ids) = vc.get_consensus_record(); + let chromosome_1: &Box = chromosome_names_map.get_by_right(&vr.get_chromosome_1()).unwrap(); + let chromosome_2: &Box = chromosome_names_map.get_by_right(&vr.get_chromosome_2()).unwrap(); + let position_1_depth: u32 = depths_map[chromosome_1][(vr.get_position_1() - 1) as usize]; + let position_2_depth: u32 = depths_map[chromosome_2][(vr.get_position_2() - 1) as usize]; + let (is_repeat, repeat_length_) = is_repeat_indel(vr, &chromosome_names_map, &fasta_map); + let repeat_length: usize = repeat_length_.min(max_slippage_repeat_length) as usize; + let min_read_support_1: usize = if position_1_depth == 0 { + let repeat_idx: usize = if is_repeat { repeat_length } else { 0 }; + min_read_support_index[repeat_idx][max_depth as usize - 1] as usize + } else { + let repeat_idx: usize = if is_repeat { repeat_length } else { 0 }; + min_read_support_index[repeat_idx][position_1_depth as usize - 1] as usize + }; + let min_read_support_2: usize = if position_2_depth == 0 { + let repeat_idx: usize = if is_repeat { repeat_length } else { 0 }; + min_read_support_index[repeat_idx][max_depth as usize - 1] as usize + } else { + let repeat_idx: usize = if is_repeat { repeat_length } else { 0 }; + min_read_support_index[repeat_idx][position_2_depth as usize - 1] as usize + }; + if curr_read_support < min_read_support_1 || curr_read_support < min_read_support_2 { + return false; + } + + // Strand bias — skip for variants whose alt support is intrinsically + // strand-asymmetric (breakpoints / translocations). For these, all reads + // spanning the breakend share the same strand pair *by construction*, so + // a one-sided strand distribution is the variant signature, not an + // artifact. The Fisher's-exact check is only meaningful for SNVs/indels. + match vr.get_variant_type() { + VariantType::Breakpoint | VariantType::Translocation => {} + _ => { + let mut alt_fwd_1: u64 = 0u64; + let mut alt_rev_1: u64 = 0u64; + let mut alt_fwd_2: u64 = 0u64; + let mut alt_rev_2: u64 = 0u64; + for vr in &vc.variant_records { + if *vr.get_strand_1() == Strand::Forward { + alt_fwd_1 += 1; + } else { + alt_rev_1 += 1; + } + if *vr.get_strand_2() == Strand::Forward { + alt_fwd_2 += 1; + } else { + alt_rev_2 += 1; + } + } + let (ref_fwd_1, ref_rev_1) = strands_map.get(chromosome_1).unwrap().get(vr.get_position_1() as usize - 1).unwrap(); + let (ref_fwd_2, ref_rev_2) = strands_map.get(chromosome_2).unwrap().get(vr.get_position_2() as usize - 1).unwrap(); + if has_strand_bias(alt_fwd_1, alt_rev_1, *ref_fwd_1 as u64, *ref_rev_1 as u64, 0.05f64) || + has_strand_bias(alt_fwd_2, alt_rev_2, *ref_fwd_2 as u64, *ref_rev_2 as u64, 0.05f64) { + return false; + } + } + } + + true + }) + .collect() + }); + log_info!("\t\t{} case variant call(s) identified.", variant_calls.len()); + + let mut case_variant_records_filtered: Vec> = Vec::new(); + for variant_call in variant_calls { + for variant_record in variant_call.variant_records { + case_variant_records_filtered.push(Arc::new(variant_record)); } } - }); - - // Cluster the case variant records - let variant_calls_: Vec = cluster_variant_records( - case_variant_records_.into_iter().map(Arc::new).collect(), - num_threads, - min_size_proportion, - max_ins_norm_edit_distance, - max_intrachromosomal_distance_tau, - max_intrachromosomal_distance, - max_interchromosomal_distance, - false - ); + log_info!("\t\t{} case variant record(s) retained after filtering.", case_variant_records_filtered.len()); - // Filter variant calls by the minimum read count - let mut case_variant_records: Vec> = Vec::new(); - for variant_call in variant_calls_ { - let (_, read_ids) = variant_call.get_consensus_record(); - if read_ids.len() >= min_reads { - for variant_record in variant_call.variant_records { - case_variant_records.push(Arc::new(variant_record)); + // Filter out variant records near control variant records + for (i, (control_bam_file, mut control_reader)) in control_readers.iter_mut().enumerate() { + if case_variant_records_filtered.is_empty() == false { + log_info!("\t\tProcessing control BAM file {}/{}.", i+1, control_bam_files.len()); + + // Fetch case BAM records + let mut control_records_map: HashMap> = fetch_bam_records( + &mut control_reader, + control_headers.get(control_bam_file).unwrap(), + control_indices.get(control_bam_file).unwrap(), + chromosome, + max(1u32, (*start).saturating_sub(padding)), + (*end).saturating_add(padding), + &control_record_positions_map.get(&control_bam_file.to_string().into_boxed_str()).unwrap(), + &control_read_names_map.get(&control_bam_file.to_string().into_boxed_str()).unwrap(), + max_records, + num_threads + ); + log_info!("\t\t\t{} BAM record(s) fetched.", control_records_map.len()); + + // Identify control variant records + let mut control_variant_records: HashSet = thread_pool.install(|| { + control_records_map + .into_par_iter() + .flat_map(|(read_id, records)| { + let read_sequence: Box = get_fastx_read_sequence(&records); + let quality_scores: Vec = get_fastx_base_quality_scores(&records); + let arc_records: Vec> = records + .into_iter() + .map(|r| Arc::new(r)) // move, not clone + .collect(); + let alignment: Alignment = Alignment::new( + read_id, + &*read_sequence, + &quality_scores, + &arc_records + ); + alignment + .get_alignment_structure() + .identify_variant_records( + min_mapping_quality, + min_base_quality, + AnalyteType::DNA + ) + }) + .collect() + }); + log_info!("\t\t\t{} control variant record(s) identified.", control_variant_records.len()); + + // Filter by chromosome (allow inter-chromosomal translocations) + control_variant_records.retain(|vr| match vr.get_variant_type() { + VariantType::Translocation => { + vr.get_chromosome_1() == chromosome_id || vr.get_chromosome_2() == chromosome_id + }, + _ => vr.get_chromosome_1() == chromosome_id + }); + + // Filter out control variants + case_variant_records_filtered = diff_variant_records( + case_variant_records_filtered, + control_variant_records.into_iter().map(Arc::new).collect(), + bin_size, + num_threads, + min_size_proportion, + max_ins_norm_edit_distance, + max_intrachromosomal_distance_tau, + max_intrachromosomal_distance, + max_interchromosomal_distance, + apply_infinite_sites_assumption, + false + ); + log_info!("\t\t\t{} case variant record(s) retained after diffing.", case_variant_records_filtered.len()); } } - } - // Filter out variant records near control variant records - for (i, control_bam_file) in control_bam_files.iter().enumerate() { - if case_variant_records.is_empty() == false { - // Fetch case BAM records - let control_bam_bai_file: &str = control_bam_bai_files[i]; - let mut control_records_map: HashMap> = fetch_bam_records( - control_bam_file, - control_bam_bai_file, - chromosome, - start, - end, - &control_read_names_map.get(&control_bam_file.to_string().into_boxed_str()).unwrap(), - num_threads - ); - - // Identify control variant records - log_info!("\tIdentifying control variant records."); - let mut control_variant_records: HashSet = thread_pool.install(|| { - control_records_map - .par_iter() - .map(|(read_id, records)| { - let read_sequence: Box = get_fastx_read_sequence(records.iter().collect::>().as_slice()); - let quality_scores: Vec = get_fastx_base_quality_scores(records.iter().collect::>().as_slice()); - let alignment: Alignment = Alignment::new( - *read_id, - &*read_sequence, - &quality_scores, - records - ); - let variant_records: Vec = alignment - .get_alignment_structure() - .identify_variant_records( - min_mapping_quality, - min_base_quality, - AnalyteType::DNA - ); - variant_records - }) - .flatten() - .collect() - }); - control_records_map.clear(); - control_records_map.shrink_to_fit(); - drop(control_records_map); - - // Filter by chromosome (allow inter-chromosomal translocations) - control_variant_records.retain(|vr| { - if vr.get_variant_type().clone() == VariantType::Translocation { - if vr.get_chromosome_1() == chromosome_id || vr.get_chromosome_2() == chromosome_id { - true - } else { - false + let mut variant_calls: Vec = cluster_variant_records( + case_variant_records_filtered, + &depths_map, + &chromosome_names_map, + num_threads, + min_size_proportion, + max_ins_norm_edit_distance, + max_intrachromosomal_distance_tau, + max_intrachromosomal_distance, + max_interchromosomal_distance, + false + ); + log_info!("\t\t\t{} case-specific variant call(s) identified.", variant_calls.len()); + + // Filter variant calls for the following: + // 1. Total depth + // 2. Alternate allele fraction + // 3. Variant read count + // 4. Strand bias + variant_calls = thread_pool.install(|| { + variant_calls + .into_par_iter() + .filter(|vc| { + // Total depth + if vc.get_total_depth() < min_total_depth as i32 { + return false; } - } else { - if vr.get_chromosome_1() == chromosome_id { - true + + // Alternate allele fraction + if vc.get_alternate_allele_fraction() < min_alt_allele_fraction { + return false; + } + + // Read count + let curr_read_support: usize = vc.get_read_ids().len(); + if curr_read_support < min_reads { + return false; + } + let (vr, consensus_read_ids) = vc.get_consensus_record(); + let chromosome_1: &Box = chromosome_names_map.get_by_right(&vr.get_chromosome_1()).unwrap(); + let chromosome_2: &Box = chromosome_names_map.get_by_right(&vr.get_chromosome_2()).unwrap(); + let position_1_depth: u32 = depths_map[chromosome_1][(vr.get_position_1() - 1) as usize]; + let position_2_depth: u32 = depths_map[chromosome_2][(vr.get_position_2() - 1) as usize]; + let (is_repeat, repeat_length_) = is_repeat_indel(vr, &chromosome_names_map, &fasta_map); + let repeat_length: usize = repeat_length_.min(max_slippage_repeat_length) as usize; + let min_read_support_1: usize = if position_1_depth == 0 { + let repeat_idx: usize = if is_repeat { repeat_length } else { 0 }; + min_read_support_index[repeat_idx][max_depth as usize - 1] as usize } else { - // intra-chromosomal variants in other chromosomes should not be called - false + let repeat_idx: usize = if is_repeat { repeat_length } else { 0 }; + min_read_support_index[repeat_idx][position_1_depth as usize - 1] as usize + }; + let min_read_support_2: usize = if position_2_depth == 0 { + let repeat_idx: usize = if is_repeat { repeat_length } else { 0 }; + min_read_support_index[repeat_idx][max_depth as usize - 1] as usize + } else { + let repeat_idx: usize = if is_repeat { repeat_length } else { 0 }; + min_read_support_index[repeat_idx][position_2_depth as usize - 1] as usize + }; + if curr_read_support < min_read_support_1 || curr_read_support < min_read_support_2 { + return false; } - } - }); - - // Filter out control variants - log_info!("\tDiffing control variant records."); - log_info!("\t{} case variant records", case_variant_records.len()); - log_info!("\t{} control variant records", control_variant_records.len()); - case_variant_records = diff_variant_records( - case_variant_records, - control_variant_records.into_iter().map(Arc::new).collect(), - bin_size, - num_threads, - min_size_proportion, - max_ins_norm_edit_distance, - max_intrachromosomal_distance_tau, - max_intrachromosomal_distance, - max_interchromosomal_distance, - apply_infinite_sites_assumption, - false - ); - } - } - log_info!("\tCluster variant records into variant calls."); - let variant_calls: Vec = cluster_variant_records( - case_variant_records, - num_threads, - min_size_proportion, - max_ins_norm_edit_distance, - max_intrachromosomal_distance_tau, - max_intrachromosomal_distance, - max_interchromosomal_distance, - false - ); + // Strand bias — skip for variants whose alt support is intrinsically + // strand-asymmetric (breakpoints / translocations). For these, all reads + // spanning the breakend share the same strand pair *by construction*, so + // a one-sided strand distribution is the variant signature, not an + // artifact. The Fisher's-exact check is only meaningful for SNVs/indels. + match vr.get_variant_type() { + VariantType::Breakpoint | VariantType::Translocation => {} + _ => { + let mut alt_fwd_1: u64 = 0u64; + let mut alt_rev_1: u64 = 0u64; + let mut alt_fwd_2: u64 = 0u64; + let mut alt_rev_2: u64 = 0u64; + for vr in &vc.variant_records { + if *vr.get_strand_1() == Strand::Forward { + alt_fwd_1 += 1; + } else { + alt_rev_1 += 1; + } + if *vr.get_strand_2() == Strand::Forward { + alt_fwd_2 += 1; + } else { + alt_rev_2 += 1; + } + } + let (ref_fwd_1, ref_rev_1) = strands_map.get(chromosome_1).unwrap().get(vr.get_position_1() as usize - 1).unwrap(); + let (ref_fwd_2, ref_rev_2) = strands_map.get(chromosome_2).unwrap().get(vr.get_position_2() as usize - 1).unwrap(); + if has_strand_bias(alt_fwd_1, alt_rev_1, *ref_fwd_1 as u64, *ref_rev_1 as u64, 0.05f64) || + has_strand_bias(alt_fwd_2, alt_rev_2, *ref_fwd_2 as u64, *ref_rev_2 as u64, 0.05f64) { + return false; + } + } + } + + true + }) + .collect() + }); - log_info!("\tAdd case-specific variant calls to the set."); - let mut variant_call_set: DNAVariantCallSet = DNAVariantCallSet::new(); - for mut variant_call in variant_calls { - if variant_call.get_consensus_record().1.len() >= min_reads { + let mut variant_call_set: DNAVariantCallSet = DNAVariantCallSet::new(); + for mut variant_call in variant_calls { // Rename the variant call ID variant_call.id = variant_call_idx; variant_call_set.add_variant_call(variant_call); variant_call_idx += 1; } - } - // Store variant_call_set in a temp file - log_info!("\tStoring variant calls into a temp file."); - let mut temp_file = NamedTempFile::new_in(dir_path).unwrap(); - let mut encoded: Vec = bincode::serialize(&variant_call_set).expect("Failed to serialize data"); - temp_file.write_all(&encoded).unwrap(); - temp_file.flush().unwrap(); - temp_files.push(temp_file); - - encoded.clear(); - encoded.shrink_to_fit(); - drop(encoded); - variant_call_set.variant_calls.clear(); - variant_call_set.variant_calls.shrink_to_fit(); - drop(variant_call_set); - pb.inc(1); - } - pb.finish_with_message("Completed identifying case-specific DNA variants."); + // Store variant_call_set in a temp file + let temp_path: TempPath = { + let temp_file = NamedTempFile::new_in(dir_path).unwrap(); + let mut writer = BufWriter::new(temp_file); + bincode::serialize_into(&mut writer, &variant_call_set) + .expect("Failed to serialize variant_call_set"); + writer.flush().unwrap(); + let temp_file: NamedTempFile = writer.into_inner().unwrap(); + temp_file.into_temp_path() + }; + temp_files.push(temp_path); + + prev_end = keep_to; - // Step 6. Load all VariantCallSet objects and merge them - log_info!("Loading all temp files and merging them into a variant call set."); - let mut variant_calls: HashSet = HashSet::new(); - for temp_file in temp_files.iter() { - let mut file = File::open(temp_file.path().to_str().unwrap()).unwrap(); - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer).unwrap(); - let variant_call_set_: DNAVariantCallSet = bincode::deserialize(&buffer).expect("Failed to deserialize data"); - buffer.clear(); - buffer.shrink_to_fit(); - for (_, variant_call) in variant_call_set_.variant_calls { - variant_calls.insert(variant_call); } + chromosome_case_variant_records.clear(); } + + // Step 10. Load all VariantCallSet objects and merge them + log_info!("Loading all temp files and merging them into a variant call set."); + let variant_call_set: DNAVariantCallSet = merge_temp_variant_call_sets( + &temp_files, + &case_read_names_map, + &chromosome_names_map, + num_threads + ); + + variant_call_set +} + +fn merge_temp_variant_call_sets( + temp_files: &[TempPath], + read_names_map: &BiMap, usize>, + chromosome_names_map: &BiMap, u16>, + num_threads: usize +) -> DNAVariantCallSet { + let thread_pool: ThreadPool = rayon::ThreadPoolBuilder::new() + .num_threads(num_threads) + .build() + .unwrap(); + + let variant_calls: HashSet = thread_pool.install(|| { + temp_files + .par_iter() + .map(|temp_path| { + let file = File::open(temp_path).unwrap(); + let mut reader = BufReader::new(file); + let set: DNAVariantCallSet = bincode::deserialize_from(&mut reader).expect("Failed to deserialize data"); + let mut local_variant_calls: HashSet = HashSet::with_capacity(set.variant_calls.len()); + for (_, vc) in set.variant_calls { + local_variant_calls.insert(vc); + } + local_variant_calls + }) + .reduce( + || HashSet::new(), + |mut acc, local| { + // Reduce rehashing during merge + acc.reserve(local.len()); + acc.extend(local); + acc + } + ) + }); + let mut variant_call_set: DNAVariantCallSet = DNAVariantCallSet::new(); + let mut variant_call_id: usize = 1; for mut variant_call in variant_calls { variant_call.id = variant_call_id; variant_call_set.add_variant_call(variant_call); variant_call_id += 1; } - variant_call_set.load_read_names(case_read_names_map); - variant_call_set.load_chromosome_names(chromosome_names_map); + + variant_call_set.load_read_names(read_names_map.clone()); + variant_call_set.load_chromosome_names(chromosome_names_map.clone()); variant_call_set } diff --git a/exacto/exacto-caller/src/algorithms/variant_calling_peptide.rs b/exacto/exacto-caller/src/algorithms/variant_calling_peptide.rs deleted file mode 100644 index b016989..0000000 --- a/exacto/exacto-caller/src/algorithms/variant_calling_peptide.rs +++ /dev/null @@ -1,486 +0,0 @@ -// // Licensed under the Apache License, Version 2.0 (the "License"); -// // you may not use this file except in compliance with the License. -// // You may obtain a copy of the License at -// // -// // http://www.apache.org/licenses/LICENSE-2.0 -// // -// // Unless required by applicable law or agreed to in writing, software -// // distributed under the License is distributed on an "AS IS" BASIS, -// // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// // See the License for the specific language governing permissions and -// // limitations under the License. -// -// -// use bio::io::bed; -// use exacto_util::prelude::*; -// use noodles_bam::{self as bam}; -// use polars::prelude::*; -// use rayon::prelude::*; -// use std::collections::{BTreeMap, HashMap, HashSet}; -// use std::str; -// use bimap::BiMap; -// -// use crate::common::bam::{create_read_names_map, fetch_bam_records}; -// use crate::log_info; -// use crate::structs::mutant_peptide::MutantPeptide; -// use crate::structs::mutant_peptides_set::MutantPeptidesSet; -// -// -// fn unpack_dna_variants( -// df: &mut DataFrame, -// bin_size: u32 -// ) -> (HashMap<(Box,u32),BTreeMap>>, -// HashMap<(Box,u32),BTreeMap>>, -// HashMap,(Box,Box)>) { -// let df_: DataFrame = df -// .with_column( -// df -// .column("variant_call_id") -// .unwrap() -// .cast(&DataType::String).unwrap() -// ) -// .unwrap() -// .clone(); -// let mut pos_1_tree_map: HashMap<(Box,u32),BTreeMap>> = HashMap::new(); -// let mut pos_2_tree_map: HashMap<(Box,u32),BTreeMap>> = HashMap::new(); -// let mut variants_map: HashMap,(Box,Box)> = HashMap::new(); -// let row_count = df_.height(); -// -// let variant_call_id_col = df_.column("variant_call_id").unwrap().str().unwrap(); -// let chromosome_1_col = df_.column("chromosome_1").unwrap().str().unwrap(); -// let position_1_col = df_.column("position_1").unwrap().i64().unwrap(); -// let strand_1_col = df_.column("strand_1").unwrap().str().unwrap(); -// let orientation_1_col = df_.column("orientation_1").unwrap().str().unwrap(); -// let chromosome_2_col = df_.column("chromosome_2").unwrap().str().unwrap(); -// let position_2_col = df_.column("position_2").unwrap().i64().unwrap(); -// let strand_2_col = df_.column("strand_2").unwrap().str().unwrap(); -// let orientation_2_col = df_.column("orientation_2").unwrap().str().unwrap(); -// let variant_type_col = df_.column("variant_type").unwrap().str().unwrap(); -// let variant_sequence_col = df_.column("variant_sequence").unwrap().str().unwrap(); -// let consensus_read_names_col = df_.column("consensus_read_names").unwrap().str().unwrap(); -// -// for row_idx in 0..row_count { -// let mut variant_sequence: Box = "".into(); -// let mut variant_sequence_length: usize = 0; -// if variant_sequence_col.get(row_idx).is_some() { -// variant_sequence = variant_sequence_col.get(row_idx).unwrap().to_string().into_boxed_str(); -// variant_sequence_length = variant_sequence.len(); -// } -// -// let variant = format!( -// "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}", -// chromosome_1_col.get(row_idx).unwrap(), -// position_1_col.get(row_idx).unwrap(), -// strand_1_col.get(row_idx).unwrap(), -// orientation_1_col.get(row_idx).unwrap(), -// chromosome_2_col.get(row_idx).unwrap(), -// position_2_col.get(row_idx).unwrap(), -// strand_2_col.get(row_idx).unwrap(), -// orientation_2_col.get(row_idx).unwrap(), -// variant_type_col.get(row_idx).unwrap(), -// variant_sequence, -// variant_sequence_length -// ); -// let variant_call_id: Box = variant_call_id_col.get(row_idx).unwrap().to_string().into_boxed_str(); -// let read_names: Box = consensus_read_names_col.get(row_idx).unwrap().to_string().into_boxed_str(); -// let chromosome_1: Box = chromosome_1_col.get(row_idx).unwrap().to_string().into_boxed_str(); -// let chromosome_2: Box = chromosome_2_col.get(row_idx).unwrap().to_string().into_boxed_str(); -// let position_1: u32 = position_1_col.get(row_idx).unwrap() as u32; -// let position_2: u32 = position_2_col.get(row_idx).unwrap() as u32; -// let zipcode_1: u32 = position_1 / bin_size; -// let zipcode_2: u32 = position_2 / bin_size; -// -// pos_1_tree_map -// .entry((chromosome_1.clone(),zipcode_1)) -// .or_insert_with(BTreeMap::new) -// .insert(position_1,variant_call_id.clone()); -// pos_2_tree_map -// .entry((chromosome_2.clone(),zipcode_2)) -// .or_insert_with(BTreeMap::new) -// .insert(position_2,variant_call_id.clone()); -// variants_map.insert(variant_call_id, (variant.into_boxed_str(),read_names)); -// } -// -// (pos_1_tree_map,pos_2_tree_map,variants_map) -// } -// -// fn unpack_rna_variants( -// df_rna_variants: &mut DataFrame -// ) -> (HashMap,Vec<(Box,Box)>>, -// HashMap,(Box,u32,Box,u32)>) { -// let df_rna_variants_: DataFrame = df_rna_variants -// .with_column( -// df_rna_variants -// .column("variant_call_id") -// .unwrap() -// .cast(&DataType::String).unwrap() -// ) -// .unwrap() -// .clone(); -// -// let mut rna_variant_call_ids_map: HashMap,(Box,u32,Box,u32)> = HashMap::new(); -// let mut rna_read_names_variants_map: HashMap,Vec<(Box,Box)>> = HashMap::new(); -// let row_count = df_rna_variants.height(); -// -// let variant_call_id_col = df_rna_variants_.column("variant_call_id").unwrap().str().unwrap(); -// let chromosome_1_col = df_rna_variants_.column("chromosome_1").unwrap().str().unwrap(); -// let position_1_col = df_rna_variants_.column("position_1").unwrap().i64().unwrap(); -// let strand_1_col = df_rna_variants_.column("strand_1").unwrap().str().unwrap(); -// let orientation_1_col = df_rna_variants_.column("orientation_1").unwrap().str().unwrap(); -// let chromosome_2_col = df_rna_variants_.column("chromosome_2").unwrap().str().unwrap(); -// let position_2_col = df_rna_variants_.column("position_2").unwrap().i64().unwrap(); -// let strand_2_col = df_rna_variants_.column("strand_2").unwrap().str().unwrap(); -// let orientation_2_col = df_rna_variants_.column("orientation_2").unwrap().str().unwrap(); -// let variant_type_col = df_rna_variants_.column("variant_type").unwrap().str().unwrap(); -// let variant_sequence_col = df_rna_variants_.column("variant_sequence").unwrap().str().unwrap(); -// let consensus_read_names_col = df_rna_variants_.column("consensus_read_names").unwrap().str().unwrap(); -// -// for row_idx in 0..row_count { -// let mut variant_sequence: Box = "".into(); -// let mut variant_sequence_length: usize = 0; -// if variant_sequence_col.get(row_idx).is_some() { -// variant_sequence = variant_sequence_col.get(row_idx).unwrap().to_string().into_boxed_str(); -// variant_sequence_length = variant_sequence.len(); -// } -// -// let rna_variant = format!( -// "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}", -// chromosome_1_col.get(row_idx).unwrap(), -// position_1_col.get(row_idx).unwrap(), -// strand_1_col.get(row_idx).unwrap(), -// orientation_1_col.get(row_idx).unwrap(), -// chromosome_2_col.get(row_idx).unwrap(), -// position_2_col.get(row_idx).unwrap(), -// strand_2_col.get(row_idx).unwrap(), -// orientation_2_col.get(row_idx).unwrap(), -// variant_type_col.get(row_idx).unwrap(), -// variant_sequence, -// variant_sequence_length -// ); -// -// let variant_call_id: Box = variant_call_id_col.get(row_idx).unwrap().to_string().into_boxed_str(); -// let chromosome_1: Box = chromosome_1_col.get(row_idx).unwrap().to_string().into_boxed_str(); -// let chromosome_2: Box = chromosome_2_col.get(row_idx).unwrap().to_string().into_boxed_str(); -// let position_1: u32 = position_1_col.get(row_idx).unwrap() as u32; -// let position_2: u32 = position_2_col.get(row_idx).unwrap() as u32; -// -// rna_variant_call_ids_map.insert(variant_call_id.clone(), (chromosome_1,position_1,chromosome_2,position_2)); -// -// if let Some(consensus_read_names) = consensus_read_names_col.get(row_idx) { -// for read_name in consensus_read_names.split(',') { -// rna_read_names_variants_map -// .entry(read_name.to_string().into_boxed_str()) -// .or_default() -// .push((variant_call_id.clone(), rna_variant.to_string().into_boxed_str())); -// } -// } -// } -// -// (rna_read_names_variants_map,rna_variant_call_ids_map) -// } -// -// /// Identify mutant k-mer sequences. -// /// -// /// Returns: -// /// * HashMap -// fn identify_mutant_kmer_sequences( -// fasta_file: &str, -// reference_fasta_file: &str, -// translations_tsv_file: &str, -// rna_variant_read_names: HashSet>, -// min_reads: usize, -// k: usize, -// num_threads: usize -// ) -> HashMap,(Box,Vec,HashSet>)> { -// let thread_pool = rayon::ThreadPoolBuilder::new() -// .num_threads(num_threads) -// .build() -// .unwrap(); -// -// // Step 1: Read translations and filter peptides with enough reads -// let mut df_translations: DataFrame = read_tsv_file(translations_tsv_file); -// df_translations = df_translations -// .with_column( -// df_translations -// .column("peptide_id") -// .unwrap() -// .cast(&DataType::String).unwrap() -// ) -// .unwrap() -// .clone(); -// let df_read_names = DataFrame::new(vec![ -// Column::from(Series::new("read_name".into(), rna_variant_read_names.iter().map(|s| s.as_ref()).collect::>())) -// ]).unwrap(); -// let supported_peptide_ids: HashSet> = df_translations -// .clone() -// .lazy() -// .join( -// df_read_names.lazy(), -// [col("read_name")], -// [col("read_name")], -// JoinArgs::new(JoinType::Inner) -// ) -// .group_by([col("peptide_id")]) -// .agg([col("read_name").n_unique().alias("unique_read_count")]) -// .filter(col("unique_read_count").gt_eq(lit(min_reads as i64))) -// .collect() -// .unwrap() -// .column("peptide_id") -// .unwrap() -// .str() -// .unwrap() -// .into_iter() -// .filter_map(|v| v.map(|id| id.to_string().into_boxed_str())) -// .collect(); -// -// // Step 2: Load and filter peptide sequences -// let mut peptide_sequences: HashMap,Box> = read_fasta_file(fasta_file).into_iter().collect(); -// peptide_sequences.retain(|key, _| supported_peptide_ids.contains(key)); -// let reference_peptide_sequences: Vec<(Box,Box)> = read_fasta_file(reference_fasta_file); -// -// // Step 3. Identify k-mers -// let query_kmers: HashSet> = thread_pool.install(|| { -// peptide_sequences -// .par_iter() -// .flat_map(|(id,sequence)| { -// if sequence.len() >= k { -// find_kmers(&**sequence, k) -// .into_keys() -// .collect::>() -// } else { -// Vec::new() -// } -// }) -// .collect() -// }); -// let reference_kmers: HashSet> = thread_pool.install(|| { -// reference_peptide_sequences -// .par_iter() -// .flat_map(|(id,sequence)| { -// if sequence.len() >= k { -// find_kmers(&**sequence, k) -// .into_keys() -// .collect::>() -// } else { -// Vec::new() -// } -// }) -// .collect() -// }); -// let unique_kmers: HashSet> = query_kmers.difference(&reference_kmers).cloned().collect(); -// -// // Step 4: Identify mutant peptides -// let mutant_peptides: Vec<(Box, Box, Vec)> = thread_pool.install(|| { -// peptide_sequences -// .par_iter() -// .filter_map(|(peptide_id, sequence)| { -// if sequence.len() >= k { -// let kmer_mask: Vec = (0..=sequence.len() - k) -// .map(|i| { -// let kmer: Box = sequence[i..i + k].to_string().into_boxed_str(); -// unique_kmers.contains(&kmer) -// }) -// .collect(); -// if kmer_mask.iter().any(|&is_unique| is_unique) { -// Some((peptide_id.clone(), sequence.clone(), kmer_mask)) -// } else { -// None -// } -// } else { -// None -// } -// }) -// .collect() -// }); -// -// // Step 5: Create mutant peptide map -// mutant_peptides -// .into_iter() -// .map(|(peptide_id, sequence, kmer_mask)| { -// let rna_read_names: HashSet> = df_translations -// .clone() -// .lazy() -// .filter(col("peptide_id").eq(lit(&*peptide_id))) -// .collect() -// .unwrap() -// .column("read_name") -// .unwrap() -// .str() -// .unwrap() -// .into_iter() -// .filter_map(|v| v.map(|s| s.to_string().into_boxed_str())) -// .collect(); -// (peptide_id, (sequence, kmer_mask, rna_read_names)) -// }) -// .collect() -// } -// -// pub fn identify_peptide_variants( -// fasta_file: &str, -// rna_bam_file: &str, -// rna_bam_bai_file: &str, -// reference_fasta_file: &str, -// translations_tsv_file: &str, -// rna_variants_tsv_file: &str, -// dna_variants_tsv_file: &str, -// exclude_bed_file: &str, -// min_reads: usize, -// k: usize, -// num_threads: usize, -// dna_variant_padding: usize -// ) -> MutantPeptidesSet { -// let dna_variant_padding_: u32 = dna_variant_padding as u32; -// -// // Step 1. Identify read names to exclude based on excluded BED file -// let mut excluded_rna_read_names: HashSet> = HashSet::new(); -// if exclude_bed_file.is_empty() == false { -// let read_names_map: BiMap,usize> = create_read_names_map( -// rna_bam_file, -// rna_bam_bai_file, -// num_threads -// ); -// let bed_records: Vec = read_bed_file(exclude_bed_file); -// for bed_record in bed_records.iter() { -// let mut records_map: HashMap> = fetch_bam_records( -// rna_bam_file, -// rna_bam_bai_file, -// bed_record.chrom(), -// bed_record.start() as usize, -// bed_record.end() as usize, -// &read_names_map, -// num_threads -// ); -// for read_id in records_map.keys() { -// let read_name: Box = read_names_map.get_by_right(read_id).unwrap().clone(); -// excluded_rna_read_names.insert(read_name); -// } -// } -// } -// -// // Step 2. Load RNA and DNA variants -// log_info!("Reading the DNA variants"); -// let mut df_rna_variants: DataFrame = read_tsv_file(rna_variants_tsv_file); -// log_info!("Reading the RNA variants"); -// let mut df_dna_variants: DataFrame = read_tsv_file(dna_variants_tsv_file); -// -// // Step 3. Unpack RNA variants -// log_info!("Unpacking the RNA variants"); -// let (rna_read_names_variants_map,rna_variant_call_ids_map) = unpack_rna_variants(&mut df_rna_variants); -// -// // Step 4. Build binary trees of the DNA variants -// log_info!("Unpacking the DNA variants"); -// let bin_size: u32 = 10000; -// let (dna_pos_1_map, dna_pos_2_map, dna_variants_map) = unpack_dna_variants(&mut df_dna_variants, bin_size); -// -// // Step 5. Identify mutant k-mer sequences -// log_info!("Identifying mutant {}-mer sequences", k); -// let mut mutant_peptides_map: HashMap,(Box,Vec,HashSet>)> = identify_mutant_kmer_sequences( -// fasta_file, -// reference_fasta_file, -// translations_tsv_file, -// rna_read_names_variants_map.keys().cloned().collect(), -// min_reads, -// k, -// num_threads -// ); -// log_info!("{} mutant peptide sequences with at least 1 unique {}-mers", mutant_peptides_map.len(), k); -// -// // Step 6. Identify mutant peptides -// log_info!("Identifying mutant peptides"); -// let thread_pool = rayon::ThreadPoolBuilder::new() -// .num_threads(num_threads) -// .build() -// .unwrap(); -// let mutant_peptides: Vec = thread_pool.install(|| { -// mutant_peptides_map -// .par_iter() -// .filter_map(|(peptide_id, (peptide_sequence, kmer_mask, rna_read_names))| { -// // Make sure none of the RNA read names is in the excluded RNA read names -// for rna_read_name in rna_read_names.iter() { -// if excluded_rna_read_names.contains(rna_read_name) { -// return None; -// } -// } -// -// // Get RNA variants -// let mut rna_variant_call_ids: HashSet> = HashSet::new(); -// let mut rna_variants_read_names_map: HashMap<(Box,Box), usize> = HashMap::new(); -// for rna_read_name in rna_read_names.iter() { -// if let Some(variants) = rna_read_names_variants_map.get(rna_read_name) { -// for (variant_call_id, rna_variant) in variants { -// *rna_variants_read_names_map -// .entry((variant_call_id.clone(), rna_variant.clone())) -// .or_insert(0) += 1; -// } -// } -// } -// let mut rna_variants = Vec::new(); -// let mut rna_variants_consensus_read_names = Vec::new(); -// for ((variant_call_id, rna_variant), count) in rna_variants_read_names_map.iter() { -// if *count == rna_read_names.len() { -// rna_variants.push(rna_variant.clone()); -// rna_variants_consensus_read_names.push( -// rna_read_names -// .iter() -// .map(|s| s.as_ref()) -// .collect::>() -// .join(",") -// .into_boxed_str(), -// ); -// rna_variant_call_ids.insert(variant_call_id.clone()); -// } -// } -// -// // Get DNA variants -// let mut dna_variants = Vec::new(); -// let mut dna_variants_consensus_read_names = Vec::new(); -// for rna_variant_call_id in rna_variant_call_ids.iter() { -// let (chromosome_1,position_1,chromosome_2,position_2) = rna_variant_call_ids_map.get(rna_variant_call_id).unwrap(); -// let zipcode_1: u32 = *position_1 / bin_size; -// let zipcode_2: u32 = *position_2 / bin_size; -// let min_pos_1: u32 = if *position_1 >= dna_variant_padding_ { *position_1 - dna_variant_padding_ } else { 0 }; -// let max_pos_1: u32 = *position_1 + dna_variant_padding_; -// let min_pos_2: u32 = if *position_2 >= dna_variant_padding_ { *position_2 - dna_variant_padding_ } else { 0 }; -// let max_pos_2: u32 = *position_2 + dna_variant_padding_; -// let mut dna_variant_call_ids: HashSet> = HashSet::new(); -// if dna_pos_1_map.contains_key(&(chromosome_1.to_string().into(),zipcode_1)) { -// for (_,dna_variant_call_id) in dna_pos_1_map.get(&(chromosome_1.to_string().into(),zipcode_1)).unwrap().range(min_pos_1..=max_pos_1) { -// dna_variant_call_ids.insert(dna_variant_call_id.clone()); -// } -// } -// if dna_pos_2_map.contains_key(&(chromosome_2.to_string().into(),zipcode_2)) { -// for (_,dna_variant_call_id) in dna_pos_2_map.get(&(chromosome_2.to_string().into(),zipcode_2)).unwrap().range(min_pos_2..=max_pos_2) { -// dna_variant_call_ids.insert(dna_variant_call_id.clone()); -// } -// } -// for dna_variant_call_id in dna_variant_call_ids.iter() { -// dna_variants.push(dna_variants_map.get(dna_variant_call_id).unwrap().0.clone()); -// dna_variants_consensus_read_names.push(dna_variants_map.get(dna_variant_call_id).unwrap().1.clone()); -// } -// } -// if !rna_variants.is_empty() && !dna_variants.is_empty() { -// Some(MutantPeptide::new( -// peptide_id.clone(), -// peptide_sequence.clone(), -// rna_read_names.iter().cloned().collect(), -// k, -// kmer_mask.clone(), -// rna_variants, -// rna_variants_consensus_read_names, -// dna_variants, -// dna_variants_consensus_read_names, -// )) -// } else { -// None -// } -// }) -// .collect::>() -// }); -// -// log_info!("Preparing a mutant peptides set"); -// let mut mutant_peptides_set: MutantPeptidesSet = MutantPeptidesSet::new(); -// for mutant_peptide in mutant_peptides { -// mutant_peptides_set.add_mutant_peptide(mutant_peptide); -// } -// mutant_peptides_set -// } diff --git a/exacto/exacto-caller/src/algorithms/variant_calling_rna.rs b/exacto/exacto-caller/src/algorithms/variant_calling_rna.rs index 848f2c4..bdcb2e2 100644 --- a/exacto/exacto-caller/src/algorithms/variant_calling_rna.rs +++ b/exacto/exacto-caller/src/algorithms/variant_calling_rna.rs @@ -13,55 +13,22 @@ use bimap::BiMap; use exacto_core::prelude::*; +use exacto_core::log_info; use indicatif::{ProgressBar, ProgressStyle}; use noodles_bam as bam; -use rayon::prelude::*; use rayon::iter::IntoParallelRefIterator; +use rayon::prelude::*; +use rayon::ThreadPool; use std::collections::{HashMap, HashSet}; +use std::env; +use std::fs::File; +use std::io::{BufReader, BufWriter, Write}; +use std::path::Path; use std::sync::Arc; +use tempfile::{NamedTempFile, TempPath}; use crate::prelude::*; -use crate::log_info; -use crate::macros::*; - - -// pub fn map_variant_records_to_reference_transcripts( -// variant_records: Vec<&VariantRecord>, -// reference_transcript_matches: Vec<&ReferenceTranscriptMatch>, -// chromosome_names_map: &BiMap,u16>, -// gene_annotator: &(impl GeneAnnotator + Sync) -// ) -> HashMap, Vec> { -// let mut variant_records_map: HashMap, Vec> = HashMap::new(); -// for variant_record in variant_records { -// let chromosome_1 = chromosome_names_map.get_by_right(&variant_record.get_chromosome_1()).unwrap().clone(); -// let chromosome_2 = chromosome_names_map.get_by_right(&variant_record.get_chromosome_2()).unwrap().clone(); -// let position_1: isize = variant_record.get_position_1() as isize; -// let position_2: isize = variant_record.get_position_2() as isize; -// let mut matched: bool = false; -// for reference_transcript_match in reference_transcript_matches.iter() { -// let reference_transcript: &Transcript = gene_annotator.get_transcript(&reference_transcript_match.reference_transcript_id).unwrap(); -// let reference_transcript_chromosome: Box = reference_transcript.chromosome.clone(); -// let reference_transcript_start: isize = reference_transcript.start as isize; -// let reference_transcript_end: isize = reference_transcript.end as isize; -// if (reference_transcript_chromosome == chromosome_1 && overlaps(position_1, position_1, reference_transcript_start, reference_transcript_end)) || -// (reference_transcript_chromosome == chromosome_2 && overlaps(position_2, position_2, reference_transcript_start, reference_transcript_end)) { -// variant_records_map -// .entry(reference_transcript_match.reference_transcript_id.clone()) -// .or_insert_with(Vec::new) -// .push(variant_record.clone()); -// matched = true; -// } -// } -// if matched == false { -// variant_records_map -// .entry(GenicRegion::Intergenic.as_str().to_string().into()) -// .or_insert_with(Vec::new) -// .push(variant_record.clone()); -// } -// } -// -// variant_records_map -// } + pub fn identify_variant_transcripts( bam_file: &str, @@ -72,120 +39,190 @@ pub fn identify_variant_transcripts( reference_transcript_selection_strategy: ReferenceTranscriptSelectionStrategy, top_k: usize, threshold: f32, - min_mapping_quality: usize, + min_mapping_quality: u16, min_base_quality: u8, - num_threads: usize + num_threads: usize, + chunk_size: usize, + temp_dir: &str ) -> TranscriptModelSet { // Step 1. Get a map of read names and IDs - let read_names_map: BiMap,usize> = create_read_names_map( + let read_names_map: BiMap, usize> = create_read_names_map( bam_file, bam_bai_file, num_threads ); // Step 2. Get chromosome names map - let chromosome_names_map: BiMap,u16> = create_chromosome_names_map(bam_file); + let chromosome_names_map: BiMap, u16> = create_chromosome_names_map(bam_file); // Step 3. Fetch all BAM records log_info!("Fetching all BAM records"); - let records_map: HashMap> = fetch_all_bam_records( + let records_map: HashMap> = fetch_all_bam_records( bam_file, bam_bai_file, &read_names_map, num_threads ); - // Step 4. Construct transcript models + // Step 4. Fetch the temp directory + let dir: String = if temp_dir.is_empty() { + // Use TMPDIR environment variable or fallback to system temp directory + env::var("TMPDIR").unwrap_or_else(|_| env::temp_dir().to_string_lossy().to_string()) + } else { + temp_dir.to_string() + }; + let dir_path = Path::new(&dir); + if !dir_path.exists() { + panic!("Directory does not exist: {}", dir); + } + let mut temp_files: Vec = Vec::new(); + + // Step 5. Construct transcript models log_info!("Constructing transcript models"); - let pb = Arc::new(ProgressBar::new(records_map.len() as u64)); - pb.set_style( - ProgressStyle::default_bar() - .template("[{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})") - .unwrap() - .progress_chars("=>-") - ); - let thread_pool = rayon::ThreadPoolBuilder::new() + let thread_pool: ThreadPool = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) .build() .unwrap(); - let transcript_models: Vec = thread_pool.install(|| { - records_map - .par_iter() - .filter_map(|(read_id, records)| { - let result = { - // Get read original sequence - let read_sequence: Box = get_fastx_read_sequence(records.iter().collect::>().as_slice()); - - // Get base quality scores - let base_quality_scores: Vec = get_fastx_base_quality_scores(records.iter().collect::>().as_slice()); - - // Construct an Alignment object - let alignment: Alignment = Alignment::new( - *read_id, - &*read_sequence, - &base_quality_scores, - records, - ); - - // Check if the maximum mapping quality score is above the minimum mapping quality - let mut max_mapping_quality = 0usize; - for alignment_record in alignment.get_alignment_records().iter() { - let mapping_quality: usize = alignment_record.record.mapping_quality().unwrap().get() as usize; - max_mapping_quality = max_mapping_quality.max(mapping_quality); - } - if min_mapping_quality > max_mapping_quality { - return None; + let entries: Vec<(usize, Vec)> = records_map.into_iter().collect(); + log_info!("{} transcript models to process", entries.len()); + for chunk in entries.chunks(chunk_size) { + let transcript_models: Vec = thread_pool.install(|| { + chunk + .into_par_iter() + .filter_map(|(read_id, records)| { + let read_name = read_names_map.get_by_right(read_id).unwrap(); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + (|| -> Result, Box> { + let read_sequence: Box = get_fastx_read_sequence(records); + let base_quality_scores: Vec = get_fastx_base_quality_scores(records); + + let alignment: Alignment = Alignment::new( + *read_id, + &*read_sequence, + &base_quality_scores, + &records.iter().map(|record| Arc::new(record.clone())).collect() + ); + + let max_mapping_quality = alignment.get_alignment_records().iter() + .map(|r| r.record.mapping_quality().unwrap().get() as u16) + .max() + .unwrap_or(0); + + if min_mapping_quality > max_mapping_quality { + return Ok(None); + } + + let mut transcript_model = TranscriptModel::new( + 1, + read_name, + alignment.get_alignment_structure(), + &chromosome_names_map, + reference_genome_fasta_file + ); + + let reference_transcript_matches: Vec = identify_reference_transcript_matches( + transcript_model.get_exons(), + gene_annotator, + &chromosome_names_map, + reference_transcript_scoring_method.clone(), + reference_transcript_selection_strategy.clone(), + top_k, + threshold + ); + + transcript_model.identify_variants( + &**read_name, + &reference_transcript_matches, + gene_annotator, + reference_genome_fasta_file, + min_mapping_quality, + min_base_quality + ); + + Ok(Some(transcript_model)) + })() + })); + + match result { + Ok(Ok(maybe_model)) => maybe_model, + Ok(Err(e)) => { + eprintln!("Error processing read name {}: {}", read_name, e); + None + } + Err(_panic) => { + eprintln!("Panic while processing read name {}", read_name); + None + } } + }) + .collect() + }); + + log_info!("\t{} transcript model(s) constructed in chunk.", transcript_models.len()); + + // Serialize chunk to temp file + let temp_path: TempPath = { + let temp_file = NamedTempFile::new_in(dir_path).unwrap(); + let mut writer = BufWriter::new(temp_file); + bincode::serialize_into(&mut writer, &transcript_models) + .expect("Failed to serialize transcript models"); + writer.flush().unwrap(); + let temp_file: NamedTempFile = writer.into_inner().unwrap(); + temp_file.into_temp_path() + }; + temp_files.push(temp_path); + } - // Construct a TranscriptModel object - let mut transcript_model: TranscriptModel = TranscriptModel::new( - 1, - alignment.get_alignment_structure(), - &chromosome_names_map, - reference_genome_fasta_file - ); - - // Identify reference transcript matches - let reference_transcript_matches: Vec = identify_reference_transcript_matches( - transcript_model.get_exons(), - gene_annotator, - &chromosome_names_map, - reference_transcript_scoring_method.clone(), - reference_transcript_selection_strategy.clone(), - top_k, - threshold - ); - - // Identify variants - transcript_model.identify_variants( - &reference_transcript_matches, - gene_annotator, - reference_genome_fasta_file, - min_mapping_quality, - min_base_quality - ); - - Some(transcript_model) - }; - pb.inc(1); - result + // Step 6. Load temp files and merge into TranscriptModelSet + log_info!("Loading temp files and merging into a transcript model set."); + let transcript_model_set: TranscriptModelSet = merge_temp_transcript_model_sets( + &temp_files, + &read_names_map, + &chromosome_names_map, + num_threads + ); + + transcript_model_set +} + +fn merge_temp_transcript_model_sets( + temp_files: &[TempPath], + read_names_map: &BiMap, usize>, + chromosome_names_map: &BiMap, u16>, + num_threads: usize +) -> TranscriptModelSet { + let thread_pool: ThreadPool = rayon::ThreadPoolBuilder::new() + .num_threads(num_threads) + .build() + .unwrap(); + + let all_models: Vec> = thread_pool.install(|| { + temp_files + .par_iter() + .map(|temp_path| { + let file = File::open(temp_path).unwrap(); + let reader = BufReader::new(file); + bincode::deserialize_from(reader) + .expect("Failed to deserialize transcript models") }) .collect() }); - pb.finish_with_message("Completed constructing transcript models."); - let mut transcript_model_set: TranscriptModelSet = TranscriptModelSet::new(); + let mut transcript_model_set = TranscriptModelSet::new(); let mut transcript_id: usize = 1; - for mut transcript_model in transcript_models { - // Check if the transcript model is a reference transcript) - if transcript_model.is_reference_transcript() == false { - transcript_model.set_transcript_id(transcript_id); - transcript_model_set.add_transcript_model(transcript_model); - transcript_id += 1; + for models in all_models { + for mut transcript_model in models { + if !transcript_model.is_reference_transcript() { + transcript_model.set_transcript_id(transcript_id); + transcript_model_set.add_transcript_model(transcript_model); + transcript_id += 1; + } } } - transcript_model_set.load_read_names(read_names_map); - transcript_model_set.load_chromosome_names(chromosome_names_map); + + transcript_model_set.load_read_names(read_names_map.clone()); + transcript_model_set.load_chromosome_names(chromosome_names_map.clone()); transcript_model_set -} +} \ No newline at end of file diff --git a/exacto/exacto-caller/src/common/constants.rs b/exacto/exacto-caller/src/common/constants.rs index dc0f6f7..1dadf85 100644 --- a/exacto/exacto-caller/src/common/constants.rs +++ b/exacto/exacto-caller/src/common/constants.rs @@ -109,7 +109,8 @@ pub enum AlignmentStructureBaseKind { Match, Mismatch, Insertion, - Unaligned + Unaligned, + Softclip } impl AlignmentStructureBaseKind { @@ -118,7 +119,8 @@ impl AlignmentStructureBaseKind { AlignmentStructureBaseKind::Match => "match", AlignmentStructureBaseKind::Mismatch => "mismatch", AlignmentStructureBaseKind::Insertion => "insertion", - AlignmentStructureBaseKind::Unaligned => "unaligned" + AlignmentStructureBaseKind::Unaligned => "unaligned", + AlignmentStructureBaseKind::Softclip => "softclip" } } @@ -127,7 +129,8 @@ impl AlignmentStructureBaseKind { AlignmentStructureBaseKind::Match => "=", AlignmentStructureBaseKind::Mismatch => "*", AlignmentStructureBaseKind::Insertion => "+", - AlignmentStructureBaseKind::Unaligned => "X" + AlignmentStructureBaseKind::Unaligned => "X", + AlignmentStructureBaseKind::Softclip => "S" } } } @@ -141,6 +144,7 @@ impl FromStr for AlignmentStructureBaseKind { "mismatch" | "*" => Ok(AlignmentStructureBaseKind::Mismatch), "insertion" | "+" => Ok(AlignmentStructureBaseKind::Insertion), "unaligned" | "X" => Ok(AlignmentStructureBaseKind::Unaligned), + "softclip" | "S" => Ok(AlignmentStructureBaseKind::Softclip), _ => Err(()) } } @@ -405,41 +409,6 @@ impl VariantType { VariantType::UTRExtension => "UTR" } } - - // pub fn as_symbol_str(&self) -> &str { - // match self { - // VariantType::Breakpoint => "#", - // VariantType::CircularRNA => "@", - // VariantType::CrypticExon => "?", - // VariantType::Deletion => "-", - // VariantType::ExonTruncation => "!", - // VariantType::FusionGene => "&", - // VariantType::Insertion => "+", - // VariantType::IntronRetention => "$", - // VariantType::MultiNucleotideVariant => "{", - // VariantType::NoncanonicalSplicing => "/", - // VariantType::SingleNucleotideVariant => "*", - // VariantType::Translocation => "^" - // } - // } - // - // pub fn from_symbol_str(s: &str) -> Result { - // match s { - // "#" => Ok(Self::Breakpoint), - // "@" => Ok(Self::CircularRNA), - // "?" => Ok(Self::CrypticExon), - // "-" => Ok(Self::Deletion), - // "!" => Ok(Self::ExonTruncation), - // "&" => Ok(Self::FusionGene), - // "+" => Ok(Self::Insertion), - // "$" => Ok(Self::IntronRetention), - // "{" => Ok(Self::MultiNucleotideVariant), - // "/" => Ok(Self::NoncanonicalSplicing), - // "*" => Ok(Self::SingleNucleotideVariant), - // "^" => Ok(Self::Translocation), - // _ => Err(()) - // } - // } } impl FromStr for VariantType { @@ -463,4 +432,3 @@ impl FromStr for VariantType { } } } - diff --git a/exacto/exacto-caller/src/common/logging.rs b/exacto/exacto-caller/src/common/logging.rs deleted file mode 100644 index 8a63109..0000000 --- a/exacto/exacto-caller/src/common/logging.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -use chrono::Local; -use env_logger::{Builder}; -use log::LevelFilter; -use once_cell::sync::OnceCell; -use std::io::Write; - - -static INIT_LOGGER: OnceCell<()> = OnceCell::new(); - - -pub fn try_init_logging() { - INIT_LOGGER.get_or_init(|| { - Builder::new() - .format(|buf, record| { - writeln!( - buf, - "{} [{}] - {}", - Local::now().format("%Y-%m-%dT%H:%M:%S"), - record.level(), - record.args() - )?; - // Explicitly flush the buffer after each log entry - buf.flush() - }) - .filter(None, LevelFilter::Info) - .init(); - }); -} \ No newline at end of file diff --git a/exacto/exacto-caller/src/common/mod.rs b/exacto/exacto-caller/src/common/mod.rs index 496eec2..0b6d2c4 100644 --- a/exacto/exacto-caller/src/common/mod.rs +++ b/exacto/exacto-caller/src/common/mod.rs @@ -1,2 +1 @@ pub mod constants; -pub mod logging; diff --git a/exacto/exacto-caller/src/lib.rs b/exacto/exacto-caller/src/lib.rs index e77c345..f6ed4d4 100644 --- a/exacto/exacto-caller/src/lib.rs +++ b/exacto/exacto-caller/src/lib.rs @@ -20,6 +20,7 @@ extern crate polars; extern crate rayon; extern crate regex; extern crate serde; +extern crate statrs; extern crate sysinfo; extern crate tempfile; @@ -27,6 +28,5 @@ extern crate tempfile; mod tests; pub mod algorithms; pub mod common; -pub mod macros; pub mod prelude; pub mod structs; diff --git a/exacto/exacto-caller/src/prelude.rs b/exacto/exacto-caller/src/prelude.rs index 2cf3302..ade2617 100644 --- a/exacto/exacto-caller/src/prelude.rs +++ b/exacto/exacto-caller/src/prelude.rs @@ -1,7 +1,6 @@ pub use crate::algorithms::reference_transcript_matching::*; pub use crate::algorithms::variant_calling::*; pub use crate::algorithms::variant_calling_dna::*; -pub use crate::algorithms::variant_calling_peptide::*; pub use crate::algorithms::variant_calling_rna::*; pub use crate::common::constants::*; pub use crate::structs::alignment::Alignment; @@ -11,6 +10,7 @@ pub use crate::structs::alignment_structure_base::AlignmentStructureBase; pub use crate::structs::alignment_structure_event::AlignmentStructureEvent; pub use crate::structs::alignment_structure_record::AlignmentStructureRecord; pub use crate::structs::graph_operation::GraphOperation; +pub use crate::structs::graph_operation_view::GraphOperationView; pub use crate::structs::mutant_peptide::MutantPeptide; pub use crate::structs::mutant_peptides_set::MutantPeptidesSet; pub use crate::structs::reference_base::ReferenceBase; diff --git a/exacto/exacto-caller/src/structs/alignment.rs b/exacto/exacto-caller/src/structs/alignment.rs index 15d3bf7..2b00ddf 100644 --- a/exacto/exacto-caller/src/structs/alignment.rs +++ b/exacto/exacto-caller/src/structs/alignment.rs @@ -17,6 +17,7 @@ use noodles_bam as bam; use noodles_sam::alignment::Record; use regex::Regex; use std::str::FromStr; +use std::sync::Arc; use crate::prelude::*; @@ -59,14 +60,14 @@ impl Alignment { read_id: usize, read_sequence: &str, base_quality_scores: &Vec, - records: &Vec + records: &Vec> ) -> Self { assert!(!records.is_empty()); // Step 1. Make sure all the BAM records come from the same read ID - let first_read_id = std::str::from_utf8(records[0].name().unwrap().as_bytes()).unwrap(); + let first_read_id: &str = std::str::from_utf8(records[0].name().unwrap().as_bytes()).unwrap(); for record in records.iter().skip(1) { - let read_id = std::str::from_utf8(record.name().unwrap().as_bytes()).unwrap(); + let read_id: &str = std::str::from_utf8(record.name().unwrap().as_bytes()).unwrap(); assert_eq!(read_id, first_read_id, "Not all records have the same read ID."); } @@ -148,7 +149,7 @@ impl Alignment { impl Alignment { fn build_alignment_records( read_sequence: &str, - records: &Vec + records: &Vec> ) -> Vec { let mut alignment_records: Vec = Vec::new(); @@ -158,23 +159,24 @@ impl Alignment { let aligned_sequence: Box = get_aligned_sequence_from_cigar(&record).into(); // Identify all start positions between aligned sequence and original read sequence - let start_positions: Vec = find_substring_positions(&*read_sequence.to_uppercase(), &*aligned_sequence.to_uppercase()); + let start_positions: Vec = find_substring_positions(&*read_sequence.to_uppercase(), &*aligned_sequence.to_uppercase()); assert!(!start_positions.is_empty(), "Could not find the aligned sequence in the original read sequence."); // Get left and right soft-clipping information of the current record - let left_softclipping: (bool, usize) = get_left_softclipping(&record); - let right_softclipping: (bool, usize) = get_right_softclipping(&record); + let left_softclipping: (bool, u32) = get_left_softclipping(&record); + let right_softclipping: (bool, u32) = get_right_softclipping(&record); // If there are multiple start positions, find where the aligned sequence starts // on the original read sequence - let mut read_start: usize = 0; - let mut read_end: usize = 0; + let mut found_alignment: bool = false; + let mut read_start: u32 = 0; + let mut read_end: u32 = 0; let reference_strand: Strand = get_alignment_strand(&record); for start_position in start_positions.iter() { // Check if the current start position aligns with the current alignment record - let end_position: usize = *start_position + aligned_sequence.len() - 1; - let num_left_bases: usize = *start_position; - let num_right_bases: usize = read_sequence.len() - end_position - 1; + let end_position: u32 = *start_position + aligned_sequence.len() as u32 - 1; + let num_left_bases: u32 = *start_position; + let num_right_bases: u32 = read_sequence.len() as u32 - end_position - 1; let mut aligned: bool = true; if reference_strand == Strand::Reverse { @@ -190,14 +192,15 @@ impl Alignment { } if aligned { - read_start = *start_position as usize; - read_end = read_start + (aligned_sequence.len() as usize) - 1; + read_start = *start_position; + read_end = read_start + (aligned_sequence.len() as u32) - 1; + found_alignment = true; break; } } - assert!(read_start != read_end, "read_start should not be the same as read_end."); - assert!(&*aligned_sequence == read_sequence[(read_start as usize)..(read_end as usize)+1].to_string(), "Aligned sequence does not match the identified part of the original read sequence."); + assert!(found_alignment, "No valid alignment found for the aligned sequence."); + assert!(&*aligned_sequence == read_sequence[(read_start as usize)..(read_end as usize + 1)].to_string(), "Aligned sequence does not match the identified part of the original read sequence."); let alignment_record: AlignmentRecord = AlignmentRecord::new( read_start, @@ -225,7 +228,7 @@ impl Alignment { let nucleotide: Nucleotide = Nucleotide::from_str(s.to_string().as_str()).unwrap(); let base_quality: u8 = base_quality_scores[i]; let alignment_base: AlignmentStructureBase = AlignmentStructureBase::new( - i as usize, + i as u32, nucleotide, base_quality ); @@ -245,134 +248,156 @@ impl Alignment { for (i, curr_alignment_record) in alignment_records.iter().enumerate() { let reference_chromosome_id: u16 = curr_alignment_record.record.reference_sequence_id().unwrap().unwrap() as u16; let reference_strand: Strand = get_alignment_strand(&curr_alignment_record.record); - let mapping_quality: usize = get_alignment_mapping_quality(&curr_alignment_record.record); + let mapping_quality: u16 = get_alignment_mapping_quality(&curr_alignment_record.record); // Identify soft-clipped insertion in the first alignment if (i == 0) && (curr_alignment_record.read_start != 0) { - let expected_softclip_length: usize = curr_alignment_record.read_start as usize; + let expected_softclip_length: u32 = curr_alignment_record.read_start; let (is_softclipped, softclip_length, reference_position) = if reference_strand == Strand::Reverse { let (is_softclipped, softclip_length) = get_right_softclipping(&curr_alignment_record.record); - assert!(is_softclipped, "The 3' end of the first alignment (read ID: {}) is soft-clipped.", read_id); + assert_eq!(is_softclipped, true, "The 3' end of the first alignment (read ID: {}) is soft-clipped.", read_id); assert_eq!(softclip_length, expected_softclip_length, "Read start position is expected to be the same as the number of soft-clipped bases."); - let reference_position: usize = get_alignment_end_position(&curr_alignment_record.record); + let reference_position: u32 = get_alignment_end_position(&curr_alignment_record.record); (is_softclipped, softclip_length, reference_position) } else { let (is_softclipped, softclip_length) = get_left_softclipping(&curr_alignment_record.record); - assert!(is_softclipped, "The 5' end of the first alignment (read ID: {}) is soft-clipped.", read_id); + assert_eq!(is_softclipped, true, "The 5' end of the first alignment (read ID: {}) is soft-clipped.", read_id); assert_eq!(softclip_length, expected_softclip_length, "Read start position is expected to be the same as the number of soft-clipped bases."); - let reference_position: usize = get_alignment_start_position(&curr_alignment_record.record) - 1; + let reference_position: u32 = get_alignment_start_position(&curr_alignment_record.record) - 1; (is_softclipped, softclip_length, reference_position) }; for j in 0..softclip_length { - let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_mut_base(j as usize); + let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_base_mut(j); alignment_base.set_mapping_quality(mapping_quality); alignment_base.set_reference_chromosome_id(reference_chromosome_id); alignment_base.set_reference_position(reference_position); alignment_base.set_reference_strand(reference_strand.clone()); - alignment_base.set_is_soft_clipped(true); - alignment_base.set_kind(AlignmentStructureBaseKind::Insertion); + // alignment_base.set_is_soft_clipped(true); + // alignment_base.set_kind(AlignmentStructureBaseKind::Insertion); + alignment_base.set_kind(AlignmentStructureBaseKind::Softclip); } } // Identify soft-clipped insertion in the last alignment - if (i == alignment_records.len() - 1) && ((curr_alignment_record.read_end as usize) != read_sequence.len() - 1) { - let expected_softclip_length: usize = read_sequence.len() - (curr_alignment_record.read_end as usize) - 1; + if (i == alignment_records.len() - 1) && ((curr_alignment_record.read_end) != read_sequence.len() as u32 - 1) { + let expected_softclip_length: u32 = read_sequence.len() as u32 - curr_alignment_record.read_end - 1; let (is_softclipped, softclip_length, reference_position) = if reference_strand == Strand::Reverse { let (is_softclipped, softclip_length) = get_left_softclipping(&curr_alignment_record.record); - assert!(is_softclipped, "The 5' end of the last alignment (read ID: {}) is soft-clipped.", read_id); + assert_eq!(is_softclipped, true, "The 5' end of the last alignment (read ID: {}) is soft-clipped.", read_id); assert_eq!(softclip_length, expected_softclip_length, "(Read length - alignment's last read position - 1) is expected to match the number of soft-clipped bases."); - let reference_position: usize = get_alignment_start_position(&curr_alignment_record.record) - 1; + let reference_position: u32 = get_alignment_start_position(&curr_alignment_record.record) - 1; (is_softclipped, softclip_length, reference_position) } else { let (is_softclipped, softclip_length) = get_right_softclipping(&curr_alignment_record.record); - assert!(is_softclipped, "The 3' end of the last alignment (read ID: {}) is soft-clipped.", read_id); + assert_eq!(is_softclipped, true, "The 3' end of the last alignment (read ID: {}) is soft-clipped.", read_id); assert_eq!(softclip_length, expected_softclip_length, "(Read length - alignment's last read position - 1) is expected to match the number of soft-clipped bases."); let reference_position = get_alignment_end_position(&curr_alignment_record.record); (is_softclipped, softclip_length, reference_position) }; - let start: usize = read_sequence.len() - softclip_length; - let end: usize = read_sequence.len(); + let start: u32 = read_sequence.len() as u32 - softclip_length; + let end: u32 = read_sequence.len() as u32; for j in start..end { - let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_mut_base(j as usize); + let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_base_mut(j); alignment_base.set_mapping_quality(mapping_quality); alignment_base.set_reference_chromosome_id(reference_chromosome_id); alignment_base.set_reference_position(reference_position); alignment_base.set_reference_strand(reference_strand.clone()); - alignment_base.set_is_soft_clipped(true); - alignment_base.set_kind(AlignmentStructureBaseKind::Insertion); + // alignment_base.set_is_soft_clipped(true); + // alignment_base.set_kind(AlignmentStructureBaseKind::Insertion); + alignment_base.set_kind(AlignmentStructureBaseKind::Softclip); } } // Identify breakpoints (soft-clipping) between alignments if i > 0 { - let mut bnd_1_read_position: usize = prev_alignment_record.read_end; - let mut bnd_2_read_position: usize = curr_alignment_record.read_start; - - // Check if the previous and the current alignments overlap - let alignments_overlap: bool = overlaps( - prev_alignment_record.read_start as isize, - prev_alignment_record.read_end as isize, - curr_alignment_record.read_start as isize, - curr_alignment_record.read_end as isize + let mut bnd_1_read_position: u32 = prev_alignment_record.read_end; + let mut bnd_2_read_position: u32 = curr_alignment_record.read_start; + + // Check if curr_alignment_record is completely contained within prev_alignment_record + let contained_1: bool = interval_contains( + prev_alignment_record.read_start, + prev_alignment_record.read_end, + curr_alignment_record.read_start, + curr_alignment_record.read_end ); - if alignments_overlap { - // If the previous and the current alignment records overlap, - // treat the overlapping part as an insertion - let (overlap_start,overlap_end) = find_overlap( - (prev_alignment_record.read_start as isize,prev_alignment_record.read_end as isize), - (curr_alignment_record.read_start as isize,curr_alignment_record.read_end as isize) - ).unwrap(); - let insertion: Box = read_sequence[(overlap_start as usize)..=(overlap_end as usize)].to_string().into_boxed_str(); - - // Retreat read positions by the length of the insertion and mark each an insertion - for j in overlap_start..=overlap_end { - let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_mut_base(j as usize); - alignment_base.set_is_embedded_insertion(true); - alignment_base.set_kind(AlignmentStructureBaseKind::Insertion); - bnd_1_read_position -= 1; - bnd_2_read_position += 1; - } - } else { - // Check if an insertion (i.e. unaligned bases) exists between the breakpoints - if prev_alignment_record.read_end + 1 != curr_alignment_record.read_start && - prev_alignment_record.read_end < curr_alignment_record.read_start { - let insertion: Box = read_sequence[(prev_alignment_record.read_end as usize) + 1..=(curr_alignment_record.read_start as usize) - 1].to_string().into_boxed_str(); - - // Mark each unaligned base an insertion - for j in (prev_alignment_record.read_end as usize) + 1..=(curr_alignment_record.read_start as usize) - 1 { - let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_mut_base(j as usize); - alignment_base.set_is_embedded_insertion(true); - alignment_base.set_kind(AlignmentStructureBaseKind::Insertion); + let contained_2: bool = interval_contains( + curr_alignment_record.read_start, + curr_alignment_record.read_end, + prev_alignment_record.read_start, + prev_alignment_record.read_end + ); + + if contained_1 == false && contained_2 == false { + // Check if the previous and the current alignments overlap + let alignments_overlap: bool = overlaps( + prev_alignment_record.read_start as isize, + prev_alignment_record.read_end as isize, + curr_alignment_record.read_start as isize, + curr_alignment_record.read_end as isize + ); + let mut insertion: Box = "".to_string().into_boxed_str(); + if alignments_overlap { + // If the previous and the current alignment records overlap, + // treat the overlapping part as an insertion + let (overlap_start,overlap_end) = find_overlap( + (prev_alignment_record.read_start as isize,prev_alignment_record.read_end as isize), + (curr_alignment_record.read_start as isize,curr_alignment_record.read_end as isize) + ).unwrap(); + insertion = read_sequence[(overlap_start as usize)..=(overlap_end as usize)].to_string().into_boxed_str(); + + // Retreat read positions by the length of the insertion and mark each an insertion + for j in overlap_start..=overlap_end { + let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_base_mut(j as u32); + // alignment_base.set_is_embedded_insertion(true); + // alignment_base.set_kind(AlignmentStructureBaseKind::Insertion); + alignment_base.set_kind(AlignmentStructureBaseKind::Softclip); + bnd_1_read_position -= 1; + bnd_2_read_position += 1; + } + } else { + // Check if an insertion (i.e. unaligned bases) exists between the breakpoints + if prev_alignment_record.read_end + 1 != curr_alignment_record.read_start && + prev_alignment_record.read_end < curr_alignment_record.read_start { + insertion = read_sequence[(prev_alignment_record.read_end as usize) + 1..=(curr_alignment_record.read_start as usize) - 1].to_string().into_boxed_str(); + + // Mark each unaligned base an insertion + for j in (prev_alignment_record.read_end) + 1..=(curr_alignment_record.read_start) - 1 { + let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_base_mut(j); + // alignment_base.set_is_embedded_insertion(true); + // alignment_base.set_kind(AlignmentStructureBaseKind::Insertion); + alignment_base.set_kind(AlignmentStructureBaseKind::Softclip); + } } } - } - - assert!(bnd_1_read_position < bnd_2_read_position); - let bnd_1_base: &AlignmentStructureBase = alignment_structure.get_base(bnd_1_read_position); - let bnd_2_base: &AlignmentStructureBase = alignment_structure.get_base(bnd_2_read_position); - let bnd_1_operation: GraphOperationType = match bnd_1_base.get_reference_strand().as_ref().unwrap() { - Strand::Forward => GraphOperationType::Downstream, - Strand::Reverse => GraphOperationType::Upstream, - Strand::Both => panic!("Unexpected strand: {}", bnd_1_base.get_reference_strand().as_ref().unwrap().as_str()), - Strand::Unknown => panic!("Unexpected strand: {}", bnd_1_base.get_reference_strand().as_ref().unwrap().as_str()) - }; - let bnd_2_operation: GraphOperationType = match bnd_2_base.get_reference_strand().as_ref().unwrap() { - Strand::Forward => GraphOperationType::Upstream, - Strand::Reverse => GraphOperationType::Downstream, - Strand::Both => panic!("Unexpected strand: {}", bnd_2_base.get_reference_strand().as_ref().unwrap().as_str()), - Strand::Unknown => panic!("Unexpected strand: {}", bnd_2_base.get_reference_strand().as_ref().unwrap().as_str()) - }; - - let alignment_event: AlignmentStructureEvent = AlignmentStructureEvent::new( - AlignmentStructureEventKind::Breakpoint, - bnd_1_read_position, - bnd_2_read_position, - bnd_1_operation.clone(), - bnd_2_operation.clone() - ); - - alignment_structure.add_event(alignment_event); + assert!(bnd_1_read_position < bnd_2_read_position, "{} < {} not satisfied", bnd_1_read_position, bnd_2_read_position); + assert!(bnd_2_read_position < read_sequence.len() as u32, "{} < {} not satisfied", bnd_2_read_position, read_sequence.len()); + + let bnd_1_base: &AlignmentStructureBase = alignment_structure.get_base(bnd_1_read_position); + let bnd_2_base: &AlignmentStructureBase = alignment_structure.get_base(bnd_2_read_position); + let bnd_1_operation: GraphOperationType = match bnd_1_base.get_reference_strand().as_ref().unwrap() { + Strand::Forward => GraphOperationType::Downstream, + Strand::Reverse => GraphOperationType::Upstream, + Strand::Both => panic!("Unexpected strand: {}", bnd_1_base.get_reference_strand().as_ref().unwrap().as_str()), + Strand::Unknown => panic!("Unexpected strand: {}", bnd_1_base.get_reference_strand().as_ref().unwrap().as_str()) + }; + let bnd_2_operation: GraphOperationType = match bnd_2_base.get_reference_strand().as_ref().unwrap() { + Strand::Forward => GraphOperationType::Upstream, + Strand::Reverse => GraphOperationType::Downstream, + Strand::Both => panic!("Unexpected strand: {}", bnd_2_base.get_reference_strand().as_ref().unwrap().as_str()), + Strand::Unknown => panic!("Unexpected strand: {}", bnd_2_base.get_reference_strand().as_ref().unwrap().as_str()) + }; + + let alignment_event: AlignmentStructureEvent = AlignmentStructureEvent::new( + AlignmentStructureEventKind::Breakpoint, + bnd_1_read_position, + bnd_2_read_position, + bnd_1_operation.clone(), + bnd_2_operation.clone() + ); + + alignment_structure.add_event(alignment_event); + } } prev_alignment_record = curr_alignment_record; } @@ -388,7 +413,7 @@ impl Alignment { let mut reference_position: isize = get_alignment_start_position(&alignment_record.record) as isize - 1; let reference_chromosome_id: u16 = alignment_record.record.reference_sequence_id().unwrap().unwrap() as u16; let reference_strand: Strand = get_alignment_strand(&alignment_record.record); - let mapping_quality: usize = get_alignment_mapping_quality(&alignment_record.record); + let mapping_quality: u16 = get_alignment_mapping_quality(&alignment_record.record); let cs_tag: String = get_tag_value(&alignment_record.record, "cs") .expect("Could not find the CS tag.") .to_string(); @@ -399,12 +424,12 @@ impl Alignment { }; // Identify SNVs, insertions, deletions, and splicing in the CS tag - let re = Regex::new(r"([:\-+*~=][0-9A-Za-z]+)").unwrap(); // or ([:][0-9]+|[-+*=][A-Za-z]+) + let re: Regex = Regex::new(r"([:\-+*~=][0-9A-Za-z]+)").unwrap(); // or ([:][0-9]+|[-+*=][A-Za-z]+) for cap in re.captures_iter(&cs_tag) { let token = &cap[0]; let mut chars = token.chars(); let cs_tag_kind: CSTagKind = CSTagKind::from_str(chars.next().unwrap().to_string().as_str()).unwrap(); - let payload = chars.as_str(); + let payload: &str = chars.as_str(); match cs_tag_kind { CSTagKind::Match => { @@ -412,10 +437,10 @@ impl Alignment { for _ in 0..length { read_position += if reference_strand == Strand::Forward { 1 } else { -1 }; reference_position += 1; - let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_mut_base(read_position as usize); + let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_base_mut(read_position as u32); alignment_base.set_mapping_quality(mapping_quality); alignment_base.set_reference_chromosome_id(reference_chromosome_id); - alignment_base.set_reference_position(reference_position as usize); + alignment_base.set_reference_position(reference_position as u32); alignment_base.set_reference_strand(reference_strand.clone()); alignment_base.set_kind(AlignmentStructureBaseKind::Match); } @@ -425,10 +450,10 @@ impl Alignment { assert_eq!(alleles.len(), 2, "1 reference allele and 1 alternate allele expected."); read_position += if reference_strand == Strand::Forward { 1 } else { -1 }; reference_position += 1; - let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_mut_base(read_position as usize); + let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_base_mut(read_position as u32); alignment_base.set_mapping_quality(mapping_quality); alignment_base.set_reference_chromosome_id(reference_chromosome_id); - alignment_base.set_reference_position(reference_position as usize); + alignment_base.set_reference_position(reference_position as u32); alignment_base.set_reference_strand(reference_strand.clone()); alignment_base.set_kind(AlignmentStructureBaseKind::Mismatch); }, @@ -437,18 +462,18 @@ impl Alignment { let length: usize = insertion.chars().count(); for _ in 0..length { read_position += if reference_strand == Strand::Forward { 1 } else { -1 }; - let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_mut_base(read_position as usize); + let alignment_base: &mut AlignmentStructureBase = alignment_structure.get_base_mut(read_position as u32); alignment_base.set_mapping_quality(mapping_quality); alignment_base.set_reference_chromosome_id(reference_chromosome_id); - alignment_base.set_reference_position(reference_position as usize); + alignment_base.set_reference_position(reference_position as u32); alignment_base.set_reference_strand(reference_strand.clone()); alignment_base.set_kind(AlignmentStructureBaseKind::Insertion); } } CSTagKind::Deletion => { let length: usize = payload.chars().count(); - let read_position_1: usize = read_position as usize; - let read_position_2: usize = if reference_strand == Strand::Forward { read_position as usize + 1 } else { read_position as usize - 1 }; + let read_position_1: u32 = read_position as u32; + let read_position_2: u32 = if reference_strand == Strand::Forward { read_position as u32 + 1 } else { read_position as u32 - 1 }; let alignment_event: AlignmentStructureEvent = if reference_strand == Strand::Forward { AlignmentStructureEvent::new( @@ -495,8 +520,12 @@ impl Alignment { acceptor_splice_site_signal = reverse_complement(&*acceptor_splice_site_signal); } - let read_position_1: usize = read_position as usize; - let read_position_2: usize = if reference_strand == Strand::Forward { read_position as usize + 1 } else { read_position as usize - 1 }; + let read_position_1: u32 = read_position as u32; + let read_position_2: u32 = if reference_strand == Strand::Forward { + read_position as u32 + 1 + } else { + read_position as u32 - 1 + }; let alignment_event: AlignmentStructureEvent = if reference_strand == Strand::Forward { AlignmentStructureEvent::new( diff --git a/exacto/exacto-caller/src/structs/alignment_record.rs b/exacto/exacto-caller/src/structs/alignment_record.rs index b412690..3924def 100644 --- a/exacto/exacto-caller/src/structs/alignment_record.rs +++ b/exacto/exacto-caller/src/structs/alignment_record.rs @@ -13,22 +13,21 @@ use exacto_core::prelude::*; use noodles_bam as bam; - -use crate::prelude::TranscriptModel; +use std::sync::Arc; #[derive(Debug)] pub struct AlignmentRecord { /// Read sequence start position - pub read_start: usize, + pub read_start: u32, /// Read sequence end position - pub read_end: usize, + pub read_end: u32, /// Reference strand pub reference_strand: Strand, - pub record: bam::Record + pub record: Arc } impl PartialEq for AlignmentRecord { @@ -41,10 +40,10 @@ impl PartialEq for AlignmentRecord { impl AlignmentRecord { pub fn new( - read_start: usize, - read_end: usize, + read_start: u32, + read_end: u32, reference_strand: Strand, - record: bam::Record + record: Arc ) -> Self { assert!(read_start <= read_end); Self { diff --git a/exacto/exacto-caller/src/structs/alignment_structure.rs b/exacto/exacto-caller/src/structs/alignment_structure.rs index c0448bf..a76c14a 100644 --- a/exacto/exacto-caller/src/structs/alignment_structure.rs +++ b/exacto/exacto-caller/src/structs/alignment_structure.rs @@ -30,11 +30,11 @@ pub struct AlignmentStructure { /// A map between (read_position_1, read_position_2) and its alignment event. /// Note that `read_position_1` is smaller than or equal to `read_position_2`. - events: HashMap<(usize, usize), AlignmentStructureEvent>, + events: HashMap<(u32, u32), AlignmentStructureEvent>, /// Events index. The key is a read position and the value is another read position /// that together appear as a key in `events`. - events_index: HashMap + events_index: HashMap } /// API methods @@ -53,13 +53,46 @@ impl AlignmentStructure { } pub fn add_event(&mut self, event: AlignmentStructureEvent) { - // Index the event - self.events_index - .insert(event.get_prev_read_position(), event.get_next_read_position()); - self.events_index - .insert(event.get_next_read_position(), event.get_prev_read_position()); + // If there already exists a deletion and a splicing is being added, + // replace the event with the splicing + if self.events.contains_key(&(event.get_prev_read_position(), event.get_next_read_position())) { + let existing_event: &AlignmentStructureEvent = self.get_event(event.get_prev_read_position(), event.get_next_read_position()); + if *event.get_kind() == AlignmentStructureEventKind::Splicing && + *existing_event.get_kind() == AlignmentStructureEventKind::Deletion { + // Make sure the events index already contains the event read positions + assert!(self.events_index.contains_key(&event.get_prev_read_position())); + assert!(self.events_index.contains_key(&event.get_next_read_position())); + + // Replace the event + self.events.insert( + (event.get_prev_read_position(), event.get_next_read_position()), + event + ); + + return; + } + } + + // If there already exists a splicing and a deletion is being added, + // do not add the event (i.e. keep the splicing) + if self.events.contains_key(&(event.get_prev_read_position(), event.get_next_read_position())) { + let existing_event: &AlignmentStructureEvent = self.get_event(event.get_prev_read_position(), event.get_next_read_position()); + if *event.get_kind() == AlignmentStructureEventKind::Deletion && + *existing_event.get_kind() == AlignmentStructureEventKind::Splicing { + // Make sure the events index already contains the event read positions + assert!(self.events_index.contains_key(&event.get_prev_read_position())); + assert!(self.events_index.contains_key(&event.get_next_read_position())); + + // Do nothing + return; + } + } + + // Otherwise, first index the event + self.events_index.insert(event.get_prev_read_position(), event.get_next_read_position()); + self.events_index.insert(event.get_next_read_position(), event.get_prev_read_position()); - // Add the event + // Then, add the event self.events.insert( (event.get_prev_read_position(), event.get_next_read_position()), event @@ -68,50 +101,46 @@ impl AlignmentStructure { pub fn contextualize( &mut self, + read_name: &str, reference_transcript_sequences: &Vec<&ReferenceTranscriptSequence>, gene_annotator: &(impl GeneAnnotator + Sync), chromosome_names_map: &BiMap, u16> ) { - // Step 1. Make sure the reference transcript sequences have unique gene IDs - let mut gene_ids: HashSet> = HashSet::new(); + // Step 1. Make sure the reference transcript sequences have unique reference gene IDs + let mut reference_gene_ids: HashSet> = HashSet::new(); for reference_transcript_sequence in reference_transcript_sequences.iter() { - if gene_ids.contains(reference_transcript_sequence.get_gene_id()) { + let inserted: bool = reference_gene_ids.insert(reference_transcript_sequence.get_gene_id().into()); + if !inserted { panic!("Only 1 reference transcript per gene ID is allowed."); } - gene_ids.insert(reference_transcript_sequence.get_gene_id().into()); } - // Step 2. Make sure each reference transcript sequence has 1 base that + // Step 2. Make sure each reference transcript sequence has at least 1 base that // overlaps with one of the self.bases - let mut reference_transcript_overlap_map: HashMap<&str, bool> = HashMap::new(); for reference_transcript_sequence in reference_transcript_sequences.iter() { - reference_transcript_overlap_map.insert(reference_transcript_sequence.get_transcript_id().into(), false); - } - if reference_transcript_sequences.is_empty() == false { - for base in self.get_bases() { - for reference_transcript_sequence in reference_transcript_sequences.iter() { - for base_reference in reference_transcript_sequence.get_bases() { - if base.get_reference_chromosome_id().unwrap() == base_reference.reference_chromosome_id && - base.get_reference_position().unwrap() == base_reference.reference_position && - base.get_reference_strand().as_ref().unwrap().clone() == base_reference.reference_strand { - reference_transcript_overlap_map.insert(reference_transcript_sequence.get_transcript_id(), true); - } - } - } - } - - // Make sure every reference transcript sequence overlaps with one of the self.bases - for reference_transcript_sequence in reference_transcript_sequences.iter() { - assert_eq!( - *reference_transcript_overlap_map.get(reference_transcript_sequence.get_transcript_id()).unwrap(), true, - "None of the ReferenceTranscriptSequence bases for {} overlaps with any of the AlignmentStructure bases.", - reference_transcript_sequence.get_transcript_id() - ); - } + let has_overlap = self.get_bases().iter() + .filter(|base| matches!( + base.get_kind(), + AlignmentStructureBaseKind::Match | + AlignmentStructureBaseKind::Mismatch | + AlignmentStructureBaseKind::Insertion + )) + .any(|base| { + reference_transcript_sequence.get_bases().iter().any(|base_reference| { + base.get_reference_chromosome_id().unwrap() == base_reference.reference_chromosome_id + && base.get_reference_position().unwrap() == base_reference.reference_position + && base.get_reference_strand().as_ref().unwrap().clone() == base_reference.reference_strand + }) + }); + assert!( + has_overlap, + "Read name: {}. None of the ReferenceTranscriptSequence bases for {} overlaps with any of the AlignmentStructure bases.", + read_name, reference_transcript_sequence.get_transcript_id() + ); } // Step 3. Index the positions of the reference transcript sequences - let mut reference_transcripts_positions_map: HashMap<(u16, usize, Strand), &ReferenceTranscriptSequence> = HashMap::new(); + let mut reference_transcripts_positions_map: HashMap<(u16, u32, Strand), &ReferenceTranscriptSequence> = HashMap::new(); for reference_transcript_sequence in reference_transcript_sequences.iter() { for base in reference_transcript_sequence.get_bases() { reference_transcripts_positions_map.insert( @@ -123,16 +152,20 @@ impl AlignmentStructure { } } - // Step 4. Identify context of each AlignmentStructureBase + // Step 4. Identify the context of each AlignmentStructureBase for i in 0..self.get_bases_length() { - let base: &mut AlignmentStructureBase = self.get_mut_base(i); - if *base.get_kind() != AlignmentStructureBaseKind::Unaligned { - let key: (u16, usize, Strand) = ( + let base: &mut AlignmentStructureBase = self.get_base_mut(i); + if matches!(base.get_kind(), + AlignmentStructureBaseKind::Match | + AlignmentStructureBaseKind::Mismatch | + AlignmentStructureBaseKind::Insertion) { + let key: (u16, u32, Strand) = ( base.get_reference_chromosome_id().unwrap(), base.get_reference_position().unwrap(), base.get_reference_strand().as_ref().unwrap().clone() ); if reference_transcripts_positions_map.contains_key(&key) { + // Base is an exonic base let reference_transcript_sequence: &ReferenceTranscriptSequence = reference_transcripts_positions_map.get(&key).unwrap(); base.set_context(AlignmentStructureBaseContext::Exonic); base.set_reference_gene_id(reference_transcript_sequence.get_gene_id().into()); @@ -150,6 +183,7 @@ impl AlignmentStructure { } assert_eq!(base.get_reference_exon_id().is_some(), true); } else { + // Base is an intronic base for reference_transcript_sequence in reference_transcript_sequences.iter() { if base.get_reference_position().unwrap() >= reference_transcript_sequence.get_transcript_start() && base.get_reference_position().unwrap() <= reference_transcript_sequence.get_transcript_end() { @@ -167,9 +201,9 @@ impl AlignmentStructure { } // Step 5. Identify reference transcript introns - let mut reference_transcripts_introns_map: HashMap> = HashMap::new(); + let mut reference_transcripts_introns_map: HashMap> = HashMap::new(); for reference_transcript_sequence in reference_transcript_sequences.iter() { - let introns: Vec<(u16, usize, usize)> = reference_transcript_sequence.get_introns(); + let introns: Vec<(u16, u32, u32)> = reference_transcript_sequence.get_introns(); for (chromosome_id, start, end) in introns.iter() { reference_transcripts_introns_map .entry(*chromosome_id) @@ -178,8 +212,8 @@ impl AlignmentStructure { } } - // Step 6. Identify context of each AlignmentStructureEvent - let event_keys: Vec<(usize, usize)> = self.get_events().keys().cloned().collect(); + // Step 6. Identify the context of each AlignmentStructureEvent + let event_keys: Vec<(u32, u32)> = self.get_events().keys().cloned().collect(); for (read_position_1, read_position_2) in event_keys { let base_1: AlignmentStructureBase = self.get_base(read_position_1).clone(); let base_2: AlignmentStructureBase = self.get_base(read_position_2).clone(); @@ -194,8 +228,8 @@ impl AlignmentStructure { AlignmentStructureEventKind::Splicing => { let reference_chromosome_id: u16 = base_1.get_reference_chromosome_id().unwrap(); let reference_strand: Strand = base_1.get_reference_strand().as_ref().unwrap().clone(); - let mut reference_start: usize = base_1.get_reference_position().unwrap(); - let mut reference_end: usize = base_2.get_reference_position().unwrap(); + let mut reference_start: u32 = base_1.get_reference_position().unwrap(); + let mut reference_end: u32 = base_2.get_reference_position().unwrap(); if reference_strand == Strand::Forward { reference_start = reference_start + 1; reference_end = reference_end - 1; @@ -208,44 +242,44 @@ impl AlignmentStructure { let reference_transcripts_introns = reference_transcripts_introns_map.get(&reference_chromosome_id).unwrap(); if reference_transcripts_introns.contains(&(reference_start, reference_end)) || reference_transcripts_introns.contains(&(reference_end, reference_start)) { - self.get_mut_event(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::CanonicalSplicing); + self.get_event_mut(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::CanonicalSplicing); } else { if gene_ids_disjoint_count_1 > 0 && gene_ids_disjoint_count_2 > 0 { - self.get_mut_event(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::FusionGene); + self.get_event_mut(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::FusionGene); } else { - self.get_mut_event(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::NonCanonicalSplicing); + self.get_event_mut(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::NonCanonicalSplicing); } } } else { - self.get_mut_event(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::NonCanonicalSplicing); + self.get_event_mut(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::NonCanonicalSplicing); } }, AlignmentStructureEventKind::Breakpoint => { if base_1.get_reference_chromosome_id().unwrap() == base_2.get_reference_chromosome_id().unwrap() { - let left_bases_set: HashSet<(u16, usize)> = (0..=read_position_1 as usize) + let left_bases_set: HashSet<(u16, u32)> = (0..=read_position_1) .map(|i| { - let base = self.get_base(i as usize); + let base = self.get_base(i); (base.get_reference_chromosome_id().unwrap(), base.get_reference_position().unwrap()) }) .collect(); - let right_bases_set: HashSet<(u16, usize)> = (read_position_2..self.get_bases_length()) + let right_bases_set: HashSet<(u16, u32)> = (read_position_2..self.get_bases_length()) .map(|i| { let base = self.get_base(i); (base.get_reference_chromosome_id().unwrap(), base.get_reference_position().unwrap()) }) .collect(); if left_bases_set.is_disjoint(&right_bases_set) == false { - self.get_mut_event(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::BackSplicing); + self.get_event_mut(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::BackSplicing); } else { if gene_ids_disjoint_count_1 > 0 && gene_ids_disjoint_count_2 > 0 { - self.get_mut_event(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::FusionGene); + self.get_event_mut(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::FusionGene); } else { - self.get_mut_event(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::NonCanonicalSplicing); + self.get_event_mut(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::NonCanonicalSplicing); } } } else { if gene_ids_disjoint_count_1 > 0 && gene_ids_disjoint_count_2 > 0 { - self.get_mut_event(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::FusionGene); + self.get_event_mut(read_position_1, read_position_2).set_context(AlignmentStructureEventContext::FusionGene); } } }, @@ -256,7 +290,7 @@ impl AlignmentStructure { } // Step 7. Identify skipped reference transcript bases - let mut reference_transcripts_bases: HashMap<(u16, usize, &Strand), (&str, &ReferenceBase)> = HashMap::new(); + let mut reference_transcripts_bases: HashMap<(u16, u32, &Strand), (&str, &ReferenceBase)> = HashMap::new(); for reference_transcript_sequence in reference_transcript_sequences.iter() { for base in reference_transcript_sequence.get_bases() { reference_transcripts_bases.insert( @@ -265,42 +299,42 @@ impl AlignmentStructure { ); } } - let alignment_structure_bases: HashSet<(u16, usize, &Strand)> = self - .get_bases() - .iter() - .map(|base| - (base.get_reference_chromosome_id().unwrap(), - base.get_reference_position().unwrap(), - base.get_reference_strand().as_ref().unwrap()) - ) - .collect(); - let mut reference_transcript_bases_skipped: HashSet<(u16, usize, &Strand)> = reference_transcripts_bases + let mut alignment_structure_bases: HashSet<(u16, u32, &Strand)> = HashSet::new(); + for base in self.get_bases() { + if matches!(base.get_kind(), + AlignmentStructureBaseKind::Match | + AlignmentStructureBaseKind::Mismatch | + AlignmentStructureBaseKind::Insertion) { + alignment_structure_bases.insert(( + base.get_reference_chromosome_id().unwrap(), + base.get_reference_position().unwrap(), + base.get_reference_strand().as_ref().unwrap() + )); + } + } + let mut reference_transcript_bases_skipped: HashSet<(u16, u32, &Strand)> = reference_transcripts_bases .keys() .cloned() .filter(|pos| !alignment_structure_bases.contains(pos)) .sorted_by_key(|&(_, pos, _)| pos) .collect(); - // Step 8. Get the alignment structure base reference positions and sort them - let mut base_reference_positions_map: HashMap, Vec<(usize, usize)>> = HashMap::new(); + // Step 8. Get the reference positions corresponding to exonic boundaries in the + // alignment structure bases and sort them + let mut exon_boundary_positions_map: HashMap, Vec<(u32, u32)>> = HashMap::new(); for ((read_position_1, read_position_2), event) in self.get_events() { - if event.get_kind() == &AlignmentStructureEventKind::Splicing { - if *event.get_context().as_ref().unwrap() == AlignmentStructureEventContext::CanonicalSplicing { - continue; - } - } let base_1: &AlignmentStructureBase = self.get_base(*read_position_1); let base_2: &AlignmentStructureBase = self.get_base(*read_position_2); if base_1.get_reference_transcript_id().is_some() { let reference_transcript_id: Box = base_1.get_reference_transcript_id().as_ref().unwrap().clone(); - base_reference_positions_map + exon_boundary_positions_map .entry(reference_transcript_id) .or_insert(Vec::new()) .push((*read_position_1, base_1.get_reference_position().unwrap())); } if base_2.get_reference_transcript_id().is_some() { let reference_transcript_id: Box = base_2.get_reference_transcript_id().as_ref().unwrap().clone(); - base_reference_positions_map + exon_boundary_positions_map .entry(reference_transcript_id) .or_insert(Vec::new()) .push((*read_position_2, base_2.get_reference_position().unwrap())); @@ -312,8 +346,11 @@ impl AlignmentStructure { let mut reference_transcript_bases: Vec<&AlignmentStructureBase> = self.get_reference_transcript_bases(&*reference_transcript_id); reference_transcript_bases.sort_by_key(|base| base.get_reference_position().unwrap()); for base in reference_transcript_bases.iter() { - if base.is_embedded_insertion() == false && base.is_soft_clipped() == false { - base_reference_positions_map + if matches!(base.get_kind(), + AlignmentStructureBaseKind::Match | + AlignmentStructureBaseKind::Mismatch | + AlignmentStructureBaseKind::Insertion) { + exon_boundary_positions_map .entry(reference_transcript_id.clone()) .or_insert(Vec::new()) .push( @@ -323,8 +360,11 @@ impl AlignmentStructure { } } for base in reference_transcript_bases.iter().rev() { - if base.is_embedded_insertion() == false && base.is_soft_clipped() == false { - base_reference_positions_map + if matches!(base.get_kind(), + AlignmentStructureBaseKind::Match | + AlignmentStructureBaseKind::Mismatch | + AlignmentStructureBaseKind::Insertion) { + exon_boundary_positions_map .entry(reference_transcript_id.clone()) .or_insert(Vec::new()) .push( @@ -334,14 +374,13 @@ impl AlignmentStructure { } } } - for vec in base_reference_positions_map.values_mut() { + for vec in exon_boundary_positions_map.values_mut() { vec.sort_by_key(|&(_, reference_position)| reference_position); } // Step 9. Identify the closet event for each skipped reference transcript base - // Key: (chromosome ID, reference position, reference strand) - // Value: (read position 1, read position 2) - let mut events_map: HashMap<(u16, usize, &Strand), (usize, usize)> = HashMap::new(); + // Map of (chromosome ID, reference position, reference strand) and (read position 1, read position 2) + let mut events_map: HashMap<(u16, u32, &Strand), (u32, u32)> = HashMap::new(); for (reference_chromosome_id, reference_position, reference_strand) in reference_transcript_bases_skipped.iter() { let reference_transcript_id: Box = reference_transcripts_bases .get(&(*reference_chromosome_id, *reference_position, reference_strand)) @@ -350,8 +389,12 @@ impl AlignmentStructure { .into(); // Identify the closest base - let vec: &Vec<(usize, usize)> = base_reference_positions_map.get(&reference_transcript_id).unwrap(); - let closest_read_position: usize = match vec.binary_search_by_key(reference_position, |&(_, base_reference_position)| base_reference_position) { + let vec: &Vec<(u32, u32)> = exon_boundary_positions_map + .get(&reference_transcript_id) + .expect( + &format!("Missing reference transcript bases for read name {} and reference transcript ID {}", read_name, reference_transcript_id) + ); + let closest_read_position: u32 = match vec.binary_search_by_key(reference_position, |&(_, base_reference_position)| base_reference_position) { Ok(idx) => { vec[idx].0 }, @@ -361,10 +404,10 @@ impl AlignmentStructure { } else if idx == vec.len() { vec[vec.len() - 1].0 } else { - let read_position_1: usize = vec[idx-1].0; - let read_position_2: usize = vec[idx].0; - let reference_position_1: usize = vec[idx-1].1; - let reference_position_2: usize = vec[idx].1; + let read_position_1: u32 = vec[idx-1].0; + let read_position_2: u32 = vec[idx].0; + let reference_position_1: u32 = vec[idx-1].1; + let reference_position_2: u32 = vec[idx].1; if reference_position.abs_diff(reference_position_1) <= reference_position.abs_diff(reference_position_2) { read_position_1 } else { @@ -389,13 +432,27 @@ impl AlignmentStructure { events_map.insert( (*reference_chromosome_id, *reference_position, reference_strand), (closest_read_position, closest_read_position) ); - } else { - assert!(self.events_index.contains_key(&closest_read_position), "Event does not exist for base position {}. Skipped reference position: {}:{}", closest_read_position, reference_chromosome_id, reference_position); - let closest_read_position_2: usize = *self.events_index.get(&closest_read_position).unwrap(); + } else if self.events_index.contains_key(&closest_read_position) { + let closest_read_position_2: u32 = *self.events_index.get(&closest_read_position).unwrap(); assert!(self.has_event_between(closest_read_position, closest_read_position_2), "Event does not exist between bases {} and {}", closest_read_position, closest_read_position_2); events_map.insert( (*reference_chromosome_id, *reference_position, reference_strand), (closest_read_position, closest_read_position_2) ); + } else { + // No event exists at this position (e.g. no splicing in this read). + // Create a boundary event to anchor the skipped reference base. + let mut event: AlignmentStructureEvent = AlignmentStructureEvent::new( + AlignmentStructureEventKind::Boundary, + closest_read_position, + closest_read_position, + GraphOperationType::Mark, + GraphOperationType::Mark + ); + event.set_context(AlignmentStructureEventContext::NonCanonicalSplicing); + self.add_event(event); + events_map.insert( + (*reference_chromosome_id, *reference_position, reference_strand), (closest_read_position, closest_read_position) + ); } } @@ -405,25 +462,39 @@ impl AlignmentStructure { for ((chromosome_id, reference_position, reference_strand), (read_position_1, read_position_2)) in events_map.iter() { let reference_base: &ReferenceBase = reference_transcripts_bases.get(&(*chromosome_id, *reference_position, reference_strand)).unwrap().1; assert!(self.has_event_between(*read_position_1, *read_position_2) == true); - self.get_mut_event(*read_position_1, *read_position_2).add_skipped_reference_base(reference_base.clone()); + self.get_event_mut(*read_position_1, *read_position_2).add_skipped_reference_base(reference_base.clone()); } } - pub fn get_base(&self, read_position: usize) -> &AlignmentStructureBase { - self.bases.get(read_position as usize).unwrap() + pub fn get_base(&self, read_position: u32) -> &AlignmentStructureBase { + match self.bases.get(read_position as usize) { + Some(base) => { + base + }, + None => { + panic!( + "Invalid read_position: {}\n + Read length: {}\n + First base: {:?}", + read_position, + self.bases.len(), + self.bases.first().unwrap() + ); + } + } } pub fn get_bases(&self) -> &Vec { &self.bases } - pub fn get_bases_length(&self) -> usize { - self.bases.len() as usize + pub fn get_bases_length(&self) -> u32 { + self.bases.len() as u32 } pub fn get_event(&self, - read_position_1: usize, - read_position_2: usize + read_position_1: u32, + read_position_2: u32 ) -> &AlignmentStructureEvent { if read_position_1 < read_position_2 { self.events @@ -436,17 +507,17 @@ impl AlignmentStructure { } } - pub fn get_events(&self) -> &HashMap<(usize, usize), AlignmentStructureEvent> { + pub fn get_events(&self) -> &HashMap<(u32, u32), AlignmentStructureEvent> { &self.events } - pub fn get_event_at_read_position(&self, read_position: usize) -> &AlignmentStructureEvent { + pub fn get_event_at_read_position(&self, read_position: u32) -> &AlignmentStructureEvent { let read_position_2 = self.events_index.get(&read_position).unwrap(); self.get_event(read_position, *read_position_2) } - pub fn get_events_of_kind(&self, kind: AlignmentStructureEventKind) -> Vec<(usize, usize, &AlignmentStructureEvent)> { - let mut events: Vec<(usize, usize, &AlignmentStructureEvent)> = Vec::new(); + pub fn get_events_of_kind(&self, kind: AlignmentStructureEventKind) -> Vec<(u32, u32, &AlignmentStructureEvent)> { + let mut events: Vec<(u32, u32, &AlignmentStructureEvent)> = Vec::new(); for ((read_position_1, read_position_2), event) in self.get_events() { if event.get_kind() == &kind { events.push((*read_position_1, *read_position_2, event)); @@ -455,14 +526,14 @@ impl AlignmentStructure { events } - pub fn get_mut_base(&mut self, read_position: usize) -> &mut AlignmentStructureBase { + pub fn get_base_mut(&mut self, read_position: u32) -> &mut AlignmentStructureBase { self.bases.get_mut(read_position as usize).unwrap() } - pub fn get_mut_event( + pub fn get_event_mut( &mut self, - read_position_1: usize, - read_position_2: usize + read_position_1: u32, + read_position_2: u32 ) -> &mut AlignmentStructureEvent { if read_position_1 < read_position_2 { assert!( @@ -514,7 +585,7 @@ impl AlignmentStructure { reference_transcript_ids } - pub fn has_event(&self, read_position: usize) -> bool { + pub fn has_event(&self, read_position: u32) -> bool { if self.events_index.contains_key(&read_position) { true } else { @@ -524,8 +595,8 @@ impl AlignmentStructure { pub fn has_event_between( &self, - read_position_1: usize, - read_position_2: usize + read_position_1: u32, + read_position_2: u32 ) -> bool { if read_position_1 < read_position_2 { self.events.contains_key(&(read_position_1, read_position_2)) @@ -534,39 +605,53 @@ impl AlignmentStructure { } } - pub fn identify_exons(&self) -> Vec { + pub fn identify_exons(&self, read_name: &str) -> Vec { // Step 1. Cluster adjacent bases by reference position + let is_exonic = |base: &AlignmentStructureBase| matches!( + base.get_kind(), + AlignmentStructureBaseKind::Match | + AlignmentStructureBaseKind::Mismatch | + AlignmentStructureBaseKind::Insertion + ); let mut uf: UnionFind = UnionFind::new(); for i in 0..self.get_bases_length() { - uf.union(i as usize, i as usize); - if i > 0 { - let prev_base: &AlignmentStructureBase = self.get_base(i - 1); - let curr_base: &AlignmentStructureBase = self.get_base(i); - if self.has_event_between(i-1, i) { - let event: &AlignmentStructureEvent = self.get_event(i-1, i); - if event.get_kind() == &AlignmentStructureEventKind::Deletion { - if prev_base.get_reference_chromosome_id().unwrap() == curr_base.get_reference_chromosome_id().unwrap() && - prev_base.get_reference_strand().as_ref().unwrap() == curr_base.get_reference_strand().as_ref().unwrap() { - uf.union(i as usize - 1, i as usize); - } - } - } else { - if prev_base.get_reference_chromosome_id().unwrap() == curr_base.get_reference_chromosome_id().unwrap() && - prev_base.get_reference_strand().as_ref().unwrap() == curr_base.get_reference_strand().as_ref().unwrap() && - prev_base.get_reference_position().unwrap().abs_diff(curr_base.get_reference_position().unwrap()) <= 1 { - uf.union(i as usize - 1, i as usize); - } - } + let curr_base: &AlignmentStructureBase = self.get_base(i); + if is_exonic(curr_base) == false { + continue; + } + uf.union(i, i); + + if i == 0 { + continue; + } + + let prev_base: &AlignmentStructureBase = self.get_base(i - 1); + if is_exonic(prev_base) == false { + continue; + } + + let should_union: bool = if self.has_event_between(i - 1, i) { + let event = self.get_event(i - 1, i); + event.get_kind() == &AlignmentStructureEventKind::Deletion + && prev_base.get_reference_chromosome_id() == curr_base.get_reference_chromosome_id() + && prev_base.get_reference_strand() == curr_base.get_reference_strand() + } else { + matches!((prev_base.get_reference_chromosome_id(), curr_base.get_reference_chromosome_id()), (Some(p), Some(c)) if p == c) && + prev_base.get_reference_strand() == curr_base.get_reference_strand() && + prev_base.get_reference_position().unwrap().abs_diff(curr_base.get_reference_position().unwrap()) <= 1 + }; + + if should_union { + uf.union(i - 1, i); } } // Step 2. Identify exonic boundaries let mut exons: Vec = Vec::new(); - let mut exon_number: u16 = 1; - let mut clusters: Vec> = uf.get_clusters(); + let mut clusters: Vec> = uf.get_clusters(); for cluster in clusters.iter() { // Sort the read positions - let mut read_positions: Vec = cluster.iter().map(|&pos| pos as usize).collect(); + let mut read_positions: Vec = cluster.iter().map(|&pos| pos).collect(); read_positions.sort(); let mut bases: Vec<&AlignmentStructureBase> = Vec::new(); @@ -574,25 +659,32 @@ impl AlignmentStructure { bases.push(self.get_base(*read_position)); } - let reference_chromosome_id: u16 = bases.first().unwrap().get_reference_chromosome_id().unwrap(); - let reference_strand: Strand = bases.first().unwrap().get_reference_strand().as_ref().unwrap().clone(); - let reference_start: usize = bases.iter().map(|base| base.get_reference_position().unwrap()).min().unwrap(); - let reference_end: usize = bases.iter().map(|base| base.get_reference_position().unwrap()).max().unwrap(); - let read_start_position: usize = bases.first().unwrap().get_read_position(); - let read_end_position: usize = bases.last().unwrap().get_read_position(); - - let exon: TranscriptModelExon = TranscriptModelExon::new( - reference_chromosome_id, - reference_start, - reference_end, - reference_strand, - exon_number, - read_start_position, - read_end_position - ); + if let Some(reference_chromosome_id) = bases.first().unwrap().get_reference_chromosome_id() { + let reference_strand: Strand = bases.first().unwrap().get_reference_strand().as_ref().unwrap().clone(); + let reference_start: u32 = bases.iter().map(|base| base.get_reference_position().unwrap()).min().unwrap(); + let reference_end: u32 = bases.iter().map(|base| base.get_reference_position().unwrap()).max().unwrap(); + let read_start_position: u32 = bases.first().unwrap().get_read_position(); + let read_end_position: u32 = bases.last().unwrap().get_read_position(); + + // Use 0 as a placeholder — exon_number assigned after sorting + let exon: TranscriptModelExon = TranscriptModelExon::new( + *reference_chromosome_id, + reference_start, + reference_end, + reference_strand, + 0, + read_start_position, + read_end_position + ); + + exons.push(exon); + } + } - exons.push(exon); - exon_number += 1; + // Sort by read order, then assign exon numbers + exons.sort_by_key(|e| e.read_start_position); + for (i, exon) in exons.iter_mut().enumerate() { + exon.exon_number = (i + 1) as u16; } exons @@ -600,7 +692,7 @@ impl AlignmentStructure { pub fn identify_introns( &self, - chromosome_names_map: &BiMap,u16>, + chromosome_names_map: &BiMap, u16>, reference_genome_fasta_file: &str ) -> Vec { let mut introns: Vec = Vec::new(); @@ -627,15 +719,15 @@ impl AlignmentStructure { let sequence_1: Box = get_fasta_sequence( &*reference_chromosome_name, - reference_start_position as usize + 1, - reference_start_position as usize + 2, + reference_start_position + 1, + reference_start_position + 2, reference_genome_fasta_file ); let sequence_2: Box = get_fasta_sequence( &*reference_chromosome_name, - reference_end_position as usize - 2, - reference_end_position as usize - 1, + reference_end_position - 2, + reference_end_position - 1, reference_genome_fasta_file ); @@ -670,34 +762,46 @@ impl AlignmentStructure { } pub fn identify_records(&self) -> Vec { - // Step 1. Cluster bases - let num_bases: usize = self.get_bases_length(); + // Step 1. Cluster Match, Mismatch, Insertion bases + let num_bases: u32 = self.get_bases_length(); let mut uf_bases: UnionFind = UnionFind::new(); for i in 0..num_bases { - let curr_base: &AlignmentStructureBase = self.get_base(i); - if !curr_base.is_embedded_insertion() { - uf_bases.union(i as usize, i as usize); + if matches!(self.get_base(i).get_kind(), + AlignmentStructureBaseKind::Match | + AlignmentStructureBaseKind::Mismatch | + AlignmentStructureBaseKind::Insertion) { + uf_bases.union(i, i); } } for i in 0..num_bases { if i > 0 { let prev_base: &AlignmentStructureBase = self.get_base(i - 1); let curr_base: &AlignmentStructureBase = self.get_base(i); - if !prev_base.is_embedded_insertion() && !curr_base.is_embedded_insertion() { + let is_prev_base_aligned: bool = matches!(prev_base.get_kind(), + AlignmentStructureBaseKind::Match | + AlignmentStructureBaseKind::Mismatch | + AlignmentStructureBaseKind::Insertion + ); + let is_curr_base_aligned: bool = matches!(curr_base.get_kind(), + AlignmentStructureBaseKind::Match | + AlignmentStructureBaseKind::Mismatch | + AlignmentStructureBaseKind::Insertion + ); + if is_prev_base_aligned && is_curr_base_aligned { if prev_base.get_context().is_some() && curr_base.get_context().is_some() { if *prev_base.get_kind() == *curr_base.get_kind() && *prev_base.get_context().as_ref().unwrap() == *curr_base.get_context().as_ref().unwrap() && prev_base.get_reference_chromosome_id().unwrap() == curr_base.get_reference_chromosome_id().unwrap() && prev_base.get_reference_position().unwrap().abs_diff(curr_base.get_reference_position().unwrap()) <= 1 && prev_base.get_reference_strand().as_ref().unwrap() == curr_base.get_reference_strand().as_ref().unwrap() { - uf_bases.union(i as usize - 1, i as usize); + uf_bases.union(i - 1, i); } } else { if *prev_base.get_kind() == *curr_base.get_kind() && prev_base.get_reference_chromosome_id().unwrap() == curr_base.get_reference_chromosome_id().unwrap() && prev_base.get_reference_position().unwrap().abs_diff(curr_base.get_reference_position().unwrap()) <= 1 && prev_base.get_reference_strand().as_ref().unwrap() == curr_base.get_reference_strand().as_ref().unwrap() { - uf_bases.union(i as usize - 1, i as usize); + uf_bases.union(i - 1, i); } } } @@ -707,25 +811,29 @@ impl AlignmentStructure { // Step 2. Record bases let mut records: Vec = Vec::new(); for cluster in uf_bases.get_clusters() { - let mut read_positions: Vec = cluster.into_iter().collect(); + let mut read_positions: Vec = cluster.into_iter().collect(); read_positions.sort(); - let bases: Vec<&AlignmentStructureBase> = read_positions.iter().map(|&i| self.get_base(i as usize)).collect(); + let bases: Vec<&AlignmentStructureBase> = read_positions.iter().map(|&i| self.get_base(i)).collect(); let first_base: &AlignmentStructureBase = bases.first().unwrap(); let last_base: &AlignmentStructureBase = bases.last().unwrap(); let mut sequence: String = String::new(); let mut base_quality_scores: Vec = Vec::new(); for base in bases.iter() { + assert_eq!( + matches!(base.get_kind(), + AlignmentStructureBaseKind::Match | + AlignmentStructureBaseKind::Mismatch | + AlignmentStructureBaseKind::Insertion + ), + true + ); sequence.push_str(base.get_nucleotide().as_str()); base_quality_scores.push(base.get_base_quality()); } - let (base_1, base_2) = if *first_base.get_kind() == AlignmentStructureBaseKind::Unaligned { + let (base_1, base_2) = if *first_base.get_reference_strand().as_ref().unwrap() == Strand::Forward { (first_base, last_base) } else { - if *first_base.get_reference_strand().as_ref().unwrap() == Strand::Forward { - (first_base, last_base) - } else { - (last_base, first_base) - } + (last_base, first_base) }; match first_base.get_kind() { AlignmentStructureBaseKind::Match => { @@ -787,37 +895,6 @@ impl AlignmentStructure { records.push(record); }, AlignmentStructureBaseKind::Insertion => { - if first_base.is_embedded_insertion() == false && last_base.is_embedded_insertion() == false { - let record: AlignmentStructureRecord = AlignmentStructureRecord::new( - first_base.get_read_position(), - last_base.get_read_position(), - sequence.as_str(), - base_quality_scores, - AlignmentStructureRecordType::Base, - AlignmentStructureKind::Base(first_base.get_kind().clone()), - first_base.get_context().as_ref().cloned().map(AlignmentStructureContext::Base), - base_1.get_reference_chromosome_id().unwrap(), - base_1.get_reference_position().unwrap(), - GraphOperationType::Downstream, - base_1.get_reference_strand().as_ref().unwrap().clone(), - base_1.get_mapping_quality().unwrap_or(0), - base_2.get_reference_chromosome_id().unwrap(), - base_2.get_reference_position().unwrap() + 1, - GraphOperationType::Upstream, - base_2.get_reference_strand().as_ref().unwrap().clone(), - base_2.get_mapping_quality().unwrap_or(0), - base_1.get_reference_gene_id().clone(), - base_1.get_reference_transcript_id().clone(), - base_1.get_reference_exon_id().clone(), - base_2.get_reference_gene_id().clone(), - base_2.get_reference_transcript_id().clone(), - base_2.get_reference_exon_id().clone(), - None - ); - records.push(record); - } - }, - AlignmentStructureBaseKind::Unaligned => { let record: AlignmentStructureRecord = AlignmentStructureRecord::new( first_base.get_read_position(), last_base.get_read_position(), @@ -825,26 +902,29 @@ impl AlignmentStructure { base_quality_scores, AlignmentStructureRecordType::Base, AlignmentStructureKind::Base(first_base.get_kind().clone()), - None, - 0, - 0, - GraphOperationType::Noop, - Strand::Unknown, - 0, - 0, - 0, - GraphOperationType::Noop, - Strand::Unknown, - 0, - None, - None, - None, - None, - None, - None, + first_base.get_context().as_ref().cloned().map(AlignmentStructureContext::Base), + base_1.get_reference_chromosome_id().unwrap(), + base_1.get_reference_position().unwrap(), + GraphOperationType::Downstream, + base_1.get_reference_strand().as_ref().unwrap().clone(), + base_1.get_mapping_quality().unwrap_or(0), + base_2.get_reference_chromosome_id().unwrap(), + base_2.get_reference_position().unwrap() + 1, + GraphOperationType::Upstream, + base_2.get_reference_strand().as_ref().unwrap().clone(), + base_2.get_mapping_quality().unwrap_or(0), + base_1.get_reference_gene_id().clone(), + base_1.get_reference_transcript_id().clone(), + base_1.get_reference_exon_id().clone(), + base_2.get_reference_gene_id().clone(), + base_2.get_reference_transcript_id().clone(), + base_2.get_reference_exon_id().clone(), None ); records.push(record); + }, + _ => { + // Do nothing } } } @@ -914,7 +994,7 @@ impl AlignmentStructure { pub fn identify_variant_records( &self, - min_mapping_quality: usize, + min_mapping_quality: u16, min_base_quality: u8, analyte_type: AnalyteType ) -> Vec { @@ -929,14 +1009,23 @@ impl AlignmentStructure { analyte_type.clone() ); - // Step 3. Identify event variant records + // Step 3. Identify terminal-softclip insertion variant records + variant_records.extend( + self.identify_terminal_softclip_insertions( + min_mapping_quality, + min_base_quality, + analyte_type.clone() + ) + ); + + // Step 4. Identify event variant records variant_records.extend(self.identify_event_variant_records( &records, min_mapping_quality, min_base_quality )); - // Step 4. Sort the variant records + // Step 5. Sort the variant records variant_records.sort_by(|a, b| { a.get_chromosome_1() .cmp(&b.get_chromosome_1()) @@ -1025,6 +1114,142 @@ impl AlignmentStructure { /// Helper functions impl AlignmentStructure { + /// Build a single `Insertion` `VariantRecord` covering read positions + /// `[start, end_inclusive]`. Mirrors the position/operation conventions + /// used by `identify_records` for Insertion-kind base clusters: + /// position_1 = base.reference_position, op_1 = Downstream + /// position_2 = base.reference_position + 1, op_2 = Upstream + fn build_softclip_insertion_record( + &self, + start: u32, + end_inclusive: u32, + min_mapping_quality: u16, + min_base_quality: u8, + ) -> Option { + let first: &AlignmentStructureBase = self.get_base(start); + + // All terminal-softclip bases set in alignment.rs::identify_breakpoint_variants + // share chromosome_id / reference_position / reference_strand / mapping_quality, + // so reading them off the first base is sufficient. + let mq: u16 = first.get_mapping_quality().unwrap(); + if mq < min_mapping_quality { + return None; + } + let chromosome_id: u16 = first.get_reference_chromosome_id().unwrap(); + let ref_position: u32 = first.get_reference_position().unwrap(); + let ref_strand: Strand = first.get_reference_strand().as_ref().unwrap().clone(); + + // Filter the inserted sequence by base quality. + let mut sequence: String = String::new(); + for i in start..=end_inclusive { + let b: &AlignmentStructureBase = self.get_base(i); + if b.get_base_quality() >= min_base_quality { + sequence.push_str(b.get_nucleotide().as_str()); + } + } + if sequence.is_empty() { + return None; + } + + let graph_operation: GraphOperation = GraphOperation::new( + chromosome_id, + ref_position, + ref_strand.clone(), + GraphOperationType::Downstream, + chromosome_id, + ref_position + 1, + ref_strand, + GraphOperationType::Upstream, + sequence.into(), + VariantType::Insertion, + ); + + Some(VariantRecord::new( + self.read_id, + start, + end_inclusive, + graph_operation, + )) + } + + /// Detect runs of `Softclip`-kind bases that touch the read termini + /// (start at read position 0 or end at the last read position) and emit + /// each as a single `Insertion` `VariantRecord`. Mid-read soft-clip runs + /// (between split alignments) are ignored — those already get a + /// `Breakpoint` event added by `identify_breakpoint_variants`. + fn identify_terminal_softclip_insertions( + &self, + min_mapping_quality: u16, + min_base_quality: u8, + analyte_type: AnalyteType + ) -> Vec { + let mut variant_records: Vec = Vec::new(); + let n: u32 = self.get_bases_length(); + if n == 0 { + return variant_records; + } + + // Leading run: contiguous Softclip bases starting at read position 0. + let mut leading_end: u32 = 0; // exclusive + while leading_end < n + && *self.get_base(leading_end).get_kind() == AlignmentStructureBaseKind::Softclip + { + leading_end += 1; + } + if leading_end > 0 { + if let Some(vr) = self.build_softclip_insertion_record( + 0, + leading_end - 1, + min_mapping_quality, + min_base_quality, + ) { + variant_records.push(vr); + } + } + + // Trailing run: contiguous Softclip bases ending at read position n-1. + let mut trailing_start: u32 = n; // inclusive + while trailing_start > 0 + && *self.get_base(trailing_start - 1).get_kind() == AlignmentStructureBaseKind::Softclip + { + trailing_start -= 1; + } + // Skip if there is no trailing run, or if the trailing run overlaps the + // leading run we already emitted (entire read is one softclip span). + if trailing_start < n && trailing_start > leading_end { + if let Some(vr) = self.build_softclip_insertion_record( + trailing_start, + n - 1, + min_mapping_quality, + min_base_quality, + ) { + variant_records.push(vr); + } + } + + // Exclude template-switching artifacts if the analyte type is RNA + if analyte_type == AnalyteType::RNA { + variant_records.retain(|vr| { + let sequence: String = vr.get_sequence().to_uppercase(); + if sequence.len() <= 3 { + if sequence.contains("CC") || + sequence.contains("GG") || + sequence.contains("GG") || + sequence.contains("CC") || + sequence.contains("GG") { + return false; + } else { + return true; + } + } else { + return true; + } + }); + } + + variant_records + } + /// Identifies base variant records. /// /// This function identifies the following variant types: @@ -1044,7 +1269,7 @@ impl AlignmentStructure { fn identify_base_variant_records( &self, records: &Vec, - min_mapping_quality: usize, + min_mapping_quality: u16, min_base_quality: u8, analyte_type: AnalyteType ) -> Vec { @@ -1071,7 +1296,6 @@ impl AlignmentStructure { (curr_record.get_start() == 0 || curr_record.get_end() == self.get_bases_length() - 1) { continue; } - let sequence: String = curr_record .get_sequence() .chars() @@ -1084,7 +1308,7 @@ impl AlignmentStructure { continue; } - let decrement: usize = sequence.len().abs_diff(curr_record.get_sequence().len()) as usize; + let decrement: u32 = sequence.len().abs_diff(curr_record.get_sequence().len()) as u32; let variant_type = if sequence.len() == 1 { VariantType::SingleNucleotideVariant @@ -1115,17 +1339,6 @@ impl AlignmentStructure { ); }, AlignmentStructureKind::Base(AlignmentStructureBaseKind::Insertion) => { - // Exclude template-switching artifacts if the analyte type is RNA - if analyte_type == AnalyteType::RNA && - (curr_record.get_start() == 0 || curr_record.get_end() == self.get_bases_length() - 1) && - curr_record.get_sequence().len() <= 3 && - (curr_record.get_sequence().to_uppercase().contains("AA") || - curr_record.get_sequence().to_uppercase().contains("CC") || - curr_record.get_sequence().to_uppercase().contains("GG") || - curr_record.get_sequence().to_uppercase().contains("TT")) { - continue; - } - let sequence: String = curr_record .get_sequence() .chars() @@ -1166,17 +1379,26 @@ impl AlignmentStructure { } } + // Return the current vector of VariantRecord objects for DNA variant calling if analyte_type == AnalyteType::DNA { return variant_records; } - // Step 2. Identify variant records based on the AlignmentStructureBase contexts + // Step 2. Get a set of all included read bases + let mut included_read_bases: HashSet<(u32, u32)> = HashSet::new(); + for variant_record in variant_records.iter() { + included_read_bases.insert( + (variant_record.read_position_1, variant_record.read_position_2) + ); + } + + // Step 3. Identify variant records based on the AlignmentStructureBase contexts - RNA variant calling let mut uf: UnionFind = UnionFind::new(); for i in 0..records.len() { let record: &AlignmentStructureRecord = records.get(i).unwrap(); if *record.get_record_type() == AlignmentStructureRecordType::Base && *record.get_context().as_ref().unwrap() != AlignmentStructureContext::Base(AlignmentStructureBaseContext::Exonic) { - uf.union(i, i); + uf.union(i as u32, i as u32); } } for i in 1..records.len() { @@ -1200,7 +1422,7 @@ impl AlignmentStructure { if *base_1.get_context().as_ref().unwrap() != AlignmentStructureBaseContext::Exonic && *base_1.get_context().as_ref().unwrap() == *base_2.get_context().as_ref().unwrap() && *base_1.get_context().as_ref().unwrap() == *curr_record.get_context().as_ref().unwrap().as_base().unwrap() { - uf.union(i - 1, i); + uf.union(i as u32 - 1, i as u32); } } }, @@ -1218,7 +1440,7 @@ impl AlignmentStructure { if *base_1.get_context().as_ref().unwrap() != AlignmentStructureBaseContext::Exonic && *base_1.get_context().as_ref().unwrap() == *base_2.get_context().as_ref().unwrap() && *base_1.get_context().as_ref().unwrap() == *prev_record.get_context().as_ref().unwrap().as_base().unwrap() { - uf.union(i - 1, i); + uf.union(i as u32 - 1, i as u32); } } } @@ -1232,26 +1454,26 @@ impl AlignmentStructure { prev_record.get_strand_2() == curr_record.get_strand_2() && (prev_record.get_position_1().abs_diff(curr_record.get_position_2()) <= 1 || prev_record.get_position_2().abs_diff(curr_record.get_position_1()) <= 1) { - uf.union(i - 1, i); + uf.union(i as u32 - 1, i as u32); } } } } for cluster in uf.get_clusters().iter() { // Sort the record positions - let mut record_indices: Vec = cluster.iter().map(|&pos| pos as usize).collect(); + let mut record_indices: Vec = cluster.iter().map(|&pos| pos).collect(); record_indices.sort(); let first_record: &AlignmentStructureRecord = records.get(*record_indices.first().unwrap() as usize).unwrap(); let last_record: &AlignmentStructureRecord = records.get(*record_indices.last().unwrap() as usize).unwrap(); - let prev_record: Option<&AlignmentStructureRecord> = if *record_indices.first().unwrap() > 0usize { + let prev_record: Option<&AlignmentStructureRecord> = if *record_indices.first().unwrap() > 0u32 { records.get(record_indices[0] as usize - 1) } else { None }; - let next_record: Option<&AlignmentStructureRecord> = if *record_indices.last().unwrap() < records.len() as usize - 1 { + let next_record: Option<&AlignmentStructureRecord> = if *record_indices.last().unwrap() < records.len() as u32 - 1 { records.get(*record_indices.last().unwrap() as usize + 1) } else { None @@ -1328,14 +1550,16 @@ impl AlignmentStructure { ) }; - variant_records.push( - VariantRecord::new( - self.read_id, - first_record.get_start(), - last_record.get_end(), - graph_operation - ) - ); + if included_read_bases.contains(&(first_record.get_start(), last_record.get_end())) == false { + variant_records.push( + VariantRecord::new( + self.read_id, + first_record.get_start(), + last_record.get_end(), + graph_operation + ) + ); + } } variant_records @@ -1360,7 +1584,7 @@ impl AlignmentStructure { fn identify_event_variant_records( &self, records: &Vec, - min_mapping_quality: usize, + min_mapping_quality: u16, min_base_quality: u8 ) -> Vec { let mut variant_records: Vec = Vec::new(); @@ -1377,9 +1601,9 @@ impl AlignmentStructure { match record.get_kind() { AlignmentStructureKind::Event(AlignmentStructureEventKind::Breakpoint) => { // Get sequence - let sequence = record.get_sequence(); - let base_quality_scores = record.get_base_quality_scores(); - let mut filtered_sequence = String::new(); + let sequence: &Box = record.get_sequence(); + let base_quality_scores: &Vec = record.get_base_quality_scores(); + let mut filtered_sequence: String = String::new(); for (base, &quality) in sequence.chars().zip(base_quality_scores) { if quality >= min_base_quality { filtered_sequence.push(base); @@ -1440,8 +1664,8 @@ impl AlignmentStructure { if record.get_skipped().is_some() { for reference_bases in record.get_skipped().as_ref().unwrap().iter() { let reference_chromosome_id: u16 = reference_bases.first().unwrap().reference_chromosome_id; - let reference_position_1: usize = reference_bases.first().unwrap().reference_position; - let reference_position_2: usize = reference_bases.last().unwrap().reference_position; + let reference_position_1: u32 = reference_bases.first().unwrap().reference_position; + let reference_position_2: u32 = reference_bases.last().unwrap().reference_position; let reference_strand: &Strand = &reference_bases.first().unwrap().reference_strand; let sequence: String = if reference_bases.first().unwrap().reference_strand == Strand::Forward { reference_bases @@ -1483,8 +1707,8 @@ impl AlignmentStructure { AlignmentStructureKind::Event(AlignmentStructureEventKind::Boundary) => { for reference_bases in record.get_skipped().as_ref().unwrap().iter() { let reference_chromosome_id: u16 = reference_bases.first().unwrap().reference_chromosome_id; - let reference_position_1: usize = reference_bases.first().unwrap().reference_position; - let reference_position_2: usize = reference_bases.last().unwrap().reference_position; + let reference_position_1: u32 = reference_bases.first().unwrap().reference_position; + let reference_position_2: u32 = reference_bases.last().unwrap().reference_position; let reference_strand: &Strand = &reference_bases.first().unwrap().reference_strand; let sequence: String = if reference_bases.first().unwrap().reference_strand == Strand::Forward { reference_bases @@ -1549,9 +1773,9 @@ impl AlignmentStructure { if record.get_context().is_some() { if record.get_context().as_ref().unwrap() == &AlignmentStructureContext::Event(AlignmentStructureEventContext::FusionGene) { // Get sequence - let sequence = record.get_sequence(); - let base_quality_scores = record.get_base_quality_scores(); - let mut filtered_sequence = String::new(); + let sequence: &Box = record.get_sequence(); + let base_quality_scores: &Vec = record.get_base_quality_scores(); + let mut filtered_sequence: String = String::new(); for (base, &quality) in sequence.chars().zip(base_quality_scores) { if quality >= min_base_quality { filtered_sequence.push(base); @@ -1585,8 +1809,8 @@ impl AlignmentStructure { if record.get_skipped().is_some() { for reference_bases in record.get_skipped().as_ref().unwrap().iter() { let reference_chromosome_id: u16 = reference_bases.first().unwrap().reference_chromosome_id; - let reference_position_1: usize = reference_bases.first().unwrap().reference_position; - let reference_position_2: usize = reference_bases.last().unwrap().reference_position; + let reference_position_1: u32 = reference_bases.first().unwrap().reference_position; + let reference_position_2: u32 = reference_bases.last().unwrap().reference_position; let reference_strand: &Strand = &reference_bases.first().unwrap().reference_strand; let sequence: String = if reference_bases.first().unwrap().reference_strand == Strand::Forward { reference_bases diff --git a/exacto/exacto-caller/src/structs/alignment_structure_base.rs b/exacto/exacto-caller/src/structs/alignment_structure_base.rs index f1b9abf..eb2773c 100644 --- a/exacto/exacto-caller/src/structs/alignment_structure_base.rs +++ b/exacto/exacto-caller/src/structs/alignment_structure_base.rs @@ -19,16 +19,14 @@ use crate::prelude::*; #[derive(Debug,Serialize,Deserialize)] pub struct AlignmentStructureBase { - read_position: usize, + read_position: u32, nucleotide: Nucleotide, base_quality: u8, kind: AlignmentStructureBaseKind, - is_soft_clipped: bool, - is_embedded_insertion: bool, - mapping_quality: Option, + mapping_quality: Option, context: Option, reference_chromosome_id: Option, - reference_position: Option, + reference_position: Option, reference_strand: Option, reference_gene_id: Option>, reference_transcript_id: Option>, @@ -38,7 +36,7 @@ pub struct AlignmentStructureBase { // API methods impl AlignmentStructureBase { pub fn new( - read_position: usize, + read_position: u32, nucleotide: Nucleotide, base_quality: u8 ) -> Self { @@ -47,8 +45,8 @@ impl AlignmentStructureBase { nucleotide, base_quality, kind: AlignmentStructureBaseKind::Unaligned, - is_soft_clipped: false, - is_embedded_insertion: false, + // is_soft_clipped: false, + // is_embedded_insertion: false, mapping_quality: None, context: None, reference_chromosome_id: None, @@ -72,7 +70,7 @@ impl AlignmentStructureBase { &self.kind } - pub fn get_mapping_quality(&self) -> &Option { + pub fn get_mapping_quality(&self) -> &Option { &self.mapping_quality } @@ -80,7 +78,7 @@ impl AlignmentStructureBase { &self.nucleotide } - pub fn get_read_position(&self) -> usize { + pub fn get_read_position(&self) -> u32 { self.read_position } @@ -96,7 +94,7 @@ impl AlignmentStructureBase { &self.reference_gene_id } - pub fn get_reference_position(&self) -> &Option { + pub fn get_reference_position(&self) -> &Option { &self.reference_position } @@ -107,32 +105,16 @@ impl AlignmentStructureBase { pub fn get_reference_transcript_id(&self) -> &Option> { &self.reference_transcript_id } - - pub fn is_embedded_insertion(&self) -> bool { - self.is_embedded_insertion - } - - pub fn is_soft_clipped(&self) -> bool { - self.is_soft_clipped - } pub fn set_context(&mut self, context: AlignmentStructureBaseContext) { self.context = Some(context); } - - pub fn set_is_soft_clipped(&mut self, value: bool) { - self.is_soft_clipped = value; - } - - pub fn set_is_embedded_insertion(&mut self, value: bool) { - self.is_embedded_insertion = value; - } pub fn set_kind(&mut self, kind: AlignmentStructureBaseKind) { self.kind = kind; } - pub fn set_mapping_quality(&mut self, value: usize) { + pub fn set_mapping_quality(&mut self, value: u16) { self.mapping_quality = Some(value); } @@ -148,7 +130,7 @@ impl AlignmentStructureBase { self.reference_gene_id = Some(value.into()); } - pub fn set_reference_position(&mut self, value: usize) { + pub fn set_reference_position(&mut self, value: u32) { self.reference_position = Some(value); } @@ -168,8 +150,6 @@ impl Clone for AlignmentStructureBase { nucleotide: self.nucleotide.clone(), base_quality: self.base_quality, kind: self.kind.clone(), - is_soft_clipped: self.is_soft_clipped, - is_embedded_insertion: self.is_embedded_insertion, mapping_quality: self.mapping_quality, context: self.context.clone(), reference_chromosome_id: self.reference_chromosome_id, diff --git a/exacto/exacto-caller/src/structs/alignment_structure_event.rs b/exacto/exacto-caller/src/structs/alignment_structure_event.rs index 7736fe5..461de73 100644 --- a/exacto/exacto-caller/src/structs/alignment_structure_event.rs +++ b/exacto/exacto-caller/src/structs/alignment_structure_event.rs @@ -21,8 +21,8 @@ use crate::prelude::*; #[derive(Debug,Serialize,Deserialize)] pub struct AlignmentStructureEvent { - prev_read_position: usize, - next_read_position: usize, + prev_read_position: u32, + next_read_position: u32, prev_graph_operation_type: GraphOperationType, next_graph_operation_type: GraphOperationType, kind: AlignmentStructureEventKind, @@ -60,8 +60,8 @@ impl Eq for AlignmentStructureEvent {} impl AlignmentStructureEvent { pub fn new( kind: AlignmentStructureEventKind, - prev_read_position: usize, - next_read_position: usize, + prev_read_position: u32, + next_read_position: u32, prev_graph_operation_type: GraphOperationType, next_graph_operation_type: GraphOperationType ) -> Self { @@ -102,18 +102,18 @@ impl AlignmentStructureEvent { &self.kind } - pub fn get_prev_read_position(&self) -> usize { + pub fn get_prev_read_position(&self) -> u32 { self.prev_read_position } + pub fn get_next_read_position(&self) -> u32 { + self.next_read_position + } + pub fn get_prev_graph_operation_type(&self) -> &GraphOperationType { &self.prev_graph_operation_type } - - pub fn get_next_read_position(&self) -> usize { - self.next_read_position - } - + pub fn get_next_graph_operation_type(&self) -> &GraphOperationType { &self.next_graph_operation_type } @@ -128,10 +128,10 @@ impl AlignmentStructureEvent { // Cluster reference bases by proximity let mut uf: UnionFind = UnionFind::new(); for i in 0..bases.len() { - uf.union(i, i); + uf.union(i as u32, i as u32); if i > 0 { if bases[i].reference_position.abs_diff(bases[i - 1].reference_position) == 1 { - uf.union(i - 1, i); + uf.union(i as u32 - 1, i as u32); } } } @@ -139,12 +139,12 @@ impl AlignmentStructureEvent { // Get the clusters for cluster in uf.get_clusters() { // Sort the cluster - let mut indices: Vec = cluster.into_iter().collect(); + let mut indices: Vec = cluster.into_iter().collect(); indices.sort(); // Get the cluster bases let selected: Vec = indices.iter() - .map(|&i| bases.get(i).unwrap().clone()) + .map(|&i| bases.get(i as usize).unwrap().clone()) .collect(); clusters.push(selected); @@ -153,7 +153,7 @@ impl AlignmentStructureEvent { // Sort clusters.sort_by_key(|v| { - let first = v.first().unwrap(); + let first: &ReferenceBase = v.first().unwrap(); (first.reference_chromosome_id, first.reference_position) }); diff --git a/exacto/exacto-caller/src/structs/alignment_structure_record.rs b/exacto/exacto-caller/src/structs/alignment_structure_record.rs index e47af25..ff27606 100644 --- a/exacto/exacto-caller/src/structs/alignment_structure_record.rs +++ b/exacto/exacto-caller/src/structs/alignment_structure_record.rs @@ -21,23 +21,23 @@ use crate::prelude::*; #[derive(Debug,Serialize,Deserialize)] pub struct AlignmentStructureRecord { - start: usize, // read position - end: usize, // read position + start: u32, // read position + end: u32, // read position sequence: Box, base_quality_scores: Vec, record_type: AlignmentStructureRecordType, kind: AlignmentStructureKind, context: Option, chromosome_1: u16, - position_1: usize, + position_1: u32, operation_1: GraphOperationType, strand_1: Strand, - mapping_quality_1: usize, + mapping_quality_1: u16, chromosome_2: u16, - position_2: usize, + position_2: u32, operation_2: GraphOperationType, strand_2: Strand, - mapping_quality_2: usize, + mapping_quality_2: u16, gene_id_1: Option>, transcript_id_1: Option>, exon_id_1: Option>, @@ -50,23 +50,23 @@ pub struct AlignmentStructureRecord { /// API methods impl AlignmentStructureRecord { pub fn new( - start: usize, - end: usize, + start: u32, + end: u32, sequence: &str, base_quality_scores: Vec, record_type: AlignmentStructureRecordType, kind: AlignmentStructureKind, context: Option, chromosome_1: u16, - position_1: usize, + position_1: u32, operation_1: GraphOperationType, strand_1: Strand, - mapping_quality_1: usize, + mapping_quality_1: u16, chromosome_2: u16, - position_2: usize, + position_2: u32, operation_2: GraphOperationType, strand_2: Strand, - mapping_quality_2: usize, + mapping_quality_2: u16, gene_id_1: Option>, transcript_id_1: Option>, exon_id_1: Option>, @@ -98,20 +98,20 @@ impl AlignmentStructureRecord { strand_2: strand_2, mapping_quality_2: mapping_quality_2, gene_id_1: gene_id_1, - transcript_id_1, - exon_id_1, - gene_id_2, - transcript_id_2, - exon_id_2, - skipped + transcript_id_1: transcript_id_1, + exon_id_1: exon_id_1, + gene_id_2: gene_id_2, + transcript_id_2: transcript_id_2, + exon_id_2: exon_id_2, + skipped: skipped } } - pub fn get_start(&self) -> usize { + pub fn get_start(&self) -> u32 { self.start } - pub fn get_end(&self) -> usize { + pub fn get_end(&self) -> u32 { self.end } @@ -139,7 +139,7 @@ impl AlignmentStructureRecord { self.chromosome_1 } - pub fn get_position_1(&self) -> usize { + pub fn get_position_1(&self) -> u32 { self.position_1 } @@ -151,7 +151,7 @@ impl AlignmentStructureRecord { &self.strand_1 } - pub fn get_mapping_quality_1(&self) -> usize { + pub fn get_mapping_quality_1(&self) -> u16 { self.mapping_quality_1 } @@ -159,7 +159,7 @@ impl AlignmentStructureRecord { self.chromosome_2 } - pub fn get_position_2(&self) -> usize { + pub fn get_position_2(&self) -> u32 { self.position_2 } @@ -171,7 +171,7 @@ impl AlignmentStructureRecord { &self.strand_2 } - pub fn get_mapping_quality_2(&self) -> usize { + pub fn get_mapping_quality_2(&self) -> u16 { self.mapping_quality_2 } diff --git a/exacto/exacto-caller/src/structs/dna_variant_call_set.rs b/exacto/exacto-caller/src/structs/dna_variant_call_set.rs index 1711d36..96d29b7 100644 --- a/exacto/exacto-caller/src/structs/dna_variant_call_set.rs +++ b/exacto/exacto-caller/src/structs/dna_variant_call_set.rs @@ -41,7 +41,7 @@ pub struct DNAVariantCallSet { /// Nested structure for position indexing: /// - Outer HashMap: Maps chromosome IDs to their position index /// - Inner BTreeMap: Maps positions to variant call IDs - position_index: HashMap>> + position_index: HashMap>> } impl DNAVariantCallSet { @@ -59,8 +59,8 @@ impl DNAVariantCallSet { let consensus_record = variant_call.get_consensus_record().0; let chromosome_1_id: u16 = consensus_record.get_chromosome_1(); let chromosome_2_id: u16 = consensus_record.get_chromosome_2(); - let position_1: usize = consensus_record.graph_operation.get_position_1(); - let position_2: usize = consensus_record.graph_operation.get_position_2(); + let position_1: u32 = consensus_record.graph_operation.get_position_1(); + let position_2: u32 = consensus_record.graph_operation.get_position_2(); // Index the variant by its first breakpoint position self.position_index @@ -91,7 +91,7 @@ impl DNAVariantCallSet { self.variant_calls.values().collect() } - pub fn get_variant_calls_by_range(&self, chromosome_id: u16, start: usize, end: usize) -> Vec<&VariantCall> { + pub fn get_variant_calls_by_range(&self, chromosome_id: u16, start: u32, end: u32) -> Vec<&VariantCall> { let mut result_ids = HashSet::new(); if let Some(position_map) = self.position_index.get(&chromosome_id) { @@ -137,16 +137,36 @@ impl DNAVariantCallSet { "self.read_names_map is empty." ); + let variant_calls: Vec<&VariantCall> = self.variant_calls.values().collect(); + + if variant_calls.is_empty() { + return DataFrame::new(vec![ + Column::from(Series::new("variant_call_id".into(), Vec::::new())), + Column::from(Series::new("chromosome_1".into(), Vec::<&str>::new())), + Column::from(Series::new("position_1".into(), Vec::::new())), + Column::from(Series::new("strand_1".into(), Vec::<&str>::new())), + Column::from(Series::new("operation_1".into(), Vec::<&str>::new())), + Column::from(Series::new("chromosome_2".into(), Vec::<&str>::new())), + Column::from(Series::new("position_2".into(), Vec::::new())), + Column::from(Series::new("strand_2".into(), Vec::<&str>::new())), + Column::from(Series::new("operation_2".into(), Vec::<&str>::new())), + Column::from(Series::new("variant_size".into(), Vec::::new())), + Column::from(Series::new("variant_type".into(), Vec::::new())), + Column::from(Series::new("sequence".into(), Vec::::new())), + Column::from(Series::new("consensus_read_names".into(), Vec::::new())), + Column::from(Series::new("consensus_read_names_count".into(), Vec::::new())), + Column::from(Series::new("read_names".into(), Vec::::new())), + Column::from(Series::new("read_names_count".into(), Vec::::new())) + ]).unwrap(); + } + let thread_pool = ThreadPoolBuilder::new() .num_threads(num_threads) .build() .unwrap(); - let mut variant_calls: Vec<&VariantCall> = Vec::new(); - for variant_call in self.variant_calls.values() { - variant_calls.push(variant_call); - } let chunk_size = (variant_calls.len() + num_threads - 1) / num_threads; + let rows: Vec<_> = thread_pool.install(|| { variant_calls .par_chunks(chunk_size) @@ -201,13 +221,12 @@ impl DNAVariantCallSet { Column::from(Series::new("operation_2".into(), rows.iter().map(|r| r.8).collect::>())), Column::from(Series::new("variant_size".into(), rows.iter().map(|r| r.9).collect::>())), Column::from(Series::new("variant_type".into(), rows.iter().map(|r| r.10.clone()).collect::>())), - Column::from(Series::new("variant_sequence".into(), rows.iter().map(|r| r.11.clone()).collect::>())), + Column::from(Series::new("sequence".into(), rows.iter().map(|r| r.11.clone()).collect::>())), Column::from(Series::new("consensus_read_names".into(), rows.iter().map(|r| r.12.clone()).collect::>())), Column::from(Series::new("consensus_read_names_count".into(), rows.iter().map(|r| r.13).collect::>())), Column::from(Series::new("read_names".into(), rows.iter().map(|r| r.14.clone()).collect::>())), Column::from(Series::new("read_names_count".into(), rows.iter().map(|r| r.15).collect::>())) - ]) - .unwrap() + ]).unwrap() } pub fn to_tsv_file(&self, output_file: &str, num_threads: usize) { diff --git a/exacto/exacto-caller/src/structs/graph_operation.rs b/exacto/exacto-caller/src/structs/graph_operation.rs index c3a927e..94d5391 100644 --- a/exacto/exacto-caller/src/structs/graph_operation.rs +++ b/exacto/exacto-caller/src/structs/graph_operation.rs @@ -22,11 +22,11 @@ use crate::prelude::*; #[derive(Debug,Eq,PartialEq,Serialize,Deserialize)] pub struct GraphOperation { chromosome_1: u16, - position_1: usize, + position_1: u32, strand_1: Strand, operation_type_1: GraphOperationType, chromosome_2: u16, - position_2: usize, + position_2: u32, strand_2: Strand, operation_type_2: GraphOperationType, sequence: Box, @@ -42,7 +42,7 @@ impl Hash for GraphOperation { self.chromosome_2.hash(state); self.position_2.hash(state); self.strand_2.hash(state); - self.operation_type_1.hash(state); + self.operation_type_2.hash(state); self.sequence.hash(state); self.variant_type.hash(state); } @@ -52,11 +52,11 @@ impl Hash for GraphOperation { impl GraphOperation { pub fn new( chromosome_1: u16, - position_1: usize, + position_1: u32, strand_1: Strand, operation_type_1: GraphOperationType, chromosome_2: u16, - position_2: usize, + position_2: u32, strand_2: Strand, operation_type_2: GraphOperationType, sequence: Box, @@ -72,7 +72,7 @@ impl GraphOperation { strand_2, operation_type_2, sequence, - variant_type, + variant_type }; Self::standardize(unstandardized) } @@ -137,11 +137,11 @@ impl GraphOperation { self.chromosome_2 } - pub fn get_position_1(&self) -> usize { + pub fn get_position_1(&self) -> u32 { self.position_1 } - pub fn get_position_2(&self) -> usize { + pub fn get_position_2(&self) -> u32 { self.position_2 } @@ -201,7 +201,7 @@ impl GraphOperation { self.get_sequence_length() as isize }, VariantType::IntronRetention => { - (self.position_2.abs_diff(self.position_1) as isize) - 1 + (self.position_2.abs_diff(self.position_1) as isize) + 1 }, VariantType::MultiNucleotideVariant => { self.get_sequence_length() as isize diff --git a/exacto/exacto-caller/src/structs/graph_operation_view.rs b/exacto/exacto-caller/src/structs/graph_operation_view.rs new file mode 100644 index 0000000..8eb0072 --- /dev/null +++ b/exacto/exacto-caller/src/structs/graph_operation_view.rs @@ -0,0 +1,228 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +use exacto_core::prelude::*; +use serde::{Serialize, Deserialize}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use crate::prelude::*; + + +#[derive(Debug,Eq,PartialEq,Serialize,Deserialize)] +pub struct GraphOperationView { + chromosome_1: Arc, + position_1: u32, + strand_1: Strand, + operation_type_1: GraphOperationType, + chromosome_2: Arc, + position_2: u32, + strand_2: Strand, + operation_type_2: GraphOperationType, + sequence: Arc, + num_cycles: Option +} + +impl Hash for GraphOperationView { + fn hash(&self, state: &mut H) { + self.chromosome_1.hash(state); + self.position_1.hash(state); + self.strand_1.hash(state); + self.operation_type_1.hash(state); + self.chromosome_2.hash(state); + self.position_2.hash(state); + self.strand_2.hash(state); + self.operation_type_2.hash(state); + self.sequence.hash(state); + self.num_cycles.hash(state); + } +} + +impl GraphOperationView { + pub fn new( + chromosome_1: &str, + position_1: u32, + operation_type_1: &GraphOperationType, + strand_1: &Strand, + chromosome_2: &str, + position_2: u32, + operation_type_2: &GraphOperationType, + strand_2: &Strand, + sequence: &str, + num_cycles: Option + ) -> Self { + if *operation_type_1 == GraphOperationType::Upstream && + *operation_type_2 == GraphOperationType::Downstream && + chromosome_1 == chromosome_2 && + position_1 < position_2 { + assert!( + num_cycles.is_some() == true, + "A cycle creating operation must have a number of cycles: {}:{}:{}:{}:{}:{}:{}:{}:{}:{:?}", + chromosome_1, + position_1, + operation_type_1.as_str(), + strand_1.as_str(), + chromosome_2, + position_2, + operation_type_2.as_str(), + strand_2.as_str(), + sequence, + num_cycles + ); + } + + let unstandardized: GraphOperationView = Self { + chromosome_1: chromosome_1.into(), + position_1: position_1, + operation_type_1: operation_type_1.clone(), + strand_1: strand_1.clone(), + chromosome_2: chromosome_2.into(), + position_2: position_2, + operation_type_2: operation_type_2.clone(), + strand_2: strand_2.clone(), + sequence: sequence.into(), + num_cycles: num_cycles + }; + Self::standardize(unstandardized) + } + + pub fn get_chromosome_1(&self) -> &str { + &*self.chromosome_1 + } + + pub fn get_chromosome_2(&self) -> &str { + &*self.chromosome_2 + } + + pub fn get_position_1(&self) -> u32 { + self.position_1 + } + + pub fn get_position_2(&self) -> u32 { + self.position_2 + } + + pub fn get_strand_1(&self) -> &Strand { + &self.strand_1 + } + + pub fn get_strand_2(&self) -> &Strand { + &self.strand_2 + } + + pub fn get_operation_type_1(&self) -> &GraphOperationType { + &self.operation_type_1 + } + + pub fn get_operation_type_2(&self) -> &GraphOperationType { + &self.operation_type_2 + } + + pub fn get_sequence(&self) -> &str { + &*self.sequence + } + + pub fn get_sequence_length(&self) -> usize { + self.sequence.len() + } + + pub fn get_standardized_sequence(&self) -> Box { + if self.strand_1 == Strand::Forward { + self.sequence.to_string().to_uppercase().into() + } else { + reverse_complement(&self.sequence).to_string().to_uppercase().into() + } + } + + pub fn get_num_cycles(&self) -> &Option { + &self.num_cycles + } + + pub fn get_variant_type(&self) -> VariantType { + if self.chromosome_1 == self.chromosome_2 { + if self.operation_type_1 == GraphOperationType::Downstream && + self.operation_type_2 == GraphOperationType::Upstream { + if self.sequence.len() == 0 { + VariantType::Deletion + } else if self.sequence.len() == 1 { + if self.position_1.abs_diff(self.position_2) == 2 { + VariantType::SingleNucleotideVariant + } else { + VariantType::Insertion + } + } else { + if self.position_1.abs_diff(self.position_2) == self.sequence.len() as u32 + 1 { + VariantType::MultiNucleotideVariant + } else { + VariantType::Insertion + } + } + } else if self.operation_type_1 == GraphOperationType::Upstream && self.operation_type_2 == GraphOperationType::Downstream { + VariantType::Breakpoint + } else if self.operation_type_1 == GraphOperationType::Downstream && self.operation_type_2 == GraphOperationType::Downstream { + VariantType::Breakpoint + } else if self.operation_type_1 == GraphOperationType::Upstream && self.operation_type_2 == GraphOperationType::Upstream { + VariantType::Breakpoint + } else { + panic!("Unsupported operation types: {:?} {:?}", self.operation_type_1, self.operation_type_2) + } + } else { + VariantType::Translocation + } + } +} + +/// Helper methods +impl GraphOperationView { + fn standardize(op: GraphOperationView) -> GraphOperationView { + let should_swap: bool = if op.chromosome_1 != op.chromosome_2 { + op.chromosome_1 > op.chromosome_2 + } else { + op.position_1 > op.position_2 + }; + + if should_swap { + GraphOperationView { + chromosome_1: op.chromosome_2, + position_1: op.position_2, + operation_type_1: op.operation_type_2, + strand_1: op.strand_2, + chromosome_2: op.chromosome_1, + position_2: op.position_1, + operation_type_2: op.operation_type_1, + strand_2: op.strand_1, + sequence: op.sequence, + num_cycles: op.num_cycles + } + } else { + op + } + } +} + +impl Clone for GraphOperationView { + fn clone(&self) -> Self { + GraphOperationView { + chromosome_1: self.chromosome_1.clone(), + position_1: self.position_1, + operation_type_1: self.operation_type_1.clone(), + strand_1: self.strand_1.clone(), + chromosome_2: self.chromosome_2.clone(), + position_2: self.position_2, + operation_type_2: self.operation_type_2.clone(), + strand_2: self.strand_2.clone(), + sequence: self.sequence.clone(), + num_cycles: self.num_cycles + } + } +} diff --git a/exacto/exacto-caller/src/structs/mod.rs b/exacto/exacto-caller/src/structs/mod.rs index db1cb98..04e8859 100644 --- a/exacto/exacto-caller/src/structs/mod.rs +++ b/exacto/exacto-caller/src/structs/mod.rs @@ -10,6 +10,7 @@ pub mod reference_transcript_match; pub mod reference_base; pub mod reference_transcript_sequence; pub mod graph_operation; +pub mod graph_operation_view; pub mod transcript_model; pub mod transcript_model_exon; pub mod transcript_model_intron; diff --git a/exacto/exacto-caller/src/structs/mutant_peptides_set.rs b/exacto/exacto-caller/src/structs/mutant_peptides_set.rs index 4cee348..536bd19 100644 --- a/exacto/exacto-caller/src/structs/mutant_peptides_set.rs +++ b/exacto/exacto-caller/src/structs/mutant_peptides_set.rs @@ -15,7 +15,7 @@ use flate2::Compression; use flate2::write::GzEncoder; use polars::prelude::*; use rayon::prelude::*; -use rayon::ThreadPoolBuilder; +use rayon::{ThreadPool, ThreadPoolBuilder}; use serde::{Serialize, Deserialize}; use std::fs::File; use std::io::{BufWriter, Write}; @@ -96,9 +96,9 @@ impl MutantPeptidesSet { } pub fn to_tsv(&self, file: &str, buffer_size: usize, num_threads: usize, gzip: bool) { - let file = File::create(file).unwrap(); + let file: File = File::create(file).unwrap(); - let thread_pool = ThreadPoolBuilder::new() + let thread_pool: ThreadPool = ThreadPoolBuilder::new() .num_threads(num_threads) .build() .unwrap(); @@ -140,21 +140,25 @@ impl MutantPeptidesSet { } else { let writer = Arc::new(Mutex::new(BufWriter::new(file))); writer.lock().unwrap().write_all(header.as_bytes()).unwrap(); - thread_pool.scope(|s| { - for (chunk_idx, chunk) in mutant_peptides.chunks(buffer_size).enumerate() { - let writer = writer.clone(); - s.spawn(move |_| { + let chunks: Vec<&[&MutantPeptide]> = mutant_peptides.chunks(buffer_size).collect(); + let buffers: Vec = thread_pool.install(|| { + chunks + .par_iter() + .map(|chunk| { let mut local_buffer = String::new(); - for (i, mutant_peptide) in chunk.iter().enumerate() { + for mutant_peptide in chunk.iter() { let row = mutant_peptide.to_tsv_string(); local_buffer.push_str(&row); } - let mut writer_guard = writer.lock().unwrap(); - writer_guard.write_all(local_buffer.as_bytes()).unwrap(); - }); - } + local_buffer + }) + .collect() }); - writer.lock().unwrap().flush().unwrap(); + let mut writer_guard = writer.lock().unwrap(); + for buffer in buffers { + writer_guard.write_all(buffer.as_bytes()).unwrap(); + } + writer_guard.flush().unwrap(); } } } diff --git a/exacto/exacto-caller/src/structs/reference_base.rs b/exacto/exacto-caller/src/structs/reference_base.rs index 83f6d42..a4c1b2f 100644 --- a/exacto/exacto-caller/src/structs/reference_base.rs +++ b/exacto/exacto-caller/src/structs/reference_base.rs @@ -20,23 +20,19 @@ use crate::prelude::*; #[derive(Debug, Serialize, Deserialize)] pub struct ReferenceBase { pub reference_chromosome_id: u16, - pub reference_position: usize, - - /// Reference nucleotide on the reference (forward or reverse) strand. - pub reference_nucleotide: Nucleotide, - + pub reference_position: u32, + pub reference_nucleotide: Nucleotide, // Reference nucleotide on the reference (forward or reverse) strand. pub reference_strand: Strand, - pub reference_gene_id: Option>, pub reference_transcript_id: Option>, - pub reference_exon_id: Option>, + pub reference_exon_id: Option> } /// API methods impl ReferenceBase { pub fn new( reference_chromosome_id: u16, - reference_position: usize, + reference_position: u32, reference_nucleotide: Nucleotide, reference_strand: Strand, reference_gene_id: Option>, diff --git a/exacto/exacto-caller/src/structs/reference_transcript_match.rs b/exacto/exacto-caller/src/structs/reference_transcript_match.rs index f6b407b..0ba8ccc 100644 --- a/exacto/exacto-caller/src/structs/reference_transcript_match.rs +++ b/exacto/exacto-caller/src/structs/reference_transcript_match.rs @@ -22,9 +22,9 @@ use crate::prelude::*; pub struct ReferenceTranscriptMatch { pub reference_gene_id: Box, pub reference_transcript_id: Box, - pub num_overlap_bases: usize, - pub num_transcript_only_bases: usize, - pub num_reference_only_bases: usize, + pub num_overlap_bases: u32, + pub num_transcript_only_bases: u32, + pub num_reference_only_bases: u32, pub scoring_method: ReferenceTranscriptScoringMethod, pub score: f32 } @@ -59,9 +59,9 @@ impl ReferenceTranscriptMatch { pub fn new( reference_gene_id: &str, reference_transcript_id: &str, - num_overlap_bases: usize, - num_transcript_only_bases: usize, - num_reference_only_bases: usize, + num_overlap_bases: u32, + num_transcript_only_bases: u32, + num_reference_only_bases: u32, scoring_method: ReferenceTranscriptScoringMethod, score: f32 ) -> Self { diff --git a/exacto/exacto-caller/src/structs/reference_transcript_sequence.rs b/exacto/exacto-caller/src/structs/reference_transcript_sequence.rs index 5223808..e5fa9cd 100644 --- a/exacto/exacto-caller/src/structs/reference_transcript_sequence.rs +++ b/exacto/exacto-caller/src/structs/reference_transcript_sequence.rs @@ -69,8 +69,8 @@ impl ReferenceTranscriptSequence { /// /// # Returns /// A vector of `(u16, usize, usize)` tuples representing intron positions (chromosome ID, start, end). - pub fn get_introns(&self) -> Vec<(u16, usize, usize)> { - let mut introns: Vec<(u16, usize, usize)> = Vec::new(); + pub fn get_introns(&self) -> Vec<(u16, u32, u32)> { + let mut introns: Vec<(u16, u32, u32)> = Vec::new(); for i in 0..(self.bases.len() - 1) { let curr_base: &ReferenceBase = self.get_base(i); let next_base: &ReferenceBase = self.get_base(i + 1); @@ -94,8 +94,8 @@ impl ReferenceTranscriptSequence { &*self.reference_transcript_id } - pub fn get_transcript_end(&self) -> usize { - let mut end: usize = 0; + pub fn get_transcript_end(&self) -> u32 { + let mut end: u32 = 0; for base in self.bases.iter() { if base.reference_position > end { end = base.reference_position @@ -104,8 +104,8 @@ impl ReferenceTranscriptSequence { end } - pub fn get_transcript_start(&self) -> usize { - let mut start: usize = usize::MAX; + pub fn get_transcript_start(&self) -> u32 { + let mut start: u32 = u32::MAX; for base in self.bases.iter() { if base.reference_position < start { start = base.reference_position @@ -135,7 +135,7 @@ impl ReferenceTranscriptSequence { impl ReferenceTranscriptSequence { pub fn from_reference_transcript( transcript: &Transcript, - chromosome_names_map: &BiMap,u16>, + chromosome_names_map: &BiMap, u16>, reference_genome_fasta_file: &str ) -> ReferenceTranscriptSequence { let mut fasta_reader = Builder::default() @@ -147,9 +147,9 @@ impl ReferenceTranscriptSequence { for exon in transcript.get_sorted_exons() { if transcript.strand == Strand::Forward { for i in exon.start..=exon.end { - let position_start = Position::try_from(i as usize).unwrap(); - let position_end = Position::try_from(i as usize).unwrap(); - let region = Region::new(reference_chromosome_name, position_start..=position_end); + let position_start: Position = Position::try_from(i as usize).unwrap(); + let position_end: Position = Position::try_from(i as usize).unwrap(); + let region: Region = Region::new(reference_chromosome_name, position_start..=position_end); let ref_record = fasta_reader.query(®ion).unwrap(); let ref_sequence_bytes: &[u8] = ref_record.sequence().as_ref(); let sequence: &str = std::str::from_utf8(ref_sequence_bytes).expect("Failed to convert sequence to UTF-8"); @@ -167,9 +167,9 @@ impl ReferenceTranscriptSequence { } } else { for i in (exon.start..=exon.end).rev() { - let position_start = Position::try_from(i as usize).unwrap(); - let position_end = Position::try_from(i as usize).unwrap(); - let region = Region::new(reference_chromosome_name, position_start..=position_end); + let position_start: Position = Position::try_from(i as usize).unwrap(); + let position_end: Position = Position::try_from(i as usize).unwrap(); + let region: Region = Region::new(reference_chromosome_name, position_start..=position_end); let ref_record = fasta_reader.query(®ion).unwrap(); let ref_sequence_bytes: &[u8] = ref_record.sequence().as_ref(); let sequence: &str = std::str::from_utf8(ref_sequence_bytes).expect("Failed to convert sequence to UTF-8"); diff --git a/exacto/exacto-caller/src/structs/rna_variant_call_set.rs b/exacto/exacto-caller/src/structs/rna_variant_call_set.rs index 66110f6..36dd16a 100644 --- a/exacto/exacto-caller/src/structs/rna_variant_call_set.rs +++ b/exacto/exacto-caller/src/structs/rna_variant_call_set.rs @@ -15,7 +15,7 @@ use bimap::BiMap; use exacto_core::prelude::*; use polars::prelude::*; use rayon::prelude::*; -use rayon::ThreadPoolBuilder; +use rayon::{ThreadPool, ThreadPoolBuilder}; use serde::{Serialize, Deserialize}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::fs::File; @@ -49,7 +49,7 @@ pub struct RNAVariantCallSet { /// Nested structure for position indexing: /// - Outer HashMap: Maps chromosome IDs to their position index /// - Inner BTreeMap: Maps positions to variant call IDs - position_index: HashMap>> + position_index: HashMap>> } impl RNAVariantCallSet { @@ -83,8 +83,8 @@ impl RNAVariantCallSet { let consensus_record = variant_call.get_consensus_record().0; let chromosome_1_id: u16 = consensus_record.get_chromosome_1(); let chromosome_2_id: u16 = consensus_record.get_chromosome_2(); - let position_1: usize = consensus_record.graph_operation.get_position_1(); - let position_2: usize = consensus_record.graph_operation.get_position_2(); + let position_1: u32 = consensus_record.graph_operation.get_position_1(); + let position_2: u32 = consensus_record.graph_operation.get_position_2(); // Index the variant by its first breakpoint position self.position_index @@ -138,11 +138,11 @@ impl RNAVariantCallSet { variant_calls_map } - pub fn get_variant_calls_by_range(&self, chromosome_id: u16, start: usize, end: usize) -> Vec<&VariantCall> { + pub fn get_variant_calls_by_range(&self, chromosome_id: u16, start: u32, end: u32) -> Vec<&VariantCall> { let mut result_ids = HashSet::new(); if let Some(position_map) = self.position_index.get(&chromosome_id) { - for (_pos, variant_call_ids) in position_map.range(start..=end) { + for (_, variant_call_ids) in position_map.range(start..=end) { result_ids.extend(variant_call_ids.iter().cloned()); } } @@ -163,7 +163,7 @@ impl RNAVariantCallSet { pub fn read_tsv_file(tsv_file: &str) -> Self { let parse_options = CsvParseOptions::default() .with_separator(b'\t'); - let df = CsvReadOptions::default() + let df: DataFrame = CsvReadOptions::default() .with_parse_options(parse_options) .with_has_header(true) .try_into_reader_with_file_path(Some(tsv_file.into())) @@ -183,8 +183,13 @@ impl RNAVariantCallSet { let strand_2_col = df.column("strand_2").unwrap().str().unwrap(); let operation_2_col = df.column("operation_2").unwrap().str().unwrap(); let variant_type_col = df.column("variant_type").unwrap().str().unwrap(); - let variant_sequence_col = df.column("variant_sequence").unwrap().str().unwrap(); - let consensus_read_names_col = df.column("consensus_read_names").unwrap().str().unwrap(); + let variant_sequence_col = df.column("sequence").unwrap().str().unwrap(); + let consensus_read_names_col = df + .column("consensus_read_names") + .unwrap() + .cast(&DataType::String) + .unwrap(); + let consensus_read_names_col = consensus_read_names_col.str().unwrap(); let read_start_col = df.column("read_start").unwrap().i64().unwrap(); let read_end_col = df.column("read_end").unwrap().i64().unwrap(); @@ -220,18 +225,18 @@ impl RNAVariantCallSet { let mut reference_transcript_ids: Vec> = reference_transcript_ids_col.get(i).unwrap().split(",").map(|s| s.into()).collect(); reference_transcript_ids.sort(); let chromosome_1: Box = chromosome_1_col.get(i).unwrap().into(); - let position_1: usize = position_1_col.get(i).unwrap() as usize; + let position_1: u32 = position_1_col.get(i).unwrap() as u32; let strand_1: Strand = Strand::from_str(strand_1_col.get(i).unwrap().into()).unwrap(); let operation_1: GraphOperationType = GraphOperationType::from_str(operation_1_col.get(i).unwrap().into()).unwrap(); let chromosome_2: Box = chromosome_2_col.get(i).unwrap().into(); - let position_2: usize = position_2_col.get(i).unwrap() as usize; + let position_2: u32 = position_2_col.get(i).unwrap() as u32; let strand_2: Strand = Strand::from_str(strand_2_col.get(i).unwrap().into()).unwrap(); let operation_2: GraphOperationType = GraphOperationType::from_str(operation_2_col.get(i).unwrap().into()).unwrap(); let variant_type: VariantType = VariantType::from_str(variant_type_col.get(i).unwrap()).unwrap(); let variant_sequence: Box = variant_sequence_col.get(i).unwrap().into(); let consensus_read_names: Box = consensus_read_names_col.get(i).unwrap().into(); - let read_start: usize = read_start_col.get(i).unwrap() as usize; - let read_end: usize = read_end_col.get(i).unwrap() as usize; + let read_start: u32 = read_start_col.get(i).unwrap() as u32; + let read_end: u32 = read_end_col.get(i).unwrap() as u32; let sequence_operation: GraphOperation = GraphOperation::new( *chromosome_names_map.get_by_left(&*chromosome_1).unwrap(), @@ -278,7 +283,7 @@ impl RNAVariantCallSet { "self.read_names_map is empty." ); - let thread_pool = ThreadPoolBuilder::new() + let thread_pool: ThreadPool = ThreadPoolBuilder::new() .num_threads(num_threads) .build() .unwrap(); @@ -344,6 +349,31 @@ impl RNAVariantCallSet { .collect() }); + if rows.is_empty() { + return DataFrame::new(vec![ + Column::from(Series::new("variant_call_id".into(), Vec::::new())), + Column::from(Series::new("transcript_model_id".into(), Vec::::new())), + Column::from(Series::new("reference_transcript_ids".into(), Vec::::new())), + Column::from(Series::new("chromosome_1".into(), Vec::<&str>::new())), + Column::from(Series::new("position_1".into(), Vec::::new())), + Column::from(Series::new("strand_1".into(), Vec::<&str>::new())), + Column::from(Series::new("operation_1".into(), Vec::<&str>::new())), + Column::from(Series::new("chromosome_2".into(), Vec::<&str>::new())), + Column::from(Series::new("position_2".into(), Vec::::new())), + Column::from(Series::new("strand_2".into(), Vec::<&str>::new())), + Column::from(Series::new("operation_2".into(), Vec::<&str>::new())), + Column::from(Series::new("variant_size".into(), Vec::::new())), + Column::from(Series::new("variant_type".into(), Vec::::new())), + Column::from(Series::new("sequence".into(), Vec::<&str>::new())), + Column::from(Series::new("consensus_read_names".into(), Vec::::new())), + Column::from(Series::new("consensus_read_names_count".into(), Vec::::new())), + Column::from(Series::new("read_names".into(), Vec::::new())), + Column::from(Series::new("read_names_count".into(), Vec::::new())), + Column::from(Series::new("read_start".into(), Vec::::new())), + Column::from(Series::new("read_end".into(), Vec::::new())) + ]).unwrap(); + } + let df: DataFrame = DataFrame::new(vec![ Column::from(Series::new("variant_call_id".into(), rows.iter().map(|r| r.0).collect::>())), Column::from(Series::new("transcript_model_id".into(), rows.iter().map(|r| r.1).collect::>())), @@ -358,7 +388,7 @@ impl RNAVariantCallSet { Column::from(Series::new("operation_2".into(), rows.iter().map(|r| r.10).collect::>())), Column::from(Series::new("variant_size".into(), rows.iter().map(|r| r.11).collect::>())), Column::from(Series::new("variant_type".into(), rows.iter().map(|r| r.12.clone()).collect::>())), - Column::from(Series::new("variant_sequence".into(), rows.iter().map(|r| r.13).collect::>())), + Column::from(Series::new("sequence".into(), rows.iter().map(|r| r.13).collect::>())), Column::from(Series::new("consensus_read_names".into(), rows.iter().map(|r| r.14.clone()).collect::>())), Column::from(Series::new("consensus_read_names_count".into(), rows.iter().map(|r| r.15).collect::>())), Column::from(Series::new("read_names".into(), rows.iter().map(|r| r.16.clone()).collect::>())), diff --git a/exacto/exacto-caller/src/structs/transcript_model.rs b/exacto/exacto-caller/src/structs/transcript_model.rs index f840ec2..2c1d574 100644 --- a/exacto/exacto-caller/src/structs/transcript_model.rs +++ b/exacto/exacto-caller/src/structs/transcript_model.rs @@ -15,7 +15,7 @@ use bimap::BiMap; use exacto_core::prelude::*; use itertools::Itertools; use std::cmp::PartialEq; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::hash::{Hash, Hasher}; use serde::{Deserialize, Serialize}; @@ -62,7 +62,7 @@ impl Eq for TranscriptModel {} impl Hash for TranscriptModel { fn hash(&self, state: &mut H) { self.id.hash(state); - let mut sorted_keys: Vec<_> = self.reference_transcript_matches_map.keys().collect(); + let mut sorted_keys: Vec<&Box> = self.reference_transcript_matches_map.keys().collect(); sorted_keys.sort(); for key in sorted_keys { key.hash(state); @@ -75,6 +75,7 @@ impl Hash for TranscriptModel { impl TranscriptModel { pub fn new( id: usize, + read_name: &str, alignment_structure: &AlignmentStructure, chromosome_names_map: &BiMap,u16>, reference_genome_fasta_file: &str @@ -82,7 +83,7 @@ impl TranscriptModel { Self { id, alignment_structure: alignment_structure.clone(), - exons: alignment_structure.identify_exons(), + exons: alignment_structure.identify_exons(read_name), introns: alignment_structure.identify_introns(&chromosome_names_map, reference_genome_fasta_file), contextualized_alignment_structures_map: HashMap::new(), reference_transcript_matches_map: HashMap::new(), @@ -115,10 +116,10 @@ impl TranscriptModel { self.alignment_structure.get_read_id() } - pub fn get_reference_end_position(&self) -> (u16, usize) { - let last_exon_strand = self.exons.last().unwrap().reference_strand.clone(); + pub fn get_reference_end_position(&self) -> (u16, u32) { + let last_exon_strand: Strand = self.exons.last().unwrap().reference_strand.clone(); if last_exon_strand == Strand::Forward { - (self.exons.last().unwrap().reference_chromosome_id, self.exons.first().unwrap().reference_end) + (self.exons.last().unwrap().reference_chromosome_id, self.exons.last().unwrap().reference_end) } else { (self.exons.first().unwrap().reference_chromosome_id, self.exons.first().unwrap().reference_end) } @@ -128,7 +129,7 @@ impl TranscriptModel { &self.reference_transcript_matches_map } - pub fn get_reference_start_position(&self) -> (u16, usize) { + pub fn get_reference_start_position(&self) -> (u16, u32) { let first_exon_strand = self.exons.first().unwrap().reference_strand.clone(); if first_exon_strand == Strand::Forward { (self.exons.first().unwrap().reference_chromosome_id, self.exons.first().unwrap().reference_start) @@ -143,20 +144,28 @@ impl TranscriptModel { pub fn identify_variants( &mut self, + read_name: &str, reference_transcript_matches: &Vec, gene_annotator: &(impl GeneAnnotator + Sync), reference_genome_fasta_file: &str, - min_mapping_quality: usize, + min_mapping_quality: u16, min_base_quality: u8 ) -> &HashMap>, Vec> { self.variant_records_map.clear(); // Step 1. Get a map of reference transcript IDs, matches, and sequences - let mut reference_transcript_ids_map: HashMap, Vec>> = HashMap::new(); + + // Maps each reference gene to its associated reference transcript IDs + let mut reference_gene_transcript_ids_map: HashMap, Vec>> = HashMap::new(); + + // Map each reference transcript ID to its associated ReferenceTranscriptMatch object let mut reference_transcript_match_map: HashMap, &ReferenceTranscriptMatch> = HashMap::new(); + + // Maps each reference transcript ID to its associated ReferenceTranscriptSequence object let mut reference_transcript_sequences_map: HashMap, ReferenceTranscriptSequence> = HashMap::new(); + for reference_transcript_match in reference_transcript_matches.iter() { - reference_transcript_ids_map + reference_gene_transcript_ids_map .entry(reference_transcript_match.reference_gene_id.clone()) .or_insert(Vec::new()) .push(reference_transcript_match.reference_transcript_id.clone()); @@ -164,10 +173,6 @@ impl TranscriptModel { reference_transcript_match.reference_transcript_id.clone(), reference_transcript_match ); - self.reference_transcript_matches_map.insert( - reference_transcript_match.reference_transcript_id.clone(), - reference_transcript_match.clone() - ); let reference_transcript: &Transcript = gene_annotator .get_transcript(&*reference_transcript_match.reference_transcript_id) .unwrap(); @@ -180,13 +185,55 @@ impl TranscriptModel { reference_transcript_match.reference_transcript_id.clone(), reference_transcript_sequence ); + self.reference_transcript_matches_map.insert( + reference_transcript_match.reference_transcript_id.clone(), + reference_transcript_match.clone() + ); } - // Step 2. Identify RNA variants based on contextualized alignment structures - if reference_transcript_ids_map.is_empty() { - let mut alignment_structure = self.alignment_structure.clone(); + // Step 2. Identify reference transcripts from different genes that overlap each other + let mut overlapping_gene_pairs: HashSet<(Box, Box)> = HashSet::new(); + let gene_ids: Vec<&Box> = reference_gene_transcript_ids_map.keys().collect(); + for i in 0..gene_ids.len() { + for j in (i + 1)..gene_ids.len() { + let gene_id_a: &Box = gene_ids[i]; + let gene_id_b: &Box = gene_ids[j]; + let transcript_ids_a: &Vec> = &reference_gene_transcript_ids_map[gene_id_a]; + let transcript_ids_b: &Vec> = &reference_gene_transcript_ids_map[gene_id_b]; + + 'outer: for tid_a in transcript_ids_a.iter() { + let transcript_a: &Transcript = gene_annotator + .get_transcript(&*tid_a) + .unwrap(); + for tid_b in transcript_ids_b.iter() { + let transcript_b: &Transcript = gene_annotator + .get_transcript(&*tid_b) + .unwrap(); + if transcript_a.chromosome == transcript_b.chromosome { + if find_overlap( + (transcript_a.start as isize, transcript_a.end as isize), + (transcript_b.start as isize, transcript_b.end as isize) + ).is_some() { + let pair = if *gene_id_a < *gene_id_b { + (gene_id_a.clone(), gene_id_b.clone()) + } else { + (gene_id_b.clone(), gene_id_a.clone()) + }; + overlapping_gene_pairs.insert(pair); + break 'outer; + } + } + } + } + } + } + + // Step 3. Identify RNA variants based on contextualized alignment structures + if reference_gene_transcript_ids_map.is_empty() { + let mut alignment_structure: AlignmentStructure = self.alignment_structure.clone(); alignment_structure.contextualize( + read_name, &vec![], gene_annotator, &self.chromosome_names_map @@ -209,10 +256,10 @@ impl TranscriptModel { ); } else { // Get all combinations of reference transcripts (1 per gene ID) - let mut keys: Vec<&str> = reference_transcript_ids_map.keys().map(|k| k.as_ref()).collect(); + let mut keys: Vec<&str> = reference_gene_transcript_ids_map.keys().map(|k| k.as_ref()).collect(); keys.sort_unstable(); let reference_transcript_ids_combinations: Vec, Box)>>= keys.iter() - .map(|k| reference_transcript_ids_map[*k].iter()) + .map(|k| reference_gene_transcript_ids_map[*k].iter()) .multi_cartesian_product() .map(|choice| { keys.iter() @@ -224,14 +271,36 @@ impl TranscriptModel { .collect(); for reference_transcript_ids_combination in reference_transcript_ids_combinations.iter() { + // Skip combinations that include transcripts from overlapping genes + let mut has_overlap: bool = false; + 'overlap_check: for i in 0..reference_transcript_ids_combination.len() { + for j in (i + 1)..reference_transcript_ids_combination.len() { + let (gene_id_a, _) = &reference_transcript_ids_combination[i]; + let (gene_id_b, _) = &reference_transcript_ids_combination[j]; + let pair = if *gene_id_a < *gene_id_b { + (gene_id_a.clone(), gene_id_b.clone()) + } else { + (gene_id_b.clone(), gene_id_a.clone()) + }; + if overlapping_gene_pairs.contains(&pair) { + has_overlap = true; + break 'overlap_check; + } + } + } + if has_overlap { + continue; + } + let mut reference_transcript_sequences: Vec<&ReferenceTranscriptSequence> = Vec::new(); let mut reference_transcript_ids: Vec> = Vec::new(); for (reference_gene_id, reference_transcript_id) in reference_transcript_ids_combination.iter() { reference_transcript_sequences.push(reference_transcript_sequences_map.get(reference_transcript_id).unwrap()); reference_transcript_ids.push(reference_transcript_id.clone()); } - let mut alignment_structure = self.alignment_structure.clone(); + let mut alignment_structure: AlignmentStructure = self.alignment_structure.clone(); alignment_structure.contextualize( + read_name, &reference_transcript_sequences, gene_annotator, &self.chromosome_names_map diff --git a/exacto/exacto-caller/src/structs/transcript_model_exon.rs b/exacto/exacto-caller/src/structs/transcript_model_exon.rs index 73b071f..54a2afb 100644 --- a/exacto/exacto-caller/src/structs/transcript_model_exon.rs +++ b/exacto/exacto-caller/src/structs/transcript_model_exon.rs @@ -20,8 +20,8 @@ use std::hash::{Hash, Hasher}; pub fn vectorize_exons( exons: &Vec, reference_chromosome_id: u16, - reference_start: usize, - reference_end: usize, + reference_start: u32, + reference_end: u32, aligned_value: i8, unaligned_value: i8 ) -> Vec { @@ -46,16 +46,12 @@ pub fn vectorize_exons( #[derive(Debug,Serialize,Deserialize)] pub struct TranscriptModelExon { pub reference_chromosome_id: u16, - pub reference_start: usize, - pub reference_end: usize, + pub reference_start: u32, + pub reference_end: u32, pub reference_strand: Strand, pub exon_number: u16, - - /// FASTX read sequence start position - pub read_start_position: usize, - - /// FASTX read sequence end position - pub read_end_position: usize + pub read_start_position: u32, // FASTX read sequence start position + pub read_end_position: u32 // FASTX read sequence end position } impl PartialEq for TranscriptModelExon { @@ -87,12 +83,12 @@ impl Hash for TranscriptModelExon { impl TranscriptModelExon { pub fn new( reference_chromosome_id: u16, - reference_start: usize, - reference_end: usize, + reference_start: u32, + reference_end: u32, reference_strand: Strand, exon_number: u16, - read_start_position: usize, - read_end_position: usize + read_start_position: u32, + read_end_position: u32 ) -> Self { assert!(read_start_position <= read_end_position, "read_start_position should be less than or equal to read_end_position."); assert!(reference_start <= reference_end, "reference_start should be less than reference_end."); diff --git a/exacto/exacto-caller/src/structs/transcript_model_intron.rs b/exacto/exacto-caller/src/structs/transcript_model_intron.rs index f793b94..bc9137c 100644 --- a/exacto/exacto-caller/src/structs/transcript_model_intron.rs +++ b/exacto/exacto-caller/src/structs/transcript_model_intron.rs @@ -20,18 +20,14 @@ use std::hash::{Hash, Hasher}; #[derive(Debug,Serialize,Deserialize)] pub struct TranscriptModelIntron { pub reference_chromosome_id: u16, - pub reference_start: usize, - pub reference_end: usize, + pub reference_start: u32, + pub reference_end: u32, pub reference_strand: Strand, pub intron_number: u16, pub donor_splice_site_signal: Box, pub acceptor_splice_site_signal: Box, - - /// Read position immediately before the intron starts (towards 5') - pub exon_5p_flank_read_position: usize, - - /// Read position immediately after the intron ends (towards 3') - pub exon_3p_flank_read_position: usize + pub exon_5p_flank_read_position: u32, // Read position immediately before the intron starts (towards 5') + pub exon_3p_flank_read_position: u32 // Read position immediately after the intron ends (towards 3') } impl PartialEq for TranscriptModelIntron { @@ -67,14 +63,14 @@ impl Hash for TranscriptModelIntron { impl TranscriptModelIntron { pub fn new( reference_chromosome_id: u16, - reference_start: usize, - reference_end: usize, + reference_start: u32, + reference_end: u32, reference_strand: Strand, intron_number: u16, donor_splice_site_signal: &str, acceptor_splice_site_signal: &str, - exon_5p_flank_read_position: usize, - exon_3p_flank_read_position: usize + exon_5p_flank_read_position: u32, + exon_3p_flank_read_position: u32 ) -> Self { assert_eq!( exon_5p_flank_read_position, exon_3p_flank_read_position - 1, diff --git a/exacto/exacto-caller/src/structs/transcript_model_set.rs b/exacto/exacto-caller/src/structs/transcript_model_set.rs index 09b27ab..1130f7a 100644 --- a/exacto/exacto-caller/src/structs/transcript_model_set.rs +++ b/exacto/exacto-caller/src/structs/transcript_model_set.rs @@ -15,7 +15,7 @@ use bimap::BiMap; use polars::prelude::*; use rayon::iter::ParallelIterator; use rayon::prelude::ParallelSlice; -use rayon::ThreadPoolBuilder; +use rayon::{ThreadPool, ThreadPoolBuilder}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::fs::File; @@ -26,12 +26,8 @@ use crate::prelude::*; #[derive(Debug,Serialize,Deserialize)] pub struct TranscriptModelSet { pub transcript_models: HashSet, - - /// A map between read names and read IDs. - pub read_names_map: BiMap, usize>, - - /// A map between chromosome names and chromosome IDs. - pub chromosome_names_map: BiMap, u16> + pub read_names_map: BiMap, usize>, // A map between read names and read IDs. + pub chromosome_names_map: BiMap, u16> // A map between chromosome names and chromosome IDs. } /// API methods @@ -163,11 +159,13 @@ impl TranscriptModelSet { let mut read_name_values: Vec = Vec::new(); let mut excluded_values: Vec = Vec::new(); + for read_id in included_read_ids.iter() { let read_name: Box = self.read_names_map.get_by_right(read_id).unwrap().clone(); read_name_values.push(read_name.to_string()); excluded_values.push(false); } + for read_id in excluded_read_ids.iter() { let read_name: Box = self.read_names_map.get_by_right(read_id).unwrap().clone(); read_name_values.push(read_name.to_string()); @@ -336,7 +334,7 @@ impl TranscriptModelSet { DataFrame::new(vec![ Column::from(Series::new("transcript_model_id".into(), merged_map.get("transcript_model_id").unwrap())), Column::from(Series::new("reference_transcript_ids".into(), merged_map.get("reference_transcript_ids").unwrap())), - Column::from(Series::new("transcript_structure_index".into(), merged_map.get("index").unwrap())), + Column::from(Series::new("index".into(), merged_map.get("index").unwrap())), Column::from(Series::new("read_start".into(), merged_map.get("read_start").unwrap())), Column::from(Series::new("read_end".into(), merged_map.get("read_end").unwrap())), Column::from(Series::new("sequence".into(), merged_map.get("sequence").unwrap())), @@ -372,7 +370,7 @@ impl TranscriptModelSet { "self.read_names_map is empty." ); - let thread_pool = ThreadPoolBuilder::new() + let thread_pool: ThreadPool = ThreadPoolBuilder::new() .num_threads(num_threads) .build() .unwrap(); @@ -399,7 +397,7 @@ impl TranscriptModelSet { } } - let chunk_size = (variant_calls_list.len() + num_threads - 1) / num_threads; + let chunk_size = ((variant_calls_list.len() + num_threads - 1) / num_threads).max(1); let rows: Vec<_> = thread_pool.install(|| { variant_calls_list .par_chunks(chunk_size) @@ -457,7 +455,7 @@ impl TranscriptModelSet { Column::from(Series::new("operation_2".into(), rows.iter().map(|r| r.10).collect::>())), Column::from(Series::new("variant_size".into(), rows.iter().map(|r| r.11).collect::>())), Column::from(Series::new("variant_type".into(), rows.iter().map(|r| r.12.clone()).collect::>())), - Column::from(Series::new("variant_sequence".into(), rows.iter().map(|r| r.13.clone()).collect::>())), + Column::from(Series::new("sequence".into(), rows.iter().map(|r| r.13.clone()).collect::>())), Column::from(Series::new("consensus_read_names".into(), rows.iter().map(|r| r.14.clone()).collect::>())), Column::from(Series::new("read_start".into(), rows.iter().map(|r| r.15.clone()).collect::>())), Column::from(Series::new("read_end".into(), rows.iter().map(|r| r.16.clone()).collect::>())) @@ -513,7 +511,7 @@ impl TranscriptModelSet { prefix: &str ) { // Step 1. Define output TSV file paths - let output_dir = if output_dir.ends_with('/') { + let output_dir: String = if output_dir.ends_with('/') { output_dir.to_string() } else { format!("{}/", output_dir) @@ -537,7 +535,7 @@ impl TranscriptModelSet { let variant_calls_tsv_file: String = make_path("rna_variant_calls"); // Step 2. Get all DataFrames to output - let mut df_exons = self.get_exons_dataframe(); + let mut df_exons: DataFrame = self.get_exons_dataframe(); let mut df_read_filter_status: DataFrame = self.get_read_filter_status_dataframe(); let mut df_read_names: DataFrame = self.get_read_names_dataframe(); let mut df_matched_reference_transcripts: DataFrame = self.get_reference_transcript_matches_dataframe(); diff --git a/exacto/exacto-caller/src/structs/variant_call.rs b/exacto/exacto-caller/src/structs/variant_call.rs index 6307767..178eb13 100644 --- a/exacto/exacto-caller/src/structs/variant_call.rs +++ b/exacto/exacto-caller/src/structs/variant_call.rs @@ -23,6 +23,7 @@ use crate::prelude::*; #[derive(Debug,Eq,Serialize,Deserialize)] pub struct VariantCall { pub id: usize, + pub total_depth: i32, pub variant_records: HashSet } @@ -77,6 +78,7 @@ impl VariantCall { pub fn new(id: usize) -> Self { Self { id: id, + total_depth: -1, variant_records: HashSet::new() } } @@ -84,6 +86,12 @@ impl VariantCall { pub fn add_variant_record(&mut self, variant_record: VariantRecord) { self.variant_records.insert(variant_record); } + + pub fn get_alternate_allele_fraction(&self) -> f32 { + assert!(self.total_depth > 0); + let num_alt_allele_read_count: usize = self.get_read_ids().len(); + num_alt_allele_read_count as f32 / self.total_depth as f32 + } /// Get the consensus VariantRecord object for this VariantCall object. /// @@ -91,29 +99,88 @@ impl VariantCall { /// /// (VariantRecord,read IDs). pub fn get_consensus_record(&self) -> (&VariantRecord, Vec) { - let mut map: HashMap<(u16, usize, GraphOperationType, u16, usize, GraphOperationType, Box, VariantType),Vec<&VariantRecord>> = HashMap::new(); - for variant_record in self.variant_records.iter() { - let key: (u16, usize, GraphOperationType, u16, usize, GraphOperationType, Box, VariantType) = ( - variant_record.graph_operation.get_chromosome_1(), - variant_record.graph_operation.get_position_1(), - variant_record.graph_operation.get_operation_type_1().clone(), - variant_record.graph_operation.get_chromosome_2(), - variant_record.graph_operation.get_position_2(), - variant_record.graph_operation.get_operation_type_2().clone(), - variant_record.get_standardized_sequence().into(), - variant_record.graph_operation.get_variant_type().clone() - ); - map - .entry(key) - .or_insert(Vec::new()) - .push(variant_record); + assert!(!self.variant_records.is_empty(), "self.variant_records is empty"); + + type Key = ( + u16, + u32, + GraphOperationType, + u16, + u32, + GraphOperationType, + Box, + VariantType + ); + + // Build a stable "key" function + let make_key = |vr: &VariantRecord| -> Key { + ( + vr.graph_operation.get_chromosome_1(), + vr.graph_operation.get_position_1(), + vr.graph_operation.get_operation_type_1().clone(), + vr.graph_operation.get_chromosome_2(), + vr.graph_operation.get_position_2(), + vr.graph_operation.get_operation_type_2().clone(), + vr.get_standardized_sequence().into(), + vr.graph_operation.get_variant_type().clone() + ) + }; + + let mut map: HashMap> = HashMap::new(); + + for vr in self.variant_records.iter() { + let key: Key = make_key(vr); + map.entry(key).or_insert_with(Vec::new).push(vr); + } + + // If there is a consensus group (>=2), return it + if let Some((_key, max_vec)) = map.iter().max_by_key(|(_, v)| v.len()) { + if max_vec.len() >= 2 { + let consensus_record = max_vec[0]; + let read_ids = max_vec.iter().map(|v| v.read_id).collect(); + return (consensus_record, read_ids); + } } - let max_vec = map + + // Otherwise, choose the record with the median variant size. + // Then tie-break by highest support among records with that same size. + let mut sizes: Vec = self.variant_records.iter().map(|vr| vr.get_variant_size()).collect(); + sizes.sort_unstable(); + let median_size: isize = sizes[(sizes.len() - 1) / 2]; + + // Filter candidates that match median size + let mut median_candidates: Vec<&VariantRecord> = self + .variant_records .iter() - .max_by_key(|(_, v)| v.len()) - .expect("self.variant_records is empty.") - .1; - (max_vec[0], max_vec.iter().map(|v| v.read_id).collect()) + .filter(|vr| vr.get_variant_size() == median_size) + .collect(); + assert!(!median_candidates.is_empty(), "median_candidates is empty"); + + // For insertion edge-case: same size, different sequences. + // Pick the candidate whose key-group has the largest support count. + // Tie-break deterministically by smallest read_id. + median_candidates.sort_by(|a, b| { + let ka: Key = make_key(a); + let kb: Key = make_key(b); + + let sa: usize = map.get(&ka).map(|v| v.len()).unwrap_or(0); + let sb: usize = map.get(&kb).map(|v| v.len()).unwrap_or(0); + + // Primary: larger support first + sb.cmp(&sa) + // Tie-breaker: smaller read_id first (deterministic) + .then_with(|| a.read_id.cmp(&b.read_id)) + }); + + let chosen: &VariantRecord = median_candidates[0]; + let chosen_key: Key = make_key(chosen); + + let read_ids: Vec = map + .get(&chosen_key) + .map(|v| v.iter().map(|vr| vr.read_id).collect()) + .unwrap_or_else(|| vec![chosen.read_id]); + + (chosen, read_ids) } pub fn get_named_consensus_record(&self, read_names_map: &BiMap,usize>) -> (&VariantRecord, Vec>) { @@ -186,6 +253,14 @@ impl VariantCall { (false, false) => panic!("No strand information available for this variant call.") } } + + pub fn get_total_depth(&self) -> i32 { + self.total_depth + } + + pub fn set_total_depth(&mut self, total_depth: i32) { + self.total_depth = total_depth; + } pub fn to_tsv_string( &self, @@ -230,6 +305,7 @@ impl Clone for VariantCall { fn clone(&self) -> Self { VariantCall { id: self.id, + total_depth: self.total_depth, variant_records: self.variant_records.clone() } } diff --git a/exacto/exacto-caller/src/structs/variant_record.rs b/exacto/exacto-caller/src/structs/variant_record.rs index 2106908..f7c36d0 100644 --- a/exacto/exacto-caller/src/structs/variant_record.rs +++ b/exacto/exacto-caller/src/structs/variant_record.rs @@ -22,8 +22,8 @@ use crate::prelude::*; #[derive(Debug,Eq,Serialize,Deserialize)] pub struct VariantRecord { pub read_id: usize, - pub read_position_1: usize, - pub read_position_2: usize, + pub read_position_1: u32, + pub read_position_2: u32, pub graph_operation: GraphOperation } @@ -52,15 +52,15 @@ impl Hash for VariantRecord { impl VariantRecord { pub fn new( read_id: usize, - read_start_position: usize, - read_end_position: usize, - sequence_operation: GraphOperation + read_start_position: u32, + read_end_position: u32, + graph_operation: GraphOperation ) -> Self { Self { read_id, read_position_1: read_start_position, read_position_2: read_end_position, - graph_operation: sequence_operation + graph_operation: graph_operation } } @@ -84,11 +84,11 @@ impl VariantRecord { self.graph_operation.get_operation_type_2() } - pub fn get_position_1(&self) -> usize { + pub fn get_position_1(&self) -> u32 { self.graph_operation.get_position_1() } - pub fn get_position_2(&self) -> usize { + pub fn get_position_2(&self) -> u32 { self.graph_operation.get_position_2() } @@ -96,11 +96,11 @@ impl VariantRecord { self.read_id } - pub fn get_read_position_1(&self) -> usize { + pub fn get_read_position_1(&self) -> u32 { self.read_position_1 } - pub fn get_read_position_2(&self) -> usize { + pub fn get_read_position_2(&self) -> u32 { self.read_position_2 } diff --git a/exacto/exacto-caller/src/structs/variant_record_cluster.rs b/exacto/exacto-caller/src/structs/variant_record_cluster.rs index b8d4d48..37a9a34 100644 --- a/exacto/exacto-caller/src/structs/variant_record_cluster.rs +++ b/exacto/exacto-caller/src/structs/variant_record_cluster.rs @@ -21,20 +21,20 @@ pub struct VariantRecordCluster { pub variant_records: Vec>, pub chromosome_1: u16, pub chromosome_2: u16, - pub min_position_1: usize, - pub max_position_1: usize, - pub min_position_2: usize, - pub max_position_2: usize + pub min_position_1: u32, + pub max_position_1: u32, + pub min_position_2: u32, + pub max_position_2: u32 } impl VariantRecordCluster { pub fn new( chromosome_1: u16, chromosome_2: u16, - min_position_1: usize, - max_position_1: usize, - min_position_2: usize, - max_position_2: usize + min_position_1: u32, + max_position_1: u32, + min_position_2: u32, + max_position_2: u32 ) -> Self { Self { variant_records: Vec::new(), diff --git a/exacto/exacto-caller/src/tests/data/bam/dna-005-normal_minimap2_mdtagged_sorted.bam b/exacto/exacto-caller/src/tests/data/bam/dna-005-normal_minimap2_mdtagged_sorted.bam index ce373a7b005d57aab1a2522e9dc7b17773ca0994..be4f5387d70c7da4c22013ccab6a90ab30ebf9bd 100644 GIT binary patch delta 393 zcmV;40e1eibEb2DABzYC000000RIL6LPG)oWC3+j(N2Rf6h$9Qy05rDa0$hz$P>;b z(}Zm}qKOH~(gE9u&^oAY@yV|+#;>(Q0SB{5o70}#({paxzZxBV9c!9%GawIRo9gGp zpV*-gd6aM_%H21K n+8M^e0ssIXiwFb&00000{{{d;LjnN#4egsx%ceK8i~%V)!-=x) delta 393 zcmV;40e1eTbG37SABzYC000000RIL6LPG)oa{;Z8-A;o*5QR|_lkHQ?trtrGqas&W zn^Y6pVu>auBxDPe64_k~YixSuQy3rD1qCeqxsn8OcF!>Lo$23hvYS*_8aCSm8D|O3 zV-iWb5KCEqQRgcN6`W^9nXlFEMpj*wOO`2X9}1oez;+pif<6R1ff=EId3SEzMJ?@p zo^0EtQS~lQqj&LyB%{$U(Ci??j$LidHulYyRR6M#fJTBONgM^`uW@u}J56m_)H_xb nA^kigWU|?VdVy{UI3?McGLnEwpb!{M2czj= LD5e8QtiebC5?RN^ literal 3880 zcmZ>A^kigWU|?VdVy{UI3?McGLkT08Vwh6h_u|TXAVzo4MhWA0guBnpNAnZUz!ga&At}xuknFIQlp?4ELr-9)}L2 z=OnmuA}RAY&A3WBZg5Y6p%cw>c0oK{Vg8K2guP3Q`sB9f#D5t_(>z195;7;jpmpGT z&MJG(w}$>h1v{=^%7`>e1%I)KKWwmeYEjF^oLS~2x%O%XkZ(W(oTt+i5Timb z$#Mhu1n2=OV1Ogg7DLtc7^=3(P&FK^fKk)e$QmUeja^=x&otg0>Q6ZLJy~(u(*(#FZG} zUYI?3>pyw@@Sz6^nX6xY0jv*r2NNy;WvfS3;_>!E5>s8&(cjWI(X;epynlYxJGzzu|m%CD9xydd$xB+ys;H7a{YohGNFAvKMix+m=4M9z<-J|ikfzYV$FC?yiw=E zb*y#vl<}90IQ>St_qFWVZdHb*S<2|MLELexC8?nrCg!@KU6O03aRBKCq`*ZwPXRG3 zp|UA=TuEzVXI28A^kigWU|?VdVy{UI3?McGLnEwpb!{M2czj=G#!lQgVAzeD3=4Uw$3OGX%miy!)Q2w^7Cjq P7)=L5F&#kaA_NHlSI*1W literal 3880 zcmZ>A^kigWU|?VdVy{UI3?McGLlGmGVwgJxMDsv6aIzVw2&7<%{c$6RAVggbR2-%s zCVvfV8qCa58WLus;V>Ewpb!{M2czj=G#!lQgVAzeD3=4Uw$3OGX%miy!)Q2w^7Cjq P7)=L5F&#kaA_NHl;tAvg diff --git a/exacto/exacto-caller/src/tests/data/bam/dna-009-normal_minimap2_mdtagged_sorted.bam b/exacto/exacto-caller/src/tests/data/bam/dna-009-normal_minimap2_mdtagged_sorted.bam index a01c1adfe39d7f40e437d1e6f4028c060d682afc..441a10597a8f027c4e789f16d64d7e834cd273c3 100644 GIT binary patch literal 208363 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

! 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 ykCCE|G1Fqd;J$lF{aOr>0+oZ@b0MZeSY?b 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

#F zm6>c~&lq2x4HR~dfWL2!uDIV6kb`VL5s=I)?IbRDUomi_%*@3=67j-$7x^ip4GIGe z;Wc8@-&jzk#S4Pge_EE3%|UMyRNI$}Zg5NpxdL^uDd^eHyNq29HT z(SVe=c;aRvM~bs$9qe}*Ru84h?~eQJWE4};-S4M?3_~hVauH|UY*pV)@j=Bs-E>x* zA+e-A8L@zoh>9tQ8Jbg6ca79(4F5as?6nHiSki0IzEp@`vE0s!3{Hx8(+8=`hK+N{T!G?Nx=4jg3FhF!+ z1TAjj!!6NKaAVX|mbz6D7%W)Qf$z8KwhS#|ZPE+07UQK`N+o_ei@?&_Lc9%LOVMit zR$;Et?9x$TbYpvT%%mU)oD;!zaH$|rcdigbagv1uCRWj}Y~Oq#>*+`}KP&-r?zM2r z`R+Wv`D~ASP|Sr@YpU2~e22ttrD10rOCOU5MXZuF7v6U{T2JSFon!y=rP$*K>-)Q7 z@AHVK;m7X5n9#?7=f`W6W8eExMBm#w7E$lp`{a(Fe(&KA&mI3?d#fL+pO+OK`(9^v zKHjAa-zT(o0`EtT`yT&$P^>B6@Aa`5sqgtH{o@%*$ktF_=nYBeihtnW+Qcya_3=bJ zEX<+?a~Lj_t60$#V)FViREJ>XMFq64l)}k4%9I4D1Xx!zyL5+OszZ)XTQ9F2K}!AS ze8-!+80+7Jv6-?)OIHV-znkgIwOg2P8XRt{8JQqv4N=V`y3r+FG#wtsWj z*;%{yN`5IGQ6h;b9n^{wgc>UUX})HKGm7Qd&E=mx zZF0A>TZOmtfN19u-}A=2jkB|{(IcT%rI)HSu{Cufxn0|Ag?s+9NiMtEG7VKHkYJ^C zM5<>!YCVp|N3TVr%t=1+((E*Inafi1ZsccEYUdMqy4?Daxm+op0X4-#-od&w%S^`D zZ|1RD`942J$|fNG=DqXJP8K`(-qjY&Cdc#6bHdrM9;w^qA9Eg?YPFjdPbReQcI!%P zmZQw zF-A}KV@tcnihMFjJicIM`!Poh5Luv~zbyoNQ1!1ZQFpYTz+IEVzcTvr=EO36x?qgZVsLZ2Q?c8xKl#Ok~mXvajQe(B<71v{gFW-7zf`ysP!rNb_#r^1l#4xOQ+< znpQYx#;N?i(!AfmzT5EG3d%JUr0&6$INk&gsHJRd!kJx}K3`FA?zK8^(ZVY_o|mUb z_5OiSX6GN#@j@!%lFhc3`3pFpNYmPmrDSB3N8tNr&c@_W1t*ft2eL~RXMcz# zNMa<3nZCdvT$`T#bn)=r>s!%piUYFvV{=jAIkntPDIS=->HF@mbnST^WqG1}8zd;b zGHF#=HM?F*nJq7CsqTk%hLiD7p$xxW!G`X^)`Hmz!**#(6o)b;MRY^pX0FM?&6uej zzm0oHs3}dDML1wrv-yeCG0UHf5e!oZ8dCbKsSlw6t0LQ(lH)zZ?TmWMXx2|zM}T9b z-IN~?Of-D!(w-~PTJ%b31$7S35|`7^Jjo&11@nrk69Y%(&<|BcB`XPmXnMs4S{Xh} zAS?;8gt0KgWk8P48m_(TM;)Y;kMcTMg=J7KqphqDFMudomFS(pb7df=P*t{ImX;K0 zy5AOWo+ip+A(()zXV=TyWjE~U_kBgygccakQe{kDhFHSon1zmT6ANp^@-??QJ5VEU zguvPOtU1%0cQ>~!+y)=H7hMDNr-=5Rxo9i}?urhyJ8al5yJk2SgzKYx`F0s{(his_ z4fGqW@h2mW!us#v@#c4mCiSAx_(47Rbb#xS2Jp7iPwX8Ord+tvNvl=jvd63y8Kjy# zmq$H(i71p0idhG$sQ^L^r&kQb2zoPZ4|&CNL}e|h2Q$}Jrat@*LJYv@7Gr1Rl(PmQ zC5v3Rbn@3VxQ2u4Xw@O)_}U2rsmjVaU{*93rtUEsoWL@Siog>ARzspn*)cz`Tb-*q ztHg+M#F4@=c)x^><_evc6vhLLiFOBrpa^AsY}en^#JmD=qj6^AnUm6L2t^TJ0b*%q zZjQSA24MnmppB)N42D6Q<>SYxSaaPiZJ;zqAEMJ2EtpG+j$!5k*FGV~{sW??N~JKS zMwgtkym67qV+1udIh>Xy>#>(WdSPFLjcsFnJfiaIH--VUijilg!6dwP)P{~Pr%WvF z(kX~qjt>u;fECT3pzn^z5;q^9E&#O()xqMq)Ge`636Qs00}t1T#Cn8TK?qM`WUr?k z3;?O~n}n&)bH0s3ppO@*-N@F4M|oBScB%v@5(hULDPDg4kh}EP+<`@co?lsO^fwF^ zj*^0@Tr{$jO;D?tIi(d@^4_<*7gP7jn}WPFhKG-q)r>A#p^SqVGu0E(oT;pMor+~m zmp&r48-<*7QCCG7AjfZY?jm$4sk~sBzSzYsxZ+!(CyNxIv3Lef%eNQM%~S@142A|( zSLrIz8o`4=256=LVl5p1MS(=Xk}f=L`zW)U43++CZ>W{LBNLK+)rd_th?{KFYry-w zIv!cob5_suHQOi@X%YkJRh)JC!GYK(>5=(QKXSXS?&(>{x8h(#=LwjFd0}kdVr(@}-!s16Z& zz9V@EU(s4ebz43W@k_LDVpXD0`EcwW?(5z93b{&OtRD$Rub%Q$tTlJrp|>oL!#bvJ zR8@lzEp~>`9%1yf+!xeQRCS~mEIb7K5!Vtg^QK*hciU@U{6u=Nr4}uGv8)Rbd@Npq z$E+G;97F%;hnVY-W$x<^hq)x|NEotss&(%ege5eKi?~3Oa4<=}^hfK?e)o?GXQED1$toET zBfte%ZtAz%8>JuCrvMqAHQlQ4R$_L= z>0j3*s0tpN%7nub6}TzEtN$)5@I)w-hGg7t%{g}=^+N5QHT%`x1GVOCqBOp-=8S5J z&@j`3E-Eqo)i=$Nfs&#Z@JH*Uwdwk@3x2XwdNJ>C8zGUza`-jyHyRl;l|`wyXh=1k z@^;j%B`9+e4-0oBszs8n*K5Bfffpng2`mZ1$gqyxpfvyt+5WM$(C!%(I9PxhXA$Fc z+j@*Bc>i&IcF-6MIG;3Tf(kbr;mbo-bhVAH=7j=6fJnz~*`R?byI4>+G}?6RgpqOY z5(UZR20e3h@MFS6xUs2!N(9jGkzh3U3Q#y@8Ob<;p$%JSsO0+wdDFSuzO=>s>1hLS zAsYb0mPz6Z;9V)0JaGzjqLoZ-8Q{}k?=VsrEXOabXX(6*{1 zL*-MjRc0a{Vy^HfXiX_aQ<|}(;S^Mb;>|Vpu~2@mHGY(GPuW2Q(=_DcPbaD^yOAGd zAcVVkL#na~dk%jgU{j!J^#rQYvWsEvm#=P$SCrYwa{&ax~V!T zoDa%NwZqJS@=87jHfEGlUSjSxzC}?TB|2n37|Il1O%W>OI2>(GZ0OwHQT3zd@ZC1` zZB^XRm1xvbiBxPpCLaZ+g2qjXGmNMd4SlPG03I_)GK6b#lMZaY(p^Qr4HaHr&k5BF z7n)un0H-NWTt^O2`8$p=iA60b*Dza}8iUb=&pAcOOHKp@>!i6d_?0Prn~a@ zAsV=Ug%IYqKtMrO7opR6ozOP4iFI}~ebS=#h_fHILOO06W0~y|MpRxp{2+L*@sRqI zjCc-MztRk1{B0MS3#Z6v8A=%Xst7M%9j2qDSkE8-d7sr(NdSfU1x*>gB=orChZRP@ zsJIUwI9Ks}(II_H8EcPlH@_Ycfq72c+p^<5=Sp; zq6*baDE>vo&Tq(>MOFO4V?Ko+mY4vMCKxy}74JiL`X*uW`Dx9X@q%{J4 zORPb*S`5%tE?{Plq=G;uU#qv1OSE+>x8*pMYEqXEi4#n)U~0TZ2^p*pQvM4!x4 z&PYTcB?fKfYR@A|8mcP*F}lSZGd09?Wpx5o^-5hUkNzpsWR3c%Ds6rFiHs)hvp%4S zAu3Byi~kNa1c&)6%MTG1M(~%1GUFt@VE1N}JjAXs0x1{+vVSvbDmX<2rVSQy4v!|6 zwvn`%v_xV7xYb~{qcN%{k=|z7j$C(3FHf=~$wEOrfi`nVe<>woiGg!G1Big;rxw~! zOQ=nfoAsfY#;%}sY#VJ&<|E9MSc2KHsP?s&1huP$v1)9e?UzL(wXtPE^9__EnjbtKjIDzX zb?QtBeoVcZe_9ft?bAE_H`(QoXakv(AzRSrKsF!^(AI*?iDrG%>W{)j_vpzw!w`G3 zC94EL>Z$rQp0|WT+VwSxlsc7OiqbLu%IJdq)_?=cZtI$v(FWT?AyDxg{$nq-U~jn& zmLJu2yZ_TdRTk@E3B6+yA_F@g>?E)A%n!8<7dW={?jmYL#h`Z>In#HVbD}}+{3&A? z(gH*Ki<3MN1tKPYjs3-dV*@yi9u#SeSMF)!)Qgjqpe#pPErL*dp@L?D{)x!7R=)@Y1Q0`2|!G%L` zrj4-ArE`d|zFT|r=ng;xG_i1^1*pC71YF3=WEhO1^1~&D{=w>k9b=KJo4r%J$4I}y9mqEajbY% zY*||;xD2=XR>ZBo_ZJ(G+bAk1<-7yD!A#E=T&S{hK^n&MCa5#@GTy52 zenCMOD9)}KJ{TUtrV1T^r@^jg1j8i!@iMk2oUR$fYtp>XPA|dWvybK@2Q|N6IfzvZ zfwl9m5Vjjq_dz~dzCR5T{Y#8+eWJCxrw3J0rYTIwUEl=kRjOO}Je6PK zB;kAX-^2dUWsBW<{;lEBG0-aa{y=3?&&=Fjhv1;_31!0HwTjvoAoYi?vkHv$Bhxj5 zpg@2iLMVp6QV#24Q4IUq_n64Z`9@#kd+e$KpXX2OZ!E4p+p4C}iTC_(W*-;LMK3`j z2b-vh&&@?2{ri9e{2h~_@60v3T~+O8h?=x|&_HdEY}9WUbKQwM*BX<=ANXt1rj|E! z&~D2y&eC0u9}7+?*kcspf-D?OpxS@lg-1!^A@K?3Y{Q*EAFQ&HBXQgeoK%Wo@W@fi zyaPG|i?HzP;>(H5ZubCh$Ud=;gDFC)6FzaHxEorvH2G4;9PCqeBh^vQ?X}~C2&QFC z2`pG6$wd89Z(~})N~a+)%L{pY74?tY-$0B6w;jKaK{;8@uLlgFzJ0Mx#ss%lf2yHrUYb^^7(FH}Jq2xb z;b4+EC;Qd=U08S2G|TbH0Go zDZ_MOxY3tOK@nMbio%{mx*#HonOk1V)pjH<-3y{M$MidvO$-m%(|pc5>TOG;cEWD^ zDiqLj@mVHTi#*NZZ=_}hhgZI{k>Uu9B%$~_?Wn2nSF-*eE9MlEyjb|P%0@-UvUCdKNRVnd66zQ^U9TgI&DY8!jzYFAo_ zOMZvm!8JQYUe&dL*%o9|Zh-9=9f~K=KK*VkOGm`<$1-+k?>&ghmEF3XWC<++CWj9s zTzKf-$Bjnj7KTKrvCbVdX+5jy;iK6QQy61&*x3cuPiMeQqJUrE*uvsJ$bkHFEZ${E zwGUE&9PgBjLaNg_IlGvNR|ctmLE?9CnUHjZ$zGHgbVzQCrJ&ph$e>jzN*yzKC~iV0 z=y&K%@F8Eleq`SX1E9W%w>^>U*Lic3PO>r)`w=W3WbvCBj!t62U&!=AP!%K5kra4j zsWR|HSrFwoR#lEZ5?COU=O-4M!yu-j>z9r(4y$mY)}p$zZXcV*C_YZ{ufVLdjtCw@ z&jWUBL-|tItuZ6~U~W>tGVoZnqURd?^v;Z?A~<$(071dO+ykSH33T!kwzxa3fkB#>2W zns2RG$+Zlr>sZQ~T0Sgnk(t!kMsux9SyHw)4drdz;Urm9yt(L<4hC&9G}Eroe>g3FYuUCkC#>Ul?xj8E9_fY(DHW%_Wji#~oc4fTivG=n8{tka zm6Qq8*5q+0AQJjXd>Ki`}C-@Y1_R@~slEM-NK#D2c3U1<_d*99VQ#YW|F_j(+58 z$geJ&!Gr!2bRcPj9wH6hH9o}dnOedh^wi~Adms}HO)tElV8VmWK^>D~ue)664yT!W z^(Qg3!5@tQE#aH6wh*Yj_z>h+M@J#3U~+q(rzoq@m=V)peVhRRL%Cno&e(kDFM z%+t`~RXzWO97`{4L8v{eXV@HCGk7XAtX=Fni0RO8#wP$pKvkalKea(MbkC=kx=(>W zo-S#V``b^5Gfk1MgSi;<9zP|>UK>afs&17nvwSc-?q$(QKArLyJTwO=66pt$hJZgvfLFHDv#yDM;BP)j@^0T zkHrsbdiuqFYkB_^;DN7z>QFC=4BA2J916v?#2d4xdRIKl3d_G;`T)C{osj^TWG&p1 z^|r*^%Eib42W;k*XVD)4$-&D7mmp0F^>q|FmPM8AN=nRl@-O$tPxR`u-w13nog2OBBkCfW%c*0LF}bj+E^IdfXvL~j4W zH@jTHY+i&Qs(dQU4W0RHM3WZH{qH}9Cyzk+FiJS;PbYf&y2$51%+zAn(eTNa6FHmE zg*#Fu5n^_q~t>!^f5F`Fa)N@Um~QC+7H2M7BRZ47n8cTth)@U48!6`DfsoM*aHK$|@Z2 z_K`L4(GxJ)dDZ{6Kiv4Y{r!XISI|q**XIu#lh2obRP1v!Dd;BY?|sP4?32mI?>}E} zZVqfngC3??|6U{ot@if>y^>n&olWEZFO*_9{~Ic19SJXohOuq|&m$InQW+zSSv-kE zmzFp+7U7TZjPMe?d~Fc)B>dI+KkbQgd{2d~PiIadUsqdKE#^r-{-;yyV^8JtpHu9~ zTljr1>Q}&jb&7qysC>P(5qW+8N5!7UMLs5zf}T%oMBb0`u0;N&$NuiXawHV_MqRp_ z>}07${t~!yeCM_}-yh$6&-}I0oDf5ErLBWl(@CUmJRGHZVLZFC>sL1#)n$k}dvSPL z-A(PV%`W_nPlkKUpg%H!xnXWC+TkAWbRyTh{uDvu(8`>VIi1}JxqA~oyCzHZd1~R0 zDc(vi5+U>O^M1lp<(F2y`cJ|D&x5y}5T(oJPSP=f0|SHaoyYc`|E{pzdHCBCTNyY? z7TW<{c4-eY>J(>N=*d66sqCn1otik7jE7*8@7#d5R9*=)diWlUIRY|tpsbT&VC7^54=I{GpTkx5=bDVQYvqprJXB(wI_hs$%88>l z9A_!ul?}T_A#W%))8-kmVx8tuD1RXzxaZD}!j4C`zyr%$cl0-&%h5o%|`srbQe?u4&q&`Kz|BSe{&7E-RJe z3m&bq`A!7V`e8dIB$C79lFD_Mk1x6dtFQ^OC0`jpGj!duaFP+D{)xeIavF1Sa0^JOe9Tw)9jF&~xs`h+9&tM9`QA{RVjZCuH?m@OAO zD#!S?L$%$CX6}qShO6THWAlu9_1Fw7hDF@0j!jm=bE3+IzMLZTk47)9_>tw1`y-{a zQiwt|2&T&BG|#)nsn#w^7R3T?4i(tmyH(W$$f2xKd9WZHW3OU#v*a*8Vu^AIs4&4x z?Au=7dm21Xj3gD$U|%fo6M>Jko(4-hXScb2M-E9_Hifb&%Yu405R|#g9r$mwr3sB; zSzqUOtR@T55dxd4J;o+xMA!D-9}V!n7PuB3U}ffj)kM+s70EmK>w%JZB$ih(5+Xu` zAwGbrIMh3OY}A?_S=16rt%vxV%Jf;F8TlIbbXlbu&!DKhG)=amW>@mmoG#iwURX;0 z$EBD{a*B$FA1}XM!Q}|TwENk3Pai%w>33H9Lz%P zOauFR%#aBRogladGNx_mDwFI&gOwthE%iWV`@T`Ml-T|#y_~D%@#a1i3UFBZ>aOTV zkpRR3s4x;!h12pAAA76RR{cC74%qWx-<)-IjY$?F_3%C=EJz9Fi(cN?qN^?-no|lq z&FIj<2QuOw`U;hs2M;1=^XozrNKfpTBPZfXHLaQn4_XtD06L?nGK#}uo=TEK&I30g z+yF%edj?e(BVe?p)=_sezVJb;ayaiahx~3H*Yd}VBNsP8Q01ALhnG~vnW^c!`C8LA ze`>G?LB2^~&~c@JMt4Ie=1Fj_2=B(Q!e>Ms&99O_8x)N&pSDpf6dxj{HYuO7ygAam z>*L5cF{Q_qQIw;0gi%8jKD%zJmnhFbksUtF?Yt(DsF@*DnL|tjxg0p2YX#LzJE}<9 zlmn0JF{-D9CYlXO_S8C)gN}y)gXd#0Ty9iv&|$?<+xTg(G?Zi=#x1bm9HZ#v?lU4@ zcW--BiSOMBk8n}pY<3ZHEo!^W+B6GKqraFY=~A>dLxnFQd<5eH=bS>OBBgwS7h((c zuY}UQz<6AR$5cE%kc9YeudPWEWLUVv@)sbc{@Y31&%|QCrT1wGqFPk}8Zr`0B$D92 zdI*q*Sbx=VNrfx3cMsHl9|SJMdjtqh{2exoH(;eg=JD>%u_eWF8ARS7d)=s_Ky9Ny zs=@1}c+Dv(dKV>eK=-kvPR4+N2+WNVFWg<}2*7Y%?l65*+X51~Aa4PwcE-Q?#uze) zCU56O3zm3zWk?ocbzc)sU~c$LEXDlHCfWN!@}2*%Mn`R4B9v!ZPH$l*GV>kf#2VQj zcy`^^{-?aOg*E`vmG9)D&YEtc-*s7)**R%sR{lon3!Q1%U;hNGXP1 zmhz3y-c)V0vrB|dIU7k;9~)+FkI9r7B%~O#zvEXmrQT0P#~uz4uFmCl;!naHb);yL zr~t>tHYy^0mkL&Ig0eE-jB=t>)$fZfoJ}gNdNC*8EO!;;vwZEfH%5McPZ_&G;9e$> z6b(gH4#rZP&wWyLXzHwGFV~{}Gu&RArgx&h5{eWJhw;AF$-@X$N5`*9?S&K?*=k{m zQ6AhX%A#30s+Smkilr|=RWcUGQV;ml*5evSofAI20ItO>5pzvI(M#S~S z`?hAXAyt2peCJz7#0wVMh>HkLj^njy4WUOtM#qj4B?BA{>k3zjl40bBlI@HjNLdfN zq1lxk2(-nyA||+r3f1gX6RiCfw6BGh`V-1yzLcfG;$_Ki(bmZlSQO?0nv`ZkNpwv#dW6ur_54qq6?wtQ85xnm zxMImI91x{bG}+aFJYmwlP_*G;q5vK=ET(R`F;O}NfnHgu z8QEV$j)O*!;#v-VV*X)xmL;t+uK0`(VbC>Or>_7rGD^;D{hu73Qkx&O?>1Psd-jB4 zothsd^7XO};_V5w{60DY4l&Q0684b>QOm)K6CQ-CVas$d&(cV|Tq>qqQEG+{q`2(2t^l(f2c?d-t)RPHbyIIq39E1RWr6%ulv1F=Z^21y%=1Fv=L@X!iJ~-`KIyYOGeF0kSwq*c%3x+-ZDF^gz zq(_8A8C`WTlkx@3u>_g}ZL!aHYBjy=-+fF4yP5-%TJ%GI;a1NFNv9@ewg(`@;ZWbQ z}D(pe(nuGtX@jQj^1Z6H?_+_@Ns@qkYv;ibKEx{Ng6js&ZO^ zTD1qZ8vJ?7?$Z14gbp7ZcAH^0+fA@P5|>H>TqU>twN56%h3JZg1{El|1E%BW}L!D&ZW!8)j0eb99 zsK?(R=*w9<+8Tetp0wcP7Xz8R8JZOA8B+-?vB+B|qQOKs;u3|{q%@e(nAdns`wXF? z+fGpH#@a6t<+u zBUq+ni1Jq%M%pf94Nng;ZA#0;a8|#+V?2S|SN26=nCDtjA4PR6tFVgD_zhb)^8Fq} z|3EZst$js8h$$`1&I@Ou-aps-O^Qq}=21@j6JeVtCEcZ_9%bKsp6+57-AStF%4piW z@7#BuXNZ;YJne&-4{#GuTn3{6Omy!Cq+yPmDpT?WAC--$Hq)h-7{n&|Pm{xN=A#MgUG5+yg6I z_!slA@T#70NLdPnER;A&+07gKb{rTaM}1kL#oU%?mvsQ-RN8=GsYPspY2qm24-Gts z^9O**vhN<*7Xd+Pb0tv-is)p@T(bP88*?!6q9_oXTO5mrD^CtGFe=Fk0At73nFt5? zx*n&`EH)|$4Y7w3G{vG0pUP;CKjX#erJ)vGWy0^6Xo|$CD~yM05^~0N;KO;e1I+S;__WZtUQL8`=@VHEV3xlUaNpCc zNP5r{0Wg1$-a&0-Nb;X{Woo!l`4g@dx}npizoRCZb1oYmmoAE{TjFJ}8$S9n zi;s?u%b4(se834l;nJ%1LViJ36l-BFJJ6Cv0j|(lZmz!UlH6sC80wG|BEQ&#GI8Al zd`@o^ri2*%eQVBZS8f)+?XAD=37!ljm8#7dd`Z>aaUYz2I+o}N7Qq14eCveQ$#7W} zxkMMlyrkRvHPuNOzM>a1PY-6jTU7dLCkSiEA}t0+uSJOC#K?WR0vg z(wfd@3;w{$$&F~Np^qj>gzJ$+&GclJz|<8S*|7t0VZ zq#oO;;@XpvJ^AF?R!Sl^1$b?$`^{0VJEy8Ss-z>hJ^G5_a8VQzD>Y}B#PC+<(%Y7O zKQ*DL&y*mxIryr$&w?Jy;NdH6Wz|zs%{}kcB-PN;^S-jB6)-guOi$G*FHLzUM%#9R z-zXIO1_>Nyit}j4Y97EHy4h;O@_{-?@DUbA`HE`1`M;8=l*j50^x)mW?+33o6wce(* zP!hzF<;SbY7?|nMf1*RU{Hlpu+QzG|8Q($GWU3mO@w72`)(#yfs`fy2AM}^0Mne!u zvS5_fbQ6yebTBqf)hha7o)FF>!eWfX*gKgGz(Ztaf2#TBYw#lGkw?UEFI=|IW*h5v z%s7M*Is^TvYVn<|2CFQa2K6z#LX!IavnjohJkAuMfs(76)Uhtrzqp1+2BjE9+$ETI zYm!*uvOU%1C!cRrJt{E+XV$AN2tko+<&PLaF*QSSv49(%{Q2M)?DZB$8RY7MUj~kp z)FDhcHEElg*#t5)9bPEvM)t~22sS6@=GLO~Si3&l+hH|~*99(e&@5?GZRk)L@{Zqo znB=r|Ph*kb^a~#WYw0$`IclKS+(|;K~kmlR{QQpAg1BbC8hIU## zprA<`P(POQR}3Usy4;+|^diQ|y)$~|GbnFG;stioe zbo{d2pI`E;87Y2L1D>I2 zHF=_nLzgbBjMu_e;-nKrI;~jBO2Nh~?g-D2r&eOd7JI2tomI1$a!5MCKppAPnSzoH z_w0EKokjd3<*IS*;g4nQV)_6yP}!F=a_!t%GRsP$#XS-*{`#Yu{+ET+kcRgpl=JFF zXv_Q#to&&+bFoomy-0t_?x>hTphAJY=3J(FKUNrtgW-2QG^S?%#n?pGekmg~x}!p# zL3%kC378g~!g~MYQ&ZGwv;%DObI?AnM@`sy@HbPZ#9wlX^*@TQL(&)WCun$BAXf1U zyEm3+%8Qnr#5=Z}vG7;DRrgFc+mvR?E~@!=qL>G--XTi15+sHvZZ45r77ydRZ&aD| z{9>Y(+FVn#V^ud5`~oYVy|xEuZn(H11{GvGP@#;FwNSBHG2L8+3&=lRMFRs`PARbR zR@Ig?H@+Z;jk@}mlxFpWlvo0Y_U9(Nt#-HQKn;; zsCKe}%X&`Y<7~uCSf5~6bRZQVKtL>5qvg*oo0oS>kddrlmQ-=mt}y=x)J!E_ zz3@*_eK<;^x}{NMnI7*JI2x);j^6m!2?P%m!w^!|{byTZ?SIez9GE&|entG^@ z4Q2Fud8LK=Gie@9(a5`u%Ikfr?3ELYWlwSTN*&T;kB&^7myakj@;!afU%o%Ib*OYt zr5c8}Jkgp*5R^OJ-}9F9P!r~|ti@);6c!AL+*4F%O)sd71``Ug&B&1C@kFP^h~j;z zxaPEHDvD_6wx_ELgC~VOHUt>`A%UGR@%EDBCbl7It;E2&01*K(dDEw{YB0ly#)SSD zvk5x&O`|QuZXHlx`+@9Kz)|u9gI`QIG^=hw7Td&)fH(;bMe-mu3@sB4>B$p=Ek(1N zz{M{a%UKeFT*pSQ``ZdQ=3j%Zp6|);o5z2@hRpt<~hCqOtbi}(GxnS0>3x2K~;>E!p7 z74cK%sPwgKa?S5L?p$3v_BZ4#YZC4P-Td~>llq4*E{E9C7OY!kBD7_j{YB z`JZ1z-ON3&u3j?t=x^5s$X{AZ^W1t>NG<8|6d7$=3bm$Byq-06Fbki~D$jT+E^O5{ zut!+LI?^i4*Z1?a*0ORJH7w84b{kHyuo^IIUyIXsKM~DK$GVOQW=8ROS-t*tXUl;; z-0eu>SW7UoxbFMbo*ihgX7uF4axRqHxD(lNQ0VrXdm2~c#%4Mwmybic4cZ{RZ++uD zuqE%e{_sh>VA$vWp3xM8P@|&D$d49x#^=n?dHA#j(*r}Mss}s!je3wWt_{<+5YEap zQu9I2`e7I!(kG})_bLmgO#ou7QhIV!%ESlPY}b2ZY0ol%jYws!JL@2Vn;_}YaM;#A zQoEI-q$L`cf?1yqXX8%i*gGkM#=cE7p(Jbbl!+~8kky&M%r-83 z>Q?^XTs$n2mwfZ+c6O-a#zpSGl!}&yhcLzSaZ|6&aOO4$Q*1@zCgIwJssvH$h`Jc0 zfhQ~wTGM!xz+3b{4}fXo9s5 z`N#~~VgwK78oiPlOB#x?XB_8yJNvBl*&reAB==}LsZJ@kMineQf*vQ(cTe8dhg5?n zt91+UZ~JAgCsh2&a2MZ3a9x9q7Xj|s)#h8`2J$%LT5lXL_9dMsZfimhjsJLMfHYTw z6|{7;fow}0J6nF|nCS6nm;5oyNOE2R6+a%PYq0}3#BOC7?BOL<=-nIT`0q)<8Luiw zo#X37;89LJnj#!F9@9$aDMPh-wEAMj;SAweOj@=#IV895#ROnD_v0S(ovbix_=2eg z`!|o6fUZrn>tTF_(KohDrQNIveM4SXNMVqW8asmvo_i6xDi_ryfE`%;E!nI8hGOqu zCJOjR0zTk1e%Hjf?^;}GDD>3)*cDAbVP<&7^@+eVjb0CnZ6|0u*E@Wig z{BC8#in9MeqeB}*1LD5U2$_=2ldR8osoXZ$MiUTccW$};UU$oM8I7n>H9NBean;ME zc@TVEQH=pY9YSEmG-q09niH?qpo_ODGfH;wVf|}4q(p%}spQgF8ayCA2U`app+)1v zROgKmhOK@Gcm7)!-iI`vx=YE#6CHsm9HD(xtD7PNF{)b}DLwb!3?jL+_wx%@lMi;2 zJy29{odCKX?woX(`0$rcS~7VAG2L*eIJpiC0KR&cxYV^-PaYBtUo71@>TK!O&qQ~k zefGB6ZdnPGocfP7VPdnCw{YNu$CGE;PVf=gBB!uVCZNFBT%`L2xy8^mmeiwnR8?kY zOI)Wj%|IAx))Ugvk*5!iU+j0ZxWE=G#N-iZ70aIg| zj%Cdj#3(GYFPNiXRRsao#Qi@4-vE*ZmZ5UZ*FIl|#eF2XVu-e9flBp3oJ@+^Xs?Ra zdqT@p42aSY*rJp(Jrj0o6jE`w7V5(^pnCCMuu79NDBYtZL<{{9(&x8!_G{Z;F$wvT9Z$5`gY8wQlTpuB1UjDSXHzPzWh z4t(hxrcEnp>Cl}ew=JrLsj6m+I#oNjD-$ot7D9`gw$g7K>-26(23Ru=Zr#kX)L1y0 zy{3e%hh$1Et0X&X*l~d8uV!m}v2!`LC0Ix6A!s94ZEPqs8YD}j6+d+d3jpZ4wG_%w zP%~g82Ob}**MLO`>8qM=ZsD=?x?p_O8Jhq;yPAi4FR;Hi{( z&Kv)R;(MyKe3!gC>!z;F*;+z9Akm{YTnOeDbh_ZHsXmAQC2FYb=T8@uY(7y1R-GVA zL~O?lgGSwz37emGQG}qW3t!p97iSeh&%({M5F1(V!6#!sDpk!+^_8TsU+Te{@6iATcF7Y)@mca18)>w>oj zY~Dzk%}2GosA*8Zmg#%b!j6MrpVYuIh%sjX>7qpX%il{SBypWn0_{~mFH6XvVB_ej5)}jE+n-{g-^k+RIyrVFBOhfJ7)iV?0n zipBIHav+#?bFFC9PLan-+DZb;lmyo*psVfHG4RSCP0tmhG0oIka!wC7D5k>^;ok{^V{d8*gO;N8*mL?YC$gpdl3S|W z;0?5rAe+*2i}n^hCNN2IYYyhAoELYavAEdt|K?&~jPvq3hQIoRlaq>B{V3mu|4~w( zLycOCrY#X{c~md@LxbyX;e&#?+WH*Lw2t zuDRaV@~($>T0yApQ_Mj3WiRL^y^bYBDv)Z1yeX^&d5geTXG&0(sZx!=Ez>65D%DLC z2@94AFdc190Vhct?t+40tz!{(szUuoChcb@&VK9x9gV{)oS*Wc2Z)X5G^8l z9c4u}`#!pTrf z2hSF(kqpA`k)eT+p`aCmg@h0GqD{-C_jZ(Y@82;i%1axgm|6tboT8+V|UZ?6*R*SN;GN(_`|CpS+w@S`M zhRMDi`9Q`M3(=1bsXU9RJ8Ooe^AQEE{cM!MLi?(Nw;Z>M2BF8?+Y|An9DB7=m&Zgo zE1{(mjkNxgS6YE;IGpg144e4dpOT{5tllU?Q&@0N)vW=&3g)t;sXjU8Pqbwlc~x=Z z-dc0>6|?)QfpD%BvTUC)7kM~rNz@3Iv}A;-#=IaNnwM~AVd%8E`mjee5Rqh$#*kOxc1p?Lm z!(lxttm4?kQLXi+MNGuJ&sh#_pkPV9$_6&WK!e>*9aGq-;BDc$t(6qqB-1FR z^3+&EGZcrIh01wr4B=npvJSe$idrMp6gIfBx1=sG9e3>CH8 zYZ^Cnda+1TFBMnTQ}VPXQ5zUKrXTbtlZp14fr|EH9D3jAgv`pA3nOMWnuv0_uSq0k zH7G-C5(olvPK|L%r7VjRYJL*`)`~cHDP1Mgz>B;Wr9)TdpcD}LW{z0cry?sG+$2hi za6DU9RaF#I$mavE7fBd@%`AIay&*4Tk~wn^+2bIJa1zIS!T2QBQx8pB4VW%KI8WV- zEr-FEnqXq507XUoNUqw5nuOYi5`?MmiX~I^CWPq73i#Szw8M8YOJSlwZt`#7Z=zoE z^xO#Yg$vkJOE4iBu+exc)EiyA)MuClwIRB3$tZYfZ+h&JOAiw+~DBbL)N6@Y;($NiyPoQmNdX$2#o7#%sa7#q7tKvsTvGNaTrYlN9)*& z=Mu%4EEzjmoC6-JjLKt+-!a+AWLz7of*nA7f36B#0^L&<2mS-J@Q795sjC~e+l{k0AJE{ zRFhVr3*O;pzmw~F-b03MLDq-<0K5`&i_IcfTYg#kX*@7rMs}gSrBEfPWRnHM6n(y> zanV}{Q$4Bg+CQoi9#1MM^dkSvS=*a@{axu}SS4TXmrobpPl|LHOGU_z?iTPw#ez6J1n% zc-YLMbPE$rPZih)X34gskKl~nEa2sI|px*bBrcP<#kV;_Uab?0z*SVyX_nHb;kv1V9k-u|7d_yA<#t9W zk#4#;F9;zLyJmsn1;~x=u~`gME+|=_K1+c!#dj4V!DF#%KgU=}%UVAgl=d5GU(B8C zYK*jW4Q^?l7YEyej(;=q%XAAtR|a>)*DW9aT%bFbTV_)#war4Q4W5_yW9uHbmbsORJSE#;lt2yX9XI^sk+|WEM(28hydq5M&Yrpzy^>S$&S;< zKG>;YqFe|$9iGBn;{nucYZ*~C5}!F_3dll$LcjM^xA`e!V}|ecp1o}jyd6QjD*uDM zdy4TiYSaZ^wr$(!LYHmZR+sfJ+qP}nwry8+*{&|@^f!~8%p_+n_QlCgCVlr_t>nF1 z$+Mo{^LA5ck@T>VNtW`gbNj&i7DvAu8zfM1VKbmgp7j{(2{s$VE%nlRe+6X-P}_Hc zxt;8CUL;gUp1M#CdR&O}`G(N zjOtT$ttTiW*9z~{92(P6cQ`LXw z6(gT`!o4bTViXpPpmglut-r3JuF;^R9Gjvew}pD$iN>+h>=v)1Rx|{__F-c&gRgQ6 z8`huiLBA}+BHVcNkC*rQ!3Fq)o~w}jZYqzr*xRC3QrCPUyYz56c@Cd)|i&{Tb{I;6=u24r{r-foD`)qo>3T=ZI7vP1N{0Sbz6(#R9WH~%)?YQ7iTpo zA?TCbE)BCz{=2v~V7GzKpO#q(AhyoGL45!CYJ8DmpIl1SR@yoepWWF$VkIy+D$ zHDj@RK$EL84SS9ioHIxE+29<;i&96)f07K^228LRWwVAsIsrYUgG3mFv@&)F;#Epi zxT;)@0=GOEeP=4C)eVpP$W{Fl(tM*`nOzLw)?g}N*G97&G-zHeoVQ%=%&yQS`HBH` zQ)AiTfLE}Uc!nGb4V@-FmiVMVtAsX9jLy8BDhrm{qY169pyF4cCGd39woFYvO9O7>P)RkW(`0gn^wbAFc{wAx{6(| zVKTscjC)%#@;0*wmW?MnSdjVxkFb-icwbCJv}yL&kqEx&2o>iuF6DUy)DN0IUW}CT zM7ZU<&e3RPe10K1iMw#Uw~p{MYRM>-!GfnO;k*SeepfW>?LY;83+KoBbN_O1@=!x< zHf*JN!Gfk%M5nN#vmlZ(U9@p7OSm;S#Y|v-cMKRTI5PD%oS4$h*&IWZ^I$CyE+s7^ zMn7oa5^g7@>ggk704h@1bzFar8uM~ar};P#X7}h=qY5Ti^UOx%3|V92J~0&IApaN- zv^0@_9#E|YyatBzqZFh82~w`^Q(H`0?pM5){X+V1v}F7foAO2^8rJNlKguzH zGXN{q*#*g;aKo-Valq8M+~v&i@(diPw1W+d$*-ydYPbZj^dp)5P`lEid;~J!I1sGp z+Bq~IC4Rsq42V+*9Gk4!84Xd1Yl{a*6u$`a(mH3bVgE-sM+p!>EpyI61`;-y|5h*v z2pZ_O9MW#mukCJ z$#fn7eyW6@^H`MuRg}0jC_&Qlai&se6(C2Y=X&b8p&OQT zk*2e)`91|Dta8R3svTkAgw@m;%?pEg*+!ek+3_FNwo5ivX!2nwZx%|k`TW+Kr&@(I z)GE+GPayrvim{`;xe*=N%hzZhdUa94X!EEkDfHo)A97w7TCq@dem|}1} zlK}G{1yRy_pRYn41@?H&L>O6T0Yd>tubZStj0YGn=^*a{_9%~?!*KKA%i1M2XJPFV znbU4Th|>`J#wDyUlB(zdA(lEK&~3=nT`GmVYqL3jDiNG}QL!?IW9L~Q7tx{fZ#tlk z836EkBZK*k69oSU5QQ_kHq4-NC|*!alR|gMo3Fc$%1BuZLv{(uaC=z-0$_+-j{uG{f`eyLjLcU{|t+n^MC!9q4@Ir&rtklDE>1P z{~3z^48?zj;y*+2pP~5AQ2b{o{xcN+8H)c5#eat4f7?*B8~{G{AVcsGZg_rCs@q4@m)`TO&s zw&(LLmGJAyod5Io>RJEm_~M@YchCEEDWT6N(k`-$LHUg*u8x3+t)*`&t0VN z%P3-$UC;M9Bj5M@Xl$p9#MXVtsD{(?@2zgrD0PZzib*Z?aw4 zeKMI{?u>1GCU4HyMnn=_D=UMSv=OLl_k_z_YL6}LyO;Ncb!tM7T^s>RTM5m!*o3QD zq__w4`hz1WYA04hY#wj`6Io_8Krr<^OMNniRC-H@&K2C$k~Edq(Ww(tl%-xsbc&(Z z-H7L6>pGpj;{S-`eXdDb_szngGOHis-?yF+{q4|XQ0dg!I^hUJ@{sNK?o=b}<*_m8w| z$XzThHE0@UG0bRmc+yp*VjGPD>c=6Sus#gM>US9FEnGpOWTO)zBYSQc2KsIqN(aYZ zj5u>@oES%8ZGI43wTH|)ZIF zB2%h%Ay*1c>KPXKjjd^Iv|?@bD*oE*QQ$(QqVtUG?ujJ{cjle-c9-oo+DT-Ko@X<$ z2f_3ZNqpp$<+B!0Otwmp>R*em6hkH{!;q*SKRk z&}=E5GnnB5WS?SWU!yFwCSjtGTH{PGO^rD1qW$M09g^pC!-zR>C7qcl_61h`Ko={@ z{m(8Vcy8Pf*Z+={Z!yWS7Mp#=y-%@f5~m^k?KD)_EvqUNBNZ1(kEe4)p;9v62tioi zZ^Q$Ku)UvAz6kO#hHh$_&`#I|xYW%c9gJa1cj_27W%e<~H@DgELwFf@6qu;&G9B}g z=2=Q?0h@pc=n`Lf9f%GP8;60+L zcvU~r+(O1Ik;_ak1>JGCDjNablU5`*AP7R&qZraCFw72CC{@6k73(Bgz18~~56ToF zNyyyQ69x7S&O%f}g{hRe)lj=3gP<)MLtdM#N4e$0lD5z6`P=x`fI_#du5=Wo!9;Kh z#-d__z6uxGy1sp&Hn^iPSO)WJsAGfCKwjey;U4R~qaYTB;Z_I%4;HF{142>g=N36I zYD|J4VhNzyMp!L9dhDr-v&uYDTrA5pAR;G4m7$=~kuWul#dqkKl#W!?98lWA0iY2ZNz6Z-a6Y_6wE^5Japa z2P`=AG_#CrR!`@ogSzx}$yk?{n_wbP5A0Ax_~)m%=-~~|y6OOjwTyu!8tvVEg#_P* zS)%;x$OO;O__WYSt0MZ7J~iN3_Vc~%XINH*n+8NW#I$+-0#!AM_TnMtvPrOm#EJx>d|$x&6t z#OxH6Q@9GkfSGk2oj5ry z{6i#U5UV}-G_&d!lNOCUV2GnmoGyM{14QUouumT>@JWk+=^zPDpT)4;-7|p{w?Pz+ zymJC>mw$AW_viX3F;YaNVX+GqhW_gztgpC2_ph%bVg%JPd{m@ZXb8lg1%UY+|<1^5E(T<0#F3lXnbDSyu>OVTV2_{ z_7));r3?gBH4Ny9Jw_cy8bOJOgI%|>DYZTd8nzG+p^9uKd-iC!5j*l)zLJ6HusTJ^ zHi-bWMgR-*)nE%URkfbz?6LU#qE|hV%>ri*9*eggTP=jom&kz|IIbl;2~j^Jr2tHc z`Lt&l+nTm2_CiI%!=Bdic$IUtr67a|81&c0W+r-oDjI$n3O9t{;5suCv{IiY5hl&z zKApG_0Hzv0LE%6YQw_+svMS>M(wy+o#V<{S*f&ONGxiFO$UPb$#kom5*z^H^b5|!) zdey|tqvC-KZIQi4nAEq&GyrbYNSOJPaBM?aJC!EJlw%O{7pyA}Myn1j%1R!Kjl;KCi%*KrK;l;&w|=9hM(mICs7z90!9CyQ zQ6c2L0e+N3gY^B(rklGao35yUWLIQd)q$5-&LV}hPM6xpqCTTQm8{=Ps#%?IS zh)OWH9CXO1awq5jjqf)QK;JnN2y6mj0oCYZo0KV7!i^ICCHyXy=~g2RtFTx{&LnZw zzm|?fIZ$B2x;)+}E_ErCIvtLf7iLiq*C!_oHB)2@EP;BMH846L4h>)LEk#y_rg=A{ zqmlcP6@mm%3i7ZBQkWdn9v<0L%?QMDuGCB{MY|pS5c;U1 z%*r3+KUrgE1`3uW>LRRT#4yP9_!aR?d*WysiB-`cbZXh284I!kV-r%seUU^GnjL`x z#Y^=#Ah&;f>EdM+g_4#w=xEZZ{4)g$c>+)e1d04`pL|-*b zWti2(APrh>0NCFWv{kb8wmn%4xTbc*d>Wq)Q7IB3&>UleG+a8f!u+a??uhMu2!5Y( zN3BXZ6lLLkO+plH#AUNdc|lxms>4GSgQxPo&{h?KOm`4_Nk}wd1TeZ3d$43dsi0Nk zLk!(w?N=hmJ-{>L9eH`gjun3gEetckBz-=xZaIlT^i*esshDDyiZM(`;HJcWxaU&5 z_}dC6_`L_o4tCNl(D+cjffYSLA)ZQ2V%0JdFKzbT@xMUdqHnJ5&|tKuf&2j%Md~E} z^ORIVd3hXw#>T8M&d5}(>H;~N(j}_;gx|dgmg7sGr*b2Qfk%1DS`Mx)szdaf8QcgS z#_wopoy1rIWe&-4IwR$8MMgY=y-*K~Ru%2%574`8R9Y=dKPXM;X1K?65O?8@fqX?Y z6$Ok^7tlY%Va=!tUE2v|bW`_wDRTA|`UMqed%j>+PrHbwCdb#mfrx`bKc&fmgNpJq z`sInDnC`sGTW<~e9%N6N60!yoE4|CmfUb*gf$5lc|9-<^y_yJkGVMkg7P#NPA-^MY zq0!fEXB_jYX0eG#!m4f7(c~5!?#UjKC{AXyhp`B;qfZy`?Qh znK$StdJKqZ^T4Jz8vguYv&$QjQ7Q(ejLY`IvfRzU7cnM)tK7~#Q3vW6K0LOu~ryaMY*qH3mLzjLA>AdQP zwt%U4q#=>e`4K+74o^8QA;L2-@{)NrpG?zy{R=|B{SVXi6ZnMd2893c;d&#;ay*X@z_;J0#h z!4DBEq9{KlE0l`j@Kkjl8I1R`m<%6^6qx<4+0b`5Y8MAKVmB~@Y&}?hX?i<9Uc8di z^V&Gn-@rl6Fdf^XR-%vu3M$B9EL8eLz0#811G2ZxRQldn(woP!8JbRVBHu+ zYMt2}L<6nxeDm5suh5Sx+#umvyszLsXF3!dsrwRS%eym@EToSCD5Ej*t|2nWt6p$G zS$6=KEWTJeRFs75%B@)o78HV=zKGzLtfmO3wIA@&xIV!=^RO7x=sx%e zI`nHTV}WVN#m4~Sv7vGnZm*3myv~`@w2jGvk|&Bo&xiwP)}zOd$G#~KXUOEdlatl! zT@3YbO2HG*786)|!l=mFzQ;5N9vmk-|H&?(?|N!Ct5MLl9YU)fjN<3)cKeEj(zhx? zzZ6}On^5)S(e9%*%ng8ls77)p9^6qK-90);Xy4#l5EriA2^yoS#4aAg=UvHrn&64F zFHkgkVOz95pQ!n}cIJ@2{QdC8f>LKiUCW{TZE>L0=iH>abBv=Wj0~_1wtD$|d!gc5 zVwrJ<$UFnS3UF@d%=|G%En9P?MR&6DsLk;?O6V#)xZ18nZ6o2zZ-V#qn`hRV&%PkS z_w^fE1g4z5Px7wK1eh34tx0@jSke zALS%>cd586pS%59wYUDX&-wG4a-Qm>nnRNArhWIs^AE9hA0f2A%IaoNr4;7{;WJbY zv}@9>-O(oUzDS#^q8pqD_xfOMjViQHV5Y?|LJb z>=;C|naxe39xQnnXwyxV85Z}@h6D{TJIR$$;mzELqfi>FT`eNu>{*3q*|}Yh=z(UXPlk*-SxE!Yy;xp$6n5 zkSIA{K!ih21UL=#;qa;;aBdi@yrBDFsUVe6OOGUqz%{aMI#aO$Q+I`V9jP;5Axh*7 zR+z43)`0ZEL@HlE-A@z1dV-$~5gYr&u>y4vnmHV;z3>a1K|6H}80vw^@YrmmUJo7k z)j%aD8Bxu-H6nNoiWhY?$hta= zA%E7AKK+ll&{(%Kvl5ze(1V4v@E-{Dy6OR08w-v@(v!xJXY37E$B|bqQ zWok*%hDIiu6jhrWvYdgf)HArz*@c-k&m6{{C-b_03H?Q`tq3Gl5@iD_K#IIYS2u-> zmJT2q0w$(L*bRT591)_STrI<{1WVhM&TDta=`wNi_j)+fM$V@LOSc8Sq~f2W?!+svOG{**!T=IshrJ+DIIc8-?E$6ps{H`s3`n z7kOOjbrS(FxN}>YHeDvE;InbA9fcTs9FmxpZd|Z|N-ID4T&$#qqr)OBau+AUxhA3% z?XNlHme7$cRQx>Y=#(k3D-K*^aZ9;;f;8gxQM>;DP??nzu*yS3uYQIK6u}GC z=7Y?io*OTND#J=447OOZ6cq%D{IVlFMv`2F7FFn^N&qZj)nON~gMvKJp)p}e)Y!A( z$+zb64w5awHiJ1AH4W(jp@PV`9v5k4PLiHe5GiaE2l7@MRq(pbCIB;d#KxPKGeB8p zH)Cc?nwbv^Bk6_s#<`=U2!IFz>(%4b(Vd~pWCjk zBu$<-?<~f?4#3P_^j6tET4Pm|COa$Z)`nyjxOfFG(1aHk62JO|=sITz`*pd{YbNd&^*^jnECc=A-~YT;;Y5f(@eU;v(59|#9wP$Wt%Hh zIY{Jx=8JNKeb-v%o5ggM%TRzW!(R?x-LP<57+RL?mpW*imZ{-XaVd<97BmGV9jjpi zY8=lk*HOMl&B01)Iu%j4zpfO$aSUMC;sKut0=ms%Au+Nu!KC}T6T7_R`u&=Q^S6{L zpm<8+jkp9@)28~{pHpv3f}H0SX$&cXef|*Jv#O0~1e8#q{eIWz$>JTJNfqd!+^!`T zE%r=#;0*2dbabFm1u({jenzMH5n_j*AL8wK)+CJ-=ozPe1Ad1uJJc8S$LLTgQ12o( zSdU%fsIw94`sG%qAuTiMbDn{4bMSj+<;+N;8o0s1CI*5K9f%D5ig^Ot(|MqZVLuKd zqZAIN&T@b+Bf^%yHG!P7FF=;eb!T=?;Cx4pAxQZWkkL=iy=gIMw{FyfL-3LIHK3f4RsE*f0SYwBIvLr|qbyVvdak9L3g zz6SDU$B zUw@s)ul-U&{;!vZ+|T_3`M<%KIsg0K=&aAnG{mstMKKtwYf{s)BZlb$ZdLbzVn zo%I)+k+tF2=2!aON@HA9*`4Yd3?;jtMO`6?`5WCCmHm%uk%$iM1R0y-%Zg6YD;=g@ zI!-axQLSF^xY}CTwaBZd%uDfX!y3!zC1aCAYFbpL8#s>5tW3&``M1f1v;9mJ?%3>F zA$P;LH?qIY8Z|ySUd~6JyFoLzjP3ZNTt`}4T`nO3Nwlq+ zJ!ujA+E0s9>GcKlYXz6XGiEo5+jOfqa^{EFe=reml+@1LO#?_nQnI!CThYAa(yYO8 zPmF$6%PUWs1v_+b8lPR)m+6^>;ja!;5KbB1hGGwU^fu*fLo>0nNii_I4Rk_*_D<#? zP##3?xpmBR<1sePyEUEQh;6bqWn@GD2MF)@McM!hLc`cOVVS~lV`LLP!)Co9{A2r! za#g%v$2R(0!&0ZjIH{{Mxtm?AyG75{;3O4Ms#a|NCx?G-W%#A_NF(5(1E8G6xZ!cN z9CGUZ12s})$0d+j`?ukBk3`(oF`SDTk3pSs1pW00n#rW?DjJwnRYt)Ba=&=!5if4b zWk@OzC>X&voZ+j=jxfYmBe0~x}R>2U}h8{WpnHVxm^9oH#aPQCVH z-Jx=O@$$|zn&C6bA+T01OLKQ2@4E18&Eu=7dZ9P^xrC!g&FQWjqvvfwdPP?D2f}f0{m;S^6#WO& z-lO~~h#er~kbwZfO%KtS(4kSp7@UMsyNl*v-}mRin3gCQDwx8tfpeB71ZFXvs|tSu zyC3PrWcz6^SH|hZ3eo9^WFr`vwKEL3clfyt%?W8RdHPqDSmEWMXVaM!qM$s5AR02p zlo!YP$>t8?I%!-sW;qCMC)N2lkU@;1$$sugYHsOB#^E6V454fxLmu3#0G&b4S4vb{ zlyC{#08e!A4|GR94OyxxmVRT+?nJDrI1+IK#!;!hcLTb9o41Nc1p{K0vgVoz^l}}} zDRiT}C9)<;7>CZm*;2pZN(#g?~%+PIOf^oGJE3W?0O;-xOx%cnLPokup~)a zXC_Xo(xrz)X%TmL)95;0w&5YXfKn4fgxvz8Z2>{(d^kQ^%+};!ZXjK$Eg6JA^Qv@) zVDW6b;A#}D^;tZ9uzr~^#^VhJ149o8@?tbkcy7O{B(aBYrBaconu_*Grr?1wxS%0; z`%;!97w?uP$jqDBJ(ypvpu9kG|t# z43Jhx9-i7_SQtGP8X7hP%$c*m-sDp#YBIr;0pS>~h{|fDrbs2+B8pl>XQD6uM^`Pb1Cgq}r<zQ;D+ZkZSW8DaRH<<4kU|3FaAtlYq32LG3VLFJ1{NeFlax1y8~5L>sl_EX zTGH^d7EX~X{e;S_?{C0MRuN%>4z<~;3B@a?@swfZ=7cDFk71jHGLnx-=QChMWjKv! ztRV7ZG9FHMJM0g&)+9S2qLfm$nBJ7#I3}PQX#D&C?}lK!anvNxs!9B zp0H0b5+Kd=q5l|FaTv5H=UGPn-A7O_VrC2tRgC@y%m|jX36cySc?(pC&OQ9wlj_Qk z*^*~o()JdDo#pA#oG?^?iXkLmHePXfpG+L3jhk7_pnw?Xhvqr51Ag(#L1vzzE2 zR$=~Ef1VYR+c7C3ayXE;ABe#`{gpPaWcy_{{ZEB0;GDZSE#MMevAPc^?J9mmogB#S zLT8sW;XKridmM3;4Uci9sO`+W1HXA(0AtFv1v#N$b}5Ms1?lkgulNhoI8R_Jd%vt^ zB}Gl;Um*4!7dD0O0?~x|z(a%}9CXUEvFO}@_;I}SLX2UUG?yhos8I0414zV3b)A;_ z3L@+rd=;{ou=AQ|;Iala`?L%^(olxGALWvZy(AS(pn$y;*laeO2`D4hMGQiv{L#_% z)3ID4fr<^FCx)63H^uUrJkWVF2}R`}hJ~6W58NEb{+}MjxPCm$8NUfe~f_ zQK#jw-Q=$r*r}RIR7%Z+TB%YtE;LtwVk zl*vRk;z2J{H@Qd%MPuq}0Dn|BSjG@$c`t1O8=(Y${Is;;Z(;R2WLu%$vkrw@yyxf| z9Yo1)Ssb}lI1?qy1BCc0fP@Z0VTMinSRZ>4S;^bzR&btg*H&o2#YyHAtVy9M`}aFC zZ7CNpT6$F(5+W;j2K;;qr76ZxY9dH*R3qURa>+9kR}!-n`cNTnJS#2Y0TVv2R?-@` zE(?56@cz>UxkRam_wnf3AkY~qQYc+0KM1VNFvaEv9LCw9iTJ_=&9o~Q9^4e<(VET- znb*ea;#7yp1mxL(2@6b&=`dpks*Hz2LWAHh05la^?Q)L0zW_4^X8^oP#~Q9=$AnM}e}!wa-_AfUAUl}S zN7M{yY%Y`q6`3%tLODx7T@S0vPRZv-QV=c#!VOQ%nb-kNBpc@hLkLSq{$uEgIzCUy zWEj=S(0kt#;vRYpO2Nb$oMZq+1+fq-nKM}U_0g)gEZPwVI8Tz+e5An`tyw3`y%>ae zYa2){4!f$gl-`)i8on#t-=G(uXyPLF118B=XOp8-aeEXx#lC{#=r#Pb7ZLw0GjTc)DXcR zKgnioWvo-_SkQ<$dLgcnr;si&3DhW$lL%Nt3=LI~h>JMLDUPWV2Uo?H<|lu~VNWxX zI-WSQ=A!tV6G>^HV0pL>@-$H>mB^4wnb<%Op`w9o13OHe2H^Ua6bCRdEiF7SV*ruq z83IVM(x@d=-{N<@2#YARqLNi(nMnbdEkyJ^lpGYKKaedEm4;(tl)plXi(7m|T6|CN zdO#;cP{qohfF}s%F(LEVeHOKINLZ!TWlob0 zOXonIUQ-CvnC0Fwy%%Y99s6*{t7C_CQg`r+!Q&{i76~@R77=9msXZ^^r%LxB?;!{` z&=Ys^Zz=bTJPMFSnDpI{JU-1Hkp)w*5D(Cbd#yBKB zggOe;&ObOuEvTX|5S)}30RbUn^8f8vWH6M?V#{a8c7kH;^> zFU3z4n)+Ot_z=+P9b9LP8*a4^LH9<66ZabxDnkZH!9DEFRhaCtXko&orM51rp^hv%#<`n19D6 zR9*Ms`@`p-8A&^g*S{~jG7i=S%*d6@Z*azHk^NMc1t|6>eUfkXM8UGJk2*x=_;A9g z`vfqOc4#}Rb_KL`18hlr-^&PI44C`bjq;oPT$w4126U z6=nWU*cbfXDu;U5Axm=r%gCr963Zh{3LUW7P!Iw6q_KQ=d zYI(jL&!Rw1?j*JSAbi6ZNU8~p59*n2NHy{FX#P#UDVZE%5Taa^W-^$12|-r=4w`d5 zRGx}#sPZsAm`)>EV5aGt4k8wi5KNf`s25TtRfBT~x3^rdaC^$Aov5OF{j=GzC8`x; z9xF*@I+_J-pcqwQ*BR&@4RTHvAlX3(96901PKY7!rvcXf`HCR{3#1dsVuoR4k^$v@ z7y?=z01X)@lEQ$G5gylcuxv>Whqcxe=TXFFio90`43R|X>n<{ihTD%9iJ4c*1hRGk z+*9^2Ah_e=CN-4d2O@$*EdE1~)9_%}kGsepjK(^M%GQ!S5#kMja1sb^!O<2U8R)AH zvso)3A{+*!j6BT#yc;79sk6=%^jbM1Y9J-eITY1z)Bq%@t8a-K#UA)k7v8C_*1vrJ zAr5tovjR3{kZk9 z2>3FeBtg-2QqhQ7miRd#E$nCw+;Z>(e3!=MJp={ukuTLnJA$SGq1zY;hEWQfl$@R1(6+;$%f(~7aCB$eqw zl0jo?E+RD|e|7`#ZJ$8O^*l zhRA081pc8Gwa@PJik5wKV~yg=JC123Eqzrnu#v#upNdfPD9oro|8wGsnhCtgRHs<% zBu30oz98loifE^^0=O3emw2Xj*UC~&!K0cSertNAG@c#@nub4V_l%E*uji$C&+pl` zCs0(MOY`1Z&OW!0=hRzUX&TP!UsrD+%aUsTe(3neft5i{HH91>Dvsj5BhAQInmkhe zcz}XTWCm?tXKj<<`Y@!}NjSPF8d#`&Ud7NlBu7|^R98ZRVT-c%Inm`u58Q zt)Pg=*w`LtpEqQ;Yeu;|H^?2Fv;Y%rvC+ynJg^GIv8L*We%@o+@b(%W9^Cs`Fip!- z;M=8x)Fe-}r+cM!)yjin-NE|DLH7mUT#>@8)*Z6?u9JY=oB2>{U|%vYC0#qz8k)T- z-z`!%iY>)~-b6cbsLDo^TxCD~~gT9T^NkdODDmj04_Rczxhx-6Rbh&UJl zz+|!wwMmRyk*-TBLuH`)0}9MfYT0ygr>d?C%#;HgaLmubQV+?!C!9~Hn15PZ<)EIu z4OKjOQ8l?@IQ^x(WCd&9I&PS06kC2 zd$gCsNUdaobeKEJ*{pnCild^)%*PdxkPn}q7KRlNQE_}5QE+P-v85!dXO|?Or6JAR z`ag@Nc?7bpqzQ7!M=4+U+tf1^h9tk$V4=W_PW?2)@ijGg9YE@vqkNrx|;LkXb54fT?Y&hEuCC7=@^G6-6eVvYG~A};0(~IPug&@@xeDxG3C_XYk@m5(_Paq z{SMSv&<+w6rHFiillc<8IHMA&In)$_;-R~KC66S!B#xGh{lkd4dI<>puu~fG01=FB ze}|St?vyPn^1U{vdjw5FE)@%g$9V&^G|P(Fz#jtADnx>=hfGOpfxl_$OxI&@6u5g^ z*TkeiG+C4>o6DKlVq~nZ@QON?vaZo*7S@NS{Bo%K?yNf?mDP8-_Yz=qC?qXNp=grV zTmYI0Rdq|~*eF<<-hbG~)M;>xWSd#mCFrYe&7Qi357wxAJJ0z!7iY2?m>vBfjWNLN zCeQJSqFSUbB>_^txXqQ;uIXzd6*0Is&>ip&41#%O3gKRGKQWhCbo+l8{hSot(~+1j zEcQcJmBm$owJ5nVbA(ThXR{}Dv|xX!j5#=$QQcO_bbj;L_?tJ}bu(BZbqjk}W-Mje z`;c!sR|lT!^wYr7SPM9rH{T`bV(oW*Db{U*~b+dHF!lhQnSS*xZ-=OCde@Rp;c zJf~t6>u>+fsi-cJrwDvqUni4oQWo=@*EZ|?1=&b2LjKRfs_c-Aq*2k|{jL){kmTd{ z_CkXTf*=@?!pc}xL>YRjpKzNM<8cuPD8=30X~JZ(lxTui1rjTY#(idC8*orljS77R zMC}91_B`7@PjHC}j7unU@e3%Pz%o!w+v$mxHVm<4nSKI*P`Ib2i1Mc`UTKK7Q(m#0 z;trxRz>1zTMS3 zVy`YNDQ_bFttBeW#)tHSkcazF9pz|eZFV@+*P@uFf%wUkN4CM_&n(j1(8 zyqN~7W1_s$RQiK37dfrtT1@itrB(d#mml?#$jYNM2w<56hl~9uW=xnXb>MT77r1$_ z=wPk_yuAeew5^-Lk5q5KGwX#i)eH|QA1Wi;ha72D64Nm;?1L09XL-LcsV77oEs5H#@Y6Gw^<@u|DZg?}39E$rkjS|>_?&I&d-9-w zMXdVL{V_}Z^`f`qe_l|0e)(qk`m*bJJ95?k-ff)qc?!JwdR_hP^I3`L`~JYl=k@-X z+WXnzHQsvD^S(c5_f`C{&GFmkHP`p$Gq&gBZ?Ek8W%#7`Bh>e!@8$Tt_G|wjmjC@y z{BNzSlJWa7)@P@u!{?1~H0Y}2f0v43;9h0&*H*V{4l|;KK|teTVieSyLUSnEhCmbl zT^6ek$|M?cUKA;Wo-=_+k`y;R7~lo>3icAPe4+34%;o2}g?95k$?}|Ce}8Pr<-gFp zs6UDQ`W(dm{-fXfbxY~{YS;VDP55;?`1coY_j(D1YrP*%{&vOs|1OI~&hmeqj`DxK z=6Zh(5_*4JzUO*Beb)AVT@U(x+}(IHExx3?p7K2(d1ITtbU)0D*s+c<_*6~Xz3Sp` zrdizE0-EDICW(*Oyr7n^mv!K4TD82wCLiavR6F4-PV%OpLpZtk%{R&b^}B z#A5Sfs;EaQYM4a-uJ$YlQ7^8GHss9t~pCb-tfg$WfK|e(TLiJ zul%RQvQlO*jbw9tfwj_Zrs_O$+ME{fAjy#0Fgckb#nGi8b;wZqtup z%IL!GnR{v0zGBZsI-A$)=9bzZHA{<%$J}(e1=CZ!>oj0OyT*w*`O7;)i=8%VaTr_L zHCFVIMfkFff&JA0A&`HHYURBc{7TNRzFg7KVIFfw6zfIz$(a*dA8vFJaDC*KA3ajY zyUMhYlfQf^Q$|M=v?7ZeuZyJMzOJg)*yaBBBJNycxJXcNCdc2-2XpLbr825?z=B_R zxLtkO%zWPb*a6Dj>!skq7BSNf{h=dkV$PaVow-__x$iZ^>#C(^O!6jk}rDo;CouTnG>R0d<440E1%&5Y~BW@M;vv zAiS5h3v}1$$6^iv_6qt19gN%`aa})bTP@SL?r}RJ+92z2HJV&Dy>uoRA%qbvpx1_i zvopFheRTOW;9K2kh7WA~#$+qUzH51ro;@=B*7?+9>@s*a!FEF;7tAfSJ#1ZAGrm(# zovSEorRk5nkCAX$AOm+&&5Y#1T!Yd9%Y0}?8jdg`f_I8zZ=l7-#+0oae}aEQtSwEP zi#zhGWNC`fG53G9cb7472l|@F8Qk67-Q69EySr1YP`v2i?(Xgm1zL1)x8knFTik6q zH|K12le^iR7n{A=g;)OcZR=ktx40TM$T3R?cArVYLlvoP6}j_)e;>kj3< z;V@9qNPrzwZzb{qDH4A0+eskGUg|+{8hHWFjF8XVCe1F|9pj#=5gSKo4Tz$koE!~L zIJjmFseu$B0E`AJXDH5b?2zGoKmLFoRKW9uDelv>Svpjb>hs%hy$FM~D6Uvt);dEo zwG>N67^%>P`W;Ee7(;BweMs3_mVDgYm&4EQlWS7?HNakMwfkfgh-4f$IO*`W@UTE; z_yW2-ZL%1r~U$yltwLC-G2Puv4Ml_?9e1=i|>YkSZrYtI3gK>P;nX!^*2|+)^GrlH&+~`4b^T77A>H{!ZG~o6)_s9Dhf3X+r;d=(kwGm_?$jh4~JNV%5siT zO@K&c_}N4=_yx@9Ar-?!Xm_88K!@1dpqZxum;AZ}%&q7-Tl9URJ$r@VEp6qgx(kaK zwXmka@~I0l06h*}qpW8#nY2tTXI3+?{HlNXG_3qwFav3&4;L3Rr5aPVMv)L9cBmz= zI$KlyAp_5nIbl?6DHt~9E*+W0jfE5k`l zJ6jV95DW*QtKFQd0m21Ce6FSi;GkfUd?mdWt#Y{(o)7-+E+`!Ua!*f`G}bLgVV+`G z787YT^}7H%LD%N ziB{c>P;w69sZVIA6M5o=6#vRrJ%`-4!;t5*eFj^}Gtn*BUjW~14!*Daz0rAKAlD zn1}Z`g0k0u9q>sDoX!+=y6hY69-NWG7!BGd2ZR=qW+o6|v7SkE$M-ZdZMXy+a=9~N z7(>olTXs)20iioUFrg+)FlRXC2<>&hp-Ljx3+qOZ(z7W$8EeA+^T=8JmxX$odSqpN zZzXzq?_M6%w4@E5ce}%SL}r9z>!KNbkT$@UUW=0{G-ew zXlzZNsHdQv;1RGAP17yHN$>^m*}i1jzcn6}jKAdr$t7U7f+}rhORsoU$Em>uG~lbt z???OV^&x4Y4tG}T%Lwi@zOXz7j3ma7NXQb3>G7SU6NqhW#y_>}ABMTuk&4x%n^9n#yi^20E z?1@|IW&f5bS1M?hK)3BU?1kTQOwn|C-a0MXKt@fX-J4Riduqr5P}Vgl!C4Dfq^EXo z3lXOte$U|!4U=Ob2h?yp2ch#4O8*eGzcuGLfY9}}Jy9D_J@eBXFas)FVh(5(ro$tq z25sk}`>O7mBRz!!S1`sKL^SA{;|p)%lbg^V&>A6-1QOZRFm}r5GZaOrev{#ImTkDx%P=@ zKCpp54Ou~*K)_N`rw%Oa&u|xRg7lqfRMoGrkl-f5(Z9{%A<28_V0RR&RV=s>aUR0> z$s}4_b5zlzeR-KNsiq`6NZD}_%X+pH=Gscvg4Zs$^ z1roYK2v-1OM^Y!*zLfvltxSaBVI$uK0256S6Me}BwkKnb3Ct!iTm4?=p01tXFx?u&G*6`DDS5u9IZ2cl5$9%g#}a84D6xl|0| zL3e3zKN~VKL4h!_>F_5NJkAPFa93J8tC1l3;CWQi3_b?bYmEsX*KQevN<0BcqeHU+ zUgvHHT#MT&JCsqgNmtP~pMh8PX;9UO52GFNatHI|rP@@b|1`Fi-w$KK-<9$vsw8kO zu&4RrbM2K>Vdy$`^`6S86TO_#9sh`8a?Gl?+fdiE;%Ia-!u)4Z7u%2j; zbn+gk<;nb75@5Nnaa4(Pa^Z=3SyEIOR8Ab9lRr6$^TI+pL5TGvW?3KxH-7Jn(&&%A zW*Wy{<092)F2(uI6E23$?~R$(peCXd-&RwLf(p$p9#kEd8=N-Ak)*i+gkk&eWXKoHYNf~tfxD2t)XgSU;Rt&I zrJ~hhfck8HeG5ccaD4Gv-PuIKnLU{qtGOtXl1TC}E7C3(Gbwo_R5FHw)B<63Qb!qW zUe3rs_|cuYFF;|)26yntR(MoWWY+^*uuA^+NM0HQ+W|S0gP$#A^kP6!8{TV=!hxi!*{M!Uc;1((-{DG1Xnq)#v*)8 zlBCQFg;WMP<)zP2)?_Z+jA5ln9YZQtbKxeoMbO5jZHoh+(C|K+(|dgSrb#A{UCuR( z8A>LEDi=IEKCBd1wG?Q5hJlVw$~YYX?sau|bYxvIx(!X?gODg-+oCp=M#b?0 z{EHti{%9uuH0m4AY}W%v4{|O8Z!c~kuMm?)V~>i49XBQ8+_SkgWe?y5!~qy;$zkW# zrGbB_HuKKPJe0heh8NX_7YV1J*%=b}y%$COV!)~^q1n-lZ>xMzSjZ1uSr(}RZ)Zdm z0ML#3*G9{hP%yu|jQu57CD#K~w7M9+q8R8iAzMuEFf!PnxJtRpUBDIeQ3>=_Xrs6i zPq+EL4CTku9!LC?Jyau~%+-9IiAR_GLEtN3Z7nQ>t( z_s)mnxv^Jxc{8zTT8Ovh*Ev1$pCzWJqQ-4hvw9a1dF=G(d!$6rz2XvIpaXA8C!#R_ z&$*RZ9{*Hco!ip>l85P5)KXWXXv%Ac)K1FP*{T=jWu;0^NRDjs0ZM`{3m#J0b&_)p z8l}!?*rH5j;zL{#A{06gd`C#=V>At2#A5AOEk+RK&-^{<9 z1-%u-=>+x_z@p4Fd{>Fa?J!kn&=h{X!0KphG(FUV6{Z;i+Z{a;`caplL|xq3jNB9A zS-539E9H5Cl#Q-O^)kW5M0HwrrXWyh$Q(rtT@-8>5^KKT!x#mB=@RU|P6wD!2pY%; z2(rajkSpHzrX5@sJ!K-N%ro5VGzW9Kt?yWmFw-M~4{8iAjv?I5hI9)gHZ$A3< z^YaSP+7A5%<)IHK)gJIS=<5a64`PnkPv(D_oHB5>M7W_G|9FBeKABG}7G8I8;3#Q6 zYUJHOOgBayzMNNH{%9q<6K`ieN3C*B3o0N?mFg1TLiNvDCfA@O>MoK*dy96*=Pv`f#$ z@4*T0jqaw9M7vTFM89=4&9}0)(_HF~RnzKx{A0qvaOb?O00d8X@4foLz<9RURb7`oO_#7st5{~rZuu!hlwM{p40WH zh@>`&z9l}}Mjb_^w0JBNG(fq0ik z(V)K88JfMEhR!;;6STBK&&{NYPPLaOxZh1+U%Qc$^Dzr)Q2u5Cf66eAgDs^VuSevZ zW~vZe`^MTgzxZUycce%4nO8FX_t)WSJ3QO^6~5YK+5^MyRENlOd^Y>?Z6mn0JkIk< zL?-8Ai%fJDDGJ47cxIXl*DebYVhB{k-na)f$SH6)Vm<{`1CrrRwCox=ZNf7NDuZ9< z6c@%6HB}lnFsFr*Tx^^ET8m`Hn?#0EV$soQeIk*=pY+|}jvA8-#VWWUg-qG;gy(mHO;9Qm?o{8b6Emm2`CDf!LI0<#n zqfonm5f`0iMDJNmFdfscwnSoG%jTwaq&d76<0#J1qIZ3awS)w3Fz=41k~Z&n6fmMp z@#$BNC*d?!L82NW6n`A|O&}(RW?*jpH7!*vO6sp7i-L}Zc}yDZ;8-d4Ls3a>K6J{) zL*(eDoVA8MBL=vkoFtFo$LRubmx?&(B9Mlru374j^#V#vOAf{5$ z-%>S}Xc1CUI~)%hJ}zn%>r$IRwW~}Sk+wAr;b}TxA|6tDSl4{($#DOh9FqVch}%T! z`TlAHGySew1nsXXlv?g9rc6(YB1KK!#h^$5NcZA_OL{y;pE zm=Ao=#c1Gf4+Dl z_=;h<)Q(8w0A5R1#S|h_peiQqv~dgXQpsbE{F(Yl97);QRArQZXy}V##Sb)9v_CF- ze5#)^*x)B$1|!YwhQ1dBF>-s`&C5KKiUtcbCfLhME zIYk7ee@Ci8if`y!?PX#sHe$M@r5LK=8c+M@Pah$<4SxZwEsj)UEItIc#GI9=Np1)@ zG{gv2ZRqaNZIg9+uvbAUFg7`L3Bg1hcr=->Pz%xfflAlorIG1(6D6*!c^P4yMFYvQ}-Xis1V4r}&)z zjm+c(!yIhCcepp%TWP9ZRrK{7%IzuM|&< zQ{^tlot$k2BxmO2Fm?OiLYKC<%639;YTF1(qGpd1Qs~b@-J+aT+cJmcLVvcA`R#l1 zd>>1mCe+l5eJ7G{$)N_X-1CFK5yg@YQsodyZiU_I9~V2}8CDv$5qf(8Kowt*0Enbj z%;F6;L|lp`NDmA2dX*Oe`46Ok3W0Uga>?R)B66@;6)|X8CZd6$WMrN=X;qjbfI!wk zeKoJ*P7IxB&OEg%;cSoiHi{1_nJ74@6$6r&${U&3+=J#sdr$(F^oCkf6A~WbZ_%tW z$Ud^?xDTl~g!NZ5zu}rwu{TYWi5j)C;Sd!%DG@sZF8nUqDiFr9`Jr^ihR`N`Ud&Wt z?$RZ*LSDaL7z>y^=W7qoc+svvjpp$FKE|4iDSaHp6M3fs+`B1pS>V^Pn6=k`abZWw zB5+&9AHXOK7S5b=IYe3IZd)j~gVzKH3Zdl*lAxIPEey2a$LeWY*7AoG*!D>(DDX4T z%GPR%O=~I{GU@NU-A9Aw+<4^7hPs6+l_3-gs2gjI(aWo_XQmrQemY5Z$m4aC?J@8& zFyg-k+X5wyKfCuq6#YD{YR}WnJBTlV( z$LV0wbsZZS`a6gUCpN?qsH$Pgh^T@nq%jlTk@rT6ldNd5P%Mr?q=FkyxG7-SInV*# zgKnBvj#QM(^KD0$?TNt+X?=Qu55vnb9?(X%#>fR-2y0hVACJg5EPB^N#S4#F-j3%= z3ZD?_lYnNM&9}}0G(>B76hQ#ix)=f9shf0G1;STd0>k?40-TM63_jL_Kvvrf>@>j_EY;o1^wc5I7C|Bg+ z4#oQU%KzoDFYjgE(eLd}`1vAi)9+@x&tD|e=xP7cKhI(pal(GD7w^Q_`TKfe6p8=q z%arij>*D)=u`Tj?jq)LjeaK=Tve<_#_92UX$YLL|*oQ3kA&Y&;Vjr^Dhb;CXi+#vq z|F$f4HvA!reaK?}&&py=AF|lLC5tJ2$YLL|*oQ3kA&Y&;Vjr^Dhb;C#$YPGKkN-tv zv9Ru!OPu$!QRh3fb=m3o_FH??=VlDW|E?*|?|#3X#qWJqCD3ux?`amL`*rd1&e;EI z!2fmIL*#yEs{8rx`)|EQ`9GJTMAvmcU9$XRitt}&F-QOFf1bsrEtAs!v$NR$N*1H$ zA^VJqzGM!^BNl#C8X=95KY~b`6gxT?;thWf{{Wmj*Y~><_W!blasD{TcbD6Acj6%8 zztFT`K1K5K6vpv-)b01Ot^9hq>38QL{Jb5u>GSf>v)FB!$isWh_HK#2mtrGBQ|}K~ zasIClasKxgWg^e}S!W{8LnLn-FdQ*?-@^X77;mJhhHeU+**|lepY9B;Jg08XH^f9x zpJ{0$RJ0PR8TEy!{4tuC-}0&&2>W4xGVy2cxcn!T-8#E)E#D{ZLH(}K7^a%Z#c;c8 z+~eU)v+84b^*u{72Bu_o2c(}XcefK*tccqR^y49})K5n~D z8v%-c8(K*Q1$OoI+glH9-Ks9>erDmV53i&Q7q2NXTcR0 zq@q7~dJ~hu{}>2g|Bki{1xQIVwB#Dg@@Qz0JizDvLzemXI_adygt=APjggCamQ8MB zebk7c$Vk6Vp#EwMrbN5wFgpu0H81JTy}R7@ZM}_d633zQ)>h(LFf&4m+_AkUx#o=3 z!>2Qljs-2k?SIeepKs~tQzm6qbnL-p-&BjI-;!mFN51%h ztoxcfJq#-v%^Vl>DLqvM7p?k`SpRfklryUa#?gRs6?4(gAYw%!hq##5Vg`0EgrQ*D zS7F^?@(auJVw>Fp8fz)a?!E;?ZAHQ=+U( zy!r2`Sh!?g$VQk)`<*uMAqsGI=?mIXCBASc9-1ih2h;*!N;TCXW|y(PZCnsHHe+7? zG|je>n|LO>%dl<~XC-?Fa5Oe6tMcW@{~Y|x6JShk_jS7;=^k_~IZ_TX9ru+Mosa(kF$@cA6JL24 zj0%<*L`BF(p*la}2=xDU(~H0bhfo<+FeLVwiyehyXx*;dW?mz6Tn+uJ!t-szxLWz( zI1IXZ)P%NmTFiZ{^0J<+0@QS!J6Cl7T)_3dVp1U>R~5im+K}XSRX5u7gMwKhhnqtg zrsHZsB?h85txygm2+Po+5Z)l!$B$5;RKS`W<1SXa-T9mZ#}gq*$|BlpPCftW-Q?W%~!3g=WzI~t$ z+R+46!hu-yafeP z3~8{?Wi{RT7DhvX<&E6IxlkN!=2;>Si>asf6>UEh3?TuWa7r~Dm_HpGh#CdXSb$AD7F>QNEKSR^WD!KV}rU{CQ3=jIr!B zJR7l=PQ2|%+|IvOp{+V45y&$pt2;9un)-}B>)pHmO>1zD4~|m)tB^v(&W2ans9F~#EqgGSaCs)T1AiRGfIWG=L3<&GE_+GK8mUJPs!W!5Mi(2Wf~#?%v_EEtSp zt_FM6(&ZXLnG`<$1I891{z}-~Lb8e{dWnCM?9?I=YV$y_v1=Nkv}$ASS$<89KF!}J zLL1O$8jR3yCBp3@9N%2l!mLSn^H*Dw5qg+XS=F8@vI1nnhx?aq28T;{p*{|_T&Rce zNg_oJfld!amX9HUgiK(%o`Q~oi_EVwars(ZjX03(MvJb-f_tv#Te+~;2E=g^E%NK5 zfNuV}MBz-yOxZS&31=T41SUziUJc`u1BvTY)-SFn-nlxS4xzF}+|IXzfD05_jtT+B zNAX%W1<)ZQp<#uIl7j8`=?IsKlA>n^l5X_FOIh_fCHR+Puv^G4;!*W1h3$%~UJAOx zkO%bu={sfvplzTmpzDElsTqjw-p~n4$iIW{hmYeEF7zfkeVNV0-Az#95PyU=_ zZMapu1SYwefChzWZyaqswJrgaPBs4>XGs zOk2^ctHG`c&J!g44U#4}NED0*6_fF&?4T&Eyg-MH)VR!MuYHdpgt(@imzZ}Du0=u9 zr>}SnH$hMptH+OErlb^{>3VA%Zc-~RmCx3gm)o`kVyznQ*j=?ujd*iRCBLV(fL+A> ziiB;be%KsPVc3;mA!v>k`d%85mrL1%D@@hkh6KFov71;`^x4owk{1#n4jMsmbWf26c!KN$M_f_=iqWd1(TQ&ep6x z(bz<;>J&Mb>04Cgseo4*oZy?DXvIbjBd^l5wHiW8ZbqhWX##p=$6!^>wSY=IPD=B9Ufor28hF=Jf+Ej!^%o<1?7q2n(jQQId1m= z53(k0sQ5x?w4YSyz}DoqA#}_@@6ULwmm|Pi6A)gX)@CcPi%JU zKIhLXAa6u2Li*N@t3e~Zcj?+a^hE9uW7D~1~%UG`a&!&(4cF-yz$x;2=iM(pf- za7GXMdU;!hM0^WOvZmp15Fxg>M6MMHHAXnbId1(9eW2*N1LTs?<;#Pg*FiB)ln+yg z&%}j2X%l)ylLyk?A}pxVmK5W{vU~LIoIFtJ&e@RsGYL{ZrU@y6+*yjDmJ?~s-OZ=( zigQG;mcQH4??BD-JEG7`lZ}bDqS_W^n0cuD28|rqUN@o#fST1se?<6*!rb(%U}mbF zQ{CSrNOWSZ#WZX1>pTg`P8HS2J1$eSf40yZq}t95$ILoUzfJM6#I84r&=Vk&K?~e9SpuA(u}>j*^sF zxv*`J4l7;XMi?alSw&81yg$46piqc~e8<2zo%%My2-WVas zgc~lk1McV)rA_4(+WO!?=BxlYBa>rHxF5jB+EBi&MUckDw_!MY!g;7MQ}i=$4RSNy z;~#z35U_j7Mq)yGIIfp8oM)jX^D`)jUr_4d#UTzomLRzp(i1urUA$j};{w0yJFw(j za^@J~*ON9nDmPzw;pbD^4Lo3rpt1X*-s~}K&z4{E7ml%Dq2wt(6$ilA%EP1TACqK5 zd~d~K6`Vru5jozY5h67e^V0x5Gk?M~tYxY2b-IeI(a4j%#nbaKHO1L-8}|KzYe$JQw9yviC0<_$n?aN$3z-%n%o)|OFh~eJV2RYr+M_ss2Lx>C&E^7F zOv`b-tCX;1t}6wO(lJaOLernfN*+cxT?n5d5FS_cX@Z}+SSveoBN&N3Ws6dEm6B@v zuHWIN(5^auPnzsRgrdtGKDzC1YcKs_mXl4&(UkmyY|op=w!`v=m3H_K~PkJ;Gb^ZjfERo?8)I7+3#>IHNB!WmKyn8BeWpV~!eaLF(6rWWw4gDlI{ z8Ij0wm0U9%7tznyX&@ZSu|adXq9nxyOBS7yfYo#~CEQ966Bw5~tmULC>Z#E*5x7=%Ek`Q0;A);|52JMkti|6L>5fX#Xq}E`rBI7%w4Hy2vUl{wYA$=XN>7Fa!S=J+l0ETGvbOS2+SD)2c zWZ2ZH4&mhE)OPMo=!E33M+x*5zdB1D>QKI~n7O7=JA1@W$q5Xt{-PIOx1`xaJ> zLPXD*_V}3hiatJHz!_KWw8s%^snPxuQhCm%zC8t10Aof)(uzhp{wL}d zcVsm~TctaA>!VXME72*;El=+CpbCcb94A>Q<|NARXpkv#_U&zqvRXRFk%(~7)xur` zdkk1e4W)V+_Qlv*o*dpAJ6}(eHw|w{vTY8OmT~z&L2L+tt(G^)sM7jWx5fM=-7zbg zE$s^S=9)t>caZC$yzg9sM!BSLXm*rsMy&~cL-y}NC+6bA88uL3Whqi66z8lv$IsBw zVf-Ixe4GT&nnPdRN-MuqD)RdZ{rP#Ed~bxq6~0l>rKxzV$lIOh(6%7vR%?<1g~gr! zvuVY1gc%{1?9BBOQ?Fwp^W3>BHgrj~i_hsoawGv7){Jmj3Wi%UUeVT?V|FUz_(FwI#DJ7d4Qq+nIa)8{`%bIGv8;Uqo;mrI{B}^%H zGGVC0yoHPuOvJAZ;c>FWQjExacU8)hayAnVNe5`ieO+1;)_8+$Ti)C+BHp303_#%o4M0I%_)lJw@E;I>kVjl|IMX>Ft{eBm{K!Do#VG-;ZGW$j0_{~ zKzt*;qGSw!3v%Wbt}FEL6^%PqwZ>sP{uFpQdgWRCt7qAZQppp z%wP6Y-Zs`?U6?BKOVzs-*$lXFg&^649~&IIGE4k7zYqI)xy-n2laWelWl_O~Ma@Le z5k@Y3aU0gm;McMkq~GW65+(SwnUa-~v4%1nuF*#<|g6gcsO1xuDVF^A7 zY$ghNeA_HhsGKDHDjE0|ES5$5+%Hu@nIosG&zNrU#Pvo zAoT7r3-p;%mroE9vZlpdo<*K0P%=iw{MI!=Qrr_Q^CC2}^*>cxYg7fv>kRa9u6MMW49FgT6&4+~O+*n4oVRs! zpmC+J27AE=U9@CYNIKtxv(gTNxufVM0#m_k&WVH~`I!HiiUh$Sqv zs<$SvL;gjm@~O7$&MCa-bBOe5O?x`TSt4iDwqN2m_{VR*zpCODY29{sO;$bM7_a|d zR>l6&sy+MT`M>cPzwETXmtD)&d2d_o{;%_`#&4H>o3E!)|C21%b$@>1Pv?I((Q5p9 zaOVG3^dGXAh~Gt$@pGSupNY@ai?;v8#gy=WY$)E0y>BR@_$~a$hT?J|;Pl_Qiv3kU zm;66<6`S&VzG!+shRx>r-Cpbt<-Hx!h`c_3dXL7gMBcV}HhtdSW3l_Yk67#@7W;_B zK4P(tSnMMf`-sIpVzG}{>?0QYh{Zl)v5#2nBNqF&V=>2(k67#@7W;oT7Hj^9#r~~W zOzR^S`-sIpVzG}{>?0QYh{Zl)vHu|!t9v{CFA|GAbw3>AZ2G8^0cDZ~DIu#Q9&Kyxj*}Oxzj23>=6&++Q@IbU*%1 zdwYZtu`@Olc|m!)tMU5Ju44a?#RTdc__RV;Aj2Wy2w_Zm&ki|Y^u@717Ll*mx#Xe-Q4_WL(7WHC0__j12G lMB;Nl&(eLrC~~j+boqGX4>lZw7yF-`#R$M4!2b2~-v9*yZGQj& literal 19813 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 diff --git a/exacto/exacto-caller/src/tests/data/bam/dna-009-normal_minimap2_mdtagged_sorted.bam.bai b/exacto/exacto-caller/src/tests/data/bam/dna-009-normal_minimap2_mdtagged_sorted.bam.bai index 9a55b6f614889371dd28490f3999b585c0ccb8e4..fee74b59d7a0d9744b1942a9a76f36ddb41df703 100644 GIT binary patch literal 3856 zcmZ>A^kigWU|;}YuSpP^fgzO<$OVG)y39aQ4$MXn%|OKv1*qz-fdye=qckMMM#Et= t96%v3nhr+O!Du=d%?G39z-T!zS`Lhs1Eb}@XgM%i4h(!b07-=~5&(pD!8rf` literal 3856 zcmZ>A^kigWU|;}YuSpP^fuWoc$OQsVUl7R*B9K5cP%%USs=8}nL73Pm4GFT*a2O2- sPza2sgVA&_nhr+u!Du-!S`Lhs1Eb}@XgM%i4vdxq178k6QXz~40O&d4d;kCd diff --git a/exacto/exacto-caller/src/tests/data/bam/dna-009-tumor_minimap2_mdtagged_sorted.bam b/exacto/exacto-caller/src/tests/data/bam/dna-009-tumor_minimap2_mdtagged_sorted.bam index 5972b5d8493bca2d725697465dbbaeffcd53f2c7..a44e281442fee369e3f213629a8a9916771464f5 100644 GIT binary patch literal 641886 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