feat: add local G2 smoothing contracts

This commit is contained in:
梁薄云
2026-07-30 16:12:06 +08:00
parent f19034db1e
commit 3e1126936d
13 changed files with 364 additions and 9 deletions
@@ -0,0 +1,15 @@
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing;
/// <summary>局部 G2 五次过渡的可配置阈值。</summary>
public sealed class LocalG2QuinticOptions
{
public double MinimumWindowLengthMeters { get; set; } = 0.20d;
public double PreferredWindowLengthMeters { get; set; } = 0.50d;
public double MaximumWindowLengthMeters { get; set; } = 0.80d;
public double MaximumDeviationMeters { get; set; } = 0.10d;
public double AbsoluteCurvatureJumpFloorPerMeter { get; set; } = 0.001d;
public double CurvatureJumpRatioOfMaximum { get; set; } = 0.05d;
public double MinimumPeakGradientImprovementRatio { get; set; } = 0.20d;
public double MaximumVariationCostRegressionRatio { get; set; } = 0.02d;
public int MaximumCandidatesPerRegion { get; set; } = 12;
}
@@ -5,7 +5,7 @@ public sealed class PathQualityMetrics
{ {
/// <summary>创建全零、不可行的质量指标。</summary> /// <summary>创建全零、不可行的质量指标。</summary>
public PathQualityMetrics() public PathQualityMetrics()
: this(false, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d) : this(false, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d, 0d)
{ {
} }
@@ -22,10 +22,41 @@ public sealed class PathQualityMetrics
double peakCurvatureChangePercent, double peakCurvatureChangePercent,
double curvatureVariationChangePercent, double curvatureVariationChangePercent,
double minimumClearanceChangeMeters) double minimumClearanceChangeMeters)
: this(
isFeasible,
pathLengthMeters,
maximumAbsoluteVehicleCurvaturePerMeter,
0d,
rootMeanSquareVehicleCurvaturePerMeter,
totalAbsoluteCurvatureVariationPerMeter,
curvatureVariationEnergy,
minimumBodyClearanceMeters,
lengthChangePercent,
peakCurvatureChangePercent,
curvatureVariationChangePercent,
minimumClearanceChangeMeters)
{
}
/// <summary>创建带有曲率导数峰值的完整质量指标快照。</summary>
public PathQualityMetrics(
bool isFeasible,
double pathLengthMeters,
double maximumAbsoluteVehicleCurvaturePerMeter,
double maximumAbsoluteVehicleCurvatureDerivativePerSquareMeter,
double rootMeanSquareVehicleCurvaturePerMeter,
double totalAbsoluteCurvatureVariationPerMeter,
double curvatureVariationEnergy,
double minimumBodyClearanceMeters,
double lengthChangePercent,
double peakCurvatureChangePercent,
double curvatureVariationChangePercent,
double minimumClearanceChangeMeters)
{ {
IsFeasible = isFeasible; IsFeasible = isFeasible;
PathLengthMeters = pathLengthMeters; PathLengthMeters = pathLengthMeters;
MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter; MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter;
MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter = maximumAbsoluteVehicleCurvatureDerivativePerSquareMeter;
RootMeanSquareVehicleCurvaturePerMeter = rootMeanSquareVehicleCurvaturePerMeter; RootMeanSquareVehicleCurvaturePerMeter = rootMeanSquareVehicleCurvaturePerMeter;
TotalAbsoluteCurvatureVariationPerMeter = totalAbsoluteCurvatureVariationPerMeter; TotalAbsoluteCurvatureVariationPerMeter = totalAbsoluteCurvatureVariationPerMeter;
CurvatureVariationEnergy = curvatureVariationEnergy; CurvatureVariationEnergy = curvatureVariationEnergy;
@@ -45,6 +76,9 @@ public sealed class PathQualityMetrics
/// <summary>绝对车辆曲率峰值,单位 1/m。</summary> /// <summary>绝对车辆曲率峰值,单位 1/m。</summary>
public double MaximumAbsoluteVehicleCurvaturePerMeter { get; } public double MaximumAbsoluteVehicleCurvaturePerMeter { get; }
/// <summary>绝对车辆曲率导数峰值,单位 1/m²。</summary>
public double MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter { get; }
/// <summary>车辆曲率均方根,单位 1/m。</summary> /// <summary>车辆曲率均方根,单位 1/m。</summary>
public double RootMeanSquareVehicleCurvaturePerMeter { get; } public double RootMeanSquareVehicleCurvaturePerMeter { get; }
@@ -54,6 +88,9 @@ public sealed class PathQualityMetrics
/// <summary>逐方向段计算的曲率变化能量。</summary> /// <summary>逐方向段计算的曲率变化能量。</summary>
public double CurvatureVariationEnergy { get; } public double CurvatureVariationEnergy { get; }
/// <summary>曲率变化代价的兼容名称。</summary>
public double CurvatureVariationCost => CurvatureVariationEnergy;
/// <summary>完整扩大车体的最小保守净空,单位 m。</summary> /// <summary>完整扩大车体的最小保守净空,单位 m。</summary>
public double MinimumBodyClearanceMeters { get; } public double MinimumBodyClearanceMeters { get; }
@@ -47,4 +47,7 @@ public sealed class PathSmoothingConfiguration
/// <summary>分段五次多项式专用参数。</summary> /// <summary>分段五次多项式专用参数。</summary>
public PiecewiseQuinticOptions PiecewiseQuintic { get; } = new PiecewiseQuinticOptions(); public PiecewiseQuinticOptions PiecewiseQuintic { get; } = new PiecewiseQuinticOptions();
/// <summary>局部 G2 五次过渡专用参数。</summary>
public LocalG2QuinticOptions LocalG2Quintic { get; } = new LocalG2QuinticOptions();
} }
@@ -0,0 +1,17 @@
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing;
/// <summary>局部 G2 区域未替换原始路径的稳定原因。</summary>
public enum PathSmoothingRegionFailureReason
{
None,
WindowUnavailable,
CandidateGenerationFailed,
Collision,
InsufficientClearance,
CurvatureExceeded,
CurvatureOvershoot,
DeviationExceeded,
InsufficientImprovement,
VariationCostRegression,
GlobalValidationRollback,
}
@@ -0,0 +1,83 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing;
/// <summary>单个局部 G2 平滑区域的不可变发布报告。</summary>
public sealed class PathSmoothingRegionReport
{
public PathSmoothingRegionReport(
int segmentIndex,
double startArcLengthMeters,
double endArcLengthMeters,
IReadOnlyList<double> curvatureJumpsPerMeter,
double plannedWindowLengthMeters,
double actualWindowLengthMeters,
double leftWindowLengthMeters,
double rightWindowLengthMeters,
int candidateCount,
int selectedCandidateIndex,
PathSmoothingRegionStatus status,
PathSmoothingRegionFailureReason failureReason,
double rawPeakCurvatureDerivativePerSquareMeter,
double resultPeakCurvatureDerivativePerSquareMeter,
double rawCurvatureVariationCost,
double resultCurvatureVariationCost,
double maximumDeviationMeters,
double minimumBodyClearanceMeters,
double maximumAbsoluteVehicleCurvaturePerMeter)
{
SegmentIndex = segmentIndex;
StartArcLengthMeters = startArcLengthMeters;
EndArcLengthMeters = endArcLengthMeters;
CurvatureJumpsPerMeter = CopyReadOnly(curvatureJumpsPerMeter);
PlannedWindowLengthMeters = plannedWindowLengthMeters;
ActualWindowLengthMeters = actualWindowLengthMeters;
LeftWindowLengthMeters = leftWindowLengthMeters;
RightWindowLengthMeters = rightWindowLengthMeters;
CandidateCount = candidateCount;
SelectedCandidateIndex = status == PathSmoothingRegionStatus.Improved
? selectedCandidateIndex
: -1;
Status = status;
FailureReason = failureReason;
RawPeakCurvatureDerivativePerSquareMeter = rawPeakCurvatureDerivativePerSquareMeter;
ResultPeakCurvatureDerivativePerSquareMeter = resultPeakCurvatureDerivativePerSquareMeter;
RawCurvatureVariationCost = rawCurvatureVariationCost;
ResultCurvatureVariationCost = resultCurvatureVariationCost;
MaximumDeviationMeters = maximumDeviationMeters;
MinimumBodyClearanceMeters = minimumBodyClearanceMeters;
MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter;
}
public int SegmentIndex { get; }
public double StartArcLengthMeters { get; }
public double EndArcLengthMeters { get; }
public IReadOnlyList<double> CurvatureJumpsPerMeter { get; }
public double PlannedWindowLengthMeters { get; }
public double ActualWindowLengthMeters { get; }
public double LeftWindowLengthMeters { get; }
public double RightWindowLengthMeters { get; }
public int CandidateCount { get; }
public int SelectedCandidateIndex { get; }
public PathSmoothingRegionStatus Status { get; }
public PathSmoothingRegionFailureReason FailureReason { get; }
public double RawPeakCurvatureDerivativePerSquareMeter { get; }
public double ResultPeakCurvatureDerivativePerSquareMeter { get; }
public double RawCurvatureVariationCost { get; }
public double ResultCurvatureVariationCost { get; }
public double MaximumDeviationMeters { get; }
public double MinimumBodyClearanceMeters { get; }
public double MaximumAbsoluteVehicleCurvaturePerMeter { get; }
private static IReadOnlyList<double> CopyReadOnly(IReadOnlyList<double> source)
{
var copy = new List<double>(source == null ? 0 : source.Count);
if (source != null)
{
for (int index = 0; index < source.Count; index++)
copy.Add(source[index]);
}
return new ReadOnlyCollection<double>(copy);
}
}
@@ -0,0 +1,8 @@
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing;
/// <summary>单个局部 G2 平滑区域的处理结果。</summary>
public enum PathSmoothingRegionStatus
{
Improved,
RetainedOriginal,
}
@@ -83,6 +83,15 @@ public sealed class PathSmoothingRequest
copy.LocalCubicBezier.HandleLengthRatio = source.LocalCubicBezier.HandleLengthRatio; copy.LocalCubicBezier.HandleLengthRatio = source.LocalCubicBezier.HandleLengthRatio;
copy.PiecewiseQuintic.KnotSpacingMeters = source.PiecewiseQuintic.KnotSpacingMeters; copy.PiecewiseQuintic.KnotSpacingMeters = source.PiecewiseQuintic.KnotSpacingMeters;
copy.PiecewiseQuintic.MinimumKnotSpacingMeters = source.PiecewiseQuintic.MinimumKnotSpacingMeters; copy.PiecewiseQuintic.MinimumKnotSpacingMeters = source.PiecewiseQuintic.MinimumKnotSpacingMeters;
copy.LocalG2Quintic.MinimumWindowLengthMeters = source.LocalG2Quintic.MinimumWindowLengthMeters;
copy.LocalG2Quintic.PreferredWindowLengthMeters = source.LocalG2Quintic.PreferredWindowLengthMeters;
copy.LocalG2Quintic.MaximumWindowLengthMeters = source.LocalG2Quintic.MaximumWindowLengthMeters;
copy.LocalG2Quintic.MaximumDeviationMeters = source.LocalG2Quintic.MaximumDeviationMeters;
copy.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter = source.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter;
copy.LocalG2Quintic.CurvatureJumpRatioOfMaximum = source.LocalG2Quintic.CurvatureJumpRatioOfMaximum;
copy.LocalG2Quintic.MinimumPeakGradientImprovementRatio = source.LocalG2Quintic.MinimumPeakGradientImprovementRatio;
copy.LocalG2Quintic.MaximumVariationCostRegressionRatio = source.LocalG2Quintic.MaximumVariationCostRegressionRatio;
copy.LocalG2Quintic.MaximumCandidatesPerRegion = source.LocalG2Quintic.MaximumCandidatesPerRegion;
return copy; return copy;
} }
} }
@@ -11,18 +11,22 @@ public sealed class PathSmoothingResult
new ReadOnlyCollection<SmoothedPathPoint>(new List<SmoothedPathPoint>()); new ReadOnlyCollection<SmoothedPathPoint>(new List<SmoothedPathPoint>());
private static readonly IReadOnlyList<SmoothedPathSegment> EmptySegments = private static readonly IReadOnlyList<SmoothedPathSegment> EmptySegments =
new ReadOnlyCollection<SmoothedPathSegment>(new List<SmoothedPathSegment>()); new ReadOnlyCollection<SmoothedPathSegment>(new List<SmoothedPathSegment>());
private static readonly IReadOnlyList<PathSmoothingRegionReport> EmptyRegionReports =
new ReadOnlyCollection<PathSmoothingRegionReport>(new List<PathSmoothingRegionReport>());
private PathSmoothingResult( private PathSmoothingResult(
PathSmoothingStatus status, PathSmoothingStatus status,
SmoothingMethod? method, SmoothingMethod? method,
IReadOnlyList<SmoothedPathPoint> path, IReadOnlyList<SmoothedPathPoint> path,
IReadOnlyList<SmoothedPathSegment> segments, IReadOnlyList<SmoothedPathSegment> segments,
PathSmoothingDiagnostics diagnostics) PathSmoothingDiagnostics diagnostics,
IReadOnlyList<PathSmoothingRegionReport> regionReports)
{ {
Status = status; Status = status;
Method = method; Method = method;
Path = path; Path = path;
Segments = segments; Segments = segments;
RegionReports = regionReports;
Diagnostics = diagnostics ?? new PathSmoothingDiagnostics( Diagnostics = diagnostics ?? new PathSmoothingDiagnostics(
new PathQualityMetrics(), new PathQualityMetrics(),
TimeSpan.Zero, TimeSpan.Zero,
@@ -43,6 +47,9 @@ public sealed class PathSmoothingResult
/// <summary>覆盖 <see cref="Path"/> 的方向分段;其他状态始终为空且不可变。</summary> /// <summary>覆盖 <see cref="Path"/> 的方向分段;其他状态始终为空且不可变。</summary>
public IReadOnlyList<SmoothedPathSegment> Segments { get; } public IReadOnlyList<SmoothedPathSegment> Segments { get; }
/// <summary>局部 G2 各检测区域的不可变报告;传统算法结果为空。</summary>
public IReadOnlyList<PathSmoothingRegionReport> RegionReports { get; }
/// <summary>本次平滑的质量和终止诊断;始终非空。</summary> /// <summary>本次平滑的质量和终止诊断;始终非空。</summary>
public PathSmoothingDiagnostics Diagnostics { get; } public PathSmoothingDiagnostics Diagnostics { get; }
@@ -59,7 +66,8 @@ public sealed class PathSmoothingResult
method, method,
CopyReadOnly(path), CopyReadOnly(path),
CopyReadOnly(segments), CopyReadOnly(segments),
diagnostics); diagnostics,
EmptyRegionReports);
} }
/// <summary>创建经过完整复核的原始粗路径回退结果。</summary> /// <summary>创建经过完整复核的原始粗路径回退结果。</summary>
@@ -75,17 +83,49 @@ public sealed class PathSmoothingResult
attemptedMethod, attemptedMethod,
CopyReadOnly(path), CopyReadOnly(path),
CopyReadOnly(segments), CopyReadOnly(segments),
diagnostics); diagnostics,
EmptyRegionReports);
}
/// <summary>发布经过完整复核的局部 G2 预平滑结果。</summary>
public static PathSmoothingResult PublishLocalG2(
PathSmoothingStatus status,
IReadOnlyList<SmoothedPathPoint> path,
IReadOnlyList<SmoothedPathSegment> segments,
PathSmoothingDiagnostics diagnostics,
IReadOnlyList<PathSmoothingRegionReport> regionReports)
{
if (status != PathSmoothingStatus.Complete &&
status != PathSmoothingStatus.PartialImprovement &&
status != PathSmoothingStatus.NotNeeded &&
status != PathSmoothingStatus.Unchanged)
throw new ArgumentException("Use a Local G2 publication status.", nameof(status));
if (regionReports == null)
throw new ArgumentNullException(nameof(regionReports));
ValidatePublishedResult(SmoothingMethod.LocalG2Quintic, path, segments, diagnostics);
return new PathSmoothingResult(
status,
SmoothingMethod.LocalG2Quintic,
CopyReadOnly(path),
CopyReadOnly(segments),
diagnostics,
CopyReadOnly(regionReports));
} }
/// <summary>创建不发布路径的失败、不可行、取消或输入无效结果。</summary> /// <summary>创建不发布路径的失败、不可行、取消或输入无效结果。</summary>
public static PathSmoothingResult Failure(PathSmoothingStatus status, PathSmoothingDiagnostics diagnostics) public static PathSmoothingResult Failure(PathSmoothingStatus status, PathSmoothingDiagnostics diagnostics)
{ {
if (status == PathSmoothingStatus.Success || status == PathSmoothingStatus.FallbackToCoarsePath) if (status == PathSmoothingStatus.Success ||
status == PathSmoothingStatus.FallbackToCoarsePath ||
status == PathSmoothingStatus.Complete ||
status == PathSmoothingStatus.PartialImprovement ||
status == PathSmoothingStatus.NotNeeded ||
status == PathSmoothingStatus.Unchanged)
throw new ArgumentException("Use Success or Fallback to publish a path.", nameof(status)); throw new ArgumentException("Use Success or Fallback to publish a path.", nameof(status));
if (!Enum.IsDefined(typeof(PathSmoothingStatus), status)) if (!Enum.IsDefined(typeof(PathSmoothingStatus), status))
throw new ArgumentOutOfRangeException(nameof(status)); throw new ArgumentOutOfRangeException(nameof(status));
return new PathSmoothingResult(status, null, EmptyPath, EmptySegments, diagnostics); return new PathSmoothingResult(status, null, EmptyPath, EmptySegments, diagnostics, EmptyRegionReports);
} }
private static void ValidatePublishedResult( private static void ValidatePublishedResult(
@@ -9,4 +9,8 @@ public enum PathSmoothingStatus
Infeasible, Infeasible,
Failed, Failed,
Cancelled, Cancelled,
Complete,
PartialImprovement,
NotNeeded,
Unchanged,
} }
@@ -17,6 +17,36 @@ public sealed class SmoothedPathPoint
double bodyClearanceMeters, double bodyClearanceMeters,
bool isGearSwitchPoint, bool isGearSwitchPoint,
SmoothedPathPointSource source) SmoothedPathPointSource source)
: this(
xMeters,
yMeters,
headingRadians,
unwrappedHeadingRadians,
arcLengthMeters,
direction,
geometricCurvaturePerMeter,
vehicleCurvaturePerMeter,
0d,
bodyClearanceMeters,
isGearSwitchPoint,
source)
{
}
/// <summary>创建带有车辆曲率对弧长导数的不可变采样点。</summary>
public SmoothedPathPoint(
double xMeters,
double yMeters,
double headingRadians,
double unwrappedHeadingRadians,
double arcLengthMeters,
TravelDirection direction,
double geometricCurvaturePerMeter,
double vehicleCurvaturePerMeter,
double vehicleCurvatureDerivativePerSquareMeter,
double bodyClearanceMeters,
bool isGearSwitchPoint,
SmoothedPathPointSource source)
{ {
X = xMeters; X = xMeters;
Y = yMeters; Y = yMeters;
@@ -26,6 +56,7 @@ public sealed class SmoothedPathPoint
Direction = direction; Direction = direction;
GeometricCurvature = geometricCurvaturePerMeter; GeometricCurvature = geometricCurvaturePerMeter;
VehicleCurvature = vehicleCurvaturePerMeter; VehicleCurvature = vehicleCurvaturePerMeter;
VehicleCurvatureDerivative = vehicleCurvatureDerivativePerSquareMeter;
BodyClearance = bodyClearanceMeters; BodyClearance = bodyClearanceMeters;
IsGearSwitchPoint = isGearSwitchPoint; IsGearSwitchPoint = isGearSwitchPoint;
Source = source; Source = source;
@@ -55,6 +86,9 @@ public sealed class SmoothedPathPoint
/// <summary>车辆模型使用的有符号曲率,单位 1/m。</summary> /// <summary>车辆模型使用的有符号曲率,单位 1/m。</summary>
public double VehicleCurvature { get; } public double VehicleCurvature { get; }
/// <summary>车辆曲率对弧长的导数 dκ/ds,单位 1/m²。</summary>
public double VehicleCurvatureDerivative { get; }
/// <summary>扩大车体后的保守净空下界,单位 m。</summary> /// <summary>扩大车体后的保守净空下界,单位 m。</summary>
public double BodyClearance { get; } public double BodyClearance { get; }
@@ -7,4 +7,5 @@ public enum SmoothedPathPointSource
Interpolated, Interpolated,
GearSwitch, GearSwitch,
CoarsePathFallback, CoarsePathFallback,
LocalG2Transition,
} }
@@ -6,4 +6,5 @@ public enum SmoothingMethod
CubicBSpline, CubicBSpline,
LocalCubicBezier, LocalCubicBezier,
PiecewiseQuintic, PiecewiseQuintic,
LocalG2Quintic,
} }
@@ -47,6 +47,10 @@ $segmentType = Get-RequiredType ($root + 'SmoothedPathSegment')
$bsplineOptionsType = Get-RequiredType ($root + 'CubicBSplineOptions') $bsplineOptionsType = Get-RequiredType ($root + 'CubicBSplineOptions')
$bezierOptionsType = Get-RequiredType ($root + 'LocalCubicBezierOptions') $bezierOptionsType = Get-RequiredType ($root + 'LocalCubicBezierOptions')
$quinticOptionsType = Get-RequiredType ($root + 'PiecewiseQuinticOptions') $quinticOptionsType = Get-RequiredType ($root + 'PiecewiseQuinticOptions')
$localOptionsType = Get-RequiredType ($root + 'LocalG2QuinticOptions')
$regionStatusType = Get-RequiredType ($root + 'PathSmoothingRegionStatus')
$regionFailureType = Get-RequiredType ($root + 'PathSmoothingRegionFailureReason')
$regionReportType = Get-RequiredType ($root + 'PathSmoothingRegionReport')
$requestType = Get-RequiredType ($root + 'PathSmoothingRequest') $requestType = Get-RequiredType ($root + 'PathSmoothingRequest')
$metricsType = Get-RequiredType ($root + 'PathQualityMetrics') $metricsType = Get-RequiredType ($root + 'PathQualityMetrics')
$diagnosticsType = Get-RequiredType ($root + 'PathSmoothingDiagnostics') $diagnosticsType = Get-RequiredType ($root + 'PathSmoothingDiagnostics')
@@ -60,9 +64,11 @@ $vehicleType = Get-RequiredType ($coarsePathRoot + 'VehicleParameters')
Assert-Equal $true $methodType.IsEnum 'SmoothingMethod must be a public enum.' 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 $statusType.IsEnum 'PathSmoothingStatus must be a public enum.'
Assert-Equal $true $sourceType.IsEnum 'SmoothedPathPointSource must be a public enum.' Assert-Equal $true $sourceType.IsEnum 'SmoothedPathPointSource must be a public enum.'
Assert-Equal 'CubicBSpline,LocalCubicBezier,PiecewiseQuintic' ([string]::Join(',', [Enum]::GetNames($methodType))) 'Smoothing method members and order must remain stable.' Assert-Equal 'CubicBSpline,LocalCubicBezier,PiecewiseQuintic,LocalG2Quintic' ([string]::Join(',', [Enum]::GetNames($methodType))) 'The Local G2 method must be appended without reordering legacy methods.'
Assert-Equal 'Success,FallbackToCoarsePath,InvalidInput,Infeasible,Failed,Cancelled' ([string]::Join(',', [Enum]::GetNames($statusType))) 'Smoothing status members and order must remain stable.' Assert-Equal 'Success,FallbackToCoarsePath,InvalidInput,Infeasible,Failed,Cancelled,Complete,PartialImprovement,NotNeeded,Unchanged' ([string]::Join(',', [Enum]::GetNames($statusType))) 'Local G2 statuses must be appended without reordering legacy statuses.'
Assert-Equal 'Anchor,Interpolated,GearSwitch,CoarsePathFallback' ([string]::Join(',', [Enum]::GetNames($sourceType))) 'Smoothed point source members and order must remain stable.' Assert-Equal 'Anchor,Interpolated,GearSwitch,CoarsePathFallback,LocalG2Transition' ([string]::Join(',', [Enum]::GetNames($sourceType))) 'Local G2 point source must be appended without reordering legacy sources.'
Assert-Equal 'Improved,RetainedOriginal' ([string]::Join(',', [Enum]::GetNames($regionStatusType))) 'Local G2 region statuses must be stable.'
Assert-Equal 'None,WindowUnavailable,CandidateGenerationFailed,Collision,InsufficientClearance,CurvatureExceeded,CurvatureOvershoot,DeviationExceeded,InsufficientImprovement,VariationCostRegression,GlobalValidationRollback' ([string]::Join(',', [Enum]::GetNames($regionFailureType))) 'Local G2 region failure reasons must be stable.'
$configuration = [Activator]::CreateInstance($configurationType) $configuration = [Activator]::CreateInstance($configurationType)
Assert-Near 0.05 $configuration.OutputSpacingMeters 'Default output spacing must be 0.05 m.' Assert-Near 0.05 $configuration.OutputSpacingMeters 'Default output spacing must be 0.05 m.'
@@ -87,6 +93,16 @@ Assert-Near (1.0 / 3.0) $bezier.HandleLengthRatio 'Bezier handle default must be
$quintic = [Activator]::CreateInstance($quinticOptionsType) $quintic = [Activator]::CreateInstance($quinticOptionsType)
Assert-Near 0.50 $quintic.KnotSpacingMeters 'Quintic knot spacing must be 0.50 m.' 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.' Assert-Near 0.10 $quintic.MinimumKnotSpacingMeters 'Quintic minimum knot spacing must be 0.10 m.'
$local = [Activator]::CreateInstance($localOptionsType)
Assert-Near 0.20 $local.MinimumWindowLengthMeters 'Minimum Local G2 window must be 0.20 m.'
Assert-Near 0.50 $local.PreferredWindowLengthMeters 'Preferred Local G2 window must be 0.50 m.'
Assert-Near 0.80 $local.MaximumWindowLengthMeters 'Maximum Local G2 window must be 0.80 m.'
Assert-Near 0.10 $local.MaximumDeviationMeters 'Maximum Local G2 deviation must be 0.10 m.'
Assert-Near 0.001 $local.AbsoluteCurvatureJumpFloorPerMeter 'Absolute jump floor must be 0.001 1/m.'
Assert-Near 0.05 $local.CurvatureJumpRatioOfMaximum 'Relative jump threshold must be 5 percent.'
Assert-Near 0.20 $local.MinimumPeakGradientImprovementRatio 'Peak improvement must be 20 percent.'
Assert-Near 0.02 $local.MaximumVariationCostRegressionRatio 'Variation cost tolerance must be 2 percent.'
Assert-Equal 12 $local.MaximumCandidatesPerRegion 'At most twelve candidates are allowed.'
$forward = [Enum]::Parse($directionType, 'Forward') $forward = [Enum]::Parse($directionType, 'Forward')
$anchor = [Enum]::Parse($sourceType, 'Anchor') $anchor = [Enum]::Parse($sourceType, 'Anchor')
@@ -104,6 +120,11 @@ Assert-Near 0.12 $point.VehicleCurvature 'Smoothed point vehicle curvature must
Assert-Near 0.44 $point.BodyClearance 'Smoothed point clearance must be stored in 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 $false $point.IsGearSwitchPoint 'Smoothed point gear-switch marker must be preserved.'
Assert-Equal 'Anchor' $point.Source.ToString() 'Smoothed point source must be preserved.' Assert-Equal 'Anchor' $point.Source.ToString() 'Smoothed point source must be preserved.'
Assert-True ($pointType.GetProperty('VehicleCurvatureDerivative') -ne $null) 'Smoothed points must expose d-kappa/d-s.'
$pointWithDerivative = [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.37, [double]0.44, $false, $anchor))
Assert-Near 0.37 $pointWithDerivative.VehicleCurvatureDerivative 'Smoothed point curvature derivative must be stored in 1/m^2.'
$segmentA = [Activator]::CreateInstance($segmentType, @(0, $forward, 0, 2, $false, $true)) $segmentA = [Activator]::CreateInstance($segmentType, @(0, $forward, 0, 2, $false, $true))
$reverse = [Enum]::Parse($directionType, 'Reverse') $reverse = [Enum]::Parse($directionType, 'Reverse')
@@ -120,6 +141,13 @@ $metrics = [Activator]::CreateInstance($metricsType)
Assert-Equal $false $metrics.IsFeasible 'Default metrics must be infeasible until analysis accepts a candidate.' 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.PathLengthMeters 'Default metrics must be zero-valued.'
Assert-Near 0.0 $metrics.MinimumBodyClearanceMeters 'Default metrics must be zero-valued.' Assert-Near 0.0 $metrics.MinimumBodyClearanceMeters 'Default metrics must be zero-valued.'
Assert-Near 0.0 $metrics.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter 'Default derivative metric must be zero-valued.'
Assert-Near 0.0 $metrics.CurvatureVariationCost 'Curvature variation cost compatibility alias must be available.'
$metricsWithDerivative = [Activator]::CreateInstance($metricsType, @(
$true,
[double]1.0, [double]0.50, [double]0.75, [double]0.0, [double]0.0,
[double]0.0, [double]0.25, [double]0.0, [double]0.0, [double]0.0, [double]0.0))
Assert-Near 0.75 $metricsWithDerivative.MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter 'Derivative-aware metrics constructor must retain the peak derivative.'
$diagnostics = [Activator]::CreateInstance($diagnosticsType) $diagnostics = [Activator]::CreateInstance($diagnosticsType)
Assert-True ($diagnostics.Metrics -ne $null) 'Default diagnostics must provide quality metrics.' Assert-True ($diagnostics.Metrics -ne $null) 'Default diagnostics must provide quality metrics.'
Assert-Equal 0 $diagnostics.RetryCount 'Default diagnostics must have no retries.' Assert-Equal 0 $diagnostics.RetryCount 'Default diagnostics must have no retries.'
@@ -166,6 +194,7 @@ Assert-Throws { $fallbackMethod.Invoke($null, @([Enum]::ToObject($methodType, 99
$fallback = $fallbackMethod.Invoke($null, @($method, $fallbackPath, $fallbackSegments, $feasibleDiagnostics)) $fallback = $fallbackMethod.Invoke($null, @($method, $fallbackPath, $fallbackSegments, $feasibleDiagnostics))
Assert-Equal 'FallbackToCoarsePath' $fallback.Status.ToString() 'Fallback factory must publish an explicit fallback status.' 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.' Assert-Equal 1 $fallback.Path.Count 'Fallback factory must publish a validated fallback path.'
Assert-Equal 0 $fallback.RegionReports.Count 'Legacy fallback results must publish empty immutable region reports.'
$failureMethod = $resultType.GetMethod('Failure') $failureMethod = $resultType.GetMethod('Failure')
Assert-True ($null -ne $failureMethod) 'PathSmoothingResult must expose Failure.' Assert-True ($null -ne $failureMethod) 'PathSmoothingResult must expose Failure.'
@@ -183,6 +212,44 @@ Assert-Throws { $failureMethod.Invoke($null, @([Enum]::Parse($statusType, 'Fallb
Assert-Throws { $failureMethod.Invoke($null, @([Enum]::ToObject($statusType, 99), $diagnostics)) } 'Failure factory must reject undefined statuses.' Assert-Throws { $failureMethod.Invoke($null, @([Enum]::ToObject($statusType, 99), $diagnostics)) } 'Failure factory must reject undefined statuses.'
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, [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.' Assert-Throws { $successMethod.Invoke($null, @($method, $fallbackPath, [Array]::CreateInstance($segmentType, 0), $diagnostics)) } 'Success factory must reject empty segments.'
Assert-Equal 0 $success.RegionReports.Count 'Legacy success results must publish empty immutable region reports.'
Assert-ReadOnlyCollection $success.RegionReports 'Legacy success region reports must be immutable.'
$curvatureJumps = [System.Collections.Generic.List[double]]::new()
$curvatureJumps.Add([double]0.20)
$report = [Activator]::CreateInstance($regionReportType, @(
0, [double]0.0, [double]0.5, $curvatureJumps,
[double]0.5, [double]0.5, [double]0.25, [double]0.25,
1, 0,
[Enum]::Parse($regionStatusType, 'Improved'), [Enum]::Parse($regionFailureType, 'None'),
[double]1.0, [double]0.5, [double]2.0, [double]1.0,
[double]0.05, [double]0.10, [double]0.80))
Assert-ReadOnlyCollection $report.CurvatureJumpsPerMeter 'Region report curvature jumps must be immutable.'
$curvatureJumps[0] = [double]9.99
Assert-Near 0.20 $report.CurvatureJumpsPerMeter[0] 'Region report must copy curvature jumps.'
$retainedReport = [Activator]::CreateInstance($regionReportType, @(
0, [double]0.0, [double]0.5, $curvatureJumps,
[double]0.5, [double]0.5, [double]0.25, [double]0.25,
1, 7,
[Enum]::Parse($regionStatusType, 'RetainedOriginal'), [Enum]::Parse($regionFailureType, 'InsufficientImprovement'),
[double]1.0, [double]1.0, [double]2.0, [double]2.0,
[double]0.0, [double]0.10, [double]0.80))
Assert-Equal -1 $retainedReport.SelectedCandidateIndex 'A region without a selected candidate must publish -1.'
$publishLocalG2Method = $resultType.GetMethod('PublishLocalG2')
Assert-True ($null -ne $publishLocalG2Method) 'PathSmoothingResult must expose PublishLocalG2.'
$reports = [Array]::CreateInstance($regionReportType, 1)
$reports.SetValue($report, 0)
$localG2Method = [Enum]::Parse($methodType, 'LocalG2Quintic')
$complete = [Enum]::Parse($statusType, 'Complete')
$localG2Result = $publishLocalG2Method.Invoke($null, @($complete, $fallbackPath, $fallbackSegments, $feasibleDiagnostics, $reports))
Assert-Equal 'Complete' $localG2Result.Status.ToString() 'PublishLocalG2 must retain Local G2 publication status.'
Assert-Equal 'LocalG2Quintic' $localG2Result.Method.ToString() 'PublishLocalG2 must publish the Local G2 method.'
Assert-Equal 1 $localG2Result.RegionReports.Count 'PublishLocalG2 must publish region reports.'
Assert-ReadOnlyCollection $localG2Result.RegionReports 'Local G2 result region reports must be immutable.'
$reports.SetValue($null, 0)
Assert-True ($null -ne $localG2Result.RegionReports[0]) 'PublishLocalG2 must copy region reports.'
Assert-Throws { $publishLocalG2Method.Invoke($null, @([Enum]::Parse($statusType, 'Success'), $fallbackPath, $fallbackSegments, $feasibleDiagnostics, $reports)) } 'PublishLocalG2 must reject legacy statuses.'
Assert-Throws { $publishLocalG2Method.Invoke($null, @($complete, [Array]::CreateInstance($pointType, 0), $fallbackSegments, $feasibleDiagnostics, $reports)) } 'PublishLocalG2 must reject an empty path.'
$requestConstructor = $requestType.GetConstructor(@( $requestConstructor = $requestType.GetConstructor(@(
[System.Collections.Generic.IReadOnlyList``1].MakeGenericType($coarsePointType), [System.Collections.Generic.IReadOnlyList``1].MakeGenericType($coarsePointType),
@@ -235,6 +302,15 @@ $requestConfiguration.LocalCubicBezier.MaximumWindowLengthMeters = [double]0.99
$requestConfiguration.LocalCubicBezier.HandleLengthRatio = [double]0.99 $requestConfiguration.LocalCubicBezier.HandleLengthRatio = [double]0.99
$requestConfiguration.PiecewiseQuintic.KnotSpacingMeters = [double]0.99 $requestConfiguration.PiecewiseQuintic.KnotSpacingMeters = [double]0.99
$requestConfiguration.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.99 $requestConfiguration.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.99
$requestConfiguration.LocalG2Quintic.MinimumWindowLengthMeters = [double]0.99
$requestConfiguration.LocalG2Quintic.PreferredWindowLengthMeters = [double]0.99
$requestConfiguration.LocalG2Quintic.MaximumWindowLengthMeters = [double]0.99
$requestConfiguration.LocalG2Quintic.MaximumDeviationMeters = [double]0.99
$requestConfiguration.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter = [double]0.99
$requestConfiguration.LocalG2Quintic.CurvatureJumpRatioOfMaximum = [double]0.99
$requestConfiguration.LocalG2Quintic.MinimumPeakGradientImprovementRatio = [double]0.99
$requestConfiguration.LocalG2Quintic.MaximumVariationCostRegressionRatio = [double]0.99
$requestConfiguration.LocalG2Quintic.MaximumCandidatesPerRegion = 99
Assert-True ($null -ne $request.CoarsePath[0]) 'Request must copy the coarse-path collection.' Assert-True ($null -ne $request.CoarsePath[0]) 'Request must copy the coarse-path collection.'
Assert-True ($null -ne $request.Segments[0]) 'Request must copy the segment collection.' Assert-True ($null -ne $request.Segments[0]) 'Request must copy the segment collection.'
Assert-Near 0.80 $request.Vehicle.LengthMeters 'Request must snapshot vehicle parameters.' Assert-Near 0.80 $request.Vehicle.LengthMeters 'Request must snapshot vehicle parameters.'
@@ -254,6 +330,15 @@ Assert-Near 0.60 $request.Configuration.LocalCubicBezier.MaximumWindowLengthMete
Assert-Near (1.0 / 3.0) $request.Configuration.LocalCubicBezier.HandleLengthRatio 'Request must snapshot Bezier options.' Assert-Near (1.0 / 3.0) $request.Configuration.LocalCubicBezier.HandleLengthRatio 'Request must snapshot Bezier options.'
Assert-Near 0.50 $request.Configuration.PiecewiseQuintic.KnotSpacingMeters 'Request must snapshot quintic options.' Assert-Near 0.50 $request.Configuration.PiecewiseQuintic.KnotSpacingMeters 'Request must snapshot quintic options.'
Assert-Near 0.10 $request.Configuration.PiecewiseQuintic.MinimumKnotSpacingMeters 'Request must snapshot quintic minimum spacing.' Assert-Near 0.10 $request.Configuration.PiecewiseQuintic.MinimumKnotSpacingMeters 'Request must snapshot quintic minimum spacing.'
Assert-Near 0.20 $request.Configuration.LocalG2Quintic.MinimumWindowLengthMeters 'Request must snapshot Local G2 minimum window.'
Assert-Near 0.50 $request.Configuration.LocalG2Quintic.PreferredWindowLengthMeters 'Request must snapshot Local G2 preferred window.'
Assert-Near 0.80 $request.Configuration.LocalG2Quintic.MaximumWindowLengthMeters 'Request must snapshot Local G2 maximum window.'
Assert-Near 0.10 $request.Configuration.LocalG2Quintic.MaximumDeviationMeters 'Request must snapshot Local G2 maximum deviation.'
Assert-Near 0.001 $request.Configuration.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter 'Request must snapshot Local G2 absolute jump floor.'
Assert-Near 0.05 $request.Configuration.LocalG2Quintic.CurvatureJumpRatioOfMaximum 'Request must snapshot Local G2 relative jump threshold.'
Assert-Near 0.20 $request.Configuration.LocalG2Quintic.MinimumPeakGradientImprovementRatio 'Request must snapshot Local G2 peak improvement threshold.'
Assert-Near 0.02 $request.Configuration.LocalG2Quintic.MaximumVariationCostRegressionRatio 'Request must snapshot Local G2 variation tolerance.'
Assert-Equal 12 $request.Configuration.LocalG2Quintic.MaximumCandidatesPerRegion 'Request must snapshot Local G2 candidate count.'
$request.Vehicle.WidthMeters = [double]9.99 $request.Vehicle.WidthMeters = [double]9.99
$request.Vehicle.SafetyMarginMeters = [double]9.99 $request.Vehicle.SafetyMarginMeters = [double]9.99
$request.Vehicle.MinimumTurningRadiusMeters = [double]9.99 $request.Vehicle.MinimumTurningRadiusMeters = [double]9.99
@@ -264,6 +349,15 @@ $request.Configuration.AllowFallbackToCoarsePath = $false
$request.Configuration.LocalCubicBezier.CornerHeadingThresholdRadians = [double]0.99 $request.Configuration.LocalCubicBezier.CornerHeadingThresholdRadians = [double]0.99
$request.Configuration.LocalCubicBezier.MaximumWindowLengthMeters = [double]0.99 $request.Configuration.LocalCubicBezier.MaximumWindowLengthMeters = [double]0.99
$request.Configuration.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.99 $request.Configuration.PiecewiseQuintic.MinimumKnotSpacingMeters = [double]0.99
$request.Configuration.LocalG2Quintic.MinimumWindowLengthMeters = [double]0.99
$request.Configuration.LocalG2Quintic.PreferredWindowLengthMeters = [double]0.99
$request.Configuration.LocalG2Quintic.MaximumWindowLengthMeters = [double]0.99
$request.Configuration.LocalG2Quintic.MaximumDeviationMeters = [double]0.99
$request.Configuration.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter = [double]0.99
$request.Configuration.LocalG2Quintic.CurvatureJumpRatioOfMaximum = [double]0.99
$request.Configuration.LocalG2Quintic.MinimumPeakGradientImprovementRatio = [double]0.99
$request.Configuration.LocalG2Quintic.MaximumVariationCostRegressionRatio = [double]0.99
$request.Configuration.LocalG2Quintic.MaximumCandidatesPerRegion = 99
Assert-Near 0.60 $request.Vehicle.WidthMeters 'Request vehicle getter must not expose mutable state.' Assert-Near 0.60 $request.Vehicle.WidthMeters 'Request vehicle getter must not expose mutable state.'
Assert-Near 0.05 $request.Vehicle.SafetyMarginMeters 'Request vehicle getter must not expose mutable state.' Assert-Near 0.05 $request.Vehicle.SafetyMarginMeters 'Request vehicle getter must not expose mutable state.'
Assert-True ($null -eq $request.Vehicle.MinimumTurningRadiusMeters) 'Request vehicle getter must not expose mutable nullable state.' Assert-True ($null -eq $request.Vehicle.MinimumTurningRadiusMeters) 'Request vehicle getter must not expose mutable nullable state.'
@@ -274,5 +368,14 @@ Assert-Equal $true $request.Configuration.AllowFallbackToCoarsePath 'Request con
Assert-Near ([Math]::PI / 18.0) $request.Configuration.LocalCubicBezier.CornerHeadingThresholdRadians 'Request configuration getter must not expose mutable Bezier options.' Assert-Near ([Math]::PI / 18.0) $request.Configuration.LocalCubicBezier.CornerHeadingThresholdRadians 'Request configuration getter must not expose mutable Bezier options.'
Assert-Near 0.60 $request.Configuration.LocalCubicBezier.MaximumWindowLengthMeters 'Request configuration getter must not expose mutable Bezier options.' Assert-Near 0.60 $request.Configuration.LocalCubicBezier.MaximumWindowLengthMeters 'Request configuration getter must not expose mutable Bezier options.'
Assert-Near 0.10 $request.Configuration.PiecewiseQuintic.MinimumKnotSpacingMeters 'Request configuration getter must not expose mutable quintic options.' Assert-Near 0.10 $request.Configuration.PiecewiseQuintic.MinimumKnotSpacingMeters 'Request configuration getter must not expose mutable quintic options.'
Assert-Near 0.20 $request.Configuration.LocalG2Quintic.MinimumWindowLengthMeters 'Request configuration getter must not expose mutable Local G2 minimum window.'
Assert-Near 0.50 $request.Configuration.LocalG2Quintic.PreferredWindowLengthMeters 'Request configuration getter must not expose mutable Local G2 preferred window.'
Assert-Near 0.80 $request.Configuration.LocalG2Quintic.MaximumWindowLengthMeters 'Request configuration getter must not expose mutable Local G2 maximum window.'
Assert-Near 0.10 $request.Configuration.LocalG2Quintic.MaximumDeviationMeters 'Request configuration getter must not expose mutable Local G2 maximum deviation.'
Assert-Near 0.001 $request.Configuration.LocalG2Quintic.AbsoluteCurvatureJumpFloorPerMeter 'Request configuration getter must not expose mutable Local G2 absolute jump floor.'
Assert-Near 0.05 $request.Configuration.LocalG2Quintic.CurvatureJumpRatioOfMaximum 'Request configuration getter must not expose mutable Local G2 relative jump threshold.'
Assert-Near 0.20 $request.Configuration.LocalG2Quintic.MinimumPeakGradientImprovementRatio 'Request configuration getter must not expose mutable Local G2 peak improvement threshold.'
Assert-Near 0.02 $request.Configuration.LocalG2Quintic.MaximumVariationCostRegressionRatio 'Request configuration getter must not expose mutable Local G2 variation tolerance.'
Assert-Equal 12 $request.Configuration.LocalG2Quintic.MaximumCandidatesPerRegion 'Request configuration getter must not expose mutable Local G2 candidate count.'
Write-Output 'Path smoothing contract checks passed.' Write-Output 'Path smoothing contract checks passed.'