votca 2026-dev
Loading...
Searching...
No Matches
trah.cc
Go to the documentation of this file.
1/*
2 * Copyright 2009-2026 The VOTCA Development Team
3 * (http://www.votca.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License")
6 *
7 * You may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20// Standard includes
21#include <algorithm>
22#include <cmath>
23
24// Local VOTCA includes
25#include "votca/xtp/trah.h"
26
27namespace votca {
28namespace xtp {
29namespace trah {
30
31namespace {
32
33// Lowest eigenpair of the projected augmented Hessian
34// [[0, a gs^T], [a gs, Hs]]; xs = y / (a y0). False if y0 vanishes (g
35// orthogonal to the lowest eigenvector), where no step can be formed.
36bool SolveProjected(const Eigen::MatrixXd& Hs, const Eigen::VectorXd& gs,
37 double a, Eigen::VectorXd& xs, double& mu) {
38 const Index m = Hs.rows();
39 Eigen::MatrixXd M = Eigen::MatrixXd::Zero(m + 1, m + 1);
40 M.block(0, 1, 1, m) = a * gs.transpose();
41 M.block(1, 0, m, 1) = a * gs;
42 M.bottomRightCorner(m, m) = Hs;
43 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(M);
44 const Eigen::VectorXd y = es.eigenvectors().col(0);
45 if (std::abs(y(0)) < 1e-14) {
46 return false;
47 }
48 xs = y.tail(m) / (a * y(0));
49 mu = es.eigenvalues()(0);
50 return true;
51}
52
53// alpha > 1 for a step with |x| in [lower_fraction, 1] x trust_radius:
54// bisection in log(alpha) on the projected problem (no Hessian products).
55// If even alpha_max gives a step longer than lower_fraction x trust_radius,
56// that step is returned (Step scales it back onto the trust radius). xs and
57// mu are updated with every solved alpha.
58double SearchAlpha(const Eigen::MatrixXd& Hs, const Eigen::VectorXd& gs,
59 double trust_radius, const StepOptions& opt,
60 Eigen::VectorXd& xs, double& mu) {
61 Eigen::VectorXd xs_hi;
62 double mu_hi = 0.0;
63 if (SolveProjected(Hs, gs, opt.alpha_max, xs_hi, mu_hi) &&
64 xs_hi.norm() > opt.lower_fraction * trust_radius) {
65 xs = xs_hi;
66 mu = mu_hi;
67 return opt.alpha_max;
68 }
69 double alpha = 1.0;
70 double lo = 1.0;
71 double hi = opt.alpha_max;
72 for (int k = 0; k < 100; ++k) {
73 const double a = std::sqrt(lo * hi);
74 Eigen::VectorXd xs_a;
75 double mu_a = 0.0;
76 if (!SolveProjected(Hs, gs, a, xs_a, mu_a)) {
77 lo = a;
78 continue;
79 }
80 alpha = a;
81 xs = xs_a;
82 mu = mu_a;
83 const double norm = xs_a.norm();
84 if (norm > trust_radius) {
85 lo = a;
86 } else if (norm < opt.lower_fraction * trust_radius) {
87 hi = a;
88 } else {
89 break;
90 }
91 }
92 return alpha;
93}
94
95// Orthonormalises v against the columns of B (twice) and normalises it.
96// False if nothing new is left.
97bool Orthonormalize(const Eigen::MatrixXd& B, Eigen::VectorXd& v) {
98 const double norm0 = v.norm();
99 if (!(norm0 > 0.0)) {
100 return false;
101 }
102 for (int pass = 0; pass < 2; ++pass) {
103 if (B.cols() > 0) {
104 v -= B * (B.transpose() * v);
105 }
106 }
107 const double norm = v.norm();
108 if (norm < 1e-10 * norm0) {
109 return false;
110 }
111 v /= norm;
112 return true;
113}
114} // namespace
115
116StepResult Step(const Eigen::VectorXd& g, const Eigen::VectorXd& diag,
117 const HessianProduct& hessian, double trust_radius,
118 const StepOptions& opt, Subspace* subspace) {
119 const Index n = g.size();
120 StepResult result;
121 result.x = Eigen::VectorXd::Zero(n);
122 const double gnorm = g.norm();
123 if (!(gnorm > 0.0)) {
124 result.converged = true;
125 return result;
126 }
127 const double eta = std::min(opt.max_forcing, gnorm);
128 const double tolerance = std::max(eta * gnorm, opt.residual_floor);
129
130 Eigen::MatrixXd B(n, 0);
131 Eigen::MatrixXd HB(n, 0);
132 auto add = [&](Eigen::VectorXd v) {
133 if (!Orthonormalize(B, v)) {
134 return false;
135 }
136 const Eigen::MatrixXd Hv = hessian(v);
137 ++result.products;
138 B.conservativeResize(n, B.cols() + 1);
139 B.col(B.cols() - 1) = v;
140 HB.conservativeResize(n, HB.cols() + 1);
141 HB.col(HB.cols() - 1) = Hv.col(0);
142 return true;
143 };
144 if (subspace != nullptr && subspace->B.cols() > 0 &&
145 subspace->B.rows() == n && subspace->g.size() == n &&
146 (subspace->g - g).norm() <= 1e-12 * gnorm) {
147 B = subspace->B;
148 HB = subspace->HB;
149 result.reused = true;
150 } else {
151 add(g / gnorm);
152 }
153
154 Eigen::VectorXd xs;
155 Eigen::VectorXd gs;
156 Eigen::MatrixXd Hs;
157 double mu = 0.0;
158 // The last solved step, in the full space, with g.x and x.H.x from the
159 // subspace it was solved in. Kept separately because B, gs and Hs change
160 // afterwards (new vectors, restarts) and a later projected solve can
161 // fail, which leaves xs from an earlier, smaller subspace.
162 bool solved = false;
163 Eigen::VectorXd x_full = Eigen::VectorXd::Zero(n);
164 double g_dot_x = 0.0;
165 double x_H_x = 0.0;
166 for (Index it = 0; it < opt.max_iterations; ++it) {
167 result.iterations = it + 1;
168 Hs = B.transpose() * HB;
169 Hs = 0.5 * (Hs + Hs.transpose()).eval();
170 gs = B.transpose() * g;
171
172 // a = 1 if its step is inside the trust radius; otherwise bisection in
173 // log(a), with no further Hessian products.
174 double alpha = 1.0;
175 if (!SolveProjected(Hs, gs, alpha, xs, mu)) {
176 break;
177 }
178 if (xs.norm() > trust_radius) {
179 alpha = SearchAlpha(Hs, gs, trust_radius, opt, xs, mu);
180 }
181
182 solved = true;
183 x_full = B * xs;
184 g_dot_x = gs.dot(xs);
185 x_H_x = xs.dot(Hs * xs);
186 result.alpha = alpha;
187 result.mu = mu;
188
189 // residual of (H - mu) x = -g in the full space
190 const Eigen::VectorXd r = HB * xs + g - mu * x_full;
191 result.residual = r.norm();
192 if (result.residual <= tolerance) {
193 result.converged = true;
194 break;
195 }
196 if (it + 1 == opt.max_iterations) {
197 break;
198 }
199 if (B.cols() >= opt.max_subspace) {
200 // restart on span{x, g}: both are combinations of B, so are their
201 // Hessian products
202 Eigen::MatrixXd W(B.cols(), 2);
203 W.col(0) = xs;
204 W.col(1) = gs;
205 Eigen::HouseholderQR<Eigen::MatrixXd> qr(W);
206 const Eigen::MatrixXd Q =
207 qr.householderQ() * Eigen::MatrixXd::Identity(B.cols(), 2);
208 B = (B * Q).eval();
209 HB = (HB * Q).eval();
210 }
211 // preconditioned residual; diag - mu > 0 for a positive diag, since
212 // mu <= 0 (the projected matrix has a zero on its diagonal)
213 Eigen::VectorXd t(n);
214 for (Index k = 0; k < n; ++k) {
215 t(k) = -r(k) / std::max(diag(k) - mu, 1e-4);
216 }
217 if (!add(t)) {
218 break;
219 }
220 }
221
222 if (subspace != nullptr) {
223 subspace->B = B;
224 subspace->HB = HB;
225 subspace->g = g;
226 }
227 if (!solved) {
228 // no projected problem could be solved (g orthogonal to its lowest
229 // eigenvector): no step
230 return result;
231 }
232 // predicted change g.x + 1/2 x.H.x, exact in the subspace; a step still
233 // outside the trust radius is scaled back onto it
234 const double norm = x_full.norm();
235 const double s = (norm > trust_radius) ? trust_radius / norm : 1.0;
236 result.linear = s * g_dot_x;
237 result.predicted = s * g_dot_x + 0.5 * s * s * x_H_x;
238 result.x = s * x_full;
239 return result;
240}
241
242bool TrustRadius::Update(double predicted, double actual, double linear,
243 double step_norm) {
244 const double radius_before = radius_;
245 const bool accepted = Update(predicted, actual);
246 if (!accepted && step_norm > 0.0) {
247 const double c = actual - linear;
248 double factor = 0.7;
249 if (linear < 0.0 && c > 0.0) {
250 factor = std::clamp(-linear / (2.0 * c), 0.1, 0.7);
251 }
252 radius_ = factor * std::min(radius_before, step_norm);
253 }
254 return accepted;
255}
256
257bool TrustRadius::Update(double predicted, double actual) {
258 if (std::abs(predicted) < noise_floor && std::abs(actual) < noise_floor) {
259 ratio_ = 1.0;
260 return true;
261 }
262 ratio_ = (std::abs(predicted) > 0.0) ? actual / predicted : -1.0;
263 if (ratio_ < 0.0) {
264 radius_ *= 0.7;
265 return false;
266 }
267 if (ratio_ <= 0.25) {
268 radius_ *= 0.7;
269 } else if (ratio_ > 0.75) {
270 radius_ = std::min(1.2 * radius_, max_radius_);
271 }
272 return true;
273}
274
275Eigen::MatrixXd RotateOrbitals(const Eigen::MatrixXd& C,
276 const Eigen::VectorXd& x, Index nocc) {
277 const Index nmo = C.cols();
278 const Index nvirt = nmo - nocc;
279 if (nocc == 0 || nvirt == 0) {
280 return C;
281 }
282 const Eigen::Map<const Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic,
283 Eigen::RowMajor>>
284 X(x.data(), nocc, nvirt);
285 // With X = U s V^T and A = X X^T = U s^2 U^T, every block is a function
286 // of A, so one symmetric eigendecomposition of the (nocc x nocc) matrix A
287 // suffices (no SVD):
288 // E_oo = U cos(s) U^T, E_ov = U (sin s / s) U^T X,
289 // E_vv = 1 + X^T U ((cos s - 1) / s^2) U^T X.
290 // The two quotients are taken from their series where s is small.
291 Eigen::SelfAdjointEigenSolver<Eigen::MatrixXd> es(X * X.transpose());
292 const Eigen::MatrixXd& U = es.eigenvectors();
293 const Index m = es.eigenvalues().size();
294 Eigen::VectorXd cos_s(m);
295 Eigen::VectorXd sinc_s(m);
296 Eigen::VectorXd cosm1_s2(m);
297 for (Index k = 0; k < m; ++k) {
298 const double s2 = std::max(es.eigenvalues()(k), 0.0);
299 const double s = std::sqrt(s2);
300 cos_s(k) = std::cos(s);
301 sinc_s(k) = (s > 1e-4) ? std::sin(s) / s : 1.0 - s2 / 6.0;
302 cosm1_s2(k) = (s > 1e-4) ? (std::cos(s) - 1.0) / s2 : -0.5 + s2 / 24.0;
303 }
304 const Eigen::MatrixXd UtX = U.transpose() * X;
305 const Eigen::MatrixXd E_oo = U * cos_s.asDiagonal() * U.transpose();
306 const Eigen::MatrixXd E_ov = U * sinc_s.asDiagonal() * UtX;
307 const Eigen::MatrixXd E_vv = Eigen::MatrixXd::Identity(nvirt, nvirt) +
308 UtX.transpose() * cosm1_s2.asDiagonal() * UtX;
309
310 const auto C_occ = C.leftCols(nocc);
311 const auto C_virt = C.rightCols(nvirt);
312 Eigen::MatrixXd result(C.rows(), nmo);
313 result.leftCols(nocc) = C_occ * E_oo - C_virt * E_ov.transpose();
314 result.rightCols(nvirt) = C_occ * E_ov + C_virt * E_vv;
315 return result;
316}
317
318Eigen::VectorXd OccVirt(const Eigen::MatrixXd& M, Index nocc) {
319 const Index nvirt = M.cols() - nocc;
320 Eigen::VectorXd out(nocc * nvirt);
321 for (Index i = 0; i < nocc; ++i) {
322 for (Index a = 0; a < nvirt; ++a) {
323 out(i * nvirt + a) = M(i, nocc + a);
324 }
325 }
326 return out;
327}
328
329} // namespace trah
330} // namespace xtp
331} // namespace votca
static constexpr double noise_floor
Definition trah.h:127
bool Update(double predicted, double actual)
Definition trah.cc:257
Trust-region augmented-Hessian (TRAH) step for orbital rotations, after Helmich-Paris,...
Definition trah.h:55
std::function< Eigen::MatrixXd(const Eigen::MatrixXd &)> HessianProduct
Hessian applied to the columns of V (n x k), returning n x k.
Definition trah.h:85
StepResult Step(const Eigen::VectorXd &g, const Eigen::VectorXd &diag, const HessianProduct &hessian, double trust_radius, const StepOptions &opt=StepOptions(), Subspace *subspace=nullptr)
Definition trah.cc:116
Eigen::MatrixXd RotateOrbitals(const Eigen::MatrixXd &C, const Eigen::VectorXd &x, Index nocc)
Definition trah.cc:275
Eigen::VectorXd OccVirt(const Eigen::MatrixXd &M, Index nocc)
Definition trah.cc:318
Charge transport classes.
Definition ERIs.h:28
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26
Eigen::VectorXd x
Definition trah.h:70
Eigen::VectorXd g
Definition trah.h:95
Eigen::MatrixXd B
Definition trah.h:93
Eigen::MatrixXd HB
Definition trah.h:94