|
| 1 | +/* |
| 2 | + * Copyright 2026 NWChemEx-Project |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +#pragma once |
| 18 | +#include <algorithm> |
| 19 | +#include <cmath> |
| 20 | +#include <cstddef> |
| 21 | +#include <simde/simde.hpp> |
| 22 | +#include <span> |
| 23 | +#include <tensorwrapper/tensorwrapper.hpp> |
| 24 | +#include <utility> |
| 25 | +#include <vector> |
| 26 | + |
| 27 | +namespace scf::eigen_solver { |
| 28 | +namespace detail { |
| 29 | + |
| 30 | +/** @brief Applies the first-order eigenvector correction to @p C in place. |
| 31 | + * |
| 32 | + * Operates on the raw element spans of the eigenvectors @p C (columns, |
| 33 | + * row-major n by n), the MO-basis Fock @p G = C^T F C (row-major n by n), and |
| 34 | + * the eigenvalues @p eps (length n). For each column i it adds |
| 35 | + * |
| 36 | + * dc_i = sum_{j : |e_i - e_j| > 2*sigma} G(j,i) / (e_i - e_j) * c_j , |
| 37 | + * |
| 38 | + * where G(j,i) is the off-diagonal coupling v_j^T (delta F) v_i evaluated in |
| 39 | + * native UQ arithmetic (so the spread shares F's error symbols) and the gap |
| 40 | + * divisor uses centers only. sigma is the max abs row-sum of the off-diagonal |
| 41 | + * coupling radii; couplings across gaps <= 2*sigma are dropped |
| 42 | + * (near-degenerate block: gauge freedom + avoids an ill-conditioned ~0 |
| 43 | + * divisor). |
| 44 | + * |
| 45 | + * Runs for every UQ type, intervals included. This is sound here because the |
| 46 | + * correction is applied ONCE to a converged solution and only reported -- it |
| 47 | + * is not propagated back through the SCF iterations, so the interval dependency |
| 48 | + * problem cannot compound it. |
| 49 | + */ |
| 50 | +struct EigenvectorUncertaintyKernel { |
| 51 | + std::size_t m_n; |
| 52 | + const tensorwrapper::buffer::Contiguous& m_C; |
| 53 | + const tensorwrapper::buffer::Contiguous& m_G; |
| 54 | + const tensorwrapper::buffer::Contiguous& m_eps; |
| 55 | + |
| 56 | + // Dispatches on the (writable) output buffer; the inputs C, G, eps share |
| 57 | + // its runtime type and are pulled with the resolved type. This avoids a |
| 58 | + // cartesian-product instantiation over every buffer's type. |
| 59 | + template<typename FloatType> |
| 60 | + void operator()(std::span<FloatType> out) { |
| 61 | + using clean_t = std::decay_t<FloatType>; |
| 62 | + // The dispatch instantiates const-qualified spans too; the writing body |
| 63 | + // is only valid (and only selected at runtime) for the mutable buffer. |
| 64 | + if constexpr(!std::is_const_v<FloatType> && |
| 65 | + tensorwrapper::types::is_uq_type_v<clean_t>) { |
| 66 | + using tensorwrapper::buffer::get_raw_data; |
| 67 | + using tensorwrapper::types::uq_center; |
| 68 | + using tensorwrapper::types::uq_upper; |
| 69 | + using value_t = decltype(uq_center(std::declval<clean_t>())); |
| 70 | + const auto n = m_n; |
| 71 | + auto radius = [](const clean_t& x) { |
| 72 | + return uq_upper(x) - uq_center(x); |
| 73 | + }; |
| 74 | + |
| 75 | + auto C = get_raw_data<clean_t>(m_C); |
| 76 | + auto G = get_raw_data<clean_t>(m_G); |
| 77 | + auto eps = get_raw_data<clean_t>(m_eps); |
| 78 | + |
| 79 | + // Degeneracy threshold: max abs row-sum of off-diagonal coupling |
| 80 | + // radii. |
| 81 | + value_t sigma(0); |
| 82 | + for(std::size_t i = 0; i < n; ++i) { |
| 83 | + value_t row(0); |
| 84 | + for(std::size_t j = 0; j < n; ++j) { |
| 85 | + if(i != j) { row += radius(G[j * n + i]); } |
| 86 | + } |
| 87 | + sigma = std::max(sigma, row); |
| 88 | + } |
| 89 | + |
| 90 | + // Eigenvalue centers. |
| 91 | + std::vector<value_t> eps_c(n); |
| 92 | + for(std::size_t i = 0; i < n; ++i) { eps_c[i] = uq_center(eps[i]); } |
| 93 | + |
| 94 | + // out = C + correction, accumulated from the ORIGINAL columns so |
| 95 | + // one column's correction never feeds into another. |
| 96 | + for(std::size_t k = 0; k < n * n; ++k) { out[k] = C[k]; } |
| 97 | + for(std::size_t i = 0; i < n; ++i) { |
| 98 | + for(std::size_t j = 0; j < n; ++j) { |
| 99 | + if(i == j) { continue; } |
| 100 | + const value_t gap = eps_c[i] - eps_c[j]; |
| 101 | + if(std::abs(gap) <= value_t(2) * sigma) { continue; } |
| 102 | + const clean_t coeff = G[j * n + i] / clean_t(gap); |
| 103 | + for(std::size_t r = 0; r < n; ++r) { |
| 104 | + out[r * n + i] += coeff * C[r * n + j]; |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | +}; |
| 111 | + |
| 112 | +} // namespace detail |
| 113 | + |
| 114 | +/** @brief Attaches first-order MO-coefficient uncertainty to converged |
| 115 | + * orbitals. |
| 116 | + * |
| 117 | + * Given the converged Fock matrix @p F (carrying the input uncertainty), the |
| 118 | + * S-orthonormal eigenvectors @p C (columns), and the eigenvalues @p eps, this |
| 119 | + * replaces each column with its first-order perturbed value |
| 120 | + * c_i + sum_{j} G(j,i)/(e_i - e_j) c_j, where G = C^T F C. The off-diagonal |
| 121 | + * G(j,i) = c_j^T (delta F) c_i is the projected SCF residual and is the source |
| 122 | + * of the coefficient uncertainty. |
| 123 | + * |
| 124 | + * Roothaan-Hall is the generalized problem F c = e S c, whose rigorous |
| 125 | + * first-order coupling is c_j^T (delta F - e_i delta S) c_i and which also |
| 126 | + * carries an S-renormalization term -1/2 (c_i^T delta S c_i) c_i. Here S is |
| 127 | + * exact (delta S = 0): the uncertainty lives at the Fock level, not in the AO |
| 128 | + * overlap integrals. Both S-dependent terms therefore vanish and the result |
| 129 | + * reduces to the ordinary projected-delta-F correction below; S still enters |
| 130 | + * through the S-orthonormality of C that the C^T F C projection bakes in. If |
| 131 | + * UQ is ever injected upstream (geometry, basis exponents) so delta S != 0, |
| 132 | + * the metric terms must be restored. |
| 133 | + * |
| 134 | + * This is a ONE-SHOT, post-convergence step. Propagating the eigenvector |
| 135 | + * spread through every SCF diagonalization compounds it (the energy's |
| 136 | + * parametric uncertainty is the Hellmann-Feynman explicit term and needs no |
| 137 | + * eigenvector spread); applying the correction once to the converged solution |
| 138 | + * avoids that feedback. Because the result does not re-enter the density, it is |
| 139 | + * applied for ALL uncertainty types, intervals included. A no-op for plain |
| 140 | + * floats. |
| 141 | + * |
| 142 | + * @param[in,out] C The converged eigenvectors (columns); corrected in place. |
| 143 | + * @param[in] F The converged, uncertainty-bearing Fock matrix. |
| 144 | + * @param[in] eps The converged eigenvalues. |
| 145 | + */ |
| 146 | +inline void attach_eigenvector_uncertainty(simde::type::tensor& C, |
| 147 | + const simde::type::tensor& F, |
| 148 | + const simde::type::tensor& eps) { |
| 149 | + using tensorwrapper::buffer::make_contiguous; |
| 150 | + using tensorwrapper::buffer::visit_contiguous_buffer; |
| 151 | + |
| 152 | + // MO-basis Fock G(i,k) = c_i^T F c_k. The diagonal is the eigenvalues; the |
| 153 | + // off-diagonals are the couplings that drive the correction. |
| 154 | + simde::type::tensor CF, G; |
| 155 | + CF("i,k") = C("j,i") * F("j,k"); |
| 156 | + G("i,k") = CF("i,j") * C("j,k"); |
| 157 | + |
| 158 | + // Read-only views of the actual data (single-arg form returns a reference; |
| 159 | + // the 2-arg form would allocate a fresh, default-initialized buffer). |
| 160 | + const auto& c_in = make_contiguous(C.buffer()); |
| 161 | + const auto& g_in = make_contiguous(G.buffer()); |
| 162 | + const auto& e_in = make_contiguous(eps.buffer()); |
| 163 | + |
| 164 | + const auto n = e_in.shape().extent(0); |
| 165 | + tensorwrapper::shape::Smooth mat_shape{n, n}; |
| 166 | + |
| 167 | + // Writable output buffer of the right shape/type; the kernel fills it. |
| 168 | + auto out_buf = make_contiguous(C.buffer(), mat_shape); |
| 169 | + |
| 170 | + detail::EigenvectorUncertaintyKernel kernel{n, c_in, g_in, e_in}; |
| 171 | + visit_contiguous_buffer(kernel, out_buf); |
| 172 | + |
| 173 | + C = simde::type::tensor(mat_shape, std::move(out_buf)); |
| 174 | +} |
| 175 | + |
| 176 | +} // namespace scf::eigen_solver |
0 commit comments