Repository: haddocking/arctic3d

Issue:
refactor interface matrix calculation
- [x] using numpy built-in functions it should be possibile to substantially accelerate some calculations
- [x] add some tests as well
- [x] move `read_int_matrix` from `clustering` module to `interface_matrix` module



Comments:

Patch:
diff --git a/src/arctic3d/modules/clustering.py b/src/arctic3d/modules/clustering.py
index d83223c..3aeb007 100644
--- a/src/arctic3d/modules/clustering.py
+++ b/src/arctic3d/modules/clustering.py
@@ -5,10 +5,10 @@ import os
 import time
 
 import matplotlib.pyplot as plt
-import numpy as np
-import pandas as pd
 from scipy.cluster.hierarchy import dendrogram, fcluster, linkage
 
+from arctic3d.modules.interface_matrix import read_int_matrix
+
 LINKAGE = "average"
 # THRESHOLD = 0.7071  # np.sqrt(2)/2
 THRESHOLD = 0.8660  # np.sqrt(3)/2
@@ -16,40 +16,6 @@ THRESHOLD = 0.8660  # np.sqrt(3)/2
 log = logging.getLogger("arctic3dlog")
 
 
-def read_int_matrix(filename):
-    """
-    Read the interface matrix.
-
-    Parameters
-    ----------
-    filename : str or Path
-        interface matrix filename
-    Returns
-    -------
-    int_matrix : np.array
-        interface matrix
-    """
-    if os.path.exists(filename):
-        int_matrix = pd.read_csv(filename, header=None, sep=" ")
-        int_matrix.columns = ["lig1", "lig2", "D"]
-        # first check: it must be a 1D condensed distance matrix
-        nligands = 0.5 + np.sqrt(0.25 + 2 * int_matrix.shape[0])
-        int_nligands = int(nligands)
-        if abs(nligands - int_nligands) > 0.00001:
-            raise Exception(
-                f"npairs {int_matrix.shape[0]}: interface matrix should be a 1D condensed distance matrix"
-            )
-        # extracting ligands' names
-        ligand_names = [int_matrix.iloc[0, 0]]
-        for lig in int_matrix.iloc[:, 1]:
-            if lig not in ligand_names:
-                ligand_names.append(lig)
-        log.debug(f"Ligand names {ligand_names}")
-        return int_matrix.iloc[:, 2], ligand_names
-    else:
-        raise Exception(f"input path {filename} does not exist!")
-
-
 def cluster_distance_matrix(int_matrix, entries, plot=False):
     """
     Does the clustering.
diff --git a/src/arctic3d/modules/interface_matrix.py b/src/arctic3d/modules/interface_matrix.py
index 19e080a..718b91f 100644
--- a/src/arctic3d/modules/interface_matrix.py
+++ b/src/arctic3d/modules/interface_matrix.py
@@ -4,6 +4,7 @@ import time
 
 import MDAnalysis as mda
 import numpy as np
+import pandas as pd
 from scipy.spatial.distance import cdist
 
 SIGMA = 1.9
@@ -61,12 +62,7 @@ def compute_scalar_product(interface_one, interface_two, Jij_mat):
         scalar product between the two interfaces
     """
     # log.debug(f"computing scal_prod between {interface_one} and {interface_two}")
-    len_one = len(interface_one)
-    len_two = len(interface_two)
-    scalar_product = 0.0
-    for res_one in range(len_one):
-        for res_two in range(len_two):
-            scalar_product += Jij_mat[interface_one[res_one], interface_two[res_two]]
+    scalar_product = Jij_mat[np.ix_(interface_one, interface_two)].sum()
     return scalar_product
 
 
@@ -284,3 +280,37 @@ def interface_matrix(interface_dict, pdb_path):
         log.warning("Too few interfaces, interface matrix was not calculated.")
         out_fl = None
     return retained_interfaces, out_fl
