High-precision calculations for one- and two-valence atomic systems
SixJTable.hpp
1#pragma once
2#include "Angular/Wigner369j.hpp"
3#include "Wavefunction/DiracSpinor.hpp" // for 'magic' 6J symbols
4#include <algorithm>
5#include <cassert>
6#include <cstdint>
7#include <cstring>
8#include <iostream>
9#include <vector>
10// #define SIXJ_USE_STD_MAP
11#ifdef SIXJ_USE_STD_MAP
12#include <unordered_map>
13#else
14#include "ankerl/unordered_dense.h"
15#endif
16
17// XXX Note: This is significantly faster if implemented in header file, not
18// sepperate cpp file. Seems due to inlineing of 'get' function
19
20namespace Angular {
21
22//! Hashmap type used to store 6j symbols.
23//! @details Defaults to ankerl::unordered_dense::map (fast, densely-stored,
24//! open-addressing). Define SIXJ_USE_STD_MAP to fall back to std::unordered_map.
25#ifdef SIXJ_USE_STD_MAP
26using SixJMap = std::unordered_map<uint64_t, double>;
27#else
28using SixJMap = ankerl::unordered_dense::map<uint64_t, double>;
29#endif
30
31//=============================================================================
32//! Returns 2*k if @p a is an integer, or 2*j if @p a is a DiracSpinor.
33template <class A>
34int twojk(const A &a) {
35 if constexpr (std::is_same_v<A, DiracSpinor>) {
36 return a.twoj();
37 } else if constexpr (std::is_same_v<A, int>) {
38 return 2 * a;
39 } else {
40 static_assert(std::is_same_v<A, std::size_t>);
41 return 2 * static_cast<int>(a);
42 }
43}
44
45/*!
46 @brief "Magic" 6j symbol accepting integers or DiracSpinors.
47 @details Pass an integer for a rank \f$k\f$, or a DiracSpinor for a
48 half-integer \f$j\f$. Example: `SixJ(Fa, Fb, k, Fc, Fd, k)`
49 evaluates \f$\{j_a,j_b,k;j_c,j_d,k\}\f$.
50 @warning Do **not** pre-multiply by 2, and do **not** pass \f$j\f$ or
51 \f$2j\f$ directly pass the DiracSpinor itself.
52*/
53template <class A, class B, class C, class D, class E, class F>
54double SixJ(const A &a, const B &b, const C &c, const D &d, const E &e,
55 const F &f) {
56 return sixj_2(twojk(a), twojk(b), twojk(c), twojk(d), twojk(e), twojk(f));
57}
58
59//==============================================================================
60
61/*!
62 @brief Lookup table for Wigner 6j symbols.
63 @details
64 Pre-computes and caches Wigner 6j symbols
65 \f$\sixj{a/2}{b/2}{c/2}{d/2}{e/2}{f/2}\f$.
66 All public functions accept arguments as \f$2j\f$ or \f$2k\f$ (integers),
67 so that half-integer spins are handled exactly.
68
69 Storage exploits symmetry: the six entries of each symbol are
70 permuted into a canonical "normal order" before lookup, so each
71 physically distinct symbol is stored only once.
72
73 @note
74 - Call fill() (or the constructor) with the maximum \f$2j\f$ or \f$k\f$
75 that will be needed; the table can be extended at any time.
76 - Requesting a symbol outside the stored range returns 0 without warning.
77 - Non-mutable accessors are thread-safe once the table is fully built.
78
79 @warning Symbols are stored up to the value passed to fill(); calls with
80 larger arguments silently return 0.
81*/
82class SixJTable {
83private:
84 SixJMap m_data{};
85 int m_max_2j_k{-1};
86 static auto s(int i) { return static_cast<uint8_t>(i); };
87
88public:
89 //! Constructs an empty table; extend later with fill() or get().
90 SixJTable() = default;
91
92 /*!
93 @brief Constructs the table, pre-filling all symbols up to @p max_2j_k.
94 @details Calls fill() internally. Typically @p max_2j_k is the maximum
95 \f$2j\f$ of the orbital set; all symbols with every argument
96 \f$\le 2 \times \texttt{max\_2j\_k}\f$ are stored.
97 @param max_2j_k Maximum value of \f$2j\f$ (or \f$k\f$) to pre-compute.
98 */
99 SixJTable(int max_2j_k) { fill(max_2j_k); }
100
101 //! Returns the maximum 2j (or k) currently stored; max(k) = 2*max(j).
102 int max_2jk() const { return m_max_2j_k; }
103
104 //! Returns the number of non-zero symbols stored in the table.
105 std::size_t size() const { return m_data.size(); }
106
107 //----------------------------------------------------------------------------
108 /*!
109 @brief Returns 6j symbol {a/2, b/2, c/2; d/2, e/2, f/2}.
110 @details Arguments are \f$2j\f$ or \f$2k\f$ as integers.
111 Returns 0 without warning if the symbol is outside the stored range.
112 @param a,b,c,d,e,f Twice the top/bottom rows of the 6j symbol.
113 */
114 inline double get_2(int a, int b, int c, int d, int e, int f) const {
115 if (Angular::sixj_zeroQ(a, b, c, d, e, f))
116 return 0.0;
117 const auto it = m_data.find(normal_order(a, b, c, d, e, f));
118 return (it == m_data.cend()) ? 0.0 : it->second;
119 }
120
121 /*!
122 @brief "Magic" table lookup: pass integers (for k) or DiracSpinors (for j).
123 @details Converts each argument via twojk() then delegates to get_2().
124 Example: `get(Fa, Fb, k, Fc, Fd, k)` returns
125 \f$\{j_a,j_b,k;j_c,j_d,k\}\f$.
126 Returns 0 without warning if the symbol is outside the stored range.
127 @warning Do **not** pre-multiply by 2.
128 */
129 template <class A, class B, class C, class D, class E, class F>
130 double get(const A &a, const B &b, const C &c, const D &d, const E &e,
131 const F &f) const {
132 return get_2(twojk(a), twojk(b), twojk(c), twojk(d), twojk(e), twojk(f));
133 }
134
135 //----------------------------------------------------------------------------
136 //! Returns true if the symbol is present in the table (absent symbols may be zero or simply out of range).
137 bool contains(int a, int b, int c, int d, int e, int f) const {
138 const auto it = m_data.find(normal_order(a, b, c, d, e, f));
139 return (it != m_data.cend());
140 }
141
142 //----------------------------------------------------------------------------
143 /*!
144 @brief Extends the table to cover all symbols up to @p max_2j_k.
145 @details Called automatically by the constructor. Only needed explicitly
146 when the required \f$2j\f$ grows beyond the original maximum.
147 No-op if @p max_2j_k does not exceed the current maximum.
148 @param max_2j_k New maximum value of \f$2j\f$ (or \f$k\f$).
149 */
150 void fill(int max_2j_k) {
151
152 if (max_2j_k <= m_max_2j_k)
153 return;
154
155 // Calculate all new *unique* 6J symbols:
156 // Take advantage of symmetries, to only calc those that are needed.
157 // We define a = min(a,b,c,d,e,f), b=min(b,d,e,f) => unique 6j symbol
158 // in "normal" order
159 const auto max_2k = 2 * max_2j_k;
160 for (int a = 0; a <= max_2k; ++a) {
161 auto a0 = a; // std::max(min_2jk, a);
162 for (int b = a0; b <= max_2k; ++b) {
163 for (int c = b; c <= max_2k; ++c) {
164 for (int d = a0; d <= max_2k; ++d) { // note: different!
165 for (int e = b; e <= max_2k; ++e) {
166 for (int f = b; f <= max_2k; ++f) {
167 if (Angular::sixj_zeroQ(a, b, c, d, e, f))
168 continue;
169 if (contains(a, b, c, d, e, f))
170 continue;
171 const auto sj = Angular::sixj_2(a, b, c, d, e, f);
172 if (std::abs(sj) > 1.0e-16) {
173 m_data[normal_order(a, b, c, d, e, f)] = sj;
174 }
175 }
176 }
177 }
178 }
179 }
180 }
181
182 // update max 2k
183 m_max_2j_k = max_2j_k;
184 }
185
186private:
187 //----------------------------------------------------------------------------
188 inline static auto make_key(uint8_t a, uint8_t b, uint8_t c, uint8_t d,
189 uint8_t e, uint8_t f) {
190 static_assert(sizeof(uint64_t) >= 6 * sizeof(uint8_t));
191 uint64_t key = 0;
192 const auto pk = reinterpret_cast<uint8_t *>(&key);
193 std::memcpy(pk, &a, sizeof(uint8_t));
194 std::memcpy(pk + 1, &b, sizeof(uint8_t));
195 std::memcpy(pk + 2, &c, sizeof(uint8_t));
196 std::memcpy(pk + 3, &d, sizeof(uint8_t));
197 std::memcpy(pk + 4, &e, sizeof(uint8_t));
198 std::memcpy(pk + 5, &f, sizeof(uint8_t));
199 return key;
200 }
201 inline static auto make_key(int a, int b, int c, int d, int e, int f) {
202 return make_key(s(a), s(b), s(c), s(d), s(e), s(f));
203 }
204
205 //----------------------------------------------------------------------------
206 inline static auto normal_order_level2(int a, int b, int c, int d, int e,
207 int f) {
208 // note: 'a' must be minimum!
209 // assert(a == std::min({a, b, c, d, e, f})); // remove
210 // {a,b,c|d,e,f} = {a,c,b|d,f,e} = {a,e,f|d,b,c} = {a,f,e|d,c,b}
211 const auto min_bcef = std::min({b, c, e, f});
212
213 if (min_bcef == b) {
214 return make_key(s(a), s(b), s(c), s(d), s(e), s(f));
215 } else if (min_bcef == c) {
216 return make_key(s(a), s(c), s(b), s(d), s(f), s(e));
217 } else if (min_bcef == e) {
218 return make_key(s(a), s(e), s(f), s(d), s(b), s(c));
219 } else if (min_bcef == f) {
220 return make_key(s(a), s(f), s(e), s(d), s(c), s(b));
221 }
222 assert(false && "Fatal error 170: unreachable");
223 }
224
225 //----------------------------------------------------------------------------
226 static uint64_t normal_order(int a, int b, int c, int d, int e, int f) {
227 // returns unique "normal ordering" of {a,b,c,d,e,f}->{i,j,k,l,m,n}
228
229 // auto t11 = make_key(a, b, c, d, e, f);
230 // auto t21 = make_key(a, c, b, d, f, e);
231 // auto t31 = make_key(b, a, c, e, d, f);
232 // auto t41 = make_key(b, c, a, e, f, d);
233 // auto t51 = make_key(c, b, a, f, e, d);
234 // auto t61 = make_key(c, a, b, f, d, e);
235 // auto t12 = make_key(a, e, f, d, b, c);
236 // auto t22 = make_key(a, f, e, d, c, b);
237 // auto t32 = make_key(b, d, f, e, a, c);
238 // auto t42 = make_key(b, f, d, e, c, a);
239 // auto t52 = make_key(c, e, d, f, b, a);
240 // auto t62 = make_key(c, d, e, f, a, b);
241 // auto t13 = make_key(d, b, f, a, e, c);
242 // auto t23 = make_key(d, c, e, a, f, b);
243 // auto t33 = make_key(e, a, f, b, d, c);
244 // auto t43 = make_key(e, c, d, b, f, a);
245 // auto t53 = make_key(f, a, e, c, d, b);
246 // auto t63 = make_key(f, b, d, c, e, a);
247 // auto t14 = make_key(d, e, c, a, b, f);
248 // auto t24 = make_key(d, f, b, a, c, e);
249 // auto t34 = make_key(e, d, c, b, a, f);
250 // auto t44 = make_key(e, f, a, b, c, d);
251 // auto t54 = make_key(f, e, a, c, b, d);
252 // auto t64 = make_key(f, d, b, c, a, e);
253 //
254 // return std::min({t11, t21, t31, t41, t51, t61, t12, t22,
255 // t32, t42, t52, t62, t13, t23, t33, t43,
256 // t53, t63, t14, t24, t34, t44, t54, t64});
257 // returns unique "normal ordering" of {a,b,c,d,e,f}->{i,j,k,l,m,n}
258 // where i = min{a,b,c,d,e,f}, j = min{b,c,e,f}
259
260 // nb: This is not quite correct, and leads to storing more 6J's than
261 // required. However, so long as we actually calculate all of them, it turns
262 // out this is faster.
263
264 const auto min = std::min({a, b, c, d, e, f});
265 // {a,b,c|d,e,f} = {b,a,c|e,d,f} = {c,a,b|f,d,e}
266 // = {d,e,c|a,b,f} = {e,d,c|b,a,f} = {f,a,e|c,d,b}
267 // at next level, use also:
268 // {a,b,c|d,e,f} = {a,c,b|d,f,e} = {a,e,f|d,b,c} = {a,f,e|d,c,b}
269 if (min == a) {
270 return normal_order_level2(a, b, c, d, e, f);
271 } else if (min == b) {
272 return normal_order_level2(b, a, c, e, d, f);
273 } else if (min == c) {
274 return normal_order_level2(c, a, b, f, d, e);
275 } else if (min == d) {
276 return normal_order_level2(d, e, c, a, b, f);
277 } else if (min == e) {
278 return normal_order_level2(e, d, c, b, a, f);
279 } else if (min == f) {
280 return normal_order_level2(f, a, e, c, d, b);
281 }
282 assert(false && "Fatal error 193: unreachable");
283 }
284};
285
286} // namespace Angular
Lookup table for Wigner 6j symbols.
Definition SixJTable.hpp:82
int max_2jk() const
Returns the maximum 2j (or k) currently stored; max(k) = 2*max(j).
Definition SixJTable.hpp:102
bool contains(int a, int b, int c, int d, int e, int f) const
Returns true if the symbol is present in the table (absent symbols may be zero or simply out of range...
Definition SixJTable.hpp:137
void fill(int max_2j_k)
Extends the table to cover all symbols up to max_2j_k.
Definition SixJTable.hpp:150
SixJTable()=default
Constructs an empty table; extend later with fill() or get().
std::size_t size() const
Returns the number of non-zero symbols stored in the table.
Definition SixJTable.hpp:105
double get_2(int a, int b, int c, int d, int e, int f) const
Returns 6j symbol {a/2, b/2, c/2; d/2, e/2, f/2}.
Definition SixJTable.hpp:114
double get(const A &a, const B &b, const C &c, const D &d, const E &e, const F &f) const
"Magic" table lookup: pass integers (for k) or DiracSpinors (for j).
Definition SixJTable.hpp:130
SixJTable(int max_2j_k)
Constructs the table, pre-filling all symbols up to max_2j_k.
Definition SixJTable.hpp:99
Angular provides functions and classes for calculating and storing angular factors (3,...
Definition CkTable.cpp:7
double sixj_2(int two_j1, int two_j2, int two_j3, int two_j4, int two_j5, int two_j6)
Wigner 6j symbol {j1 j2 j3 | j4 j5 j6}. Inputs are 2*j as integers.
Definition Wigner369j.hpp:431
ankerl::unordered_dense::map< uint64_t, double > SixJMap
Hashmap type used to store 6j symbols.
Definition SixJTable.hpp:28
int twojk(const A &a)
Returns 2*k if a is an integer, or 2*j if a is a DiracSpinor.
Definition SixJTable.hpp:34
double SixJ(const A &a, const B &b, const C &c, const D &d, const E &e, const F &f)
"Magic" 6j symbol accepting integers or DiracSpinors.
Definition SixJTable.hpp:54
bool sixj_zeroQ(int a, int b, int c, int d, int e, int f)
Returns true if the 6j symbol is zero by triangle/parity rules. Inputs are 2*j.
Definition Wigner369j.hpp:357
double f(double r, double en, int kappa, double zeff, double alpha, double m)
Upper (large) radial component.
Definition DiracContinuum.cpp:147
constexpr double c
speed of light in a.u. (=1/alpha)
Definition PhysConst_constants.hpp:63
T min(T first, Args... rest)
Returns the minimum of any number of parameters (variadic).
Definition Maths.hpp:41