varcode 7.2.0__tar.gz → 7.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. {varcode-7.2.0/varcode.egg-info → varcode-7.3.0}/PKG-INFO +1 -1
  2. {varcode-7.2.0 → varcode-7.3.0}/tests/test_effect_hypotheses.py +20 -0
  3. varcode-7.3.0/tests/test_realized_effects.py +262 -0
  4. varcode-7.3.0/tests/test_splice_graph.py +257 -0
  5. varcode-7.3.0/tests/test_transcript_layout.py +164 -0
  6. {varcode-7.2.0 → varcode-7.3.0}/varcode/__init__.py +6 -0
  7. {varcode-7.2.0 → varcode-7.3.0}/varcode/annotators/__init__.py +2 -0
  8. {varcode-7.2.0 → varcode-7.3.0}/varcode/annotators/registry.py +2 -0
  9. {varcode-7.2.0 → varcode-7.3.0}/varcode/effect_hypotheses.py +28 -1
  10. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/__init__.py +2 -0
  11. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/effect_classes.py +25 -0
  12. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/effect_ordering.py +2 -0
  13. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/effect_prediction.py +12 -5
  14. {varcode-7.2.0 → varcode-7.3.0}/varcode/genomic_layout.py +76 -6
  15. varcode-7.3.0/varcode/realized_effects.py +334 -0
  16. varcode-7.3.0/varcode/splice_graph.py +231 -0
  17. varcode-7.3.0/varcode/transcript_layout.py +502 -0
  18. varcode-7.3.0/varcode/version.py +1 -0
  19. {varcode-7.2.0 → varcode-7.3.0/varcode.egg-info}/PKG-INFO +1 -1
  20. {varcode-7.2.0 → varcode-7.3.0}/varcode.egg-info/SOURCES.txt +6 -0
  21. varcode-7.2.0/varcode/version.py +0 -1
  22. {varcode-7.2.0 → varcode-7.3.0}/LICENSE +0 -0
  23. {varcode-7.2.0 → varcode-7.3.0}/MANIFEST.in +0 -0
  24. {varcode-7.2.0 → varcode-7.3.0}/README.md +0 -0
  25. {varcode-7.2.0 → varcode-7.3.0}/pyproject.toml +0 -0
  26. {varcode-7.2.0 → varcode-7.3.0}/requirements.txt +0 -0
  27. {varcode-7.2.0 → varcode-7.3.0}/setup.cfg +0 -0
  28. {varcode-7.2.0 → varcode-7.3.0}/tests/test_annotator_divergence_scenarios.py +0 -0
  29. {varcode-7.2.0 → varcode-7.3.0}/tests/test_annotator_parity_adversarial.py +0 -0
  30. {varcode-7.2.0 → varcode-7.3.0}/tests/test_annotators.py +0 -0
  31. {varcode-7.2.0 → varcode-7.3.0}/tests/test_cli_effects.py +0 -0
  32. {varcode-7.2.0 → varcode-7.3.0}/tests/test_cli_genes.py +0 -0
  33. {varcode-7.2.0 → varcode-7.3.0}/tests/test_collection_filtering.py +0 -0
  34. {varcode-7.2.0 → varcode-7.3.0}/tests/test_collection_variants_attr_consistency.py +0 -0
  35. {varcode-7.2.0 → varcode-7.3.0}/tests/test_common.py +0 -0
  36. {varcode-7.2.0 → varcode-7.3.0}/tests/test_cosmic_mutations.py +0 -0
  37. {varcode-7.2.0 → varcode-7.3.0}/tests/test_cryptic_exons.py +0 -0
  38. {varcode-7.2.0 → varcode-7.3.0}/tests/test_csv_roundtrip.py +0 -0
  39. {varcode-7.2.0 → varcode-7.3.0}/tests/test_dbnsfp_validation.py +0 -0
  40. {varcode-7.2.0 → varcode-7.3.0}/tests/test_effect_annotation_errors.py +0 -0
  41. {varcode-7.2.0 → varcode-7.3.0}/tests/test_effect_candidates.py +0 -0
  42. {varcode-7.2.0 → varcode-7.3.0}/tests/test_effect_classes.py +0 -0
  43. {varcode-7.2.0 → varcode-7.3.0}/tests/test_effect_collection.py +0 -0
  44. {varcode-7.2.0 → varcode-7.3.0}/tests/test_effect_collection_serialization.py +0 -0
  45. {varcode-7.2.0 → varcode-7.3.0}/tests/test_effect_collection_sort_order.py +0 -0
  46. {varcode-7.2.0 → varcode-7.3.0}/tests/test_effects_from_mutagenix_variants.py +0 -0
  47. {varcode-7.2.0 → varcode-7.3.0}/tests/test_exonic_splice_site.py +0 -0
  48. {varcode-7.2.0 → varcode-7.3.0}/tests/test_fast_path.py +0 -0
  49. {varcode-7.2.0 → varcode-7.3.0}/tests/test_frameshift_cterminus_regression.py +0 -0
  50. {varcode-7.2.0 → varcode-7.3.0}/tests/test_frameshift_helpers.py +0 -0
  51. {varcode-7.2.0 → varcode-7.3.0}/tests/test_genome_sequence.py +0 -0
  52. {varcode-7.2.0 → varcode-7.3.0}/tests/test_genomic_layout.py +0 -0
  53. {varcode-7.2.0 → varcode-7.3.0}/tests/test_genotype.py +0 -0
  54. {varcode-7.2.0 → varcode-7.3.0}/tests/test_genotype_from_vcf.py +0 -0
  55. {varcode-7.2.0 → varcode-7.3.0}/tests/test_germline_context.py +0 -0
  56. {varcode-7.2.0 → varcode-7.3.0}/tests/test_germline_effects.py +0 -0
  57. {varcode-7.2.0 → varcode-7.3.0}/tests/test_haplotype_effect.py +0 -0
  58. {varcode-7.2.0 → varcode-7.3.0}/tests/test_maf.py +0 -0
  59. {varcode-7.2.0 → varcode-7.3.0}/tests/test_mitochondrial.py +0 -0
  60. {varcode-7.2.0 → varcode-7.3.0}/tests/test_mm10_klf6_frameshift.py +0 -0
  61. {varcode-7.2.0 → varcode-7.3.0}/tests/test_mouse.py +0 -0
  62. {varcode-7.2.0 → varcode-7.3.0}/tests/test_mutant_transcript.py +0 -0
  63. {varcode-7.2.0 → varcode-7.3.0}/tests/test_mutate.py +0 -0
  64. {varcode-7.2.0 → varcode-7.3.0}/tests/test_no_duplicate_variants.py +0 -0
  65. {varcode-7.2.0 → varcode-7.3.0}/tests/test_osteosarc_fusions.py +0 -0
  66. {varcode-7.2.0 → varcode-7.3.0}/tests/test_premature_stop_short_description.py +0 -0
  67. {varcode-7.2.0 → varcode-7.3.0}/tests/test_problematic_variants.py +0 -0
  68. {varcode-7.2.0 → varcode-7.3.0}/tests/test_protein_diff_parity.py +0 -0
  69. {varcode-7.2.0 → varcode-7.3.0}/tests/test_read_phase_resolver.py +0 -0
  70. {varcode-7.2.0 → varcode-7.3.0}/tests/test_real_world_callers.py +0 -0
  71. {varcode-7.2.0 → varcode-7.3.0}/tests/test_reference.py +0 -0
  72. {varcode-7.2.0 → varcode-7.3.0}/tests/test_reference_mismatch_error.py +0 -0
  73. {varcode-7.2.0 → varcode-7.3.0}/tests/test_reference_segments.py +0 -0
  74. {varcode-7.2.0 → varcode-7.3.0}/tests/test_rna_evidence.py +0 -0
  75. {varcode-7.2.0 → varcode-7.3.0}/tests/test_rna_read_phasing_source.py +0 -0
  76. {varcode-7.2.0 → varcode-7.3.0}/tests/test_selenocysteine.py +0 -0
  77. {varcode-7.2.0 → varcode-7.3.0}/tests/test_silent_aa_pos.py +0 -0
  78. {varcode-7.2.0 → varcode-7.3.0}/tests/test_silent_hgvs_description.py +0 -0
  79. {varcode-7.2.0 → varcode-7.3.0}/tests/test_splice_outcomes.py +0 -0
  80. {varcode-7.2.0 → varcode-7.3.0}/tests/test_splice_site_effects.py +0 -0
  81. {varcode-7.2.0 → varcode-7.3.0}/tests/test_stop_codon_classification_bugs.py +0 -0
  82. {varcode-7.2.0 → varcode-7.3.0}/tests/test_string_helpers.py +0 -0
  83. {varcode-7.2.0 → varcode-7.3.0}/tests/test_structural_variant_annotator.py +0 -0
  84. {varcode-7.2.0 → varcode-7.3.0}/tests/test_structural_variants.py +0 -0
  85. {varcode-7.2.0 → varcode-7.3.0}/tests/test_symbolic_alleles.py +0 -0
  86. {varcode-7.2.0 → varcode-7.3.0}/tests/test_timings.py +0 -0
  87. {varcode-7.2.0 → varcode-7.3.0}/tests/test_transforms_left_align.py +0 -0
  88. {varcode-7.2.0 → varcode-7.3.0}/tests/test_transforms_pair_breakends.py +0 -0
  89. {varcode-7.2.0 → varcode-7.3.0}/tests/test_variant.py +0 -0
  90. {varcode-7.2.0 → varcode-7.3.0}/tests/test_variant_collection.py +0 -0
  91. {varcode-7.2.0 → varcode-7.3.0}/tests/test_vcf.py +0 -0
  92. {varcode-7.2.0 → varcode-7.3.0}/tests/test_vcf_output.py +0 -0
  93. {varcode-7.2.0 → varcode-7.3.0}/tests/test_vcf_parsing.py +0 -0
  94. {varcode-7.2.0 → varcode-7.3.0}/tests/test_vcf_phase_resolver.py +0 -0
  95. {varcode-7.2.0 → varcode-7.3.0}/tests/test_vcf_spec_examples.py +0 -0
  96. {varcode-7.2.0 → varcode-7.3.0}/varcode/annotators/fast.py +0 -0
  97. {varcode-7.2.0 → varcode-7.3.0}/varcode/annotators/protein_diff.py +0 -0
  98. {varcode-7.2.0 → varcode-7.3.0}/varcode/annotators/structural_variant.py +0 -0
  99. {varcode-7.2.0 → varcode-7.3.0}/varcode/cli/__init__.py +0 -0
  100. {varcode-7.2.0 → varcode-7.3.0}/varcode/cli/effects_script.py +0 -0
  101. {varcode-7.2.0 → varcode-7.3.0}/varcode/cli/genes_script.py +0 -0
  102. {varcode-7.2.0 → varcode-7.3.0}/varcode/cli/logging.conf +0 -0
  103. {varcode-7.2.0 → varcode-7.3.0}/varcode/cli/variant_args.py +0 -0
  104. {varcode-7.2.0 → varcode-7.3.0}/varcode/cli/version_info.py +0 -0
  105. {varcode-7.2.0 → varcode-7.3.0}/varcode/common.py +0 -0
  106. {varcode-7.2.0 → varcode-7.3.0}/varcode/cryptic_exons.py +0 -0
  107. {varcode-7.2.0 → varcode-7.3.0}/varcode/csv_helpers.py +0 -0
  108. {varcode-7.2.0 → varcode-7.3.0}/varcode/effect_candidates.py +0 -0
  109. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/classify.py +0 -0
  110. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/codon_tables.py +0 -0
  111. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/common.py +0 -0
  112. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/effect_collection.py +0 -0
  113. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/effect_helpers.py +0 -0
  114. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/effect_prediction_coding.py +0 -0
  115. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/effect_prediction_coding_frameshift.py +0 -0
  116. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/effect_prediction_coding_in_frame.py +0 -0
  117. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/fast_path.py +0 -0
  118. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/mutate.py +0 -0
  119. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/transcript_helpers.py +0 -0
  120. {varcode-7.2.0 → varcode-7.3.0}/varcode/effects/translate.py +0 -0
  121. {varcode-7.2.0 → varcode-7.3.0}/varcode/errors.py +0 -0
  122. {varcode-7.2.0 → varcode-7.3.0}/varcode/genome.py +0 -0
  123. {varcode-7.2.0 → varcode-7.3.0}/varcode/genome_sequence.py +0 -0
  124. {varcode-7.2.0 → varcode-7.3.0}/varcode/genotype.py +0 -0
  125. {varcode-7.2.0 → varcode-7.3.0}/varcode/germline.py +0 -0
  126. {varcode-7.2.0 → varcode-7.3.0}/varcode/maf.py +0 -0
  127. {varcode-7.2.0 → varcode-7.3.0}/varcode/mutant_transcript.py +0 -0
  128. {varcode-7.2.0 → varcode-7.3.0}/varcode/nucleotides.py +0 -0
  129. {varcode-7.2.0 → varcode-7.3.0}/varcode/phasing.py +0 -0
  130. {varcode-7.2.0 → varcode-7.3.0}/varcode/reference.py +0 -0
  131. {varcode-7.2.0 → varcode-7.3.0}/varcode/rna_evidence.py +0 -0
  132. {varcode-7.2.0 → varcode-7.3.0}/varcode/rna_read_phasing.py +0 -0
  133. {varcode-7.2.0 → varcode-7.3.0}/varcode/splice_outcomes.py +0 -0
  134. {varcode-7.2.0 → varcode-7.3.0}/varcode/string_helpers.py +0 -0
  135. {varcode-7.2.0 → varcode-7.3.0}/varcode/structural_variant.py +0 -0
  136. {varcode-7.2.0 → varcode-7.3.0}/varcode/sv_allele_parser.py +0 -0
  137. {varcode-7.2.0 → varcode-7.3.0}/varcode/transforms/__init__.py +0 -0
  138. {varcode-7.2.0 → varcode-7.3.0}/varcode/ucsc_reference_names.py +0 -0
  139. {varcode-7.2.0 → varcode-7.3.0}/varcode/util.py +0 -0
  140. {varcode-7.2.0 → varcode-7.3.0}/varcode/variant.py +0 -0
  141. {varcode-7.2.0 → varcode-7.3.0}/varcode/variant_collection.py +0 -0
  142. {varcode-7.2.0 → varcode-7.3.0}/varcode/vcf.py +0 -0
  143. {varcode-7.2.0 → varcode-7.3.0}/varcode/vcf_output.py +0 -0
  144. {varcode-7.2.0 → varcode-7.3.0}/varcode/vcf_parsing.py +0 -0
  145. {varcode-7.2.0 → varcode-7.3.0}/varcode.egg-info/dependency_links.txt +0 -0
  146. {varcode-7.2.0 → varcode-7.3.0}/varcode.egg-info/entry_points.txt +0 -0
  147. {varcode-7.2.0 → varcode-7.3.0}/varcode.egg-info/requires.txt +0 -0
  148. {varcode-7.2.0 → varcode-7.3.0}/varcode.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: varcode
