Skip to content

Commit 817bc32

Browse files
authored
Merge pull request #527 from VariantEffect/feature/bencap/526/standalone-baseline-score
Allow ranges to contain a standalone baseline score
2 parents e29eb2f + a50daca commit 817bc32

File tree

6 files changed

+149
-19
lines changed

6 files changed

+149
-19
lines changed

src/mavedb/lib/annotation/classification.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def functional_classification_of_variant(
3333
# This view model object is much simpler to work with.
3434
score_ranges = ScoreSetRanges(**mapped_variant.variant.score_set.score_ranges).investigator_provided
3535

36-
if not score_ranges:
36+
if not score_ranges or not score_ranges.ranges:
3737
raise ValueError(
3838
f"Variant {mapped_variant.variant.urn} does not have investigator-provided score ranges."
3939
" Unable to classify functional impact."
@@ -71,7 +71,7 @@ def zeiberg_calibration_clinical_classification_of_variant(
7171

7272
score_ranges = ScoreSetRanges(**mapped_variant.variant.score_set.score_ranges).zeiberg_calibration
7373

74-
if not score_ranges:
74+
if not score_ranges or not score_ranges.ranges:
7575
raise ValueError(
7676
f"Variant {mapped_variant.variant.urn} does not have pillar project score ranges."
7777
" Unable to classify clinical impact."

src/mavedb/lib/annotation/util.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def _can_annotate_variant_base_assumptions(mapped_variant: MappedVariant) -> boo
162162
return True
163163

164164

165-
def _variant_score_ranges_have_required_keys_for_annotation(
165+
def _variant_score_ranges_have_required_keys_and_ranges_for_annotation(
166166
mapped_variant: MappedVariant, key_options: list[str]
167167
) -> bool:
168168
"""
@@ -173,7 +173,8 @@ def _variant_score_ranges_have_required_keys_for_annotation(
173173
key_options (list[str]): List of possible score range keys to check for in the score set.
174174
175175
Returns:
176-
bool: False if none of the required keys are found or if all found keys have None values.
176+
bool: False if none of the required keys are found or if all found keys have None values or if all found keys
177+
do not have range data.
177178
Returns True (implicitly) if at least one required key exists with a non-None value.
178179
"""
179180
if mapped_variant.variant.score_set.score_ranges is None:
@@ -182,6 +183,7 @@ def _variant_score_ranges_have_required_keys_for_annotation(
182183
if not any(
183184
range_key in mapped_variant.variant.score_set.score_ranges
184185
and mapped_variant.variant.score_set.score_ranges[range_key] is not None
186+
and mapped_variant.variant.score_set.score_ranges[range_key]["ranges"]
185187
for range_key in key_options
186188
):
187189
return False
@@ -209,14 +211,14 @@ def can_annotate_variant_for_pathogenicity_evidence(mapped_variant: MappedVarian
209211
Notes:
210212
The function performs two main validation checks:
211213
1. Basic annotation assumptions via _can_annotate_variant_base_assumptions
212-
2. Required clinical range keys via _variant_score_ranges_have_required_keys_for_annotation
214+
2. Required clinical range keys via _variant_score_ranges_have_required_keys_and_ranges_for_annotation
213215
214216
Both checks must pass for the variant to be considered eligible for
215217
pathogenicity evidence annotation.
216218
"""
217219
if not _can_annotate_variant_base_assumptions(mapped_variant):
218220
return False
219-
if not _variant_score_ranges_have_required_keys_for_annotation(mapped_variant, CLINICAL_RANGES):
221+
if not _variant_score_ranges_have_required_keys_and_ranges_for_annotation(mapped_variant, CLINICAL_RANGES):
220222
return False
221223

222224
return True
@@ -245,7 +247,7 @@ def can_annotate_variant_for_functional_statement(mapped_variant: MappedVariant)
245247
"""
246248
if not _can_annotate_variant_base_assumptions(mapped_variant):
247249
return False
248-
if not _variant_score_ranges_have_required_keys_for_annotation(mapped_variant, FUNCTIONAL_RANGES):
250+
if not _variant_score_ranges_have_required_keys_and_ranges_for_annotation(mapped_variant, FUNCTIONAL_RANGES):
249251
return False
250252

251253
return True

src/mavedb/view_models/score_range.py

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,9 @@ def validate_baseline_score(self: "BrnichScoreRangesBase") -> "BrnichScoreRanges
205205

206206
if baseline_score is not None:
207207
if not any(range_model.classification == "normal" for range_model in ranges):
208-
raise ValidationError("A baseline score has been provided, but no normal classification range exists.")
208+
# For now, we do not raise an error if a baseline score is provided but no normal range exists.
209+
# raise ValidationError("A baseline score has been provided, but no normal classification range exists.")
210+
return self
209211

210212
normal_ranges = [range_model.range for range_model in ranges if range_model.classification == "normal"]
211213

@@ -336,6 +338,90 @@ class ScottScoreRanges(BrnichScoreRanges, SavedScottScoreRanges):
336338
research_use_only: bool = False
337339

338340

341+
##############################################################################################################
342+
# IGVF Coding Variant Focus Group (CVFG) range models
343+
##############################################################################################################
344+
345+
# Controls: All Variants
346+
347+
348+
class IGVFCodingVariantFocusGroupControlScoreRangesBase(BrnichScoreRangesBase):
349+
title: str = "IGVF Coding Variant Focus Group -- Controls: All Variants"
350+
research_use_only: bool = False
351+
352+
353+
class IGVFCodingVariantFocusGroupControlScoreRangesModify(
354+
BrnichScoreRangesModify, IGVFCodingVariantFocusGroupControlScoreRangesBase
355+
):
356+
title: str = "IGVF Coding Variant Focus Group -- Controls: All Variants"
357+
research_use_only: bool = False
358+
359+
360+
class IGVFCodingVariantFocusGroupControlScoreRangesCreate(
361+
BrnichScoreRangesCreate, IGVFCodingVariantFocusGroupControlScoreRangesModify
362+
):
363+
title: str = "IGVF Coding Variant Focus Group -- Controls: All Variants"
364+
research_use_only: bool = False
365+
366+
367+
class SavedIGVFCodingVariantFocusGroupControlScoreRanges(
368+
SavedBrnichScoreRanges, IGVFCodingVariantFocusGroupControlScoreRangesBase
369+
):
370+
record_type: str = None # type: ignore
371+
372+
title: str = "IGVF Coding Variant Focus Group -- Controls: All Variants"
373+
research_use_only: bool = False
374+
375+
_record_type_factory = record_type_validator()(set_record_type)
376+
377+
378+
class IGVFCodingVariantFocusGroupControlScoreRanges(
379+
BrnichScoreRanges, SavedIGVFCodingVariantFocusGroupControlScoreRanges
380+
):
381+
title: str = "IGVF Coding Variant Focus Group -- Controls: All Variants"
382+
research_use_only: bool = False
383+
384+
385+
# Controls: Missense Variants
386+
387+
388+
class IGVFCodingVariantFocusGroupMissenseScoreRangesBase(BrnichScoreRangesBase):
389+
title: str = "IGVF Coding Variant Focus Group -- Controls: Missense Variants Only"
390+
research_use_only: bool = False
391+
392+
393+
class IGVFCodingVariantFocusGroupMissenseScoreRangesModify(
394+
BrnichScoreRangesModify, IGVFCodingVariantFocusGroupMissenseScoreRangesBase
395+
):
396+
title: str = "IGVF Coding Variant Focus Group -- Controls: Missense Variants Only"
397+
research_use_only: bool = False
398+
399+
400+
class IGVFCodingVariantFocusGroupMissenseScoreRangesCreate(
401+
BrnichScoreRangesCreate, IGVFCodingVariantFocusGroupMissenseScoreRangesModify
402+
):
403+
title: str = "IGVF Coding Variant Focus Group -- Controls: Missense Variants Only"
404+
research_use_only: bool = False
405+
406+
407+
class SavedIGVFCodingVariantFocusGroupMissenseScoreRanges(
408+
SavedBrnichScoreRanges, IGVFCodingVariantFocusGroupMissenseScoreRangesBase
409+
):
410+
record_type: str = None # type: ignore
411+
412+
title: str = "IGVF Coding Variant Focus Group -- Controls: Missense Variants Only"
413+
research_use_only: bool = False
414+
415+
_record_type_factory = record_type_validator()(set_record_type)
416+
417+
418+
class IGVFCodingVariantFocusGroupMissenseScoreRanges(
419+
BrnichScoreRanges, SavedIGVFCodingVariantFocusGroupMissenseScoreRanges
420+
):
421+
title: str = "IGVF Coding Variant Focus Group -- Controls: Missense Variants Only"
422+
research_use_only: bool = False
423+
424+
339425
##############################################################################################################
340426
# Zeiberg specific calibration models
341427
##############################################################################################################
@@ -450,12 +536,20 @@ class ScoreSetRangesBase(BaseModel):
450536
investigator_provided: Optional[InvestigatorScoreRangesBase] = None
451537
scott_calibration: Optional[ScottScoreRangesBase] = None
452538
zeiberg_calibration: Optional[ZeibergCalibrationScoreRangesBase] = None
539+
cvfg_all_variants: Optional[IGVFCodingVariantFocusGroupControlScoreRangesBase] = None
540+
cvfg_missense_variants: Optional[IGVFCodingVariantFocusGroupMissenseScoreRangesBase] = None
453541

454542
_fields_to_exclude_for_validatation = {"record_type"}
455543

456544
@model_validator(mode="after")
457545
def score_range_labels_must_be_unique(self: "ScoreSetRangesBase") -> "ScoreSetRangesBase":
458-
for container in (self.investigator_provided, self.zeiberg_calibration, self.scott_calibration):
546+
for container in (
547+
self.investigator_provided,
548+
self.zeiberg_calibration,
549+
self.scott_calibration,
550+
self.cvfg_all_variants,
551+
self.cvfg_missense_variants,
552+
):
459553
if container is None:
460554
continue
461555

@@ -478,12 +572,16 @@ class ScoreSetRangesModify(ScoreSetRangesBase):
478572
investigator_provided: Optional[InvestigatorScoreRangesModify] = None
479573
scott_calibration: Optional[ScottScoreRangesModify] = None
480574
zeiberg_calibration: Optional[ZeibergCalibrationScoreRangesModify] = None
575+
cvfg_all_variants: Optional[IGVFCodingVariantFocusGroupControlScoreRangesModify] = None
576+
cvfg_missense_variants: Optional[IGVFCodingVariantFocusGroupMissenseScoreRangesModify] = None
481577

482578

483579
class ScoreSetRangesCreate(ScoreSetRangesModify):
484580
investigator_provided: Optional[InvestigatorScoreRangesCreate] = None
485581
scott_calibration: Optional[ScottScoreRangesCreate] = None
486582
zeiberg_calibration: Optional[ZeibergCalibrationScoreRangesCreate] = None
583+
cvfg_all_variants: Optional[IGVFCodingVariantFocusGroupControlScoreRangesCreate] = None
584+
cvfg_missense_variants: Optional[IGVFCodingVariantFocusGroupMissenseScoreRangesCreate] = None
487585

488586

489587
class SavedScoreSetRanges(ScoreSetRangesBase):
@@ -492,6 +590,8 @@ class SavedScoreSetRanges(ScoreSetRangesBase):
492590
investigator_provided: Optional[SavedInvestigatorScoreRanges] = None
493591
scott_calibration: Optional[SavedScottScoreRanges] = None
494592
zeiberg_calibration: Optional[SavedZeibergCalibrationScoreRanges] = None
593+
cvfg_all_variants: Optional[SavedIGVFCodingVariantFocusGroupControlScoreRanges] = None
594+
cvfg_missense_variants: Optional[SavedIGVFCodingVariantFocusGroupMissenseScoreRanges] = None
495595

496596
_record_type_factory = record_type_validator()(set_record_type)
497597

@@ -500,3 +600,5 @@ class ScoreSetRanges(SavedScoreSetRanges):
500600
investigator_provided: Optional[InvestigatorScoreRanges] = None
501601
scott_calibration: Optional[ScottScoreRanges] = None
502602
zeiberg_calibration: Optional[ZeibergCalibrationScoreRanges] = None
603+
cvfg_all_variants: Optional[IGVFCodingVariantFocusGroupControlScoreRanges] = None
604+
cvfg_missense_variants: Optional[IGVFCodingVariantFocusGroupMissenseScoreRanges] = None

tests/lib/annotation/test_annotate.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ def test_variant_functional_impact_statement_no_score_ranges(mock_mapped_variant
1919
assert result is None
2020

2121

22+
def test_variant_functional_impact_statement_no_score_range_data(mock_mapped_variant):
23+
mock_mapped_variant.variant.score_set.score_ranges["investigator_provided"]["ranges"] = []
24+
result = variant_functional_impact_statement(mock_mapped_variant)
25+
26+
assert result is None
27+
28+
2229
def test_variant_functional_impact_statement_no_score(mock_mapped_variant):
2330
mock_mapped_variant.variant.data = {"score_data": {"score": None}}
2431
result = variant_functional_impact_statement(mock_mapped_variant)
@@ -73,6 +80,13 @@ def test_variant_pathogenicity_evidence_with_score_ranges_no_thresholds(mock_map
7380
assert result is None
7481

7582

83+
def test_variant_pathogenicity_evidence_with_score_ranges_no_threshold_data(mock_mapped_variant):
84+
mock_mapped_variant.variant.score_set.score_ranges["zeiberg_calibration"]["ranges"] = []
85+
result = variant_pathogenicity_evidence(mock_mapped_variant)
86+
87+
assert result is None
88+
89+
7690
def test_variant_pathogenicity_evidence_with_score_ranges_with_thresholds(mock_mapped_variant):
7791
result = variant_pathogenicity_evidence(mock_mapped_variant)
7892

tests/lib/annotation/test_util.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
from mavedb.lib.annotation.util import (
55
variation_from_mapped_variant,
66
_can_annotate_variant_base_assumptions,
7-
_variant_score_ranges_have_required_keys_for_annotation,
7+
_variant_score_ranges_have_required_keys_and_ranges_for_annotation,
88
can_annotate_variant_for_functional_statement,
99
can_annotate_variant_for_pathogenicity_evidence,
1010
)
1111

12-
from tests.helpers.constants import TEST_VALID_POST_MAPPED_VRS_ALLELE, TEST_SEQUENCE_LOCATION_ACCESSION
12+
from tests.helpers.constants import (
13+
TEST_VALID_POST_MAPPED_VRS_ALLELE,
14+
TEST_SEQUENCE_LOCATION_ACCESSION,
15+
TEST_BRNICH_SCORE_SET_RANGE_WITH_SOURCE,
16+
)
1317
from unittest.mock import patch
1418

1519

@@ -53,28 +57,35 @@ def test_score_range_check_returns_false_when_keys_are_none(mock_mapped_variant)
5357
mock_mapped_variant.variant.score_set.score_ranges = None
5458
key_options = ["required_key1", "required_key2"]
5559

56-
assert _variant_score_ranges_have_required_keys_for_annotation(mock_mapped_variant, key_options) is False
60+
assert _variant_score_ranges_have_required_keys_and_ranges_for_annotation(mock_mapped_variant, key_options) is False
5761

5862

5963
def test_score_range_check_returns_false_when_no_keys_present(mock_mapped_variant):
60-
mock_mapped_variant.variant.score_set.score_ranges = {"other_key": "value"}
64+
mock_mapped_variant.variant.score_set.score_ranges = {"other_key": TEST_BRNICH_SCORE_SET_RANGE_WITH_SOURCE}
6165
key_options = ["required_key1", "required_key2"]
6266

63-
assert _variant_score_ranges_have_required_keys_for_annotation(mock_mapped_variant, key_options) is False
67+
assert _variant_score_ranges_have_required_keys_and_ranges_for_annotation(mock_mapped_variant, key_options) is False
6468

6569

6670
def test_score_range_check_returns_false_when_key_present_but_value_is_none(mock_mapped_variant):
6771
mock_mapped_variant.variant.score_set.score_ranges = {"required_key1": None}
6872
key_options = ["required_key1", "required_key2"]
6973

70-
assert _variant_score_ranges_have_required_keys_for_annotation(mock_mapped_variant, key_options) is False
74+
assert _variant_score_ranges_have_required_keys_and_ranges_for_annotation(mock_mapped_variant, key_options) is False
75+
76+
77+
def test_score_range_check_returns_false_when_key_present_but_range_value_is_empty(mock_mapped_variant):
78+
mock_mapped_variant.variant.score_set.score_ranges = {"required_key1": {"ranges": []}}
79+
key_options = ["required_key1", "required_key2"]
80+
81+
assert _variant_score_ranges_have_required_keys_and_ranges_for_annotation(mock_mapped_variant, key_options) is False
7182

7283

7384
def test_score_range_check_returns_none_when_at_least_one_key_has_value(mock_mapped_variant):
74-
mock_mapped_variant.variant.score_set.score_ranges = {"required_key1": "value"}
85+
mock_mapped_variant.variant.score_set.score_ranges = {"required_key1": TEST_BRNICH_SCORE_SET_RANGE_WITH_SOURCE}
7586
key_options = ["required_key1", "required_key2"]
7687

77-
assert _variant_score_ranges_have_required_keys_for_annotation(mock_mapped_variant, key_options) is True
88+
assert _variant_score_ranges_have_required_keys_and_ranges_for_annotation(mock_mapped_variant, key_options) is True
7889

7990

8091
## Test clinical range check
@@ -89,7 +100,7 @@ def test_clinical_range_check_returns_false_when_base_assumptions_fail(mock_mapp
89100

90101
@pytest.mark.parametrize("clinical_ranges", [["clinical_range"], ["other_clinical_range"]])
91102
def test_clinical_range_check_returns_false_when_clinical_ranges_check_fails(mock_mapped_variant, clinical_ranges):
92-
mock_mapped_variant.variant.score_set.score_ranges = {"unrelated_key": "value"}
103+
mock_mapped_variant.variant.score_set.score_ranges = {"unrelated_key": TEST_BRNICH_SCORE_SET_RANGE_WITH_SOURCE}
93104

94105
with patch("mavedb.lib.annotation.util.CLINICAL_RANGES", clinical_ranges):
95106
result = can_annotate_variant_for_pathogenicity_evidence(mock_mapped_variant)
@@ -116,7 +127,7 @@ def test_functional_range_check_returns_false_when_base_assumptions_fail(mock_ma
116127
def test_functional_range_check_returns_false_when_functional_ranges_check_fails(
117128
mock_mapped_variant, functional_ranges
118129
):
119-
mock_mapped_variant.variant.score_set.score_ranges = {"unrelated_key": "value"}
130+
mock_mapped_variant.variant.score_set.score_ranges = {"unrelated_key": TEST_BRNICH_SCORE_SET_RANGE_WITH_SOURCE}
120131

121132
with patch("mavedb.lib.annotation.util.FUNCTIONAL_RANGES", functional_ranges):
122133
result = can_annotate_variant_for_functional_statement(mock_mapped_variant)

tests/view_models/test_score_range.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,7 @@ def test_score_ranges_zeiberg_calibration_ranges_boundaries_may_be_adjacent(
646646
ScoreRangesModel(**valid_data)
647647

648648

649+
@pytest.mark.skip("Not applicable currently. Baseline score may be provided on its own.")
649650
@pytest.mark.parametrize(
650651
"ScoreRangesModel",
651652
[

0 commit comments

Comments
 (0)