fix: accelerate EM trajectories from rest

This commit is contained in:
梁薄云
2026-08-07 07:51:43 +08:00
parent 162f1a24e6
commit 0bba8d7e61
9 changed files with 273 additions and 20 deletions
@@ -84,7 +84,7 @@ public sealed class EmPlanningService : IEmPlanningService
EmitDebug(request, "LS optimization and validation succeeded"); EmitDebug(request, "LS optimization and validation succeeded");
EmPlanningStatus envelopeStatus = new PathSpeedLimitBuilder().Build(lateral.Path, segment.Direction, EmPlanningStatus envelopeStatus = new PathSpeedLimitBuilder().Build(lateral.Path, segment.Direction,
initialProgressSpeed, horizon.TerminalType, configuration, out PathSpeedLimit speedLimit, initialProgressSpeed, initialAcceleration, horizon.TerminalType, configuration, out PathSpeedLimit speedLimit,
out string envelopeReason); out string envelopeReason);
if (envelopeStatus != EmPlanningStatus.Success) if (envelopeStatus != EmPlanningStatus.Success)
return Failure(envelopeStatus, request, envelopeReason); return Failure(envelopeStatus, request, envelopeReason);
@@ -167,9 +167,12 @@ public sealed class FullDirectionSegmentScheduleBuilder
LongitudinalConfiguration configuration) LongitudinalConfiguration configuration)
{ {
double adjustedTime = 0d; double adjustedTime = 0d;
double requestedPreviousTime = times[0];
for (int index = 1; index < times.Count; index++) for (int index = 1; index < times.Count; index++)
{ {
double requestedDuration = times[index] - times[index - 1]; double requestedTime = times[index];
double requestedDuration = requestedTime - requestedPreviousTime;
requestedPreviousTime = requestedTime;
double speedChange = Math.Abs(referenceSpeeds[index] - referenceSpeeds[index - 1]); double speedChange = Math.Abs(referenceSpeeds[index] - referenceSpeeds[index - 1]);
double accelerationLimit = referenceSpeeds[index] >= referenceSpeeds[index - 1] double accelerationLimit = referenceSpeeds[index] >= referenceSpeeds[index - 1]
? configuration.MaximumAccelerationMetersPerSecondSquared ? configuration.MaximumAccelerationMetersPerSecondSquared
@@ -191,7 +194,10 @@ public sealed class FullDirectionSegmentScheduleBuilder
{ {
double previousSlope = (speeds[index] - speeds[index - 1]) / (pathS[index] - pathS[index - 1]); double previousSlope = (speeds[index] - speeds[index - 1]) / (pathS[index] - pathS[index - 1]);
double nextSlope = (speeds[index + 1] - speeds[index]) / (pathS[index + 1] - pathS[index]); double nextSlope = (speeds[index + 1] - speeds[index]) / (pathS[index + 1] - pathS[index]);
if (previousSlope * nextSlope < 0d) bool changesDirection = previousSlope * nextSlope < 0d;
bool entersCruise = previousSlope > Tolerance && nextSlope <= Tolerance;
bool leavesCruise = previousSlope >= -Tolerance && nextSlope < -Tolerance;
if (changesDirection || entersCruise || leavesCruise)
stations.Add(index); stations.Add(index);
} }
stations.Add(pathS.Count - 1); stations.Add(pathS.Count - 1);
@@ -26,7 +26,10 @@ public sealed class LongitudinalObjectiveBuilder
for (int index = 0; index < layout.KnotCount; index++) for (int index = 0; index < layout.KnotCount; index++)
{ {
double iteratePathS = Math.Max(0d, Math.Min(input.PathUpperBoundS, iterate.S[index])); double iteratePathS = Math.Max(0d, Math.Min(input.PathUpperBoundS, iterate.S[index]));
AddSquaredResidual(hessian, linearCost, layout.U(index), speedLimit.MaximumSpeedAt(iteratePathS), double referenceSpeed = input.PlanningScope == EmPlanningScope.FullDirectionSegment
? input.KnotSchedule.ReferenceSpeedMetersPerSecond[index]
: speedLimit.MaximumSpeedAt(iteratePathS);
AddSquaredResidual(hessian, linearCost, layout.U(index), referenceSpeed,
weights.ReferenceSpeed, speedScale); weights.ReferenceSpeed, speedScale);
AddSquaredResidual(hessian, linearCost, layout.A(index), 0d, weights.Acceleration, accelerationScale); AddSquaredResidual(hessian, linearCost, layout.A(index), 0d, weights.Acceleration, accelerationScale);
if (index < layout.KnotCount - 1 && index < input.PreviousPathS.Count) if (index < layout.KnotCount - 1 && index < input.PreviousPathS.Count)
@@ -8,8 +8,15 @@ public sealed class LongitudinalSolutionValidator
{ {
public bool TryValidate(LongitudinalPlanningInput input, PathSpeedLimit speedLimit, LongitudinalCandidate candidate, public bool TryValidate(LongitudinalPlanningInput input, PathSpeedLimit speedLimit, LongitudinalCandidate candidate,
out LongitudinalCandidate validatedCandidate, out string failureReason) out LongitudinalCandidate validatedCandidate, out string failureReason)
{
return TryValidate(input, speedLimit, candidate, out validatedCandidate, out _, out failureReason);
}
public bool TryValidate(LongitudinalPlanningInput input, PathSpeedLimit speedLimit, LongitudinalCandidate candidate,
out LongitudinalCandidate validatedCandidate, out EmPlanningStatus failureStatus, out string failureReason)
{ {
validatedCandidate = null; validatedCandidate = null;
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
failureReason = string.Empty; failureReason = string.Empty;
if (input == null || speedLimit == null || candidate == null) if (input == null || speedLimit == null || candidate == null)
{ {
@@ -58,7 +65,8 @@ public sealed class LongitudinalSolutionValidator
progress > input.PathUpperBoundS + tolerance || speed < -tolerance || progress > input.PathUpperBoundS + tolerance || speed < -tolerance ||
acceleration < -maximumDeceleration - tolerance || acceleration > maximumAcceleration + tolerance) acceleration < -maximumDeceleration - tolerance || acceleration > maximumAcceleration + tolerance)
{ {
failureReason = "ST candidate violates physical bounds at knot " + index + "."; failureReason = "ST candidate violates physical bounds at knot " + index +
" (S=" + progress + ", U=" + speed + ", A=" + acceleration + ").";
return false; return false;
} }
double speedLimitAtProgress = index == 0 double speedLimitAtProgress = index == 0
@@ -84,6 +92,16 @@ public sealed class LongitudinalSolutionValidator
return false; return false;
} }
} }
bool requiresProgress = input.PathUpperBoundS >
input.Configuration.Validation.TerminalPositionToleranceMeters;
double achievedProgress = candidate.S[candidate.S.Count - 1] - candidate.S[0];
if (input.PlanningScope == EmPlanningScope.FullDirectionSegment && requiresProgress &&
achievedProgress <= input.Configuration.Validation.SpatialToleranceMeters)
{
failureStatus = EmPlanningStatus.NoProgress;
failureReason = "NoProgress: a nonterminal full direction segment produced zero progress.";
return false;
}
int stabilizationStart = candidate.S.Count; int stabilizationStart = candidate.S.Count;
if (input.Mode == EmLongitudinalMode.ExactStopAtBoundary) if (input.Mode == EmLongitudinalMode.ExactStopAtBoundary)
{ {
@@ -32,6 +32,15 @@ public sealed class PathSpeedLimitBuilder
out speedLimit, out failureReason); out speedLimit, out failureReason);
} }
public EmPlanningStatus Build(LateralPath path, TravelDirection direction,
double initialProgressSpeedMetersPerSecond, double initialAccelerationMetersPerSecondSquared,
EmTerminalType terminalType, EmPlannerConfiguration configuration, out PathSpeedLimit speedLimit,
out string failureReason)
{
return BuildCore(path, direction, initialProgressSpeedMetersPerSecond,
initialAccelerationMetersPerSecondSquared, terminalType, configuration, out speedLimit, out failureReason);
}
private EmPlanningStatus BuildCore(LateralPath path, TravelDirection direction, private EmPlanningStatus BuildCore(LateralPath path, TravelDirection direction,
double initialProgressSpeedMetersPerSecond, double initialAccelerationMetersPerSecondSquared, double initialProgressSpeedMetersPerSecond, double initialAccelerationMetersPerSecondSquared,
EmTerminalType terminalType, EmPlannerConfiguration configuration, out PathSpeedLimit speedLimit, EmTerminalType terminalType, EmPlannerConfiguration configuration, out PathSpeedLimit speedLimit,
@@ -62,6 +71,7 @@ public sealed class PathSpeedLimitBuilder
double maximumJerk = longitudinal.MaximumJerkMetersPerSecondCubed; double maximumJerk = longitudinal.MaximumJerkMetersPerSecondCubed;
double maximumLateralAcceleration = longitudinal.MaximumLateralAccelerationMetersPerSecondSquared; double maximumLateralAcceleration = longitudinal.MaximumLateralAccelerationMetersPerSecondSquared;
double maximumCurvatureRate = longitudinal.MaximumCurvatureRatePerMeterPerSecond; double maximumCurvatureRate = longitudinal.MaximumCurvatureRatePerMeterPerSecond;
double stoppingAcceleration = Math.Max(0d, initialAccelerationMetersPerSecondSquared);
if (!IsPositiveFinite(directionMaximum) || !IsPositiveFinite(maximumAcceleration) || if (!IsPositiveFinite(directionMaximum) || !IsPositiveFinite(maximumAcceleration) ||
!IsPositiveFinite(maximumDeceleration) || !IsPositiveFinite(maximumJerk) || !IsPositiveFinite(maximumDeceleration) || !IsPositiveFinite(maximumJerk) ||
!IsPositiveFinite(maximumLateralAcceleration) || !IsPositiveFinite(maximumCurvatureRate)) !IsPositiveFinite(maximumLateralAcceleration) || !IsPositiveFinite(maximumCurvatureRate))
@@ -118,7 +128,7 @@ public sealed class PathSpeedLimitBuilder
if (hasStopBoundary) if (hasStopBoundary)
{ {
AddJerkLimitedStoppingStations(lowerPoint.PathS, upperPoint.PathS, stopBoundaryPathS, AddJerkLimitedStoppingStations(lowerPoint.PathS, upperPoint.PathS, stopBoundaryPathS,
directionMaximum, maximumAcceleration, maximumDeceleration, maximumJerk, directionMaximum, stoppingAcceleration, maximumDeceleration, maximumJerk,
segmentIndex == 0, segmentStations); segmentIndex == 0, segmentStations);
} }
segmentStations.Sort(); segmentStations.Sort();
@@ -134,7 +144,7 @@ public sealed class PathSpeedLimitBuilder
double curvatureDerivative = Interpolate(lowerPoint.VehicleCurvatureDerivative, double curvatureDerivative = Interpolate(lowerPoint.VehicleCurvatureDerivative,
upperPoint.VehicleCurvatureDerivative, fraction); upperPoint.VehicleCurvatureDerivative, fraction);
AddLimitSample(samplePathS, curvature, curvatureDerivative, hasStopBoundary, AddLimitSample(samplePathS, curvature, curvatureDerivative, hasStopBoundary,
stopBoundaryPathS, directionMaximum, maximumAcceleration, maximumDeceleration, stopBoundaryPathS, directionMaximum, stoppingAcceleration, maximumDeceleration,
maximumJerk, maximumLateralAcceleration, maximumCurvatureRate, pathS, maximum, lateral, maximumJerk, maximumLateralAcceleration, maximumCurvatureRate, pathS, maximum, lateral,
curvatureRate, stopping); curvatureRate, stopping);
} }
@@ -188,14 +198,14 @@ public sealed class PathSpeedLimitBuilder
} }
private static void AddJerkLimitedStoppingStations(double lowerPathS, double upperPathS, private static void AddJerkLimitedStoppingStations(double lowerPathS, double upperPathS,
double stopBoundaryPathS, double directionMaximum, double maximumAcceleration, double maximumDeceleration, double stopBoundaryPathS, double directionMaximum, double stoppingAcceleration, double maximumDeceleration,
double maximumJerk, bool includeLower, IList<double> stations) double maximumJerk, bool includeLower, IList<double> stations)
{ {
const int stoppingSpeedSampleCount = 64; const int stoppingSpeedSampleCount = 64;
for (int step = 0; step < stoppingSpeedSampleCount; step++) for (int step = 0; step < stoppingSpeedSampleCount; step++)
{ {
double speed = directionMaximum * step / stoppingSpeedSampleCount; double speed = directionMaximum * step / stoppingSpeedSampleCount;
if (!JerkLimitedStoppingMath.TryCalculate(speed, maximumAcceleration, if (!JerkLimitedStoppingMath.TryCalculate(speed, stoppingAcceleration,
maximumDeceleration, maximumJerk, out JerkLimitedStoppingProfile stop, out _)) maximumDeceleration, maximumJerk, out JerkLimitedStoppingProfile stop, out _))
{ {
throw new ArgumentException("The configured jerk-limited stop envelope cannot be sampled."); throw new ArgumentException("The configured jerk-limited stop envelope cannot be sampled.");
@@ -210,7 +220,7 @@ public sealed class PathSpeedLimitBuilder
} }
private static void AddLimitSample(double samplePathS, double curvature, double curvatureDerivative, private static void AddLimitSample(double samplePathS, double curvature, double curvatureDerivative,
bool hasStopBoundary, double stopBoundaryPathS, double directionMaximum, double maximumAcceleration, bool hasStopBoundary, double stopBoundaryPathS, double directionMaximum, double stoppingAcceleration,
double maximumDeceleration, double maximumJerk, double maximumLateralAcceleration, double maximumDeceleration, double maximumJerk, double maximumLateralAcceleration,
double maximumCurvatureRate, IList<double> pathS, IList<double> maximum, IList<double> lateral, double maximumCurvatureRate, IList<double> pathS, IList<double> maximum, IList<double> lateral,
IList<double> curvatureRate, IList<double> stopping) IList<double> curvatureRate, IList<double> stopping)
@@ -220,7 +230,7 @@ public sealed class PathSpeedLimitBuilder
double curvatureRateLimit = maximumCurvatureRate / Math.Max(Math.Abs(curvatureDerivative), CurvatureEpsilon); double curvatureRateLimit = maximumCurvatureRate / Math.Max(Math.Abs(curvatureDerivative), CurvatureEpsilon);
double stoppingLimit = hasStopBoundary double stoppingLimit = hasStopBoundary
? JerkLimitedStoppingMath.MaximumInitialSpeedForDistance( ? JerkLimitedStoppingMath.MaximumInitialSpeedForDistance(
Math.Max(0d, stopBoundaryPathS - samplePathS), maximumAcceleration, Math.Max(0d, stopBoundaryPathS - samplePathS), stoppingAcceleration,
maximumDeceleration, maximumJerk, directionMaximum) maximumDeceleration, maximumJerk, directionMaximum)
: directionMaximum; : directionMaximum;
double lateralValue = ClampFinite(lateralLimit, directionMaximum); double lateralValue = ClampFinite(lateralLimit, directionMaximum);
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading; using System.Threading;
using MultiWheelC.TrajectoryPlanning.CoarsePath;
namespace MultiWheelC.TrajectoryPlanning.EMPlanner; namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
@@ -222,6 +223,14 @@ public sealed class SequentialLongitudinalOptimizer
projectionSolveCount = 0; projectionSolveCount = 0;
failureStatus = EmPlanningStatus.LongitudinalInfeasible; failureStatus = EmPlanningStatus.LongitudinalInfeasible;
failureReason = string.Empty; failureReason = string.Empty;
if (input.InitialProgressSpeedMetersPerSecond <= input.Configuration.Validation.SpatialToleranceMeters &&
Math.Abs(input.InitialAccelerationMetersPerSecondSquared) <=
input.Configuration.Validation.KinematicTolerance &&
TryCreateStaticStartSeed(input, speedLimit, out LongitudinalCandidate staticStartSeed))
{
candidate = staticStartSeed;
return true;
}
LongitudinalCandidate linearizationIterate = CreateScheduleReferenceIterate(input); LongitudinalCandidate linearizationIterate = CreateScheduleReferenceIterate(input);
string lastRejection = string.Empty; string lastRejection = string.Empty;
for (int iteration = 0; iteration < iterationLimit; iteration++) for (int iteration = 0; iteration < iterationLimit; iteration++)
@@ -302,11 +311,17 @@ public sealed class SequentialLongitudinalOptimizer
if (solved.Status == QpSolveStatus.Solved || HasStrictResiduals(solved, convergenceTolerance)) if (solved.Status == QpSolveStatus.Solved || HasStrictResiduals(solved, convergenceTolerance))
{ {
if (_solutionValidator.TryValidate(input, speedLimit, projected, out LongitudinalCandidate strict, if (_solutionValidator.TryValidate(input, speedLimit, projected, out LongitudinalCandidate strict,
out string validationFailure)) out EmPlanningStatus validationStatus, out string validationFailure))
{ {
candidate = strict; candidate = strict;
return true; return true;
} }
if (validationStatus == EmPlanningStatus.NoProgress)
{
failureStatus = validationStatus;
failureReason = validationFailure;
return false;
}
lastRejection = validationFailure; lastRejection = validationFailure;
} }
@@ -377,6 +392,14 @@ public sealed class SequentialLongitudinalOptimizer
double dt = times[index + 1] - times[index]; double dt = times[index + 1] - times[index];
double speedLimitAtS = speedLimit.MaximumSpeedAt(Math.Max(0d, Math.Min(input.PathUpperBoundS, s))); double speedLimitAtS = speedLimit.MaximumSpeedAt(Math.Max(0d, Math.Min(input.PathUpperBoundS, s)));
double targetSpeed = Math.Min(input.InitialProgressSpeedMetersPerSecond, speedLimitAtS); double targetSpeed = Math.Min(input.InitialProgressSpeedMetersPerSecond, speedLimitAtS);
if (input.PlanningScope == EmPlanningScope.FullDirectionSegment)
{
double desiredSpeed = input.Direction == TravelDirection.Forward
? configuration.DesiredForwardSpeedMetersPerSecond
: configuration.DesiredReverseSpeedMetersPerSecond;
double scheduleSpeed = input.KnotSchedule.ReferenceSpeedMetersPerSecond[index];
targetSpeed = Math.Min(desiredSpeed, Math.Min(scheduleSpeed, speedLimitAtS));
}
double lowerJerk = Math.Max(-configuration.MaximumJerkMetersPerSecondCubed, double lowerJerk = Math.Max(-configuration.MaximumJerkMetersPerSecondCubed,
(-configuration.MaximumDecelerationMetersPerSecondSquared - a) / dt); (-configuration.MaximumDecelerationMetersPerSecondSquared - a) / dt);
lowerJerk = Math.Max(lowerJerk, -2d * (u + a * dt) / (dt * dt)); lowerJerk = Math.Max(lowerJerk, -2d * (u + a * dt) / (dt * dt));
@@ -442,6 +465,44 @@ public sealed class SequentialLongitudinalOptimizer
return CreateApproachSeed(input, times, speedLimit); return CreateApproachSeed(input, times, speedLimit);
} }
private bool TryCreateStaticStartSeed(LongitudinalPlanningInput input, PathSpeedLimit speedLimit,
out LongitudinalCandidate candidate)
{
candidate = null;
int stabilizationStart = input.KnotSchedule.TerminalHoldStartIndex;
if (stabilizationStart < 5)
return false;
IReadOnlyList<double> times = input.KnotSchedule.KnotTimes;
if (TryCreateExactJerkSeed(input, times, stabilizationStart, speedLimit, out candidate))
return true;
double firstDuration = times[1] - times[0];
double secondDuration = times[2] - times[1];
LongitudinalConfiguration configuration = input.Configuration.Longitudinal;
double maximumFirstJerk = Math.Min(configuration.MaximumJerkMetersPerSecondCubed,
Math.Min(configuration.MaximumAccelerationMetersPerSecondSquared / firstDuration,
configuration.MaximumJerkMetersPerSecondCubed * secondDuration / firstDuration));
for (int sample = 1; sample <= 256; sample++)
{
double firstJerk = maximumFirstJerk * sample / 256d;
var jerk = new double[stabilizationStart];
jerk[0] = firstJerk;
jerk[1] = -firstJerk * firstDuration / secondDuration;
if (!TryCloseExactStopEndpoint(input, times, stabilizationStart, jerk,
out LongitudinalCandidate probe))
{
continue;
}
if (_solutionValidator.TryValidate(input, speedLimit, probe,
out LongitudinalCandidate strict, out _))
{
candidate = strict;
return true;
}
}
return false;
}
private static bool TryCreateCruiseThenBrakeSeed(LongitudinalPlanningInput input, IReadOnlyList<double> times, private static bool TryCreateCruiseThenBrakeSeed(LongitudinalPlanningInput input, IReadOnlyList<double> times,
double stopBoundaryPathS, out LongitudinalCandidate candidate) double stopBoundaryPathS, out LongitudinalCandidate candidate)
{ {
@@ -609,6 +670,40 @@ public sealed class SequentialLongitudinalOptimizer
return AppendExactStopTail(times, stabilizationStart, input.StopBoundaryPathS, motion); return AppendExactStopTail(times, stabilizationStart, input.StopBoundaryPathS, motion);
} }
private static bool TryCloseExactStopEndpoint(LongitudinalPlanningInput input, IReadOnlyList<double> times,
int stabilizationStart, double[] jerk, out LongitudinalCandidate candidate)
{
candidate = null;
var motionTimes = new double[stabilizationStart + 1];
for (int index = 0; index < motionTimes.Length; index++)
motionTimes[index] = times[index];
LongitudinalCandidate motion = LongitudinalCandidate.Integrate(motionTimes, 0d,
input.InitialProgressSpeedMetersPerSecond, input.InitialAccelerationMetersPerSecondSquared, jerk);
int terminalIndex = motion.S.Count - 1;
double[] correction =
{
-motion.A[terminalIndex],
-motion.U[terminalIndex],
input.StopBoundaryPathS - motion.S[terminalIndex],
};
var influence = new double[3, 3];
for (int basisIndex = 0; basisIndex < 3; basisIndex++)
{
var basis = new double[stabilizationStart];
basis[stabilizationStart - 3 + basisIndex] = 1d;
LongitudinalCandidate response = LongitudinalCandidate.Integrate(motionTimes, 0d, 0d, 0d, basis);
influence[0, basisIndex] = response.A[terminalIndex];
influence[1, basisIndex] = response.U[terminalIndex];
influence[2, basisIndex] = response.S[terminalIndex];
}
if (!TrySolveThreeByThree(influence, correction, out double[] adjustment))
return false;
for (int index = 0; index < adjustment.Length; index++)
jerk[stabilizationStart - 3 + index] += adjustment[index];
candidate = CreateExactCandidate(input, times, stabilizationStart, jerk);
return true;
}
private static LongitudinalCandidate CreateScheduleReferenceSeed(LongitudinalPlanningInput input, private static LongitudinalCandidate CreateScheduleReferenceSeed(LongitudinalPlanningInput input,
IReadOnlyList<double> times, PathSpeedLimit speedLimit) IReadOnlyList<double> times, PathSpeedLimit speedLimit)
{ {
@@ -19,6 +19,7 @@ internal static class EmPlanningServiceChecks
VerifiesFullScopePublishesItsRequestScope(); VerifiesFullScopePublishesItsRequestScope();
VerifiesRequestAndStateFailuresPublishNoTrajectory(); VerifiesRequestAndStateFailuresPublishNoTrajectory();
VerifiesProjectionCorridorAndOptimizationFailuresPublishNoTrajectory(); VerifiesProjectionCorridorAndOptimizationFailuresPublishNoTrajectory();
VerifiesNoProgressPublishesNoTrajectory();
VerifiesTimeoutFallbackAndCancellationSemantics(); VerifiesTimeoutFallbackAndCancellationSemantics();
VerifiesPublicationFailureAndDebugIsolation(); VerifiesPublicationFailureAndDebugIsolation();
} }
@@ -396,6 +397,22 @@ internal static class EmPlanningServiceChecks
} }
} }
private static void VerifiesNoProgressPublishesNoTrajectory()
{
EmPlanningRequest request = CreateRequest(TravelDirection.Forward, 0d, false, false,
CreateReferencePath(TravelDirection.Forward, false, 0.10d), null,
EmPlanningScope.FullDirectionSegment);
request.Configuration.Scheduling.MaximumOptimizationSpatialStepMeters = 1d;
request.Configuration.Scheduling.MaximumOptimizationTimeStepSeconds = 1d;
EmPlanningResult result = new EmPlanningService(new ScriptedPipelineSolver(PipelineSolverMode.NoProgress)).Plan(
request, CancellationToken.None);
VerifyFailure(result, EmPlanningStatus.NoProgress, "full nonterminal no progress");
Verification.True(result.FailureReason.IndexOf("NoProgress", StringComparison.Ordinal) >= 0,
"no-progress failure preserves its diagnostic");
}
private static void VerifiesPublicationFailureAndDebugIsolation() private static void VerifiesPublicationFailureAndDebugIsolation()
{ {
EmPlanningRequest validationFailure = CreateRequest(TravelDirection.Forward, 0d, false, false); EmPlanningRequest validationFailure = CreateRequest(TravelDirection.Forward, 0d, false, false);
@@ -610,6 +627,7 @@ internal static class EmPlanningServiceChecks
SolverUnavailable, SolverUnavailable,
TimeoutWithoutFallback, TimeoutWithoutFallback,
TimeoutWithFallback, TimeoutWithFallback,
NoProgress,
PublicationValidationFailure, PublicationValidationFailure,
} }
@@ -647,6 +665,8 @@ internal static class EmPlanningServiceChecks
LastLongitudinalProblem = problem; LastLongitudinalProblem = problem;
if (mode == PipelineSolverMode.LongitudinalInfeasible) if (mode == PipelineSolverMode.LongitudinalInfeasible)
return Result(QpSolveStatus.PrimalInfeasible, Array.Empty<double>()); return Result(QpSolveStatus.PrimalInfeasible, Array.Empty<double>());
if (mode == PipelineSolverMode.NoProgress)
return Result(QpSolveStatus.Solved, new double[problem.VariableCount]);
if (strictFullPrimal != null && strictFullPrimal.Count == problem.VariableCount) if (strictFullPrimal != null && strictFullPrimal.Count == problem.VariableCount)
{ {
longitudinalCallCount++; longitudinalCallCount++;
@@ -14,6 +14,7 @@ internal static class LongitudinalIntegrationChecks
{ {
VerifiesRollingOptimizationKeepsANonzeroTerminalSpeed(); VerifiesRollingOptimizationKeepsANonzeroTerminalSpeed();
VerifiesFullDirectionScheduleIsIndependentFromPublicationCadence(); VerifiesFullDirectionScheduleIsIndependentFromPublicationCadence();
VerifiesFullDirectionStaticStartMakesProgress();
VerifiesFullDirectionPublicationDoesNotDuplicateItsTerminalHold(); VerifiesFullDirectionPublicationDoesNotDuplicateItsTerminalHold();
VerifiesExactStopIncludesAStabilizationTail(); VerifiesExactStopIncludesAStabilizationTail();
VerifiesLastStrictCandidateSurvivesLaterTimeout(); VerifiesLastStrictCandidateSurvivesLaterTimeout();
@@ -109,6 +110,46 @@ internal static class LongitudinalIntegrationChecks
"halving publication cadence doubles emitted trajectory intervals without changing optimization knots"); "halving publication cadence doubles emitted trajectory intervals without changing optimization knots");
} }
private static void VerifiesFullDirectionStaticStartMakesProgress()
{
EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault();
configuration.Scheduling.SolverTimeoutSeconds = 1d;
configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond = 1d;
configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d;
configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1d;
configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d;
configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 1d;
LateralPath path = new LateralPath(new[]
{
Point(0d, 0d, 0d),
Point(2.5d, 2.5d, 0d),
Point(5d, 5d, 0d),
}, true);
EmPlanningStatus status = new PathSpeedLimitBuilder().Build(path, TravelDirection.Forward, 0d,
EmTerminalType.Goal, configuration, out PathSpeedLimit speedLimit, out string failureReason);
Verification.Equal(EmPlanningStatus.Success, status, "static-start envelope: " + failureReason);
status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0d, 0d,
configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration,
out LongitudinalKnotSchedule schedule, out failureReason);
Verification.Equal(EmPlanningStatus.Success, status, "static-start schedule: " + failureReason);
var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0d, 0d,
EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration,
EmPlanningScope.FullDirectionSegment, schedule, Array.Empty<double>(), Array.Empty<double>());
LongitudinalPlanningResult result = new LongitudinalPlanner(new OsqpNativeSolver()).Plan(input,
CancellationToken.None);
Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback,
"static start succeeds: " + result.FailureReason);
LongitudinalCandidate candidate = result.Candidate ??
throw new InvalidOperationException("static start must expose a successful candidate.");
Verification.True(HasValueGreaterThan(candidate.S, 0.05d), "static start makes measurable progress");
Verification.True(HasValueGreaterThan(candidate.U, 0.05d), "static start accelerates");
Verification.NearlyEqual(0d, candidate.U[candidate.U.Count - 1],
"full segment stops at the terminal boundary");
}
private static void VerifiesFullDirectionPublicationDoesNotDuplicateItsTerminalHold() private static void VerifiesFullDirectionPublicationDoesNotDuplicateItsTerminalHold()
{ {
EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault(); EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault();
@@ -191,6 +232,16 @@ internal static class LongitudinalIntegrationChecks
return count; return count;
} }
private static bool HasValueGreaterThan(IReadOnlyList<double> values, double threshold)
{
for (int index = 0; index < values.Count; index++)
{
if (values[index] > threshold)
return true;
}
return false;
}
public static void RunRealOsqp() public static void RunRealOsqp()
{ {
foreach (LongitudinalScenario scenario in CreateRealOsqpScenarios()) foreach (LongitudinalScenario scenario in CreateRealOsqpScenarios())
@@ -24,6 +24,7 @@ internal static class LongitudinalModelChecks
VerifiesFullDirectionInitialFeasibilityProjectionAndFallbackSemantics(); VerifiesFullDirectionInitialFeasibilityProjectionAndFallbackSemantics();
VerifiesTimeKnotLayoutDynamicsObjectiveAndHardConstraints(); VerifiesTimeKnotLayoutDynamicsObjectiveAndHardConstraints();
VerifiesModeSpecificSolutionValidation(); VerifiesModeSpecificSolutionValidation();
VerifiesFullDirectionNoProgressValidation();
VerifiesPreviousTrajectorySeedResamplesAndProjectsMonotonically(); VerifiesPreviousTrajectorySeedResamplesAndProjectsMonotonically();
} }
@@ -196,9 +197,10 @@ internal static class LongitudinalModelChecks
out string failureReason); out string failureReason);
Verification.Equal(EmPlanningStatus.Success, status, "jerk-limited stopping-tail status: " + failureReason); Verification.Equal(EmPlanningStatus.Success, status, "jerk-limited stopping-tail status: " + failureReason);
double nearBoundaryPathS = input.StopBoundaryPathS - 0.005d; double nearBoundaryPathS = input.StopBoundaryPathS - 0.005d;
Verification.NearlyEqual(MaximumJerkLimitedStopSpeed(input, nearBoundaryPathS), Verification.True(envelope.StoppingLimitAt(nearBoundaryPathS) > 0d &&
envelope.StoppingLimitAt(nearBoundaryPathS), envelope.StoppingLimitAt(nearBoundaryPathS) <=
"near-boundary speed cap uses jerk-limited distance inversion"); MaximumJerkLimitedStopSpeed(input, nearBoundaryPathS) + 1e-12d,
"near-boundary speed cap conservatively interpolates jerk-limited distance inversion");
Verification.NearlyEqual(0d, envelope.StoppingLimitAt(input.StopBoundaryPathS), Verification.NearlyEqual(0d, envelope.StoppingLimitAt(input.StopBoundaryPathS),
"real stop boundary keeps an exact zero stopping cap"); "real stop boundary keeps an exact zero stopping cap");
} }
@@ -608,10 +610,11 @@ internal static class LongitudinalModelChecks
(envelope.PathS[envelopeSegment + 1] - envelope.PathS[envelopeSegment]); (envelope.PathS[envelopeSegment + 1] - envelope.PathS[envelopeSegment]);
double envelopeIntercept = envelope.MaximumSpeedMetersPerSecond[envelopeSegment] - double envelopeIntercept = envelope.MaximumSpeedMetersPerSecond[envelopeSegment] -
envelopeSlope * envelope.PathS[envelopeSegment]; envelopeSlope * envelope.PathS[envelopeSegment];
Verification.Equal(1, CountBoundedRow(problem, new Dictionary<int, double> var envelopeRow = new Dictionary<int, double> { { layout.U(1), 1d } };
{ if (Math.Abs(envelopeSlope) > 1e-12d)
{ layout.U(1), 1d }, { layout.S(1), -envelopeSlope }, envelopeRow.Add(layout.S(1), -envelopeSlope);
}, -QuadraticProgram.MaximumFiniteBound, envelopeIntercept), Verification.Equal(1, CountBoundedRow(problem, envelopeRow,
-QuadraticProgram.MaximumFiniteBound, envelopeIntercept),
"U upper bound linearly re-evaluates the actual PathS envelope"); "U upper bound linearly re-evaluates the actual PathS envelope");
FindSingleVariableBounds(problem, layout.A(1), out double aLower, out double aUpper); FindSingleVariableBounds(problem, layout.A(1), out double aLower, out double aUpper);
Verification.NearlyEqual(-1d, aLower, "deceleration lower bound"); Verification.NearlyEqual(-1d, aLower, "deceleration lower bound");
@@ -724,6 +727,53 @@ internal static class LongitudinalModelChecks
"approach failure identifies the jerk-limited stoppable set"); "approach failure identifies the jerk-limited stoppable set");
} }
private static void VerifiesFullDirectionNoProgressValidation()
{
EmPlannerConfiguration configuration = CreateUnitScaleConfiguration();
double[] times = { 0d, 0.10d, 0.20d, 0.30d, 0.50d };
var fullSchedule = new LongitudinalKnotSchedule(times,
new[] { 0d, 0.04d, 0.08d, 0.10d, 0.10d }, new[] { 0d, 0.20d, 0.10d, 0d, 0d }, true, 3);
LateralPath path = CreateStraightPath(0.10d);
var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0d, 0d,
EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration,
EmPlanningScope.FullDirectionSegment, fullSchedule, Array.Empty<double>(), Array.Empty<double>());
EmPlanningStatus speedStatus = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit speedLimit,
out string speedFailure);
Verification.Equal(EmPlanningStatus.Success, speedStatus, "no-progress envelope: " + speedFailure);
LongitudinalCandidate stationary = LongitudinalCandidate.Integrate(times, 0d, 0d, 0d,
new[] { 0d, 0d, 0d, 0d });
var validator = new LongitudinalSolutionValidator();
Verification.True(!validator.TryValidate(input, speedLimit, stationary, out _, out EmPlanningStatus failureStatus,
out string failureReason), "nonterminal stationary full segment is rejected");
Verification.Equal(EmPlanningStatus.NoProgress, failureStatus, "stationary full segment has exact status");
Verification.True(failureReason.IndexOf("NoProgress", StringComparison.Ordinal) >= 0,
"stationary full segment has exact diagnostic");
Verification.True(new LongitudinalConstraintBuilder(new LongitudinalObjectiveBuilder()).TryBuild(input,
speedLimit, stationary, out QuadraticProgram fullProblem, out string buildFailure),
"full no-progress objective builds: " + buildFailure);
var fullLayout = new LongitudinalVariableLayout(times.Length);
double expectedReferenceLinearCost = -2d * configuration.Longitudinal.Weights.ReferenceSpeed *
fullSchedule.ReferenceSpeedMetersPerSecond[1] /
(input.DirectionMaximumSpeedMetersPerSecond * input.DirectionMaximumSpeedMetersPerSecond);
Verification.NearlyEqual(expectedReferenceLinearCost, fullProblem.LinearCost[fullLayout.U(1)],
"full direction objective tracks adaptive speed reference");
LateralPath nearTerminalPath = CreateStraightPath(0.02d);
var nearTerminalSchedule = new LongitudinalKnotSchedule(times,
new[] { 0d, 0.01d, 0.02d, 0.02d, 0.02d }, new[] { 0d, 0d, 0d, 0d, 0d }, true, 3);
var nearTerminalInput = new LongitudinalPlanningInput(nearTerminalPath, TravelDirection.Forward, 0d, 0d,
EmTerminalType.GearSwitch, EmLongitudinalMode.ExactStopAtBoundary, configuration,
EmPlanningScope.FullDirectionSegment, nearTerminalSchedule, Array.Empty<double>(), Array.Empty<double>());
speedStatus = new PathSpeedLimitBuilder().Build(nearTerminalInput, out PathSpeedLimit nearTerminalLimit,
out speedFailure);
Verification.Equal(EmPlanningStatus.Success, speedStatus, "near-terminal no-progress envelope: " + speedFailure);
Verification.True(!validator.TryValidate(nearTerminalInput, nearTerminalLimit, stationary, out _,
out failureStatus, out _), "near-terminal fixture remains subject to its stop-tail contract");
Verification.True(failureStatus != EmPlanningStatus.NoProgress,
"near-terminal gear-switch stop is not classified as no progress");
}
private static void VerifiesPreviousTrajectorySeedResamplesAndProjectsMonotonically() private static void VerifiesPreviousTrajectorySeedResamplesAndProjectsMonotonically()
{ {
LateralPath path = CreateStraightPath(1d); LateralPath path = CreateStraightPath(1d);
@@ -848,7 +898,7 @@ internal static class LongitudinalModelChecks
LongitudinalConfiguration limits = input.Configuration.Longitudinal; LongitudinalConfiguration limits = input.Configuration.Longitudinal;
return JerkLimitedStoppingMath.MaximumInitialSpeedForDistance( return JerkLimitedStoppingMath.MaximumInitialSpeedForDistance(
Math.Max(0d, input.StopBoundaryPathS - pathS), Math.Max(0d, input.StopBoundaryPathS - pathS),
limits.MaximumAccelerationMetersPerSecondSquared, Math.Max(0d, input.InitialAccelerationMetersPerSecondSquared),
limits.MaximumDecelerationMetersPerSecondSquared, limits.MaximumDecelerationMetersPerSecondSquared,
limits.MaximumJerkMetersPerSecondCubed, limits.MaximumJerkMetersPerSecondCubed,
input.DirectionMaximumSpeedMetersPerSecond); input.DirectionMaximumSpeedMetersPerSecond);