From 3d4761b79f8957cd439985ca39f3d3e6edeb1437 Mon Sep 17 00:00:00 2001 From: hugofluhr Date: Fri, 18 Aug 2023 11:14:04 +0200 Subject: [PATCH 1/7] [enh]-added smoothness metric and test --- nigsp/objects.py | 7 +++++++ nigsp/operations/__init__.py | 2 +- nigsp/operations/metrics.py | 19 +++++++++++++++++++ nigsp/tests/test_metrics.py | 10 ++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/nigsp/objects.py b/nigsp/objects.py index 489fb10..46110a4 100644 --- a/nigsp/objects.py +++ b/nigsp/objects.py @@ -43,6 +43,7 @@ def __init__( surr_split=None, sdi=None, gsdi=None, + smoothness=None, fc=None, fc_split=None, ): @@ -82,6 +83,7 @@ def __init__( self.surr_split = deepcopy(surr_split) self.sdi = deepcopy(sdi) self.gsdi = deepcopy(gsdi) + self.smoothness = deepcopy(smoothness) self.fc = deepcopy(fc) self.fc_split = deepcopy(fc_split) @@ -168,6 +170,11 @@ def compute_gsdi(self, mean=False, keys=None): # pragma: no cover self.gsdi = operations.gsdi(self.ts_split, mean, keys) return self + def compute_smoothness(self, signal): # pragma: no cover + """Implement metrics.smoothness as class method.""" + self.smoothness = operations.smoothness(self.lapl_mtx, signal) + return self + def create_surrogates(self, sc_type="informed", n_surr=1000, seed=None): """Implement surrogates.sc_informed and sc_uninformed as class method.""" sc_args = {"timeseries": self.timeseries, "n_surr": n_surr} diff --git a/nigsp/operations/__init__.py b/nigsp/operations/__init__.py index 6835232..878bb8f 100644 --- a/nigsp/operations/__init__.py +++ b/nigsp/operations/__init__.py @@ -15,7 +15,7 @@ # Import all operations. from .graph import nodestrength, zerocross from .laplacian import decomposition, symmetric_normalised_laplacian -from .metrics import functional_connectivity, gsdi, sdi +from .metrics import functional_connectivity, gsdi, sdi, smoothness from .nifti import apply_atlas, apply_mask, mat_to_vol, unfold_atlas, unmask, vol_to_mat from .surrogates import random_sign, sc_informed, sc_uninformed, test_significance from .timeseries import ( diff --git a/nigsp/operations/metrics.py b/nigsp/operations/metrics.py index 4555c3e..311da4c 100644 --- a/nigsp/operations/metrics.py +++ b/nigsp/operations/metrics.py @@ -246,6 +246,25 @@ def _fc(timeseries, mean=False): return fc +def smoothness(laplacian, signal): + """Compute the smoothness of a signal over the graph corresponding to given laplacian. + + Parameters + ---------- + node_signal : numpy.ndarray + any signal defined with one value per node. + laplacian : numpy.ndarray + graph laplacian to use. + + Returns + ------- + smoothness : float + the smoothness of the signal. + """ + LGR.info("Compute signal smoothness.") + return np.dot(signal.T, np.dot(laplacian, signal)) + + """ Copyright 2022, Stefano Moia. diff --git a/nigsp/tests/test_metrics.py b/nigsp/tests/test_metrics.py index 55d4b30..b400759 100644 --- a/nigsp/tests/test_metrics.py +++ b/nigsp/tests/test_metrics.py @@ -124,3 +124,13 @@ def test_functional_connectivity(): for k in fcd.keys(): assert (fcd[k] == np.corrcoef(tsd[k])).all() + + +def test_smoothness_calculation(): + signal = rand(10, 1) + laplacian = rand(10, 10) + + expected_smoothness = np.dot(signal.T, np.dot(laplacian, signal)) + computed_smoothness = metrics.smoothness(laplacian, signal) + + assert np.isclose(expected_smoothness, computed_smoothness, rtol=1e-10) From af2c4276b454a2095ac6e1d7c79d094bcb07deb0 Mon Sep 17 00:00:00 2001 From: hugofluhr Date: Fri, 18 Aug 2023 11:15:52 +0200 Subject: [PATCH 2/7] fixed name of test fct --- nigsp/tests/test_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nigsp/tests/test_metrics.py b/nigsp/tests/test_metrics.py index b400759..de5a34d 100644 --- a/nigsp/tests/test_metrics.py +++ b/nigsp/tests/test_metrics.py @@ -126,7 +126,7 @@ def test_functional_connectivity(): assert (fcd[k] == np.corrcoef(tsd[k])).all() -def test_smoothness_calculation(): +def test_smoothness(): signal = rand(10, 1) laplacian = rand(10, 10) From 1ce603d10692bd2048a442a027301e03503ca2ce Mon Sep 17 00:00:00 2001 From: hugofluhr Date: Fri, 18 Aug 2023 11:51:07 +0200 Subject: [PATCH 3/7] reformat --- nigsp/tests/test_metrics.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/nigsp/tests/test_metrics.py b/nigsp/tests/test_metrics.py index de5a34d..51f0816 100644 --- a/nigsp/tests/test_metrics.py +++ b/nigsp/tests/test_metrics.py @@ -72,6 +72,16 @@ def test_gsdi(): assert (gsdi_out["beta_over_alpha"] == gsdi_in).all() +def test_smoothness(): + signal = rand(10, 1) + laplacian = rand(10, 10) + + expected_smoothness = np.dot(signal.T, np.dot(laplacian, signal)) + computed_smoothness = metrics.smoothness(laplacian, signal) + + assert np.isclose(expected_smoothness, computed_smoothness, rtol=1e-10) + + # ### Break tests def test_break_sdi(): ts1 = np.arange(1, 3)[..., np.newaxis] @@ -124,13 +134,3 @@ def test_functional_connectivity(): for k in fcd.keys(): assert (fcd[k] == np.corrcoef(tsd[k])).all() - - -def test_smoothness(): - signal = rand(10, 1) - laplacian = rand(10, 10) - - expected_smoothness = np.dot(signal.T, np.dot(laplacian, signal)) - computed_smoothness = metrics.smoothness(laplacian, signal) - - assert np.isclose(expected_smoothness, computed_smoothness, rtol=1e-10) From c1ed94315d71fc4cac8ccca6c57b17fbd6ac43c1 Mon Sep 17 00:00:00 2001 From: hugofluhr Date: Mon, 21 Aug 2023 12:02:21 +0200 Subject: [PATCH 4/7] added citations for smoothness --- nigsp/operations/metrics.py | 8 ++++++++ nigsp/references.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/nigsp/operations/metrics.py b/nigsp/operations/metrics.py index 311da4c..fb48d0e 100644 --- a/nigsp/operations/metrics.py +++ b/nigsp/operations/metrics.py @@ -246,6 +246,7 @@ def _fc(timeseries, mean=False): return fc +@due.dcite(references.SHUMAN_2013) def smoothness(laplacian, signal): """Compute the smoothness of a signal over the graph corresponding to given laplacian. @@ -260,6 +261,13 @@ def smoothness(laplacian, signal): ------- smoothness : float the smoothness of the signal. + + References + ---------- + .. [1] D. I. Shuman, S. K. Narang, P. Frossard, A. Ortega and P. Vandergheynst, + "The emerging field of signal processing on graphs: Extending high-dimensional + data analysis to networks and other irregular domains," in IEEE Signal + Processing Magazine, vol. 30, no. 3, pp. 83-98, May 2013 """ LGR.info("Compute signal smoothness.") return np.dot(signal.T, np.dot(laplacian, signal)) diff --git a/nigsp/references.py b/nigsp/references.py index 305e06e..db94506 100644 --- a/nigsp/references.py +++ b/nigsp/references.py @@ -1,6 +1,8 @@ """References to be imported and injected throughout the package.""" from nigsp.due import Doi +SHUMAN_2013 = Doi("10.1109/MSP.2012.2235192") + PRETI_2019 = Doi("10.1038/s41467-019-12765-7") GRIFFA_2022 = Doi("10.1016/j.neuroimage.2022.118970") From a8ecda64143d92cd47e4bc0f295eba4c0312755f Mon Sep 17 00:00:00 2001 From: hugofluhr Date: Mon, 21 Aug 2023 17:30:57 +0200 Subject: [PATCH 5/7] add smoothness to CLI and supported metrics --- nigsp/cli/run.py | 12 ++++++++++++ nigsp/operations/metrics.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/nigsp/cli/run.py b/nigsp/cli/run.py index 19180dc..9f0d3fa 100644 --- a/nigsp/cli/run.py +++ b/nigsp/cli/run.py @@ -91,6 +91,18 @@ def _get_parser(): ), default=None, ) + opt_metrics.add_argument( + "-smooth", + "--smoothness", + dest="comp_metric", + action="append_const", + const="smoothness", + help=( + "Compute the signal smoothness " + "(see Shuman et al, 2013, IEEE Signal Proc. Mag.)" + ), + default=None, + ) opt_proc = parser.add_argument_group("Optional Arguments for data processing") opt_proc.add_argument( diff --git a/nigsp/operations/metrics.py b/nigsp/operations/metrics.py index fb48d0e..bb967e0 100644 --- a/nigsp/operations/metrics.py +++ b/nigsp/operations/metrics.py @@ -19,7 +19,7 @@ LGR = logging.getLogger(__name__) -SUPPORTED_METRICS = ["sdi", "dfc", "fc"] +SUPPORTED_METRICS = ["sdi", "dfc", "fc", "smoothness"] @due.dcite(references.PRETI_2019) From f51ddd7653370dc79cf30b0059f642800615be31 Mon Sep 17 00:00:00 2001 From: hugofluhr Date: Tue, 22 Aug 2023 14:06:42 +0200 Subject: [PATCH 6/7] fixed typo --- nigsp/operations/metrics.py | 28 ++++++++++++++++++++++++++-- nigsp/tests/test_metrics.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/nigsp/operations/metrics.py b/nigsp/operations/metrics.py index bb967e0..f5b72c6 100644 --- a/nigsp/operations/metrics.py +++ b/nigsp/operations/metrics.py @@ -248,7 +248,7 @@ def _fc(timeseries, mean=False): @due.dcite(references.SHUMAN_2013) def smoothness(laplacian, signal): - """Compute the smoothness of a signal over the graph corresponding to given laplacian. + """Compute the smoothness of a signal (as defined in [1]) over the graph corresponding to given Laplacian. Parameters ---------- @@ -270,7 +270,31 @@ def smoothness(laplacian, signal): Processing Magazine, vol. 30, no. 3, pp. 83-98, May 2013 """ LGR.info("Compute signal smoothness.") - return np.dot(signal.T, np.dot(laplacian, signal)) + + # Checking shape of signal + if signal.ndim == 1: + LGR.warning( + "2D array required, but signal is a vector. Adding empty dimension." + ) + signal = np.expand_dims(signal, -1) + elif signal.ndim > 2: + raise ValueError("Signal should be a 2D array.") + + # Checking if laplacian is square + if laplacian.shape[0] != laplacian.shape[1]: + raise ValueError("Laplacian should be a square matrix.") + + # Checking that the dimensions of signal and laplacian are compatible + if signal.shape[0] != laplacian.shape[0]: + if signal.shape[1] == laplacian.shape[0]: + LGR.warning( + "It seems that the signal needs to be transposed to the correct shape." + ) + signal = np.swapaxes(signal, 0, 1) + else: + raise ValueError("The dimensions of the signal and Laplacian don't match.") + + return np.matmul(signal.T, np.matmul(laplacian, signal)) """ diff --git a/nigsp/tests/test_metrics.py b/nigsp/tests/test_metrics.py index 51f0816..8f4da29 100644 --- a/nigsp/tests/test_metrics.py +++ b/nigsp/tests/test_metrics.py @@ -73,13 +73,13 @@ def test_gsdi(): def test_smoothness(): - signal = rand(10, 1) + signal = rand(10, 2) laplacian = rand(10, 10) expected_smoothness = np.dot(signal.T, np.dot(laplacian, signal)) computed_smoothness = metrics.smoothness(laplacian, signal) - assert np.isclose(expected_smoothness, computed_smoothness, rtol=1e-10) + assert (expected_smoothness == computed_smoothness).all() # ### Break tests @@ -134,3 +134,29 @@ def test_functional_connectivity(): for k in fcd.keys(): assert (fcd[k] == np.corrcoef(tsd[k])).all() + + +def test_break_smoothness(): + # shape of signal + signal = rand(3, 3, 3) + laplacian = rand(3, 3) + + with raises(ValueError) as errorinfo: + metrics.smoothness(laplacian, signal) + assert "should be a 2D" in str(errorinfo.value) + + # shape of laplacian + signal = rand(10, 2) + laplacian = rand(10, 9) + + with raises(ValueError) as errorinfo: + metrics.smoothness(laplacian, signal) + assert "a square matrix" in str(errorinfo.value) + + # shape mismatch + signal = rand(10, 2) + laplacian = rand(9, 9) + + with raises(ValueError) as errorinfo: + metrics.smoothness(laplacian, signal) + assert "don't match" in str(errorinfo.value) From 854c047f7e9dbdc888124cfa097bb6a6d4f4d380 Mon Sep 17 00:00:00 2001 From: hugofluhr Date: Wed, 23 Aug 2023 10:44:18 +0200 Subject: [PATCH 7/7] additional tests for different inputs --- nigsp/tests/test_metrics.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/nigsp/tests/test_metrics.py b/nigsp/tests/test_metrics.py index 8f4da29..3dd5900 100644 --- a/nigsp/tests/test_metrics.py +++ b/nigsp/tests/test_metrics.py @@ -3,7 +3,7 @@ import numpy as np from numpy.random import rand -from pytest import raises +from pytest import raises, warns from nigsp.operations import metrics from nigsp.utils import prepare_ndim_iteration @@ -73,13 +73,22 @@ def test_gsdi(): def test_smoothness(): - signal = rand(10, 2) + s1 = rand(10) + s2 = rand(10, 2) + s3 = rand(2, 10) laplacian = rand(10, 10) - expected_smoothness = np.dot(signal.T, np.dot(laplacian, signal)) - computed_smoothness = metrics.smoothness(laplacian, signal) + expected_smoothness1 = np.dot(s1.T, np.dot(laplacian, s1)) + expected_smoothness2 = np.dot(s2.T, np.dot(laplacian, s2)) + expected_smoothness3 = np.dot(s3, np.dot(laplacian, s3.T)) + + computed_smoothness1 = metrics.smoothness(laplacian, s1) + computed_smoothness2 = metrics.smoothness(laplacian, s2) + computed_smoothness3 = metrics.smoothness(laplacian, s3) - assert (expected_smoothness == computed_smoothness).all() + assert (expected_smoothness1 == computed_smoothness1).all() + assert (expected_smoothness2 == computed_smoothness2).all() + assert (expected_smoothness3 == computed_smoothness3).all() # ### Break tests