diff --git a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/PvalueGeneScorer.java b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/PvalueGeneScorer.java index e0e1866ca..1db2d0e50 100644 --- a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/PvalueGeneScorer.java +++ b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/PvalueGeneScorer.java @@ -44,7 +44,7 @@ public PvalueGeneScorer(String probandId, Pedigree.Individual.Sex probandSex, In this.genePriorityScoreCalculator = new GenePriorityScoreCalculator(); this.pValueCalculator = Objects.requireNonNull(pValueCalculator); AcmgEvidenceAssigner acmgEvidenceAssigner = new Acmg2015EvidenceAssigner(probandId, inheritanceModeAnnotator.getPedigree()); - AcmgEvidenceClassifier acmgEvidenceClassifier = new Acgs2020Classifier(); + AcmgEvidenceClassifier acmgEvidenceClassifier = new Acmg2020PointsBasedClassifier(); this.acmgAssignmentCalculator = new AcmgAssignmentCalculator(acmgEvidenceAssigner, acmgEvidenceClassifier); } @@ -101,6 +101,15 @@ private GeneScore calculateGeneScore(Gene gene, ModeOfInheritance modeOfInherita GenePriorityScoreCalculator.GenePriorityScore priorityScore = genePriorityScoreCalculator.calculateGenePriorityScore(gene, modeOfInheritance); + List> compatibleDiseaseMatches = priorityScore.getCompatibleDiseaseMatches(); + + List acmgAssignments = acmgAssignmentCalculator.calculateAcmgAssignments(modeOfInheritance, gene, contributingVariants, compatibleDiseaseMatches); + +// double variantScore = acmgAssignments.stream() +// .mapToDouble(acmgAssignment -> acmgAssignment.acmgEvidence().postProbPath()) +// .average() +// .orElse(0.1); // 0.1 is the equivalent of a 0-point VUS + double variantScore = contributingVariants.stream() .mapToDouble(VariantEvaluation::getVariantScore) .average() @@ -110,10 +119,6 @@ private GeneScore calculateGeneScore(Gene gene, ModeOfInheritance modeOfInherita double pValue = pValueCalculator.calculatePvalueFromCombinedScore(combinedScore); - List> compatibleDiseaseMatches = priorityScore.getCompatibleDiseaseMatches(); - - List acmgAssignments = acmgAssignmentCalculator.calculateAcmgAssignments(modeOfInheritance, gene, contributingVariants, compatibleDiseaseMatches); - return GeneScore.builder() .geneIdentifier(gene.getGeneIdentifier()) .modeOfInheritance(modeOfInheritance) diff --git a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/RawScoreGeneScorer.java b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/RawScoreGeneScorer.java index e4f1e8abe..e9ad03353 100644 --- a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/RawScoreGeneScorer.java +++ b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/RawScoreGeneScorer.java @@ -70,7 +70,7 @@ public RawScoreGeneScorer(String probandId, Sex probandSex, InheritanceModeAnnot this.contributingAlleleCalculator = new ContributingAlleleCalculator(probandId, probandSex, inheritanceModeAnnotator); this.genePriorityScoreCalculator = new GenePriorityScoreCalculator(); AcmgEvidenceAssigner acmgEvidenceAssigner = new Acmg2015EvidenceAssigner(probandId, inheritanceModeAnnotator.getPedigree()); - AcmgEvidenceClassifier acmgEvidenceClassifier = new Acgs2020Classifier(); + AcmgEvidenceClassifier acmgEvidenceClassifier = new Acmg2020PointsBasedClassifier(); this.acmgAssignmentCalculator = new AcmgAssignmentCalculator(acmgEvidenceAssigner, acmgEvidenceClassifier); } diff --git a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acgs2020Classifier.java b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acgs2020Classifier.java index 7c7746632..dbd1f1c8b 100644 --- a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acgs2020Classifier.java +++ b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acgs2020Classifier.java @@ -78,6 +78,9 @@ private static boolean isPathogenic(int pvs, int ps, int pm, int pp) { } private static boolean isLikelyPathogenic(int pvs, int ps, int pm, int pp) { + if (pvs == 1 && pp == 1) { + return true; + } if (ps >= 2) { return true; } diff --git a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015Classifier.java b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015Classifier.java index 29671b8ea..c521839cd 100644 --- a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015Classifier.java +++ b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015Classifier.java @@ -78,7 +78,7 @@ private static boolean isPathogenic(int pvs, int ps, int pm, int pp) { } private static boolean isLikelyPathogenic(int pvs, int ps, int pm, int pp) { - if (pvs == 1 && pm == 1) { + if (pvs == 1 && (pm == 1 || pp == 1)) { return true; } if (ps == 1 && (pm == 1 || pm == 2) || ps == 1 && pp >= 2) { diff --git a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015EvidenceAssigner.java b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015EvidenceAssigner.java index 15f32e46e..ee1dd23aa 100644 --- a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015EvidenceAssigner.java +++ b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015EvidenceAssigner.java @@ -80,12 +80,18 @@ public Acmg2015EvidenceAssigner(String probandId, Pedigree pedigree) { */ // https://www.ncbi.nlm.nih.gov/clinvar/variation/464/ - check in ClinVar VCF if there is MOI information for a classification public AcmgEvidence assignVariantAcmgEvidence(VariantEvaluation variantEvaluation, ModeOfInheritance modeOfInheritance, List contributingVariants, List knownDiseases, List> compatibleDiseaseMatches) { - // try strict ACMG assignments only if there are known disease-gene associations - if (knownDiseases.isEmpty()) { - return AcmgEvidence.empty(); + AcmgEvidence.Builder acmgEvidenceBuilder = AcmgEvidence.builder(); + + FrequencyData frequencyData = variantEvaluation.getFrequencyData(); + // BA1 "Allele frequency is >5% in Exome Sequencing Project, 1000 Genomes Project, or Exome Aggregation Consortium" + if (frequencyData.getMaxFreq() >= 5.0) { + acmgEvidenceBuilder.add(BA1); + // BA1 is supposed to be used as a filtering criterion where no other evidence need be considered. + return acmgEvidenceBuilder.build(); } + // PM2 "Absent from controls (or at extremely low frequency if recessive) in Exome Sequencing Project, 1000 Genomes Project, or Exome Aggregation Consortium" + assignPM2(acmgEvidenceBuilder, frequencyData); - AcmgEvidence.Builder acmgEvidenceBuilder = AcmgEvidence.builder(); boolean hasCompatibleDiseaseMatches = !compatibleDiseaseMatches.isEmpty(); @@ -106,12 +112,6 @@ public AcmgEvidence assignVariantAcmgEvidence(VariantEvaluation variantEvaluatio assignBS4(acmgEvidenceBuilder, variantEvaluation, proband); } - FrequencyData frequencyData = variantEvaluation.getFrequencyData(); - // PM2 "Absent from controls (or at extremely low frequency if recessive) in Exome Sequencing Project, 1000 Genomes Project, or Exome Aggregation Consortium" - assignPM2(acmgEvidenceBuilder, frequencyData); - // BA1 "Allele frequency is >5% in Exome Sequencing Project, 1000 Genomes Project, or Exome Aggregation Consortium" - assignBA1(acmgEvidenceBuilder, frequencyData); - // PM3 "For recessive disorders, detected in trans with a pathogenic variant" assignPM3orBP2(acmgEvidenceBuilder, variantEvaluation, modeOfInheritance, contributingVariants, hasCompatibleDiseaseMatches); // PM4 Protein length changes as a result of in-frame deletions/insertions in a nonrepeat region or stop-loss variants @@ -365,6 +365,7 @@ private boolean possibleDeNovo(SampleGenotype ancestorGenotype, SampleGenotype p private void assignPM1(Map acmgEvidenceBuilder) { // TODO - need UniProt domain / site info and clinvar counts // can upgrade to STRONG + // https://www.cell.com/ajhg/fulltext/S0002-9297(22)00461-X suggests to limit the combined evidence from PM1 and PP3 to strong } /** @@ -372,7 +373,7 @@ private void assignPM1(Map acmgEvidenceBuilder) { */ private void assignPM2(AcmgEvidence.Builder acmgEvidenceBuilder, FrequencyData frequencyData) { if (!frequencyData.hasEspData() && !frequencyData.hasExacData() && !frequencyData.hasDbSnpData()) { - acmgEvidenceBuilder.add(PM2); + acmgEvidenceBuilder.add(PM2, Evidence.SUPPORTING); } // TODO: require disease incidence in carriers and penetrance to be able to calculate expected frequencies for AR } @@ -523,15 +524,6 @@ private void assignPP4(AcmgEvidence.Builder acmgEvidenceBuilder, List5% in Exome Sequencing Project, 1000 Genomes Project, or Exome Aggregation Consortium" - */ - private void assignBA1(AcmgEvidence.Builder acmgEvidenceBuilder, FrequencyData frequencyData) { - if (frequencyData.getMaxFreq() >= 5.0) { - acmgEvidenceBuilder.add(BA1); - } - } - /** * BS4 "Lack of segregation in affected members of a family" */ diff --git a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2020PointsBasedClassifier.java b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2020PointsBasedClassifier.java new file mode 100644 index 000000000..95eda7faf --- /dev/null +++ b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2020PointsBasedClassifier.java @@ -0,0 +1,58 @@ +package org.monarchinitiative.exomiser.core.analysis.util.acmg; + +/** + * This is recommended by the ClinGen Sequence Variant Interpretation (SVI) group + *

+ * Fitting a naturally scaled point system to the ACMG/AMP variant classification guidelines - Tavtigian et al. 2020, + * (DOI:10.1002/humu.24088). + *

+ */ +public class Acmg2020PointsBasedClassifier implements AcmgEvidenceClassifier { + + @Override + public AcmgClassification classify(AcmgEvidence acmgEvidence) { + + if (acmgEvidence.ba() == 1) { + return AcmgClassification.BENIGN; + } + + return classification(acmgEvidence.points()); + } + + protected AcmgClassification classification(double points) { + if (points >= 10) { + return AcmgClassification.PATHOGENIC; + } + if (points >= 6 && points <= 9) { + return AcmgClassification.LIKELY_PATHOGENIC; + } + if (points >= 0 && points <= 5) { + return AcmgClassification.UNCERTAIN_SIGNIFICANCE; + } + if (points >= -6 && points <= -1) { + return AcmgClassification.LIKELY_BENIGN; + } + // points <= -7 + return AcmgClassification.BENIGN; + } + + public double score(AcmgEvidence acmgEvidence) { + if (acmgEvidence.ba() == 1) { + return 0; + } + int maxPath = Math.min(scorePaths(acmgEvidence), 10); + int minBenign = Math.max(scoreBenign(acmgEvidence), -4); + + double score = ((maxPath - minBenign) - -4) / (double) (10 - -4); + return Math.max(Math.min(score, 1.0), 0.0); + } + + private int scorePaths(AcmgEvidence acmgEvidence) { + return acmgEvidence.pp() + (acmgEvidence.pm() * 2) + (acmgEvidence.ps() * 4) + (acmgEvidence.pvs() * 8); + } + + private int scoreBenign(AcmgEvidence acmgEvidence) { + return acmgEvidence.bp() + (acmgEvidence.bs() * 4); + } + +} diff --git a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgEvidence.java b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgEvidence.java index 01a578c74..442cbe3b4 100644 --- a/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgEvidence.java +++ b/exomiser-core/src/main/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgEvidence.java @@ -32,6 +32,21 @@ */ public class AcmgEvidence { + // These constants are derived in "Modeling the ACMG/AMP Variant Classification Guidelines as a Bayesian + // Classification Framework" Tavtigian et al. 2018, DOI:10.1038/gim.2017.210 + // https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6336098/bin/NIHMS915467-supplement-Supplemental_Table_S1.xlsx + + // Very_Strong == (2 * Strong) == (2 * Moderate) == (2 * Supporting) + // therefore points Supporting = 1, Moderate = 2, Strong = 4, Very_strong = 8 can be assigned and these fit to a + // Bayesian classification framework where (using the combining rules from Riggs et al. 2016) the posterior + // probabilities are Path >= 0.99, LikelyPath 0.90 - 0.98, LikelyBenign 0.1 - 0.01, Benign < 0.01 + + private static final double PRIOR_PROB = 0.1; + private static final double ODDS_PATH_VERY_STRONG = 350.0; + private static final double EXPONENTIAL_PROGRESSION = 2.0; + private static final double SUPPORTING_EVIDENCE_EXPONENT = Math.pow(EXPONENTIAL_PROGRESSION, -3); // 0.125 + private static final double ODDS_PATH_SUPPORTING = Math.pow(ODDS_PATH_VERY_STRONG, SUPPORTING_EVIDENCE_EXPONENT); // 2.08 + private static final AcmgEvidence EMPTY = new AcmgEvidence(Map.of()); @JsonProperty @@ -43,12 +58,17 @@ public class AcmgEvidence { private int pp = 0; private int ba = 0; + private int bvs = 0; private int bs = 0; + private int bm = 0; private int bp = 0; + private int points = 0; + private AcmgEvidence(Map evidence) { this.evidence = evidence == null || evidence.isEmpty() ? Map.of() : Collections.unmodifiableMap(new EnumMap<>(evidence)); countCriteriaEvidence(this.evidence); + points = pathPoints() - benignPoints(); } @JsonCreator @@ -89,9 +109,15 @@ private void countCriteriaEvidence(Map evidence) { case STAND_ALONE: ba++; break; + case VERY_STRONG: + bvs++; + break; case STRONG: bs++; break; + case MODERATE: + bm++; + break; case SUPPORTING: bp++; break; @@ -103,6 +129,21 @@ private void countCriteriaEvidence(Map evidence) { } } + public int pathPoints() { + return (int) (pp + pm * 2.0 + ps * 4.0 + pvs * 8.0); + } + + public int benignPoints() { + // n.b. here BA1 is given the equivalent weight as PVS1. This was *not* specified in the two papers. Specifically, + // in the 2018 paper they state "We excluded BA1, “benign stand alone” because it is used as absolute evidence + // that a variant is benign, irrespective of other evidence, which is contrary to Bayesian reasoning. The BA1 + // filter is useful for excluding a variant from entering a Bayesian framework, and will be addressed separately + // by the ClinGen Sequence Variant Interpretation (SVI) Working Group." + // Similarly, BM and BVS have been added here because it is possible to assign a Moderate, Strong or VeryStrong + // modifier to BP4 which will result in a VUS rather than LB if not included + return (int) (bp + bm * 2.0 + bs * 4.0 + bvs * 8.0 + ba * 8.0); + } + public boolean hasCriterion(AcmgCriterion acmgCriterion) { return evidence.containsKey(acmgCriterion); } @@ -144,14 +185,34 @@ public int ba() { return ba; } + public int bvs() { + return bvs; + } + public int bs() { return bs; } + public int bm() { + return bm; + } + public int bp() { return bp; } + public int points() { + return points; + } + + public double postProbPath() { + // Equation 2 from Tavtigian et al., 2020 (DOI: 10.1002/humu.24088) which is a re-written from of equation 5 from + // Tavtigian et al., 2018 (DOI: 10.1038/gim.2017.210) + double oddsPath = Math.pow(ODDS_PATH_SUPPORTING, points); + // posteriorProbability = (OddsPathogenicity*Prior P)/((OddsPathogenicity−1)*Prior_P+1) + return (oddsPath * PRIOR_PROB) / ((oddsPath - 1) * PRIOR_PROB + 1); + } + public static AcmgEvidence.Builder builder() { return new Builder(); } diff --git a/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acgs2020ClassifierTest.java b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acgs2020ClassifierTest.java index e7b396a62..aff317ed9 100644 --- a/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acgs2020ClassifierTest.java +++ b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acgs2020ClassifierTest.java @@ -91,6 +91,7 @@ void classifiesPathogenic(String criteria) { @CsvSource({ // (c) >=2 Strong "PS1 PS2", + "PVS1 PP1", // updated rule https://clinicalgenome.org/site/assets/files/5182/pm2_-_svi_recommendation_-_approved_sept2020.pdf // (b) 1 Strong (PS1–PS4) AND // 1–2 moderate (PM1–PM6) OR "PS1 PM1 PM2", @@ -143,7 +144,7 @@ void classifiesLikelyBenign(String criteria) { @ParameterizedTest @CsvSource({ // (i) Other criteria shown above are not met - "PVS1 PP1", +// "PVS1 PP1", // "PS1 PP1", // VUS_Hot "PM1 PM2 PP1", // VUS_Hot // "PM1 PP1 PP2 PP3", // VUS_Hot - classified as LP diff --git a/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015ClassifierTest.java b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015ClassifierTest.java index 64629e1d3..9d7dc52c2 100644 --- a/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015ClassifierTest.java +++ b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015ClassifierTest.java @@ -71,6 +71,7 @@ void classifiesPathogenic(String criteria) { @CsvSource({ // (i) 1 Very strong (PVS1) AND 1 moderate (PM1–PM6) "PVS1 PM1", + "PVS1 PP1", // updated rule https://clinicalgenome.org/site/assets/files/5182/pm2_-_svi_recommendation_-_approved_sept2020.pdf // OR (ii) 1 Strong (PS1–PS4) AND 1–2 moderate (PM1–PM6) "PS1 PM1 PM2", // OR (iii) 1 Strong (PS1–PS4) AND ≥2 supporting (PP1–PP5) @@ -121,7 +122,7 @@ void classifiesLikelyBenign(String criteria) { @ParameterizedTest @CsvSource({ // (i) Other criteria shown above are not met - "PVS1 PP1", +// "PVS1 PP1", - Changed to LP in https://clinicalgenome.org/site/assets/files/5182/pm2_-_svi_recommendation_-_approved_sept2020.pdf "PS1 PP1", // VUS_Hot "PM1 PM2 PP1", // VUS_Hot // "PM1 PP1 PP2 PP3", // VUS_Hot - classified as LP diff --git a/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015EvidenceAssignerTest.java b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015EvidenceAssignerTest.java index 0bce7474e..568d2a2e9 100644 --- a/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015EvidenceAssignerTest.java +++ b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2015EvidenceAssignerTest.java @@ -194,11 +194,11 @@ void testAssignsPM2() { AcmgEvidence acmgEvidence = instance.assignVariantAcmgEvidence(variantEvaluation, ModeOfInheritance.AUTOSOMAL_DOMINANT, List.of(variantEvaluation), List.of(cowdenSyndrome), List.of()); - assertThat(acmgEvidence, equalTo(AcmgEvidence.builder().add(AcmgCriterion.PM2).build())); + assertThat(acmgEvidence, equalTo(AcmgEvidence.builder().add(AcmgCriterion.PM2, Evidence.SUPPORTING).build())); } @Test - void testVariantMustBeInGeneWithKnownDiseaseAssociationForAcmgCriteriaToBeAssigned() { + void testVariantNeedNotBeInGeneWithKnownDiseaseAssociationForAcmgCriteriaToBeAssigned() { Acmg2015EvidenceAssigner instance = new Acmg2015EvidenceAssigner("proband", Pedigree.empty()); VariantEvaluation variantEvaluation = TestFactory.variantBuilder(1, 12345, "A", "G") // n.b. missing frequency data - should trigger PM2 @@ -207,7 +207,8 @@ void testVariantMustBeInGeneWithKnownDiseaseAssociationForAcmgCriteriaToBeAssign // Requires variant to be in gene associated with a disorder in order that any ACMG criteria can be applied AcmgEvidence acmgEvidence = instance.assignVariantAcmgEvidence(variantEvaluation, ModeOfInheritance.AUTOSOMAL_DOMINANT, List.of(variantEvaluation), List.of(), List.of()); - assertThat(acmgEvidence, equalTo(AcmgEvidence.empty())); + AcmgEvidence expected = AcmgEvidence.builder().add(PM2, Evidence.SUPPORTING).build(); + assertThat(acmgEvidence, equalTo(expected)); } @Test diff --git a/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2020PointsBasedClassifierTest.java b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2020PointsBasedClassifierTest.java new file mode 100644 index 000000000..1a1f57c7f --- /dev/null +++ b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/Acmg2020PointsBasedClassifierTest.java @@ -0,0 +1,189 @@ +package org.monarchinitiative.exomiser.core.analysis.util.acmg; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class Acmg2020PointsBasedClassifierTest { + + + private final AcmgEvidenceClassifier instance = new Acmg2020PointsBasedClassifier(); + + private AcmgEvidence parseAcmgEvidence(String criteria) { + AcmgEvidence.Builder acmgEvidenceBuilder = AcmgEvidence.builder(); + for (String criterion : criteria.split(" ")) { + String[] criteriaModifier = criterion.split("_"); + AcmgCriterion acmgCriterion = AcmgCriterion.valueOf(criteriaModifier[0]); + if (criteriaModifier.length == 2) { + AcmgCriterion.Evidence evidence = AcmgCriterion.Evidence.parseValue(criteriaModifier[1]); + acmgEvidenceBuilder.add(acmgCriterion, evidence); + } else { + acmgEvidenceBuilder.add(acmgCriterion); + } + } + return acmgEvidenceBuilder.build(); + } + + @ParameterizedTest + @CsvSource({ + "PVS1 PP5_VeryStrong", +// (a) 1 Very strong (PVS1) AND +// ≥1 Strong (PS1–PS4) OR + "PVS1 PS1", + "PVS1 PS2", + "PVS1 PS3", + "PVS1 PS4", + "PVS1 PS1 PS2", + "PVS1 PS1 PS2 PS3", +// ≥1 moderate (PM1–PM6) OR + "PVS1 PM1", + "PVS1 PM1 PM2", + "PVS1 PM1 PM2 PM3", + "PVS1 PM1 PM2 PM3 PM4", + "PVS1 PM1 PM2 PP1", +// ≥2 supporting (PP1–PP5) + "PVS1 PP1 PP2", + "PVS1 PP1 PP2 PP3", +// (b) ≥3 Strong (PS1-PS4) + "PS1 PS2 PS3", +// (c) 2 Strong (PS1-PS4) AND +// ≥1 moderate (PM1–PM6) OR + "PS1 PS2 PM1", +// ≥2 supporting (PP1–PP5) + "PS1 PS2 PP1 PP2", +// (d) 1 Strong (PS1-PS4) AND +// ≥3 moderate (PM1–PM6) OR + "PS1 PM1 PM2 PM3", +// ≥2 moderate (PM1–PM6) AND ≥2 supporting (PP1–PP5) OR + "PS1 PM1 PM2 PP1 PP2", +// ≥1 moderate (PM1–PM6) AND ≥4 supporting (PP1–PP5) + "PS1 PM1 PP1 PP2 PP3 PP4", + }) + void classifiesPathogenic(String criteria) { + AcmgEvidence acmgEvidence = parseAcmgEvidence(criteria); + assertThat(instance.classify(acmgEvidence), equalTo(AcmgClassification.PATHOGENIC)); + } + + @ParameterizedTest + @CsvSource({ + "PVS1 PP1", +// (c) >=2 Strong + "PS1 PS2", + "PS1 PS2 PP1", +// (b) 1 Strong (PS1–PS4) AND +// 1–2 moderate (PM1–PM6) OR + "PS1 PM1 PM2", + "PS1 PM1", +// ≥2 supporting (PP1–PP5) + "PS1 PP1 PP2", + "PS1 PP1 PP2 PP3", +// (c) ≥3 Moderate (PM1–PM6) OR + "PM1 PM2 PM3", +// 2 Moderate (PM1–PM6) AND ≥2 supporting (PP1–PP5) OR + "PM1 PM2 PP1 PP2", + "PM1 PM2 PP1 PP2 PP3", + "PM1 PM2 PP1 PP2 PP3 PP4", +// 1 Moderate (PM1–PM6) AND ≥4 supporting (PP1–PP5) + "PM1 PP1 PP2 PP3 PP4", + "PM1 PP1 PP2 PP3 PP4 PP5", + //PM2, PP3_Strong, PP4, PP5_Strong + }) + void classifiesLikelyPathogenic(String criteria) { + AcmgEvidence acmgEvidence = parseAcmgEvidence(criteria); + assertThat(instance.classify(acmgEvidence), equalTo(AcmgClassification.LIKELY_PATHOGENIC)); + } + + @ParameterizedTest + @CsvSource({ +// (i) Other criteria shown above are not met + "PS1 PP1", // VUS_Hot + "PM1 PM2 PP1", // VUS_Hot + "PM1 PP1 PP2 PP3", // VUS_Hot - classified as LP + "PS1", // VUS_Warm + "PM1 PM2", // VUS_Warm + "PM1 PP1 PP2", // VUS_Warm + "PP1 PP2 PP3 PP4", // VUS_Warm + "PM1 PP2", // VUS_Tepid + "PP1 PP2 PP3", // VUS_Warm + "PM1", // VUS_Cool + "PP1 PP2", // VUS_Warm + "PP1", // VUS_Cold +// OR (ii) the criteria for benign and pathogenic are contradictory + "PS1 BP1", +// "BS1 BP1 PVS1 PS1", // This is LP (12P, 5B = 7 points) +// "BA1 PVS1 PM1", // is this possible? + "BP1 BP2 BP3 PM1 PP1 PP2 PP3 PP4", + "PP1 BP1", + }) + void classifiesUncertain(String criteria) { + AcmgEvidence acmgEvidence = parseAcmgEvidence(criteria); + assertThat(instance.classify(acmgEvidence), equalTo(AcmgClassification.UNCERTAIN_SIGNIFICANCE)); + } + + @ParameterizedTest + @CsvSource({ +// (i) 1 Strong (BS1–BS4) and 1 supporting (BP1–BP7) + "BS1 BP1", +// OR (ii) ≥2 Supporting (BP1–BP7) + "BP1 BP2", + "BP4", + "BP4_Moderate", // This is possible + "BP1 BP2 BP3", + "BP4_Moderate BP6", + }) + void classifiesLikelyBenign(String criteria) { + AcmgEvidence acmgEvidence = parseAcmgEvidence(criteria); + assertThat(instance.classify(acmgEvidence), equalTo(AcmgClassification.LIKELY_BENIGN)); + } + + @ParameterizedTest + @CsvSource({ +// (i) 1 Stand-alone (BA1) + "BA1", + "BA1 BS1 BP1", +// OR (ii) ≥2 Strong (BS1–BS4) + "BS1 BS2", + "BS1 BS2 BS3" + }) + void classifiesBenign(String criteria) { + AcmgEvidence acmgEvidence = parseAcmgEvidence(criteria); + assertThat(instance.classify(acmgEvidence), equalTo(AcmgClassification.BENIGN)); + } + + @Test + void noEvidenceClassifiesUnknown() { + assertThat(instance.classify(AcmgEvidence.empty()), equalTo(AcmgClassification.UNCERTAIN_SIGNIFICANCE)); + } + + + @ParameterizedTest + @CsvSource({ + "11, PATHOGENIC", + "10, PATHOGENIC", + "9, LIKELY_PATHOGENIC", + "8, LIKELY_PATHOGENIC", + "7, LIKELY_PATHOGENIC", + "6, LIKELY_PATHOGENIC", + "5, UNCERTAIN_SIGNIFICANCE", + "4, UNCERTAIN_SIGNIFICANCE", + "3, UNCERTAIN_SIGNIFICANCE", + "2, UNCERTAIN_SIGNIFICANCE", + "1, UNCERTAIN_SIGNIFICANCE", + "0, UNCERTAIN_SIGNIFICANCE", + "-1, LIKELY_BENIGN", + "-2, LIKELY_BENIGN", + "-3, LIKELY_BENIGN", + "-4, LIKELY_BENIGN", + "-5, LIKELY_BENIGN", + "-6, LIKELY_BENIGN", + "-7, BENIGN", + "-8, BENIGN", + }) + void testPointsClassification(int points, AcmgClassification acmgClassification) { + assertThat(new Acmg2020PointsBasedClassifier().classification(points), equalTo(acmgClassification)); + } + +} \ No newline at end of file diff --git a/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgAssignmentCalculatorTest.java b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgAssignmentCalculatorTest.java index 7b1c7b928..309c0d4e5 100644 --- a/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgAssignmentCalculatorTest.java +++ b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgAssignmentCalculatorTest.java @@ -93,7 +93,7 @@ void calculatePathAcmgAssignments() { AcmgEvidence acmgEvidence = AcmgEvidence.builder() .add(AcmgCriterion.PVS1) - .add(AcmgCriterion.PM2) + .add(AcmgCriterion.PM2, AcmgCriterion.Evidence.SUPPORTING) .add(AcmgCriterion.PP3, AcmgCriterion.Evidence.STRONG) .add(AcmgCriterion.PP4) .add(AcmgCriterion.PP5, AcmgCriterion.Evidence.STRONG) @@ -167,12 +167,11 @@ void calculateBenignAcmgAssignments() { AcmgEvidence acmgEvidence = AcmgEvidence.builder() .add(AcmgCriterion.BA1) - .add(AcmgCriterion.BP4, AcmgCriterion.Evidence.VERY_STRONG) .build(); AcmgAssignment acmgAssignment = AcmgAssignment.of(variantEvaluation, gene.getGeneIdentifier(), ModeOfInheritance.AUTOSOMAL_DOMINANT, cowdenSyndrome, acmgEvidence, AcmgClassification.BENIGN); AcmgEvidenceAssigner acmgEvidenceAssigner = new Acmg2015EvidenceAssigner("proband", justProband("proband", MALE)); - AcmgAssignmentCalculator instance = new AcmgAssignmentCalculator(acmgEvidenceAssigner, new Acgs2020Classifier()); + AcmgAssignmentCalculator instance = new AcmgAssignmentCalculator(acmgEvidenceAssigner, new Acmg2020PointsBasedClassifier()); List acmgAssignments = instance.calculateAcmgAssignments(ModeOfInheritance.AUTOSOMAL_DOMINANT, gene, List.of(variantEvaluation), compatibleDiseaseMatches); assertThat(acmgAssignments, equalTo(List.of(acmgAssignment))); } diff --git a/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgEvidenceTest.java b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgEvidenceTest.java index 72e8860e7..f2568b849 100644 --- a/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgEvidenceTest.java +++ b/exomiser-core/src/test/java/org/monarchinitiative/exomiser/core/analysis/util/acmg/AcmgEvidenceTest.java @@ -21,10 +21,13 @@ package org.monarchinitiative.exomiser.core.analysis.util.acmg; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import java.util.Map; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; import static org.monarchinitiative.exomiser.core.analysis.util.acmg.AcmgCriterion.*; @@ -218,4 +221,46 @@ void testEvidence() { .build(); assertThat(instance.evidence(), equalTo(Map.of(PVS1, PVS1.evidence(), PM1, Evidence.VERY_STRONG))); } + + @ParameterizedTest + @CsvSource({ + "PVS1 PS1, 12, 0.999", + "PVS1 PM2 PM6 BP5, 11, 0.997", + "PVS1 PM2_Supporting, 9, 0.988", + "PS1 PS2 BP1, 7, 0.949", + "PS1 PS2 BP4 BP5, 6, 0.900", + "PS1 PS2 BS1, 4, 0.675", + "PM2 PP1, 3, 0.500", + "PM2, 2, 0.325", + "PP1, 1, 0.188", + "PP1 BP1, 0, 0.1", + "BP4, -1, 0.051", + "BP1, -1, 0.051", + "BP4_Moderate, -2, 0.025", + "BP1 BP2, -2, 0.025", + "BP4_Strong, -4, 0.006", + "BS1 BP1, -5, 0.003", + "BP4_VeryStrong, -8, 0.000", // There are no (current) recommendations where a Benign_VeryStrong is used, but it seems like a logical addition + "BA1, -8, 0.000", // BA1 is a bit of an anomaly - it's intended as a hard filter, so doesn't fit into the points system + }) + void testPosteriorProb(String criteria, int points, double posteriorProb) { + AcmgEvidence acmgEvidence = parseAcmgEvidence(criteria); + assertThat(acmgEvidence.points(), equalTo(points)); + assertThat(acmgEvidence.postProbPath(), closeTo(posteriorProb, 0.001)); + } + + private AcmgEvidence parseAcmgEvidence(String criteria) { + AcmgEvidence.Builder acmgEvidenceBuilder = AcmgEvidence.builder(); + for (String criterion : criteria.split(" ")) { + String[] criteriaModifier = criterion.split("_"); + AcmgCriterion acmgCriterion = AcmgCriterion.valueOf(criteriaModifier[0]); + if (criteriaModifier.length == 2) { + AcmgCriterion.Evidence evidence = AcmgCriterion.Evidence.parseValue(criteriaModifier[1]); + acmgEvidenceBuilder.add(acmgCriterion, evidence); + } else { + acmgEvidenceBuilder.add(acmgCriterion); + } + } + return acmgEvidenceBuilder.build(); + } } \ No newline at end of file