383 lines
16 KiB
Markdown
383 lines
16 KiB
Markdown
# EM Planner OSQP Backend Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Provide a solver-independent convex-QP contract and a pinned Windows x64 OSQP 1.0.0 backend that loads `osqp.dll` safely from the plugin directory.
|
|
|
|
**Architecture:** Mathematical planners build validated immutable CSC problems against `IQpSolver`; the OSQP adapter owns all native memory and maps native outcomes into planner-neutral statuses. The upstream shared library is built with a fixed ABI configuration, preloaded by absolute path, version-checked, and never allowed to crash the host when absent or incompatible.
|
|
|
|
**Tech Stack:** C# 10, .NET Standard 2.0, P/Invoke with Cdecl, OSQP 1.0.0 C API, builtin QDLDL algebra, CMake 3.18+, Visual Studio x64 compiler.
|
|
|
|
## Global Constraints
|
|
|
|
- This plan depends on `2026-08-03-em-planner-foundation-implementation.md` Task 1 and its verification host.
|
|
- Pin upstream source tag `v1.0.0`; do not bind the incompatible 0.6 API.
|
|
- Build Windows x64, double precision, 32-bit indices, unpacked settings, builtin algebra, shared library, no MKL or CUDA.
|
|
- Required build switches: `OSQP_USE_FLOAT=OFF`, `OSQP_USE_LONG=OFF`, `OSQP_PACK_SETTINGS=OFF`, `OSQP_ALGEBRA_BACKEND=builtin`, `OSQP_BUILD_SHARED_LIB=ON`.
|
|
- Native library filename in source and deployed plugin is exactly `osqp.dll`.
|
|
- `ClumsyPilot.dll` locates and preloads the sibling DLL from `Assembly.Location`; current directory and system `PATH` are not inputs.
|
|
- Every native entry point uses `CallingConvention.Cdecl`.
|
|
- All pinned arrays, CSC wrappers, settings, and solver handles are released in reverse acquisition order.
|
|
- `SolvedInaccurate` is publishable only after independent strict residual and domain validation.
|
|
- Missing DLL, wrong architecture, version mismatch, invalid exports, and setup failure return structured solver outcomes.
|
|
- Native verbose output is disabled.
|
|
|
|
---
|
|
|
|
## Locked File Structure
|
|
|
|
```text
|
|
ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/
|
|
├── IQpSolver.cs
|
|
├── QpSolveResult.cs
|
|
├── QpSolveStatus.cs
|
|
├── QpSolverSettings.cs
|
|
├── QuadraticProgram.cs
|
|
├── SparseCscMatrix.cs
|
|
├── SparseTripletBuilder.cs
|
|
└── Osqp/
|
|
├── OsqpNativeLoader.cs
|
|
├── OsqpNativeMethods.cs
|
|
├── OsqpNativeSolver.cs
|
|
├── OsqpNativeStructures.cs
|
|
└── OsqpStatusMapper.cs
|
|
|
|
ClumsyPilot/ThirdParty/OSQP/
|
|
├── build-win-x64.ps1
|
|
├── LICENSE
|
|
├── NOTICE
|
|
├── VERSION
|
|
├── SHA256SUMS
|
|
└── win-x64/osqp.dll
|
|
|
|
ClumsyPilot/tests/EMPlannerVerificationHost/
|
|
├── OptimizationChecks.cs
|
|
└── OsqpChecks.cs
|
|
```
|
|
|
|
## Shared Interfaces
|
|
|
|
```csharp
|
|
public interface IQpSolver
|
|
{
|
|
QpSolveResult Solve(QuadraticProgram problem, QpSolverSettings settings,
|
|
IReadOnlyList<double> warmStart, CancellationToken cancellationToken);
|
|
}
|
|
|
|
public sealed class QuadraticProgram
|
|
{
|
|
public QuadraticProgram(SparseCscMatrix upperTriangularP, IReadOnlyList<double> q,
|
|
SparseCscMatrix a, IReadOnlyList<double> lowerBounds,
|
|
IReadOnlyList<double> upperBounds);
|
|
public int VariableCount { get; }
|
|
public int ConstraintCount { get; }
|
|
}
|
|
|
|
public sealed class QpSolveResult
|
|
{
|
|
public QpSolveStatus Status { get; }
|
|
public IReadOnlyList<double> Primal { get; }
|
|
public double Objective { get; }
|
|
public double PrimalResidual { get; }
|
|
public double DualResidual { get; }
|
|
public int Iterations { get; }
|
|
public TimeSpan SolveTime { get; }
|
|
public string NativeStatus { get; }
|
|
public string Diagnostic { get; }
|
|
}
|
|
```
|
|
|
|
Official references used to lock this ABI:
|
|
|
|
- `https://osqp.org/docs/interfaces/C.html`
|
|
- `https://osqp.org/docs/get_started/migration_guide.html`
|
|
- `https://github.com/osqp/osqp/tree/v1.0.0`
|
|
|
|
### Task 1: Solver-Neutral Sparse QP Contracts
|
|
|
|
**Files:**
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/IQpSolver.cs`
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QpSolveResult.cs`
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QpSolveStatus.cs`
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QpSolverSettings.cs`
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/QuadraticProgram.cs`
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/SparseCscMatrix.cs`
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/SparseTripletBuilder.cs`
|
|
- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/OptimizationChecks.cs`
|
|
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `NumericGuard` and cancellation tokens.
|
|
- Produces: the shared interfaces above and deterministic sparse-matrix assembly used by LS and ST.
|
|
|
|
- [ ] **Step 1: Write failing CSC canonicalization checks**
|
|
|
|
Build triplets in shuffled order with duplicate coordinates and assert the resulting CSC matrix:
|
|
|
|
```text
|
|
has ColumnPointers length ColumnCount+1
|
|
sorts row indices ascending inside each column
|
|
sums duplicate coordinates
|
|
drops exact zero sums
|
|
rejects NaN, infinity, negative indices, and out-of-range indices
|
|
stores only the upper triangle for P
|
|
```
|
|
|
|
Also construct the micro problem `min 0.5*x^2 - 2*x` subject to `0 <= x <= 1` and assert its immutable arrays cannot be changed through the source lists.
|
|
|
|
- [ ] **Step 2: Run and verify solver contracts are absent**
|
|
|
|
```powershell
|
|
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- optimization
|
|
```
|
|
|
|
Expected: build failure naming `SparseTripletBuilder`.
|
|
|
|
- [ ] **Step 3: Implement canonical CSC and QP validation**
|
|
|
|
`SparseCscMatrix` stores copied arrays `Values`, `RowIndices`, and `ColumnPointers`. Validate monotonic pointers, `ColumnPointers[0]==0`, final pointer equals nonzero count, and all rows are in range. `QuadraticProgram` enforces square P, matching variable dimensions, matching constraint dimensions, `lower<=upper`, finite coefficients, and bounds limited to `±1e30` rather than CLR infinity.
|
|
|
|
Use these exact statuses:
|
|
|
|
```csharp
|
|
public enum QpSolveStatus
|
|
{
|
|
Solved,
|
|
SolvedInaccurate,
|
|
PrimalInfeasible,
|
|
DualInfeasible,
|
|
MaximumIterations,
|
|
TimeLimit,
|
|
Cancelled,
|
|
SolverUnavailable,
|
|
InvalidProblem,
|
|
NativeError
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run optimization checks**
|
|
|
|
Expected: `PASS optimization`.
|
|
|
|
- [ ] **Step 5: Commit QP contracts**
|
|
|
|
```powershell
|
|
git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization ClumsyPilot/tests/EMPlannerVerificationHost
|
|
git commit -m "feat: add solver-neutral QP contracts"
|
|
```
|
|
|
|
### Task 2: Reproducible OSQP 1.0.0 Native Package
|
|
|
|
**Files:**
|
|
- Create: all files under `ClumsyPilot/ThirdParty/OSQP/`
|
|
|
|
**Interfaces:**
|
|
- Consumes: Git, CMake 3.18+, and a Visual Studio x64 compiler.
|
|
- Produces: a versioned `win-x64/osqp.dll` with a recorded SHA-256 and matching license files.
|
|
|
|
- [ ] **Step 1: Write the native build script**
|
|
|
|
`build-win-x64.ps1` must create a unique temporary directory, clone only tag `v1.0.0`, configure with this exact command shape, and remove the temporary directory in `finally`:
|
|
|
|
```powershell
|
|
cmake -S $sourceRoot -B $buildRoot -A x64 `
|
|
-DOSQP_ALGEBRA_BACKEND=builtin `
|
|
-DOSQP_BUILD_SHARED_LIB=ON `
|
|
-DOSQP_BUILD_STATIC_LIB=OFF `
|
|
-DOSQP_BUILD_DEMO_EXE=OFF `
|
|
-DOSQP_BUILD_UNITTESTS=OFF `
|
|
-DOSQP_USE_FLOAT=OFF `
|
|
-DOSQP_USE_LONG=OFF `
|
|
-DOSQP_PACK_SETTINGS=OFF `
|
|
-DOSQP_ENABLE_PRINTING=OFF `
|
|
-DOSQP_CODEGEN=OFF `
|
|
-DOSQP_ENABLE_DERIVATIVES=OFF
|
|
cmake --build $buildRoot --config Release --target osqp
|
|
```
|
|
|
|
The script resolves the generated DLL explicitly, verifies exactly one match, copies upstream `LICENSE` and `NOTICE`, writes `VERSION` with tag and build flags, computes `Get-FileHash -Algorithm SHA256`, and writes `SHA256SUMS` using a lowercase hexadecimal digest.
|
|
|
|
- [ ] **Step 2: Execute the build script**
|
|
|
|
```powershell
|
|
powershell -ExecutionPolicy Bypass -File ClumsyPilot/ThirdParty/OSQP/build-win-x64.ps1
|
|
```
|
|
|
|
Expected: `win-x64/osqp.dll`, `LICENSE`, `NOTICE`, `VERSION`, and `SHA256SUMS` exist; the script prints `OSQP v1.0.0 win-x64 package ready`.
|
|
|
|
- [ ] **Step 3: Verify architecture, exports, and hash**
|
|
|
|
Use `dumpbin /headers` to assert machine `x64`, `dumpbin /exports` to assert `osqp_version`, `osqp_setup`, `osqp_solve`, and `osqp_cleanup`, then recompute SHA-256 and compare with `SHA256SUMS`. A missing tool is a failed packaging gate, not a skipped check.
|
|
|
|
- [ ] **Step 4: Verify license contents came from the pinned tag**
|
|
|
|
Compare bytes against the tag checkout before the temporary checkout is removed. Expected: exact equality for both files.
|
|
|
|
- [ ] **Step 5: Commit the reproducible native package**
|
|
|
|
```powershell
|
|
git add ClumsyPilot/ThirdParty/OSQP
|
|
git commit -m "build: pin OSQP 1.0.0 win-x64"
|
|
```
|
|
|
|
### Task 3: Absolute-Path Native Loader and ABI Structures
|
|
|
|
**Files:**
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp/OsqpNativeLoader.cs`
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp/OsqpNativeMethods.cs`
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp/OsqpNativeStructures.cs`
|
|
- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/OsqpChecks.cs`
|
|
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs`
|
|
|
|
**Interfaces:**
|
|
- Consumes: the pinned DLL and Task 1 QP contracts.
|
|
- Produces: a process-lifetime loader plus exact OSQP 1.0.0 double/int32 structures.
|
|
|
|
- [ ] **Step 1: Write failing loader checks**
|
|
|
|
Copy the verification host output to a temporary `plugins` directory with `ClumsyPilot.dll` and, in separate cases, no native DLL, a text file named `osqp.dll`, and the real DLL. Assert the first two return `SolverUnavailable` diagnostics without `BadImageFormatException` escaping; the real DLL reports version `1.0.0`. Start 16 parallel first-use calls and assert a single stable module handle.
|
|
|
|
- [ ] **Step 2: Run the OSQP group without loader implementation**
|
|
|
|
Expected: build failure naming `OsqpNativeLoader`.
|
|
|
|
- [ ] **Step 3: Implement loader and ABI definitions**
|
|
|
|
Use Windows `LoadLibraryW`, `GetProcAddress`, and `FreeLibrary` from `kernel32`; resolve the plugin directory from `typeof(OsqpNativeLoader).Assembly.Location`. Reject `IntPtr.Size != 8`. Preload the absolute sibling path and retain the handle for process lifetime.
|
|
|
|
Define `OSQPInt` as C# `int` and `OSQPFloat` as C# `double`, matching the pinned build. Define sequential layouts for `OSQPCscMatrix`, `OSQPSettings`, `OSQPInfo`, `OSQPSolution`, and the four-pointer prefix of `OSQPSolver` exactly as the v1.0.0 public headers specify. Add an internal layout check for expected offsets and total sizes before the first solve.
|
|
|
|
Declare only these native functions initially:
|
|
|
|
```text
|
|
osqp_version
|
|
osqp_set_default_settings
|
|
osqp_setup
|
|
osqp_warm_start
|
|
osqp_solve
|
|
osqp_cleanup
|
|
```
|
|
|
|
Do not depend on `OSQPCscMatrix_new`, `OSQPCscMatrix_free`, `OSQPSettings_new`, or `OSQPSettings_free`: those helpers are not marked with the public export macro in the pinned header. Allocate the two CSC structures and settings block with `Marshal.AllocHGlobal`, initialize settings through `osqp_set_default_settings`, and release those managed-owned blocks with `Marshal.FreeHGlobal`.
|
|
|
|
- [ ] **Step 4: Run missing, corrupt, real, and concurrent loader checks**
|
|
|
|
Expected: `PASS osqp-loader` and no process crash.
|
|
|
|
- [ ] **Step 5: Commit loader and structures**
|
|
|
|
```powershell
|
|
git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp ClumsyPilot/tests/EMPlannerVerificationHost
|
|
git commit -m "feat: load pinned OSQP native library"
|
|
```
|
|
|
|
### Task 4: OSQP Solve Lifecycle and Status Mapping
|
|
|
|
**Files:**
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp/OsqpNativeSolver.cs`
|
|
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp/OsqpStatusMapper.cs`
|
|
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/OsqpChecks.cs`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `IQpSolver`, Task 3 native functions, and `QpSolverSettings`.
|
|
- Produces: `OsqpNativeSolver : IQpSolver`.
|
|
|
|
- [ ] **Step 1: Write failing solve/status checks**
|
|
|
|
Test three fixed QPs:
|
|
|
|
```text
|
|
bounded optimum: min 0.5*x^2 - 2*x, 0<=x<=1, expected x=1
|
|
equality optimum: min x^2+y^2, x+y=1, expected x=y=0.5
|
|
infeasible: x>=1 and x<=0, expected PrimalInfeasible
|
|
```
|
|
|
|
Assert residuals, iteration count, objective, native status, and solve time are populated. Add a `1e-9 second` time-limit case that maps only to `TimeLimit` or a valid solved status; no native status may be silently treated as solved.
|
|
|
|
- [ ] **Step 2: Run and verify `OsqpNativeSolver` is absent**
|
|
|
|
Expected: build failure naming `OsqpNativeSolver`.
|
|
|
|
- [ ] **Step 3: Implement one-shot native ownership**
|
|
|
|
Pin P/Q/A/L/U and optional warm-start arrays; allocate and populate P/A `OSQPCscMatrix` blocks; allocate settings and initialize it through `osqp_set_default_settings`; overwrite `verbose=0`, `warm_starting`, `polishing`, `max_iter`, `eps_abs`, `eps_rel`, and `time_limit`; call setup, optional warm start, solve, then marshal solution and info. Copy all result values before cleanup. Release the solver through `osqp_cleanup`, then settings/matrix blocks through `Marshal.FreeHGlobal`, then array pins in reverse order inside `finally`.
|
|
|
|
Map native status values exactly:
|
|
|
|
```text
|
|
1 Solved
|
|
2 SolvedInaccurate
|
|
3/4 PrimalInfeasible
|
|
5/6 DualInfeasible
|
|
7 MaximumIterations
|
|
8 TimeLimit
|
|
9/10/11 NativeError
|
|
```
|
|
|
|
Cancellation is checked before native setup and after solve. OSQP's configured time limit is the bound for a solve already inside native code.
|
|
|
|
- [ ] **Step 4: Run all OSQP checks repeatedly**
|
|
|
|
```powershell
|
|
1..20 | ForEach-Object {
|
|
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- osqp
|
|
if ($LASTEXITCODE -ne 0) { throw "OSQP verification failed on iteration $_" }
|
|
}
|
|
```
|
|
|
|
Expected: every iteration prints `PASS osqp-loader` and `PASS osqp-solve`.
|
|
|
|
- [ ] **Step 5: Commit solver lifecycle**
|
|
|
|
```powershell
|
|
git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Optimization/Osqp ClumsyPilot/tests/EMPlannerVerificationHost/OsqpChecks.cs
|
|
git commit -m "feat: solve QPs through OSQP"
|
|
```
|
|
|
|
### Task 5: Backend Completion Gate
|
|
|
|
**Files:**
|
|
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md`
|
|
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs`
|
|
|
|
**Interfaces:**
|
|
- Consumes: all tasks in this plan.
|
|
- Produces: the stable `IQpSolver` boundary required by LS and ST plans.
|
|
|
|
- [ ] **Step 1: Document native deployment and diagnostics**
|
|
|
|
Add the exact source/deployment layouts, pinned version, build flags, license placement, absolute loading rule, and solver status mapping to the README.
|
|
|
|
- [ ] **Step 2: Run optimization and OSQP gates**
|
|
|
|
```powershell
|
|
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- optimization
|
|
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- osqp
|
|
git diff --check
|
|
```
|
|
|
|
Expected: both groups pass and Git reports no whitespace errors.
|
|
|
|
- [ ] **Step 3: Verify the DLL is self-contained**
|
|
|
|
Run a dependency inspection on `win-x64/osqp.dll`. Expected: only Windows system/runtime DLLs; no MKL, CUDA, or separately deployed QDLDL DLL.
|
|
|
|
- [ ] **Step 4: Verify clean plugin-directory loading**
|
|
|
|
Copy only `ClumsyPilot.dll` and `osqp.dll` to a fresh directory, copy the verification host executable beside them, and run the micro QP with the working directory set elsewhere. Expected: solved result, proving loading does not depend on current directory.
|
|
|
|
- [ ] **Step 5: Commit backend documentation**
|
|
|
|
```powershell
|
|
git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs
|
|
git commit -m "docs: describe OSQP plugin deployment"
|
|
```
|
|
|
|
## Completion Gate
|
|
|
|
- `IQpSolver` contains no OSQP-specific type.
|
|
- Pinned native metadata, license, notice, hash, and DLL agree with OSQP v1.0.0.
|
|
- Loader failures are structured and never terminate the host.
|
|
- Fixed feasible and infeasible QPs map to the correct statuses with finite diagnostics.
|
|
- Twenty repeated solve/cleanup cycles pass without handle growth or access violations.
|