votca 2026-dev
Loading...
Searching...
No Matches
podcoupling.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
21#include "votca/xtp/aomatrix.h"
22#include "votca/xtp/basisset.h"
23#include "votca/xtp/citations.h"
24#include <Eigen/Eigenvalues>
25#include <algorithm>
26#include <sstream>
28
29namespace votca {
30namespace xtp {
31
32std::vector<Index> MapAtomsToAOIndices(
33 const std::vector<Index>& atom_indices,
34 const std::vector<Index>& func_per_atom) {
35 // Prefix sum: AOs of atom a are [ao_start[a], ao_start[a+1]). Valid
36 // because AO bases are built atom by atom, in atom order.
37 std::vector<Index> ao_start(func_per_atom.size() + 1, 0);
38 for (size_t a = 0; a < func_per_atom.size(); ++a) {
39 ao_start[a + 1] = ao_start[a] + func_per_atom[a];
40 }
41
42 std::vector<Index> ao_indices;
43 for (Index atom_index : atom_indices) {
44 if (atom_index < 0 ||
45 atom_index >= static_cast<Index>(func_per_atom.size())) {
46 throw std::runtime_error("MapAtomsToAOIndices: atom index " +
47 std::to_string(atom_index) +
48 " is out of range (0.." +
49 std::to_string(func_per_atom.size() - 1) + ").");
50 }
51 // Contiguous per atom, but not overall for scattered atom indices.
52 for (Index ao = ao_start[atom_index]; ao < ao_start[atom_index + 1]; ++ao) {
53 ao_indices.push_back(ao);
54 }
55 }
56 return ao_indices;
57}
58
59namespace {
60// Square sub-matrix of full_matrix at the given (possibly non-contiguous)
61// row/column indices, gathered directly without permuting full_matrix.
62Eigen::MatrixXd GatherSubMatrix(const Eigen::MatrixXd& full_matrix,
63 const std::vector<Index>& indices) {
64 Index n = static_cast<Index>(indices.size());
65 Eigen::MatrixXd result(n, n);
66 for (Index i = 0; i < n; ++i) {
67 for (Index j = 0; j < n; ++j) {
68 result(i, j) = full_matrix(indices[size_t(i)], indices[size_t(j)]);
69 }
70 }
71 return result;
72}
73
74// Rectangular block of full_matrix with rows from one fragment and
75// columns from the other (donor-acceptor block).
76Eigen::MatrixXd GatherOffDiagonalBlock(const Eigen::MatrixXd& full_matrix,
77 const std::vector<Index>& indices_row,
78 const std::vector<Index>& indices_col) {
79 Index n_row = static_cast<Index>(indices_row.size());
80 Index n_col = static_cast<Index>(indices_col.size());
81 Eigen::MatrixXd result(n_row, n_col);
82 for (Index i = 0; i < n_row; ++i) {
83 for (Index j = 0; j < n_col; ++j) {
84 result(i, j) =
85 full_matrix(indices_row[size_t(i)], indices_col[size_t(j)]);
86 }
87 }
88 return result;
89}
90} // namespace
91
92namespace {
93// Number of occupied orbitals of a fragment, estimated as half its
94// nuclear charge rounded to the nearest integer (neutral closed-shell
95// fragment), as for the neutral reference population in
96// DFTEngine::BuildCDFTConstraint. Only an estimate for covalently bonded
97// fragments, whose electron count is not well defined.
98Index CountFragmentElectrons(const QMMolecule& mol,
99 const std::vector<Index>& atoms) {
100 double nuccharge = 0.0;
101 for (Index atom_index : atoms) {
102 nuccharge += static_cast<double>(mol[atom_index].getNuccharge());
103 }
104 return static_cast<Index>(std::lround(nuccharge / 2.0));
105}
106
107// Fragment-local analogue of DFTcoupling::DetermineRangeOfStates
108// (minimal = homo_index - numberofstates + 1, maximal = lumo_index +
109// numberofstates - 1), without degeneracy handling. n_basis is the
110// fragment's AO count, i.e. its number of orbitals.
111std::pair<Index, Index> DetermineFragmentRangeOfStates(Index homo_index,
112 Index lumo_index,
113 Index numberofstates,
114 Index n_basis) {
115 Index minimal = homo_index - numberofstates + 1;
116 Index maximal = lumo_index + numberofstates - 1;
117 if (minimal < 0 || maximal >= n_basis) {
118 throw std::runtime_error("PODCoupling: requested numberofstates=" +
119 std::to_string(numberofstates) +
120 " exceeds the fragment's orbital range (0.." +
121 std::to_string(n_basis - 1) + ").");
122 }
123 return {minimal, maximal - minimal + 1};
124}
125} // namespace
126
128 std::vector<Index> fragment_A_atoms,
129 std::vector<Index> fragment_B_atoms)
130 : orbitals_(orbitals),
131 pLog_(log),
132 fragment_A_atoms_(std::move(fragment_A_atoms)),
133 fragment_B_atoms_(std::move(fragment_B_atoms)) {
134 const QMMolecule& mol = orbitals_.QMAtoms();
135 nocc_A_ = CountFragmentElectrons(mol, fragment_A_atoms_);
136 nocc_B_ = CountFragmentElectrons(mol, fragment_B_atoms_);
137}
138
140 Index numberofstatesB) {
141 citations::Cite("Ghan:2020");
142 const QMMolecule& mol = orbitals_.QMAtoms();
143
144 AOBasis full_dftbasis;
145 {
146 BasisSet basisset;
147 basisset.Load(orbitals_.getDFTbasisName());
148 full_dftbasis.Fill(basisset, mol);
149 }
150 AOOverlap overlap;
151 overlap.Fill(full_dftbasis);
152 const Eigen::MatrixXd& S = overlap.Matrix();
153
154 // AO Fock matrix from the converged MOs, F_AO = S*C*eps*C^T*S (Orbitals
155 // does not store F_AO). Exact if C is S-orthonormal (C^T*S*C = I) and
156 // square, i.e. no linearly dependent basis functions were removed.
157 const Eigen::MatrixXd& C = orbitals_.MOs().eigenvectors();
158 const Eigen::VectorXd& eps = orbitals_.MOs().eigenvalues();
159 Eigen::MatrixXd F = S * C * eps.asDiagonal() * C.transpose() * S;
160
161 std::vector<Index> ao_indices_A =
163 std::vector<Index> ao_indices_B =
165
166 Eigen::MatrixXd F_AA = GatherSubMatrix(F, ao_indices_A);
167 Eigen::MatrixXd S_AA = GatherSubMatrix(S, ao_indices_A);
168 Eigen::MatrixXd F_BB = GatherSubMatrix(F, ao_indices_B);
169 Eigen::MatrixXd S_BB = GatherSubMatrix(S, ao_indices_B);
170
171 // Diagonalize each fragment block in the original, non-orthogonal AO
172 // basis (generalized problem F c = e S c). This distinguishes POD2 from
173 // POD, which Lowdin-orthogonalizes the whole AO basis first; that mixes
174 // AOs across fragments increasingly with basis size (arXiv:1512.00200).
175 Eigen::GeneralizedSelfAdjointEigenSolver<Eigen::MatrixXd> es_A(F_AA, S_AA);
176 Eigen::GeneralizedSelfAdjointEigenSolver<Eigen::MatrixXd> es_B(F_BB, S_BB);
177 if (es_A.info() != Eigen::Success || es_B.info() != Eigen::Success) {
178 throw std::runtime_error(
179 "PODCoupling: generalized eigenvalue solve failed for one or both "
180 "fragment Fock sub-blocks; the fragment overlap matrix may be "
181 "numerically singular (check the fragment definition).");
182 }
183
184 // Needed by GetFragmentOrbital and DescribeFragmentOrbitalComposition.
185 fragment_A_eigenvectors_ = es_A.eigenvectors();
186 fragment_B_eigenvectors_ = es_B.eigenvectors();
187 ao_indices_A_ = ao_indices_A;
188 ao_indices_B_ = ao_indices_B;
189 nao_full_ = F.rows();
190 S_AA_ = S_AA;
191 S_BB_ = S_BB;
192
193 Range_orbA_ = DetermineFragmentRangeOfStates(getFragmentAHomoIndex(),
195 numberofstatesA, F_AA.rows());
196 Range_orbB_ = DetermineFragmentRangeOfStates(getFragmentBHomoIndex(),
198 numberofstatesB, F_BB.rows());
199 Index levelsA = Range_orbA_.second;
200 Index levelsB = Range_orbB_.second;
201
203 << TimeStamp()
204 << " PODCoupling: fragment A HOMO=" << getFragmentAHomoIndex()
205 << ", LUMO=" << getFragmentALumoIndex()
206 << " (both estimated), range covers orbitals [" << Range_orbA_.first
207 << ", " << (Range_orbA_.first + levelsA - 1) << "]" << std::flush;
209 << TimeStamp()
210 << " PODCoupling: fragment B HOMO=" << getFragmentBHomoIndex()
211 << ", LUMO=" << getFragmentBLumoIndex()
212 << " (both estimated), range covers orbitals [" << Range_orbB_.first
213 << ", " << (Range_orbB_.first + levelsB - 1) << "]" << std::flush;
214
215 // Off-diagonal AO blocks, independent of the orbital pair.
216 Eigen::MatrixXd F_AB = GatherOffDiagonalBlock(F, ao_indices_A, ao_indices_B);
217 Eigen::MatrixXd S_AB_block =
218 GatherOffDiagonalBlock(S, ao_indices_A, ao_indices_B);
219
220 JAB_ = Eigen::MatrixXd(levelsA, levelsB);
221 for (Index i = 0; i < levelsA; ++i) {
222 Eigen::VectorXd orbital_A = es_A.eigenvectors().col(Range_orbA_.first + i);
223 double e_A_hartree = es_A.eigenvalues()(Range_orbA_.first + i);
224 for (Index j = 0; j < levelsB; ++j) {
225 Eigen::VectorXd orbital_B =
226 es_B.eigenvectors().col(Range_orbB_.first + j);
227 double e_B_hartree = es_B.eigenvalues()(Range_orbB_.first + j);
228
229 double J_AB = orbital_A.dot(F_AB * orbital_B);
230 // Overlap of the two fragment orbitals.
231 double S_AB = orbital_A.dot(S_AB_block * orbital_B);
232
233 // The fragment orbitals are not orthogonal, so J_AB contains an
234 // overlap contribution (off by nearly a factor of 2 compared with
235 // half the splitting of a symmetric dimer). Lowdin-type correction
236 // as in DIPRO (Baumeier, Kirkpatrick, Andrienko, PCCP 12, 11103
237 // (2010), Eq. 10): t_AB = (J_AB - 0.5*(e_A+e_B)*S_AB) / (1-S_AB^2).
238 double denominator = 1.0 - S_AB * S_AB;
239 JAB_(i, j) =
240 (J_AB - 0.5 * (e_A_hartree + e_B_hartree) * S_AB) / denominator;
241
242 // Diagnostic: the correction becomes unstable for |S_AB| -> 1,
243 // which is more likely for covalently bonded fragments. A large
244 // coupling alone is not a problem: for 2,2'-bithiophene S_AB = 0.063
245 // and t_AB is about 3.2 eV (ethylene dimer: S_AB about 0.03).
247 << TimeStamp()
248 << " PODCoupling diagnostic: pair (A=" << (Range_orbA_.first + i)
249 << ", B=" << (Range_orbB_.first + j) << "): S_AB=" << S_AB
250 << ", (1-S_AB^2)=" << denominator
251 << ", raw J_AB=" << (J_AB * tools::conv::hrt2ev)
252 << " eV, corrected=" << (JAB_(i, j) * tools::conv::hrt2ev) << " eV"
253 << std::flush;
254 }
255 }
256}
257
258double PODCoupling::getCouplingElement(Index levelA, Index levelB) const {
259 Index indexA = levelA - Range_orbA_.first;
260 Index indexB = levelB - Range_orbB_.first;
261 if (indexA < 0 || indexA >= JAB_.rows() || indexB < 0 ||
262 indexB >= JAB_.cols()) {
263 throw std::runtime_error(
264 "PODCoupling::getCouplingElement: requested levelA=" +
265 std::to_string(levelA) + "/levelB=" + std::to_string(levelB) +
266 " is outside the range covered by the most recent "
267 "CalculateCouplings call.");
268 }
269 return JAB_(indexA, indexB);
270}
271
272Eigen::VectorXd PODCoupling::GetFragmentOrbital(bool fragment_A,
273 Index level) const {
274 const Eigen::MatrixXd& eigenvectors =
276 const std::vector<Index>& ao_indices =
277 fragment_A ? ao_indices_A_ : ao_indices_B_;
278 const std::pair<Index, Index>& range = fragment_A ? Range_orbA_ : Range_orbB_;
279
280 Index index = level - range.first;
281 if (index < 0 || index >= range.second) {
282 throw std::runtime_error(
283 "PODCoupling::GetFragmentOrbital: requested level=" +
284 std::to_string(level) +
285 " is outside the range covered by the most recent "
286 "CalculateCouplings call.");
287 }
288
289 // Scatter the fragment-local coefficients into the full AO basis
290 // (zero on AOs outside the fragment). The eigenvector matrix holds all
291 // fragment orbitals, so it is indexed with the absolute level, not
292 // with the range-relative index used for JAB_.
293 Eigen::VectorXd fragment_local = eigenvectors.col(level);
294 Eigen::VectorXd full_basis = Eigen::VectorXd::Zero(nao_full_);
295 for (Index i = 0; i < Index(ao_indices.size()); ++i) {
296 full_basis(ao_indices[size_t(i)]) = fragment_local(i);
297 }
298 return full_basis;
299}
300
302 Index level,
303 Index top_n) const {
304 const Eigen::MatrixXd& eigenvectors =
306 const std::vector<Index>& ao_indices =
307 fragment_A ? ao_indices_A_ : ao_indices_B_;
308 const std::vector<Index>& fragment_atoms =
310 const std::pair<Index, Index>& range = fragment_A ? Range_orbA_ : Range_orbB_;
311
312 Index index = level - range.first;
313 if (index < 0 || index >= range.second) {
314 throw std::runtime_error(
315 "PODCoupling::DescribeFragmentOrbitalComposition: requested level=" +
316 std::to_string(level) +
317 " is outside the range covered by the most recent "
318 "CalculateCouplings call.");
319 }
320 Eigen::VectorXd fragment_local = eigenvectors.col(level);
321
322 // AO index -> (atom index, shell L) lookup; the AOBasis is rebuilt as
323 // in CalculateCouplings, since Orbitals does not store it.
324 const QMMolecule& mol = orbitals_.QMAtoms();
325 AOBasis full_dftbasis;
326 {
327 BasisSet basisset;
328 basisset.Load(orbitals_.getDFTbasisName());
329 full_dftbasis.Fill(basisset, mol);
330 }
331 std::vector<Index> ao_to_atom(full_dftbasis.AOBasisSize(), -1);
332 std::vector<L> ao_to_L(full_dftbasis.AOBasisSize());
333 for (const AOShell& shell : full_dftbasis) {
334 Index offset = shell.getStartIndex();
335 for (Index k = 0; k < shell.getNumFunc(); ++k) {
336 ao_to_atom[size_t(offset + k)] = shell.getAtomIndex();
337 ao_to_L[size_t(offset + k)] = shell.getL();
338 }
339 }
340
341 // Mulliken population P_i = c_i*(S*c)_i with the fragment overlap
342 // block S; sums to c^T*S*c = 1 (S-normalized eigenvectors). Ranked by
343 // |P_i| rather than |c_i| (see podcoupling.h).
344 const Eigen::MatrixXd& S_local = fragment_A ? S_AA_ : S_BB_;
345 Eigen::VectorXd mulliken_population =
346 fragment_local.cwiseProduct(S_local * fragment_local);
347
348 // Sort local AOs by |P_i|, largest first; ao_indices maps a local AO to
349 // its full-molecule index for ao_to_atom/ao_to_L.
350 std::vector<Index> order(fragment_local.size());
351 for (Index i = 0; i < fragment_local.size(); ++i) {
352 order[size_t(i)] = i;
353 }
354 std::sort(order.begin(), order.end(), [&](Index a, Index b) {
355 return std::abs(mulliken_population(a)) > std::abs(mulliken_population(b));
356 });
357
358 std::ostringstream out;
359 out << "Top " << std::min(top_n, Index(order.size()))
360 << " AO Mulliken populations for fragment " << (fragment_A ? "A" : "B")
361 << " orbital " << level << ":";
362 for (Index rank = 0; rank < std::min(top_n, Index(order.size())); ++rank) {
363 Index local_ao = order[size_t(rank)];
364 Index full_ao = ao_indices[size_t(local_ao)];
365 Index atom_index = ao_to_atom[size_t(full_ao)];
366 std::string element = atom_index >= 0 ? mol[atom_index].getElement() : "?";
367 out << "\n " << (rank + 1)
368 << ". population=" << mulliken_population(local_ao)
369 << " (coeff=" << fragment_local(local_ao) << "), atom " << atom_index
370 << " (" << element << ", fragment atom "
371 << (std::find(fragment_atoms.begin(), fragment_atoms.end(),
372 atom_index) -
373 fragment_atoms.begin())
374 << "), shell " << EnumToString(ao_to_L[size_t(full_ao)]);
375 }
376 return out.str();
377}
378
379} // namespace xtp
380} // namespace votca
Container to hold Basisfunctions for all atoms.
Definition aobasis.h:42
Index AOBasisSize() const
Definition aobasis.h:46
void Fill(const BasisSet &bs, const QMMolecule &atoms)
Definition aobasis.cc:85
const std::vector< Index > & getFuncPerAtom() const
Definition aobasis.h:72
void Fill(const AOBasis &aobasis) final
const Eigen::MatrixXd & Matrix() const
Definition aomatrix.h:52
void Load(const std::string &name)
Definition basisset.cc:149
Logger is used for thread-safe output of messages.
Definition logger.h:164
Container for molecular orbitals and derived one-particle data.
Definition orbitals.h:47
Eigen::MatrixXd JAB_
Index getFragmentAHomoIndex() const
Definition podcoupling.h:78
std::pair< Index, Index > Range_orbA_
std::vector< Index > ao_indices_A_
std::pair< Index, Index > Range_orbB_
void CalculateCouplings(Index numberofstatesA, Index numberofstatesB)
double getCouplingElement(Index levelA, Index levelB) const
Eigen::MatrixXd fragment_B_eigenvectors_
Eigen::VectorXd GetFragmentOrbital(bool fragment_A, Index level) const
std::vector< Index > ao_indices_B_
std::vector< Index > fragment_B_atoms_
Eigen::MatrixXd fragment_A_eigenvectors_
std::vector< Index > fragment_A_atoms_
Index getFragmentBLumoIndex() const
Definition podcoupling.h:81
Index getFragmentALumoIndex() const
Definition podcoupling.h:79
Index getFragmentBHomoIndex() const
Definition podcoupling.h:80
Eigen::MatrixXd S_BB_
std::string DescribeFragmentOrbitalComposition(bool fragment_A, Index level, Index top_n=5) const
Eigen::MatrixXd S_AA_
PODCoupling(Orbitals &orbitals, Logger *log, std::vector< Index > fragment_A_atoms, std::vector< Index > fragment_B_atoms)
Timestamp returns the current time as a string Example: cout << TimeStamp().
Definition logger.h:224
#define XTP_LOG(level, log)
Definition logger.h:40
STL namespace.
const double hrt2ev
Definition constants.h:53
void Cite(const std::string &key)
marks a reference as used (thread safe; unknown keys are printed as such)
Definition citations.cc:289
Charge transport classes.
Definition ERIs.h:28
std::string EnumToString(L l)
Definition basisset.cc:60
std::vector< Index > MapAtomsToAOIndices(const std::vector< Index > &atom_indices, const std::vector< Index > &func_per_atom)
Provides a means for comparing floating point numbers.
Definition basebead.h:33
Eigen::Index Index
Definition types.h:26