443 lines
18 KiB
C#
443 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Threading;
|
|
using MultiWheelC.TrajectoryPlanning.CoarsePath;
|
|
using MultiWheelC.TrajectoryPlanning.Mapping;
|
|
using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing;
|
|
using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation;
|
|
|
|
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Algorithms;
|
|
|
|
/// <summary>在有限强度计划内运行单一算法,并以共享分析和安全复核决定是否接受候选。</summary>
|
|
internal sealed class SmoothingAlgorithmRunner
|
|
{
|
|
private static readonly double[] RetryStrengthScales = { 1d, 0.75d, 0.50d, 0.25d };
|
|
private readonly PathGeometryAnalyzer _analyzer;
|
|
private readonly SmoothedPathValidator _validator;
|
|
|
|
internal SmoothingAlgorithmRunner()
|
|
: this(new PathGeometryAnalyzer(), new SmoothedPathValidator())
|
|
{
|
|
}
|
|
|
|
internal SmoothingAlgorithmRunner(PathGeometryAnalyzer analyzer, SmoothedPathValidator validator)
|
|
{
|
|
_analyzer = analyzer ?? throw new ArgumentNullException(nameof(analyzer));
|
|
_validator = validator ?? throw new ArgumentNullException(nameof(validator));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 依次尝试配置的有限强度比例。取消直接向上传播,由门面转换为最终状态;
|
|
/// 只有算法明确标记为可重试的不可行性才会使用较低强度;终止失败和统一复核失败均不重试。
|
|
/// </summary>
|
|
internal AlgorithmRunResult Run(
|
|
IPathSmoother smoother,
|
|
SmoothingAlgorithmInput input,
|
|
PathSmoothingConfiguration configuration,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var attemptedStrengths = new List<double>();
|
|
var failureReasons = new List<string>();
|
|
if (smoother == null || input == null || input.Options == null || configuration == null)
|
|
return AlgorithmRunResult.Failed("平滑算法、输入或配置无效。", attemptedStrengths, failureReasons);
|
|
|
|
foreach (double scale in RetryStrengthScales)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
double effectiveStrength = configuration.SmoothingStrength * scale;
|
|
if (!IsPositiveFinite(effectiveStrength))
|
|
return AlgorithmRunResult.Failed("平滑强度或重试比例无效。", attemptedStrengths, failureReasons);
|
|
|
|
attemptedStrengths.Add(effectiveStrength);
|
|
SmoothingCandidate candidate = smoother.Smooth(input, effectiveStrength, cancellationToken);
|
|
if (candidate == null)
|
|
return AlgorithmRunResult.Failed("平滑算法未返回候选。", attemptedStrengths, failureReasons);
|
|
if (candidate.Status == SmoothingCandidateStatus.RetryableInfeasible)
|
|
{
|
|
failureReasons.Add(candidate.Reason);
|
|
continue;
|
|
}
|
|
if (candidate.Status == SmoothingCandidateStatus.Failed)
|
|
{
|
|
failureReasons.Add(candidate.Reason);
|
|
return AlgorithmRunResult.Failed(candidate.Reason, attemptedStrengths, failureReasons);
|
|
}
|
|
if (candidate.Status != SmoothingCandidateStatus.Success)
|
|
{
|
|
const string unknownStatusReason = "平滑算法返回未知候选状态。";
|
|
failureReasons.Add(unknownStatusReason);
|
|
return AlgorithmRunResult.Failed(unknownStatusReason, attemptedStrengths, failureReasons);
|
|
}
|
|
|
|
if (!_analyzer.TryAnalyze(candidate.Segments, configuration.OutputSpacingMeters,
|
|
out PathGeometryAnalysis analysis, out string reason))
|
|
{
|
|
failureReasons.Add(reason);
|
|
return AlgorithmRunResult.Failed(reason, attemptedStrengths, failureReasons);
|
|
}
|
|
|
|
if (_validator.TryValidate(analysis.Path, analysis.Segments, input.OriginalPath, input.Map, input.Vehicle,
|
|
input.MaximumCollisionCheckStepMeters, out IReadOnlyList<SmoothedPathPoint> safePath,
|
|
out double minimumClearanceMeters, out reason))
|
|
{
|
|
return AlgorithmRunResult.Success(safePath, analysis.Segments,
|
|
CreateMetrics(analysis, minimumClearanceMeters), effectiveStrength,
|
|
attemptedStrengths, failureReasons);
|
|
}
|
|
|
|
failureReasons.Add(reason);
|
|
return AlgorithmRunResult.Failed(reason, attemptedStrengths, failureReasons);
|
|
}
|
|
|
|
return AlgorithmRunResult.Infeasible(null, attemptedStrengths, failureReasons);
|
|
}
|
|
|
|
private static PathQualityMetrics CreateMetrics(PathGeometryAnalysis analysis, double minimumClearanceMeters)
|
|
{
|
|
return new PathQualityMetrics(
|
|
true,
|
|
analysis.PathLengthMeters,
|
|
analysis.MaximumAbsoluteVehicleCurvaturePerMeter,
|
|
analysis.RootMeanSquareVehicleCurvaturePerMeter,
|
|
analysis.TotalAbsoluteCurvatureVariationPerMeter,
|
|
analysis.CurvatureVariationEnergy,
|
|
minimumClearanceMeters,
|
|
0d,
|
|
0d,
|
|
0d,
|
|
0d);
|
|
}
|
|
|
|
private static bool IsPositiveFinite(double value)
|
|
{
|
|
return !double.IsNaN(value) && !double.IsInfinity(value) && value > 0d;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reflection-only deterministic coverage seam. It is nested in an internal runner and intentionally
|
|
/// does not construct or register a production smoothing method.
|
|
/// </summary>
|
|
public static class TestHooks
|
|
{
|
|
/// <summary>执行一个固定的内部假平滑器场景并返回可反射读取的快照。</summary>
|
|
public static RunnerTestSnapshot Execute(string scenario)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(scenario)) throw new ArgumentException("A scenario is required.", nameof(scenario));
|
|
|
|
var cancellationSource = new CancellationTokenSource();
|
|
var smoother = new DeterministicTestSmoother(ParseScenario(scenario), cancellationSource);
|
|
var runner = new SmoothingAlgorithmRunner();
|
|
SmoothingAlgorithmInput input = CreateTestInput();
|
|
var configuration = new PathSmoothingConfiguration();
|
|
try
|
|
{
|
|
AlgorithmRunResult result = runner.Run(smoother, input, configuration, cancellationSource.Token);
|
|
return RunnerTestSnapshot.FromResult(result, false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return new RunnerTestSnapshot(
|
|
"OperationCanceledException",
|
|
smoother.AttemptedStrengths,
|
|
0,
|
|
0,
|
|
0,
|
|
true);
|
|
}
|
|
finally
|
|
{
|
|
cancellationSource.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>供 PowerShell 断言使用的不可变执行摘要。</summary>
|
|
public sealed class RunnerTestSnapshot
|
|
{
|
|
internal RunnerTestSnapshot(
|
|
string status,
|
|
IReadOnlyList<double> attemptedStrengths,
|
|
int acceptedPathPointCount,
|
|
int rejectedComparisonCandidatePointCount,
|
|
int failureCount,
|
|
bool cancellationPropagated)
|
|
{
|
|
Status = status ?? string.Empty;
|
|
AttemptedStrengths = CopyReadOnly(attemptedStrengths);
|
|
AcceptedPathPointCount = acceptedPathPointCount;
|
|
RejectedComparisonCandidatePointCount = rejectedComparisonCandidatePointCount;
|
|
FailureCount = failureCount;
|
|
CancellationPropagated = cancellationPropagated;
|
|
}
|
|
|
|
public string Status { get; }
|
|
public IReadOnlyList<double> AttemptedStrengths { get; }
|
|
public int AcceptedPathPointCount { get; }
|
|
public int RejectedComparisonCandidatePointCount { get; }
|
|
public int FailureCount { get; }
|
|
public bool CancellationPropagated { get; }
|
|
|
|
internal static RunnerTestSnapshot FromResult(AlgorithmRunResult result, bool cancellationPropagated)
|
|
{
|
|
int rejectedPointCount = result.RejectedComparisonCandidate == null
|
|
? 0
|
|
: CountPoints(result.RejectedComparisonCandidate.Segments);
|
|
return new RunnerTestSnapshot(
|
|
result.Status.ToString(),
|
|
result.AttemptedStrengths,
|
|
result.Path.Count,
|
|
rejectedPointCount,
|
|
result.FailureReasons.Count,
|
|
cancellationPropagated);
|
|
}
|
|
|
|
private static IReadOnlyList<T> CopyReadOnly<T>(IReadOnlyList<T> source)
|
|
{
|
|
var copy = new List<T>(source == null ? 0 : source.Count);
|
|
if (source != null)
|
|
{
|
|
for (int index = 0; index < source.Count; index++) copy.Add(source[index]);
|
|
}
|
|
return new ReadOnlyCollection<T>(copy);
|
|
}
|
|
}
|
|
|
|
private enum TestScenario
|
|
{
|
|
RetryableInfeasible,
|
|
AcceptFirst,
|
|
TerminalFailed,
|
|
CancelBeforeNextAttempt,
|
|
}
|
|
|
|
private sealed class DeterministicTestSmoother : IPathSmoother
|
|
{
|
|
private readonly TestScenario _scenario;
|
|
private readonly CancellationTokenSource _cancellationSource;
|
|
|
|
internal DeterministicTestSmoother(TestScenario scenario, CancellationTokenSource cancellationSource)
|
|
{
|
|
_scenario = scenario;
|
|
_cancellationSource = cancellationSource;
|
|
AttemptedStrengths = new List<double>();
|
|
}
|
|
|
|
public SmoothingMethod Method => SmoothingMethod.CubicBSpline;
|
|
|
|
internal List<double> AttemptedStrengths { get; }
|
|
|
|
public SmoothingCandidate Smooth(
|
|
SmoothingAlgorithmInput input,
|
|
double effectiveStrength,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
AttemptedStrengths.Add(effectiveStrength);
|
|
if (_scenario == TestScenario.TerminalFailed)
|
|
return SmoothingCandidate.Failed("确定性数值退化。");
|
|
if (_scenario == TestScenario.AcceptFirst)
|
|
return CreateAcceptedCandidate();
|
|
|
|
if (_scenario == TestScenario.CancelBeforeNextAttempt)
|
|
_cancellationSource.Cancel();
|
|
return SmoothingCandidate.RetryableInfeasible("确定性可重试不可行。" );
|
|
}
|
|
}
|
|
|
|
private static TestScenario ParseScenario(string scenario)
|
|
{
|
|
if (string.Equals(scenario, nameof(TestScenario.RetryableInfeasible), StringComparison.Ordinal)) return TestScenario.RetryableInfeasible;
|
|
if (string.Equals(scenario, nameof(TestScenario.AcceptFirst), StringComparison.Ordinal)) return TestScenario.AcceptFirst;
|
|
if (string.Equals(scenario, nameof(TestScenario.TerminalFailed), StringComparison.Ordinal)) return TestScenario.TerminalFailed;
|
|
if (string.Equals(scenario, nameof(TestScenario.CancelBeforeNextAttempt), StringComparison.Ordinal)) return TestScenario.CancelBeforeNextAttempt;
|
|
throw new ArgumentOutOfRangeException(nameof(scenario));
|
|
}
|
|
|
|
private static SmoothingAlgorithmInput CreateTestInput()
|
|
{
|
|
var mapRequest = new PlanningMapRequest
|
|
{
|
|
Bounds = new MapBoundsMm(0f, 5000f, 0f, 5000f),
|
|
ResolutionMm = 50f,
|
|
AllowExplicitEmptyMap = true,
|
|
};
|
|
PlanningMapBuildResult mapResult = new PlanningMapFactory().Create(mapRequest);
|
|
if (!mapResult.Succeeded || mapResult.Map == null)
|
|
throw new InvalidOperationException("The runner test hook could not create its empty map.");
|
|
|
|
var originalSegments = new List<PreparedDirectionSegment>
|
|
{
|
|
new PreparedDirectionSegment(
|
|
0,
|
|
TravelDirection.Forward,
|
|
new List<SmoothingPoint2D>
|
|
{
|
|
CreatePoint(0.5d, 0.5d, 0d),
|
|
CreatePoint(1.5d, 0.5d, 1d),
|
|
},
|
|
false,
|
|
false),
|
|
};
|
|
var vehicle = new VehicleParameters
|
|
{
|
|
LengthMeters = 0.20d,
|
|
WidthMeters = 0.20d,
|
|
SafetyMarginMeters = 0d,
|
|
MaximumCurvaturePerMeter = 100d,
|
|
MinimumTurningRadiusMeters = 0.01d,
|
|
};
|
|
return new SmoothingAlgorithmInput(
|
|
new PreparedPath(originalSegments),
|
|
mapResult.Map,
|
|
vehicle,
|
|
0.05d,
|
|
0.02d,
|
|
new SmoothingOptionsSnapshot(new PathSmoothingConfiguration()));
|
|
}
|
|
|
|
private static SmoothingCandidate CreateAcceptedCandidate()
|
|
{
|
|
return SmoothingCandidate.Success(new List<PreparedDirectionSegment>
|
|
{
|
|
new PreparedDirectionSegment(
|
|
0,
|
|
TravelDirection.Forward,
|
|
new List<SmoothingPoint2D>
|
|
{
|
|
CreatePoint(0.5d, 0.5d, 0d),
|
|
CreatePoint(1.5d, 0.5d, 1d),
|
|
},
|
|
false,
|
|
false),
|
|
});
|
|
}
|
|
|
|
private static SmoothingPoint2D CreatePoint(double x, double y, double arcLength)
|
|
{
|
|
return new SmoothingPoint2D(
|
|
x,
|
|
y,
|
|
arcLength,
|
|
0d,
|
|
0d,
|
|
1d,
|
|
false,
|
|
SmoothedPathPointSource.Anchor);
|
|
}
|
|
|
|
private static int CountPoints(IReadOnlyList<PreparedDirectionSegment> segments)
|
|
{
|
|
int count = 0;
|
|
for (int index = 0; index < segments.Count; index++) count += segments[index].Points.Count;
|
|
return count;
|
|
}
|
|
}
|
|
|
|
/// <summary>内部运行结果;只有成功路径可被正式门面发布,拒绝候选仅供比较诊断读取。</summary>
|
|
internal sealed class AlgorithmRunResult
|
|
{
|
|
private static readonly IReadOnlyList<SmoothedPathPoint> EmptyPath =
|
|
new ReadOnlyCollection<SmoothedPathPoint>(new List<SmoothedPathPoint>());
|
|
private static readonly IReadOnlyList<SmoothedPathSegment> EmptySegments =
|
|
new ReadOnlyCollection<SmoothedPathSegment>(new List<SmoothedPathSegment>());
|
|
|
|
private AlgorithmRunResult(
|
|
PathSmoothingStatus status,
|
|
IReadOnlyList<SmoothedPathPoint> path,
|
|
IReadOnlyList<SmoothedPathSegment> segments,
|
|
PathQualityMetrics metrics,
|
|
double acceptedStrength,
|
|
SmoothingCandidate rejectedComparisonCandidate,
|
|
IReadOnlyList<double> attemptedStrengths,
|
|
IReadOnlyList<string> failureReasons,
|
|
string reason)
|
|
{
|
|
Status = status;
|
|
Path = path ?? EmptyPath;
|
|
Segments = segments ?? EmptySegments;
|
|
Metrics = metrics ?? new PathQualityMetrics();
|
|
AcceptedStrength = acceptedStrength;
|
|
RejectedComparisonCandidate = rejectedComparisonCandidate;
|
|
AttemptedStrengths = CopyReadOnly(attemptedStrengths);
|
|
FailureReasons = CopyReadOnly(failureReasons);
|
|
Reason = reason ?? string.Empty;
|
|
}
|
|
|
|
internal PathSmoothingStatus Status { get; }
|
|
internal IReadOnlyList<SmoothedPathPoint> Path { get; }
|
|
internal IReadOnlyList<SmoothedPathSegment> Segments { get; }
|
|
internal PathQualityMetrics Metrics { get; }
|
|
internal double AcceptedStrength { get; }
|
|
internal SmoothingCandidate RejectedComparisonCandidate { get; }
|
|
internal IReadOnlyList<double> AttemptedStrengths { get; }
|
|
internal IReadOnlyList<string> FailureReasons { get; }
|
|
internal string Reason { get; }
|
|
|
|
internal static AlgorithmRunResult Success(
|
|
IReadOnlyList<SmoothedPathPoint> path,
|
|
IReadOnlyList<SmoothedPathSegment> segments,
|
|
PathQualityMetrics metrics,
|
|
double acceptedStrength,
|
|
IReadOnlyList<double> attemptedStrengths,
|
|
IReadOnlyList<string> failureReasons)
|
|
{
|
|
return new AlgorithmRunResult(
|
|
PathSmoothingStatus.Success,
|
|
CopyReadOnly(path),
|
|
CopyReadOnly(segments),
|
|
metrics,
|
|
acceptedStrength,
|
|
null,
|
|
attemptedStrengths,
|
|
failureReasons,
|
|
string.Empty);
|
|
}
|
|
|
|
internal static AlgorithmRunResult Infeasible(
|
|
SmoothingCandidate rejectedComparisonCandidate,
|
|
IReadOnlyList<double> attemptedStrengths,
|
|
IReadOnlyList<string> failureReasons)
|
|
{
|
|
string reason = failureReasons == null || failureReasons.Count == 0
|
|
? "所有有限平滑尝试均未通过复核。"
|
|
: failureReasons[failureReasons.Count - 1];
|
|
return new AlgorithmRunResult(
|
|
PathSmoothingStatus.Infeasible,
|
|
null,
|
|
null,
|
|
null,
|
|
0d,
|
|
rejectedComparisonCandidate,
|
|
attemptedStrengths,
|
|
failureReasons,
|
|
reason);
|
|
}
|
|
|
|
internal static AlgorithmRunResult Failed(
|
|
string reason,
|
|
IReadOnlyList<double> attemptedStrengths,
|
|
IReadOnlyList<string> failureReasons)
|
|
{
|
|
return new AlgorithmRunResult(
|
|
PathSmoothingStatus.Failed,
|
|
null,
|
|
null,
|
|
null,
|
|
0d,
|
|
null,
|
|
attemptedStrengths,
|
|
failureReasons,
|
|
reason);
|
|
}
|
|
|
|
private static IReadOnlyList<T> CopyReadOnly<T>(IReadOnlyList<T> source)
|
|
{
|
|
var copy = new List<T>(source == null ? 0 : source.Count);
|
|
if (source != null)
|
|
{
|
|
for (int index = 0; index < source.Count; index++) copy.Add(source[index]);
|
|
}
|
|
return new ReadOnlyCollection<T>(copy);
|
|
}
|
|
}
|
|
}
|