+
+
+def read_int_matrix(filename):
+    """
+    Read the interface matrix.
+
+    Parameters
+    ----------
+    filename : str or Path
+        interface matrix filename
+    Returns
+    -------
+    int_matrix : np.array
+        interface matrix
+    """
+    if os.path.exists(filename):
+        int_matrix = pd.read_csv(filename, header=None, sep=" ")
+        int_matrix.columns = ["lig1", "lig2", "D"]
+        # first check: it must be a 1D condensed distance matrix
+        nligands = 0.5 + np.sqrt(0.25 + 2 * int_matrix.shape[0])
+        int_nligands = int(nligands)
+        if abs(nligands - int_nligands) > 0.00001:
+            raise Exception(
+                f"npairs {int_matrix.shape[0]}: interface matrix should be a 1D condensed distance matrix"
+            )
+        # extracting ligands' names
+        ligand_names = [int_matrix.iloc[0, 0]]
+        for lig in int_matrix.iloc[:, 1]:
+            if lig not in ligand_names:
+                ligand_names.append(lig)
+        log.debug(f"Ligand names {ligand_names}")
+        return int_matrix.iloc[:, 2], ligand_names
+    else:
+        raise Exception(f"input path {filename} does not exist!")


Test patch:
diff --git a/tests/test_clustering.py b/tests/test_clustering.py
index 07104e8..7c8ff17 100644
--- a/tests/test_clustering.py
+++ b/tests/test_clustering.py
@@ -1,33 +1,9 @@
-from pathlib import Path
-
 import numpy as np
-import pytest
 
 from arctic3d.modules.clustering import (  # write_clusters,; write_residues,
     cluster_distance_matrix,
-    read_int_matrix,
 )
 
-from . import golden_data
-
-
-def test_read_int_matrix_nonexisting():
-    """Test error on non-existing path."""
-    non_ex_path = "../dummy"
-    with pytest.raises(Exception):
-        read_int_matrix(non_ex_path)
-
-
-def test_read_int_matrix():
-    """Test correct reading of interface matrix."""
-    matrix_path = Path(golden_data, "interface_matrix.txt")
-    expected_int_matrix = np.array([0.9972, 0.3742, 0.9736, 0.9996, 0.8841, 0.9991])
-    expected_ligands = ["int_1", "int_2", "int_3", "int_4"]
-    observed_int_matrix, observed_ligands = read_int_matrix(matrix_path)
-    assert expected_ligands == observed_ligands
-    # now checking the matrix
-    np.testing.assert_allclose(expected_int_matrix, observed_int_matrix, atol=0.0001)
-
 
 def test_cluster_distance_matrix():
     """Test correct clustering"""
diff --git a/tests/test_interface_matrix.py b/tests/test_interface_matrix.py
index d39ac4b..576e89e 100644
--- a/tests/test_interface_matrix.py
+++ b/tests/test_interface_matrix.py
@@ -9,16 +9,23 @@ from arctic3d.modules.interface_matrix import (
     compute_scalar_product,
     filter_interfaces,
     get_coupling_matrix,
+    interface_matrix,
+    read_int_matrix,
 )
 
 from . import golden_data
 
 
 @pytest.fixture
-def example_mdu():
+def example_pdbpath():
+    """Example pdb path."""
+    return Path(golden_data, "1rypB_r_b.pdb")
+
+
+@pytest.fixture
+def example_mdu(example_pdbpath):
     """Example mdanalysis universe."""
-    pdb_path = Path(golden_data, "1rypB_r_b.pdb")
-    return mda.Universe(pdb_path)
+    return mda.Universe(example_pdbpath)
 
 
 @pytest.fixture
@@ -28,6 +35,18 @@ def example_interface_dict():
     return interface_dict
 
 
+@pytest.fixture
+def reference_jij():
+    jij = np.array(
+        [
+            [1.0, 0.358133, 0.031553],
+            [0.358133, 1.0, 0.366509],
+            [0.031553, 0.366509, 1.0],
+        ]
+    )
+    return jij
+
+
 def test_check_residues_coverage():
     """Test check_residues_coverage."""
     interface_one = [1, 2, 3]
@@ -38,7 +57,6 @@ def test_check_residues_coverage():
     assert filtered_int_one == interface_one
 
     cov_two, filtered_int_two = check_residues_coverage(interface_two, pdb_resids)
