Repository: haddocking/arctic3d

Issue:
address stochiometry issue by increasing the number of interfaces
when the stochiometry of the complex is not 1:1, the uniprot ID-uniprot ID pairwise interface takes into account geometrically different interfaces, thus leading to unfeasible predictions. We should take into account pdb and chain information in the following way:

1. consider the chain id in the json file. But what if a residue in a pdb gives the following record?`{"pdbId":"5wfq","entityId":1,"chainIds":"R,Q"}` as for example the results for `Q07889` (interactor `P01112`). In this case the interfaces are impossible to distinguish
2. consider the pdb id of each pdb as a potential interface. This will increase the number of interfaces that are going to be analysed, but hopefully will add more information.

Discussion:

Patch:
diff --git a/src/arctic3d/cli.py b/src/arctic3d/cli.py
index dc7ad7c..c325584 100644
--- a/src/arctic3d/cli.py
+++ b/src/arctic3d/cli.py
@@ -76,6 +76,12 @@ argument_parser.add_argument(
     help=".json file containing the pdb data",
 )
 
+argument_parser.add_argument(
+    "--full",
+    help="consider full uniprot-pdb-chain information in the retrieval",
+    default=False,
+)
+
 
 def load_args(arguments):
     """
@@ -127,6 +133,7 @@ def main(
     run_dir,
     interface_data,
     pdb_data,
+    full,
 ):
     """Main function."""
     log.setLevel("DEBUG")
@@ -165,11 +172,11 @@ def main(
     else:
         if interface_data:
             interface_residues = get_interface_residues(
-                uniprot_id, out_uniprot, out_pdb, input_files["interface_data"]
+                uniprot_id, out_uniprot, out_pdb, input_files["interface_data"], full
             )
         else:
             interface_residues = get_interface_residues(
-                uniprot_id, out_uniprot, out_pdb
+                uniprot_id, out_uniprot, out_pdb, full
             )
 
     log.info(f"Interface Residues: {interface_residues}")
diff --git a/src/arctic3d/modules/interface.py b/src/arctic3d/modules/interface.py
index 31db63e..9777ead 100644
--- a/src/arctic3d/modules/interface.py
+++ b/src/arctic3d/modules/interface.py
@@ -165,7 +165,7 @@ def read_interface_residues(interface_file):
 
 
 def get_interface_residues(
-    uniprot_id, out_uniprot_string, out_pdb_string, interface_data=None
+    uniprot_id, out_uniprot_string, out_pdb_string, full, interface_data=None
 ):
     """
     Get interface residues.
