quantumhall-matrixelements 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ """Landau-level plane-wave form factors and exchange kernels.
2
+
3
+ This package provides reusable numerical kernels for quantum Hall matrix
4
+ elements in a Landau-level basis:
5
+
6
+ - `get_form_factors` for plane-wave form factors :math:`F_{n',n}(G)`.
7
+ - `get_exchange_kernels` (and backend-specific variants) for exchange kernels
8
+ :math:`X_{n_1 m_1 n_2 m_2}(G)` built from LL wavefunctions.
9
+ - Optional symmetry diagnostics for sanity-checking kernel implementations.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ import numpy as np
16
+
17
+ from .planewave import get_form_factors
18
+ from .exchange_gausslag import get_exchange_kernels_GaussLag
19
+ from .exchange_hankel import get_exchange_kernels_hankel
20
+ from .exchange_legendre import get_exchange_kernels_GaussLegendre
21
+
22
+ if TYPE_CHECKING:
23
+ from numpy.typing import NDArray
24
+
25
+ ComplexArray = NDArray[np.complex128]
26
+ RealArray = NDArray[np.float64]
27
+
28
+
29
+ def get_exchange_kernels(
30
+ G_magnitudes: "RealArray",
31
+ G_angles: "RealArray",
32
+ nmax: int,
33
+ *,
34
+ method: str | None = None,
35
+ **kwargs,
36
+ ) -> "ComplexArray":
37
+ """Dispatcher for exchange kernels.
38
+
39
+ Parameters
40
+ ----------
41
+ G_magnitudes, G_angles :
42
+ Arrays describing the reciprocal vectors :math:`G` in polar form.
43
+ Both must have the same shape; broadcasting is not applied.
44
+ nmax :
45
+ Number of Landau levels (0..nmax-1) to include.
46
+ method :
47
+ Backend selector:
48
+
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``).
57
+
58
+ Notes
59
+ -----
60
+ Both backends return kernels normalized for :math:`\\kappa = 1`. Any
61
+ physical interaction strength should be applied by the caller.
62
+ """
63
+ chosen = (method or "gausslegendre").strip().lower()
64
+ if chosen in {"gausslag", "gauss-lag", "gausslaguerre", "gauss-laguerre", "gl"}:
65
+ return get_exchange_kernels_GaussLag(G_magnitudes, G_angles, nmax, **kwargs)
66
+ if chosen in {"hankel", "hk"}:
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'.")
71
+
72
+
73
+ __all__ = [
74
+ "get_form_factors",
75
+ "get_exchange_kernels",
76
+ "get_exchange_kernels_GaussLag",
77
+ "get_exchange_kernels_hankel",
78
+ "get_exchange_kernels_GaussLegendre",
79
+ ]
80
+
@@ -0,0 +1,50 @@
1
+ """Debug helper to print exchange-kernel symmetry deviations.
2
+
3
+ This is not part of the public API; it exists only to make it easy to
4
+ inspect symmetry errors for different backends via
5
+
6
+ python -m quantumhall_matrixelements._debug_symmetry
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import numpy as np
11
+
12
+ from . import get_exchange_kernels
13
+
14
+
15
+ def main() -> None:
16
+ nmax = 2
17
+ Gs_dimless = np.array([0.0, 1.0, 1.0])
18
+ thetas = np.array([0.0, 0.0, np.pi])
19
+ thetas_minus = (thetas + np.pi) % (2 * np.pi)
20
+
21
+ methods = ["gausslag", "hankel"]
22
+
23
+ for method in methods:
24
+ print(f"=== method={method} ===")
25
+ X_G = get_exchange_kernels(Gs_dimless, thetas, nmax, method=method)
26
+ X_mG = get_exchange_kernels(Gs_dimless, thetas_minus, nmax, method=method)
27
+
28
+ expected_mG = np.transpose(X_G, (0, 3, 4, 1, 2)).conj()
29
+ diff_G_to_mG = float(np.max(np.abs(X_mG - expected_mG)))
30
+ print(f"max |X(-G) - (X^T(G))†| = {diff_G_to_mG:.3e}")
31
+
32
+ idx = np.arange(nmax)
33
+ N = (
34
+ idx[:, None, None, None]
35
+ - idx[None, :, None, None]
36
+ - idx[None, None, :, None]
37
+ + idx[None, None, None, :]
38
+ )
39
+ phase = (-1.0) ** np.abs(N)
40
+ expected_internal = phase[None, ...] * expected_mG
41
+ diff_internal = float(np.max(np.abs(X_G - expected_internal)))
42
+ print(
43
+ "max |X(G) - (-1)^|N| (X^T(G))†| = "
44
+ f"{diff_internal:.3e}",
45
+ )
46
+
47
+
48
+ if __name__ == "__main__": # pragma: no cover - manual debug entry point
49
+ main()
50
+
@@ -0,0 +1,53 @@
1
+ """Diagnostic helpers for exchange-kernel symmetry checks."""
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ import numpy as np
7
+
8
+ from . import get_exchange_kernels
9
+
10
+ if TYPE_CHECKING: # pragma: no cover - aliases only
11
+ from numpy.typing import NDArray
12
+
13
+ RealArray = NDArray[np.float64]
14
+ ComplexArray = NDArray[np.complex128]
15
+
16
+ __all__ = [
17
+ "verify_exchange_kernel_symmetries",
18
+ ]
19
+
20
+
21
+ def verify_exchange_kernel_symmetries(
22
+ G_magnitudes: "RealArray",
23
+ G_angles: "RealArray",
24
+ nmax: int,
25
+ rtol: float = 1e-7,
26
+ atol: float = 1e-9,
27
+ ) -> None:
28
+ """Verify the exchange-kernel G-inversion symmetry implied by Σ^F(-G)=Σ^F(G)^†.
29
+
30
+ With the convention
31
+
32
+ Σ^F_{mn}(G) = - Σ_{r,t} X_{nrtm}(G) ρ_{tr}(G),
33
+
34
+ Hermiticity Σ^F(-G) = Σ^F(G)^† for densities obeying ρ(-G)=ρ(G)^† requires
35
+
36
+ X_{nrtm}(-G) = X_{m r t n}(G)^*,
37
+
38
+ i.e. in array form (Xs[g, n1, m1, n2, m2]):
39
+
40
+ Xs[g, n1, m1, n2, m2]_(-G) = Xs[g, m2, n2, m1, n1]_G^*.
41
+ """
42
+ Xs_G = get_exchange_kernels(G_magnitudes, G_angles, nmax)
43
+ G_angles_minus = (G_angles + np.pi) % (2 * np.pi)
44
+ Xs_minusG = get_exchange_kernels(G_magnitudes, G_angles_minus, nmax)
45
+
46
+ # expected_minus[g, n1, m1, n2, m2] = X_G[g, m2, n2, m1, n1]^*
47
+ expected_Xs_minusG = np.transpose(Xs_G, (0, 4, 3, 2, 1)).conj()
48
+ if not np.allclose(Xs_minusG, expected_Xs_minusG, rtol=rtol, atol=atol):
49
+ diff = float(np.max(np.abs(Xs_minusG - expected_Xs_minusG)))
50
+ raise AssertionError(
51
+ f"Exchange kernel G-inversion symmetry failed: max|Δ|={diff:.3e} "
52
+ f"(rtol={rtol}, atol={atol})"
53
+ )
@@ -0,0 +1,163 @@
1
+ """Exchange kernels via generalized Gauss–Laguerre quadrature."""
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
+
10
+ if TYPE_CHECKING:
11
+ from numpy.typing import NDArray
12
+
13
+ ComplexArray = NDArray[np.complex128]
14
+ RealArray = NDArray[np.float64]
15
+
16
+
17
+ def _N_order(n1: int, m1: int, n2: int, m2: int) -> int:
18
+ return (n1 - m1) - (m2 - n2)
19
+
20
+
21
+ def _parity_factor(N: int) -> int:
22
+ """(-1)^((N+|N|)/2) → (-1)^N for N>=0, and 1 for N<0."""
23
+ return (-1) ** ((N + abs(N)) // 2)
24
+
25
+
26
+ @lru_cache(maxsize=None)
27
+ def _lag_nodes_weights(nquad: int, alpha: float):
28
+ """Generalized Gauss–Laguerre nodes/weights for ∫_0^∞ e^{-z} z^α f(z) dz."""
29
+ x, w = sps.roots_genlaguerre(nquad, alpha)
30
+ return x, w
31
+
32
+
33
+ @lru_cache(maxsize=None)
34
+ def _logfact(n: int) -> float:
35
+ return float(sps.gammaln(n + 1))
36
+
37
+
38
+ def _C_and_indices(n1: int, m1: int, n2: int, m2: int):
39
+ """Constants and Laguerre parameters for f_{n1,m1} * f_{m2,n2}."""
40
+ p, d1 = min(n1, m1), abs(n1 - m1)
41
+ q, d2 = min(m2, n2), abs(m2 - n2)
42
+ logC = 0.5 * ((_logfact(p) - _logfact(p + d1)) + (_logfact(q) - _logfact(q + d2)))
43
+ C = np.exp(logC)
44
+ return C, p, d1, q, d2
45
+
46
+
47
+ _L_cache: dict[tuple[int, int, float, int], np.ndarray] = {}
48
+
49
+
50
+ def _laguerre_on_grid(p: int, d: int, alpha: float, nquad: int, z):
51
+ key = (p, d, float(alpha), int(nquad))
52
+ L = _L_cache.get(key)
53
+ if L is None:
54
+ L = sps.eval_genlaguerre(p, d, z)
55
+ _L_cache[key] = L
56
+ return L
57
+
58
+
59
+ def get_exchange_kernels_GaussLag(
60
+ G_magnitudes,
61
+ G_angles,
62
+ nmax: int,
63
+ *,
64
+ potential: str = "coulomb",
65
+ kappa: float = 1.0,
66
+ V_of_q=None,
67
+ nquad: int = 200,
68
+ ell: float = 1.0,
69
+ ) -> "ComplexArray":
70
+ """Compute X_{n1,m1,n2,m2}(G) using analytic angle and Gauss–Laguerre radial quadrature.
71
+
72
+ Parameters
73
+ ----------
74
+ G_magnitudes, G_angles :
75
+ Arrays of the same shape describing |G| and polar angle θ_G.
76
+ nmax :
77
+ Number of Landau levels.
78
+ potential :
79
+ Either ``'coulomb'`` (default) or ``'general'``. In the latter case
80
+ a callable ``V_of_q(q)`` must be provided.
81
+ kappa :
82
+ Interaction strength prefactor. For Coulomb this corresponds to
83
+ :math:`\\kappa = e^2/(\\varepsilon\\ell_B)/\\hbar\\omega_c`.
84
+ V_of_q :
85
+ Callable ``V_of_q(q) -> V(q)`` used when ``potential='general'``.
86
+ nquad :
87
+ Number of Gauss–Laguerre quadrature points.
88
+ ell :
89
+ Magnetic length ℓ_B (default 1.0); |G| is interpreted in 1/ℓ_B units.
90
+
91
+ Returns
92
+ -------
93
+ Xs : (nG, nmax, nmax, nmax, nmax) complex array
94
+ Exchange kernels normalized with the chosen kappa.
95
+ """
96
+ G_magnitudes = np.asarray(G_magnitudes, dtype=float)
97
+ G_angles = np.asarray(G_angles, dtype=float)
98
+ if G_magnitudes.shape != G_angles.shape:
99
+ raise ValueError("G_magnitudes and G_angles must have the same shape.")
100
+ nG = G_magnitudes.size
101
+
102
+ Gscaled = G_magnitudes * float(ell)
103
+ Xs = np.zeros((nG, nmax, nmax, nmax, nmax), dtype=np.complex128)
104
+
105
+ J_cache: dict[tuple[int, float], np.ndarray] = {}
106
+
107
+ for n1 in range(nmax):
108
+ for m1 in range(nmax):
109
+ for n2 in range(nmax):
110
+ for m2 in range(nmax):
111
+ N = _N_order(n1, m1, n2, m2)
112
+ absN = abs(N)
113
+ C, p, d1, q, d2 = _C_and_indices(n1, m1, n2, m2)
114
+
115
+ if potential == "coulomb":
116
+ alpha = 0.5 * (d1 + d2 - 1)
117
+ if alpha <= -1:
118
+ raise ValueError(f"Invalid alpha={alpha} for Coulomb case.")
119
+ z, w = _lag_nodes_weights(nquad, alpha)
120
+ L1 = _laguerre_on_grid(p, d1, alpha, nquad, z)
121
+ L2 = _laguerre_on_grid(q, d2, alpha, nquad, z)
122
+ W = w * L1 * L2
123
+ key = (absN, float(alpha))
124
+ J_abs = J_cache.get(key)
125
+ if J_abs is None:
126
+ arg = np.sqrt(2.0 * z)[None, :] * Gscaled[:, None]
127
+ J_abs = sps.jv(absN, arg)
128
+ J_cache[key] = J_abs
129
+ signN = _parity_factor(N)
130
+ radial = (signN * J_abs) @ W
131
+ phase_factor = (1j) ** (d1 - d2)
132
+ pref = (kappa * C / np.sqrt(2.0)) * phase_factor
133
+ else:
134
+ if not callable(V_of_q):
135
+ raise ValueError(
136
+ "For potential='general', provide V_of_q: callable(q)->V(q)."
137
+ )
138
+ alpha = 0.5 * (d1 + d2)
139
+ z, w = _lag_nodes_weights(nquad, alpha)
140
+ L1 = _laguerre_on_grid(p, d1, alpha, nquad, z)
141
+ L2 = _laguerre_on_grid(q, d2, alpha, nquad, z)
142
+ qvals = np.sqrt(2.0 * z) / float(ell)
143
+ Veff = V_of_q(qvals) / (2.0 * np.pi * float(ell) ** 2)
144
+ W = w * L1 * L2 * Veff
145
+ key = (absN, float(alpha))
146
+ J_abs = J_cache.get(key)
147
+ if J_abs is None:
148
+ arg = np.sqrt(2.0 * z)[None, :] * Gscaled[:, None]
149
+ J_abs = sps.jv(absN, arg)
150
+ J_cache[key] = J_abs
151
+ signN = _parity_factor(N)
152
+ radial = (signN * J_abs) @ W
153
+ phase_factor = (1j) ** (d1 - d2)
154
+ pref = C * phase_factor
155
+
156
+ phase = np.exp(-1j * N * G_angles)
157
+ Xs[:, n1, m1, n2, m2] = (pref * phase) * radial
158
+
159
+ return Xs
160
+
161
+
162
+ __all__ = ["get_exchange_kernels_GaussLag"]
163
+
@@ -0,0 +1,165 @@
1
+ """Exchange kernels via Hankel transforms."""
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
+ from hankel import HankelTransform
9
+ from scipy.special import genlaguerre, rgamma
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
+ return (-1) ** ((N + abs(N)) // 2)
24
+
25
+
26
+ @lru_cache(maxsize=None)
27
+ def _get_hankel_transformer(order: int) -> HankelTransform:
28
+ """Cached HankelTransform instance for a given Bessel order."""
29
+ return HankelTransform(nu=order, N=6000, h=7e-6)
30
+
31
+
32
+ def _radial_exchange_integrand_rgamma(
33
+ q_magnitudes,
34
+ n1,
35
+ m1,
36
+ n2,
37
+ m2,
38
+ potential: str | callable = "coulomb",
39
+ kappa: float = 1.0,
40
+ ):
41
+ """Build integrand g(q) for Hankel transform with rgamma-normalization.
42
+
43
+ For Coulomb: g(q) = κ F1F2 / q. We parameterize q = √2 r and absorb factors
44
+ into a stable radial base constructed from generalized Laguerres.
45
+ """
46
+ q = np.asarray(q_magnitudes, dtype=float)
47
+ r = q / np.sqrt(2.0)
48
+
49
+ Δ1, N1 = abs(n1 - m1), min(n1, m1)
50
+ Δ2, N2 = abs(n2 - m2), min(n2, m2)
51
+
52
+ power = Δ1 + Δ2 - 1
53
+
54
+ lag1 = genlaguerre(N1, Δ1)
55
+ lag2 = genlaguerre(N2, Δ2)
56
+
57
+ nrm1 = np.sqrt(lag1(0))
58
+ nrm2 = np.sqrt(lag2(0))
59
+ nrm0 = np.sqrt(rgamma(1 + Δ1) * rgamma(1 + Δ2))
60
+
61
+ z = r * r
62
+ base = np.exp(-z) * nrm0 * (r**power) * (lag1(z) / nrm1) * (lag2(z) / nrm2)
63
+
64
+ if potential == "coulomb":
65
+ return (kappa / np.sqrt(2.0)) * base
66
+ if potential == "constant":
67
+ return (kappa / (2 * np.pi)) * (r * base)
68
+ if callable(potential):
69
+ qphys = np.sqrt(2.0) * r
70
+ return (potential(qphys) / (2 * np.pi)) * (r * base)
71
+ raise ValueError("potential must be 'coulomb', 'constant', or callable V(q)")
72
+
73
+
74
+ def get_exchange_kernels_hankel(
75
+ G_magnitudes: "RealArray",
76
+ G_angles: "RealArray",
77
+ nmax: int,
78
+ **kwargs,
79
+ ) -> "ComplexArray":
80
+ """Compute X_{n1,m1,n2,m2}(G) via Hankel transforms (κ=1 convention).
81
+
82
+ This backend parametrizes the radial integral via Hankel transforms with
83
+ robust Laguerre-based normalization and explicit control over the Bessel
84
+ order. It is numerically more intensive than the Gauss–Laguerre backend
85
+ but can be useful for cross-checks or alternative potentials.
86
+ """
87
+ G_magnitudes = np.asarray(G_magnitudes, dtype=float)
88
+ G_angles = np.asarray(G_angles, dtype=float)
89
+ if G_magnitudes.shape != G_angles.shape:
90
+ raise ValueError("G_magnitudes and G_angles must have same shape")
91
+
92
+ # Unique |G| to shrink Hankel workload (same radial value for all with same |G|)
93
+ # Note: np.unique sorts; use inverse map to scatter back to original order.
94
+ k_unique, inv_idx = np.unique(G_magnitudes, return_inverse=True)
95
+ nG = G_magnitudes.size
96
+
97
+ # Precompute angular phase vectors for all possible N values
98
+ # N = (n1 - m1) - (m2 - n2) ∈ [Nmin, Nmax]
99
+ N_min = -2 * (nmax - 1)
100
+ N_max = +2 * (nmax - 1)
101
+ Ns = np.arange(N_min, N_max + 1, dtype=int)
102
+ phase_by_N: dict[int, ComplexArray] = {}
103
+ for N in Ns:
104
+ phase = -N * G_angles
105
+ phase_by_N[int(N)] = (np.cos(phase) + 1j * np.sin(phase)) * _parity_factor(int(N))
106
+
107
+ # Small lookup for internal (i)^(d1-d2), indexed by d1,d2 in [0..nmax-1]
108
+ d_vals = np.arange(nmax, dtype=int)
109
+ phase_internal_table = (1j) ** (d_vals[:, None] - d_vals[None, :]) # (nmax,nmax)
110
+ d_lookup = np.abs(np.subtract.outer(np.arange(nmax), np.arange(nmax))) # (nmax,nmax)
111
+ # Precompute abs diffs (d) and mins (Nmin) for quick indexing
112
+ d_mat = d_lookup # alias
113
+ Nmin_mat = np.minimum(
114
+ np.arange(nmax)[:, None].repeat(nmax, axis=1),
115
+ np.arange(nmax)[None, :].repeat(nmax, axis=0),
116
+ )
117
+
118
+ # Cache for radial Hankel transforms keyed by (d1,N1,d2,N2,absN)
119
+ radial_cache: dict[tuple[int, int, int, int, int], np.ndarray] = {}
120
+
121
+ # Output
122
+ Xs = np.zeros((nG, nmax, nmax, nmax, nmax), dtype=np.complex128)
123
+
124
+ # Helper to fetch/compute the radial piece for given indices
125
+ def get_radial_block(n1: int, m1: int, n2: int, m2: int, absN: int) -> np.ndarray:
126
+ d1 = int(d_mat[n1, m1])
127
+ d2 = int(d_mat[n2, m2])
128
+ N1 = int(Nmin_mat[n1, m1])
129
+ N2 = int(Nmin_mat[n2, m2])
130
+ key = (d1, N1, d2, N2, int(absN))
131
+ arr = radial_cache.get(key)
132
+ if arr is not None:
133
+ return arr
134
+
135
+ def integrand(q):
136
+ return _radial_exchange_integrand_rgamma(q, n1, m1, n2, m2)
137
+
138
+ ht = _get_hankel_transformer(absN)
139
+ # Compute only on unique radii, then cache
140
+ arr_unique = ht.transform(integrand, k_unique, ret_err=False)
141
+ radial_cache[key] = arr_unique
142
+ return arr_unique
143
+
144
+ # Main assignment: iterate indices but reuse cached radial data and phases
145
+ for n1 in range(nmax):
146
+ for m1 in range(nmax):
147
+ d1 = int(d_mat[n1, m1])
148
+ for n2 in range(nmax):
149
+ for m2 in range(nmax):
150
+ d2 = int(d_mat[n2, m2])
151
+ N = _N_order(n1, m1, n2, m2)
152
+ absN = abs(N)
153
+ # Radial part (on unique k), then scatter to all G via inv_idx
154
+ X_radial_unique = get_radial_block(n1, m1, n2, m2, absN)
155
+ X_radial = X_radial_unique[inv_idx]
156
+ # Angular/internal phases
157
+ phase_internal = phase_internal_table[d1, d2]
158
+ phase_angle = phase_by_N[N]
159
+ Xs[:, n1, m1, n2, m2] = phase_internal * phase_angle * X_radial
160
+
161
+ return Xs
162
+
163
+
164
+ __all__ = ["get_exchange_kernels_hankel"]
165
+
@@ -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"]
@@ -0,0 +1,87 @@
1
+ """Plane-wave form factors F_{n',n}(G) in a Landau-level basis."""
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ import numpy as np
7
+ from scipy.special import eval_genlaguerre, gammaln
8
+
9
+ if TYPE_CHECKING:
10
+ from numpy.typing import NDArray
11
+
12
+ ComplexArray = NDArray[np.complex128]
13
+ RealArray = NDArray[np.float64]
14
+ IntArray = NDArray[np.int64]
15
+
16
+
17
+ def _analytic_form_factor(
18
+ n_row: "IntArray",
19
+ n_col: "IntArray",
20
+ q_magnitudes: "RealArray",
21
+ q_angles: "RealArray",
22
+ lB: float,
23
+ ) -> "ComplexArray":
24
+ """Vectorized Landau level form factor F_{n_row, n_col}(q).
25
+
26
+ F_{n',n}(q) = i^{|n-n'|} e^{i(n-n')θ}
27
+ sqrt(n_min!/n_max!) (|q|ℓ/√2)^{|n'-n|}
28
+ L_{n_min}^{|n'-n|}(|q|²ℓ²/2) e^{-|q|²ℓ²/4}
29
+ """
30
+ n_min = np.minimum(n_row, n_col)
31
+ n_max = np.maximum(n_row, n_col)
32
+ delta_n_abs = np.abs(n_row - n_col)
33
+
34
+ ql = q_magnitudes * lB
35
+ arg_z = 0.5 * (ql**2)
36
+
37
+ log_ratio = 0.5 * (gammaln(n_min + 1) - gammaln(n_max + 1))
38
+ ratio = np.exp(log_ratio)
39
+
40
+ laguerre_poly = eval_genlaguerre(n_min, delta_n_abs, arg_z)
41
+
42
+ # Phase convention: F_{n',n}(q) ∝ i^{|Δn|} e^{i (n - n') θ}
43
+ angles = (n_col - n_row) * q_angles + (np.pi / 2) * delta_n_abs
44
+ angular_phase = np.cos(angles) + 1j * np.sin(angles)
45
+
46
+ F = (
47
+ angular_phase
48
+ * ratio
49
+ * np.power(ql / np.sqrt(2.0), delta_n_abs)
50
+ * laguerre_poly
51
+ * np.exp(-0.5 * arg_z)
52
+ )
53
+ return F if F.ndim > 0 else F[()]
54
+
55
+
56
+ def get_form_factors(
57
+ q_magnitudes: "RealArray", q_angles: "RealArray", nmax: int, lB: float = 1.0
58
+ ) -> "ComplexArray":
59
+ """Precompute F_{n',n}(G) for all G and Landau levels.
60
+
61
+ Parameters
62
+ ----------
63
+ q_magnitudes, q_angles :
64
+ Arrays with the same shape, describing |G|ℓ_B and polar angle θ.
65
+ nmax :
66
+ Number of Landau levels (0..nmax-1).
67
+ lB :
68
+ Magnetic length ℓ_B (default 1.0). ``q_magnitudes`` are understood
69
+ to be in units of 1/ℓ_B.
70
+
71
+ Returns
72
+ -------
73
+ F : (nG, nmax, nmax) complex array
74
+ Plane-wave form factors F_{n',n}(G).
75
+ """
76
+ n_indices = np.arange(nmax)
77
+ return _analytic_form_factor(
78
+ n_row=n_indices[None, :, None],
79
+ n_col=n_indices[None, None, :],
80
+ q_magnitudes=np.asarray(q_magnitudes)[:, None, None],
81
+ q_angles=np.asarray(q_angles)[:, None, None],
82
+ lB=lB,
83
+ ).astype(np.complex128)
84
+
85
+
86
+ __all__ = ["get_form_factors"]
87
+
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: quantumhall_matrixelements
3
+ Version: 0.1.0
4
+ Summary: Landau-level plane-wave form factors and exchange kernels for quantum Hall systems.
5
+ Author-email: Tobias Wolf <public@wolft.xyz>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/wolft/quantumhall_matrixelements
8
+ Project-URL: Issues, https://github.com/wolft/quantumhall_matrixelements/issues
9
+ Keywords: quantum Hall,matrix elements,form factors,exchange kernels,Landau levels
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Topic :: Scientific/Engineering :: Physics
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.26
21
+ Requires-Dist: scipy>=1.11
22
+ Requires-Dist: hankel>=1.2
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0; extra == "dev"
25
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
26
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
27
+ Requires-Dist: mypy>=1.10; extra == "dev"
28
+ Requires-Dist: build; extra == "dev"
29
+ Requires-Dist: twine; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # quantumhall-matrixelements: Quantum Hall Landau-Level Matrix Elements
33
+
34
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17646027.svg)](https://doi.org/10.5281/zenodo.17646027)
35
+
36
+ Landau-level plane-wave form factors and exchange kernels for quantum Hall systems.
37
+
38
+ This library factors out the continuum matrix-element kernels used in Hartree–Fock and
39
+ related calculations into a small, reusable package. It provides:
40
+
41
+ - Analytic Landau-level plane-wave form factors $F_{n',n}(\mathbf{q})$.
42
+ - Exchange kernels $X_{n_1 m_1 n_2 m_2}(\mathbf{G})$ computed via:
43
+ - Gauss-Legendre quadrature with rational mapping (`gausslegendre` backend, default).
44
+ - Generalized Gauss–Laguerre quadrature (`gausslag` backend).
45
+ - Hankel-transform based integration (`hankel` backend).
46
+ - Symmetry diagnostics for verifying kernel implementations on a given G-grid.
47
+
48
+ ## Backends and Reliability
49
+
50
+ The package provides three backends for computing exchange kernels, each with different performance and stability characteristics:
51
+
52
+ 1. **`gausslegendre` (Default)**:
53
+ - **Method**: Gauss-Legendre quadrature mapped from $[-1, 1]$ to $[0, \infty)$ via a rational mapping.
54
+ - **Pros**: Fast and numerically stable for all Landau level indices ($n$).
55
+ - **Cons**: May require tuning `nquad` for extremely large momenta or indices ($n > 100$).
56
+ - **Recommended for**: General usage, especially for large $n$ ($n \ge 10$).
57
+
58
+ 2. **`gausslag`**:
59
+ - **Method**: Generalized Gauss-Laguerre quadrature.
60
+ - **Pros**: Very fast for small $n$.
61
+ - **Cons**: Numerically unstable for large $n$ ($n \ge 12$) due to high-order Laguerre polynomial roots.
62
+ - **Recommended for**: Small systems ($n < 10$) where speed is critical.
63
+
64
+ 3. **`hankel`**:
65
+ - **Method**: Discrete Hankel transform.
66
+ - **Pros**: High precision and stability.
67
+ - **Cons**: Significantly slower than quadrature methods.
68
+ - **Recommended for**: Reference calculations and verifying other backends.
69
+
70
+ ## Mathematical Definitions
71
+
72
+ ### Plane-Wave Form Factors
73
+
74
+ The form factors are the matrix elements of the plane-wave operator $e^{i \mathbf{q} \cdot \mathbf{R}}$ in the Landau level basis $|n\rangle$:
75
+
76
+ $$ F_{n',n}(\mathbf{q}) = \langle n' | e^{i \mathbf{q} \cdot \mathbf{R}} | n \rangle $$
77
+
78
+ Analytically, these are given by:
79
+
80
+ $$
81
+ F_{n',n}(\mathbf{q}) =
82
+ i^{|n-n'|}
83
+ e^{i(n-n')\theta_{\mathbf{q}}}
84
+ \sqrt{\frac{n_{<}!}{n_{>}!}}
85
+ \left( \frac{|\mathbf{q}|\ell_{B}}{\sqrt{2}} \right)^{|n-n'|}
86
+ L_{n_<}^{|n-n'|}\left( \frac{|\mathbf{q}|^2 \ell_{B}^2}{2} \right)
87
+ e^{-|\mathbf{q}|^2 \ell_{B}^2/4}
88
+ $$
89
+
90
+ where $n_< = \min(n, n')$, $n_> = \max(n, n')$, and $L_n^\alpha$ are the generalized Laguerre polynomials, and $\ell_B$ is the magnetic length.
91
+
92
+ ### Exchange Kernels
93
+
94
+ The exchange kernels $X_{n_1 m_1 n_2 m_2}(\mathbf{G})$ are defined as the Fourier transform of the interaction potential weighted by the form factors:
95
+
96
+ $$ X_{n_1 m_1 n_2 m_2}(\mathbf{G}) = \int \frac{d^2 q}{(2\pi)^2} V(q) F_{n_1, m_1}(\mathbf{q}) F_{m_2, n_2}(-\mathbf{q}) e^{-i \mathbf{q} \cdot \mathbf{G} \ell_B^2} $$
97
+
98
+ where $V(q)$ is the interaction potential. For the Coulomb interaction, $V(q) = \frac{2\pi e^2}{\epsilon q}$.
99
+
100
+ ### Units and Interaction Strength
101
+
102
+ The package performs calculations in dimensionless units where lengths are scaled by $\ell_B$. The interaction strength is parameterized by a dimensionless prefactor $\kappa$.
103
+
104
+ - **Coulomb Interaction**: The code assumes a potential of the form $V(q) = \kappa \frac{2\pi \ell_B}{q \ell_B}$ (in effective dimensionless form).
105
+ - If you set `kappa = 1.0`, the resulting exchange kernels will be in units of the **Coulomb energy scale** $E_C = e^2 / (\epsilon \ell_B)$.
106
+ - If you want the results in units of the cyclotron energy $\hbar \omega_c$, you should set $\kappa = E_C / (\hbar \omega_c) = (e^2/\epsilon \ell_B) / (\hbar \omega_c)$.
107
+
108
+ - **General Potential**: For a general $V(q)$, the function `V_of_q` should return values in your desired energy units. The integration measure $d^2q/(2\pi)^2$ introduces a factor of $1/\ell_B^2$, so ensure your potential scaling is consistent.
109
+
110
+ ## Installation
111
+
112
+ From PyPI (once published):
113
+
114
+ ```bash
115
+ pip install quantumhall-matrixelements
116
+ ```
117
+
118
+ From a local checkout (development install):
119
+
120
+ ```bash
121
+ pip install -e .[dev]
122
+ ```
123
+
124
+ ## Basic usage
125
+
126
+ ```python
127
+ import numpy as np
128
+ from quantumhall_matrixelements import (
129
+ get_form_factors,
130
+ get_exchange_kernels,
131
+ )
132
+
133
+ # Simple G set: G0=(0,0), G+=(1,0), G-=(-1,0)
134
+ Gs_dimless = np.array([0.0, 1.0, 1.0])
135
+ thetas = np.array([0.0, 0.0, np.pi])
136
+ nmax = 2
137
+
138
+ F = get_form_factors(Gs_dimless, thetas, nmax) # shape (nG, nmax, nmax)
139
+ X = get_exchange_kernels(Gs_dimless, thetas, nmax) # default 'gausslegendre' backend
140
+
141
+ print("F shape:", F.shape)
142
+ print("X shape:", X.shape)
143
+ ```
144
+
145
+ For more detailed examples, see the example scripts under `examples/` and the tests under `tests/`.
146
+
147
+ ## Citation
148
+
149
+ If you use the package `quantumhall-matrixelements` in academic work, please cite:
150
+
151
+ > Tobias Wolf, *quantumhall-matrixelements: Quantum Hall Landau-Level Matrix Elements*, version 0.1.0, 2025.
152
+ > DOI: https://doi.org/10.5281/zenodo.17646027
153
+
154
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17646027.svg)](https://doi.org/10.5281/zenodo.17646027)
155
+
156
+ A machine-readable `CITATION.cff` file is included in the repository and can be used with tools that support it (for example, GitHub’s “Cite this repository” button).
157
+
158
+ ## Development
159
+
160
+ - Run tests and coverage:
161
+
162
+ ```bash
163
+ pytest
164
+ ```
165
+
166
+ - Lint and type-check:
167
+
168
+ ```bash
169
+ ruff check .
170
+ mypy .
171
+ ```
172
+
173
+ ## Authors and license
174
+
175
+ - Author: Tobias Wolf
176
+ - Copyright © 2025 Tobias Wolf
177
+ - License: MIT (see `LICENSE`).
@@ -0,0 +1,12 @@
1
+ quantumhall_matrixelements/__init__.py,sha256=4PGAH9H62gBRkHsuxQFpbbKe7l6KuFw2uS9staaE4UM,2827
2
+ quantumhall_matrixelements/_debug_symmetry.py,sha256=ieQc5fVwroC3TLcIkfeGNmEvJde6heyC6gOvRS3RQq4,1543
3
+ quantumhall_matrixelements/diagnostic.py,sha256=A6f8EzEWZiK5B54xKSS7qgdX2XXQi9FymVUVJPmb_wY,1637
4
+ quantumhall_matrixelements/exchange_gausslag.py,sha256=vqF8eBGXatEa1dqOYKOxzAQE2ZB8p4zJ5PTlP6H7wzY,5875
5
+ quantumhall_matrixelements/exchange_hankel.py,sha256=PCNKMcLa_71JzvfvHKp4fd0dFL5YJh_KraVGUIaaNDI,5784
6
+ quantumhall_matrixelements/exchange_legendre.py,sha256=qYcd27g90R7Fv8dxP-sFL2l0eAflUULCJozXp9Y6IlU,6787
7
+ quantumhall_matrixelements/planewave.py,sha256=v_xvM8P-jynXL8ML8TEb5k_qfnJ1ztDvZltSdYLvkT0,2466
8
+ quantumhall_matrixelements-0.1.0.dist-info/licenses/LICENSE,sha256=jFCGBoPJ8yDW1bqyaj2jwNsZQMmVE42yqE42hGBqJEQ,1069
9
+ quantumhall_matrixelements-0.1.0.dist-info/METADATA,sha256=WaL5Kb4tRU5bpDpZ12EJ_qj4_lEf91wsAMa7h6dXCco,6949
10
+ quantumhall_matrixelements-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ quantumhall_matrixelements-0.1.0.dist-info/top_level.txt,sha256=-Xq3KN3v4pwYuKSv92x2m3SC2GXYGYcKoE1Wp0IdAi0,27
12
+ quantumhall_matrixelements-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tobias Wolf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1 @@
1
+ quantumhall_matrixelements