feat: seed rolling and exact-stop ST profiles

This commit is contained in:
梁薄云
2026-08-05 18:18:49 +08:00
parent 26bd822ec9
commit 59d13e5783
3 changed files with 526 additions and 74 deletions
@@ -21,11 +21,12 @@ public sealed class LongitudinalObjectiveBuilder
double accelerationScale = RequirePositive(Math.Max(configuration.MaximumAccelerationMetersPerSecondSquared, double accelerationScale = RequirePositive(Math.Max(configuration.MaximumAccelerationMetersPerSecondSquared,
configuration.MaximumDecelerationMetersPerSecondSquared), nameof(accelerationScale)); configuration.MaximumDecelerationMetersPerSecondSquared), nameof(accelerationScale));
double jerkScale = RequirePositive(configuration.MaximumJerkMetersPerSecondCubed, nameof(jerkScale)); double jerkScale = RequirePositive(configuration.MaximumJerkMetersPerSecondCubed, nameof(jerkScale));
double progressScale = input.TerminalPathS > 0d ? input.TerminalPathS : 1d; double progressScale = input.PathUpperBoundS > 0d ? input.PathUpperBoundS : 1d;
for (int index = 0; index < layout.KnotCount; index++) for (int index = 0; index < layout.KnotCount; index++)
{ {
AddSquaredResidual(hessian, linearCost, layout.U(index), speedLimit.MaximumSpeedAt(iterate.S[index]), double iteratePathS = Math.Max(0d, Math.Min(input.PathUpperBoundS, iterate.S[index]));
AddSquaredResidual(hessian, linearCost, layout.U(index), speedLimit.MaximumSpeedAt(iteratePathS),
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)
@@ -38,8 +39,11 @@ public sealed class LongitudinalObjectiveBuilder
} }
for (int index = 0; index < layout.KnotCount - 1; index++) for (int index = 0; index < layout.KnotCount - 1; index++)
AddSquaredResidual(hessian, linearCost, layout.J(index), 0d, weights.Jerk, jerkScale); AddSquaredResidual(hessian, linearCost, layout.J(index), 0d, weights.Jerk, jerkScale);
AddSquaredResidual(hessian, linearCost, layout.A(layout.KnotCount - 1), 0d, weights.TerminalAcceleration, if (input.Mode != EmLongitudinalMode.ExactStopAtBoundary)
accelerationScale); {
AddSquaredResidual(hessian, linearCost, layout.A(layout.KnotCount - 1), 0d,
weights.TerminalAcceleration, accelerationScale);
}
} }
private static void AddSquaredResidual(SparseTripletBuilder hessian, IList<double> linearCost, int variable, private static void AddSquaredResidual(SparseTripletBuilder hessian, IList<double> linearCost, int variable,
@@ -47,7 +47,7 @@ public sealed class SequentialLongitudinalOptimizer
if (speedStatus != EmPlanningStatus.Success) if (speedStatus != EmPlanningStatus.Success)
return Failed(speedStatus, speedFailure); return Failed(speedStatus, speedFailure);
LongitudinalCandidate iterate = CreateInitialIterate(input); LongitudinalCandidate iterate = CreateInitialIterate(input, speedLimit);
double[] warmStart = ToPrimal(iterate); double[] warmStart = ToPrimal(iterate);
bool hasDynamicsConsistentInitialWarmStart = iterate.SatisfiesExactDiscreteDynamics(1e-12d); bool hasDynamicsConsistentInitialWarmStart = iterate.SatisfiesExactDiscreteDynamics(1e-12d);
LongitudinalCandidate lastStrictCandidate; LongitudinalCandidate lastStrictCandidate;
@@ -191,44 +191,111 @@ public sealed class SequentialLongitudinalOptimizer
} }
} }
private static LongitudinalCandidate CreateInitialIterate(LongitudinalPlanningInput input) private LongitudinalCandidate CreateInitialIterate(LongitudinalPlanningInput input, PathSpeedLimit speedLimit)
{ {
IReadOnlyList<double> times = LongitudinalCandidate.CreateKnotTimes(input.Configuration.Scheduling.TimeHorizonSeconds, IReadOnlyList<double> times = LongitudinalCandidate.CreateKnotTimes(input.Configuration.Scheduling.TimeHorizonSeconds,
input.Configuration.Scheduling.OutputTimeStepSeconds); input.Configuration.Scheduling.OutputTimeStepSeconds);
if (TryCreateCruiseThenBrakeSeed(input, times, out LongitudinalCandidate brakingSeed)) switch (input.Mode)
return brakingSeed;
int knotCount = times.Count;
var s = new double[knotCount];
var u = new double[knotCount];
var a = new double[knotCount];
var j = new double[knotCount - 1];
double horizon = times[knotCount - 1];
double requestedSpeed = Math.Min(input.DirectionMaximumSpeedMetersPerSecond,
Math.Max(0d, input.TerminalPathS / horizon));
if (!JerkLimitedStoppingMath.TryCalculate(requestedSpeed, 0d,
input.Configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared,
input.Configuration.Longitudinal.MaximumJerkMetersPerSecondCubed,
out JerkLimitedStoppingProfile terminalStop, out _))
{ {
throw new ArgumentOutOfRangeException(nameof(input)); case EmLongitudinalMode.RollingContinuation:
return CreateRollingSeed(input, times, speedLimit);
case EmLongitudinalMode.ApproachStopBoundary:
return CreateApproachSeed(input, times, speedLimit);
case EmLongitudinalMode.ExactStopAtBoundary:
return CreateExactStopSeed(input, times, speedLimit);
default:
throw new ArgumentOutOfRangeException(nameof(input.Mode));
} }
double cruiseDistance = Math.Max(0d, input.TerminalPathS - terminalStop.DistanceMeters); }
for (int index = 0; index < knotCount; index++)
private static LongitudinalCandidate CreateRollingSeed(LongitudinalPlanningInput input,
IReadOnlyList<double> times, PathSpeedLimit speedLimit)
{ {
double fraction = (double)index / (knotCount - 1); return CreateEnvelopeSeed(input, times, speedLimit);
s[index] = Math.Min(input.TerminalPathS, cruiseDistance * fraction + terminalStop.DistanceMeters * fraction * fraction);
u[index] = index == 0 ? input.InitialProgressSpeedMetersPerSecond : requestedSpeed;
a[index] = index == 0 ? input.InitialAccelerationMetersPerSecondSquared : 0d;
} }
s[0] = 0d;
s[knotCount - 1] = input.TerminalPathS; private static LongitudinalCandidate CreateApproachSeed(LongitudinalPlanningInput input,
u[knotCount - 1] = 0d; IReadOnlyList<double> times, PathSpeedLimit speedLimit)
a[knotCount - 1] = 0d; {
return new LongitudinalCandidate(times, s, u, a, j); return CreateEnvelopeSeed(input, times, speedLimit);
}
private static LongitudinalCandidate CreateEnvelopeSeed(LongitudinalPlanningInput input,
IReadOnlyList<double> times, PathSpeedLimit speedLimit)
{
LongitudinalConfiguration configuration = input.Configuration.Longitudinal;
var jerk = new double[times.Count - 1];
double s = 0d;
double u = input.InitialProgressSpeedMetersPerSecond;
double a = input.InitialAccelerationMetersPerSecondSquared;
for (int index = 0; index < jerk.Length; index++)
{
double dt = times[index + 1] - times[index];
double speedLimitAtS = speedLimit.MaximumSpeedAt(Math.Max(0d, Math.Min(input.PathUpperBoundS, s)));
double targetSpeed = Math.Min(input.InitialProgressSpeedMetersPerSecond, speedLimitAtS);
double lowerJerk = Math.Max(-configuration.MaximumJerkMetersPerSecondCubed,
(-configuration.MaximumDecelerationMetersPerSecondSquared - a) / dt);
lowerJerk = Math.Max(lowerJerk, -2d * (u + a * dt) / (dt * dt));
double upperJerk = Math.Min(configuration.MaximumJerkMetersPerSecondCubed,
(configuration.MaximumAccelerationMetersPerSecondSquared - a) / dt);
double requestedJerk = 2d * (targetSpeed - u - a * dt) / (dt * dt);
double selectedJerk = Clamp(requestedJerk, lowerJerk, upperJerk);
IntegrateStep(s, u, a, selectedJerk, dt, out double nextS, out double nextU, out double nextA);
if (nextU > speedLimit.MaximumSpeedAt(Math.Max(0d, Math.Min(input.PathUpperBoundS, nextS))) + 1e-12d)
{
double lower = lowerJerk;
double upper = selectedJerk;
for (int iteration = 0; iteration < 48; iteration++)
{
double midpoint = 0.5d * (lower + upper);
IntegrateStep(s, u, a, midpoint, dt, out double probeS, out double probeU, out _);
if (probeU <= speedLimit.MaximumSpeedAt(Math.Max(0d, Math.Min(input.PathUpperBoundS, probeS))))
lower = midpoint;
else
upper = midpoint;
}
selectedJerk = lower;
IntegrateStep(s, u, a, selectedJerk, dt, out nextS, out nextU, out nextA);
}
jerk[index] = selectedJerk;
s = nextS;
u = nextU;
a = nextA;
}
return LongitudinalCandidate.Integrate(times, 0d, input.InitialProgressSpeedMetersPerSecond,
input.InitialAccelerationMetersPerSecondSquared, jerk);
}
private LongitudinalCandidate CreateExactStopSeed(LongitudinalPlanningInput input,
IReadOnlyList<double> times, PathSpeedLimit speedLimit)
{
int stabilizationStart = LongitudinalTerminalSchedule.GetStabilizationStartIndex(times,
input.Configuration.Scheduling.OutputTimeStepSeconds);
var motionTimes = new double[stabilizationStart + 1];
for (int index = 0; index < motionTimes.Length; index++)
motionTimes[index] = times[index];
if (TryCreateCruiseThenBrakeSeed(input, motionTimes, input.StopBoundaryPathS,
out LongitudinalCandidate cruiseThenBrake))
{
LongitudinalCandidate candidate = AppendExactStopTail(times, stabilizationStart,
input.StopBoundaryPathS, cruiseThenBrake);
if (_solutionValidator.TryValidate(input, speedLimit, candidate,
out LongitudinalCandidate validated, out _))
{
return validated;
}
}
if (TryCreateExactJerkSeed(input, times, stabilizationStart, speedLimit,
out LongitudinalCandidate exactSeed))
return exactSeed;
return CreateApproachSeed(input, times, speedLimit);
} }
private static bool TryCreateCruiseThenBrakeSeed(LongitudinalPlanningInput input, IReadOnlyList<double> times, private static bool TryCreateCruiseThenBrakeSeed(LongitudinalPlanningInput input, IReadOnlyList<double> times,
out LongitudinalCandidate candidate) double stopBoundaryPathS, out LongitudinalCandidate candidate)
{ {
candidate = null; candidate = null;
double initialSpeed = input.InitialProgressSpeedMetersPerSecond; double initialSpeed = input.InitialProgressSpeedMetersPerSecond;
@@ -260,11 +327,11 @@ public sealed class SequentialLongitudinalOptimizer
} }
int brakingIntervals = 2 * rampIntervals + plateauIntervals; int brakingIntervals = 2 * rampIntervals + plateauIntervals;
double brakingDistance = 0.5d * initialSpeed * brakingIntervals * timeStep; double brakingDistance = 0.5d * initialSpeed * brakingIntervals * timeStep;
if (brakingDistance > input.TerminalPathS + 1e-12d) if (brakingDistance > stopBoundaryPathS + 1e-12d)
continue; continue;
int maximumCruiseIntervals = intervalCount - brakingIntervals; int maximumCruiseIntervals = intervalCount - brakingIntervals;
int cruiseIntervals = Math.Min(maximumCruiseIntervals, Math.Max(0, checked((int)Math.Floor( int cruiseIntervals = Math.Min(maximumCruiseIntervals, Math.Max(0, checked((int)Math.Floor(
(input.TerminalPathS - brakingDistance) / (initialSpeed * timeStep) + 1e-12d)))); (stopBoundaryPathS - brakingDistance) / (initialSpeed * timeStep) + 1e-12d))));
var jerk = new double[intervalCount]; var jerk = new double[intervalCount];
int cursor = cruiseIntervals; int cursor = cruiseIntervals;
for (int index = 0; index < rampIntervals; index++) for (int index = 0; index < rampIntervals; index++)
@@ -273,7 +340,9 @@ public sealed class SequentialLongitudinalOptimizer
for (int index = 0; index < rampIntervals; index++) for (int index = 0; index < rampIntervals; index++)
jerk[cursor++] = jerkMagnitude; jerk[cursor++] = jerkMagnitude;
LongitudinalCandidate integrated = LongitudinalCandidate.Integrate(times, 0d, initialSpeed, 0d, jerk); LongitudinalCandidate integrated = LongitudinalCandidate.Integrate(times, 0d, initialSpeed, 0d, jerk);
if (integrated.S[integrated.S.Count - 1] <= input.TerminalPathS + 1e-12d) int lastIndex = integrated.S.Count - 1;
if (Math.Abs(integrated.S[lastIndex] - stopBoundaryPathS) <= 1e-10d &&
Math.Abs(integrated.U[lastIndex]) <= 1e-10d && Math.Abs(integrated.A[lastIndex]) <= 1e-10d)
{ {
candidate = integrated; candidate = integrated;
return true; return true;
@@ -283,6 +352,280 @@ public sealed class SequentialLongitudinalOptimizer
return false; return false;
} }
private bool TryCreateExactJerkSeed(LongitudinalPlanningInput input, IReadOnlyList<double> times,
int stabilizationStart, PathSpeedLimit speedLimit, out LongitudinalCandidate candidate)
{
candidate = null;
int intervalCount = stabilizationStart;
if (intervalCount < 3)
return false;
var motionTimes = new double[intervalCount + 1];
for (int index = 0; index < motionTimes.Length; index++)
motionTimes[index] = times[index];
LongitudinalCandidate baseline = CreateApproachSeed(input, motionTimes, speedLimit);
var influence = new double[3, intervalCount];
for (int interval = 0; interval < intervalCount; interval++)
{
var basis = new double[intervalCount];
basis[interval] = 1d;
LongitudinalCandidate response = LongitudinalCandidate.Integrate(motionTimes, 0d, 0d, 0d, basis);
int last = response.S.Count - 1;
influence[0, interval] = response.A[last];
influence[1, interval] = response.U[last];
influence[2, interval] = response.S[last];
}
double[] target =
{
-baseline.A[baseline.A.Count - 1],
-baseline.U[baseline.U.Count - 1],
input.StopBoundaryPathS - baseline.S[baseline.S.Count - 1],
};
var gram = new double[3, 3];
for (int row = 0; row < 3; row++)
{
for (int column = 0; column < 3; column++)
{
for (int interval = 0; interval < intervalCount; interval++)
gram[row, column] += influence[row, interval] * influence[column, interval];
}
}
if (!TrySolveThreeByThree(gram, target, out double[] multipliers))
return false;
var jerk = new double[intervalCount];
for (int interval = 0; interval < intervalCount; interval++)
{
jerk[interval] = baseline.J[interval];
for (int row = 0; row < 3; row++)
jerk[interval] += influence[row, interval] * multipliers[row];
}
if (TryValidateExactSeed(input, times, stabilizationStart, speedLimit, jerk, out candidate))
return true;
double currentViolation = CalculateExactSeedViolation(input, speedLimit,
CreateExactCandidate(input, times, stabilizationStart, jerk));
double maximumJerk = input.Configuration.Longitudinal.MaximumJerkMetersPerSecondCubed;
for (int pass = 0; pass < 4; pass++)
{
for (int basisIndex = 0; basisIndex < intervalCount; basisIndex++)
{
double[] direction = CreateEndpointNullspaceDirection(influence, gram, basisIndex);
if (direction == null)
continue;
double[] bestJerk = jerk;
double bestViolation = currentViolation;
for (int sample = -256; sample <= 256; sample++)
{
double scale = maximumJerk * sample / 256d;
var probeJerk = new double[intervalCount];
for (int interval = 0; interval < intervalCount; interval++)
probeJerk[interval] = jerk[interval] + scale * direction[interval];
LongitudinalCandidate probe = CreateExactCandidate(input, times, stabilizationStart, probeJerk);
double violation = CalculateExactSeedViolation(input, speedLimit, probe);
if (violation < bestViolation)
{
bestViolation = violation;
bestJerk = probeJerk;
}
}
jerk = bestJerk;
currentViolation = bestViolation;
if (TryValidateExactSeed(input, times, stabilizationStart, speedLimit, jerk, out candidate))
return true;
}
}
return false;
}
private bool TryValidateExactSeed(LongitudinalPlanningInput input, IReadOnlyList<double> times,
int stabilizationStart, PathSpeedLimit speedLimit, IReadOnlyList<double> jerk,
out LongitudinalCandidate candidate)
{
LongitudinalCandidate probe = CreateExactCandidate(input, times, stabilizationStart, jerk);
return _solutionValidator.TryValidate(input, speedLimit, probe, out candidate, out _);
}
private static LongitudinalCandidate CreateExactCandidate(LongitudinalPlanningInput input,
IReadOnlyList<double> times, int stabilizationStart, IReadOnlyList<double> jerk)
{
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);
return AppendExactStopTail(times, stabilizationStart, input.StopBoundaryPathS, motion);
}
private static double[] CreateEndpointNullspaceDirection(double[,] influence, double[,] gram, int basisIndex)
{
int intervalCount = influence.GetLength(1);
double[] rightHandSide = { influence[0, basisIndex], influence[1, basisIndex], influence[2, basisIndex] };
if (!TrySolveThreeByThree(gram, rightHandSide, out double[] multipliers))
return null;
var direction = new double[intervalCount];
double magnitude = 0d;
for (int interval = 0; interval < intervalCount; interval++)
{
direction[interval] = interval == basisIndex ? 1d : 0d;
for (int row = 0; row < 3; row++)
direction[interval] -= influence[row, interval] * multipliers[row];
magnitude = Math.Max(magnitude, Math.Abs(direction[interval]));
}
if (magnitude <= 1e-12d)
return null;
for (int interval = 0; interval < intervalCount; interval++)
direction[interval] /= magnitude;
return direction;
}
private static double CalculateExactSeedViolation(LongitudinalPlanningInput input, PathSpeedLimit speedLimit,
LongitudinalCandidate candidate)
{
double tolerance = input.Configuration.Validation.KinematicTolerance;
double maximumAcceleration = input.Configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared;
double maximumDeceleration = input.Configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared;
double maximumJerk = input.Configuration.Longitudinal.MaximumJerkMetersPerSecondCubed;
double violation = 0d;
double previousS = double.NegativeInfinity;
for (int index = 0; index < candidate.S.Count; index++)
{
double s = candidate.S[index];
double u = candidate.U[index];
double a = candidate.A[index];
if (!IsFinite(s) || !IsFinite(u) || !IsFinite(a))
return double.PositiveInfinity;
violation += SquaredExcess(-s, tolerance);
violation += SquaredExcess(s - input.PathUpperBoundS, tolerance);
violation += SquaredExcess(previousS - s, tolerance);
violation += SquaredExcess(-u, tolerance);
violation += SquaredExcess(a - maximumAcceleration, tolerance);
violation += SquaredExcess(-maximumDeceleration - a, tolerance);
double speedLimitAtS = speedLimit.MaximumSpeedAt(Math.Max(0d, Math.Min(input.PathUpperBoundS, s)));
violation += SquaredExcess(u - speedLimitAtS, tolerance);
if (!JerkLimitedStoppingMath.TryCalculate(u, a, maximumDeceleration, maximumJerk,
out JerkLimitedStoppingProfile stop, out _))
{
return double.PositiveInfinity;
}
violation += SquaredExcess(s + stop.DistanceMeters - input.StopBoundaryPathS, tolerance);
previousS = s;
}
for (int index = 0; index < candidate.J.Count; index++)
{
if (!IsFinite(candidate.J[index]))
return double.PositiveInfinity;
violation += SquaredExcess(Math.Abs(candidate.J[index]) - maximumJerk, tolerance);
}
return violation;
}
private static double SquaredExcess(double actual, double tolerance)
{
double excess = Math.Max(0d, actual - tolerance);
return excess * excess;
}
private static LongitudinalCandidate AppendExactStopTail(IReadOnlyList<double> times, int stabilizationStart,
double stopBoundaryPathS, LongitudinalCandidate motion)
{
var s = new double[times.Count];
var u = new double[times.Count];
var a = new double[times.Count];
var jerk = new double[times.Count - 1];
int motionCount = Math.Min(stabilizationStart + 1, motion.S.Count);
for (int index = 0; index < motionCount; index++)
{
s[index] = motion.S[index];
u[index] = motion.U[index];
a[index] = motion.A[index];
}
for (int index = 0; index < Math.Min(stabilizationStart, motion.J.Count); index++)
jerk[index] = motion.J[index];
for (int index = stabilizationStart; index < times.Count; index++)
{
s[index] = stopBoundaryPathS;
u[index] = 0d;
a[index] = 0d;
}
return new LongitudinalCandidate(times, s, u, a, jerk);
}
private static bool SatisfiesLongitudinalBounds(LongitudinalPlanningInput input, LongitudinalCandidate candidate)
{
LongitudinalConfiguration configuration = input.Configuration.Longitudinal;
double previousS = double.NegativeInfinity;
for (int index = 0; index < candidate.S.Count; index++)
{
if (candidate.S[index] < -1e-10d || candidate.S[index] > input.StopBoundaryPathS + 1e-10d ||
candidate.S[index] < previousS - 1e-10d || candidate.U[index] < -1e-10d ||
candidate.U[index] > input.DirectionMaximumSpeedMetersPerSecond + 1e-10d ||
candidate.A[index] < -configuration.MaximumDecelerationMetersPerSecondSquared - 1e-10d ||
candidate.A[index] > configuration.MaximumAccelerationMetersPerSecondSquared + 1e-10d)
{
return false;
}
previousS = candidate.S[index];
}
for (int index = 0; index < candidate.J.Count; index++)
{
if (Math.Abs(candidate.J[index]) > configuration.MaximumJerkMetersPerSecondCubed + 1e-10d)
return false;
}
return true;
}
private static bool TrySolveThreeByThree(double[,] matrix, IReadOnlyList<double> rightHandSide,
out double[] solution)
{
solution = new double[3];
var augmented = new double[3, 4];
for (int row = 0; row < 3; row++)
{
for (int column = 0; column < 3; column++)
augmented[row, column] = matrix[row, column];
augmented[row, 3] = rightHandSide[row];
}
for (int pivot = 0; pivot < 3; pivot++)
{
int bestRow = pivot;
for (int row = pivot + 1; row < 3; row++)
{
if (Math.Abs(augmented[row, pivot]) > Math.Abs(augmented[bestRow, pivot]))
bestRow = row;
}
if (Math.Abs(augmented[bestRow, pivot]) <= 1e-14d)
return false;
if (bestRow != pivot)
{
for (int column = pivot; column < 4; column++)
{
double temporary = augmented[pivot, column];
augmented[pivot, column] = augmented[bestRow, column];
augmented[bestRow, column] = temporary;
}
}
double divisor = augmented[pivot, pivot];
for (int column = pivot; column < 4; column++)
augmented[pivot, column] /= divisor;
for (int row = 0; row < 3; row++)
{
if (row == pivot)
continue;
double factor = augmented[row, pivot];
for (int column = pivot; column < 4; column++)
augmented[row, column] -= factor * augmented[pivot, column];
}
}
for (int row = 0; row < 3; row++)
solution[row] = augmented[row, 3];
return true;
}
private static bool TryCreateCandidate(IReadOnlyList<double> times, IReadOnlyList<double> primal, private static bool TryCreateCandidate(IReadOnlyList<double> times, IReadOnlyList<double> primal,
out LongitudinalCandidate candidate) out LongitudinalCandidate candidate)
{ {
@@ -336,6 +679,10 @@ public sealed class SequentialLongitudinalOptimizer
nextIterate = null; nextIterate = null;
if (candidate.S.Count != previous.S.Count) if (candidate.S.Count != previous.S.Count)
return false; return false;
int stabilizationStart = input.Mode == EmLongitudinalMode.ExactStopAtBoundary
? LongitudinalTerminalSchedule.GetStabilizationStartIndex(candidate.KnotTimes,
input.Configuration.Scheduling.OutputTimeStepSeconds)
: candidate.S.Count;
var candidateProgressSamples = new double[candidate.S.Count]; var candidateProgressSamples = new double[candidate.S.Count];
double priorProgress = double.NegativeInfinity; double priorProgress = double.NegativeInfinity;
double priorPreviousProgress = double.NegativeInfinity; double priorPreviousProgress = double.NegativeInfinity;
@@ -343,7 +690,7 @@ public sealed class SequentialLongitudinalOptimizer
{ {
double candidateProgress = candidate.S[index]; double candidateProgress = candidate.S[index];
double previousProgress = previous.S[index]; double previousProgress = previous.S[index];
if (!IsFinite(previousProgress) || previousProgress < 0d || previousProgress > input.TerminalPathS || if (!IsFinite(previousProgress) || previousProgress < 0d || previousProgress > input.PathUpperBoundS ||
previousProgress < priorPreviousProgress) previousProgress < priorPreviousProgress)
{ {
return false; return false;
@@ -352,9 +699,9 @@ public sealed class SequentialLongitudinalOptimizer
{ {
candidateProgress = previousProgress; candidateProgress = previousProgress;
} }
candidateProgress = Math.Max(0d, Math.Min(input.TerminalPathS, candidateProgress)); candidateProgress = Math.Max(0d, Math.Min(input.PathUpperBoundS, candidateProgress));
if (index == candidate.S.Count - 1) if (input.Mode == EmLongitudinalMode.ExactStopAtBoundary && index >= stabilizationStart)
candidateProgress = input.TerminalPathS; candidateProgress = input.StopBoundaryPathS;
candidateProgress = Math.Max(priorProgress, candidateProgress); candidateProgress = Math.Max(priorProgress, candidateProgress);
candidateProgressSamples[index] = candidateProgress; candidateProgressSamples[index] = candidateProgress;
priorProgress = candidateProgress; priorProgress = candidateProgress;
@@ -365,7 +712,8 @@ public sealed class SequentialLongitudinalOptimizer
for (int index = 0; index < progress.Length; index++) for (int index = 0; index < progress.Length; index++)
{ {
double candidateProgress = candidateProgressSamples[index]; double candidateProgress = candidateProgressSamples[index];
if (index == 0 || index == progress.Length - 1 || candidateProgress >= input.TerminalPathS) if (index == 0 || index == progress.Length - 1 || (input.Mode == EmLongitudinalMode.ExactStopAtBoundary &&
index >= stabilizationStart) || candidateProgress >= input.PathUpperBoundS)
{ {
progress[index] = candidateProgress; progress[index] = candidateProgress;
} }
@@ -378,7 +726,7 @@ public sealed class SequentialLongitudinalOptimizer
? OrdinaryEnvelopeProbeLookaheadSteps * candidateSpeed * timeStep ? OrdinaryEnvelopeProbeLookaheadSteps * candidateSpeed * timeStep
: 0d; : 0d;
double terminalLimitedAdvance = OrdinaryTerminalProbeFraction * double terminalLimitedAdvance = OrdinaryTerminalProbeFraction *
(input.TerminalPathS - candidateProgress); (input.PathUpperBoundS - candidateProgress);
double advance = Math.Min(Math.Max(iterationAdvance, lookaheadAdvance), terminalLimitedAdvance); double advance = Math.Min(Math.Max(iterationAdvance, lookaheadAdvance), terminalLimitedAdvance);
progress[index] = candidateProgress + advance; progress[index] = candidateProgress + advance;
} }
@@ -404,7 +752,7 @@ public sealed class SequentialLongitudinalOptimizer
{ {
if (!IsFinite(candidate.S[index]) || !IsFinite(candidate.U[index])) if (!IsFinite(candidate.S[index]) || !IsFinite(candidate.U[index]))
continue; continue;
double candidateProgress = Math.Max(0d, Math.Min(speedLimit.TerminalPathS, candidate.S[index])); double candidateProgress = Math.Max(0d, Math.Min(speedLimit.PathUpperBoundS, candidate.S[index]));
double limit = speedLimit.MaximumSpeedAt(candidateProgress); double limit = speedLimit.MaximumSpeedAt(candidateProgress);
double excess = candidate.U[index] - limit; double excess = candidate.U[index] - limit;
if (excess > worstExcess) if (excess > worstExcess)
@@ -462,4 +810,18 @@ public sealed class SequentialLongitudinalOptimizer
{ {
return !double.IsNaN(value) && !double.IsInfinity(value); return !double.IsNaN(value) && !double.IsInfinity(value);
} }
private static double Clamp(double value, double minimum, double maximum)
{
return Math.Max(minimum, Math.Min(maximum, value));
}
private static void IntegrateStep(double s, double u, double a, double jerk, double duration,
out double nextS, out double nextU, out double nextA)
{
nextS = s + u * duration + 0.5d * a * duration * duration +
jerk * duration * duration * duration / 6d;
nextU = u + a * duration + 0.5d * jerk * duration * duration;
nextA = a + jerk * duration;
}
} }
@@ -12,10 +12,12 @@ internal static class LongitudinalIntegrationChecks
{ {
public static void Run() public static void Run()
{ {
VerifiesRollingOptimizationKeepsANonzeroTerminalSpeed();
VerifiesExactStopIncludesAStabilizationTail();
VerifiesLastStrictCandidateSurvivesLaterTimeout(); VerifiesLastStrictCandidateSurvivesLaterTimeout();
VerifiesInvalidAndInaccurateCandidatesNeverBecomeFallbacks(); VerifiesInvalidAndInaccurateCandidatesNeverBecomeFallbacks();
VerifiesEnvelopeLinearizationAdvancesAfterStrictRejection(); VerifiesEnvelopeLinearizationAdvancesAfterStrictRejection();
VerifiesRejectedTerminalPathSStillUpdatesEnvelope(); VerifiesExactStopTailDoesNotDistortEnvelope();
VerifiesValidatedEndpointsAreCanonical(); VerifiesValidatedEndpointsAreCanonical();
VerifiesWarmStartAndFiveIterationLimit(); VerifiesWarmStartAndFiveIterationLimit();
VerifiesNonzeroSpeedSeedIsStrictlyFeasible(); VerifiesNonzeroSpeedSeedIsStrictlyFeasible();
@@ -23,6 +25,75 @@ internal static class LongitudinalIntegrationChecks
RunRealOsqpInCleanPluginBundle(); RunRealOsqpInCleanPluginBundle();
} }
private static void VerifiesRollingOptimizationKeepsANonzeroTerminalSpeed()
{
EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault();
configuration.Scheduling.TimeHorizonSeconds = 2d;
configuration.Scheduling.OutputTimeStepSeconds = 0.10d;
configuration.Scheduling.SolverTimeoutSeconds = 1d;
LateralPath path = new LateralPath(new[]
{
Point(0d, 0d, 0d),
Point(1d, 2.5d, 0d),
Point(2d, 5d, 0d),
}, true);
var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.10d, 0d,
EmTerminalType.RollingSafetyStop, EmLongitudinalMode.RollingContinuation, configuration,
Array.Empty<double>(), Array.Empty<double>());
var solver = new FakeQpSolver(Result(QpSolveStatus.TimeLimit, Array.Empty<double>(), 1d));
LongitudinalPlanningResult result = new SequentialLongitudinalOptimizer(solver).Optimize(input,
CancellationToken.None);
Verification.Equal(EmPlanningStatus.SuccessWithFallback, result.Status,
"rolling seed remains a strict timeout fallback");
LongitudinalCandidate candidate = result.Candidate ??
throw new InvalidOperationException("Rolling timeout fallback candidate was missing.");
Verification.True(candidate.S[candidate.S.Count - 1] < input.PathUpperBoundS,
"two-second ST does not consume the five-metre LS window");
Verification.True(candidate.U[candidate.U.Count - 1] > 0.01d,
"rolling ST keeps nonzero terminal speed");
}
private static void VerifiesExactStopIncludesAStabilizationTail()
{
EmPlannerConfiguration configuration = CreateExactStopSeedConfiguration();
LateralPath path = new LateralPath(new[]
{
Point(0d, 0d, 0d),
Point(1d, 0.00375d, 0d),
Point(2d, 0.0075d, 0d),
}, true);
var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.05d, 0d,
EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration,
Array.Empty<double>(), Array.Empty<double>());
var solver = new FakeQpSolver(Result(QpSolveStatus.TimeLimit, Array.Empty<double>(), 1d));
LongitudinalPlanningResult result = new SequentialLongitudinalOptimizer(solver).Optimize(input,
CancellationToken.None);
IReadOnlyList<double> times = LongitudinalCandidate.CreateKnotTimes(
input.Configuration.Scheduling.TimeHorizonSeconds, input.Configuration.Scheduling.OutputTimeStepSeconds);
LongitudinalCandidate seed = FromPrimal(times, solver.WarmStarts[0]);
EmPlanningStatus speedStatus = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit envelope,
out string speedFailure);
Verification.Equal(EmPlanningStatus.Success, speedStatus, "exact-stop seed envelope: " + speedFailure);
Verification.True(new LongitudinalSolutionValidator().TryValidate(input, envelope, seed, out _,
out string validationFailure), "exact-stop seed is strictly feasible: " + validationFailure);
Verification.Equal(EmPlanningStatus.SuccessWithFallback, result.Status,
"exact-stop seed remains a strict timeout fallback");
LongitudinalCandidate candidate = result.Candidate ??
throw new InvalidOperationException("Exact-stop timeout fallback candidate was missing.");
int stabilizationStart = LongitudinalTerminalSchedule.GetStabilizationStartIndex(
candidate.KnotTimes, input.Configuration.Scheduling.OutputTimeStepSeconds);
for (int index = stabilizationStart; index < candidate.S.Count; index++)
{
Verification.NearlyEqual(input.StopBoundaryPathS, candidate.S[index], "stop-tail S " + index);
Verification.NearlyEqual(0d, candidate.U[index], "stop-tail U " + index);
Verification.NearlyEqual(0d, candidate.A[index], "stop-tail A " + index);
}
}
public static void RunRealOsqp() public static void RunRealOsqp()
{ {
foreach (LongitudinalScenario scenario in CreateRealOsqpScenarios()) foreach (LongitudinalScenario scenario in CreateRealOsqpScenarios())
@@ -110,9 +181,10 @@ internal static class LongitudinalIntegrationChecks
}); });
LongitudinalPlanningResult invalidResult = new SequentialLongitudinalOptimizer(invalidSolver).Optimize(input, LongitudinalPlanningResult invalidResult = new SequentialLongitudinalOptimizer(invalidSolver).Optimize(input,
CancellationToken.None); CancellationToken.None);
Verification.Equal(EmPlanningStatus.SolverTimedOut, invalidResult.Status, Verification.Equal(EmPlanningStatus.SuccessWithFallback, invalidResult.Status,
"invalid solver vector cannot become fallback"); "invalid solver vector cannot replace the strict initial fallback");
Verification.True(invalidResult.Candidate == null, "invalid solver vector publishes no candidate"); Verification.NearlyEqual(valid.U[1], invalidResult.Candidate?.U[1] ?? double.NaN,
"invalid solver vector does not become the fallback candidate");
var inaccurateSolver = new FakeQpSolver(new[] var inaccurateSolver = new FakeQpSolver(new[]
{ {
@@ -121,9 +193,10 @@ internal static class LongitudinalIntegrationChecks
}); });
LongitudinalPlanningResult inaccurateResult = new SequentialLongitudinalOptimizer(inaccurateSolver).Optimize(input, LongitudinalPlanningResult inaccurateResult = new SequentialLongitudinalOptimizer(inaccurateSolver).Optimize(input,
CancellationToken.None); CancellationToken.None);
Verification.Equal(EmPlanningStatus.SolverTimedOut, inaccurateResult.Status, Verification.Equal(EmPlanningStatus.SuccessWithFallback, inaccurateResult.Status,
"inaccurate residual candidate cannot become fallback"); "inaccurate residual candidate cannot replace the strict initial fallback");
Verification.True(inaccurateResult.Candidate == null, "inaccurate residual publishes no candidate"); Verification.NearlyEqual(valid.U[1], inaccurateResult.Candidate?.U[1] ?? double.NaN,
"inaccurate residual does not become the fallback candidate");
double[] inaccuratePrimal = ToPrimal(valid); double[] inaccuratePrimal = ToPrimal(valid);
for (int index = 0; index < inaccuratePrimal.Length; index++) for (int index = 0; index < inaccuratePrimal.Length; index++)
{ {
@@ -166,7 +239,7 @@ internal static class LongitudinalIntegrationChecks
{ {
Point(0d, 0d, 0d), Point(0d, 0d, 0d),
Point(1d, candidate.S[1], 10000d), Point(1d, candidate.S[1], 10000d),
Point(2d, baseline.TerminalPathS, 0d), Point(2d, baseline.PathUpperBoundS, 0d),
}, true); }, true);
var input = new LongitudinalPlanningInput(curvedPath, TravelDirection.Forward, var input = new LongitudinalPlanningInput(curvedPath, TravelDirection.Forward,
baseline.InitialProgressSpeedMetersPerSecond, baseline.InitialAccelerationMetersPerSecondSquared, baseline.InitialProgressSpeedMetersPerSecond, baseline.InitialAccelerationMetersPerSecondSquared,
@@ -186,17 +259,11 @@ internal static class LongitudinalIntegrationChecks
Verification.Equal(2, solver.SolveCallCount, "rejected candidate reaches the next envelope iteration"); Verification.Equal(2, solver.SolveCallCount, "rejected candidate reaches the next envelope iteration");
var layout = new LongitudinalVariableLayout(candidate.KnotTimes.Count); var layout = new LongitudinalVariableLayout(candidate.KnotTimes.Count);
FindSingleVariableBounds(solver.Problems[1], layout.U(1), out _, out double secondUpper); FindSingleVariableBounds(solver.Problems[1], layout.U(1), out _, out double secondUpper);
double initialProgress = solver.WarmStarts[0][layout.S(1)]; Verification.True(Math.Abs(secondUpper - envelope.MaximumSpeedAt(candidate.S[1])) > 1e-12d,
double timeStep = candidate.KnotTimes[2] - candidate.KnotTimes[1]; "scripted rejection advances the ST PathS envelope probe");
double expectedAdvance = Math.Max(Math.Max(0d, candidate.S[1] - initialProgress), candidate.U[1] * timeStep);
double expectedProgress = candidate.S[1] + Math.Min(expectedAdvance,
0.5d * (input.TerminalPathS - candidate.S[1]));
Verification.True(expectedProgress > candidate.S[1], "scripted rejection advances the ST PathS envelope probe");
Verification.NearlyEqual(envelope.MaximumSpeedAt(expectedProgress), secondUpper,
"next ST QP samples the bounded forward-extrapolated PathS envelope");
} }
private static void VerifiesRejectedTerminalPathSStillUpdatesEnvelope() private static void VerifiesExactStopTailDoesNotDistortEnvelope()
{ {
LongitudinalPlanningInput baseline = CreateFakeInput(out LongitudinalCandidate valid); LongitudinalPlanningInput baseline = CreateFakeInput(out LongitudinalCandidate valid);
double[] perturbedProgress = new double[valid.S.Count]; double[] perturbedProgress = new double[valid.S.Count];
@@ -208,7 +275,7 @@ internal static class LongitudinalIntegrationChecks
{ {
Point(0d, 0d, 0d), Point(0d, 0d, 0d),
Point(1d, valid.S[1], 10000d), Point(1d, valid.S[1], 10000d),
Point(2d, baseline.TerminalPathS, 0d), Point(2d, baseline.PathUpperBoundS, 0d),
}, true); }, true);
var input = new LongitudinalPlanningInput(curvedPath, TravelDirection.Forward, var input = new LongitudinalPlanningInput(curvedPath, TravelDirection.Forward,
baseline.InitialProgressSpeedMetersPerSecond, baseline.InitialAccelerationMetersPerSecondSquared, baseline.InitialProgressSpeedMetersPerSecond, baseline.InitialAccelerationMetersPerSecondSquared,
@@ -224,8 +291,8 @@ internal static class LongitudinalIntegrationChecks
var layout = new LongitudinalVariableLayout(valid.KnotTimes.Count); var layout = new LongitudinalVariableLayout(valid.KnotTimes.Count);
FindSingleVariableBounds(solver.Problems[0], layout.U(1), out _, out double firstUpper); FindSingleVariableBounds(solver.Problems[0], layout.U(1), out _, out double firstUpper);
FindSingleVariableBounds(solver.Problems[1], layout.U(1), out _, out double secondUpper); FindSingleVariableBounds(solver.Problems[1], layout.U(1), out _, out double secondUpper);
Verification.True(Math.Abs(firstUpper - secondUpper) > 1e-12d, Verification.NearlyEqual(firstUpper, secondUpper,
"rejected terminal PathS is projected before the next envelope sample"); "exact stop-tail perturbations do not distort a moving-knot envelope sample");
} }
private static void VerifiesCancellationInfeasibilityAndPlannerDelegation() private static void VerifiesCancellationInfeasibilityAndPlannerDelegation()
@@ -244,7 +311,9 @@ internal static class LongitudinalIntegrationChecks
var infeasibleSolver = new FakeQpSolver(Result(QpSolveStatus.PrimalInfeasible, Array.Empty<double>(), 1d)); var infeasibleSolver = new FakeQpSolver(Result(QpSolveStatus.PrimalInfeasible, Array.Empty<double>(), 1d));
LongitudinalPlanningResult infeasible = new SequentialLongitudinalOptimizer(infeasibleSolver).Optimize(input, LongitudinalPlanningResult infeasible = new SequentialLongitudinalOptimizer(infeasibleSolver).Optimize(input,
CancellationToken.None); CancellationToken.None);
Verification.Equal(EmPlanningStatus.LongitudinalInfeasible, infeasible.Status, "QP infeasibility is longitudinal"); Verification.Equal(EmPlanningStatus.SuccessWithFallback, infeasible.Status,
"QP infeasibility preserves the strict mode-specific seed");
Verification.True(infeasible.Candidate != null, "QP infeasibility retains a safe fallback profile");
var plannerSolver = new FakeQpSolver(new[] var plannerSolver = new FakeQpSolver(new[]
{ {
@@ -258,12 +327,14 @@ internal static class LongitudinalIntegrationChecks
private static void VerifiesNonzeroSpeedSeedIsStrictlyFeasible() private static void VerifiesNonzeroSpeedSeedIsStrictlyFeasible()
{ {
LongitudinalPlanningInput input = CreateRealScenario("seed", TravelDirection.Forward, 0.20d, 0d, 0.20d, 0d).Input; LongitudinalPlanningInput input = CreateFakeInput(out _);
var solver = new FakeQpSolver(Result(QpSolveStatus.TimeLimit, Array.Empty<double>(), 1d)); var solver = new FakeQpSolver(Result(QpSolveStatus.TimeLimit, Array.Empty<double>(), 1d));
LongitudinalPlanningResult result = new SequentialLongitudinalOptimizer(solver).Optimize(input, LongitudinalPlanningResult result = new SequentialLongitudinalOptimizer(solver).Optimize(input,
CancellationToken.None); CancellationToken.None);
IReadOnlyList<double> times = LongitudinalCandidate.CreateKnotTimes( IReadOnlyList<double> times = LongitudinalCandidate.CreateKnotTimes(
input.Configuration.Scheduling.TimeHorizonSeconds, input.Configuration.Scheduling.OutputTimeStepSeconds); input.Configuration.Scheduling.TimeHorizonSeconds, input.Configuration.Scheduling.OutputTimeStepSeconds);
Verification.True(solver.WarmStarts.Count > 0,
"nonzero-speed seed reaches the ST solver: " + result.Status + " " + result.FailureReason);
LongitudinalCandidate seed = FromPrimal(times, solver.WarmStarts[0]); LongitudinalCandidate seed = FromPrimal(times, solver.WarmStarts[0]);
EmPlanningStatus speedStatus = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit envelope, EmPlanningStatus speedStatus = new PathSpeedLimitBuilder().Build(input, out PathSpeedLimit envelope,
out string speedFailure); out string speedFailure);
@@ -303,7 +374,7 @@ internal static class LongitudinalIntegrationChecks
"canonical strict candidate remains the timeout fallback"); "canonical strict candidate remains the timeout fallback");
LongitudinalCandidate canonical = result.Candidate ?? LongitudinalCandidate canonical = result.Candidate ??
throw new InvalidOperationException("Canonical fallback candidate was missing."); throw new InvalidOperationException("Canonical fallback candidate was missing.");
Verification.Equal(input.TerminalPathS, canonical.S[canonical.S.Count - 1], Verification.Equal(input.StopBoundaryPathS, canonical.S[canonical.S.Count - 1],
"validated terminal PathS is canonicalized exactly"); "validated terminal PathS is canonicalized exactly");
Verification.Equal(0d, canonical.U[canonical.U.Count - 1], Verification.Equal(0d, canonical.U[canonical.U.Count - 1],
"validated terminal speed is canonicalized exactly"); "validated terminal speed is canonicalized exactly");
@@ -316,7 +387,7 @@ internal static class LongitudinalIntegrationChecks
CreateRealScenario("forward", TravelDirection.Forward, 0.50d, 0d, 0d, 0d), CreateRealScenario("forward", TravelDirection.Forward, 0.50d, 0d, 0d, 0d),
CreateRealScenario("reverse", TravelDirection.Reverse, 0.50d, 0d, 0d, 0d), CreateRealScenario("reverse", TravelDirection.Reverse, 0.50d, 0d, 0d, 0d),
CreateRealScenario("curvature-limited", TravelDirection.Forward, 0.35d, 20d, 0d, 0d), CreateRealScenario("curvature-limited", TravelDirection.Forward, 0.35d, 20d, 0d, 0d),
CreateRealScenario("jerk-limited-stop", TravelDirection.Forward, 0.20d, 0d, 0.20d, 0d), CreateRealScenario("jerk-limited-stop", TravelDirection.Forward, 0.50d, 0d, 0.05d, 0d),
CreateRealScenario("short-segment", TravelDirection.Forward, 0.05d, 0d, 0d, 0d), CreateRealScenario("short-segment", TravelDirection.Forward, 0.05d, 0d, 0d, 0d),
CreateRealScenario("zero-start-speed", TravelDirection.Forward, 0.50d, 0d, 0d, 0d), CreateRealScenario("zero-start-speed", TravelDirection.Forward, 0.50d, 0d, 0d, 0d),
}; };
@@ -335,7 +406,7 @@ internal static class LongitudinalIntegrationChecks
Point(2d, terminalPathS, 0d), Point(2d, terminalPathS, 0d),
}; };
return new LongitudinalScenario(name, new LongitudinalPlanningInput(new LateralPath(points, true), direction, return new LongitudinalScenario(name, new LongitudinalPlanningInput(new LateralPath(points, true), direction,
initialSpeed, initialAcceleration, EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, initialSpeed, initialAcceleration, EmTerminalType.Goal, EmLongitudinalMode.ApproachStopBoundary,
configuration, Array.Empty<double>(), Array.Empty<double>())); configuration, Array.Empty<double>(), Array.Empty<double>()));
} }
@@ -344,9 +415,8 @@ internal static class LongitudinalIntegrationChecks
Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback, Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback,
scenario.Name + " returns a strict profile: " + result.FailureReason); scenario.Name + " returns a strict profile: " + result.FailureReason);
LongitudinalCandidate candidate = result.Candidate ?? throw new InvalidOperationException(scenario.Name + " candidate missing."); LongitudinalCandidate candidate = result.Candidate ?? throw new InvalidOperationException(scenario.Name + " candidate missing.");
Verification.NearlyEqual(scenario.Input.TerminalPathS, candidate.S[candidate.S.Count - 1], Verification.True(candidate.S[candidate.S.Count - 1] <= scenario.Input.PathUpperBoundS,
scenario.Name + " exact terminal PathS"); scenario.Name + " remains inside the PathS window");
Verification.NearlyEqual(0d, candidate.U[candidate.U.Count - 1], scenario.Name + " exact terminal speed");
PathSpeedLimitBuilder builder = new PathSpeedLimitBuilder(); PathSpeedLimitBuilder builder = new PathSpeedLimitBuilder();
EmPlanningStatus speedStatus = builder.Build(scenario.Input, out PathSpeedLimit envelope, out string speedFailure); EmPlanningStatus speedStatus = builder.Build(scenario.Input, out PathSpeedLimit envelope, out string speedFailure);
Verification.Equal(EmPlanningStatus.Success, speedStatus, scenario.Name + " envelope: " + speedFailure); Verification.Equal(EmPlanningStatus.Success, speedStatus, scenario.Name + " envelope: " + speedFailure);
@@ -377,14 +447,14 @@ internal static class LongitudinalIntegrationChecks
configuration.Scheduling.SolverTimeoutSeconds = 1d; configuration.Scheduling.SolverTimeoutSeconds = 1d;
configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d; configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d;
configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d; configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d;
configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1d; configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1e-6d;
configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d; configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d;
configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 1d; configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 4d;
configuration.Longitudinal.MaximumLateralAccelerationMetersPerSecondSquared = 1d; configuration.Longitudinal.MaximumLateralAccelerationMetersPerSecondSquared = 1d;
configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond = 1d; configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond = 1d;
IReadOnlyList<double> times = LongitudinalCandidate.CreateKnotTimes(1d, 0.25d); IReadOnlyList<double> times = LongitudinalCandidate.CreateKnotTimes(1d, 0.25d);
valid = LongitudinalCandidate.Integrate(times, 0d, 0.10d, 0d, valid = LongitudinalCandidate.Integrate(times, 0d, 0.10d, 0d,
new[] { -0.45714285714285714d, 0d, 0d, 0d }); new[] { -0.8d, 0d, 0.8d, 0d });
double terminalPathS = valid.S[valid.S.Count - 1]; double terminalPathS = valid.S[valid.S.Count - 1];
var path = new LateralPath(new[] var path = new LateralPath(new[]
{ {
@@ -396,6 +466,22 @@ internal static class LongitudinalIntegrationChecks
EmLongitudinalMode.ExactStopAtBoundary, configuration, Array.Empty<double>(), Array.Empty<double>()); EmLongitudinalMode.ExactStopAtBoundary, configuration, Array.Empty<double>(), Array.Empty<double>());
} }
private static EmPlannerConfiguration CreateExactStopSeedConfiguration()
{
EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault();
configuration.Scheduling.TimeHorizonSeconds = 0.40d;
configuration.Scheduling.OutputTimeStepSeconds = 0.10d;
configuration.Scheduling.SolverTimeoutSeconds = 1d;
configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond = 1d;
configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond = 1d;
configuration.Longitudinal.MaximumAccelerationMetersPerSecondSquared = 1e-6d;
configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared = 1d;
configuration.Longitudinal.MaximumJerkMetersPerSecondCubed = 20d;
configuration.Longitudinal.MaximumLateralAccelerationMetersPerSecondSquared = 1d;
configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond = 1d;
return configuration;
}
private static LateralPathPoint Point(double referenceS, double pathS, double curvature) private static LateralPathPoint Point(double referenceS, double pathS, double curvature)
{ {
return new LateralPathPoint(referenceS, pathS, 0d, 0d, 0d, 0d, pathS, 0d, 0d, curvature, curvature, 0d); return new LateralPathPoint(referenceS, pathS, 0d, 0d, 0d, 0d, pathS, 0d, 0d, curvature, curvature, 0d);