Skip to content

Commit

Permalink
Change for issue #502 - downgrade PM2 - PM2_Supporting for variants l…
Browse files Browse the repository at this point in the history
…acking frequency information.

Update Acgs2020Classifier and Acmg2015Classifier to allow for PVS1 and PM2_Supporting are sufficient to trigger LIKELY_PATHOGENIC
Change for issue #514 update AcmgEvidence to fit a Bayesian points-based system
Add new Acmg2020PointsBasedClassifier to classify AcmgEvidence based on points
  • Loading branch information
julesjacobsen committed Sep 6, 2023
1 parent b4250c5 commit 1625dc0
Show file tree
Hide file tree
Showing 13 changed files with 390 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -101,6 +101,15 @@ private GeneScore calculateGeneScore(Gene gene, ModeOfInheritance modeOfInherita

GenePriorityScoreCalculator.GenePriorityScore priorityScore = genePriorityScoreCalculator.calculateGenePriorityScore(gene, modeOfInheritance);

List<ModelPhenotypeMatch<Disease>> compatibleDiseaseMatches = priorityScore.getCompatibleDiseaseMatches();

List<AcmgAssignment> 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()
Expand All @@ -110,10 +119,6 @@ private GeneScore calculateGeneScore(Gene gene, ModeOfInheritance modeOfInherita

double pValue = pValueCalculator.calculatePvalueFromCombinedScore(combinedScore);

List<ModelPhenotypeMatch<Disease>> compatibleDiseaseMatches = priorityScore.getCompatibleDiseaseMatches();

List<AcmgAssignment> acmgAssignments = acmgAssignmentCalculator.calculateAcmgAssignments(modeOfInheritance, gene, contributingVariants, compatibleDiseaseMatches);

return GeneScore.builder()
.geneIdentifier(gene.getGeneIdentifier())
.modeOfInheritance(modeOfInheritance)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VariantEvaluation> contributingVariants, List<Disease> knownDiseases, List<ModelPhenotypeMatch<Disease>> 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();

Expand All @@ -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
Expand Down Expand Up @@ -395,14 +395,15 @@ private boolean possibleDeNovo(SampleGenotype ancestorGenotype, SampleGenotype p
private void assignPM1(Map<AcmgCriterion, Evidence> 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
}

/**
* PM2 "Absent from controls (or at extremely low frequency if recessive) in Exome Sequencing Project, 1000 Genomes Project, or Exome Aggregation Consortium"
*/
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
}
Expand Down Expand Up @@ -553,15 +554,6 @@ private void assignPP4(AcmgEvidence.Builder acmgEvidenceBuilder, List<ModelPheno
}
}

/**
* BA1 "Allele frequency is >5% 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"
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.monarchinitiative.exomiser.core.analysis.util.acmg;

/**
* This is recommended by the ClinGen <a href = "https://clinicalgenome.org/working-groups/sequence-variant-interpretation/">Sequence Variant Interpretation (SVI)</a> group
* <p>
* Fitting a naturally scaled point system to the ACMG/AMP variant classification guidelines - Tavtigian et al. 2020,
* (DOI:10.1002/humu.24088).
* </p>
*/
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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<AcmgCriterion, Evidence> evidence) {
this.evidence = evidence == null || evidence.isEmpty() ? Map.of() : Collections.unmodifiableMap(new EnumMap<>(evidence));
countCriteriaEvidence(this.evidence);
points = pathPoints() - benignPoints();
}

@JsonCreator
Expand Down Expand Up @@ -89,9 +109,15 @@ private void countCriteriaEvidence(Map<AcmgCriterion, Evidence> evidence) {
case STAND_ALONE:
ba++;
break;
case VERY_STRONG:
bvs++;
break;
case STRONG:
bs++;
break;
case MODERATE:
bm++;
break;
case SUPPORTING:
bp++;
break;
Expand All @@ -103,6 +129,21 @@ private void countCriteriaEvidence(Map<AcmgCriterion, Evidence> 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);
}
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 1625dc0

Please sign in to comment.