Skip to content

Commit 47faad2

Browse files
committed
Fixes to signal swapping/priority logic
1 parent 29b30ea commit 47faad2

3 files changed

Lines changed: 308 additions & 126 deletions

File tree

colocus/api/filters.py

Lines changed: 162 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717

1818
from colocus.core import models
19-
from colocus.core.constants import ANALYSIS_TYPES, GWAS, EQTL, MEQTL, METABQTL, PQTL
19+
from colocus.core.constants import ANALYSIS_TYPES
2020

2121

2222
def parse_region(region):
@@ -102,6 +102,92 @@ def filter_queryset(self, queryset):
102102
Q(**{f'{field_name}': float('-inf')}) | Q(**{f'{field_name}__isnull': True})
103103
)
104104

105+
# Add conditional annotations for ordering
106+
# These are necessary because on a per-row basis, signals may be swapped depending on user preference
107+
# (e.g. analysis_priority), so we need to create consistent "primary" and "secondary" signal fields
108+
queryset = queryset.annotate(
109+
primary_signal_trait=Case(
110+
When(use_signal1_as_primary=True, then=F('signal1__analysis__trait__uuid')),
111+
default=F('signal2__analysis__trait__uuid')
112+
),
113+
secondary_signal_trait=Case(
114+
When(use_signal1_as_primary=True, then=F('signal2__analysis__trait__uuid')),
115+
default=F('signal1__analysis__trait__uuid')
116+
),
117+
primary_signal_chrom=Case(
118+
When(use_signal1_as_primary=True, then=F('signal1__lead_variant__chrom')),
119+
default=F('signal2__lead_variant__chrom')
120+
),
121+
secondary_signal_chrom=Case(
122+
When(use_signal1_as_primary=True, then=F('signal2__lead_variant__chrom')),
123+
default=F('signal1__lead_variant__chrom')
124+
),
125+
primary_signal_pos=Case(
126+
When(use_signal1_as_primary=True, then=F('signal1__lead_variant__pos')),
127+
default=F('signal2__lead_variant__pos')
128+
),
129+
secondary_signal_pos=Case(
130+
When(use_signal1_as_primary=True, then=F('signal2__lead_variant__pos')),
131+
default=F('signal1__lead_variant__pos')
132+
),
133+
primary_signal_logp=Case(
134+
When(use_signal1_as_primary=True, then=F('signal1__neg_log_p')),
135+
default=F('signal2__neg_log_p')
136+
),
137+
secondary_signal_logp=Case(
138+
When(use_signal1_as_primary=True, then=F('signal2__neg_log_p')),
139+
default=F('signal1__neg_log_p')
140+
),
141+
primary_signal_tissue=Case(
142+
When(use_signal1_as_primary=True, then=F('signal1__analysis__tissue')),
143+
default=F('signal2__analysis__tissue')
144+
),
145+
secondary_signal_tissue=Case(
146+
When(use_signal1_as_primary=True, then=F('signal2__analysis__tissue')),
147+
default=F('signal1__analysis__tissue')
148+
),
149+
primary_signal_cell_type=Case(
150+
When(use_signal1_as_primary=True, then=F('signal1__analysis__cell_type')),
151+
default=F('signal2__analysis__cell_type')
152+
),
153+
secondary_signal_cell_type=Case(
154+
When(use_signal1_as_primary=True, then=F('signal2__analysis__cell_type')),
155+
default=F('signal1__analysis__cell_type')
156+
),
157+
primary_signal_study=Case(
158+
When(use_signal1_as_primary=True, then=F('signal1__analysis__study__uuid')),
159+
default=F('signal2__analysis__study__uuid')
160+
),
161+
secondary_signal_study=Case(
162+
When(use_signal1_as_primary=True, then=F('signal2__analysis__study__uuid')),
163+
default=F('signal1__analysis__study__uuid')
164+
),
165+
primary_signal_gene_ens_id=Case(
166+
When(use_signal1_as_primary=True, then=F('signal1__analysis__trait__gene__ens_id')),
167+
default=F('signal2__analysis__trait__gene__ens_id')
168+
),
169+
secondary_signal_gene_ens_id=Case(
170+
When(use_signal1_as_primary=True, then=F('signal2__analysis__trait__gene__ens_id')),
171+
default=F('signal1__analysis__trait__gene__ens_id')
172+
),
173+
primary_signal_gene_symbol=Case(
174+
When(use_signal1_as_primary=True, then=F('signal1__analysis__trait__gene__symbol')),
175+
default=F('signal2__analysis__trait__gene__symbol')
176+
),
177+
secondary_signal_gene_symbol=Case(
178+
When(use_signal1_as_primary=True, then=F('signal2__analysis__trait__gene__symbol')),
179+
default=F('signal1__analysis__trait__gene__symbol')
180+
),
181+
primary_signal_exon_ens_id=Case(
182+
When(use_signal1_as_primary=True, then=F('signal1__analysis__trait__exon__ens_id')),
183+
default=F('signal2__analysis__trait__exon__ens_id')
184+
),
185+
secondary_signal_exon_ens_id=Case(
186+
When(use_signal1_as_primary=True, then=F('signal2__analysis__trait__exon__ens_id')),
187+
default=F('signal1__analysis__trait__exon__ens_id')
188+
),
189+
)
190+
105191
return super().filter_queryset(queryset)
106192

