using System;
using System.Collections.Generic;
using MultiWheelC.TrajectoryPlanning.CoarsePath;
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)
{
if (input == null)
{
speedLimit = null;
failureReason = "Longitudinal planning input is required.";
return EmPlanningStatus.InvalidInput;
}
return BuildCore(input.Path, input.Direction, input.InitialProgressSpeedMetersPerSecond,
input.InitialAccelerationMetersPerSecondSquared, input.TerminalType, input.Configuration,
out speedLimit, out failureReason);
}
public EmPlanningStatus Build(LateralPath path, TravelDirection direction,
double initialProgressSpeedMetersPerSecond, EmTerminalType terminalType,
EmPlannerConfiguration configuration, out PathSpeedLimit speedLimit, out string failureReason)
{
return BuildCore(path, direction, initialProgressSpeedMetersPerSecond, 0d, terminalType, configuration,
out speedLimit, out failureReason);
}
private EmPlanningStatus BuildCore(LateralPath path, TravelDirection direction,
double initialProgressSpeedMetersPerSecond, double initialAccelerationMetersPerSecondSquared,
EmTerminalType terminalType, EmPlannerConfiguration configuration, out PathSpeedLimit speedLimit,
out string failureReason)
{
speedLimit = null;
failureReason = string.Empty;
if (path == null || !path.IsIndependentlyValidated || path.Points.Count < 2 ||
!Enum.IsDefined(typeof(TravelDirection), direction) || !Enum.IsDefined(typeof(EmTerminalType), terminalType) ||
configuration == null || !IsFinite(initialProgressSpeedMetersPerSecond) ||
initialProgressSpeedMetersPerSecond < 0d || !IsFinite(initialAccelerationMetersPerSecondSquared))
{
failureReason = "Longitudinal planning input is required.";
return EmPlanningStatus.InvalidInput;
}
if (configuration.Longitudinal == null)
{
failureReason = "Longitudinal configuration is required.";
return EmPlanningStatus.InvalidInput;
}
LongitudinalConfiguration longitudinal = configuration.Longitudinal;
double directionMaximum = direction == TravelDirection.Forward
? longitudinal.MaximumForwardSpeedMetersPerSecond
: longitudinal.MaximumReverseSpeedMetersPerSecond;
double maximumAcceleration = longitudinal.MaximumAccelerationMetersPerSecondSquared;
double maximumDeceleration = longitudinal.MaximumDecelerationMetersPerSecondSquared;
double maximumJerk = longitudinal.MaximumJerkMetersPerSecondCubed;
double maximumLateralAcceleration = longitudinal.MaximumLateralAccelerationMetersPerSecondSquared;
double maximumCurvatureRate = longitudinal.MaximumCurvatureRatePerMeterPerSecond;
if (!IsPositiveFinite(directionMaximum) || !IsPositiveFinite(maximumAcceleration) ||
!IsPositiveFinite(maximumDeceleration) || !IsPositiveFinite(maximumJerk) ||
!IsPositiveFinite(maximumLateralAcceleration) || !IsPositiveFinite(maximumCurvatureRate))
{
failureReason = "Longitudinal limits must be positive and finite.";
return EmPlanningStatus.InvalidInput;
}
if (initialProgressSpeedMetersPerSecond > directionMaximum + StopDistanceToleranceMeters ||
initialAccelerationMetersPerSecondSquared < -maximumDeceleration - StopDistanceToleranceMeters ||
initialAccelerationMetersPerSecondSquared > maximumAcceleration + StopDistanceToleranceMeters)
{
failureReason = "The initial longitudinal state violates the configured hard bounds.";
return EmPlanningStatus.InvalidInput;
}
bool hasStopBoundary = terminalType != EmTerminalType.RollingSafetyStop;
double stopBoundaryPathS = path.Points[path.Points.Count - 1].PathS;
if (hasStopBoundary)
{
if (!JerkLimitedStoppingMath.TryCalculate(initialProgressSpeedMetersPerSecond,
initialAccelerationMetersPerSecondSquared, maximumDeceleration, maximumJerk,
out JerkLimitedStoppingProfile stopProfile, out failureReason))
{
return EmPlanningStatus.InvalidInput;
}
if (stopProfile.DistanceMeters + StopDistanceToleranceMeters > stopBoundaryPathS)
{
failureReason = "The available actual PathS distance is insufficient for the jerk-limited stop.";
return EmPlanningStatus.StoppingDistanceInsufficient;
}
}
if (configuration.Scheduling == null ||
!IsPositiveFinite(configuration.Scheduling.MaximumOptimizationSpatialStepMeters))
{
failureReason = "The optimization spatial step required to refine the PathS speed envelope is invalid.";
return EmPlanningStatus.InvalidInput;
}
double maximumStationSpacing = configuration.Scheduling.MaximumOptimizationSpatialStepMeters;
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 < path.Points.Count - 1; segmentIndex++)
{
LateralPathPoint lowerPoint = path.Points[segmentIndex];
LateralPathPoint upperPoint = 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 (hasStopBoundary)
{
AddJerkLimitedStoppingStations(lowerPoint.PathS, upperPoint.PathS, 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, hasStopBoundary,
stopBoundaryPathS, directionMaximum, maximumAcceleration, maximumDeceleration,
maximumJerk, maximumLateralAcceleration, maximumCurvatureRate, pathS, maximum, lateral,
curvatureRate, stopping);
}
}
try
{
speedLimit = new PathSpeedLimit(pathS, maximum, lateral, curvatureRate, stopping, directionMaximum,
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);
}
}