462 lines
23 KiB
C#
462 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
|
|
namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
|
|
|
|
/// <summary>Bounded ST envelope iteration retaining only independently validated physical candidates.</summary>
|
|
public sealed class SequentialLongitudinalOptimizer
|
|
{
|
|
private const int MaximumEnvelopeIterations = 5;
|
|
private const double OrdinaryEnvelopeProbeLookaheadSteps = 1d;
|
|
private const double OrdinaryTerminalProbeFraction = 0.5d;
|
|
private readonly IQpSolver _qpSolver;
|
|
private readonly PathSpeedLimitBuilder _speedLimitBuilder;
|
|
private readonly LongitudinalConstraintBuilder _constraintBuilder;
|
|
private readonly LongitudinalSolutionValidator _solutionValidator;
|
|
|
|
public SequentialLongitudinalOptimizer(IQpSolver qpSolver)
|
|
: this(qpSolver, new PathSpeedLimitBuilder(), new LongitudinalConstraintBuilder(new LongitudinalObjectiveBuilder()),
|
|
new LongitudinalSolutionValidator())
|
|
{
|
|
}
|
|
|
|
internal SequentialLongitudinalOptimizer(IQpSolver qpSolver, PathSpeedLimitBuilder speedLimitBuilder,
|
|
LongitudinalConstraintBuilder constraintBuilder, LongitudinalSolutionValidator solutionValidator)
|
|
{
|
|
_qpSolver = qpSolver ?? throw new ArgumentNullException(nameof(qpSolver));
|
|
_speedLimitBuilder = speedLimitBuilder ?? throw new ArgumentNullException(nameof(speedLimitBuilder));
|
|
_constraintBuilder = constraintBuilder ?? throw new ArgumentNullException(nameof(constraintBuilder));
|
|
_solutionValidator = solutionValidator ?? throw new ArgumentNullException(nameof(solutionValidator));
|
|
}
|
|
|
|
public LongitudinalPlanningResult Optimize(LongitudinalPlanningInput input, CancellationToken cancellationToken)
|
|
{
|
|
if (input == null)
|
|
return Failed(EmPlanningStatus.InvalidInput, "Longitudinal planning input is required.");
|
|
if (cancellationToken.IsCancellationRequested)
|
|
return Failed(EmPlanningStatus.Cancelled, "Longitudinal optimization was cancelled.");
|
|
if (!TryCreateSettings(input, out QpSolverSettings settings, out TimeSpan totalBudget, out double convergenceTolerance,
|
|
out int iterationLimit, out string configurationFailure))
|
|
{
|
|
return Failed(EmPlanningStatus.InvalidInput, configurationFailure);
|
|
}
|
|
|
|
EmPlanningStatus speedStatus = _speedLimitBuilder.Build(input, out PathSpeedLimit speedLimit, out string speedFailure);
|
|
if (speedStatus != EmPlanningStatus.Success)
|
|
return Failed(speedStatus, speedFailure);
|
|
|
|
LongitudinalCandidate iterate = CreateInitialIterate(input);
|
|
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++)
|
|
{
|
|
if (cancellationToken.IsCancellationRequested)
|
|
return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.Cancelled, "Longitudinal optimization was cancelled.");
|
|
TimeSpan remainingBudget = totalBudget - stopwatch.Elapsed;
|
|
if (remainingBudget <= TimeSpan.Zero)
|
|
{
|
|
return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.SolverTimedOut,
|
|
"Longitudinal optimization exhausted its solve budget.");
|
|
}
|
|
if (!_constraintBuilder.TryBuild(input, speedLimit, iterate, out QuadraticProgram problem, out string buildFailure))
|
|
{
|
|
return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.LongitudinalInfeasible,
|
|
"Longitudinal constraints are infeasible: " + buildFailure);
|
|
}
|
|
|
|
QpSolveResult solved = _qpSolver.Solve(problem,
|
|
new QpSolverSettings(settings.MaximumIterations, settings.AbsoluteTolerance, settings.RelativeTolerance,
|
|
remainingBudget, settings.EnableWarmStart && (iteration > 0 || hasDynamicsConsistentInitialWarmStart),
|
|
settings.EnablePolishing,
|
|
settings.EnableNativeVerboseOutput),
|
|
warmStart, cancellationToken);
|
|
if (solved == null)
|
|
return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.Failed, "The longitudinal QP solver returned no result.");
|
|
if (solved.Status == QpSolveStatus.TimeLimit || solved.Status == QpSolveStatus.MaximumIterations)
|
|
{
|
|
return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.SolverTimedOut,
|
|
"The longitudinal QP solver timed out (status=" + solved.NativeStatus +
|
|
", iterations=" + solved.Iterations + ", primal=" + solved.PrimalResidual +
|
|
", dual=" + solved.DualResidual + "): " + solved.Diagnostic);
|
|
}
|
|
if (solved.Status == QpSolveStatus.Cancelled)
|
|
return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.Cancelled,
|
|
"The longitudinal QP solver was cancelled: " + solved.Diagnostic);
|
|
if (solved.Status == QpSolveStatus.PrimalInfeasible || solved.Status == QpSolveStatus.DualInfeasible)
|
|
{
|
|
return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.LongitudinalInfeasible,
|
|
"The longitudinal QP solver reported infeasibility: " + solved.Diagnostic);
|
|
}
|
|
if (solved.Status == QpSolveStatus.SolverUnavailable)
|
|
return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.SolverUnavailable,
|
|
"The longitudinal QP solver is unavailable: " + solved.Diagnostic);
|
|
if (solved.Status != QpSolveStatus.Solved && solved.Status != QpSolveStatus.SolvedInaccurate)
|
|
{
|
|
return FallbackOrFailure(lastStrictCandidate, EmPlanningStatus.Failed,
|
|
"The longitudinal QP solver failed: " + solved.Diagnostic);
|
|
}
|
|
if (solved.Status == QpSolveStatus.SolvedInaccurate && !HasStrictResiduals(solved, convergenceTolerance))
|
|
{
|
|
lastCandidateRejection = "SolvedInaccurate residuals exceed the strict acceptance tolerance" +
|
|
" (primal=" + solved.PrimalResidual + ", dual=" + solved.DualResidual + ").";
|
|
if (TryCreateCandidate(iterate.KnotTimes, solved.Primal, out LongitudinalCandidate inaccurateCandidate))
|
|
warmStart = ToPrimal(inaccurateCandidate);
|
|
continue;
|
|
}
|
|
if (!TryCreateCandidate(iterate.KnotTimes, solved.Primal, out LongitudinalCandidate candidate))
|
|
{
|
|
lastCandidateRejection = "The solver primal does not match the ST variable layout.";
|
|
continue;
|
|
}
|
|
if (!_solutionValidator.TryValidate(input, speedLimit, candidate, out LongitudinalCandidate validated,
|
|
out string validationFailure))
|
|
{
|
|
string rejection = validationFailure + CreateEnvelopeDiagnostic(speedLimit, iterate, candidate,
|
|
iteration + 1);
|
|
lastCandidateRejection = string.IsNullOrEmpty(lastCandidateRejection)
|
|
? rejection
|
|
: lastCandidateRejection + " | " + rejection;
|
|
if (TryCreateEnvelopeIterate(input, iterate, candidate, out LongitudinalCandidate nextIterate))
|
|
{
|
|
iterate = nextIterate;
|
|
warmStart = ToPrimal(candidate);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
double maximumChange = MaximumProgressOrSpeedChange(iterate, validated);
|
|
double relativeObjectiveImprovement = hasPreviousObjective
|
|
? RelativeObjectiveImprovement(previousObjective, solved.Objective)
|
|
: double.PositiveInfinity;
|
|
lastStrictCandidate = CopyCandidate(validated);
|
|
iterate = validated;
|
|
warmStart = ToPrimal(validated);
|
|
previousObjective = solved.Objective;
|
|
hasPreviousObjective = true;
|
|
if (maximumChange <= convergenceTolerance && relativeObjectiveImprovement <= convergenceTolerance)
|
|
return new LongitudinalPlanningResult(EmPlanningStatus.Success, lastStrictCandidate, string.Empty);
|
|
}
|
|
|
|
return lastStrictCandidate == null
|
|
? Failed(EmPlanningStatus.LongitudinalInfeasible, "No strictly validated longitudinal candidate was found. " +
|
|
lastCandidateRejection)
|
|
: new LongitudinalPlanningResult(EmPlanningStatus.Success, lastStrictCandidate, string.Empty);
|
|
}
|
|
|
|
private static bool TryCreateSettings(LongitudinalPlanningInput input, out QpSolverSettings settings,
|
|
out TimeSpan totalBudget, out double convergenceTolerance, out int iterationLimit, out string failureReason)
|
|
{
|
|
settings = null;
|
|
totalBudget = TimeSpan.Zero;
|
|
convergenceTolerance = 0d;
|
|
iterationLimit = 0;
|
|
failureReason = string.Empty;
|
|
if (input.Configuration == null || input.Configuration.Solver == null || input.Configuration.Scheduling == null)
|
|
{
|
|
failureReason = "Longitudinal solver configuration is required.";
|
|
return false;
|
|
}
|
|
SolverConfiguration solver = input.Configuration.Solver;
|
|
SchedulingConfiguration scheduling = input.Configuration.Scheduling;
|
|
if (solver.MaximumOuterIterations <= 0 || solver.MaximumOsqpIterations <= 0 ||
|
|
!IsPositiveFinite(solver.AbsoluteTolerance) || !IsPositiveFinite(solver.RelativeTolerance) ||
|
|
!IsPositiveFinite(solver.StrictResidualTolerance) || !IsPositiveFinite(scheduling.SolverTimeoutSeconds))
|
|
{
|
|
failureReason = "Longitudinal solver configuration is invalid.";
|
|
return false;
|
|
}
|
|
try
|
|
{
|
|
totalBudget = TimeSpan.FromSeconds(scheduling.SolverTimeoutSeconds);
|
|
settings = new QpSolverSettings(solver.MaximumOsqpIterations, solver.AbsoluteTolerance, solver.RelativeTolerance,
|
|
totalBudget, solver.WarmStart, solver.Polish, solver.NativeVerbose);
|
|
convergenceTolerance = solver.StrictResidualTolerance;
|
|
iterationLimit = Math.Min(MaximumEnvelopeIterations, solver.MaximumOuterIterations);
|
|
return true;
|
|
}
|
|
catch (ArgumentException exception)
|
|
{
|
|
failureReason = exception.Message;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static LongitudinalCandidate CreateInitialIterate(LongitudinalPlanningInput input)
|
|
{
|
|
IReadOnlyList<double> times = LongitudinalCandidate.CreateKnotTimes(input.Configuration.Scheduling.TimeHorizonSeconds,
|
|
input.Configuration.Scheduling.OutputTimeStepSeconds);
|
|
if (TryCreateCruiseThenBrakeSeed(input, times, out LongitudinalCandidate brakingSeed))
|
|
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));
|
|
LongitudinalStoppingProfile terminalStop = LongitudinalStoppingMath.Calculate(requestedSpeed, 0d,
|
|
input.Configuration.Longitudinal.MaximumDecelerationMetersPerSecondSquared,
|
|
input.Configuration.Longitudinal.MaximumJerkMetersPerSecondCubed);
|
|
double cruiseDistance = Math.Max(0d, input.TerminalPathS - terminalStop.DistanceMeters);
|
|
for (int index = 0; index < knotCount; index++)
|
|
{
|
|
double fraction = (double)index / (knotCount - 1);
|
|
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;
|
|
u[knotCount - 1] = 0d;
|
|
a[knotCount - 1] = 0d;
|
|
return new LongitudinalCandidate(times, s, u, a, j);
|
|
}
|
|
|
|
private static bool TryCreateCruiseThenBrakeSeed(LongitudinalPlanningInput input, IReadOnlyList<double> times,
|
|
out LongitudinalCandidate candidate)
|
|
{
|
|
candidate = null;
|
|
double initialSpeed = input.InitialProgressSpeedMetersPerSecond;
|
|
double initialAcceleration = input.InitialAccelerationMetersPerSecondSquared;
|
|
if (initialSpeed <= 0d || Math.Abs(initialAcceleration) > 1e-12d)
|
|
return false;
|
|
double timeStep = times[1] - times[0];
|
|
for (int index = 1; index < times.Count - 1; index++)
|
|
{
|
|
if (Math.Abs((times[index + 1] - times[index]) - timeStep) > 1e-12d)
|
|
return false;
|
|
}
|
|
LongitudinalConfiguration configuration = input.Configuration.Longitudinal;
|
|
int intervalCount = times.Count - 1;
|
|
int maximumRampIntervals = Math.Min(intervalCount / 2, checked((int)Math.Floor(
|
|
configuration.MaximumDecelerationMetersPerSecondSquared /
|
|
(configuration.MaximumJerkMetersPerSecondCubed * timeStep))));
|
|
for (int rampIntervals = maximumRampIntervals; rampIntervals >= 1; rampIntervals--)
|
|
{
|
|
for (int plateauIntervals = 0; 2 * rampIntervals + plateauIntervals <= intervalCount; plateauIntervals++)
|
|
{
|
|
double jerkMagnitude = initialSpeed / (rampIntervals * (rampIntervals + plateauIntervals) *
|
|
timeStep * timeStep);
|
|
double peakDeceleration = jerkMagnitude * rampIntervals * timeStep;
|
|
if (jerkMagnitude > configuration.MaximumJerkMetersPerSecondCubed + 1e-12d ||
|
|
peakDeceleration > configuration.MaximumDecelerationMetersPerSecondSquared + 1e-12d)
|
|
{
|
|
continue;
|
|
}
|
|
int brakingIntervals = 2 * rampIntervals + plateauIntervals;
|
|
double brakingDistance = 0.5d * initialSpeed * brakingIntervals * timeStep;
|
|
if (brakingDistance > input.TerminalPathS + 1e-12d)
|
|
continue;
|
|
int maximumCruiseIntervals = intervalCount - brakingIntervals;
|
|
int cruiseIntervals = Math.Min(maximumCruiseIntervals, Math.Max(0, checked((int)Math.Floor(
|
|
(input.TerminalPathS - brakingDistance) / (initialSpeed * timeStep) + 1e-12d))));
|
|
var jerk = new double[intervalCount];
|
|
int cursor = cruiseIntervals;
|
|
for (int index = 0; index < rampIntervals; index++)
|
|
jerk[cursor++] = -jerkMagnitude;
|
|
cursor += plateauIntervals;
|
|
for (int index = 0; index < rampIntervals; index++)
|
|
jerk[cursor++] = jerkMagnitude;
|
|
LongitudinalCandidate integrated = LongitudinalCandidate.Integrate(times, 0d, initialSpeed, 0d, jerk);
|
|
if (integrated.S[integrated.S.Count - 1] <= input.TerminalPathS + 1e-12d)
|
|
{
|
|
candidate = integrated;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool TryCreateCandidate(IReadOnlyList<double> times, IReadOnlyList<double> primal,
|
|
out LongitudinalCandidate candidate)
|
|
{
|
|
candidate = null;
|
|
if (primal == null)
|
|
return false;
|
|
try
|
|
{
|
|
var layout = new LongitudinalVariableLayout(times.Count);
|
|
if (primal.Count != layout.VariableCount)
|
|
return false;
|
|
var s = new double[layout.KnotCount];
|
|
var u = new double[layout.KnotCount];
|
|
var a = new double[layout.KnotCount];
|
|
var j = new double[layout.KnotCount - 1];
|
|
for (int index = 0; index < layout.KnotCount; index++)
|
|
{
|
|
s[index] = primal[layout.S(index)];
|
|
u[index] = primal[layout.U(index)];
|
|
a[index] = primal[layout.A(index)];
|
|
}
|
|
for (int index = 0; index < layout.KnotCount - 1; index++)
|
|
j[index] = primal[layout.J(index)];
|
|
candidate = new LongitudinalCandidate(times, s, u, a, j);
|
|
return true;
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static double[] ToPrimal(LongitudinalCandidate candidate)
|
|
{
|
|
var layout = new LongitudinalVariableLayout(candidate.KnotTimes.Count);
|
|
var primal = new double[layout.VariableCount];
|
|
for (int index = 0; index < layout.KnotCount; index++)
|
|
{
|
|
primal[layout.S(index)] = candidate.S[index];
|
|
primal[layout.U(index)] = candidate.U[index];
|
|
primal[layout.A(index)] = candidate.A[index];
|
|
}
|
|
for (int index = 0; index < layout.KnotCount - 1; index++)
|
|
primal[layout.J(index)] = candidate.J[index];
|
|
return primal;
|
|
}
|
|
|
|
private static bool TryCreateEnvelopeIterate(LongitudinalPlanningInput input, LongitudinalCandidate previous,
|
|
LongitudinalCandidate candidate, out LongitudinalCandidate nextIterate)
|
|
{
|
|
nextIterate = null;
|
|
if (candidate.S.Count != previous.S.Count)
|
|
return false;
|
|
var candidateProgressSamples = new double[candidate.S.Count];
|
|
double priorProgress = double.NegativeInfinity;
|
|
double priorPreviousProgress = double.NegativeInfinity;
|
|
for (int index = 0; index < candidate.S.Count; index++)
|
|
{
|
|
double candidateProgress = candidate.S[index];
|
|
double previousProgress = previous.S[index];
|
|
if (!IsFinite(previousProgress) || previousProgress < 0d || previousProgress > input.TerminalPathS ||
|
|
previousProgress < priorPreviousProgress)
|
|
{
|
|
return false;
|
|
}
|
|
if (!IsFinite(candidateProgress))
|
|
{
|
|
candidateProgress = previousProgress;
|
|
}
|
|
candidateProgress = Math.Max(0d, Math.Min(input.TerminalPathS, candidateProgress));
|
|
if (index == candidate.S.Count - 1)
|
|
candidateProgress = input.TerminalPathS;
|
|
candidateProgress = Math.Max(priorProgress, candidateProgress);
|
|
candidateProgressSamples[index] = candidateProgress;
|
|
priorProgress = candidateProgress;
|
|
priorPreviousProgress = previousProgress;
|
|
}
|
|
var progress = new double[candidate.S.Count];
|
|
double previousNextProgress = 0d;
|
|
for (int index = 0; index < progress.Length; index++)
|
|
{
|
|
double candidateProgress = candidateProgressSamples[index];
|
|
if (index == 0 || index == progress.Length - 1 || candidateProgress >= input.TerminalPathS)
|
|
{
|
|
progress[index] = candidateProgress;
|
|
}
|
|
else
|
|
{
|
|
double timeStep = candidate.KnotTimes[index + 1] - candidate.KnotTimes[index];
|
|
double iterationAdvance = Math.Max(0d, candidateProgress - previous.S[index]);
|
|
double candidateSpeed = IsFinite(candidate.U[index]) ? Math.Max(0d, candidate.U[index]) : 0d;
|
|
double lookaheadAdvance = IsFinite(candidate.U[index])
|
|
? OrdinaryEnvelopeProbeLookaheadSteps * candidateSpeed * timeStep
|
|
: 0d;
|
|
double terminalLimitedAdvance = OrdinaryTerminalProbeFraction *
|
|
(input.TerminalPathS - candidateProgress);
|
|
double advance = Math.Min(Math.Max(iterationAdvance, lookaheadAdvance), terminalLimitedAdvance);
|
|
progress[index] = candidateProgress + advance;
|
|
}
|
|
progress[index] = Math.Max(previousNextProgress, progress[index]);
|
|
previousNextProgress = progress[index];
|
|
}
|
|
nextIterate = new LongitudinalCandidate(candidate.KnotTimes, progress, previous.U, previous.A, previous.J);
|
|
return true;
|
|
}
|
|
|
|
private static bool HasStrictResiduals(QpSolveResult result, double tolerance)
|
|
{
|
|
return IsPositiveFinite(tolerance) && result.PrimalResidual >= 0d && result.DualResidual >= 0d &&
|
|
result.PrimalResidual <= tolerance && result.DualResidual <= tolerance;
|
|
}
|
|
|
|
private static string CreateEnvelopeDiagnostic(PathSpeedLimit speedLimit, LongitudinalCandidate iterate,
|
|
LongitudinalCandidate candidate, int iteration)
|
|
{
|
|
int worstIndex = -1;
|
|
double worstExcess = double.NegativeInfinity;
|
|
for (int index = 0; index < candidate.S.Count; index++)
|
|
{
|
|
if (!IsFinite(candidate.S[index]) || !IsFinite(candidate.U[index]))
|
|
continue;
|
|
double candidateProgress = Math.Max(0d, Math.Min(speedLimit.TerminalPathS, candidate.S[index]));
|
|
double limit = speedLimit.MaximumSpeedAt(candidateProgress);
|
|
double excess = candidate.U[index] - limit;
|
|
if (excess > worstExcess)
|
|
{
|
|
worstExcess = excess;
|
|
worstIndex = index;
|
|
}
|
|
}
|
|
if (worstIndex < 0)
|
|
return "";
|
|
return " Envelope iteration " + iteration + " used PathS=" + iterate.S[worstIndex] +
|
|
" and produced PathS=" + candidate.S[worstIndex] + " at its largest speed-envelope excess.";
|
|
}
|
|
|
|
private static double MaximumProgressOrSpeedChange(LongitudinalCandidate previous, LongitudinalCandidate current)
|
|
{
|
|
double maximum = 0d;
|
|
for (int index = 0; index < previous.S.Count; index++)
|
|
{
|
|
maximum = Math.Max(maximum, Math.Abs(current.S[index] - previous.S[index]));
|
|
maximum = Math.Max(maximum, Math.Abs(current.U[index] - previous.U[index]));
|
|
}
|
|
return maximum;
|
|
}
|
|
|
|
private static double RelativeObjectiveImprovement(double previous, double current)
|
|
{
|
|
return Math.Abs(previous - current) / Math.Max(1d, Math.Abs(previous));
|
|
}
|
|
|
|
private static LongitudinalPlanningResult FallbackOrFailure(LongitudinalCandidate candidate,
|
|
EmPlanningStatus failureStatus, string failureReason)
|
|
{
|
|
return candidate == null
|
|
? Failed(failureStatus, failureReason)
|
|
: new LongitudinalPlanningResult(EmPlanningStatus.SuccessWithFallback, candidate, failureReason);
|
|
}
|
|
|
|
private static LongitudinalPlanningResult Failed(EmPlanningStatus status, string reason)
|
|
{
|
|
return new LongitudinalPlanningResult(status, null, reason);
|
|
}
|
|
|
|
private static LongitudinalCandidate CopyCandidate(LongitudinalCandidate source)
|
|
{
|
|
return new LongitudinalCandidate(source.KnotTimes, source.S, source.U, source.A, source.J);
|
|
}
|
|
|
|
private static bool IsPositiveFinite(double value)
|
|
{
|
|
return IsFinite(value) && value > 0d;
|
|
}
|
|
|
|
private static bool IsFinite(double value)
|
|
{
|
|
return !double.IsNaN(value) && !double.IsInfinity(value);
|
|
}
|
|
}
|