107193
def create_query(self, field, value):
@@ -228,27 +314,55 @@ def filter_region(self, queryset, name, value):
228314
if match:
229315
chrom, start_pos, end_pos = match
230316

231-
pos_field = None
232-
if "signal1" in name:
233-
pos_field = "signal1__lead_variant__pos"
234-
elif "signal2" in name:
235-
pos_field = "signal2__lead_variant__pos"
236-
237-
chrom_field = None
238317
if "signal1" in name:
239318
chrom_field = "signal1__lead_variant__chrom"
319+
pos_field = "signal1__lead_variant__pos"
320+
return queryset.filter(
321+
Q(**{
322+
"use_signal1_as_primary": True,
323+
f'{chrom_field}': chrom,
324+
f'{pos_field}__gte': start_pos,
325+
f'{pos_field}__lte': end_pos}) |
326+
Q(**{
327+
"use_signal1_as_primary": False,
328+
f'{chrom_field.replace("signal1", "signal2")}': chrom,
329+
f'{pos_field.replace("signal1", "signal2")}__gte': start_pos,
330+
f'{pos_field.replace("signal1", "signal2")}__lte': end_pos})
331+
)
240332
elif "signal2" in name:
241333
chrom_field = "signal2__lead_variant__chrom"
242-
243-
if chrom_field and pos_field:
334+
pos_field = "signal2__lead_variant__pos"
244335
return queryset.filter(
245-
Q(**{f'{chrom_field}': chrom})
246-
& Q(**{f'{pos_field}__gte': start_pos})
247-
& Q(**{f'{pos_field}__lte': end_pos})
336+
Q(**{
337+
"use_signal1_as_primary": True,
338+
f'{chrom_field}': chrom,
339+
f'{pos_field}__gte': start_pos,
340+
f'{pos_field}__lte': end_pos}) |
341+
Q(**{
342+
"use_signal1_as_primary": False,
343+
f'{chrom_field.replace("signal2", "signal1")}': chrom,
344+
f'{pos_field.replace("signal2", "signal1")}__gte': start_pos,
345+
f'{pos_field.replace("signal2", "signal1")}__lte': end_pos})
346+
)
347+
else:
348+
return queryset.filter(
349+
Q(**{
350+
"signal1__lead_variant__chrom": chrom,
351+
"signal1__lead_variant__pos__gte": start_pos,
352+
"signal1__lead_variant__pos__lte": end_pos}) |
353+
Q(**{
354+
"signal2__lead_variant__chrom": chrom,
355+
"signal2__lead_variant__pos__gte": start_pos,
356+
"signal2__lead_variant__pos__lte": end_pos})
248357
)
249358

250359
return queryset
251360

361+
region = CharFilter(
362+
method='filter_region',
363+
label="Only retrieve results (for signal1 or signal2) within a specified region given as chr:start-end"
364+
)
365+
252366
signal1_region = CharFilter(
253367
method='filter_region',
254368
label="Only retrieve signal 1 results within a specified region given as chr:start-end")
@@ -262,11 +376,24 @@ def filter_region(self, queryset, name, value):
262376
signal2_analysis = CharFilter(field_name='signal2__analysis__uuid', lookup_expr='exact',
263377
label="Signal 2 analysis UUID")
264378

