using System; using System.Collections.Generic; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; /// Assembles one lateral SQP QP with exact dynamics and finite hard bounds. public sealed class LateralConstraintBuilder { private const double Epsilon = 1e-12d; private readonly LateralObjectiveBuilder _objectiveBuilder; public LateralConstraintBuilder(LateralObjectiveBuilder objectiveBuilder) { _objectiveBuilder = objectiveBuilder ?? throw new ArgumentNullException(nameof(objectiveBuilder)); } public bool TryBuild(LateralPlanningInput input, LateralCandidate linearization, out QuadraticProgram problem, out string failureReason) { problem = null; failureReason = string.Empty; if (input == null || linearization == null) { failureReason = "Lateral input and linearization are required."; return false; } if (!TryValidateCandidateStations(input, linearization, out failureReason)) return false; try { var layout = new LateralVariableLayout(input.ReferenceStations.Count); var hessian = new SparseTripletBuilder(layout.VariableCount, layout.VariableCount, true); var linearCost = new double[layout.VariableCount]; _objectiveBuilder.AddTerms(input, layout, linearization, hessian, linearCost); int terminalRows = input.TerminalType == EmTerminalType.RollingSafetyStop ? 0 : 2; var constraints = new SparseTripletBuilder(8 * layout.StationCount - 2 + terminalRows, layout.VariableCount); var lower = new List(); var upper = new List(); int row = 0; if (!TryAddLateralBounds(input, layout, linearization, constraints, lower, upper, ref row, out failureReason)) return false; AddDerivativeBounds(input, layout, constraints, lower, upper, ref row); AddCurvatureBounds(input, layout, linearization, constraints, lower, upper, ref row); AddStartConstraints(input, layout, constraints, lower, upper, ref row); AddExactDynamics(input.ReferenceStations, layout, constraints, lower, upper, ref row); if (input.TerminalType != EmTerminalType.RollingSafetyStop) AddTerminalConstraints(layout, constraints, lower, upper, ref row); if (row != 8 * layout.StationCount - 2 + terminalRows) throw new InvalidOperationException("Lateral constraint row accounting is inconsistent."); problem = new QuadraticProgram(hessian.Build(), linearCost, constraints.Build(), lower, upper); return true; } catch (ArgumentException exception) { failureReason = exception.Message; return false; } } private static bool TryValidateCandidateStations(LateralPlanningInput input, LateralCandidate candidate, out string failureReason) { failureReason = string.Empty; if (candidate.ReferenceStations.Count != input.ReferenceStations.Count) { failureReason = "Linearization station count does not match the lateral input."; return false; } for (int index = 0; index < input.ReferenceStations.Count; index++) { if (Math.Abs(candidate.ReferenceStations[index] - input.ReferenceStations[index]) > Epsilon) { failureReason = "Linearization stations do not match the lateral input."; return false; } } return true; } private static bool TryAddLateralBounds(LateralPlanningInput input, LateralVariableLayout layout, LateralCandidate linearization, SparseTripletBuilder constraints, IList lower, IList upper, ref int row, out string failureReason) { failureReason = string.Empty; double maximumOffset = RequireNonnegative(input.Configuration.Corridor.MaximumLateralOffsetMeters, "maximum lateral offset"); double trustRegion = RequirePositive(input.Configuration.Lateral.MaximumLateralStepPerIterationMeters, "lateral trust region"); double minimumDenominator = RequirePositive(input.Configuration.Frenet.MinimumFrenetDenominator, "minimum Frenet denominator"); for (int station = 0; station < layout.StationCount; station++) { LateralInterval corridor = input.Corridor.Stations[station]; double minimum = Math.Max(corridor.MinimumL, Math.Max(-maximumOffset, linearization.L[station] - trustRegion)); double maximum = Math.Min(corridor.MaximumL, Math.Min(maximumOffset, linearization.L[station] + trustRegion)); double referenceCurvature = ReferencePathInterpolator.Interpolate(input.ReferenceSegment, input.ReferenceStations[station]).GeometricCurvature; if (referenceCurvature > 0d) maximum = Math.Min(maximum, (1d - minimumDenominator) / referenceCurvature); else if (referenceCurvature < 0d) minimum = Math.Max(minimum, (1d - minimumDenominator) / referenceCurvature); if (!IsFinite(minimum) || !IsFinite(maximum) || minimum > maximum + Epsilon) { failureReason = "The lateral corridor, offset, trust-region, and Frenet denominator bounds do not intersect at station " + station + "."; return false; } AddSingleVariableRow(constraints, lower, upper, ref row, layout.L(station), minimum, maximum); } return true; } private static void AddDerivativeBounds(LateralPlanningInput input, LateralVariableLayout layout, SparseTripletBuilder constraints, IList lower, IList upper, ref int row) { double slope = RequirePositive(input.Configuration.Lateral.MaximumLateralSlope, "maximum lateral slope"); double second = RequirePositive(input.Configuration.Lateral.MaximumLateralSecondDerivativePerMeter, "maximum lateral second derivative"); double third = RequirePositive(input.Configuration.Lateral.MaximumLateralThirdDerivativePerSquareMeter, "maximum lateral third derivative"); for (int station = 0; station < layout.StationCount; station++) { AddSingleVariableRow(constraints, lower, upper, ref row, layout.DL(station), -slope, slope); AddSingleVariableRow(constraints, lower, upper, ref row, layout.DDL(station), -second, second); } for (int interval = 0; interval < layout.StationCount - 1; interval++) AddSingleVariableRow(constraints, lower, upper, ref row, layout.DDDL(interval), -third, third); } private static void AddCurvatureBounds(LateralPlanningInput input, LateralVariableLayout layout, LateralCandidate linearization, SparseTripletBuilder constraints, IList lower, IList upper, ref int row) { double maximumCurvature = LateralCurvatureLinearization.GetMaximumVehicleCurvature(input.Vehicle); IReadOnlyList affines = LateralCurvatureLinearization.Create(input, layout, linearization); for (int station = 0; station < affines.Count; station++) { LateralCurvatureLinearization affine = affines[station]; AddRow(constraints, lower, upper, ref row, -maximumCurvature - affine.Constant, maximumCurvature - affine.Constant, affine.VariableIndices, affine.Gradient); } } private static void AddStartConstraints(LateralPlanningInput input, LateralVariableLayout layout, SparseTripletBuilder constraints, IList lower, IList upper, ref int row) { double denominator = 1d - input.StartProjection.ReferencePoint.GeometricCurvature * input.StartProjection.LateralOffset; double startSlope = denominator * Math.Tan(input.StartProjection.HeadingError); if (!IsFinite(startSlope)) throw new ArgumentException("The start lateral slope is non-finite.", nameof(input)); AddSingleVariableRow(constraints, lower, upper, ref row, layout.L(0), input.StartProjection.LateralOffset, input.StartProjection.LateralOffset); AddSingleVariableRow(constraints, lower, upper, ref row, layout.DL(0), startSlope, startSlope); } private static void AddExactDynamics(IReadOnlyList stations, LateralVariableLayout layout, SparseTripletBuilder constraints, IList lower, IList upper, ref int row) { for (int interval = 0; interval < layout.StationCount - 1; interval++) { double ds = stations[interval + 1] - stations[interval]; AddRow(constraints, lower, upper, ref row, 0d, 0d, new[] { layout.DDL(interval), layout.DDL(interval + 1), layout.DDDL(interval) }, new[] { -1d, 1d, -ds }); AddRow(constraints, lower, upper, ref row, 0d, 0d, new[] { layout.DL(interval), layout.DL(interval + 1), layout.DDL(interval), layout.DDDL(interval) }, new[] { -1d, 1d, -ds, -0.5d * ds * ds }); AddRow(constraints, lower, upper, ref row, 0d, 0d, new[] { layout.L(interval), layout.L(interval + 1), layout.DL(interval), layout.DDL(interval), layout.DDDL(interval) }, new[] { -1d, 1d, -ds, -0.5d * ds * ds, -ds * ds * ds / 6d }); } } private static void AddTerminalConstraints(LateralVariableLayout layout, SparseTripletBuilder constraints, IList lower, IList upper, ref int row) { AddSingleVariableRow(constraints, lower, upper, ref row, layout.L(layout.StationCount - 1), 0d, 0d); AddSingleVariableRow(constraints, lower, upper, ref row, layout.DL(layout.StationCount - 1), 0d, 0d); } private static void AddSingleVariableRow(SparseTripletBuilder constraints, IList lower, IList upper, ref int row, int variable, double minimum, double maximum) { AddRow(constraints, lower, upper, ref row, minimum, maximum, new[] { variable }, new[] { 1d }); } private static void AddRow(SparseTripletBuilder constraints, IList lower, IList upper, ref int row, double minimum, double maximum, IReadOnlyList variables, IReadOnlyList coefficients) { if (!IsFinite(minimum) || !IsFinite(maximum) || minimum > maximum || variables.Count != coefficients.Count) throw new ArgumentException("Lateral constraint bounds are invalid."); for (int index = 0; index < variables.Count; index++) constraints.Add(row, variables[index], coefficients[index]); lower.Add(minimum); upper.Add(maximum); row++; } private static double RequirePositive(double value, string name) { if (!IsFinite(value) || value <= 0d) throw new ArgumentOutOfRangeException(name); return value; } private static double RequireNonnegative(double value, string name) { if (!IsFinite(value) || value < 0d) throw new ArgumentOutOfRangeException(name); return value; } private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } }