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>
580 lines
22 KiB
C#
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}";
|
|
}
|
|
}
|