Loading...
Searching...
No Matches
eonc::LORRotation Class Reference

#include <LORRotation.h>

Inheritance diagram for eonc::LORRotation:

Public Member Functions

 LORRotation (std::shared_ptr< Matter > matter, const Parameters &params, std::shared_ptr< Potential > pot)
 ~LORRotation ()=default
void compute (std::shared_ptr< Matter > matter, AtomMatrix initialDirection)
double getEigenvalue ()
AtomMatrix getEigenvector ()
Public Member Functions inherited from eonc::LowestEigenmode
 LowestEigenmode (std::shared_ptr< Potential > potPassed, const Parameters &parameters)
 ~LowestEigenmode ()=default

Static Public Member Functions

static VectorXd translateHUnitOrthoP3 (const VectorXd &N, const VectorXd &Theta, const VectorXd &P, const VectorXd &HN, const VectorXd &HTheta, const VectorXd &HP)
 Force-translation identity for H·P3 when P3 is the unit Gram-Schmidt residual of P against orthonormal {N, Θ} and H is linear: P_ortho = P - (N·P) N - (Θ·P) Θ P3 = P_ortho / ||P_ortho|| H P3 = (H P - (N·P) H N - (Θ·P) H Θ) / ||P_ortho|| (Not Gram-Schmidt on HP in ambient space.) Returns zero vector if ||P_ortho|| is negligible.

Public Attributes

std::vector< double > curvatureHistory
bool convergedOnResidual {false}
Public Attributes inherited from eonc::LowestEigenmode
long totalForceCalls {0}
double statsTorque {0.0}
double statsCurvature {0.0}
double statsAngle {0.0}
long statsRotations {0}
long totalIterations {0}

Private Attributes

eonc::log::Scoped log
VectorXd eigenvector
double eigenvalue {0.0}
std::shared_ptr< Matterx0
std::shared_ptr< Matterx1

Additional Inherited Members

Static Public Attributes inherited from eonc::LowestEigenmode
static const char MINMODE_DIMER [] = "dimer"
static const char MINMODE_GPRDIMER [] = "gprdimer"
static const char MINMODE_LANCZOS [] = "lanczos"
static const char MINMODE_DAVIDSON [] = "davidson"
Protected Attributes inherited from eonc::LowestEigenmode
std::shared_ptr< Potentialpot
const Parametersparams

Detailed Description

Definition at line 25 of file LORRotation.h.

Constructor & Destructor Documentation

◆ LORRotation()

LORRotation::LORRotation ( std::shared_ptr< Matter > matter,
const Parameters & params,
std::shared_ptr< Potential > pot )

Definition at line 29 of file LORRotation.cpp.

33 auto x1Pot = (pot->needsPerImageInstance() && params.main_options.parallel)
35 : pot;
36 x0 = std::make_shared<Matter>(pot, params);
37 x1 = std::make_shared<Matter>(x1Pot, params);
38 *x0 = *matter;
39 *x1 = *matter;
42 eigenvector.resize(3 * matter->numberOfAtoms());
43 eigenvector.setZero();
44}
std::shared_ptr< Matter > x0
Definition LORRotation.h:30
std::shared_ptr< Matter > x1
Definition LORRotation.h:31
VectorXd eigenvector
Definition LORRotation.h:28
const Parameters & params
std::shared_ptr< Potential > pot
LowestEigenmode(std::shared_ptr< Potential > potPassed, const Parameters &parameters)
std::shared_ptr< Potential > makePotential(const Parameters &params)

◆ ~LORRotation()

eonc::LORRotation::~LORRotation ( )
default

Member Function Documentation

◆ compute()

void LORRotation::compute ( std::shared_ptr< Matter > matter,
AtomMatrix initialDirection )

Definition at line 61 of file LORRotation.cpp.

