From 41cc8d171b572fd90577d0bb65c2079c0917af01 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Fri, 24 Jan 2025 11:14:32 -0500 Subject: [PATCH 01/26] fix: rewrite `normalized_hypergraph_laplacian` Fixes the implementation of `normalized_hypergraph_laplacian` to prevent negative eigenvalues. Rewrites core matrix calculations in full definition. --- xgi/linalg/laplacian_matrix.py | 70 ++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index 1ebdd686a..64e7db069 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -47,10 +47,15 @@ from warnings import warn import numpy as np -from scipy.sparse import csr_array, diags +from scipy.sparse import csr_array, diags, identity from ..exception import XGIError -from .hypergraph_matrix import adjacency_matrix, clique_motif_matrix, degree_matrix +from .hypergraph_matrix import ( + adjacency_matrix, + clique_motif_matrix, + degree_matrix, + incidence_matrix +) __all__ = [ "laplacian", @@ -183,13 +188,15 @@ def multiorder_laplacian( return (L_multi, rowdict) if index else L_multi -def normalized_hypergraph_laplacian(H, sparse=True, index=False): +def normalized_hypergraph_laplacian(H, weights=None, sparse=True, index=False): """Compute the normalized Laplacian. Parameters ---------- H : Hypergraph Hypergraph + weights : list of floats or None, optional + Hyperedge weights, by default None (every edge weighted as 1). sparse : bool, optional whether or not the laplacian is sparse, by default True index : bool, optional @@ -209,6 +216,9 @@ def normalized_hypergraph_laplacian(H, sparse=True, index=False): XGIError If there are isolated nodes. + XGIError + If there are negative edge weights. + References ---------- "Learning with Hypergraphs: Clustering, Classification, and Embedding" @@ -221,16 +231,60 @@ def normalized_hypergraph_laplacian(H, sparse=True, index=False): "Every node must be a member of an edge to avoid divide by zero error!" ) - D = degree_matrix(H) - A, rowdict = clique_motif_matrix(H, sparse=sparse, index=True) + if weights is not None: + if weights is not list: + raise XGIError("Edge weights must be given as a list!") + if len(weights) != H.num_edges: + raise XGIError("There must be as many edge weights as there are edges!") + if np.any(weights <= 0): + raise XGIError("Edge weights must be strictly positive!") + + + Dv = degree_matrix(H) + ( + H_, + rowdict, + _ # Discard edge name-index mapping + ) = incidence_matrix(H, sparse=sparse, index=True) if sparse: - Dinvsqrt = csr_array(diags(np.power(D, -0.5))) + Dv_invsqrt = csr_array(diags(np.power(Dv, -0.5))) + + De_inv = diags(list( + map( + lambda x: 1/x, + np.sum(H_, axis=0) + ) + )) + + if weights is not None: + W = diags(weights) + else: + W = identity(H.num_edges) + eye = csr_array((H.num_nodes, H.num_nodes)) eye.setdiag(1) else: - Dinvsqrt = np.diag(np.power(D, -0.5)) + Dv_invsqrt = np.diag(np.power(Dv, -0.5)) + + De_inv = np.diag(list( + map( + lambda x: 1/x, + np.sum(H_, axis=0) + ) + )) + + if weights is not None: + W = np.diag(weights) + else: + W = np.identity(H.num_edges) + eye = np.eye(H.num_nodes) - L = 0.5 * (eye - Dinvsqrt @ A @ Dinvsqrt) + + # PERF: There is a faster way to do this calculation if unweighted. + # W can be ignored entirely if unweighted, but it adds a couple conditionals and complicates the code. + # It is untested, but I suspect the performance change is negligible. + L = (eye - (Dv_invsqrt @ H_ @ W @ De_inv @ np.transpose(H_) @ Dv_invsqrt)) + return (L, rowdict) if index else L From a74ea9397d730b831d2b521657fa0fa9dc7a71fd Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Fri, 24 Jan 2025 11:16:34 -0500 Subject: [PATCH 02/26] test: add unit tests for updated laplacian Adds a proprty test for eigenvalue sign. Adds error tests for new `weights` variable. --- tests/linalg/test_matrix.py | 40 +++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/tests/linalg/test_matrix.py b/tests/linalg/test_matrix.py index cd59ff2f6..3e2def76c 100644 --- a/tests/linalg/test_matrix.py +++ b/tests/linalg/test_matrix.py @@ -2,8 +2,10 @@ import pytest from scipy.sparse import csr_array from scipy.sparse.linalg import norm as spnorm +from scipy.linalg import eigh import xgi +from xgi.exception import XGIError def test_incidence_matrix(edgelist1, edgelist3, edgelist4): @@ -605,24 +607,32 @@ def test_normalized_hypergraph_laplacian(): assert isinstance(L2, np.ndarray) assert np.all(L1.toarray() == L2) - assert np.all(np.diag(L2) == 0.5) - L3, d = xgi.normalized_hypergraph_laplacian(H, index=True) + evals = eigh(L2, eigvals_only=True) + negative_evals = list(filter(lambda e: e < 0, evals)) + assert (not negative_evals) | (np.allclose(negative_evals, 0)) + L3, d = xgi.normalized_hypergraph_laplacian(H, index=True) assert d == {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8} - true_L = np.array( - [ - [0.5, -0.5, -0.5, 0.0, 0.0, 0.0, 0.0, 0.0], - [-0.5, 0.5, -0.5, 0.0, 0.0, 0.0, 0.0, 0.0], - [-0.5, -0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.5, -0.35355339, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, -0.35355339, 0.5, -0.35355339, -0.35355339], - [0.0, 0.0, 0.0, 0.0, 0.0, -0.35355339, 0.5, -0.5], - [0.0, 0.0, 0.0, 0.0, 0.0, -0.35355339, -0.5, 0.5], - ] - ) - assert np.allclose(true_L, L2) + + # Weights error handling + ## Type errors + with pytest.raises(XGIError): + xgi.normalized_hypergraph_laplacian(H, weights=1) + with pytest.raises(XGIError): + xgi.normalized_hypergraph_laplacian(H, weights="1") + + ## Length errors + with pytest.raises(XGIError): # too few + xgi.normalized_hypergraph_laplacian(H, weights=[1]) + with pytest.raises(XGIError): # too many + xgi.normalized_hypergraph_laplacian(H, weights=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + + ## Value errors + with pytest.raises(XGIError): # zeros + xgi.normalized_hypergraph_laplacian(H, weights=[0, 1, 1, 1]) + with pytest.raises(XGIError): # negatives + xgi.normalized_hypergraph_laplacian(H, weights=[-1, 1, 1, 1]) def test_empty_order(edgelist6): From 5a924a31fb5bd9d1f65cffc5d162d193498f7f17 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Fri, 24 Jan 2025 11:19:37 -0500 Subject: [PATCH 03/26] doc: update `normalized_hypergraph_laplacian` doc Updated 'Raises' portion of function docstring to include type and length error catches on `weights` parameter. --- xgi/linalg/laplacian_matrix.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index 64e7db069..c8c2f4e30 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -217,7 +217,10 @@ def normalized_hypergraph_laplacian(H, weights=None, sparse=True, index=False): If there are isolated nodes. XGIError - If there are negative edge weights. + If there are an incorrect number of edge weights. + + XGIError + If there are non-positive edge weights. References ---------- From c85c72be49a550c3cba58d4652bffe29cccbe753 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Sat, 25 Jan 2025 09:59:07 -0500 Subject: [PATCH 04/26] test: added issue #657 m.w.e. as test --- tests/linalg/test_matrix.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/linalg/test_matrix.py b/tests/linalg/test_matrix.py index 3e2def76c..d628b6700 100644 --- a/tests/linalg/test_matrix.py +++ b/tests/linalg/test_matrix.py @@ -1,8 +1,8 @@ import numpy as np import pytest +from scipy.linalg import eigh from scipy.sparse import csr_array from scipy.sparse.linalg import norm as spnorm -from scipy.linalg import eigh import xgi from xgi.exception import XGIError @@ -615,6 +615,19 @@ def test_normalized_hypergraph_laplacian(): L3, d = xgi.normalized_hypergraph_laplacian(H, index=True) assert d == {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8} + el_mwe = [ + {1, 2, 3}, + {1, 4, 5}, + {1, 6, 7, 8}, + {4, 9, 10, 11, 12}, + {1, 13, 14, 15, 16}, + {4, 17, 18}, + ] + H = xgi.Hypergraph(E) + L = xgi.normalized_hypergraph_laplacian(H, sparse=False) + evals, evecs = eigh(L) + assert np.all(evals >= 0) + # Weights error handling ## Type errors with pytest.raises(XGIError): @@ -626,7 +639,9 @@ def test_normalized_hypergraph_laplacian(): with pytest.raises(XGIError): # too few xgi.normalized_hypergraph_laplacian(H, weights=[1]) with pytest.raises(XGIError): # too many - xgi.normalized_hypergraph_laplacian(H, weights=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + xgi.normalized_hypergraph_laplacian( + H, weights=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + ) ## Value errors with pytest.raises(XGIError): # zeros From 48810745bbe1b8e9c75464e5bbc3ae3f5c8f47cf Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Sun, 26 Jan 2025 11:24:13 -0500 Subject: [PATCH 05/26] fix(test): fix typo in unit test --- tests/linalg/test_matrix.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/linalg/test_matrix.py b/tests/linalg/test_matrix.py index d628b6700..28bdc4959 100644 --- a/tests/linalg/test_matrix.py +++ b/tests/linalg/test_matrix.py @@ -610,7 +610,7 @@ def test_normalized_hypergraph_laplacian(): evals = eigh(L2, eigvals_only=True) negative_evals = list(filter(lambda e: e < 0, evals)) - assert (not negative_evals) | (np.allclose(negative_evals, 0)) + assert (not negative_evals) or (np.allclose(negative_evals, 0)) L3, d = xgi.normalized_hypergraph_laplacian(H, index=True) assert d == {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8} @@ -623,9 +623,9 @@ def test_normalized_hypergraph_laplacian(): {1, 13, 14, 15, 16}, {4, 17, 18}, ] - H = xgi.Hypergraph(E) + H = xgi.Hypergraph(el_mwe) L = xgi.normalized_hypergraph_laplacian(H, sparse=False) - evals, evecs = eigh(L) + evals = eigh(L, eigvals_only=True) assert np.all(evals >= 0) # Weights error handling From 5de2a16003a1f7207767500ed6d15ecb9d353fac Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:39:50 -0500 Subject: [PATCH 06/26] Update xgi/linalg/laplacian_matrix.py Implement suggestion - Edge weight matrix Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index c8c2f4e30..3a019d10f 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -270,12 +270,8 @@ def normalized_hypergraph_laplacian(H, weights=None, sparse=True, index=False): else: Dv_invsqrt = np.diag(np.power(Dv, -0.5)) - De_inv = np.diag(list( - map( - lambda x: 1/x, - np.sum(H_, axis=0) - ) - )) + De = np.sum(H_, axis=0) + De_inv = np.diag(1 / De) if weights is not None: W = np.diag(weights) From 938e51bc331a57156d579ac8b43bac24ff0f8951 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Tue, 28 Jan 2025 16:27:01 -0500 Subject: [PATCH 07/26] test: add sqrt(d) eigenvector, true_L tests --- tests/linalg/test_matrix.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/linalg/test_matrix.py b/tests/linalg/test_matrix.py index 28bdc4959..a816fe242 100644 --- a/tests/linalg/test_matrix.py +++ b/tests/linalg/test_matrix.py @@ -1,6 +1,6 @@ import numpy as np import pytest -from scipy.linalg import eigh +from scipy.linalg import eigh, eigvalsh from scipy.sparse import csr_array from scipy.sparse.linalg import norm as spnorm @@ -615,6 +615,24 @@ def test_normalized_hypergraph_laplacian(): L3, d = xgi.normalized_hypergraph_laplacian(H, index=True) assert d == {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8} + # sqrt(d) is eigenvector with eigenvalue 0 + sqrt_d = np.array([np.sqrt(d) for d in H.nodes.degree.aslist()]) + assert np.allclose(L3 @ sqrt_d, 0) + + true_L = np.array( + [ + [0.666667, -0.333333, -0.333333, -0.0, -0.0, -0.0, -0.0, -0.0], + [-0.333333, 0.666667, -0.333333, -0.0, -0.0, -0.0, -0.0, -0.0], + [-0.333333, -0.333333, 0.666667, -0.0, -0.0, -0.0, -0.0, -0.0], + [-0.0, -0.0, -0.0, 0.0, -0.0, -0.0, -0.0, -0.0], + [-0.0, -0.0, -0.0, -0.0, 0.5, -0.353553, -0.0, -0.0], + [-0.0, -0.0, -0.0, -0.0, -0.353553, 0.583333, -0.235702, -0.235702], + [-0.0, -0.0, -0.0, -0.0, -0.0, -0.235702, 0.666667, -0.333333], + [-0.0, -0.0, -0.0, -0.0, -0.0, -0.235702, -0.333333, 0.666667], + ] + ) + assert np.allclose(true_L, L2) + el_mwe = [ {1, 2, 3}, {1, 4, 5}, @@ -625,7 +643,7 @@ def test_normalized_hypergraph_laplacian(): ] H = xgi.Hypergraph(el_mwe) L = xgi.normalized_hypergraph_laplacian(H, sparse=False) - evals = eigh(L, eigvals_only=True) + evals = eigvalsh(L) assert np.all(evals >= 0) # Weights error handling From 60f3802c9e860bdb96a73c8ad0a0668c145c28bd Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Tue, 28 Jan 2025 17:02:33 -0500 Subject: [PATCH 08/26] feat: update weighted argument for laplacian --- tests/linalg/test_matrix.py | 37 ++++++++++---------------- xgi/linalg/laplacian_matrix.py | 47 +++++++++++----------------------- 2 files changed, 29 insertions(+), 55 deletions(-) diff --git a/tests/linalg/test_matrix.py b/tests/linalg/test_matrix.py index a816fe242..a017030cc 100644 --- a/tests/linalg/test_matrix.py +++ b/tests/linalg/test_matrix.py @@ -641,31 +641,22 @@ def test_normalized_hypergraph_laplacian(): {1, 13, 14, 15, 16}, {4, 17, 18}, ] - H = xgi.Hypergraph(el_mwe) - L = xgi.normalized_hypergraph_laplacian(H, sparse=False) - evals = eigvalsh(L) - assert np.all(evals >= 0) + H_mwe = xgi.Hypergraph(el_mwe) + L_mwe = xgi.normalized_hypergraph_laplacian(H_mwe, sparse=False) + evals_mwe = eigvalsh(L_mwe) + assert np.all(evals_mwe >= 0) # Weights error handling - ## Type errors - with pytest.raises(XGIError): - xgi.normalized_hypergraph_laplacian(H, weights=1) - with pytest.raises(XGIError): - xgi.normalized_hypergraph_laplacian(H, weights="1") - - ## Length errors - with pytest.raises(XGIError): # too few - xgi.normalized_hypergraph_laplacian(H, weights=[1]) - with pytest.raises(XGIError): # too many - xgi.normalized_hypergraph_laplacian( - H, weights=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - ) - - ## Value errors - with pytest.raises(XGIError): # zeros - xgi.normalized_hypergraph_laplacian(H, weights=[0, 1, 1, 1]) - with pytest.raises(XGIError): # negatives - xgi.normalized_hypergraph_laplacian(H, weights=[-1, 1, 1, 1]) + ## Default + L_mwe_wtd = xgi.normalized_hypergraph_laplacian(H_mwe, weighted=True, sparse=False) + assert np.allclose(L_mwe, L_mwe_wtd) + + ## Uniform weight + H_mwe.set_edge_attributes(2, name="weight") + L_mwe_wtd_uni = xgi.normalized_hypergraph_laplacian( + H_mwe, weighted=True, sparse=False + ) + assert np.allclose(2 * L_mwe_wtd - np.eye(H_mwe.num_nodes), L_mwe_wtd_uni) def test_empty_order(edgelist6): diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index 3a019d10f..c7f6d9595 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -54,7 +54,7 @@ adjacency_matrix, clique_motif_matrix, degree_matrix, - incidence_matrix + incidence_matrix, ) __all__ = [ @@ -188,7 +188,7 @@ def multiorder_laplacian( return (L_multi, rowdict) if index else L_multi -def normalized_hypergraph_laplacian(H, weights=None, sparse=True, index=False): +def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False): """Compute the normalized Laplacian. Parameters @@ -234,34 +234,21 @@ def normalized_hypergraph_laplacian(H, weights=None, sparse=True, index=False): "Every node must be a member of an edge to avoid divide by zero error!" ) - if weights is not None: - if weights is not list: - raise XGIError("Edge weights must be given as a list!") - if len(weights) != H.num_edges: - raise XGIError("There must be as many edge weights as there are edges!") - if np.any(weights <= 0): - raise XGIError("Edge weights must be strictly positive!") - - - Dv = degree_matrix(H) ( - H_, + incidence, rowdict, - _ # Discard edge name-index mapping + _, # Discard edge name-index mapping ) = incidence_matrix(H, sparse=sparse, index=True) + Dv = degree_matrix(H) + De = np.sum(incidence, axis=0) + if sparse: Dv_invsqrt = csr_array(diags(np.power(Dv, -0.5))) + De_inv = diags(1 / De) - De_inv = diags(list( - map( - lambda x: 1/x, - np.sum(H_, axis=0) - ) - )) - - if weights is not None: - W = diags(weights) + if weighted: + W = diags([H.edges[edge_idx].get("weight", 1) for edge_idx in H.edges]) else: W = identity(H.num_edges) @@ -269,21 +256,17 @@ def normalized_hypergraph_laplacian(H, weights=None, sparse=True, index=False): eye.setdiag(1) else: Dv_invsqrt = np.diag(np.power(Dv, -0.5)) - - De = np.sum(H_, axis=0) De_inv = np.diag(1 / De) - if weights is not None: - W = np.diag(weights) + if weighted: + W = np.diag([H.edges[edge_idx].get("weight", 1) for edge_idx in H.edges]) else: W = np.identity(H.num_edges) eye = np.eye(H.num_nodes) - - # PERF: There is a faster way to do this calculation if unweighted. - # W can be ignored entirely if unweighted, but it adds a couple conditionals and complicates the code. - # It is untested, but I suspect the performance change is negligible. - L = (eye - (Dv_invsqrt @ H_ @ W @ De_inv @ np.transpose(H_) @ Dv_invsqrt)) + L = eye - ( + Dv_invsqrt @ incidence @ W @ De_inv @ np.transpose(incidence) @ Dv_invsqrt + ) return (L, rowdict) if index else L From 573eaad7c41708e9c0e640c613d3746588147014 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Tue, 28 Jan 2025 17:05:05 -0500 Subject: [PATCH 09/26] doc: update normalized_hypergraph_laplacian args --- xgi/linalg/laplacian_matrix.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index c7f6d9595..fadb041f4 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -195,8 +195,8 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) ---------- H : Hypergraph Hypergraph - weights : list of floats or None, optional - Hyperedge weights, by default None (every edge weighted as 1). + weighted : bool, optional + whether or not to use hyperedge weightr, by default False (every edge weighted as 1). sparse : bool, optional whether or not the laplacian is sparse, by default True index : bool, optional @@ -216,12 +216,6 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) XGIError If there are isolated nodes. - XGIError - If there are an incorrect number of edge weights. - - XGIError - If there are non-positive edge weights. - References ---------- "Learning with Hypergraphs: Clustering, Classification, and Embedding" From 726340bde6bccab413bb168e1c4abc00fe04c455 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:11:32 -0500 Subject: [PATCH 10/26] Update xgi/linalg/laplacian_matrix.py Fix docstring typo Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index fadb041f4..af1c27953 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -196,7 +196,7 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) H : Hypergraph Hypergraph weighted : bool, optional - whether or not to use hyperedge weightr, by default False (every edge weighted as 1). + whether or not to use hyperedge weights, by default False (every edge weighted as 1). sparse : bool, optional whether or not the laplacian is sparse, by default True index : bool, optional From e1cf9106bbfcbe57449975980ee6a8796fba5f79 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:13:27 -0500 Subject: [PATCH 11/26] Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index af1c27953..cd9bd98f4 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -238,7 +238,7 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) De = np.sum(incidence, axis=0) if sparse: - Dv_invsqrt = csr_array(diags(np.power(Dv, -0.5))) + Dv_invsqrt = diags_array(np.power(Dv, -0.5), format="csr") De_inv = diags(1 / De) if weighted: From 91b7522e1e32d8233969dd853d2adfe256488e2b Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:13:33 -0500 Subject: [PATCH 12/26] Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index cd9bd98f4..c6e6e3231 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -236,6 +236,9 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) Dv = degree_matrix(H) De = np.sum(incidence, axis=0) + if weighted: + weights = [H.edges[edge_idx].get("weight", 1) for edge_idx in H.edges] + if sparse: Dv_invsqrt = diags_array(np.power(Dv, -0.5), format="csr") From 1fc3593660197a2b009b512f769450ce580ac5c4 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:13:40 -0500 Subject: [PATCH 13/26] Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index c6e6e3231..026803efa 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -245,7 +245,7 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) De_inv = diags(1 / De) if weighted: - W = diags([H.edges[edge_idx].get("weight", 1) for edge_idx in H.edges]) + W = diags_array(weights, format="csr") else: W = identity(H.num_edges) From 5689658ec3712c57f8d498fa47f1bd997c8bd659 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:13:46 -0500 Subject: [PATCH 14/26] Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index 026803efa..f5a8ad8fb 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -249,8 +249,7 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) else: W = identity(H.num_edges) - eye = csr_array((H.num_nodes, H.num_nodes)) - eye.setdiag(1) + eye = eye_array(H.num_nodes, format="csr") else: Dv_invsqrt = np.diag(np.power(Dv, -0.5)) De_inv = np.diag(1 / De) From 6f128f4de0a6eae06a9abadad276be3cb2a7a6b0 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:13:51 -0500 Subject: [PATCH 15/26] Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index f5a8ad8fb..47198210a 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -247,7 +247,7 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) if weighted: W = diags_array(weights, format="csr") else: - W = identity(H.num_edges) + W = eye_array(H.num_edges, format="csr") eye = eye_array(H.num_nodes, format="csr") else: From 292017c35641c15c64192a80a5dbc583d60d8daf Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:13:57 -0500 Subject: [PATCH 16/26] Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index 47198210a..4bb88134f 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -255,7 +255,7 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) De_inv = np.diag(1 / De) if weighted: - W = np.diag([H.edges[edge_idx].get("weight", 1) for edge_idx in H.edges]) + W = np.diag(weights) else: W = np.identity(H.num_edges) From 10a6738332cead9ef1db5c1ad0ff992c39628a96 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:14:04 -0500 Subject: [PATCH 17/26] Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index 4bb88134f..be2894364 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -257,7 +257,7 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) if weighted: W = np.diag(weights) else: - W = np.identity(H.num_edges) + W = np.eye(H.num_edges) eye = np.eye(H.num_nodes) From fd0197b5dcb224ed3e5395cbd485f18f7abe9290 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:14:09 -0500 Subject: [PATCH 18/26] Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index be2894364..806e167da 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -261,8 +261,6 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) eye = np.eye(H.num_nodes) - L = eye - ( - Dv_invsqrt @ incidence @ W @ De_inv @ np.transpose(incidence) @ Dv_invsqrt - ) + L = eye - Dv_invsqrt @ incidence @ W @ De_inv @ incidence.T @ Dv_invsqrt return (L, rowdict) if index else L From be0e6c9d5e6e0ba56fbc35666345f3f555a33a34 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:39:50 -0500 Subject: [PATCH 19/26] Update xgi/linalg/laplacian_matrix.py Implement suggestion - Edge weight matrix Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index c8c2f4e30..3a019d10f 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -270,12 +270,8 @@ def normalized_hypergraph_laplacian(H, weights=None, sparse=True, index=False): else: Dv_invsqrt = np.diag(np.power(Dv, -0.5)) - De_inv = np.diag(list( - map( - lambda x: 1/x, - np.sum(H_, axis=0) - ) - )) + De = np.sum(H_, axis=0) + De_inv = np.diag(1 / De) if weights is not None: W = np.diag(weights) From 251e21d43e7b50e729027e3c0c8b5b49420e9687 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Tue, 28 Jan 2025 17:02:33 -0500 Subject: [PATCH 20/26] feat: update weighted argument for laplacian --- tests/linalg/test_matrix.py | 37 ++++++++++---------------- xgi/linalg/laplacian_matrix.py | 47 +++++++++++----------------------- 2 files changed, 29 insertions(+), 55 deletions(-) diff --git a/tests/linalg/test_matrix.py b/tests/linalg/test_matrix.py index a816fe242..a017030cc 100644 --- a/tests/linalg/test_matrix.py +++ b/tests/linalg/test_matrix.py @@ -641,31 +641,22 @@ def test_normalized_hypergraph_laplacian(): {1, 13, 14, 15, 16}, {4, 17, 18}, ] - H = xgi.Hypergraph(el_mwe) - L = xgi.normalized_hypergraph_laplacian(H, sparse=False) - evals = eigvalsh(L) - assert np.all(evals >= 0) + H_mwe = xgi.Hypergraph(el_mwe) + L_mwe = xgi.normalized_hypergraph_laplacian(H_mwe, sparse=False) + evals_mwe = eigvalsh(L_mwe) + assert np.all(evals_mwe >= 0) # Weights error handling - ## Type errors - with pytest.raises(XGIError): - xgi.normalized_hypergraph_laplacian(H, weights=1) - with pytest.raises(XGIError): - xgi.normalized_hypergraph_laplacian(H, weights="1") - - ## Length errors - with pytest.raises(XGIError): # too few - xgi.normalized_hypergraph_laplacian(H, weights=[1]) - with pytest.raises(XGIError): # too many - xgi.normalized_hypergraph_laplacian( - H, weights=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - ) - - ## Value errors - with pytest.raises(XGIError): # zeros - xgi.normalized_hypergraph_laplacian(H, weights=[0, 1, 1, 1]) - with pytest.raises(XGIError): # negatives - xgi.normalized_hypergraph_laplacian(H, weights=[-1, 1, 1, 1]) + ## Default + L_mwe_wtd = xgi.normalized_hypergraph_laplacian(H_mwe, weighted=True, sparse=False) + assert np.allclose(L_mwe, L_mwe_wtd) + + ## Uniform weight + H_mwe.set_edge_attributes(2, name="weight") + L_mwe_wtd_uni = xgi.normalized_hypergraph_laplacian( + H_mwe, weighted=True, sparse=False + ) + assert np.allclose(2 * L_mwe_wtd - np.eye(H_mwe.num_nodes), L_mwe_wtd_uni) def test_empty_order(edgelist6): diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index 3a019d10f..c7f6d9595 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -54,7 +54,7 @@ adjacency_matrix, clique_motif_matrix, degree_matrix, - incidence_matrix + incidence_matrix, ) __all__ = [ @@ -188,7 +188,7 @@ def multiorder_laplacian( return (L_multi, rowdict) if index else L_multi -def normalized_hypergraph_laplacian(H, weights=None, sparse=True, index=False): +def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False): """Compute the normalized Laplacian. Parameters @@ -234,34 +234,21 @@ def normalized_hypergraph_laplacian(H, weights=None, sparse=True, index=False): "Every node must be a member of an edge to avoid divide by zero error!" ) - if weights is not None: - if weights is not list: - raise XGIError("Edge weights must be given as a list!") - if len(weights) != H.num_edges: - raise XGIError("There must be as many edge weights as there are edges!") - if np.any(weights <= 0): - raise XGIError("Edge weights must be strictly positive!") - - - Dv = degree_matrix(H) ( - H_, + incidence, rowdict, - _ # Discard edge name-index mapping + _, # Discard edge name-index mapping ) = incidence_matrix(H, sparse=sparse, index=True) + Dv = degree_matrix(H) + De = np.sum(incidence, axis=0) + if sparse: Dv_invsqrt = csr_array(diags(np.power(Dv, -0.5))) + De_inv = diags(1 / De) - De_inv = diags(list( - map( - lambda x: 1/x, - np.sum(H_, axis=0) - ) - )) - - if weights is not None: - W = diags(weights) + if weighted: + W = diags([H.edges[edge_idx].get("weight", 1) for edge_idx in H.edges]) else: W = identity(H.num_edges) @@ -269,21 +256,17 @@ def normalized_hypergraph_laplacian(H, weights=None, sparse=True, index=False): eye.setdiag(1) else: Dv_invsqrt = np.diag(np.power(Dv, -0.5)) - - De = np.sum(H_, axis=0) De_inv = np.diag(1 / De) - if weights is not None: - W = np.diag(weights) + if weighted: + W = np.diag([H.edges[edge_idx].get("weight", 1) for edge_idx in H.edges]) else: W = np.identity(H.num_edges) eye = np.eye(H.num_nodes) - - # PERF: There is a faster way to do this calculation if unweighted. - # W can be ignored entirely if unweighted, but it adds a couple conditionals and complicates the code. - # It is untested, but I suspect the performance change is negligible. - L = (eye - (Dv_invsqrt @ H_ @ W @ De_inv @ np.transpose(H_) @ Dv_invsqrt)) + L = eye - ( + Dv_invsqrt @ incidence @ W @ De_inv @ np.transpose(incidence) @ Dv_invsqrt + ) return (L, rowdict) if index else L From 1b0fa8ead503e1123dd5a7d7272fea2cf5e0dffd Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Tue, 28 Jan 2025 17:05:05 -0500 Subject: [PATCH 21/26] doc: update normalized_hypergraph_laplacian args --- xgi/linalg/laplacian_matrix.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index c7f6d9595..fadb041f4 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -195,8 +195,8 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) ---------- H : Hypergraph Hypergraph - weights : list of floats or None, optional - Hyperedge weights, by default None (every edge weighted as 1). + weighted : bool, optional + whether or not to use hyperedge weightr, by default False (every edge weighted as 1). sparse : bool, optional whether or not the laplacian is sparse, by default True index : bool, optional @@ -216,12 +216,6 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) XGIError If there are isolated nodes. - XGIError - If there are an incorrect number of edge weights. - - XGIError - If there are non-positive edge weights. - References ---------- "Learning with Hypergraphs: Clustering, Classification, and Embedding" From 60762ea24048b893d6d43623e072309d6a18d466 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:11:32 -0500 Subject: [PATCH 22/26] Update xgi/linalg/laplacian_matrix.py Fix docstring typo Co-authored-by: Maxime Lucas Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index fadb041f4..806e167da 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -196,7 +196,7 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) H : Hypergraph Hypergraph weighted : bool, optional - whether or not to use hyperedge weightr, by default False (every edge weighted as 1). + whether or not to use hyperedge weights, by default False (every edge weighted as 1). sparse : bool, optional whether or not the laplacian is sparse, by default True index : bool, optional @@ -236,31 +236,31 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) Dv = degree_matrix(H) De = np.sum(incidence, axis=0) + if weighted: + weights = [H.edges[edge_idx].get("weight", 1) for edge_idx in H.edges] + if sparse: - Dv_invsqrt = csr_array(diags(np.power(Dv, -0.5))) + Dv_invsqrt = diags_array(np.power(Dv, -0.5), format="csr") De_inv = diags(1 / De) if weighted: - W = diags([H.edges[edge_idx].get("weight", 1) for edge_idx in H.edges]) + W = diags_array(weights, format="csr") else: - W = identity(H.num_edges) + W = eye_array(H.num_edges, format="csr") - eye = csr_array((H.num_nodes, H.num_nodes)) - eye.setdiag(1) + eye = eye_array(H.num_nodes, format="csr") else: Dv_invsqrt = np.diag(np.power(Dv, -0.5)) De_inv = np.diag(1 / De) if weighted: - W = np.diag([H.edges[edge_idx].get("weight", 1) for edge_idx in H.edges]) + W = np.diag(weights) else: - W = np.identity(H.num_edges) + W = np.eye(H.num_edges) eye = np.eye(H.num_nodes) - L = eye - ( - Dv_invsqrt @ incidence @ W @ De_inv @ np.transpose(incidence) @ Dv_invsqrt - ) + L = eye - Dv_invsqrt @ incidence @ W @ De_inv @ incidence.T @ Dv_invsqrt return (L, rowdict) if index else L From 059d05c3865d863b36aa156ed3347820a7da7f65 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Wed, 29 Jan 2025 10:48:39 -0500 Subject: [PATCH 23/26] test: separated #647 m.w.e. into new test Add the minimum(?) working example of issue #647 as new `test_` function. Tidied `test_normalized_hypergraph_laplacian` and added L2 and L3 comparison. --- tests/linalg/test_matrix.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/tests/linalg/test_matrix.py b/tests/linalg/test_matrix.py index a017030cc..07e44379e 100644 --- a/tests/linalg/test_matrix.py +++ b/tests/linalg/test_matrix.py @@ -608,18 +608,21 @@ def test_normalized_hypergraph_laplacian(): assert isinstance(L2, np.ndarray) assert np.all(L1.toarray() == L2) + # Eigenvalues are all non-negative evals = eigh(L2, eigvals_only=True) negative_evals = list(filter(lambda e: e < 0, evals)) assert (not negative_evals) or (np.allclose(negative_evals, 0)) L3, d = xgi.normalized_hypergraph_laplacian(H, index=True) assert d == {0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8} + assert np.allclose(L3.toarray(), L2) # sqrt(d) is eigenvector with eigenvalue 0 sqrt_d = np.array([np.sqrt(d) for d in H.nodes.degree.aslist()]) assert np.allclose(L3 @ sqrt_d, 0) - true_L = np.array( + # Exact Laplacian calculation + true_L3 = np.array( [ [0.666667, -0.333333, -0.333333, -0.0, -0.0, -0.0, -0.0, -0.0], [-0.333333, 0.666667, -0.333333, -0.0, -0.0, -0.0, -0.0, -0.0], @@ -631,9 +634,11 @@ def test_normalized_hypergraph_laplacian(): [-0.0, -0.0, -0.0, -0.0, -0.0, -0.235702, -0.333333, 0.666667], ] ) - assert np.allclose(true_L, L2) + assert np.allclose(true_L3, L3.toarray()) - el_mwe = [ + +def test_fix_647(): + el = [ {1, 2, 3}, {1, 4, 5}, {1, 6, 7, 8}, @@ -641,22 +646,25 @@ def test_normalized_hypergraph_laplacian(): {1, 13, 14, 15, 16}, {4, 17, 18}, ] - H_mwe = xgi.Hypergraph(el_mwe) - L_mwe = xgi.normalized_hypergraph_laplacian(H_mwe, sparse=False) - evals_mwe = eigvalsh(L_mwe) + H = xgi.Hypergraph(el) + L = xgi.normalized_hypergraph_laplacian(H, sparse=False) + + # Eigenvalues non-negative + evals_mwe = eigvalsh(L) assert np.all(evals_mwe >= 0) # Weights error handling - ## Default - L_mwe_wtd = xgi.normalized_hypergraph_laplacian(H_mwe, weighted=True, sparse=False) - assert np.allclose(L_mwe, L_mwe_wtd) + ## Default value when "weight" attribute unavailable + L_wtd = xgi.normalized_hypergraph_laplacian(H, weighted=True, sparse=False) + assert np.allclose(L, L_wtd) ## Uniform weight - H_mwe.set_edge_attributes(2, name="weight") - L_mwe_wtd_uni = xgi.normalized_hypergraph_laplacian( - H_mwe, weighted=True, sparse=False + H_wtd = H.copy() + H_wtd.set_edge_attributes(2, name="weight") + L_wtd_uniform = xgi.normalized_hypergraph_laplacian( + H_wtd, weighted=True, sparse=False ) - assert np.allclose(2 * L_mwe_wtd - np.eye(H_mwe.num_nodes), L_mwe_wtd_uni) + assert np.allclose(2 * L_wtd - np.eye(H_wtd.num_nodes), L_wtd_uniform) def test_empty_order(edgelist6): From 88cdd4cce80866e6ea00aa393101655bd96e0cc4 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Wed, 29 Jan 2025 10:51:30 -0500 Subject: [PATCH 24/26] feat: update scipy sparse array to modern use --- xgi/linalg/laplacian_matrix.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index 806e167da..b983a52d3 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -47,7 +47,7 @@ from warnings import warn import numpy as np -from scipy.sparse import csr_array, diags, identity +from scipy.sparse import csr_array, diags_array, eye_array from ..exception import XGIError from .hypergraph_matrix import ( @@ -236,31 +236,23 @@ def normalized_hypergraph_laplacian(H, weighted=False, sparse=True, index=False) Dv = degree_matrix(H) De = np.sum(incidence, axis=0) + if weighted: weights = [H.edges[edge_idx].get("weight", 1) for edge_idx in H.edges] - + else: + weights = [1] * H.num_edges if sparse: Dv_invsqrt = diags_array(np.power(Dv, -0.5), format="csr") - De_inv = diags(1 / De) - - if weighted: - W = diags_array(weights, format="csr") - else: - W = eye_array(H.num_edges, format="csr") - + De_inv = diags_array(1 / De, format="csr") + W = diags_array(weights, format="csr") eye = eye_array(H.num_nodes, format="csr") else: Dv_invsqrt = np.diag(np.power(Dv, -0.5)) De_inv = np.diag(1 / De) - - if weighted: - W = np.diag(weights) - else: - W = np.eye(H.num_edges) - + W = np.diag(weights) eye = np.eye(H.num_nodes) - L = eye - Dv_invsqrt @ incidence @ W @ De_inv @ incidence.T @ Dv_invsqrt + L = eye - Dv_invsqrt @ incidence @ W @ De_inv @ np.tranpose(incidence) @ Dv_invsqrt return (L, rowdict) if index else L From 1d5598bdffab1e844998d1b7e5053d45a0a1c44a Mon Sep 17 00:00:00 2001 From: Daniel Kaiser Date: Wed, 29 Jan 2025 11:20:11 -0500 Subject: [PATCH 25/26] chore: update diags_array scipy use in 'laplacian' --- xgi/linalg/laplacian_matrix.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index d20f11b69..df0ca522a 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -52,7 +52,6 @@ from ..exception import XGIError from .hypergraph_matrix import ( adjacency_matrix, - clique_motif_matrix, degree_matrix, incidence_matrix, ) @@ -107,7 +106,7 @@ def laplacian(H, order=1, sparse=False, rescale_per_node=False, index=False): return (L, {}) if index else L if sparse: - K = csr_array(diags(degree_matrix(H, order=order))) + K = csr_array(diags_array(degree_matrix(H, order=order))) else: K = np.diag(degree_matrix(H, order=order)) From 327e6cd177b414ecc0ec30fd4a945a71d546d9b2 Mon Sep 17 00:00:00 2001 From: Daniel Kaiser <56703624+kaiser-dan@users.noreply.github.com> Date: Thu, 30 Jan 2025 09:58:24 -0500 Subject: [PATCH 26/26] Update xgi/linalg/laplacian_matrix.py Co-authored-by: Maxime Lucas --- xgi/linalg/laplacian_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xgi/linalg/laplacian_matrix.py b/xgi/linalg/laplacian_matrix.py index df0ca522a..47a3ff323 100644 --- a/xgi/linalg/laplacian_matrix.py +++ b/xgi/linalg/laplacian_matrix.py @@ -106,7 +106,7 @@ def laplacian(H, order=1, sparse=False, rescale_per_node=False, index=False): return (L, {}) if index else L if sparse: - K = csr_array(diags_array(degree_matrix(H, order=order))) + K = diags_array(degree_matrix(H, order=order), format="csr") else: K = np.diag(degree_matrix(H, order=order))