Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #4909 - Tavtigian Bayesian point sums #4920

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ About changelog [here](https://keepachangelog.com/en/1.0.0/)
- More SVI recommendation links on the ACMG page
- IGV buttons for SMN CN page
- Warnings on ACMG classifications for potentially conflicting classification pairs
- ACMG Bayesian foundation point scale after Tavtigian for variant heat profile
### Changed
- Variants query backend allows rank_score filtering
- Added script to tabulate causatives clinical filter rank
Expand Down
14 changes: 4 additions & 10 deletions scout/server/blueprints/variant/templates/variant/acmg.html
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,6 @@ <h4>Evidence of {{ category }}</h4>
</div>
{% endblock %}

{% macro preview() %}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused...

<div class="card panel-default">
<div class="card-body">
<span class="text-muted">Classification</span>
<h5>Likely Pathogenic</h5>
</div>
</div>
{% endmacro %}

{% block scripts %}
{{ super() }}

Expand Down Expand Up @@ -173,10 +164,13 @@ <h5>Likely Pathogenic</h5>
// add new selection
$('#acmg-' + data.classification).addClass('btn-primary');

var temperature_span = document.getElementById("temperature_span");
temperature_span.innerHTML = 'Score ' + data.points + ' <span class="fa ' + data.temperature_icon + '"></span> ' + data.temperature + ' (' + data.point_classification + ')'
temperature_span.className = 'badge bg-' + data.temperature_class

// Update any classification conflicts
var conflicts_div = document.getElementById("conflicts_div");
conflicts_div.innerHTML = data.conflicts.join("<br>");

});
}
</script>
Expand Down
6 changes: 4 additions & 2 deletions scout/server/blueprints/variant/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
)
from scout.server.extensions import loqusdb, store
from scout.server.utils import institute_and_case, public_endpoint, templated
from scout.utils.acmg import get_acmg, get_acmg_conflicts
from scout.utils.acmg import get_acmg, get_acmg_conflicts, get_acmg_temperature
from scout.utils.ensembl_rest_clients import EnsemblRestApiClient

LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -347,8 +347,10 @@ def acmg():
"""Calculate an ACMG classification from submitted criteria."""
criteria = request.args.getlist("criterion")
classification = get_acmg(criteria)

acmg_bayesian = get_acmg_temperature(criteria)
acmg_conflicts = get_acmg_conflicts(criteria)
return jsonify({"classification": classification, "conflicts": acmg_conflicts})
return jsonify({"classification": classification, "conflicts": acmg_conflicts, **acmg_bayesian})


