Refactor ElkSharp hybrid routing and document speed path
This commit is contained in:
@@ -0,0 +1,644 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
private static StrategyEvaluationResult EvaluateStrategy(
|
||||
StrategyWorkItem workItem,
|
||||
ElkRoutedEdge[] baselineEdges,
|
||||
ElkPositionedNode[] nodes,
|
||||
ElkLayoutOptions layoutOptions,
|
||||
IterativeRoutingConfig config,
|
||||
CancellationToken cancellationToken,
|
||||
ElkLayoutRunDiagnostics? diagnostics)
|
||||
{
|
||||
using var diagnosticsScope = diagnostics is null
|
||||
? null
|
||||
: ElkLayoutDiagnostics.Attach(diagnostics);
|
||||
|
||||
const int maxAllowedNodeCrossings = 0;
|
||||
|
||||
var strategy = workItem.Strategy;
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] start: bend={strategy.RoutingParams.BendPenalty:F0} " +
|
||||
$"diag={strategy.RoutingParams.DiagonalPenalty:F0} soft={strategy.RoutingParams.SoftObstacleWeight:F2} " +
|
||||
$"clearance={strategy.MinLineClearance:F1}");
|
||||
|
||||
var bestAttemptScore = (EdgeRoutingScore?)null;
|
||||
ElkRoutedEdge[]? bestAttemptEdges = null;
|
||||
var bestAttemptRetryState = new RoutingRetryState(
|
||||
RemainingShortHighways: int.MaxValue,
|
||||
RepeatCollectorCorridorViolations: int.MaxValue,
|
||||
RepeatCollectorNodeClearanceViolations: int.MaxValue,
|
||||
TargetApproachJoinViolations: int.MaxValue,
|
||||
TargetApproachBacktrackingViolations: int.MaxValue,
|
||||
ExcessiveDetourViolations: int.MaxValue,
|
||||
SharedLaneViolations: int.MaxValue,
|
||||
BoundarySlotViolations: int.MaxValue,
|
||||
BelowGraphViolations: int.MaxValue,
|
||||
UnderNodeViolations: int.MaxValue,
|
||||
LongDiagonalViolations: int.MaxValue,
|
||||
ProximityViolations: int.MaxValue,
|
||||
EntryAngleViolations: int.MaxValue,
|
||||
GatewaySourceExitViolations: int.MaxValue,
|
||||
LabelProximityViolations: int.MaxValue,
|
||||
EdgeCrossings: int.MaxValue);
|
||||
var repairSeedScore = (EdgeRoutingScore?)null;
|
||||
ElkRoutedEdge[]? repairSeedEdges = null;
|
||||
var repairSeedRetryState = new RoutingRetryState(
|
||||
RemainingShortHighways: int.MaxValue,
|
||||
RepeatCollectorCorridorViolations: int.MaxValue,
|
||||
RepeatCollectorNodeClearanceViolations: int.MaxValue,
|
||||
TargetApproachJoinViolations: int.MaxValue,
|
||||
TargetApproachBacktrackingViolations: int.MaxValue,
|
||||
ExcessiveDetourViolations: int.MaxValue,
|
||||
SharedLaneViolations: int.MaxValue,
|
||||
BoundarySlotViolations: int.MaxValue,
|
||||
BelowGraphViolations: int.MaxValue,
|
||||
UnderNodeViolations: int.MaxValue,
|
||||
LongDiagonalViolations: int.MaxValue,
|
||||
ProximityViolations: int.MaxValue,
|
||||
EntryAngleViolations: int.MaxValue,
|
||||
GatewaySourceExitViolations: int.MaxValue,
|
||||
LabelProximityViolations: int.MaxValue,
|
||||
EdgeCrossings: int.MaxValue);
|
||||
var attemptDetails = new List<ElkIterativeAttemptDiagnostics>();
|
||||
var fallbackSolutions = new List<CandidateSolution>();
|
||||
CandidateSolution? validSolution = null;
|
||||
var outcome = "no-valid";
|
||||
var attempts = 0;
|
||||
var maxAttempts = config.MaxAdaptationsPerStrategy;
|
||||
var stagnantAttempts = 0;
|
||||
string? lastPlateauFingerprint = null;
|
||||
var repeatedPlateauFingerprintCount = 0;
|
||||
var recentBlockingCycleFingerprints = new List<string>(4);
|
||||
string? lastPlannedRepairFocusFingerprint = null;
|
||||
var repeatedPlannedRepairFocusCount = 0;
|
||||
string? lastRepairFocusFingerprint = null;
|
||||
var repeatedRepairFocusCount = 0;
|
||||
var hasLastAttemptState = false;
|
||||
var lastAttemptRetryState = new RoutingRetryState(
|
||||
RemainingShortHighways: int.MaxValue,
|
||||
RepeatCollectorCorridorViolations: int.MaxValue,
|
||||
RepeatCollectorNodeClearanceViolations: int.MaxValue,
|
||||
TargetApproachJoinViolations: int.MaxValue,
|
||||
TargetApproachBacktrackingViolations: int.MaxValue,
|
||||
ExcessiveDetourViolations: int.MaxValue,
|
||||
SharedLaneViolations: int.MaxValue,
|
||||
BoundarySlotViolations: int.MaxValue,
|
||||
BelowGraphViolations: int.MaxValue,
|
||||
UnderNodeViolations: int.MaxValue,
|
||||
LongDiagonalViolations: int.MaxValue,
|
||||
ProximityViolations: int.MaxValue,
|
||||
EntryAngleViolations: int.MaxValue,
|
||||
GatewaySourceExitViolations: int.MaxValue,
|
||||
LabelProximityViolations: int.MaxValue,
|
||||
EdgeCrossings: int.MaxValue);
|
||||
var lastAttemptNodeCrossings = int.MaxValue;
|
||||
var consecutiveNonImprovingAttempts = 0;
|
||||
var strategyStopwatch = Stopwatch.StartNew();
|
||||
ElkIterativeStrategyDiagnostics? liveStrategyDiagnostics = null;
|
||||
|
||||
if (diagnostics is not null)
|
||||
{
|
||||
liveStrategyDiagnostics = new ElkIterativeStrategyDiagnostics
|
||||
{
|
||||
StrategyIndex = workItem.StrategyIndex,
|
||||
OrderingName = workItem.StrategyName,
|
||||
Attempts = 0,
|
||||
TotalDurationMs = 0,
|
||||
BestScore = null,
|
||||
Outcome = "running",
|
||||
BendPenalty = workItem.Strategy.RoutingParams.BendPenalty,
|
||||
DiagonalPenalty = workItem.Strategy.RoutingParams.DiagonalPenalty,
|
||||
SoftObstacleWeight = workItem.Strategy.RoutingParams.SoftObstacleWeight,
|
||||
RegisteredLive = true,
|
||||
};
|
||||
lock (diagnostics.SyncRoot)
|
||||
{
|
||||
diagnostics.IterativeStrategies.Add(liveStrategyDiagnostics);
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
|
||||
}
|
||||
|
||||
for (var attempt = 0; attempt < maxAttempts; attempt++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
attempts++;
|
||||
var attemptStopwatch = Stopwatch.StartNew();
|
||||
var phaseTimings = new List<ElkIterativePhaseDiagnostics>();
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] attempt {attempt + 1} start");
|
||||
|
||||
T MeasurePhase<T>(string phaseName, Func<T> action)
|
||||
{
|
||||
var phaseStopwatch = Stopwatch.StartNew();
|
||||
var value = action();
|
||||
phaseStopwatch.Stop();
|
||||
phaseTimings.Add(new ElkIterativePhaseDiagnostics
|
||||
{
|
||||
Phase = phaseName,
|
||||
DurationMs = Math.Round(phaseStopwatch.Elapsed.TotalMilliseconds, 3),
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
RepairPlan? repairPlan = null;
|
||||
RouteAllEdgesResult? routeResult;
|
||||
if (attempt == 0 || repairSeedEdges is null || repairSeedScore is null)
|
||||
{
|
||||
routeResult = MeasurePhase(
|
||||
"route-all-edges",
|
||||
() => RouteAllEdges(baselineEdges, nodes, config.ObstacleMargin, strategy, cancellationToken));
|
||||
}
|
||||
else
|
||||
{
|
||||
repairPlan = MeasurePhase(
|
||||
"select-repair-targets",
|
||||
() => BuildRepairPlan(repairSeedEdges, nodes, repairSeedScore.Value, repairSeedRetryState, strategy, attempt));
|
||||
if (repairPlan is null)
|
||||
{
|
||||
outcome = $"no-repair-targets({DescribeRetryState(repairSeedRetryState)})@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
|
||||
break;
|
||||
}
|
||||
|
||||
var plannedRepairFocusFingerprint = BuildRepairFocusFingerprint(repairPlan);
|
||||
if (!string.IsNullOrEmpty(plannedRepairFocusFingerprint))
|
||||
{
|
||||
if (string.Equals(plannedRepairFocusFingerprint, lastPlannedRepairFocusFingerprint, StringComparison.Ordinal))
|
||||
{
|
||||
repeatedPlannedRepairFocusCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastPlannedRepairFocusFingerprint = plannedRepairFocusFingerprint;
|
||||
repeatedPlannedRepairFocusCount = 0;
|
||||
}
|
||||
|
||||
if (repeatedPlannedRepairFocusCount >= 1 && attempt >= 2)
|
||||
{
|
||||
outcome = $"stalled-same-repair-plan({DescribeRetryState(repairSeedRetryState)})@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lastPlannedRepairFocusFingerprint = null;
|
||||
repeatedPlannedRepairFocusCount = 0;
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] attempt {attempt + 1} local-repair " +
|
||||
$"edges=[{string.Join(", ", repairPlan.Value.EdgeIds)}] reasons=[{string.Join(", ", repairPlan.Value.Reasons)}]");
|
||||
routeResult = MeasurePhase(
|
||||
"route-penalized-edges",
|
||||
() => RepairPenalizedEdges(
|
||||
repairSeedEdges,
|
||||
nodes,
|
||||
config.ObstacleMargin,
|
||||
strategy,
|
||||
repairPlan.Value,
|
||||
cancellationToken,
|
||||
config.MaxParallelRepairBuilds));
|
||||
}
|
||||
if (routeResult is null)
|
||||
{
|
||||
outcome = "route-failed";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}@attempt{attempt + 1}");
|
||||
break;
|
||||
}
|
||||
|
||||
var candidateEdges = routeResult.Edges;
|
||||
var scopedCleanupEdgeIds = routeResult.Diagnostics?.Mode == "local-repair"
|
||||
? routeResult.Diagnostics.RepairedEdgeIds.ToArray()
|
||||
: null;
|
||||
if (scopedCleanupEdgeIds is { Length: > 0 })
|
||||
{
|
||||
candidateEdges = MeasurePhase(
|
||||
"targeted-terminal-rule-cleanup",
|
||||
() => ApplyTerminalRuleCleanupRound(
|
||||
candidateEdges,
|
||||
nodes,
|
||||
layoutOptions.Direction,
|
||||
strategy.MinLineClearance,
|
||||
scopedCleanupEdgeIds));
|
||||
}
|
||||
else
|
||||
{
|
||||
candidateEdges = MeasurePhase(
|
||||
"snap-anchors",
|
||||
() => ElkEdgePostProcessor.SnapAnchorsToNodeBoundary(candidateEdges, nodes));
|
||||
candidateEdges = MeasurePhase(
|
||||
"eliminate-diagonals",
|
||||
() => ElkEdgePostProcessor.EliminateDiagonalSegments(candidateEdges, nodes));
|
||||
candidateEdges = MeasurePhase(
|
||||
"avoid-node-crossings-1",
|
||||
() => ElkEdgePostProcessor.AvoidNodeCrossings(candidateEdges, nodes, layoutOptions.Direction));
|
||||
candidateEdges = MeasurePhase(
|
||||
"simplify-1",
|
||||
() => ElkEdgePostProcessorSimplify.SimplifyEdgePaths(candidateEdges, nodes));
|
||||
candidateEdges = MeasurePhase(
|
||||
"tighten-corridors",
|
||||
() => ElkEdgePostProcessorSimplify.TightenOuterCorridors(candidateEdges, nodes));
|
||||
if (HighwayProcessingEnabled)
|
||||
{
|
||||
candidateEdges = MeasurePhase(
|
||||
"break-short-highways",
|
||||
() => ElkEdgeRouterHighway.BreakShortHighways(candidateEdges, nodes));
|
||||
}
|
||||
|
||||
candidateEdges = MeasurePhase(
|
||||
"terminal-rule-cleanup",
|
||||
() => ApplyTerminalRuleCleanupRound(
|
||||
candidateEdges,
|
||||
nodes,
|
||||
layoutOptions.Direction,
|
||||
strategy.MinLineClearance));
|
||||
}
|
||||
|
||||
var score = MeasurePhase(
|
||||
"compute-score",
|
||||
() => ElkEdgeRoutingScoring.ComputeScore(candidateEdges, nodes));
|
||||
var remainingBrokenHighways = HighwayProcessingEnabled
|
||||
? MeasurePhase(
|
||||
"detect-broken-highways",
|
||||
() => ElkEdgeRouterHighway.DetectRemainingBrokenHighways(candidateEdges, nodes).Count)
|
||||
: 0;
|
||||
var retryState = BuildRetryState(score, remainingBrokenHighways);
|
||||
|
||||
if (retryState.RequiresBlockingRetry || retryState.RequiresLengthRetry)
|
||||
{
|
||||
var focusedRepair = MeasurePhase(
|
||||
"repair-verified-issues",
|
||||
() => TryApplyVerifiedIssueRepairRound(
|
||||
candidateEdges,
|
||||
nodes,
|
||||
config.ObstacleMargin,
|
||||
strategy,
|
||||
retryState,
|
||||
layoutOptions.Direction,
|
||||
cancellationToken,
|
||||
config.MaxParallelRepairBuilds));
|
||||
if (focusedRepair is { } repaired
|
||||
&& IsBetterBoundarySlotRepairCandidate(
|
||||
repaired.Score,
|
||||
repaired.RetryState,
|
||||
score,
|
||||
retryState))
|
||||
{
|
||||
candidateEdges = repaired.Edges;
|
||||
score = repaired.Score;
|
||||
remainingBrokenHighways = repaired.RemainingBrokenHighways;
|
||||
retryState = repaired.RetryState;
|
||||
}
|
||||
}
|
||||
|
||||
if (retryState.RequiresBlockingRetry || retryState.RequiresLengthRetry)
|
||||
{
|
||||
var stabilizedEdges = MeasurePhase(
|
||||
"stabilize-terminal-rules",
|
||||
() => ApplyTerminalRuleCleanupRound(
|
||||
candidateEdges,
|
||||
nodes,
|
||||
layoutOptions.Direction,
|
||||
strategy.MinLineClearance,
|
||||
scopedCleanupEdgeIds));
|
||||
var stabilizedScore = MeasurePhase(
|
||||
"compute-score-stabilized",
|
||||
() => ElkEdgeRoutingScoring.ComputeScore(stabilizedEdges, nodes));
|
||||
var stabilizedBrokenHighways = HighwayProcessingEnabled
|
||||
? MeasurePhase(
|
||||
"detect-broken-highways-stabilized",
|
||||
() => ElkEdgeRouterHighway.DetectRemainingBrokenHighways(stabilizedEdges, nodes).Count)
|
||||
: 0;
|
||||
var stabilizedRetryState = BuildRetryState(stabilizedScore, stabilizedBrokenHighways);
|
||||
if (IsBetterBoundarySlotRepairCandidate(
|
||||
stabilizedScore,
|
||||
stabilizedRetryState,
|
||||
score,
|
||||
retryState))
|
||||
{
|
||||
candidateEdges = stabilizedEdges;
|
||||
score = stabilizedScore;
|
||||
remainingBrokenHighways = stabilizedBrokenHighways;
|
||||
retryState = stabilizedRetryState;
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt == 0)
|
||||
{
|
||||
maxAttempts = DetermineAdaptiveAttemptBudget(retryState, config.MaxAdaptationsPerStrategy);
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] attempt-budget={maxAttempts}");
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] attempt {attempt + 1} " +
|
||||
$"score={score.Value:F0} retry={DescribeRetryState(retryState)}");
|
||||
var candidate = new CandidateSolution(score, retryState, candidateEdges, workItem.StrategyIndex);
|
||||
fallbackSolutions.Add(candidate);
|
||||
repairSeedScore = score;
|
||||
repairSeedEdges = candidateEdges;
|
||||
repairSeedRetryState = retryState;
|
||||
|
||||
var repairFocusFingerprint = BuildRepairFocusFingerprint(repairPlan);
|
||||
if (!string.IsNullOrEmpty(repairFocusFingerprint))
|
||||
{
|
||||
if (string.Equals(repairFocusFingerprint, lastRepairFocusFingerprint, StringComparison.Ordinal))
|
||||
{
|
||||
repeatedRepairFocusCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastRepairFocusFingerprint = repairFocusFingerprint;
|
||||
repeatedRepairFocusCount = 0;
|
||||
}
|
||||
|
||||
if (repeatedRepairFocusCount >= 1 && attempt >= 3)
|
||||
{
|
||||
outcome = $"stalled-same-focus({DescribeRetryState(retryState)})@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lastRepairFocusFingerprint = null;
|
||||
repeatedRepairFocusCount = 0;
|
||||
}
|
||||
|
||||
var plateauFingerprint = BuildPlateauFingerprint(retryState, repairPlan);
|
||||
if (string.Equals(plateauFingerprint, lastPlateauFingerprint, StringComparison.Ordinal))
|
||||
{
|
||||
repeatedPlateauFingerprintCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastPlateauFingerprint = plateauFingerprint;
|
||||
repeatedPlateauFingerprintCount = 0;
|
||||
}
|
||||
|
||||
if (repeatedPlateauFingerprintCount >= 2 && attempt >= 3)
|
||||
{
|
||||
outcome = $"stalled-repeat({DescribeRetryState(retryState)})@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
|
||||
break;
|
||||
}
|
||||
|
||||
var blockingCycleFingerprint = BuildBlockingCycleFingerprint(retryState, repairPlan);
|
||||
if (ShouldStopForBlockingCycle(
|
||||
recentBlockingCycleFingerprints,
|
||||
blockingCycleFingerprint,
|
||||
retryState,
|
||||
bestAttemptRetryState,
|
||||
attempt))
|
||||
{
|
||||
outcome = $"stalled-cycle({DescribeRetryState(retryState)})@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
|
||||
break;
|
||||
}
|
||||
|
||||
AppendRecentFingerprint(recentBlockingCycleFingerprints, blockingCycleFingerprint, 4);
|
||||
|
||||
if (hasLastAttemptState)
|
||||
{
|
||||
var retryStateComparison = CompareRetryStates(retryState, lastAttemptRetryState);
|
||||
var improvedBoundarySlotsSinceLast =
|
||||
retryState.BoundarySlotViolations < lastAttemptRetryState.BoundarySlotViolations
|
||||
&& score.NodeCrossings <= lastAttemptNodeCrossings
|
||||
&& !HasBlockingBoundarySlotPromotionRegression(retryState, lastAttemptRetryState);
|
||||
if (!improvedBoundarySlotsSinceLast
|
||||
&& retryStateComparison >= 0
|
||||
&& score.NodeCrossings >= lastAttemptNodeCrossings)
|
||||
{
|
||||
consecutiveNonImprovingAttempts++;
|
||||
}
|
||||
else
|
||||
{
|
||||
consecutiveNonImprovingAttempts = 0;
|
||||
}
|
||||
|
||||
if (consecutiveNonImprovingAttempts >= 2 && attempt >= 3)
|
||||
{
|
||||
outcome = $"stalled-no-progress({DescribeRetryState(retryState)})@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hasLastAttemptState = true;
|
||||
lastAttemptRetryState = retryState;
|
||||
lastAttemptNodeCrossings = score.NodeCrossings;
|
||||
|
||||
var improvedAttempt = bestAttemptScore is null
|
||||
|| IsBetterBoundarySlotRepairCandidate(
|
||||
score,
|
||||
retryState,
|
||||
bestAttemptScore.Value,
|
||||
bestAttemptRetryState);
|
||||
var improvedRuleState = bestAttemptScore is null
|
||||
|| (retryState.BoundarySlotViolations < bestAttemptRetryState.BoundarySlotViolations
|
||||
&& score.NodeCrossings <= bestAttemptScore.Value.NodeCrossings
|
||||
&& !HasBlockingBoundarySlotPromotionRegression(retryState, bestAttemptRetryState))
|
||||
|| CompareRetryStates(retryState, bestAttemptRetryState) < 0
|
||||
|| score.NodeCrossings < bestAttemptScore.Value.NodeCrossings;
|
||||
if (improvedAttempt)
|
||||
{
|
||||
bestAttemptScore = score;
|
||||
bestAttemptEdges = candidateEdges;
|
||||
bestAttemptRetryState = retryState;
|
||||
stagnantAttempts = improvedRuleState
|
||||
? 0
|
||||
: stagnantAttempts + 1;
|
||||
if (ShouldStopForStagnation(stagnantAttempts, attempt, config.MaxAdaptationsPerStrategy))
|
||||
{
|
||||
outcome = $"stalled({DescribeRetryState(retryState)})@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
stagnantAttempts++;
|
||||
if (ShouldStopForStagnation(stagnantAttempts, attempt, config.MaxAdaptationsPerStrategy))
|
||||
{
|
||||
outcome = $"stalled({DescribeRetryState(retryState)})@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var attemptOutcome = score.NodeCrossings > maxAllowedNodeCrossings
|
||||
? $"hard-violation(nc={score.NodeCrossings}>{maxAllowedNodeCrossings})"
|
||||
: retryState.RequiresPrimaryRetry
|
||||
? $"retry({DescribeRetryState(retryState)})"
|
||||
: ShouldRetryForEdgeCrossings(retryState, attempt, config.MaxAdaptationsPerStrategy)
|
||||
? $"retry(edge-crossings={retryState.EdgeCrossings})"
|
||||
: "valid";
|
||||
|
||||
if (diagnostics is not null)
|
||||
{
|
||||
attemptStopwatch.Stop();
|
||||
var attemptDiagnostics = new ElkIterativeAttemptDiagnostics
|
||||
{
|
||||
Attempt = attempt + 1,
|
||||
TotalDurationMs = Math.Round(attemptStopwatch.Elapsed.TotalMilliseconds, 3),
|
||||
Score = score,
|
||||
Outcome = attemptOutcome,
|
||||
RouteDiagnostics = routeResult.Diagnostics,
|
||||
Edges = candidateEdges,
|
||||
};
|
||||
attemptDiagnostics.PhaseTimings.AddRange(phaseTimings);
|
||||
attemptDetails.Add(attemptDiagnostics);
|
||||
|
||||
if (liveStrategyDiagnostics is not null)
|
||||
{
|
||||
lock (diagnostics.SyncRoot)
|
||||
{
|
||||
liveStrategyDiagnostics.Attempts = attempts;
|
||||
liveStrategyDiagnostics.TotalDurationMs = Math.Round(strategyStopwatch.Elapsed.TotalMilliseconds, 3);
|
||||
liveStrategyDiagnostics.BestScore = bestAttemptScore;
|
||||
liveStrategyDiagnostics.Outcome = attemptOutcome;
|
||||
liveStrategyDiagnostics.AttemptDetails.Add(attemptDiagnostics);
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
if (score.NodeCrossings > maxAllowedNodeCrossings)
|
||||
{
|
||||
outcome = $"{attemptOutcome}@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] adapting after node crossing violation");
|
||||
strategy.AdaptForViolations(score, attempt, retryState);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (retryState.RemainingShortHighways > 0
|
||||
|| retryState.RepeatCollectorCorridorViolations > 0
|
||||
|| retryState.RepeatCollectorNodeClearanceViolations > 0
|
||||
|| retryState.TargetApproachJoinViolations > 0
|
||||
|| retryState.TargetApproachBacktrackingViolations > 0
|
||||
|| retryState.SharedLaneViolations > 0
|
||||
|| retryState.BelowGraphViolations > 0
|
||||
|| retryState.UnderNodeViolations > 0
|
||||
|| retryState.LongDiagonalViolations > 0
|
||||
|| retryState.EntryAngleViolations > 0
|
||||
|| retryState.GatewaySourceExitViolations > 0)
|
||||
{
|
||||
if (ShouldRetryForPrimaryViolations(retryState, attempt, config.MaxAdaptationsPerStrategy))
|
||||
{
|
||||
outcome = $"{attemptOutcome}@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] adapting for blocking violations");
|
||||
strategy.AdaptForViolations(score, attempt, retryState);
|
||||
continue;
|
||||
}
|
||||
|
||||
outcome = $"invalid({DescribeRetryState(retryState)})@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (retryState.RequiresLengthRetry)
|
||||
{
|
||||
if (ShouldRetryForPrimaryViolations(retryState, attempt, config.MaxAdaptationsPerStrategy))
|
||||
{
|
||||
outcome = $"{attemptOutcome}@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] adapting for shortest-path / detour violations");
|
||||
strategy.AdaptForViolations(score, attempt, retryState);
|
||||
continue;
|
||||
}
|
||||
|
||||
outcome = $"invalid({DescribeRetryState(retryState)})@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (retryState.RequiresQualityRetry)
|
||||
{
|
||||
if (ShouldRetryForPrimaryViolations(retryState, attempt, config.MaxAdaptationsPerStrategy))
|
||||
{
|
||||
outcome = $"{attemptOutcome}@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] adapting for quality/length violations");
|
||||
strategy.AdaptForViolations(score, attempt, retryState);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (ShouldRetryForEdgeCrossings(retryState, attempt, config.MaxAdaptationsPerStrategy))
|
||||
{
|
||||
outcome = $"{attemptOutcome}@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] adapting for edge crossings");
|
||||
strategy.AdaptForViolations(score, attempt, retryState);
|
||||
continue;
|
||||
}
|
||||
|
||||
var residualSoftViolations = retryState.RequiresQualityRetry || retryState.EdgeCrossings > 0;
|
||||
outcome = residualSoftViolations
|
||||
? $"valid-soft({DescribeRetryState(retryState)})@attempt{attempt + 1}"
|
||||
: $"valid@attempt{attempt + 1}";
|
||||
ElkLayoutDiagnostics.LogProgress(
|
||||
$"Strategy {workItem.StrategyIndex} [{workItem.StrategyName}] {outcome}");
|
||||
validSolution = candidate;
|
||||
break;
|
||||
}
|
||||
|
||||
var stratDiag = new ElkIterativeStrategyDiagnostics
|
||||
{
|
||||
StrategyIndex = workItem.StrategyIndex,
|
||||
OrderingName = workItem.StrategyName,
|
||||
Attempts = attempts,
|
||||
TotalDurationMs = Math.Round(strategyStopwatch.Elapsed.TotalMilliseconds, 3),
|
||||
BestScore = bestAttemptScore,
|
||||
Outcome = outcome,
|
||||
BendPenalty = workItem.Strategy.RoutingParams.BendPenalty,
|
||||
DiagonalPenalty = workItem.Strategy.RoutingParams.DiagonalPenalty,
|
||||
SoftObstacleWeight = workItem.Strategy.RoutingParams.SoftObstacleWeight,
|
||||
BestEdges = bestAttemptEdges,
|
||||
};
|
||||
stratDiag.AttemptDetails.AddRange(attemptDetails);
|
||||
|
||||
if (liveStrategyDiagnostics is not null && diagnostics is not null)
|
||||
{
|
||||
lock (diagnostics.SyncRoot)
|
||||
{
|
||||
liveStrategyDiagnostics.Attempts = attempts;
|
||||
liveStrategyDiagnostics.TotalDurationMs = Math.Round(strategyStopwatch.Elapsed.TotalMilliseconds, 3);
|
||||
liveStrategyDiagnostics.BestScore = bestAttemptScore;
|
||||
liveStrategyDiagnostics.Outcome = outcome;
|
||||
liveStrategyDiagnostics.BestEdges = bestAttemptEdges;
|
||||
}
|
||||
|
||||
ElkLayoutDiagnostics.FlushSnapshot(diagnostics);
|
||||
}
|
||||
|
||||
return new StrategyEvaluationResult(
|
||||
workItem.StrategyIndex,
|
||||
fallbackSolutions,
|
||||
validSolution,
|
||||
stratDiag);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user