mod path

module path

The pure-Rust SA driver loop. Multi-step paths between minima, where one hop cannot cross. Multi-step paths between minima, for landscapes where one hop cannot cross.

Basin hopping is a breadth-first search of depth one: propose, relax, accept. On a multi-funnel landscape that is the wrong depth, and the measurement says so rather than the intuition. From the structure a 75-point Lennard-Jones search settles into, none of 1800 single moves across the whole kernel set reaches anything lower; every move that leaves the basin lands higher, and the moves that land lowest do not leave. The target is 1.2 below and no single proposal goes there.

No bias, temperature law, collective variable or acquisition rule creates a path that is not in the move graph. What is needed is a search of depth greater than one: a sequence of intermediate structures between where the chain is and somewhere structurally different, each relaxed, so the corridor between two funnels is examined rather than jumped.

Fully relaxing an interpolated image does not do this, and the measurement is unambiguous. Between the structure a 75-point search settles into and each of four constructed morphologies, every image relaxed either back to the start or onto the endpoint: the deepest structure each path produced was that path’s own endpoint, to every printed digit. A fully relaxed image slides into whichever basin owns it and the corridor is never sampled.

transverse_path removes the component of the gradient along the path before descending, so an image falls to the valley floor while staying where it was placed. That projection is what a nudged elastic band uses and the reason it is used.

Two things make it work rather than merely run.

The endpoints must be structurally different. Interpolating between two structures in one funnel lands back in it, which is consistent with the archive-based escape moves in this campaign scoring zero of eight on LJ75 while holding only structures the chain itself had reached.

The images must be relaxed individually and charged. An unrelaxed image is not a minimum and says nothing about which basin it belongs to, and a path whose cost is not charged is not comparable with the hopping it replaces.

Functions

fn interpolate_path<R, E>(a: ArrayView1<f64>, b: ArrayView1<f64>, n_images: usize, mut relax: R, mut left_start: E) -> PathOutcome
where
    R: FnMut(ArrayView1<f64>) -> Option<(f64, Array1<f64>)>,
    E: FnMut(ArrayView1<f64>) -> bool

Relaxes images interpolated between two structures.

relax returns the value and relaxed structure, charging the caller’s ledger, and returns None when the budget is spent, which ends the path where it stands rather than silently returning a shorter one as complete.

left_start decides whether a relaxed image is outside the basin the path began in. It is the caller’s because basin identity is: on a cluster it is a shape distance against a, and a threshold there is a length.

Images are taken away from the endpoints. A fraction near zero relaxes back to a and one near one to b, and neither is a crossing.

fn transverse_path<G>(a: ArrayView1<f64>, b: ArrayView1<f64>, n_images: usize, steps: usize, step_size: f64, mut grad: G) -> Vec<(f64, Array1<f64>)>
where
    G: FnMut(ArrayView1<f64>) -> Option<(f64, Array1<f64>)>

Relaxes images perpendicular to the path between two structures.

grad returns the value and gradient at a point, charging the caller’s ledger, and None when the budget is spent.

Each image descends with the component of the gradient along the path removed, so it settles into the valley floor at its own position rather than sliding to an endpoint. The images that come out are candidate structures along the corridor; a caller relaxes the promising ones fully.

Structs and Unions

struct PathOutcome

What a path attempt produced.

points: Vec<PathPoint>

Every image that relaxed successfully, in order of lambda.

best: Option<usize>

The deepest of them, if any.

escapes: Vec<usize>

Images that left the starting basin, by the caller’s test.

Implementations

impl PathOutcome

Functions

fn best_escape(&self) -> Option<&PathPoint>

The deepest structure that also left the starting basin.

This, not the deepest overall, is what a stuck chain wants: the deepest image is usually a relaxation back into the basin the path started in.

struct PathPoint

A minimum found along a path, with where it came from.

lambda: f64

Interpolation fraction the image started at, in (0, 1).

energy: f64

Relaxed value.

state: Array1<f64>

Relaxed structure.

struct StallDetector

Tracks whether a chain has stopped making progress, so a path is attempted when hopping has stalled rather than on a schedule.

A path costs many relaxations and is worth paying for only when the cheap mechanism has stopped working. The signal is the one this landscape gives plainly: near a deep minimum roughly nineteen proposals in twenty return to where they started, and the incumbent stops moving.

patience: usize

Hops without an improvement before the chain counts as stalled.

stalls: usize

Times a stall was reported.

Implementations

impl StallDetector

Functions

fn new(patience: usize) -> Self

Detector that reports a stall after patience hops without progress.

fn observe(&mut self, energy: f64) -> bool

Records a hop’s value. Returns whether the chain is now stalled.

Reporting resets the counter, so a stall is reported once per stretch rather than on every hop after the threshold.

fn since_improvement(&self) -> usize

Hops since the last improvement.