votca 2026-dev
Loading...
Searching...
No Matches
qp_solver_utils.h
Go to the documentation of this file.
1/*
2 * Copyright 2009-2023 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#pragma once
21#ifndef VOTCA_XTP_QP_SOLVER_UTILS_H
22#define VOTCA_XTP_QP_SOLVER_UTILS_H
23
24#include <algorithm>
25#include <cmath>
26#include <cstddef>
27#include <cstdio>
28#include <limits>
29#include <stdexcept>
30#include <string>
31#include <vector>
32
33#include <boost/optional.hpp>
34
35// Local VOTCA includes
36#include "eigen.h"
37
38namespace votca {
39namespace xtp {
40namespace qp_solver {
41
42// Batched evaluation hook: a QP function may offer Prefetch(frequencies) to
43// evaluate several points at once; others are evaluated point by point.
44template <typename QPFunc>
45auto PrefetchIfAvailable(const QPFunc& fqp, const std::vector<double>& nodes,
46 int) -> decltype(fqp.Prefetch(nodes), void()) {
47 fqp.Prefetch(nodes);
48}
49template <typename QPFunc>
50void PrefetchIfAvailable(const QPFunc&, const std::vector<double>&, long) {}
51
53
80
82 double omega = 0.0;
83 double residual = 0.0;
84 double deriv = 0.0;
85 double Z = 0.0;
86 double distance_to_ref = 0.0;
87 bool accepted = false;
88};
89
97
99 double g_sc_limit = 1e-5;
101
102 // The new grid-search design separates four previously entangled concerns:
103 // (1) full QP search extent
104 // (2) dense sign-change detection spacing
105 // (3) adaptive shell spacing / shell count
106 // (4) interval refinement method (configured elsewhere)
107 // This makes the search numerics easier to reason about and allows robust
108 // dense fallback without forcing the adaptive scan to inherit the same mesh.
109 //
110 // qp_full_window_half_width defines the full symmetric interval around the
111 // current reference frequency that can be searched when no narrower window
112 // is imposed by the caller.
114
115 // qp_dense_spacing is only for the robust dense scan that detects sign
116 // changes before interval refinement. It is intentionally independent from
117 // the adaptive search spacing.
118 double qp_dense_spacing = 0.002;
119
120 // The adaptive search starts at the reference energy and expands outward in
121 // shells. The shell spacing can be set directly or derived from a requested
122 // number of shells per half-window. This search is meant to find the most
123 // relevant root early, while the dense scan remains the robust fallback.
125 Index qp_adaptive_shell_count = 0; // 0 -> use width
126
127 double min_accepted_Z = 0.05;
128 double max_accepted_Z = 1.5;
129
130 // Among accepted roots take the one closest to the reference frequency
131 // (the previous iteration's solution) instead of the largest weight; set
132 // for evGW iterations after the first with gw.qp_root_continuity.
134};
135
136// Legacy mapping helpers preserve the old qp_grid_steps / qp_grid_spacing
137// behavior for existing XML files and tests. They are translation utilities
138// only; the actual search code below works exclusively with the decoupled
139// canonical options.
140template <typename Opt>
141inline double LegacyFullWindowHalfWidth(const Opt& opt) {
142 if (opt.qp_grid_steps <= 1 || opt.qp_grid_spacing <= 0.0) {
143 return -1.0;
144 }
145 return 0.5 * opt.qp_grid_spacing * double(opt.qp_grid_steps - 1);
146}
147
148template <typename Opt>
149inline double LegacyAdaptiveShellWidth(const Opt& opt) {
150 if (opt.qp_grid_steps <= 1 || opt.qp_grid_spacing <= 0.0) {
151 return -1.0;
152 }
153
154 const double full_window_width =
155 opt.qp_grid_spacing * double(opt.qp_grid_steps - 1);
156
157 // Reconstruct the historical "coarse" spacing as closely as possible.
158 // The hard-coded 21-point floor is legacy behavior from the original
159 // implementation and intentionally remains confined to this compatibility
160 // path instead of the new adaptive search itself.
161 const Index base_coarse_steps = std::max<Index>(21, opt.qp_grid_steps / 4);
162
163 if (base_coarse_steps <= 1) {
164 return 4.0 * opt.qp_grid_spacing;
165 }
166
167 return full_window_width / double(base_coarse_steps - 1);
168}
169
170// NormalizeGridSearchOptions is the single entry point that converts a mix of
171// new and legacy settings into one canonical set of search controls. New-style
172// options always take precedence; legacy qp_grid_steps / qp_grid_spacing only
173// fill values that were left unset by the caller.
174template <typename Opt>
175inline void NormalizeGridSearchOptions(Opt& opt) {
176 const bool has_legacy = (opt.qp_grid_steps > 1 && opt.qp_grid_spacing > 0.0);
177
178 if (opt.qp_full_window_half_width <= 0.0) {
179 opt.qp_full_window_half_width =
180 has_legacy ? LegacyFullWindowHalfWidth(opt) : 0.75;
181 }
182
183 if (opt.qp_dense_spacing <= 0.0) {
184 opt.qp_dense_spacing = has_legacy ? opt.qp_grid_spacing : 0.002;
185 }
186
187 if (opt.qp_adaptive_shell_count <= 0 && opt.qp_adaptive_shell_width <= 0.0) {
188 opt.qp_adaptive_shell_width =
189 has_legacy ? LegacyAdaptiveShellWidth(opt) : 0.025;
190 }
191
192 if (opt.qp_full_window_half_width <= 0.0) {
193 throw std::runtime_error(
194 "Invalid QP search setup: qp_full_window_half_width must be > 0");
195 }
196
197 if (opt.qp_dense_spacing <= 0.0) {
198 throw std::runtime_error(
199 "Invalid QP search setup: qp_dense_spacing must be > 0");
200 }
201
202 if (opt.qp_adaptive_shell_count <= 0 && opt.qp_adaptive_shell_width <= 0.0) {
203 throw std::runtime_error(
204 "Invalid QP search setup: need qp_adaptive_shell_width > 0 or "
205 "qp_adaptive_shell_count > 0");
206 }
207}
208
209// Convert either an explicit shell width or an explicit shell count into the
210// actual shell spacing used by the adaptive outward scan.
212 if (opt.qp_adaptive_shell_count > 0) {
213 return opt.qp_full_window_half_width /
214 static_cast<double>(opt.qp_adaptive_shell_count);
215 }
216 return opt.qp_adaptive_shell_width;
217}
218
225inline double RootBracketTolerance(const SolverOptions& opt) {
226 return 0.5 * std::max(opt.min_accepted_Z, 1e-3) * opt.g_sc_limit;
227}
228
229template <typename QPFunc>
230double SolveQP_Bisection(double lowerbound, double f_lowerbound,
231 double upperbound, double f_upperbound,
232 const QPFunc& f, const SolverOptions& opt) {
233 if (f_lowerbound * f_upperbound > 0.0) {
234 throw std::runtime_error(
235 "Bisection needs a positive and negative function value");
236 }
237
238 // Halving reaches xtol after log2(width / xtol) steps (about 20 for the
239 // brackets of the grid search). The cap only matters if the bracket cannot
240 // shrink below xtol (non-finite values, xtol below the spacing of doubles);
241 // the midpoint returned then fails AcceptRoot instead of looping forever.
242 const double xtol = RootBracketTolerance(opt);
243 const Index max_iter = std::max<Index>(opt.qp_bisection_max_iter, 100);
244 for (Index iter = 0; iter < max_iter; ++iter) {
245 const double c = 0.5 * (lowerbound + upperbound);
246 if (std::abs(upperbound - lowerbound) < xtol) {
247 return c;
248 }
249
250 const double y_c = f.value(c, EvalStage::Refine);
251 if (std::abs(y_c) < opt.g_sc_limit) {
252 return c;
253 }
254
255 if (y_c * f_lowerbound > 0.0) {
256 lowerbound = c;
257 f_lowerbound = y_c;
258 } else {
259 upperbound = c;
260 f_upperbound = y_c;
261 }
262 }
263 return 0.5 * (lowerbound + upperbound);
264}
265
266template <typename QPFunc>
267double SolveQP_Brent(double lowerbound, double f_lowerbound, double upperbound,
268 double f_upperbound, const QPFunc& f,
269 const SolverOptions& opt) {
270 if (f_lowerbound * f_upperbound > 0.0) {
271 throw std::runtime_error(
272 "Brent needs a positive and negative function value");
273 }
274
275 double a = lowerbound;
276 double b = upperbound;
277 double fa = f_lowerbound;
278 double fb = f_upperbound;
279
280 // c is the previous bracket endpoint
281 double c = a;
282 double fc = fa;
283
284 // d and e track the last and second-last step sizes
285 double d = b - a;
286 double e = d;
287
288 for (Index iter = 0; iter < opt.qp_bisection_max_iter; ++iter) {
289
290 // Ensure that b is the best current estimate
291 if ((fb > 0.0 && fc > 0.0) || (fb < 0.0 && fc < 0.0)) {
292 c = a;
293 fc = fa;
294 d = b - a;
295 e = d;
296 }
297
298 if (std::abs(fc) < std::abs(fb)) {
299 a = b;
300 b = c;
301 c = a;
302 fa = fb;
303 fb = fc;
304 fc = fa;
305 }
306
307 // stop on the acceptance criterion |f| < g_sc_limit, or once the
308 // bracket [b, c] is narrower than RootBracketTolerance (b is returned,
309 // an end of the bracket, so its distance to the root is below that)
310 const double tol = 0.5 * RootBracketTolerance(opt);
311 const double m = 0.5 * (c - b);
312
313 if (std::abs(m) < tol || std::abs(fb) < opt.g_sc_limit) {
314 return b;
315 }
316
317 if (std::abs(e) >= tol && std::abs(fa) > std::abs(fb)) {
318 // Attempt inverse interpolation
319 double s = fb / fa;
320 double p = 0.0;
321 double q = 0.0;
322
323 if (a == c) {
324 // Secant step
325 p = 2.0 * m * s;
326 q = 1.0 - s;
327 } else {
328 // Inverse quadratic interpolation
329 double q1 = fa / fc;
330 double r = fb / fc;
331 p = s * (2.0 * m * q1 * (q1 - r) - (b - a) * (r - 1.0));
332 q = (q1 - 1.0) * (r - 1.0) * (s - 1.0);
333 }
334
335 if (p > 0.0) {
336 q = -q;
337 }
338 p = std::abs(p);
339
340 // Accept interpolation only if it is safe
341 if (q != 0.0 && 2.0 * p < std::min(3.0 * m * q - std::abs(tol * q),
342 std::abs(e * q))) {
343 e = d;
344 d = p / q;
345 } else {
346 d = m;
347 e = m;
348 }
349 } else {
350 d = m;
351 e = m;
352 }
353
354 a = b;
355 fa = fb;
356
357 if (std::abs(d) > tol) {
358 b += d;
359 } else {
360 b += (m > 0.0 ? tol : -tol);
361 }
362
363 fb = f.value(b, EvalStage::Refine);
364 }
365
366 // not converged (e.g. a pole instead of a root inside the bracket): the
367 // best estimate is returned and judged by AcceptRoot like any other
368 return b;
369}
370
371inline bool AcceptRoot(const RootCandidate& cand, const SolverOptions& opt) {
372 if (!std::isfinite(cand.omega) || !std::isfinite(cand.Z)) {
373 return false;
374 }
375
376 if (std::abs(cand.residual) > opt.g_sc_limit) {
377 return false;
378 }
379
380 if (cand.Z <= 0.0) {
381 return false;
382 }
383
384 if (cand.Z < opt.min_accepted_Z) {
385 return false;
386 }
387
388 if (cand.Z > opt.max_accepted_Z) {
389 return false;
390 }
391
392 return true;
393}
394
395inline double ScoreRoot(const RootCandidate& cand) {
396 return cand.Z - 0.1 * cand.distance_to_ref;
397}
398
401inline const RootCandidate& SelectRoot(const std::vector<RootCandidate>& roots,
402 const SolverOptions& opt) {
403 if (opt.prefer_nearest_root) {
404 return *std::min_element(
405 roots.begin(), roots.end(),
406 [](const RootCandidate& a, const RootCandidate& b) {
407 return a.distance_to_ref < b.distance_to_ref;
408 });
409 }
410 return *std::max_element(roots.begin(), roots.end(),
411 [](const RootCandidate& a, const RootCandidate& b) {
412 return ScoreRoot(a) < ScoreRoot(b);
413 });
414}
415
420 bool competing = false;
421 double omega = 0.0; // chosen root
422 double Z = 0.0;
423 double omega_alt = 0.0; // the largest-weight other root
424 double Z_alt = 0.0;
425};
426
428 const std::vector<RootCandidate>& roots, double chosen) {
429 CompetingRoots result;
430 if (roots.size() < 2) {
431 return result;
432 }
433 std::vector<RootCandidate> sorted = roots;
434 std::sort(
435 sorted.begin(), sorted.end(),
436 [](const RootCandidate& a, const RootCandidate& b) { return a.Z > b.Z; });
437 if (sorted[1].Z < 0.5 * sorted[0].Z) {
438 return result;
439 }
440 result.competing = true;
441 for (const RootCandidate& r : sorted) {
442 if (r.omega == chosen) {
443 result.omega = r.omega;
444 result.Z = r.Z;
445 } else if (result.Z_alt == 0.0) {
446 result.omega_alt = r.omega;
447 result.Z_alt = r.Z;
448 }
449 }
450 return result;
451}
452
453template <typename QPFunc>
454boost::optional<RootCandidate> RefineQPInterval(
455 double lowerbound, double f_lowerbound, double upperbound,
456 double f_upperbound, const QPFunc& f, double reference,
457 const SolverOptions& opt, bool use_brent) {
458 RootCandidate cand;
459
460 if (!std::isfinite(lowerbound) || !std::isfinite(upperbound) ||
461 !std::isfinite(f_lowerbound) || !std::isfinite(f_upperbound)) {
462 return boost::none;
463 }
464
465 // Near-exact-zero brackets: one endpoint is already essentially at the root.
466 // Bisection and Brent both require opposite signs; handle this case directly
467 // by returning whichever endpoint is closer to zero. This occurs when the
468 // local mini-scan inside refine_and_store finds a near-zero node and inserts
469 // a same-sign bracket -- those brackets are valid root indicators but cannot
470 // be refined by interval methods.
471 const bool left_near_zero = std::abs(f_lowerbound) <= opt.g_sc_limit;
472 const bool right_near_zero = std::abs(f_upperbound) <= opt.g_sc_limit;
473 const bool same_sign = (f_lowerbound * f_upperbound > 0.0);
474
475 if (same_sign) {
476 if (left_near_zero || right_near_zero) {
477 // Pick the endpoint closest to zero as the root estimate
478 cand.omega = (std::abs(f_lowerbound) <= std::abs(f_upperbound))
479 ? lowerbound
480 : upperbound;
481 } else {
482 // No bracket and no near-zero endpoint -- cannot refine, skip silently
483 return boost::none;
484 }
485 } else {
486 cand.omega = use_brent
487 ? SolveQP_Brent(lowerbound, f_lowerbound, upperbound,
488 f_upperbound, f, opt)
489 : SolveQP_Bisection(lowerbound, f_lowerbound, upperbound,
490 f_upperbound, f, opt);
491 }
492
493 cand.residual = f.value(cand.omega, EvalStage::Refine);
494 cand.deriv = f.deriv(cand.omega);
495
496 if (std::abs(cand.deriv) > 1e-14) {
497 cand.Z = -1.0 / cand.deriv;
498 } else {
499 cand.Z = std::numeric_limits<double>::infinity();
500 }
501
502 cand.distance_to_ref = std::abs(cand.omega - reference);
503 cand.accepted = AcceptRoot(cand, opt);
504
505 cand.accepted = AcceptRoot(cand, opt);
506
507 /*std::cout << "DEBUG ROOT "
508 << "omega=" << cand.omega
509 << " residual=" << cand.residual
510 << " deriv=" << cand.deriv
511 << " Z=" << cand.Z
512 << " accepted=" << cand.accepted
513 << std::endl;*/
514 return cand;
515}
516
523template <typename QPFunc>
524boost::optional<RootCandidate> SolveQP_Tracked(
525 QPFunc& fqp, double center, double halfwidth, double Z_previous,
526 double min_Z_fraction, const SolverOptions& opt, bool use_brent = false) {
527 const double spacing = opt.qp_dense_spacing;
528 const Index n_steps = std::max<Index>(
529 2, static_cast<Index>(std::ceil(2.0 * halfwidth / spacing)) + 1);
530 const double left = center - halfwidth;
531 const double dx = 2.0 * halfwidth / static_cast<double>(n_steps - 1);
532 std::vector<double> nodes(static_cast<std::size_t>(n_steps));
533 for (Index i = 0; i < n_steps; ++i) {
534 nodes[std::size_t(i)] = left + static_cast<double>(i) * dx;
535 }
536 PrefetchIfAvailable(fqp, nodes, 0);
537
538 boost::optional<RootCandidate> best;
539 double f_prev = fqp.value(nodes[0], EvalStage::Scan);
540 for (Index i = 1; i < n_steps; ++i) {
541 const double x = nodes[std::size_t(i)];
542 const double f = fqp.value(x, EvalStage::Scan);
543 if (f_prev * f <= 0.0) {
544 auto cand = RefineQPInterval(nodes[std::size_t(i - 1)], f_prev, x, f, fqp,
545 center, opt, use_brent);
546 if (cand && cand->accepted && cand->Z >= min_Z_fraction * Z_previous &&
547 (!best || cand->distance_to_ref < best->distance_to_ref)) {
548 best = cand;
549 }
550 }
551 f_prev = f;
552 }
553 return best;
554}
555
556template <typename QPFunc>
557boost::optional<double> SolveQP_Grid_Windowed(
558 QPFunc& fqp, double frequency0, double left_limit, double right_limit,
559 Index gw_sc_iteration, const SolverOptions& opt,
560 WindowDiagnostics* wdiag = nullptr,
561 std::vector<RootCandidate>* accepted_roots_out = nullptr,
562 std::vector<RootCandidate>* rejected_roots_out = nullptr,
563 bool use_brent = false) {
564 struct SamplePoIndex {
565 double omega = 0.0;
566 double fval = 0.0;
567 };
568
569 WindowDiagnostics local_diag;
570 std::vector<RootCandidate> accepted_roots;
571 std::vector<RootCandidate> rejected_roots;
572
573 if (left_limit >= right_limit) {
574 if (wdiag != nullptr) {
575 *wdiag = local_diag;
576 }
577 if (accepted_roots_out != nullptr) {
578 *accepted_roots_out = accepted_roots;
579 }
580 if (rejected_roots_out != nullptr) {
581 *rejected_roots_out = rejected_roots;
582 }
583 return boost::none;
584 }
585
586 const double shell_width = EffectiveAdaptiveShellWidth(opt);
587
588 // The adaptive search is centered on the current reference frequency. On
589 // the first GW iteration, a local linearized estimate is used when it stays
590 // inside the requested window; later iterations keep the current frequency as
591 // the center. From that center, the scan expands symmetrically in shells.
592 double center = frequency0;
593
594 if (gw_sc_iteration == 0) {
595 const double f0 = fqp.value(frequency0, EvalStage::Other);
596 const double df0 = fqp.deriv(frequency0);
597 if (std::isfinite(f0) && std::isfinite(df0) && std::abs(df0) > 1e-6) {
598 const double w_lin = frequency0 - f0 / df0;
599 if (std::isfinite(w_lin) && w_lin >= left_limit && w_lin <= right_limit) {
600 center = w_lin;
601 }
602 }
603 }
604
605 center = std::max(left_limit, std::min(right_limit, center));
606
607 // Compute how many shells are needed to reach both limits from the chosen
608 // center. Unlike the legacy implementation, this depends only on the
609 // explicit adaptive shell control, not on the dense scan spacing.
610 const double max_shell_reach =
611 std::max(center - left_limit, right_limit - center);
612 const Index n_shells =
613 static_cast<Index>(std::ceil(max_shell_reach / shell_width));
614
615 auto refine_and_store = [&](double a, double fa, double b, double fb,
616 Index shell_idx) {
617 if (b < a) {
618 std::swap(a, b);
619 std::swap(fa, fb);
620 }
621
622 // Deterministic local mini-scan inside the coarse sign-change interval.
623 // This restores much of the old dense-grid robustness while keeping the
624 // fast coarse search globally.
625 struct LocalBracket {
626 double left = 0.0;
627 double f_left = 0.0;
628 double right = 0.0;
629 double f_right = 0.0;
630 double midpoint() const { return 0.5 * (left + right); }
631 };
632
633 const Index local_substeps = 12;
634 std::vector<LocalBracket> local_brackets;
635 local_brackets.reserve(static_cast<std::size_t>(local_substeps));
636
637 if (b > a) {
638 const double dx = (b - a) / static_cast<double>(local_substeps);
639 {
640 std::vector<double> nodes;
641 for (Index i = 1; i < local_substeps; ++i) {
642 nodes.push_back(a + static_cast<double>(i) * dx);
643 }
644 PrefetchIfAvailable(fqp, nodes, 0);
645 }
646
647 double x_prev = a;
648 double f_prev = fa;
649
650 for (Index i = 1; i <= local_substeps; ++i) {
651 const double x_curr =
652 (i == local_substeps) ? b : (a + static_cast<double>(i) * dx);
653 const double f_curr =
654 (i == local_substeps) ? fb : fqp.value(x_curr, EvalStage::Scan);
655
656 // Standard sign change
657 if ((f_prev < 0.0 && f_curr > 0.0) || (f_prev > 0.0 && f_curr < 0.0)) {
658 local_brackets.push_back({x_prev, f_prev, x_curr, f_curr});
659 }
660
661 // Near-exact zero at the left node
662 if (std::abs(f_prev) <= opt.g_sc_limit && x_prev < x_curr) {
663 local_brackets.push_back({x_prev, f_prev, x_curr, f_curr});
664 }
665
666 // Near-exact zero at the right node
667 if (std::abs(f_curr) <= opt.g_sc_limit && x_prev < x_curr) {
668 local_brackets.push_back({x_prev, f_prev, x_curr, f_curr});
669 }
670
671 x_prev = x_curr;
672 f_prev = f_curr;
673 }
674 }
675
676 // Choose the local bracket deterministically:
677 // midpoint closest to the search center, tie-broken by lower left edge.
678 if (!local_brackets.empty()) {
679 auto best_it = local_brackets.begin();
680 double best_dist = std::abs(best_it->midpoint() - center);
681
682 for (auto it = local_brackets.begin() + 1; it != local_brackets.end();
683 ++it) {
684 const double dist = std::abs(it->midpoint() - center);
685 if (dist < best_dist - 1e-14 ||
686 (std::abs(dist - best_dist) <= 1e-14 && it->left < best_it->left)) {
687 best_it = it;
688 best_dist = dist;
689 }
690 }
691
692 a = best_it->left;
693 fa = best_it->f_left;
694 b = best_it->right;
695 fb = best_it->f_right;
696 }
697
698 auto cand_opt =
699 RefineQPInterval(a, fa, b, fb, fqp, frequency0, opt, use_brent);
700 if (!cand_opt) {
701 return;
702 }
703
704 if (local_diag.first_interval_shell < 0) {
705 local_diag.first_interval_shell = shell_idx;
706 }
707 ++local_diag.intervals_found;
708
709 const RootCandidate& cand = cand_opt.value();
710 if (cand.accepted) {
711 if (local_diag.first_accepted_shell < 0) {
712 local_diag.first_accepted_shell = shell_idx;
713 }
714 accepted_roots.push_back(cand);
715 } else {
716 rejected_roots.push_back(cand);
717 }
718 };
719
720 // All shell points are known in advance: evaluate them in one batch.
721 {
722 std::vector<double> nodes{center};
723 for (Index shell = 1; shell <= n_shells; ++shell) {
724 const double delta = double(shell) * shell_width;
725 if (center - delta >= left_limit) {
726 nodes.push_back(center - delta);
727 }
728 if (center + delta <= right_limit) {
729 nodes.push_back(center + delta);
730 }
731 }
732 nodes.push_back(left_limit);
733 nodes.push_back(right_limit);
734 PrefetchIfAvailable(fqp, nodes, 0);
735 }
736
737 SamplePoIndex center_pt{center, fqp.value(center, EvalStage::Scan)};
738
739 bool left_active = true;
740 bool right_active = true;
741 SamplePoIndex left_prev = center_pt;
742 SamplePoIndex right_prev = center_pt;
743
744 for (Index shell = 1; shell <= n_shells; ++shell) {
745 local_diag.shells_explored = shell;
746 bool added_this_shell = false;
747 const double delta = double(shell) * shell_width;
748
749 // Each shell adds one point to the left and one to the right of the
750 // center. Whenever a sign change is detected between neighboring samples,
751 // that interval is refined by bisection or Brent and the resulting root is
752 // classified as accepted or rejected using the Z-based acceptance test.
753
754 if (left_active) {
755 const double omega_left = center - delta;
756 if (omega_left >= left_limit) {
757 SamplePoIndex left_curr{omega_left,
758 fqp.value(omega_left, EvalStage::Scan)};
759 added_this_shell = true;
760
761 if (left_prev.fval * left_curr.fval < 0.0) {
762 refine_and_store(left_curr.omega, left_curr.fval, left_prev.omega,
763 left_prev.fval, shell);
764 }
765 left_prev = left_curr;
766 } else {
767 left_active = false;
768 }
769 }
770
771 if (right_active) {
772 const double omega_right = center + delta;
773 if (omega_right <= right_limit) {
774 SamplePoIndex right_curr{omega_right,
775 fqp.value(omega_right, EvalStage::Scan)};
776 added_this_shell = true;
777
778 if (right_prev.fval * right_curr.fval < 0.0) {
779 refine_and_store(right_prev.omega, right_prev.fval, right_curr.omega,
780 right_curr.fval, shell);
781 }
782 right_prev = right_curr;
783 } else {
784 right_active = false;
785 }
786 }
787
788 if (!added_this_shell && !left_active && !right_active) {
789 break;
790 }
791 }
792
793 if (left_prev.omega > left_limit + 1e-12) {
794 SamplePoIndex left_end{left_limit, fqp.value(left_limit, EvalStage::Scan)};
795 if (left_end.fval * left_prev.fval < 0.0) {
796 refine_and_store(left_end.omega, left_end.fval, left_prev.omega,
797 left_prev.fval, local_diag.shells_explored + 1);
798 }
799 }
800
801 if (right_prev.omega < right_limit - 1e-12) {
802 SamplePoIndex right_end{right_limit,
803 fqp.value(right_limit, EvalStage::Scan)};
804 if (right_prev.fval * right_end.fval < 0.0) {
805 refine_and_store(right_prev.omega, right_prev.fval, right_end.omega,
806 right_end.fval, local_diag.shells_explored + 1);
807 }
808 }
809
810 // Preference order for the adaptive search:
811 // 1. return the best accepted root found in the scanned shells
812 // 2. if the caller allows it, return the best rejected root
813 // The outer GW driver uses this to enforce that restricted-window searches
814 // only succeed on accepted roots; otherwise it escalates to a full dense
815 // scan.
816 if (!accepted_roots.empty()) {
817 const RootCandidate* best = &SelectRoot(accepted_roots, opt);
818
819 local_diag.chosen_shell = static_cast<int>(
820 std::llround(std::abs(best->omega - center) / shell_width));
821
822 if (wdiag != nullptr) {
823 *wdiag = local_diag;
824 }
825 if (accepted_roots_out != nullptr) {
826 *accepted_roots_out = accepted_roots;
827 }
828 if (rejected_roots_out != nullptr) {
829 *rejected_roots_out = rejected_roots;
830 }
831 return best->omega;
832 }
833
834 if (!rejected_roots.empty()) {
835 auto least_bad =
836 std::max_element(rejected_roots.begin(), rejected_roots.end(),
837 [](const RootCandidate& a, const RootCandidate& b) {
838 return ScoreRoot(a) < ScoreRoot(b);
839 });
840
841 local_diag.chosen_shell = static_cast<int>(
842 std::llround(std::abs(least_bad->omega - center) / shell_width));
843
844 if (wdiag != nullptr) {
845 *wdiag = local_diag;
846 }
847 if (accepted_roots_out != nullptr) {
848 *accepted_roots_out = accepted_roots;
849 }
850 if (rejected_roots_out != nullptr) {
851 *rejected_roots_out = rejected_roots;
852 }
853 return least_bad->omega;
854 }
855
856 if (wdiag != nullptr) {
857 *wdiag = local_diag;
858 }
859 if (accepted_roots_out != nullptr) {
860 *accepted_roots_out = accepted_roots;
861 }
862 if (rejected_roots_out != nullptr) {
863 *rejected_roots_out = rejected_roots;
864 }
865 return boost::none;
866}
867
871inline boost::optional<double> LinearisedRoot(double w0, double f0,
872 double fprime) {
873 if (std::abs(fprime) <= 1e-9) {
874 return boost::none;
875 }
876 return w0 - f0 / fprime;
877}
878
882inline std::string LargestResiduals(const Eigen::VectorXd& solution,
883 const Eigen::VectorXd& input,
884 Index first_level, Index n = 5) {
885 const Index size = std::min(solution.size(), input.size());
886 std::vector<Index> order(static_cast<std::size_t>(size));
887 for (Index i = 0; i < size; ++i) {
888 order[std::size_t(i)] = i;
889 }
890 const Index shown = std::min(n, size);
891 std::partial_sort(order.begin(), order.begin() + shown, order.end(),
892 [&](Index a, Index b) {
893 return std::abs(solution(a) - input(a)) >
894 std::abs(solution(b) - input(b));
895 });
896 std::string out;
897 char buffer[96];
898 for (Index k = 0; k < shown; ++k) {
899 const Index i = order[std::size_t(k)];
900 std::snprintf(buffer, sizeof(buffer), "%s%ld: %+.6f -> %+.6f (%+.1e)",
901 (k == 0) ? "" : ", ", static_cast<long>(i + first_level),
902 input(i), solution(i), solution(i) - input(i));
903 out += buffer;
904 }
905 return out;
906}
907
908} // namespace qp_solver
909} // namespace xtp
910} // namespace votca
911
912#endif
boost::optional< RootCandidate > SolveQP_Tracked(QPFunc &fqp, double center, double halfwidth, double Z_previous, double min_Z_fraction, const SolverOptions &opt, bool use_brent=false)
double LegacyAdaptiveShellWidth(const Opt &opt)
CompetingRoots CheckCompetingRoots(const std::vector< RootCandidate > &roots, double chosen)
boost::optional< double > SolveQP_Grid_Windowed(QPFunc &fqp, double frequency0, double left_limit, double right_limit, Index gw_sc_iteration, const SolverOptions &opt, WindowDiagnostics *wdiag=nullptr, std::vector< RootCandidate > *accepted_roots_out=nullptr, std::vector< RootCandidate > *rejected_roots_out=nullptr, bool use_brent=false)
bool AcceptRoot(const RootCandidate &cand, const SolverOptions &opt)
auto PrefetchIfAvailable(const QPFunc &fqp, const std::vector< double > &nodes, int) -> decltype(fqp.Prefetch(nodes), void())
double ScoreRoot(const RootCandidate &cand)
std::string LargestResiduals(const Eigen::VectorXd &solution, const Eigen::VectorXd &input, Index first_level, Index n=5)
void NormalizeGridSearchOptions(Opt &opt)
double SolveQP_Brent(double lowerbound, double f_lowerbound, double upperbound, double f_upperbound, const QPFunc &f, const SolverOptions &opt)
double RootBracketTolerance(const SolverOptions &opt)
double SolveQP_Bisection(double lowerbound, double f_lowerbound, double upperbound, double f_upperbound, const QPFunc &f, const SolverOptions &opt)
boost::optional< double > LinearisedRoot(double w0, double f0, double fprime)
const RootCandidate & SelectRoot(const std::vector< RootCandidate > &roots, const SolverOptions &opt)
boost::optional< RootCandidate > RefineQPInterval(double lowerbound, double f_lowerbound, double upperbound, double f_upperbound, const QPFunc &f, double reference, const SolverOptions &opt, bool use_brent)
double LegacyFullWindowHalfWidth(const Opt &opt)
double EffectiveAdaptiveShellWidth(const SolverOptions &opt)
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26
void Add(const Stats &other)
std::size_t TotalSigmaCalls() const