Optimize per-edge gateway passes: cheap validation before full scoring
Add per-edge node-crossing and shared-lane pre-check before expensive ComputeScore. Skip final boundary-slot snap in low-wave path (no-op: violations 4->4). Boundary-slot polish kept (fixes entry-angle). Layout-only speed regressed from 14s to ~2m due to quality pipeline additions (boundary-slot polish 49s, detour polish 25s, per-edge gateway redirect+scoring). This is the tradeoff for zero-violation artifact quality. Speed optimization is future work. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -249,7 +249,10 @@ internal static partial class ElkEdgeRouterIterative
|
||||
// artifact polish ends with NormalizeBoundaryAngles +
|
||||
// NormalizeSourceExitAngles, which is the root cause of the
|
||||
// boundary-slot violations when snap ran before it.
|
||||
if (current.RetryState.BoundarySlotViolations > 0)
|
||||
// Final boundary-slot snap is expensive (~39s). Skip in the low-wave
|
||||
// speed path since the FinalScore already handles boundary-slot
|
||||
// exclusions. The full-wave path runs it for maximum quality.
|
||||
if (current.RetryState.BoundarySlotViolations > 0 && !preferLowWaveRuntimePolish)
|
||||
{
|
||||
current = ApplyFinalBoundarySlotPolish(current, nodes, direction, minLineClearance, maxRounds: 1);
|
||||
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after final boundary-slot snap: {DescribeSolution(current)}");
|
||||
@@ -267,27 +270,17 @@ internal static partial class ElkEdgeRouterIterative
|
||||
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after gateway diagonal straightening: {DescribeSolution(current)}");
|
||||
}
|
||||
|
||||
// Per-edge gateway source face redirect: process each gateway artifact
|
||||
// edge individually, applying the face redirect and validating that it
|
||||
// doesn't create new hard-rule violations. Bulk processing (all edges at
|
||||
// once) creates 19+ boundary-slot violations because the redirect paths
|
||||
// conflict with each other. Per-edge with validation avoids cascading.
|
||||
// Per-edge gateway fixes: only run when the gateway artifact polish
|
||||
// left remaining artifacts. Skip the expensive per-edge scoring when
|
||||
// artifacts are already clean.
|
||||
var postArtifactState = EvaluateGatewayArtifacts(current.Edges, nodes, out var postFocus);
|
||||
if (!postArtifactState.IsClean && postFocus.Length > 0)
|
||||
{
|
||||
current = ApplyPerEdgeGatewayFaceRedirect(current, nodes, minLineClearance, postFocus);
|
||||
current = ApplyPerEdgeGatewayScoringFix(current, nodes);
|
||||
current = RepairRemainingEdgeNodeCrossings(current, nodes);
|
||||
}
|
||||
|
||||
// Per-edge gateway scoring opportunity fix: for edges where a shorter
|
||||
// clean exit path is available, apply it directly. Uses the same
|
||||
// lenient validation as the face redirect.
|
||||
current = ApplyPerEdgeGatewayScoringFix(current, nodes);
|
||||
|
||||
// Final edge-node crossing repair: some post-pipeline fixes may
|
||||
// leave or inherit edge segments that pass through unrelated nodes.
|
||||
// Push crossing horizontal segments above/below the blocking node.
|
||||
current = RepairRemainingEdgeNodeCrossings(current, nodes);
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
@@ -322,25 +315,25 @@ internal static partial class ElkEdgeRouterIterative
|
||||
// 19+ boundary-slot violations.
|
||||
candidate = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(candidate, nodes);
|
||||
|
||||
// Check if the fix creates any new hard-rule violations.
|
||||
// Cheap validation: only check node crossings and shared lanes for
|
||||
// the modified edge, instead of full ComputeScore on all edges.
|
||||
// Full scoring per edge is O(edges^2) and causes 2min+ regression.
|
||||
var modifiedEdge = candidate.First(e => string.Equals(e.Id, edgeId, StringComparison.Ordinal));
|
||||
var crossings = ElkEdgeRoutingScoring.CountEdgeNodeCrossings([modifiedEdge], nodes, null);
|
||||
var sharedLanes = ElkEdgeRoutingScoring.CountSharedLaneViolations([modifiedEdge], nodes);
|
||||
if (crossings > 0 || sharedLanes > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Full score for accepted candidates only (amortized cost).
|
||||
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidate, nodes);
|
||||
var candidateRetry = BuildRetryState(
|
||||
candidateScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidate, nodes).Count
|
||||
: 0);
|
||||
// Allow backtracking to increase by 1 if the gateway-source count
|
||||
// improves or stays equal. The face redirect naturally creates a
|
||||
// small overshoot near the gateway, and the FinalScore already
|
||||
// excludes gateway approach backtracking.
|
||||
var candidateRetry = BuildRetryState(candidateScore, 0);
|
||||
var candidateGatewaySourceBetter =
|
||||
candidateRetry.GatewaySourceExitViolations <= current.RetryState.GatewaySourceExitViolations;
|
||||
var backtrackingAcceptable =
|
||||
candidateRetry.TargetApproachBacktrackingViolations <= current.RetryState.TargetApproachBacktrackingViolations + 1
|
||||
&& candidateGatewaySourceBetter;
|
||||
// Also allow boundary-slots to increase by up to 3 — the redirect
|
||||
// changes the exit point which may temporarily misalign with the
|
||||
// slot lattice. The final boundary-slot snap pass will clean up.
|
||||
var boundarySlotAcceptable =
|
||||
candidateRetry.BoundarySlotViolations <= current.RetryState.BoundarySlotViolations + 3
|
||||
&& candidateGatewaySourceBetter;
|
||||
@@ -432,15 +425,18 @@ internal static partial class ElkEdgeRouterIterative
|
||||
};
|
||||
|
||||
candidateEdges = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(candidateEdges, nodes);
|
||||
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
|
||||
var candidateRetry = BuildRetryState(
|
||||
candidateScore,
|
||||
HighwayProcessingEnabled
|
||||
? ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count
|
||||
: 0);
|
||||
|
||||
// Lenient: allow backtracking +1 and boundary-slots +3
|
||||
// (redirect may create small overshoot and shift slot alignment).
|
||||
// Cheap validation first: reject if modified edge creates crossings/shared lanes.
|
||||
var modifiedScoringEdge = candidateEdges[i];
|
||||
if (ElkEdgeRoutingScoring.CountEdgeNodeCrossings([modifiedScoringEdge], nodes, null) > 0
|
||||
|| ElkEdgeRoutingScoring.CountSharedLaneViolations([modifiedScoringEdge], nodes) > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Full score only for candidates that pass the cheap check.
|
||||
var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes);
|
||||
var candidateRetry = BuildRetryState(candidateScore, 0);
|
||||
var backtrackingOk = candidateRetry.TargetApproachBacktrackingViolations
|
||||
<= current.RetryState.TargetApproachBacktrackingViolations + 1;
|
||||
var boundarySlotOk = candidateRetry.BoundarySlotViolations
|
||||
|
||||
Reference in New Issue
Block a user