3
- Version: 7.2.0
3
+ Version: 7.3.0
4
4
  Summary: Variant annotation in Python
5
5
  Author-email: Alex Rubinsteyn <alex.rubinsteyn@unc.edu>
6
6
  Project-URL: Homepage, https://github.com/openvax/varcode
@@ -181,3 +181,23 @@ def test_ordering_uses_ordinal_rank_when_scores_are_uncalibrated():
181
181
 
182
182
  assert ordered[0].effect is low_priority
183
183
  assert ordered[0].probability is None
184
+
185
+
186
+ def test_ordering_prefers_fewer_cis_variants_after_rank_and_priority():
187
+ baseline = product("ABCDEFG")
188
+ outcomes = (
189
+ ClassifiedOutcome(
190
+ EffectHypothesis(
191
+ phase=(("g", "cis"),), enumeration_index=0),
192
+ effect("cis"), baseline, product("ABCDEX"), 3),
193
+ ClassifiedOutcome(
194
+ EffectHypothesis(
195
+ phase=(("g", "trans"),), enumeration_index=1),
196
+ effect("reference-like"), baseline, product("ABCDEY"), 3),
197
+ )
198
+ candidates = merge_classified_outcomes(outcomes)
199
+
200
+ ordered = order_realized_candidates(candidates, lambda value: 1)
201
+
202
+ assert [type(candidate.effect).__name__ for candidate in ordered] == [
203
+ "reference-like", "cis"]
@@ -0,0 +1,262 @@
1
+ """End-to-end tests for the opt-in realized effect engine."""
2
+
3
+ import pytest
4
+ from pyensembl import cached_release
5
+
6
+ from varcode import (
7
+ Completeness,
8
+ GermlineContext,
9
+ StructuralVariant,
10
+ Variant,
11
+ get_annotator,
12
+ predict_realized_effect,
13
+ )
14
+ from varcode.effects.effect_classes import (
15
+ Deletion,
16
+ GeneFusion,
17
+ Insertion,
18
+ Substitution,
19
+ Unresolved,
20
+ )
21
+
22
+ from .test_splice_graph import (
23
+ LongIntronTranscript,
24
+ Transcript,
25
+ deletion,
26
+ long_provider,
27
+ point,
28
+ provider,
29
+ )
30
+
31
+
32
+ ensembl_grch38 = cached_release(81)
33
+ CFTR_TRANSCRIPT_ID = "ENST00000003084"
34
+
35
+
36
+ def test_realized_annotator_is_registered_but_not_default():
37
+ annotator = get_annotator("realized")
38
+
39
+ assert annotator.name == "realized"
40
+ assert get_annotator("fast").name == "fast"
41
+
42
+
43
+ def test_weak_exonic_variant_keeps_normal_coding_effect_first():
44
+ transcript = Transcript()
45
+ variant = point(9, "A", "G")
46
+
47
+ result = predict_realized_effect(
48
+ (variant,), transcript, sequence_provider=provider())
49
+
50
+ assert type(result) is Substitution
51
+ assert result.aa_ref == "Q"
52
+ assert result.aa_alt == "R"
53
+ assert all(rank == 0 for rank in result.mechanism_rank)
54
+ assert result.probability is None
55
+
56
+
57
+ def test_strong_donor_variant_returns_ordinary_deletion_as_top_effect():
58
+ transcript = Transcript()
59
+ variant = point(11, "G", "A")
60
+
61
+ result = predict_realized_effect(
62
+ (variant,), transcript, sequence_provider=provider())
63
+
64
+ assert type(result) is Deletion
65
+ assert result.aa_ref == "Q"
66
+ assert [
67
+ type(candidate.effect).__name__ for candidate in result.candidates
68
+ ] == ["Deletion", "FrameShift", "Unresolved", "Intronic"]
69
+ unresolved = [
70
+ candidate.effect for candidate in result.candidates
71
+ if isinstance(candidate.effect, Unresolved)]
72
+ assert [effect.mechanism for effect in unresolved] == ["cryptic"]
73
+
74
+
75
+ def test_same_mutant_with_different_phase_baselines_does_not_merge():
76
+ transcript = Transcript()
77
+ somatic = point(11, "G", "A")
78
+ germline = point(9, "A", "G")
79
+
80
+ result = predict_realized_effect(
81
+ (somatic,), transcript, germline_variants=(germline,),
82
+ sequence_provider=provider())
83
+
84
+ deletion_candidates = [
85
+ candidate for candidate in result.candidates
86
+ if type(candidate.effect) is Deletion]
87
+ assert len(deletion_candidates) == 2
88
+ assert {
89
+ candidate.outcomes[0].baseline.protein_sequence
90
+ for candidate in deletion_candidates
91
+ } == {"MQA", "MRA"}
92
+ assert {
93
+ candidate.outcomes[0].mutant.protein_sequence
94
+ for candidate in deletion_candidates
95
+ } == {"MA"}
96
+
97
+
98
+ def test_sv_deletion_does_not_merge_away_patient_specific_baseline():
99
+ transcript = Transcript()
100
+ somatic = deletion(8, 10)
101
+ germline = point(9, "A", "G")
102
+
103
+ result = predict_realized_effect(
104
+ (somatic,), transcript, germline_variants=(germline,),
105
+ sequence_provider=provider())
106
+
107
+ assert type(result) is Deletion
108
+ deletion_candidates = [
109
+ candidate for candidate in result.candidates
110
+ if type(candidate.effect) is Deletion]
111
+ assert len(deletion_candidates) == 2
112
+ assert {
113
+ candidate.outcomes[0].baseline.protein_sequence
114
+ for candidate in deletion_candidates
115
+ } == {"MQA", "MRA"}
116
+
117
+
118
+ def test_multiple_somatic_variants_are_realized_on_one_product():
119
+ transcript = Transcript()
120
+ variants = (point(8, "C", "A"), point(9, "A", "G"))
121
+
122
+ result = predict_realized_effect(
123
+ variants, transcript, sequence_provider=provider())
124
+
125
+ assert type(result) is Substitution
126
+ assert result.aa_ref == "Q"
127
+ assert result.aa_alt == "R"
128
+ assert result.variants == variants
129
+
130
+
131
+ def test_combined_phase_and_splice_hypotheses_obey_global_cap():
132
+ transcript = Transcript()
133
+ somatic = point(11, "G", "A")
134
+ germline = point(9, "A", "G")
135
+
136
+ with pytest.raises(ValueError, match="Combined phase/splice"):
137
+ predict_realized_effect(
138
+ (somatic,), transcript, germline_variants=(germline,),
139
+ sequence_provider=provider(), max_hypotheses=7)
140
+
141
+
142
+ def test_real_cftr_missense_matches_existing_varcode_result():
143
+ transcript = ensembl_grch38.transcript_by_id(CFTR_TRANSCRIPT_ID)
144
+ variant = Variant("7", 117531100, "T", "A", ensembl_grch38)
145
+
146
+ result = predict_realized_effect((variant,), transcript)
147
+
148
+ assert type(result) is Substitution
149
+ assert result.short_description == "p.L159M"
150
+ assert len(result.candidates) == 1
151
+
152
+
153
+ def test_effects_dispatches_realized_annotator_with_germline_context():
154
+ somatic = Variant("7", 117531100, "T", "A", ensembl_grch38)
155
+ germline = Variant("7", 117531101, "T", "C", ensembl_grch38)
156
+ context = GermlineContext.from_variants(
157
+ [germline], reference_name="GRCh38")
158
+
159
+ effects = somatic.effects(annotator="realized", germline=context)
160
+ result = next(
161
+ effect for effect in effects
162
+ if effect.transcript.id == CFTR_TRANSCRIPT_ID)
163
+
164
+ assert type(result) is Substitution
165
+ assert len(result.candidates) == 2
166
+ assert {
167
+ candidate.outcomes[0].baseline.protein_sequence[158]
168
+ for candidate in result.candidates
169
+ } == {"L", "S"}
170
+
171
+
172
+ def test_realized_dispatch_preserves_sparse_germline_uncertainty_flag():
173
+ somatic = Variant("7", 117531100, "T", "A", ensembl_grch38)
174
+ distant = Variant("7", 117587799, "C", "T", ensembl_grch38)
175
+ context = GermlineContext.from_variants(
176
+ [distant], reference_name="GRCh38",
177
+ completeness=Completeness.SPARSE)
178
+
179
+ effects = somatic.effects(annotator="realized", germline=context)
180
+ result = next(
181
+ effect for effect in effects
182
+ if effect.transcript.id == CFTR_TRANSCRIPT_ID)
183
+
184
+ assert result.germline_unknown is True
185
+
186
+
187
+ def test_real_cftr_donor_plus_one_enumerates_splice_mechanisms():
188
+ transcript = ensembl_grch38.transcript_by_id(CFTR_TRANSCRIPT_ID)
189
+ variant = Variant("7", 117531115, "G", "A", ensembl_grch38)
190
+
191
+ result = predict_realized_effect((variant,), transcript)
192
+
193
+ assert type(result) is Deletion
194
+ assert [
195
+ candidate.effect.mechanism
196
+ for candidate in result.candidates
197
+ if isinstance(candidate.effect, Unresolved)
198
+ ] == ["intron_retention", "cryptic"]
199
+ assert result.probability is None
200
+
201
+
202
+ @pytest.mark.parametrize(
203
+ "sv_type,expected_class",
204
+ [("DEL", Deletion), ("DUP", Insertion)])
205
+ def test_real_cftr_whole_exon_sv_is_classified_from_realized_product(
206
+ sv_type, expected_class):
207
+ transcript = ensembl_grch38.transcript_by_id(CFTR_TRANSCRIPT_ID)
208
+ exon = transcript.exons[4]
209
+ variant = StructuralVariant(
210
+ contig="7",
211
+ start=exon.start - 300,
212
+ end=exon.end + 300,
213
+ sv_type=sv_type,
214
+ genome=ensembl_grch38)
215
+
216
+ result = predict_realized_effect((variant,), transcript)
217
+
218
+ assert type(result) is expected_class
219
+ assert len(result.candidates) == 1
220
+
221
+
222
+ @pytest.mark.parametrize(
223
+ "reverse,position,ref,alt",
224
+ [(False, 23, "G", "A"), (True, 178, "C", "T")])
225
+ def test_cryptic_donor_is_realized_on_mutated_haplotype_both_strands(
226
+ reverse, position, ref, alt):
227
+ transcript = LongIntronTranscript(reverse=reverse)
228
+ variant = point(position, ref, alt)
229
+
230
+ result = predict_realized_effect(
231
+ (variant,), transcript, sequence_provider=long_provider(reverse))
232
+
233
+ cryptic = next(
234
+ candidate for candidate in result.candidates
235
+ if candidate.ordinal_key == (2,))
236
+ assert not isinstance(cryptic.effect, Unresolved)
237
+ evidence = cryptic.outcomes[0].mutant.evidence
238
+ resolved = evidence["resolved_splice_choices"]
239
+ assert len(resolved) == 1
240
+ assert resolved[0][1] == "cryptic"
241
+ assert resolved[0][3] > 0
242
+
243
+
244
+ def test_realized_annotator_routes_bnd_through_verified_fusion_builder():
245
+ genome = cached_release(95)
246
+ otx1 = genome.transcript_by_id("ENST00000282549")
247
+ variant = StructuralVariant(
248
+ contig="2",
249
+ start=63_053_516,
250
+ sv_type="BND",
251
+ alt="N]2:25955847]",
252
+ mate_contig="2",
253
+ mate_start=25_955_847,
254
+ mate_orientation="]]",
255
+ genome=genome)
256
+
257
+ result = predict_realized_effect((variant,), otx1)
258
+
259
+ assert isinstance(result, GeneFusion)
260
+ assert result.partner_transcript.id == "ENST00000264712"
261
+ assert result.mutant_protein_sequence == (
262
+ "MMSYLKQPPYGMNGLGLAGPAMDLLHPSVGYPETS")
@@ -0,0 +1,257 @@
1
+ """Tests for splice-site detection on realized forward/reverse layouts."""
2
+
3
+ from types import SimpleNamespace
4
+
5
+ import pytest
6
+
7
+ from varcode.genomic_layout import GenomicLayout
8
+ from varcode.nucleotides import reverse_complement
9
+ from varcode.splice_graph import (
10
+ disrupted_splice_sites,
11
+ enumerate_layout_splice_plans,
12
+ )
13
+ from varcode.transcript_layout import build_exon_runs, realize_exon_path
14
+
15
+
16
+ FORWARD = "ATGGTAGCAAGTAGGCTGTAGTAG"
17
+
18
+
19
+ def _long_forward_sequence():
20
+ bases = list("A" * 200)
21
+ for start, sequence in (
22
+ (1, "ATG"), (4, "GT"),
23
+ (18, "AG"), (20, "CAA"), (23, "GT"),
24
+ (29, "CAGGTAAGT"),
25
+ (118, "AG"), (120, "GCT"), (123, "GT"),
26
+ (138, "AG"), (140, "TAG")):
27
+ bases[start - 1:start - 1 + len(sequence)] = sequence
28
+ return "".join(bases)
29
+
30
+
31
+ LONG_FORWARD = _long_forward_sequence()
32
+
33
+
34
+ class Exon:
35
+ def __init__(self, start, end):
36
+ self.start = start
37
+ self.end = end
38
+
39
+
40
+ class Transcript:
41
+ contig = "x"
42
+ start = 1
43
+ end = 24
44
+ is_protein_coding = True
45
+ complete = True
46
+ protein_sequence = "MQA"
47
+ stop_codon_spliced_offsets = (9, 10, 11)
48
+ id = "synthetic-splice"
49
+ name = "SYN-001"
50
+
51
+ def __init__(self, reverse=False):
52
+ self.gene = SimpleNamespace(id="synthetic-gene", name="SYN")
53
+ self.on_backward_strand = reverse
54
+ intervals = ((1, 3), (8, 10), (15, 17), (22, 24))
55
+ if reverse:
56
+ intervals = tuple(
57
+ (25 - end, 25 - start)
58
+ for start, end in reversed(intervals))
59
+ self.start_codon_positions = (22, 23, 24)
60
+ else:
61
+ self.start_codon_positions = (1, 2, 3)
62
+ self.start_codon_spliced_offsets = (0, 1, 2)
63
+ self.exons = tuple(Exon(*interval) for interval in intervals)
64
+
65
+ def spliced_offset(self, position):
66
+ offset = 0
67
+ exons = sorted(
68
+ self.exons, key=lambda exon: exon.start,
69
+ reverse=self.on_backward_strand)
70
+ for exon in exons:
71
+ if exon.start <= position <= exon.end:
72
+ if self.on_backward_strand:
73
+ return offset + exon.end - position
74
+ return offset + position - exon.start
75
+ offset += exon.end - exon.start + 1
76
+ raise ValueError(position)
77
+
78
+
79
+ class LongIntronTranscript(Transcript):
80
+ end = 142
81
+ id = "synthetic-long-intron"
82
+
83
+ def __init__(self, reverse=False):
84
+ self.gene = SimpleNamespace(id="synthetic-gene", name="SYN")
85
+ self.on_backward_strand = reverse
86
+ intervals = ((1, 3), (20, 22), (120, 122), (140, 142))
87
+ if reverse:
88
+ intervals = tuple(
89
+ (201 - end, 201 - start)
90
+ for start, end in reversed(intervals))
91
+ self.start_codon_positions = (198, 199, 200)
92
+ else:
93
+ self.start_codon_positions = (1, 2, 3)
94
+ self.start = min(start for start, _ in intervals)
95
+ self.end = max(end for _, end in intervals)
96
+ self.start_codon_spliced_offsets = (0, 1, 2)
97
+ self.exons = tuple(Exon(*interval) for interval in intervals)
98
+
99
+
100
+ def long_provider(reverse=False):
101
+ chromosome = reverse_complement(LONG_FORWARD) if reverse else LONG_FORWARD
102
+
103
+ def get_sequence(contig, start, end):
104
+ assert contig == "x"
105
+ sequence = chromosome[start - 1:end]
106
+ return sequence + "A" * (end - start + 1 - len(sequence))
107
+
108
+ return get_sequence
109
+
110
+
111
+ def provider(reverse=False):
112
+ chromosome = reverse_complement(FORWARD) if reverse else FORWARD
113
+
114
+ def get_sequence(contig, start, end):
115
+ assert contig == "x"
116
+ return chromosome[start - 1:end]
117
+
118
+ return get_sequence
119
+
120
+
121
+ def point(start, ref, alt):
122
+ return SimpleNamespace(
123
+ contig="x", trimmed_base1_start=start,
124
+ trimmed_base1_end=start + len(ref) - 1,
125
+ trimmed_ref=ref, trimmed_alt=alt, is_structural=False)
126
+
127
+
128
+ def deletion(start, end):
129
+ return SimpleNamespace(
130
+ contig="x", start=start, end=end,
131
+ affected_start=start, affected_end=end,
132
+ sv_type="DEL", alt_assembly=None, is_structural=True)
133
+
134
+
135
+ def statuses_for(variant=None, reverse=False, with_sequence=True):
136
+ transcript = Transcript(reverse)
137
+ layout = GenomicLayout.from_transcript(
138
+ transcript,
139
+ flank=0,
140
+ sequence_provider=provider(reverse) if with_sequence else None)
141
+ if variant is not None:
142
+ layout = layout.apply_point_variant(
143
+ variant, validate_reference=with_sequence)
144
+ runs = build_exon_runs(transcript, layout)
145
+ return disrupted_splice_sites(transcript, layout, runs)
146
+
147
+
148
+ @pytest.mark.parametrize("reverse", [False, True])
149
+ def test_reference_splice_sites_are_intact(reverse):
150
+ assert statuses_for(reverse=reverse) == ()
151
+
152
+
153
+ def test_donor_canonical_and_weak_windows_are_distinct():
154
+ strong = statuses_for(point(11, "G", "A"))
155
+ weak_intronic = statuses_for(point(14, "G", "A"))
156
+ weak_exonic = statuses_for(point(10, "A", "C"))
157
+
158
+ assert [(s.key.side, s.key.exon_number, s.strength) for s in strong] == [
159
+ ("donor", 2, "lost")]
160
+ donor = next(
161
+ status for status in weak_intronic
162
+ if status.key.side == "donor" and status.key.exon_number == 2)
163
+ assert donor.strength == "weak_intronic"
164
+ assert weak_exonic[0].strength == "weak_exonic"
165
+
166
+
167
+ def test_reverse_strand_donor_uses_transcript_direction():
168
+ # Forward position 11 maps to reverse-complement position 14.
169
+ statuses = statuses_for(point(14, "C", "T"), reverse=True)
170
+
171
+ assert [(s.key.side, s.key.exon_number, s.strength) for s in statuses] == [
172
+ ("donor", 2, "lost")]
173
+
174
+
175
+ def test_tier_zero_detects_explicit_canonical_site_edit_without_fasta():
176
+ statuses = statuses_for(
177
+ point(11, "G", "A"), with_sequence=False)
178
+
179
+ assert statuses[0].strength == "lost"
180
+
181
+
182
+ def test_exon_truncation_is_a_lost_splice_site():
183
+ transcript = Transcript()
184
+ layout = GenomicLayout.from_transcript(
185
+ transcript, flank=0, sequence_provider=provider())
186
+ layout = layout.apply_structural_variant(deletion(10, 14))
187
+
188
+ statuses = disrupted_splice_sites(
189
+ transcript, layout, build_exon_runs(transcript, layout))
190
+
191
+ assert any(
192
+ status.key.side == "donor"
193
+ and status.key.exon_number == 2
194
+ and status.strength == "lost"
195
+ for status in statuses)
196
+
197
+
198
+ def test_strong_site_options_are_ordinal_not_probabilities():
199
+ transcript = Transcript()
200
+ layout = GenomicLayout.from_transcript(
201
+ transcript, flank=0, sequence_provider=provider())
202
+ layout = layout.apply_point_variant(point(11, "G", "A"))
203
+
204
+ plans = enumerate_layout_splice_plans(transcript, layout)
205
+ mechanisms = [
206
+ tuple(option.mechanism for _, option in plan.choices)
207
+ for plan in plans]
208
+
209
+ assert mechanisms == [
210
+ ("exon_skip",),
211
+ ("intron_retention",),
212
+ ("cryptic",),
213
+ ("normal",),
214
+ ]
215
+ assert all(plan.probability is None for plan in plans)
216
+ skipped = realize_exon_path(
217
+ transcript, layout, kept_run_keys=plans[0].kept_runs)
218
+ assert skipped.cdna_sequence == "ATGGCTTAG"
219
+
220
+
221
+ def test_two_sites_on_skipped_exon_do_not_form_impossible_cartesian_pair():
222
+ transcript = Transcript()
223
+ layout = GenomicLayout.from_transcript(
224
+ transcript, flank=0, sequence_provider=provider())
225
+ # Destroy exon 2's acceptor and donor.
226
+ layout = layout.apply_variants((
227
+ point(6, "A", "C"),
228
+ point(11, "G", "A"),
229
+ ))
230
+
231
+ plans = enumerate_layout_splice_plans(transcript, layout)
232
+
233
+ assert plans
234
+ assert all(not (
235
+ len(plan.choices) == 2
236
+ and all(option.mechanism == "exon_skip"
237
+ for _, option in plan.choices))
238
+ for plan in plans)
239
+
240
+
241
+ def test_retained_intron_cannot_coexist_with_skipped_downstream_exon():
242
+ transcript = Transcript()
243
+ layout = GenomicLayout.from_transcript(
244
+ transcript, flank=0, sequence_provider=provider())
245
+ # Destroy the donors of exon 2 and exon 3. Retaining intron 2 requires
246
+ # exon 3, so it cannot coexist with exon 3 skipping.
247
+ layout = layout.apply_variants((
248
+ point(11, "G", "A"),
249
+ point(18, "G", "A"),
250
+ ))
251
+
252
+ plans = enumerate_layout_splice_plans(transcript, layout)
253
+
254
+ assert all(not (
255
+ dict((key.exon_number, option.mechanism) for key, option in plan.choices)
256
+ == {2: "intron_retention", 3: "exon_skip"})
257
+ for plan in plans)