diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs
new file mode 100644
index 0000000..7bc1e64
--- /dev/null
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/LocalG2QuinticOptions.cs
@@ -0,0 +1,15 @@
+namespace MultiWheelC.TrajectoryPlanning.PathSmoothing;
+
+/// 局部 G2 五次过渡的可配置阈值。
+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;
+}
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs
index fdd088e..251b387 100644
--- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathQualityMetrics.cs
@@ -5,7 +5,7 @@ public sealed class 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 curvatureVariationChangePercent,
double minimumClearanceChangeMeters)
+ : this(
+ isFeasible,
+ pathLengthMeters,
+ maximumAbsoluteVehicleCurvaturePerMeter,
+ 0d,
+ rootMeanSquareVehicleCurvaturePerMeter,
+ totalAbsoluteCurvatureVariationPerMeter,
+ curvatureVariationEnergy,
+ minimumBodyClearanceMeters,
+ lengthChangePercent,
+ peakCurvatureChangePercent,
+ curvatureVariationChangePercent,
+ minimumClearanceChangeMeters)
+ {
+ }
+
+ /// 创建带有曲率导数峰值的完整质量指标快照。
+ 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;
PathLengthMeters = pathLengthMeters;
MaximumAbsoluteVehicleCurvaturePerMeter = maximumAbsoluteVehicleCurvaturePerMeter;
+ MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter = maximumAbsoluteVehicleCurvatureDerivativePerSquareMeter;
RootMeanSquareVehicleCurvaturePerMeter = rootMeanSquareVehicleCurvaturePerMeter;
TotalAbsoluteCurvatureVariationPerMeter = totalAbsoluteCurvatureVariationPerMeter;
CurvatureVariationEnergy = curvatureVariationEnergy;
@@ -45,6 +76,9 @@ public sealed class PathQualityMetrics
/// 绝对车辆曲率峰值,单位 1/m。
public double MaximumAbsoluteVehicleCurvaturePerMeter { get; }
+ /// 绝对车辆曲率导数峰值,单位 1/m²。
+ public double MaximumAbsoluteVehicleCurvatureDerivativePerSquareMeter { get; }
+
/// 车辆曲率均方根,单位 1/m。
public double RootMeanSquareVehicleCurvaturePerMeter { get; }
@@ -54,6 +88,9 @@ public sealed class PathQualityMetrics
/// 逐方向段计算的曲率变化能量。
public double CurvatureVariationEnergy { get; }
+ /// 曲率变化代价的兼容名称。
+ public double CurvatureVariationCost => CurvatureVariationEnergy;
+
/// 完整扩大车体的最小保守净空,单位 m。
public double MinimumBodyClearanceMeters { get; }
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs
index ab1f2a1..1aca9c6 100644
--- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingConfiguration.cs
@@ -47,4 +47,7 @@ public sealed class PathSmoothingConfiguration
/// 分段五次多项式专用参数。
public PiecewiseQuinticOptions PiecewiseQuintic { get; } = new PiecewiseQuinticOptions();
+
+ /// 局部 G2 五次过渡专用参数。
+ public LocalG2QuinticOptions LocalG2Quintic { get; } = new LocalG2QuinticOptions();
}
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs
new file mode 100644
index 0000000..91b5bf2
--- /dev/null
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionFailureReason.cs
@@ -0,0 +1,17 @@
+namespace MultiWheelC.TrajectoryPlanning.PathSmoothing;
+
+/// 局部 G2 区域未替换原始路径的稳定原因。
+public enum PathSmoothingRegionFailureReason
+{
+ None,
+ WindowUnavailable,
+ CandidateGenerationFailed,
+ Collision,
+ InsufficientClearance,
+ CurvatureExceeded,
+ CurvatureOvershoot,
+ DeviationExceeded,
+ InsufficientImprovement,
+ VariationCostRegression,
+ GlobalValidationRollback,
+}
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs
new file mode 100644
index 0000000..4004314
--- /dev/null
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionReport.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace MultiWheelC.TrajectoryPlanning.PathSmoothing;
+
+/// 单个局部 G2 平滑区域的不可变发布报告。
+public sealed class PathSmoothingRegionReport
+{
+ public PathSmoothingRegionReport(
+ int segmentIndex,
+ double startArcLengthMeters,
+ double endArcLengthMeters,
+ IReadOnlyList 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 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 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/PathSmoothingRegionStatus.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs
new file mode 100644
index 0000000..752d3ef
--- /dev/null
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRegionStatus.cs
@@ -0,0 +1,8 @@
+namespace MultiWheelC.TrajectoryPlanning.PathSmoothing;
+
+/// 单个局部 G2 平滑区域的处理结果。
+public enum PathSmoothingRegionStatus
+{
+ Improved,
+ RetainedOriginal,
+}
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs
index 2d12340..ebe8106 100644
--- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingRequest.cs
@@ -83,6 +83,15 @@ public sealed class PathSmoothingRequest
copy.LocalCubicBezier.HandleLengthRatio = source.LocalCubicBezier.HandleLengthRatio;
copy.PiecewiseQuintic.KnotSpacingMeters = source.PiecewiseQuintic.KnotSpacingMeters;
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;
}
}
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs
index 1555b4f..d964caa 100644
--- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingResult.cs
@@ -11,18 +11,22 @@ public sealed class PathSmoothingResult
new ReadOnlyCollection(new List());
private static readonly IReadOnlyList EmptySegments =
new ReadOnlyCollection(new List());
+ private static readonly IReadOnlyList EmptyRegionReports =
+ new ReadOnlyCollection(new List());
private PathSmoothingResult(
PathSmoothingStatus status,
SmoothingMethod? method,
IReadOnlyList path,
IReadOnlyList segments,
- PathSmoothingDiagnostics diagnostics)
+ PathSmoothingDiagnostics diagnostics,
+ IReadOnlyList regionReports)
{
Status = status;
Method = method;
Path = path;
Segments = segments;
+ RegionReports = regionReports;
Diagnostics = diagnostics ?? new PathSmoothingDiagnostics(
new PathQualityMetrics(),
TimeSpan.Zero,
@@ -43,6 +47,9 @@ public sealed class PathSmoothingResult
/// 覆盖 的方向分段;其他状态始终为空且不可变。
public IReadOnlyList Segments { get; }
+ /// 局部 G2 各检测区域的不可变报告;传统算法结果为空。
+ public IReadOnlyList RegionReports { get; }
+
/// 本次平滑的质量和终止诊断;始终非空。
public PathSmoothingDiagnostics Diagnostics { get; }
@@ -59,7 +66,8 @@ public sealed class PathSmoothingResult
method,
CopyReadOnly(path),
CopyReadOnly(segments),
- diagnostics);
+ diagnostics,
+ EmptyRegionReports);
}
/// 创建经过完整复核的原始粗路径回退结果。
@@ -75,17 +83,49 @@ public sealed class PathSmoothingResult
attemptedMethod,
CopyReadOnly(path),
CopyReadOnly(segments),
- diagnostics);
+ diagnostics,
+ EmptyRegionReports);
+ }
+
+ /// 发布经过完整复核的局部 G2 预平滑结果。
+ public static PathSmoothingResult PublishLocalG2(
+ PathSmoothingStatus status,
+ IReadOnlyList path,
+ IReadOnlyList segments,
+ PathSmoothingDiagnostics diagnostics,
+ IReadOnlyList 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));
}
/// 创建不发布路径的失败、不可行、取消或输入无效结果。
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));
if (!Enum.IsDefined(typeof(PathSmoothingStatus), 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(
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs
index 1212230..df1cdab 100644
--- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/PathSmoothingStatus.cs
@@ -9,4 +9,8 @@ public enum PathSmoothingStatus
Infeasible,
Failed,
Cancelled,
+ Complete,
+ PartialImprovement,
+ NotNeeded,
+ Unchanged,
}
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs
index 5103a4a..b0a3c8a 100644
--- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPoint.cs
@@ -17,6 +17,36 @@ public sealed class SmoothedPathPoint
double bodyClearanceMeters,
bool isGearSwitchPoint,
SmoothedPathPointSource source)
+ : this(
+ xMeters,
+ yMeters,
+ headingRadians,
+ unwrappedHeadingRadians,
+ arcLengthMeters,
+ direction,
+ geometricCurvaturePerMeter,
+ vehicleCurvaturePerMeter,
+ 0d,
+ bodyClearanceMeters,
+ isGearSwitchPoint,
+ source)
+ {
+ }
+
+ /// 创建带有车辆曲率对弧长导数的不可变采样点。
+ 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;
Y = yMeters;
@@ -26,6 +56,7 @@ public sealed class SmoothedPathPoint
Direction = direction;
GeometricCurvature = geometricCurvaturePerMeter;
VehicleCurvature = vehicleCurvaturePerMeter;
+ VehicleCurvatureDerivative = vehicleCurvatureDerivativePerSquareMeter;
BodyClearance = bodyClearanceMeters;
IsGearSwitchPoint = isGearSwitchPoint;
Source = source;
@@ -55,6 +86,9 @@ public sealed class SmoothedPathPoint
/// 车辆模型使用的有符号曲率,单位 1/m。
public double VehicleCurvature { get; }
+ /// 车辆曲率对弧长的导数 dκ/ds,单位 1/m²。
+ public double VehicleCurvatureDerivative { get; }
+
/// 扩大车体后的保守净空下界,单位 m。
public double BodyClearance { get; }
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs
index f3140e1..9469fc6 100644
--- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothedPathPointSource.cs
@@ -7,4 +7,5 @@ public enum SmoothedPathPointSource
Interpolated,
GearSwitch,
CoarsePathFallback,
+ LocalG2Transition,
}
diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs
index 41d57e6..890e02f 100644
--- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs
+++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Contracts/SmoothingMethod.cs
@@ -6,4 +6,5 @@ public enum SmoothingMethod
CubicBSpline,
LocalCubicBezier,
PiecewiseQuintic,
+ LocalG2Quintic,
}
diff --git a/ClumsyPilot/tests/verify_path_smoothing_contracts.ps1 b/ClumsyPilot/tests/verify_path_smoothing_contracts.ps1
index b6b36a6..6838d1f 100644
--- a/ClumsyPilot/tests/verify_path_smoothing_contracts.ps1
+++ b/ClumsyPilot/tests/verify_path_smoothing_contracts.ps1
@@ -47,6 +47,10 @@ $segmentType = Get-RequiredType ($root + 'SmoothedPathSegment')
$bsplineOptionsType = Get-RequiredType ($root + 'CubicBSplineOptions')
$bezierOptionsType = Get-RequiredType ($root + 'LocalCubicBezierOptions')
$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')
$metricsType = Get-RequiredType ($root + 'PathQualityMetrics')
$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 $statusType.IsEnum 'PathSmoothingStatus 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 'Success,FallbackToCoarsePath,InvalidInput,Infeasible,Failed,Cancelled' ([string]::Join(',', [Enum]::GetNames($statusType))) 'Smoothing status members and order must remain stable.'
-Assert-Equal 'Anchor,Interpolated,GearSwitch,CoarsePathFallback' ([string]::Join(',', [Enum]::GetNames($sourceType))) 'Smoothed point source 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,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,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)
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)
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.'
+$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')
$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-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-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))
$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-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.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)
Assert-True ($diagnostics.Metrics -ne $null) 'Default diagnostics must provide quality metrics.'
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))
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 0 $fallback.RegionReports.Count 'Legacy fallback results must publish empty immutable region reports.'
$failureMethod = $resultType.GetMethod('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 { $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-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(@(
[System.Collections.Generic.IReadOnlyList``1].MakeGenericType($coarsePointType),
@@ -235,6 +302,15 @@ $requestConfiguration.LocalCubicBezier.MaximumWindowLengthMeters = [double]0.99
$requestConfiguration.LocalCubicBezier.HandleLengthRatio = [double]0.99
$requestConfiguration.PiecewiseQuintic.KnotSpacingMeters = [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.Segments[0]) 'Request must copy the segment collection.'
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 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.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.SafetyMarginMeters = [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.MaximumWindowLengthMeters = [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.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.'
@@ -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 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.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.'