|
1 | | -from .unionfind import UnionFind |
2 | | -from ..reeb.lowerstar import LowerStar |
| 1 | +from itertools import groupby as _groupby |
| 2 | + |
3 | 3 | import numpy as np |
4 | 4 |
|
| 5 | +from ..reeb.lowerstar import LowerStar |
| 6 | +from .unionfind import UnionFind |
| 7 | + |
5 | 8 |
|
6 | 9 | def is_face(sigma, tau): |
7 | 10 | """ |
@@ -72,120 +75,164 @@ def computeReeb(K: LowerStar, verbose=False): |
72 | 75 |
|
73 | 76 | funcVals = [(i, K.filtration([i])) for i in K.iter_vertices()] |
74 | 77 | funcVals.sort(key=lambda x: x[1]) # Sort by filtration value |
| 78 | + # Group vertices that share the same filtration value into batches. |
| 79 | + # A horizontal edge (both endpoints at the same height) must be processed |
| 80 | + # within one batch so it properly merges its endpoints into a single Reeb node. |
| 81 | + grouped = [ |
| 82 | + (filt, list(grp)) |
| 83 | + for filt, grp in _groupby(funcVals, key=lambda x: x[1]) |
| 84 | + ] |
75 | 85 |
|
76 | 86 | R = ReebGraph() |
77 | | - |
78 | 87 | currentLevelSet = [] |
79 | | - components = {} |
80 | 88 | half_edge_index = 0 |
81 | | - |
82 | | - # This will keep track of the components represented by every vertex in the graph so far. |
83 | | - # It will be vertName: connected_component (given as a list of lists) represented by that vertex |
84 | 89 | vert_to_component = {} |
85 | | - |
86 | 90 | edges_at_prev_level = [] |
87 | 91 |
|
88 | | - for i, (vert, filt) in enumerate(funcVals): |
89 | | - if verbose: |
90 | | - print(f"\n---\n Processing {vert} at func val {filt:.2f}") |
| 92 | + def _dedup(lst): |
| 93 | + seen = set() |
| 94 | + out = [] |
| 95 | + for s in lst: |
| 96 | + key = tuple(sorted(s)) |
| 97 | + if key not in seen: |
| 98 | + seen.add(key) |
| 99 | + out.append(s) |
| 100 | + return out |
| 101 | + |
| 102 | + for group_idx, (filt, group_verts) in enumerate(grouped): |
91 | 103 | now_min = filt |
92 | | - now_max = funcVals[i + 1][1] if i + 1 < len(funcVals) else np.inf |
93 | | - star = K.get_star([vert]) |
94 | | - lower_star = [s[0] for s in star if s[1] <= filt and len(s[0]) > 1] |
95 | | - upper_star = [s[0] for s in star if s[1] > filt and len(s[0]) > 1] |
| 104 | + now_max = ( |
| 105 | + grouped[group_idx + 1][0] if group_idx + 1 < len(grouped) else np.inf |
| 106 | + ) |
| 107 | + vert_names = [v for v, _ in group_verts] |
96 | 108 |
|
97 | 109 | if verbose: |
98 | | - print(f" Lower star simplices: {lower_star}") |
99 | | - print(f" Upper star simplices: {upper_star}") |
| 110 | + print(f"\n---\n Processing group at func val {filt:.2f}: {vert_names}") |
| 111 | + |
| 112 | + # Classify all simplex stars for this batch into three groups: |
| 113 | + # |
| 114 | + # lower_nonhoriz: s_filt == filt AND at least one vertex strictly below filt. |
| 115 | + # These were added to currentLevelSet by an earlier vertex; remove them now. |
| 116 | + # |
| 117 | + # horizontal: s_filt == filt AND ALL vertices are at filt. |
| 118 | + # These connect same-height vertices in the same batch. |
| 119 | + # Add them temporarily so they link the endpoints into one component. |
| 120 | + # |
| 121 | + # upper: s_filt > filt. |
| 122 | + # Add persistently; they carry the level set upward to the next critical point. |
| 123 | + # |
| 124 | + # Note: because filt(simplex) = max(vertex filtrations) >= filt for any simplex |
| 125 | + # in the star of a vertex at height filt, s_filt < filt is impossible here. |
| 126 | + all_lower_nonhoriz = [] |
| 127 | + all_upper = [] |
| 128 | + all_horizontal = [] |
| 129 | + |
| 130 | + for vert in vert_names: |
| 131 | + for s in K.get_star([vert]): |
| 132 | + simplex, s_filt = s[0], s[1] |
| 133 | + if len(simplex) <= 1: |
| 134 | + continue |
| 135 | + if s_filt > filt: |
| 136 | + all_upper.append(simplex) |
| 137 | + elif all(K.filtration([u]) == filt for u in simplex): |
| 138 | + all_horizontal.append(simplex) |
| 139 | + else: |
| 140 | + all_lower_nonhoriz.append(simplex) |
| 141 | + |
| 142 | + all_lower_nonhoriz = _dedup(all_lower_nonhoriz) |
| 143 | + all_upper = _dedup(all_upper) |
| 144 | + all_horizontal = _dedup(all_horizontal) |
100 | 145 |
|
101 | | - # ---- |
102 | | - # Update the levelset list |
103 | | - # ---- |
| 146 | + if verbose: |
| 147 | + print(f" Lower (non-horiz) simplices: {all_lower_nonhoriz}") |
| 148 | + print(f" Horizontal simplices: {all_horizontal}") |
| 149 | + print(f" Upper simplices: {all_upper}") |
104 | 150 |
|
105 | | - for s in lower_star: |
106 | | - # Remove from current level set |
| 151 | + # Step 1: Remove the lower non-horizontal simplices from the active level set. |
| 152 | + for s in all_lower_nonhoriz: |
107 | 153 | if s in currentLevelSet: |
108 | 154 | currentLevelSet.remove(s) |
109 | 155 |
|
110 | | - currentLevelSet.append([vert]) # Add the vertex itself to the level set |
111 | | - components_at_vertex = get_levelset_components(currentLevelSet) |
| 156 | + # Step 2: Add this batch's vertices and its horizontal simplices to the level set. |
| 157 | + for vert in vert_names: |
| 158 | + currentLevelSet.append([vert]) |
| 159 | + for s in all_horizontal: |
| 160 | + if s not in currentLevelSet: |
| 161 | + currentLevelSet.append(s) |
| 162 | + |
| 163 | + if verbose: |
| 164 | + print(f" Current level set: {currentLevelSet}") |
| 165 | + |
| 166 | + # Step 3: Compute connected components; create one Reeb node per component. |
| 167 | + components_at_level = get_levelset_components(currentLevelSet) |
112 | 168 |
|
113 | 169 | if verbose: |
114 | | - print(f" Current level set simplices: {currentLevelSet}") |
115 | | - print(f" Level set components at vertex {vert} (func val {filt:.2f}):") |
116 | | - for comp in components_at_vertex.values(): |
117 | | - print(f" Component: {comp}") |
| 170 | + print(f" Level set components:") |
| 171 | + for comp in components_at_level.values(): |
| 172 | + print(f" {comp}") |
118 | 173 |
|
119 | 174 | verts_at_level = [] |
120 | | - for rep, comp in components_at_vertex.items(): |
121 | | - # Add a vertex for each component in this levelset |
| 175 | + for rep, comp in components_at_level.items(): |
122 | 176 | nextNodeName = R.get_next_vert_name() |
123 | 177 | R.add_node(nextNodeName, now_min) |
124 | | - vert_to_component[nextNodeName] = ( |
125 | | - comp # Store the component represented by this vertex |
126 | | - ) |
| 178 | + vert_to_component[nextNodeName] = comp |
127 | 179 | verts_at_level.append(nextNodeName) |
128 | 180 |
|
129 | | - # Check if any simplex in vertex component is a subset of any of simplices in a previous edge's component |
| 181 | + # Connect incoming half-edge sentinels whose component contains a face |
| 182 | + # of a simplex in this component. |
130 | 183 | for e in edges_at_prev_level: |
131 | 184 | prev_comp = vert_to_component[e] |
132 | 185 | if any( |
133 | | - [ |
134 | | - is_face(prev_simp, simp) |
135 | | - for simp in comp |
136 | | - for prev_simp in prev_comp |
137 | | - ] |
| 186 | + is_face(prev_simp, simp) |
| 187 | + for simp in comp |
| 188 | + for prev_simp in prev_comp |
138 | 189 | ): |
139 | 190 | R.add_edge(e, nextNodeName) |
140 | 191 |
|
141 | | - # ---- |
142 | | - # Add the edge vertices for after the vertex is passed |
143 | | - # ---- |
144 | | - |
145 | | - # Remove the vertex from the level set |
146 | | - if [vert] in currentLevelSet: |
147 | | - currentLevelSet.remove([vert]) |
| 192 | + # Step 4: Remove vertices and horizontal simplices – they live only at this exact height. |
| 193 | + for vert in vert_names: |
| 194 | + if [vert] in currentLevelSet: |
| 195 | + currentLevelSet.remove([vert]) |
| 196 | + for s in all_horizontal: |
| 197 | + if s in currentLevelSet: |
| 198 | + currentLevelSet.remove(s) |
148 | 199 |
|
149 | | - # Add the upper star to the current level set |
150 | | - for s in upper_star: |
| 200 | + # Step 5: Add upper-star simplices to carry the level set forward. |
| 201 | + for s in all_upper: |
151 | 202 | if s not in currentLevelSet: |
152 | 203 | currentLevelSet.append(s) |
153 | 204 |
|
154 | | - components = get_levelset_components(currentLevelSet) |
155 | 205 | if verbose: |
156 | | - print(f"\n Updated current level set simplices: {currentLevelSet}") |
157 | | - print(f" Level set components after vertex {vert} (func val {filt:.2f}):") |
158 | | - for comp in components.values(): |
159 | | - print(f" Component: {comp}") |
160 | | - # ---- |
161 | | - # Set up a vertex in the Reeb graph for each connected component |
162 | | - # These will represent edges |
163 | | - # These are at height (now_min + now_max)/2 |
164 | | - # ---- |
| 206 | + print(f"\n Updated level set: {currentLevelSet}") |
| 207 | + |
| 208 | + # Step 6: Compute components above this level; create half-edge sentinel nodes |
| 209 | + # at height (now_min + now_max) / 2 and connect them downward to the Reeb nodes |
| 210 | + # created in Step 3. |
| 211 | + components_above = get_levelset_components(currentLevelSet) |
| 212 | + |
| 213 | + if verbose: |
| 214 | + print(f" Level set components above:") |
| 215 | + for comp in components_above.values(): |
| 216 | + print(f" {comp}") |
| 217 | + |
165 | 218 | edges_at_prev_level = [] |
166 | | - for comp in components.values(): |
167 | | - # Create a new vertex in the Reeb graph |
| 219 | + for comp in components_above.values(): |
168 | 220 | e_name = "e_" + str(half_edge_index) |
169 | 221 | R.add_node(e_name, (now_min + now_max) / 2) |
170 | | - vert_to_component[e_name] = ( |
171 | | - comp # Store the component represented by this half edge top |
172 | | - ) |
| 222 | + vert_to_component[e_name] = comp |
173 | 223 | half_edge_index += 1 |
174 | 224 | edges_at_prev_level.append(e_name) |
175 | 225 |
|
176 | | - # Now connect to the vertices at this level |
| 226 | + # Connect downward to Reeb nodes at this level. |
177 | 227 | for v in verts_at_level: |
178 | | - |
179 | | - # Get the component represented by vertex v |
180 | 228 | prev_comp = vert_to_component[v] |
181 | | - |
182 | 229 | if any( |
183 | | - [ |
184 | | - is_face(simp, prev_simp) |
185 | | - for simp in comp |
186 | | - for prev_simp in prev_comp |
187 | | - ] |
| 230 | + is_face(simp, prev_simp) |
| 231 | + for simp in comp |
| 232 | + for prev_simp in prev_comp |
188 | 233 | ): |
189 | 234 | R.add_edge(v, e_name) |
190 | 235 |
|
191 | 236 | return R |
| 237 | + |
| 238 | + return R |
0 commit comments