From 8b89cf48ede1b3ba4f07a4dbcaf238319fdf3b13 Mon Sep 17 00:00:00 2001 From: yemeen Date: Mon, 7 Apr 2025 17:49:49 -0400 Subject: [PATCH 1/4] Add SECT class for Smooth Euler Characteristic Transform --- src/ect/__init__.py | 12 +++++---- src/ect/sect.py | 59 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 src/ect/sect.py diff --git a/src/ect/__init__.py b/src/ect/__init__.py index c45984d..63c0614 100644 --- a/src/ect/__init__.py +++ b/src/ect/__init__.py @@ -12,12 +12,14 @@ from .embed_graph import EmbeddedGraph from .embed_cw import EmbeddedCW from .directions import Directions +from .sect import SECT from .utils import examples __all__ = [ - 'ECT', - 'EmbeddedGraph', - 'EmbeddedCW', - 'Directions', - 'examples', + "ECT", + "SECT", + "EmbeddedGraph", + "EmbeddedCW", + "Directions", + "examples", ] diff --git a/src/ect/sect.py b/src/ect/sect.py new file mode 100644 index 0000000..da981ef --- /dev/null +++ b/src/ect/sect.py @@ -0,0 +1,59 @@ +from ect import ECT +from .embed_graph import EmbeddedGraph +from .embed_cw import EmbeddedCW +from .directions import Directions +from .results import ECTResult +from typing import Optional, Union +import numpy as np + + +class SECT(ECT): + """ + A class to calculate the Smooth Euler Characteristic Transform (SECT). + Inherits from ECT and applies smoothing to the final result. + """ + + def __init__( + self, + directions: Optional[Directions] = None, + num_dirs: Optional[int] = None, + num_thresh: Optional[int] = None, + bound_radius: Optional[float] = None, + thresholds: Optional[np.ndarray] = None, + dtype=np.float32, + ): + """Initialize SECT calculator with smoothing parameter + + Args: + directions: Optional pre-configured Directions object + num_dirs: Number of directions to sample (ignored if directions provided) + num_thresh: Number of threshold values (required if directions not provided) + bound_radius: Optional radius for bounding circle + thresholds: Optional array of thresholds + dtype: Data type for output array + """ + super().__init__( + directions, num_dirs, num_thresh, bound_radius, thresholds, dtype + ) + + def calculate( + self, + graph: Union[EmbeddedGraph, EmbeddedCW], + theta: Optional[float] = None, + override_bound_radius: Optional[float] = None, + ) -> ECTResult: + """Calculate Smooth Euler Characteristic Transform (SECT) + + Args: + graph: The input graph to calculate the SECT for + theta: The angle in [0,2π] for the direction to calculate the SECT + override_bound_radius: Optional override for bounding radius + + Returns: + ECTResult: The smoothed transform result containing the matrix, + directions, and thresholds + """ + ect_result = super().calculate(graph, theta, override_bound_radius) + return ECTResult( + ect_result, ect_result.directions, ect_result.thresholds + ).smooth() From 0dea835baca062a613b8724a3572762da1e05b64 Mon Sep 17 00:00:00 2001 From: yemeen Date: Mon, 7 Apr 2025 17:50:10 -0400 Subject: [PATCH 2/4] Add SECT unit tests for inheritance and calculation --- tests/test_sect.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/test_sect.py diff --git a/tests/test_sect.py b/tests/test_sect.py new file mode 100644 index 0000000..e34ca55 --- /dev/null +++ b/tests/test_sect.py @@ -0,0 +1,76 @@ +import unittest +import numpy as np +from ect import SECT, ECT +from ect.utils.examples import create_example_graph +from ect.directions import Directions + + +class TestSECT(unittest.TestCase): + def setUp(self): + """Set up test fixtures""" + self.graph = create_example_graph() + self.num_dirs = 8 + self.num_thresh = 10 + self.sect = SECT(num_dirs=self.num_dirs, num_thresh=self.num_thresh) + + def test_inheritance(self): + """Test that SECT properly inherits from ECT""" + self.assertIsInstance(self.sect, ECT) + self.assertTrue(hasattr(self.sect, "calculate")) + + def test_calculate_output_shape(self): + """Test that SECT calculation returns correct shape""" + result = self.sect.calculate(self.graph) + + self.assertEqual(result.shape[0], self.num_dirs) + self.assertEqual(result.shape[1], self.num_thresh) + self.assertEqual(len(result.thresholds), self.num_thresh) + self.assertEqual(len(result.directions), self.num_dirs) + + def test_smoothing_effect(self): + """Test that smoothing is actually applied""" + # Calculate both ECT and SECT + ect = ECT(num_dirs=self.num_dirs, num_thresh=self.num_thresh) + sect = SECT(num_dirs=self.num_dirs, num_thresh=self.num_thresh) + + ect_result = ect.calculate(self.graph) + sect_result = sect.calculate(self.graph) + + # Verify results are different due to smoothing + self.assertFalse(np.allclose(ect_result, sect_result)) + + # Verify smoothing preserves direction count + self.assertEqual( + np.sum(ect_result, axis=1).shape, + np.sum(sect_result, axis=1).shape, + ) + + def test_with_theta(self): + """Test SECT calculation with specific theta value""" + theta = np.pi / 4 + result = self.sect.calculate(self.graph, theta=theta) + + # Should only have one direction when theta is specified + self.assertEqual(result.shape[0], 1) + self.assertEqual(result.shape[1], self.num_thresh) + + def test_with_override_radius(self): + """Test SECT calculation with override_bound_radius""" + override_radius = 2.0 + result = self.sect.calculate(self.graph, override_bound_radius=override_radius) + + # Check that thresholds are within the override radius + self.assertLessEqual(np.max(np.abs(result.thresholds)), override_radius) + + def test_smooth_matrix_properties(self): + """Test properties of the smoothed matrix""" + result = self.sect.calculate(self.graph) + + # Smoothed values should be finite + self.assertTrue(np.all(np.isfinite(result))) + + # Shape should be preserved after smoothing + self.assertEqual(result.shape, (self.num_dirs, self.num_thresh)) + + # Verify result is float type after smoothing + self.assertTrue(np.issubdtype(result.dtype, np.floating)) From 5d5765ae1cc6b192ed74c9f4d7927da4ad15347c Mon Sep 17 00:00:00 2001 From: Liz Munch Date: Tue, 15 Apr 2025 16:51:59 -0400 Subject: [PATCH 3/4] Added missing documentation --- doc_source/directions.md | 6 ++++++ doc_source/ect_on_graphs.md | 5 +++++ doc_source/modules.rst | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 doc_source/directions.md diff --git a/doc_source/directions.md b/doc_source/directions.md new file mode 100644 index 0000000..35f204e --- /dev/null +++ b/doc_source/directions.md @@ -0,0 +1,6 @@ +# Directions + +```{eval-rst} +.. automodule:: ect.directions + :members: +``` \ No newline at end of file diff --git a/doc_source/ect_on_graphs.md b/doc_source/ect_on_graphs.md index 7333ad5..ecda18f 100644 --- a/doc_source/ect_on_graphs.md +++ b/doc_source/ect_on_graphs.md @@ -4,3 +4,8 @@ .. automodule:: ect.ect_graph :members: ``` + +```{eval-rst} +.. automodule:: ect.sect + :members: +``` diff --git a/doc_source/modules.rst b/doc_source/modules.rst index c47c1e7..13c0753 100644 --- a/doc_source/modules.rst +++ b/doc_source/modules.rst @@ -7,4 +7,5 @@ Table of Contents Embedded graphs Embedded CW complex - ECT on graphs \ No newline at end of file + ECT on graphs + Directions \ No newline at end of file From 2811a621c736074d83e2e60687e50cfcac9b6c24 Mon Sep 17 00:00:00 2001 From: Liz Munch Date: Tue, 15 Apr 2025 16:53:32 -0400 Subject: [PATCH 4/4] Increment number --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b5aa6dc..18aa335 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ect" -version = "1.0.0" +version = "1.0.2" authors = [ { name="Liz Munch", email="muncheli@msu.edu" }, ]