# EM Planner Lateral LS 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:** Optimize a collision-free, curvature-feasible lateral path inside the selected static corridor using sequential convex programming over OSQP QPs.

**Architecture:** Discretize `l, dl, ddl, dddl` over exact reference-S stations, build normalized quadratic costs and linear integration/corridor constraints, and linearize nonlinear vehicle curvature inside an outer trust-region loop. Reconstruct each accepted candidate in world coordinates, recompute true path arc length, and independently validate it before exposing it to ST.

**Tech Stack:** C# 10, .NET Standard 2.0, foundation Frenet/corridor types, solver-neutral `IQpSolver`, OSQP backend for integration checks.

## Global Constraints

- This plan depends on completion of the foundation and OSQP-backend plans.
- LS runs on exactly one current direction segment and uses `ReferenceS` as its independent variable.
- `l>0` is left of travel for both forward and reverse; do not reinterpret it as body-left in reverse.
- Corridor bounds, maximum lateral offset, trust region, start state, terminal event, Frenet denominator, and vehicle curvature are hard constraints.
- Initial derivative bounds: `|Δl|<=0.05 m` per SQP iteration, `|dl|<=0.50`, `|ddl|<=1.00 1/m`, `|dddl|<=2.00 1/m²`.
- Enforce `1-referenceK*l >= 0.20` at every knot.
- SQP outer-iteration limit is `5`; OSQP iteration limit is `4000`; absolute/relative tolerances are `1e-5`.
- Cost weights: reference `10`, heading `1`, second derivative `5`, third derivative `10`, curvature `5`, curvature variation `20`, previous trajectory `5`, rolling terminal `10`.
- Every cost term is divided by the square of its physical scale before its weight is applied.
- LS scales are maximum lateral offset for L, maximum slope for DL, maximum second derivative for DDL, maximum third derivative for DDDL, vehicle maximum curvature for curvature, and `max(1, reference max |dk/ds|)` for curvature variation.
- Gear-switch and goal terminals require `l=0` and `dl=0`; a rolling safety terminal uses a soft terminal penalty.
- Only the last independently validated feasible candidate may survive a later timeout or failed outer iteration.
- The output world path is re-parameterized by actual `PathS`; later ST code must not use `ReferenceS` as traveled distance.

---

## Locked File Structure

```text
ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/
├── LateralCandidate.cs
├── LateralConstraintBuilder.cs
├── LateralGeometryEvaluator.cs
├── LateralObjectiveBuilder.cs
├── LateralPath.cs
├── LateralPathPoint.cs
├── LateralPlanner.cs
├── LateralPlanningInput.cs
├── LateralPlanningResult.cs
├── LateralSolutionValidator.cs
├── LateralVariableLayout.cs
└── SequentialConvexOptimizer.cs

ClumsyPilot/tests/EMPlannerVerificationHost/
├── FakeQpSolver.cs
├── LateralModelChecks.cs
└── LateralIntegrationChecks.cs
```

## Shared Interfaces

```csharp
public sealed class LateralPlanningInput
{
    public LateralPlanningInput(DirectionSegmentView referenceSegment,
        StaticCorridor corridor, FrenetProjection startProjection,
        EmTerminalType terminalType, VehicleParameters vehicle,
        EmPlannerConfiguration configuration,
        IReadOnlyList<FrenetProjection> previousTrajectorySeed);
}

public sealed class LateralPlanner
{
    public LateralPlanner(IQpSolver qpSolver);
    public LateralPlanningResult Plan(LateralPlanningInput input,
        CancellationToken cancellationToken);
}

public sealed class LateralPathPoint
{
    public double ReferenceS { get; }
    public double PathS { get; }
    public double L { get; }
    public double DL { get; }
    public double DDL { get; }
    public double DDDL { get; }
    public double X { get; }
    public double Y { get; }
    public double VehicleYaw { get; }
    public double GeometricCurvature { get; }
    public double VehicleCurvature { get; }
    public double VehicleCurvatureDerivative { get; }
}
```

### Task 1: Variable Layout and Exact Discrete Lateral Dynamics

**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralVariableLayout.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPlanningInput.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralCandidate.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPathPoint.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPath.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPlanningResult.cs`
- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs`

**Interfaces:**
- Consumes: foundation corridor, reference, configuration, and terminal types.
- Produces: deterministic variable indices and immutable lateral inputs/results.

- [ ] **Step 1: Write failing layout and dynamics checks**

For `N=4`, assert disjoint contiguous ranges for `l[0..3]`, `dl[0..3]`, `ddl[0..3]`, and `dddl[0..2]`, with total variable count `4*N-1`. For unequal S gaps, verify the integration equations:

```text
ddl[i+1] = ddl[i] + ds*dddl[i]
dl[i+1]  = dl[i] + ds*ddl[i] + 0.5*ds^2*dddl[i]
l[i+1]   = l[i] + ds*dl[i] + 0.5*ds^2*ddl[i] + (ds^3/6)*dddl[i]
```

Reject fewer than two stations, non-increasing S, corridor/input station mismatch, and a start projection outside the first hard interval.

- [ ] **Step 2: Run the lateral-model group and verify failure**

```powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-model
```

Expected: build failure naming `LateralVariableLayout`.

- [ ] **Step 3: Implement layouts and immutable model types**

Expose index methods `L(i)`, `DL(i)`, `DDL(i)`, and `DDDL(i)` that range-check every input. Copy all station and seed lists. A failed result has no candidate; success and fallback results require a non-empty independently validated `LateralPath`.

- [ ] **Step 4: Run the model checks**

Expected: `PASS lateral-model`.

- [ ] **Step 5: Commit lateral model types**

```powershell
git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral ClumsyPilot/tests/EMPlannerVerificationHost
git commit -m "feat: add lateral optimization model"
```

### Task 2: Normalized Objective and Linear Hard Constraints

**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralObjectiveBuilder.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralConstraintBuilder.cs`
- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/FakeQpSolver.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs`

**Interfaces:**
- Consumes: Task 1 layout, `SparseTripletBuilder`, corridor bounds, a linearization candidate, and approved LS weights.
- Produces: a validated `QuadraticProgram` for one SQP iteration.

- [ ] **Step 1: Write failing coefficient-level QP checks**

For a three-station straight reference with unit scales, inspect P, q, A, lower, and upper arrays and assert:

```text
reference cost adds 2*w_l to P(l_i,l_i)
jerk cost adds 2*w_dddl to P(dddl_i,dddl_i)
previous-seed cost adds 2*w_previous and -2*w_previous*l_previous
every integration equality appears once with equal lower/upper bounds
corridor, derivative, trust-region, and Frenet-denominator rows use hard finite bounds
gear/goal terminal rows force l_N=0 and dl_N=0
rolling terminal adds objective terms but no zero terminal equalities
```

The test must also show every weight is applied after division by its named scale squared.

- [ ] **Step 2: Run and verify builders are absent**

Expected: build failure naming `LateralObjectiveBuilder`.

- [ ] **Step 3: Implement objective and hard-row assembly**

Build the OSQP objective convention `0.5*x'Px + q'x`, so a squared residual `w*((x-target)/scale)^2` contributes `2w/scale²` to P and `-2w*target/scale²` to q. Assemble integration rows exactly from Task 1. Intersect corridor bounds with maximum offset, trust region, and linearized denominator bounds before adding each L row; return infeasible before calling the solver when an intersection is empty.

Use `FakeQpSolver` only in the verification host. It records the last problem/settings/warm start and returns a caller-supplied `QpSolveResult`.

- [ ] **Step 4: Run coefficient-level checks**

Expected: `PASS lateral-model`; no coefficient comparison tolerance larger than `1e-10`.

- [ ] **Step 5: Commit QP assembly**

```powershell
git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral ClumsyPilot/tests/EMPlannerVerificationHost
git commit -m "feat: assemble lateral LS quadratic programs"
```

### Task 3: Nonlinear Geometry Evaluation and Independent Validation

