using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; namespace MultiWheelC.TrajectoryPlanning.EMPlanner; internal static class OsqpChecks { public static void Run() { var pluginDirectories = new List(); try { string missingNativeDirectory = CreatePluginDirectory(); pluginDirectories.Add(missingNativeDirectory); ProbeResult missingNative = RunProbe(missingNativeDirectory); EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.SolverUnavailable, missingNative.Status, "missing native status"); EMPlannerVerificationHost.Verification.True(!string.IsNullOrEmpty(missingNative.Diagnostic), "missing native diagnostic"); string corruptNativeDirectory = CreatePluginDirectory(); pluginDirectories.Add(corruptNativeDirectory); File.WriteAllText(Path.Combine(corruptNativeDirectory, "osqp.dll"), "not a native library"); ProbeResult corruptNative = RunProbe(corruptNativeDirectory); EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.SolverUnavailable, corruptNative.Status, "corrupt native status"); EMPlannerVerificationHost.Verification.True(!string.IsNullOrEmpty(corruptNative.Diagnostic), "corrupt native diagnostic"); EMPlannerVerificationHost.Verification.True(corruptNative.RawOutput.IndexOf("BadImageFormatException", StringComparison.Ordinal) < 0, "corrupt native does not escape BadImageFormatException"); string realNativeDirectory = CreatePluginDirectory(); pluginDirectories.Add(realNativeDirectory); string realNativePath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", "ThirdParty", "OSQP", "win-x64", "osqp.dll")); EMPlannerVerificationHost.Verification.True(File.Exists(realNativePath), "pinned native package exists"); File.Copy(realNativePath, Path.Combine(realNativeDirectory, "osqp.dll"), false); ProbeResult realNative = RunProbe(realNativeDirectory); EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.Solved, realNative.Status, "real native status"); EMPlannerVerificationHost.Verification.Equal("1.0.0", realNative.Version, "real native version"); EMPlannerVerificationHost.Verification.True(realNative.ModuleHandle != IntPtr.Zero, "real native module handle"); EMPlannerVerificationHost.Verification.Equal(true, realNative.ConcurrentHandleStable, "concurrent native module handle"); EMPlannerVerificationHost.Verification.Equal(true, realNative.SolveChecksPassed, "native solve checks"); Console.WriteLine("PASS osqp-solve"); } finally { for (int index = 0; index < pluginDirectories.Count; index++) { if (Directory.Exists(pluginDirectories[index])) Directory.Delete(pluginDirectories[index], true); } } } public static void RunProbe() { var results = new OsqpNativeLoadResult[16]; Parallel.For(0, results.Length, index => results[index] = OsqpNativeLoader.Load()); OsqpNativeLoadResult first = results[0]; bool sameHandle = true; for (int index = 1; index < results.Length; index++) { sameHandle = sameHandle && results[index].Status == first.Status && results[index].ModuleHandle == first.ModuleHandle; } Console.WriteLine("STATUS=" + first.Status); Console.WriteLine("VERSION=" + first.Version); Console.WriteLine("HANDLE=" + first.ModuleHandle.ToInt64()); Console.WriteLine("DIAGNOSTIC=" + first.Diagnostic.Replace('\r', ' ').Replace('\n', ' ')); Console.WriteLine("CONCURRENT_HANDLE_STABLE=" + sameHandle); if (first.Status == QpSolveStatus.Solved) { VerifyNativeSolveLifecycle(); Console.WriteLine("SOLVE_CHECKS=PASS"); } } private static string CreatePluginDirectory() { string sourceDirectory = AppContext.BaseDirectory; string destinationDirectory = Path.Combine(Path.GetTempPath(), "em-planner-osqp-loader-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(destinationDirectory); string[] hostFiles = Directory.GetFiles(sourceDirectory); for (int index = 0; index < hostFiles.Length; index++) { if (string.Equals(Path.GetFileName(hostFiles[index]), "osqp.dll", StringComparison.OrdinalIgnoreCase)) continue; string destinationPath = Path.Combine(destinationDirectory, Path.GetFileName(hostFiles[index])); File.Copy(hostFiles[index], destinationPath, false); } EMPlannerVerificationHost.Verification.True(File.Exists(Path.Combine(destinationDirectory, "ClumsyPilot.dll")), "copied plugin ClumsyPilot.dll"); return destinationDirectory; } private static void VerifyNativeSolveLifecycle() { var solver = new OsqpNativeSolver(); QpSolveResult bounded = solver.Solve( CreateBoundedOptimumProblem(), CreateSettings(TimeSpan.FromMilliseconds(100)), new[] { 0d }, CancellationToken.None); EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.Solved, bounded.Status, "bounded optimum status"); AssertPopulatedResult(bounded, "bounded optimum"); EMPlannerVerificationHost.Verification.Equal(1, bounded.Primal.Count, "bounded optimum primal count"); AssertClose(1d, bounded.Primal[0], 1e-5d, "bounded optimum primal"); AssertClose(-1.5d, bounded.Objective, 1e-5d, "bounded optimum objective"); EMPlannerVerificationHost.Verification.True(bounded.PrimalResidual <= 1e-5d, "bounded optimum primal residual"); EMPlannerVerificationHost.Verification.True(bounded.DualResidual <= 1e-5d, "bounded optimum dual residual"); QpSolveResult equality = solver.Solve( CreateEqualityOptimumProblem(), CreateSettings(TimeSpan.FromMilliseconds(100)), new[] { 0.5d, 0.5d }, CancellationToken.None); EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.Solved, equality.Status, "equality optimum status"); AssertPopulatedResult(equality, "equality optimum"); EMPlannerVerificationHost.Verification.Equal(2, equality.Primal.Count, "equality optimum primal count"); AssertClose(0.5d, equality.Primal[0], 1e-5d, "equality optimum first primal"); AssertClose(0.5d, equality.Primal[1], 1e-5d, "equality optimum second primal"); AssertClose(0.5d, equality.Objective, 1e-5d, "equality optimum objective"); EMPlannerVerificationHost.Verification.True(equality.PrimalResidual <= 1e-5d, "equality optimum primal residual"); EMPlannerVerificationHost.Verification.True(equality.DualResidual <= 1e-5d, "equality optimum dual residual"); QpSolveResult infeasible = solver.Solve( CreateInfeasibleProblem(), CreateSettings(TimeSpan.FromMilliseconds(100)), null, CancellationToken.None); EMPlannerVerificationHost.Verification.Equal(QpSolveStatus.PrimalInfeasible, infeasible.Status, "infeasible status"); AssertPopulatedResult(infeasible, "infeasible"); QpSolveResult tinyTimeLimit = solver.Solve( CreateEqualityOptimumProblem(), CreateSettings(TimeSpan.FromTicks(1)), null, CancellationToken.None); EMPlannerVerificationHost.Verification.True( tinyTimeLimit.Status == QpSolveStatus.TimeLimit || tinyTimeLimit.Status == QpSolveStatus.Solved || tinyTimeLimit.Status == QpSolveStatus.SolvedInaccurate, "tiny time-limit status maps to a time limit or solved state"); AssertPopulatedResult(tinyTimeLimit, "tiny time-limit"); } private static QuadraticProgram CreateBoundedOptimumProblem() { var hessian = new SparseTripletBuilder(1, 1, true); hessian.Add(0, 0, 1d); var constraints = new SparseTripletBuilder(1, 1); constraints.Add(0, 0, 1d); return new QuadraticProgram(hessian.Build(), new[] { -2d }, constraints.Build(), new[] { 0d }, new[] { 1d }); } private static QuadraticProgram CreateEqualityOptimumProblem() { var hessian = new SparseTripletBuilder(2, 2, true); hessian.Add(0, 0, 2d); hessian.Add(1, 1, 2d); var constraints = new SparseTripletBuilder(1, 2); constraints.Add(0, 0, 1d); constraints.Add(0, 1, 1d); return new QuadraticProgram(hessian.Build(), new[] { 0d, 0d }, constraints.Build(), new[] { 1d }, new[] { 1d }); } private static QuadraticProgram CreateInfeasibleProblem() { var hessian = new SparseTripletBuilder(1, 1, true); hessian.Add(0, 0, 1d); var constraints = new SparseTripletBuilder(2, 1); constraints.Add(0, 0, 1d); constraints.Add(1, 0, 1d); return new QuadraticProgram( hessian.Build(), new[] { 0d }, constraints.Build(), new[] { 1d, -QuadraticProgram.MaximumFiniteBound }, new[] { QuadraticProgram.MaximumFiniteBound, 0d }); } private static QpSolverSettings CreateSettings(TimeSpan timeLimit) { return new QpSolverSettings(4000, 1e-6d, 1e-6d, timeLimit, true, true, false); } private static void AssertPopulatedResult(QpSolveResult result, string name) { EMPlannerVerificationHost.Verification.True(result.Iterations >= 0, name + " iterations"); EMPlannerVerificationHost.Verification.True(result.SolveTime >= TimeSpan.Zero, name + " solve time"); EMPlannerVerificationHost.Verification.True(!double.IsNaN(result.Objective) && !double.IsInfinity(result.Objective), name + " objective"); EMPlannerVerificationHost.Verification.True(!double.IsNaN(result.PrimalResidual) && !double.IsInfinity(result.PrimalResidual), name + " primal residual"); EMPlannerVerificationHost.Verification.True(!double.IsNaN(result.DualResidual) && !double.IsInfinity(result.DualResidual), name + " dual residual"); EMPlannerVerificationHost.Verification.True(!string.IsNullOrWhiteSpace(result.NativeStatus), name + " native status"); } private static void AssertClose(double expected, double actual, double tolerance, string name) { EMPlannerVerificationHost.Verification.True(Math.Abs(expected - actual) <= tolerance, name + " expected " + expected + " but was " + actual); } private static ProbeResult RunProbe(string pluginDirectory) { var startInfo = new ProcessStartInfo { FileName = Path.Combine(pluginDirectory, "EMPlannerVerificationHost.exe"), Arguments = "osqp-probe", WorkingDirectory = pluginDirectory, UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, }; using (var process = new Process { StartInfo = startInfo }) { process.Start(); string standardOutput = process.StandardOutput.ReadToEnd(); string standardError = process.StandardError.ReadToEnd(); process.WaitForExit(); if (process.ExitCode != 0) throw new InvalidOperationException("OSQP loader probe exited " + process.ExitCode + ": " + standardError + standardOutput); return ProbeResult.Parse(standardOutput + standardError); } } private sealed class ProbeResult { private ProbeResult(QpSolveStatus status, string version, IntPtr moduleHandle, string diagnostic, bool concurrentHandleStable, bool solveChecksPassed, string rawOutput) { Status = status; Version = version; ModuleHandle = moduleHandle; Diagnostic = diagnostic; ConcurrentHandleStable = concurrentHandleStable; SolveChecksPassed = solveChecksPassed; RawOutput = rawOutput; } public QpSolveStatus Status { get; } public string Version { get; } public IntPtr ModuleHandle { get; } public string Diagnostic { get; } public bool ConcurrentHandleStable { get; } public bool SolveChecksPassed { get; } public string RawOutput { get; } public static ProbeResult Parse(string output) { string statusText = ReadValue(output, "STATUS="); string version = ReadValue(output, "VERSION="); string handleText = ReadValue(output, "HANDLE="); string diagnostic = ReadValue(output, "DIAGNOSTIC="); string stableText = ReadValue(output, "CONCURRENT_HANDLE_STABLE="); string solveChecksText = ReadOptionalValue(output, "SOLVE_CHECKS="); QpSolveStatus status; if (!Enum.TryParse(statusText, out status)) throw new InvalidOperationException("Loader probe reported an invalid status: " + statusText); long handleValue; if (!long.TryParse(handleText, out handleValue)) throw new InvalidOperationException("Loader probe reported an invalid handle: " + handleText); bool concurrentHandleStable; if (!bool.TryParse(stableText, out concurrentHandleStable)) throw new InvalidOperationException("Loader probe reported an invalid concurrency flag: " + stableText); return new ProbeResult(status, version, new IntPtr(handleValue), diagnostic, concurrentHandleStable, string.Equals(solveChecksText, "PASS", StringComparison.Ordinal), output); } private static string ReadValue(string output, string prefix) { string[] lines = output.Replace("\r", string.Empty).Split('\n'); for (int index = 0; index < lines.Length; index++) { if (lines[index].StartsWith(prefix, StringComparison.Ordinal)) return lines[index].Substring(prefix.Length); } throw new InvalidOperationException("Loader probe did not report " + prefix + ". Output: " + output); } private static string ReadOptionalValue(string output, string prefix) { string[] lines = output.Replace("\r", string.Empty).Split('\n'); for (int index = 0; index < lines.Length; index++) { if (lines[index].StartsWith(prefix, StringComparison.Ordinal)) return lines[index].Substring(prefix.Length); } return string.Empty; } } }