Skip to content

Commit

Permalink
feat: Update ACMG criteria for CNVs (#76) (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
gromdimon authored Oct 11, 2023
1 parent 22b2001 commit 0b572f6
Show file tree
Hide file tree
Showing 14 changed files with 2,158 additions and 922 deletions.
2 changes: 2 additions & 0 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ sqlalchemy = "*"
tenacity = "*"
uvicorn = "*"
alembic = "*"
requests = "*"
types-requests = "*"

[dev-packages]
aiosqlite = "*"
Expand Down
516 changes: 255 additions & 261 deletions backend/Pipfile.lock

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions backend/app/api/internal/endpoints/remote.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
"""Reverse proxies to external/remote services."""

import ssl

import httpx
import requests
import urllib3
from fastapi import APIRouter, BackgroundTasks, Request, Response
from fastapi.responses import JSONResponse, StreamingResponse
from starlette.background import BackgroundTask
Expand Down Expand Up @@ -99,3 +103,45 @@ async def acmg(request: Request):
if key.lower() in acmg_rating:
acmg_rating[key.lower()] = value == 1
return JSONResponse(acmg_rating)


class CustomHttpAdapter(requests.adapters.HTTPAdapter):
# "Transport adapter" that allows us to use custom ssl_context.

def __init__(self, ssl_context=None, **kwargs):
self.ssl_context = ssl_context
super().__init__(**kwargs)

def init_poolmanager(self, connections, maxsize, block=False):
self.poolmanager = urllib3.poolmanager.PoolManager(
num_pools=connections, maxsize=maxsize, block=block, ssl_context=self.ssl_context
)


def get_legacy_session():
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
ctx.options |= 0x4 # OP_LEGACY_SERVER_CONNECT
session = requests.session()
session.mount("https://", CustomHttpAdapter(ctx))
return session


@router.get("/cnv/acmg/{path:path}")
async def cnv_acmg(request: Request):
"""Implement searching for ACMG classification for CNVs."""
query_params = request.query_params
chromosome = query_params.get("chromosome")
start = query_params.get("start")
end = query_params.get("end")
func = query_params.get("func")

if not chromosome or not start or not end or not func:
return Response(status_code=400, content="Missing query parameters")

backend_resp = get_legacy_session().post(
"https://phoenix.bgi.com/api/acit/jobs/",
data={"chromosome": chromosome, "start": start, "end": end, "func": func, "error": 0},
)
if backend_resp.status_code != 200:
return Response(status_code=backend_resp.status_code, content=backend_resp.content)
return JSONResponse(backend_resp.json())
248 changes: 248 additions & 0 deletions frontend/src/components/SvDetails/AcmgRating.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import {
ACMG_CRITERIA_CNV_DEFS,
ACMG_CRITERIA_CNV_GAIN,
ACMG_CRITERIA_CNV_LOSS,
AcmgCriteriaCNVGain,
AcmgCriteriaCNVLoss,
Presence,
StateSourceCNV
} from '@/lib/acmgCNV'
import { StoreState } from '@/stores/misc'
import { useSvAcmgRatingStore } from '@/stores/svAcmgRating'
import type { SvRecord } from '@/stores/svInfo'
const props = defineProps({
svRecord: Object as () => SvRecord | undefined
})
const acmgRatingStore = useSvAcmgRatingStore()
const resetAcmgRating = () => {
if (!acmgRatingStore.acmgRating) {
return
}
acmgRatingStore.acmgRating.setUserToAutoCNV()
}
const calculateAcmgClass = computed((): string => {
if (!acmgRatingStore.acmgRating) {
return ''
}
const [acmgClass] = acmgRatingStore.acmgRating.getAcmgClass()
return acmgClass
})
const calculateAcmgScore = computed((): number => {
if (!acmgRatingStore.acmgRating) {
return 0
}
const [, score] = acmgRatingStore.acmgRating.getAcmgClass()
return score
})
watch(
() => [props.svRecord, acmgRatingStore.storeState],
async () => {
if (props.svRecord && acmgRatingStore.storeState === StoreState.Active) {
await acmgRatingStore.setAcmgRating(props.svRecord)
}
}
)
onMounted(async () => {
if (props.svRecord) {
await acmgRatingStore.setAcmgRating(props.svRecord)
}
})
const switchCriteria = (
criteria: AcmgCriteriaCNVLoss | AcmgCriteriaCNVGain,
presence: Presence
) => {
if (presence === Presence.Present) {
acmgRatingStore.acmgRating.setPresence(StateSourceCNV.User, criteria, Presence.Absent)
} else {
acmgRatingStore.acmgRating.setPresence(StateSourceCNV.User, criteria, Presence.Present)
}
// Unset conflicting criteria
const conflictingEvidence = ACMG_CRITERIA_CNV_DEFS.get(criteria)?.conflictingEvidence
if (conflictingEvidence) {
for (const conflictingCriteria of conflictingEvidence) {
acmgRatingStore.acmgRating.setPresence(
StateSourceCNV.User,
conflictingCriteria,
Presence.Absent
)
}
}
}
</script>

<template>
<div v-if="acmgRatingStore !== undefined">
<v-row>
<v-col cols="12" md="3"></v-col>
<v-col cols="12" md="6" class="section">
<div>
<div>
<h2 for="acmg-class"><strong>ACMG classification:</strong></h2>
</div>
<h1 title="Automatically determined ACMG class (Richards et al., 2015)">
{{ calculateAcmgClass }} with score: {{ calculateAcmgScore }}
</h1>
<router-link to="/acmg-docs" target="_blank">
Further documentation <v-icon>mdi-open-in-new</v-icon>
</router-link>
</div>
<div class="button-group">
<v-btn color="black" variant="outlined" @click="resetAcmgRating()"> Reset </v-btn>
</div>
<div>
<div>
<div>
<v-icon>mdi-information-outline</v-icon>
Select all fulfilled criteria to get the classification following Rooney Riggs
<i>et al.</i> (2020).
</div>
</div>
</div>
</v-col>
<v-col cols="12" md="3"></v-col>
</v-row>
<v-row>
<v-col class="d-flex flex-row flex-wrap">
<v-table>
<thead>
<tr>
<th width="20%">Evidence</th>
<th width="74%">Description</th>
<th width="3%">Suggested points</th>
<th width="3%">Max score</th>
</tr>
</thead>
<tbody v-if="props.svRecord?.svType === 'DEL'">
<tr v-for="criteria in ACMG_CRITERIA_CNV_LOSS" :key="criteria">
<td>
<v-switch
:label="criteria"
:model-value="
acmgRatingStore.acmgRating.getCriteriaCNVState(criteria).presence ===
Presence.Present
"
@update:model-value="
switchCriteria(
criteria,
acmgRatingStore.acmgRating.getCriteriaCNVState(criteria).presence
)
"
color="primary"
hide-details="auto"
density="compact"
class="switch"
>
</v-switch>
<div v-if="ACMG_CRITERIA_CNV_DEFS.get(criteria)?.slider">
<v-slider
:model-value="
acmgRatingStore.acmgRating.getCriteriaCNVState(criteria).score ?? 0
"
@update:model-value="
acmgRatingStore.acmgRating.setScore(StateSourceCNV.User, criteria, $event)
"
:min="ACMG_CRITERIA_CNV_DEFS.get(criteria)?.minScore ?? 0"
:max="ACMG_CRITERIA_CNV_DEFS.get(criteria)?.maxScore ?? 0"
:step="0.05"
thumb-label
thumb-size="10"
class="slider"
/>
</div>
</td>
<td>
{{ ACMG_CRITERIA_CNV_DEFS.get(criteria)?.hint }}
<br />
{{ ACMG_CRITERIA_CNV_DEFS.get(criteria)?.description ?? '' }}
<br />
Conflicting evidence:
{{ ACMG_CRITERIA_CNV_DEFS.get(criteria)?.conflictingEvidence }}
</td>
<td>{{ ACMG_CRITERIA_CNV_DEFS.get(criteria)?.defaultScore }}</td>
<td>{{ ACMG_CRITERIA_CNV_DEFS.get(criteria)?.maxScore }}</td>
</tr>
</tbody>
<tbody v-else-if="props.svRecord?.svType === 'DUP'">
<tr v-for="criteria in ACMG_CRITERIA_CNV_GAIN" :key="criteria">
<td>
<v-switch
:label="criteria"
:model-value="
acmgRatingStore.acmgRating.getCriteriaCNVState(criteria).presence ===
Presence.Present
"
@update:model-value="
switchCriteria(
criteria,
acmgRatingStore.acmgRating.getCriteriaCNVState(criteria).presence
)
"
color="primary"
hide-details="auto"
density="compact"
class="switch"
>
</v-switch>
<div v-if="ACMG_CRITERIA_CNV_DEFS.get(criteria)?.slider">
<v-slider
:model-value="
acmgRatingStore.acmgRating.getCriteriaCNVState(criteria).score ?? 0
"
@update:model-value="
acmgRatingStore.acmgRating.setScore(StateSourceCNV.User, criteria, $event)
"
:min="ACMG_CRITERIA_CNV_DEFS.get(criteria)?.minScore ?? 0"
:max="ACMG_CRITERIA_CNV_DEFS.get(criteria)?.maxScore ?? 1"
:step="0.05"
thumb-label
thumb-size="10"
class="slider"
/>
</div>
</td>
<td>
{{ ACMG_CRITERIA_CNV_DEFS.get(criteria)?.hint }}
<br />
{{ ACMG_CRITERIA_CNV_DEFS.get(criteria)?.description ?? '' }}
<br />
Conflicting evidence:
{{ ACMG_CRITERIA_CNV_DEFS.get(criteria)?.conflictingEvidence }}
</td>
<td>{{ ACMG_CRITERIA_CNV_DEFS.get(criteria)?.defaultScore }}</td>
<td>{{ ACMG_CRITERIA_CNV_DEFS.get(criteria)?.maxScore }}</td>
</tr>
</tbody>
</v-table>
</v-col>
</v-row>
</div>
<div v-else>
<div class="d-flex align-center justify-center" style="min-height: 300px">
<h3>Loading ACMG information</h3>
<v-progress-circular indeterminate></v-progress-circular>
</div>
</div>
</template>
<style scoped>
.switch {
margin-left: 10px;
padding: 0px;
}
.slider {
margin-top: 10px;
}
</style>
2 changes: 1 addition & 1 deletion frontend/src/components/SvDetails/SvGenes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ const onRowClicked = (event: Event, { item }: { item: GeneInfo }): void => {

<template>
<div>
<div>
<div style="max-width: 1300px">
<v-data-table
v-model:items-per-page="itemsPerPage"
:headers="headers"
Expand Down
Loading

0 comments on commit 0b572f6

Please sign in to comment.