using System; using System.Collections.Generic; using System.Collections.ObjectModel; using MultiWheelC.TrajectoryPlanning.CoarsePath; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; /// Immutable longitudinal inputs derived only from an independently validated lateral PathS path. public sealed class LongitudinalPlanningInput { private const double PathSTolerance = 1e-12d; public LongitudinalPlanningInput(LateralPath path, TravelDirection direction, double initialProgressSpeedMetersPerSecond, double initialAccelerationMetersPerSecondSquared, EmTerminalType terminalType, EmLongitudinalMode mode, EmPlannerConfiguration configuration, IReadOnlyList previousPathS, IReadOnlyList previousProgressSpeedMetersPerSecond) : this(path, direction, initialProgressSpeedMetersPerSecond, initialAccelerationMetersPerSecondSquared, terminalType, mode, configuration, EmPlanningScope.RollingHorizon, CreateRollingSchedule(configuration), previousPathS, previousProgressSpeedMetersPerSecond) { } public LongitudinalPlanningInput(LateralPath path, TravelDirection direction, double initialProgressSpeedMetersPerSecond, double initialAccelerationMetersPerSecondSquared, EmTerminalType terminalType, EmLongitudinalMode mode, EmPlannerConfiguration configuration, EmPlanningScope planningScope, LongitudinalKnotSchedule knotSchedule, IReadOnlyList previousPathS, IReadOnlyList previousProgressSpeedMetersPerSecond) { if (path == null || !path.IsIndependentlyValidated || path.Points.Count < 2) throw new ArgumentException("Longitudinal planning requires an independently validated lateral path with at least two points.", nameof(path)); if (!Enum.IsDefined(typeof(TravelDirection), direction)) throw new ArgumentOutOfRangeException(nameof(direction)); if (!IsFinite(initialProgressSpeedMetersPerSecond) || initialProgressSpeedMetersPerSecond < 0d) throw new ArgumentOutOfRangeException(nameof(initialProgressSpeedMetersPerSecond)); if (!IsFinite(initialAccelerationMetersPerSecondSquared)) throw new ArgumentOutOfRangeException(nameof(initialAccelerationMetersPerSecondSquared)); if (!Enum.IsDefined(typeof(EmTerminalType), terminalType)) throw new ArgumentOutOfRangeException(nameof(terminalType)); if (!Enum.IsDefined(typeof(EmLongitudinalMode), mode)) throw new ArgumentOutOfRangeException(nameof(mode)); if (mode == EmLongitudinalMode.RollingContinuation && terminalType != EmTerminalType.RollingSafetyStop) throw new ArgumentException("Rolling continuation requires a rolling window boundary."); if (mode != EmLongitudinalMode.RollingContinuation && terminalType == EmTerminalType.RollingSafetyStop) throw new ArgumentException("Stop-boundary modes require Goal or GearSwitch."); if (configuration == null) throw new ArgumentNullException(nameof(configuration)); if (!Enum.IsDefined(typeof(EmPlanningScope), planningScope)) throw new ArgumentOutOfRangeException(nameof(planningScope)); if (knotSchedule == null) throw new ArgumentNullException(nameof(knotSchedule)); if ((planningScope == EmPlanningScope.FullDirectionSegment) != knotSchedule.IsAdaptive) throw new ArgumentException("Full-direction planning requires an adaptive schedule and rolling planning requires a rolling schedule.", nameof(knotSchedule)); Path = CopyAndValidatePath(path); Direction = direction; InitialProgressSpeedMetersPerSecond = initialProgressSpeedMetersPerSecond; InitialAccelerationMetersPerSecondSquared = initialAccelerationMetersPerSecondSquared; TerminalType = terminalType; Mode = mode; Configuration = configuration.Copy(); PlanningScope = planningScope; KnotSchedule = knotSchedule.Copy(); PreviousPathS = CopyFiniteNonnegative(previousPathS, nameof(previousPathS)); PreviousProgressSpeedMetersPerSecond = CopyFiniteNonnegative(previousProgressSpeedMetersPerSecond, nameof(previousProgressSpeedMetersPerSecond)); if (PreviousPathS.Count != PreviousProgressSpeedMetersPerSecond.Count) throw new ArgumentException("Previous path-S and progress-speed samples must have matching counts.", nameof(previousProgressSpeedMetersPerSecond)); } public LongitudinalPlanningInput(LateralPath path, TravelDirection direction, double initialProgressSpeedMetersPerSecond, double initialAccelerationMetersPerSecondSquared, EmTerminalType terminalType, EmPlannerConfiguration configuration, IReadOnlyList previousPathS, IReadOnlyList previousProgressSpeedMetersPerSecond) : this(path, direction, initialProgressSpeedMetersPerSecond, initialAccelerationMetersPerSecondSquared, terminalType, terminalType == EmTerminalType.RollingSafetyStop ? EmLongitudinalMode.RollingContinuation : EmLongitudinalMode.ExactStopAtBoundary, configuration, previousPathS, previousProgressSpeedMetersPerSecond) { } public LateralPath Path { get; } public TravelDirection Direction { get; } public double InitialProgressSpeedMetersPerSecond { get; } public double InitialAccelerationMetersPerSecondSquared { get; } public EmTerminalType TerminalType { get; } public EmLongitudinalMode Mode { get; } public EmPlannerConfiguration Configuration { get; } public EmPlanningScope PlanningScope { get; } public LongitudinalKnotSchedule KnotSchedule { get; } public IReadOnlyList PreviousPathS { get; } public IReadOnlyList PreviousProgressSpeedMetersPerSecond { get; } public double PathUpperBoundS { get { return Path.Points[Path.Points.Count - 1].PathS; } } public bool HasStopBoundary { get { return TerminalType != EmTerminalType.RollingSafetyStop; } } public double StopBoundaryPathS { get { return HasStopBoundary ? PathUpperBoundS : double.NaN; } } public EmBoundaryType StopBoundaryType { get { return TerminalType == EmTerminalType.Goal ? EmBoundaryType.Goal : TerminalType == EmTerminalType.GearSwitch ? EmBoundaryType.GearSwitchApproach : EmBoundaryType.None; } } public double TerminalPathS { get { return PathUpperBoundS; } } public double DirectionMaximumSpeedMetersPerSecond { get { return Direction == TravelDirection.Forward ? Configuration.Longitudinal.MaximumForwardSpeedMetersPerSecond : Configuration.Longitudinal.MaximumReverseSpeedMetersPerSecond; } } private static LateralPath CopyAndValidatePath(LateralPath source) { var copy = new List(source.Points.Count); double previousPathS = double.NegativeInfinity; for (int index = 0; index < source.Points.Count; index++) { LateralPathPoint point = source.Points[index]; if (point == null || !IsFinite(point.PathS) || point.PathS <= previousPathS) throw new ArgumentException("Lateral PathS must be finite and strictly increasing for ST planning.", nameof(source)); if (index == 0 && Math.Abs(point.PathS) > PathSTolerance) throw new ArgumentException("The lateral PathS supplied to ST must begin at zero.", nameof(source)); copy.Add(new LateralPathPoint(point.ReferenceS, point.PathS, point.L, point.DL, point.DDL, point.DDDL, point.X, point.Y, point.VehicleYaw, point.GeometricCurvature, point.VehicleCurvature, point.VehicleCurvatureDerivative)); previousPathS = point.PathS; } return new LateralPath(copy, true); } private static IReadOnlyList CopyFiniteNonnegative(IReadOnlyList source, string parameterName) { var copy = new List(source == null ? 0 : source.Count); if (source != null) { for (int index = 0; index < source.Count; index++) { if (!IsFinite(source[index]) || source[index] < 0d) throw new ArgumentOutOfRangeException(parameterName); copy.Add(source[index]); } } return new ReadOnlyCollection(copy); } private static LongitudinalKnotSchedule CreateRollingSchedule(EmPlannerConfiguration configuration) { if (configuration == null || configuration.Scheduling == null) throw new ArgumentNullException(nameof(configuration)); return LongitudinalKnotSchedule.CreateRolling(configuration.Scheduling.TimeHorizonSeconds, configuration.Scheduling.OutputTimeStepSeconds); } private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } }