Skip to content

Commit c41c4a5

Browse files
authored
Merge pull request #104 from bcgsc/feat/DEVSU-2885-accept-hrd-cutoff
Feat/devsu 2885 accept hrd cutoff
2 parents b728677 + 6bdc33e commit c41c4a5

6 files changed

Lines changed: 162 additions & 14 deletions

File tree

pori_python/ipr/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@
3838
'variantTypeName': 'moderate signature',
3939
},
4040
}
41+
HRD_SIGNATURE_OVER_CUTOFF = HRD_MAPPING['homologous recombination deficiency strong signature']

pori_python/ipr/content.spec.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,12 +548,37 @@
548548
},
549549
"score": {
550550
"type": "number"
551+
},
552+
"cutoff": {
553+
"type": "number"
551554
}
552555
},
556+
"type": "object",
553557
"required": [
554558
"score"
555559
],
556-
"type": "object"
560+
"oneOf": [
561+
{
562+
"required": [
563+
"kbCategory"
564+
],
565+
"not": {
566+
"required": [
567+
"cutoff"
568+
]
569+
}
570+
},
571+
{
572+
"required": [
573+
"cutoff"
574+
],
575+
"not": {
576+
"required": [
577+
"kbCategory"
578+
]
579+
}
580+
}
581+
]
557582
},
558583
"images": {
559584
"items": {

pori_python/ipr/inputs.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
HLA_SIGNATURE_VARIANT_TYPE,
2828
MSI_MAPPING,
2929
HRD_MAPPING,
30+
HRD_SIGNATURE_OVER_CUTOFF,
3031
TMB_SIGNATURE,
3132
TMB_SIGNATURE_VARIANT_TYPE,
3233
)
@@ -558,13 +559,42 @@ def preprocess_hrd(hrd: Any) -> Iterable[Dict]:
558559
"""
559560
Process hrd input into preformatted signature input.
560561
HRD gets mapped to corresponding GraphKB Signature CategoryVariants.
562+
563+
Either a cutoff or a kbcategory is expected.
564+
If a cutoff is provided, the score is compared to the cutoff
565+
to determine whether to create the signature variant.
566+
If a kbCategory is provided, the signature variant is created based on the category.
567+
If neither are provided, a warning is logged and no signature variant is created.
561568
"""
562569
if hrd:
570+
hrd_cutoff = hrd.get('cutoff', None)
563571
hrd_cat = hrd.get('kbCategory', '')
572+
hrd_score = hrd.get('score', None)
564573

565-
hrd_variant = HRD_MAPPING.get(hrd_cat, None)
574+
if hrd_cutoff and hrd_cat:
575+
raise ValueError(
576+
'In the HRD section, only one of cutoff and kbcategory should be provided.'
577+
)
566578

567-
# Signature CategoryVariant created either for msi or mss
579+
if not (hrd_cutoff or hrd_cat):
580+
logger.warning(
581+
'No hrd category or cutoff provided; score will be loaded with no variant matching.'
582+
)
583+
584+
if hrd_cutoff:
585+
if not hrd_score:
586+
raise ValueError(
587+
'In the HRD section, if cutoff is provided a score must also be provided.'
588+
)
589+
590+
if hrd_score >= hrd_cutoff:
591+
hrd_variant = HRD_SIGNATURE_OVER_CUTOFF
592+
else:
593+
return []
594+
elif hrd_cat:
595+
hrd_variant = HRD_MAPPING.get(hrd_cat, None)
596+
597+
# Signature CategoryVariant created for hrd
568598
if hrd_variant:
569599
return [hrd_variant]
570600

pori_python/ipr/ipr.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,6 @@ def get_kb_disease_matches(
668668
verbose: bool = True,
669669
useSubgraphsRoute: bool = True,
670670
) -> list[Dict]:
671-
672671
disease_matches = []
673672

674673
if not kb_disease_match:

tests/test_ipr/test_inputs.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,83 @@ def test_preprocess_hrd(self) -> None:
292292
signatureNames = {r.get('signatureName', '') for r in self.hrd}
293293
assert len(EXPECTED_HRD.symmetric_difference(signatureNames)) == 0
294294

295+
def test_preprocess_hrd_cutoff_above(self) -> None:
296+
"""Test HRD with cutoff where score >= cutoff returns strong signature."""
297+
hrd = preprocess_hrd(
298+
{
299+
'score': 75,
300+
'cutoff': 50,
301+
}
302+
)
303+
assert len(hrd) == 1
304+
assert hrd[0]['signatureName'] == 'homologous recombination deficiency'
305+
assert hrd[0]['variantTypeName'] == 'strong signature'
306+
307+
def test_preprocess_hrd_cutoff_below(self) -> None:
308+
"""Test HRD with cutoff where score < cutoff returns moderate signature."""
309+
hrd = preprocess_hrd(
310+
{
311+
'score': 25,
312+
'cutoff': 50,
313+
}
314+
)
315+
assert len(hrd) == 0
316+
317+
def test_preprocess_hrd_cutoff_equal(self) -> None:
318+
"""Test HRD with cutoff where score == cutoff returns strong signature."""
319+
hrd = preprocess_hrd(
320+
{
321+
'score': 50,
322+
'cutoff': 50,
323+
}
324+
)
325+
assert len(hrd) == 1
326+
assert hrd[0]['signatureName'] == 'homologous recombination deficiency'
327+
assert hrd[0]['variantTypeName'] == 'strong signature'
328+
329+
def test_preprocess_hrd_cutoff_missing_score(self) -> None:
330+
"""Test HRD with cutoff but missing score raises ValueError."""
331+
with pytest.raises(ValueError, match='if cutoff is provided a score must also be provided'):
332+
preprocess_hrd(
333+
{
334+
'cutoff': 50,
335+
}
336+
)
337+
338+
def test_preprocess_hrd_cutoff_and_kbcategory(self) -> None:
339+
"""Test HRD with both cutoff and kbCategory raises ValueError."""
340+
with pytest.raises(
341+
ValueError, match='only one of cutoff and kbcategory should be provided'
342+
):
343+
preprocess_hrd(
344+
{
345+
'score': 75,
346+
'cutoff': 50,
347+
'kbCategory': 'homologous recombination deficiency strong signature',
348+
}
349+
)
350+
351+
def test_preprocess_hrd_kbcategory_moderate(self) -> None:
352+
"""Test HRD with kbCategory moderate signature."""
353+
hrd = preprocess_hrd(
354+
{
355+
'kbCategory': 'homologous recombination deficiency moderate signature',
356+
}
357+
)
358+
assert len(hrd) == 1
359+
assert hrd[0]['signatureName'] == 'homologous recombination deficiency'
360+
assert hrd[0]['variantTypeName'] == 'moderate signature'
361+
362+
def test_preprocess_hrd_empty(self) -> None:
363+
"""Test HRD with empty input returns empty list."""
364+
hrd = preprocess_hrd({})
365+
assert hrd == []
366+
367+
def test_preprocess_hrd_none(self) -> None:
368+
"""Test HRD with None input returns empty list."""
369+
hrd = preprocess_hrd(None)
370+
assert hrd == []
371+
295372
def test_preprocess_signature_variants(self) -> None:
296373
records = preprocess_signature_variants(
297374
[

tests/test_ipr/test_upload.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def loaded_reports(tmp_path_factory) -> Generator:
7474
],
7575
'hrd': {
7676
'score': 9999.0,
77-
'kbCategory': 'homologous recombination deficiency strong signature',
77+
'cutoff': 5,
7878
},
7979
'expressionVariants': json.loads(
8080
pd.read_csv(get_test_file('expression.short.tab'), sep='\t').to_json(orient='records')
@@ -106,6 +106,7 @@ def loaded_reports(tmp_path_factory) -> Generator:
106106
'caption': 'Test adding a caption to an image',
107107
}
108108
],
109+
'config': 'test config',
109110
}
110111

