|
| 1 | +"""Exchange kernels via Gauss-Legendre quadrature with rational mapping.""" |
| 2 | +from __future__ import annotations |
| 3 | + |
| 4 | +from functools import lru_cache |
| 5 | +from typing import TYPE_CHECKING |
| 6 | + |
| 7 | +import numpy as np |
| 8 | +import scipy.special as sps |
| 9 | +from scipy.special import roots_legendre |
| 10 | + |
| 11 | +if TYPE_CHECKING: |
| 12 | + from numpy.typing import NDArray |
| 13 | + |
| 14 | + ComplexArray = NDArray[np.complex128] |
| 15 | + RealArray = NDArray[np.float64] |
| 16 | + |
| 17 | + |
| 18 | +def _N_order(n1: int, m1: int, n2: int, m2: int) -> int: |
| 19 | + return (n1 - m1) - (m2 - n2) |
| 20 | + |
| 21 | + |
| 22 | +def _parity_factor(N: int) -> int: |
| 23 | + """(-1)^((N+|N|)/2) → (-1)^N for N>=0, and 1 for N<0.""" |
| 24 | + return (-1) ** ((N + abs(N)) // 2) |
| 25 | + |
| 26 | + |
| 27 | +@lru_cache(maxsize=None) |
| 28 | +def _logfact(n: int) -> float: |
| 29 | + return float(sps.gammaln(n + 1)) |
| 30 | + |
| 31 | + |
| 32 | +def _C_and_indices(n1: int, m1: int, n2: int, m2: int): |
| 33 | + """Constants and Laguerre parameters for f_{n1,m1} * f_{m2,n2}.""" |
| 34 | + p, d1 = min(n1, m1), abs(n1 - m1) |
| 35 | + q, d2 = min(m2, n2), abs(m2 - n2) |
| 36 | + logC = 0.5 * ((_logfact(p) - _logfact(p + d1)) + (_logfact(q) - _logfact(q + d2))) |
| 37 | + C = np.exp(logC) |
| 38 | + return C, p, d1, q, d2 |
| 39 | + |
| 40 | + |
| 41 | +@lru_cache(maxsize=None) |
| 42 | +def _legendre_nodes_weights_mapped(nquad: int, scale: float): |
| 43 | + """ |
| 44 | + Gauss-Legendre nodes/weights mapped from [-1, 1] to [0, inf). |
| 45 | + Mapping: z = scale * (1+x)/(1-x) |
| 46 | + Jacobian: dz/dx = scale * 2/(1-x)^2 |
| 47 | + """ |
| 48 | + x, w_leg = roots_legendre(nquad) |
| 49 | + denom = 1.0 - x |
| 50 | + z = scale * (1.0 + x) / denom |
| 51 | + w = w_leg * (scale * 2.0 / (denom * denom)) |
| 52 | + return z, w |
| 53 | + |
| 54 | + |
| 55 | +def get_exchange_kernels_GaussLegendre( |
| 56 | + G_magnitudes, |
| 57 | + G_angles, |
| 58 | + nmax: int, |
| 59 | + *, |
| 60 | + potential: str = "coulomb", |
| 61 | + kappa: float = 1.0, |
| 62 | + V_of_q=None, |
| 63 | + nquad: int = 1000, |
| 64 | + scale: float = 0.5, |
| 65 | + ell: float = 1.0, |
| 66 | +) -> "ComplexArray": |
| 67 | + """Compute X_{n1,m1,n2,m2}(G) using Gauss-Legendre quadrature with rational mapping. |
| 68 | +
|
| 69 | + This backend maps the semi-infinite radial integral to the finite interval [-1, 1] |
| 70 | + using the rational mapping z = scale * (1+x)/(1-x). It avoids the numerical instability |
| 71 | + of Gauss-Laguerre quadrature for large quantum numbers while remaining faster than |
| 72 | + Hankel transforms. |
| 73 | +
|
| 74 | + Parameters |
| 75 | + ---------- |
| 76 | + G_magnitudes, G_angles : |
| 77 | + Arrays of the same shape describing |G| and polar angle θ_G. |
| 78 | + nmax : |
| 79 | + Number of Landau levels. |
| 80 | + potential : |
| 81 | + Either ``'coulomb'`` (default) or ``'general'``. |
| 82 | + kappa : |
| 83 | + Interaction strength prefactor. |
| 84 | + V_of_q : |
| 85 | + Callable ``V_of_q(q) -> V(q)`` used when ``potential='general'``. |
| 86 | + nquad : |
| 87 | + Number of quadrature points (default 1000). |
| 88 | + scale : |
| 89 | + Mapping scale factor (default 0.5). Controls the distribution of points. |
| 90 | + Smaller values cluster points near the peak of the integrand for large n. |
| 91 | + ell : |
| 92 | + Magnetic length ℓ_B (default 1.0). |
| 93 | +
|
| 94 | + Returns |
| 95 | + ------- |
| 96 | + Xs : (nG, nmax, nmax, nmax, nmax) complex array |
| 97 | + """ |
| 98 | + G_magnitudes = np.asarray(G_magnitudes, dtype=float) |
| 99 | + G_angles = np.asarray(G_angles, dtype=float) |
| 100 | + if G_magnitudes.shape != G_angles.shape: |
| 101 | + raise ValueError("G_magnitudes and G_angles must have the same shape.") |
| 102 | + nG = G_magnitudes.size |
| 103 | + |
| 104 | + Gscaled = G_magnitudes * float(ell) |
| 105 | + Xs = np.zeros((nG, nmax, nmax, nmax, nmax), dtype=np.complex128) |
| 106 | + |
| 107 | + # Get mapped grid |
| 108 | + z, w = _legendre_nodes_weights_mapped(nquad, scale) |
| 109 | + |
| 110 | + # Precompute Bessel functions J_N(sqrt(2z)*G) |
| 111 | + # We cache by absN to avoid recomputing |
| 112 | + J_cache: dict[int, np.ndarray] = {} |
| 113 | + |
| 114 | + # Cache for Laguerre evaluations |
| 115 | + # We evaluate L_n^d(z) for many n, d. |
| 116 | + # Since n, d are small integers, we can just compute on the fly or use sps.eval_genlaguerre |
| 117 | + # sps.eval_genlaguerre is efficient enough. |
| 118 | + |
| 119 | + for n1 in range(nmax): |
| 120 | + for m1 in range(nmax): |
| 121 | + for n2 in range(nmax): |
| 122 | + for m2 in range(nmax): |
| 123 | + N = _N_order(n1, m1, n2, m2) |
| 124 | + absN = abs(N) |
| 125 | + C, p, d1, q, d2 = _C_and_indices(n1, m1, n2, m2) |
| 126 | + |
| 127 | + # Compute radial integral |
| 128 | + if potential == "coulomb": |
| 129 | + # Integrand factor: exp(-z) * z^alpha * L * L * J |
| 130 | + # alpha = (d1 + d2 - 1) / 2 |
| 131 | + alpha = 0.5 * (d1 + d2 - 1) |
| 132 | + |
| 133 | + L1 = sps.eval_genlaguerre(p, d1, z) |
| 134 | + L2 = sps.eval_genlaguerre(q, d2, z) |
| 135 | + |
| 136 | + # Bessel part |
| 137 | + if absN not in J_cache: |
| 138 | + arg = np.sqrt(2.0 * z)[None, :] * Gscaled[:, None] |
| 139 | + J_cache[absN] = sps.jv(absN, arg) |
| 140 | + J_abs = J_cache[absN] |
| 141 | + |
| 142 | + # Full integrand term (excluding J and weights) |
| 143 | + # exp(-z) handles x->1 decay |
| 144 | + # z^alpha handles x->-1 behavior |
| 145 | + term = np.exp(-z) * (z**alpha) * L1 * L2 |
| 146 | + |
| 147 | + # Sum over quadrature points |
| 148 | + # J_abs is (nG, nquad), term is (nquad,), w is (nquad,) |
| 149 | + # Result is (nG,) |
| 150 | + radial = (J_abs * term) @ w |
| 151 | + |
| 152 | + signN = _parity_factor(N) |
| 153 | + phase_factor = (1j) ** (d1 - d2) |
| 154 | + pref = (kappa * C / np.sqrt(2.0)) * phase_factor |
| 155 | + |
| 156 | + else: |
| 157 | + # General potential |
| 158 | + if not callable(V_of_q): |
| 159 | + raise ValueError("Provide V_of_q for potential='general'") |
| 160 | + |
| 161 | + alpha = 0.5 * (d1 + d2) |
| 162 | + L1 = sps.eval_genlaguerre(p, d1, z) |
| 163 | + L2 = sps.eval_genlaguerre(q, d2, z) |
| 164 | + |
| 165 | + qvals = np.sqrt(2.0 * z) / float(ell) |
| 166 | + Veff = V_of_q(qvals) / (2.0 * np.pi * float(ell) ** 2) |
| 167 | + |
| 168 | + if absN not in J_cache: |
| 169 | + arg = np.sqrt(2.0 * z)[None, :] * Gscaled[:, None] |
| 170 | + J_cache[absN] = sps.jv(absN, arg) |
| 171 | + J_abs = J_cache[absN] |
| 172 | + |
| 173 | + term = np.exp(-z) * (z**alpha) * L1 * L2 * Veff |
| 174 | + radial = (J_abs * term) @ w |
| 175 | + |
| 176 | + signN = _parity_factor(N) |
| 177 | + phase_factor = (1j) ** (d1 - d2) |
| 178 | + pref = C * phase_factor |
| 179 | + |
| 180 | + phase = np.exp(-1j * N * G_angles) |
| 181 | + Xs[:, n1, m1, n2, m2] = (pref * phase) * (signN * radial) |
| 182 | + |
| 183 | + return Xs |
| 184 | + |
| 185 | + |
| 186 | +__all__ = ["get_exchange_kernels_GaussLegendre"] |
0 commit comments