From 6b30ebce0226c1f1328d76ca24bde28f69876e57 Mon Sep 17 00:00:00 2001 From: ishikaghosh2201 <112980412+ishikaghosh2201@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:24:05 -0400 Subject: [PATCH 1/2] fixed open slice bug --- cereeberus/cereeberus/reeb/reebgraph.py | 25 ++++++++-- tests/test_reeb_class.py | 65 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/cereeberus/cereeberus/reeb/reebgraph.py b/cereeberus/cereeberus/reeb/reebgraph.py index 973c2b19..653d3e49 100644 --- a/cereeberus/cereeberus/reeb/reebgraph.py +++ b/cereeberus/cereeberus/reeb/reebgraph.py @@ -699,6 +699,10 @@ def slice(self, a, b, type="open", verbose=False): Returns: ReebGraph: The subgraph of the Reeb graph with image in (a,b). """ + + if type == "open" and a == b: + # (a, a) is an empty interval for open type + return ReebGraph() if type == "open": v_list = [v for v in self.nodes() if self.f[v] > a and self.f[v] < b] elif type == "closed": @@ -706,10 +710,19 @@ def slice(self, a, b, type="open", verbose=False): # 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] - # Include the edges that cover the entire slice. + # Include the edges that cover the entire slice, , including edges whose + # endpoint sits exactly on an excluded open bound. Skip edges already + # selected through an included endpoint so they aren't counted twice. # Note this assumes that all edges are ordered twoards teh upper function value e_list.extend( - [e for e in self.edges() if self.f[e[0]] < a and self.f[e[1]] > b] + [ + e + for e in self.edges() + if e[0] not in v_list + and e[1] not in v_list + and self.f[e[0]] <= a + and self.f[e[1]] >= b + ] ) # Make a dictionary of counts to deal with multiedges @@ -953,8 +966,12 @@ def smoothing_and_maps(self, eps=1, verbose=False): if not finite_nodes: # the whole graph is infinite valued (eg MergeTree with only a root). There's nothing to smooth, so just return the graph - return(self, {v: v for v in self.nodes}, {e: [e] for e in self.edges(keys=True)}) - + return ( + self, + {v: v for v in self.nodes}, + {e: [e] for e in self.edges(keys=True)}, + ) + f_finite = {v: self.f[v] for v in finite_nodes} G_finite = ReebGraph(self.subgraph(finite_nodes), f_finite) R_eps, map_V, map_E = G_finite.smoothing_and_maps(eps=eps, verbose=verbose) diff --git a/tests/test_reeb_class.py b/tests/test_reeb_class.py index 93855f4f..580b51dc 100644 --- a/tests/test_reeb_class.py +++ b/tests/test_reeb_class.py @@ -347,6 +347,71 @@ def test_add_edge_collapse_respects_reset_pos_false(self): R.set_pos_from_f() self.check_reeb(R) + + def _edge(self, multiplicity=1, high_first=False): + # One interval from f=0 to f=2, optionally with parallel copies. + R = ReebGraph() + if high_first: + R.add_node('high', 2, reset_pos=False) + R.add_node('low', 0, reset_pos=False) + else: + R.add_node('low', 0, reset_pos=False) + R.add_node('high', 2, reset_pos=False) + for _ in range(multiplicity): + if high_first: + R.add_edge('high', 'low', reset_pos=False) + else: + R.add_edge('low', 'high', reset_pos=False) + R.set_pos_from_f() + return R + + def _shape(self, H): + return (len(H.nodes), len(H.edges), H.number_connected_components()) + + def test_slice_open_endpoint_on_bound(self): + # An edge whose endpoint sits exactly on an open bound still has its + # interior inside the slice, so it must not be dropped. + for high_first in [False, True]: + R = self._edge(high_first=high_first) + for a, b in [(0, 2), (0, 1), (1, 2)]: + H = R.slice(a, b) + self.assertEqual(self._shape(H), (2, 1, 1), f'open slice ({a}, {b})') + self.assertEqual(sorted(H.f.values()), [a, b]) + self.check_reeb(H) + + def test_slice_open_parallel_edges_on_bounds(self): + # Removing the shared endpoints leaves the parallel edges disconnected. + R = self._edge(multiplicity=3) + H = R.slice(0, 2) + self.assertEqual(self._shape(H), (6, 3, 3)) + self.check_reeb(H) + + # Torus example: vertices at 0, 1, 4, 5 with a double edge from 1 to 4. + T = ex_rg.torus() + H = T.slice(1, 4) + self.assertEqual(self._shape(H), (4, 2, 2)) + self.check_reeb(H) + + def test_slice_open_zero_width_is_empty(self): + # (a, a) is empty, even when edges cross height a. + T = ex_rg.torus() + for a in [0, 1, 2, 4, 5]: + H = T.slice(a, a) + self.assertEqual(len(H.nodes), 0, f'open slice ({a}, {a})') + self.assertEqual(len(H.edges), 0) + + def test_slice_closed_unchanged(self): + # Closed slices keep boundary vertices and don't double-count edges. + R = self._edge(multiplicity=3) + self.assertEqual(self._shape(R.slice(0, 2, type='closed')), (2, 3, 1)) + self.assertEqual(self._shape(R.slice(0, 1, type='closed')), (4, 3, 1)) + self.assertEqual(self._shape(R.slice(1, 1, type='closed')), (3, 0, 3)) + + T = ex_rg.torus() + 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)) + + From 11dff9564cee958bf127fb5f902db618943663b1 Mon Sep 17 00:00:00 2001 From: Liz Munch Date: Thu, 24 Sep 2026 14:47:24 -0400 Subject: [PATCH 2/2] Increment the version number --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b35c4338..8d5ce164 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cereeberus" -version = "0.1.21" +version = "0.1.22" authors = [ { name="Liz Munch", email="muncheli@msu.edu" }, ]