Skip to content

Commit e3e7df1

Browse files
authored
Merge pull request #5035 from broadinstitute/conflicting-clinvar-search
Conflicting clinvar search
2 parents 8397e82 + 2d8deec commit e3e7df1

File tree

8 files changed

+81
-44
lines changed

8 files changed

+81
-44
lines changed

clickhouse_search/fixtures/clickhouse_search.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"populations": [
4949
[35805, 0.29499999, 121372, 0.41530353, 0, 24061, 5872],
5050
[72672, 0.28899795, 251462, 0.4116475, 0, 11567],
51-
[0, 0, 0, 0, 0, 0],
51+
[927, 0.03444932, 26912, 0.04027665, 0, 0],
5252
[65154, 0.246152, 264690, 47604, 8775]
5353
],
5454
"sorted_transcript_consequences": [

clickhouse_search/managers.py

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
from seqr.utils.search.constants import INHERITANCE_FILTERS, ANY_AFFECTED, AFFECTED, UNAFFECTED, MALE_SEXES, \
1515
X_LINKED_RECESSIVE, REF_REF, REF_ALT, ALT_ALT, HAS_ALT, HAS_REF, SPLICE_AI_FIELD, SCREEN_KEY, UTR_ANNOTATOR_KEY, \
1616
EXTENDED_SPLICE_KEY, MOTIF_FEATURES_KEY, REGULATORY_FEATURES_KEY, CLINVAR_KEY, HGMD_KEY, NEW_SV_FIELD, \
17-
EXTENDED_SPLICE_REGION_CONSEQUENCE, CLINVAR_PATH_RANGES, CLINVAR_PATH_SIGNIFICANCES, PATH_FREQ_OVERRIDE_CUTOFF, \
17+
EXTENDED_SPLICE_REGION_CONSEQUENCE, CLINVAR_PATH_RANGES, CLINVAR_PATH_SIGNIFICANCES, CLINVAR_LIKELY_PATH_FILTER, \
18+
CLINVAR_CONFLICTING_P_LP, PATH_FREQ_OVERRIDE_CUTOFF, \
1819
HGMD_CLASS_FILTERS, SV_TYPE_FILTER_FIELD, SV_CONSEQUENCES_FIELD, COMPOUND_HET, COMPOUND_HET_ALLOW_HOM_ALTS
1920
from seqr.utils.xpos_utils import get_xpos, MIN_POS, MAX_POS
2021

@@ -42,6 +43,36 @@ def _clinvar_tuple(self):
4243
output_field=NamedTupleField(list(self.clinvar_fields.values()), null_if_empty=True, null_empty_arrays=True),
4344
)
4445