265-
signal1_trait = CharFilter(field_name='signal1__analysis__trait__uuid', lookup_expr='exact',
379+
signal1_trait = CharFilter(method='filter_signal1_trait', lookup_expr='exact',
266380
label="Signal 1 trait UUID")
267-
signal2_trait = CharFilter(field_name='signal2__analysis__trait__uuid', lookup_expr='exact',
381+
382+
def filter_signal1_trait(self, queryset, name, value):
383+
return queryset.filter(
384+
Q(use_signal1_as_primary=True, signal1__analysis__trait__uuid=value) |
385+
Q(use_signal1_as_primary=False, signal2__analysis__trait__uuid=value)
386+
)
387+
388+
signal2_trait = CharFilter(method='filter_signal2_trait', lookup_expr='exact',
268389
label="Signal 2 trait UUID")
269390

391+
def filter_signal2_trait(self, queryset, name, value):
392+
return queryset.filter(
393+
Q(use_signal1_as_primary=True, signal2__analysis__trait__uuid=value) |
394+
Q(use_signal1_as_primary=False, signal1__analysis__trait__uuid=value)
395+
)
396+
270397
signal1_min_logp = NumberFilter(field_name='signal1__neg_log_p', lookup_expr='gte',
271398
label="Minimum -log10 p-value for signal 1")
272399
signal2_min_logp = NumberFilter(field_name='signal2__neg_log_p', lookup_expr='gte',
@@ -284,26 +411,26 @@ def filter_region(self, queryset, name, value):
284411
('r2', 'r2'),
285412
('n_coloc_between_traits', 'n_coloc_between_traits'),
286413
*(f"logp_max_over_{e[0].lower()}" for e in ANALYSIS_TYPES),
287-
('signal1__neg_log_p', 'signal1_logp'),
288-
('signal2__neg_log_p', 'signal2_logp'),
289-
('signal1__lead_variant__chrom', 'signal1_chrom'),
290-
('signal1__lead_variant__pos', 'signal1_pos'),
291-
('signal1__analysis__trait__uuid', 'signal1_trait'),
292-
('signal2__analysis__trait__uuid', 'signal2_trait'),
293-
('signal2__lead_variant__chrom', 'signal2_chrom'),
294-
('signal2__lead_variant__pos', 'signal2_pos'),
295-
('signal1__analysis__trait__gene__ens_id', 'signal1_gene_ens_id'),
296-
('signal1__analysis__trait__gene__symbol', 'signal1_gene_symbol'),
297-
('signal1__analysis__tissue', 'signal1_tissue'),
298-
('signal1__analysis__cell_type', 'signal1_cell_type'),
299-
('signal1__analysis__trait__exon__ens_id', 'signal1_exon_ens_id'),
300-
('signal2__analysis__trait__gene__ens_id', 'signal2_gene_ens_id'),
301-
('signal2__analysis__trait__gene__symbol', 'signal2_gene_symbol'),
302-
('signal2__analysis__tissue', 'signal2_tissue'),
303-
('signal2__analysis__cell_type', 'signal2_cell_type'),
304-
('signal2__analysis__trait__exon__ens_id', 'signal2_exon_ens_id'),
305-
('signal1__analysis__study__uuid', 'signal1_study'),
306-
('signal2__analysis__study__uuid', 'signal2_study')
414+
('primary_signal_logp', 'signal1_logp'),
415+
('secondary_signal_logp', 'signal2_logp'),
416+
('primary_signal_chrom', 'signal1_chrom'),
417+
('primary_signal_pos', 'signal1_pos'),
418+
('primary_signal_trait', 'signal1_trait'),
419+
('secondary_signal_trait', 'signal2_trait'),
420+
('secondary_signal_chrom', 'signal2_chrom'),
421+
('secondary_signal_pos', 'signal2_pos'),
422+
('primary_signal_gene_ens_id', 'signal1_gene_ens_id'),
423+
('primary_signal_gene_symbol', 'signal1_gene_symbol'),
424+
('primary_signal_tissue', 'signal1_tissue'),
425+
('primary_signal_cell_type', 'signal1_cell_type'),
426+
('primary_signal_exon_ens_id', 'signal1_exon_ens_id'),
427+
('secondary_signal_gene_ens_id', 'signal2_gene_ens_id'),
428+
('secondary_signal_gene_symbol', 'signal2_gene_symbol'),
429+
('secondary_signal_tissue', 'signal2_tissue'),
430+
('secondary_signal_cell_type', 'signal2_cell_type'),
431+
('secondary_signal_exon_ens_id', 'signal2_exon_ens_id'),
432+
('primary_signal_study', 'signal1_study'),
433+
('secondary_signal_study', 'signal2_study')
307434
)
308435
)
309436

colocus/api/serializers.py

Lines changed: 14 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -313,91 +313,29 @@ class ColocResultSerializer(drf_serializers.ModelSerializer):
313313
label="Number of coloc results between signal1's trait and signal2's trait")
314314

