feat: assemble lateral LS quadratic programs

This commit is contained in:
梁薄云
2026-08-04 00:58:09 +08:00
parent c225b17d37
commit f703d418ab
4 changed files with 766 additions and 8 deletions
@@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
/// <summary>Assembles one lateral SQP QP with exact dynamics and finite hard bounds.</summary>
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(7 * layout.StationCount - 2 + terminalRows, layout.VariableCount);
var lower = new List<double>();
var upper = new List<double>();
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);
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 != 7 * 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<double> lower, IList<double> 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<double> lower, IList<double> 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 AddStartConstraints(LateralPlanningInput input, LateralVariableLayout layout,
SparseTripletBuilder constraints, IList<double> lower, IList<double> 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<double> stations, LateralVariableLayout layout,
SparseTripletBuilder constraints, IList<double> lower, IList<double> 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<double> lower, IList<double> 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<double> lower, IList<double> 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<double> lower, IList<double> upper, ref int row,
double minimum, double maximum, IReadOnlyList<int> variables, IReadOnlyList<double> 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);
}
}
@@ -0,0 +1,248 @@
using System;
using System.Collections.Generic;
using MultiWheelC.TrajectoryPlanning.CoarsePath;
namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
/// <summary>Builds normalized squared-residual costs in OSQP's 0.5*x'P*x + q'x convention.</summary>
public sealed class LateralObjectiveBuilder
{
public void AddTerms(LateralPlanningInput input, LateralVariableLayout layout, LateralCandidate linearization,
SparseTripletBuilder hessian, IList<double> linearCost)
{
if (input == null)
throw new ArgumentNullException(nameof(input));
if (layout == null)
throw new ArgumentNullException(nameof(layout));
if (linearization == null)
throw new ArgumentNullException(nameof(linearization));
if (hessian == null)
throw new ArgumentNullException(nameof(hessian));
if (linearCost == null || linearCost.Count != layout.VariableCount)
throw new ArgumentException("Linear cost must match the lateral layout.", nameof(linearCost));
LateralConfiguration lateral = input.Configuration.Lateral ?? throw new ArgumentException("Missing lateral configuration.");
LateralWeights weights = lateral.Weights ?? throw new ArgumentException("Missing lateral weights.");
double lateralScale = RequirePositive(input.Configuration.Corridor.MaximumLateralOffsetMeters, "lateral scale");
double slopeScale = RequirePositive(lateral.MaximumLateralSlope, "slope scale");
double secondDerivativeScale = RequirePositive(lateral.MaximumLateralSecondDerivativePerMeter, "second-derivative scale");
double thirdDerivativeScale = RequirePositive(lateral.MaximumLateralThirdDerivativePerSquareMeter, "third-derivative scale");
double curvatureScale = RequirePositive(GetMaximumVehicleCurvature(input.Vehicle), "curvature scale");
double curvatureVariationScale = GetCurvatureVariationScale(input);
for (int station = 0; station < layout.StationCount; station++)
{
AddSquaredResidual(hessian, linearCost, new[] { layout.L(station) }, new[] { 1d }, 0d,
weights.ReferenceOffset, lateralScale);
AddSquaredResidual(hessian, linearCost, new[] { layout.DL(station) }, new[] { 1d }, 0d,
weights.HeadingDeviation, slopeScale);
AddSquaredResidual(hessian, linearCost, new[] { layout.DDL(station) }, new[] { 1d }, 0d,
weights.SecondDerivative, secondDerivativeScale);
}
for (int interval = 0; interval < layout.StationCount - 1; interval++)
{
AddSquaredResidual(hessian, linearCost, new[] { layout.DDDL(interval) }, new[] { 1d }, 0d,
weights.ThirdDerivative, thirdDerivativeScale);
}
AddPreviousTrajectoryTerms(input, layout, hessian, linearCost, weights.PreviousTrajectory, lateralScale);
CurvatureAffine[] curvature = CreateCurvatureAffines(input, layout, linearization);
for (int station = 0; station < curvature.Length; station++)
{
AddSquaredResidual(hessian, linearCost, curvature[station].Indices, curvature[station].Gradient,
curvature[station].Constant, weights.Curvature, curvatureScale);
}
AddCurvatureVariationTerms(input.ReferenceStations, curvature, hessian, linearCost, weights.CurvatureVariation,
curvatureVariationScale);
if (input.TerminalType == EmTerminalType.RollingSafetyStop)
{
AddSquaredResidual(hessian, linearCost, new[] { layout.L(layout.StationCount - 1) }, new[] { 1d }, 0d,
weights.RollingTerminal, lateralScale);
}
}
private static void AddPreviousTrajectoryTerms(LateralPlanningInput input, LateralVariableLayout layout,
SparseTripletBuilder hessian, IList<double> linearCost, double weight, double lateralScale)
{
if (input.PreviousTrajectorySeed.Count == 0)
return;
for (int station = 0; station < layout.StationCount; station++)
{
double previousL = InterpolatePreviousL(input.PreviousTrajectorySeed, input.ReferenceStations[station]);
AddSquaredResidual(hessian, linearCost, new[] { layout.L(station) }, new[] { 1d }, -previousL,
weight, lateralScale);
}
}
private static CurvatureAffine[] CreateCurvatureAffines(LateralPlanningInput input, LateralVariableLayout layout,
LateralCandidate linearization)
{
var affines = new CurvatureAffine[layout.StationCount];
double directionSign = input.ReferenceSegment.Direction == TravelDirection.Forward ? 1d : -1d;
for (int station = 0; station < layout.StationCount; station++)
{
FrenetReferencePoint reference = ReferencePathInterpolator.Interpolate(input.ReferenceSegment,
input.ReferenceStations[station]);
double l = linearization.L[station];
double dl = linearization.DL[station];
double ddl = linearization.DDL[station];
double referenceCurvature = reference.GeometricCurvature;
double referenceCurvatureDerivative = directionSign * reference.VehicleCurvatureDerivative;
double a = 1d - referenceCurvature * l;
double denominatorSquared = a * a + dl * dl;
if (!IsFinite(denominatorSquared) || denominatorSquared <= 0d)
throw new ArgumentException("Curvature linearization denominator is invalid.", nameof(linearization));
double denominatorPow3Over2 = denominatorSquared * Math.Sqrt(denominatorSquared);
double denominatorPow5Over2 = denominatorPow3Over2 * denominatorSquared;
double numerator = a * a * referenceCurvature + a * ddl +
referenceCurvatureDerivative * l * dl + 2d * referenceCurvature * dl * dl;
double geometricCurvature = numerator / denominatorPow3Over2;
double dNumeratorDLateral = -2d * a * referenceCurvature * referenceCurvature -
referenceCurvature * ddl + referenceCurvatureDerivative * dl;
double dNumeratorDSlope = referenceCurvatureDerivative * l + 4d * referenceCurvature * dl;
double dDenominatorSquaredDLateral = -2d * a * referenceCurvature;
double dDenominatorSquaredDSlope = 2d * dl;
double dGeometricDLateral = dNumeratorDLateral / denominatorPow3Over2 -
1.5d * numerator * dDenominatorSquaredDLateral / denominatorPow5Over2;
double dGeometricDSlope = dNumeratorDSlope / denominatorPow3Over2 -
1.5d * numerator * dDenominatorSquaredDSlope / denominatorPow5Over2;
double dGeometricDSecondDerivative = a / denominatorPow3Over2;
double vehicleCurvature = directionSign * geometricCurvature;
double[] gradient =
{
directionSign * dGeometricDLateral,
directionSign * dGeometricDSlope,
directionSign * dGeometricDSecondDerivative,
};
double constant = vehicleCurvature - gradient[0] * l - gradient[1] * dl - gradient[2] * ddl;
if (!IsFinite(vehicleCurvature) || !IsFinite(constant) || !IsFinite(gradient[0]) ||
!IsFinite(gradient[1]) || !IsFinite(gradient[2]))
{
throw new ArgumentException("Curvature linearization is non-finite.", nameof(linearization));
}
affines[station] = new CurvatureAffine(new[] { layout.L(station), layout.DL(station), layout.DDL(station) },
gradient, constant);
}
return affines;
}
private static void AddCurvatureVariationTerms(IReadOnlyList<double> stations, CurvatureAffine[] curvature,
SparseTripletBuilder hessian, IList<double> linearCost, double weight, double scale)
{
for (int station = 0; station < curvature.Length; station++)
{
int lower = station == 0 ? 0 : station - 1;
int upper = station == curvature.Length - 1 ? curvature.Length - 1 : station + 1;
double ds = stations[upper] - stations[lower];
if (!IsFinite(ds) || ds <= 0d)
throw new ArgumentException("Curvature variation requires strictly increasing stations.", nameof(stations));
CurvatureAffine left = curvature[lower];
CurvatureAffine right = curvature[upper];
var indices = new int[left.Indices.Length + right.Indices.Length];
var gradient = new double[indices.Length];
for (int index = 0; index < left.Indices.Length; index++)
{
indices[index] = left.Indices[index];
gradient[index] = -left.Gradient[index] / ds;
indices[left.Indices.Length + index] = right.Indices[index];
gradient[left.Indices.Length + index] = right.Gradient[index] / ds;
}
AddSquaredResidual(hessian, linearCost, indices, gradient, (right.Constant - left.Constant) / ds, weight, scale);
}
}
private static void AddSquaredResidual(SparseTripletBuilder hessian, IList<double> linearCost,
IReadOnlyList<int> indices, IReadOnlyList<double> gradient, double constant, double weight, double scale)
{
if (indices.Count != gradient.Count || indices.Count == 0 || !IsFinite(constant))
throw new ArgumentException("Affine residual is invalid.");
if (!IsFinite(weight) || weight < 0d)
throw new ArgumentOutOfRangeException(nameof(weight));
double coefficient = 2d * weight / (scale * scale);
for (int left = 0; left < indices.Count; left++)
{
if (!IsFinite(gradient[left]) || indices[left] < 0 || indices[left] >= linearCost.Count)
throw new ArgumentOutOfRangeException(nameof(gradient));
linearCost[indices[left]] += coefficient * constant * gradient[left];
for (int right = left; right < indices.Count; right++)
{
if (!IsFinite(gradient[right]) || indices[right] < 0 || indices[right] >= linearCost.Count)
throw new ArgumentOutOfRangeException(nameof(gradient));
int row = Math.Min(indices[left], indices[right]);
int column = Math.Max(indices[left], indices[right]);
hessian.Add(row, column, coefficient * gradient[left] * gradient[right]);
}
}
}
private static double InterpolatePreviousL(IReadOnlyList<FrenetProjection> seed, double referenceS)
{
if (referenceS <= seed[0].ReferenceS)
return seed[0].LateralOffset;
for (int index = 1; index < seed.Count; index++)
{
if (referenceS <= seed[index].ReferenceS)
{
FrenetProjection lower = seed[index - 1];
FrenetProjection upper = seed[index];
double span = upper.ReferenceS - lower.ReferenceS;
if (span <= 0d)
return upper.LateralOffset;
return lower.LateralOffset + (upper.LateralOffset - lower.LateralOffset) *
(referenceS - lower.ReferenceS) / span;
}
}
return seed[seed.Count - 1].LateralOffset;
}
private static double GetCurvatureVariationScale(LateralPlanningInput input)
{
double maximum = 0d;
for (int station = 0; station < input.ReferenceStations.Count; station++)
{
FrenetReferencePoint reference = ReferencePathInterpolator.Interpolate(input.ReferenceSegment,
input.ReferenceStations[station]);
maximum = Math.Max(maximum, Math.Abs(reference.VehicleCurvatureDerivative));
}
return Math.Max(1d, maximum);
}
private static double GetMaximumVehicleCurvature(VehicleParameters vehicle)
{
if (vehicle == null)
throw new ArgumentNullException(nameof(vehicle));
if (vehicle.MaximumCurvaturePerMeter.HasValue)
return vehicle.MaximumCurvaturePerMeter.Value;
if (vehicle.MinimumTurningRadiusMeters.HasValue && vehicle.MinimumTurningRadiusMeters.Value > 0d)
return 1d / vehicle.MinimumTurningRadiusMeters.Value;
throw new ArgumentException("Vehicle maximum curvature is required.", nameof(vehicle));
}
private static double RequirePositive(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);
}
private sealed class CurvatureAffine
{
public CurvatureAffine(int[] indices, double[] gradient, double constant)
{
Indices = indices;
Gradient = gradient;
Constant = constant;
}
public int[] Indices { get; }
public double[] Gradient { get; }
public double Constant { get; }
}
}