46+
@classmethod
47+
def _clinvar_path_q(cls, pathogenicity):
48+
clinvar_path_filters = [
49+
f for f in (pathogenicity or {}).get(CLINVAR_KEY) or [] if f in CLINVAR_PATH_SIGNIFICANCES
50+
]
51+
return cls._clinvar_filter_q(clinvar_path_filters) if clinvar_path_filters else None
52+
53+
@classmethod
54+
def _clinvar_filter_q(cls, clinvar_filters):
55+
ranges = [[None, None]]
56+
for path_filter, start, end in CLINVAR_PATH_RANGES:
57+
if path_filter in clinvar_filters:
58+
ranges[-1][1] = end
59+
if ranges[-1][0] is None:
60+
ranges[-1][0] = start
61+
elif ranges[-1] != [None, None]:
62+
ranges.append([None, None])
63+
ranges = [r for r in ranges if r[0] is not None]
64+
65+
clinvar_qs = [cls._clinvar_range_q(path_range) for path_range in ranges]
66+
67+
if CLINVAR_CONFLICTING_P_LP in clinvar_filters:
68+
max_path = next(end for path_filter, _, end in CLINVAR_PATH_RANGES if path_filter == CLINVAR_LIKELY_PATH_FILTER)
69+
clinvar_qs.append(cls._clinvar_conflicting_path_filter({1: (max_path, "{field} <= '{value}'")}))
70+
71+
clinvar_q = clinvar_qs[0]
72+
for q in clinvar_qs[1:]:
73+
clinvar_q |= q
74+
return clinvar_q
75+
4576
def _seqr_pop_fields(self, seqr_populations):
4677
sample_types = [
4778
sample_type.lower() for sample_type in
@@ -605,34 +636,13 @@ def _hgmd_filter(hgmd):
605636
return ('{field}__classification__range', (min_class, max_class))
606637
return ('{field}__classification__gt', min_class)
607638

608-
@classmethod
609-
def _clinvar_filter_q(cls, clinvar_filters, _get_range_q=None):
610-
ranges = [[None, None]]
611-
for path_filter, start, end in CLINVAR_PATH_RANGES:
612-
if path_filter in clinvar_filters:
613-
ranges[-1][1] = end
614-
if ranges[-1][0] is None:
615-
ranges[-1][0] = start
616-
elif ranges[-1] != [None, None]:
617-
ranges.append([None, None])
618-
ranges = [r for r in ranges if r[0] is not None]
619-
620-
clinvar_qs = [(_get_range_q or cls._clinvar_range_q)(path_range) for path_range in ranges]
621-
clinvar_q = clinvar_qs[0]
622-
for q in clinvar_qs[1:]:
623-
clinvar_q |= q
624-
return clinvar_q
625-
626-
@classmethod
627-
def _clinvar_range_q(cls, path_range):
639+
@staticmethod
640+
def _clinvar_range_q(path_range):
628641
return Q(clinvar__0__range=path_range, clinvar_key__isnull=False)
629642

630-
@classmethod
631-
def _clinvar_path_q(cls, pathogenicity, _get_range_q=None):
632-
clinvar_path_filters = [
633-
f for f in (pathogenicity or {}).get(CLINVAR_KEY) or [] if f in CLINVAR_PATH_SIGNIFICANCES
634-
]
635-
return cls._clinvar_filter_q(clinvar_path_filters, _get_range_q=_get_range_q) if clinvar_path_filters else None
643+
@staticmethod
644+
def _clinvar_conflicting_path_filter(conflicting_filter):
645+
return Q(clinvar__5__array_exists=conflicting_filter, clinvar_key__isnull=False)
636646

637647
def explode_gene_id(self, gene_id_key):
638648
consequence_field = self.GENE_CONSEQUENCE_FIELD if self.has_annotation(self.GENE_CONSEQUENCE_FIELD) else self.transcript_field
@@ -766,6 +776,14 @@ def result_values(self, sample_data=None):
766776
def _has_clinvar(self):
767777
return hasattr(self.model, 'clinvar_join')
768778

779+
@staticmethod
780+
def _clinvar_range_q(path_range):
781+
return Q(clinvar_join__pathogenicity__range=path_range)
782+
783+
@staticmethod
784+
def _clinvar_conflicting_path_filter(conflicting_filter):
785+
return Q(clinvar_join__conflicting_pathogenicities__array_exists=conflicting_filter)
786+
769787
def _search_call_data(self, entries, sample_data, inheritance_mode=None, inheritance_filter=None, qualityFilter=None, pathogenicity=None, annotate_carriers=False, annotate_hom_alts=False, skip_individual_guid=False, **kwargs):
770788
project_guids = sample_data['project_guids']
771789
project_filter = Q(project_guid__in=project_guids) if len(project_guids) > 1 else Q(project_guid=project_guids[0])
@@ -797,9 +815,7 @@ def _search_call_data(self, entries, sample_data, inheritance_mode=None, inherit
797815
quality_filter = qualityFilter or {}
798816
individual_genotype_filter = (inheritance_filter or {}).get('genotype')
799817
if inheritance_mode or individual_genotype_filter or quality_filter:
800-
clinvar_override_q = AnnotationsQuerySet._clinvar_path_q(
801-
pathogenicity, _get_range_q=lambda path_range: Q(clinvar_join__pathogenicity__range=path_range),
802-
) if self._has_clinvar() else None
818+
clinvar_override_q = self._clinvar_path_q(pathogenicity) if self._has_clinvar() else None
803819
inheritance_q, quality_q, gt_filter, family_missing_type_samples, unaffected_samples = self._get_inheritance_quality_qs(
804820
sample_data, multi_sample_type_families, inheritance_mode, individual_genotype_filter, quality_filter, clinvar_override_q,
805821
annotate_carriers, inheritance_filter=inheritance_filter or {},

clickhouse_search/search_tests.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -576,14 +576,22 @@ def test_quality_filter(self):
576576
{},
577577
]
578578

579+
quality_filter['min_gq'] = 50
580+
self._assert_expected_search(
581+
[VARIANT1, selected_family_3_variant, MITO_VARIANT1, MITO_VARIANT3], quality_filter=quality_filter,
582+
annotations=annotations, pathogenicity={'clinvar': ['likely_pathogenic', 'vus_or_conflicting']},
583+
cached_variant_fields=cached_variant_fields[:1] + cached_variant_fields[2:],
584+
)
585+
579586
self._assert_expected_search(
580587
[VARIANT1, VARIANT2, selected_family_3_variant, MITO_VARIANT1, MITO_VARIANT3], quality_filter=quality_filter,
581-
annotations=annotations, pathogenicity={'clinvar': ['likely_pathogenic', 'vus_or_conflicting']}, cached_variant_fields=cached_variant_fields,
588+
annotations=annotations, pathogenicity={'clinvar': ['likely_pathogenic', 'conflicting_p_lp', 'vus_or_conflicting']},
589+
cached_variant_fields=cached_variant_fields,
582590
)
583591

584592
self._assert_expected_search(
585593
[VARIANT2, selected_family_3_variant, MITO_VARIANT1], quality_filter=quality_filter,
586-
annotations=annotations, pathogenicity={'clinvar': ['pathogenic']}, cached_variant_fields=cached_variant_fields[1:],
594+
annotations=annotations, pathogenicity={'clinvar': ['pathogenic', 'conflicting_p_lp']}, cached_variant_fields=cached_variant_fields[1:],
587595
)
588596

589597
self._set_grch37_search()
@@ -802,14 +810,14 @@ def test_frequency_filter(self):
802810

803811
annotations = {'splice_ai': '0.0'} # Ensures no variants are filtered out by annotation/path filters
804812
self._assert_expected_search(
805-
[VARIANT1, VARIANT2, VARIANT4, MITO_VARIANT1],
813+
[VARIANT1, VARIANT4, MITO_VARIANT1],
806814
freqs={'gnomad_genomes': {'af': 0.01, 'hh': 10}, 'gnomad_mito': {'af': 0.01}},
807815
annotations=annotations, pathogenicity={'clinvar': ['pathogenic', 'likely_pathogenic', 'vus_or_conflicting']},
808816
)
809817

810818
self._assert_expected_search(
811819
[VARIANT2, VARIANT4, MITO_VARIANT1], freqs={'gnomad_genomes': {'af': 0.01}, 'gnomad_mito': {'af': 0.01}},
812-
annotations=annotations, pathogenicity={'clinvar': ['pathogenic', 'vus_or_conflicting']},
820+
annotations=annotations, pathogenicity={'clinvar': ['pathogenic', 'conflicting_p_lp', 'vus_or_conflicting']},
813821
)
814822

815823
def test_annotations_filter(self):
@@ -821,6 +829,8 @@ def test_annotations_filter(self):
821829
[VARIANT1, VARIANT2, MITO_VARIANT1, MITO_VARIANT3], pathogenicity=pathogenicity,
822830
)
823831

832+
self._assert_expected_search([VARIANT2], pathogenicity={'clinvar': ['conflicting_p_lp']})
833+
824834
exclude = {'clinvar': pathogenicity['clinvar'][1:]}
825835
pathogenicity['clinvar'] = pathogenicity['clinvar'][:1]
826836
snv_38_only_annotations = {'SCREEN': ['CTCF-only', 'DNase-only'], 'UTRAnnotator': ['5_prime_UTR_stop_codon_loss_variant']}
@@ -1256,7 +1266,7 @@ def test_sort(self):
12561266
)
12571267

