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; /// 在有限强度计划内运行单一算法,并以共享分析和安全复核决定是否接受候选。 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)); } /// /// 依次尝试配置的有限强度比例。取消直接向上传播,由门面转换为最终状态; /// 只有算法明确标记为可重试的不可行性才会使用较低强度;终止失败和统一复核失败均不重试。 /// internal AlgorithmRunResult Run( IPathSmoother smoother, SmoothingAlgorithmInput input, PathSmoothingConfiguration configuration, CancellationToken cancellationToken) { var attemptedStrengths = new List(); var failureReasons = new List(); 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 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; } /// /// Reflection-only deterministic coverage seam. It is nested in an internal runner and intentionally /// does not construct or register a production smoothing method. /// public static class TestHooks { /// 执行一个固定的内部假平滑器场景并返回可反射读取的快照。 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(); } } /// 供 PowerShell 断言使用的不可变执行摘要。 public sealed class RunnerTestSnapshot { internal RunnerTestSnapshot( string status, IReadOnlyList 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 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 CopyReadOnly(IReadOnlyList source) { var copy = new List(source == null ? 0 : source.Count); if (source != null) { for (int index = 0; index < source.Count; index++) copy.Add(source[index]); } return new ReadOnlyCollection(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(); } public SmoothingMethod Method => SmoothingMethod.CubicBSpline; internal List 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 { new PreparedDirectionSegment( 0, TravelDirection.Forward, new List { 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 { new PreparedDirectionSegment( 0, TravelDirection.Forward, new List { 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 segments) { int count = 0; for (int index = 0; index < segments.Count; index++) count += segments[index].Points.Count; return count; } } /// 内部运行结果;只有成功路径可被正式门面发布,拒绝候选仅供比较诊断读取。 internal sealed class AlgorithmRunResult { private static readonly IReadOnlyList EmptyPath = new ReadOnlyCollection(new List()); private static readonly IReadOnlyList EmptySegments = new ReadOnlyCollection(new List()); private AlgorithmRunResult( PathSmoothingStatus status, IReadOnlyList path, IReadOnlyList segments, PathQualityMetrics metrics, double acceptedStrength, SmoothingCandidate rejectedComparisonCandidate, IReadOnlyList attemptedStrengths, IReadOnlyList 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 Path { get; } internal IReadOnlyList Segments { get; } internal PathQualityMetrics Metrics { get; } internal double AcceptedStrength { get; } internal SmoothingCandidate RejectedComparisonCandidate { get; } internal IReadOnlyList AttemptedStrengths { get; } internal IReadOnlyList FailureReasons { get; } internal string Reason { get; } internal static AlgorithmRunResult Success( IReadOnlyList path, IReadOnlyList segments, PathQualityMetrics metrics, double acceptedStrength, IReadOnlyList attemptedStrengths, IReadOnlyList failureReasons) { return new AlgorithmRunResult( PathSmoothingStatus.Success, CopyReadOnly(path), CopyReadOnly(segments), metrics, acceptedStrength, null, attemptedStrengths, failureReasons, string.Empty); } internal static AlgorithmRunResult Infeasible( SmoothingCandidate rejectedComparisonCandidate, IReadOnlyList attemptedStrengths, IReadOnlyList 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 attemptedStrengths, IReadOnlyList failureReasons) { return new AlgorithmRunResult( PathSmoothingStatus.Failed, null, null, null, 0d, null, attemptedStrengths, failureReasons, reason); } private static IReadOnlyList CopyReadOnly(IReadOnlyList source) { var copy = new List(source == null ? 0 : source.Count); if (source != null) { for (int index = 0; index < source.Count; index++) copy.Add(source[index]); } return new ReadOnlyCollection(copy); } } }