374 lines
16 KiB
C#
374 lines
16 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
|
|
namespace StellaOps.ElkSharp;
|
|
|
|
internal static partial class ElkEdgeRouterIterative
|
|
{
|
|
private static RepairPlan? BuildRepairPlan(
|
|
ElkRoutedEdge[] edges,
|
|
ElkPositionedNode[] nodes,
|
|
EdgeRoutingScore score,
|
|
RoutingRetryState retryState,
|
|
RoutingStrategy strategy,
|
|
int attempt)
|
|
{
|
|
if (edges.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var severityByEdgeId = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
var preferredShortestEdgeIds = new HashSet<string>(StringComparer.Ordinal);
|
|
var routeRepairEdgeIds = new HashSet<string>(StringComparer.Ordinal);
|
|
var mandatoryEdgeIds = new HashSet<string>(StringComparer.Ordinal);
|
|
var severityByReason = new Dictionary<string, Dictionary<string, int>>(StringComparer.Ordinal);
|
|
var reasons = new List<string>();
|
|
var prioritizeBlockingAndLengthOnly = retryState.RequiresBlockingRetry || retryState.RequiresLengthRetry;
|
|
|
|
void AddReason(string reason)
|
|
{
|
|
if (!reasons.Contains(reason, StringComparer.Ordinal))
|
|
{
|
|
reasons.Add(reason);
|
|
}
|
|
}
|
|
|
|
void AddEdgeIds(
|
|
IEnumerable<string> edgeIds,
|
|
int severity,
|
|
string reason,
|
|
bool requiresRouteRepair = false,
|
|
bool mandatoryRepair = false)
|
|
{
|
|
AddReason(reason);
|
|
if (!severityByReason.TryGetValue(reason, out var reasonSeverity))
|
|
{
|
|
reasonSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
severityByReason[reason] = reasonSeverity;
|
|
}
|
|
|
|
foreach (var edgeId in edgeIds)
|
|
{
|
|
severityByEdgeId[edgeId] = severityByEdgeId.GetValueOrDefault(edgeId) + severity;
|
|
reasonSeverity[edgeId] = reasonSeverity.GetValueOrDefault(edgeId) + severity;
|
|
if (requiresRouteRepair)
|
|
{
|
|
routeRepairEdgeIds.Add(edgeId);
|
|
}
|
|
|
|
if (mandatoryRepair)
|
|
{
|
|
mandatoryEdgeIds.Add(edgeId);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MergeSeverity(
|
|
Dictionary<string, int> metricSeverity,
|
|
string reason,
|
|
bool preferShortestRepair = false,
|
|
bool requiresRouteRepair = false,
|
|
bool mandatoryRepair = false)
|
|
{
|
|
if (metricSeverity.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AddReason(reason);
|
|
if (!severityByReason.TryGetValue(reason, out var reasonSeverity))
|
|
{
|
|
reasonSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
severityByReason[reason] = reasonSeverity;
|
|
}
|
|
|
|
foreach (var (edgeId, severity) in metricSeverity)
|
|
{
|
|
severityByEdgeId[edgeId] = severityByEdgeId.GetValueOrDefault(edgeId) + severity;
|
|
reasonSeverity[edgeId] = reasonSeverity.GetValueOrDefault(edgeId) + severity;
|
|
if (preferShortestRepair || requiresRouteRepair)
|
|
{
|
|
routeRepairEdgeIds.Add(edgeId);
|
|
}
|
|
|
|
if (mandatoryRepair)
|
|
{
|
|
mandatoryEdgeIds.Add(edgeId);
|
|
}
|
|
|
|
if (preferShortestRepair)
|
|
{
|
|
preferredShortestEdgeIds.Add(edgeId);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (attempt == 1 && retryState.RequiresLengthRetry && !retryState.RequiresBlockingRetry)
|
|
{
|
|
if (retryState.TargetApproachBacktrackingViolations > 0)
|
|
{
|
|
var backtrackingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(edges, nodes, backtrackingSeverity, 2_250);
|
|
MergeSeverity(backtrackingSeverity, "approach-backtracking", preferShortestRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.ExcessiveDetourViolations > 0)
|
|
{
|
|
var detourSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountExcessiveDetourViolations(edges, nodes, detourSeverity, 2_000);
|
|
MergeSeverity(detourSeverity, "detour-priority", preferShortestRepair: true, mandatoryRepair: true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (score.NodeCrossings > 0)
|
|
{
|
|
var nodeCrossingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountEdgeNodeCrossings(edges, nodes, nodeCrossingSeverity, 2_500);
|
|
MergeSeverity(nodeCrossingSeverity, "node-crossings", requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.RemainingShortHighways > 0)
|
|
{
|
|
var brokenHighways = ElkEdgeRouterHighway.DetectRemainingBrokenHighways(edges, nodes);
|
|
AddEdgeIds(
|
|
brokenHighways.SelectMany(highway => highway.EdgeIds).Distinct(StringComparer.Ordinal),
|
|
2_000,
|
|
"short-highways",
|
|
requiresRouteRepair: true,
|
|
mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.RepeatCollectorCorridorViolations > 0)
|
|
{
|
|
var collectorSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountRepeatCollectorCorridorViolations(edges, nodes, collectorSeverity, 2_000);
|
|
MergeSeverity(collectorSeverity, "collector-corridors", requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.RepeatCollectorNodeClearanceViolations > 0)
|
|
{
|
|
var collectorClearanceSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountRepeatCollectorNodeClearanceViolations(edges, nodes, collectorClearanceSeverity, 2_000);
|
|
MergeSeverity(collectorClearanceSeverity, "collector-clearance", requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.TargetApproachJoinViolations > 0)
|
|
{
|
|
var targetJoinSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(edges, nodes, targetJoinSeverity, 1_500);
|
|
MergeSeverity(targetJoinSeverity, "target-joins", requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.TargetApproachBacktrackingViolations > 0)
|
|
{
|
|
var backtrackingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(edges, nodes, backtrackingSeverity, 1_600);
|
|
MergeSeverity(backtrackingSeverity, "approach-backtracking", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.BelowGraphViolations > 0)
|
|
{
|
|
var belowGraphSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountBelowGraphViolations(edges, nodes, belowGraphSeverity, 2_500);
|
|
MergeSeverity(belowGraphSeverity, "below-graph", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.UnderNodeViolations > 0)
|
|
{
|
|
var underNodeSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountUnderNodeViolations(edges, nodes, underNodeSeverity, 2_500);
|
|
MergeSeverity(underNodeSeverity, "under-node", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.LongDiagonalViolations > 0)
|
|
{
|
|
var longDiagonalSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountLongDiagonalViolations(edges, nodes, longDiagonalSeverity, 2_250);
|
|
MergeSeverity(longDiagonalSeverity, "long-diagonal", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.ExcessiveDetourViolations > 0)
|
|
{
|
|
var detourSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountExcessiveDetourViolations(edges, nodes, detourSeverity, 1_250);
|
|
MergeSeverity(detourSeverity, "detour", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.SharedLaneViolations > 0)
|
|
{
|
|
var sharedLaneSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountSharedLaneViolations(edges, nodes, sharedLaneSeverity, 2_000);
|
|
MergeSeverity(sharedLaneSeverity, "shared-lanes", requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.BoundarySlotViolations > 0)
|
|
{
|
|
var boundarySlotSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes, boundarySlotSeverity, 2_000);
|
|
MergeSeverity(boundarySlotSeverity, "boundary-slots", requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (!prioritizeBlockingAndLengthOnly && retryState.ProximityViolations > 0)
|
|
{
|
|
var proximitySeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountProximityViolations(edges, nodes, proximitySeverity, 350);
|
|
MergeSeverity(proximitySeverity, "proximity", requiresRouteRepair: true);
|
|
}
|
|
|
|
if (retryState.EntryAngleViolations > 0)
|
|
{
|
|
var entrySeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountBadBoundaryAngles(edges, nodes, entrySeverity, 450);
|
|
MergeSeverity(entrySeverity, "entry", requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (retryState.GatewaySourceExitViolations > 0)
|
|
{
|
|
var gatewaySourceSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(edges, nodes, gatewaySourceSeverity, 2_500);
|
|
MergeSeverity(gatewaySourceSeverity, "gateway-source-exit", preferShortestRepair: true, requiresRouteRepair: true, mandatoryRepair: true);
|
|
}
|
|
|
|
if (!prioritizeBlockingAndLengthOnly && retryState.LabelProximityViolations > 0)
|
|
{
|
|
var labelSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountLabelProximityViolations(edges, nodes, labelSeverity, 300);
|
|
MergeSeverity(labelSeverity, "label", requiresRouteRepair: true);
|
|
}
|
|
|
|
if (!prioritizeBlockingAndLengthOnly && retryState.EdgeCrossings > 0)
|
|
{
|
|
var edgeCrossingSeverity = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
ElkEdgeRoutingScoring.CountEdgeEdgeCrossings(edges, edgeCrossingSeverity, 200);
|
|
MergeSeverity(edgeCrossingSeverity, "edge-crossings", requiresRouteRepair: true);
|
|
}
|
|
}
|
|
|
|
if (severityByEdgeId.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var orderRankByEdgeId = strategy.EdgeOrder
|
|
.Select((edgeIndex, rank) => new { edgeIndex, rank })
|
|
.Where(item => item.edgeIndex >= 0 && item.edgeIndex < edges.Length)
|
|
.ToDictionary(item => edges[item.edgeIndex].Id, item => item.rank, StringComparer.Ordinal);
|
|
var seedSelectedEdgeIds = new List<string>();
|
|
foreach (var reason in reasons)
|
|
{
|
|
if (!severityByReason.TryGetValue(reason, out var reasonSeverity)
|
|
|| reasonSeverity.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var picksForReason = reason is "detour" or "detour-priority" or "approach-backtracking"
|
|
? 2
|
|
: 1;
|
|
var rankedReasonEdgeIds = reasonSeverity
|
|
.OrderByDescending(pair => pair.Value)
|
|
.ThenBy(pair => orderRankByEdgeId.GetValueOrDefault(pair.Key, int.MaxValue))
|
|
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
|
|
.Select(pair => pair.Key)
|
|
.ToArray();
|
|
foreach (var edgeId in RotateOrderedEdgeIds(rankedReasonEdgeIds, attempt))
|
|
{
|
|
if (seedSelectedEdgeIds.Contains(edgeId, StringComparer.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
seedSelectedEdgeIds.Add(edgeId);
|
|
if (--picksForReason == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var maxEdgeRepairs = DetermineRepairEdgeBudget(
|
|
retryState,
|
|
attempt == 1 && retryState.RequiresLengthRetry,
|
|
seedSelectedEdgeIds.Count,
|
|
mandatoryEdgeIds.Count);
|
|
var orderedEdgeIds = severityByEdgeId
|
|
.OrderByDescending(pair => pair.Value)
|
|
.ThenBy(pair => orderRankByEdgeId.GetValueOrDefault(pair.Key, int.MaxValue))
|
|
.ThenBy(pair => pair.Key, StringComparer.Ordinal)
|
|
.Select(pair => pair.Key)
|
|
.ToArray();
|
|
var prioritizedShortestEdgeIds = orderedEdgeIds
|
|
.Where(preferredShortestEdgeIds.Contains)
|
|
.Take(Math.Min(Math.Max(2, seedSelectedEdgeIds.Count), maxEdgeRepairs))
|
|
.ToArray();
|
|
var orderedMandatoryEdgeIds = orderedEdgeIds
|
|
.Where(mandatoryEdgeIds.Contains)
|
|
.ToArray();
|
|
var mandatoryFocusBudget = DetermineMandatoryFocusBudget(
|
|
orderedMandatoryEdgeIds.Length,
|
|
maxEdgeRepairs,
|
|
seedSelectedEdgeIds.Count);
|
|
var focusedMandatoryEdgeIds = RotateOrderedEdgeIds(orderedMandatoryEdgeIds, attempt)
|
|
.Take(mandatoryFocusBudget)
|
|
.ToArray();
|
|
var effectiveEdgeRepairBudget = Math.Max(maxEdgeRepairs, focusedMandatoryEdgeIds.Length);
|
|
var selectedEdgeIds = focusedMandatoryEdgeIds
|
|
.Concat(seedSelectedEdgeIds)
|
|
.Concat(prioritizedShortestEdgeIds)
|
|
.Distinct(StringComparer.Ordinal)
|
|
.Take(effectiveEdgeRepairBudget)
|
|
.ToArray();
|
|
if (reasons.Contains("collector-corridors", StringComparer.Ordinal))
|
|
{
|
|
selectedEdgeIds = ExpandRepeatCollectorRepairSet(selectedEdgeIds, edges, nodes);
|
|
}
|
|
|
|
if (reasons.Contains("target-joins", StringComparer.Ordinal))
|
|
{
|
|
selectedEdgeIds = ExpandTargetApproachJoinRepairSet(selectedEdgeIds, edges, nodes, strategy.MinLineClearance);
|
|
}
|
|
|
|
if (reasons.Contains("under-node", StringComparer.Ordinal))
|
|
{
|
|
selectedEdgeIds = ExpandUnderNodeRepairSet(selectedEdgeIds, edges, nodes);
|
|
}
|
|
|
|
if (reasons.Contains("shared-lanes", StringComparer.Ordinal))
|
|
{
|
|
selectedEdgeIds = ExpandSharedLaneRepairSet(selectedEdgeIds, edges, nodes);
|
|
}
|
|
|
|
if (selectedEdgeIds.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var preferredSelectedEdgeIds = selectedEdgeIds
|
|
.Where(preferredShortestEdgeIds.Contains)
|
|
.ToArray();
|
|
var routeRepairSelectedEdgeIds = selectedEdgeIds
|
|
.Where(routeRepairEdgeIds.Contains)
|
|
.ToArray();
|
|
|
|
var edgeIndices = selectedEdgeIds
|
|
.Select(edgeId => Array.FindIndex(edges, edge => string.Equals(edge.Id, edgeId, StringComparison.Ordinal)))
|
|
.Where(edgeIndex => edgeIndex >= 0)
|
|
.ToArray();
|
|
if (edgeIndices.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new RepairPlan(
|
|
edgeIndices,
|
|
selectedEdgeIds,
|
|
preferredSelectedEdgeIds,
|
|
routeRepairSelectedEdgeIds,
|
|
reasons.ToArray());
|
|
}
|
|
|
|
}
|