12581268
self._assert_expected_search(
1259-
[VARIANT2, MITO_VARIANT1, MITO_VARIANT2, VARIANT4, VARIANT1, MITO_VARIANT3, VARIANT3, GCNV_VARIANT1, GCNV_VARIANT2, GCNV_VARIANT3, GCNV_VARIANT4],
1269+
[MITO_VARIANT1, MITO_VARIANT2, VARIANT4, VARIANT1, VARIANT2, MITO_VARIANT3, VARIANT3, GCNV_VARIANT1, GCNV_VARIANT2, GCNV_VARIANT3, GCNV_VARIANT4],
12601270
sort='gnomad',
12611271
)
12621272

clickhouse_search/test_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
'topmed': {'af': 0.246152, 'ac': 65154, 'an': 264690, 'hom': 8775, 'het': 47604},
129129
'exac': {'af': 0.29499999, 'ac': 35805, 'an': 121372, 'hom': 5872, 'hemi': 0, 'het': 24061, 'filter_af': 0.41530353},
130130
'gnomad_exomes': {'af': 0.28899795, 'ac': 72672, 'an': 251462, 'hom': 11567, 'hemi': 0, 'filter_af': 0.4116475},
131-
'gnomad_genomes': {'af': 0, 'ac': 0, 'an': 0, 'hom': 0, 'hemi': 0, 'filter_af': 0},
131+
'gnomad_genomes': {'af': 0.03444932, 'ac': 927, 'an': 26912, 'hom': 0, 'hemi': 0, 'filter_af': 0.04027665},
132132
},
133133
'predictions': {
134134
'cadd': 20.9,

seqr/fixtures/variant_searches.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@
8080
],
8181
"clinvar": [
8282
"pathogenic",
83-
"likely_pathogenic"
83+
"likely_pathogenic",
84+
"conflicting_p_lp"
8485
]
8586
},
8687
"qualityFilter": {
@@ -175,7 +176,8 @@
175176
],
176177
"clinvar": [
177178
"pathogenic",
178-
"likely_pathogenic"
179+
"likely_pathogenic",
180+
"conflicting_p_lp"
179181
]
180182
},
181183
"qualityFilter": {
@@ -321,6 +323,7 @@
321323
"clinvar": [
322324
"pathogenic",
323325
"likely_pathogenic",
326+
"conflicting_p_lp",
324327
"vus_or_conflicting"
325328
]
326329
},
@@ -422,6 +425,7 @@
422425
"clinvar": [
423426
"pathogenic",
424427
"likely_pathogenic",
428+
"conflicting_p_lp",
425429
"vus_or_conflicting"
426430
]
427431
},

