votca 2026-dev
Loading...
Searching...
No Matches
podcoupling.h
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#pragma once
21#ifndef VOTCA_XTP_PODCOUPLING_H
22#define VOTCA_XTP_PODCOUPLING_H
23
24#include "logger.h"
25#include "votca/xtp/orbitals.h"
26#include <votca/tools/types.h>
27#include <votca/xtp/aobasis.h>
28
29namespace votca {
30namespace xtp {
31
32// Maps a fragment's own atom indices to the full, concatenated list of
33// AO basis-function indices belonging to those atoms, in the SAME
34// order the atom indices were given (not sorted) -- confirmed
35// necessary directly, with the user, before implementing anything
36// else here: the actual, real use case (matching this session's own
37// CDFT tests) has fragment atom indices that are a genuinely
38// DISJOINT, scattered set, not a contiguous, block-wise range (e.g.
39// "atoms 1-5" for one fragment, "atoms 6-11" for the other). Each
40// INDIVIDUAL atom's own AOs ARE always contiguous (a direct
41// consequence of how AO basis sets are constructed, atom by atom, in
42// order -- confirmed via AOBasis::getFuncPerAtom(), not assumed), but
43// a FRAGMENT's own AOs, across scattered atoms, generally are not --
44// this function does the (otherwise easy to get subtly wrong)
45// gathering explicitly, once, in one place, rather than leaving every
46// caller to reimplement it.
47//
48// Deliberately takes func_per_atom directly (AOBasis::getFuncPerAtom()'s
49// own return value) rather than a full AOBasis -- makes this function
50// trivially unit-testable with a small, synthetic vector, with no need
51// to construct any real molecule/basis set at all.
52std::vector<Index> MapAtomsToAOIndices(const std::vector<Index>& atom_indices,
53 const std::vector<Index>& func_per_atom);
54
55// Projection Operator Diabatization, POD2 variant (Ghan et al., avoiding
56// the original POD's own global Lowdin-orthogonalization step, which
57// was found to make results basis-set-unstable -- see
58// arXiv:1512.00200 "Critical analysis of fragment-orbital DFT schemes
59// for the calculation of electronic coupling values" for the direct
60// comparison against the original POD, FODFT, and CDFT that motivated
61// choosing this specific variant).
62//
63// Unlike CDFT (a self-consistent, constrained calculation on the
64// intact supermolecule) or FODFT (two separate, ISOLATED fragment
65// calculations, which cannot represent a covalent bond crossing the
66// fragment boundary at all), POD2 starts from a SINGLE, already-
67// converged, ORDINARY (unconstrained, neutral, ground-state) DFT
68// calculation on the intact, covalently-bonded supermolecule -- no
69// bond-cutting, no excited-state calculation, no CDFT constraint of
70// any kind. The converged Fock matrix's own donor/acceptor AO
71// sub-blocks are separately diagonalized (in the ORIGINAL AO basis,
72// not a globally Lowdin-orthogonalized one -- this is the specific
73// "2" in POD2), giving fragment-localized "diabatic" orbitals
74// directly; the coupling is then read off from the off-diagonal
75// donor-acceptor block of the same Fock matrix, transformed into this
76// new, fragment-block-diagonal basis.
78 public:
79 PODCoupling(Orbitals& orbitals, Logger* log,
80 std::vector<Index> fragment_A_atoms,
81 std::vector<Index> fragment_B_atoms);
82
83 // Computes the FULL, pairwise coupling matrix between a symmetric
84 // range of fragment A's own orbitals and a symmetric range of
85 // fragment B's own orbitals, covering BOTH occupied (hole-transport)
86 // and virtual (electron-transport) orbitals in a single call --
87 // matching DFTcoupling's own, established convention exactly
88 // (numberofstatesA/B, DetermineRangeOfStates, getCouplingElement),
89 // per direct agreement with the user rather than inventing a
90 // different one for this class.
91 //
92 // numberofstatesA/B = N means fragment A/B's own range covers N
93 // orbitals below its own HOMO (inclusive) through N orbitals above
94 // its own LUMO (inclusive) -- i.e. {HOMO-N+1, ..., HOMO, LUMO, ...,
95 // LUMO+N-1}, exactly matching
96 // DFTcoupling::DetermineRangeOfStates's own definition (minimal =
97 // HOMO - numberofstates + 1, maximal = LUMO + numberofstates - 1).
98 // N=1 gives just {HOMO, LUMO}.
99 void CalculateCouplings(Index numberofstatesA, Index numberofstatesB);
100
101 // Coupling [Hartree] between fragment A's orbital at ABSOLUTE (not
102 // fragment-local-range-relative) fragment-orbital index levelA and
103 // fragment B's orbital at absolute index levelB -- e.g. levelA =
104 // getFragmentAHomoIndex() for fragment A's own HOMO. Must be within
105 // the range covered by the most recent CalculateCouplings call, or
106 // this throws -- matching DFTcoupling::getCouplingElement's own
107 // convention of taking absolute MO indices (there, absolute within
108 // the monomer's own, separate Orbitals; here, absolute within the
109 // fragment's own, separate eigendecomposition -- there is no
110 // separate, standalone "fragment A calculation" with its own MO
111 // numbering to refer to at all, since fragment orbitals come from
112 // block-diagonalizing sub-blocks of the SAME, single supermolecule
113 // Fock matrix -- see this class's own, leading comment above).
114 double getCouplingElement(Index levelA, Index levelB) const;
115
116 // Each fragment's own HOMO/LUMO index, in the SAME, absolute index
117 // space getCouplingElement expects for levelA/levelB -- lets a
118 // caller request e.g. "fragment A's HOMO-1, fragment B's LUMO+1"
119 // without needing to know the fragment's own total AO-basis size or
120 // do this arithmetic itself. Available immediately after
121 // construction -- unlike Range_orbA/B (only meaningful after
122 // CalculateCouplings has actually run), each fragment's own HOMO/
123 // LUMO index depends only on its own nuclear charge (see
124 // CountFragmentElectrons in podcoupling.cc), not on which orbital
125 // range was actually requested/computed.
126 Index getFragmentAHomoIndex() const { return nocc_A_ - 1; }
128 Index getFragmentBHomoIndex() const { return nocc_B_ - 1; }
130
131 // Returns fragment A's (if fragment_A is true, else fragment B's)
132 // orbital at absolute index level -- same index convention as
133 // getCouplingElement's own levelA/levelB above -- EMBEDDED into the
134 // full, whole-molecule AO basis (i.e. a vector of length equal to
135 // the full molecule's own total number of AO basis functions, with
136 // zero coefficients everywhere outside this fragment's own AO
137 // indices). This is what visualizing a fragment orbital (e.g. via a
138 // cube file, which needs a coefficient for every AO of the full
139 // molecule the grid is built over) actually requires -- the
140 // fragment orbital's own, "native" representation (a much shorter
141 // vector, over only this fragment's own AOs) is what getCouplingElement's
142 // own internal computation uses, but is not, on its own, something a
143 // whole-molecule grid/cube-file writer can consume at all. Must be
144 // within the range covered by the most recent CalculateCouplings
145 // call, or this throws, matching getCouplingElement's own behavior.
146 Eigen::VectorXd GetFragmentOrbital(bool fragment_A, Index level) const;
147
148 // Human-readable breakdown of which AOs actually dominate a given
149 // fragment orbital -- the top_n largest-|Mulliken population|
150 // AOs, each labeled by which atom (index + element) and shell type
151 // (s/p/d/...) it belongs to. Added directly to answer a real,
152 // concrete question: large electron density visible on the OTHER
153 // fragment's own physical space, in a cube-file rendering of a
154 // fragment orbital, does NOT mean a nonzero coefficient on the
155 // other fragment's own AOs -- there is never one, by construction
156 // (see GetFragmentOrbital's own comment above). It means a large
157 // contribution from THIS fragment's own AO, whose own basis
158 // function has a spatial tail reaching into the other fragment's
159 // space -- this shows directly which AO, on which atom, that
160 // actually is.
161 //
162 // Ranks by Mulliken population (c_i*(S*c)_i), NOT raw |coefficient|
163 // -- confirmed directly necessary, from a real, misleading run: in
164 // a non-orthogonal AO basis, several strongly-overlapping AOs (e.g.
165 // multiple contracted S-shells on nearby atoms) can develop huge,
166 // largely-cancelling raw coefficients that dominate a plain
167 // |coefficient| ranking while contributing little to the orbital's
168 // own, actual normalized shape -- the Mulliken weighting corrects
169 // for this by explicitly accounting for overlap between AOs, the
170 // same way ordinary Mulliken population analysis does.
171 std::string DescribeFragmentOrbitalComposition(bool fragment_A, Index level,
172 Index top_n = 5) const;
173
174 private:
177 std::vector<Index> fragment_A_atoms_;
178 std::vector<Index> fragment_B_atoms_;
179
180 // Each fragment's own, ESTIMATED number of occupied orbitals -- see
181 // this class's own .cc file (CountFragmentElectrons) for how this is
182 // computed and why it is only ever an estimate for a covalently-
183 // bonded fragment specifically. Computed once, in the constructor,
184 // since it depends only on each fragment's own atoms/nuclear
185 // charge, not on anything CalculateCouplings itself computes.
188
189 // Set by CalculateCouplings; std::pair<start, size>, in each
190 // fragment's own, absolute orbital-index space (see
191 // getFragmentAHomoIndex/etc.'s own comment above) -- matching
192 // DFTcoupling::Range_orbA/Range_orbB's own convention exactly.
193 std::pair<Index, Index> Range_orbA_;
194 std::pair<Index, Index> Range_orbB_;
195
196 // (Range_orbA_.second, Range_orbB_.second)-sized coupling matrix
197 // [Hartree], set by CalculateCouplings -- JAB_(i,j) is the coupling
198 // between fragment A's own (Range_orbA_.first + i)-th orbital and
199 // fragment B's own (Range_orbB_.first + j)-th orbital. Matches
200 // DFTcoupling's own JAB member directly, except sized only to the
201 // relevant off-diagonal block (DFTcoupling's own JAB additionally
202 // spans the A-A and B-B diagonal blocks in one, larger, combined
203 // matrix; POD2's own fragment orbitals are already, separately
204 // block-diagonal by construction, so there is no equivalent
205 // diagonal-block information to store here at all).
206 Eigen::MatrixXd JAB_;
207
208 // Set by CalculateCouplings, needed by GetFragmentOrbital to embed a
209 // fragment orbital back into the full, whole-molecule AO basis: each
210 // fragment's own eigenvectors (from its own, separate generalized
211 // eigenvalue solve -- es_A/es_B in podcoupling.cc), each fragment's
212 // own AO indices within the full molecule (the same mapping
213 // MapAtomsToAOIndices already computes for CalculateCouplings' own,
214 // internal use), and the full molecule's own total AO count (to
215 // size the embedded, zero-padded result vector correctly). Cheap to
216 // store in full -- each is only ever fragment-sized
217 // (n_A x n_A/n_A), not full-molecule-sized.
220 std::vector<Index> ao_indices_A_;
221 std::vector<Index> ao_indices_B_;
223
224 // Each fragment's own AO overlap sub-block (S_AA/S_BB, already
225 // computed internally by CalculateCouplings for the generalized
226 // eigenvalue solve itself) -- stored here specifically for
227 // DescribeFragmentOrbitalComposition's own use: a plain, raw AO
228 // coefficient is NOT, on its own, a meaningful measure of that AO's
229 // real contribution to a normalized orbital in a non-orthogonal
230 // basis (confirmed directly, from a real, misleading run: several
231 // strongly-overlapping S-shells developed enormous, largely-
232 // cancelling raw coefficients that dominated a plain |coefficient|
233 // ranking while contributing little to the orbital's actual shape).
234 // The standard, correct fix is a Mulliken-style weighting,
235 // c_i*(S*c)_i, which needs this fragment's own S explicitly.
236 Eigen::MatrixXd S_AA_;
237 Eigen::MatrixXd S_BB_;
238};
239
240} // namespace xtp
241} // namespace votca
242
243#endif // VOTCA_XTP_PODCOUPLING_H
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
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
Index getFragmentALumoIndex() const
Index getFragmentBHomoIndex() const
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)
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