Skip to content

Commit 2aee464

Browse files
committed
Guard compressed kernel allocations
1 parent 1dafb9f commit 2aee464

7 files changed

Lines changed: 182 additions & 14 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ Calling ``get_exchange_kernels_compressed(select=None)`` still builds the
9898
canonical symmetry-reduced list, so it avoids 5D materialization but not the
9999
underlying O(``nmax^4``) output scaling.
100100

101+
The public compressed API also guards the numeric ``(nG, n_select)`` values
102+
array via ``compressed_limit_bytes`` (default 512 MiB). If you omit ``select``,
103+
``canonical_select_max_entries`` separately guards the number of canonical
104+
representatives before any numeric backend work begins.
105+
101106
For the ``'laguerre'`` backend, dense Gauss-Legendre work tables are guarded by
102107
``workspace_limit_bytes`` (default 512 MiB). Pass ``workspace_limit_bytes=None``
103108
to disable that backend-level guard.

src/quantumhall_matrixelements/__init__.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
from numpy.typing import NDArray
2020

2121
from ._materialize import (
22+
DEFAULT_COMPRESSED_LIMIT_BYTES,
2223
DEFAULT_FULL_TENSOR_LIMIT_BYTES,
24+
guard_compressed_values_allocation,
2325
guard_full_tensor_materialization,
2426
materialize_full_tensor,
2527
)
26-
from ._select import DEFAULT_CANONICAL_SELECT_MAX_ENTRIES
28+
from ._select import DEFAULT_CANONICAL_SELECT_MAX_ENTRIES, estimate_canonical_select_size
2729
from .diagnostic import get_exchange_kernels_opposite_field, get_form_factors_opposite_field
2830
from .exchange_hankel import get_exchange_kernels_hankel
2931
from .exchange_laguerre import (
@@ -144,6 +146,7 @@ def get_exchange_kernels_compressed(
144146
method: str | None = None,
145147
select: Iterable[Quad] | None = None,
146148
canonical_select_max_entries: int | None = DEFAULT_CANONICAL_SELECT_MAX_ENTRIES,
149+
compressed_limit_bytes: float | int | None = DEFAULT_COMPRESSED_LIMIT_BYTES,
147150
**kwargs: Any,
148151
) -> tuple[ComplexArray, list[Quad]]:
149152
"""Return the compressed exchange-kernel representation ``(values, select_list)``.
@@ -153,8 +156,9 @@ def get_exchange_kernels_compressed(
153156
154157
If ``select`` is omitted, the backend still constructs the canonical
155158
symmetry-reduced list, so the returned representation remains O(``nmax^4``)
156-
in the number of stored entries. Pass an explicit ``select=...`` to compute
157-
only the entries you need.
159+
in the number of stored entries. ``compressed_limit_bytes`` caps the
160+
resulting ``(nG, n_select)`` complex output array. Pass an explicit
161+
``select=...`` to compute only the entries you need.
158162
"""
159163
chosen = (method or "laguerre").strip().lower()
160164
backend_fn: Any
@@ -175,13 +179,28 @@ def get_exchange_kernels_compressed(
175179
if G_magnitudes.shape != G_angles.shape:
176180
raise ValueError("G_magnitudes and G_angles must have the same shape.")
177181

182+
select_list: list[Quad] | None
183+
if select is None:
184+
select_list = None
185+
n_select_est = estimate_canonical_select_size(int(nmax))
186+
else:
187+
select_list = [cast(Quad, tuple(int(x) for x in quad)) for quad in select]
188+
n_select_est = len(select_list)
189+
190+
guard_compressed_values_allocation(
191+
nG=int(G_magnitudes.size),
192+
n_select=int(n_select_est),
193+
compressed_limit_bytes=compressed_limit_bytes,
194+
backend_name=f"{chosen} exchange kernels",
195+
)
196+
178197
return cast(
179198
tuple[ComplexArray, list[Quad]],
180199
backend_fn(
181200
G_magnitudes,
182201
G_angles,
183202
nmax,
184-
select=select,
203+
select=select_list,
185204
canonical_select_max_entries=canonical_select_max_entries,
186205
**kwargs,
187206
),

src/quantumhall_matrixelements/_materialize.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
# Default soft cap for allocating a full (nG, nmax, nmax, nmax, nmax) complex tensor.
99
DEFAULT_FULL_TENSOR_LIMIT_BYTES = 512 * 1024 * 1024 # 512 MiB
10+
# Default soft cap for allocating compressed (nG, n_select) complex arrays.
11+
DEFAULT_COMPRESSED_LIMIT_BYTES = DEFAULT_FULL_TENSOR_LIMIT_BYTES
1012
# Default soft cap for dense backend work tables such as quadrature precomputes.
1113
DEFAULT_WORKSPACE_LIMIT_BYTES = DEFAULT_FULL_TENSOR_LIMIT_BYTES
1214

@@ -90,6 +92,30 @@ def guard_full_tensor_materialization(
9092
)
9193

9294

95+
def guard_compressed_values_allocation(
96+
*,
97+
nG: int,
98+
n_select: int,
99+
compressed_limit_bytes: float | int | None = DEFAULT_COMPRESSED_LIMIT_BYTES,
100+
backend_name: str = "exchange kernels",
101+
dtype: Any = np.complex128,
102+
) -> None:
103+
"""Raise proactively if a compressed ``(nG, n_select)`` array would be too large."""
104+
if compressed_limit_bytes is None:
105+
return
106+
107+
est = float(nG) * float(n_select) * np.dtype(dtype).itemsize
108+
if est > float(compressed_limit_bytes):
109+
human_est = format_bytes(est)
110+
human_lim = format_bytes(float(compressed_limit_bytes))
111+
raise MemoryError(
112+
f"Refusing to allocate compressed ({nG}, {n_select}) values array "
113+
f"(~{human_est} > {human_lim}) for {backend_name}; pass a smaller "
114+
"explicit select=..., split the G grid, increase compressed_limit_bytes, "
115+
"or pass compressed_limit_bytes=None to disable this guard."
116+
)
117+
118+
93119
# --------------------------------------------------------------------------- #
94120
# Helpers for controlled materialization / reconstruction #
95121
# --------------------------------------------------------------------------- #
@@ -142,6 +168,8 @@ def materialize_full_tensor(
142168

143169
__all__ = [
144170
"guard_full_tensor_materialization",
171+
"guard_compressed_values_allocation",
172+
"DEFAULT_COMPRESSED_LIMIT_BYTES",
145173
"DEFAULT_FULL_TENSOR_LIMIT_BYTES",
146174
"DEFAULT_WORKSPACE_LIMIT_BYTES",
147175
"build_canonical_select",

src/quantumhall_matrixelements/_select.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
DEFAULT_CANONICAL_SELECT_MAX_ENTRIES = 2_000_000
2222

2323

24+
def estimate_canonical_select_size(nmax: int) -> int:
25+
"""Return the size of the canonical symmetry-reduced select list."""
26+
n_pairs = int(nmax) * int(nmax)
27+
return (n_pairs * (n_pairs + 1)) // 2
28+
29+
2430
def _coerce_quad(nmax: int, quad: Sequence[int]) -> Quad:
2531
if len(quad) != 4:
2632
raise ValueError("select entries must be (n1, m1, n2, m2) tuples.")
@@ -45,8 +51,7 @@ def normalize_select(
4551
if canonical_select_max_entries is not None:
4652
# number of (n,m) pairs is nmax^2; canonical selection is the upper
4753
# triangle in that pair space.
48-
n_pairs = int(nmax) * int(nmax)
49-
n_select = (n_pairs * (n_pairs + 1)) // 2
54+
n_select = estimate_canonical_select_size(nmax)
5055
if n_select > int(canonical_select_max_entries):
5156
raise MemoryError(
5257
f"Refusing to build canonical select list for nmax={nmax}: "
@@ -71,5 +76,6 @@ def normalize_select(
7176

7277
__all__ = [
7378
"DEFAULT_CANONICAL_SELECT_MAX_ENTRIES",
79+
"estimate_canonical_select_size",
7480
"normalize_select",
7581
]

src/quantumhall_matrixelements/fock.py

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import numpy as np
88
from numpy.typing import NDArray
99

10+
from ._materialize import DEFAULT_COMPRESSED_LIMIT_BYTES, guard_compressed_values_allocation
11+
from ._select import estimate_canonical_select_size
1012
from .exchange_hankel import get_exchange_kernels_hankel
1113
from .exchange_laguerre import (
1214
QuadratureParams,
@@ -21,6 +23,7 @@
2123
_FAST_LAGUERRE_PASSTHROUGH_KEYS = {
2224
"adaptive_nquad",
2325
"canonical_select_max_entries",
26+
"compressed_limit_bytes",
2427
"kappa",
2528
"nquad",
2629
"potential",
@@ -93,6 +96,37 @@ def apply(rho: ComplexArray, include_minus: bool = True) -> ComplexArray:
9396
return apply
9497

9598

99+
def _guard_compressed_kernel_values(
100+
G_magnitudes: RealArray,
101+
G_angles: RealArray,
102+
nmax: int,
103+
*,
104+
select: Iterable[Quad] | None,
105+
compressed_limit_bytes: float | int | None,
106+
backend_name: str,
107+
) -> tuple[RealArray, RealArray, list[Quad] | None]:
108+
G_magnitudes_arr = cast(RealArray, np.asarray(G_magnitudes, dtype=np.float64).ravel())
109+
G_angles_arr = cast(RealArray, np.asarray(G_angles, dtype=np.float64).ravel())
110+
if G_magnitudes_arr.shape != G_angles_arr.shape:
111+
raise ValueError("G_magnitudes and G_angles must have the same shape.")
112+
113+
select_list: list[Quad] | None
114+
if select is None:
115+
select_list = None
116+
n_select = estimate_canonical_select_size(int(nmax))
117+
else:
118+
select_list = [cast(Quad, tuple(int(x) for x in quad)) for quad in select]
119+
n_select = len(select_list)
120+
121+
guard_compressed_values_allocation(
122+
nG=int(G_magnitudes_arr.size),
123+
n_select=int(n_select),
124+
compressed_limit_bytes=compressed_limit_bytes,
125+
backend_name=backend_name,
126+
)
127+
return G_magnitudes_arr, G_angles_arr, select_list
128+
129+
96130
def _build_compressed_constructor_hf(
97131
values: ComplexArray,
98132
select: list[Quad],
@@ -295,6 +329,10 @@ def get_fockmatrix_constructor(
295329
"""
296330

297331
chosen = (method or "laguerre").strip().lower()
332+
backend_kwargs = dict(kwargs)
333+
compressed_limit_bytes = backend_kwargs.pop(
334+
"compressed_limit_bytes", DEFAULT_COMPRESSED_LIMIT_BYTES
335+
)
298336
backend: Any
299337
if chosen in {"hankel", "hk"}:
300338
backend = get_exchange_kernels_hankel
@@ -317,14 +355,23 @@ def get_fockmatrix_constructor(
317355
"Use 'laguerre', 'ogata', or 'hankel'."
318356
)
319357

358+
G_magnitudes_arr, G_angles_arr, select_list = _guard_compressed_kernel_values(
359+
G_magnitudes,
360+
G_angles,
361+
nmax,
362+
select=select,
363+
compressed_limit_bytes=compressed_limit_bytes,
364+
backend_name=f"{chosen} exchange kernels",
365+
)
366+
320367
values, select_list = cast(
321368
tuple[ComplexArray, list[Quad]],
322369
backend(
323-
G_magnitudes,
324-
G_angles,
370+
G_magnitudes_arr,
371+
G_angles_arr,
325372
nmax,
326-
select=select,
327-
**kwargs,
373+
select=select_list,
374+
**backend_kwargs,
328375
),
329376
)
330377

@@ -349,6 +396,10 @@ def get_fockmatrix_constructor_hf(
349396
Σ^F_{n m}(G) = - Σ_{r,t} X_{m r n t}(G) ρ^*_{t r}(G).
350397
"""
351398
chosen = (method or "laguerre").strip().lower()
399+
backend_kwargs = dict(kwargs)
400+
compressed_limit_bytes = backend_kwargs.pop(
401+
"compressed_limit_bytes", DEFAULT_COMPRESSED_LIMIT_BYTES
402+
)
352403
backend: Any
353404
if chosen in {"hankel", "hk"}:
354405
backend = get_exchange_kernels_hankel
@@ -362,14 +413,23 @@ def get_fockmatrix_constructor_hf(
362413
"Use 'laguerre', 'ogata', or 'hankel'."
363414
)
364415

416+
G_magnitudes_arr, G_angles_arr, select_list = _guard_compressed_kernel_values(
417+
G_magnitudes,
418+
G_angles,
419+
nmax,
420+
select=select,
421+
compressed_limit_bytes=compressed_limit_bytes,
422+
backend_name=f"{chosen} exchange kernels",
423+
)
424+
365425
values, select_list = cast(
366426
tuple[ComplexArray, list[Quad]],
367427
backend(
368-
G_magnitudes,
369-
G_angles,
428+
G_magnitudes_arr,
429+
G_angles_arr,
370430
nmax,
371-
select=select,
372-
**kwargs,
431+
select=select_list,
432+
**backend_kwargs,
373433
),
374434
)
375435
return _build_compressed_constructor_hf(values, select_list, nmax)

tests/test_api_guards.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,38 @@ def test_canonical_select_guard_is_bypassed_for_explicit_select():
6666
assert select_list == [(0, 0, 0, 0)]
6767

6868

69+
def test_compressed_values_guard_raises_before_backend_work():
70+
Gs = np.array([0.0, 1.0], dtype=float)
71+
thetas = np.array([0.0, 0.1], dtype=float)
72+
73+
with pytest.raises(MemoryError):
74+
get_exchange_kernels_compressed(
75+
Gs,
76+
thetas,
77+
2,
78+
method="ogata",
79+
select=[(0, 0, 0, 0)],
80+
compressed_limit_bytes=1,
81+
)
82+
83+
84+
def test_compressed_values_limit_none_bypasses_guard():
85+
Gs = np.array([0.0, 1.0], dtype=float)
86+
thetas = np.array([0.0, 0.1], dtype=float)
87+
select = [(0, 0, 0, 0)]
88+
89+
values, select_list = get_exchange_kernels_compressed(
90+
Gs,
91+
thetas,
92+
2,
93+
method="ogata",
94+
select=select,
95+
compressed_limit_bytes=None,
96+
)
97+
assert values.shape == (2, 1)
98+
assert select_list == select
99+
100+
69101
def test_laguerre_workspace_guard_raises_for_compressed_call():
70102
Gs = np.array([12.0], dtype=float)
71103
thetas = np.array([0.0], dtype=float)

tests/test_fock_constructor.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,21 @@ def test_fock_constructor_laguerre_workspace_guard_uses_fast_path():
9999
nquad=80,
100100
workspace_limit_bytes=1,
101101
)
102+
103+
104+
def test_fock_constructor_laguerre_accepts_compressed_limit_on_fast_path():
105+
Gs = np.array([0.0, 1.2])
106+
thetas = np.array([0.0, 0.3])
107+
rho = _random_hermitian_rho(len(Gs), 3, seed=7)
108+
109+
ref = get_fockmatrix_constructor(Gs, thetas, 3, method="laguerre", nquad=200)(rho)
110+
out = get_fockmatrix_constructor(
111+
Gs,
112+
thetas,
113+
3,
114+
method="laguerre",
115+
nquad=200,
116+
compressed_limit_bytes=1,
117+
)(rho)
118+
119+
assert np.allclose(out, ref, rtol=1e-10, atol=1e-12)

0 commit comments

Comments
 (0)