From adf65bf27e57bf29e17be5975318ac2e1a2b98b5 Mon Sep 17 00:00:00 2001 From: Kristina Galikova Date: Tue, 11 Apr 2023 22:57:01 +0100 Subject: [PATCH 1/5] feat: add double antibody logic to crossmatch api --- txmatching/utils/hla_system/hla_crossmatch.py | 2 +- txmatching/web/api/crossmatch_api.py | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/txmatching/utils/hla_system/hla_crossmatch.py b/txmatching/utils/hla_system/hla_crossmatch.py index 2a797b9b3..b2cdeaba8 100644 --- a/txmatching/utils/hla_system/hla_crossmatch.py +++ b/txmatching/utils/hla_system/hla_crossmatch.py @@ -37,7 +37,7 @@ class AntibodyMatchForHLAType: def get_crossmatched_antibodies_per_group(donor_hla_typing: HLATyping, recipient_antibodies: HLAAntibodies, - use_high_resolution: bool): + use_high_resolution: bool) -> List[AntibodyMatchForHLAGroup]: if is_recipient_type_a(recipient_antibodies): antibody_matches_for_groups = do_crossmatch_in_type_a(donor_hla_typing, recipient_antibodies, diff --git a/txmatching/web/api/crossmatch_api.py b/txmatching/web/api/crossmatch_api.py index 9f2a6b270..d9fbc4ed7 100644 --- a/txmatching/web/api/crossmatch_api.py +++ b/txmatching/web/api/crossmatch_api.py @@ -1,15 +1,14 @@ from flask_restx import Resource from txmatching.auth.exceptions import TXMNotImplementedFeatureException -from txmatching.utils.hla_system.hla_preparation_utils import create_hla_typing, create_hla_type, \ - create_antibody from txmatching.data_transfer_objects.crossmatch.crossmatch_dto import CrossmatchDTOIn, AntibodyMatchForHLAType, \ CrossmatchDTOOut from txmatching.data_transfer_objects.crossmatch.crossmatch_in_swagger import CrossmatchJsonIn, CrossmatchJsonOut from txmatching.data_transfer_objects.patients.patient_parameters_dto import HLATypingRawDTO -from txmatching.patients.hla_model import HLATypeRaw, HLAAntibodies -from txmatching.utils.enums import HLAAntibodyType +from txmatching.patients.hla_model import HLAAntibodies, HLATypeRaw from txmatching.utils.hla_system.hla_crossmatch import get_crossmatched_antibodies_per_group +from txmatching.utils.hla_system.hla_preparation_utils import create_hla_typing, create_hla_type, \ + create_antibody from txmatching.utils.hla_system.hla_transformations.hla_transformations_store import \ parse_hla_antibodies_raw_and_return_parsing_issue_list, parse_hla_typing_raw_and_return_parsing_issue_list from txmatching.web.web_utils.namespaces import crossmatch_api @@ -49,21 +48,28 @@ def post(self): antibody_matches_for_hla_type = [AntibodyMatchForHLAType(hla_type=create_hla_type(raw_code=hla), antibody_matches=[]) - for hla in crossmatch_dto.donor_hla_typing] + for hla in crossmatch_dto.donor_hla_typing] for match_per_group in crossmatched_antibodies_per_group: for antibody_group_match in match_per_group.antibody_matches: - if antibody_group_match.hla_antibody.type == HLAAntibodyType.THEORETICAL or \ - antibody_group_match.hla_antibody.second_raw_code: - raise TXMNotImplementedFeatureException( - 'This functionality is not currently available for dual antibodies. ' - 'We apologize and will try to change this in future versions.') # get AntibodyMatchForHLAType object with the same hla_type # as the antibody_group_match and append the antibody_group_match - common_matches = [antibody_hla_match for antibody_hla_match in - antibody_matches_for_hla_type - if antibody_hla_match.hla_type.code == antibody_group_match.hla_antibody.code] + if antibody_group_match.hla_antibody.second_raw_code: + common_matches_alpha = [antibody_hla_match for antibody_hla_match in + antibody_matches_for_hla_type + if antibody_hla_match.hla_type.code == antibody_group_match.hla_antibody.code] + common_matches_beta = [antibody_hla_match for antibody_hla_match in + antibody_matches_for_hla_type + if antibody_hla_match.hla_type.code == antibody_group_match.hla_antibody.second_code] + if not (common_matches_alpha and common_matches_beta): + continue + common_matches = common_matches_alpha + common_matches_beta + else: + common_matches = [antibody_hla_match for antibody_hla_match in + antibody_matches_for_hla_type + if antibody_hla_match.hla_type.code == antibody_group_match.hla_antibody.code] if common_matches: - common_matches[0].antibody_matches.append(antibody_group_match) + for common_match in common_matches: + common_match.antibody_matches.append(antibody_group_match) return response_ok(CrossmatchDTOOut( hla_to_antibody=antibody_matches_for_hla_type, From 764747cb769c859e8a997523768e7d6e26164214 Mon Sep 17 00:00:00 2001 From: Kristina Galikova Date: Wed, 12 Apr 2023 13:45:24 +0100 Subject: [PATCH 2/5] tests: add tests for double antibody crossmatch api --- tests/web/test_do_crossmatch_api.py | 59 ++++++++++++++++++----------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/tests/web/test_do_crossmatch_api.py b/tests/web/test_do_crossmatch_api.py index fabf2a491..0336cd24d 100644 --- a/tests/web/test_do_crossmatch_api.py +++ b/tests/web/test_do_crossmatch_api.py @@ -1,4 +1,6 @@ from tests.test_utilities.prepare_app_for_tests import DbTests +from txmatching.utils.hla_system.hla_transformations.parsing_issue_detail import \ + ParsingIssueDetail from txmatching.web import API_VERSION, CROSSMATCH_NAMESPACE @@ -30,14 +32,12 @@ def test_do_crossmatch_api(self): self.assertEqual([], res.json['hla_to_antibody'][1]['antibody_matches']) self.assertEqual( - 'All antibodies are in high resolution, some of them below cutoff and less then 20 were provided. ' - 'This is fine and antibodies will be processed properly, but we are assuming that not all antibodies ' - 'the patient was tested for were sent. It is better to send all to improve crossmatch estimation.', - res.json['parsing_issues'][0]['message']) - self.assertEqual('This HLA group should contain at least one antigen.', - res.json['parsing_issues'][1]['message']) - self.assertEqual('This HLA group should contain at least one antigen.', - res.json['parsing_issues'][2]['message']) + ParsingIssueDetail.INSUFFICIENT_NUMBER_OF_ANTIBODIES_IN_HIGH_RES.value, + res.json['parsing_issues'][0]['parsing_issue_detail']) + self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + res.json['parsing_issues'][1]['parsing_issue_detail']) + self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + res.json['parsing_issues'][2]['parsing_issue_detail']) def test_do_crossmatch_api_with_different_code_formats(self): # case: donor - HIGH_RES, recipient - SPLIT @@ -68,10 +68,10 @@ def test_do_crossmatch_api_with_different_code_formats(self): self.assertEqual([], res.json['hla_to_antibody'][1]['antibody_matches']) self.assertEqual( - 'This HLA group should contain at least one antigen.', - res.json['parsing_issues'][0]['message']) - self.assertEqual('This HLA group should contain at least one antigen.', - res.json['parsing_issues'][1]['message']) + ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + res.json['parsing_issues'][0]['parsing_issue_detail']) + self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + res.json['parsing_issues'][1]['parsing_issue_detail']) # case: donor - SPLIT, recipient - HIGH_RES json = { @@ -101,16 +101,14 @@ def test_do_crossmatch_api_with_different_code_formats(self): self.assertEqual([], res.json['hla_to_antibody'][1]['antibody_matches']) self.assertEqual( - 'All antibodies are in high resolution, some of them below cutoff and less then 20 were provided. ' - 'This is fine and antibodies will be processed properly, but we are assuming that not all antibodies ' - 'the patient was tested for were sent. It is better to send all to improve crossmatch estimation.', - res.json['parsing_issues'][0]['message']) - self.assertEqual('This HLA group should contain at least one antigen.', - res.json['parsing_issues'][1]['message']) + ParsingIssueDetail.INSUFFICIENT_NUMBER_OF_ANTIBODIES_IN_HIGH_RES.value, + res.json['parsing_issues'][0]['parsing_issue_detail']) + self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + res.json['parsing_issues'][1]['parsing_issue_detail']) def test_theoretical_and_double_antibodies_not_implemented(self): json = { - "donor_hla_typing": ['DPA1*01:03', 'DPA1*02:01', 'DPA1*01:04'], + "donor_hla_typing": ['DPA1*01:03', 'DPB1*03:01', 'DPA1*01:04'], "recipient_antibodies": [{'mfi': 2100, 'name': 'DP[01:04,03:01]', 'cutoff': 2000 @@ -132,7 +130,22 @@ def test_theoretical_and_double_antibodies_not_implemented(self): with self.app.test_client() as client: res = client.post(f'{API_VERSION}/{CROSSMATCH_NAMESPACE}/do-crossmatch', json=json, headers=self.auth_headers) - self.assertEqual(501, res.status_code) - self.assertEqual('This functionality is not currently available for dual antibodies. ' - 'We apologize and will try to change this in future versions.', - res.json['message']) + self.assertCountEqual([ParsingIssueDetail.CREATED_THEORETICAL_ANTIBODY.value, + ParsingIssueDetail.INSUFFICIENT_NUMBER_OF_ANTIBODIES_IN_HIGH_RES.value, + ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value], + [parsing_issue['parsing_issue_detail'] for parsing_issue in res.json['parsing_issues']]) + double_antibody_match = {'hla_antibody': {'code': {'broad': 'DPA1', 'high_res': 'DPA1*01:04', 'split': 'DPA1'}, + 'cutoff': 2000, + 'mfi': 2100, + 'raw_code': 'DPA1*01:04', + 'second_code': {'broad': 'DP3', 'high_res': 'DPB1*03:01', 'split': 'DP3'}, + 'second_raw_code': 'DPB1*03:01', 'type': 'NORMAL'}, + 'match_type': 'HIGH_RES'} + self.assertEqual( + [double_antibody_match], + res.json['hla_to_antibody'][1]['antibody_matches']) + self.assertEqual( + [double_antibody_match], + res.json['hla_to_antibody'][2]['antibody_matches']) From dc0f084bdfed44b84c2b944f6c4175bd087ef6d2 Mon Sep 17 00:00:00 2001 From: Kristina Galikova Date: Sun, 16 Apr 2023 16:51:38 +0100 Subject: [PATCH 3/5] tests: rename test and add testing case for theoretical antibodies --- tests/web/test_do_crossmatch_api.py | 48 ++++++++++++++++++++-------- txmatching/web/api/crossmatch_api.py | 13 +++----- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/tests/web/test_do_crossmatch_api.py b/tests/web/test_do_crossmatch_api.py index 0336cd24d..c9f4f5d73 100644 --- a/tests/web/test_do_crossmatch_api.py +++ b/tests/web/test_do_crossmatch_api.py @@ -32,11 +32,11 @@ def test_do_crossmatch_api(self): self.assertEqual([], res.json['hla_to_antibody'][1]['antibody_matches']) self.assertEqual( - ParsingIssueDetail.INSUFFICIENT_NUMBER_OF_ANTIBODIES_IN_HIGH_RES.value, + ParsingIssueDetail.INSUFFICIENT_NUMBER_OF_ANTIBODIES_IN_HIGH_RES, res.json['parsing_issues'][0]['parsing_issue_detail']) - self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY, res.json['parsing_issues'][1]['parsing_issue_detail']) - self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY, res.json['parsing_issues'][2]['parsing_issue_detail']) def test_do_crossmatch_api_with_different_code_formats(self): @@ -68,9 +68,9 @@ def test_do_crossmatch_api_with_different_code_formats(self): self.assertEqual([], res.json['hla_to_antibody'][1]['antibody_matches']) self.assertEqual( - ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY, res.json['parsing_issues'][0]['parsing_issue_detail']) - self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY, res.json['parsing_issues'][1]['parsing_issue_detail']) # case: donor - SPLIT, recipient - HIGH_RES @@ -101,14 +101,14 @@ def test_do_crossmatch_api_with_different_code_formats(self): self.assertEqual([], res.json['hla_to_antibody'][1]['antibody_matches']) self.assertEqual( - ParsingIssueDetail.INSUFFICIENT_NUMBER_OF_ANTIBODIES_IN_HIGH_RES.value, + ParsingIssueDetail.INSUFFICIENT_NUMBER_OF_ANTIBODIES_IN_HIGH_RES, res.json['parsing_issues'][0]['parsing_issue_detail']) - self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, + self.assertEqual(ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY, res.json['parsing_issues'][1]['parsing_issue_detail']) - def test_theoretical_and_double_antibodies_not_implemented(self): + def test_theoretical_and_double_antibodies(self): json = { - "donor_hla_typing": ['DPA1*01:03', 'DPB1*03:01', 'DPA1*01:04'], + "donor_hla_typing": ['DPA1*01:03', 'DPB1*03:01', 'DPA1*01:04', 'DPA1*02:01'], "recipient_antibodies": [{'mfi': 2100, 'name': 'DP[01:04,03:01]', 'cutoff': 2000 @@ -124,17 +124,26 @@ def test_theoretical_and_double_antibodies_not_implemented(self): {'mfi': 1000, 'name': 'DP[01:03,03:01]', 'cutoff': 2000 + }, + {'mfi': 3000, + 'name': 'DP[02:01,01:01]', + 'cutoff': 2000 + }, + {'mfi': 1000, + 'name': 'DP[02:01,01:01]', + 'cutoff': 2000 }], } with self.app.test_client() as client: res = client.post(f'{API_VERSION}/{CROSSMATCH_NAMESPACE}/do-crossmatch', json=json, headers=self.auth_headers) - self.assertCountEqual([ParsingIssueDetail.CREATED_THEORETICAL_ANTIBODY.value, - ParsingIssueDetail.INSUFFICIENT_NUMBER_OF_ANTIBODIES_IN_HIGH_RES.value, - ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, - ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value, - ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY.value], + self.assertCountEqual([ParsingIssueDetail.CREATED_THEORETICAL_ANTIBODY, + ParsingIssueDetail.CREATED_THEORETICAL_ANTIBODY, + ParsingIssueDetail.INSUFFICIENT_NUMBER_OF_ANTIBODIES_IN_HIGH_RES, + ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY, + ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY, + ParsingIssueDetail.BASIC_HLA_GROUP_IS_EMPTY], [parsing_issue['parsing_issue_detail'] for parsing_issue in res.json['parsing_issues']]) double_antibody_match = {'hla_antibody': {'code': {'broad': 'DPA1', 'high_res': 'DPA1*01:04', 'split': 'DPA1'}, 'cutoff': 2000, @@ -143,9 +152,20 @@ def test_theoretical_and_double_antibodies_not_implemented(self): 'second_code': {'broad': 'DP3', 'high_res': 'DPB1*03:01', 'split': 'DP3'}, 'second_raw_code': 'DPB1*03:01', 'type': 'NORMAL'}, 'match_type': 'HIGH_RES'} + theoretical_antibody_match = {'hla_antibody': {'code': {'broad': 'DPA2', 'high_res': 'DPA1*02:01', 'split': 'DPA2'}, + 'cutoff': 2000, + 'mfi': 2000, + 'raw_code': 'DPA1*02:01', + 'second_code': None, + 'second_raw_code': None, + 'type': 'THEORETICAL'}, + 'match_type': 'THEORETICAL'} self.assertEqual( [double_antibody_match], res.json['hla_to_antibody'][1]['antibody_matches']) self.assertEqual( [double_antibody_match], res.json['hla_to_antibody'][2]['antibody_matches']) + self.assertEqual( + theoretical_antibody_match, + res.json['hla_to_antibody'][3]['antibody_matches'][1]) diff --git a/txmatching/web/api/crossmatch_api.py b/txmatching/web/api/crossmatch_api.py index d9fbc4ed7..8814fc62f 100644 --- a/txmatching/web/api/crossmatch_api.py +++ b/txmatching/web/api/crossmatch_api.py @@ -54,15 +54,10 @@ def post(self): # get AntibodyMatchForHLAType object with the same hla_type # as the antibody_group_match and append the antibody_group_match if antibody_group_match.hla_antibody.second_raw_code: - common_matches_alpha = [antibody_hla_match for antibody_hla_match in - antibody_matches_for_hla_type - if antibody_hla_match.hla_type.code == antibody_group_match.hla_antibody.code] - common_matches_beta = [antibody_hla_match for antibody_hla_match in - antibody_matches_for_hla_type - if antibody_hla_match.hla_type.code == antibody_group_match.hla_antibody.second_code] - if not (common_matches_alpha and common_matches_beta): - continue - common_matches = common_matches_alpha + common_matches_beta + common_matches = [antibody_hla_match for antibody_hla_match in + antibody_matches_for_hla_type + if antibody_hla_match.hla_type.code in (antibody_group_match.hla_antibody.code, + antibody_group_match.hla_antibody.second_code)] else: common_matches = [antibody_hla_match for antibody_hla_match in antibody_matches_for_hla_type From 3be8976718f993292f3c3c6801d7c52133afec42 Mon Sep 17 00:00:00 2001 From: Kristina Galikova Date: Sun, 16 Apr 2023 19:52:48 +0100 Subject: [PATCH 4/5] fix: tests for crossmatch api --- tests/web/test_do_crossmatch_api.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/web/test_do_crossmatch_api.py b/tests/web/test_do_crossmatch_api.py index c9f4f5d73..9523c7480 100644 --- a/tests/web/test_do_crossmatch_api.py +++ b/tests/web/test_do_crossmatch_api.py @@ -154,18 +154,17 @@ def test_theoretical_and_double_antibodies(self): 'match_type': 'HIGH_RES'} theoretical_antibody_match = {'hla_antibody': {'code': {'broad': 'DPA2', 'high_res': 'DPA1*02:01', 'split': 'DPA2'}, 'cutoff': 2000, - 'mfi': 2000, - 'raw_code': 'DPA1*02:01', + 'mfi': 3000, + 'raw_code': + 'DPA1*02:01', 'second_code': None, 'second_raw_code': None, 'type': 'THEORETICAL'}, 'match_type': 'THEORETICAL'} - self.assertEqual( - [double_antibody_match], - res.json['hla_to_antibody'][1]['antibody_matches']) - self.assertEqual( - [double_antibody_match], - res.json['hla_to_antibody'][2]['antibody_matches']) - self.assertEqual( - theoretical_antibody_match, - res.json['hla_to_antibody'][3]['antibody_matches'][1]) + print(res.json['hla_to_antibody'][3]['antibody_matches']) + self.assertTrue( + double_antibody_match in res.json['hla_to_antibody'][1]['antibody_matches']) + self.assertTrue( + double_antibody_match in res.json['hla_to_antibody'][2]['antibody_matches']) + self.assertTrue( + theoretical_antibody_match in res.json['hla_to_antibody'][3]['antibody_matches']) From 74adb2df113bd7f7fb35ffd4c026cc680727884c Mon Sep 17 00:00:00 2001 From: Kristina Galikova Date: Tue, 18 Apr 2023 14:09:21 +0100 Subject: [PATCH 5/5] tests: delete print line --- tests/web/test_do_crossmatch_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/web/test_do_crossmatch_api.py b/tests/web/test_do_crossmatch_api.py index 9523c7480..949c0f495 100644 --- a/tests/web/test_do_crossmatch_api.py +++ b/tests/web/test_do_crossmatch_api.py @@ -161,7 +161,7 @@ def test_theoretical_and_double_antibodies(self): 'second_raw_code': None, 'type': 'THEORETICAL'}, 'match_type': 'THEORETICAL'} - print(res.json['hla_to_antibody'][3]['antibody_matches']) + self.assertTrue( double_antibody_match in res.json['hla_to_antibody'][1]['antibody_matches']) self.assertTrue(