Skip to content

Commit cd05cbd

Browse files
committed
feat: add Gauss-Legendre quadrature backend for exchange kernels and update documentation and dispatcher.
1 parent 23e579e commit cd05cbd

6 files changed

Lines changed: 355 additions & 8 deletions

File tree

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,33 @@ related calculations into a small, reusable package. It provides:
77

88
- Analytic Landau-level plane-wave form factors $F_{n',n}(\mathbf{q})$.
99
- Exchange kernels $X_{n_1 m_1 n_2 m_2}(\mathbf{G})$ computed via:
10+
- Gauss-Legendre quadrature with rational mapping (`gausslegendre` backend, default).
1011
- Generalized Gauss–Laguerre quadrature (`gausslag` backend).
1112
- Hankel-transform based integration (`hankel` backend).
1213
- Symmetry diagnostics for verifying kernel implementations on a given G-grid.
1314

15+
## Backends and Reliability
16+
17+
The package provides three backends for computing exchange kernels, each with different performance and stability characteristics:
18+
19+
1. **`gausslegendre` (Default)**:
20+
- **Method**: Gauss-Legendre quadrature mapped from $[-1, 1]$ to $[0, \infty)$ via a rational mapping.
21+
- **Pros**: Fast and numerically stable for all Landau level indices ($n$).
22+
- **Cons**: May require tuning `nquad` for extremely large momenta or indices ($n > 100$).
23+
- **Recommended for**: General usage, especially for large $n$ ($n \ge 10$).
24+
25+
2. **`gausslag`**:
26+
- **Method**: Generalized Gauss-Laguerre quadrature.
27+
- **Pros**: Very fast for small $n$.
28+
- **Cons**: Numerically unstable for large $n$ ($n \ge 12$) due to high-order Laguerre polynomial roots.
29+
- **Recommended for**: Small systems ($n < 10$) where speed is critical.
30+
31+
3. **`hankel`**:
32+
- **Method**: Discrete Hankel transform.
33+
- **Pros**: High precision and stability.
34+
- **Cons**: Significantly slower than quadrature methods.
35+
- **Recommended for**: Reference calculations and verifying other backends.
36+
1437
## Mathematical Definitions
1538

1639
### Plane-Wave Form Factors
@@ -66,7 +89,7 @@ thetas = np.array([0.0, 0.0, np.pi])
6689
nmax = 2
6790

6891
F = get_form_factors(Gs_dimless, thetas, nmax) # shape (nG, nmax, nmax)
69-
X = get_exchange_kernels(Gs_dimless, thetas, nmax) # default 'gausslag' backend
92+
X = get_exchange_kernels(Gs_dimless, thetas, nmax) # default 'gausslegendre' backend
7093

7194
print("F shape:", F.shape)
7295
print("X shape:", X.shape)

src/quantumhall_matrixelements/__init__.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .planewave import get_form_factors
1818
from .exchange_gausslag import get_exchange_kernels_GaussLag
1919
from .exchange_hankel import get_exchange_kernels_hankel
20+
from .exchange_legendre import get_exchange_kernels_GaussLegendre
2021

2122
if TYPE_CHECKING:
2223
from numpy.typing import NDArray
@@ -31,6 +32,7 @@ def get_exchange_kernels(
3132
nmax: int,
3233
*,
3334
method: str | None = None,
35+
**kwargs,
3436
) -> "ComplexArray":
3537
"""Dispatcher for exchange kernels.
3638
@@ -44,27 +46,35 @@ def get_exchange_kernels(
4446
method :
4547
Backend selector:
4648
47-
- ``'gausslag'`` (default): generalized Gauss–Laguerre quadrature
48-
over the radial integral, using the analytic angular dependence.
49-
- ``'hankel'`` : Hankel-transform based implementation.
49+
- ``'gausslegendre'`` (default): Gauss-Legendre quadrature with rational mapping.
50+
Recommended for all nmax.
51+
- ``'gausslag'``: generalized Gauss–Laguerre quadrature.
52+
Fast for small nmax (< 10), but unstable for large nmax.
53+
- ``'hankel'``: Hankel-transform based implementation.
54+
55+
**kwargs :
56+
Additional arguments passed to the backend (e.g. ``nquad``, ``scale``).
5057
5158
Notes
5259
-----
5360
Both backends return kernels normalized for :math:`\\kappa = 1`. Any
5461
physical interaction strength should be applied by the caller.
5562
"""
56-
chosen = (method or "gausslag").strip().lower()
63+
chosen = (method or "gausslegendre").strip().lower()
5764
if chosen in {"gausslag", "gauss-lag", "gausslaguerre", "gauss-laguerre", "gl"}:
58-
return get_exchange_kernels_GaussLag(G_magnitudes, G_angles, nmax)
65+
return get_exchange_kernels_GaussLag(G_magnitudes, G_angles, nmax, **kwargs)
5966
if chosen in {"hankel", "hk"}:
60-
return get_exchange_kernels_hankel(G_magnitudes, G_angles, nmax)
61-
raise ValueError(f"Unknown exchange-kernel method: {method!r}. Use 'gausslag' or 'hankel'.")
67+
return get_exchange_kernels_hankel(G_magnitudes, G_angles, nmax, **kwargs)
68+
if chosen in {"gausslegendre", "gauss-legendre", "legendre", "leg"}:
69+
return get_exchange_kernels_GaussLegendre(G_magnitudes, G_angles, nmax, **kwargs)
70+
raise ValueError(f"Unknown exchange-kernel method: {method!r}. Use 'gausslegendre', 'gausslag', or 'hankel'.")
6271

6372

6473
__all__ = [
6574
"get_form_factors",
6675
"get_exchange_kernels",
6776
"get_exchange_kernels_GaussLag",
6877
"get_exchange_kernels_hankel",
78+
"get_exchange_kernels_GaussLegendre",
6979
]
7080

src/quantumhall_matrixelements/exchange_hankel.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def get_exchange_kernels_hankel(
7575
G_magnitudes: "RealArray",
7676
G_angles: "RealArray",
7777
nmax: int,
78+
**kwargs,
7879
) -> "ComplexArray":
7980
"""Compute X_{n1,m1,n2,m2}(G) via Hankel transforms (κ=1 convention).
8081
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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"]

tests/test_exchange_legendre.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import numpy as np
2+
import pytest
3+
from quantumhall_matrixelements import get_exchange_kernels_GaussLegendre, get_exchange_kernels
4+
5+
def test_legendre_basic_shape():
6+
nmax = 2
7+
Gs_dimless = np.array([0.0, 1.0])
8+
thetas = np.array([0.0, np.pi])
9+
X = get_exchange_kernels_GaussLegendre(Gs_dimless, thetas, nmax, nquad=100)
10+
assert X.shape == (2, nmax, nmax, nmax, nmax)
11+
assert np.isfinite(X).all()
12+
13+
def test_legendre_vs_hankel_small_n():
14+
"""Verify agreement with Hankel for small n."""
15+
nmax = 2
16+
Gs_dimless = np.array([0.5, 1.5])
17+
thetas = np.array([0.0, 0.2])
18+
19+
X_leg = get_exchange_kernels(Gs_dimless, thetas, nmax, method="gausslegendre", nquad=500)
20+
X_hk = get_exchange_kernels(Gs_dimless, thetas, nmax, method="hankel")
21+
22+
assert np.allclose(X_leg, X_hk, rtol=1e-3, atol=1e-3)
23+
24+
def test_legendre_large_n_stability():
25+
"""Verify that it runs without error for large n (where gausslag fails)."""
26+
nmax = 15
27+
Gs_dimless = np.array([1.0])
28+
thetas = np.array([0.0])
29+
30+
# This should not raise an error
31+
X = get_exchange_kernels_GaussLegendre(Gs_dimless, thetas, nmax, nquad=500)
32+
assert np.isfinite(X).all()

0 commit comments

Comments
 (0)