From ebf7d7d3d4c1dcf0e2086daf1541c1b7af1e8dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=81=E8=96=84=E4=BA=91?= Date: Tue, 28 Jul 2026 16:48:24 +0800 Subject: [PATCH] feat: define path smoothing contracts --- .../Contracts/CubicBSplineOptions.cs | 8 + .../Contracts/LocalCubicBezierOptions.cs | 16 ++ .../Contracts/PathQualityMetrics.cs | 71 +++++++ .../Contracts/PathSmoothingConfiguration.cs | 50 +++++ .../Contracts/PathSmoothingDiagnostics.cs | 80 ++++++++ .../Contracts/PathSmoothingRequest.cs | 51 +++++ .../Contracts/PathSmoothingResult.cs | 108 +++++++++++ .../Contracts/PathSmoothingStatus.cs | 12 ++ .../Contracts/PiecewiseQuinticOptions.cs | 11 ++ .../Contracts/SmoothedPathPoint.cs | 66 +++++++ .../Contracts/SmoothedPathPointSource.cs | 10 + .../Contracts/SmoothedPathSegment.cs | 41 ++++ .../Contracts/SmoothingMethod.cs | 9 + .../tests/verify_path_smoothing_contracts.ps1 | 179 ++++++++++++++++++ 14 files changed, 712 insertions(+) create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs create mode 100644 ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs new file mode 100644 index 0000000..07b2615 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/CubicBSplineOptions.cs @@ -0,0 +1,8 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 三次 B 样条平滑参数。 +public sealed class CubicBSplineOptions +{ + /// 端点切向控制柄相对于相邻弦长的比例。 + public double EndpointTangentScale { get; set; } = 1d / 3d; +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs new file mode 100644 index 0000000..bd99eec --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalCubicBezierOptions.cs @@ -0,0 +1,16 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 局部三次 Bézier 平滑参数。 +public sealed class LocalCubicBezierOptions +{ + /// 判定为明显转角的最小航向变化,单位 rad。 + public double CornerHeadingThresholdRadians { get; set; } = Math.PI / 18d; + + /// 单个局部平滑窗口的最大弧长,单位 m。 + public double MaximumWindowLengthMeters { get; set; } = 0.60d; + + /// 控制柄相对于窗口局部弦长的比例。 + public double HandleLengthRatio { get; set; } = 1d / 3d; +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs new file mode 100644 index 0000000..fdd088e --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs @@ -0,0 +1,71 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 一条原始或平滑路径的不可变质量指标。 +public sealed class PathQualityMetrics +{ + /// 创建全零、不可行的质量指标。 + public PathQualityMetrics() + : this(false, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d) + { + } + + /// 创建完整的质量指标快照。 + public PathQualityMetrics( + bool isFeasible, + double pathLengthMeters, + double maximumAbsoluteVehicleCurvaturePerMeter, + double rootMeanSquareVehicleCurvaturePerMeter, + double totalAbsoluteCurvatureVariationPerMeter, + double curvatureVariationEnergy, + double minimumBodyClearanceMeters, + double lengthChangePercent, + double peakCurvatureChangePercent, + double curvatureVariationChangePercent, + double minimumClearanceChangeMeters) + { + IsFeasible = isFeasible; + PathLengthMeters = pathLengthMeters; + MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter; + RootMeanSquareVehicleCurvaturePerMeter = rootMeanSquareVehicleCurvaturePerMeter; + TotalAbsoluteCurvatureVariationPerMeter = totalAbsoluteCurvatureVariationPerMeter; + CurvatureVariationEnergy = curvatureVariationEnergy; + MinimumBodyClearanceMeters = minimumBodyClearanceMeters; + LengthChangePercent = lengthChangePercent; + PeakCurvatureChangePercent = peakCurvatureChangePercent; + CurvatureVariationChangePercent = curvatureVariationChangePercent; + MinimumClearanceChangeMeters = minimumClearanceChangeMeters; + } + + /// 该路径是否通过完整安全和运动学复核。 + public bool IsFeasible { get; } + + /// 路径总弧长,单位 m。 + public double PathLengthMeters { get; } + + /// 绝对车辆曲率峰值,单位 1/m。 + public double MaximumAbsoluteVehicleCurvaturePerMeter { get; } + + /// 车辆曲率均方根,单位 1/m。 + public double RootMeanSquareVehicleCurvaturePerMeter { get; } + + /// 逐方向段累加的绝对曲率变化,单位 1/m。 + public double TotalAbsoluteCurvatureVariationPerMeter { get; } + + /// 逐方向段计算的曲率变化能量。 + public double CurvatureVariationEnergy { get; } + + /// 完整扩大车体的最小保守净空,单位 m。 + public double MinimumBodyClearanceMeters { get; } + + /// 相对原始粗路径的长度变化百分比。 + public double LengthChangePercent { get; } + + /// 相对原始粗路径的峰值曲率变化百分比。 + public double PeakCurvatureChangePercent { get; } + + /// 相对原始粗路径的曲率变化百分比。 + public double CurvatureVariationChangePercent { get; } + + /// 相对原始粗路径的最小净空变化,单位 m。 + public double MinimumClearanceChangeMeters { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs new file mode 100644 index 0000000..ab1f2a1 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 路径平滑的公共配置;所有距离使用 m。 +public sealed class PathSmoothingConfiguration +{ + /// 创建带有安全默认值的平滑配置。 + public PathSmoothingConfiguration() + { + OutputSpacingMeters = 0.05d; + MaximumCollisionCheckStepMeters = 0.025d; + MinimumClearanceReserveMeters = 0.02d; + SmoothingStrength = 1d; + AllowFallbackToCoarsePath = true; + RetryStrengthScales = new ReadOnlyCollection( + new List { 1d, 0.75d, 0.50d, 0.25d }); + } + + /// 正式单算法入口使用的方法。 + public SmoothingMethod Method { get; set; } + + /// 输出路径的目标弧长采样间距,单位 m。 + public double OutputSpacingMeters { get; set; } + + /// 扫掠碰撞检查的最大步长,单位 m。 + public double MaximumCollisionCheckStepMeters { get; set; } + + /// 平滑候选必须在最小净空之外保留的额外余量,单位 m。 + public double MinimumClearanceReserveMeters { get; set; } + + /// 算法初始平滑强度。 + public double SmoothingStrength { get; set; } + + /// 所有平滑尝试失败时是否允许发布经过复核的原粗路径。 + public bool AllowFallbackToCoarsePath { get; set; } + + /// 有限且严格递减的平滑强度重试比例。 + public IReadOnlyList RetryStrengthScales { get; } + + /// 三次 B 样条专用参数。 + public CubicBSplineOptions CubicBSpline { get; } = new CubicBSplineOptions(); + + /// 局部三次 Bézier 专用参数。 + public LocalCubicBezierOptions LocalCubicBezier { get; } = new LocalCubicBezierOptions(); + + /// 分段五次多项式专用参数。 + public PiecewiseQuinticOptions PiecewiseQuintic { get; } = new PiecewiseQuinticOptions(); +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs new file mode 100644 index 0000000..7f49d76 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingDiagnostics.cs @@ -0,0 +1,80 @@ +using System; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 一次平滑尝试的不可变诊断信息。 +public sealed class PathSmoothingDiagnostics +{ + /// 创建不含路径指标的默认诊断信息。 + public PathSmoothingDiagnostics() + : this(new PathQualityMetrics(), TimeSpan.Zero, 0, 0d, string.Empty) + { + } + + /// 创建完整的平滑诊断快照。 + public PathSmoothingDiagnostics( + PathQualityMetrics metrics, + TimeSpan elapsed, + int retryCount, + double acceptedStrength, + string terminationReason = null) + { + Metrics = metrics ?? new PathQualityMetrics(); + Elapsed = elapsed; + RetryCount = retryCount; + AcceptedStrength = acceptedStrength; + TerminationReason = terminationReason ?? string.Empty; + } + + /// 使用所有测量量创建平滑诊断快照。 + public PathSmoothingDiagnostics( + bool isFeasible, + double pathLengthMeters, + double maximumAbsoluteVehicleCurvaturePerMeter, + double rootMeanSquareVehicleCurvaturePerMeter, + double totalAbsoluteCurvatureVariationPerMeter, + double curvatureVariationEnergy, + double minimumBodyClearanceMeters, + double lengthChangePercent, + double peakCurvatureChangePercent, + double curvatureVariationChangePercent, + double minimumClearanceChangeMeters, + TimeSpan elapsed, + int retryCount, + double acceptedStrength, + string terminationReason = null) + : this( + new PathQualityMetrics( + isFeasible, + pathLengthMeters, + maximumAbsoluteVehicleCurvaturePerMeter, + rootMeanSquareVehicleCurvaturePerMeter, + totalAbsoluteCurvatureVariationPerMeter, + curvatureVariationEnergy, + minimumBodyClearanceMeters, + lengthChangePercent, + peakCurvatureChangePercent, + curvatureVariationChangePercent, + minimumClearanceChangeMeters), + elapsed, + retryCount, + acceptedStrength, + terminationReason) + { + } + + /// 路径质量指标;始终非空。 + public PathQualityMetrics Metrics { get; } + + /// 从算法入口到返回诊断的耗时。 + public TimeSpan Elapsed { get; } + + /// 已执行的安全强度重试次数。 + public int RetryCount { get; } + + /// 通过复核的平滑强度;未接受候选时为零。 + public double AcceptedStrength { get; } + + /// 面向调用方的稳定终止说明。 + public string TerminationReason { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs new file mode 100644 index 0000000..a9bc37e --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using MultiWheelC.TrajectoryPlanning.CoarsePath; +using MultiWheelC.TrajectoryPlanning.Mapping; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 路径平滑所需的原始粗路径、复核上下文和配置。 +public sealed class PathSmoothingRequest +{ + /// 创建路径平滑请求,并复制粗路径和方向分段集合。 + public PathSmoothingRequest( + IReadOnlyList coarsePath, + IReadOnlyList segments, + PlanningGridMap map, + VehicleParameters vehicle, + PathSmoothingConfiguration configuration) + { + CoarsePath = CopyReadOnly(coarsePath); + Segments = CopyReadOnly(segments); + Map = map; + Vehicle = vehicle; + Configuration = configuration; + } + + /// 原始粗路径的不可变快照。 + public IReadOnlyList CoarsePath { get; } + + /// 原始粗路径方向分段的不可变快照。 + public IReadOnlyList Segments { get; } + + /// 用于平滑后完整车体复核的规划栅格地图。 + public PlanningGridMap Map { get; } + + /// 车辆几何与最大曲率约束。 + public VehicleParameters Vehicle { get; } + + /// 本次平滑的配置。 + public PathSmoothingConfiguration Configuration { 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/Contracts/PathSmoothingResult.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs new file mode 100644 index 0000000..d63e094 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 路径平滑的最终不可变结果。 +public sealed class PathSmoothingResult +{ + private static readonly IReadOnlyList EmptyPath = + new ReadOnlyCollection(new List()); + private static readonly IReadOnlyList EmptySegments = + new ReadOnlyCollection(new List()); + + private PathSmoothingResult( + PathSmoothingStatus status, + SmoothingMethod? method, + IReadOnlyList path, + IReadOnlyList segments, + PathSmoothingDiagnostics diagnostics) + { + Status = status; + Method = method; + Path = path; + Segments = segments; + Diagnostics = diagnostics ?? new PathSmoothingDiagnostics( + new PathQualityMetrics(), + TimeSpan.Zero, + 0, + 0d, + "No smoothing diagnostics were supplied."); + } + + /// 最终发布状态。 + public PathSmoothingStatus Status { get; } + + /// 成功或回退时的实际(或尝试)平滑方法;失败时为空。 + public SmoothingMethod? Method { get; } + + /// 成功或经过复核的回退路径;其他状态始终为空且不可变。 + public IReadOnlyList Path { get; } + + /// 覆盖 的方向分段;其他状态始终为空且不可变。 + public IReadOnlyList Segments { get; } + + /// 本次平滑的质量和终止诊断;始终非空。 + public PathSmoothingDiagnostics Diagnostics { get; } + + /// 创建已通过所有复核的平滑结果。 + public static PathSmoothingResult Success( + SmoothingMethod method, + IReadOnlyList path, + IReadOnlyList segments, + PathSmoothingDiagnostics diagnostics) + { + ValidatePublishedPath(path, segments); + return new PathSmoothingResult( + PathSmoothingStatus.Success, + method, + CopyReadOnly(path), + CopyReadOnly(segments), + diagnostics); + } + + /// 创建经过完整复核的原始粗路径回退结果。 + public static PathSmoothingResult Fallback( + SmoothingMethod attemptedMethod, + IReadOnlyList path, + IReadOnlyList segments, + PathSmoothingDiagnostics diagnostics) + { + ValidatePublishedPath(path, segments); + return new PathSmoothingResult( + PathSmoothingStatus.FallbackToCoarsePath, + attemptedMethod, + CopyReadOnly(path), + CopyReadOnly(segments), + diagnostics); + } + + /// 创建不发布路径的失败、不可行、取消或输入无效结果。 + public static PathSmoothingResult Failure(PathSmoothingStatus status, PathSmoothingDiagnostics diagnostics) + { + if (status == PathSmoothingStatus.Success || status == PathSmoothingStatus.FallbackToCoarsePath) + throw new ArgumentException("Use Success or Fallback to publish a path.", nameof(status)); + if (!Enum.IsDefined(typeof(PathSmoothingStatus), status)) + throw new ArgumentOutOfRangeException(nameof(status)); + return new PathSmoothingResult(status, null, EmptyPath, EmptySegments, diagnostics); + } + + private static void ValidatePublishedPath( + IReadOnlyList path, + IReadOnlyList segments) + { + if (path == null || path.Count == 0) + throw new ArgumentException("Published smoothing results require a non-empty path.", nameof(path)); + if (segments == null || segments.Count == 0) + throw new ArgumentException("Published smoothing results require non-empty segments.", nameof(segments)); + } + + private static IReadOnlyList CopyReadOnly(IReadOnlyList source) + { + var copy = new List(source.Count); + for (int index = 0; index < source.Count; index++) + copy.Add(source[index]); + return new ReadOnlyCollection(copy); + } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs new file mode 100644 index 0000000..1212230 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs @@ -0,0 +1,12 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 路径平滑的最终发布状态。 +public enum PathSmoothingStatus +{ + Success, + FallbackToCoarsePath, + InvalidInput, + Infeasible, + Failed, + Cancelled, +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs new file mode 100644 index 0000000..4e45f06 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PiecewiseQuinticOptions.cs @@ -0,0 +1,11 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 分段五次 Hermite 平滑参数。 +public sealed class PiecewiseQuinticOptions +{ + /// 相邻内部结点的目标距离,单位 m。 + public double KnotSpacingMeters { get; set; } = 0.50d; + + /// 允许创建内部结点的最小间距,单位 m。 + public double MinimumKnotSpacingMeters { get; set; } = 0.10d; +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs new file mode 100644 index 0000000..5103a4a --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs @@ -0,0 +1,66 @@ +using MultiWheelC.TrajectoryPlanning.CoarsePath; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 平滑空间路径上的不可变采样点;位置和长度单位为 m,航向为 rad,曲率为 1/m。 +public sealed class SmoothedPathPoint +{ + public SmoothedPathPoint( + double xMeters, + double yMeters, + double headingRadians, + double unwrappedHeadingRadians, + double arcLengthMeters, + TravelDirection direction, + double geometricCurvaturePerMeter, + double vehicleCurvaturePerMeter, + double bodyClearanceMeters, + bool isGearSwitchPoint, + SmoothedPathPointSource source) + { + X = xMeters; + Y = yMeters; + Heading = headingRadians; + UnwrappedHeading = unwrappedHeadingRadians; + ArcLength = arcLengthMeters; + Direction = direction; + GeometricCurvature = geometricCurvaturePerMeter; + VehicleCurvature = vehicleCurvaturePerMeter; + BodyClearance = bodyClearanceMeters; + IsGearSwitchPoint = isGearSwitchPoint; + Source = source; + } + + /// 世界 X 坐标,单位 m。 + public double X { get; } + + /// 世界 Y 坐标,单位 m。 + public double Y { get; } + + /// 归一化的车辆航向,单位 rad。 + public double Heading { get; } + + /// 连续展开的车辆航向,单位 rad。 + public double UnwrappedHeading { get; } + + /// 从完整路径起点累计的弧长,单位 m。 + public double ArcLength { get; } + + /// 该点所在连续方向段的行驶方向。 + public TravelDirection Direction { get; } + + /// 按几何弧长计算的有符号路径曲率,单位 1/m。 + public double GeometricCurvature { get; } + + /// 车辆模型使用的有符号曲率,单位 1/m。 + public double VehicleCurvature { get; } + + /// 扩大车体后的保守净空下界,单位 m。 + public double BodyClearance { get; } + + /// 该点是否为新方向段开始的换向点。 + public bool IsGearSwitchPoint { get; } + + /// 该点在平滑流程中的来源。 + public SmoothedPathPointSource Source { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs new file mode 100644 index 0000000..f3140e1 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs @@ -0,0 +1,10 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 平滑路径点的来源。 +public enum SmoothedPathPointSource +{ + Anchor, + Interpolated, + GearSwitch, + CoarsePathFallback, +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs new file mode 100644 index 0000000..5ce1d95 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathSegment.cs @@ -0,0 +1,41 @@ +using MultiWheelC.TrajectoryPlanning.CoarsePath; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 平滑路径中方向一致的连续点范围;起止索引均包含在内。 +public sealed class SmoothedPathSegment +{ + public SmoothedPathSegment( + int segmentIndex, + TravelDirection direction, + int startIndex, + int endIndex, + bool startsAtGearSwitch, + bool endsAtGearSwitch) + { + SegmentIndex = segmentIndex; + Direction = direction; + StartIndex = startIndex; + EndIndex = endIndex; + StartsAtGearSwitch = startsAtGearSwitch; + EndsAtGearSwitch = endsAtGearSwitch; + } + + /// 从零开始的分段序号。 + public int SegmentIndex { get; } + + /// 本段行驶方向。 + public TravelDirection Direction { get; } + + /// 本段在平滑路径中的起始包含式索引。 + public int StartIndex { get; } + + /// 本段在平滑路径中的结束包含式索引。 + public int EndIndex { get; } + + /// 本段首点是否为换向后保留的新方向点。 + public bool StartsAtGearSwitch { get; } + + /// 本段末点是否紧邻下一方向段的换向对。 + public bool EndsAtGearSwitch { get; } +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs new file mode 100644 index 0000000..41d57e6 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs @@ -0,0 +1,9 @@ +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing; + +/// 支持的粗路径平滑方法。 +public enum SmoothingMethod +{ + CubicBSpline, + LocalCubicBezier, + PiecewiseQuintic, +} diff --git a/ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 b/ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 new file mode 100644 index 0000000..affdb25 --- /dev/null +++ b/ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 @@ -0,0 +1,179 @@ +param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll')) +$ErrorActionPreference = 'Stop' + +$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath)) +$root = 'MultiWheelC.TrajectoryPlanning.PathSmoothing.' +$coarsePathRoot = 'MultiWheelC.TrajectoryPlanning.CoarsePath.' +$mappingRoot = 'MultiWheelC.TrajectoryPlanning.Mapping.' + +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, [string]$Message) { + if ([Math]::Abs($Expected - $Actual) -gt 0.000001d) { + throw "$Message Expected=$Expected Actual=$Actual" + } +} + +function Assert-Throws([scriptblock]$Action, [string]$Message) { + $threw = $false + try { & $Action } + catch { $threw = $true } + if (-not $threw) { throw $Message } +} + +function Assert-ReadOnlyCollection($Collection, [string]$Message) { + $list = [System.Collections.IList]$Collection + Assert-True ($null -ne $list) "$Message The collection must implement IList." + Assert-True $list.IsReadOnly "$Message The collection must report IsReadOnly." + Assert-Throws { $list.Add($null) } "$Message The collection must reject Add." +} + +function Get-RequiredType([string]$Name) { + return $assembly.GetType($Name, $true) +} + +$configurationType = Get-RequiredType ($root + 'PathSmoothingConfiguration') +$methodType = Get-RequiredType ($root + 'SmoothingMethod') +$statusType = Get-RequiredType ($root + 'PathSmoothingStatus') +$sourceType = Get-RequiredType ($root + 'SmoothedPathPointSource') +$pointType = Get-RequiredType ($root + 'SmoothedPathPoint') +$segmentType = Get-RequiredType ($root + 'SmoothedPathSegment') +$bsplineOptionsType = Get-RequiredType ($root + 'CubicBSplineOptions') +$bezierOptionsType = Get-RequiredType ($root + 'LocalCubicBezierOptions') +$quinticOptionsType = Get-RequiredType ($root + 'PiecewiseQuinticOptions') +$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') +$metricsType = Get-RequiredType ($root + 'PathQualityMetrics') +$diagnosticsType = Get-RequiredType ($root + 'PathSmoothingDiagnostics') +$resultType = Get-RequiredType ($root + 'PathSmoothingResult') +$directionType = Get-RequiredType ($coarsePathRoot + 'TravelDirection') +$coarsePointType = Get-RequiredType ($coarsePathRoot + 'CoarsePathPoint') +$coarseSegmentType = Get-RequiredType ($coarsePathRoot + 'PathSegment') +$mapType = Get-RequiredType ($mappingRoot + 'PlanningGridMap') +$vehicleType = Get-RequiredType ($coarsePathRoot + 'VehicleParameters') + +Assert-Equal $true $methodType.IsEnum 'SmoothingMethod must be a public enum.' +Assert-Equal $true $statusType.IsEnum 'PathSmoothingStatus must be a public enum.' +Assert-Equal $true $sourceType.IsEnum 'SmoothedPathPointSource must be a public enum.' +Assert-Equal 'CubicBSpline' ([Enum]::GetNames($methodType)[0]) 'Smoothing method order must remain stable.' +Assert-Equal 'LocalCubicBezier' ([Enum]::GetNames($methodType)[1]) 'Smoothing method order must remain stable.' +Assert-Equal 'PiecewiseQuintic' ([Enum]::GetNames($methodType)[2]) 'Smoothing method order must remain stable.' +Assert-Equal 'Success' ([Enum]::GetNames($statusType)[0]) 'Smoothing status order must remain stable.' +Assert-Equal 'FallbackToCoarsePath' ([Enum]::GetNames($statusType)[1]) 'Fallback status must be explicit.' + +$configuration = [Activator]::CreateInstance($configurationType) +Assert-Near 0.05 $configuration.OutputSpacingMeters 'Default output spacing must be 0.05 m.' +Assert-Near 0.025 $configuration.MaximumCollisionCheckStepMeters 'Default collision step must be 0.025 m.' +Assert-Near 0.02 $configuration.MinimumClearanceReserveMeters 'Default clearance reserve must be 0.02 m.' +Assert-Near 1.0 $configuration.SmoothingStrength 'Default smoothing strength must be 1.0.' +Assert-Equal $true $configuration.AllowFallbackToCoarsePath 'Fallback must be enabled by default.' +Assert-Equal 4 $configuration.RetryStrengthScales.Count 'Retry schedule must contain four entries.' +Assert-Near 1.0 $configuration.RetryStrengthScales[0] 'First retry scale must be 1.0.' +Assert-Near 0.25 $configuration.RetryStrengthScales[3] 'Last retry scale must be 0.25.' +Assert-ReadOnlyCollection $configuration.RetryStrengthScales 'Retry schedule must be immutable.' +Assert-Near (1.0 / 3.0) ([Activator]::CreateInstance($bsplineOptionsType)).EndpointTangentScale 'B-spline endpoint tangent default must be one third.' +$bezier = [Activator]::CreateInstance($bezierOptionsType) +Assert-Near ([Math]::PI / 18.0) $bezier.CornerHeadingThresholdRadians 'Bezier corner threshold must be 10 degrees.' +Assert-Near 0.60 $bezier.MaximumWindowLengthMeters 'Bezier window default must be 0.60 m.' +Assert-Near (1.0 / 3.0) $bezier.HandleLengthRatio 'Bezier handle default must be one third.' +$quintic = [Activator]::CreateInstance($quinticOptionsType) +Assert-Near 0.50 $quintic.KnotSpacingMeters 'Quintic knot spacing must be 0.50 m.' +Assert-Near 0.10 $quintic.MinimumKnotSpacingMeters 'Quintic minimum knot spacing must be 0.10 m.' + +$forward = [Enum]::Parse($directionType, 'Forward') +$anchor = [Enum]::Parse($sourceType, 'Anchor') +$point = [Activator]::CreateInstance($pointType, @( + [double]1.25, [double]-2.50, [double]0.30, [double]6.58, [double]4.75, + $forward, [double]0.12, [double]0.12, [double]0.44, $false, $anchor)) +Assert-Near 1.25 $point.X 'Smoothed point X must be stored in m.' +Assert-Near -2.50 $point.Y 'Smoothed point Y must be stored in m.' +Assert-Near 0.30 $point.Heading 'Smoothed point heading must be stored in rad.' +Assert-Near 6.58 $point.UnwrappedHeading 'Smoothed point unwrapped heading must be stored in rad.' +Assert-Near 4.75 $point.ArcLength 'Smoothed point arc length must be stored in m.' +Assert-Equal 'Forward' $point.Direction.ToString() 'Smoothed point direction must be preserved.' +Assert-Near 0.12 $point.GeometricCurvature 'Smoothed point geometric curvature must be stored in 1/m.' +Assert-Near 0.12 $point.VehicleCurvature 'Smoothed point vehicle curvature must be stored in 1/m.' +Assert-Near 0.44 $point.BodyClearance 'Smoothed point clearance must be stored in m.' +Assert-Equal $false $point.IsGearSwitchPoint 'Smoothed point gear-switch marker must be preserved.' +Assert-Equal 'Anchor' $point.Source.ToString() 'Smoothed point source must be preserved.' + +$segmentA = [Activator]::CreateInstance($segmentType, @(0, $forward, 0, 2, $false, $true)) +$reverse = [Enum]::Parse($directionType, 'Reverse') +$segmentB = [Activator]::CreateInstance($segmentType, @(1, $reverse, 3, 5, $true, $false)) +Assert-Equal 0 $segmentA.SegmentIndex 'First smoothing segment index must be retained.' +Assert-Equal 'Forward' $segmentA.Direction.ToString() 'First smoothing segment direction must be retained.' +Assert-Equal 2 $segmentA.EndIndex 'First smoothing segment end index must be retained.' +Assert-Equal $true $segmentA.EndsAtGearSwitch 'First smoothing segment switch flag must be retained.' +Assert-Equal 1 $segmentB.SegmentIndex 'Second smoothing segment index must be retained.' +Assert-Equal 'Reverse' $segmentB.Direction.ToString() 'Second smoothing segment direction must be retained.' +Assert-Equal $true $segmentB.StartsAtGearSwitch 'Second smoothing segment switch flag must be retained.' + +$metrics = [Activator]::CreateInstance($metricsType) +Assert-Equal $false $metrics.IsFeasible 'Default metrics must be infeasible until analysis accepts a candidate.' +Assert-Near 0.0 $metrics.PathLengthMeters 'Default metrics must be zero-valued.' +Assert-Near 0.0 $metrics.MinimumBodyClearanceMeters 'Default metrics must be zero-valued.' +$diagnostics = [Activator]::CreateInstance($diagnosticsType) +Assert-True ($diagnostics.Metrics -ne $null) 'Default diagnostics must provide quality metrics.' +Assert-Equal 0 $diagnostics.RetryCount 'Default diagnostics must have no retries.' +Assert-Near 0.0 $diagnostics.AcceptedStrength 'Default diagnostics must have zero accepted strength.' + +$pointArray = [Array]::CreateInstance($pointType, 1) +$pointArray.SetValue($point, 0) +$segmentArray = [Array]::CreateInstance($segmentType, 2) +$segmentArray.SetValue($segmentA, 0) +$segmentArray.SetValue($segmentB, 1) +$method = [Enum]::Parse($methodType, 'CubicBSpline') +$successMethod = $resultType.GetMethod('Success') +Assert-True ($null -ne $successMethod) 'PathSmoothingResult must expose Success.' +$success = $successMethod.Invoke($null, @($method, $pointArray, $segmentArray, $diagnostics)) +Assert-Equal 'Success' $success.Status.ToString() 'Success factory must publish Success status.' +Assert-Equal 'CubicBSpline' $success.Method.ToString() 'Success factory must retain the selected method.' +Assert-Equal 1 $success.Path.Count 'Success factory must publish the provided path.' +Assert-Equal 2 $success.Segments.Count 'Success factory must publish the provided segments.' +Assert-ReadOnlyCollection $success.Path 'Success path must be immutable.' +Assert-ReadOnlyCollection $success.Segments 'Success segments must be immutable.' +$pointArray.SetValue($null, 0) +$segmentArray.SetValue($null, 0) +Assert-True ($null -ne $success.Path[0]) 'Success factory must copy path collections.' +Assert-True ($null -ne $success.Segments[0]) 'Success factory must copy segment collections.' + +$fallbackMethod = $resultType.GetMethod('Fallback') +Assert-True ($null -ne $fallbackMethod) 'PathSmoothingResult must expose Fallback.' +$fallbackPath = [Array]::CreateInstance($pointType, 1) +$fallbackPath.SetValue($point, 0) +$fallbackSegments = [Array]::CreateInstance($segmentType, 1) +$fallbackSegments.SetValue($segmentA, 0) +$fallback = $fallbackMethod.Invoke($null, @($method, $fallbackPath, $fallbackSegments, $diagnostics)) +Assert-Equal 'FallbackToCoarsePath' $fallback.Status.ToString() 'Fallback factory must publish an explicit fallback status.' +Assert-Equal 1 $fallback.Path.Count 'Fallback factory must publish a validated fallback path.' + +$failureMethod = $resultType.GetMethod('Failure') +Assert-True ($null -ne $failureMethod) 'PathSmoothingResult must expose Failure.' +$failed = $failureMethod.Invoke( + $null, + @([Enum]::Parse($statusType, 'InvalidInput'), + [Activator]::CreateInstance($diagnosticsType))) +Assert-Equal 'InvalidInput' $failed.Status.ToString() 'Failure factory must retain failure status.' +Assert-Equal 0 $failed.Path.Count 'Failure must publish no path.' +Assert-Equal 0 $failed.Segments.Count 'Failure must publish no segments.' +Assert-ReadOnlyCollection $failed.Path 'Failure path must be immutable.' +Assert-ReadOnlyCollection $failed.Segments 'Failure segments must be immutable.' +Assert-Throws { $failureMethod.Invoke($null, @([Enum]::Parse($statusType, 'Success'), $diagnostics)) } 'Failure factory must reject Success.' +Assert-Throws { $failureMethod.Invoke($null, @([Enum]::Parse($statusType, 'FallbackToCoarsePath'), $diagnostics)) } 'Failure factory must reject fallback status.' +Assert-Throws { $successMethod.Invoke($null, @($method, [Array]::CreateInstance($pointType, 0), $fallbackSegments, $diagnostics)) } 'Success factory must reject an empty path.' +Assert-Throws { $successMethod.Invoke($null, @($method, $fallbackPath, [Array]::CreateInstance($segmentType, 0), $diagnostics)) } 'Success factory must reject empty segments.' + +$requestConstructor = $requestType.GetConstructor(@( + [System.Collections.Generic.IReadOnlyList``1].MakeGenericType($coarsePointType), + [System.Collections.Generic.IReadOnlyList``1].MakeGenericType($coarseSegmentType), + $mapType, + $vehicleType, + $configurationType)) +Assert-True ($null -ne $requestConstructor) 'PathSmoothingRequest must expose the public five-argument constructor.' + +Write-Output 'Path smoothing contract checks passed.'