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