// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // coupling.hpp // Angular momentum coupling coefficients (Wigner 3-j, 6-j, 9-j symbols) // // Provided functions: // wigner3j(j1,j2,j3, m1,m2,m3) — Wigner 3-j symbol // wigner6j(j1,j2,j3, j4,j5,j6) — Wigner 6-j symbol // wigner9j(j1,...,j9) — Wigner 9-j symbol // clebschGordan(j1,m1, j2,m2, J,M) — Clebsch-Gordan coefficient // // j values are half-integers (managed as twice the int: two_j = 2j) // Supported type: double (for exact computation, Rational/Int is recommended) #ifndef SANGI_SPECIAL_COUPLING_HPP #define SANGI_SPECIAL_COUPLING_HPP #include #include #include #include #include namespace sangi { namespace special { // ================================================================ // Internal utility: log factorial // ================================================================ namespace detail { inline double log_factorial(int n) { if (n < 0) return std::numeric_limits::quiet_NaN(); if (n <= 1) return 0.0; return std::lgamma(static_cast(n + 1)); } // Triangle condition: |j1-j2| <= j3 <= j1+j2 (tested in 2j units) inline bool triangle(int two_j1, int two_j2, int two_j3) { if ((two_j1 + two_j2 + two_j3) % 2 != 0) return false; if (two_j3 < std::abs(two_j1 - two_j2)) return false; if (two_j3 > two_j1 + two_j2) return false; return true; } // Δ(abc) = sqrt[(a+b-c)!(a-b+c)!(-a+b+c)! / (a+b+c+1)!] (2j input) inline double triangle_coeff(int two_a, int two_b, int two_c) { int s = (two_a + two_b + two_c) / 2; int n1 = s - two_c; // (a+b-c) in 2j units / 2? No. // In half-integer: a+b-c = (two_a+two_b-two_c)/2 int abc = (two_a + two_b - two_c) / 2; int acb = (two_a - two_b + two_c) / 2; int bca = (-two_a + two_b + two_c) / 2; int sum = (two_a + two_b + two_c) / 2 + 1; return log_factorial(abc) + log_factorial(acb) + log_factorial(bca) - log_factorial(sum); } } // namespace detail // ================================================================ // Wigner 3-j symbol // (j1 j2 j3) (-1)^{j1-j2-m3} // (m1 m2 m3) = ───────────────── · CG(j1,m1,j2,m2; j3,-m3) // sqrt(2j3+1) // // Input: two_j1, two_j2, two_j3, two_m1, two_m2, two_m3 (all in 2j, 2m units) // ================================================================ inline double wigner3j(int two_j1, int two_j2, int two_j3, int two_m1, int two_m2, int two_m3) { // Selection-rule check if (two_m1 + two_m2 + two_m3 != 0) return 0.0; if (std::abs(two_m1) > two_j1) return 0.0; if (std::abs(two_m2) > two_j2) return 0.0; if (std::abs(two_m3) > two_j3) return 0.0; if (!detail::triangle(two_j1, two_j2, two_j3)) return 0.0; // j+m must be integer → two_j + two_m must be even if ((two_j1 + two_m1) % 2 != 0) return 0.0; if ((two_j2 + two_m2) % 2 != 0) return 0.0; if ((two_j3 + two_m3) % 2 != 0) return 0.0; // Racah formula double log_prefactor = 0.5 * detail::triangle_coeff(two_j1, two_j2, two_j3); // sqrt factor from factorials of (j+m) and (j-m) log_prefactor += 0.5 * ( detail::log_factorial((two_j1 + two_m1) / 2) + detail::log_factorial((two_j1 - two_m1) / 2) + detail::log_factorial((two_j2 + two_m2) / 2) + detail::log_factorial((two_j2 - two_m2) / 2) + detail::log_factorial((two_j3 + two_m3) / 2) + detail::log_factorial((two_j3 - two_m3) / 2) ); // Sum over t int t_min = std::max({0, (two_j2 - two_j3 - two_m1) / 2, (two_j1 + two_m2 - two_j3) / 2}); // Wait, need to be more careful. Use half-integer values. // t ranges where all factorial arguments are non-negative int a1 = (two_j1 + two_j2 - two_j3) / 2; // j1+j2-j3 int a2 = (two_j1 - two_m1) / 2; // j1-m1 int a3 = (two_j2 + two_m2) / 2; // j2+m2 int c1 = (-two_j2 + two_j3 + two_m1) / 2; // j3-j2+m1 int c2 = (-two_j1 + two_j3 - two_m2) / 2; // j3-j1-m2 t_min = std::max({0, -c1, -c2}); int t_max = std::min({a1, a2, a3}); double sum = 0.0; for (int t = t_min; t <= t_max; ++t) { double log_term = detail::log_factorial(t) + detail::log_factorial(a1 - t) + detail::log_factorial(a2 - t) + detail::log_factorial(a3 - t) + detail::log_factorial(t + c1) + detail::log_factorial(t + c2); double sign = (t % 2 == 0) ? 1.0 : -1.0; sum += sign * std::exp(log_prefactor - log_term); } // Phase: (-1)^{(j1-j2-m3)/1} but in 2j units: (-1)^{(two_j1-two_j2-two_m3)/2} int phase_exp = (two_j1 - two_j2 - two_m3) / 2; double phase = (phase_exp % 2 == 0) ? 1.0 : -1.0; return phase * sum; } // ================================================================ // Clebsch-Gordan coefficient // = (-1)^{j1-j2+M} sqrt(2J+1) · (j1 j2 J ) // (m1 m2 -M) // ================================================================ inline double clebschGordan(int two_j1, int two_m1, int two_j2, int two_m2, int two_J, int two_M) { if (two_m1 + two_m2 != two_M) return 0.0; int phase_exp = (two_j1 - two_j2 + two_M) / 2; double phase = (phase_exp % 2 == 0) ? 1.0 : -1.0; double sqrt_2J1 = std::sqrt(static_cast(two_J + 1)); return phase * sqrt_2J1 * wigner3j(two_j1, two_j2, two_J, two_m1, two_m2, -two_M); } // ================================================================ // Wigner 6-j symbol {j1 j2 j3} // {j4 j5 j6} // Racah formula // ================================================================ inline double wigner6j(int two_j1, int two_j2, int two_j3, int two_j4, int two_j5, int two_j6) { // Triangle conditions (4 triads) if (!detail::triangle(two_j1, two_j2, two_j3)) return 0.0; if (!detail::triangle(two_j1, two_j5, two_j6)) return 0.0; if (!detail::triangle(two_j4, two_j2, two_j6)) return 0.0; if (!detail::triangle(two_j4, two_j5, two_j3)) return 0.0; double log_delta = detail::triangle_coeff(two_j1, two_j2, two_j3) + detail::triangle_coeff(two_j1, two_j5, two_j6) + detail::triangle_coeff(two_j4, two_j2, two_j6) + detail::triangle_coeff(two_j4, two_j5, two_j3); log_delta *= 0.5; // Range of t int t_min = std::max({ (two_j1 + two_j2 + two_j3) / 2, (two_j1 + two_j5 + two_j6) / 2, (two_j4 + two_j2 + two_j6) / 2, (two_j4 + two_j5 + two_j3) / 2 }); int t_max = std::min({ (two_j1 + two_j2 + two_j4 + two_j5) / 2, (two_j2 + two_j3 + two_j5 + two_j6) / 2, (two_j1 + two_j3 + two_j4 + two_j6) / 2 }); double sum = 0.0; for (int t = t_min; t <= t_max; ++t) { double log_num = detail::log_factorial(t + 1); double log_den = detail::log_factorial(t - (two_j1 + two_j2 + two_j3) / 2) + detail::log_factorial(t - (two_j1 + two_j5 + two_j6) / 2) + detail::log_factorial(t - (two_j4 + two_j2 + two_j6) / 2) + detail::log_factorial(t - (two_j4 + two_j5 + two_j3) / 2) + detail::log_factorial((two_j1 + two_j2 + two_j4 + two_j5) / 2 - t) + detail::log_factorial((two_j2 + two_j3 + two_j5 + two_j6) / 2 - t) + detail::log_factorial((two_j1 + two_j3 + two_j4 + two_j6) / 2 - t); double sign = (t % 2 == 0) ? 1.0 : -1.0; sum += sign * std::exp(log_delta + log_num - log_den); } return sum; } // ================================================================ // Wigner 9-j symbol: expressed as a sum of products of 3-j symbols // {j1 j2 j3} // {j4 j5 j6} = Σ_t (2t+1) {j1 j4 j7} {j2 j5 j8} {j3 j6 j9} // {j7 j8 j9} {j8 t j6} {j4 t j3} {j1 t j5} // ... (product of Wigner 6-j symbols) // ================================================================ inline double wigner9j(int two_j1, int two_j2, int two_j3, int two_j4, int two_j5, int two_j6, int two_j7, int two_j8, int two_j9) { // 9-j = Σ_t (-1)^{2t} (2t+1) {j1 j4 j7} {j2 j5 j8} {j3 j6 j9} // {j8 j9 t} {j4 j6 t} {j2 j7 t} // Wait, let me use the standard formula: // {j1 j2 j3} // {j4 j5 j6} = Σ_t (-1)^{2t} (2t+1) // {j7 j8 j9} × {j1 j4 j7} {j2 j5 j8} {j3 j6 j9} // {j8 j9 t } {j4 j6 t } {j7 j2 t } // No, the standard definition: // 9j = Σ_{two_t} (-1)^{two_t} (two_t+1) // × 6j(j1,j4,j7, j8,j9,t) × 6j(j2,j5,j8, j4,t,j6) × 6j(j3,j6,j9, t,j1,j2) // Wait, I need to look up the correct formula more carefully. // Standard: 9j{a b c; d e f; g h j} = // Σ_x (-1)^{2x}(2x+1) 6j{a b c; f j x} 6j{d e f; b x h} 6j{g h j; x a d} int two_t_min = std::max({ std::abs(two_j1 - two_j9), std::abs(two_j2 - two_j6), std::abs(two_j4 - two_j8) }); int two_t_max = std::min({ two_j1 + two_j9, two_j2 + two_j6, two_j4 + two_j8 }); double sum = 0.0; for (int two_t = two_t_min; two_t <= two_t_max; two_t += 2) { double sign = (two_t % 2 == 0) ? 1.0 : -1.0; // (-1)^{2t} = (-1)^{two_t} // Actually (-1)^{2t} where t = two_t/2, so 2t = two_t. (-1)^{two_t}. double w6j_1 = wigner6j(two_j1, two_j2, two_j3, two_j6, two_j9, two_t); double w6j_2 = wigner6j(two_j4, two_j5, two_j6, two_j2, two_t, two_j8); double w6j_3 = wigner6j(two_j7, two_j8, two_j9, two_t, two_j1, two_j4); sum += sign * static_cast(two_t + 1) * w6j_1 * w6j_2 * w6j_3; } return sum; } } // namespace special } // namespace sangi #endif // SANGI_SPECIAL_COUPLING_HPP