Files
Jungfraujoch/image_analysis/scale_merge/AndersonMixing.cpp
T
leonarski_fandClaude Opus 5.5 0894230671 Anderson: accelerate a scaling loop only near its fixed point, and never again once its step grows (RESULT-CHANGING)
The Anderson commit before this one changed three rc176 battery results, which is why it was
reverted there. One of them is the acceleration's own doing: on a split myoglobin sweep (P2_1,
ISa 2.5) the partials loops never settle - one walks at a step of ~2e-2 for as long as it runs -
and the fulls loops walk from their first round (their cross-validated lambda moves every round,
so the map itself is not fixed). Restarting the history whenever the step grew only rebuilt a
one- or two-round history and extrapolated along the walk: the stalled loops ended somewhere
else, the pre-pass fulls smoothing window went from ~7 frames to ~1, and CC1/2 fell 0.701 -> 0.595.

Two rules now confine the acceleration to the regime it is for - a map contracting to its fixed
point:
- a loop is accelerated only once its plain step is below ten times the tolerance (1e-2);
- the first round whose weighted residual grew turns the acceleration off for the rest of the
  loop (AndersonMixing::Stopped), instead of restarting it.
A loop that walks therefore stays the plain loop it was in rc176. The growth rule alone gave
CC1/2 0.625; with the onset, 0.701, as rc176.

Measured against rc176 bc191bb80 (GPU, workstation):
- myob_x06da_split: P2_1, CC1/2 0.701, d 1.81 A - as rc176; 481 -> 257 scaling iterations.
- 7qij 434 -> 126 iterations, 9v16 273 -> 101, same space group and statistics.
- ci tier (29 sets, no null model): 28 unchanged beyond noise; sum of times 580 -> 585 s
  (9gdj +9 s, below).
The two other changes are NOT Anderson's and stay:
- insu_I_x06da_weak I23 -> I222: the all-observations search refuses I23 on a merge chi2 ratio
  2.08 against a 1.85 bound; plain scaling converged to 1e-5 gives 2.07-2.09 and refuses it too.
  Only the plain loop stopped at 1e-3 passed - the bound was set on unconverged scaling.
- 9gdj +9 s, same answer: the early beam-centre comparison's "loses beyond noise" margin
  (-0.002 against 2 sigma 0.0016) moves by ~0.07 between scaling paths, so the converged loop no
  longer takes the shortcut and both centres are judged on all frames; the file's centre is kept
  either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-19 18:49:24 +02:00

52 lines
2.0 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "AndersonMixing.h"
std::vector<double> AndersonMixing::Next(const std::vector<double> &x, const std::vector<double> &fx,
const std::vector<double> &w) {
// Anderson acceleration, type II, after Walker & Ni (2011) SIAM J. Numer. Anal. 49, 1715-1735
// (Anderson (1965) J. ACM 12, 547-560): with dX and dR the differences of the states and of the
// residuals of consecutive rounds, gamma = argmin |r - dR gamma|_w and the next state is
// F(x) - (dX + dR) gamma.
const int n = static_cast<int>(x.size());
const Eigen::Map<const Eigen::VectorXd> xv(x.data(), n), fxv(fx.data(), n), wv(w.data(), n);
const Eigen::VectorXd r = fxv - xv;
std::vector<uint8_t> support(n);
for (int i = 0; i < n; ++i) support[i] = w[i] > 0.0 ? 1 : 0;
const double r_norm = (wv.array() * r.array().square()).sum();
if (stopped || (x_last.size() == n && r_norm > r_norm_last)) {
stopped = true;
return fx;
}
if (x_last.size() == n && support == support_last) {
dx.push_back(xv - x_last);
dr.push_back(r - r_last);
if (static_cast<int>(dx.size()) > depth) {
dx.pop_front();
dr.pop_front();
}
} else if (!dx.empty()) {
dx.clear();
dr.clear();
++restarts;
}
x_last = xv;
r_last = r;
support_last = std::move(support);
r_norm_last = r_norm;
std::vector<double> out = fx;
const int m = static_cast<int>(dr.size());
if (m == 0) return out;
const Eigen::VectorXd sqrt_w = wv.cwiseMax(0.0).cwiseSqrt();
Eigen::MatrixXd M(n, m);
for (int j = 0; j < m; ++j) M.col(j) = sqrt_w.cwiseProduct(dr[j]);
const Eigen::VectorXd gamma = M.colPivHouseholderQr().solve(sqrt_w.cwiseProduct(r));
for (int j = 0; j < m; ++j)
for (int i = 0; i < n; ++i)
if (w[i] > 0.0) out[i] -= gamma[j] * (dx[j][i] + dr[j][i]);
return out;
}