votca 2026-dev
Loading...
Searching...
No Matches
exact_pole_sums.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 <array>
23#include <cmath>
24#include <vector>
25
26// Local VOTCA includes
28
29namespace votca {
30namespace xtp {
31namespace exact_pole_sums {
32
33namespace {
34
35// Batches smaller than this are summed directly
36constexpr Index min_batch_for_split = 24;
37
38// Far poles are summed at Chebyshev nodes of the frequency range [c-h, c+h]
39// and interpolated. For a pole at distance d = u h from the centre the
40// kernel is analytic inside the Bernstein ellipse with
41// rho = u + sqrt(u^2 - 1), and n-node interpolation converges like
42// rho^-n. Poles are grouped by u; each group uses enough nodes for
43// rho^-n < 1e-17 with a margin of about 1.5 in n:
44// u in [2, 3): rho >= 3.73, 48 nodes (rho^-48 ~ 3e-28)
45// u in [3, 5): rho >= 5.83, 32 nodes (~ 3e-25)
46// u in [5, 10): rho >= 9.90, 24 nodes (~ 8e-25)
47// u >= 10: rho >= 19.9, 16 nodes (~ 2e-21)
48// Poles closer than 2 h are summed at every frequency.
49constexpr int n_classes = 4;
50constexpr std::array<double, n_classes> class_umin = {2.0, 3.0, 5.0, 10.0};
51constexpr std::array<Index, n_classes> class_nodes = {48, 32, 24, 16};
52constexpr Index max_nodes = 48;
53
54inline double Kernel(double t, double eta2) { return t / (t * t + eta2); }
55
56// sign of Omega in the pole position: -1 occupied, +1 virtual
57inline double PoleSign(Index m, Index n_occ) {
58 return (m < n_occ) ? -1.0 : 1.0;
59}
60
61} // namespace
62
63double Value(const ExactPoles& poles, const double* residues, double w) {
64 const double eta2 = poles.eta * poles.eta;
65 const Index B = poles.nlevels();
66 // t = (w - e_m) - sign_m Omega_s
67 std::vector<double> d(static_cast<std::size_t>(B));
68 std::vector<double> sgn(static_cast<std::size_t>(B));
69 for (Index m = 0; m < B; ++m) {
70 d[std::size_t(m)] = w - poles.energies(m);
71 sgn[std::size_t(m)] = PoleSign(m, poles.n_occ);
72 }
73 double sum = 0.0;
74 for (Index s = 0; s < poles.nmodes(); ++s) {
75 const double omega = poles.omegas(s);
76 const double* r = residues + B * s;
77 double part = 0.0;
78#pragma omp simd reduction(+ : part)
79 for (Index m = 0; m < B; ++m) {
80 const double t = d[std::size_t(m)] - sgn[std::size_t(m)] * omega;
81 part += r[m] * r[m] * Kernel(t, eta2);
82 }
83 sum += part;
84 }
85 return sum;
86}
87
88double Derivative(const ExactPoles& poles, const double* residues, double w) {
89 const double eta2 = poles.eta * poles.eta;
90 const Index B = poles.nlevels();
91 std::vector<double> d(static_cast<std::size_t>(B));
92 std::vector<double> sgn(static_cast<std::size_t>(B));
93 for (Index m = 0; m < B; ++m) {
94 d[std::size_t(m)] = w - poles.energies(m);
95 sgn[std::size_t(m)] = PoleSign(m, poles.n_occ);
96 }
97 double sum = 0.0;
98 for (Index s = 0; s < poles.nmodes(); ++s) {
99 const double omega = poles.omegas(s);
100 const double* r = residues + B * s;
101 double part = 0.0;
102#pragma omp simd reduction(+ : part)
103 for (Index m = 0; m < B; ++m) {
104 const double t = d[std::size_t(m)] - sgn[std::size_t(m)] * omega;
105 const double t2 = t * t;
106 const double denom = t2 + eta2;
107 part += r[m] * r[m] * (eta2 - t2) / (denom * denom);
108 }
109 sum += part;
110 }
111 return sum;
112}
113
114namespace {
115// sum_p weight(r_p) f(w - z_p) at every frequency
116template <class Weight>
117Eigen::VectorXd DirectSum(const ExactPoles& poles, const double* r_all,
118 const Eigen::VectorXd& w, Weight weight) {
119 const double eta2 = poles.eta * poles.eta;
120 const Index B = poles.nlevels();
121 const Index F = w.size();
122 Eigen::VectorXd acc = Eigen::VectorXd::Zero(F);
123 double* out = acc.data();
124 const double* x = w.data();
125 for (Index s = 0; s < poles.nmodes(); ++s) {
126 const double omega = poles.omegas(s);
127 const double* r = r_all + B * s;
128 for (Index m = 0; m < B; ++m) {
129 const double a = weight(r[m]);
130 const double z = poles.energies(m) + PoleSign(m, poles.n_occ) * omega;
131#pragma omp simd
132 for (Index f = 0; f < F; ++f) {
133 out[f] += a * Kernel(x[f] - z, eta2);
134 }
135 }
136 }
137 return acc;
138}
139} // namespace
140
141Eigen::VectorXd ValuesDirect(const ExactPoles& poles, const double* residues,
142 const Eigen::VectorXd& w) {
143 return DirectSum(poles, residues, w, [](double r) { return r * r; });
144}
145
146Eigen::VectorXd ValuesDirectWeighted(const ExactPoles& poles,
147 const double* weights,
148 const Eigen::VectorXd& w) {
149 return DirectSum(poles, weights, w, [](double a) { return a; });
150}
151
152Eigen::VectorXd ValuesDirect(const ExactPoles& poles, const double* residues,
153 const Eigen::VectorXd& w,
154 const double* mode_factors) {
155 if (mode_factors == nullptr) {
156 return ValuesDirect(poles, residues, w);
157 }
158 const double eta2 = poles.eta * poles.eta;
159 const Index B = poles.nlevels();
160 const Index F = w.size();
161 Eigen::VectorXd acc = Eigen::VectorXd::Zero(F);
162 double* out = acc.data();
163 const double* x = w.data();
164 for (Index s = 0; s < poles.nmodes(); ++s) {
165 const double factor = mode_factors[s];
166 if (factor == 0.0) {
167 continue;
168 }
169 const double omega = poles.omegas(s);
170 const double* r = residues + B * s;
171 for (Index m = 0; m < B; ++m) {
172 const double a = factor * r[m] * r[m];
173 const double z = poles.energies(m) + PoleSign(m, poles.n_occ) * omega;
174#pragma omp simd
175 for (Index f = 0; f < F; ++f) {
176 out[f] += a * Kernel(x[f] - z, eta2);
177 }
178 }
179 }
180 return acc;
181}
182
183Eigen::VectorXd Values(const ExactPoles& poles, const double* residues,
184 const Eigen::VectorXd& w) {
185 return Values(poles, residues, w, nullptr);
186}
187
188Eigen::VectorXd Values(const ExactPoles& poles, const double* residues,
189 const Eigen::VectorXd& w, const double* mode_factors) {
190 const Index F = w.size();
191 if (F < min_batch_for_split) {
192 return ValuesDirect(poles, residues, w, mode_factors);
193 }
194 const double lo = w.minCoeff();
195 const double hi = w.maxCoeff();
196 const double c = 0.5 * (lo + hi);
197 const double h = 0.5 * (hi - lo);
198 if (!(h > 0.0)) {
199 return ValuesDirect(poles, residues, w, mode_factors);
200 }
201 const double eta2 = poles.eta * poles.eta;
202 const Index B = poles.nlevels();
203 const double inv_h = 1.0 / h;
204
205 // nodes and their angles per class
206 std::array<std::array<double, max_nodes>, n_classes> node{};
207 std::array<std::array<double, max_nodes>, n_classes> theta{};
208 for (int k = 0; k < n_classes; ++k) {
209 const Index n = class_nodes[std::size_t(k)];
210 for (Index j = 0; j < n; ++j) {
211 theta[std::size_t(k)][std::size_t(j)] =
212 M_PI * (double(j) + 0.5) / double(n);
213 node[std::size_t(k)][std::size_t(j)] =
214 c + h * std::cos(theta[std::size_t(k)][std::size_t(j)]);
215 }
216 }
217 std::array<std::array<double, max_nodes>, n_classes> far{};
218 Eigen::VectorXd acc = Eigen::VectorXd::Zero(F);
219 double* out = acc.data();
220 const double* x = w.data();
221 for (Index s = 0; s < poles.nmodes(); ++s) {
222 const double factor = (mode_factors == nullptr) ? 1.0 : mode_factors[s];
223 if (factor == 0.0) {
224 continue;
225 }
226 const double omega = poles.omegas(s);
227 const double* r = residues + B * s;
228 for (Index m = 0; m < B; ++m) {
229 const double a = factor * r[m] * r[m];
230 const double z = poles.energies(m) + PoleSign(m, poles.n_occ) * omega;
231 const double u = std::abs(z - c) * inv_h;
232 if (u < class_umin[0]) {
233#pragma omp simd
234 for (Index f = 0; f < F; ++f) {
235 out[f] += a * Kernel(x[f] - z, eta2);
236 }
237 continue;
238 }
239 const int k = (u >= class_umin[3]) ? 3
240 : (u >= class_umin[2]) ? 2
241 : (u >= class_umin[1]) ? 1
242 : 0;
243 const Index n = class_nodes[std::size_t(k)];
244 const double* nk = node[std::size_t(k)].data();
245 double* fk = far[std::size_t(k)].data();
246#pragma omp simd
247 for (Index j = 0; j < n; ++j) {
248 fk[j] += a * Kernel(nk[j] - z, eta2);
249 }
250 }
251 }
252 // per class: Chebyshev coefficients of the far part on [lo, hi], summed
253 // into one series of max_nodes terms, evaluated by Clenshaw
254 std::array<double, max_nodes> coef{};
255 for (int k = 0; k < n_classes; ++k) {
256 const Index n = class_nodes[std::size_t(k)];
257 for (Index j = 0; j < n; ++j) {
258 double sum = 0.0;
259 for (Index i = 0; i < n; ++i) {
260 sum += far[std::size_t(k)][std::size_t(i)] *
261 std::cos(double(j) * theta[std::size_t(k)][std::size_t(i)]);
262 }
263 coef[std::size_t(j)] += 2.0 * sum / double(n) * ((j == 0) ? 0.5 : 1.0);
264 }
265 }
266 for (Index f = 0; f < F; ++f) {
267 const double u = (x[f] - c) * inv_h;
268 double b1 = 0.0;
269 double b2 = 0.0;
270 for (Index j = max_nodes - 1; j >= 1; --j) {
271 const double b0 = 2.0 * u * b1 - b2 + coef[std::size_t(j)];
272 b2 = b1;
273 b1 = b0;
274 }
275 out[f] += u * b1 - b2 + coef[0];
276 }
277 return acc;
278}
279
280Eigen::MatrixXd OffDiagonal(const ExactPoles& poles,
281 const Eigen::MatrixXd& residues_all,
282 const Eigen::VectorXd& w) {
283 const Index P = residues_all.rows();
284 const Index Q = residues_all.cols();
285 const Index B = poles.nlevels();
286 const double eta2 = poles.eta * poles.eta;
287 // chunks of poles of about 32 MB of weighted residues each
288 const Index chunk =
289 std::max<Index>(256, Index(4e6 / double(std::max<Index>(Q, 1))));
290 const Index nchunks = (P + chunk - 1) / chunk;
291 Eigen::MatrixXd T = Eigen::MatrixXd::Zero(Q, Q);
292#pragma omp parallel
293 {
294 Eigen::MatrixXd T_local = Eigen::MatrixXd::Zero(Q, Q);
295 Eigen::MatrixXd weighted;
296#pragma omp for schedule(dynamic)
297 for (Index ic = 0; ic < nchunks; ++ic) {
298 const Index p0 = ic * chunk;
299 const Index np = std::min(chunk, P - p0);
300 // weighted(p, b) = r_b(p) f(w_b - z_p)
301 weighted.resize(np, Q);
302 for (Index b = 0; b < Q; ++b) {
303 const double wb = w(b);
304 for (Index i = 0; i < np; ++i) {
305 const Index p = p0 + i;
306 const Index m = p % B;
307 const Index s = p / B;
308 const double z =
309 poles.energies(m) + PoleSign(m, poles.n_occ) * poles.omegas(s);
310 weighted(i, b) = residues_all(p, b) * Kernel(wb - z, eta2);
311 }
312 }
313 // T(a, b) += sum_p r_a(p) r_b(p) f(w_b - z_p)
314 T_local.noalias() +=
315 residues_all.middleRows(p0, np).transpose() * weighted;
316 }
317#pragma omp critical
318 T += T_local;
319 }
320 Eigen::MatrixXd result = T + T.transpose();
321 result.diagonal().setZero();
322 return result;
323}
324
325} // namespace exact_pole_sums
326} // namespace xtp
327} // namespace votca
double Value(const ExactPoles &poles, const double *residues, double w)
S_n(w) at one frequency.
double Derivative(const ExactPoles &poles, const double *residues, double w)
dS_n/dw at one frequency
Eigen::VectorXd ValuesDirect(const ExactPoles &poles, const double *residues, const Eigen::VectorXd &w)
The same sum term by term at every frequency (reference for tests).
Eigen::MatrixXd OffDiagonal(const ExactPoles &poles, const Eigen::MatrixXd &residues_all, const Eigen::VectorXd &w)
Eigen::VectorXd Values(const ExactPoles &poles, const double *residues, const Eigen::VectorXd &w)
Eigen::VectorXd ValuesDirectWeighted(const ExactPoles &poles, const double *weights, const Eigen::VectorXd &w)
sum_p a_p f(w - z_p) with given weights a_p (e.g. r_n1 r_n2)
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
Pole sums of the exact (RPA spectral) correlation self-energy.
const Eigen::VectorXd & energies
const Eigen::VectorXd & omegas