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 +
|
// artifact polish ends with NormalizeBoundaryAngles +
|
||||||
// NormalizeSourceExitAngles, which is the root cause of the
|
// NormalizeSourceExitAngles, which is the root cause of the
|
||||||
// boundary-slot violations when snap ran before it.
|
// 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);
|
current = ApplyFinalBoundarySlotPolish(current, nodes, direction, minLineClearance, maxRounds: 1);
|
||||||
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after final boundary-slot snap: {DescribeSolution(current)}");
|
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)}");
|
ElkLayoutDiagnostics.LogProgress($"Hybrid winner refinement after gateway diagonal straightening: {DescribeSolution(current)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-edge gateway source face redirect: process each gateway artifact
|
// Per-edge gateway fixes: only run when the gateway artifact polish
|
||||||
// edge individually, applying the face redirect and validating that it
|
// left remaining artifacts. Skip the expensive per-edge scoring when
|
||||||
// doesn't create new hard-rule violations. Bulk processing (all edges at
|
// artifacts are already clean.
|
||||||
// once) creates 19+ boundary-slot violations because the redirect paths
|
|
||||||
// conflict with each other. Per-edge with validation avoids cascading.
|
|
||||||
var postArtifactState = EvaluateGatewayArtifacts(current.Edges, nodes, out var postFocus);
|
var postArtifactState = EvaluateGatewayArtifacts(current.Edges, nodes, out var postFocus);
|
||||||
if (!postArtifactState.IsClean && postFocus.Length > 0)
|
if (!postArtifactState.IsClean && postFocus.Length > 0)
|
||||||
{
|
{
|
||||||
current = ApplyPerEdgeGatewayFaceRedirect(current, nodes, minLineClearance, postFocus);
|
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;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,25 +315,25 @@ internal static partial class ElkEdgeRouterIterative
|
|||||||
// 19+ boundary-slot violations.
|
// 19+ boundary-slot violations.
|
||||||
candidate = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(candidate, nodes);
|
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 candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidate, nodes);
|
||||||
var candidateRetry = BuildRetryState(
|
var candidateRetry = BuildRetryState(candidateScore, 0);
|
||||||
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 candidateGatewaySourceBetter =
|
var candidateGatewaySourceBetter =
|
||||||
candidateRetry.GatewaySourceExitViolations <= current.RetryState.GatewaySourceExitViolations;
|
candidateRetry.GatewaySourceExitViolations <= current.RetryState.GatewaySourceExitViolations;
|
||||||
var backtrackingAcceptable =
|
var backtrackingAcceptable =
|
||||||
candidateRetry.TargetApproachBacktrackingViolations <= current.RetryState.TargetApproachBacktrackingViolations + 1
|
candidateRetry.TargetApproachBacktrackingViolations <= current.RetryState.TargetApproachBacktrackingViolations + 1
|
||||||
&& candidateGatewaySourceBetter;
|
&& 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 =
|
var boundarySlotAcceptable =
|
||||||
candidateRetry.BoundarySlotViolations <= current.RetryState.BoundarySlotViolations + 3
|
candidateRetry.BoundarySlotViolations <= current.RetryState.BoundarySlotViolations + 3
|
||||||
&& candidateGatewaySourceBetter;
|
&& candidateGatewaySourceBetter;
|
||||||
@@ -432,15 +425,18 @@ internal static partial class ElkEdgeRouterIterative
|
|||||||
};
|
};
|
||||||
|
|
||||||
candidateEdges = ElkEdgePostProcessor.StraightenGatewayCornerDiagonals(candidateEdges, nodes);
|
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
|
// Cheap validation first: reject if modified edge creates crossings/shared lanes.
|
||||||
// (redirect may create small overshoot and shift slot alignment).
|
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
|
var backtrackingOk = candidateRetry.TargetApproachBacktrackingViolations
|
||||||
<= current.RetryState.TargetApproachBacktrackingViolations + 1;
|
<= current.RetryState.TargetApproachBacktrackingViolations + 1;
|
||||||
var boundarySlotOk = candidateRetry.BoundarySlotViolations
|
var boundarySlotOk = candidateRetry.BoundarySlotViolations
|
||||||
|
|||||||
Reference in New Issue
Block a user