mod warm_lbfgs

module warm_lbfgs

Quasi-Newton relaxation whose curvature persists between calls.

Handle around rgmin::Lbfgs. Limited-memory quasi-Newton relaxation whose curvature survives between calls.

The solver is rgmin::Lbfgs: L-BFGS two-loop recursion (Nocedal-Wright 7.4, scaling 7.20) with the strong Wolfe conditions (algorithms 3.5 and 3.6). This type is the hopping-chain handle: it keeps the pair history across relaxations, exposes forget when the structure changes, and offers a watch hook for screening predictors.

Measured status

On a 75-point Lennard-Jones cluster, 400 relaxations from perturbed minima with overlapping pairs repaired first, against SciPy’s L-BFGS-B on the identical protocol:

arm

evals per relaxation

worst final |g|

cold

386.1

1.45e-5

warm

386.1

1.45e-5

L-BFGS-B

273

1.43e-5

Both arms converge and cost 1.4 times a production minimiser. The deepest structure found over the 400 was -388.26 here against -386.84 for the reference, which is a property of which basins the perturbations landed in rather than of the minimisers.

The retained curvature does not survive, and the name is now vestigial

Warm and cold agree to the last digit because the memory is empty at the end of every relaxation: 0 of 400 started with stored curvature. A relaxation terminates by line-search failure at a gradient near 1e-5, short of the 1e-6 tolerance, and that path discards the memory before returning. So the premise this type was built on, that curvature at a new start resembles curvature at the old minimum and is worth carrying, is not something this implementation tests. It is a correct L-BFGS whose warm start never engages.

Two facts from building it do transfer. Backtracking that enforces only sufficient decrease produces curvature pairs describing curvature never measured, and since every direction is built from the stored pairs, one bad pair degrades the whole memory: with Armijo alone, retaining curvature was 1.8 times worse than discarding it, and the strong Wolfe conditions reverse the sign. And a relaxation benchmark has to check the final gradient, since an unrepaired overlap returns a gradient near 1e13 that no line search recovers from, making a cost comparison count failures instead of work.

Structs and Unions

struct WarmLbfgs

L-BFGS with memory that persists across relaxations.

Delegates the two-loop map and strong Wolfe search to rgmin::Lbfgs. Public fields are copied onto the inner solver at the start of each WarmLbfgs::minimize call.

max_pairs: usize

Pairs retained; the usual choice is between five and ten.

gtol: f64

Gradient infinity norm below which a relaxation is converged.

armijo: f64

Armijo sufficient-decrease constant, c1 in the Wolfe conditions.

curvature: f64

Curvature constant, c2. The usual choice for quasi-Newton is 0.9.

max_line_evals: usize

Line-search evaluations attempted before the direction is abandoned.

Implementations

impl WarmLbfgs

Functions

fn forget(&mut self)

Discards the stored curvature.

Called when the chain moves somewhere structurally different, where the retained pairs describe a Hessian that no longer applies.

fn is_empty(&self) -> bool

True when no curvature is stored.

fn len(&self) -> usize

Pairs currently held.

fn minimize<F>(&mut self, x0: ArrayView1<f64>, max_iter: usize, fg: F) -> (f64, Array1<f64>, usize)
where
    F: FnMut(ArrayView1<f64>) -> Option<(f64, Array1<f64>)>

Relaxes x0, calling fg for value and gradient.

fg returns None when the caller’s budget is spent, which ends the relaxation where it stands. Returns the value, the point, and the number of evaluations used.

fn minimize_recognized<F, R>(&mut self, x0: ArrayView1<f64>, max_iter: usize, fg: F, recognise: R) -> (f64, Array1<f64>, usize, bool)
where
    F: FnMut(ArrayView1<f64>) -> Option<(f64, Array1<f64>)>,
    R: FnMut(usize, f64, ArrayView1<f64>) -> Option<(f64, Array1<f64>)>

Relaxes x0, offering each accepted iterate to watch.

watch receives the iteration index and the value at that iterate, and returning false ends the relaxation there. The point is a caller that stops on a decision rather than on an iteration count: a screening pass exists to answer one question, and the trajectory it produces says when the answer is settled.

The hook sits at the top of the iteration, not inside the line search, so what it sees is always an accepted point with its value and gradient consistent. Stopping mid-search would return a trial step the optimizer had not adopted. Relaxes x0, consulting recognise at each accepted iterate.

The warm layer’s face of rgmin::Lbfgs::minimize_recognized: a recogniser that certifies where this descent ends – a minimum already on file whose catchment the iterate has entered – ends the relaxation with the stand-in and refunds the rest of the descent. The flag separates refunded descents from completed ones, which is what an auditing caller needs to estimate its recogniser’s error rate against the budget Hop.refund_with_errors prices.

fn minimize_watched<F, W>(&mut self, x0: ArrayView1<f64>, max_iter: usize, fg: F, watch: W) -> (f64, Array1<f64>, usize)
where
    F: FnMut(ArrayView1<f64>) -> Option<(f64, Array1<f64>)>,
    W: FnMut(usize, f64) -> bool
fn record_pair(&mut self, s: Array1<f64>, y: Array1<f64>)

Store one accepted curvature pair.

fn two_loop(&self, g: ArrayView1<f64>) -> Array1<f64>

Two-loop direction (d=-Hg) on the stored pairs.

Traits implemented

impl Default for WarmLbfgs