111112
json_file.write_text(
@@ -254,15 +255,30 @@ def test_copy_variants_loaded(self, loaded_reports) -> None:
254255
async_equals_sync = stringify_sorted(section) == stringify_sorted(async_section)
255256
assert async_equals_sync
256257

257-
# # Uncomment when signatureVariants are supported in pori_ipr_api
258-
# def test_signature_variants_loaded(self, loaded_reports) -> None:
259-
# section = get_section(loaded_reports["sync"], "signature-variants")
260-
# kbmatched = [item for item in section if item["kbMatches"]]
261-
# assert ("SBS2", "high signature") in [
262-
# (item["signatureName"], item["variantTypeName"]) for item in kbmatched
263-
# ]
264-
# async_section = get_section(loaded_reports["async"], "signature-variants")
265-
# assert compare_sections(section, async_section)
258+
def test_signature_variants_loaded(self, loaded_reports) -> None:
259+
section = get_section(loaded_reports['sync'], 'signature-variants')
260+
kbmatched = [item for item in section if item['kbMatches']]
261+
# Check for COSMIC signatures
262+
assert ('SBS2', 'high signature') in [
263+
(item['signatureName'], item['variantTypeName']) for item in kbmatched
264+
]
265+
# Check for HRD signature (score 9999 > cutoff 5, so strong signature)
266+
assert ('homologous recombination deficiency', 'strong signature') in [
267+
(item['signatureName'], item['variantTypeName']) for item in kbmatched
268+
]
269+
# Check for MSI signature
270+
assert ('microsatellite instability', 'high signature') in [
271+
(item['signatureName'], item['variantTypeName']) for item in kbmatched
272+
]
273+
async_section = get_section(loaded_reports['async'], 'signature-variants')
274+
async_equals_sync = stringify_sorted(section) == stringify_sorted(async_section)
275+
assert async_equals_sync
276+
277+
def test_hrd_score_in_report(self, loaded_reports) -> None:
278+
"""Test that HRD score is present in the loaded report."""
279+
report = loaded_reports['sync'][1]['reports'][0]
280+
assert 'hrdScore' in report
281+
assert report['hrdScore'] == 9999.0
266282

267283
def test_kb_matches_loaded(self, loaded_reports) -> None:
268284
section = get_section(loaded_reports['sync'], 'kb-matches')

0 commit comments

Comments
 (0)