315315
def get_signal1(self, obj):
316-
request = self.context.get('request')
317-
analysis_type_priority = request and request.query_params.get('analysis_type_priority')
318-
319-
if analysis_type_priority:
320-
order = analysis_type_priority.split(",")
321-
322-
try:
323-
order1 = order.index(obj.signal1.analysis.analysis_type) if obj.signal1.analysis.analysis_type in order else None
324-
except (ValueError, AttributeError):
325-
order1 = None
326-
327-
try:
328-
order2 = order.index(obj.signal2.analysis.analysis_type) if obj.signal2.analysis.analysis_type in order else None
329-
except (ValueError, AttributeError):
330-
order2 = None
331-
332-
if order1 is None and order2 is None:
333-
signal = obj.signal1
334-
335-
elif order1 is not None:
336-
if order1 == 0:
337-
signal = obj.signal1
338-
elif order1 == 1:
339-
signal = obj.signal2
340-
341-
elif order2 is not None:
342-
if order2 == 0:
343-
signal = obj.signal2
344-
elif order2 == 1:
345-
signal = obj.signal1
346-
347-
elif order1 == order2:
348-
return obj.signal1
349-
elif order1 < order2:
350-
signal = obj.signal1
351-
elif order1 > order2:
352-
signal = obj.signal2
353-
316+
# Use annotated field if available, otherwise fall back to current logic
317+
if hasattr(obj, 'use_signal1_as_primary'):
318+
signal = obj.signal1 if obj.use_signal1_as_primary else obj.signal2
354319
else:
355320
signal = obj.signal1
356321

322+
if signal is None:
323+
# Otherwise the FinemappedSignalSerializer will serialize a bunch of null fields under a signal object,
324+
# rather than just making the entire object null (which it should be in the response).
325+
return None
326+
357327
return FinemappedSignalSerializer(signal, context=self.context).data
358328

359329
def get_signal2(self, obj):
360-
request = self.context.get('request')
361-
analysis_type_priority = request and request.query_params.get('analysis_type_priority')
362-
363-
if analysis_type_priority:
364-
order = analysis_type_priority.split(",")
365-
366-
try:
367-
order1 = order.index(obj.signal1.analysis.analysis_type) if obj.signal1.analysis.analysis_type in order else None
368-
except (ValueError, AttributeError):
369-
order1 = None
370-
371-
try:
372-
order2 = order.index(obj.signal2.analysis.analysis_type) if obj.signal2.analysis.analysis_type in order else None
373-
except (ValueError, AttributeError):
374-
order2 = None
375-
376-
if order1 is None and order2 is None:
377-
signal = obj.signal2
378-
379-
elif order1 is not None:
380-
if order1 == 0:
381-
signal = obj.signal2
382-
elif order1 == 1:
383-
signal = obj.signal1
384-
385-
elif order2 is not None:
386-
if order2 == 0:
387-
signal = obj.signal1
388-
elif order2 == 1:
389-
signal = obj.signal2
390-
391-
elif order1 == order2:
392-
return obj.signal2
393-
elif order1 < order2:
394-
signal = obj.signal2
395-
elif order1 > order2:
396-
signal = obj.signal1
397-
330+
# Use annotated field if available, otherwise fall back to current logic
331+
if hasattr(obj, 'use_signal1_as_primary'):
332+
signal = obj.signal2 if obj.use_signal1_as_primary else obj.signal1
398333
else:
399334
signal = obj.signal2
400335

336+
if signal is None:
337+
return None
338+
401339
return FinemappedSignalSerializer(signal, context=self.context).data
402340

403341
class Meta:

0 commit comments

Comments
 (0)