Refactor ElkSharp hybrid routing and document speed path
This commit is contained in:
@@ -0,0 +1,522 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
private readonly record struct HybridRepairBatch(
|
||||
string[] EdgeIds,
|
||||
string[] ConflictKeys);
|
||||
|
||||
private static CandidateSolution OptimizeHybrid(
|
||||
ElkRoutedEdge[] baselineProcessed,
|
||||
EdgeRoutingScore baselineProcessedScore,
|
||||
RoutingRetryState baselineRetryState,
|
||||
ElkPositionedNode[] nodes,
|
||||
ElkLayoutOptions layoutOptions,
|
||||
IterativeRoutingConfig config,
|
||||
double minLineClearance,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var strategy = BuildHybridStrategy(baselineProcessed, nodes, baselineProcessedScore, baselineRetryState, minLineClearance);
|
||||
var current = new CandidateSolution(baselineProcessedScore, baselineRetryState, baselineProcessed, 0);
|
||||
var diagnostics = ElkLayoutDiagnostics.Current;
|
||||
var attemptCounter = 0;
|
||||
ElkIterativeStrategyDiagnostics? liveStrategyDiagnostics = null;
|
||||
if (diagnostics is not null)
|
||||
{
|
||||
liveStrategyDiagnostics = new ElkIterativeStrategyDiagnostics
|
||||
{
|
||||
StrategyIndex = 1,
|
||||
OrderingName = "hybrid",
|
||||
Attempts = 0,
|
||||
TotalDurationMs = 0d,
|
||||
BestScore = baselineProcessedScore,
|
||||
Outcome = $"baseline({DescribeRetryState(baselineRetryState)})",
|
||||
RegisteredLive = true,
|
||||
BestEdges = baselineProcessed,
|
||||
};
|
||||
lock (diagnostics.SyncRoot)
|
||||
{
|
||||
diagnostics.IterativeStrategies.Add(liveStrategyDiagnostics);
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
|
||||
}
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Hybrid routing start: score={current.Score.Value:F0} retry={DescribeRetryState(current.RetryState)} waves={config.MaxRepairWaves}");
|
||||
|
||||
for (var wave = 0; wave < config.MaxRepairWaves; wave++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!current.RetryState.RequiresPrimaryRetry && current.Score.EdgeCrossings == 0)
|
||||
{
|
||||
ElkLayoutDiagnostics.LogProgress($"Hybrid routing converged before wave {wave + 1}");
|
||||
break;
|
||||
}
|
||||
|
||||
var repairPlan = BuildRepairPlan(
|
||||
current.Edges,
|
||||
nodes,
|
||||
current.Score,
|
||||
current.RetryState,
|
||||
strategy,
|
||||
wave + 1);
|
||||
if (repairPlan is null || repairPlan.Value.EdgeIds.Length == 0)
|
||||
{
|
||||
ElkLayoutDiagnostics.LogProgress($"Hybrid routing wave {wave + 1}: no repair plan");
|
||||
break;
|
||||
}
|
||||
|
||||
var batches = BuildHybridRepairBatches(current.Edges, nodes, repairPlan.Value);
|
||||
if (batches.Count == 0)
|
||||
{
|
||||
ElkLayoutDiagnostics.LogProgress($"Hybrid routing wave {wave + 1}: no independent repair batches");
|
||||
break;
|
||||
}
|
||||
|
||||
var waveImproved = false;
|
||||
for (var batchIndex = 0; batchIndex < batches.Count; batchIndex++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var batch = batches[batchIndex];
|
||||
var batchPlan = BuildHybridBatchPlan(repairPlan.Value, batch.EdgeIds);
|
||||
if (batchPlan.EdgeIds.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Hybrid routing wave {wave + 1} batch {batchIndex + 1}/{batches.Count}: " +
|
||||
$"edges=[{string.Join(", ", batchPlan.EdgeIds)}] keys=[{string.Join(", ", batch.ConflictKeys)}]");
|
||||
|
||||
var attemptStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
if (!TryApplyHybridRepairBatch(
|
||||
current,
|
||||
nodes,
|
||||
layoutOptions.Direction,
|
||||
config,
|
||||
strategy,
|
||||
batchPlan,
|
||||
cancellationToken,
|
||||
out var promoted,
|
||||
out var attempted,
|
||||
out var routeDiagnostics))
|
||||
{
|
||||
attemptStopwatch.Stop();
|
||||
attemptCounter++;
|
||||
RecordHybridAttempt(
|
||||
diagnostics,
|
||||
liveStrategyDiagnostics,
|
||||
attemptCounter,
|
||||
attemptStopwatch.Elapsed.TotalMilliseconds,
|
||||
attempted.Score,
|
||||
BuildHybridAttemptOutcome(attempted.RetryState, improved: false),
|
||||
routeDiagnostics,
|
||||
attempted.Edges);
|
||||
continue;
|
||||
}
|
||||
|
||||
attemptStopwatch.Stop();
|
||||
attemptCounter++;
|
||||
RecordHybridAttempt(
|
||||
diagnostics,
|
||||
liveStrategyDiagnostics,
|
||||
attemptCounter,
|
||||
attemptStopwatch.Elapsed.TotalMilliseconds,
|
||||
attempted.Score,
|
||||
BuildHybridAttemptOutcome(attempted.RetryState, improved: true),
|
||||
routeDiagnostics,
|
||||
attempted.Edges);
|
||||
current = promoted;
|
||||
waveImproved = true;
|
||||
}
|
||||
|
||||
if (waveImproved)
|
||||
{
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Hybrid routing wave {wave + 1} improved: score={current.Score.Value:F0} retry={DescribeRetryState(current.RetryState)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!current.RetryState.RequiresPrimaryRetry && current.Score.EdgeCrossings == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Hybrid routing wave {wave + 1} stalled; adapting strategy for retry={DescribeRetryState(current.RetryState)}");
|
||||
strategy.AdaptForViolations(current.Score, wave, current.RetryState);
|
||||
}
|
||||
|
||||
if (config.MaxRepairWaves <= 1
|
||||
&& (current.RetryState.RequiresPrimaryRetry || current.Score.EdgeCrossings > 0))
|
||||
{
|
||||
var focusedRepairStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var focusedRepair = TryApplyVerifiedIssueRepairRound(
|
||||
current.Edges,
|
||||
nodes,
|
||||
config.ObstacleMargin,
|
||||
strategy,
|
||||
current.RetryState,
|
||||
layoutOptions.Direction,
|
||||
cancellationToken,
|
||||
config.MaxParallelRepairBuilds,
|
||||
trustIndependentParallelBuilds: true,
|
||||
useHybridCleanup: true);
|
||||
focusedRepairStopwatch.Stop();
|
||||
if (focusedRepair is { } repaired)
|
||||
{
|
||||
var improved = IsBetterBoundarySlotRepairCandidate(
|
||||
repaired.Score,
|
||||
repaired.RetryState,
|
||||
current.Score,
|
||||
current.RetryState);
|
||||
attemptCounter++;
|
||||
RecordHybridAttempt(
|
||||
diagnostics,
|
||||
liveStrategyDiagnostics,
|
||||
attemptCounter,
|
||||
focusedRepairStopwatch.Elapsed.TotalMilliseconds,
|
||||
repaired.Score,
|
||||
BuildHybridAttemptOutcome(repaired.RetryState, improved),
|
||||
repaired.RouteDiagnostics,
|
||||
repaired.Edges);
|
||||
if (improved)
|
||||
{
|
||||
current = current with
|
||||
{
|
||||
Score = repaired.Score,
|
||||
RetryState = repaired.RetryState,
|
||||
Edges = repaired.Edges,
|
||||
};
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Hybrid focused post-wave repair improved: score={current.Score.Value:F0} retry={DescribeRetryState(current.RetryState)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current = RefineHybridWinningSolution(
|
||||
current,
|
||||
nodes,
|
||||
layoutOptions.Direction,
|
||||
minLineClearance,
|
||||
preferLowWaveRuntimePolish: config.MaxRepairWaves <= 2);
|
||||
if (liveStrategyDiagnostics is not null)
|
||||
{
|
||||
lock (diagnostics!.SyncRoot)
|
||||
{
|
||||
liveStrategyDiagnostics.Attempts = attemptCounter;
|
||||
liveStrategyDiagnostics.BestScore = current.Score;
|
||||
liveStrategyDiagnostics.BestEdges = current.Edges;
|
||||
liveStrategyDiagnostics.Outcome = current.RetryState.RequiresPrimaryRetry
|
||||
? $"retry({DescribeRetryState(current.RetryState)})"
|
||||
: "valid";
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.FlushSnapshot(diagnostics!);
|
||||
}
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Hybrid routing complete: score={current.Score.Value:F0} retry={DescribeRetryState(current.RetryState)}");
|
||||
return current;
|
||||
}
|
||||
|
||||
private static RoutingStrategy BuildHybridStrategy(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes,
|
||||
EdgeRoutingScore baselineScore,
|
||||
RoutingRetryState baselineRetryState,
|
||||
double minLineClearance)
|
||||
{
|
||||
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var connectionCount = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
var sourceId = edge.SourceNodeId ?? string.Empty;
|
||||
var targetId = edge.TargetNodeId ?? string.Empty;
|
||||
connectionCount[sourceId] = connectionCount.GetValueOrDefault(sourceId) + 1;
|
||||
connectionCount[targetId] = connectionCount.GetValueOrDefault(targetId) + 1;
|
||||
}
|
||||
|
||||
var useConnectedOrdering = baselineRetryState.RequiresBlockingRetry
|
||||
|| baselineRetryState.SharedLaneViolations > 0
|
||||
|| baselineRetryState.TargetApproachJoinViolations > 0;
|
||||
var edgeOrder = useConnectedOrdering
|
||||
? OrderByMostConnectedFirst(edges, connectionCount)
|
||||
: OrderByLongestFirst(edges, nodesById);
|
||||
var routingParams = baselineRetryState.RequiresBlockingRetry
|
||||
? new AStarRoutingParams(18d, 400d, 600d, 3.0d, minLineClearance, 40d, true)
|
||||
: new AStarRoutingParams(18d, 200d, 500d, 2.0d, minLineClearance, 40d, true);
|
||||
var strategy = new RoutingStrategy
|
||||
{
|
||||
EdgeOrder = edgeOrder,
|
||||
BaseLineClearance = minLineClearance,
|
||||
MinLineClearance = minLineClearance,
|
||||
RoutingParams = routingParams,
|
||||
};
|
||||
|
||||
if (baselineRetryState.RequiresPrimaryRetry || baselineScore.EdgeCrossings > 0)
|
||||
{
|
||||
strategy.AdaptForViolations(baselineScore, 0, baselineRetryState);
|
||||
}
|
||||
|
||||
return strategy;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<HybridRepairBatch> BuildHybridRepairBatches(
|
||||
IReadOnlyCollection<ElkRoutedEdge> edges,
|
||||
IReadOnlyCollection<ElkPositionedNode> nodes,
|
||||
RepairPlan repairPlan)
|
||||
{
|
||||
var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal);
|
||||
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var orderedBatches = new List<(List<string> EdgeIds, HashSet<string> ConflictKeys)>();
|
||||
|
||||
foreach (var edgeId in repairPlan.EdgeIds)
|
||||
{
|
||||
if (!edgesById.TryGetValue(edgeId, out var edge))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var conflictKeys = GetHybridConflictKeys(edge, nodesById);
|
||||
var assigned = false;
|
||||
foreach (var batch in orderedBatches)
|
||||
{
|
||||
if (batch.ConflictKeys.Overlaps(conflictKeys))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
batch.EdgeIds.Add(edgeId);
|
||||
batch.ConflictKeys.UnionWith(conflictKeys);
|
||||
assigned = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (assigned)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
orderedBatches.Add((
|
||||
EdgeIds: [edgeId],
|
||||
ConflictKeys: new HashSet<string>(conflictKeys, StringComparer.Ordinal)));
|
||||
}
|
||||
|
||||
return orderedBatches
|
||||
.Select(batch => new HybridRepairBatch(
|
||||
batch.EdgeIds.ToArray(),
|
||||
batch.ConflictKeys.OrderBy(key => key, StringComparer.Ordinal).ToArray()))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static string[] GetHybridConflictKeys(
|
||||
ElkRoutedEdge edge,
|
||||
IReadOnlyDictionary<string, ElkPositionedNode> nodesById)
|
||||
{
|
||||
var keys = new HashSet<string>(StringComparer.Ordinal);
|
||||
var sourceId = edge.SourceNodeId ?? string.Empty;
|
||||
var targetId = edge.TargetNodeId ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(sourceId))
|
||||
{
|
||||
keys.Add($"source:{sourceId}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(targetId))
|
||||
{
|
||||
keys.Add($"target:{targetId}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sourceId) && !string.IsNullOrEmpty(targetId))
|
||||
{
|
||||
keys.Add($"pair:{sourceId}->{targetId}");
|
||||
}
|
||||
|
||||
var path = ExtractPath(edge);
|
||||
if (path.Count >= 2
|
||||
&& nodesById.TryGetValue(sourceId, out var sourceNode))
|
||||
{
|
||||
var sourceSide = ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(path[0], path[1], sourceNode);
|
||||
keys.Add($"source-side:{sourceId}:{sourceSide}");
|
||||
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
||||
{
|
||||
keys.Add($"gateway-source:{sourceId}");
|
||||
}
|
||||
}
|
||||
|
||||
if (path.Count >= 2
|
||||
&& nodesById.TryGetValue(targetId, out var targetNode))
|
||||
{
|
||||
var previousPoint = path[^2];
|
||||
var targetSide = ResolveEntrySide(path[^1], previousPoint, targetNode);
|
||||
keys.Add($"target-side:{targetId}:{targetSide}");
|
||||
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
||||
{
|
||||
keys.Add($"gateway-target:{targetId}");
|
||||
}
|
||||
}
|
||||
|
||||
if (ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label))
|
||||
{
|
||||
keys.Add($"collector:{sourceId}:{targetId}");
|
||||
}
|
||||
|
||||
return keys.OrderBy(key => key, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
private static RepairPlan BuildHybridBatchPlan(RepairPlan repairPlan, IReadOnlyCollection<string> batchEdgeIds)
|
||||
{
|
||||
var batchEdgeSet = batchEdgeIds.ToHashSet(StringComparer.Ordinal);
|
||||
var edgeIds = repairPlan.EdgeIds.Where(batchEdgeSet.Contains).ToArray();
|
||||
var preferredShortestEdgeIds = repairPlan.PreferredShortestEdgeIds.Where(batchEdgeSet.Contains).ToArray();
|
||||
var routeRepairEdgeIds = repairPlan.RouteRepairEdgeIds.Where(batchEdgeSet.Contains).ToArray();
|
||||
var edgeIndexById = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < repairPlan.EdgeIds.Length && i < repairPlan.EdgeIndices.Length; i++)
|
||||
{
|
||||
edgeIndexById[repairPlan.EdgeIds[i]] = repairPlan.EdgeIndices[i];
|
||||
}
|
||||
|
||||
var edgeIndices = edgeIds
|
||||
.Select(edgeId => edgeIndexById.GetValueOrDefault(edgeId, -1))
|
||||
.Where(index => index >= 0)
|
||||
.ToArray();
|
||||
|
||||
return new RepairPlan(
|
||||
edgeIndices,
|
||||
edgeIds,
|
||||
preferredShortestEdgeIds,
|
||||
routeRepairEdgeIds,
|
||||
repairPlan.Reasons);
|
||||
}
|
||||
|
||||
private static bool TryApplyHybridRepairBatch(
|
||||
CandidateSolution current,
|
||||
ElkPositionedNode[] nodes,
|
||||
ElkLayoutDirection direction,
|
||||
IterativeRoutingConfig config,
|
||||
RoutingStrategy strategy,
|
||||
RepairPlan repairPlan,
|
||||
CancellationToken cancellationToken,
|
||||
out CandidateSolution promoted,
|
||||
out CandidateSolution attempted,
|
||||
out ElkIterativeRouteDiagnostics routeDiagnostics)
|
||||
{
|
||||
promoted = current;
|
||||
attempted = current;
|
||||
var restrictedEdgeIds = repairPlan.EdgeIds;
|
||||
var routed = RepairPenalizedEdges(
|
||||
current.Edges,
|
||||
nodes,
|
||||
config.ObstacleMargin,
|
||||
strategy,
|
||||
repairPlan,
|
||||
cancellationToken,
|
||||
config.MaxParallelRepairBuilds,
|
||||
trustIndependentParallelBuilds: true);
|
||||
routeDiagnostics = routed.Diagnostics;
|
||||
var candidateEdges = ApplyHybridTerminalRuleCleanupRound(
|
||||
routed.Edges,
|
||||
nodes,
|
||||
direction,
|
||||
strategy.MinLineClearance,
|
||||
restrictedEdgeIds);
|
||||
candidateEdges = ChoosePreferredHardRuleLayout(current.Edges, candidateEdges, nodes);
|
||||
|
||||
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
|
||||
var remainingBrokenHighways = HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
|
||||
: 0;
|
||||
var candidateRetryState = BuildRetryState(candidateScore, remainingBrokenHighways);
|
||||
|
||||
if (candidateRetryState.RequiresBlockingRetry || candidateRetryState.RequiresLengthRetry)
|
||||
{
|
||||
var focusedRepair = TryApplyVerifiedIssueRepairRound(
|
||||
candidateEdges,
|
||||
nodes,
|
||||
config.ObstacleMargin,
|
||||
strategy,
|
||||
candidateRetryState,
|
||||
direction,
|
||||
cancellationToken,
|
||||
config.MaxParallelRepairBuilds,
|
||||
trustIndependentParallelBuilds: true,
|
||||
useHybridCleanup: true);
|
||||
if (focusedRepair is { } repaired
|
||||
&& IsBetterBoundarySlotRepairCandidate(
|
||||
repaired.Score,
|
||||
repaired.RetryState,
|
||||
candidateScore,
|
||||
candidateRetryState))
|
||||
{
|
||||
candidateEdges = repaired.Edges;
|
||||
candidateScore = repaired.Score;
|
||||
remainingBrokenHighways = repaired.RemainingBrokenHighways;
|
||||
candidateRetryState = repaired.RetryState;
|
||||
}
|
||||
}
|
||||
|
||||
attempted = current with
|
||||
{
|
||||
Score = candidateScore,
|
||||
RetryState = candidateRetryState,
|
||||
Edges = candidateEdges,
|
||||
};
|
||||
|
||||
if (!IsBetterBoundarySlotRepairCandidate(
|
||||
candidateScore,
|
||||
candidateRetryState,
|
||||
current.Score,
|
||||
current.RetryState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
promoted = attempted;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void RecordHybridAttempt(
|
||||
ElkLayoutRunDiagnostics? diagnostics,
|
||||
ElkIterativeStrategyDiagnostics? liveStrategyDiagnostics,
|
||||
int attempt,
|
||||
double durationMs,
|
||||
EdgeRoutingScore score,
|
||||
string outcome,
|
||||
ElkIterativeRouteDiagnostics routeDiagnostics,
|
||||
ElkRoutedEdge[] edges)
|
||||
{
|
||||
if (diagnostics is null || liveStrategyDiagnostics is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var attemptDiagnostics = new ElkIterativeAttemptDiagnostics
|
||||
{
|
||||
Attempt = attempt,
|
||||
TotalDurationMs = Math.Round(durationMs, 3),
|
||||
Score = score,
|
||||
Outcome = outcome,
|
||||
RouteDiagnostics = routeDiagnostics,
|
||||
Edges = edges,
|
||||
};
|
||||
|
||||
lock (diagnostics.SyncRoot)
|
||||
{
|
||||
liveStrategyDiagnostics.Attempts = attempt;
|
||||
liveStrategyDiagnostics.BestScore = score;
|
||||
liveStrategyDiagnostics.AttemptDetails.Add(attemptDiagnostics);
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
|
||||
}
|
||||
|
||||
private static string BuildHybridAttemptOutcome(RoutingRetryState retryState, bool improved)
|
||||
{
|
||||
if (!retryState.RequiresPrimaryRetry)
|
||||
{
|
||||
return improved ? "valid" : "rejected-valid";
|
||||
}
|
||||
|
||||
var retry = $"retry({DescribeRetryState(retryState)})";
|
||||
return improved ? retry : $"rejected-{retry}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user