From 757cc0482fe2afad399cb0e5d9d44d37d1c9e88b Mon Sep 17 00:00:00 2001 From: ishikaghosh2201 <112980412+ishikaghosh2201@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:22:28 -0400 Subject: [PATCH 1/4] Fix O(n) reset_pos cost in ReebGraph.slice() --- cereeberus/cereeberus/reeb/reebgraph.py | 56 +++++++++++++------------ tests/test_reeb_class.py | 36 ++++++++++++++++ 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/cereeberus/cereeberus/reeb/reebgraph.py b/cereeberus/cereeberus/reeb/reebgraph.py index 05a03049..5975d376 100644 --- a/cereeberus/cereeberus/reeb/reebgraph.py +++ b/cereeberus/cereeberus/reeb/reebgraph.py @@ -5,11 +5,11 @@ import numpy as np from ..draw import draw +from collections import Counter # from build.lib.cereeberus.reeb import graph - class ReebGraph(nx.MultiDiGraph): """ A Reeb graph stored as a networkx ``MultiDiGraph``. The function values are stored as a dictionary. The directedness of the edges follows the convention that the edge goes from the lower function value to the higher function value node. @@ -79,23 +79,23 @@ def copy(self): """ # Create a new ReebGraph with copies of the nodes and edges H = ReebGraph() - + # Copy the function values dictionary H.f = self.f.copy() - + # Copy all nodes and edges from the parent MultiDiGraph for v in self.nodes(): H.add_node(v, self.f[v], reset_pos=False) - + for u, v, key in self.edges(keys=True): super(ReebGraph, H).add_edge(u, v, key) - + # Copy position information if it exists - if hasattr(self, 'pos_f') and self.pos_f: + if hasattr(self, "pos_f") and self.pos_f: H.pos_f = self.pos_f.copy() - if hasattr(self, 'pos') and self.pos: + if hasattr(self, "pos") and self.pos: H.pos = self.pos.copy() - + return H def branch_decomp(self): @@ -107,6 +107,7 @@ def branch_decomp(self): ``decompose`` method has already been called on this graph. """ from .branchdecomp import BranchDecomp + bd = BranchDecomp() bd.decompose(self) return bd @@ -185,18 +186,18 @@ def number_connected_components(self): def get_upward_path(self, start_vertex): """Return an upward path from the starting vertex by greedy dynamic choice. - + Input: start_vertex: a vertex in the graph to start from - + Output: path: a list of vertices representing the upward path - + """ - # Check that the vertex is in the graph + # Check that the vertex is in the graph if start_vertex not in self.nodes: - raise ValueError(f"The vertex {start_vertex} is not in the Reeb graph.") - + raise ValueError(f"The vertex {start_vertex} is not in the Reeb graph.") + path = [start_vertex] while self.up_degree(path[-1]) > 0: s = next(self.successors(path[-1])) @@ -518,7 +519,7 @@ def remove_edges_from(self, edges, reset_pos=True): if reset_pos: self.set_pos_from_f() - + def remove_path_from(self, path, reset_pos=True): """Remove a path from the Reeb graph. A path is a list of vertices, and this method will remove one edge along each step of the path. @@ -695,9 +696,9 @@ def slice(self, a, b, type="open", verbose=False): ReebGraph: The subgraph of the Reeb graph with image in (a,b). """ if type == "open": - v_list = [v for v in self.nodes() if self.f[v] > a and self.f[v] < b] + v_list = set(v for v in self.nodes() if self.f[v] > a and self.f[v] < b) elif type == "closed": - v_list = [v for v in self.nodes() if self.f[v] >= a and self.f[v] <= b] + v_list = set(v for v in self.nodes() if self.f[v] >= a and self.f[v] <= b) # Keep the edges where either endpoint (or both) is in (a,b) e_list = [e for e in self.edges() if e[0] in v_list or e[1] in v_list] @@ -708,7 +709,7 @@ def slice(self, a, b, type="open", verbose=False): ) # Make a dictionary of counts to deal with multiedges - e_dict = {e: e_list.count(e) for e in e_list} + e_dict = Counter(e_list) if verbose: print("Vertices (v,f(v)):", [(v, self.f[v]) for v in v_list]) @@ -719,7 +720,7 @@ def slice(self, a, b, type="open", verbose=False): H = ReebGraph() for v in v_list: - H.add_node(v, self.f[v]) + H.add_node(v, self.f[v], reset_pos=False) for e in e_dict: if e[0] in v_list and e[1] in v_list: @@ -728,7 +729,7 @@ def slice(self, a, b, type="open", verbose=False): print(f"Adding {e_dict[e]} of edge {e} entirely inside slice:") for i in range(e_dict[e]): # Add an edge for each copy in the list - H.add_edge(e[0], e[1]) + H.add_edge(e[0], e[1], reset_pos=False) elif e[0] not in v_list and e[1] not in v_list: # The edge is entirely crossing the slice, so we add two vertices and an edge @@ -740,9 +741,9 @@ def slice(self, a, b, type="open", verbose=False): for i in range(e_dict[e]): v1 = "-".join([str(v) for v in e]) + "_" + str(i) + "_lower" v2 = "-".join([str(v) for v in e]) + "_" + str(i) + "_upper" - H.add_node(v1, a) - H.add_node(v2, b) - H.add_edge(v1, v2) + H.add_node(v1, a, reset_pos=False) + H.add_node(v2, b, reset_pos=False) + H.add_edge(v1, v2, reset_pos=False) else: # One vertex is in the set and one is out. # Need to check (for the closed case) that this isn't an edge going up from the top bound or down from the bottom bound @@ -781,8 +782,8 @@ def slice(self, a, b, type="open", verbose=False): func_val = b # Add a new vertex called edge_name with value b - H.add_node(edge_name, func_val) - H.add_edge(e[0], edge_name) + H.add_node(edge_name, func_val, reset_pos=False) + H.add_edge(e[0], edge_name, reset_pos=False) else: # The higher edge is in the set, so the other vertex must have # value below the min @@ -799,8 +800,9 @@ def slice(self, a, b, type="open", verbose=False): func_val = a # Add a new vertex called edge_name with value a - H.add_node(edge_name, func_val) - H.add_edge(edge_name, e[1]) + H.add_node(edge_name, func_val, reset_pos=False) + H.add_edge(edge_name, e[1], reset_pos=False) + H.set_pos_from_f() return H def connected_components(self): diff --git a/tests/test_reeb_class.py b/tests/test_reeb_class.py index 85fa8b29..07a0d4ff 100644 --- a/tests/test_reeb_class.py +++ b/tests/test_reeb_class.py @@ -285,8 +285,44 @@ def test_set_pos_from_f_preserves_y_function_values(self): self.assertEqual(R.pos_f[v][1], R.f[v]) + def test_slice(self): + # This test makes sure you can slice a Reeb graph. + R = ex_rg.juggling_man() + R.add_edge(7,9) + # Example chosen so that we have vertices with value on the endpoints (we're assuming open interval so shouldn't be included) + # We also have at least one edge that completely crosses the interval in question + H = R.slice( 2,5) + self.assertEqual(H.number_connected_components(),3 ) + self.check_reeb(H) + + # Example chosen so that we have vertices with value on the endpoints (we're using closed interval so now these should be included) + H = R.slice( 2,5, type = 'closed') + + self.assertEqual(H.number_connected_components(),2 ) + self.check_reeb(H) + + def test_slice_multiedge(self): + # Slicing across a multiedge should produce one new lower/upper vertex + # pair per parallel copy, and the result should still be a well-formed + # Reeb graph (positions computed, edges pointing upward, etc). + R = ex_rg.torus() # nodes a=0, b=1, c=4, d=5, with a double edge b-c + + # Interval (2,3) falls strictly between b and c, so v_list is empty and + # both copies of the b-c multiedge cross the slice entirely. + H = R.slice(2, 3) + + self.assertEqual(len(H.nodes), 4) # 2 lower + 2 upper subdivision vertices + self.assertEqual(len(H.edges), 2) # one edge per copy of the multiedge + self.assertEqual(H.number_connected_components(), 2) + self.check_reeb(H) + + # Same check for the closed-interval case + H = R.slice(2, 3, type='closed') + self.assertEqual(len(H.nodes), 4) + self.assertEqual(len(H.edges), 2) + self.check_reeb(H) From b995f683549d9bd5ef230bd5f4a7f689807756b1 Mon Sep 17 00:00:00 2001 From: ishikaghosh2201 <112980412+ishikaghosh2201@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:52:40 -0400 Subject: [PATCH 2/4] Fix bad merge resolution in test_reeb_class.py --- tests/test_reeb_class.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/tests/test_reeb_class.py b/tests/test_reeb_class.py index a1f84bc3..527ad5fa 100644 --- a/tests/test_reeb_class.py +++ b/tests/test_reeb_class.py @@ -299,16 +299,8 @@ def test_set_pos_from_f_preserves_y_function_values(self): self.assertEqual(set(R.nodes), set(R.pos_f.keys())) for v in R.nodes: self.assertEqual(R.pos_f[v][1], R.f[v]) - - - def test_slice(self): - # This test makes sure you can slice a Reeb graph. - R = ex_rg.juggling_man() - R.add_edge(7,9) - - # Example chosen so that we have vertices with value on the endpoints (we're assuming open interval so shouldn't be included) - # We also have at least one edge that completely crosses the interval in question - H = R.slice( 2,5) + + def test_remove_node_deferred_pos_cleanup(self): # Regression test: remove_node(reset_pos=False) must still drop the # removed vertex's pos_f entry immediately. Previously this cleanup @@ -419,16 +411,9 @@ def test_slice_closed_unchanged(self): self.assertEqual(self._shape(T.slice(1, 4, type='closed')), (2, 2, 1)) self.assertEqual(self._shape(T.slice(2, 2, type='closed')), (2, 0, 2)) - - self.assertEqual(H.number_connected_components(),3 ) - self.check_reeb(H) - - # Example chosen so that we have vertices with value on the endpoints (we're using closed interval so now these should be included) - H = R.slice( 2,5, type = 'closed') - - self.assertEqual(H.number_connected_components(),2 ) - self.check_reeb(H) - + + + def test_slice_multiedge(self): # Slicing across a multiedge should produce one new lower/upper vertex # pair per parallel copy, and the result should still be a well-formed From 1e29e80ffe1315b080db33e736f220f62f75b9ea Mon Sep 17 00:00:00 2001 From: Ishika Ghosh Date: Mon, 28 Sep 2026 10:54:34 -0400 Subject: [PATCH 3/4] Update pulp version constraint in requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e1fa66f1..a462920b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ networkx matplotlib pandas scipy -pulp +pulp<4 gudhi sphinx nbsphinx From fb0c7cdce8f92a666a9376c35b892a8a58e0d2fc Mon Sep 17 00:00:00 2001 From: Ishika Ghosh Date: Mon, 28 Sep 2026 10:54:51 -0400 Subject: [PATCH 4/4] Update pulp dependency in pyproject.toml Specify version constraint for pulp package --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d70d00e1..f21a1d91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = ["numpy", "matplotlib", "pandas", "scipy", - "pulp", + "pulp<4", "gudhi", "scikit-learn" ] @@ -34,4 +34,4 @@ cereeberus = ["data"] [project.urls] repository = "https://github.com/MunchLab/ceREEBerus" homepage = "https://munchlab.github.io/ceREEBerus/" -documentation = "https://munchlab.github.io/ceREEBerus/" \ No newline at end of file +documentation = "https://munchlab.github.io/ceREEBerus/"