using System;
using System.Collections.Generic;
namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
/// Builds curvature-aware, stopping-aware speed limits over actual optimized PathS.
public sealed class PathSpeedLimitBuilder
{
internal const double CurvatureEpsilon = 1e-10d;
private const double StopDistanceToleranceMeters = 1e-8d;
private const double StationMergeToleranceMeters = 1e-12d;
public EmPlanningStatus Build(LongitudinalPlanningInput input, out PathSpeedLimit speedLimit, out string failureReason)
{
speedLimit = null;
failureReason = string.Empty;
if (input == null)
{
failureReason = "Longitudinal planning input is required.";
return EmPlanningStatus.InvalidInput;
}
if (!TryGetLimits(input, out double directionMaximum, out double maximumAcceleration, out double maximumDeceleration,
out double maximumJerk, out double maximumLateralAcceleration, out double maximumCurvatureRate,
out failureReason))
{
return EmPlanningStatus.InvalidInput;
}
if (input.InitialProgressSpeedMetersPerSecond > directionMaximum + StopDistanceToleranceMeters ||
input.InitialAccelerationMetersPerSecondSquared < -maximumDeceleration - StopDistanceToleranceMeters ||
input.InitialAccelerationMetersPerSecondSquared > maximumAcceleration + StopDistanceToleranceMeters)
{
failureReason = "The initial longitudinal state violates the configured hard bounds.";
return EmPlanningStatus.InvalidInput;
}
if (input.HasStopBoundary)
{
if (!JerkLimitedStoppingMath.TryCalculate(input.InitialProgressSpeedMetersPerSecond,
input.InitialAccelerationMetersPerSecondSquared, maximumDeceleration, maximumJerk,
out JerkLimitedStoppingProfile stopProfile, out failureReason))
{
return EmPlanningStatus.InvalidInput;
}
if (stopProfile.DistanceMeters + StopDistanceToleranceMeters > input.StopBoundaryPathS)
{
failureReason = "The available actual PathS distance is insufficient for the jerk-limited stop.";
return EmPlanningStatus.StoppingDistanceInsufficient;
}
}
if (input.Configuration.Scheduling == null || !IsPositiveFinite(input.Configuration.Scheduling.OutputTimeStepSeconds))
{
failureReason = "The output time step required to refine the PathS speed envelope is invalid.";
return EmPlanningStatus.InvalidInput;
}
double maximumStationSpacing = directionMaximum * input.Configuration.Scheduling.OutputTimeStepSeconds;
var pathS = new List();
var maximum = new List();
var lateral = new List();
var curvatureRate = new List();
var stopping = new List();
for (int segmentIndex = 0; segmentIndex < input.Path.Points.Count - 1; segmentIndex++)
{
LateralPathPoint lowerPoint = input.Path.Points[segmentIndex];
LateralPathPoint upperPoint = input.Path.Points[segmentIndex + 1];
double span = upperPoint.PathS - lowerPoint.PathS;
int subdivisions = Math.Max(1, checked((int)Math.Ceiling(span / maximumStationSpacing)));
var segmentStations = new List(subdivisions + 16);
for (int subdivision = segmentIndex == 0 ? 0 : 1; subdivision <= subdivisions; subdivision++)
segmentStations.Add(Interpolate(lowerPoint.PathS, upperPoint.PathS, (double)subdivision / subdivisions));
if (input.HasStopBoundary)
{
AddJerkLimitedStoppingStations(lowerPoint.PathS, upperPoint.PathS, input.StopBoundaryPathS,
directionMaximum, maximumAcceleration, maximumDeceleration, maximumJerk,
segmentIndex == 0, segmentStations);
}
segmentStations.Sort();
double previousStation = double.NegativeInfinity;
for (int stationIndex = 0; stationIndex < segmentStations.Count; stationIndex++)
{
double samplePathS = segmentStations[stationIndex];
if (samplePathS <= previousStation + StationMergeToleranceMeters)
continue;
previousStation = samplePathS;
double fraction = (samplePathS - lowerPoint.PathS) / span;
double curvature = Interpolate(lowerPoint.VehicleCurvature, upperPoint.VehicleCurvature, fraction);
double curvatureDerivative = Interpolate(lowerPoint.VehicleCurvatureDerivative,
upperPoint.VehicleCurvatureDerivative, fraction);
AddLimitSample(samplePathS, curvature, curvatureDerivative, input.HasStopBoundary,
input.StopBoundaryPathS, directionMaximum, maximumAcceleration, maximumDeceleration,
maximumJerk, maximumLateralAcceleration, maximumCurvatureRate, pathS, maximum, lateral,
curvatureRate, stopping);
}
}
try
{
speedLimit = new PathSpeedLimit(pathS, maximum, lateral, curvatureRate, stopping, directionMaximum,
input.HasStopBoundary);
return EmPlanningStatus.Success;
}
catch (ArgumentException exception)
{
failureReason = exception.Message;
return EmPlanningStatus.InvalidInput;
}
}
internal static bool TryGetLimits(LongitudinalPlanningInput input, out double directionMaximum,
out double maximumAcceleration, out double maximumDeceleration, out double maximumJerk,
out double maximumLateralAcceleration, out double maximumCurvatureRate, out string failureReason)
{
directionMaximum = 0d;
maximumAcceleration = 0d;
maximumDeceleration = 0d;
maximumJerk = 0d;
maximumLateralAcceleration = 0d;
maximumCurvatureRate = 0d;
failureReason = string.Empty;
if (input.Configuration == null || input.Configuration.Longitudinal == null)
{
failureReason = "Longitudinal configuration is required.";
return false;
}
LongitudinalConfiguration configuration = input.Configuration.Longitudinal;
directionMaximum = input.DirectionMaximumSpeedMetersPerSecond;
maximumAcceleration = configuration.MaximumAccelerationMetersPerSecondSquared;
maximumDeceleration = configuration.MaximumDecelerationMetersPerSecondSquared;
maximumJerk = configuration.MaximumJerkMetersPerSecondCubed;
maximumLateralAcceleration = configuration.MaximumLateralAccelerationMetersPerSecondSquared;
maximumCurvatureRate = configuration.MaximumCurvatureRatePerMeterPerSecond;
if (!IsPositiveFinite(directionMaximum) || !IsPositiveFinite(maximumAcceleration) ||
!IsPositiveFinite(maximumDeceleration) || !IsPositiveFinite(maximumJerk) ||
!IsPositiveFinite(maximumLateralAcceleration) || !IsPositiveFinite(maximumCurvatureRate))
{
failureReason = "Longitudinal limits must be positive and finite.";
return false;
}
return true;
}
private static void AddJerkLimitedStoppingStations(double lowerPathS, double upperPathS,
double stopBoundaryPathS, double directionMaximum, double maximumAcceleration, double maximumDeceleration,
double maximumJerk, bool includeLower, IList stations)
{
const int stoppingSpeedSampleCount = 64;
for (int step = 0; step < stoppingSpeedSampleCount; step++)
{
double speed = directionMaximum * step / stoppingSpeedSampleCount;
if (!JerkLimitedStoppingMath.TryCalculate(speed, maximumAcceleration,
maximumDeceleration, maximumJerk, out JerkLimitedStoppingProfile stop, out _))
{
throw new ArgumentException("The configured jerk-limited stop envelope cannot be sampled.");
}
double station = stopBoundaryPathS - stop.DistanceMeters;
bool aboveLower = includeLower
? station >= lowerPathS - StationMergeToleranceMeters
: station > lowerPathS + StationMergeToleranceMeters;
if (aboveLower && station <= upperPathS + StationMergeToleranceMeters)
stations.Add(Math.Max(lowerPathS, Math.Min(upperPathS, station)));
}
}
private static void AddLimitSample(double samplePathS, double curvature, double curvatureDerivative,
bool hasStopBoundary, double stopBoundaryPathS, double directionMaximum, double maximumAcceleration,
double maximumDeceleration, double maximumJerk, double maximumLateralAcceleration,
double maximumCurvatureRate, IList pathS, IList maximum, IList lateral,
IList curvatureRate, IList stopping)
{
bool terminal = hasStopBoundary && samplePathS >= stopBoundaryPathS;
double lateralLimit = Math.Sqrt(maximumLateralAcceleration / Math.Max(Math.Abs(curvature), CurvatureEpsilon));
double curvatureRateLimit = maximumCurvatureRate / Math.Max(Math.Abs(curvatureDerivative), CurvatureEpsilon);
double stoppingLimit = hasStopBoundary
? JerkLimitedStoppingMath.MaximumInitialSpeedForDistance(
Math.Max(0d, stopBoundaryPathS - samplePathS), maximumAcceleration,
maximumDeceleration, maximumJerk, directionMaximum)
: directionMaximum;
double lateralValue = ClampFinite(lateralLimit, directionMaximum);
double curvatureRateValue = ClampFinite(curvatureRateLimit, directionMaximum);
double stoppingValue = terminal ? 0d : ClampFinite(stoppingLimit, directionMaximum);
pathS.Add(samplePathS);
lateral.Add(lateralValue);
curvatureRate.Add(curvatureRateValue);
stopping.Add(stoppingValue);
maximum.Add(terminal ? 0d : Math.Min(directionMaximum,
Math.Min(lateralValue, Math.Min(curvatureRateValue, stoppingValue))));
}
private static double Interpolate(double lower, double upper, double fraction)
{
return lower + (upper - lower) * fraction;
}
private static double ClampFinite(double value, double maximum)
{
if (!IsFinite(value) || value < 0d)
throw new ArgumentOutOfRangeException(nameof(value));
return Math.Min(maximum, value);
}
private static bool IsPositiveFinite(double value)
{
return IsFinite(value) && value > 0d;
}
private static bool IsFinite(double value)
{
return !double.IsNaN(value) && !double.IsInfinity(value);
}
}