66from ga4gh .va_spec .base .enums import StrengthOfEvidenceProvided
77
88from mavedb .models .mapped_variant import MappedVariant
9- from mavedb .lib .annotation .constants import ZEIBERG_CALIBRATION_CALIBRATION_STRENGTH_OF_EVIDENCE_MAP
10- from mavedb .lib .validation .utilities import inf_or_float
11- from mavedb .view_models .score_range import ScoreSetRanges
9+ from mavedb .view_models .score_calibration import FunctionalRange
1210
1311logger = logging .getLogger (__name__ )
1412
@@ -24,18 +22,30 @@ class ExperimentalVariantFunctionalImpactClassification(StrEnum):
2422def functional_classification_of_variant (
2523 mapped_variant : MappedVariant ,
2624) -> ExperimentalVariantFunctionalImpactClassification :
27- if mapped_variant .variant .score_set .score_ranges is None :
25+ """Classify a variant's functional impact as normal, abnormal, or indeterminate.
26+
27+ Uses the primary score calibration and its functional ranges.
28+ Raises ValueError if required calibration or score is missing.
29+ """
30+ if not mapped_variant .variant .score_set .score_calibrations :
2831 raise ValueError (
29- f"Variant { mapped_variant .variant .urn } does not have a score set with score ranges ."
32+ f"Variant { mapped_variant .variant .urn } does not have a score set with score calibrations ."
3033 " Unable to classify functional impact."
3134 )
3235
33- # This view model object is much simpler to work with.
34- score_ranges = ScoreSetRanges (** mapped_variant .variant .score_set .score_ranges ).investigator_provided
36+ # TODO#494: Support for multiple calibrations (all non-research use only).
37+ score_calibrations = mapped_variant .variant .score_set .score_calibrations or []
38+ primary_calibration = next ((c for c in score_calibrations if c .primary ), None )
39+
40+ if not primary_calibration :
41+ raise ValueError (
42+ f"Variant { mapped_variant .variant .urn } does not have a primary score calibration."
43+ " Unable to classify functional impact."
44+ )
3545
36- if not score_ranges or not score_ranges . ranges :
46+ if not primary_calibration . functional_ranges :
3747 raise ValueError (
38- f"Variant { mapped_variant .variant .urn } does not have investigator-provided score ranges ."
48+ f"Variant { mapped_variant .variant .urn } does not have ranges defined in its primary score calibration ."
3949 " Unable to classify functional impact."
4050 )
4151
@@ -47,33 +57,48 @@ def functional_classification_of_variant(
4757 " Unable to classify functional impact."
4858 )
4959
50- for range in score_ranges .ranges :
51- lower_bound , upper_bound = inf_or_float (range .range [0 ], lower = True ), inf_or_float (range .range [1 ], lower = False )
52- if functional_score > lower_bound and functional_score <= upper_bound :
53- if range .classification == "normal" :
60+ for functional_range in primary_calibration .functional_ranges :
61+ # It's easier to reason with the view model objects for functional ranges than the JSONB fields in the raw database object.
62+ functional_range_view = FunctionalRange .model_validate (functional_range )
63+
64+ if functional_range_view .is_contained_by_range (functional_score ):
65+ if functional_range_view .classification == "normal" :
5466 return ExperimentalVariantFunctionalImpactClassification .NORMAL
55- elif range .classification == "abnormal" :
67+ elif functional_range_view .classification == "abnormal" :
5668 return ExperimentalVariantFunctionalImpactClassification .ABNORMAL
5769 else :
5870 return ExperimentalVariantFunctionalImpactClassification .INDETERMINATE
5971
6072 return ExperimentalVariantFunctionalImpactClassification .INDETERMINATE
6173
6274
63- def zeiberg_calibration_clinical_classification_of_variant (
75+ def pathogenicity_classification_of_variant (
6476 mapped_variant : MappedVariant ,
6577) -> tuple [VariantPathogenicityEvidenceLine .Criterion , Optional [StrengthOfEvidenceProvided ]]:
66- if mapped_variant .variant .score_set .score_ranges is None :
78+ """Classify a variant's pathogenicity and evidence strength using clinical calibration.
79+
80+ Uses the first clinical score calibration and its functional ranges.
81+ Raises ValueError if required calibration, score, or evidence strength is missing.
82+ """
83+ if not mapped_variant .variant .score_set .score_calibrations :
6784 raise ValueError (
68- f"Variant { mapped_variant .variant .urn } does not have a score set with score thresholds ."
85+ f"Variant { mapped_variant .variant .urn } does not have a score set with score calibrations ."
6986 " Unable to classify clinical impact."
7087 )
7188
72- score_ranges = ScoreSetRanges (** mapped_variant .variant .score_set .score_ranges ).zeiberg_calibration
89+ # TODO#494: Support multiple clinical calibrations.
90+ score_calibrations = mapped_variant .variant .score_set .score_calibrations or []
91+ primary_calibration = next ((c for c in score_calibrations if c .primary ), None )
92+
93+ if not primary_calibration :
94+ raise ValueError (
95+ f"Variant { mapped_variant .variant .urn } does not have a primary score calibration."
96+ " Unable to classify clinical impact."
97+ )
7398
74- if not score_ranges or not score_ranges . ranges :
99+ if not primary_calibration . functional_ranges :
75100 raise ValueError (
76- f"Variant { mapped_variant .variant .urn } does not have pillar project score ranges ."
101+ f"Variant { mapped_variant .variant .urn } does not have ranges defined in its primary score calibration ."
77102 " Unable to classify clinical impact."
78103 )
79104
@@ -85,9 +110,44 @@ def zeiberg_calibration_clinical_classification_of_variant(
85110 " Unable to classify clinical impact."
86111 )
87112
88- for range in score_ranges .ranges :
89- lower_bound , upper_bound = inf_or_float (range .range [0 ], lower = True ), inf_or_float (range .range [1 ], lower = False )
90- if functional_score > lower_bound and functional_score <= upper_bound :
91- return ZEIBERG_CALIBRATION_CALIBRATION_STRENGTH_OF_EVIDENCE_MAP [range .evidence_strength ]
92-
93- return ZEIBERG_CALIBRATION_CALIBRATION_STRENGTH_OF_EVIDENCE_MAP [0 ]
113+ for pathogenicity_range in primary_calibration .functional_ranges :
114+ # It's easier to reason with the view model objects for functional ranges than the JSONB fields in the raw database object.
115+ pathogenicity_range_view = FunctionalRange .model_validate (pathogenicity_range )
116+
117+ if pathogenicity_range_view .is_contained_by_range (functional_score ):
118+ if pathogenicity_range_view .acmg_classification is None :
119+ return (VariantPathogenicityEvidenceLine .Criterion .PS3 , None )
120+
121+ # More of a type guard, as the ACMGClassification model we construct above enforces that
122+ # criterion and evidence strength are mutually defined.
123+ if (
124+ pathogenicity_range_view .acmg_classification .evidence_strength is None
125+ or pathogenicity_range_view .acmg_classification .criterion is None
126+ ): # pragma: no cover - enforced by model validators in FunctionalRange view model
127+ return (VariantPathogenicityEvidenceLine .Criterion .PS3 , None )
128+
129+ # TODO#540: Handle moderate+
130+ if (
131+ pathogenicity_range_view .acmg_classification .evidence_strength .name
132+ not in StrengthOfEvidenceProvided ._member_names_
133+ ):
134+ raise ValueError (
135+ f"Variant { mapped_variant .variant .urn } is contained in a clinical calibration range with an invalid evidence strength."
136+ " Unable to classify clinical impact."
137+ )
138+
139+ if (
140+ pathogenicity_range_view .acmg_classification .criterion .name
141+ not in VariantPathogenicityEvidenceLine .Criterion ._member_names_
142+ ): # pragma: no cover - enforced by model validators in FunctionalRange view model
143+ raise ValueError (
144+ f"Variant { mapped_variant .variant .urn } is contained in a clinical calibration range with an invalid criterion."
145+ " Unable to classify clinical impact."
146+ )
147+
148+ return (
149+ VariantPathogenicityEvidenceLine .Criterion [pathogenicity_range_view .acmg_classification .criterion .name ],
150+ StrengthOfEvidenceProvided [pathogenicity_range_view .acmg_classification .evidence_strength .name ],
151+ )
152+
153+ return (VariantPathogenicityEvidenceLine .Criterion .PS3 , None )
0 commit comments