feat: derive adaptive full-segment ST schedule
This commit is contained in:
+211
-13
@@ -47,18 +47,40 @@ public sealed class SequentialLongitudinalOptimizer
|
||||
if (speedStatus != EmPlanningStatus.Success)
|
||||
return Failed(speedStatus, speedFailure);
|
||||
|
||||
LongitudinalCandidate iterate = CreateInitialIterate(input, speedLimit);
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
LongitudinalCandidate iterate;
|
||||
LongitudinalCandidate lastStrictCandidate = null;
|
||||
int remainingObjectiveIterations = iterationLimit;
|
||||
if (input.PlanningScope == EmPlanningScope.FullDirectionSegment &&
|
||||
input.Mode == EmLongitudinalMode.ExactStopAtBoundary)
|
||||
{
|
||||
if (!TryCreateInitialFeasibleCandidate(input, speedLimit, settings, totalBudget, convergenceTolerance,
|
||||
iterationLimit, stopwatch, cancellationToken, out iterate, out int projectionSolveCount,
|
||||
out EmPlanningStatus projectionStatus, out string projectionFailure))
|
||||
{
|
||||
return Failed(projectionStatus, projectionFailure);
|
||||
}
|
||||
lastStrictCandidate = CopyCandidate(iterate);
|
||||
remainingObjectiveIterations -= projectionSolveCount;
|
||||
if (remainingObjectiveIterations <= 0)
|
||||
{
|
||||
return new LongitudinalPlanningResult(EmPlanningStatus.SuccessWithFallback, lastStrictCandidate,
|
||||
"The strict initial feasibility projection consumed the configured outer-iteration budget.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
iterate = CreateInitialIterate(input, speedLimit);
|
||||
if (!_solutionValidator.TryValidate(input, speedLimit, iterate, out lastStrictCandidate, out _))
|
||||
lastStrictCandidate = null;
|
||||
}
|
||||
double[] warmStart = ToPrimal(iterate);
|
||||
bool hasDynamicsConsistentInitialWarmStart = iterate.SatisfiesExactDiscreteDynamics(1e-12d);
|
||||
LongitudinalCandidate lastStrictCandidate;
|
||||
if (!_solutionValidator.TryValidate(input, speedLimit, iterate, out lastStrictCandidate, out _))
|
||||
lastStrictCandidate = null;
|
||||
string lastCandidateRejection = string.Empty;
|
||||
bool hasPreviousObjective = false;
|
||||
double previousObjective = 0d;
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
for (int iteration = 0; iteration < iterationLimit; iteration++)
|
||||
for (int iteration = 0; iteration < remainingObjectiveIterations; iteration++)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.Cancelled, "Longitudinal optimization was cancelled.");
|
||||
@@ -191,10 +213,132 @@ public sealed class SequentialLongitudinalOptimizer
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryCreateInitialFeasibleCandidate(LongitudinalPlanningInput input, PathSpeedLimit speedLimit,
|
||||
QpSolverSettings settings, TimeSpan totalBudget, double convergenceTolerance, int iterationLimit,
|
||||
Stopwatch stopwatch, CancellationToken cancellationToken, out LongitudinalCandidate candidate,
|
||||
out int projectionSolveCount, out EmPlanningStatus failureStatus, out string failureReason)
|
||||
{
|
||||
candidate = null;
|
||||
projectionSolveCount = 0;
|
||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||
failureReason = string.Empty;
|
||||
LongitudinalCandidate linearizationIterate = CreateScheduleReferenceIterate(input);
|
||||
string lastRejection = string.Empty;
|
||||
for (int iteration = 0; iteration < iterationLimit; iteration++)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
failureStatus = EmPlanningStatus.Cancelled;
|
||||
failureReason = "Initial full-direction feasibility projection was cancelled.";
|
||||
return false;
|
||||
}
|
||||
TimeSpan remainingBudget = totalBudget - stopwatch.Elapsed;
|
||||
if (remainingBudget <= TimeSpan.Zero)
|
||||
{
|
||||
failureStatus = EmPlanningStatus.SolverTimedOut;
|
||||
failureReason = "Initial full-direction feasibility projection exhausted the shared solve budget.";
|
||||
return false;
|
||||
}
|
||||
if (!_constraintBuilder.TryBuildInitialFeasibilityProjection(input, speedLimit, linearizationIterate,
|
||||
out QuadraticProgram problem, out string buildFailure))
|
||||
{
|
||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||
failureReason = "Initial full-direction feasibility constraints are infeasible: " + buildFailure;
|
||||
return false;
|
||||
}
|
||||
|
||||
double projectionTolerance = Math.Min(settings.AbsoluteTolerance,
|
||||
input.Configuration.Validation.KinematicTolerance * 0.1d);
|
||||
QpSolveResult solved = _qpSolver.Solve(problem,
|
||||
new QpSolverSettings(settings.MaximumIterations, projectionTolerance, projectionTolerance,
|
||||
remainingBudget, settings.EnableWarmStart && linearizationIterate.SatisfiesExactDiscreteDynamics(1e-12d),
|
||||
settings.EnablePolishing, settings.EnableNativeVerboseOutput),
|
||||
ToPrimal(linearizationIterate), cancellationToken);
|
||||
projectionSolveCount++;
|
||||
if (solved == null)
|
||||
{
|
||||
failureStatus = EmPlanningStatus.Failed;
|
||||
failureReason = "The initial full-direction feasibility solver returned no result.";
|
||||
return false;
|
||||
}
|
||||
if (solved.Status == QpSolveStatus.TimeLimit || solved.Status == QpSolveStatus.MaximumIterations)
|
||||
{
|
||||
failureStatus = EmPlanningStatus.SolverTimedOut;
|
||||
failureReason = "Initial full-direction feasibility projection timed out (status=" + solved.NativeStatus +
|
||||
", iterations=" + solved.Iterations + ", primal=" + solved.PrimalResidual + ", dual=" +
|
||||
solved.DualResidual + "): " + solved.Diagnostic;
|
||||
return false;
|
||||
}
|
||||
if (solved.Status == QpSolveStatus.Cancelled)
|
||||
{
|
||||
failureStatus = EmPlanningStatus.Cancelled;
|
||||
failureReason = "Initial full-direction feasibility projection was cancelled: " + solved.Diagnostic;
|
||||
return false;
|
||||
}
|
||||
if (solved.Status == QpSolveStatus.PrimalInfeasible || solved.Status == QpSolveStatus.DualInfeasible)
|
||||
{
|
||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||
failureReason = "Initial full-direction feasibility projection is infeasible: " + solved.Diagnostic;
|
||||
return false;
|
||||
}
|
||||
if (solved.Status == QpSolveStatus.SolverUnavailable)
|
||||
{
|
||||
failureStatus = EmPlanningStatus.SolverUnavailable;
|
||||
failureReason = "Initial full-direction feasibility solver is unavailable: " + solved.Diagnostic;
|
||||
return false;
|
||||
}
|
||||
if (solved.Status != QpSolveStatus.Solved && solved.Status != QpSolveStatus.SolvedInaccurate)
|
||||
{
|
||||
failureStatus = EmPlanningStatus.Failed;
|
||||
failureReason = "Initial full-direction feasibility solver failed: " + solved.Diagnostic;
|
||||
return false;
|
||||
}
|
||||
if (!TryCreateCandidate(input.KnotSchedule.KnotTimes, solved.Primal, out LongitudinalCandidate projected))
|
||||
{
|
||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||
failureReason = "Initial full-direction feasibility solver primal does not match the ST layout.";
|
||||
return false;
|
||||
}
|
||||
if (solved.Status == QpSolveStatus.Solved || HasStrictResiduals(solved, convergenceTolerance))
|
||||
{
|
||||
if (_solutionValidator.TryValidate(input, speedLimit, projected, out LongitudinalCandidate strict,
|
||||
out string validationFailure))
|
||||
{
|
||||
candidate = strict;
|
||||
return true;
|
||||
}
|
||||
lastRejection = validationFailure;
|
||||
}
|
||||
|
||||
if (!TryCreateFeasibilityEnvelopeIterate(input, projected,
|
||||
out LongitudinalCandidate nextLinearization))
|
||||
{
|
||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||
failureReason = "Initial full-direction feasibility candidate could not be relinearized against the PathS envelope.";
|
||||
return false;
|
||||
}
|
||||
linearizationIterate = nextLinearization;
|
||||
if (solved.Status == QpSolveStatus.SolvedInaccurate)
|
||||
lastRejection = "Initial feasibility projection residuals exceed the strict acceptance tolerance.";
|
||||
else if (string.IsNullOrEmpty(lastRejection))
|
||||
lastRejection = "Initial feasibility projection violated the strict physical validator.";
|
||||
}
|
||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||
failureReason = "Initial full-direction feasibility projection exhausted the configured outer iterations. " +
|
||||
lastRejection;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static LongitudinalCandidate CreateScheduleReferenceIterate(LongitudinalPlanningInput input)
|
||||
{
|
||||
int knotCount = input.KnotSchedule.KnotTimes.Count;
|
||||
return new LongitudinalCandidate(input.KnotSchedule.KnotTimes, input.KnotSchedule.ReferencePathS,
|
||||
input.KnotSchedule.ReferenceSpeedMetersPerSecond, new double[knotCount], new double[knotCount - 1]);
|
||||
}
|
||||
|
||||
private LongitudinalCandidate CreateInitialIterate(LongitudinalPlanningInput input, PathSpeedLimit speedLimit)
|
||||
{
|
||||
IReadOnlyList<double> times = LongitudinalCandidate.CreateKnotTimes(input.Configuration.Scheduling.TimeHorizonSeconds,
|
||||
input.Configuration.Scheduling.OutputTimeStepSeconds);
|
||||
IReadOnlyList<double> times = input.KnotSchedule.KnotTimes;
|
||||
switch (input.Mode)
|
||||
{
|
||||
case EmLongitudinalMode.RollingContinuation:
|
||||
@@ -269,6 +413,10 @@ public sealed class SequentialLongitudinalOptimizer
|
||||
private LongitudinalCandidate CreateExactStopSeed(LongitudinalPlanningInput input,
|
||||
IReadOnlyList<double> times, PathSpeedLimit speedLimit)
|
||||
{
|
||||
if (input.PlanningScope == EmPlanningScope.FullDirectionSegment)
|
||||
{
|
||||
throw new InvalidOperationException("Full-direction exact-stop planning requires the initial feasibility projection.");
|
||||
}
|
||||
int stabilizationStart = LongitudinalTerminalSchedule.GetStabilizationStartIndex(times,
|
||||
input.Configuration.Scheduling.OutputTimeStepSeconds);
|
||||
var motionTimes = new double[stabilizationStart + 1];
|
||||
@@ -364,7 +512,7 @@ public sealed class SequentialLongitudinalOptimizer
|
||||
for (int index = 0; index < motionTimes.Length; index++)
|
||||
motionTimes[index] = times[index];
|
||||
|
||||
LongitudinalCandidate baseline = CreateApproachSeed(input, motionTimes, speedLimit);
|
||||
LongitudinalCandidate baseline = CreateScheduleReferenceSeed(input, motionTimes, speedLimit);
|
||||
var influence = new double[3, intervalCount];
|
||||
for (int interval = 0; interval < intervalCount; interval++)
|
||||
{
|
||||
@@ -461,6 +609,30 @@ public sealed class SequentialLongitudinalOptimizer
|
||||
return AppendExactStopTail(times, stabilizationStart, input.StopBoundaryPathS, motion);
|
||||
}
|
||||
|
||||
private static LongitudinalCandidate CreateScheduleReferenceSeed(LongitudinalPlanningInput input,
|
||||
IReadOnlyList<double> times, PathSpeedLimit speedLimit)
|
||||
{
|
||||
LongitudinalConfiguration configuration = input.Configuration.Longitudinal;
|
||||
var jerk = new double[times.Count - 1];
|
||||
double speed = input.InitialProgressSpeedMetersPerSecond;
|
||||
double acceleration = input.InitialAccelerationMetersPerSecondSquared;
|
||||
for (int index = 0; index < jerk.Length; index++)
|
||||
{
|
||||
double dt = times[index + 1] - times[index];
|
||||
double targetSpeed = input.KnotSchedule.ReferenceSpeedMetersPerSecond[index + 1];
|
||||
double lowerJerk = Math.Max(-configuration.MaximumJerkMetersPerSecondCubed,
|
||||
(-configuration.MaximumDecelerationMetersPerSecondSquared - acceleration) / dt);
|
||||
double upperJerk = Math.Min(configuration.MaximumJerkMetersPerSecondCubed,
|
||||
(configuration.MaximumAccelerationMetersPerSecondSquared - acceleration) / dt);
|
||||
double requestedJerk = 2d * (targetSpeed - speed - acceleration * dt) / (dt * dt);
|
||||
double selectedJerk = Clamp(requestedJerk, lowerJerk, upperJerk);
|
||||
jerk[index] = selectedJerk;
|
||||
IntegrateStep(0d, speed, acceleration, selectedJerk, dt, out _, out speed, out acceleration);
|
||||
}
|
||||
return LongitudinalCandidate.Integrate(times, 0d, input.InitialProgressSpeedMetersPerSecond,
|
||||
input.InitialAccelerationMetersPerSecondSquared, jerk);
|
||||
}
|
||||
|
||||
private static double[] CreateEndpointNullspaceDirection(double[,] influence, double[,] gram, int basisIndex)
|
||||
{
|
||||
int intervalCount = influence.GetLength(1);
|
||||
@@ -679,10 +851,12 @@ public sealed class SequentialLongitudinalOptimizer
|
||||
nextIterate = null;
|
||||
if (candidate.S.Count != previous.S.Count)
|
||||
return false;
|
||||
int stabilizationStart = input.Mode == EmLongitudinalMode.ExactStopAtBoundary
|
||||
? LongitudinalTerminalSchedule.GetStabilizationStartIndex(candidate.KnotTimes,
|
||||
input.Configuration.Scheduling.OutputTimeStepSeconds)
|
||||
: candidate.S.Count;
|
||||
int stabilizationStart = input.Mode != EmLongitudinalMode.ExactStopAtBoundary
|
||||
? candidate.S.Count
|
||||
: input.PlanningScope == EmPlanningScope.FullDirectionSegment
|
||||
? input.KnotSchedule.TerminalHoldStartIndex
|
||||
: LongitudinalTerminalSchedule.GetStabilizationStartIndex(candidate.KnotTimes,
|
||||
input.Configuration.Scheduling.OutputTimeStepSeconds);
|
||||
var candidateProgressSamples = new double[candidate.S.Count];
|
||||
double priorProgress = double.NegativeInfinity;
|
||||
double priorPreviousProgress = double.NegativeInfinity;
|
||||
@@ -737,6 +911,30 @@ public sealed class SequentialLongitudinalOptimizer
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCreateFeasibilityEnvelopeIterate(LongitudinalPlanningInput input,
|
||||
LongitudinalCandidate candidate, out LongitudinalCandidate nextIterate)
|
||||
{
|
||||
nextIterate = null;
|
||||
int stabilizationStart = input.KnotSchedule.TerminalHoldStartIndex;
|
||||
var pathS = new double[candidate.S.Count];
|
||||
double previousPathS = double.NegativeInfinity;
|
||||
double tolerance = input.Configuration.Validation.KinematicTolerance;
|
||||
for (int index = 0; index < pathS.Length; index++)
|
||||
{
|
||||
double value = candidate.S[index];
|
||||
if (!IsFinite(value) || value < -tolerance || value > input.PathUpperBoundS + tolerance ||
|
||||
value < previousPathS - tolerance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
value = Math.Max(0d, Math.Min(input.PathUpperBoundS, value));
|
||||
pathS[index] = index >= stabilizationStart ? input.StopBoundaryPathS : Math.Max(previousPathS, value);
|
||||
previousPathS = pathS[index];
|
||||
}
|
||||
nextIterate = new LongitudinalCandidate(candidate.KnotTimes, pathS, candidate.U, candidate.A, candidate.J);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HasStrictResiduals(QpSolveResult result, double tolerance)
|
||||
{
|
||||
return IsPositiveFinite(tolerance) && result.PrimalResidual >= 0d && result.DualResidual >= 0d &&
|
||||
|
||||
Reference in New Issue
Block a user