62 {
63 const int dim = static_cast<int>(3 * matter->numberOfAtoms());
64 const VectorXd freeMask = matter->getFreeV();
67 curvatureHistory.clear();
68 convergedOnResidual = false;
69 eigenvalue = 0.0;
70 eigenvector = VectorXd::Zero(dim);
71
72 // Fully fixed system: no free DOF — avoid normalize() NaN.
73 if (freeMask.norm() < 1e-14 || matter->numberOfFreeAtoms() == 0) {
74 QUILL_LOG_WARNING(log, "[LOR] no free atoms; skipping rotation");
75 return;
76 }
77
78 VectorXd N = VectorXd::Map(initialDirectionAtomMatrix.data(), dim);
79 N = N.array() * freeMask.array();
80 if (N.norm() < 1e-10) {
81 N.setRandom();
82 N = N.array() * freeMask.array();
83 }
84 if (N.norm() < 1e-14) {
85 QUILL_LOG_WARNING(log, "[LOR] free mask yields zero direction; skip");
86 return;
87 }
88 N.normalize();
89
90 *x0 = *matter;
91 *x1 = *matter;
92 const VectorXd x0_r = x0->getPositionsV();
93 const double delta = params.main_options.finiteDifference;
94
95 // rotations_max <= 0 → Parameters default (10); no silent upper clamp.
96 const long rotBudget = params.dimer_options.rotations_max > 0
97 ? params.dimer_options.rotations_max
98 : 10;
99 const int rotmax = static_cast<int>(std::max<long>(1, rotBudget));
100
101 // Dedicated LOR residual tolerance (not classical torque_min).
102 const double residualTol =
103 std::max(1e-3, params.dimer_options.lor_residual_tol);
104 auto relativeResidual = [](double fnorm, double cn) {
105 return fnorm / (std::abs(cn) + 1.0);
106 };
107
108 double bestCN = std::numeric_limits<double>::infinity();
109 VectorXd bestN = N;
110 VectorXd bestHN = VectorXd::Zero(dim);
111
112 // Cache center forces once (paper: FD products reuse F(R0)).
113 const VectorXd F0 = x0->getForcesV();
114 totalForceCalls += 1;
115
116 auto applyMask = [&](VectorXd &v) { v = v.array() * freeMask.array(); };
117
118 auto unitize = [&](VectorXd &v) -> double {
119 applyMask(v);
120 const double n = v.norm();
121 if (n > 1e-14) {
122 v /= n;
123 }
124 return n;
125 };
126
127 // One new force at R0+δ·dir; F0 cached (Algorithm I: ≤1 new FD per rotation).
128 auto hessianAlong = [&](const VectorXd &v) -> VectorXd {
129 VectorXd dir = v.array() * freeMask.array();
130 const double nrm = dir.norm();
131 if (nrm < 1e-14) {
132 return VectorXd::Zero(dim);
133 }
134 dir /= nrm;
135 x1->setPositionsV(x0_r + delta * dir);
136 const VectorXd F1 = x1->getForcesV();
137 totalForceCalls += 1;
138 VectorXd Hv = -(F1 - F0) / delta;
139 applyMask(Hv);
140 return Hv * nrm;
141 };
142
143 auto trackBest = [&](double cn, const VectorXd &nVec, const VectorXd &hnVec) {
144 if (cn < bestCN) {
145 bestCN = cn;
146 bestN = nVec;
147 bestHN = hnVec;
148 }
149 };
150 // Record Ritz C only when non-increasing (paper quadratic + translation).
151 // Strictly non-increasing (1e-4 float). Returns whether the sample was kept
152 // (callers must only trackBest on kept samples so ev == min(history)).
153 auto appendHistory = [&](double cn) -> bool {
154 if (curvatureHistory.empty() || cn <= curvatureHistory.back() + 1e-4) {
155 curvatureHistory.push_back(cn);
156 return true;
157 }
158 return false;
159 };
160
161 // --- Algorithm I start: H N, F_⊥ ---
162 VectorXd HN = hessianAlong(N);
163 applyMask(HN);
164 double CN = N.dot(HN);
165 if (appendHistory(CN)) {
166 trackBest(CN, N, HN);
167 }
168
169 VectorXd F = HN - CN * N;
170 applyMask(F);
171 double Fnorm = F.norm();
172
173 QUILL_LOG_INFO(
174 log, "[LOR] iter=0 ||F_perp||={:.6e} C_N={:.6f} (Algorithm I start)",
175 Fnorm, CN);
176
177 if (relativeResidual(Fnorm, CN) < residualTol) {
178 convergedOnResidual = true;
179 eigenvalue = CN;
180 eigenvector = N;
181 QUILL_LOG_INFO(log, "[LOR] converged on residual at start");
182 return;
183 }
184
185 VectorXd Theta = F / Fnorm;
186 VectorXd HTheta = hessianAlong(Theta); // iteration-1 second FD (H Θ)
187 applyMask(HTheta);
188
189 // 2×2 Ritz in span{N, Θ}
190 Eigen::Matrix2d A2;
191 A2(0, 0) = N.dot(HN);
192 A2(0, 1) = N.dot(HTheta);
193 A2(1, 0) = A2(0, 1);
194 A2(1, 1) = Theta.dot(HTheta);
195 Eigen::SelfAdjointEigenSolver<Eigen::Matrix2d> es2(A2);
196 Eigen::Vector2d coeffs = es2.eigenvectors().col(0);
197 double a = coeffs(0);
198 double b = coeffs(1);
199
200 VectorXd Nlin = a * N + b * Theta;
201 const double nN = unitize(Nlin);
202 VectorXd HNlin = a * HN + b * HTheta;
203 if (nN > 1e-14) {
204 HNlin /= nN;
205 }
206 N = Nlin;
207 HN = HNlin;
208
209 VectorXd P = Theta;
210 VectorXd HP = HTheta;
211
212 CN = N.dot(HN);
213 if (appendHistory(CN)) {
214 trackBest(CN, N, HN);
215 }
216 F = HN - CN * N;
217 applyMask(F);
218 Fnorm = F.norm();
219 statsRotations = 1;
220
221 QUILL_LOG_INFO(
222 log, "[LOR] iter=1 (2x2) ||F_perp||={:.6e} C_N={:.6f} a={:.4f} b={:.4f}",
223 Fnorm, CN, a, b);
224
225 for (int k = 2; k <= rotmax; ++k) {
226 if (relativeResidual(Fnorm, CN) < residualTol) {
227 convergedOnResidual = true;
228 QUILL_LOG_INFO(log,
229 "[LOR] converged residual iter={} ||F||={:.6e} rel={:.6e}",
230 k, Fnorm, relativeResidual(Fnorm, CN));
231 break;
232 }
233
234 Theta = F / Fnorm;
235 // Exactly one new FD this iteration: H · Θ (force translation for N, P)
236 HTheta = hessianAlong(Theta);
237 applyMask(HTheta);
238
239 // Orthonormalize P vs N,Θ for stable 3×3 (intentional vs paper GEP on B≠I)
240 VectorXd P3 = P - N.dot(P) * N - Theta.dot(P) * Theta;
241 applyMask(P3);
242 const double pNrm = P3.norm();
243
244 auto applyRitz2 = [&]() {
245 Eigen::Matrix2d A2b;
246 A2b(0, 0) = N.dot(HN);
247 A2b(0, 1) = N.dot(HTheta);
248 A2b(1, 0) = A2b(0, 1);
249 A2b(1, 1) = Theta.dot(HTheta);
250 Eigen::SelfAdjointEigenSolver<Eigen::Matrix2d> es2b(A2b);
251 Eigen::Vector2d c2 = es2b.eigenvectors().col(0);
252 a = c2(0);
253 b = c2(1);
254 VectorXd Nnew = a * N + b * Theta;
255 VectorXd HNnew = a * HN + b * HTheta;
256 const double nn = unitize(Nnew);
257 if (nn > 1e-14) {
258 HNnew /= nn;
259 }
260 const double cNew = Nnew.dot(HNnew);
261 if (!curvatureHistory.empty() && cNew > curvatureHistory.back() + 1e-4) {
262 QUILL_LOG_INFO(log,
263 "[LOR] curvature stall (2x2 fallback) iter={} "
264 "C_prev={:.6f} C_new={:.6f} (reject)",
265 k, curvatureHistory.back(), cNew);
266 N = bestN;
267 HN = bestHN;
268 CN = bestCN;
269 F = HN - CN * N;
270 applyMask(F);
271 Fnorm = F.norm();
272 statsRotations = k;
273 return false;
274 }
275 P = Theta;
276 HP = HTheta;
277 N = Nnew;
278 HN = HNnew;
279 CN = cNew;
280 if (appendHistory(CN)) {
281 trackBest(CN, N, HN);
282 }
283 F = HN - CN * N;
284 applyMask(F);
285 Fnorm = F.norm();
286 statsRotations = k;
287 return true;
288 };
289
290 if (pNrm < 1e-8) {
291 if (!applyRitz2()) {
292 break;
293 }
294 continue;
295 }
296 P3 /= pNrm;
297 // H·P3 by force-translation linearity (not ambient GS on HP):
298 // H P3 = (HP - (N·P) HN - (Θ·P) HΘ) / ||P_ortho|| with P3 = P_ortho/||...||
299 VectorXd HP3 = translateHUnitOrthoP3(N, Theta, P, HN, HTheta, HP);
300 applyMask(HP3);
301 if (HP3.norm() < 1e-14) {
302 if (!applyRitz2()) {
303 break;
304 }
305 continue;
306 }
307
308 Eigen::Matrix3d A3 = Eigen::Matrix3d::Zero();
309 const VectorXd basis[3] = {N, Theta, P3};
310 const VectorXd Hbasis[3] = {HN, HTheta, HP3};
311 for (int i = 0; i < 3; ++i) {
312 for (int j = i; j < 3; ++j) {
313 A3(i, j) = basis[i].dot(Hbasis[j]);
314 A3(j, i) = A3(i, j);
315 }
316 }
317 A3 = 0.5 * (A3 + A3.transpose());
318 Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> ges(A3);
319 if (ges.info() != Eigen::Success) {
320 QUILL_LOG_WARNING(log, "[LOR] 3x3 eigen failed at iter={}; stop", k);
321 break;
322 }
323 const Eigen::Vector3d c3 = ges.eigenvectors().col(0);
324 a = c3(0);
325 b = c3(1);
326 const double c = c3(2);
327
328 // Save pre-update for stall revert without FD
329 const VectorXd Nprev = N;
330 const VectorXd HNprev = HN;
331 const double CNprev = CN;
332
333 VectorXd Nnew = a * N + b * Theta + c * P3;
334 VectorXd Pnew = b * Theta + c * P3;
335 VectorXd HNnew = a * HN + b * HTheta + c * HP3;
336 VectorXd HPnew = b * HTheta + c * HP3;
337
338 const double nNew = unitize(Nnew);
339 if (nNew > 1e-14) {
340 HNnew /= nNew;
341 }
342 const double pNew = unitize(Pnew);
343 if (pNew > 1e-14) {
344 HPnew /= pNew;
345 }
346
347 const double CNnew = Nnew.dot(HNnew);
348 if (!curvatureHistory.empty() && CNnew > curvatureHistory.back() + 1e-4) {
349 QUILL_LOG_INFO(log,
350 "[LOR] curvature stall iter={} C_prev={:.6f} C_new={:.6f} "
351 "(reject update; keep prior mode, no extra FD)",
352 k, curvatureHistory.back(), CNnew);
353 N = bestN;
354 HN = bestHN;
355 CN = bestCN;
356 F = HN - CN * N;
357 applyMask(F);
358 Fnorm = F.norm();
359 statsRotations = k;
360 break;
361 }
362
363 N = Nnew;
364 P = Pnew;
365 HN = HNnew;
366 HP = HPnew;
367 CN = CNnew;
368 if (appendHistory(CN)) {
369 trackBest(CN, N, HN);
370 }
371 (void)Nprev;
372 (void)HNprev;
373 (void)CNprev;
374
375 F = HN - CN * N;
376 applyMask(F);
377 Fnorm = F.norm();
378 statsRotations = k;
379
380 QUILL_LOG_INFO(log,
381 "[LOR] iter={} (3x3) ||F_perp||={:.6e} C_N={:.6f} a={:.4f} "
382 "b={:.4f} c={:.4f}",
383 k, Fnorm, CN, a, b, c);
384
385 if (relativeResidual(Fnorm, CN) < residualTol) {
386 convergedOnResidual = true;
387 break;
388 }
389 }
390
391 // Return best softest mode from accepted Ritz steps (no mandatory final FD).
392 N = bestN;
393 HN = bestHN;
394 CN = bestCN;
395 eigenvalue = CN;
396 eigenvector = N;
397 QUILL_LOG_INFO(log,
398 "[LOR] done rotations={} force_calls={} C_N={:.6f} "
399 "converged_residual={}",
402}
eonc::log::Scoped log
Definition LORRotation.h:27
static VectorXd translateHUnitOrthoP3(const VectorXd &N, const VectorXd &Theta, const VectorXd &P, const VectorXd &HN, const VectorXd &HTheta, const VectorXd &HP)
Force-translation identity for H·P3 when P3 is the unit Gram-Schmidt residual of P against orthonorma...
std::vector< double > curvatureHistory
Definition LORRotation.h:45

◆ getEigenvalue()

double eonc::LORRotation::getEigenvalue ( )
inline

Definition at line 39 of file LORRotation.h.

39{ return eigenvalue; }

◆ getEigenvector()

AtomMatrix eonc::LORRotation::getEigenvector ( )
inline

Definition at line 40 of file LORRotation.h.

40 {
41 return AtomMatrix::Map(eigenvector.data(), eigenvector.size() / 3, 3);
42 }

◆ translateHUnitOrthoP3()

VectorXd LORRotation::translateHUnitOrthoP3 ( const VectorXd & N,
const VectorXd & Theta,
const VectorXd & P,
const VectorXd & HN,
const VectorXd & HTheta,
const VectorXd & HP )
staticnodiscard

Force-translation identity for H·P3 when P3 is the unit Gram-Schmidt residual of P against orthonormal {N, Θ} and H is linear: P_ortho = P - (N·P) N - (Θ·P) Θ P3 = P_ortho / ||P_ortho|| H P3 = (H P - (N·P) H N - (Θ·P) H Θ) / ||P_ortho|| (Not Gram-Schmidt on HP in ambient space.) Returns zero vector if ||P_ortho|| is negligible.

Shipped LOR 3×3 path uses this helper.

Definition at line 47 of file LORRotation.cpp.

49 {
50 // P_ortho = (I - N Nᵀ - Θ Θᵀ) P with {N, Θ} orthonormal
51 const VectorXd P_ortho = P - N.dot(P) * N - Theta.dot(P) * Theta;
52 const double pNrm = P_ortho.norm();
53 if (pNrm < 1e-14) {
54 return VectorXd::Zero(P.size());
55 }
56 // H P_ortho = HP - (N·P) HN - (Θ·P) HΘ (linearity of H — not GS on HP)
57 const VectorXd HPortho = HP - N.dot(P) * HN - Theta.dot(P) * HTheta;
58 return HPortho / pNrm; // = H (P_ortho / ||P_ortho||) = H P3
59}

Member Data Documentation

◆ convergedOnResidual

bool eonc::LORRotation::convergedOnResidual {false}

Definition at line 46 of file LORRotation.h.

46{false};

◆ curvatureHistory

std::vector<double> eonc::LORRotation::curvatureHistory

Definition at line 45 of file LORRotation.h.

◆ eigenvalue

double eonc::LORRotation::eigenvalue {0.0}
private

Definition at line 29 of file LORRotation.h.

29{0.0};

◆ eigenvector

VectorXd eonc::LORRotation::eigenvector
private

Definition at line 28 of file LORRotation.h.

◆ log

eonc::log::Scoped eonc::LORRotation::log
private

Definition at line 27 of file LORRotation.h.

◆ x0

std::shared_ptr<Matter> eonc::LORRotation::x0
private

Definition at line 30 of file LORRotation.h.

◆ x1

std::shared_ptr<Matter> eonc::LORRotation::x1
private

Definition at line 31 of file LORRotation.h.


The documentation for this class was generated from the following files: