Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.Hybrid.cs
master c3c6f2d0c6 Use node-sized corridor grid spacing for cleaner edge routing
Replace fixed IntermediateGridSpacing=40 with average node height (~100px).
A* grid cells are now node-sized in corridors, forcing edges through wide
lanes between node rows. Fine node-boundary lines (±18px margin) still
provide precise resolution near nodes for clean joins.

Visual improvement is dramatic: edges no longer hug node boundaries.

NodeSpacing=50 test set. Remaining: ExcessiveDetourViolations=1 and
edge/9 under-node flush. Target-join, shared-lane, boundary-angle,
long-diagonal all clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:11:10 +03:00

580 lines
22 KiB
C#

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);
// Corridor grid spacing: use average node height so the A* grid cells
// are node-sized. Edges route through wide corridors between node rows,
// not through narrow gaps. The fine node-boundary lines (at obstacle
// edge ± 18px margin) still provide precise resolution near nodes.
var serviceNodesForGrid = nodes.Where(n => n.Kind is not "Start" and not "End").ToArray();
var avgNodeHeight = serviceNodesForGrid.Length > 0 ? serviceNodesForGrid.Average(n => n.Height) : 88d;
var corridorGridSpacing = Math.Max(40d, avgNodeHeight);
var routingParams = baselineRetryState.RequiresBlockingRetry
? new AStarRoutingParams(18d, 400d, 600d, 3.0d, minLineClearance, corridorGridSpacing, true)
: new AStarRoutingParams(18d, 200d, 500d, 2.0d, minLineClearance, corridorGridSpacing, 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);
// Build conflict zones: each repair edge gets a bounding box from its
// routed path plus a margin. Two edges conflict if their zones overlap
// spatially, or if they share a repeat-collector label on the same
// source-target pair.
var zones = new List<(string EdgeId, ConflictZone Zone)>();
foreach (var edgeId in repairPlan.EdgeIds)
{
if (!edgesById.TryGetValue(edgeId, out var edge))
{
continue;
}
zones.Add((edgeId, BuildConflictZone(edge, nodesById)));
}
// Greedy first-fit batching: assign each edge to the first batch
// whose existing zones don't spatially conflict.
var orderedBatches = new List<(List<string> EdgeIds, List<ConflictZone> Zones)>();
foreach (var (edgeId, zone) in zones)
{
var assigned = false;
foreach (var batch in orderedBatches)
{
if (batch.Zones.Any(existing => existing.ConflictsWith(zone)))
{
continue;
}
batch.EdgeIds.Add(edgeId);
batch.Zones.Add(zone);
assigned = true;
break;
}
if (!assigned)
{
orderedBatches.Add((EdgeIds: [edgeId], Zones: [zone]));
}
}
return orderedBatches
.Select(batch => new HybridRepairBatch(
batch.EdgeIds.ToArray(),
batch.Zones.SelectMany(z => z.DescriptiveKeys).Distinct(StringComparer.Ordinal)
.OrderBy(key => key, StringComparer.Ordinal).ToArray()))
.ToArray();
}
/// <summary>
/// Geometric conflict zone for an edge: bounding box of its routed path
/// expanded by a margin, plus endpoint node IDs for collector-label
/// conflict detection.
/// </summary>
private static ConflictZone BuildConflictZone(
ElkRoutedEdge edge,
IReadOnlyDictionary<string, ElkPositionedNode> nodesById)
{
var path = ExtractPath(edge);
const double margin = 40d;
// Compute bounding box from path + source/target node extents.
var minX = double.MaxValue;
var minY = double.MaxValue;
var maxX = double.MinValue;
var maxY = double.MinValue;
foreach (var point in path)
{
minX = Math.Min(minX, point.X);
minY = Math.Min(minY, point.Y);
maxX = Math.Max(maxX, point.X);
maxY = Math.Max(maxY, point.Y);
}
// Include source/target node extents in the zone.
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var srcNode))
{
minX = Math.Min(minX, srcNode.X);
minY = Math.Min(minY, srcNode.Y);
maxX = Math.Max(maxX, srcNode.X + srcNode.Width);
maxY = Math.Max(maxY, srcNode.Y + srcNode.Height);
}
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var tgtNode))
{
minX = Math.Min(minX, tgtNode.X);
minY = Math.Min(minY, tgtNode.Y);
maxX = Math.Max(maxX, tgtNode.X + tgtNode.Width);
maxY = Math.Max(maxY, tgtNode.Y + tgtNode.Height);
}
var isCollector = ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label);
var descriptiveKeys = new List<string>(3);
if (!string.IsNullOrEmpty(edge.SourceNodeId))
{
descriptiveKeys.Add($"source:{edge.SourceNodeId}");
}
if (!string.IsNullOrEmpty(edge.TargetNodeId))
{
descriptiveKeys.Add($"target:{edge.TargetNodeId}");
}
if (isCollector)
{
descriptiveKeys.Add($"collector:{edge.SourceNodeId}:{edge.TargetNodeId}");
}
return new ConflictZone(
MinX: minX - margin,
MinY: minY - margin,
MaxX: maxX + margin,
MaxY: maxY + margin,
SourceNodeId: edge.SourceNodeId ?? string.Empty,
TargetNodeId: edge.TargetNodeId ?? string.Empty,
IsCollector: isCollector,
DescriptiveKeys: descriptiveKeys.ToArray());
}
private readonly record struct ConflictZone(
double MinX, double MinY, double MaxX, double MaxY,
string SourceNodeId, string TargetNodeId,
bool IsCollector,
string[] DescriptiveKeys)
{
/// <summary>
/// Two zones conflict if their bounding boxes overlap spatially,
/// or if they are both collectors on the same source-target pair.
/// </summary>
internal bool ConflictsWith(ConflictZone other)
{
// Collector edges on the same pair always conflict.
if (IsCollector && other.IsCollector
&& string.Equals(SourceNodeId, other.SourceNodeId, StringComparison.Ordinal)
&& string.Equals(TargetNodeId, other.TargetNodeId, StringComparison.Ordinal))
{
return true;
}
// Spatial overlap check.
return MinX < other.MaxX && MaxX > other.MinX
&& MinY < other.MaxY && MaxY > other.MinY;
}
}
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}";
}
}