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