Repository: graspologic-org/graspologic

Issue:
Implement Padded SGM algorithm
<!--
The issue tracker is a tool to address bugs in GraSPy itself. If you'd like to report a bug in GraSPy, fill out the template below and provide any extra information that may be useful / related to your problem.
-->

## functionality
Input Graphs G1 and G2 with respective vertex sets {1,...,n_1} and {1,...,n_2} and with respective adjacency matrices A and B, and suppose n_1<n_2, use the SGM function to the padded A and B, then return the restriction of permutation matrix to the vertices of G1.

The output can be compared with Figure 3 in paper seeded graph matching: https://arxiv.org/pdf/1209.0367.pdf.



Discussion:
@SHAAAAN if you still have code/experiments for this in any form, would you mind just linking them here? 
Sure. Here is what I have done so far. 
https://github.com/SHAAAAN/MyGraspy/blob/master/codes/padding.ipynb  
I met some problems when I tried to merge it with the original SGM algo. Hope this is useful for other talented people~
Thanks so much Shan! Hope all is well for you
I have some code for this so I can take this. Would be a relatively quick fix once QAP is in scipy (#348).

Resolving patch:
diff --git a/graspologic/match/gmp.py b/graspologic/match/gmp.py
--- a/graspologic/match/gmp.py
+++ b/graspologic/match/gmp.py
@@ -57,6 +57,17 @@ class GraphMatch(BaseEstimator):
         Gives users the option to solve QAP rather than the Graph Matching Problem
         (GMP). This is accomplished through trivial negation of the objective function.
 
+    padding : string (default = 'adopted')
+        Allows user to specify padding scheme if `A` and `B` are not of equal size.
+        Say that `A` and `B` have :math:`n_1` and :math:`n_2` nodes, respectively, and
+        :math:`n_1 < n_2`.
+
+        "adopted" : matches `A` to the best fitting induced subgraph of `B`. Reduces the
+        affinity between isolated vertices added to `A` through padding and low-density
+        subgraphs of `B`.
+
+        "naive" : matches `A` to the best fitting subgraph of `B`.
+
     Attributes
     ----------
 
@@ -95,6 +106,7 @@ def __init__(
         shuffle_input=True,
         eps=0.1,
         gmp=True,
+        padding="adopted",
     ):
 
         if type(n_init) is int and n_init > 0:
@@ -130,6 +142,14 @@ def __init__(
         else:
             msg = '"gmp" must be a boolean'
             raise TypeError(msg)
+        if isinstance(padding, str) and padding in {"adopted", "naive"}:
+            self.padding = padding
+        elif isinstance(padding, str):
+            msg = 'Invalid "padding" parameter string'
+            raise ValueError(msg)
+        else:
+            msg = '"padding" parameter must be of type string'
+            raise TypeError(msg)
 
     def fit(self, A, B, seeds_A=[], seeds_B=[]):
         """
@@ -160,10 +180,11 @@ def fit(self, A, B, seeds_A=[], seeds_B=[]):
         seeds_A = column_or_1d(seeds_A)
         seeds_B = column_or_1d(seeds_B)
 
+        # pads A and B according to section 2.5 of [2]
         if A.shape[0] != B.shape[0]:
-            msg = "Adjacency matrices must be of equal size"
-            raise ValueError(msg)
-        elif A.shape[0] != A.shape[1] or B.shape[0] != B.shape[1]:
+            A, B = _adj_pad(A, B, self.padding)
+
+        if A.shape[0] != A.shape[1] or B.shape[0] != B.shape[1]:
             msg = "Adjacency matrix entries must be square"
             raise ValueError(msg)
         elif seeds_A.shape[0] != seeds_B.shape[0]:
@@ -332,6 +353,27 @@ def fit_predict(self, A, B, seeds_A=[], seeds_B=[]):
         return self.perm_inds_
 
 
+def _adj_pad(A, B, method):
+    def pad(X, n):
+        X_pad = np.zeros((n[1], n[1]))
+        X_pad[: n[0], : n[0]] = X
+        return X_pad
+
+    A_n = A.shape[0]
+    B_n = B.shape[0]
+    n = np.sort([A_n, B_n])
+    if method == "adopted":
+        A = 2 * A - np.ones((A_n, A_n))
+        B = 2 * B - np.ones((B_n, B_n))
+
+    if A.shape[0] == n[0]:
+        A = pad(A, n)
+    else:
+        B = pad(B, n)
+
+    return A, B
+
+
 def _unshuffle(array, n):
     unshuffle = np.array(range(n))
     unshuffle[array] = np.array(range(n))


Test patch:
diff --git a/tests/match/test_match.py b/tests/match/test_match.py
--- a/tests/match/test_match.py
+++ b/tests/match/test_match.py
@@ -7,6 +7,9 @@
 import random
 from graspologic.match import GraphMatch as GMP
 from graspologic.match import SinkhornKnopp as SK
+from graspologic.simulations import er_np
+
+np.random.seed(0)
 
 
 class TestGMP:
@@ -14,6 +17,7 @@ class TestGMP:
     def setup_class(cls):
         cls.barycenter = GMP(gmp=False)
         cls.rand = GMP(n_init=100, init_method="rand", gmp=False)
+        cls.barygm = GMP(gmp=True)
 
     def test_SGM_inputs(self):
         with pytest.raises(TypeError):
@@ -28,6 +32,10 @@ def test_SGM_inputs(self):
             GMP(eps=-1)
         with pytest.raises(TypeError):
             GMP(gmp="hey")
+        with pytest.raises(TypeError):
+            GMP(padding=2)
+        with pytest.raises(ValueError):
+            GMP(padding="hey")
         with pytest.raises(ValueError):
             GMP().fit(
                 np.random.random((3, 4)),
@@ -35,13 +43,6 @@ def test_SGM_inputs(self):
                 np.arange(2),
                 np.arange(2),
             )
-        with pytest.raises(ValueError):
-            GMP().fit(
-                np.random.random((3, 3)),
-                np.random.random((4, 4)),
-                np.arange(2),
-                np.arange(2),
-            )
         with pytest.raises(ValueError):
             GMP().fit(
                 np.random.random((3, 4)),
@@ -73,25 +74,6 @@ def _get_AB(self, qap_prob):
             return A, B
 
     def test_barycenter_SGM(self):
-        A, B = self._get_AB("lipa20a")
-        lipa20a = self.barycenter.fit(A, B)
-        score = lipa20a.score_
-        assert 3683 <= score < 3900
-
-        A, B = self._get_AB("lipa20b")
-        lipa20b = self.barycenter.fit(A, B)
-        score = lipa20b.score_
-        assert score == 27076
-
-        A, B = self._get_AB("lipa30a")
-        lipa30a = self.barycenter.fit(A, B)
-        score = lipa30a.score_
-        assert 13178 <= score < 13650
-
-        A, B = self._get_AB("lipa30b")
-        lipa30b = self.barycenter.fit(A, B)
-        score = lipa30b.score_
-        assert score == 151426
 
         A, B = self._get_AB("chr12c")
         n = A.shape[0]
@@ -108,21 +90,6 @@ def test_barycenter_SGM(self):
         score = chr12c.score_
         assert 11156 == score
 
-        A, B = self._get_AB("chr15a")
-        n = A.shape[0]
-        pi = np.array([5, 10, 8, 13, 12, 11, 14, 2, 4, 6, 7, 15, 3, 1, 9]) - [1] * n
-        W1 = [0, 5, 11, 14]
-        W2 = [pi[z] for z in W1]
-        chr15a = self.barycenter.fit(A, B, W1, W2)
-        score = chr15a.score_
-        assert 9896 <= score < 20000
-
-        W1 = np.sort(random.sample(list(range(n)), n - 1))
-        W2 = [pi[z] for z in W1]
-        chr15a = self.barycenter.fit(A, B, W1, W2)
-        score = chr15a.score_
-        assert 9896 == score
-
     def test_rand_SGM(self):
         A, B = self._get_AB("chr12c")
         chr12c = self.rand.fit(A, B)
@@ -137,18 +104,17 @@ def test_rand_SGM(self):
         score = chr12c.score_
         assert 11156 <= score < 12500
 
-        A, B = self._get_AB("chr15a")
-        chr15a = self.rand.fit(A, B)
-        score = chr15a.score_
-        assert 9896 <= score < 12000
+    def test_padding(self):
+        n = 50
+        p = 0.4
 
-        n = A.shape[0]
-        pi = np.array([5, 10, 8, 13, 12, 11, 14, 2, 4, 6, 7, 15, 3, 1, 9]) - [1] * n
-        W1 = [0, 5, 11, 14]
-        W2 = [pi[z] for z in W1]
-        chr15a = self.rand.fit(A, B, W1, W2)
-        score = chr15a.score_
-        assert 9896 <= score < 10000
+        np.random.seed(1)
+        G1 = er_np(n=n, p=p)
+        G2 = G1[: (n - 1), : (n - 1)]  # remove two nodes
+        gmp_adopted = GMP(padding="adopted")
+        res = gmp_adopted.fit(G1, G2)
+
+        assert 1.0 == (sum(res.perm_inds_ == np.arange(n)) / n)
 
 
 class TestSinkhornKnopp: