feat: solve QPs through OSQP

This commit is contained in:
梁薄云
2026-08-04 00:19:26 +08:00
parent 2051827416
commit a957fda6c5
3 changed files with 461 additions and 2 deletions
@@ -0,0 +1,296 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using MultiWheelC.TrajectoryPlanning.Utils;
namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
public sealed class OsqpNativeSolver : IQpSolver
{
public QpSolveResult Solve(
QuadraticProgram problem,
QpSolverSettings settings,
IReadOnlyList<double> warmStart,
CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
return CreateFailure(QpSolveStatus.Cancelled, "OSQP solve was cancelled before native setup.");
if (problem == null)
return CreateFailure(QpSolveStatus.InvalidProblem, "A quadratic program is required.");
if (settings == null)
return CreateFailure(QpSolveStatus.InvalidProblem, "Solver settings are required.");
double[] copiedWarmStart;
string warmStartDiagnostic;
if (!TryCopyWarmStart(warmStart, problem.VariableCount, out copiedWarmStart, out warmStartDiagnostic))
return CreateFailure(QpSolveStatus.InvalidProblem, warmStartDiagnostic);
OsqpNativeApi api;
OsqpNativeLoadResult loadResult;
if (!OsqpNativeLoader.TryGetLoadedApi(out api, out loadResult))
return CreateFailure(loadResult.Status, loadResult.Diagnostic, loadResult.Version);
var pinnedArrays = new OsqpPinnedArrays();
OsqpNativeSetupMemory setupMemory = null;
IntPtr solver = IntPtr.Zero;
try
{
IntPtr pValues = pinnedArrays.Pin(Copy(problem.UpperTriangularP.Values));
IntPtr pRows = pinnedArrays.Pin(Copy(problem.UpperTriangularP.RowIndices));
IntPtr pColumns = pinnedArrays.Pin(Copy(problem.UpperTriangularP.ColumnPointers));
IntPtr q = pinnedArrays.Pin(Copy(problem.LinearCost));
IntPtr aValues = pinnedArrays.Pin(Copy(problem.ConstraintMatrix.Values));
IntPtr aRows = pinnedArrays.Pin(Copy(problem.ConstraintMatrix.RowIndices));
IntPtr aColumns = pinnedArrays.Pin(Copy(problem.ConstraintMatrix.ColumnPointers));
IntPtr lowerBounds = pinnedArrays.Pin(Copy(problem.LowerBounds));
IntPtr upperBounds = pinnedArrays.Pin(Copy(problem.UpperBounds));
IntPtr warmStartPointer = pinnedArrays.Pin(copiedWarmStart);
setupMemory = OsqpNativeStructures.AllocateSetupMemory(api.SetDefaultSettings);
Marshal.StructureToPtr(
CreateCsc(problem.UpperTriangularP, pColumns, pRows, pValues),
setupMemory.UpperTriangularP,
false);
Marshal.StructureToPtr(
CreateCsc(problem.ConstraintMatrix, aColumns, aRows, aValues),
setupMemory.Constraints,
false);
var nativeSettings = (OsqpSettings)Marshal.PtrToStructure(setupMemory.Settings, typeof(OsqpSettings));
nativeSettings.Verbose = 0;
nativeSettings.WarmStarting = settings.EnableWarmStart ? 1 : 0;
nativeSettings.Polishing = settings.EnablePolishing ? 1 : 0;
nativeSettings.MaximumIterations = settings.MaximumIterations;
nativeSettings.AbsoluteTolerance = settings.AbsoluteTolerance;
nativeSettings.RelativeTolerance = settings.RelativeTolerance;
nativeSettings.TimeLimit = settings.TimeLimit.TotalSeconds;
Marshal.StructureToPtr(nativeSettings, setupMemory.Settings, false);
int setupStatus = api.Setup(
out solver,
setupMemory.UpperTriangularP,
q,
setupMemory.Constraints,
lowerBounds,
upperBounds,
problem.ConstraintCount,
problem.VariableCount,
setupMemory.Settings);
if (setupStatus != 0 || solver == IntPtr.Zero)
return CreateFailure(QpSolveStatus.NativeError, "OSQP setup failed with code " + setupStatus + ".", "setup=" + setupStatus);
if (settings.EnableWarmStart && copiedWarmStart != null)
{
int warmStartStatus = api.WarmStart(solver, warmStartPointer, IntPtr.Zero);
if (warmStartStatus != 0)
return CreateFailure(QpSolveStatus.NativeError, "OSQP warm start failed with code " + warmStartStatus + ".", "warm_start=" + warmStartStatus);
}
int solveStatus = api.Solve(solver);
if (solveStatus != 0)
return CreateFailure(QpSolveStatus.NativeError, "OSQP solve failed with code " + solveStatus + ".", "solve=" + solveStatus);
if (cancellationToken.IsCancellationRequested)
return CreateFailure(QpSolveStatus.Cancelled, "OSQP solve was cancelled after native completion.");
return ReadSolveResult(solver, problem.VariableCount);
}
catch (Exception exception)
{
return CreateFailure(QpSolveStatus.NativeError, "OSQP native solve failed: " + exception.Message);
}
finally
{
if (solver != IntPtr.Zero)
{
try
{
api.Cleanup(solver);
}
catch
{
}
}
if (setupMemory != null)
setupMemory.Dispose();
pinnedArrays.Dispose();
}
}
private static QpSolveResult ReadSolveResult(IntPtr solver, int variableCount)
{
var solverPrefix = (OsqpSolverPrefix)Marshal.PtrToStructure(solver, typeof(OsqpSolverPrefix));
if (solverPrefix.Info == IntPtr.Zero)
return CreateFailure(QpSolveStatus.NativeError, "OSQP solve returned no information block.");
var info = (OsqpInfo)Marshal.PtrToStructure(solverPrefix.Info, typeof(OsqpInfo));
string nativeStatus = ReadNativeStatus(info);
QpSolveStatus status = OsqpStatusMapper.Map(info.StatusValue);
double[] primal;
string primalDiagnostic;
if (!TryReadPrimal(solverPrefix.Solution, variableCount, out primal, out primalDiagnostic))
return CreateFailure(QpSolveStatus.NativeError, primalDiagnostic, nativeStatus);
string diagnostic = string.Empty;
double objective = ToFiniteMetric(info.ObjectiveValue, "objective", ref diagnostic);
double primalResidual = ToFiniteMetric(info.PrimalResidual, "primal residual", ref diagnostic);
double dualResidual = ToFiniteMetric(info.DualResidual, "dual residual", ref diagnostic);
return new QpSolveResult(
status,
primal,
objective,
primalResidual,
dualResidual,
Math.Max(0, info.Iterations),
ToSolveTime(info.SolveTime),
nativeStatus,
diagnostic);
}
private static bool TryReadPrimal(IntPtr solutionPointer, int variableCount, out double[] primal, out string diagnostic)
{
primal = new double[0];
diagnostic = string.Empty;
if (solutionPointer == IntPtr.Zero)
{
diagnostic = "OSQP solve returned no solution block.";
return false;
}
var solution = (OsqpSolution)Marshal.PtrToStructure(solutionPointer, typeof(OsqpSolution));
if (variableCount == 0)
return true;
if (solution.Primal == IntPtr.Zero)
{
diagnostic = "OSQP solve returned no primal vector.";
return false;
}
primal = new double[variableCount];
Marshal.Copy(solution.Primal, primal, 0, primal.Length);
for (int index = 0; index < primal.Length; index++)
{
if (!NumericGuard.IsFinite(primal[index]))
{
diagnostic = "OSQP solve returned a non-finite primal value.";
return false;
}
}
return true;
}
private static OsqpCscMatrix CreateCsc(SparseCscMatrix matrix, IntPtr columnPointers, IntPtr rowIndices, IntPtr values)
{
return new OsqpCscMatrix
{
RowCount = matrix.RowCount,
ColumnCount = matrix.ColumnCount,
ColumnPointers = columnPointers,
RowIndices = rowIndices,
Values = values,
MaximumNonZeroCount = matrix.Values.Count,
NonZeroCount = -1,
OwnsData = 0,
};
}
private static bool TryCopyWarmStart(
IReadOnlyList<double> warmStart,
int variableCount,
out double[] copiedWarmStart,
out string diagnostic)
{
copiedWarmStart = null;
diagnostic = string.Empty;
if (warmStart == null)
return true;
if (warmStart.Count != variableCount)
{
diagnostic = "OSQP warm start length must match the quadratic-program variable count.";
return false;
}
copiedWarmStart = Copy(warmStart);
for (int index = 0; index < copiedWarmStart.Length; index++)
{
if (!NumericGuard.IsFinite(copiedWarmStart[index]))
{
diagnostic = "OSQP warm start values must be finite.";
return false;
}
}
return true;
}
private static T[] Copy<T>(IReadOnlyList<T> values)
{
var copied = new T[values.Count];
for (int index = 0; index < copied.Length; index++)
copied[index] = values[index];
return copied;
}
private static string ReadNativeStatus(OsqpInfo info)
{
byte[] bytes = info.Status ?? new byte[0];
int length = 0;
while (length < bytes.Length && bytes[length] != 0)
length++;
string nativeStatus = Encoding.ASCII.GetString(bytes, 0, length).Trim();
return string.IsNullOrEmpty(nativeStatus) ? "status=" + info.StatusValue : nativeStatus;
}
private static double ToFiniteMetric(double value, string metricName, ref string diagnostic)
{
if (NumericGuard.IsFinite(value))
return value;
diagnostic = string.IsNullOrEmpty(diagnostic)
? "OSQP returned a non-finite " + metricName + "; the planner-neutral diagnostic was normalized to zero."
: diagnostic;
return 0d;
}
private static TimeSpan ToSolveTime(double seconds)
{
if (!NumericGuard.IsFinite(seconds) || seconds <= 0d)
return TimeSpan.Zero;
if (seconds >= TimeSpan.MaxValue.TotalSeconds)
return TimeSpan.MaxValue;
return TimeSpan.FromSeconds(seconds);
}
private static QpSolveResult CreateFailure(QpSolveStatus status, string diagnostic, string nativeStatus = "")
{
return new QpSolveResult(status, new double[0], 0d, 0d, 0d, 0, TimeSpan.Zero, nativeStatus, diagnostic);
}
private sealed class OsqpPinnedArrays : IDisposable
{
private readonly List<GCHandle> handles = new List<GCHandle>();
public IntPtr Pin<T>(T[] values) where T : struct
{
if (values == null || values.Length == 0)
return IntPtr.Zero;
GCHandle handle = GCHandle.Alloc(values, GCHandleType.Pinned);
handles.Add(handle);
return handle.AddrOfPinnedObject();
}
public void Dispose()
{
for (int index = handles.Count - 1; index >= 0; index--)
{
if (handles[index].IsAllocated)
handles[index].Free();
}
handles.Clear();
}
}
}
@@ -0,0 +1,30 @@
namespace MultiWheelC.TrajectoryPlanning.EMPlanner;
internal static class OsqpStatusMapper
{
public static QpSolveStatus Map(int nativeStatusValue)
{
switch (nativeStatusValue)
{
case 1:
return QpSolveStatus.Solved;
case 2:
return QpSolveStatus.SolvedInaccurate;
case 3:
case 4:
return QpSolveStatus.PrimalInfeasible;
case 5:
case 6:
return QpSolveStatus.DualInfeasible;
case 7:
return QpSolveStatus.MaximumIterations;
case 8:
return QpSolveStatus.TimeLimit;
case 9:
case 10:
case 11:
default:
return QpSolveStatus.NativeError;
}
}
}