**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralGeometryEvaluator.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralSolutionValidator.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs`

**Interfaces:**
- Consumes: solved `l/dl/ddl/dddl`, Frenet interpolation, direction, and vehicle curvature limit.
- Produces: world-space `LateralPath` with recomputed `PathS`, curvature, and validation residuals.

- [ ] **Step 1: Write failing reconstruction and curvature checks**

Cover straight and constant-curvature references in both directions. Assert:

```text
world X/Y use x_ref-l*sin(travelYaw), y_ref+l*cos(travelYaw)
vehicle yaw adds PI only for reverse
PathS[0]=0 and increments by actual reconstructed chord/geometry length
PathS is strictly increasing even when ReferenceS gaps vary
VehicleCurvature = directionSign*GeometricCurvature
yawRate identity remains valid for a signed test speed
denominator below 0.20 is rejected
curvature beyond vehicle limit is rejected
non-finite values are rejected
```

- [ ] **Step 2: Run and verify geometry evaluator is absent**

Expected: build failure naming `LateralGeometryEvaluator`.

- [ ] **Step 3: Implement evaluation and strict validation**

Evaluate geometry from the full Frenet derivative formulas used by the design, not a small-angle replacement. Compute unwrapped travel yaw first, derive geometric curvature with respect to actual path direction, convert to vehicle curvature using direction sign, and compute curvature derivative over actual `PathS`. Use centred differences internally and one-sided endpoints.

The validator independently recomputes corridor membership, start/terminal residuals, derivative limits, denominator, curvature limit, finite values, and strictly increasing S. It does not trust solver residuals or reuse the QP constraint matrix as its only proof.

- [ ] **Step 4: Run geometry checks**

Expected: `PASS lateral-model`, including forward/reverse mirrored cases.

- [ ] **Step 5: Commit nonlinear evaluation**

```powershell
git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral ClumsyPilot/tests/EMPlannerVerificationHost/LateralModelChecks.cs
git commit -m "feat: validate lateral path geometry"
```

### Task 4: Sequential Convex Outer Loop and Feasible-Candidate Fallback

**Files:**
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/SequentialConvexOptimizer.cs`
- Create: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral/LateralPlanner.cs`
- Create: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs`
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs`

**Interfaces:**
- Consumes: Tasks 1–3, `IQpSolver`, warm starts, cancellation, and timeout settings.
- Produces: `LateralPlanningResult` with the last strict feasible path or an explicit failure.

- [ ] **Step 1: Write failing SQP state-machine checks with `FakeQpSolver`**

Script solver outcomes and assert:

```text
first solved candidate is validated before becoming fallback
second timeout returns first candidate as SuccessWithFallback
an invalid solved vector never replaces the fallback
SolvedInaccurate requires QP residual <=1e-5 and full lateral validation
trust region is centred on the previous iterate and never exceeds 0.05 m
outer loop stops after at most 5 calls
cancellation before a call returns Cancelled
no feasible candidate plus timeout returns SolverTimedOut with no path
```

- [ ] **Step 2: Run the lateral-integration group and verify failure**

Expected: build failure naming `SequentialConvexOptimizer`.

- [ ] **Step 3: Implement the outer loop**

Initialize from the previous trajectory seed when it covers all stations; otherwise use the corridor-clamped zero-offset seed. Per iteration: linearize geometry, assemble the QP, solve with the remaining time budget, evaluate world geometry, validate independently, store a deep copy if feasible, and test convergence using max absolute L change plus objective improvement. Warm-start the next QP with the complete previous primal vector.

Return the most specific status. A timeout/cancellation after a validated candidate maps to fallback success; infeasible corridor/QP with no candidate maps to lateral infeasible.

- [ ] **Step 4: Run scripted SQP checks**

Expected: `PASS lateral-integration`.

- [ ] **Step 5: Commit SQP orchestration**

```powershell
git add ClumsyPilot/ParkrobTrajplanner/EMPlanner/Lateral ClumsyPilot/tests/EMPlannerVerificationHost
git commit -m "feat: optimize lateral paths with SQP"
```

### Task 5: Real-OSQP Lateral Scenarios and Gate

**Files:**
- Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs`
- Modify: `ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md`

**Interfaces:**
- Consumes: `OsqpNativeSolver`, foundation fixtures, and complete LS pipeline.
- Produces: a verified lateral path contract ready for ST.

- [ ] **Step 1: Add fixed real-solver scenarios**

Run: straight empty map forward, straight empty map reverse, gentle curve, static obstacle narrowing the existing corridor, gear-switch terminal, and rolling terminal. Assert every result is solved or documented fallback, stays in corridor, respects curvature, and ends at the exact ReferenceS anchor.

- [ ] **Step 2: Add determinism and topology assertions**

Run each scenario twice with identical inputs. Compare status, point count, and every numeric output within `1e-10`; assert the obstacle case remains in the seed-connected interval and does not cross to the disconnected side.

- [ ] **Step 3: Run the complete lateral gate**

```powershell
dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- lateral-all
git diff --check
```

Expected output:

```text
PASS lateral-model
PASS lateral-integration
PASS lateral-real-osqp
```

- [ ] **Step 4: Document LS variables, hard constraints, costs, and fallback**

Add the exact equations, normalization scales, terminal differences, `ReferenceS`/`PathS` boundary, and last-feasible publication rule to the README.

- [ ] **Step 5: Commit lateral integration evidence**

```powershell
git add ClumsyPilot/tests/EMPlannerVerificationHost/LateralIntegrationChecks.cs ClumsyPilot/ParkrobTrajplanner/EMPlanner/README.md
git commit -m "test: verify lateral LS scenarios"
```

## Completion Gate

- Coefficient-level tests prove the intended normalized QP, not merely a plausible output path.
- Forward and reverse reconstructed geometry obey the same world-coordinate convention.
- Exact gear/goal terminal L conditions and rolling soft terminal behavior are distinct.
- No candidate outside hard corridor, denominator, derivative, curvature, or boundary constraints is published.
- The published lateral path has actual strictly increasing `PathS` ready for longitudinal optimization.
