33
44from typing import Mapping , Sequence
55
6+ import numpy as np
7+
68from ..data .types import HomotopyCoherenceMethod
79from ..data .utils import edge_idx_decode
10+ from ..utils .distance_metrics import compute_loop_frechet
11+
12+
13+ def global_h1_death_scale (persistence_diagram : list | None ) -> float | None :
14+ if not persistence_diagram or len (persistence_diagram ) < 2 :
15+ return None
16+ deaths = np .asarray (persistence_diagram [1 ][1 ], dtype = float )
17+ finite = deaths [np .isfinite (deaths )]
18+ return float (finite .max ()) if finite .size else None
819
920
1021def _flip_triangle (
@@ -33,25 +44,65 @@ def _flip_triangle(
3344 return cycle ^ triangle
3445
3546
36- def _perimeter (cycle : frozenset [int ], edge_lengths : Mapping [int , float ]) -> float :
37- return sum (edge_lengths [edge ] for edge in cycle )
47+ def _cycle_vertices (cycle : frozenset [int ], num_vertices : int ) -> list [int ]:
48+ adjacency : dict [int , list [int ]] = {}
49+ for edge in cycle :
50+ tail , head = edge_idx_decode (edge , num_vertices )
51+ adjacency .setdefault (tail , []).append (head )
52+ adjacency .setdefault (head , []).append (tail )
53+ start = next (iter (adjacency ))
54+ order = [start ]
55+ previous , current = - 1 , start
56+ while len (order ) <= len (adjacency ):
57+ neighbours = adjacency [current ]
58+ if len (neighbours ) != 2 :
59+ break
60+ first , second = neighbours
61+ following = first if first != previous else second
62+ if following == start :
63+ break
64+ order .append (following )
65+ previous , current = current , following
66+ return order
67+
68+
69+ def _cycle_coords (
70+ cycle : frozenset [int ], num_vertices : int , embedding : np .ndarray
71+ ) -> np .ndarray :
72+ vertices = _cycle_vertices (cycle , num_vertices )
73+ return np .ascontiguousarray (embedding [vertices ], dtype = np .float64 )
74+
75+
76+ def _frechet_to_target (
77+ cycle : frozenset [int ],
78+ target_coords : np .ndarray ,
79+ num_vertices : int ,
80+ embedding : np .ndarray ,
81+ ) -> float :
82+ cycle_coords = _cycle_coords (cycle , num_vertices , embedding )
83+ return float (
84+ min (
85+ compute_loop_frechet (cycle_coords , target_coords ),
86+ compute_loop_frechet (target_coords , cycle_coords ),
87+ )
88+ )
3889
3990
4091def _greedy_reversal (
4192 source : frozenset [int ],
4293 target : frozenset [int ],
4394 triangles : tuple [frozenset [int ], ...],
44- edge_lengths : Mapping [int , float ],
4595 num_vertices : int ,
96+ embedding : np .ndarray ,
97+ death_scale : float | None = None ,
4698) -> float | None :
47- """Follow one valid triangle order greedily by target perimeter distance."""
48-
49- target_length = _perimeter (target , edge_lengths )
50- source_length = _perimeter (source , edge_lengths )
99+ target_coords = _cycle_coords (target , num_vertices , embedding )
100+ source_coords = _cycle_coords (source , num_vertices , embedding )
51101 cycle = source
52- length = source_length
53102 reversal = 0.0
54103 total_step_abs = 0.0
104+ max_step_abs = 0.0
105+ n_steps = 0
55106 remaining = set (range (len (triangles )))
56107
57108 while remaining :
@@ -63,47 +114,59 @@ def _greedy_reversal(
63114 if next_cycle is None :
64115 continue
65116
66- current_remaining_cost = abs (target_length - length )
67- next_length = _perimeter (next_cycle , edge_lengths )
68- next_remaining_cost = abs (target_length - next_length )
69- step_reversal = (
70- max (
71- next_remaining_cost - current_remaining_cost ,
72- 0.0 ,
73- )
74- / length
117+ current_remaining_cost = _frechet_to_target (
118+ cycle , target_coords , num_vertices , embedding
119+ )
120+ next_remaining_cost = _frechet_to_target (
121+ next_cycle , target_coords , num_vertices , embedding
122+ )
123+ current_departed_cost = _frechet_to_target (
124+ cycle , source_coords , num_vertices , embedding
125+ )
126+ next_departed_cost = _frechet_to_target (
127+ next_cycle , source_coords , num_vertices , embedding
128+ )
129+ step_reversal = max (
130+ next_remaining_cost - current_remaining_cost ,
131+ 0.0 ,
132+ )
133+ step_abs = abs (next_remaining_cost - current_remaining_cost ) + abs (
134+ next_departed_cost - current_departed_cost
75135 )
76- step_abs = abs (next_length - length ) / length
77136 key = (next_remaining_cost , triangle_index )
78137 if best_key is None or key < best_key :
79138 best_key = key
80139 best_step = (
81140 triangle_index ,
82141 next_cycle ,
83- next_length ,
84142 step_reversal ,
85143 step_abs ,
86144 )
87145
88146 if best_step is None :
89147 return None
90148
91- triangle_index , cycle , length , step_reversal , step_abs = best_step
149+ triangle_index , cycle , step_reversal , step_abs = best_step
92150 remaining .remove (triangle_index )
93151 reversal += step_reversal
94152 total_step_abs += step_abs
153+ max_step_abs = max (max_step_abs , step_abs )
154+ n_steps += 1
95155
96- return 1 - reversal / total_step_abs if cycle == target else None
156+ if cycle != target :
157+ return None
158+ # multiply by 2 due to two way frechet
159+ return 1 - min (max_step_abs / (2 * death_scale ), 1 )
97160
98161
99162def path_finding_coherence (
100163 source_edges : Sequence [int ],
101164 target_edges : Sequence [int ],
102165 triangles : Sequence [Sequence [int ]],
103- edge_lengths : Mapping [int , float ],
104166 num_vertices : int ,
167+ embedding : np .ndarray ,
168+ death_scale : float | None = None ,
105169) -> float | None :
106- """Return coherence from a greedy valid triangle order."""
107170
108171 source = frozenset (source_edges )
109172 target = frozenset (target_edges )
@@ -112,35 +175,40 @@ def path_finding_coherence(
112175 source ,
113176 target ,
114177 triangle_sets ,
115- edge_lengths ,
116178 num_vertices ,
179+ embedding ,
180+ death_scale ,
117181 )
182+
118183 reverse = _greedy_reversal (
119184 target ,
120185 source ,
121186 triangle_sets ,
122- edge_lengths ,
123187 num_vertices ,
188+ embedding ,
189+ death_scale ,
124190 )
125191
126192 candidates = [cost for cost in (forward , reverse ) if cost is not None ]
127193 if not candidates :
128194 return None
129- return max (candidates )
195+ return min (candidates )
130196
131197
132198def compute_coherence (
133199 source_edges : Sequence [int ],
134200 target_edges : Sequence [int ],
135201 triangles : Sequence [Sequence [int ]],
136- edge_lengths : Mapping [int , float ],
137202 num_vertices : int ,
203+ embedding : np .ndarray ,
204+ death_scale : float | None = None ,
138205 method : HomotopyCoherenceMethod = "path_finding" ,
139206) -> float | None :
140207 return path_finding_coherence (
141208 source_edges ,
142209 target_edges ,
143210 triangles ,
144- edge_lengths ,
145211 num_vertices ,
212+ embedding ,
213+ death_scale ,
146214 )
0 commit comments