Repository: neurodata/graspy
Pull request #72 (2018-12-02 21:14:28)

Misc bug fixes and additions
 - Fixes gridplot axis reversal bug, and makes sure gridplot preserves spacing of empty blocks of nodes 
 - Adds ability to modify scalar weight of diag aug 
 - Makes multigraph lcc recurse when necessary
 - Makes sure lcc isn't modifying input graph

netlify[bot]: Deploy preview for *graspy* processing.

Built with commit 8c1f241b715ad6d0d742c8aad3820c4f6aaee6ba

https://app.netlify.com/sites/graspy/deploys/5c044b359a063f2d8e75eae2
codecov-io: # [Codecov](https://codecov.io/gh/neurodata/graspy/pull/72?src=pr&el=h1) Report
> Merging [#72](https://codecov.io/gh/neurodata/graspy/pull/72?src=pr&el=desc) into [master](https://codecov.io/gh/neurodata/graspy/commit/6ba31d316e9e7334ea71058df70de5307af715b8?src=pr&el=desc) will **decrease** coverage by `0.28%`.
> The diff coverage is `85.71%`.

[![Impacted file tree graph](https://codecov.io/gh/neurodata/graspy/pull/72/graphs/tree.svg?width=650&token=vPDLP4YV5u&height=150&src=pr)](https://codecov.io/gh/neurodata/graspy/pull/72?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #72      +/-   ##
==========================================
- Coverage   96.52%   96.23%   -0.29%     
==========================================
  Files          19       19              
  Lines         892      904      +12     
==========================================
+ Hits          861      870       +9     
- Misses         31       34       +3
```


| [Impacted Files](https://codecov.io/gh/neurodata/graspy/pull/72?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [graspy/plot/plot.py](https://codecov.io/gh/neurodata/graspy/pull/72/diff?src=pr&el=tree#diff-Z3Jhc3B5L3Bsb3QvcGxvdC5weQ==) | `91.39% <100%> (+0.11%)` | :arrow_up: |
| [graspy/utils/utils.py](https://codecov.io/gh/neurodata/graspy/pull/72/diff?src=pr&el=tree#diff-Z3Jhc3B5L3V0aWxzL3V0aWxzLnB5) | `92.12% <83.33%> (-1.9%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/neurodata/graspy/pull/72?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/neurodata/graspy/pull/72?src=pr&el=footer). Last update [6ba31d3...8c1f241](https://codecov.io/gh/neurodata/graspy/pull/72?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/graspy/plot/plot.py b/graspy/plot/plot.py
index 6f3f67be..38ccacb6 100644
--- a/graspy/plot/plot.py
+++ b/graspy/plot/plot.py
@@ -262,17 +262,18 @@ def gridplot(X,
 
     dfs = []
     for idx, graph in enumerate(graphs):
-        cdx, rdx = np.where(graph > 0)
-        weights = graph[(cdx, rdx)]
+        rdx, cdx = np.where(graph > 0)
+        weights = graph[(rdx, cdx)]
         df = pd.DataFrame(
-            np.vstack([cdx[::-1], rdx, weights]).T,
-            columns=['cdx', 'rdx', 'Weights'])
+            np.vstack([rdx, cdx, weights]).T,
+            columns=['rdx', 'cdx','Weights'])
         df['Type'] = [labels[idx]] * len(cdx)
         dfs.append(df)
 
     df = pd.concat(dfs, axis=0)
 
     with sns.plotting_context(context, font_scale=font_scale):
+        sns.set_style('white')
         plot = sns.relplot(
             data=df,
             x='cdx',
@@ -282,14 +283,20 @@ def gridplot(X,
             sizes=sizes,
             alpha=alpha,
             palette=palette,
-            height=height)
+            height=height,
+            facet_kws={'sharex':True,
+                       'sharey':True,
+                       'xlim':(0,graph.shape[0]),
+                       'ylim':(0,graph.shape[0]),})
         plot.ax.axis('off')
+        plot.ax.invert_yaxis()
         if title is not None:
             plot.set(title=title)
 
     return plot
 
 
+
 def pairplot(X,
              Y=None,
              col_names=None,
diff --git a/graspy/utils/utils.py b/graspy/utils/utils.py
index d82ea823..1d793f8f 100644
--- a/graspy/utils/utils.py
+++ b/graspy/utils/utils.py
@@ -128,7 +128,7 @@ def remove_loops(graph):
     return graph
 
 
-def to_laplace(graph, form='I-DAD'):
+def to_laplace(graph, form='DAD'):
     r"""
     A function to convert graph adjacency matrix to graph laplacian. 
 
@@ -170,7 +170,7 @@ def to_laplace(graph, form='I-DAD'):
     elif form == 'DAD':
         L = np.dot(D_root, adj_matrix)
         L = np.dot(L, D_root)
-    return symmetrize(L, method='avg')
+    return symmetrize(L, method='avg') # sometimes machine prec. makes this necessary
 
 def is_fully_connected(graph):
     '''
@@ -244,18 +244,20 @@ def get_lcc(graph, return_inds=False):
             g_object = nx.Graph()
         else:
             g_object = nx.DiGraph()
-        graph = nx.from_numpy_matrix(graph, create_using=g_object)
+        graph = nx.from_numpy_array(graph, create_using=g_object)
     if type(graph) in [nx.Graph, nx.MultiGraph]:
-        graph = max(nx.connected_component_subgraphs(graph), key=len)
+        lcc_nodes = max(nx.connected_components(graph), key=len)
     elif type(graph) in [nx.DiGraph, nx.MultiDiGraph]:
-        graph = max(nx.weakly_connected_component_subgraphs(graph), key=len)        
+        lcc_nodes = max(nx.weakly_connected_components(graph), key=len) 
+    lcc = graph.subgraph(lcc_nodes).copy()
+    lcc.remove_nodes_from([n for n in lcc if n not in lcc_nodes])
     if return_inds:
-        nodelist = list(graph.nodes)
+        nodelist = list(lcc_nodes)
     if input_ndarray:
-        graph = nx.to_numpy_array(graph)
+        lcc = nx.to_numpy_array(lcc)
     if return_inds:
-        return graph, nodelist
-    return graph
+        return lcc, nodelist
+    return lcc
 
 def get_multigraph_lcc(graphs, return_inds=False):
     '''
@@ -295,10 +297,21 @@ def get_multigraph_lcc(graphs, return_inds=False):
     new_graphs = []        
     for graph in graphs:
         if type(graph) is np.ndarray:
-            graph = graph[inds_intersection,:][:,inds_intersection]
+            lcc = graph[inds_intersection,:][:,inds_intersection]
         else: 
-            graph = graph.subgraph(inds_intersection)
-        new_graphs.append(graph)
+            lcc = graph.subgraph(inds_intersection).copy()
+            lcc.remove_nodes_from([n for n in lcc if n not in inds_intersection])
+        new_graphs.append(lcc)
+    # this is not guaranteed be connected after one iteration because taking the 
+    # intersection of nodes among graphs can cause some components to become 
+    # disconnected, so, we check for this and run again if necessary 
+    recurse = False
+    for new_graph in new_graphs:
+        if not is_fully_connected(new_graph):
+            recurse = True
+            break 
+    if recurse: 
+        new_graphs, inds_intersection = get_multigraph_lcc(new_graphs, return_inds=True) 
     if type(graphs) != list:
         new_graphs = np.stack(new_graphs)
     if return_inds:
@@ -306,7 +319,7 @@ def get_multigraph_lcc(graphs, return_inds=False):
     else:
         return new_graphs
 
-def augment_diagonal(graph):
+def augment_diagonal(graph, weight=1):
     '''
     Replaces the diagonal of adjacency matrix with 
     :math: \frac{degree}{num_verts - 1} for the degree associated
@@ -327,6 +340,6 @@ def augment_diagonal(graph):
     # use out degree for directed graph 
     # ignore self loops in either case
     degrees = np.count_nonzero(graph, axis=1)
-    diag = degrees / divisor
+    diag = weight * degrees / divisor
     graph += np.diag(diag)
     return graph


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 7e06d2f7..fa11eff1 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -60,6 +60,13 @@ class TestInput(unittest.TestCase):
 
         self.assertTrue(np.allclose(L_normed, expected_L_normed, rtol=1e-04))
 
+    def test_to_laplace_symmetric(self):
+        A = np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]])
+
+        L_normed = gus.to_laplace(A, form='DAD')
+
+        self.assertTrue(gus.is_symmetric(L_normed))
+
     def test_to_laplace_unsuported(self):
         with self.assertRaises(TypeError):
             gus.to_laplace(self.A, form='MOM')