diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs new file mode 100644 index 0000000..52e5dc9 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonEntry.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; + +/// 原始基线或一种平滑方法的不可变比较条目。 +public sealed class PathSmoothingComparisonEntry +{ + /// 为测试、离线分析和排序创建不携带路径几何的候选条目。 + public PathSmoothingComparisonEntry( + SmoothingMethod method, + PathSmoothingStatus status, + PathQualityMetrics metrics, + SmoothingTimingSummary timing, + string stableGeometryDigest, + string diagnostic) + : this(method, false, status, metrics, timing, stableGeometryDigest, diagnostic, Empty(), Empty()) + { + } + + private PathSmoothingComparisonEntry( + SmoothingMethod? method, + bool isRawPathBaseline, + PathSmoothingStatus status, + PathQualityMetrics metrics, + SmoothingTimingSummary timing, + string stableGeometryDigest, + string diagnostic, + IReadOnlyList path, + IReadOnlyList segments) + { + Method = method; + IsRawPathBaseline = isRawPathBaseline; + Status = status; + Metrics = metrics ?? new PathQualityMetrics(); + Timing = timing ?? new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, false, "未提供计时结果。"); + StableGeometryDigest = stableGeometryDigest ?? string.Empty; + Diagnostic = diagnostic ?? string.Empty; + Path = CopyReadOnly(path); + Segments = CopyReadOnly(segments); + } + + /// 候选所代表的方法;原始粗路径基线为空。 + public SmoothingMethod? Method { get; } + + /// 是否为单独分析的原始粗路径基线。 + public bool IsRawPathBaseline { get; } + + /// 本条目的最终状态。 + public PathSmoothingStatus Status { get; } + + /// 使用原始基线规范化后的质量指标。 + public PathQualityMetrics Metrics { get; } + + /// 方法的五次测量计时;基线不参与计时排名。 + public SmoothingTimingSummary Timing { get; } + + /// 由状态、分段元数据和完整路径 IEEE 754 位模式生成的 SHA-256 摘要。 + public string StableGeometryDigest { get; } + + /// 面向报告和诊断的稳定说明。 + public string Diagnostic { get; } + + /// 仅供比较与报告读取的正式路径;失败时为空。 + public IReadOnlyList Path { get; } + + /// 覆盖 的方向段;失败时为空。 + public IReadOnlyList Segments { get; } + + /// 条目能否参与方法推荐。 + public bool IsEligibleForRecommendation => + !IsRawPathBaseline && + Status == PathSmoothingStatus.Success && + Metrics.IsFeasible && + Timing.IsDeterministic; + + internal static PathSmoothingComparisonEntry CreateCandidate( + SmoothingMethod method, + PathSmoothingStatus status, + PathQualityMetrics metrics, + SmoothingTimingSummary timing, + string stableGeometryDigest, + string diagnostic, + IReadOnlyList path, + IReadOnlyList segments) + { + return new PathSmoothingComparisonEntry( + method, false, status, metrics, timing, stableGeometryDigest, diagnostic, path, segments); + } + + internal static PathSmoothingComparisonEntry CreateRawPathBaseline( + PathSmoothingStatus status, + PathQualityMetrics metrics, + string stableGeometryDigest, + string diagnostic, + IReadOnlyList path, + IReadOnlyList segments) + { + return new PathSmoothingComparisonEntry( + null, true, status, metrics, + new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, true, string.Empty), + stableGeometryDigest, diagnostic, path, segments); + } + + private static IReadOnlyList Empty() + { + return new ReadOnlyCollection(new List()); + } + + 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); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs new file mode 100644 index 0000000..b7ee33e --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonRequest.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; + +/// 同一粗路径的离线平滑比较请求。 +public sealed class PathSmoothingComparisonRequest +{ + private static readonly SmoothingMethod[] DefaultMethods = + { + SmoothingMethod.CubicBSpline, + SmoothingMethod.LocalCubicBezier, + SmoothingMethod.PiecewiseQuintic, + }; + + /// 创建比较请求,并固定原始输入与方法顺序。 + public PathSmoothingComparisonRequest( + PathSmoothingRequest smoothingRequest, + IReadOnlyList methods = null) + { + SmoothingRequest = CopyRequest(smoothingRequest); + Methods = CopyMethods(methods ?? DefaultMethods); + } + + /// 所有方法共享的不可变粗路径、地图、车辆和配置快照。 + public PathSmoothingRequest SmoothingRequest { get; } + + /// 按调用方指定稳定顺序运行的方法集合。 + public IReadOnlyList Methods { get; } + + private static PathSmoothingRequest CopyRequest(PathSmoothingRequest source) + { + if (source == null) return null; + return new PathSmoothingRequest( + source.CoarsePath, + source.Segments, + source.Map, + source.Vehicle, + source.Configuration); + } + + private static IReadOnlyList CopyMethods(IReadOnlyList source) + { + var copy = new List(source == null ? 0 : source.Count); + if (source != null) + { + for (int index = 0; index < source.Count; index++) + { + SmoothingMethod method = source[index]; + if (!Enum.IsDefined(typeof(SmoothingMethod), method)) + throw new ArgumentOutOfRangeException(nameof(source), "比较方法无效。"); + if (copy.Contains(method)) + throw new ArgumentException("比较方法不能重复。", nameof(source)); + copy.Add(method); + } + } + return new ReadOnlyCollection(copy); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs new file mode 100644 index 0000000..94ed422 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/PathSmoothingComparisonResult.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; + +/// 一次离线比较的不可变基线、方法条目和推荐结论。 +public sealed class PathSmoothingComparisonResult +{ + internal PathSmoothingComparisonResult( + PathSmoothingComparisonEntry rawPathBaseline, + IReadOnlyList entries, + SmoothingMethod? recommendedMethod, + bool isCancelled, + string diagnostic) + { + RawPathBaseline = rawPathBaseline ?? throw new ArgumentNullException(nameof(rawPathBaseline)); + Entries = CopyReadOnly(entries); + RecommendedMethod = recommendedMethod; + IsCancelled = isCancelled; + Diagnostic = diagnostic ?? string.Empty; + } + + /// 独立分析的原始粗路径;不属于任何候选方法。 + public PathSmoothingComparisonEntry RawPathBaseline { get; } + + /// 每个请求方法恰有一个条目;取消时可能只包含已完成的方法。 + public IReadOnlyList Entries { get; } + + /// 按公开字典序选择的方法;没有合格方法或取消时为空。 + public SmoothingMethod? RecommendedMethod { get; } + + /// 比较是否在启动后续方法前被取消。 + public bool IsCancelled { get; } + + /// 整个比较的稳定状态说明。 + public string Diagnostic { get; } + + 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); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs new file mode 100644 index 0000000..9844037 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingMethodRanker.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; + +/// 按公开字典序选择唯一的推荐平滑方法。 +public static class SmoothingMethodRanker +{ + /// 从当前场景的可行且确定性候选中选择最佳方法;没有合格候选时返回空。 + public static SmoothingMethod? Rank(IReadOnlyList entries) + { + PathSmoothingComparisonEntry best = null; + if (entries == null) return null; + + for (int index = 0; index < entries.Count; index++) + { + PathSmoothingComparisonEntry candidate = entries[index]; + if (candidate == null || !candidate.IsEligibleForRecommendation) continue; + if (best == null || Compare(candidate, best) < 0) best = candidate; + } + return best == null ? (SmoothingMethod?)null : best.Method; + } + + private static int Compare(PathSmoothingComparisonEntry left, PathSmoothingComparisonEntry right) + { + int comparison = CompareAscending(left.Metrics.CurvatureVariationEnergy, right.Metrics.CurvatureVariationEnergy); + if (comparison != 0) return comparison; + + comparison = CompareAscending( + left.Metrics.MaximumAbsoluteVehicleCurvaturePerMeter, + right.Metrics.MaximumAbsoluteVehicleCurvaturePerMeter); + if (comparison != 0) return comparison; + + comparison = CompareDescending(left.Metrics.MinimumBodyClearanceMeters, right.Metrics.MinimumBodyClearanceMeters); + if (comparison != 0) return comparison; + + comparison = CompareAscending(left.Metrics.LengthChangePercent, right.Metrics.LengthChangePercent); + if (comparison != 0) return comparison; + + comparison = CompareAscending(left.Timing.MedianElapsedMilliseconds, right.Timing.MedianElapsedMilliseconds); + if (comparison != 0) return comparison; + + return ((int)left.Method.Value).CompareTo((int)right.Method.Value); + } + + private static int CompareAscending(double left, double right) + { + return Normalize(left).CompareTo(Normalize(right)); + } + + private static int CompareDescending(double left, double right) + { + return Normalize(right).CompareTo(Normalize(left)); + } + + private static double Normalize(double value) + { + return double.IsNaN(value) || double.IsInfinity(value) ? double.PositiveInfinity : value; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs new file mode 100644 index 0000000..ae4a635 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/SmoothingTimingSummary.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; + +/// 一个方法的固定五次计时样本和确定性结论。 +public sealed class SmoothingTimingSummary +{ + /// 创建一个只接受五个已测量样本的计时汇总。 + public SmoothingTimingSummary( + IReadOnlyList measuredElapsedMilliseconds, + bool isDeterministic, + string diagnostic) + { + if (measuredElapsedMilliseconds == null || measuredElapsedMilliseconds.Count != 5) + throw new ArgumentException("计时汇总必须包含五个已测量样本。", nameof(measuredElapsedMilliseconds)); + + var copy = new List(measuredElapsedMilliseconds.Count); + for (int index = 0; index < measuredElapsedMilliseconds.Count; index++) + { + double value = measuredElapsedMilliseconds[index]; + if (double.IsNaN(value) || double.IsInfinity(value) || value < 0d) + throw new ArgumentOutOfRangeException(nameof(measuredElapsedMilliseconds), "计时样本必须为有限非负数。"); + copy.Add(value); + } + + MeasuredElapsedMilliseconds = new ReadOnlyCollection(copy); + MedianElapsedMilliseconds = Median(copy); + IsDeterministic = isDeterministic; + Diagnostic = diagnostic ?? string.Empty; + } + + /// 不含预热执行的五个实测耗时,单位 ms。 + public IReadOnlyList MeasuredElapsedMilliseconds { get; } + + /// 五个实测耗时的稳定中位数,单位 ms。 + public double MedianElapsedMilliseconds { get; } + + /// 五次输出是否具有相同的状态和稳定几何摘要。 + public bool IsDeterministic { get; } + + /// 非确定性或测量失败的稳定诊断说明。 + public string Diagnostic { get; } + + /// 从五次测量结果中生成计时汇总,并拒绝状态或几何不稳定的输出。 + public static SmoothingTimingSummary FromMeasurements( + IReadOnlyList measuredElapsedMilliseconds, + IReadOnlyList measuredResults) + { + if (measuredResults == null || measuredResults.Count != 5) + throw new ArgumentException("确定性检查必须包含五个测量结果。", nameof(measuredResults)); + for (int index = 0; index < measuredResults.Count; index++) + { + if (measuredResults[index] == null) + throw new ArgumentException("确定性检查不能包含空测量结果。", nameof(measuredResults)); + } + + PathSmoothingResult canonical = measuredResults[0]; + string canonicalDigest = StableGeometryDigest.Compute(canonical); + for (int index = 1; index < measuredResults.Count; index++) + { + PathSmoothingResult measured = measuredResults[index]; + string digest = StableGeometryDigest.Compute(measured); + if (measured.Status != canonical.Status || + measured.Path.Count != canonical.Path.Count || + measured.Segments.Count != canonical.Segments.Count || + !string.Equals(digest, canonicalDigest, StringComparison.Ordinal)) + { + return new SmoothingTimingSummary( + measuredElapsedMilliseconds, + false, + "五次测量的状态、点数、分段数或稳定几何摘要不一致。"); + } + } + + return new SmoothingTimingSummary(measuredElapsedMilliseconds, true, string.Empty); + } + + internal static double Median(IReadOnlyList values) + { + var sorted = new double[values.Count]; + for (int index = 0; index < values.Count; index++) sorted[index] = values[index]; + Array.Sort(sorted); + int middle = sorted.Length / 2; + return sorted.Length % 2 == 1 + ? sorted[middle] + : (sorted[middle - 1] + sorted[middle]) / 2d; + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs new file mode 100644 index 0000000..ee9d105 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Comparison/StableGeometryDigest.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; + +/// 为重复执行结果生成与进程无关的稳定几何 SHA-256 摘要。 +public static class StableGeometryDigest +{ + /// 计算正式平滑结果的状态、方法、分段和路径位模式摘要。 + public static string Compute(PathSmoothingResult result) + { + if (result == null) throw new ArgumentNullException(nameof(result)); + return Compute(result.Status, result.Method, result.Path, result.Segments); + } + + /// 计算任意已分析路径的稳定摘要。 + public static string Compute( + PathSmoothingStatus status, + SmoothingMethod? method, + IReadOnlyList path, + IReadOnlyList segments) + { + using (var stream = new MemoryStream()) + { + WriteInt32(stream, 1); + WriteInt32(stream, (int)status); + WriteBoolean(stream, method.HasValue); + if (method.HasValue) WriteInt32(stream, (int)method.Value); + + WriteInt32(stream, path == null ? 0 : path.Count); + if (path != null) + { + for (int index = 0; index < path.Count; index++) WritePoint(stream, path[index]); + } + + WriteInt32(stream, segments == null ? 0 : segments.Count); + if (segments != null) + { + for (int index = 0; index < segments.Count; index++) WriteSegment(stream, segments[index]); + } + + using (SHA256 sha256 = SHA256.Create()) + { + byte[] hash = sha256.ComputeHash(stream.ToArray()); + var builder = new StringBuilder(hash.Length * 2); + for (int index = 0; index < hash.Length; index++) builder.Append(hash[index].ToString("x2")); + return builder.ToString(); + } + } + } + + private static void WritePoint(Stream stream, SmoothedPathPoint point) + { + if (point == null) throw new ArgumentException("稳定路径摘要不能包含空点。", nameof(point)); + WriteDouble(stream, point.X); + WriteDouble(stream, point.Y); + WriteDouble(stream, point.Heading); + WriteDouble(stream, point.UnwrappedHeading); + WriteDouble(stream, point.ArcLength); + WriteInt32(stream, (int)point.Direction); + WriteDouble(stream, point.GeometricCurvature); + WriteDouble(stream, point.VehicleCurvature); + WriteDouble(stream, point.BodyClearance); + WriteBoolean(stream, point.IsGearSwitchPoint); + WriteInt32(stream, (int)point.Source); + } + + private static void WriteSegment(Stream stream, SmoothedPathSegment segment) + { + if (segment == null) throw new ArgumentException("稳定路径摘要不能包含空方向段。", nameof(segment)); + WriteInt32(stream, segment.SegmentIndex); + WriteInt32(stream, (int)segment.Direction); + WriteInt32(stream, segment.StartIndex); + WriteInt32(stream, segment.EndIndex); + WriteBoolean(stream, segment.StartsAtGearSwitch); + WriteBoolean(stream, segment.EndsAtGearSwitch); + } + + private static void WriteDouble(Stream stream, double value) + { + WriteInt64(stream, BitConverter.DoubleToInt64Bits(value)); + } + + private static void WriteBoolean(Stream stream, bool value) + { + stream.WriteByte(value ? (byte)1 : (byte)0); + } + + private static void WriteInt32(Stream stream, int value) + { + unchecked + { + stream.WriteByte((byte)value); + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)(value >> 16)); + stream.WriteByte((byte)(value >> 24)); + } + } + + private static void WriteInt64(Stream stream, long value) + { + unchecked + { + ulong bits = (ulong)value; + for (int index = 0; index < 8; index++) stream.WriteByte((byte)(bits >> (index * 8))); + } + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs new file mode 100644 index 0000000..3c42601 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Facade/PathSmoothingComparisonService.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Comparison; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing; +using MultiWheelC.TrajectoryPlanning.PathSmoothing.Validation; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade; + +/// 以固定预热和五次测量隔离比较所有请求平滑方法的离线入口。 +public sealed class PathSmoothingComparisonService +{ + private readonly PathSmoothingService _smoothingService = new PathSmoothingService(); + private readonly PathSmoothingPreprocessor _preprocessor = new PathSmoothingPreprocessor(); + private readonly PathGeometryAnalyzer _analyzer = new PathGeometryAnalyzer(); + private readonly SmoothedPathValidator _validator = new SmoothedPathValidator(); + + /// 比较所有请求方法;一个方法的失败不会阻止其他方法,取消会停止后续启动。 + public PathSmoothingComparisonResult Compare( + PathSmoothingComparisonRequest request, + CancellationToken cancellationToken = default) + { + PathSmoothingComparisonEntry baseline = CreateRawPathBaseline(request, out string baselineReason); + var entries = new List(); + + if (cancellationToken.IsCancellationRequested) + return Cancelled(baseline, entries); + + if (request == null || request.SmoothingRequest == null) + return new PathSmoothingComparisonResult(baseline, entries, null, false, baselineReason); + + for (int methodIndex = 0; methodIndex < request.Methods.Count; methodIndex++) + { + if (cancellationToken.IsCancellationRequested) + return Cancelled(baseline, entries); + + SmoothingMethod method = request.Methods[methodIndex]; + if (!TryCompareMethod( + request.SmoothingRequest, + method, + baseline.Metrics, + cancellationToken, + out PathSmoothingComparisonEntry entry)) + return Cancelled(baseline, entries); + entries.Add(entry); + } + + return new PathSmoothingComparisonResult( + baseline, + entries, + SmoothingMethodRanker.Rank(entries), + false, + baselineReason); + } + + private bool TryCompareMethod( + PathSmoothingRequest sourceRequest, + SmoothingMethod method, + PathQualityMetrics rawMetrics, + CancellationToken cancellationToken, + out PathSmoothingComparisonEntry entry) + { + entry = null; + try + { + PathSmoothingRequest methodRequest = CreateMethodRequest(sourceRequest, method); + cancellationToken.ThrowIfCancellationRequested(); + PathSmoothingResult warmup = _smoothingService.Smooth(methodRequest, cancellationToken); + if (warmup.Status == PathSmoothingStatus.Cancelled || cancellationToken.IsCancellationRequested) return false; + + var timings = new List(5); + var measuredResults = new List(5); + for (int sampleIndex = 0; sampleIndex < 5; sampleIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + Stopwatch stopwatch = Stopwatch.StartNew(); + PathSmoothingResult result = _smoothingService.Smooth(methodRequest, cancellationToken); + stopwatch.Stop(); + if (result.Status == PathSmoothingStatus.Cancelled || cancellationToken.IsCancellationRequested) return false; + timings.Add(stopwatch.Elapsed.TotalMilliseconds); + measuredResults.Add(result); + } + + PathSmoothingResult canonical = measuredResults[0]; + string digest = StableGeometryDigest.Compute(canonical); + SmoothingTimingSummary timing = SmoothingTimingSummary.FromMeasurements(timings, measuredResults); + string diagnostic = string.IsNullOrWhiteSpace(timing.Diagnostic) + ? canonical.Diagnostics.TerminationReason + : timing.Diagnostic; + + PathQualityMetrics metrics = canonical.Status == PathSmoothingStatus.Success + ? NormalizeMetrics(canonical.Diagnostics.Metrics, rawMetrics) + : new PathQualityMetrics(); + entry = PathSmoothingComparisonEntry.CreateCandidate( + method, + canonical.Status, + metrics, + timing, + digest, + diagnostic, + canonical.Path, + canonical.Segments); + return true; + } + catch (OperationCanceledException) + { + return false; + } + catch (Exception exception) + { + entry = PathSmoothingComparisonEntry.CreateCandidate( + method, + PathSmoothingStatus.Failed, + new PathQualityMetrics(), + new SmoothingTimingSummary(new double[] { 0d, 0d, 0d, 0d, 0d }, false, exception.GetType().Name), + string.Empty, + exception.Message, + null, + null); + return true; + } + } + + private PathSmoothingComparisonEntry CreateRawPathBaseline( + PathSmoothingComparisonRequest request, + out string reason) + { + reason = string.Empty; + if (request == null || request.SmoothingRequest == null) + return FailedBaseline(PathSmoothingStatus.InvalidInput, "比较请求为空。", out reason); + + PathSmoothingRequest smoothingRequest = request.SmoothingRequest; + PathSmoothingConfiguration configuration = smoothingRequest.Configuration; + if (configuration == null || smoothingRequest.Map == null || smoothingRequest.Vehicle == null) + return FailedBaseline(PathSmoothingStatus.InvalidInput, "比较请求缺少可用的地图、车辆或配置。", out reason); + + if (!_preprocessor.TryPrepare(smoothingRequest, out PreparedPath preparedPath, out reason)) + return FailedBaseline(PathSmoothingStatus.InvalidInput, reason, out reason); + if (!_analyzer.TryAnalyze(preparedPath.Segments, configuration.OutputSpacingMeters, out PathGeometryAnalysis analysis, out reason)) + return FailedBaseline(PathSmoothingStatus.InvalidInput, reason, out reason); + if (!_validator.TryValidate( + analysis.Path, + analysis.Segments, + preparedPath, + smoothingRequest.Map, + smoothingRequest.Vehicle, + configuration.MaximumCollisionCheckStepMeters, + out IReadOnlyList safePath, + out double minimumClearanceMeters, + out reason)) + { + return FailedBaseline(PathSmoothingStatus.InvalidInput, reason, out reason); + } + + PathQualityMetrics metrics = CreateMetrics(analysis, minimumClearanceMeters); + string digest = StableGeometryDigest.Compute(PathSmoothingStatus.Success, null, safePath, analysis.Segments); + return PathSmoothingComparisonEntry.CreateRawPathBaseline( + PathSmoothingStatus.Success, + metrics, + digest, + string.Empty, + safePath, + analysis.Segments); + } + + private static PathSmoothingComparisonEntry FailedBaseline( + PathSmoothingStatus status, + string failureReason, + out string reason) + { + reason = failureReason ?? string.Empty; + return PathSmoothingComparisonEntry.CreateRawPathBaseline( + status, + new PathQualityMetrics(), + StableGeometryDigest.Compute(status, null, null, null), + reason, + null, + null); + } + + private static PathSmoothingComparisonResult Cancelled( + PathSmoothingComparisonEntry baseline, + IReadOnlyList entries) + { + return new PathSmoothingComparisonResult(baseline, entries, null, true, "路径平滑比较已取消。"); + } + + private static PathSmoothingRequest CreateMethodRequest(PathSmoothingRequest source, SmoothingMethod method) + { + PathSmoothingConfiguration configuration = source.Configuration; + configuration.Method = method; + configuration.AllowFallbackToCoarsePath = false; + return new PathSmoothingRequest( + source.CoarsePath, + source.Segments, + source.Map, + source.Vehicle, + configuration); + } + + 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 PathQualityMetrics NormalizeMetrics( + PathQualityMetrics candidate, + PathQualityMetrics raw) + { + if (candidate == null || raw == null || !candidate.IsFeasible) + return new PathQualityMetrics(); + + return new PathQualityMetrics( + true, + candidate.PathLengthMeters, + candidate.MaximumAbsoluteVehicleCurvaturePerMeter, + candidate.RootMeanSquareVehicleCurvaturePerMeter, + candidate.TotalAbsoluteCurvatureVariationPerMeter, + candidate.CurvatureVariationEnergy, + candidate.MinimumBodyClearanceMeters, + RelativePercentOrAbsoluteDelta(candidate.PathLengthMeters, raw.PathLengthMeters), + RelativePercentOrAbsoluteDelta( + candidate.MaximumAbsoluteVehicleCurvaturePerMeter, + raw.MaximumAbsoluteVehicleCurvaturePerMeter), + RelativePercentOrAbsoluteDelta( + candidate.TotalAbsoluteCurvatureVariationPerMeter, + raw.TotalAbsoluteCurvatureVariationPerMeter), + candidate.MinimumBodyClearanceMeters - raw.MinimumBodyClearanceMeters); + } + + private static double RelativePercentOrAbsoluteDelta(double candidate, double raw) + { + double delta = candidate - raw; + return Math.Abs(raw) < 1e-12d ? delta : delta / raw * 100d; + } +} diff --git a/ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 b/ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 new file mode 100644 index 0000000..a331e62 --- /dev/null +++ b/ClumsyPilot/tests/verify_path_smoothing_comparison.ps1 @@ -0,0 +1,230 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) + +$ErrorActionPreference = 'Stop' +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) + +function Assert-True($Actual, [string]$Message) { + if (-not $Actual) { throw $Message } +} + +function Assert-Equal($Expected, $Actual, [string]$Message) { + if ($Expected -ne $Actual) { throw "$Message Expected=$Expected Actual=$Actual" } +} + +function Assert-Near([double]$Expected, [double]$Actual, [double]$Tolerance, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt $Tolerance) { + throw "$Message Expected=$Expected Actual=$Actual Tolerance=$Tolerance" + } +} + +function Get-RequiredType([string]$Name) { + return $assembly.GetType($Name, $true) +} + +function New-Map { + $mapRequest = [Activator]::CreateInstance($mapRequestType) + $mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000)) + $mapRequest.ResolutionMm = [single]50 + $mapRequest.AllowExplicitEmptyMap = $true + $map = [Activator]::CreateInstance($mapFactoryType).Create($mapRequest).Map + Assert-True ($null -ne $map) 'Comparison test must create a planning map.' + return $map +} + +function New-Vehicle { + $vehicle = [Activator]::CreateInstance($vehicleType) + $vehicle.LengthMeters = [double]0.20 + $vehicle.WidthMeters = [double]0.20 + $vehicle.SafetyMarginMeters = [double]0.0 + $vehicle.MaximumCurvaturePerMeter = [double]100.0 + return $vehicle +} + +function New-CoarsePoint([double]$X, [double]$Y, [double]$ArcLength) { + return [Activator]::CreateInstance($coarsePointType, @( + $X, $Y, [double]0.0, [double]0.0, $ArcLength, $forward, + [double]0.0, [double]1.0, $false, $coarseAnchor)) +} + +function New-SmoothingRequest { + $points = [Array]::CreateInstance($coarsePointType, 2) + $points.SetValue((New-CoarsePoint 0.5 0.5 0.0), 0) + $points.SetValue((New-CoarsePoint 1.5 0.5 1.0), 1) + $segments = [Array]::CreateInstance($coarseSegmentType, 1) + $segments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 1, $false, $false)), 0) + $configuration = [Activator]::CreateInstance($configurationType) + return [Activator]::CreateInstance($smoothingRequestType, @($points, $segments, (New-Map), (New-Vehicle), $configuration)) +} + +function New-CornerSmoothingRequest { + $points = [Array]::CreateInstance($coarsePointType, 4) + $points.SetValue((New-CoarsePoint 0.5 0.5 0.0), 0) + $points.SetValue((New-CoarsePoint 1.0 0.5 0.5), 1) + $points.SetValue((New-CoarsePoint 1.0 1.0 1.0), 2) + $points.SetValue((New-CoarsePoint 1.5 1.0 1.5), 3) + $segments = [Array]::CreateInstance($coarseSegmentType, 1) + $segments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(0, $forward, 0, 3, $false, $false)), 0) + $configuration = [Activator]::CreateInstance($configurationType) + return [Activator]::CreateInstance($smoothingRequestType, @($points, $segments, (New-Map), (New-Vehicle), $configuration)) +} + +function New-Metrics( + [double]$VariationEnergy, + [double]$PeakCurvature, + [double]$MinimumClearance, + [double]$LengthChangePercent) { + return [Activator]::CreateInstance($metricsType, @( + $true, [double]1.0, $PeakCurvature, [double]0.0, [double]0.0, $VariationEnergy, + $MinimumClearance, $LengthChangePercent, [double]0.0, [double]0.0, [double]0.0)) +} + +function New-Timing([double]$MedianMilliseconds) { + [double[]]$samples = @($MedianMilliseconds, $MedianMilliseconds, $MedianMilliseconds, $MedianMilliseconds, $MedianMilliseconds) + return [Activator]::CreateInstance($timingType, @($samples, $true, '')) +} + +function New-Entry( + $Method, + [double]$VariationEnergy, + [double]$PeakCurvature, + [double]$MinimumClearance, + [double]$LengthChangePercent, + [double]$MedianMilliseconds) { + [object[]]$arguments = @( + $Method, $successStatus, (New-Metrics $VariationEnergy $PeakCurvature $MinimumClearance $LengthChangePercent), + (New-Timing $MedianMilliseconds), ('synthetic-' + $Method.ToString()), '') + return [Activator]::CreateInstance($entryType, $arguments) +} + +$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' +$comparison = $root + 'Comparison.' +$facade = $root + 'Facade.' +$coarsePath = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' +$mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.' + +$comparisonServiceType = Get-RequiredType ($facade + 'PathSmoothingComparisonService') +$comparisonRequestType = Get-RequiredType ($comparison + 'PathSmoothingComparisonRequest') +$comparisonResultType = Get-RequiredType ($comparison + 'PathSmoothingComparisonResult') +$entryType = Get-RequiredType ($comparison + 'PathSmoothingComparisonEntry') +$timingType = Get-RequiredType ($comparison + 'SmoothingTimingSummary') +$rankerType = Get-RequiredType ($comparison + 'SmoothingMethodRanker') +$smoothingRequestType = Get-RequiredType ($root + 'PathSmoothingRequest') +$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') +$metricsType = Get-RequiredType ($root + 'PathQualityMetrics') +$methodType = Get-RequiredType ($root + 'SmoothingMethod') +$statusType = Get-RequiredType ($root + 'PathSmoothingStatus') +$smoothingResultType = Get-RequiredType ($root + 'PathSmoothingResult') +$diagnosticsType = Get-RequiredType ($root + 'PathSmoothingDiagnostics') +$coarsePointType = Get-RequiredType ($coarsePath + 'CoarsePathPoint') +$coarseSegmentType = Get-RequiredType ($coarsePath + 'PathSegment') +$directionType = Get-RequiredType ($coarsePath + 'TravelDirection') +$coarsePointSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource') +$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters') +$boundsType = Get-RequiredType ($mapping + 'MapBoundsMm') +$mapRequestType = Get-RequiredType ($mapping + 'PlanningMapRequest') +$mapFactoryType = Get-RequiredType ($mapping + 'PlanningMapFactory') + +$forward = [Enum]::Parse($directionType, 'Forward') +$coarseAnchor = [Enum]::Parse($coarsePointSourceType, 'Start') +$cubicBSpline = [Enum]::Parse($methodType, 'CubicBSpline') +$localCubicBezier = [Enum]::Parse($methodType, 'LocalCubicBezier') +$piecewiseQuintic = [Enum]::Parse($methodType, 'PiecewiseQuintic') +$successStatus = [Enum]::Parse($statusType, 'Success') + +Assert-True $comparisonServiceType.IsPublic 'Comparison service must be public.' +$compareMethod = $comparisonServiceType.GetMethod('Compare', [Type[]]@($comparisonRequestType, [Threading.CancellationToken])) +Assert-True ($null -ne $compareMethod) 'Comparison service must expose Compare(PathSmoothingComparisonRequest, CancellationToken).' +Assert-Equal $comparisonResultType $compareMethod.ReturnType 'Compare must return PathSmoothingComparisonResult.' + +$methods = [Array]::CreateInstance($methodType, 3) +$methods.SetValue($cubicBSpline, 0) +$methods.SetValue($localCubicBezier, 1) +$methods.SetValue($piecewiseQuintic, 2) +$comparisonRequest = [Activator]::CreateInstance($comparisonRequestType, @((New-SmoothingRequest), $methods)) +$comparisonService = [Activator]::CreateInstance($comparisonServiceType) +$result = $compareMethod.Invoke($comparisonService, @($comparisonRequest, [Threading.CancellationToken]::None)) + +Assert-True ($null -ne $result.RawPathBaseline) 'Comparison must publish a separately analyzed raw-path baseline.' +Assert-True $result.RawPathBaseline.IsRawPathBaseline 'Raw baseline must be explicitly marked and excluded from candidates.' +Assert-Equal 3 $result.Entries.Count 'Comparison must contain exactly one entry for every requested method.' +Assert-True ($null -ne $result.RecommendedMethod) 'A comparison with feasible methods must select a recommendation.' +foreach ($entry in $result.Entries) { + Assert-True (-not $entry.IsRawPathBaseline) 'Candidate entries must not be marked as the raw baseline.' + Assert-Equal 5 $entry.Timing.MeasuredElapsedMilliseconds.Count 'Warm-up must be excluded and exactly five measurements retained.' + Assert-True $entry.Timing.IsDeterministic 'Repeated deterministic smoothing geometry must remain eligible for recommendation.' + Assert-True (-not [string]::IsNullOrWhiteSpace($entry.StableGeometryDigest)) 'Every measured candidate must expose a stable geometry digest.' +} + +$fromMeasurementsMethod = $timingType.GetMethod('FromMeasurements') +Assert-True ($null -ne $fromMeasurementsMethod) 'Timing summary must analyze the five measured outputs for deterministic geometry.' +$failureFactory = $smoothingResultType.GetMethod('Failure') +$invalidInputStatus = [Enum]::Parse($statusType, 'InvalidInput') +$infeasibleStatus = [Enum]::Parse($statusType, 'Infeasible') +$inconsistentResults = [Array]::CreateInstance($smoothingResultType, 5) +for ($index = 0; $index -lt 5; $index++) { + $status = if ($index -eq 4) { $infeasibleStatus } else { $invalidInputStatus } + [object[]]$failureArguments = New-Object object[] 2 + $failureArguments[0] = $status + $failureArguments[1] = [Activator]::CreateInstance($diagnosticsType) + $inconsistentResults.SetValue($failureFactory.Invoke($null, $failureArguments), $index) +} +[object[]]$timingArguments = New-Object object[] 2 +$timingArguments[0] = [double[]]@(1.0, 2.0, 3.0, 4.0, 5.0) +$timingArguments[1] = $inconsistentResults +$nonDeterministicTiming = $fromMeasurementsMethod.Invoke($null, $timingArguments) +Assert-True (-not $nonDeterministicTiming.IsDeterministic) 'A status, point-count, segment-count, or digest mismatch must be non-deterministic.' +Assert-True (-not [string]::IsNullOrWhiteSpace($nonDeterministicTiming.Diagnostic)) 'Non-deterministic measurements must publish a stable diagnostic.' + +# All published comparison metrics must be normalized against the separately analyzed raw baseline. +$cornerMethods = [Array]::CreateInstance($methodType, 1) +$cornerMethods.SetValue($cubicBSpline, 0) +$cornerRequest = [Activator]::CreateInstance($comparisonRequestType, @((New-CornerSmoothingRequest), $cornerMethods)) +$cornerResult = $compareMethod.Invoke($comparisonService, @($cornerRequest, [Threading.CancellationToken]::None)) +$cornerEntry = $cornerResult.Entries[0] +Assert-Equal 'Success' $cornerEntry.Status.ToString() 'The unconstrained empty-map corner fixture must produce a B-spline comparison candidate.' +$expectedLengthChange = (($cornerEntry.Path[$cornerEntry.Path.Count - 1].ArcLength - $cornerResult.RawPathBaseline.Metrics.PathLengthMeters) / + $cornerResult.RawPathBaseline.Metrics.PathLengthMeters) * 100.0 +Assert-Near $expectedLengthChange $cornerEntry.Metrics.LengthChangePercent 0.000001 'Candidate length change must be normalized relative to the raw baseline.' + +# The ranker must apply every public tie-break in order. Each pair ties all prior criteria. +$entryListType = [Collections.Generic.IReadOnlyList``1].MakeGenericType(@($entryType)) +$rankMethod = $rankerType.GetMethod('Rank', [Type[]]@($entryListType)) +Assert-True ($null -ne $rankMethod) 'SmoothingMethodRanker must expose Rank(IReadOnlyList).' + +function Assert-Rank($ExpectedMethod, [object[]]$Entries, [string]$Message) { + $typedEntries = [Array]::CreateInstance($entryType, $Entries.Count) + for ($index = 0; $index -lt $Entries.Count; $index++) { $typedEntries.SetValue($Entries[$index], $index) } + [object[]]$invokeArguments = New-Object object[] 1 + $invokeArguments[0] = $typedEntries + $actual = $rankMethod.Invoke($null, $invokeArguments) + Assert-Equal $ExpectedMethod.ToString() $actual.ToString() $Message +} + +Assert-Rank $cubicBSpline @( + (New-Entry $cubicBSpline 1.0 0.5 0.8 5.0 10.0), + (New-Entry $localCubicBezier 2.0 0.1 1.0 1.0 1.0)) 'Variation-energy tie-break must take priority over later criteria.' +Assert-Rank $cubicBSpline @( + (New-Entry $cubicBSpline 1.0 0.2 0.8 5.0 10.0), + (New-Entry $localCubicBezier 1.0 0.3 1.0 1.0 1.0)) 'Peak-curvature tie-break must follow variation energy.' +Assert-Rank $cubicBSpline @( + (New-Entry $cubicBSpline 1.0 0.2 0.9 5.0 10.0), + (New-Entry $localCubicBezier 1.0 0.2 0.8 1.0 1.0)) 'Clearance-loss tie-break must follow peak curvature.' +Assert-Rank $cubicBSpline @( + (New-Entry $cubicBSpline 1.0 0.2 0.9 2.0 10.0), + (New-Entry $localCubicBezier 1.0 0.2 0.9 3.0 1.0)) 'Length-change tie-break must follow clearance loss.' +Assert-Rank $cubicBSpline @( + (New-Entry $cubicBSpline 1.0 0.2 0.9 2.0 5.0), + (New-Entry $localCubicBezier 1.0 0.2 0.9 2.0 6.0)) 'Median elapsed tie-break must be last.' + +$cancelSource = [Threading.CancellationTokenSource]::new() +try { + $cancelSource.Cancel() + $cancelled = $compareMethod.Invoke($comparisonService, @($comparisonRequest, $cancelSource.Token)) + Assert-True $cancelled.IsCancelled 'Cancellation must stop comparison before subsequent methods start.' + Assert-Equal $null $cancelled.RecommendedMethod 'Cancelled comparison must not make a recommendation.' +} +finally { + $cancelSource.Dispose() +} + +Write-Output 'Path smoothing comparison checks passed.'