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 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 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(IReadOnlyList 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 handles = new List(); public IntPtr Pin(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(); } } }