seqr/utils/search/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
CLINVAR_KEY = 'clinvar'
6464
CLINVAR_PATH_FILTER = 'pathogenic'
6565
CLINVAR_LIKELY_PATH_FILTER = 'likely_pathogenic'
66-
CLINVAR_PATH_SIGNIFICANCES = {CLINVAR_PATH_FILTER, CLINVAR_LIKELY_PATH_FILTER}
66+
CLINVAR_CONFLICTING_P_LP = 'conflicting_p_lp'
67+
CLINVAR_PATH_SIGNIFICANCES = {CLINVAR_PATH_FILTER, CLINVAR_LIKELY_PATH_FILTER, CLINVAR_CONFLICTING_P_LP}
6768
PATH_FREQ_OVERRIDE_CUTOFF = 0.05
6869
CLINVAR_PATH_RANGES = [
6970
(CLINVAR_PATH_FILTER, 'Pathogenic', 'Pathogenic/Likely_risk_allele'),

seqr/views/apis/variant_search_api_tests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ def test_query_variants(self, mock_get_variants, mock_get_gene_counts, mock_erro
528528
['12', '48367227', 'TC', 'T', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
529529
'', '2', 'AIP (None)|Known gene for phenotype (None)|Excluded (None)', 'a later note (None)|test n\xf8te (None)', '', '', '', '', '', '',
530530
'', '', '', '', '', '', '', '', '', '', '', ''],
531-
['1', '38724419', 'T', 'G', 'ENSG00000177000', 'missense_variant', '10', '0.29499999', '0',
531+
['1', '38724419', 'T', 'G', 'ENSG00000177000', 'missense_variant', '10', '0.29499999', '0.03444932',
532532
'0.28899795', '0.246152', '20.9', '0.197',
533533
'2.001', '0.0', '0.1', '0.05', '', '', 'rs1801131', 'ENST00000383791.8:c.156A>C',
534534
'ENSP00000373301.3:p.Leu52Phe', 'Conflicting_classifications_of_pathogenicity', '1', '2', '', '', '', '', '', 'HG00731', '2', '', '99', '1.0',
@@ -562,7 +562,7 @@ def test_query_variants(self, mock_get_variants, mock_get_gene_counts, mock_erro
562562
['12', '48367227', 'TC', 'T', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
563563
'2', 'AIP (None)|Known gene for phenotype (None)|Excluded (None)', 'a later note (None)|test n\xf8te (None)',
564564
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '',],
565-
['1', '38724419', 'T', 'G', 'ENSG00000177000', 'missense_variant', '10', '0.29499999', '0',
565+
['1', '38724419', 'T', 'G', 'ENSG00000177000', 'missense_variant', '10', '0.29499999', '0.03444932',
566566
'0.28899795', '0.246152', '20.9', '0.197',
567567
'2.001', '0.0', '0.1', '0.05', '', '', 'rs1801131', 'ENST00000383791.8:c.156A>C',
568568
'ENSP00000373301.3:p.Leu52Phe', 'Conflicting_classifications_of_pathogenicity', '1', '2', '', '', 'HG00731', '2', '', '99', '1.0',

ui/pages/Search/constants.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export const INHERITANCE_FILTER_JSON_OPTIONS = INHERITANCE_FILTER_OPTIONS.map(
135135
const CLINVAR_NAME = 'clinvar'
136136
const CLIVAR_PATH = 'pathogenic'
137137
const CLINVAR_LIKELY_PATH = 'likely_pathogenic'
138+
const CLINVAR_CONFLICTING_P_LP = 'conflicting_p_lp'
138139
const CLINVAR_UNCERTAIN = 'vus_or_conflicting'
139140
const CLINVAR_OPTIONS = [
140141
{
@@ -145,6 +146,11 @@ const CLINVAR_OPTIONS = [
145146
text: 'Likely Pathogenic (LP)',
146147
value: CLINVAR_LIKELY_PATH,
147148
},
149+
{
150+
description: 'Clinvar variant of with conflicting interpretations, at least one of which is Pathogenic (P) or Likely Pathogenic (LP)',
151+
text: 'Conflicting with P/LP',
152+
value: CLINVAR_CONFLICTING_P_LP,
153+
},
148154
{
149155
description: 'Clinvar variant of uncertain significance or variant with conflicting interpretations',
150156
text: 'VUS or Conflicting',
@@ -214,14 +220,14 @@ export const HGMD_PATHOGENICITY_FILTER_OPTIONS = [
214220
{
215221
text: 'Pathogenic/ Likely Path.',
216222
value: {
217-
[CLINVAR_NAME]: [CLIVAR_PATH, CLINVAR_LIKELY_PATH],
223+
[CLINVAR_NAME]: [CLIVAR_PATH, CLINVAR_LIKELY_PATH, CLINVAR_CONFLICTING_P_LP],
218224
[HGMD_NAME]: [HGMD_DM],
219225
},
220226
},
221227
{
222228
text: 'Not Benign',
223229
value: {
224-
[CLINVAR_NAME]: [CLIVAR_PATH, CLINVAR_LIKELY_PATH, CLINVAR_UNCERTAIN],
230+
[CLINVAR_NAME]: [CLIVAR_PATH, CLINVAR_LIKELY_PATH, CLINVAR_CONFLICTING_P_LP, CLINVAR_UNCERTAIN],
225231
[HGMD_NAME]: HGMD_OPTIONS.map(({ value }) => value),
226232
},
227233
},

0 commit comments

Comments
 (0)