@variant_bp.route(
Expand Down
162 changes: 135 additions & 27 deletions scout/utils/acmg.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# coding=UTF-8


from typing import Optional

from scout.constants import ACMG_COMPLETE_MAP
from scout.constants.acmg import ACMG_POTENTIAL_CONFLICTS


Expand Down Expand Up @@ -161,38 +165,31 @@ def is_likely_benign(bs_terms, bp_terms):
return False


def get_acmg(acmg_terms):
"""Use the algorithm described in ACMG paper to get a ACMG calssification

Modifying strength of a term is possible by adding a string describing its new level: "PP1_Strong" or
"PVS1_Moderate".

If no terms return None

Args:
acmg_terms(set(str)): A collection of prediction terms

Returns:
prediction(str): in ['uncertain_significance','benign','likely_benign',
'likely_pathogenic','pathogenic']

def get_acmg_criteria(acmg_terms: set) -> tuple:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored slightly to have one function to sort the terms, that can be reused by both the Richards and the Tavtigian classifier.

"""
if not acmg_terms:
return None
prediction = "uncertain_significance"
# This variable indicates if Pathogenecity Very Strong exists
Given a set of ACMG evidence criteria terms, that may be strength modified with suffixes.
For each term,
first, Strength modified criteria suffixes should count towards their modified score.
then, we need to see if we match any of the two stand-alone terms. If so, set their respective booleans.
finally, check remaining prefixes if no suffix match or stand-alone criteria match

Return a tuple with
pvs: This variable indicates if Pathogenicity Very Strong exists
ps_terms: Collection of terms with Pathogenicity Strong
pm_terms: Collection of terms with Pathogenicity moderate
pp_terms: Collection of terms with Pathogenicity supporting
ba: This variable indicates if Benign impact stand-alone exists
bs_terms: Collection of terms with Benign evidence Strong
bp_terms: Collection of terms with supporting Benign evidence
"""

pvs = False
# Collection of terms with Pathogenecity Strong
ps_terms = []
# Collection of terms with Pathogenecity moderate
pm_terms = []
# Collection of terms with Pathogenecity supporting
pp_terms = []
# This variable indicates if Benign impact stand-alone exists

ba = False
# Collection of terms with Benign evidence Strong
bs_terms = []
# Collection of terms with supporting Benign evidence
bp_terms = []

suffix_map = {
Expand Down Expand Up @@ -220,20 +217,47 @@ def get_acmg(acmg_terms):
continue
break
else:
# Do we match any of the two standalone terms
if term.startswith("PVS"):
pvs = True
elif term.startswith("BA"):
ba = True
else: # Check remaining prefixes if no suffix match or standalone criteria match
else:
for prefix, term_list in prefix_map.items():
if term.startswith(prefix):
term_list.append(term)
break

return (pvs, ps_terms, pm_terms, pp_terms, ba, bs_terms, bp_terms)


def get_acmg(acmg_terms: set) -> Optional[str]:
"""Use the algorithm described in ACMG paper (Richards 2015) to get a ACMG classification

Modifying strength of a term is possible by adding a string describing its new level: "PP1_Strong" or
"PVS1_Moderate".

BA is considered fully Stand Alone.

If no terms return None

Args:
acmg_terms(set(str)): A collection of prediction terms

Returns:
prediction(str): in ['uncertain_significance','benign','likely_benign',
'likely_pathogenic','pathogenic']

"""
if not acmg_terms:
return None

(pvs, ps_terms, pm_terms, pp_terms, ba, bs_terms, bp_terms) = get_acmg_criteria(acmg_terms)

if ba:
return "benign"

prediction = "uncertain_significance"

pathogenic = is_pathogenic(pvs, ps_terms, pm_terms, pp_terms)
likely_pathogenic = is_likely_pathogenic(pvs, ps_terms, pm_terms, pp_terms)
benign = is_benign(ba, bs_terms)
Expand All @@ -255,6 +279,90 @@ def get_acmg(acmg_terms):
return prediction


def get_acmg_temperature(acmg_terms: set) -> Optional[dict]:
"""
Use the algorithm described in Tavtigian 2020 to classifiy variants.

PVS 8 points, S 4, M 2, P 1.
This gives:

P > 10
LP 6 < p < 9
VUS 0 < p < 5
LB -1 < p < -6
B < -7

If no terms return None

Args:
acmg_terms(set(str)): A collection of prediction terms

Returns:
dict:
temperature:
(points, temperature, point_classification)

"""
TEMPERATURE_STRINGS = {
-1: {"label": "B/LB", "color": "success", "icon": "fa-times"},
0: {"label": "Ice cold", "color": "info", "icon": "fa-icicles"},
1: {"label": "Cold", "color": "info", "icon": "fa-snowman"},
2: {"label": "Cold", "color": "info", "icon": "fa-snowflake"},
3: {"label": "Tepid", "color": "yellow", "icon": "fa-temperature-half"},
4: {"label": "Warm", "color": "orange", "icon": "fa-mug-hot"},
5: {"label": "Hot", "color": "red", "icon": "fa-pepper-hot"},
6: {"label": "LP/P", "color": "danger", "icon": "fa-stethoscope"},
}

if not acmg_terms:
return {}

(pvs, ps_terms, pm_terms, pp_terms, ba, bs_terms, bp_terms) = get_acmg_criteria(acmg_terms)

if ba:
points = -8
else:
points = (
8 * pvs
+ 4 * len(ps_terms)
+ 2 * len(pm_terms)
+ len(pp_terms)
- 4 * len(bs_terms)
- len(bp_terms)
)

if points <= -7:
point_classification = "benign"
temperature_icon = TEMPERATURE_STRINGS[-1].get("icon")
elif points <= -1:
point_classification = "likely_benign"
temperature_icon = TEMPERATURE_STRINGS[-1].get("icon")
elif points <= 5:
point_classification = "uncertain_significance"
elif points <= 9:
point_classification = "likely_pathogenic"
temperature_icon = TEMPERATURE_STRINGS[6].get("icon")
elif points >= 10:
point_classification = "pathogenic"
temperature_icon = TEMPERATURE_STRINGS[6].get("icon")

temperature_class = ACMG_COMPLETE_MAP[point_classification].get("color")
temperature = ACMG_COMPLETE_MAP[point_classification].get("label")

if point_classification == "uncertain_significance":
temperature_class = TEMPERATURE_STRINGS[points].get("color")
temperature = TEMPERATURE_STRINGS[points].get("label")
temperature_icon = TEMPERATURE_STRINGS[points].get("icon")

return {
"points": points,
"temperature": temperature,
"temperature_class": temperature_class,
"temperature_icon": temperature_icon,
"point_classification": ACMG_COMPLETE_MAP[point_classification].get("short"),
}


def get_acmg_conflicts(acmg_terms: set) -> list:
"""Check potential conflict paris, return list of reference strings."""

Expand Down
39 changes: 30 additions & 9 deletions tests/utils/test_acmg.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from scout.utils.acmg import (
get_acmg,
get_acmg_conflicts,
get_acmg_temperature,
is_benign,
is_likely_benign,
is_likely_pathogenic,
Expand Down Expand Up @@ -385,47 +386,47 @@ def test_get_acmg_no_terms():


def test_get_acmg_pathogenic():
acmg_terms = ["PVS1", "PS1"]
acmg_terms = {"PVS1", "PS1"}
res = get_acmg(acmg_terms)
assert res == "pathogenic"

acmg_terms = ["PVS1", "PS1", "BS1"]
acmg_terms = {"PVS1", "PS1", "BS1"}
res = get_acmg(acmg_terms)
assert res == "pathogenic"


def test_get_acmg_modifier():
acmg_terms = ["PVS1", "PS1"]
acmg_terms = {"PVS1", "PS1"}
res = get_acmg(acmg_terms)
assert res == "pathogenic"

acmg_terms = ["PVS1_Moderate", "PS1"]
acmg_terms = {"PVS1_Moderate", "PS1"}
res = get_acmg(acmg_terms)
assert res == "likely_pathogenic"


def test_get_acmg_uncertain():
acmg_terms = ["PVS1"]
acmg_terms = {"PVS1"}
res = get_acmg(acmg_terms)
assert res == "uncertain_significance"

acmg_terms = ["PVS1", "PS1", "BS1", "BS2"]
acmg_terms = {"PVS1", "PS1", "BS1", "BS2"}
res = get_acmg(acmg_terms)
assert res == "uncertain_significance"


def test_get_acmg_stand_alone_benign():
acmg_terms = ["PVS1", "PS1", "BA1"]
acmg_terms = {"PVS1", "PS1", "BA1"}
res = get_acmg(acmg_terms)
assert res == "benign"

bs_terms = ["BA1", "BS1", "BP1"]
acmg_terms = {"BA1", "BS1", "BP1"}
res = get_acmg(acmg_terms)
assert res == "benign"


def test_acmg_modifier_on_both_benign_and_pathogenic():
acmg_terms = ["PS3_Moderate", "PP1_Moderate", "PP3", "BS1_Supporting"]
acmg_terms = {"PS3_Moderate", "PP1_Moderate", "PP3", "BS1_Supporting"}
res = get_acmg(acmg_terms)
assert res == "uncertain_significance"

Expand All @@ -440,3 +441,23 @@ def test_acmg_conflicts():
acmg_terms = {"PVS1", "PM4"}
conflicts = get_acmg_conflicts(acmg_terms)
assert len(conflicts) == 1


def test_acmg_temperature():
acmg_terms = {"PVS1", "PS1", "PP1", "BS1", "BS2"}
res = get_acmg_temperature(acmg_terms)
assert res["points"] == 5
assert res["temperature"] == "Hot"
assert res["point_classification"] == "VUS"

acmg_terms = {"PS3_Moderate", "PP1_Moderate", "PP3", "BS1_Supporting"}
res = get_acmg_temperature(acmg_terms)
assert res["points"] == 4
assert res["temperature"] == "Warm"
assert res["point_classification"] == "VUS"

acmg_terms = {"PVS1", "BS2", "BP1"}
res = get_acmg_temperature(acmg_terms)
assert res["points"] == 3
assert res["temperature"] == "Tepid"
assert res["point_classification"] == "VUS"
Loading