Skip to main content

Module crossover

Module crossover 

Source
Expand description

NLP crossover: hand a converged interior-point iterate to the active-set path so the solve ends on an exact active set (Byrd, Nocedal & Waltz, “KNITRO: An Integrated Package for Nonlinear Optimization”, 2006, §7; gh#612).

§Why

An interior-point method never puts an iterate on a constraint: the fraction-to-boundary rule keeps every slack strictly positive, so at termination “which constraints are active” is an inference from a tolerance test, not a fact the solve established. Where strict complementarity holds that inference is right and this whole phase is a no-op by design. Where it fails — a weakly active bound whose slack and multiplier are both O(√μ) — the interior solve cannot answer the question at all, and three subsystems downstream are already paying for that:

  1. [pounce_sensitivity]’s covariance() classifies activity into STRONGLY ACTIVE / WEAKLY ACTIVE / AMBIGUOUS / UNIDENTIFIED (docs/src/sensitivity.md). The AMBIGUOUS class exists precisely because a barrier geometry cannot decide.
  2. A degenerate solution collapses the reduced Hessian, which has come back as an inertia problem repeatedly (#540, #541, #544, #592, the feral_singular_pivot_floor knob) and been met each time on the perturbation side.
  3. The active-set SQP’s warm start could only come from a previous SQP solve (docs/src/active-set-sqp.md), so a sequence whose first solve wants the IPM had no way to hand off. Crossover is that missing edge: after it runs, crate::application::IpoptApplication::last_sqp_working_set returns a working set the next algorithm=active-set-sqp solve can consume.

pounce-convex has had the LP form of this for a while ([pounce_convex::crossover]); this is the NLP analogue, and it borrows that module’s two load-bearing ideas: crossover is a bridge, not a new solver, and it is never-regress — the crossed-over point replaces the interior one only when it is at least as good a KKT point.

§What it does (paper §7)

  1. The IPM terminates at (x, y, z) within E_tol.
  2. Estimate the active set A by a tolerance test on primal distance and multiplier magnitude — crate::sqp::classify_working_set, the same classifier the sensitivity-corrector handoff already uses.
  3. Take one EQP step over A plus a line search on the penalty model. If the result satisfies the stopping tolerances, terminate. This is the common path and it solves no LPs, so on a well-behaved problem crossover costs about one iteration.
  4. Otherwise run the full active-set algorithm from the interior iterate, seeded with A and with ν₀ a little above the largest |multiplier| at the interior solution.

§Where this departs from the paper

KNITRO’s active-set path is SLQP: an LP phase picks the working set and an EQP phase computes the step, so its step 3 is a literal EQP solve and its step 4 sizes an LP trust region (their eq. 7.22) to exclude every inactive constraint. POUNCE’s active-set path is an ordinary line-search SQP over pounce-qp’s working-set interface, so:

  • Step 3 is expressed as one pounce-qp QpSolver::solve_with_working_set against the NLP linearization at the interior iterate, warm-started with A. That call factorizes the hinted active set to recover a primal, then pivots — which is exactly “solve the EQP over A, and fix A where the tolerance test got it wrong”. The paper’s guarantee that step 3 avoids an LP is preserved: pounce-qp solves no LP either.
  • Step 4’s LP trust region has no analogue and is not implemented; the ν₀ half of that setup is, since the ℓ₁ merit the SQP already carries takes exactly that parameter.

§It runs against the declared bounds

The caller hands this an crate::sqp::IpoptNlpAdapter::new_with_declared_bounds, not a plain one, and that is load-bearing rather than tidy. bound_relax_factor (default 1e-8) widens every bound before the interior solve starts, so a point sitting exactly on a bound the user declared is a full 1e-8 inside the relaxed one. Measured against the relaxed bounds, a pivot that lands precisely on the binding constraint reads as strictly interior, and the identification step then correctly reports an empty active set — crossover would run, succeed, and answer nothing. Worse, the pivot itself would stop 1e-8 shy of each constraint, because against the relaxed problem that point genuinely is optimal.

So the whole phase is posed on the model as written. The consequence to be aware of is that the returned point can sit on a declared bound rather than inside it, which is constr_viol_tol-legal by construction (the relaxation is capped there) and is the result being asked for.

§Never-regress

Crossover is a strict refinement of a solve that has already succeeded, so the bar is not “did it solve” but “is this at least as good a KKT point”. accepts applies three gates against the interior iterate — constraint violation, stationarity, and objective — and any failure returns the interior solution untouched. Nothing here can turn a converged solve into a failed one: on every abandonment path the caller keeps what the IPM produced.

§Cost and defaults

Off by default (crossover=no). It runs strictly after convergence, so enabling it moves no interior trajectory and needs no baseline fixture sweep (contrast an initial-point or merit-function change, per CLAUDE.md).

Structs§

CrossoverOptions
Tuning for the crossover phase. Populated from the crossover* options by crate::application::IpoptApplication.
CrossoverReport
What crossover did. Retrieved from crate::application::IpoptApplication::crossover_report.
CrossoverSeed
The converged interior-point iterate, in the algorithm’s (compressed, scaled) space — the same space crate::sqp::IpoptNlpAdapter presents, so no translation is needed between the two engines.

Enums§

CrossoverDecline
Why crossover did not replace the interior iterate. Reported rather than swallowed: “crossover ran and declined” and “crossover never ran” are different facts about a solve, and the AMBIGUOUS-activity consumers this exists for need to tell them apart.
CrossoverPhase
Which of the paper’s two paths produced the returned point.

Functions§

accepts
Never-regress gate. The crossed-over point is accepted only when it is at least as good a KKT point of the original NLP as the interior iterate, on all three of feasibility, stationarity, and objective.
run
Run the crossover phase (paper §7 steps 2-4).