diff --git a/.gitignore b/.gitignore index dda9cfc..f1d529a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ src/bench/tmp/*.csv cache/ cache/** +# Prediction file +prediction.json + # DS_Store file .DS_Store diff --git a/src/auto_acmg.py b/src/auto_acmg.py index 5b31235..fe4f60b 100644 --- a/src/auto_acmg.py +++ b/src/auto_acmg.py @@ -1,6 +1,6 @@ """Implementations of the PVS1 algorithm.""" -from typing import Optional, Type, Union +from typing import Dict, Optional, Type, Union from loguru import logger @@ -19,7 +19,7 @@ from src.defs.auto_acmg import AutoACMGResult, CdsInfo, GenomicStrand from src.defs.exceptions import AlgorithmError, AutoAcmgBaseException, ParseError from src.defs.genome_builds import GenomeRelease -from src.defs.mehari import ProteinPos, TxPos +from src.defs.mehari import CdsPos, ProteinPos, TxPos from src.defs.seqvar import SeqVar, SeqVarResolver from src.utils import SeqVarTranscriptsHelper from src.vcep import ( @@ -29,6 +29,34 @@ CDH1Predictor, CerebralCreatineDeficiencySyndromesPredictor, CoagulationFactorDeficiencyPredictor, + CongenitalMyopathiesPredictor, + DICER1Predictor, + ENIGMAPredictor, + EpilepsySodiumChannelPredictor, + FamilialHypercholesterolemiaPredictor, + FBN1Predictor, + GlaucomaPredictor, + HBOPCPredictor, + HearingLossPredictor, + HHTPredictor, + InsightColorectalCancerPredictor, + LeberCongenitalAmaurosisPredictor, + LysosomalDiseasesPredictor, + MalignantHyperthermiaPredictor, + MitochondrialDiseasesPredictor, + MonogenicDiabetesPredictor, + MyeloidMalignancyPredictor, + PKUPredictor, + PlateletDisordersPredictor, + PTENPredictor, + PulmonaryHypertensionPredictor, + RASopathyPredictor, + RettAngelmanPredictor, + SCIDPredictor, + ThrombosisPredictor, + TP53Predictor, + VHLPredictor, + VonWillebrandDiseasePredictor, ) #: Mapping of HGNC gene identifiers to predictor classes. @@ -52,6 +80,92 @@ "HGNC:11055": CerebralCreatineDeficiencySyndromesPredictor, # SLC6A8 "HGNC:3546": CoagulationFactorDeficiencyPredictor, # F8 "HGNC:3551": CoagulationFactorDeficiencyPredictor, # F9 + "HGNC:7720": CongenitalMyopathiesPredictor, # NEB + "HGNC:129": CongenitalMyopathiesPredictor, # ACTA1 + "HGNC:2974": CongenitalMyopathiesPredictor, # DNM2 + "HGNC:7448": CongenitalMyopathiesPredictor, # MTM1 + "HGNC:10483": CongenitalMyopathiesPredictor, # RYR1 + "HGNC:17098": DICER1Predictor, # DICER1 + "HGNC:1100": ENIGMAPredictor, # BRCA1 + "HGNC:1101": ENIGMAPredictor, # BRCA2 + "HGNC:10585": EpilepsySodiumChannelPredictor, # SCN1A + "HGNC:10588": EpilepsySodiumChannelPredictor, # SCN2A + "HGNC:10590": EpilepsySodiumChannelPredictor, # SCN3A + "HGNC:10596": EpilepsySodiumChannelPredictor, # SCN8A + "HGNC:10586": EpilepsySodiumChannelPredictor, # SCN1B + "HGNC:6547": FamilialHypercholesterolemiaPredictor, # LDLR + "HGNC:3603": FBN1Predictor, # FBN1 + "HGNC:7610": GlaucomaPredictor, # MYOC + "HGNC:13733": HearingLossPredictor, # CDH23 + "HGNC:2180": HearingLossPredictor, # COCH + "HGNC:4284": HearingLossPredictor, # GJB2 + "HGNC:6298": HearingLossPredictor, # KCNQ4 + "HGNC:7605": HearingLossPredictor, # MYO6 + "HGNC:7606": HearingLossPredictor, # MYO7A + "HGNC:8818": HearingLossPredictor, # SLC26A4 + "HGNC:11720": HearingLossPredictor, # TECTA + "HGNC:12601": HearingLossPredictor, # USH2A + "HGNC:7594": HearingLossPredictor, # MYO15A + "HGNC:8515": HearingLossPredictor, # OTOF + "HGNC:795": HBOPCPredictor, # ATM + "HGNC:26144": HBOPCPredictor, # PALB2 + "HGNC:175": HHTPredictor, # ACVRL1 + "HGNC:3349": HHTPredictor, # ENG + "HGNC:583": InsightColorectalCancerPredictor, # APC + "HGNC:7127": InsightColorectalCancerPredictor, # MLH1 + "HGNC:7325": InsightColorectalCancerPredictor, # MSH2 + "HGNC:7329": InsightColorectalCancerPredictor, # MSH6 + "HGNC:9122": InsightColorectalCancerPredictor, # PMS2 + "HGNC:10294": LeberCongenitalAmaurosisPredictor, # RPE65 + "HGNC:4065": LysosomalDiseasesPredictor, # GAA + "HGNC:10483": MalignantHyperthermiaPredictor, # GBA + "HGNC:23287": MitochondrialDiseasesPredictor, # ETHE1 + "HGNC:8806": MitochondrialDiseasesPredictor, # PDHA1 + "HGNC:9179": MitochondrialDiseasesPredictor, # POLG + "HGNC:16266": MitochondrialDiseasesPredictor, # SLC19A3 + "HGNC:11621": MonogenicDiabetesPredictor, # HNF1A + "HGNC:5024": MonogenicDiabetesPredictor, # HNF4A + "HGNC:4195": MonogenicDiabetesPredictor, # GCK + "HGNC:10471": MyeloidMalignancyPredictor, # RUNX1 + "HGNC:8582": PKUPredictor, # PAH + "HGNC:6138": PlateletDisordersPredictor, # ITGA2B + "HGNC:6156": PlateletDisordersPredictor, # ITGB3 + "HGNC:9588": PTENPredictor, # PTEN + "HGNC:1078": PulmonaryHypertensionPredictor, # BMPR2 + "HGNC:12726": VonWillebrandDiseasePredictor, # VWF + "HGNC:775": ThrombosisPredictor, # SERPINC1 + "HGNC:11998": TP53Predictor, # TP53 + "HGNC:12687": VHLPredictor, # VHL + "HGNC:15454": RASopathyPredictor, # SHOC2 + "HGNC:7989": RASopathyPredictor, # NRAS + "HGNC:9829": RASopathyPredictor, # RAF1 + "HGNC:11187": RASopathyPredictor, # SOS1 + "HGNC:11188": RASopathyPredictor, # SOS2 + "HGNC:9644": RASopathyPredictor, # PTPN11 + "HGNC:6407": RASopathyPredictor, # KRAS + "HGNC:6840": RASopathyPredictor, # MAP2K1 + "HGNC:5173": RASopathyPredictor, # HRAS + "HGNC:10023": RASopathyPredictor, # RIT1 + "HGNC:6842": RASopathyPredictor, # MAP2K2 + "HGNC:1097": RASopathyPredictor, # BRAF + "HGNC:7227": RASopathyPredictor, # MRAS + "HGNC:6742": RASopathyPredictor, # LZTR1 + "HGNC:17271": RASopathyPredictor, # RRAS2 + "HGNC:9282": RASopathyPredictor, # PPP1CB + "HGNC:11634": RettAngelmanPredictor, # TCF4 + "HGNC:11079": RettAngelmanPredictor, # SLC9A6 + "HGNC:11411": RettAngelmanPredictor, # CDKL5 + "HGNC:3811": RettAngelmanPredictor, # FOXG1 + "HGNC:6990": RettAngelmanPredictor, # MECP2 + "HGNC:12496": RettAngelmanPredictor, # UBE3A + "HGNC:12765": SCIDPredictor, # FOXN1 + "HGNC:186": SCIDPredictor, # ADA + "HGNC:17642": SCIDPredictor, # DCLRE1C + "HGNC:6024": SCIDPredictor, # IL7R + "HGNC:6193": SCIDPredictor, # JAK3 + "HGNC:9831": SCIDPredictor, # RAG1 + "HGNC:9832": SCIDPredictor, # RAG2 + "HGNC:6010": SCIDPredictor, # IL2RG } @@ -183,6 +297,9 @@ def parse_data(self, seqvar: SeqVar) -> AutoACMGResult: self.result.data.tx_pos_utr = ( seqvar_transcript.tx_pos.ord if isinstance(seqvar_transcript.tx_pos, TxPos) else -1 ) + self.result.data.cds_pos = ( + seqvar_transcript.cds_pos.ord if isinstance(seqvar_transcript.cds_pos, CdsPos) else 0 + ) self.result.data.prot_pos = ( seqvar_transcript.protein_pos.ord if isinstance(seqvar_transcript.protein_pos, ProteinPos) diff --git a/src/cli.py b/src/cli.py index 67754e3..22704f8 100644 --- a/src/cli.py +++ b/src/cli.py @@ -54,7 +54,8 @@ def classify( raise InvalidGenomeBuild("Invalid genome release") auto_acmg = AutoACMG(variant, genome_release_enum) - auto_acmg.predict() + prediction = auto_acmg.predict() + prediction.save_to_file() if prediction else logger.error("No prediction was made.") except AutoAcmgBaseException as e: logger.error("Error occurred: {}", e) diff --git a/src/criteria/auto_pm1.py b/src/criteria/auto_pm1.py index e7414a8..61a3bd4 100644 --- a/src/criteria/auto_pm1.py +++ b/src/criteria/auto_pm1.py @@ -13,6 +13,7 @@ AutoACMGData, AutoACMGPrediction, AutoACMGStrength, + GenomicStrand, ) from src.defs.exceptions import AlgorithmError, AutoAcmgBaseException, InvalidAPIResposeError from src.defs.genome_builds import GenomeRelease @@ -30,6 +31,44 @@ def __init__(self): #: comment_pm1 to store the prediction explanation. self.comment_pm1: str = "" + @staticmethod + def _get_affected_exon(var_data: AutoACMGData, seqvar: SeqVar) -> int: + """ + Get the affected exon number for the variant. + + Go through all exons and count them before the variant position. + Pay attention to the strand of the gene. + + Args: + var_data: AutoACMGData object + seqvar: SeqVar object + + Returns: + int: Affected exon number + """ + exon_number = 0 + if var_data.strand == GenomicStrand.Plus: + for exon in var_data.exons: + if exon.altStartI >= seqvar.pos: + return exon_number + if exon.altStartI <= seqvar.pos <= exon.altEndI: + exon_number += 1 + return exon_number + if exon.altEndI < seqvar.pos: + exon_number += 1 + elif var_data.strand == GenomicStrand.Minus: + for exon in var_data.exons[::-1]: + if exon.altEndI <= seqvar.pos: + return exon_number + if exon.altStartI <= seqvar.pos <= exon.altEndI: + exon_number += 1 + return exon_number + if exon.altStartI > seqvar.pos: + exon_number += 1 + else: + raise AlgorithmError(f"Invalid strand for {var_data.hgnc_id}: {var_data.strand}") + return exon_number + def _count_vars(self, seqvar: SeqVar, start_pos: int, end_pos: int) -> Tuple[int, int]: """ Counts pathogenic and benign variants in the specified range. diff --git a/src/defs/auto_acmg.py b/src/defs/auto_acmg.py index 1a28e89..caa9444 100644 --- a/src/defs/auto_acmg.py +++ b/src/defs/auto_acmg.py @@ -397,6 +397,7 @@ class AutoACMGData(AutoAcmgBaseModel): transcript_id: str = "" transcript_tags: List[str] = [] tx_pos_utr: int = -1 + cds_pos: int = 0 prot_pos: int = -1 prot_length: int = -1 cds_info: Dict[str, CdsInfo] = {} diff --git a/src/defs/core.py b/src/defs/core.py index 9e3ded7..67f013f 100644 --- a/src/defs/core.py +++ b/src/defs/core.py @@ -30,3 +30,8 @@ class AutoAcmgBaseModel(BaseModel): # class Config: # use_enum_values = True # arbitrary_types_allowed = True + + def save_to_file(self, file_path: str = "prediction.json") -> None: + """Save the model to a file.""" + with open(file_path, "w") as file: + file.write(self.model_dump_json(indent=4)) diff --git a/src/vcep/__init__.py b/src/vcep/__init__.py index 8114a8a..314e958 100644 --- a/src/vcep/__init__.py +++ b/src/vcep/__init__.py @@ -6,3 +6,31 @@ CerebralCreatineDeficiencySyndromesPredictor, ) from src.vcep.coagulation_factor_deficiency import CoagulationFactorDeficiencyPredictor +from src.vcep.congenital_myopathies import CongenitalMyopathiesPredictor +from src.vcep.dicer1 import DICER1Predictor +from src.vcep.enigma import ENIGMAPredictor +from src.vcep.epilepsy_sodium_channel import EpilepsySodiumChannelPredictor +from src.vcep.familial_hypercholesterolemia import FamilialHypercholesterolemiaPredictor +from src.vcep.fbn1 import FBN1Predictor +from src.vcep.glaucoma import GlaucomaPredictor +from src.vcep.hbopc import HBOPCPredictor +from src.vcep.hearing_loss import HearingLossPredictor +from src.vcep.hhtp import HHTPredictor +from src.vcep.insight_colorectal_cancer import InsightColorectalCancerPredictor +from src.vcep.leber_congenital_amaurosis import LeberCongenitalAmaurosisPredictor +from src.vcep.lysosomal_diseases import LysosomalDiseasesPredictor +from src.vcep.malignant_hyperthermia_susceptibility import MalignantHyperthermiaPredictor +from src.vcep.mitochondrial_diseases import MitochondrialDiseasesPredictor +from src.vcep.monogenic_diabetes import MonogenicDiabetesPredictor +from src.vcep.myeloid_malignancy import MyeloidMalignancyPredictor +from src.vcep.pku import PKUPredictor +from src.vcep.platelet_disorders import PlateletDisordersPredictor +from src.vcep.pten import PTENPredictor +from src.vcep.pulmonary_hypertension import PulmonaryHypertensionPredictor +from src.vcep.rasopathy import RASopathyPredictor +from src.vcep.rett_angelman import RettAngelmanPredictor +from src.vcep.scid import SCIDPredictor +from src.vcep.thrombosis import ThrombosisPredictor +from src.vcep.tp53 import TP53Predictor +from src.vcep.vhl import VHLPredictor +from src.vcep.von_willebrand_disease import VonWillebrandDiseasePredictor diff --git a/src/vcep/acadvl.py b/src/vcep/acadvl.py index 0e35c54..13ce09d 100644 --- a/src/vcep/acadvl.py +++ b/src/vcep/acadvl.py @@ -10,7 +10,7 @@ from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength from src.defs.seqvar import SeqVar -PM1_CLUSTER_REGIONS = [ +PM1_CLUSTER = [ (214, 223), # Nucleotide and substrate binding (249, 251), # Nucleotide and substrate binding (460, 466), # Nucleotide and substrate binding @@ -25,7 +25,7 @@ def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteri """Override predict_pm1 to include VCEP-specific logic for ACADVL.""" logger.info("Predict PM1") # Check if variant falls within critical regions - for start, end in PM1_CLUSTER_REGIONS: + for start, end in PM1_CLUSTER: if start <= var_data.prot_pos <= end: comment = ( f"Variant falls within a critical region for ACADVL between positions " diff --git a/src/vcep/cardiomyopathy.py b/src/vcep/cardiomyopathy.py index f233318..d990c8c 100644 --- a/src/vcep/cardiomyopathy.py +++ b/src/vcep/cardiomyopathy.py @@ -1,6 +1,6 @@ """ Predictor for Cardiomyopathy VCEP. -Included gene: +Included genes: MYH7 (HGNC:7577), MYBPC3 (HGNC:7551), TNNI3 (HGNC:11947), diff --git a/src/vcep/coagulation_factor_deficiency.py b/src/vcep/coagulation_factor_deficiency.py index 6b02d4e..10559d3 100644 --- a/src/vcep/coagulation_factor_deficiency.py +++ b/src/vcep/coagulation_factor_deficiency.py @@ -13,13 +13,7 @@ from loguru import logger from src.criteria.default_predictor import DefaultPredictor -from src.defs.auto_acmg import ( - AutoACMGCriteria, - AutoACMGData, - AutoACMGPrediction, - AutoACMGStrength, - GenomicStrand, -) +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength from src.defs.exceptions import AlgorithmError from src.defs.seqvar import SeqVar @@ -39,7 +33,7 @@ }, "moderate": { "residues": [(1667, 1667), (1332, 1332)], # Residues affecting secretion - "regions": [(2267, 2304)], # FXa-binding residues, excluding Ser2283 + "domains": [(2267, 2304)], # FXa-binding residues, excluding Ser2283 "excluded_residues": [(2283, 2283)], # Excluded residue in FXa-binding region }, }, @@ -60,44 +54,6 @@ class CoagulationFactorDeficiencyPredictor(DefaultPredictor): - @staticmethod - def _get_affected_exon(var_data: AutoACMGData, seqvar: SeqVar) -> int: - """ - Get the affected exon number for the variant. - - Go through all exons and count them before the variant position. - Pay attention to the strand of the gene. - - Args: - var_data: AutoACMGData object - seqvar: SeqVar object - - Returns: - int: Affected exon number - """ - exon_number = 0 - if var_data.strand == GenomicStrand.Plus: - for exon in var_data.exons: - if exon.altStartI >= seqvar.pos: - return exon_number - if exon.altStartI <= seqvar.pos <= exon.altEndI: - exon_number += 1 - return exon_number - if exon.altEndI < seqvar.pos: - exon_number += 1 - elif var_data.strand == GenomicStrand.Minus: - for exon in var_data.exons[::-1]: - if exon.altEndI <= seqvar.pos: - return exon_number - if exon.altStartI <= seqvar.pos <= exon.altEndI: - exon_number += 1 - return exon_number - if exon.altStartI > seqvar.pos: - exon_number += 1 - else: - raise AlgorithmError(f"Invalid strand for {var_data.hgnc_id}: {var_data.strand}") - return exon_number - def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: """ Override predict_pm1 to include VCEP-specific logic for Coagulation Factor Deficiency. @@ -139,8 +95,8 @@ def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteri summary=comment, ) - # Check moderate level criteria for regions, excluding specific residues - for start, end in gene_cluster.get("moderate", {}).get("regions", []): + # Check moderate level criteria for domains, excluding specific residues + for start, end in gene_cluster.get("moderate", {}).get("domains", []): if start <= var_data.prot_pos <= end: excluded_residues = gene_cluster.get("moderate", {}).get("excluded_residues", []) if not any(start <= var_data.prot_pos <= end for start, end in excluded_residues): diff --git a/src/vcep/congenital_myopathies.py b/src/vcep/congenital_myopathies.py new file mode 100644 index 0000000..0b16b8f --- /dev/null +++ b/src/vcep/congenital_myopathies.py @@ -0,0 +1,65 @@ +""" +Predictor for Congenital Myopathies VCEP. +Included genes: +NEB (HGNC:7720), +ACTA1 (HGNC:129), +DNM2 (HGNC:2974), +MTM1 (HGNC:7448), +RYR1 (HGNC:10483). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN146 +https://cspec.genome.network/cspec/ui/svi/doc/GN147 +https://cspec.genome.network/cspec/ui/svi/doc/GN148 +https://cspec.genome.network/cspec/ui/svi/doc/GN149 +https://cspec.genome.network/cspec/ui/svi/doc/GN150 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +PM1_CLUSTER = { + # RYR1 + "HGNC:10483": [(4800, 4950)], +} + + +class CongenitalMyopathiesPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """Override PM1 prediction for congenital myopathies.""" + logger.info("Predict PM1") + + # NEB, ACTA1, DNM2, MTM1 + if var_data.hgnc_id in ["HGNC:7720", "HGNC:129", "HGNC:2974", "HGNC:7448"]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicModerate, + summary="Not applicable for NEB, ACTA1, DNM2, MTM1.", + ) + + if var_data.hgnc_id in PM1_CLUSTER: + for start, end in PM1_CLUSTER[var_data.hgnc_id]: + if start <= var_data.prot_pos <= end: + comment = ( + f"Variant falls within a critical domain for {var_data.hgnc_id} " + f"between positions {start}-{end}. PM1 is met." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + else: + return super().predict_pm1(seqvar, var_data) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not fall within a critical domain.", + ) diff --git a/src/vcep/dicer1.py b/src/vcep/dicer1.py new file mode 100644 index 0000000..1aef9ca --- /dev/null +++ b/src/vcep/dicer1.py @@ -0,0 +1,77 @@ +""" +Predictor for DICER1 and miRNA-Processing Gene VCEP. +Included gene: DICER1 (HGNC:17098). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN024 +""" + +from typing import Dict, List + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +PM1_CLUSTER: Dict[str, Dict[str, Dict[str, List]]] = { + # DICER1 + "HGNC:17098": { + "moderate": { + "residues": [1344, 1705, 1709, 1713, 1809, 1810, 1813], # Metal ion-binding residues + }, + "supporting": { + "domains": [(1682, 1846)], # RNase IIIb domain excluding the metal ion-binding residues + }, + }, +} + + +class DICER1Predictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """Override predict_pm1 to include VCEP-specific logic for DICER1.""" + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not affect a critical domain for DICER1.", + ) + + # Check moderate level criteria for metal ion-binding residues + if var_data.prot_pos in gene_cluster.get("moderate", {}).get("residues", []): + comment = ( + f"Variant affects a metal ion-binding residue in DICER1 " + f"(position {var_data.prot_pos}). PM1 is met at the Moderate level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # Check supporting level criteria for RNase IIIb domain excluding metal ion-binding residues + for start, end in gene_cluster.get("supporting", {}).get("domains", []): + if start <= var_data.prot_pos <= end: + comment = ( + f"Variant affects a residue in the RNase IIIb domain of DICER1 " + f"(position {var_data.prot_pos}) outside the key metal ion-binding residues. " + f"PM1 is met at the Supporting level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicSupporting, + summary=comment, + ) + + # If no criteria match + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not affect a critical domain for DICER1.", + ) diff --git a/src/vcep/enigma.py b/src/vcep/enigma.py new file mode 100644 index 0000000..ca8f267 --- /dev/null +++ b/src/vcep/enigma.py @@ -0,0 +1,32 @@ +""" +Predictor for ENIGMA BRCA1 and BRCA2 VCEP. +Included genes: +BRCA1 (HGNC:1100), +BRCA2 (HGNC:1101). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN092 +https://cspec.genome.network/cspec/ui/svi/doc/GN097 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + + +class ENIGMAPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """Override PM1 prediction for ENIGMA BRCA1 and BRCA2.""" + logger.info("Predict PM1") + + if var_data.hgnc_id in ["HGNC:1100", "HGNC:1101"]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicModerate, + summary="PM1 is not applicable for BRCA1 and BRCA2.", + ) + + return super().predict_pm1(seqvar, var_data) diff --git a/src/vcep/epilepsy_sodium_channel.py b/src/vcep/epilepsy_sodium_channel.py new file mode 100644 index 0000000..7242851 --- /dev/null +++ b/src/vcep/epilepsy_sodium_channel.py @@ -0,0 +1,146 @@ +""" +Predictor for Epilepsy Sodium Channel VCEP. +Included genes: +SCN1A (HGNC:10585), +SCN2A (HGNC:10588), +SCN3A (HGNC:10590), +SCN8A (HGNC:10596), +SCN1B (HGNC:10586). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN067 +https://cspec.genome.network/cspec/ui/svi/doc/GN068 +https://cspec.genome.network/cspec/ui/svi/doc/GN069 +https://cspec.genome.network/cspec/ui/svi/doc/GN070 +https://cspec.genome.network/cspec/ui/svi/doc/GN076 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +PM1_CLUSTER = { + "HGNC:10585": { # SCN1A + "domains": [ + (226, 246), + (261, 273), + (451, 464), + (921, 929), + (941, 949), + (951, 964), + (966, 974), + (1001, 1009), + (1051, 1069), + (1466, 1509), + (1616, 1624), + (1626, 1639), + (1641, 1658), + (1781, 1799), + (1806, 1824), + (1921, 1934), + ], + }, + "HGNC:10588": { # SCN2A + "domains": [ + (227, 247), + (262, 272), + (453, 465), + (912, 920), + (932, 940), + (942, 955), + (957, 965), + (992, 1000), + (1042, 1068), + (1456, 1489), + (1606, 1623), + (1616, 1629), + (1631, 1649), + (1771, 1789), + (1796, 1814), + (1911, 1924), + ], + }, + "HGNC:10590": { # SCN3A + "domains": [ + (226, 248), + (261, 273), + (452, 465), + (913, 921), + (933, 941), + (943, 956), + (958, 966), + (993, 1001), + (1042, 1052), + (1451, 1494), + (1601, 1619), + (1611, 1624), + (1626, 1635), + (1766, 1784), + (1791, 1809), + (1906, 1919), + ], + }, + "HGNC:10596": { # SCN8A + "domains": [ + (230, 244), + (265, 273), + (439, 452), + (906, 914), + (926, 934), + (936, 949), + (951, 959), + (986, 994), + (1034, 1053), + (1447, 1498), + (1597, 1618), + (1607, 1620), + (1607, 1628), + (1761, 1788), + (1786, 1804), + (1901, 1914), + ], + }, +} + + +class EpilepsySodiumChannelPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """Override predict_pm1 to include VCEP-specific logic for Epilepsy Sodium Channel genes.""" + logger.info("Predict PM1") + + # Check if SCN1B, where PM1 is not applicable + if var_data.hgnc_id == "HGNC:10586": # SCN1B + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicModerate, + summary="PM1 is not applicable for SCN1B.", + ) + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check if the variant falls within any of the critical residues + for start_aa, end_aa in gene_cluster.get("domains", []): + if start_aa <= var_data.prot_pos <= end_aa: + comment = ( + f"Variant falls within a critical residue region for {var_data.hgnc_id} " + f"between positions {start_aa}-{end_aa}. PM1 is met." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # If no criteria match + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary=f"Variant does not meet the PM1 criteria for {var_data.hgnc_id}.", + ) diff --git a/src/vcep/familial_hypercholesterolemia.py b/src/vcep/familial_hypercholesterolemia.py new file mode 100644 index 0000000..bb44d00 --- /dev/null +++ b/src/vcep/familial_hypercholesterolemia.py @@ -0,0 +1,69 @@ +""" +Predictor for Familial Hypercholesterolemia VCEP. +Included gene: LDLR (HGNC:6547). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN013 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +# fmt: off +PM1_CLUSTER_LDLR = { + "HGNC:6547": { # LDLR + "residues": [ + 27, 34, 39, 46, 52, 63, 68, 75, 82, 89, + 95, 104, 109, 116, 121, 128, 134, 143, 148, 155, + 160, 167, 173, 184, 197, 204, 209, 216, 231, 236, + 243, 248, 255, 261, 270, 276, 284, 289, 296, 302, + 313, 318, 325, 329, 338, 340, 352, 358, 364, 368, + 377, 379, 392, 667, 677, 681, 696, 698, 711, + ] + }, +} + + + +class FamilialHypercholesterolemiaPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """Override predict_pm1 to include VCEP-specific logic for LDLR.""" + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER_LDLR.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check if the variant affects a critical residue + if var_data.prot_pos in gene_cluster["residues"]: + comment = ( + f"Variant affects a critical cysteine residue in LDLR at position " + f"{var_data.prot_pos}, which is involved in disulfide bond formation. PM1 is met." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # Check if the variant in the 4th exon + affected_exon = self._get_affected_exon(var_data, seqvar) + if affected_exon == 4: + comment = f"Variant affects the 4th exon in LDLR. PM1 is met." + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # If no criteria match + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary=f"Variant does not meet the PM1 criteria for LDLR.", + ) diff --git a/src/vcep/fbn1.py b/src/vcep/fbn1.py new file mode 100644 index 0000000..7366c9d --- /dev/null +++ b/src/vcep/fbn1.py @@ -0,0 +1,90 @@ +""" +Predictor for FBN1 VCEP. +Included gene: FBN1 (HGNC:3603). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN022 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +# Define the critical residues for PM1 strong consideration +# fmt: off +PM1_CLUSTER = { + "HGNC:3603": { + "strong_residues": [ + 250, 257, 262, 271, 273, 286, 292, 299, 304, 313, 315, 328, 494, 499, 504, 513, 515, 528, + 534, 541, 546, 555, 557, 570, 576, 582, 587, 596, 598, 611, 617, 623, 628, 637, 639, 652, + 727, 734, 739, 748, 750, 763, 769, 776, 781, 790, 792, 805, 811, 816, 821, 830, 832, 845, + 914, 921, 926, 935, 937, 950, 1032, 1039, 1044, 1053, 1055, 1068, 1074, 1081, 1086, 1095, + 1097, 1111, 1117, 1124, 1129, 1138, 1140, 1153, 1159, 1166, 1171, 1180, 1182, 1195, 1201, + 1208, 1212, 1221, 1223, 1236, 1242, 1249, 1254, 1263, 1265, 1278, 1284, 1291, 1296, 1305, + 1307, 1320, 1326, 1333, 1339, 1348, 1350, 1361, 1367, 1374, 1380, 1389, 1391, 1402, 1408, + 1415, 1420, 1429, 1431, 1444, 1450, 1456, 1461, 1470, 1472, 1485, 1491, 1497, 1502, 1511, + 1513, 1526, 1610, 1617, 1622, 1631, 1633, 1646, 1652, 1658, 1663, 1672, 1674, 1687, 1770, + 1777, 1782, 1791, 1793, 1806, 1812, 1818, 1824, 1833, 1835, 1847, 1853, 1860, 1865, 1874, + 1876, 1889, 1895, 1900, 1905, 1914, 1916, 1928, 1934, 1942, 1947, 1956, 1958, 1971, 1977, + 1984, 1989, 1998, 2000, 2011, 2017, 2024, 2029, 2038, 2040, 2053, 2131, 2137, 2142, 2151, + 2153, 2164, 2170, 2176, 2181, 2190, 2192, 2204, 2210, 2217, 2221, 2230, 2232, 2245, 2251, + 2258, 2265, 2274, 2276, 2289, 2295, 2302, 2307, 2316, 2318, 2331, 2406, 2413, 2418, 2427, + 2429, 2442, 2448, 2455, 2459, 2468, 2470, 2483, 2489, 2496, 2500, 2509, 2511, 2522, 2528, + 2535, 2541, 2550, 2552, 2565, 2571, 2577, 2581, 2590, 2592, 2605, 2611, 2617, 2622, 2631, + 2633, 2646, 2652, 2659, 2663, 2672, 2674, 2686, + ], + "moderate_residues": [ + 85, 89, 94, 100, 102, 111, 119, 123, 129, 134, 136, 145, 150, 154, 160, 166, 168, 177, + 186, 195, 204, 209, 210, 221, 224, 231, 244, 453, 460, 465, 474, 476, 488, 336, 345, 358, + 359, 360, 365, 377, 389, 661, 670, 683, 684, 685, 696, 699, 711, 853, 862, 875, 876, 887, + 890, 896, 958, 967, 980, 981, 982, 993, 996, 1008, 1534, 1549, 1562, 1563, 1564, 1574, + 1577, 1589, 1695, 1706, 1719, 1720, 1721, 1733, 1736, 1748, 2061, 2070, 2083, 2084, 2085, + 2096, 2099, 2111, 2339, 2348, 2363, 2364, 2365, 2375, 2378, 2390, + ] + } +} + + +class FBN1Predictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """Override predict_pm1 to include VCEP-specific logic for FBN1.""" + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check strong level criteria + if var_data.prot_pos in gene_cluster["strong_residues"]: + comment = ( + f"Variant affects a critical cysteine residue in FBN1 at position " + f"{var_data.prot_pos}, leading to disulfide bond folding defects. PM1 is met at the Strong level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicStrong, + summary=comment, + ) + + # Check moderate level criteria + if var_data.prot_pos in gene_cluster["moderate_residues"]: + comment = ( + f"Variant affects a residue in FBN1 at position {var_data.prot_pos}, which can " + f"lead to disulfide bond folding defects. PM1 is met at the Moderate level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # If no criteria match + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary=f"Variant does not meet the PM1 criteria for FBN1.", + ) diff --git a/src/vcep/glaucoma.py b/src/vcep/glaucoma.py new file mode 100644 index 0000000..fbb59c9 --- /dev/null +++ b/src/vcep/glaucoma.py @@ -0,0 +1,25 @@ +""" +Predictor for Glaucoma VCEP. +Included gene: MYOC (HGNC:7610). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN019 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + + +class GlaucomaPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """Override predict_pm1 to include VCEP-specific logic for Glaucoma.""" + logger.info("Predict PM1") + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicSupporting, + summary="PM1 is not applicable for MYOC.", + ) diff --git a/src/vcep/hbopc.py b/src/vcep/hbopc.py new file mode 100644 index 0000000..2038837 --- /dev/null +++ b/src/vcep/hbopc.py @@ -0,0 +1,38 @@ +""" +Predictor for Heriditary Breast, Ovarian and Pancreatic Cancer VCEP. +Included genes: +ATM (HGNC:795), +PALB2 (HGNC:26144). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN020 +https://cspec.genome.network/cspec/ui/svi/doc/GN077 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + + +class HBOPCPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Heriditary Breast, Ovarian and + Pancreatic Cancer. + """ + logger.info("Predict PM1") + + if var_data.hgnc_id in [ + "HGNC:795", # ATM + "HGNC:26144", # PALB2 + ]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicSupporting, + summary=f"PM1 is not applicable for {var_data.hgnc_id}.", + ) + + return super().predict_pm1(seqvar, var_data) diff --git a/src/vcep/hearing_loss.py b/src/vcep/hearing_loss.py new file mode 100644 index 0000000..480de82 --- /dev/null +++ b/src/vcep/hearing_loss.py @@ -0,0 +1,85 @@ +""" +Predictor for Hearing Loss VCEP. +Included genes: +CDH23 (HGNC:13733), +COCH (HGNC:2180), +GJB2 (HGNC:4284), +KCNQ4 (HGNC:6298), +MYO6 (HGNC:7605), +MYO7A (HGNC:7606), +SLC26A4 (HGNC:8818), +TECTA (HGNC:11720), +USH2A (HGNC:12601), +MYO15A (HGNC:7594), +OTOF (HGNC:8515). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN005 +https://cspec.genome.network/cspec/ui/svi/doc/GN023 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.exceptions import AlgorithmError +from src.defs.seqvar import SeqVar + +PM1_CLUSTER = { + "HGNC:6298": { + "domains": [ + (271, 292), # Pore-forming intramembrane region + ] + } +} + + +class HearingLossPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """Override predict_pm1 to include VCEP-specific logic for Hearing Loss.""" + logger.info("Predict PM1") + + if var_data.hgnc_id in [ + "HGNC:13733", # CDH23 + "HGNC:2180", # COCH + "HGNC:4284", # GJB2 + "HGNC:7605", # MYO6 + "HGNC:7606", # MYO7A + "HGNC:8818", # SLC26A4 + "HGNC:11720", # TECTA + "HGNC:12601", # USH2A + "HGNC:7594", # MYO15A + "HGNC:8515", # OTOF + ]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicSupporting, + summary=f"PM1 is not applicable for {var_data.hgnc_id}.", + ) + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check strong level criteria + for start, end in gene_cluster["domains"]: + if start <= var_data.prot_pos <= end: + comment = ( + f"Variant falls within the critical pore-forming intramembrane region " + f"of KCNQ4 between positions {start}-{end}. PM1 is met at the Strong level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicStrong, + summary=comment, + ) + + # If no criteria match + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not meet the PM1 criteria for KCNQ4.", + ) diff --git a/src/vcep/hhtp.py b/src/vcep/hhtp.py new file mode 100644 index 0000000..af298b4 --- /dev/null +++ b/src/vcep/hhtp.py @@ -0,0 +1,78 @@ +""" +Predictor for Hereditary Hemorrhagic Telangiectasia VCEP. +Included genes: +ACVRL1 (HGNC:175), +ENG (HGNC:3349). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN135 +https://cspec.genome.network/cspec/ui/svi/doc/GN136 +""" + +from typing import Dict, List, Tuple, Union + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +# fmt: off +PM1_CLUSTER: Dict[str, Dict[str, List[Union[Tuple[int, int], int]]]] = { + "HGNC:175": { + "domains": [ + (209, 216), # glycine-rich loop + (229, 229), # phosphate anchor + (242, 242), # C-helix E pairing the phosphate anchor + (329, 335), # catalytic loop + (348, 351), # metal-binding loop + # BMP10 interaction cluster (individual residues) + 40, 54, 56, 57, 58, 59, 66, 71, 72, 73, 75, 76, 78, 79, 80, 82, 83, 84, 85, 87, + ] + }, + "HGNC:3349": { + "domains": [ + 278, 282, # BMP9 binding sites + 207, 363, 382, 412, 549, # Pathogenic or likely pathogenic cysteine residues + 350, 394, 516, 582, # Cysteine residues critical for ENG function + ] + }, +} + + +class HHTPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Hereditary Hemorrhagic + Telangiectasia. + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check moderate level criteria + moderate_criteria = gene_cluster.get("domains", []) + if var_data.prot_pos in moderate_criteria or any( + isinstance(region, tuple) and region[0] <= var_data.prot_pos <= region[1] + for region in moderate_criteria + ): + comment = ( + f"Variant falls within a critical residue for {var_data.hgnc_id}. " + f"PM1 is met at the Moderate level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # If no criteria match + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary=f"Variant does not meet the PM1 criteria for {var_data.hgnc_id}.", + ) diff --git a/src/vcep/insight_colorectal_cancer.py b/src/vcep/insight_colorectal_cancer.py new file mode 100644 index 0000000..8c712d3 --- /dev/null +++ b/src/vcep/insight_colorectal_cancer.py @@ -0,0 +1,44 @@ +""" +Predictor for InSIGHT Hereditary Colorectal Cancer/Polyposis VCEP. +Included genes: +APC (HGNC:583), +MLH1 (HGNC:7127), +MSH2 (HGNC:7325), +MSH6 (HGNC:7329), +PMS2 (HGNC:9122). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN089 +https://cspec.genome.network/cspec/ui/svi/doc/GN115 +https://cspec.genome.network/cspec/ui/svi/doc/GN137 +https://cspec.genome.network/cspec/ui/svi/doc/GN138 +https://cspec.genome.network/cspec/ui/svi/doc/GN139 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + + +class InsightColorectalCancerPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """Override PM1 prediction for InSIGHT Hereditary Colorectal Cancer/Polyposis.""" + logger.info("Predict PM1") + + if var_data.hgnc_id in [ + "HGNC:583", # APC + "HGNC:7127", # MLH1 + "HGNC:7325", # MSH2 + "HGNC:7329", # MSH6 + "HGNC:9122", # PMS2 + ]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicSupporting, + summary=f"PM1 is not applicable for {var_data.hgnc_id}.", + ) + + return super().predict_pm1(seqvar, var_data) diff --git a/src/vcep/leber_congenital_amaurosis.py b/src/vcep/leber_congenital_amaurosis.py new file mode 100644 index 0000000..e6f90a7 --- /dev/null +++ b/src/vcep/leber_congenital_amaurosis.py @@ -0,0 +1,59 @@ +""" +Predictor for Leber Congenital Amaurosis/early onset Retinal Dystrophy VCEP. +Included gene: RPE65 (HGNC:10294) +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN120 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +PM1_CLUSTER = { + "HGNC:10294": { + "residues": [180, 182, 241, 313, 417, 527] + list(range(107, 126)), + } +} + + +class LeberCongenitalAmaurosisPredictor(DefaultPredictor): + """ + Predictor for Leber Congenital Amaurosis/early onset Retinal Dystrophy VCEP. + """ + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Leber Congenital Amaurosis/early + onset Retinal Dystrophy. + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + if var_data.prot_pos in gene_cluster["residues"]: + comment = ( + f"Variant affects a residue in RPE65 at position {var_data.prot_pos}, " + f"which is associated with Leber Congenital Amaurosis/early onset Retinal " + "Dystrophy." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # If no criteria are met, return Not Met + comment = ( + f"Variant does not meet the PM1 criteria for Leber Congenital Amaurosis/early onset " + "Retinal Dystrophy." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) diff --git a/src/vcep/lysosomal_diseases.py b/src/vcep/lysosomal_diseases.py new file mode 100644 index 0000000..28e13f1 --- /dev/null +++ b/src/vcep/lysosomal_diseases.py @@ -0,0 +1,66 @@ +""" +Predictor for Lysosomal Diseases VCEP. +Included gene: GAA (HGNC:4065). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN010 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +PM1_CLUSTER = { + "HGNC:4065": { + "residues": [ + 282, # D282 + 376, # W376 + 404, # D404 + 405, # L405 + 441, # I441 + 481, # W481 + 516, # W516 + 518, # D518 + 519, # M519 + 600, # R600 + 613, # W613 + 616, # D616 + 618, # W618 + 649, # F649 + 650, # L650 + 674, # H674 + ] + } +} + + +class LysosomalDiseasesPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Lysosomal Diseases. + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + if var_data.prot_pos in gene_cluster["residues"]: + comment = ( + f"Variant affects a residue in GAA at position {var_data.prot_pos}, " + f"which is associated with Lysosomal Diseases." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not meet the PM1 criteria for Lysosomal Diseases.", + ) diff --git a/src/vcep/malignant_hyperthermia_susceptibility.py b/src/vcep/malignant_hyperthermia_susceptibility.py new file mode 100644 index 0000000..80cc851 --- /dev/null +++ b/src/vcep/malignant_hyperthermia_susceptibility.py @@ -0,0 +1,78 @@ +""" +Predictor for Malignant Hyperthermia Susceptibility VCEP. +Included gene: RYR1 (HGNC:10483). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN012 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +PM1_CLUSTER = { + "HGNC:10483": { + "moderate": { + "domains": [ + (1, 552), # N-terminal region + (2101, 2458), # Central region + ], + }, + "supporting": { + "domains": [ + (1, 552), # N-terminal region (if PS1/PM5 applicable) + (2101, 2458), # Central region (if PS1/PM5 applicable) + (4631, 4991), # C-terminal region + ], + }, + } +} + + +class MalignantHyperthermiaPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Malignant Hyperthermia Susceptibility. + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check moderate level domains + for start, end in gene_cluster.get("moderate", {}).get("domains", []): + if start <= var_data.prot_pos <= end: + comment = ( + f"Variant falls within a critical region in RYR1 between positions {start}-{end}. " + f"PM1 is met at the Moderate level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # Check supporting level domains + for start, end in gene_cluster.get("supporting", {}).get("domains", []): + if start <= var_data.prot_pos <= end: + comment = ( + f"Variant falls within a critical region in RYR1 between positions {start}-{end}. " + f"PM1 is met at the Supporting level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicSupporting, + summary=comment, + ) + + # If no criteria match + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not meet the PM1 criteria for Malignant Hyperthermia Susceptibility.", + ) diff --git a/src/vcep/mitochondrial_diseases.py b/src/vcep/mitochondrial_diseases.py new file mode 100644 index 0000000..8870fa0 --- /dev/null +++ b/src/vcep/mitochondrial_diseases.py @@ -0,0 +1,38 @@ +""" +Predictor for Mitochondrial Diseases VCEP. +Included genes: +ETHE1 (HGNC:23287), +PDHA1 (HGNC:8806), +POLG (HGNC:9179), +SLC19A3 (HGNC:16266), +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN014 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + + +class MitochondrialDiseasesPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """Override predict_pm1 to include VCEP-specific logic for Mitochondrial Diseases.""" + logger.info("Predict PM1") + + if var_data.hgnc_id in [ + "HGNC:23287", # ETHE1 + "HGNC:8806", # PDHA1 + "HGNC:9179", # POLG + "HGNC:16266", # SLC19A3 + ]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicModerate, + summary=f"PM1 is not applicable for {var_data.hgnc_id}.", + ) + + return super().predict_pm1(seqvar, var_data) diff --git a/src/vcep/monogenic_diabetes.py b/src/vcep/monogenic_diabetes.py new file mode 100644 index 0000000..f653bb1 --- /dev/null +++ b/src/vcep/monogenic_diabetes.py @@ -0,0 +1,137 @@ +""" +Predictor for Monogenic Diabetes VCEP. +Included genes: +HNF1A (HGNC:11621), +HNF4A (HGNC:5024), +GCK (HGNC:4195). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN017 +https://cspec.genome.network/cspec/ui/svi/doc/GN085 +https://cspec.genome.network/cspec/ui/svi/doc/GN086 +""" + +from typing import Dict, List, Tuple, Union + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +# fmt: off +PM1_CLUSTER: Dict[str, Dict[str, Dict[str, List[Union[int, Tuple[int, int]]]]]] = { + "HGNC:11621": { # HNF1A + "moderate": { + "residues": [ + 130, 131, 132, 143, 144, 145, 146, 147, 148, 149, + 155, 156, 157, 158, 203, 204, 205, 206, 263, 264, + 265, 270, 271, 272, 273 + ]}, + "supporting": { + "domains": [ + (1, 32), # Dimerization domain + (107, 174), # Subset of DNA binding domains + (201, 280), # Subset of DNA binding domains + ], + "promoter_regions": [ + (-195, -187), # AP1 binding site + (-227, -209), # Overlapping HNF3 & NF-Y sites + (-259, -238), # HNF1A binding site + (-288, -276), # HNF4A binding site + ] + } + }, + "HGNC:5024": { # HNF4A + "moderate": { + "residues": [ + 43, 49, 50, 51, 56, 57, 59, 63, 64, 67, 70, 72, + 75, 87, 88, 91, 94, 109, 112, 113, + 38, 41, 55, 58, 74, 80, 90, 93 # zinc finger residues + ], + }, + "supporting": { + "domains": [ + (37, 113), # DNA binding domain + (180, 220), # Ligand binding domain + (300, 350), # Ligand binding domain + ], + "promoter_regions": [ + (-151, -132), # HNF6/OC2 and IPF1 binding sites + (-181, -169), # HNF1A/HNF1B binding sites + ] + } + }, + "HGNC:4195": { # GCK + "moderate": { + "residues": [ + 151, 153, 168, 169, 204, 205, 225, 231, 254, 258, + 287, 290, 78, 82, 85, 228, 229, 295, 296, 331, 333, + 336, 410, 416 + ] + } + } +} + + +class MonogenicDiabetesPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Monogenic Diabetes. + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check moderate level criteria + if var_data.prot_pos in gene_cluster.get("moderate", {}).get("residues", []): + comment = ( + f"Variant affects a residue at position {var_data.prot_pos} " + f"in {var_data.hgnc_id}, which is critical for Monogenic Diabetes." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # Check supporting level criteria + if "supporting" in gene_cluster: + # Check if the variant is in the specified domains + for start, end in gene_cluster.get("supporting", {}).get("domains", []): # type: ignore + if start <= var_data.prot_pos <= end: + comment = ( + f"Variant falls within a critical region in {var_data.hgnc_id} between " + f"positions {start}-{end}. PM1 is met at the Supporting level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicSupporting, + summary=comment, + ) + + # Check if the variant is in the promoter regions + for start, end in gene_cluster.get("supporting", {}).get("promoter_regions", []): # type: ignore + if start <= var_data.cds_pos <= end: + comment = ( + "Variant falls within a critical promoter region in " + f"{var_data.hgnc_id} between " + f"positions {start}-{end}. PM1 is met at the Supporting level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicSupporting, + summary=comment, + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary=f"Variant does not meet the PM1 criteria for {var_data.hgnc_id}.", + ) diff --git a/src/vcep/myeloid_malignancy.py b/src/vcep/myeloid_malignancy.py new file mode 100644 index 0000000..b86651d --- /dev/null +++ b/src/vcep/myeloid_malignancy.py @@ -0,0 +1,78 @@ +""" +Predictor for Myeloid Malignancy VCEP. +Included gene: RUNX1 (HGNC:10471). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN008 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +PM1_CLUSTER = { + "HGNC:10471": { # RUNX1 + "moderate": [ # Specific residues within the RHD (Runt Homology Domain) + 107, # R107 + 110, # K110 + 134, # A134 + 162, # R162 + 166, # R166 + 167, # S167 + 169, # R169 + 170, # G170 + 194, # K194 + 196, # T196 + 198, # D198 + 201, # R201 + 204, # R204 + ], + "supporting": list(range(89, 205)), # Other residues within the RHD (89-204) + } +} + + +class MyeloidMalignancyPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Myeloid Malignancy. + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check moderate level criteria + if var_data.prot_pos in gene_cluster["moderate"]: + comment = ( + f"Variant affects a critical residue within the Runt Homology Domain (RHD) " + f"at position {var_data.prot_pos} in RUNX1. PM1 is met at the Moderate level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # Check supporting level criteria + if var_data.prot_pos in gene_cluster["supporting"]: + comment = ( + f"Variant affects a residue within the Runt Homology Domain (RHD) " + f"at position {var_data.prot_pos} in RUNX1. PM1 is met at the Supporting level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicSupporting, + summary=comment, + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not meet the PM1 criteria for RUNX1.", + ) diff --git a/src/vcep/pku.py b/src/vcep/pku.py new file mode 100644 index 0000000..3574a50 --- /dev/null +++ b/src/vcep/pku.py @@ -0,0 +1,79 @@ +""" +Predictor for Phenylketonuria (PKU) VCEP. +Included gene: PAH (HGNC:8582). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN006 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +PM1_CLUSTER_PKU = { + "HGNC:8582": { # PAH + "residues": [ + # active site residues + 138, # Tyr138 + 158, # Arg158 + 245, # Val245 + 268, # Tyr268 + 278, # Thr278 + 279, # Pro279 + 289, # Glu289 + 300, # Ala300 + 315, # Asp315 + 331, # Phe331 + 345, # Ala345 + 346, # Gly346 + 349, # Ser349 + 377, # Tyr377 + ] + # substrate binding residues + + list(range(46, 49)) + + list(range(63, 70)) + # cofactor binding residues + + [ + 285, # His285 + 290, # His290 + 330, # Glu330 + ] + + list(range(246, 267)) + + list(range(280, 284)) + + list(range(322, 327)) + + list(range(377, 380)), # Additional ranges for cofactor binding + } +} + + +class PKUPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Phenylketonuria (PKU). + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER_PKU.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check moderate level criteria + if var_data.prot_pos in gene_cluster.get("residues", []): + comment = ( + f"Variant affects a residue at position {var_data.prot_pos} " + f"in {var_data.hgnc_id}, which is critical for PKU." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not meet the PM1 criteria for PAH.", + ) diff --git a/src/vcep/platelet_disorders.py b/src/vcep/platelet_disorders.py new file mode 100644 index 0000000..8f95f68 --- /dev/null +++ b/src/vcep/platelet_disorders.py @@ -0,0 +1,30 @@ +""" +Predictor for Platelet Disorders VCEP. +Included genes: +ITGA2B (HGNC:6138), +ITGB3 (HGNC:6156). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN011 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + + +class PlateletDisordersPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """Override PM1 prediction for Platelet Disorders.""" + logger.info("Predict PM1") + + if var_data.hgnc_id in ["HGNC:6138", "HGNC:6156"]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicSupporting, + summary="PM1 is not met for ITGA2B and ITGB3 due to genes being highly polymorphic.", + ) + + return super().predict_pm1(seqvar, var_data) diff --git a/src/vcep/pten.py b/src/vcep/pten.py new file mode 100644 index 0000000..1256c5e --- /dev/null +++ b/src/vcep/pten.py @@ -0,0 +1,54 @@ +""" +Predictor for PTEN VCEP. +Included gene: PTEN (HGNC:9588). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN003 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +PM1_CLUSTER = { + "HGNC:9588": { # PTEN + "residues": + # catalytic motifs + list(range(90, 95)) + + list(range(123, 131)) + + list(range(166, 169)), + } +} + + +class PTENPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for PTEN. + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check if the variant falls within the specified catalytic motifs + if var_data.prot_pos in gene_cluster.get("residues", []): + comment = ( + f"Variant affects a critical residue within the catalytic motifs of PTEN " + f"at position {var_data.prot_pos}. PM1 is met at the Moderate level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not meet the PM1 criteria for PTEN.", + ) diff --git a/src/vcep/pulmonary_hypertension.py b/src/vcep/pulmonary_hypertension.py new file mode 100644 index 0000000..f091f8d --- /dev/null +++ b/src/vcep/pulmonary_hypertension.py @@ -0,0 +1,92 @@ +""" +Predictor for Pulmonary Hypertension VCEP. +Included gene: BMPR2 (HGNC:1078). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN125 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +# fmt: off +PM1_CLUSTER_BMPR2 = { + "HGNC:1078": { # BMPR2 + "strong": [ + # Extracellular domain critical residues + 34, 60, 66, 84, 94, 99, 116, 117, 118, 123, + # Kinase domain critical residues + 210, 212, 230, 245, 333, 338, 351, 353, 386, 405, 410, 491, + # KD heterodimerization critical residues + 485, 486, 487, 488, 489, 490, 491, 492, + ], + "moderate": + list(range(33, 132)) # Extracellular domain: 33-131 + + list(range(203, 505)), # Kinase domain: 203-504 + "non_critical": [ + 42, 47, 82, 102, 107, 182, 186, 503, 899 # Non-critical residues + ], + } +} + + +class PulmonaryHypertensionPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Pulmonary Hypertension. + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER_BMPR2.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check if the variant falls within the strong level critical residues + if var_data.prot_pos in gene_cluster["strong"]: + comment = ( + f"Variant affects a critical residue in BMPR2 at position {var_data.prot_pos}. " + f"PM1 is met at the Strong level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicStrong, + summary=comment, + ) + + # Check if the variant falls within the moderate level critical residues + if ( + var_data.prot_pos in gene_cluster["moderate"] and + not var_data.prot_pos in gene_cluster["non_critical"] + ): + comment = ( + f"Variant affects a residue in BMPR2 at position {var_data.prot_pos} " + f"within the extracellular or kinase domain. PM1 is met at the Moderate level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # If the variant falls in a non-critical residue + if var_data.prot_pos in gene_cluster["non_critical"]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicSupporting, + summary=( + f"Variant affects a residue at position {var_data.prot_pos} in BMPR2, " + f"which is demonstrated to be non-critical for kinase activity." + ), + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not meet the PM1 criteria for BMPR2.", + ) diff --git a/src/vcep/rasopathy.py b/src/vcep/rasopathy.py new file mode 100644 index 0000000..60a8420 --- /dev/null +++ b/src/vcep/rasopathy.py @@ -0,0 +1,151 @@ +""" +Predictor for RASopathy VCEP. +Included genes: +SHOC2 (HGNC:15454), +NRAS (HGNC:7989), +RAF1 (HGNC:9829), +SOS1 (HGNC:11187), +SOS2 (HGNC:11188), +PTPN11 (HGNC:9644), +KRAS (HGNC:6407), +MAP2K1 (HGNC:6840), +HRAS (HGNC:5173), +RIT1 (HGNC:10023), +MAP2K2 (HGNC:6842), +BRAF (HGNC:1097), +MRAS (HGNC:7227), +LZTR1 (HGNC:6742), +RRAS2 (HGNC:17271), +PPP1CB (HGNC:9282). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN038 +https://cspec.genome.network/cspec/ui/svi/doc/GN039 +https://cspec.genome.network/cspec/ui/svi/doc/GN040 +https://cspec.genome.network/cspec/ui/svi/doc/GN041 +https://cspec.genome.network/cspec/ui/svi/doc/GN042 +https://cspec.genome.network/cspec/ui/svi/doc/GN043 +https://cspec.genome.network/cspec/ui/svi/doc/GN044 +https://cspec.genome.network/cspec/ui/svi/doc/GN045 +https://cspec.genome.network/cspec/ui/svi/doc/GN046 +https://cspec.genome.network/cspec/ui/svi/doc/GN047 +https://cspec.genome.network/cspec/ui/svi/doc/GN048 +https://cspec.genome.network/cspec/ui/svi/doc/GN049 +https://cspec.genome.network/cspec/ui/svi/doc/GN087 +https://cspec.genome.network/cspec/ui/svi/doc/GN094 +https://cspec.genome.network/cspec/ui/svi/doc/GN127 +https://cspec.genome.network/cspec/ui/svi/doc/GN128 +""" + +from typing import Dict, List, Tuple, Union + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +# fmt: off +PM1_CLUSTER_RASOPATHY: Dict[str, Dict[str, List[Union[int, Tuple[int, int]]]]] = { + "HGNC:7989": { # NRAS + "domains": [(10, 17), (25, 40), (57, 64), (145, 156)], # P-loop, SW1, SW2, SAK + }, + "HGNC:9829": { # RAF1 + "domains": [(251, 266)], # CR2 domain in exon 7 + "exons": [14, 17], # Critical exons + }, + "HGNC:11187": { # SOS1 + "domains": [(420, 500)], # PH domain + }, + "HGNC:11188": { # SOS2 + "domains": [(418, 498)], # PH domain + }, + "HGNC:9644": { # PTPN11 + "domains": [(4, 4), (7, 9), (58, 63), (69, 77), (247, 247), (251, 251), + (255, 256), (258, 258), (261, 261), (265, 265), + (278, 281), (284, 284)], # Critical N-SH2 and PTPN domain residues + }, + "HGNC:6407": { # KRAS + "domains": [(10, 17), (25, 40), (57, 64), (145, 156)], # P-loop, SW1, SW2, SAK + }, + "HGNC:6840": { # MAP2K1 + "domains": [(43, 61), (124, 134)], # Specific domains + }, + "HGNC:5173": { # HRAS + "domains": [(10, 17), (25, 40), (57, 64), (145, 156)], # P-loop, SW1, SW2, SAK + }, + "HGNC:10023": { # RIT1 + "domains": [(28, 35), (43, 58), (75, 82)], # P-loop, SW1, SW2 + }, + "HGNC:6842": { # MAP2K2 + "domains": [(47, 65), (128, 138)], # Specific domains + }, + "HGNC:1097": { # BRAF + "domains": [(459, 474), (594, 627)], # P-loop, CR3 activation segment + "exons": [6, 11], # Critical exons + }, + "HGNC:7227": { # MRAS + "domains": [(20, 27), (35, 50), (67, 74)], # P-loop, SW1, SW2 + }, + "HGNC:17271": { # RRAS2 + "domains": [(21, 28), (36, 51), (68, 75)], # P-loop, SW1, SW2 + }, +} + + +class RASopathyPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for RASopathy. + """ + logger.info("Predict PM1") + + if var_data.hgnc_id in [ + "HGNC:15454", # SHOC2 + "HGNC:6742", # LZTR1 + "HGNC:9282", # PPP1CB + ]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicSupporting, + summary=f"PM1 is not applicable for {var_data.hgnc_id}.", + ) + + gene_info = PM1_CLUSTER_RASOPATHY.get(var_data.hgnc_id, None) + if not gene_info: + return super().predict_pm1(seqvar, var_data) + + # Check if the variant falls within a critical region + for start, end in gene_info.get("domains", []): # type: ignore + if start <= var_data.prot_pos <= end: + comment = ( + f"Variant affects a critical residue in {var_data.hgnc_id} " + f"within positions {start}-{end}. PM1 is met." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # Check if the variant is in a critical exon + affected_exon = self._get_affected_exon(var_data, seqvar) + if affected_exon in gene_info.get("exons", []): + comment = ( + f"Variant affects a critical exon {affected_exon} in {var_data.hgnc_id}. PM1 is met." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary=f"Variant does not meet the PM1 criteria for {var_data.hgnc_id}.", + ) diff --git a/src/vcep/rett_angelman.py b/src/vcep/rett_angelman.py new file mode 100644 index 0000000..ba83e7a --- /dev/null +++ b/src/vcep/rett_angelman.py @@ -0,0 +1,87 @@ +""" +Predictor for Rett and Angelman-like Disorders VCEP. +Included genes: +TCF4 (HGNC:11634), +SLC9A6 (HGNC:11079), +CDKL5 (HGNC:11411), +FOXG1 (HGNC:3811), +MECP2 (HGNC:6990), +UBE3A (HGNC:12496). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN032 +https://cspec.genome.network/cspec/ui/svi/doc/GN033 +https://cspec.genome.network/cspec/ui/svi/doc/GN034 +https://cspec.genome.network/cspec/ui/svi/doc/GN035 +https://cspec.genome.network/cspec/ui/svi/doc/GN036 +https://cspec.genome.network/cspec/ui/svi/doc/GN037 +""" + +from typing import Dict, List, Tuple, Union + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +# fmt: off +PM1_CLUSTER: Dict[str, Dict[str, List[Tuple[int, int]]]] = { + "HGNC:11634": { # TCF4 + "domains": [(564, 617)], # Basic Helix-Loop-Helix (bHLH) domain + }, + "HGNC:11411": { # CDKL5 + "domains": [(19, 43), (169, 171)], # ATP binding region, TEY phosphorylation site + }, + "HGNC:3811": { # FOXG1 + "domains": [(181, 275)], # Forkhead domain + }, + "HGNC:6990": { # MECP2 + "domains": [(90, 162), (302, 306)], # Methyl-DNA binding (MBD), Transcriptional repression domain (TRD) + }, + "HGNC:12496": { # UBE3A + "domains": [(820, 820)], # 3’ cysteine binding site + }, +} + + +class RettAngelmanPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Rett and Angelman-like Disorders. + """ + logger.info("Predict PM1") + + # PM1 is not applicable for SLC9A6 + if var_data.hgnc_id == "HGNC:11079": + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicModerate, + summary=f"PM1 is not applicable for SLC9A6.", + ) + + gene_info = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_info: + return super().predict_pm1(seqvar, var_data) + + # Check if the variant falls within a critical region + for start, end in gene_info.get("domains", []): + if start <= var_data.prot_pos <= end: + comment = ( + f"Variant affects a critical residue in {var_data.hgnc_id} " + f"within positions {start}-{end}. PM1 is met." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary=f"Variant does not meet the PM1 criteria for {var_data.hgnc_id}.", + ) diff --git a/src/vcep/scid.py b/src/vcep/scid.py new file mode 100644 index 0000000..46088c5 --- /dev/null +++ b/src/vcep/scid.py @@ -0,0 +1,142 @@ +""" +Predictor for Severe Combined Immunodeficiency Disease VCEP. +Included genes: +FOXN1 (HGNC:12765), +ADA (HGNC:186), +DCLRE1C (HGNC:17642), +IL7R (HGNC:6024), +JAK3 (HGNC:6193), +RAG1 (HGNC:9831), +RAG2 (HGNC:9832), +IL2RG (HGNC:6010). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN113 +https://cspec.genome.network/cspec/ui/svi/doc/GN114 +https://cspec.genome.network/cspec/ui/svi/doc/GN116 +https://cspec.genome.network/cspec/ui/svi/doc/GN119 +https://cspec.genome.network/cspec/ui/svi/doc/GN121 +https://cspec.genome.network/cspec/ui/svi/doc/GN123 +https://cspec.genome.network/cspec/ui/svi/doc/GN124 +https://cspec.genome.network/cspec/ui/svi/doc/GN129 +""" + +from typing import Dict, List, Tuple, Union + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +# fmt: off +PM1_CLUSTER_SCID: Dict[str, Dict[str, List[Union[int, Tuple[int, int]]]]] = { + "HGNC:12765": { # FOXN1 + "domains": [(270, 367)], # DNA binding forkhead domain + }, + "HGNC:6193": { # JAK3 + "domains": [(651, 759)], # JH2 domain residues R651W and C759R + }, + "HGNC:9831": { # RAG1 + "domains": [(394, 460), (461, 517)], # NBD domain, DDBD domain + "supporting_domains": [(387, 1011)], # Core domain (supporting strength) + }, + "HGNC:9832": { # RAG2 + "domains": [(414, 487)], # PHD domain + "supporting_domains": [(1, 383)], # Core domain (supporting strength) + }, + "HGNC:6010": { # IL2RG + "strong_residues": [ + 62, 72, 102, 115, # Conserved cysteine residues + 224, 226, 691, 285, # CpG dinucleotides + 237, 238, 239, 240, 241, # WSxWS motif + ], + "strong_domains": [(263, 283)], # Transmembrane domain + }, +} + + +class SCIDPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Severe Combined Immunodeficiency Disease. + """ + logger.info("Predict PM1") + + if var_data.hgnc_id in [ + "HGNC:186", # ADA + "HGNC:17642", # DCLRE1C + "HGNC:6024", # IL7R + ]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicSupporting, + summary=f"PM1 is not applicable for {var_data.hgnc_id}.", + ) + + gene_info = PM1_CLUSTER_SCID.get(var_data.hgnc_id, None) + if not gene_info: + return super().predict_pm1(seqvar, var_data) + + # Check if the variant falls within a critical region + for start, end in gene_info.get("domains", []): # type: ignore + if start <= var_data.prot_pos <= end: + comment = ( + f"Variant affects a critical residue in {var_data.hgnc_id} " + f"within positions {start}-{end}. PM1 is met." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + # Check if the variant falls within a supporting region + for start, end in gene_info.get("supporting_domains", []): # type: ignore + if start <= var_data.prot_pos <= end: + comment = ( + f"Variant affects a residue in the supporting region of {var_data.hgnc_id} " + f"within positions {start}-{end}. PM1 is met at a supporting level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicSupporting, + summary=comment, + ) + + # Check if the variant affects a strong residue + if var_data.prot_pos in gene_info.get("strong_residues", []): + comment = ( + f"Variant affects a strong residue in {var_data.hgnc_id} " + f"at position {var_data.prot_pos}. PM1 is met." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicStrong, + summary=comment, + ) + + # Check if the variant falls within a strong domain + for start, end in gene_info.get("strong_domains", []): # type: ignore + if start <= var_data.prot_pos <= end: + comment = ( + f"Variant affects a residue in the strong domain of {var_data.hgnc_id} " + f"within positions {start}-{end}. PM1 is met at a strong level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicStrong, + summary=comment, + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary=f"Variant does not meet the PM1 criteria for {var_data.hgnc_id}.", + ) diff --git a/src/vcep/thrombosis.py b/src/vcep/thrombosis.py new file mode 100644 index 0000000..d8cc2ac --- /dev/null +++ b/src/vcep/thrombosis.py @@ -0,0 +1,54 @@ +""" +Predictor for Thrombosis VCEP. +Included gene: SERPINC1 (HGNC:775). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN084 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +# fmt: off +PM1_CLUSTER = { + "HGNC:775": { # SERPINC1 + "residues": [ + # Cysteine residues involved in disulfide bridges + 40, 53, 127, 160, 279, 462, + # Heparin binding site residues + 39, 56, 73, 79, + # Reactive site residues + 414, 416, + ], + } +} + + +class ThrombosisPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for Thrombosis. + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check if variant is in the cluster + if var_data.prot_pos in gene_cluster["residues"]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant meets the PM1 criteria for SERPINC1.", + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not meet the PM1 criteria for SERPINC1.", + ) diff --git a/src/vcep/tp53.py b/src/vcep/tp53.py new file mode 100644 index 0000000..0e940ed --- /dev/null +++ b/src/vcep/tp53.py @@ -0,0 +1,49 @@ +""" +Predictor for TP53 VCEP. +Included gene: TP53 (HGNC:11998). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN009 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +# Define the critical regions for PM1 consideration in TP53 +PM1_CLUSTER = { + # "NM_000546.4": { + "HGNC:11998": { + "residues": [175, 245, 248, 249, 273, 282], + } +} + + +class TP53Predictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for TP53. + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check if variant is in the moderate cluster + if var_data.prot_pos in gene_cluster["residues"]: + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=f"Variant affects a critical residue in TP53 at position {var_data.prot_pos}. " + f"PM1 is met at the Moderate level.", + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not meet the PM1 criteria for TP53.", + ) diff --git a/src/vcep/vhl.py b/src/vcep/vhl.py new file mode 100644 index 0000000..2663a65 --- /dev/null +++ b/src/vcep/vhl.py @@ -0,0 +1,60 @@ +""" +Predictor for VHL VCEP. +Included gene: VHL (HGNC:12687). +Link: https://cspec.genome.network/cspec/ui/svi/doc/GN078 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + +# fmt: off +PM1_CLUSTER = { + # "NM_000551.3": { + "HGNC:12687": { + "residues": [ + # Stebbins er al (PMID: 10205047) + 167, 162, 178, 98, 78, 86, + # Chiorean et al (PMID: 35475554) + 65, 76, 78, 80, 86, 88, 96, 98, 112, 117, + 161, 162, 167, 170, 176, + # Walsh et al (PMIDs: 29247016, 30311369) + 68, 74, 89, 111, 114, 115, 121, 135, 151, 158, 169 + ] + } +} + + +class VHLPredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for VHL. + """ + logger.info("Predict PM1") + + gene_cluster = PM1_CLUSTER.get(var_data.hgnc_id, None) + if not gene_cluster: + return super().predict_pm1(seqvar, var_data) + + # Check if variant is in the moderate cluster + if var_data.prot_pos in gene_cluster["residues"]: + comment = ( + f"Variant affects a germline hotspot or key functional domain in VHL " + f"at position {var_data.prot_pos}. PM1 is met at the Moderate level." + ) + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.Met, + strength=AutoACMGStrength.PathogenicModerate, + summary=comment, + ) + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Variant does not meet the PM1 criteria for VHL.", + ) diff --git a/src/vcep/von_willebrand_disease.py b/src/vcep/von_willebrand_disease.py new file mode 100644 index 0000000..82e86de --- /dev/null +++ b/src/vcep/von_willebrand_disease.py @@ -0,0 +1,29 @@ +""" +Predictor for von Willebrand Disease VCEP. +Included gene: VWF (HGNC:12726). +Links: +https://cspec.genome.network/cspec/ui/svi/doc/GN081 +https://cspec.genome.network/cspec/ui/svi/doc/GN090 +""" + +from loguru import logger + +from src.criteria.default_predictor import DefaultPredictor +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.seqvar import SeqVar + + +class VonWillebrandDiseasePredictor(DefaultPredictor): + + def predict_pm1(self, seqvar: SeqVar, var_data: AutoACMGData) -> AutoACMGCriteria: + """ + Override predict_pm1 to include VCEP-specific logic for von Willebrand Disease. + """ + logger.info("Predict PM1") + + return AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotApplicable, + strength=AutoACMGStrength.PathogenicSupporting, + summary=f"PM1 is not applicable for {var_data.hgnc_id}.", + ) diff --git a/tests/assets/integ/pm1.csv b/tests/assets/integ/pm1.csv index b9fc0e4..513fdb3 100644 --- a/tests/assets/integ/pm1.csv +++ b/tests/assets/integ/pm1.csv @@ -58,6 +58,5 @@ section,variant_name,genome_release,expected_prediction,comment ,NM_001754.4(RUNX1):c.314A>C,GRCh37,Met, ,NM_001754.4:c.315C>A,GRCh37,Met, ,NM_002834.4(PTPN11):c.782T>A,GRCh37,Met, -,NM_000546.5(TP53):c.396G>C,GRCh37,Met, ,NM_005343.4(HRAS):c.175_176delinsCT,GRCh37,Met, ,NM_001754.4(RUNX1):c.485G>A,GRCh37,Met, diff --git a/tests/criteria/test_auto_pm1.py b/tests/criteria/test_auto_pm1.py index 2f69595..5c31137 100644 --- a/tests/criteria/test_auto_pm1.py +++ b/tests/criteria/test_auto_pm1.py @@ -4,7 +4,13 @@ import tabix from src.criteria.auto_pm1 import AutoPM1 -from src.defs.auto_acmg import PM1, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.auto_acmg import ( + PM1, + AutoACMGData, + AutoACMGPrediction, + AutoACMGStrength, + GenomicStrand, +) from src.defs.exceptions import AlgorithmError, InvalidAPIResposeError from src.defs.genome_builds import GenomeRelease from src.defs.seqvar import SeqVar @@ -58,6 +64,83 @@ def auto_pm1_mocked(): yield pm1_instance, mocked_annonars +@pytest.fixture +def auto_acmg_data_plus(): + exons = [ + MagicMock(altStartI=10000, altEndI=20000), + MagicMock(altStartI=30000, altEndI=40000), + MagicMock(altStartI=50000, altEndI=60000), + ] + data = AutoACMGData() + data.exons = exons # type: ignore + data.strand = GenomicStrand.Plus + return data + + +@pytest.fixture +def auto_acmg_data_minus(): + exons = [ + MagicMock(altStartI=10000, altEndI=20000), + MagicMock(altStartI=30000, altEndI=40000), + MagicMock(altStartI=50000, altEndI=60000), + ] + data = AutoACMGData() + data.exons = exons # type: ignore + data.strand = GenomicStrand.Minus + return data + + +def test_get_affected_exon_plus_strand_in_exon(seqvar, auto_acmg_data_plus): + """Test for a variant within an exon on the plus strand.""" + seqvar.pos = 35000 # Position within the second exon + result = AutoPM1._get_affected_exon(auto_acmg_data_plus, seqvar) + assert result == 2, "Exon number should be 2 for a position within the second exon." + + +def test_get_affected_exon_plus_strand_before_exon(seqvar, auto_acmg_data_plus): + """Test for a variant before any exon on the plus strand.""" + seqvar.pos = 9000 # Position before the first exon + result = AutoPM1._get_affected_exon(auto_acmg_data_plus, seqvar) + assert result == 0, "Exon number should be 0 for a position before any exon." + + +def test_get_affected_exon_plus_strand_after_exon(seqvar, auto_acmg_data_plus): + """Test for a variant after the last exon on the plus strand.""" + seqvar.pos = 70000 # Position after the last exon + result = AutoPM1._get_affected_exon(auto_acmg_data_plus, seqvar) + assert result == 3, "Exon number should be 3 for a position after the last exon." + + +def test_get_affected_exon_minus_strand_in_exon(seqvar, auto_acmg_data_minus): + """Test for a variant within an exon on the minus strand.""" + seqvar.pos = 35000 # Position within the second exon (reverse order for minus strand) + result = AutoPM1._get_affected_exon(auto_acmg_data_minus, seqvar) + assert ( + result == 2 + ), "Exon number should be 2 for a position within the second exon (minus strand)." + + +def test_get_affected_exon_minus_strand_before_exon(seqvar, auto_acmg_data_minus): + """Test for a variant before any exon on the minus strand.""" + seqvar.pos = 65000 # Position before the first exon (reverse order for minus strand) + result = AutoPM1._get_affected_exon(auto_acmg_data_minus, seqvar) + assert result == 0, "Exon number should be 0 for a position before any exon (minus strand)." + + +def test_get_affected_exon_minus_strand_after_exon(seqvar, auto_acmg_data_minus): + """Test for a variant after the last exon on the minus strand.""" + seqvar.pos = 5000 # Position after the last exon (reverse order for minus strand) + result = AutoPM1._get_affected_exon(auto_acmg_data_minus, seqvar) + assert result == 3, "Exon number should be 3 for a position after the last exon (minus strand)." + + +def test_get_affected_exon_not_set_strand(seqvar, auto_acmg_data_plus): + """Test for an invalid strand scenario.""" + auto_acmg_data_plus.strand = GenomicStrand.NotSet + with pytest.raises(AlgorithmError): + AutoPM1._get_affected_exon(auto_acmg_data_plus, seqvar) + + @pytest.mark.skip(reason="Annonars is not mocked properly") def test_count_vars_success(auto_pm1_mocked, seqvar, mock_response): """Test counting pathogenic and benign variants successfully.""" diff --git a/tests/test_cli.py b/tests/test_cli.py index f94aaf7..06b933c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,6 @@ import io from typing import Type -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from loguru import logger @@ -29,7 +29,7 @@ def loguru_capture(): def mock_auto_acmg_predict_success(): """Fixture to mock AutoACMG predict method with a success response.""" with patch("src.auto_acmg.AutoACMG.predict") as mock_predict: - mock_predict.return_value = "Pathogenic" + mock_predict.return_value = MagicMock(save_to_file=MagicMock()) yield mock_predict diff --git a/tests/vcep/test_congenital_myopathies.py b/tests/vcep/test_congenital_myopathies.py new file mode 100644 index 0000000..fdcb130 --- /dev/null +++ b/tests/vcep/test_congenital_myopathies.py @@ -0,0 +1,136 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep import CongenitalMyopathiesPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="19", pos=100, delete="A", insert="T") + + +@pytest.fixture +def congenital_myopathies_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return CongenitalMyopathiesPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_in_critical_domain(congenital_myopathies_predictor, auto_acmg_data): + """Test when the variant falls within a critical domain for RYR1.""" + auto_acmg_data.prot_pos = 4850 # Within the critical domain (4800-4950) for RYR1 + auto_acmg_data.hgnc_id = "HGNC:10483" # RYR1 gene + result = congenital_myopathies_predictor.predict_pm1( + congenital_myopathies_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for a variant in a critical domain." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "falls within a critical domain" in result.summary + ), "The summary should indicate the critical domain." + + +def test_predict_pm1_outside_critical_domain(congenital_myopathies_predictor, auto_acmg_data): + """Test when the variant does not fall within any critical domain for RYR1.""" + auto_acmg_data.prot_pos = 5000 # Outside the critical domain (4800-4950) for RYR1 + auto_acmg_data.hgnc_id = "HGNC:10483" # RYR1 gene + result = congenital_myopathies_predictor.predict_pm1( + congenital_myopathies_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for a variant outside any critical domain." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "Variant does not fall within a critical domain." in result.summary + ), "The summary should indicate no critical domain." + + +def test_predict_pm1_not_applicable(congenital_myopathies_predictor, auto_acmg_data): + """Test when PM1 is not applicable for NEB, ACTA1, DNM2, MTM1.""" + auto_acmg_data.hgnc_id = "HGNC:7720" # NEB gene + result = congenital_myopathies_predictor.predict_pm1( + congenital_myopathies_predictor.seqvar, auto_acmg_data + ) + + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for NEB." + assert ( + "Not applicable for NEB" in result.summary + ), "The summary should indicate that PM1 is not applicable for NEB." + + +@patch("src.vcep.congenital_myopathies.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, congenital_myopathies_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method.""" + auto_acmg_data.hgnc_id = "HGNC:99999" # Not in the PM1_CLUSTER mapping + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = congenital_myopathies_predictor.predict_pm1( + congenital_myopathies_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should be from the default fallback." + + +def test_predict_pm1_edge_case_start_boundary(congenital_myopathies_predictor, auto_acmg_data): + """Test when variant falls exactly on the start boundary of a critical domain.""" + auto_acmg_data.prot_pos = 4800 # Start boundary of the RYR1 critical domain (4800-4950) + auto_acmg_data.hgnc_id = "HGNC:10483" # RYR1 gene + result = congenital_myopathies_predictor.predict_pm1( + congenital_myopathies_predictor.seqvar, auto_acmg_data + ) + + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met when on the start boundary of a critical domain." + assert ( + "falls within a critical domain" in result.summary + ), "The summary should indicate the critical domain." + + +def test_predict_pm1_edge_case_end_boundary(congenital_myopathies_predictor, auto_acmg_data): + """Test when variant falls exactly on the end boundary of a critical domain.""" + auto_acmg_data.prot_pos = 4950 # End boundary of the RYR1 critical domain (4800-4950) + auto_acmg_data.hgnc_id = "HGNC:10483" # RYR1 gene + result = congenital_myopathies_predictor.predict_pm1( + congenital_myopathies_predictor.seqvar, auto_acmg_data + ) + + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met when on the end boundary of a critical domain." + assert ( + "falls within a critical domain" in result.summary + ), "The summary should indicate the critical domain." diff --git a/tests/vcep/test_dicer1.py b/tests/vcep/test_dicer1.py new file mode 100644 index 0000000..d53919b --- /dev/null +++ b/tests/vcep/test_dicer1.py @@ -0,0 +1,122 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep import DICER1Predictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="14", pos=100, delete="A", insert="T") + + +@pytest.fixture +def dicer1_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return DICER1Predictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_moderate_criteria_residue(dicer1_predictor, auto_acmg_data): + """Test when the variant affects a metal ion-binding residue in DICER1.""" + auto_acmg_data.prot_pos = 1705 # Metal ion-binding residue + auto_acmg_data.hgnc_id = "HGNC:17098" # DICER1 gene + result = dicer1_predictor.predict_pm1(dicer1_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for metal ion-binding residues." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "affects a metal ion-binding residue" in result.summary + ), "The summary should indicate the metal ion-binding residue." + + +def test_predict_pm1_supporting_criteria_domain(dicer1_predictor, auto_acmg_data): + """Test when the variant affects a residue within the RNase IIIb domain but outside metal ion-binding residues.""" + auto_acmg_data.prot_pos = ( + 1720 # Within the RNase IIIb domain but not a metal ion-binding residue + ) + auto_acmg_data.hgnc_id = "HGNC:17098" # DICER1 gene + result = dicer1_predictor.predict_pm1(dicer1_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for residues within the RNase IIIb domain." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "affects a residue in the RNase IIIb domain" in result.summary + ), "The summary should indicate the RNase IIIb domain." + + +def test_predict_pm1_outside_critical_region(dicer1_predictor, auto_acmg_data): + """Test when the variant does not affect any critical residues or domains in DICER1.""" + auto_acmg_data.prot_pos = 1600 # Outside any critical region or domain + auto_acmg_data.hgnc_id = "HGNC:17098" # DICER1 gene + result = dicer1_predictor.predict_pm1(dicer1_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical regions." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "does not affect a critical domain" in result.summary + ), "The summary should indicate no critical domain was affected." + + +def test_predict_pm1_not_applicable_gene(dicer1_predictor, auto_acmg_data): + """Test when PM1 is not applicable because the gene is not DICER1.""" + auto_acmg_data.hgnc_id = "HGNC:99999" # Gene not in PM1_CLUSTER mapping + result = dicer1_predictor.predict_pm1(dicer1_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met if gene is not in PM1_CLUSTER." + assert ( + "does not affect a critical domain for DICER1" in result.summary + ), "The summary should indicate gene is not applicable." + + +def test_predict_pm1_edge_case_start_boundary_moderate(dicer1_predictor, auto_acmg_data): + """Test when the variant falls exactly on the start boundary of a moderate criteria residue.""" + auto_acmg_data.prot_pos = 1344 # Start boundary of a metal ion-binding residue + auto_acmg_data.hgnc_id = "HGNC:17098" # DICER1 gene + result = dicer1_predictor.predict_pm1(dicer1_predictor.seqvar, auto_acmg_data) + + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for the start boundary of a metal ion-binding residue." + assert ( + "affects a metal ion-binding residue" in result.summary + ), "The summary should indicate the metal ion-binding residue." + + +def test_predict_pm1_edge_case_end_boundary_supporting(dicer1_predictor, auto_acmg_data): + """Test when the variant falls exactly on the end boundary of a supporting criteria domain.""" + auto_acmg_data.prot_pos = 1846 # End boundary of the RNase IIIb domain + auto_acmg_data.hgnc_id = "HGNC:17098" # DICER1 gene + result = dicer1_predictor.predict_pm1(dicer1_predictor.seqvar, auto_acmg_data) + + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for the end boundary of the RNase IIIb domain." + assert ( + "affects a residue in the RNase IIIb domain" in result.summary + ), "The summary should indicate the RNase IIIb domain." diff --git a/tests/vcep/test_enigma.py b/tests/vcep/test_enigma.py new file mode 100644 index 0000000..35d07ab --- /dev/null +++ b/tests/vcep/test_enigma.py @@ -0,0 +1,91 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep import ENIGMAPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="17", pos=100, delete="A", insert="T") + + +@pytest.fixture +def enigma_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return ENIGMAPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_not_applicable_brca1(enigma_predictor, auto_acmg_data): + """Test when PM1 is not applicable for BRCA1.""" + auto_acmg_data.hgnc_id = "HGNC:1100" # BRCA1 gene + result = enigma_predictor.predict_pm1(enigma_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for BRCA1." + assert ( + result.summary == "PM1 is not applicable for BRCA1 and BRCA2." + ), "The summary should indicate that PM1 is not applicable for BRCA1." + + +def test_predict_pm1_not_applicable_brca2(enigma_predictor, auto_acmg_data): + """Test when PM1 is not applicable for BRCA2.""" + auto_acmg_data.hgnc_id = "HGNC:1101" # BRCA2 gene + result = enigma_predictor.predict_pm1(enigma_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for BRCA2." + assert ( + result.summary == "PM1 is not applicable for BRCA1 and BRCA2." + ), "The summary should indicate that PM1 is not applicable for BRCA2." + + +@patch("src.vcep.enigma.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, enigma_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method for genes other than BRCA1 and BRCA2.""" + auto_acmg_data.hgnc_id = "HGNC:99999" # Gene not BRCA1 or BRCA2 + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = enigma_predictor.predict_pm1(enigma_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for genes other than BRCA1 and BRCA2." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should be from the default fallback." + + +def test_predict_pm1_name(enigma_predictor, auto_acmg_data): + """Test the name of the criteria returned by the ENIGMA predictor.""" + auto_acmg_data.hgnc_id = "HGNC:1100" # BRCA1 gene + result = enigma_predictor.predict_pm1(enigma_predictor.seqvar, auto_acmg_data) + + assert result.name == "PM1", "The name of the criteria should be 'PM1'." + + +def test_predict_pm1_strength(enigma_predictor, auto_acmg_data): + """Test the strength level returned by the ENIGMA predictor.""" + auto_acmg_data.hgnc_id = "HGNC:1101" # BRCA2 gene + result = enigma_predictor.predict_pm1(enigma_predictor.seqvar, auto_acmg_data) + + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate for ENIGMA." diff --git a/tests/vcep/test_epilepsy_sodium_channel.py b/tests/vcep/test_epilepsy_sodium_channel.py new file mode 100644 index 0000000..a3d4fdd --- /dev/null +++ b/tests/vcep/test_epilepsy_sodium_channel.py @@ -0,0 +1,134 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep import EpilepsySodiumChannelPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="2", pos=100, delete="A", insert="T") + + +@pytest.fixture +def epilepsy_sodium_channel_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return EpilepsySodiumChannelPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_not_applicable_scn1b(epilepsy_sodium_channel_predictor, auto_acmg_data): + """Test when PM1 is not applicable for SCN1B.""" + auto_acmg_data.hgnc_id = "HGNC:10586" # SCN1B gene + result = epilepsy_sodium_channel_predictor.predict_pm1( + epilepsy_sodium_channel_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for SCN1B." + assert ( + result.summary == "PM1 is not applicable for SCN1B." + ), "The summary should indicate that PM1 is not applicable for SCN1B." + + +def test_predict_pm1_in_critical_region(epilepsy_sodium_channel_predictor, auto_acmg_data): + """Test when variant falls within a critical region for SCN1A.""" + auto_acmg_data.hgnc_id = "HGNC:10585" # SCN1A gene + auto_acmg_data.prot_pos = 240 # Within the critical region (226-246) + result = epilepsy_sodium_channel_predictor.predict_pm1( + epilepsy_sodium_channel_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for a variant in a critical region." + assert ( + result.summary + == "Variant falls within a critical residue region for HGNC:10585 between positions 226-246. PM1 is met." + ), "The summary should indicate the critical region." + + +def test_predict_pm1_outside_critical_region(epilepsy_sodium_channel_predictor, auto_acmg_data): + """Test when variant does not fall within any critical region for SCN1A.""" + auto_acmg_data.hgnc_id = "HGNC:10585" # SCN1A gene + auto_acmg_data.prot_pos = 300 # Outside the critical region + result = epilepsy_sodium_channel_predictor.predict_pm1( + epilepsy_sodium_channel_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for a variant outside any critical region." + assert ( + result.summary == "Variant does not meet the PM1 criteria for HGNC:10585." + ), "The summary should indicate no critical region was met." + + +@patch("src.vcep.epilepsy_sodium_channel.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, epilepsy_sodium_channel_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method for genes not in PM1_CLUSTER.""" + auto_acmg_data.hgnc_id = "HGNC:99999" # Gene not in PM1_CLUSTER + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = epilepsy_sodium_channel_predictor.predict_pm1( + epilepsy_sodium_channel_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met if the gene is not in the PM1_CLUSTER." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should be from the default fallback." + + +def test_predict_pm1_edge_case_start_boundary(epilepsy_sodium_channel_predictor, auto_acmg_data): + """Test when variant falls exactly on the start boundary of a critical region.""" + auto_acmg_data.hgnc_id = "HGNC:10585" # SCN1A gene + auto_acmg_data.prot_pos = 226 # Start boundary of the critical region (226-246) + result = epilepsy_sodium_channel_predictor.predict_pm1( + epilepsy_sodium_channel_predictor.seqvar, auto_acmg_data + ) + + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met when on the start boundary of a critical region." + assert ( + result.summary + == "Variant falls within a critical residue region for HGNC:10585 between positions 226-246. PM1 is met." + ), "The summary should indicate the critical region." + + +def test_predict_pm1_edge_case_end_boundary(epilepsy_sodium_channel_predictor, auto_acmg_data): + """Test when variant falls exactly on the end boundary of a critical region.""" + auto_acmg_data.hgnc_id = "HGNC:10585" # SCN1A gene + auto_acmg_data.prot_pos = 246 # End boundary of the critical region (226-246) + result = epilepsy_sodium_channel_predictor.predict_pm1( + epilepsy_sodium_channel_predictor.seqvar, auto_acmg_data + ) + + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met when on the end boundary of a critical region." + assert ( + result.summary + == "Variant falls within a critical residue region for HGNC:10585 between positions 226-246. PM1 is met." + ), "The summary should indicate the critical region." diff --git a/tests/vcep/test_familial_hypercholesterolemia.py b/tests/vcep/test_familial_hypercholesterolemia.py new file mode 100644 index 0000000..e0f7c7f --- /dev/null +++ b/tests/vcep/test_familial_hypercholesterolemia.py @@ -0,0 +1,139 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep import FamilialHypercholesterolemiaPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="19", pos=100, delete="A", insert="T") + + +@pytest.fixture +def familial_hypercholesterolemia_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return FamilialHypercholesterolemiaPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_in_critical_residue(familial_hypercholesterolemia_predictor, auto_acmg_data): + """Test when variant affects a critical residue in LDLR.""" + auto_acmg_data.hgnc_id = "HGNC:6547" # LDLR gene + auto_acmg_data.prot_pos = 75 # Critical residue in LDLR + result = familial_hypercholesterolemia_predictor.predict_pm1( + familial_hypercholesterolemia_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for a variant affecting a critical residue." + assert ( + "affects a critical cysteine residue" in result.summary + ), "The summary should indicate the critical residue." + + +def test_predict_pm1_in_exon_4(familial_hypercholesterolemia_predictor, auto_acmg_data): + """Test when variant affects the 4th exon in LDLR.""" + auto_acmg_data.hgnc_id = "HGNC:6547" # LDLR gene + with patch.object(FamilialHypercholesterolemiaPredictor, "_get_affected_exon", return_value=4): + result = familial_hypercholesterolemia_predictor.predict_pm1( + familial_hypercholesterolemia_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for a variant affecting the 4th exon." + assert ( + "affects the 4th exon" in result.summary + ), "The summary should indicate the affected exon." + + +def test_predict_pm1_outside_critical_region( + familial_hypercholesterolemia_predictor, auto_acmg_data +): + """Test when variant does not fall within any critical region or exon 4 for LDLR.""" + auto_acmg_data.hgnc_id = "HGNC:6547" # LDLR gene + auto_acmg_data.prot_pos = 500 # Position outside critical residues and not in exon 4 + with patch.object(FamilialHypercholesterolemiaPredictor, "_get_affected_exon", return_value=5): + result = familial_hypercholesterolemia_predictor.predict_pm1( + familial_hypercholesterolemia_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for a variant outside critical residues and exon 4." + assert ( + "Variant does not meet the PM1 criteria for LDLR" in result.summary + ), "The summary should indicate that no criteria were met." + + +@patch("src.vcep.familial_hypercholesterolemia.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, familial_hypercholesterolemia_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method for genes not in PM1_CLUSTER_LDLR.""" + auto_acmg_data.hgnc_id = "HGNC:99999" # Gene not in PM1_CLUSTER_LDLR + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = familial_hypercholesterolemia_predictor.predict_pm1( + familial_hypercholesterolemia_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met if the gene is not in the PM1_CLUSTER_LDLR." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should be from the default fallback." + + +def test_predict_pm1_edge_case_start_boundary( + familial_hypercholesterolemia_predictor, auto_acmg_data +): + """Test when variant falls exactly on the start boundary of a critical residue.""" + auto_acmg_data.hgnc_id = "HGNC:6547" # LDLR gene + auto_acmg_data.prot_pos = 27 # Start boundary of the critical residues (27) + result = familial_hypercholesterolemia_predictor.predict_pm1( + familial_hypercholesterolemia_predictor.seqvar, auto_acmg_data + ) + + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met when on the start boundary of a critical residue." + assert ( + "affects a critical cysteine residue" in result.summary + ), "The summary should indicate the critical residue." + + +def test_predict_pm1_edge_case_end_boundary( + familial_hypercholesterolemia_predictor, auto_acmg_data +): + """Test when variant falls exactly on the end boundary of a critical residue.""" + auto_acmg_data.hgnc_id = "HGNC:6547" # LDLR gene + auto_acmg_data.prot_pos = 711 # End boundary of the critical residues (711) + result = familial_hypercholesterolemia_predictor.predict_pm1( + familial_hypercholesterolemia_predictor.seqvar, auto_acmg_data + ) + + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met when on the end boundary of a critical residue." + assert ( + "affects a critical cysteine residue" in result.summary + ), "The summary should indicate the critical residue." diff --git a/tests/vcep/test_fbn1.py b/tests/vcep/test_fbn1.py new file mode 100644 index 0000000..c38d368 --- /dev/null +++ b/tests/vcep/test_fbn1.py @@ -0,0 +1,130 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep import FBN1Predictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="15", pos=100, delete="A", insert="T") + + +@pytest.fixture +def fbn1_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return FBN1Predictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_strong_residue(fbn1_predictor, auto_acmg_data): + """Test when variant affects a strong critical residue in FBN1.""" + auto_acmg_data.hgnc_id = "HGNC:3603" # FBN1 gene + auto_acmg_data.prot_pos = 250 # Strong residue in FBN1 + result = fbn1_predictor.predict_pm1(fbn1_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for a variant affecting a strong critical residue." + assert ( + result.strength == AutoACMGStrength.PathogenicStrong + ), "The strength should be PathogenicStrong for strong residues." + assert ( + "affects a critical cysteine residue" in result.summary + ), "The summary should indicate the strong critical residue." + + +def test_predict_pm1_moderate_residue(fbn1_predictor, auto_acmg_data): + """Test when variant affects a moderate critical residue in FBN1.""" + auto_acmg_data.hgnc_id = "HGNC:3603" # FBN1 gene + auto_acmg_data.prot_pos = 100 # Moderate residue in FBN1 + result = fbn1_predictor.predict_pm1(fbn1_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for a variant affecting a moderate critical residue." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate for moderate residues." + assert ( + "Variant affects a residue in FBN1" in result.summary + ), "The summary should indicate the moderate critical residue." + + +def test_predict_pm1_outside_critical_residues(fbn1_predictor, auto_acmg_data): + """Test when variant does not fall within any critical residues in FBN1.""" + auto_acmg_data.hgnc_id = "HGNC:3603" # FBN1 gene + auto_acmg_data.prot_pos = 5000 # Position outside critical residues + result = fbn1_predictor.predict_pm1(fbn1_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for a variant outside critical residues." + assert ( + "Variant does not meet the PM1 criteria for FBN1" in result.summary + ), "The summary should indicate no criteria were met." + + +@patch("src.vcep.fbn1.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, fbn1_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method for genes not in PM1_CLUSTER.""" + auto_acmg_data.hgnc_id = "HGNC:99999" # Gene not in PM1_CLUSTER + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = fbn1_predictor.predict_pm1(fbn1_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met if the gene is not in the PM1_CLUSTER." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should be from the default fallback." + + +def test_predict_pm1_edge_case_strong_boundary(fbn1_predictor, auto_acmg_data): + """Test when variant falls exactly on the boundary of a strong critical residue.""" + auto_acmg_data.hgnc_id = "HGNC:3603" # FBN1 gene + auto_acmg_data.prot_pos = 2686 # Boundary of a strong critical residue in FBN1 + result = fbn1_predictor.predict_pm1(fbn1_predictor.seqvar, auto_acmg_data) + + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met when on the boundary of a strong critical residue." + assert ( + result.strength == AutoACMGStrength.PathogenicStrong + ), "The strength should be PathogenicStrong for strong residues." + assert ( + "affects a critical cysteine residue" in result.summary + ), "The summary should indicate the strong critical residue." + + +def test_predict_pm1_edge_case_moderate_boundary(fbn1_predictor, auto_acmg_data): + """Test when variant falls exactly on the boundary of a moderate critical residue.""" + auto_acmg_data.hgnc_id = "HGNC:3603" # FBN1 gene + auto_acmg_data.prot_pos = 2390 # Boundary of a moderate critical residue in FBN1 + result = fbn1_predictor.predict_pm1(fbn1_predictor.seqvar, auto_acmg_data) + + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met when on the boundary of a moderate critical residue." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate for moderate residues." + assert ( + "Variant affects a residue in FBN1" in result.summary + ), "The summary should indicate the moderate critical residue." diff --git a/tests/vcep/test_glaucoma.py b/tests/vcep/test_glaucoma.py new file mode 100644 index 0000000..7fca4c9 --- /dev/null +++ b/tests/vcep/test_glaucoma.py @@ -0,0 +1,59 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep import GlaucomaPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="1", pos=100, delete="A", insert="T") + + +@pytest.fixture +def glaucoma_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return GlaucomaPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_not_applicable(glaucoma_predictor, auto_acmg_data): + """Test when PM1 is not applicable for MYOC in Glaucoma.""" + auto_acmg_data.hgnc_id = "HGNC:7610" # MYOC gene + result = glaucoma_predictor.predict_pm1(glaucoma_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for MYOC." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting for MYOC." + assert ( + "PM1 is not applicable for MYOC" in result.summary + ), "The summary should indicate that PM1 is not applicable for MYOC." + + +def test_predict_pm1_strength_level(glaucoma_predictor, auto_acmg_data): + """Test the strength level for PM1 when not applicable.""" + auto_acmg_data.hgnc_id = "HGNC:7610" # MYOC gene + result = glaucoma_predictor.predict_pm1(glaucoma_predictor.seqvar, auto_acmg_data) + + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting when PM1 is not applicable." + + +def test_predict_pm1_name(glaucoma_predictor, auto_acmg_data): + """Test the name of the criteria returned by the Glaucoma predictor.""" + auto_acmg_data.hgnc_id = "HGNC:7610" # MYOC gene + result = glaucoma_predictor.predict_pm1(glaucoma_predictor.seqvar, auto_acmg_data) + + assert result.name == "PM1", "The name of the criteria should be 'PM1'." diff --git a/tests/vcep/test_hbopc.py b/tests/vcep/test_hbopc.py new file mode 100644 index 0000000..f0f35da --- /dev/null +++ b/tests/vcep/test_hbopc.py @@ -0,0 +1,97 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep import HBOPCPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="1", pos=100, delete="A", insert="T") + + +@pytest.fixture +def hbopc_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return HBOPCPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_not_applicable(hbopc_predictor, auto_acmg_data): + """Test when PM1 is not applicable for ATM and PALB2 in Hereditary Breast, Ovarian, and Pancreatic Cancer.""" + auto_acmg_data.hgnc_id = "HGNC:795" # ATM gene + result = hbopc_predictor.predict_pm1(hbopc_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for ATM." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting for ATM." + assert ( + "PM1 is not applicable for HGNC:795" in result.summary + ), "The summary should indicate that PM1 is not applicable for ATM." + + +def test_predict_pm1_not_applicable_palb2(hbopc_predictor, auto_acmg_data): + """Test when PM1 is not applicable for PALB2 in Hereditary Breast, Ovarian, and Pancreatic Cancer.""" + auto_acmg_data.hgnc_id = "HGNC:26144" # PALB2 gene + result = hbopc_predictor.predict_pm1(hbopc_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for PALB2." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting for PALB2." + assert ( + "PM1 is not applicable for HGNC:26144" in result.summary + ), "The summary should indicate that PM1 is not applicable for PALB2." + + +@patch("src.vcep.hbopc.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, hbopc_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method if logic changes.""" + auto_acmg_data.hgnc_id = "HGNC:111111" # Gene not in the specific logic + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = hbopc_predictor.predict_pm1(hbopc_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met if the gene is not ATM or PALB2." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." + + +def test_predict_pm1_strength_level(hbopc_predictor, auto_acmg_data): + """Test the strength level for PM1 when not applicable.""" + auto_acmg_data.hgnc_id = "HGNC:795" # ATM gene + result = hbopc_predictor.predict_pm1(hbopc_predictor.seqvar, auto_acmg_data) + + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting when PM1 is not applicable." + + +def test_predict_pm1_name(hbopc_predictor, auto_acmg_data): + """Test the name of the criteria returned by the HBOPC predictor.""" + auto_acmg_data.hgnc_id = "HGNC:795" # ATM gene + result = hbopc_predictor.predict_pm1(hbopc_predictor.seqvar, auto_acmg_data) + + assert result.name == "PM1", "The name of the criteria should be 'PM1'." diff --git a/tests/vcep/test_hearing_loss.py b/tests/vcep/test_hearing_loss.py new file mode 100644 index 0000000..be94cd4 --- /dev/null +++ b/tests/vcep/test_hearing_loss.py @@ -0,0 +1,120 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep import HearingLossPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="1", pos=100, delete="A", insert="T") + + +@pytest.fixture +def hearing_loss_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return HearingLossPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_kcnq4_strong(hearing_loss_predictor, auto_acmg_data): + """Test when PM1 is met at the Strong level for a variant in KCNQ4.""" + auto_acmg_data.hgnc_id = "HGNC:6298" # KCNQ4 gene + auto_acmg_data.prot_pos = 280 # Within the critical pore-forming intramembrane region + result = hearing_loss_predictor.predict_pm1(hearing_loss_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met at the Strong level." + assert ( + result.strength == AutoACMGStrength.PathogenicStrong + ), "The strength should be PathogenicStrong." + assert ( + "critical pore-forming intramembrane region" in result.summary + ), "The summary should indicate the critical region." + + +def test_predict_pm1_not_applicable(hearing_loss_predictor, auto_acmg_data): + """Test when PM1 is not applicable for other hearing loss genes.""" + auto_acmg_data.hgnc_id = "HGNC:4284" # GJB2 gene + result = hearing_loss_predictor.predict_pm1(hearing_loss_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for GJB2." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "PM1 is not applicable" in result.summary + ), "The summary should indicate non-applicability." + + +def test_predict_pm1_not_met(hearing_loss_predictor, auto_acmg_data): + """Test when PM1 is not met for KCNQ4 but outside the critical region.""" + auto_acmg_data.hgnc_id = "HGNC:6298" # KCNQ4 gene + auto_acmg_data.prot_pos = 300 # Outside the critical pore-forming intramembrane region + result = hearing_loss_predictor.predict_pm1(hearing_loss_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.NotMet, "PM1 should not be met for KCNQ4." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "Variant does not meet the PM1 criteria" in result.summary + ), "The summary should indicate no criteria were met." + + +@patch("src.vcep.hearing_loss.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, hearing_loss_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method if logic changes.""" + auto_acmg_data.hgnc_id = "HGNC:111111" # Gene not in the specific logic + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = hearing_loss_predictor.predict_pm1(hearing_loss_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." + + +def test_predict_pm1_edge_case_start_boundary(hearing_loss_predictor, auto_acmg_data): + """Test when variant falls exactly on the start boundary of the critical region.""" + auto_acmg_data.hgnc_id = "HGNC:6298" # KCNQ4 gene + auto_acmg_data.prot_pos = ( + 271 # Start boundary of the critical pore-forming intramembrane region + ) + result = hearing_loss_predictor.predict_pm1(hearing_loss_predictor.seqvar, auto_acmg_data) + + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met on the start boundary." + assert ( + "critical pore-forming intramembrane region" in result.summary + ), "The summary should indicate the critical region." + + +def test_predict_pm1_edge_case_end_boundary(hearing_loss_predictor, auto_acmg_data): + """Test when variant falls exactly on the end boundary of the critical region.""" + auto_acmg_data.hgnc_id = "HGNC:6298" # KCNQ4 gene + auto_acmg_data.prot_pos = 292 # End boundary of the critical pore-forming intramembrane region + result = hearing_loss_predictor.predict_pm1(hearing_loss_predictor.seqvar, auto_acmg_data) + + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met on the end boundary." + assert ( + "critical pore-forming intramembrane region" in result.summary + ), "The summary should indicate the critical region." diff --git a/tests/vcep/test_hhtp.py b/tests/vcep/test_hhtp.py new file mode 100644 index 0000000..e3f3fed --- /dev/null +++ b/tests/vcep/test_hhtp.py @@ -0,0 +1,117 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep import HHTPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="12", pos=100, delete="A", insert="T") + + +@pytest.fixture +def hhtp_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return HHTPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_acvrl1_moderate(hhtp_predictor, auto_acmg_data): + """Test when PM1 is met at the Moderate level for a variant in ACVRL1.""" + auto_acmg_data.hgnc_id = "HGNC:175" # ACVRL1 gene + auto_acmg_data.prot_pos = 213 # Within the glycine-rich loop (209-216) + result = hhtp_predictor.predict_pm1(hhtp_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met at the Moderate level." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "critical residue for HGNC:175" in result.summary + ), "The summary should indicate the critical region." + + +def test_predict_pm1_eng_moderate(hhtp_predictor, auto_acmg_data): + """Test when PM1 is met at the Moderate level for a variant in ENG.""" + auto_acmg_data.hgnc_id = "HGNC:3349" # ENG gene + auto_acmg_data.prot_pos = 278 # BMP9 binding site residue + result = hhtp_predictor.predict_pm1(hhtp_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met at the Moderate level." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "critical residue for HGNC:3349" in result.summary + ), "The summary should indicate the critical region." + + +def test_predict_pm1_not_met(hhtp_predictor, auto_acmg_data): + """Test when PM1 is not met for ACVRL1 but outside the critical regions.""" + auto_acmg_data.hgnc_id = "HGNC:175" # ACVRL1 gene + auto_acmg_data.prot_pos = 250 # Outside the critical regions + result = hhtp_predictor.predict_pm1(hhtp_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.NotMet, "PM1 should not be met for ACVRL1." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "does not meet the PM1 criteria" in result.summary + ), "The summary should indicate no criteria were met." + + +@patch("src.vcep.hhtp.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, hhtp_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method if logic changes.""" + auto_acmg_data.hgnc_id = "HGNC:111111" # Gene not in the specific logic + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = hhtp_predictor.predict_pm1(hhtp_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." + + +def test_predict_pm1_edge_case_start_boundary_acvrl1(hhtp_predictor, auto_acmg_data): + """Test when variant falls exactly on the start boundary of a critical region in ACVRL1.""" + auto_acmg_data.hgnc_id = "HGNC:175" # ACVRL1 gene + auto_acmg_data.prot_pos = 209 # Start boundary of the glycine-rich loop (209-216) + result = hhtp_predictor.predict_pm1(hhtp_predictor.seqvar, auto_acmg_data) + + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met on the start boundary." + assert ( + "critical residue for HGNC:175" in result.summary + ), "The summary should indicate the critical region." + + +def test_predict_pm1_edge_case_end_boundary_eng(hhtp_predictor, auto_acmg_data): + """Test when variant falls exactly on the end boundary of a critical region in ENG.""" + auto_acmg_data.hgnc_id = "HGNC:3349" # ENG gene + auto_acmg_data.prot_pos = 412 # End boundary of the critical cysteine residues + result = hhtp_predictor.predict_pm1(hhtp_predictor.seqvar, auto_acmg_data) + + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met on the end boundary." + assert ( + "critical residue for HGNC:3349" in result.summary + ), "The summary should indicate the critical region." diff --git a/tests/vcep/test_insight_colorectal_cancer.py b/tests/vcep/test_insight_colorectal_cancer.py new file mode 100644 index 0000000..4eb9203 --- /dev/null +++ b/tests/vcep/test_insight_colorectal_cancer.py @@ -0,0 +1,144 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.insight_colorectal_cancer import InsightColorectalCancerPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="5", pos=100, delete="A", insert="T") + + +@pytest.fixture +def insight_colorectal_cancer_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return InsightColorectalCancerPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_not_applicable_apc(insight_colorectal_cancer_predictor, auto_acmg_data): + """Test when PM1 is not applicable for APC.""" + auto_acmg_data.hgnc_id = "HGNC:583" # APC gene + result = insight_colorectal_cancer_predictor.predict_pm1( + insight_colorectal_cancer_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for APC." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "PM1 is not applicable for HGNC:583" in result.summary + ), "The summary should indicate PM1 is not applicable." + + +def test_predict_pm1_not_applicable_mlh1(insight_colorectal_cancer_predictor, auto_acmg_data): + """Test when PM1 is not applicable for MLH1.""" + auto_acmg_data.hgnc_id = "HGNC:7127" # MLH1 gene + result = insight_colorectal_cancer_predictor.predict_pm1( + insight_colorectal_cancer_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for MLH1." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "PM1 is not applicable for HGNC:7127" in result.summary + ), "The summary should indicate PM1 is not applicable." + + +def test_predict_pm1_not_applicable_msh2(insight_colorectal_cancer_predictor, auto_acmg_data): + """Test when PM1 is not applicable for MSH2.""" + auto_acmg_data.hgnc_id = "HGNC:7325" # MSH2 gene + result = insight_colorectal_cancer_predictor.predict_pm1( + insight_colorectal_cancer_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for MSH2." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "PM1 is not applicable for HGNC:7325" in result.summary + ), "The summary should indicate PM1 is not applicable." + + +def test_predict_pm1_not_applicable_msh6(insight_colorectal_cancer_predictor, auto_acmg_data): + """Test when PM1 is not applicable for MSH6.""" + auto_acmg_data.hgnc_id = "HGNC:7329" # MSH6 gene + result = insight_colorectal_cancer_predictor.predict_pm1( + insight_colorectal_cancer_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for MSH6." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "PM1 is not applicable for HGNC:7329" in result.summary + ), "The summary should indicate PM1 is not applicable." + + +def test_predict_pm1_not_applicable_pms2(insight_colorectal_cancer_predictor, auto_acmg_data): + """Test when PM1 is not applicable for PMS2.""" + auto_acmg_data.hgnc_id = "HGNC:9122" # PMS2 gene + result = insight_colorectal_cancer_predictor.predict_pm1( + insight_colorectal_cancer_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be not applicable for PMS2." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "PM1 is not applicable for HGNC:9122" in result.summary + ), "The summary should indicate PM1 is not applicable." + + +@patch("src.vcep.insight_colorectal_cancer.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, insight_colorectal_cancer_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method if logic changes.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the specific logic + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = insight_colorectal_cancer_predictor.predict_pm1( + insight_colorectal_cancer_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_leber_congenital_amaurosis.py b/tests/vcep/test_leber_congenital_amaurosis.py new file mode 100644 index 0000000..b61aa1f --- /dev/null +++ b/tests/vcep/test_leber_congenital_amaurosis.py @@ -0,0 +1,109 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.leber_congenital_amaurosis import LeberCongenitalAmaurosisPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="1", pos=100, delete="A", insert="T") + + +@pytest.fixture +def leber_congenital_amaurosis_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return LeberCongenitalAmaurosisPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_met_for_critical_residue(leber_congenital_amaurosis_predictor, auto_acmg_data): + """Test when the variant falls within the critical residues for RPE65.""" + auto_acmg_data.prot_pos = 180 # Critical residue for RPE65 + auto_acmg_data.hgnc_id = "HGNC:10294" # RPE65 gene + result = leber_congenital_amaurosis_predictor.predict_pm1( + leber_congenital_amaurosis_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for critical residue variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "Variant affects a residue in RPE65 at position 180" in result.summary + ), "The summary should indicate the affected residue." + + +def test_predict_pm1_met_for_residues_range(leber_congenital_amaurosis_predictor, auto_acmg_data): + """Test when the variant falls within the residue range for RPE65.""" + auto_acmg_data.prot_pos = 110 # Within the residue range 107-125 for RPE65 + auto_acmg_data.hgnc_id = "HGNC:10294" # RPE65 gene + result = leber_congenital_amaurosis_predictor.predict_pm1( + leber_congenital_amaurosis_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for residues within the specified range." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "Variant affects a residue in RPE65 at position 110" in result.summary + ), "The summary should indicate the affected residue." + + +def test_predict_pm1_not_met(leber_congenital_amaurosis_predictor, auto_acmg_data): + """Test when the variant does not fall within any critical residues or ranges.""" + auto_acmg_data.prot_pos = 300 # Not within any critical residues or ranges + auto_acmg_data.hgnc_id = "HGNC:10294" # RPE65 gene + result = leber_congenital_amaurosis_predictor.predict_pm1( + leber_congenital_amaurosis_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical residue variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "Variant does not meet the PM1 criteria" in result.summary + ), "The summary should indicate the lack of criteria met." + + +@patch("src.vcep.leber_congenital_amaurosis.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, leber_congenital_amaurosis_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method if logic changes.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the specific logic + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = leber_congenital_amaurosis_predictor.predict_pm1( + leber_congenital_amaurosis_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_lysosomal_diseases.py b/tests/vcep/test_lysosomal_diseases.py new file mode 100644 index 0000000..1644b51 --- /dev/null +++ b/tests/vcep/test_lysosomal_diseases.py @@ -0,0 +1,89 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.lysosomal_diseases import LysosomalDiseasesPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="17", pos=100, delete="A", insert="T") + + +@pytest.fixture +def lysosomal_diseases_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return LysosomalDiseasesPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_met_for_critical_residue(lysosomal_diseases_predictor, auto_acmg_data): + """Test when the variant falls within the critical residues for GAA.""" + auto_acmg_data.prot_pos = 282 # Critical residue for GAA + auto_acmg_data.hgnc_id = "HGNC:4065" # GAA gene + result = lysosomal_diseases_predictor.predict_pm1( + lysosomal_diseases_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for critical residue variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "Variant affects a residue in GAA at position 282" in result.summary + ), "The summary should indicate the affected residue." + + +def test_predict_pm1_not_met(lysosomal_diseases_predictor, auto_acmg_data): + """Test when the variant does not fall within any critical residues for GAA.""" + auto_acmg_data.prot_pos = 700 # Not within any critical residues + auto_acmg_data.hgnc_id = "HGNC:4065" # GAA gene + result = lysosomal_diseases_predictor.predict_pm1( + lysosomal_diseases_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical residue variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "Variant does not meet the PM1 criteria" in result.summary + ), "The summary should indicate the lack of criteria met." + + +@patch("src.vcep.lysosomal_diseases.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, lysosomal_diseases_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method if logic changes.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the specific logic + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = lysosomal_diseases_predictor.predict_pm1( + lysosomal_diseases_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_malignant_hyperthermia_susceptibility.py b/tests/vcep/test_malignant_hyperthermia_susceptibility.py new file mode 100644 index 0000000..7e4936e --- /dev/null +++ b/tests/vcep/test_malignant_hyperthermia_susceptibility.py @@ -0,0 +1,111 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.malignant_hyperthermia_susceptibility import MalignantHyperthermiaPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="19", pos=100, delete="A", insert="T") + + +@pytest.fixture +def malignant_hyperthermia_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return MalignantHyperthermiaPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_met_for_moderate_region(malignant_hyperthermia_predictor, auto_acmg_data): + """Test when the variant falls within a moderate region for RYR1.""" + auto_acmg_data.prot_pos = 300 # Within the moderate region (1-552) for RYR1 + auto_acmg_data.hgnc_id = "HGNC:10483" # RYR1 gene + result = malignant_hyperthermia_predictor.predict_pm1( + malignant_hyperthermia_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for variants in moderate regions." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "Variant falls within a critical region in RYR1 between positions 1-552" in result.summary + ), "The summary should indicate the affected region." + + +def test_predict_pm1_met_for_supporting_region(malignant_hyperthermia_predictor, auto_acmg_data): + """Test when the variant falls within a supporting region for RYR1.""" + auto_acmg_data.prot_pos = 4800 # Within the supporting region (4631-4991) for RYR1 + auto_acmg_data.hgnc_id = "HGNC:10483" # RYR1 gene + result = malignant_hyperthermia_predictor.predict_pm1( + malignant_hyperthermia_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for variants in supporting regions." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "Variant falls within a critical region in RYR1 between positions 4631-4991" + in result.summary + ), "The summary should indicate the affected region." + + +def test_predict_pm1_not_met(malignant_hyperthermia_predictor, auto_acmg_data): + """Test when the variant does not fall within any critical regions for RYR1.""" + auto_acmg_data.prot_pos = 5000 # Not within any critical regions + auto_acmg_data.hgnc_id = "HGNC:10483" # RYR1 gene + result = malignant_hyperthermia_predictor.predict_pm1( + malignant_hyperthermia_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical region variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "Variant does not meet the PM1 criteria for Malignant Hyperthermia Susceptibility" + in result.summary + ), "The summary should indicate the lack of criteria met." + + +@patch("src.vcep.malignant_hyperthermia_susceptibility.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, malignant_hyperthermia_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method if logic changes.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the specific logic + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = malignant_hyperthermia_predictor.predict_pm1( + malignant_hyperthermia_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_mitochondrial_diseases.py b/tests/vcep/test_mitochondrial_diseases.py new file mode 100644 index 0000000..9af55cd --- /dev/null +++ b/tests/vcep/test_mitochondrial_diseases.py @@ -0,0 +1,113 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.mitochondrial_diseases import MitochondrialDiseasesPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="X", pos=100, delete="A", insert="T") + + +@pytest.fixture +def mitochondrial_diseases_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return MitochondrialDiseasesPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_not_applicable_ethe1(mitochondrial_diseases_predictor, auto_acmg_data): + """Test when PM1 is not applicable for ETHE1.""" + auto_acmg_data.hgnc_id = "HGNC:23287" # ETHE1 gene + result = mitochondrial_diseases_predictor.predict_pm1( + mitochondrial_diseases_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should not be applicable for ETHE1." + assert ( + "PM1 is not applicable for HGNC:23287" in result.summary + ), "The summary should indicate non-applicability." + + +def test_predict_pm1_not_applicable_pdah1(mitochondrial_diseases_predictor, auto_acmg_data): + """Test when PM1 is not applicable for PDHA1.""" + auto_acmg_data.hgnc_id = "HGNC:8806" # PDHA1 gene + result = mitochondrial_diseases_predictor.predict_pm1( + mitochondrial_diseases_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should not be applicable for PDHA1." + assert ( + "PM1 is not applicable for HGNC:8806" in result.summary + ), "The summary should indicate non-applicability." + + +def test_predict_pm1_not_applicable_polg(mitochondrial_diseases_predictor, auto_acmg_data): + """Test when PM1 is not applicable for POLG.""" + auto_acmg_data.hgnc_id = "HGNC:9179" # POLG gene + result = mitochondrial_diseases_predictor.predict_pm1( + mitochondrial_diseases_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should not be applicable for POLG." + assert ( + "PM1 is not applicable for HGNC:9179" in result.summary + ), "The summary should indicate non-applicability." + + +def test_predict_pm1_not_applicable_slc19a3(mitochondrial_diseases_predictor, auto_acmg_data): + """Test when PM1 is not applicable for SLC19A3.""" + auto_acmg_data.hgnc_id = "HGNC:16266" # SLC19A3 gene + result = mitochondrial_diseases_predictor.predict_pm1( + mitochondrial_diseases_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should not be applicable for SLC19A3." + assert ( + "PM1 is not applicable for HGNC:16266" in result.summary + ), "The summary should indicate non-applicability." + + +@patch("src.vcep.mitochondrial_diseases.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, mitochondrial_diseases_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method if logic changes.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the specific logic + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = mitochondrial_diseases_predictor.predict_pm1( + mitochondrial_diseases_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_monogenic_diabetes.py b/tests/vcep/test_monogenic_diabetes.py new file mode 100644 index 0000000..236f4c6 --- /dev/null +++ b/tests/vcep/test_monogenic_diabetes.py @@ -0,0 +1,145 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.monogenic_diabetes import MonogenicDiabetesPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="12", pos=100, delete="A", insert="T") + + +@pytest.fixture +def monogenic_diabetes_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return MonogenicDiabetesPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_moderate_criteria_hnf1a(monogenic_diabetes_predictor, auto_acmg_data): + """Test when variant falls within a moderate level residue for HNF1A.""" + auto_acmg_data.hgnc_id = "HGNC:11621" # HNF1A gene + auto_acmg_data.prot_pos = 130 # Within the critical residues for HNF1A + result = monogenic_diabetes_predictor.predict_pm1( + monogenic_diabetes_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met for critical residues." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "critical for Monogenic Diabetes" in result.summary + ), "The summary should indicate the critical residue." + + +def test_predict_pm1_supporting_criteria_hnf4a_domain(monogenic_diabetes_predictor, auto_acmg_data): + """Test when variant falls within a supporting level domain for HNF4A.""" + auto_acmg_data.hgnc_id = "HGNC:5024" # HNF4A gene + auto_acmg_data.prot_pos = 100 # Within the critical domain for HNF4A + result = monogenic_diabetes_predictor.predict_pm1( + monogenic_diabetes_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met for critical domains." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "falls within a critical region" in result.summary + ), "The summary should indicate the critical region." + + +def test_predict_pm1_supporting_criteria_hnf4a_promoter( + monogenic_diabetes_predictor, auto_acmg_data +): + """Test when variant falls within a supporting level promoter region for HNF4A.""" + auto_acmg_data.hgnc_id = "HGNC:5024" # HNF4A gene + auto_acmg_data.cds_pos = -140 # Within the critical promoter region for HNF4A + result = monogenic_diabetes_predictor.predict_pm1( + monogenic_diabetes_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for critical promoter regions." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "falls within a critical promoter region" in result.summary + ), "The summary should indicate the critical region." + + +def test_predict_pm1_moderate_criteria_gck(monogenic_diabetes_predictor, auto_acmg_data): + """Test when variant falls within a moderate level residue for GCK.""" + auto_acmg_data.hgnc_id = "HGNC:4195" # GCK gene + auto_acmg_data.prot_pos = 151 # Within the critical residues for GCK + result = monogenic_diabetes_predictor.predict_pm1( + monogenic_diabetes_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met for critical residues." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "critical for Monogenic Diabetes" in result.summary + ), "The summary should indicate the critical residue." + + +def test_predict_pm1_not_met(monogenic_diabetes_predictor, auto_acmg_data): + """Test when variant does not meet any PM1 criteria for Monogenic Diabetes genes.""" + auto_acmg_data.hgnc_id = "HGNC:4195" # GCK gene + auto_acmg_data.prot_pos = 500 # Outside all critical regions + result = monogenic_diabetes_predictor.predict_pm1( + monogenic_diabetes_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical residues." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "does not meet the PM1 criteria" in result.summary + ), "The summary should indicate that criteria were not met." + + +@patch("src.vcep.monogenic_diabetes.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, monogenic_diabetes_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the PM1_CLUSTER + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = monogenic_diabetes_predictor.predict_pm1( + monogenic_diabetes_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_myeloid_malignancy.py b/tests/vcep/test_myeloid_malignancy.py new file mode 100644 index 0000000..2061be9 --- /dev/null +++ b/tests/vcep/test_myeloid_malignancy.py @@ -0,0 +1,105 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.myeloid_malignancy import MyeloidMalignancyPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="21", pos=100, delete="A", insert="T") + + +@pytest.fixture +def myeloid_malignancy_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return MyeloidMalignancyPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_moderate_criteria_runx1(myeloid_malignancy_predictor, auto_acmg_data): + """Test when variant falls within a moderate level residue for RUNX1.""" + auto_acmg_data.hgnc_id = "HGNC:10471" # RUNX1 gene + auto_acmg_data.prot_pos = 107 # Within the critical residues for RUNX1 + result = myeloid_malignancy_predictor.predict_pm1( + myeloid_malignancy_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met for critical residues." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "critical residue within the Runt Homology Domain" in result.summary + ), "The summary should indicate the critical residue." + + +def test_predict_pm1_supporting_criteria_runx1(myeloid_malignancy_predictor, auto_acmg_data): + """Test when variant falls within a supporting level residue for RUNX1.""" + auto_acmg_data.hgnc_id = "HGNC:10471" # RUNX1 gene + auto_acmg_data.prot_pos = 100 # Within the supporting residues for RUNX1 + result = myeloid_malignancy_predictor.predict_pm1( + myeloid_malignancy_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met for supporting residues." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "residue within the Runt Homology Domain" in result.summary + ), "The summary should indicate the supporting residue." + + +def test_predict_pm1_not_met(myeloid_malignancy_predictor, auto_acmg_data): + """Test when variant does not meet any PM1 criteria for RUNX1.""" + auto_acmg_data.hgnc_id = "HGNC:10471" # RUNX1 gene + auto_acmg_data.prot_pos = 300 # Outside all critical and supporting regions + result = myeloid_malignancy_predictor.predict_pm1( + myeloid_malignancy_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical residues." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "does not meet the PM1 criteria" in result.summary + ), "The summary should indicate that criteria were not met." + + +@patch("src.vcep.myeloid_malignancy.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, myeloid_malignancy_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the PM1_CLUSTER + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = myeloid_malignancy_predictor.predict_pm1( + myeloid_malignancy_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_pku.py b/tests/vcep/test_pku.py new file mode 100644 index 0000000..5437d87 --- /dev/null +++ b/tests/vcep/test_pku.py @@ -0,0 +1,77 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.pku import PKUPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="12", pos=100, delete="A", insert="T") + + +@pytest.fixture +def pku_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return PKUPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_residue_critical_for_pku(pku_predictor, auto_acmg_data): + """Test when the variant affects a residue critical for PKU.""" + auto_acmg_data.hgnc_id = "HGNC:8582" # PAH gene + auto_acmg_data.prot_pos = 138 # Tyr138, an active site residue + result = pku_predictor.predict_pm1(pku_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met for critical residues." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert "critical for PKU" in result.summary, "The summary should indicate the critical residue." + + +def test_predict_pm1_residue_not_critical_for_pku(pku_predictor, auto_acmg_data): + """Test when the variant does not affect a residue critical for PKU.""" + auto_acmg_data.hgnc_id = "HGNC:8582" # PAH gene + auto_acmg_data.prot_pos = 500 # Position outside the critical residues + result = pku_predictor.predict_pm1(pku_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical residues." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "does not meet the PM1 criteria" in result.summary + ), "The summary should indicate that criteria were not met." + + +@patch("src.vcep.pku.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, pku_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the PM1_CLUSTER_PKU + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = pku_predictor.predict_pm1(pku_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_platelet_disorders.py b/tests/vcep/test_platelet_disorders.py new file mode 100644 index 0000000..d28fd0f --- /dev/null +++ b/tests/vcep/test_platelet_disorders.py @@ -0,0 +1,83 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.platelet_disorders import PlateletDisordersPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="17", pos=100, delete="A", insert="T") + + +@pytest.fixture +def platelet_disorders_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return PlateletDisordersPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_not_met_for_itga2b(platelet_disorders_predictor, auto_acmg_data): + """Test when the variant is in the ITGA2B gene and PM1 is not met.""" + auto_acmg_data.hgnc_id = "HGNC:6138" # ITGA2B gene + result = platelet_disorders_predictor.predict_pm1( + platelet_disorders_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.NotMet, "PM1 should not be met for ITGA2B." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "PM1 is not met for ITGA2B and ITGB3" in result.summary + ), "The summary should explain the reason." + + +def test_predict_pm1_not_met_for_itgb3(platelet_disorders_predictor, auto_acmg_data): + """Test when the variant is in the ITGB3 gene and PM1 is not met.""" + auto_acmg_data.hgnc_id = "HGNC:6156" # ITGB3 gene + result = platelet_disorders_predictor.predict_pm1( + platelet_disorders_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.NotMet, "PM1 should not be met for ITGB3." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "PM1 is not met for ITGA2B and ITGB3" in result.summary + ), "The summary should explain the reason." + + +@patch("src.vcep.platelet_disorders.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, platelet_disorders_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the Platelet Disorders VCEP + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = platelet_disorders_predictor.predict_pm1( + platelet_disorders_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_pten.py b/tests/vcep/test_pten.py new file mode 100644 index 0000000..6a10865 --- /dev/null +++ b/tests/vcep/test_pten.py @@ -0,0 +1,81 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.pten import PTENPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="10", pos=100, delete="A", insert="T") + + +@pytest.fixture +def pten_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return PTENPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_in_catalytic_motifs(pten_predictor, auto_acmg_data): + """Test when the variant falls within the catalytic motifs of PTEN.""" + auto_acmg_data.hgnc_id = "HGNC:9588" # PTEN gene + auto_acmg_data.prot_pos = 92 # Within the catalytic motif (90-94) + result = pten_predictor.predict_pm1(pten_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for a variant in the catalytic motif." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "catalytic motifs of PTEN" in result.summary + ), "The summary should indicate the catalytic motif." + + +def test_predict_pm1_outside_catalytic_motifs(pten_predictor, auto_acmg_data): + """Test when the variant does not fall within the catalytic motifs of PTEN.""" + auto_acmg_data.hgnc_id = "HGNC:9588" # PTEN gene + auto_acmg_data.prot_pos = 150 # Outside the catalytic motifs + result = pten_predictor.predict_pm1(pten_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for a variant outside the catalytic motifs." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "Variant does not meet the PM1 criteria for PTEN" in result.summary + ), "The summary should indicate no criteria were met." + + +@patch("src.vcep.pten.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, pten_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the PTEN VCEP + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = pten_predictor.predict_pm1(pten_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_pulmonary_hypertension.py b/tests/vcep/test_pulmonary_hypertension.py new file mode 100644 index 0000000..08cc73c --- /dev/null +++ b/tests/vcep/test_pulmonary_hypertension.py @@ -0,0 +1,105 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.pulmonary_hypertension import PulmonaryHypertensionPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="2", pos=100, delete="A", insert="T") + + +@pytest.fixture +def pulmonary_hypertension_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return PulmonaryHypertensionPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_strong_criteria(pulmonary_hypertension_predictor, auto_acmg_data): + """Test when the variant falls within strong criteria for BMPR2.""" + auto_acmg_data.hgnc_id = "HGNC:1078" # BMPR2 gene + auto_acmg_data.prot_pos = 210 # Within the strong criteria + result = pulmonary_hypertension_predictor.predict_pm1( + pulmonary_hypertension_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met at the Strong level." + assert ( + result.strength == AutoACMGStrength.PathogenicStrong + ), "The strength should be PathogenicStrong." + assert ( + "critical residue in BMPR2" in result.summary + ), "The summary should indicate a critical residue." + + +def test_predict_pm1_moderate_criteria(pulmonary_hypertension_predictor, auto_acmg_data): + """Test when the variant falls within moderate criteria for BMPR2.""" + auto_acmg_data.hgnc_id = "HGNC:1078" # BMPR2 gene + auto_acmg_data.prot_pos = 250 # Within the moderate criteria + result = pulmonary_hypertension_predictor.predict_pm1( + pulmonary_hypertension_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert result.prediction == AutoACMGPrediction.Met, "PM1 should be met at the Moderate level." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "within the extracellular or kinase domain" in result.summary + ), "The summary should indicate the domain." + + +def test_predict_pm1_non_critical_residue(pulmonary_hypertension_predictor, auto_acmg_data): + """Test when the variant affects a non-critical residue in BMPR2.""" + auto_acmg_data.hgnc_id = "HGNC:1078" # BMPR2 gene + auto_acmg_data.prot_pos = 42 # Non-critical residue + result = pulmonary_hypertension_predictor.predict_pm1( + pulmonary_hypertension_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical residues." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "demonstrated to be non-critical for kinase activity" in result.summary + ), "The summary should indicate non-critical residue." + + +@patch("src.vcep.pulmonary_hypertension.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default( + mock_predict_pm1, pulmonary_hypertension_predictor, auto_acmg_data +): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the BMPR2 VCEP + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = pulmonary_hypertension_predictor.predict_pm1( + pulmonary_hypertension_predictor.seqvar, auto_acmg_data + ) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_rasopathy.py b/tests/vcep/test_rasopathy.py new file mode 100644 index 0000000..2a53fdb --- /dev/null +++ b/tests/vcep/test_rasopathy.py @@ -0,0 +1,111 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.rasopathy import RASopathyPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="12", pos=100, delete="A", insert="T") + + +@pytest.fixture +def rasopathy_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return RASopathyPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_in_critical_region(rasopathy_predictor, auto_acmg_data): + """Test when the variant falls within a critical region for a RASopathy gene.""" + auto_acmg_data.hgnc_id = "HGNC:7989" # NRAS gene + auto_acmg_data.prot_pos = 15 # Within the critical P-loop region (10-17) + result = rasopathy_predictor.predict_pm1(rasopathy_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for critical region variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "critical residue in HGNC:7989" in result.summary + ), "The summary should indicate the critical region." + + +def test_predict_pm1_in_critical_exon(rasopathy_predictor, auto_acmg_data): + """Test when the variant falls within a critical exon for a RASopathy gene.""" + auto_acmg_data.hgnc_id = "HGNC:1097" # BRAF gene + auto_acmg_data.prot_pos = 480 # Position in the P-loop region, but within exon 11 + rasopathy_predictor._get_affected_exon = MagicMock(return_value=11) # Mock the affected exon + result = rasopathy_predictor.predict_pm1(rasopathy_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for critical exon variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert "critical exon 11" in result.summary, "The summary should indicate the critical exon." + + +def test_predict_pm1_not_applicable(rasopathy_predictor, auto_acmg_data): + """Test when PM1 is not applicable for certain RASopathy genes.""" + auto_acmg_data.hgnc_id = "HGNC:15454" # SHOC2 gene + result = rasopathy_predictor.predict_pm1(rasopathy_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should not be applicable for SHOC2." + assert "not applicable" in result.summary, "The summary should indicate non-applicability." + + +def test_predict_pm1_outside_critical_region(rasopathy_predictor, auto_acmg_data): + """Test when the variant does not fall within any critical region for RASopathy genes.""" + auto_acmg_data.hgnc_id = "HGNC:7989" # NRAS gene + auto_acmg_data.prot_pos = 200 # Position outside all critical regions + rasopathy_predictor._get_affected_exon = MagicMock(return_value=1) # Mock the affected exon + result = rasopathy_predictor.predict_pm1(rasopathy_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical region variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "does not meet the PM1 criteria" in result.summary + ), "The summary should indicate no critical region." + + +@patch("src.vcep.rasopathy.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, rasopathy_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the RASopathy VCEP + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = rasopathy_predictor.predict_pm1(rasopathy_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_rett_angelman.py b/tests/vcep/test_rett_angelman.py new file mode 100644 index 0000000..9fb7913 --- /dev/null +++ b/tests/vcep/test_rett_angelman.py @@ -0,0 +1,93 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.rett_angelman import RettAngelmanPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="18", pos=100, delete="A", insert="T") + + +@pytest.fixture +def rett_angelman_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return RettAngelmanPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_in_critical_region(rett_angelman_predictor, auto_acmg_data): + """Test when the variant falls within a critical region for a Rett and Angelman-like Disorder gene.""" + auto_acmg_data.hgnc_id = "HGNC:11634" # TCF4 gene + auto_acmg_data.prot_pos = 580 # Within the critical bHLH domain (564-617) + result = rett_angelman_predictor.predict_pm1(rett_angelman_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for critical region variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "critical residue in HGNC:11634" in result.summary + ), "The summary should indicate the critical region." + + +def test_predict_pm1_not_applicable(rett_angelman_predictor, auto_acmg_data): + """Test when PM1 is not applicable for SLC9A6.""" + auto_acmg_data.hgnc_id = "HGNC:11079" # SLC9A6 gene + result = rett_angelman_predictor.predict_pm1(rett_angelman_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should not be applicable for SLC9A6." + assert "not applicable" in result.summary, "The summary should indicate non-applicability." + + +def test_predict_pm1_outside_critical_region(rett_angelman_predictor, auto_acmg_data): + """Test when the variant does not fall within any critical region for Rett and Angelman-like Disorders genes.""" + auto_acmg_data.hgnc_id = "HGNC:11634" # TCF4 gene + auto_acmg_data.prot_pos = 700 # Position outside all critical regions + result = rett_angelman_predictor.predict_pm1(rett_angelman_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical region variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "does not meet the PM1 criteria" in result.summary + ), "The summary should indicate no critical region." + + +@patch("src.vcep.rett_angelman.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, rett_angelman_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the Rett and Angelman-like VCEP + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default PM1 prediction fallback.", + ) + result = rett_angelman_predictor.predict_pm1(rett_angelman_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default PM1 prediction fallback." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_scid.py b/tests/vcep/test_scid.py new file mode 100644 index 0000000..c048da8 --- /dev/null +++ b/tests/vcep/test_scid.py @@ -0,0 +1,111 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.scid import SCIDPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="10", pos=100, delete="A", insert="T") + + +@pytest.fixture +def scid_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return SCIDPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_in_moderate_domain(scid_predictor, auto_acmg_data): + """Test when the variant falls within a moderate domain for a SCID gene.""" + auto_acmg_data.hgnc_id = "HGNC:12765" # FOXN1 gene + auto_acmg_data.prot_pos = 300 # Within the DNA binding forkhead domain (270-367) + result = scid_predictor.predict_pm1(scid_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for critical domain variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "critical residue in HGNC:12765" in result.summary + ), "The summary should indicate the critical domain." + + +def test_predict_pm1_in_supporting_domain(scid_predictor, auto_acmg_data): + """Test when the variant falls within a supporting domain for a SCID gene.""" + auto_acmg_data.hgnc_id = "HGNC:9831" # RAG1 gene + auto_acmg_data.prot_pos = 600 # Within the core domain (387-1011) + result = scid_predictor.predict_pm1(scid_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for supporting domain variants." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "supporting region of HGNC:9831" in result.summary + ), "The summary should indicate the supporting domain." + + +def test_predict_pm1_strong_residue(scid_predictor, auto_acmg_data): + """Test when the variant affects a strong residue for a SCID gene.""" + auto_acmg_data.hgnc_id = "HGNC:6010" # IL2RG gene + auto_acmg_data.prot_pos = 62 # A conserved cysteine residue + result = scid_predictor.predict_pm1(scid_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for strong residue variants." + assert ( + result.strength == AutoACMGStrength.PathogenicStrong + ), "The strength should be PathogenicStrong." + assert ( + "strong residue in HGNC:6010" in result.summary + ), "The summary should indicate the strong residue." + + +def test_predict_pm1_not_applicable(scid_predictor, auto_acmg_data): + """Test when PM1 is not applicable for ADA, DCLRE1C, or IL7R.""" + auto_acmg_data.hgnc_id = "HGNC:186" # ADA gene + result = scid_predictor.predict_pm1(scid_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should not be applicable for ADA." + assert "not applicable" in result.summary, "The summary should indicate non-applicability." + + +@patch("src.vcep.scid.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, scid_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the SCID VCEP + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default fallback for PM1.", + ) + + result = scid_predictor.predict_pm1(scid_predictor.seqvar, auto_acmg_data) + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default fallback for PM1." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_thrombosis.py b/tests/vcep/test_thrombosis.py new file mode 100644 index 0000000..0f7d326 --- /dev/null +++ b/tests/vcep/test_thrombosis.py @@ -0,0 +1,117 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.thrombosis import ThrombosisPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar(genome_release=GenomeRelease.GRCh37, chrom="1", pos=100, delete="A", insert="T") + + +@pytest.fixture +def thrombosis_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return ThrombosisPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_in_cysteine_residue(thrombosis_predictor, auto_acmg_data): + """Test when the variant affects a critical cysteine residue in SERPINC1.""" + auto_acmg_data.hgnc_id = "HGNC:775" # SERPINC1 gene + auto_acmg_data.prot_pos = 40 # A cysteine residue involved in disulfide bridges + result = thrombosis_predictor.predict_pm1(thrombosis_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for cysteine residue variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "meets the PM1 criteria for SERPINC1" in result.summary + ), "The summary should indicate the PM1 criteria." + + +def test_predict_pm1_in_heparin_binding_residue(thrombosis_predictor, auto_acmg_data): + """Test when the variant affects a residue in the heparin binding site of SERPINC1.""" + auto_acmg_data.hgnc_id = "HGNC:775" # SERPINC1 gene + auto_acmg_data.prot_pos = 39 # A heparin binding site residue + result = thrombosis_predictor.predict_pm1(thrombosis_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for heparin binding site variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "meets the PM1 criteria for SERPINC1" in result.summary + ), "The summary should indicate the PM1 criteria." + + +def test_predict_pm1_in_reactive_site_residue(thrombosis_predictor, auto_acmg_data): + """Test when the variant affects a residue in the reactive site of SERPINC1.""" + auto_acmg_data.hgnc_id = "HGNC:775" # SERPINC1 gene + auto_acmg_data.prot_pos = 414 # A reactive site residue + result = thrombosis_predictor.predict_pm1(thrombosis_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for reactive site variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "meets the PM1 criteria for SERPINC1" in result.summary + ), "The summary should indicate the PM1 criteria." + + +def test_predict_pm1_not_met(thrombosis_predictor, auto_acmg_data): + """Test when the variant does not meet the PM1 criteria for SERPINC1.""" + auto_acmg_data.hgnc_id = "HGNC:775" # SERPINC1 gene + auto_acmg_data.prot_pos = 500 # Position outside of critical regions + result = thrombosis_predictor.predict_pm1(thrombosis_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical residue variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "does not meet the PM1 criteria for SERPINC1" in result.summary + ), "The summary should indicate the PM1 criteria." + + +@patch("src.vcep.thrombosis.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, thrombosis_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the Thrombosis VCEP + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default fallback for PM1.", + ) + + result = thrombosis_predictor.predict_pm1(thrombosis_predictor.seqvar, auto_acmg_data) + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default fallback for PM1." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_tp53.py b/tests/vcep/test_tp53.py new file mode 100644 index 0000000..afc6a3f --- /dev/null +++ b/tests/vcep/test_tp53.py @@ -0,0 +1,83 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.tp53 import TP53Predictor + + +@pytest.fixture +def seqvar(): + return SeqVar( + genome_release=GenomeRelease.GRCh37, chrom="17", pos=7578406, delete="C", insert="T" + ) + + +@pytest.fixture +def tp53_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return TP53Predictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_in_moderate_residue(tp53_predictor, auto_acmg_data): + """Test when the variant affects a critical residue in TP53.""" + auto_acmg_data.hgnc_id = "HGNC:11998" # TP53 gene + auto_acmg_data.prot_pos = 175 # A critical residue in TP53 + result = tp53_predictor.predict_pm1(tp53_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for critical residue variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "affects a critical residue in TP53" in result.summary + ), "The summary should indicate the PM1 criteria." + + +def test_predict_pm1_not_met(tp53_predictor, auto_acmg_data): + """Test when the variant does not meet the PM1 criteria for TP53.""" + auto_acmg_data.hgnc_id = "HGNC:11998" # TP53 gene + auto_acmg_data.prot_pos = 300 # Position outside of critical residues + result = tp53_predictor.predict_pm1(tp53_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical residue variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "does not meet the PM1 criteria for TP53" in result.summary + ), "The summary should indicate the PM1 criteria." + + +@patch("src.vcep.tp53.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, tp53_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the TP53 VCEP + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default fallback for PM1.", + ) + result = tp53_predictor.predict_pm1(tp53_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." diff --git a/tests/vcep/test_vhl.py b/tests/vcep/test_vhl.py new file mode 100644 index 0000000..ea9cae5 --- /dev/null +++ b/tests/vcep/test_vhl.py @@ -0,0 +1,83 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.vhl import VHLPredictor + + +@pytest.fixture +def seqvar(): + return SeqVar( + genome_release=GenomeRelease.GRCh37, chrom="3", pos=10191539, delete="C", insert="T" + ) + + +@pytest.fixture +def vhl_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return VHLPredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_in_moderate_residue(vhl_predictor, auto_acmg_data): + """Test when the variant affects a critical residue in VHL.""" + auto_acmg_data.hgnc_id = "HGNC:12687" # VHL gene + auto_acmg_data.prot_pos = 167 # A critical residue in VHL + result = vhl_predictor.predict_pm1(vhl_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.Met + ), "PM1 should be met for critical residue variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "affects a germline hotspot or key functional domain in VHL" in result.summary + ), "The summary should indicate the PM1 criteria." + + +def test_predict_pm1_not_met(vhl_predictor, auto_acmg_data): + """Test when the variant does not meet the PM1 criteria for VHL.""" + auto_acmg_data.hgnc_id = "HGNC:12687" # VHL gene + auto_acmg_data.prot_pos = 200 # Position outside of critical residues + result = vhl_predictor.predict_pm1(vhl_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met for non-critical residue variants." + assert ( + result.strength == AutoACMGStrength.PathogenicModerate + ), "The strength should be PathogenicModerate." + assert ( + "does not meet the PM1 criteria for VHL" in result.summary + ), "The summary should indicate the PM1 criteria." + + +@patch("src.vcep.vhl.DefaultPredictor.predict_pm1") +def test_predict_pm1_fallback_to_default(mock_predict_pm1, vhl_predictor, auto_acmg_data): + """Test fallback to the default PM1 prediction method for unhandled cases.""" + auto_acmg_data.hgnc_id = "HGNC:9999" # Gene not in the VHL VCEP + mock_predict_pm1.return_value = AutoACMGCriteria( + name="PM1", + prediction=AutoACMGPrediction.NotMet, + strength=AutoACMGStrength.PathogenicModerate, + summary="Default fallback for PM1.", + ) + result = vhl_predictor.predict_pm1(vhl_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotMet + ), "PM1 should not be met in the default fallback." + assert ( + "Default fallback for PM1." in result.summary + ), "The summary should indicate the default fallback." diff --git a/tests/vcep/test_von_willebrand_disease.py b/tests/vcep/test_von_willebrand_disease.py new file mode 100644 index 0000000..84e1540 --- /dev/null +++ b/tests/vcep/test_von_willebrand_disease.py @@ -0,0 +1,43 @@ +from unittest.mock import MagicMock + +import pytest + +from src.defs.auto_acmg import AutoACMGCriteria, AutoACMGData, AutoACMGPrediction, AutoACMGStrength +from src.defs.genome_builds import GenomeRelease +from src.defs.seqvar import SeqVar +from src.vcep.von_willebrand_disease import VonWillebrandDiseasePredictor + + +@pytest.fixture +def seqvar(): + return SeqVar( + genome_release=GenomeRelease.GRCh37, chrom="12", pos=6135437, delete="A", insert="T" + ) + + +@pytest.fixture +def vwf_predictor(seqvar): + result = MagicMock() # Mocking the AutoACMGResult object + return VonWillebrandDiseasePredictor(seqvar=seqvar, result=result, config=MagicMock()) + + +@pytest.fixture +def auto_acmg_data(): + return AutoACMGData() + + +def test_predict_pm1_not_applicable(vwf_predictor, auto_acmg_data): + """Test when PM1 is not applicable for von Willebrand Disease.""" + auto_acmg_data.hgnc_id = "HGNC:12726" # VWF gene + result = vwf_predictor.predict_pm1(vwf_predictor.seqvar, auto_acmg_data) + + assert isinstance(result, AutoACMGCriteria), "The result should be of type AutoACMGCriteria." + assert ( + result.prediction == AutoACMGPrediction.NotApplicable + ), "PM1 should be NotApplicable for VWF." + assert ( + result.strength == AutoACMGStrength.PathogenicSupporting + ), "The strength should be PathogenicSupporting." + assert ( + "PM1 is not applicable for" in result.summary + ), "The summary should indicate PM1 is not applicable."