@@ -178,6 +178,10 @@ def get_interface_residues(
         comma-separated uniprot IDs to exclude.
     out_pdb_string : str or None
         comma-separated pdb IDs to exclude.
+    full : bool
+        consider full information in interface retrieval
+    interface_data : str or Path or None
+        interface data .json file
 
     Returns
     -------
@@ -209,13 +213,15 @@ def get_interface_residues(
 
     if interface_api_data and len(interface_api_data) != 0:
         interface_dict = parse_interface_data(
-            uniprot_id, interface_api_data, out_uniprot_set, out_pdb_set
+            uniprot_id, interface_api_data, out_uniprot_set, out_pdb_set, full
         )
 
     return interface_dict
 
 
-def parse_interface_data(uniprot_id, interface_data, out_uniprot_set, out_pdb_set):
+def parse_interface_data(
+    uniprot_id, interface_data, out_uniprot_set, out_pdb_set, full
+):
     """
     Parse interface data.
 
@@ -229,6 +235,8 @@ def parse_interface_data(uniprot_id, interface_data, out_uniprot_set, out_pdb_se
         Set of Uniprot IDs to exclude. Empty if none.
     out_pdb_set : set
         Set of PDB files to exclude.
+    full : bool
+        Consider full information in interface retrieval.
 
     Returns
     -------
@@ -240,25 +248,40 @@ def parse_interface_data(uniprot_id, interface_data, out_uniprot_set, out_pdb_se
     for element in interface_data[uniprot_id]["data"]:
         partner_uniprotid = element["accession"]
         if partner_uniprotid not in out_uniprot_set:
-            interface_dict[partner_uniprotid] = []
+            log.info(f"Parsing partner uniprot ID {partner_uniprotid}")
             for residue_entry in element["residues"]:
                 start = residue_entry["startIndex"]
                 end = residue_entry["endIndex"]
-                # check on pdb exclusion
-                accept = True
-                if out_pdb_set:
-                    int_pdbs_list = residue_entry["interactingPDBEntries"]
-                    int_pdbs = set([data["pdbId"] for data in int_pdbs_list])
-                    if int_pdbs.issubset(out_pdb_set):
-                        # all interacting pdbs must be excluded
-                        accept = False
-                if accept:
-                    for interface_res in range(start, end + 1):
-                        interface_dict[partner_uniprotid].append(interface_res)
-            # if the list is empty, pop the key away
-            if len(interface_dict[partner_uniprotid]) == 0:
-                log.info(f"Discarding uniprot ID {partner_uniprotid} due to out_pdb")
-                interface_dict.pop(partner_uniprotid)
+                if full is False:
+                    # consider all pdb files as a whole
+                    accept = True
+                    key = partner_uniprotid
+                    if out_pdb_set:
+                        int_pdbs_list = residue_entry["interactingPDBEntries"]
+                        int_pdbs = set([data["pdbId"] for data in int_pdbs_list])
+                        if int_pdbs.issubset(out_pdb_set):
+                            # all interacting pdbs must be excluded
+                            accept = False
+                    if accept:
+                        if key not in interface_dict.keys():
+                            interface_dict[key] = []
+                        for interface_res in range(start, end + 1):
+                            interface_dict[partner_uniprotid].append(interface_res)
+                else:
+                    # iterate over pdb records
+                    for pdb_record in residue_entry["interactingPDBEntries"]:
+                        # entries can have missing chainIds field, especially for PRD_* like uniprot IDs
+                        chain_ids = [""]
+                        if "chainIds" in pdb_record.keys():
+                            chain_ids = pdb_record["chainIds"].split(",")
+                        # if there are two or more chainIds, we discard the current entry
+                        if pdb_record["pdbId"] not in out_pdb_set:
+                            for chain_id in chain_ids:
+                                key = f"{partner_uniprotid}-{pdb_record['pdbId']}-{chain_id}"
+                                if key not in interface_dict.keys():
+                                    interface_dict[key] = []
+                                for interface_res in range(start, end + 1):
+                                    interface_dict[key].append(interface_res)
         else:
             log.info(f"found uniprot ID {partner_uniprotid}. It will be discarded.")
 
diff --git a/src/arctic3d/modules/pdb.py b/src/arctic3d/modules/pdb.py
index d417f9b..0aff703 100644
--- a/src/arctic3d/modules/pdb.py
+++ b/src/arctic3d/modules/pdb.py
@@ -287,7 +287,7 @@ def keep_atoms(inp_pdb_f):
 def validate_api_hit(
     fetch_list,
     resolution_cutoff=3.0,
-    coverage_cutoff=0.1,
+    coverage_cutoff=0.0,
     max_pdb_num=20,
 ):
     """


Test patch:
diff --git a/tests/test_interface.py b/tests/test_interface.py
index f51ec9c..f30e572 100644
--- a/tests/test_interface.py
+++ b/tests/test_interface.py
@@ -97,7 +97,7 @@ def test_error_parse_out_pdb():
 def test_interface_data(inp_interface_data):
     """Test interface_data input json file."""
     obs_interface_residues = get_interface_residues(
-        "P40202", None, None, interface_data=inp_interface_data
+        "P40202", None, None, full=False, interface_data=inp_interface_data
     )
     exp_interface_residues = {
         "P00441": [85, 137, 138, 187, 217, 218, 222, 229, 231, 232],
@@ -105,3 +105,19 @@ def test_interface_data(inp_interface_data):
         "P40202": [136, 137, 138, 183, 184, 186, 187, 217, 218],
     }
     assert obs_interface_residues == exp_interface_residues
+
+
+def test_full_interface(inp_interface_data):
+    """Test interface_data input json file with full option."""
+    obs_interface_residues = get_interface_residues(
+        "P40202", None, None, full=True, interface_data=inp_interface_data
+    )
+    exp_interface_residues = {
+        "P00441-5u9m-A": [85, 137, 138, 187, 217, 218, 222, 229, 231, 232],
+        "P00441-5u9m-C": [85, 137, 138, 187, 217, 218, 222, 229, 231, 232],
+        "P00445-1jk9-A": [136, 137, 138, 187, 217, 218, 226, 229, 230],
+        "P00445-1jk9-C": [136, 137, 138, 187, 217, 218, 226, 229, 230],
+        "P40202-1qup-A": [136, 137, 138, 183, 184, 186, 187, 217, 218],
+        "P40202-1qup-B": [136, 137, 138, 183, 184, 186, 187, 217, 218],
+    }
+    assert obs_interface_residues == exp_interface_residues