62 {
63 const int dim = static_cast<int>(3 * matter->numberOfAtoms());
64 const VectorXd freeMask = matter->getFreeV();
71
72
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
92 const VectorXd x0_r =
x0->getPositionsV();
93 const double delta =
params.main_options.finiteDifference;
94
95
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
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
113 const VectorXd F0 =
x0->getForcesV();
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
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();
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
151
152
153 auto appendHistory = [&](double cn) -> bool {
156 return true;
157 }
158 return false;
159 };
160
161
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) {
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);
187 applyMask(HTheta);
188
189
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();
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) {
229 "[LOR] converged residual iter={} ||F||={:.6e} rel={:.6e}",
230 k, Fnorm, relativeResidual(Fnorm, CN));
231 break;
232 }
233
234 Theta = F / Fnorm;
235
236 HTheta = hessianAlong(Theta);
237 applyMask(HTheta);
238
239
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);
263 "[LOR] curvature stall (2x2 fallback) iter={} "
264 "C_prev={:.6f} C_new={:.6f} (reject)",
266 N = bestN;
267 HN = bestHN;
268 CN = bestCN;
269 F = HN - CN * N;
270 applyMask(F);
271 Fnorm = F.norm();
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();
287 return true;
288 };
289
290 if (pNrm < 1e-8) {
291 if (!applyRitz2()) {
292 break;
293 }
294 continue;
295 }
296 P3 /= pNrm;
297
298
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
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);
350 "[LOR] curvature stall iter={} C_prev={:.6f} C_new={:.6f} "
351 "(reject update; keep prior mode, no extra FD)",
353 N = bestN;
354 HN = bestHN;
355 CN = bestCN;
356 F = HN - CN * N;
357 applyMask(F);
358 Fnorm = F.norm();
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();
379
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) {
387 break;
388 }
389 }
390
391
392 N = bestN;
393 HN = bestHN;
394 CN = bestCN;
398 "[LOR] done rotations={} force_calls={} C_N={:.6f} "
399 "converged_residual={}",
402}
std::vector< double > curvatureHistory
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...