High-precision calculations for one- and two-valence atomic systems
BoundState.hpp
1#pragma once
2#include "AdamsMoulton.hpp"
3#include "Physics/PhysConst_constants.hpp"
4#include "Wavefunction/DiracSpinor.hpp"
5#include <memory>
6#include <utility>
7#include <vector>
8class Grid;
9
10//! Functions and classes used to solve the Dirac equation.
11namespace DiracODE {
12
13//==============================================================================
14/*!
15 @brief Solves bound-state problem for local potential (en < 0).
16 @details
17 Solves \f$ (H_0 + v - \epsilon_a)F_a = 0 \f$ for the bound state.
18 en0 is the initial energy guess (must be reasonably good).
19 eps is the convergence target for the energy.
20 - @p v is the local potential (e.g., v = v_dir + v_nuc)
21 - @p H_off_diag is an optional off-diagonal potential
22 - @p alpha: \f$ \alpha = \lambda\alpha_0 \f$ is the effective fine-structure constant
23*/
24void boundState(DiracSpinor &Fa, const double en0, const std::vector<double> &v,
25 const std::vector<double> &H_off_diag = {},
26 const double alpha = PhysConst::alpha, double eps = 1.0e-14,
27 const DiracSpinor *const VxFa = nullptr,
28 const DiracSpinor *const Fa0 = nullptr, double zion = 1,
29 double mass = 1.0);
30
31//! Factory overload: constructs a DiracSpinor(n, kappa, gr) and calls boundState().
33 int n, int kappa, const double en0, const std::shared_ptr<const Grid> &gr,
34 const std::vector<double> &v, const std::vector<double> &H_off_diag = {},
35 const double alpha = PhysConst::alpha, double eps = 1.0e-14,
36 const DiracSpinor *const VxFa = nullptr,
37 const DiracSpinor *const Fa0 = nullptr, double zion = 1, double mass = 1.0) {
38 DiracSpinor Fnk = DiracSpinor(n, kappa, gr);
39 boundState(Fnk, en0, v, H_off_diag, alpha, eps, VxFa, Fa0, zion, mass);
40 return Fnk;
41}
42
43//! For given energy en, solves DE with correct boundary conditions at the origin
44void regularAtOrigin(DiracSpinor &Fa, const double en,
45 const std::vector<double> &v,
46 const std::vector<double> &H_off_diag, const double alpha,
47 const DiracSpinor *const VxFa = nullptr,
48 const DiracSpinor *const Fa0 = nullptr, double zion = 1,
49 double mass = 1.0);
50
51//! For given energy en, solves (local) DE with correct boundary conditions at infinity
52void regularAtInfinity(DiracSpinor &Fa, const double en,
53 const std::vector<double> &v,
54 const std::vector<double> &H_off_diag,
55 const double alpha,
56 const DiracSpinor *const VxFa = nullptr,
57 const DiracSpinor *const Fa0 = nullptr, double zion = 1,
58 double mass = 1.0);
59
60namespace Internal {
61
62//==============================================================================
63//! Parameters used for Adams-Moulton bound-state solver.
64namespace Param {
65
66//! K (# steps) for Adams-Moulton method (between 1 and 12).
67constexpr std::size_t K_Adams = 7;
68//! Parameter to determine 'asymptotically large r'.
69constexpr double cALR = 550.0;
70//! Max # attempts at converging bound-state energy.
71constexpr int max_its = 99;
72//! Fractional size of 'large' energy update steps (~12%).
73constexpr double lfrac_de = 0.12;
74//! Number of grid points either side of the classical turning point.
75constexpr int d_ctp = 4;
76
77//! Order of coefficients in the large-r asymptotic expansion.
78constexpr int nx = 15;
79//! Convergence threshold for the asymptotic expansion.
80constexpr double nx_eps = 1.e-12;
81
82//! Relative cutoff for the large-r tail: extend outwards until |f| drops below
83//! tail_cut * max|f|. See @ref extendTail.
84constexpr double tail_cut = 1.0e-16;
85
86//! Weighting function for meshing inward/outward solutions at the turning point.
87//! Must be positive; index i may be negative [ctp - d_ctp].
88constexpr auto weight = [](std::size_t i) {
89 return 1.0 / static_cast<double>(i * i + 1);
90};
91
92static_assert(
93 Param::K_Adams >= 1 && Param::K_Adams <= AdamsMoulton::K_max,
94 "\nFAIL in DiracODE: parameter K_Adams must be between 5 and 8\n");
95
96} // namespace Param
97
98//==============================================================================
99/*!
100 @brief Derivative matrix for the radial Dirac equation, dF/du = D(u)*F(u) + S(u).
101 @details
102 Implements AdamsMoulton::DerivativeMatrix<std::size_t, double>, using the
103 grid index i as the argument type (T = std::size_t). The ODE is solved in
104 terms of the grid parameter u (where r = r(u)), so all matrix elements
105 include a dr/du Jacobian factor.
106
107 The radial Dirac equation for a central potential v(r) is:
108
109 \f[
110 \frac{d}{du}\begin{pmatrix}f\\g\end{pmatrix}
111 = \frac{dr}{du}
112 \begin{pmatrix}
113 -\kappa/r + \alpha H_\text{mag} & \alpha(\varepsilon - v) + 2mc \\
114 \alpha(v - \varepsilon) & \kappa/r - \alpha H_\text{mag}
115 \end{pmatrix}
116 \begin{pmatrix}f\\g\end{pmatrix}
117 + S
118 \f]
119
120 where \f$ c = 1/\alpha \f$ is the speed of light in atomic units.
121 The optional inhomogeneous source S encodes the exchange interaction:
122
123 \f[
124 S_f = -\alpha \, [V_x F_a]_g \, \frac{dr}{du}, \quad
125 S_g = +\alpha \, [V_x F_a]_f \, \frac{dr}{du}.
126 \f]
127
128 @note Non-copyable; the constructor stores raw pointers to the grid and
129 potential arrays, which must outlive this object.
130*/
131struct DiracDerivative : AdamsMoulton::DerivativeMatrix<std::size_t, double> {
132
133 /*!
134 @brief Constructs the Dirac derivative matrix for a given orbital and potential.
135 @param in_grid Radial grid.
136 @param in_v Local potential v(r).
137 @param in_k Orbital kappa quantum number.
138 @param in_en Orbital energy.
139 @param in_alpha Fine-structure constant (alpha).
140 @param V_off_diag Optional off-diagonal (magnetic) potential H_mag(r).
141 If empty, treated as zero.
142 @param VxFa Optional exchange potential VxFa. If nullptr, ignored.
143 @param iFa0 Optional inhomogeneous source spinor. If nullptr, ignored.
144 @param zion Effective ionic charge (used for boundary conditions).
145 @param in_mass Effective particle mass in atomic units (default 1).
146 */
147 DiracDerivative(const Grid &in_grid, const std::vector<double> &in_v,
148 const int in_k, const double in_en, const double in_alpha,
149 const std::vector<double> &V_off_diag = {},
150 const DiracSpinor *const VxFa = nullptr,
151 const DiracSpinor *const iFa0 = nullptr, double zion = 1,
152 double in_mass = 1.0);
153
154 const Grid *const pgr;
155 const std::vector<double> *const v;
156 const std::vector<double> *const Hmag;
157 const DiracSpinor *const VxFa;
158 const DiracSpinor *const Fa0;
159 const double zion = 1.0;
160 const int k;
161 const double en, alpha, cc;
162 double mass;
163
164 //! D matrix elements (see @ref DiracDerivative for definitions); index i is grid point.
165 double a(std::size_t i) const final;
166 double b(std::size_t i) const final;
167 double c(std::size_t i) const final;
168 double d(std::size_t i) const final;
169 //! Inhomogeneous source terms from exchange potential VxFa.
170 double Sf(std::size_t i) const final;
171 double Sg(std::size_t i) const final;
172
173 DiracDerivative(const DiracDerivative &) = delete;
174 void operator=(const DiracDerivative &) = delete;
175};
176
177//==============================================================================
178// Holds the state of the bound-state iteration: the energy brackets used by the
179// large (bisection) energy steps, plus the current solution state that is
180// passed between the solver steps (trial solution -> energy adjust -> tail).
181struct TrackEnGuess {
182 // Energy brackets (updated by largeEnergyChange / smallEnergyChangePT):
183 int count_toomany = 0;
184 int count_toofew = 0;
185 double high_en = 0.0;
186 double low_en = 0.0;
187
188 // Current solution state, carried between steps:
189 double en = 0.0; // current energy guess
190 double eps = 1.0; // convergence parameter, |de/e|
191 std::size_t pinf = 0; // practical infinity (index after last nonzero)
192 std::size_t ctp = 0; // matching point (near classical turning point)
193 int its = 1; // iteration counter
194 bool correct_nodes = false; // current solution has the required # of nodes
195 std::vector<double> dg{}; // g_out - g_in near ctp (for the PT energy step)
196};
197
198//==============================================================================
199//! Builds the trial solution at the current energy: finds pinf and the matching
200//! point, integrates out/in, and joins. Returns sofar with pinf, ctp, dg set.
201TrackEnGuess trialSolution(DiracSpinor *Fn, const std::vector<double> &v,
202 const std::vector<double> &H_off_diag, double alpha,
203 const DiracSpinor *VxFa, const DiracSpinor *Fa0,
204 double zion, double mass, std::vector<double> &f_in,
205 std::vector<double> &g_in, TrackEnGuess sofar);
206
207//! Counts nodes and updates the energy guess (PT step if # nodes correct, else a
208//! large bracketing step). Returns sofar with en, eps, correct_nodes updated.
209TrackEnGuess adjustEnergy(DiracSpinor *Fn, int required_nodes, double alpha,
210 TrackEnGuess sofar);
211
212//! Extends the decaying tail past pinf (or, on failure, just zeros the stale
213//! tail) and sets Fn.max_pt(). Mutates Fn; reads sofar.
214void adjustTail(DiracSpinor *Fn, const std::vector<double> &v,
215 const std::vector<double> &H_off_diag, double alpha,
216 double zion, double mass, const TrackEnGuess &sofar);
217
218//==============================================================================
219//! Returns grid index of "practical infinity": the point where f(r) drops effectively to zero.
220std::size_t findPracticalInfinity(const double en, const std::vector<double> &v,
221 const std::vector<double> &r,
222 const double alr, const double mass = 1.0);
223
224//! Returns grid index of the classical turning point, where |V(r)| = |E|.
225std::size_t findClassicalTurningPoint(const double en,
226 const std::vector<double> &v,
227 std::size_t pinf, std::size_t d_ctp);
228
229/*!
230 @brief Constructs a trial Dirac solution with correct boundary conditions at both ends.
231 @details
232 Integrates outwards from the origin and inwards from practical infinity, then
233 joins the two solutions at the classical turning point (ctp). If the energy is
234 not an eigenvalue, there will be a discontinuity ('kink') in g at ctp. The
235 difference dg = g_out - g_in at the joining region is stored and used for the
236 perturbation-theory energy update.
237*/
238void trialDiracSolution(std::vector<double> &f, std::vector<double> &g,
239 std::vector<double> &dg, std::vector<double> &f_in,
240 std::vector<double> &g_in, const double en,
241 const int ka, const std::vector<double> &v,
242 const std::vector<double> &H_off_diag, const Grid &gr,
243 std::size_t ctp, std::size_t d_ctp, std::size_t pinf,
244 const double alpha,
245 const DiracSpinor *const VxFa = nullptr,
246 const DiracSpinor *const Fa0 = nullptr, double zion = 1,
247 double mass = 1.0);
248
249//! Returns the number of nodes (sign changes) in f up to index maxi.
250int countNodes(const std::vector<double> &f, const std::size_t maxi);
251
252//! Makes a large (bisection-style) energy update; updates TrackEnGuess accordingly.
253void largeEnergyChange(double *en, TrackEnGuess *sofar, double frac_de,
254 bool toomany_nodes);
255
256//! Returns an updated energy using first-order perturbation theory, given f and dg at ctp.
257//! Also tightens the energy brackets in sofar (sign of the PT step tells which
258//! side of the eigenvalue en lies on).
259double smallEnergyChangePT(const double en, const double anorm,
260 const std::vector<double> &f,
261 const std::vector<double> &dg, std::size_t ctp,
262 std::size_t d_ctp, const double alpha,
263 TrackEnGuess *sofar);
264
265/*!
266 @brief Integrates the Dirac equation outwards from the origin.
267 @details
268 Integrates up to index @p final (not inclusive); if final=0, integrates to
269 f.size(). The solution satisfies the boundary condition at r=0 but not at large r.
270*/
271void solve_Dirac_outwards(std::vector<double> &f, std::vector<double> &g,
272 const DiracDerivative &Hd, std::size_t final = 0);
273
274/*!
275 @brief Integrates the Dirac equation inwards from pinf to ctp.
276 @details
277 The solution satisfies the boundary condition at large r (practical infinity)
278 but not at the origin.
279*/
280void solve_Dirac_inwards(std::vector<double> &f, std::vector<double> &g,
281 const DiracDerivative &Hd, std::size_t ctp,
282 std::size_t pinf, double mass = 1.0);
283
284//==============================================================================
285/*!
286 @brief Adjusts the practical infinity so the orbital tail ends at the cutoff.
287 @details
288 After the bound state has been solved (energy converged, in/out solutions
289 joined), the orbital is defined up to the "practical infinity" @p pinf, which
290 is set by a fixed criterion [findPracticalInfinity] and so does not in general
291 sit at a controlled value of |f|. This routine moves @p pinf so that the last
292 nonzero point has |f| ~ @p tail_cut * max|f|, i.e. so the orbital ends where it
293 has properly decayed (no abrupt truncation, and no redundant near-zero tail).
294
295 - If |f| at @p pinf is still above the cutoff, the decaying tail is continued
296 outward (towards larger r). Two methods are used, in order of preference:
297 - Preferred: continue the outward Adams-Moulton integration of the
298 homogeneous radial Dirac equation, seeded from the last @c K_Adams solved
299 points before @p pinf (the same solver used for the bulk of the orbital).
300 - Fallback: in the classically-forbidden region the outward solution is
301 numerically unstable, as the exponentially growing solution eventually
302 dominates. This shows up as f(r) either changing sign or growing in
303 magnitude. If that happens, the remaining tail is instead built from the
304 large-r asymptotic (exponential-decay) expansion [AsymptoticSpinor],
305 scaled to match the solved orbital at @p pinf. This is the same expansion
306 used for the inward boundary condition; see the methods document,
307 Sec. 3.3.1 (following Johnson 2007).
308
309 - If |f| at @p pinf is already below the cutoff, @p pinf is instead reduced
310 (moved inward) to the first point where |f| >= cutoff, and the trimmed tail
311 is zeroed.
312
313 Must be called on the converged (but not-yet-normalised) orbital: the cutoff
314 is relative to max|f|, which requires the full solution. The orbital is left
315 un-normalised; the caller should (re)compute the norm afterwards.
316
317 @param f Upper radial component, modified in the tail region.
318 @param g Lower radial component, modified in the tail region.
319 @param Hd Homogeneous Dirac derivative matrix (built with the converged
320 energy; exchange/source terms should not be included).
321 @param pinf Current practical infinity (index after final nonzero point).
322 @param tail_cut Relative cutoff: target |f(new_pinf-1)| ~ tail_cut * max|f|.
323
324 @return The new practical infinity (index after the final nonzero point).
325
326 @note Components beyond the returned index are set to zero.
327 @warning @p Hd must use the same converged energy as the solved orbital;
328 passing an inconsistent energy will produce a discontinuous tail.
329*/
330std::size_t extendTail(std::vector<double> &f, std::vector<double> &g,
331 const DiracDerivative &Hd, std::size_t pinf,
332 double tail_cut);
333
334/*!
335 @brief Joins the inward and outward solutions into a single wavefunction.
336 @details
337 Produces a solution with correct boundary conditions at both r=0 and large r,
338 matched at ctp using a weighted average over [ctp-d_ctp, ctp+d_ctp].
339 The resulting wavefunction may not be smooth at the joining point if the
340 energy is not an eigenvalue; the discontinuity in g is stored in dg.
341*/
342void joinInOutSolutions(std::vector<double> &f, std::vector<double> &g,
343 std::vector<double> &dg,
344 const std::vector<double> &f_in,
345 const std::vector<double> &g_in, std::size_t ctp,
346 std::size_t d_ctp, std::size_t pinf);
347
348} // namespace Internal
349} // namespace DiracODE
Stores radial Dirac spinor: F_nk = (f, g)
Definition DiracSpinor.hpp:42
Non-uniform radial grid with Jacobian, suitable for atomic structure calculations.
Definition Grid.hpp:85
constexpr double lfrac_de
Fractional size of 'large' energy update steps (~12%).
Definition BoundState.hpp:73
constexpr int max_its
Max # attempts at converging bound-state energy.
Definition BoundState.hpp:71
constexpr double tail_cut
Relative cutoff for the large-r tail: extend outwards until |f| drops below tail_cut * max|f|....
Definition BoundState.hpp:84
constexpr double cALR
Parameter to determine 'asymptotically large r'.
Definition BoundState.hpp:69
constexpr std::size_t K_Adams
K (# steps) for Adams-Moulton method (between 1 and 12).
Definition BoundState.hpp:67
constexpr int nx
Order of coefficients in the large-r asymptotic expansion.
Definition BoundState.hpp:78
constexpr auto weight
Weighting function for meshing inward/outward solutions at the turning point. Must be positive; index...
Definition BoundState.hpp:88
constexpr double nx_eps
Convergence threshold for the asymptotic expansion.
Definition BoundState.hpp:80
constexpr int d_ctp
Number of grid points either side of the classical turning point.
Definition BoundState.hpp:75
Functions and classes used to solve the Dirac equation.
Definition AsymptoticSpinor.hpp:8
void regularAtInfinity(DiracSpinor &Fa, const double en, const std::vector< double > &v, const std::vector< double > &H_mag, const double alpha, const DiracSpinor *const VxFa, const DiracSpinor *const Fa0, double zion, double mass)
For given energy en, solves (local) DE with correct boundary conditions at infinity.
Definition BoundState.cpp:98
void regularAtOrigin(DiracSpinor &Fa, const double en, const std::vector< double > &v, const std::vector< double > &H_mag, const double alpha, const DiracSpinor *const VxFa, const DiracSpinor *const Fa0, double zion, double mass)
For given energy en, solves DE with correct boundary conditions at the origin.
Definition BoundState.cpp:77
void boundState(DiracSpinor &Fn, const double en0, const std::vector< double > &v, const std::vector< double > &H_mag, const double alpha, double eps_goal, const DiracSpinor *const VxFa, const DiracSpinor *const Fa0, double zion, double mass)
Solves bound-state problem for local potential (en < 0).
Definition BoundState.cpp:23
constexpr double alpha
Fine-structure constant: alpha = 1/137.035 999 177(21) [CODATA 2022].
Definition PhysConst_constants.hpp:24
Pure-virtual struct defining the derivative matrix for a 2x2 ODE system.
Definition AdamsMoulton.hpp:47
Derivative matrix for the radial Dirac equation, dF/du = D(u)*F(u) + S(u).
Definition BoundState.hpp:131
double a(std::size_t i) const final
D matrix elements (see DiracDerivative for definitions); index i is grid point.
Definition BoundState.cpp:643
double Sf(std::size_t i) const final
Inhomogeneous source terms from exchange potential VxFa.
Definition BoundState.cpp:655