| | | 1 | | using System.Collections.Generic; |
| | | 2 | | using System.Threading; |
| | | 3 | | using System.Threading.Tasks; |
| | | 4 | | using Itinero.Network.Enumerators.Edges; |
| | | 5 | | using Itinero.Network.Tiles; |
| | | 6 | | using Itinero.Profiles; |
| | | 7 | | using Itinero.Routing.Costs; |
| | | 8 | | |
| | | 9 | | namespace Itinero.Network.Search.Islands; |
| | | 10 | | |
| | | 11 | | /// <summary> |
| | | 12 | | /// Per-edge island classifier. See <c>docs/island-detection-algorithm.md</c> |
| | | 13 | | /// in publish-api for the spec. Short version: |
| | | 14 | | /// |
| | | 15 | | /// We answer two reachability questions for the seed in the dg: |
| | | 16 | | /// - Forward: is there a path seed ↝ sentinel (seed reaches MainNet)? |
| | | 17 | | /// - Backward: is there a path sentinel ↝ seed (MainNet reaches seed)? |
| | | 18 | | /// |
| | | 19 | | /// NotIsland iff both are yes; Island iff either is no. Two BFS searches |
| | | 20 | | /// (forward + backward) run concurrently. After every <see cref="IslandDirectedGraph.AddDirectedLink"/> |
| | | 21 | | /// we propagate two per-component sticky sets: |
| | | 22 | | /// - <c>inForward</c> — components reachable from seed via <c>_outgoing</c>. |
| | | 23 | | /// - <c>inBackward</c> — components reachable from seed via <c>_incoming</c>. |
| | | 24 | | /// Each component enters each set at most once, so total propagation across |
| | | 25 | | /// the classification is O(closure-size). |
| | | 26 | | /// |
| | | 27 | | /// Termination: |
| | | 28 | | /// - sentinel ∈ inForward ∩ inBackward → NotIsland. |
| | | 29 | | /// - forwardQueue empty ∧ sentinel ∉ inForward → Island. |
| | | 30 | | /// - backwardQueue empty ∧ sentinel ∉ inBackward → Island. |
| | | 31 | | /// </summary> |
| | | 32 | | public static class IslandClassifier |
| | | 33 | | { |
| | | 34 | | public static async Task<IslandStatus> ClassifyAsync( |
| | | 35 | | RoutingNetwork network, |
| | | 36 | | Profile profile, |
| | | 37 | | EdgeId seed, |
| | | 38 | | CancellationToken cancellationToken, |
| | | 39 | | IslandKind kind = IslandKind.Full) |
| | 2136 | 40 | | { |
| | 2136 | 41 | | var islands = network.IslandManager.GetIslandsFor(profile); |
| | 2136 | 42 | | var dg = network.IslandManager.GetOrCreateDirectedGraph(profile, kind); |
| | 2136 | 43 | | var maxIslandSize = network.IslandManager.MaxIslandSize; |
| | 2136 | 44 | | var costFunction = IslandKindCostFunctions.GetFor(network, profile, kind); |
| | 2136 | 45 | | var probe = network.GetEdgeEnumerator(); |
| | 2136 | 46 | | var sentinel = IslandDirectedGraph.MainNetworkSentinel; |
| | | 47 | | |
| | | 48 | | // Pre-population secondary oracle for Full: any edge in the NonLocal |
| | | 49 | | // MainNet sentinel is guaranteed to be in Full MainNet too, since |
| | | 50 | | // N-only paths are valid Full paths. |
| | 2136 | 51 | | IslandDirectedGraph? nonLocalDg = null; |
| | 2136 | 52 | | if (kind == IslandKind.Full) |
| | 1091 | 53 | | { |
| | 1091 | 54 | | nonLocalDg = network.IslandManager.GetOrCreateDirectedGraph(profile, IslandKind.NonLocal); |
| | 1091 | 55 | | } |
| | | 56 | | |
| | | 57 | | // Oracle / cached-state short-circuits. |
| | 2138 | 58 | | if (IsKnownIsland(seed, kind, islands)) return IslandStatus.Island; |
| | 3054 | 59 | | if (dg.IsNotIsland(seed)) return IslandStatus.NotIsland; |
| | 1214 | 60 | | if (nonLocalDg != null && nonLocalDg.IsNotIsland(seed)) |
| | 1032 | 61 | | { |
| | 1032 | 62 | | dg.AddVertex(seed); |
| | 1032 | 63 | | dg.CollapseToMainNetwork(seed); |
| | 1032 | 64 | | return IslandStatus.NotIsland; |
| | | 65 | | } |
| | | 66 | | |
| | | 67 | | // Edge must exist and be traversable in at least one direction. |
| | 182 | 68 | | if (!probe.MoveTo(seed, true)) return IslandStatus.Unknown; |
| | 182 | 69 | | var seedHead = probe.Head; |
| | 182 | 70 | | var seedTail = probe.Tail; |
| | 182 | 71 | | var canFwd = costFunction.GetIslandBuilderCost(probe); |
| | 182 | 72 | | if (!probe.MoveTo(seed, false)) return IslandStatus.Unknown; |
| | 182 | 73 | | var canBwd = costFunction.GetIslandBuilderCost(probe); |
| | 185 | 74 | | if (!canFwd && !canBwd) return IslandStatus.Unknown; |
| | | 75 | | |
| | | 76 | | // Set up. Seed starts in both F and B (it is trivially reachable from itself in either direction). |
| | 179 | 77 | | dg.AddVertex(seed); |
| | 179 | 78 | | var ctx = new Ctx(network, dg, nonLocalDg, kind, islands, costFunction, maxIslandSize); |
| | 179 | 79 | | ctx.InForward.Add(dg.Find(seed)); |
| | 179 | 80 | | ctx.InBackward.Add(dg.Find(seed)); |
| | | 81 | | |
| | | 82 | | // Process seed at both endpoints. Each AddDirectedLink inside |
| | | 83 | | // ProcessAtEndpointAsync updates inForward/inBackward and enqueues |
| | | 84 | | // newly-marked edges into the right queue. |
| | 179 | 85 | | await ProcessAtEndpointAsync(seed, seedHead, ctx, cancellationToken); |
| | 179 | 86 | | if (cancellationToken.IsCancellationRequested) return IslandStatus.Unknown; |
| | 179 | 87 | | await ProcessAtEndpointAsync(seed, seedTail, ctx, cancellationToken); |
| | 179 | 88 | | if (cancellationToken.IsCancellationRequested) return IslandStatus.Unknown; |
| | 179 | 89 | | dg.SetProcessed(seed); |
| | | 90 | | |
| | | 91 | | // Main loop: termination checks are O(1) lookups on the sentinel. |
| | 1098 | 92 | | while (true) |
| | 1098 | 93 | | { |
| | | 94 | | // Graduation: if seed was absorbed into the sentinel by an eager |
| | | 95 | | // cycle-merge or size-threshold collapse, return immediately. |
| | 1243 | 96 | | if (dg.IsNotIsland(seed)) return IslandStatus.NotIsland; |
| | | 97 | | |
| | 953 | 98 | | var sentinelInF = ctx.InForward.Contains(sentinel); |
| | 953 | 99 | | var sentinelInB = ctx.InBackward.Contains(sentinel); |
| | 953 | 100 | | if (sentinelInF && sentinelInB) |
| | 1 | 101 | | { |
| | 1 | 102 | | dg.CollapseToMainNetwork(seed); |
| | 1 | 103 | | return IslandStatus.NotIsland; |
| | | 104 | | } |
| | 952 | 105 | | if (ctx.ForwardQueue.Count == 0 && !sentinelInF) |
| | 29 | 106 | | { |
| | 29 | 107 | | islands.SetEdgeOnIsland(seed, kind); |
| | 29 | 108 | | return IslandStatus.Island; |
| | | 109 | | } |
| | 923 | 110 | | if (ctx.BackwardQueue.Count == 0 && !sentinelInB) |
| | 4 | 111 | | { |
| | 4 | 112 | | islands.SetEdgeOnIsland(seed, kind); |
| | 4 | 113 | | return IslandStatus.Island; |
| | | 114 | | } |
| | | 115 | | |
| | 919 | 116 | | if (ctx.ForwardQueue.Count > 0) |
| | 919 | 117 | | { |
| | 919 | 118 | | var e = ctx.ForwardQueue.Dequeue(); |
| | 919 | 119 | | await ProcessEdgeAsync(e, ctx, cancellationToken); |
| | 919 | 120 | | if (cancellationToken.IsCancellationRequested) return IslandStatus.Unknown; |
| | 919 | 121 | | } |
| | 919 | 122 | | if (ctx.BackwardQueue.Count > 0) |
| | 917 | 123 | | { |
| | 917 | 124 | | var e = ctx.BackwardQueue.Dequeue(); |
| | 917 | 125 | | await ProcessEdgeAsync(e, ctx, cancellationToken); |
| | 917 | 126 | | if (cancellationToken.IsCancellationRequested) return IslandStatus.Unknown; |
| | 917 | 127 | | } |
| | 919 | 128 | | } |
| | 2136 | 129 | | } |
| | | 130 | | |
| | | 131 | | public static async Task BuildForTileAsync( |
| | | 132 | | RoutingNetwork network, |
| | | 133 | | Profile profile, |
| | | 134 | | uint tileId, |
| | | 135 | | CancellationToken cancellationToken) |
| | 10 | 136 | | { |
| | 10 | 137 | | if (cancellationToken.IsCancellationRequested) return; |
| | | 138 | | |
| | 10 | 139 | | var islands = network.IslandManager.GetIslandsFor(profile); |
| | 10 | 140 | | if (islands.GetTileDone(tileId)) return; |
| | | 141 | | |
| | | 142 | | // Use the Full cost function to enumerate traversable edges and to |
| | | 143 | | // detect L-tagged ones (NonLocalCostFunction masks L away — we need |
| | | 144 | | // the raw tag here for both edge-gathering and L-set computation). |
| | 10 | 145 | | var fullCostFunction = IslandKindCostFunctions.GetFor(network, profile, IslandKind.Full); |
| | 10 | 146 | | await network.UsageNotifier.NotifyVertex(network, new VertexId(tileId, 0), cancellationToken); |
| | 10 | 147 | | if (cancellationToken.IsCancellationRequested) return; |
| | 10 | 148 | | var tile = network.GetTileForRead(tileId); |
| | 10 | 149 | | if (tile == null) return; |
| | | 150 | | |
| | 10 | 151 | | var probe = network.GetEdgeEnumerator(); |
| | 10 | 152 | | var tileEnum = new NetworkTileEnumerator(); |
| | 10 | 153 | | tileEnum.MoveTo(tile); |
| | 10 | 154 | | var v = new VertexId(tileId, 0); |
| | 10 | 155 | | var edges = new List<EdgeId>(); |
| | 10 | 156 | | var lEdges = new HashSet<EdgeId>(); |
| | 574 | 157 | | while (tileEnum.MoveTo(v)) |
| | 564 | 158 | | { |
| | 2655 | 159 | | while (tileEnum.MoveNext()) |
| | 2091 | 160 | | { |
| | 3141 | 161 | | if (!tileEnum.Forward) continue; |
| | 1041 | 162 | | var edgeId = tileEnum.EdgeId; |
| | 1041 | 163 | | if (!probe.MoveTo(edgeId, true)) continue; |
| | 1041 | 164 | | var fwd = fullCostFunction.Get(probe, true); |
| | 1041 | 165 | | var canFwd = fwd is { canAccess: true, turnCost: < double.MaxValue }; |
| | 1041 | 166 | | if (!probe.MoveTo(edgeId, false)) continue; |
| | 1041 | 167 | | var bwd = fullCostFunction.Get(probe, true); |
| | 1041 | 168 | | var canBwd = bwd is { canAccess: true, turnCost: < double.MaxValue }; |
| | 2082 | 169 | | if (canFwd || canBwd) edges.Add(edgeId); |
| | 1043 | 170 | | if (fwd.localAccess || bwd.localAccess) lEdges.Add(edgeId); |
| | 1041 | 171 | | } |
| | 564 | 172 | | v = new VertexId(tileId, v.LocalId + 1); |
| | 564 | 173 | | } |
| | | 174 | | |
| | | 175 | | // NonLocal pass first — classifies the N-only subgraph. L-tagged |
| | | 176 | | // seeds are masked out by NonLocalCostFunction and return Unknown. |
| | 2112 | 177 | | foreach (var edge in edges) |
| | 1041 | 178 | | { |
| | 1041 | 179 | | if (cancellationToken.IsCancellationRequested) return; |
| | 1041 | 180 | | await ClassifyAsync(network, profile, edge, cancellationToken, IslandKind.NonLocal); |
| | 1041 | 181 | | } |
| | | 182 | | |
| | | 183 | | // Full pass — reuses NonLocal MainNet as a positive oracle to short- |
| | | 184 | | // circuit edges already known to be in N-mainland. |
| | 2112 | 185 | | foreach (var edge in edges) |
| | 1041 | 186 | | { |
| | 1041 | 187 | | if (cancellationToken.IsCancellationRequested) return; |
| | 1041 | 188 | | await ClassifyAsync(network, profile, edge, cancellationToken, IslandKind.Full); |
| | 1041 | 189 | | } |
| | | 190 | | |
| | | 191 | | // Locals = NonLocal-Island ∩ Full-NotIsland ∩ non-L. These are the |
| | | 192 | | // non-L edges only reachable from main-N through an L-edge first. |
| | | 193 | | // L-tagged edges and Full-Island edges are excluded. |
| | 2112 | 194 | | foreach (var edge in edges) |
| | 1041 | 195 | | { |
| | 1043 | 196 | | if (lEdges.Contains(edge)) continue; |
| | 1041 | 197 | | if (islands.IsEdgeOnIsland(edge)) continue; |
| | 2068 | 198 | | if (!islands.IsEdgeOnIsland(edge, IslandKind.NonLocal)) continue; |
| | 6 | 199 | | islands.SetEdgeLocal(edge); |
| | 6 | 200 | | } |
| | | 201 | | |
| | 10 | 202 | | islands.ClearNonLocalIslandEdges(); |
| | 10 | 203 | | islands.SetTileDone(tileId); |
| | 10 | 204 | | } |
| | | 205 | | |
| | | 206 | | /// <summary> |
| | | 207 | | /// Kind-aware "known island" oracle. For Full this is the persistent |
| | | 208 | | /// Full-Islands set. For NonLocal an edge is known to be a NonLocal-Island |
| | | 209 | | /// if it is in Full-Islands (Full-Island ⟹ NonLocal-Island), in |
| | | 210 | | /// <c>_localEdges</c> (Local edges are unreachable via N-only paths), or |
| | | 211 | | /// in the transient <c>_nonLocalIslandEdges</c> set written during the |
| | | 212 | | /// current NonLocal pass. |
| | | 213 | | /// </summary> |
| | | 214 | | private static bool IsKnownIsland(EdgeId edgeId, IslandKind kind, Islands islands) |
| | 70794 | 215 | | { |
| | 70794 | 216 | | if (kind == IslandKind.NonLocal) |
| | 69422 | 217 | | { |
| | 69422 | 218 | | if (islands.IsEdgeOnIsland(edgeId)) return true; |
| | 69422 | 219 | | if (islands.IsEdgeLocal(edgeId)) return true; |
| | 69422 | 220 | | return islands.IsEdgeOnIsland(edgeId, IslandKind.NonLocal); |
| | | 221 | | } |
| | 1372 | 222 | | return islands.IsEdgeOnIsland(edgeId); |
| | 70794 | 223 | | } |
| | | 224 | | |
| | | 225 | | /// <summary> |
| | | 226 | | /// Per-classification working state. Owns the two queues, the per-queue |
| | | 227 | | /// dedup sets, and the F/B sticky sets used to track per-component |
| | | 228 | | /// reachability to/from the seed in the dg. |
| | | 229 | | /// </summary> |
| | | 230 | | private sealed class Ctx |
| | | 231 | | { |
| | | 232 | | public readonly RoutingNetwork Network; |
| | | 233 | | public readonly IslandDirectedGraph Dg; |
| | | 234 | | public readonly IslandDirectedGraph? NonLocalDg; |
| | | 235 | | public readonly IslandKind Kind; |
| | | 236 | | public readonly Islands Islands; |
| | | 237 | | public readonly ICostFunction CostFunction; |
| | | 238 | | public readonly int MaxIslandSize; |
| | 179 | 239 | | public readonly Queue<EdgeId> ForwardQueue = new(); |
| | 179 | 240 | | public readonly Queue<EdgeId> BackwardQueue = new(); |
| | 179 | 241 | | public readonly HashSet<EdgeId> QueuedForward = new(); |
| | 179 | 242 | | public readonly HashSet<EdgeId> QueuedBackward = new(); |
| | 179 | 243 | | public readonly HashSet<EdgeId> InForward = new(); |
| | 179 | 244 | | public readonly HashSet<EdgeId> InBackward = new(); |
| | | 245 | | |
| | 179 | 246 | | public Ctx(RoutingNetwork network, IslandDirectedGraph dg, |
| | 179 | 247 | | IslandDirectedGraph? nonLocalDg, IslandKind kind, Islands islands, |
| | 179 | 248 | | ICostFunction costFunction, int maxIslandSize) |
| | 179 | 249 | | { |
| | 179 | 250 | | Network = network; |
| | 179 | 251 | | Dg = dg; |
| | 179 | 252 | | NonLocalDg = nonLocalDg; |
| | 179 | 253 | | Kind = kind; |
| | 179 | 254 | | Islands = islands; |
| | 179 | 255 | | CostFunction = costFunction; |
| | 179 | 256 | | MaxIslandSize = maxIslandSize; |
| | 179 | 257 | | } |
| | | 258 | | } |
| | | 259 | | |
| | | 260 | | private static async Task ProcessEdgeAsync(EdgeId edgeId, Ctx ctx, CancellationToken cancellationToken) |
| | 1836 | 261 | | { |
| | 2756 | 262 | | if (ctx.Dg.IsProcessed(edgeId)) return; |
| | 916 | 263 | | var probe = ctx.Network.GetEdgeEnumerator(); |
| | 916 | 264 | | if (!probe.MoveTo(edgeId, true)) { ctx.Dg.SetProcessed(edgeId); return; } |
| | 916 | 265 | | var head = probe.Head; |
| | 916 | 266 | | var tail = probe.Tail; |
| | 916 | 267 | | await ProcessAtEndpointAsync(edgeId, head, ctx, cancellationToken); |
| | 916 | 268 | | if (cancellationToken.IsCancellationRequested) return; |
| | 916 | 269 | | await ProcessAtEndpointAsync(edgeId, tail, ctx, cancellationToken); |
| | 916 | 270 | | ctx.Dg.SetProcessed(edgeId); |
| | 1836 | 271 | | } |
| | | 272 | | |
| | | 273 | | private static async Task ProcessAtEndpointAsync( |
| | | 274 | | EdgeId edgeId, VertexId vertex, Ctx ctx, CancellationToken cancellationToken) |
| | 2190 | 275 | | { |
| | 2190 | 276 | | await ctx.Network.UsageNotifier.NotifyVertex(ctx.Network, vertex, cancellationToken); |
| | 2190 | 277 | | if (cancellationToken.IsCancellationRequested) return; |
| | | 278 | | |
| | 2190 | 279 | | var edgeIdFrom = ctx.Network.GetEdgeEnumerator(); |
| | 2190 | 280 | | if (!edgeIdFrom.MoveTo(edgeId, true)) return; |
| | 2190 | 281 | | var arrivalForward = edgeIdFrom.Head == vertex; |
| | 2190 | 282 | | if (!arrivalForward) |
| | 1095 | 283 | | { |
| | 1095 | 284 | | if (!edgeIdFrom.MoveTo(edgeId, false)) return; |
| | 1095 | 285 | | } |
| | 2190 | 286 | | var edgeIdTo = ctx.Network.GetEdgeEnumerator(); |
| | 2190 | 287 | | if (!edgeIdTo.MoveTo(edgeId, !arrivalForward)) return; |
| | 2190 | 288 | | var neighborArriving = ctx.Network.GetEdgeEnumerator(); |
| | | 289 | | |
| | 2190 | 290 | | var enumerator = ctx.Network.GetEdgeEnumerator(); |
| | 2190 | 291 | | if (!enumerator.MoveTo(vertex)) return; |
| | 10223 | 292 | | while (enumerator.MoveNext()) |
| | 8033 | 293 | | { |
| | 10223 | 294 | | if (enumerator.EdgeId == edgeId) continue; |
| | 5843 | 295 | | var neighborId = enumerator.EdgeId; |
| | 5843 | 296 | | var iterationForward = enumerator.Forward; |
| | | 297 | | |
| | 5843 | 298 | | var canGoTo = ctx.CostFunction.GetIslandBuilderCost(edgeIdFrom, enumerator); |
| | 5843 | 299 | | neighborArriving.MoveTo(neighborId, !iterationForward); |
| | 5843 | 300 | | var canComeFrom = ctx.CostFunction.GetIslandBuilderCost(neighborArriving, edgeIdTo); |
| | | 301 | | |
| | 5843 | 302 | | enumerator.MoveTo(vertex); |
| | 14332 | 303 | | while (enumerator.MoveNext()) |
| | 14332 | 304 | | { |
| | 20175 | 305 | | if (enumerator.EdgeId == neighborId) break; |
| | 8489 | 306 | | } |
| | | 307 | | |
| | 5856 | 308 | | if (!canGoTo && !canComeFrom) continue; |
| | | 309 | | |
| | | 310 | | // Oracle. |
| | | 311 | | EdgeId neighborDgVertex; |
| | | 312 | | bool isKnown; |
| | 5830 | 313 | | if (IsKnownIsland(neighborId, ctx.Kind, ctx.Islands)) |
| | 13 | 314 | | { |
| | 13 | 315 | | ctx.Dg.AddVertex(neighborId); |
| | 13 | 316 | | neighborDgVertex = neighborId; |
| | 13 | 317 | | isKnown = true; |
| | 13 | 318 | | } |
| | 5817 | 319 | | else if (ctx.Dg.IsNotIsland(neighborId) || |
| | 5817 | 320 | | (ctx.NonLocalDg != null && ctx.NonLocalDg.IsNotIsland(neighborId)) || |
| | 5817 | 321 | | ctx.Islands.GetTileDone(neighborId.TileId)) |
| | 362 | 322 | | { |
| | 362 | 323 | | neighborDgVertex = IslandDirectedGraph.MainNetworkSentinel; |
| | 362 | 324 | | isKnown = true; |
| | 362 | 325 | | } |
| | | 326 | | else |
| | 5455 | 327 | | { |
| | 5455 | 328 | | ctx.Dg.AddVertex(neighborId); |
| | 5455 | 329 | | neighborDgVertex = neighborId; |
| | 5455 | 330 | | isKnown = false; |
| | 5455 | 331 | | } |
| | | 332 | | |
| | 11578 | 333 | | if (canGoTo) AddLinkAndPropagate(edgeId, neighborDgVertex, ctx); |
| | 11584 | 334 | | if (canComeFrom) AddLinkAndPropagate(neighborDgVertex, edgeId, ctx); |
| | | 335 | | |
| | | 336 | | // The neighbour's queue assignment is handled by the propagation |
| | | 337 | | // (newly inForward → forwardQueue; newly inBackward → backwardQueue). |
| | | 338 | | // For known-island neighbours nothing needs to be queued; the dg |
| | | 339 | | // link still got added so cycle detection sees it. |
| | 5830 | 340 | | _ = isKnown; |
| | 5830 | 341 | | } |
| | 2190 | 342 | | } |
| | | 343 | | |
| | | 344 | | /// <summary> |
| | | 345 | | /// Adds a directed link <c>a → b</c> to the dg and incrementally maintains |
| | | 346 | | /// the per-classification <see cref="Ctx.InForward"/> and |
| | | 347 | | /// <see cref="Ctx.InBackward"/> sets through any propagation the new link |
| | | 348 | | /// causes. Newly-marked components are enqueued to the appropriate queue. |
| | | 349 | | /// </summary> |
| | | 350 | | private static void AddLinkAndPropagate(EdgeId a, EdgeId b, Ctx ctx) |
| | 11502 | 351 | | { |
| | 11502 | 352 | | var aRootBefore = ctx.Dg.Find(a); |
| | 11502 | 353 | | var bRootBefore = ctx.Dg.Find(b); |
| | 19784 | 354 | | if (aRootBefore == bRootBefore) return; |
| | | 355 | | |
| | | 356 | | // Capture F/B membership of both endpoints BEFORE the link is added. |
| | | 357 | | // If the call causes a cycle-merge, the original roots disappear and |
| | | 358 | | // we'll need to consolidate. |
| | 3220 | 359 | | var aInF = ctx.InForward.Contains(aRootBefore); |
| | 3220 | 360 | | var bInF = ctx.InForward.Contains(bRootBefore); |
| | 3220 | 361 | | var aInB = ctx.InBackward.Contains(aRootBefore); |
| | 3220 | 362 | | var bInB = ctx.InBackward.Contains(bRootBefore); |
| | | 363 | | |
| | 3220 | 364 | | var merged = ctx.Dg.AddDirectedLink(a, b); |
| | 4787 | 365 | | if (merged) MaybeCollapse(ctx.Dg, a, ctx.MaxIslandSize); |
| | | 366 | | |
| | 3220 | 367 | | if (merged) |
| | 1567 | 368 | | { |
| | | 369 | | // Cycle close: a and b (and possibly others) are now one component. |
| | | 370 | | // Combine F/B membership into the new root and propagate. |
| | 1567 | 371 | | RekeyAfterMerge(ctx); |
| | 1567 | 372 | | var newRoot = ctx.Dg.Find(a); |
| | 1567 | 373 | | var nowInF = aInF || bInF; |
| | 1567 | 374 | | var nowInB = aInB || bInB; |
| | | 375 | | |
| | 1567 | 376 | | if (nowInF) |
| | 1557 | 377 | | { |
| | 1557 | 378 | | ctx.InForward.Add(newRoot); |
| | | 379 | | // The merge can absorb previously-isolated components whose |
| | | 380 | | // members were never queued (because they weren't in F yet). |
| | | 381 | | // Re-enqueue idempotently — `queuedForward` dedups. |
| | 1557 | 382 | | EnqueueMembers(newRoot, ctx.ForwardQueue, ctx.QueuedForward, ctx); |
| | | 383 | | // Outgoing chains from the new root may lead to components |
| | | 384 | | // that weren't in F before; propagate into each. |
| | 5019 | 385 | | foreach (var t in ctx.Dg.GetOutgoingRoots(newRoot)) |
| | 174 | 386 | | { |
| | 174 | 387 | | if (!ctx.InForward.Contains(ctx.Dg.Find(t))) PropagateForward(t, ctx); |
| | 174 | 388 | | } |
| | 1557 | 389 | | } |
| | 1567 | 390 | | if (nowInB) |
| | 1558 | 391 | | { |
| | 1558 | 392 | | ctx.InBackward.Add(newRoot); |
| | 1558 | 393 | | EnqueueMembers(newRoot, ctx.BackwardQueue, ctx.QueuedBackward, ctx); |
| | 5030 | 394 | | foreach (var s in ctx.Dg.GetIncomingRoots(newRoot)) |
| | 178 | 395 | | { |
| | 181 | 396 | | if (!ctx.InBackward.Contains(ctx.Dg.Find(s))) PropagateBackward(s, ctx); |
| | 178 | 397 | | } |
| | 1558 | 398 | | } |
| | 1567 | 399 | | } |
| | | 400 | | else |
| | 1653 | 401 | | { |
| | | 402 | | // Plain link added (no merge). Propagate F/B across it if applicable. |
| | | 403 | | // - If a was in F, b's outgoing closure joins F. |
| | | 404 | | // - If b was in B, a's incoming closure joins B. |
| | 3221 | 405 | | if (aInF && !bInF) PropagateForward(bRootBefore, ctx); |
| | 1681 | 406 | | if (bInB && !aInB) PropagateBackward(aRootBefore, ctx); |
| | 1653 | 407 | | } |
| | 11502 | 408 | | } |
| | | 409 | | |
| | | 410 | | /// <summary> |
| | | 411 | | /// BFS from <paramref name="fromRoot"/> through <c>_outgoing</c> chains, |
| | | 412 | | /// marking newly-reached components as <c>inForward</c> and enqueueing |
| | | 413 | | /// their unprocessed members to the forward queue. Each component enters |
| | | 414 | | /// <c>inForward</c> at most once. |
| | | 415 | | /// </summary> |
| | | 416 | | private static void PropagateForward(EdgeId fromRoot, Ctx ctx) |
| | 1568 | 417 | | { |
| | 1568 | 418 | | var sentinel = IslandDirectedGraph.MainNetworkSentinel; |
| | 1568 | 419 | | var stack = new Stack<EdgeId>(); |
| | 1568 | 420 | | stack.Push(fromRoot); |
| | 3139 | 421 | | while (stack.Count > 0) |
| | 1571 | 422 | | { |
| | 1571 | 423 | | var current = ctx.Dg.Find(stack.Pop()); |
| | 1571 | 424 | | if (!ctx.InForward.Add(current)) continue; |
| | | 425 | | |
| | 1571 | 426 | | if (current != sentinel) |
| | 1453 | 427 | | { |
| | 1453 | 428 | | EnqueueMembers(current, ctx.ForwardQueue, ctx.QueuedForward, ctx); |
| | 1453 | 429 | | } |
| | | 430 | | |
| | 4719 | 431 | | foreach (var t in ctx.Dg.GetOutgoingRoots(current)) |
| | 3 | 432 | | { |
| | 6 | 433 | | if (!ctx.InForward.Contains(ctx.Dg.Find(t))) stack.Push(t); |
| | 3 | 434 | | } |
| | 1571 | 435 | | } |
| | 1568 | 436 | | } |
| | | 437 | | |
| | | 438 | | /// <summary> |
| | | 439 | | /// Mirror of <see cref="PropagateForward"/> walking <c>_incoming</c>. |
| | | 440 | | /// </summary> |
| | | 441 | | private static void PropagateBackward(EdgeId fromRoot, Ctx ctx) |
| | 31 | 442 | | { |
| | 31 | 443 | | var sentinel = IslandDirectedGraph.MainNetworkSentinel; |
| | 31 | 444 | | var stack = new Stack<EdgeId>(); |
| | 31 | 445 | | stack.Push(fromRoot); |
| | 75 | 446 | | while (stack.Count > 0) |
| | 44 | 447 | | { |
| | 44 | 448 | | var current = ctx.Dg.Find(stack.Pop()); |
| | 44 | 449 | | if (!ctx.InBackward.Add(current)) continue; |
| | | 450 | | |
| | 44 | 451 | | if (current != sentinel) |
| | 36 | 452 | | { |
| | 36 | 453 | | EnqueueMembers(current, ctx.BackwardQueue, ctx.QueuedBackward, ctx); |
| | 36 | 454 | | } |
| | | 455 | | |
| | 158 | 456 | | foreach (var s in ctx.Dg.GetIncomingRoots(current)) |
| | 13 | 457 | | { |
| | 26 | 458 | | if (!ctx.InBackward.Contains(ctx.Dg.Find(s))) stack.Push(s); |
| | 13 | 459 | | } |
| | 44 | 460 | | } |
| | 31 | 461 | | } |
| | | 462 | | |
| | | 463 | | private static void EnqueueMembers(EdgeId root, Queue<EdgeId> queue, HashSet<EdgeId> queued, Ctx ctx) |
| | 4604 | 464 | | { |
| | 4604 | 465 | | var members = ctx.Dg.GetMembers(root); |
| | 4971 | 466 | | if (members == null) return; |
| | 542673 | 467 | | foreach (var m in members) |
| | 264981 | 468 | | { |
| | 467134 | 469 | | if (ctx.Dg.IsProcessed(m)) continue; |
| | | 470 | | // Don't queue known-island members. They were added to the dg only |
| | | 471 | | // so cycle detection sees them; we never expand through them. |
| | 62831 | 472 | | if (IsKnownIsland(m, ctx.Kind, ctx.Islands)) continue; |
| | 122536 | 473 | | if (!queued.Add(m)) continue; |
| | 3114 | 474 | | queue.Enqueue(m); |
| | 3114 | 475 | | } |
| | 4604 | 476 | | } |
| | | 477 | | |
| | | 478 | | /// <summary> |
| | | 479 | | /// Re-canonicalise the F and B sets after a merge. Each set's entries are |
| | | 480 | | /// component roots; a merge can collapse multiple of them into one. We |
| | | 481 | | /// replace stale entries with their current <see cref="Find"/> result and |
| | | 482 | | /// dedup. |
| | | 483 | | /// </summary> |
| | | 484 | | private static void RekeyAfterMerge(Ctx ctx) |
| | 1567 | 485 | | { |
| | 1567 | 486 | | Rekey(ctx.InForward, ctx.Dg); |
| | 1567 | 487 | | Rekey(ctx.InBackward, ctx.Dg); |
| | 1567 | 488 | | } |
| | | 489 | | |
| | | 490 | | private static void Rekey(HashSet<EdgeId> set, IslandDirectedGraph dg) |
| | 3134 | 491 | | { |
| | 3134 | 492 | | if (set.Count == 0) return; |
| | 3134 | 493 | | var fresh = new HashSet<EdgeId>(set.Count); |
| | 24786 | 494 | | foreach (var r in set) fresh.Add(dg.Find(r)); |
| | 4428 | 495 | | if (fresh.Count == set.Count && SetEquals(fresh, set)) return; |
| | 1840 | 496 | | set.Clear(); |
| | 11673 | 497 | | foreach (var r in fresh) set.Add(r); |
| | 3134 | 498 | | } |
| | | 499 | | |
| | | 500 | | private static bool SetEquals(HashSet<EdgeId> a, HashSet<EdgeId> b) |
| | 1568 | 501 | | { |
| | 1568 | 502 | | if (a.Count != b.Count) return false; |
| | 10050 | 503 | | foreach (var x in a) if (!b.Contains(x)) return false; |
| | 1294 | 504 | | return true; |
| | 1568 | 505 | | } |
| | | 506 | | |
| | | 507 | | private static void MaybeCollapse(IslandDirectedGraph dg, EdgeId a, int maxIslandSize) |
| | 1567 | 508 | | { |
| | 1567 | 509 | | var size = dg.GetSize(dg.Find(a)); |
| | 1755 | 510 | | if (size >= maxIslandSize) dg.CollapseToMainNetwork(a); |
| | 1567 | 511 | | } |
| | | 512 | | } |