varcode 7.1.2__tar.gz → 7.2.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.
- {varcode-7.1.2/varcode.egg-info → varcode-7.2.0}/PKG-INFO +1 -1
- varcode-7.2.0/tests/test_effect_hypotheses.py +183 -0
- varcode-7.2.0/tests/test_genomic_layout.py +160 -0
- varcode-7.2.0/varcode/effect_hypotheses.py +319 -0
- varcode-7.2.0/varcode/genomic_layout.py +554 -0
- varcode-7.2.0/varcode/version.py +1 -0
- {varcode-7.1.2 → varcode-7.2.0/varcode.egg-info}/PKG-INFO +1 -1
- {varcode-7.1.2 → varcode-7.2.0}/varcode.egg-info/SOURCES.txt +4 -0
- varcode-7.1.2/varcode/version.py +0 -1
- {varcode-7.1.2 → varcode-7.2.0}/LICENSE +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/MANIFEST.in +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/README.md +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/pyproject.toml +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/requirements.txt +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/setup.cfg +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_annotator_divergence_scenarios.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_annotator_parity_adversarial.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_annotators.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_cli_effects.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_cli_genes.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_collection_filtering.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_collection_variants_attr_consistency.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_common.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_cosmic_mutations.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_cryptic_exons.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_csv_roundtrip.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_dbnsfp_validation.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_effect_annotation_errors.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_effect_candidates.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_effect_classes.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_effect_collection.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_effect_collection_serialization.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_effect_collection_sort_order.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_effects_from_mutagenix_variants.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_exonic_splice_site.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_fast_path.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_frameshift_cterminus_regression.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_frameshift_helpers.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_genome_sequence.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_genotype.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_genotype_from_vcf.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_germline_context.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_germline_effects.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_haplotype_effect.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_maf.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_mitochondrial.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_mm10_klf6_frameshift.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_mouse.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_mutant_transcript.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_mutate.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_no_duplicate_variants.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_osteosarc_fusions.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_premature_stop_short_description.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_problematic_variants.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_protein_diff_parity.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_read_phase_resolver.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_real_world_callers.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_reference.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_reference_mismatch_error.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_reference_segments.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_rna_evidence.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_rna_read_phasing_source.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_selenocysteine.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_silent_aa_pos.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_silent_hgvs_description.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_splice_outcomes.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_splice_site_effects.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_stop_codon_classification_bugs.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_string_helpers.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_structural_variant_annotator.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_structural_variants.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_symbolic_alleles.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_timings.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_transforms_left_align.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_transforms_pair_breakends.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_variant.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_variant_collection.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_vcf.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_vcf_output.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_vcf_parsing.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_vcf_phase_resolver.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/tests/test_vcf_spec_examples.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/__init__.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/annotators/__init__.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/annotators/fast.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/annotators/protein_diff.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/annotators/registry.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/annotators/structural_variant.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/cli/__init__.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/cli/effects_script.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/cli/genes_script.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/cli/logging.conf +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/cli/variant_args.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/cli/version_info.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/common.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/cryptic_exons.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/csv_helpers.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effect_candidates.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/__init__.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/classify.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/codon_tables.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/common.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/effect_classes.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/effect_collection.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/effect_helpers.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/effect_ordering.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/effect_prediction.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/effect_prediction_coding.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/effect_prediction_coding_frameshift.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/effect_prediction_coding_in_frame.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/fast_path.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/mutate.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/transcript_helpers.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/effects/translate.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/errors.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/genome.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/genome_sequence.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/genotype.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/germline.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/maf.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/mutant_transcript.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/nucleotides.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/phasing.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/reference.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/rna_evidence.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/rna_read_phasing.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/splice_outcomes.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/string_helpers.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/structural_variant.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/sv_allele_parser.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/transforms/__init__.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/ucsc_reference_names.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/util.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/variant.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/variant_collection.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/vcf.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/vcf_output.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode/vcf_parsing.py +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode.egg-info/dependency_links.txt +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode.egg-info/entry_points.txt +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode.egg-info/requires.txt +0 -0
- {varcode-7.1.2 → varcode-7.2.0}/varcode.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Tests for graph-valid hypotheses and baseline-aware merging."""
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from varcode.effect_hypotheses import (
|
|
8
|
+
ClassifiedOutcome,
|
|
9
|
+
EffectHypothesis,
|
|
10
|
+
RealizedTranscriptProduct,
|
|
11
|
+
SpliceAxis,
|
|
12
|
+
SpliceOption,
|
|
13
|
+
SpliceSiteKey,
|
|
14
|
+
enumerate_splice_plans,
|
|
15
|
+
merge_classified_outcomes,
|
|
16
|
+
order_realized_candidates,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def effect(name="Deletion", priority=5):
|
|
21
|
+
cls = type(name, (), {})
|
|
22
|
+
value = cls()
|
|
23
|
+
value.aa_mutation_start_offset = 3
|
|
24
|
+
value.aa_ref = "D"
|
|
25
|
+
value.aa_alt = ""
|
|
26
|
+
value.priority = priority
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def product(protein, cdna="AAACCC", junctions=()):
|
|
31
|
+
return RealizedTranscriptProduct(
|
|
32
|
+
transcript=None,
|
|
33
|
+
cdna_sequence=cdna,
|
|
34
|
+
protein_sequence=protein,
|
|
35
|
+
junction_signature=junctions)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_splice_enumeration_omits_site_removed_by_another_choice():
|
|
39
|
+
exon4 = (4, 1)
|
|
40
|
+
acceptor = SpliceAxis(
|
|
41
|
+
key=SpliceSiteKey("acceptor", 4),
|
|
42
|
+
anchor_runs=(exon4,),
|
|
43
|
+
options=(
|
|
44
|
+
SpliceOption("normal", 0, required_runs=(exon4,)),
|
|
45
|
+
SpliceOption("exon_skip", 1, skipped_runs=(exon4,)),
|
|
46
|
+
))
|
|
47
|
+
donor = SpliceAxis(
|
|
48
|
+
key=SpliceSiteKey("donor", 4),
|
|
49
|
+
anchor_runs=(exon4,),
|
|
50
|
+
options=(
|
|
51
|
+
SpliceOption("normal", 0, required_runs=(exon4,)),
|
|
52
|
+
SpliceOption("exon_skip", 1, skipped_runs=(exon4,)),
|
|
53
|
+
))
|
|
54
|
+
|
|
55
|
+
plans = enumerate_splice_plans((acceptor, donor), (exon4,))
|
|
56
|
+
mechanisms = [
|
|
57
|
+
tuple(option.mechanism for _, option in plan.choices)
|
|
58
|
+
for plan in plans]
|
|
59
|
+
|
|
60
|
+
assert mechanisms == [("normal", "normal"), ("exon_skip",),
|
|
61
|
+
("exon_skip",)]
|
|
62
|
+
assert all(len(plan.choices) != 2 or plan.kept_runs for plan in plans)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_splice_rule_rank_is_not_reported_as_probability():
|
|
66
|
+
axis = SpliceAxis(
|
|
67
|
+
key=SpliceSiteKey("donor", 4),
|
|
68
|
+
anchor_runs=(4,),
|
|
69
|
+
options=(SpliceOption("exon_skip", 0, skipped_runs=(4,)),))
|
|
70
|
+
|
|
71
|
+
(plan,) = enumerate_splice_plans((axis,), (4,))
|
|
72
|
+
|
|
73
|
+
assert plan.ordinal_key == (0,)
|
|
74
|
+
assert plan.probability is None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_calibrated_splice_probabilities_are_multiplied():
|
|
78
|
+
axes = tuple(
|
|
79
|
+
SpliceAxis(
|
|
80
|
+
key=SpliceSiteKey("donor", exon),
|
|
81
|
+
anchor_runs=(exon,),
|
|
82
|
+
options=(SpliceOption(
|
|
83
|
+
"normal", 0, required_runs=(exon,), probability=p),))
|
|
84
|
+
for exon, p in ((4, 0.8), (5, 0.5)))
|
|
85
|
+
|
|
86
|
+
(plan,) = enumerate_splice_plans(axes, (4, 5))
|
|
87
|
+
|
|
88
|
+
assert plan.probability == pytest.approx(0.4)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_same_mutant_different_patient_baseline_does_not_merge():
|
|
92
|
+
mutant = product("ABCDE")
|
|
93
|
+
outcome_a = ClassifiedOutcome(
|
|
94
|
+
hypothesis=EffectHypothesis(phase=(("g", "trans"),)),
|
|
95
|
+
effect=effect(),
|
|
96
|
+
baseline=product("ABCDEFG"),
|
|
97
|
+
mutant=mutant,
|
|
98
|
+
change_aa_offset=3)
|
|
99
|
+
outcome_b = ClassifiedOutcome(
|
|
100
|
+
hypothesis=EffectHypothesis(phase=(("g", "cis"),)),
|
|
101
|
+
effect=effect(),
|
|
102
|
+
baseline=product("ABCXEFG"),
|
|
103
|
+
mutant=mutant,
|
|
104
|
+
change_aa_offset=3)
|
|
105
|
+
|
|
106
|
+
candidates = merge_classified_outcomes((outcome_a, outcome_b))
|
|
107
|
+
|
|
108
|
+
assert len(candidates) == 2
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_identical_comparisons_merge_and_preserve_hypotheses():
|
|
112
|
+
baseline = product("ABCDEFG")
|
|
113
|
+
mutant = product("ABCDE")
|
|
114
|
+
outcomes = tuple(
|
|
115
|
+
ClassifiedOutcome(
|
|
116
|
+
hypothesis=EffectHypothesis(
|
|
117
|
+
phase=(("g", phase),), phase_probability=0.5,
|
|
118
|
+
enumeration_index=index),
|
|
119
|
+
effect=effect(),
|
|
120
|
+
baseline=baseline,
|
|
121
|
+
mutant=mutant,
|
|
122
|
+
change_aa_offset=3)
|
|
123
|
+
for index, phase in enumerate(("trans", "cis")))
|
|
124
|
+
|
|
125
|
+
(candidate,) = merge_classified_outcomes(outcomes)
|
|
126
|
+
|
|
127
|
+
assert len(candidate.hypotheses) == 2
|
|
128
|
+
assert candidate.phase_mass == 1.0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_candidate_probability_combines_calibrated_splice_and_phase():
|
|
132
|
+
baseline = product("ABCDEFG")
|
|
133
|
+
mutant = product("ABCDE")
|
|
134
|
+
plan = SimpleNamespace(ordinal_key=(0,), probability=0.4)
|
|
135
|
+
outcomes = tuple(
|
|
136
|
+
ClassifiedOutcome(
|
|
137
|
+
EffectHypothesis(
|
|
138
|
+
phase=(("g", phase),), phase_probability=0.5,
|
|
139
|
+
splice_plan=plan),
|
|
140
|
+
effect(), baseline, mutant, 3)
|
|
141
|
+
for phase in ("trans", "cis"))
|
|
142
|
+
|
|
143
|
+
(candidate,) = merge_classified_outcomes(outcomes)
|
|
144
|
+
|
|
145
|
+
assert candidate.probability == pytest.approx(0.4)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_junction_signature_is_part_of_product_identity():
|
|
149
|
+
baseline = product("ABCDEFG")
|
|
150
|
+
outcomes = (
|
|
151
|
+
ClassifiedOutcome(
|
|
152
|
+
EffectHypothesis(), effect(), baseline,
|
|
153
|
+
product("ABCDE", junctions=((4, 6),)), 3),
|
|
154
|
+
ClassifiedOutcome(
|
|
155
|
+
EffectHypothesis(), effect(), baseline,
|
|
156
|
+
product("ABCDE", junctions=((4, 7),)), 3),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
assert len(merge_classified_outcomes(outcomes)) == 2
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_ordering_uses_ordinal_rank_when_scores_are_uncalibrated():
|
|
163
|
+
baseline = product("ABCDEFG")
|
|
164
|
+
mutant = product("ABCDE")
|
|
165
|
+
low_priority = effect("Deletion", priority=5)
|
|
166
|
+
high_priority = effect("FrameShift", priority=10)
|
|
167
|
+
plans = [
|
|
168
|
+
SimpleNamespace(ordinal_key=(0,), probability=None),
|
|
169
|
+
SimpleNamespace(ordinal_key=(1,), probability=None),
|
|
170
|
+
]
|
|
171
|
+
outcomes = tuple(
|
|
172
|
+
ClassifiedOutcome(
|
|
173
|
+
EffectHypothesis(splice_plan=plan, enumeration_index=index),
|
|
174
|
+
current_effect, baseline, mutant, 3)
|
|
175
|
+
for index, (plan, current_effect) in enumerate(
|
|
176
|
+
zip(plans, (low_priority, high_priority))))
|
|
177
|
+
candidates = merge_classified_outcomes(outcomes)
|
|
178
|
+
|
|
179
|
+
ordered = order_realized_candidates(
|
|
180
|
+
candidates, lambda value: value.priority)
|
|
181
|
+
|
|
182
|
+
assert ordered[0].effect is low_priority
|
|
183
|
+
assert ordered[0].probability is None
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Tests for the coordinate-aware realized-genome layout."""
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from varcode.genomic_layout import (
|
|
8
|
+
GenomicLayout,
|
|
9
|
+
LayoutSegment,
|
|
10
|
+
SequenceUnavailable,
|
|
11
|
+
join_breakends,
|
|
12
|
+
project_intervals,
|
|
13
|
+
)
|
|
14
|
+
from varcode.structural_variant import Breakend
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SEQUENCES = {
|
|
18
|
+
"a": "AACCGGTTAACC",
|
|
19
|
+
"b": "TTGGCCAATTGG",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def provider(contig, start, end):
|
|
24
|
+
return SEQUENCES[contig][start - 1:end]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def point(contig, start, ref, alt):
|
|
28
|
+
return SimpleNamespace(
|
|
29
|
+
contig=contig,
|
|
30
|
+
trimmed_base1_start=start,
|
|
31
|
+
trimmed_base1_end=start + len(ref) - 1 if ref else start,
|
|
32
|
+
trimmed_ref=ref,
|
|
33
|
+
trimmed_alt=alt,
|
|
34
|
+
is_structural=False)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def structural(kind, start, end, affected_start=None, affected_end=None,
|
|
38
|
+
alt_assembly=None):
|
|
39
|
+
return SimpleNamespace(
|
|
40
|
+
contig="a",
|
|
41
|
+
start=start,
|
|
42
|
+
end=end,
|
|
43
|
+
affected_start=(start if affected_start is None else affected_start),
|
|
44
|
+
affected_end=(end if affected_end is None else affected_end),
|
|
45
|
+
sv_type=kind,
|
|
46
|
+
alt_assembly=alt_assembly,
|
|
47
|
+
is_structural=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_layout_is_lazy_until_materialized():
|
|
51
|
+
layout = GenomicLayout.from_interval("a", 1, 12)
|
|
52
|
+
assert layout.length == 12
|
|
53
|
+
with pytest.raises(SequenceUnavailable):
|
|
54
|
+
layout.materialize()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_point_variants_preserve_origins_on_both_strands():
|
|
58
|
+
forward = GenomicLayout.from_interval(
|
|
59
|
+
"a", 1, 12, sequence_provider=provider)
|
|
60
|
+
reverse = GenomicLayout.from_interval(
|
|
61
|
+
"a", 1, 12, strand="-", sequence_provider=provider)
|
|
62
|
+
variant = point("a", 3, "C", "T")
|
|
63
|
+
|
|
64
|
+
forward_mutant = forward.apply_point_variant(variant)
|
|
65
|
+
reverse_mutant = reverse.apply_point_variant(variant)
|
|
66
|
+
|
|
67
|
+
assert forward_mutant.materialize() == "AATCGGTTAACC"
|
|
68
|
+
assert reverse_mutant.materialize() == "GGTTAACCGATT"
|
|
69
|
+
assert forward_mutant.origins()[2] == ("a", 3, "alternate")
|
|
70
|
+
assert reverse_mutant.origins()[-3] == ("a", 3, "alternate")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_indels_have_explicit_inserted_and_deleted_origins():
|
|
74
|
+
layout = GenomicLayout.from_interval(
|
|
75
|
+
"a", 1, 12, sequence_provider=provider)
|
|
76
|
+
inserted = layout.apply_point_variant(point("a", 4, "", "TA"))
|
|
77
|
+
deleted = layout.apply_point_variant(point("a", 3, "CC", ""))
|
|
78
|
+
|
|
79
|
+
assert inserted.materialize() == "AACCTA GGTTAACC".replace(" ", "")
|
|
80
|
+
assert [origin[1] for origin in inserted.origins()][4:6] == [None, None]
|
|
81
|
+
assert deleted.materialize() == "AAGGTTAACC"
|
|
82
|
+
assert 3 not in [origin[1] for origin in deleted.origins()]
|
|
83
|
+
assert 4 not in [origin[1] for origin in deleted.origins()]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_typed_deletion_does_not_remove_retained_padding_anchor():
|
|
87
|
+
layout = GenomicLayout.from_interval(
|
|
88
|
+
"a", 1, 12, sequence_provider=provider)
|
|
89
|
+
deletion = structural(
|
|
90
|
+
"DEL", start=2, end=6, affected_start=3, affected_end=6)
|
|
91
|
+
|
|
92
|
+
mutant = layout.apply_structural_variant(deletion)
|
|
93
|
+
|
|
94
|
+
assert mutant.materialize() == "AATTAACC"
|
|
95
|
+
assert [origin[1] for origin in mutant.origins()][:3] == [1, 2, 7]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_duplication_and_inversion_work_in_transcript_order():
|
|
99
|
+
forward = GenomicLayout.from_interval(
|
|
100
|
+
"a", 1, 12, sequence_provider=provider)
|
|
101
|
+
reverse = GenomicLayout.from_interval(
|
|
102
|
+
"a", 1, 12, strand="-", sequence_provider=provider)
|
|
103
|
+
|
|
104
|
+
forward_dup = forward.apply_structural_variant(structural("DUP", 3, 6))
|
|
105
|
+
reverse_dup = reverse.apply_structural_variant(structural("DUP", 3, 6))
|
|
106
|
+
inverted = forward.apply_structural_variant(structural("INV", 2, 5))
|
|
107
|
+
|
|
108
|
+
assert forward_dup.materialize() == "AACCGGCCGGTTAACC"
|
|
109
|
+
assert reverse_dup.materialize() == "GGTTAACCGGCCGGTT"
|
|
110
|
+
assert inverted.materialize() == "ACGGTGTTAACC"
|
|
111
|
+
assert {kind for _, _, kind in inverted.origins()[1:5]} == {"inverted"}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_apply_variants_deletes_an_earlier_point_edit_on_same_haplotype():
|
|
115
|
+
layout = GenomicLayout.from_interval(
|
|
116
|
+
"a", 1, 12, sequence_provider=provider)
|
|
117
|
+
variants = [
|
|
118
|
+
structural("DEL", 2, 6, affected_start=3, affected_end=6),
|
|
119
|
+
point("a", 4, "C", "T"),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
mutant = layout.apply_variants(variants)
|
|
123
|
+
|
|
124
|
+
assert mutant.materialize() == "AATTAACC"
|
|
125
|
+
assert all(kind != "alternate" for _, _, kind in mutant.origins())
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_cross_contig_breakend_join_orients_retained_sides():
|
|
129
|
+
left = GenomicLayout.from_interval(
|
|
130
|
+
"a", 1, 12, sequence_provider=provider)
|
|
131
|
+
right = GenomicLayout.from_interval(
|
|
132
|
+
"b", 1, 12, strand="-", sequence_provider=provider)
|
|
133
|
+
|
|
134
|
+
fusion = join_breakends(
|
|
135
|
+
left,
|
|
136
|
+
Breakend("a", 3, "left"),
|
|
137
|
+
right,
|
|
138
|
+
Breakend("b", 8, "right"))
|
|
139
|
+
|
|
140
|
+
assert fusion.materialize() == "AACATTGG"
|
|
141
|
+
assert fusion.endpoint_origin(first=False) == ("b", 12)
|
|
142
|
+
assert fusion.origins()[2][:2] == ("a", 3)
|
|
143
|
+
assert fusion.origins()[3][:2] == ("b", 8)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_project_intervals_keeps_duplicate_exon_occurrences():
|
|
147
|
+
layout = GenomicLayout.from_interval(
|
|
148
|
+
"a", 1, 12, sequence_provider=provider)
|
|
149
|
+
duplicated = layout.apply_structural_variant(structural("DUP", 3, 6))
|
|
150
|
+
|
|
151
|
+
runs = project_intervals(duplicated, (("a", 3, 6),))
|
|
152
|
+
|
|
153
|
+
assert [run.materialize() for run in runs] == ["CCGG", "CCGG"]
|
|
154
|
+
assert runs[0].segments[0].origin_kind == "reference"
|
|
155
|
+
assert runs[1].segments[0].origin_kind == "duplicated"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_inserted_segment_requires_sequence():
|
|
159
|
+
with pytest.raises(ValueError):
|
|
160
|
+
LayoutSegment(None, None, None)
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
2
|
+
# you may not use this file except in compliance with the License.
|
|
3
|
+
# You may obtain a copy of the License at
|
|
4
|
+
#
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
#
|
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
10
|
+
# See the License for the specific language governing permissions and
|
|
11
|
+
# limitations under the License.
|
|
12
|
+
|
|
13
|
+
"""Composable hypothesis and realized-product data types.
|
|
14
|
+
|
|
15
|
+
These types deliberately keep three concepts separate:
|
|
16
|
+
|
|
17
|
+
* a hypothesis records uncertain biological choices;
|
|
18
|
+
* a transcript product records what sequence those choices produce;
|
|
19
|
+
* a classified outcome compares a mutant product with the patient's own
|
|
20
|
+
baseline product.
|
|
21
|
+
|
|
22
|
+
Keeping the baseline in the merge identity is load-bearing. Two phase
|
|
23
|
+
hypotheses can produce the same mutant protein while deleting different
|
|
24
|
+
patient-specific residues; those are different effects even though the
|
|
25
|
+
mutant molecule alone is identical.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import itertools
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from typing import Any, Mapping, Optional, Tuple
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, order=True)
|
|
34
|
+
class SpliceSiteKey:
|
|
35
|
+
"""Stable identity of a splice site in a rearranged layout."""
|
|
36
|
+
|
|
37
|
+
side: str
|
|
38
|
+
exon_number: int
|
|
39
|
+
occurrence: int = 1
|
|
40
|
+
|
|
41
|
+
def __post_init__(self):
|
|
42
|
+
if self.side not in ("donor", "acceptor"):
|
|
43
|
+
raise ValueError("side must be 'donor' or 'acceptor'")
|
|
44
|
+
if self.exon_number < 1 or self.occurrence < 1:
|
|
45
|
+
raise ValueError("exon_number and occurrence must be positive")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class SpliceOption:
|
|
50
|
+
"""One graph rewrite offered at a disrupted splice site.
|
|
51
|
+
|
|
52
|
+
``ordinal_rank`` expresses preference only. It is not a probability.
|
|
53
|
+
A calibrated scorer can independently populate ``probability``.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
mechanism: str
|
|
57
|
+
ordinal_rank: int
|
|
58
|
+
skipped_runs: Tuple[Any, ...] = field(default_factory=tuple)
|
|
59
|
+
required_runs: Tuple[Any, ...] = field(default_factory=tuple)
|
|
60
|
+
probability: Optional[float] = None
|
|
61
|
+
evidence: Mapping[str, Any] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
def __post_init__(self):
|
|
64
|
+
if self.ordinal_rank < 0:
|
|
65
|
+
raise ValueError("ordinal_rank must be non-negative")
|
|
66
|
+
if self.probability is not None and not 0.0 <= self.probability <= 1.0:
|
|
67
|
+
raise ValueError("probability must be between zero and one")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class SpliceAxis:
|
|
72
|
+
"""Choices attached to one splice-graph site."""
|
|
73
|
+
|
|
74
|
+
key: SpliceSiteKey
|
|
75
|
+
anchor_runs: Tuple[Any, ...]
|
|
76
|
+
options: Tuple[SpliceOption, ...]
|
|
77
|
+
|
|
78
|
+
def __post_init__(self):
|
|
79
|
+
if not self.anchor_runs:
|
|
80
|
+
raise ValueError("A splice axis requires at least one anchor run")
|
|
81
|
+
if not self.options:
|
|
82
|
+
raise ValueError("A splice axis requires at least one option")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class SplicePlan:
|
|
87
|
+
"""A graph-valid set of splice choices."""
|
|
88
|
+
|
|
89
|
+
choices: Tuple[Tuple[SpliceSiteKey, SpliceOption], ...]
|
|
90
|
+
kept_runs: Tuple[Any, ...]
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def ordinal_key(self):
|
|
94
|
+
"""Lexicographic mechanism preference; lower is preferred."""
|
|
95
|
+
return tuple(option.ordinal_rank for _, option in self.choices)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def probability(self):
|
|
99
|
+
"""Joint probability only when every selected option is calibrated."""
|
|
100
|
+
probabilities = [option.probability for _, option in self.choices]
|
|
101
|
+
if any(value is None for value in probabilities):
|
|
102
|
+
return None
|
|
103
|
+
result = 1.0
|
|
104
|
+
for value in probabilities:
|
|
105
|
+
result *= value
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def enumerate_splice_plans(axes, run_keys, max_plans=64):
|
|
110
|
+
"""Enumerate graph-valid plans without multiplying dead splice sites.
|
|
111
|
+
|
|
112
|
+
A Cartesian product treats every initially disrupted site as independent.
|
|
113
|
+
That produces impossible combinations when one choice skips the exon that
|
|
114
|
+
another choice needs. Here every axis also has an implicit inactive
|
|
115
|
+
choice. It may be inactive only when another selected rewrite removes
|
|
116
|
+
all of its anchor runs. Conversely, a selected option is rejected when
|
|
117
|
+
another rewrite removes one of its required runs.
|
|
118
|
+
"""
|
|
119
|
+
axes = tuple(sorted(axes, key=lambda axis: axis.key))
|
|
120
|
+
run_keys = tuple(run_keys)
|
|
121
|
+
plans = []
|
|
122
|
+
# Put real choices before the inactive sentinel so enumeration order
|
|
123
|
+
# follows each axis's declared biological preference.
|
|
124
|
+
selections = [(axis.options + (None,)) for axis in axes]
|
|
125
|
+
for combination in itertools.product(*selections):
|
|
126
|
+
skipped_by = {}
|
|
127
|
+
for axis, option in zip(axes, combination):
|
|
128
|
+
if option is None:
|
|
129
|
+
continue
|
|
130
|
+
for run in option.skipped_runs:
|
|
131
|
+
skipped_by.setdefault(run, set()).add(axis.key)
|
|
132
|
+
kept = tuple(run for run in run_keys if run not in skipped_by)
|
|
133
|
+
kept_set = set(kept)
|
|
134
|
+
valid = True
|
|
135
|
+
choices = []
|
|
136
|
+
for axis, option in zip(axes, combination):
|
|
137
|
+
removed_by_other = any(
|
|
138
|
+
any(owner != axis.key for owner in skipped_by.get(run, ()))
|
|
139
|
+
for run in axis.anchor_runs)
|
|
140
|
+
if option is None:
|
|
141
|
+
if not all(run not in kept_set for run in axis.anchor_runs):
|
|
142
|
+
valid = False
|
|
143
|
+
break
|
|
144
|
+
continue
|
|
145
|
+
if removed_by_other:
|
|
146
|
+
valid = False
|
|
147
|
+
break
|
|
148
|
+
if any(run not in kept_set for run in option.required_runs):
|
|
149
|
+
valid = False
|
|
150
|
+
break
|
|
151
|
+
choices.append((axis.key, option))
|
|
152
|
+
if not valid:
|
|
153
|
+
continue
|
|
154
|
+
plans.append(SplicePlan(tuple(choices), kept))
|
|
155
|
+
if len(plans) > max_plans:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
"Splice hypothesis count exceeds max_plans=%d" % max_plans)
|
|
158
|
+
return tuple(plans)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True)
|
|
162
|
+
class EffectHypothesis:
|
|
163
|
+
"""One assignment of phase, splicing and structural resolution."""
|
|
164
|
+
|
|
165
|
+
phase: Tuple[Tuple[Any, str], ...] = field(default_factory=tuple)
|
|
166
|
+
splice_plan: Optional[SplicePlan] = None
|
|
167
|
+
phase_probability: Optional[float] = 1.0
|
|
168
|
+
evidence: Mapping[str, Any] = field(default_factory=dict)
|
|
169
|
+
enumeration_index: int = 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True)
|
|
173
|
+
class RealizedTranscriptProduct:
|
|
174
|
+
"""A materialized mRNA/protein plus its non-reference junctions."""
|
|
175
|
+
|
|
176
|
+
transcript: Optional[Any]
|
|
177
|
+
cdna_sequence: Optional[str]
|
|
178
|
+
protein_sequence: Optional[str]
|
|
179
|
+
junction_signature: Tuple[Tuple[Any, ...], ...] = field(
|
|
180
|
+
default_factory=tuple)
|
|
181
|
+
start_codon_present: Optional[bool] = None
|
|
182
|
+
evidence: Mapping[str, Any] = field(default_factory=dict)
|
|
183
|
+
|
|
184
|
+
def local_protein(self, offset, flank=30):
|
|
185
|
+
"""Protein window around a change, or the full product if unknown."""
|
|
186
|
+
if self.protein_sequence is None:
|
|
187
|
+
return None
|
|
188
|
+
if offset is None:
|
|
189
|
+
return self.protein_sequence
|
|
190
|
+
start = max(0, offset - flank)
|
|
191
|
+
end = min(len(self.protein_sequence), offset + flank + 1)
|
|
192
|
+
return self.protein_sequence[start:end]
|
|
193
|
+
|
|
194
|
+
def local_cdna(self, offset, flank=90):
|
|
195
|
+
"""cDNA window for products with no translated difference."""
|
|
196
|
+
if self.cdna_sequence is None:
|
|
197
|
+
return None
|
|
198
|
+
if offset is None:
|
|
199
|
+
return self.cdna_sequence
|
|
200
|
+
start = max(0, offset - flank)
|
|
201
|
+
end = min(len(self.cdna_sequence), offset + flank + 1)
|
|
202
|
+
return self.cdna_sequence[start:end]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _effect_identity(effect):
|
|
206
|
+
"""Stable semantic fields used in candidate merge identity."""
|
|
207
|
+
return (
|
|
208
|
+
type(effect).__name__,
|
|
209
|
+
getattr(effect, "aa_mutation_start_offset", None),
|
|
210
|
+
getattr(effect, "aa_ref", None),
|
|
211
|
+
getattr(effect, "aa_alt", None),
|
|
212
|
+
getattr(effect, "splice_signal", None),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclass(frozen=True)
|
|
217
|
+
class ClassifiedOutcome:
|
|
218
|
+
"""An effect classified between a baseline and mutant product."""
|
|
219
|
+
|
|
220
|
+
hypothesis: EffectHypothesis
|
|
221
|
+
effect: Any
|
|
222
|
+
baseline: RealizedTranscriptProduct
|
|
223
|
+
mutant: RealizedTranscriptProduct
|
|
224
|
+
change_aa_offset: Optional[int] = None
|
|
225
|
+
change_cdna_offset: Optional[int] = None
|
|
226
|
+
|
|
227
|
+
def merge_key(self, protein_flank=30, cdna_flank=90):
|
|
228
|
+
"""Identity of the comparison, not merely the mutant product."""
|
|
229
|
+
return (
|
|
230
|
+
_effect_identity(self.effect),
|
|
231
|
+
self.baseline.local_protein(self.change_aa_offset, protein_flank),
|
|
232
|
+
self.mutant.local_protein(self.change_aa_offset, protein_flank),
|
|
233
|
+
self.baseline.local_cdna(self.change_cdna_offset, cdna_flank),
|
|
234
|
+
self.mutant.local_cdna(self.change_cdna_offset, cdna_flank),
|
|
235
|
+
self.baseline.junction_signature,
|
|
236
|
+
self.mutant.junction_signature,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass(frozen=True)
|
|
241
|
+
class RealizedEffectCandidate:
|
|
242
|
+
"""Merged equivalent outcomes with all hypothesis provenance retained."""
|
|
243
|
+
|
|
244
|
+
effect: Any
|
|
245
|
+
outcomes: Tuple[ClassifiedOutcome, ...]
|
|
246
|
+
ordinal_key: Tuple[int, ...]
|
|
247
|
+
probability: Optional[float]
|
|
248
|
+
phase_mass: Optional[float]
|
|
249
|
+
first_enumeration_index: int
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def hypotheses(self):
|
|
253
|
+
return tuple(outcome.hypothesis for outcome in self.outcomes)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def merge_classified_outcomes(outcomes, protein_flank=30, cdna_flank=90):
|
|
257
|
+
"""Merge only outcomes with identical baseline→mutant comparisons."""
|
|
258
|
+
groups = {}
|
|
259
|
+
order = []
|
|
260
|
+
for outcome in outcomes:
|
|
261
|
+
key = outcome.merge_key(protein_flank, cdna_flank)
|
|
262
|
+
if key not in groups:
|
|
263
|
+
groups[key] = []
|
|
264
|
+
order.append(key)
|
|
265
|
+
groups[key].append(outcome)
|
|
266
|
+
|
|
267
|
+
candidates = []
|
|
268
|
+
for key in order:
|
|
269
|
+
group = tuple(groups[key])
|
|
270
|
+
plans = [outcome.hypothesis.splice_plan for outcome in group]
|
|
271
|
+
ordinal_keys = [
|
|
272
|
+
plan.ordinal_key if plan is not None else () for plan in plans]
|
|
273
|
+
probabilities = []
|
|
274
|
+
for outcome, plan in zip(group, plans):
|
|
275
|
+
splice_probability = (
|
|
276
|
+
plan.probability if plan is not None else 1.0)
|
|
277
|
+
phase_probability = outcome.hypothesis.phase_probability
|
|
278
|
+
probabilities.append(
|
|
279
|
+
splice_probability * phase_probability
|
|
280
|
+
if splice_probability is not None
|
|
281
|
+
and phase_probability is not None
|
|
282
|
+
else None)
|
|
283
|
+
probability = (
|
|
284
|
+
sum(probabilities)
|
|
285
|
+
if all(value is not None for value in probabilities) else None)
|
|
286
|
+
phase_probabilities = [
|
|
287
|
+
outcome.hypothesis.phase_probability for outcome in group]
|
|
288
|
+
phase_mass = (
|
|
289
|
+
sum(phase_probabilities)
|
|
290
|
+
if all(value is not None for value in phase_probabilities)
|
|
291
|
+
else None)
|
|
292
|
+
candidates.append(RealizedEffectCandidate(
|
|
293
|
+
effect=group[0].effect,
|
|
294
|
+
outcomes=group,
|
|
295
|
+
ordinal_key=min(ordinal_keys),
|
|
296
|
+
probability=probability,
|
|
297
|
+
phase_mass=phase_mass,
|
|
298
|
+
first_enumeration_index=min(
|
|
299
|
+
outcome.hypothesis.enumeration_index for outcome in group)))
|
|
300
|
+
return tuple(candidates)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def order_realized_candidates(candidates, effect_priority_fn):
|
|
304
|
+
"""Order candidates without pretending ordinal rules are probabilities."""
|
|
305
|
+
candidates = tuple(candidates)
|
|
306
|
+
all_calibrated = bool(candidates) and all(
|
|
307
|
+
candidate.probability is not None for candidate in candidates)
|
|
308
|
+
|
|
309
|
+
def key(candidate):
|
|
310
|
+
probability_key = (
|
|
311
|
+
-candidate.probability if all_calibrated else 0.0)
|
|
312
|
+
return (
|
|
313
|
+
probability_key,
|
|
314
|
+
candidate.ordinal_key,
|
|
315
|
+
-effect_priority_fn(candidate.effect),
|
|
316
|
+
candidate.first_enumeration_index,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return tuple(sorted(candidates, key=key))
|