-
     assert cov_two == 0.75
     expected_filtered_int_two = [2, 3, 4]
     assert expected_filtered_int_two == filtered_int_two
@@ -52,35 +70,23 @@ def test_get_coupling_matrix_empty(example_mdu):
     assert observed_jij == expected_jij
 
 
-def test_get_coupling_matrix(example_mdu):
+def test_get_coupling_matrix(example_mdu, reference_jij):
     """Test get_coupling_matrix with a set of residues"""
     int_resids = [1, 2, 3]
-    expected_jij = np.array(
-        [
-            [1.0, 0.358133, 0.031553],
-            [0.358133, 1.0, 0.366509],
-            [0.031553, 0.366509, 1.0],
-        ]
-    )
     observed_jij = get_coupling_matrix(example_mdu, int_resids)
-    np.testing.assert_allclose(expected_jij, observed_jij, atol=0.00001)
+    np.testing.assert_allclose(reference_jij, observed_jij, atol=0.00001)
 
 
-def test_compute_scalar_product():
+def test_compute_scalar_product(reference_jij):
     """Test compute_scalar_product."""
-    jij = np.array(
-        [
-            [1.0, 0.358133, 0.031553],
-            [0.358133, 1.0, 0.366509],
-            [0.031553, 0.366509, 1.0],
-        ]
-    )
     interface_one = [0, 1, 2]
-    observed_norm = compute_scalar_product(interface_one, interface_one, jij)
+    observed_norm = compute_scalar_product(interface_one, interface_one, reference_jij)
     expected_norm = 4.51239
     np.testing.assert_allclose(expected_norm, observed_norm, atol=0.00001)
     interface_two = [0, 1]
-    observed_scal_prod = compute_scalar_product(interface_one, interface_two, jij)
+    observed_scal_prod = compute_scalar_product(
+        interface_one, interface_two, reference_jij
+    )
     expected_scal_prod = 3.11433
     np.testing.assert_allclose(expected_scal_prod, observed_scal_prod, atol=0.00001)
 
@@ -91,3 +97,42 @@ def test_filter_interfaces(example_mdu, example_interface_dict):
     pdb_resids = example_mdu.select_atoms("name CA").resids
     observed_filter_dict = filter_interfaces(example_interface_dict, pdb_resids)
     assert expected_filter_dict == observed_filter_dict
+
+
+def test_interface_matrix(example_interface_dict, example_pdbpath):
+    """Test interface_matrix"""
+    # defining expected quantities
+    expected_interface_matrix = np.array([0.2515])
+    expected_int_filename = "interface_matrix.txt"
+    expected_filter_dict = {"int_1": [1, 2], "int_2": [1, 2, 4]}
+    expected_interface_names = ["int_1", "int_2"]
+    # calculate interface matrix
+    observed_filter_ints, observed_int_filename = interface_matrix(
+        example_interface_dict, example_pdbpath
+    )
+    assert expected_filter_dict == observed_filter_ints
+    assert expected_int_filename == observed_int_filename
+    # read interface matrix
+    observed_int_matrix, obs_int_names = read_int_matrix(observed_int_filename)
+    np.testing.assert_allclose(
+        expected_interface_matrix, observed_int_matrix, atol=0.00001
+    )
+    assert expected_interface_names == obs_int_names
+
+
+def test_read_int_matrix_nonexisting():
+    """Test error on non-existing path."""
+    non_ex_path = "../dummy"
+    with pytest.raises(Exception):
+        read_int_matrix(non_ex_path)
+
+
+def test_read_int_matrix():
+    """Test correct reading of interface matrix."""
+    matrix_path = Path(golden_data, "interface_matrix.txt")
+    expected_int_matrix = np.array([0.9972, 0.3742, 0.9736, 0.9996, 0.8841, 0.9991])
+    expected_ligands = ["int_1", "int_2", "int_3", "int_4"]
+    observed_int_matrix, observed_ligands = read_int_matrix(matrix_path)
+    assert expected_ligands == observed_ligands
+    # now checking the matrix
+    np.testing.assert_allclose(expected_int_matrix, observed_int_matrix, atol=0.0001)