dense-Evolution / tests /unit /test_chunk.py
Tatopenn's picture
Sync mirror with GitHub main (v8.1.62): subpackage refactor, mitigation promotions, MCP server refactor
2a87ede verified
Raw History Blame Contribute Delete
27.4 kB
"""
Unit tests for dense_evolution/chunk.py -- the Chunk anti-OOM engine
(single-process multi-chunk dispatch, the jax.lax.scan-fused multi-chunk
kernel, utility surfaces, and real multi-device distributed dispatch).
Split out of the original monolithic test_dense_evolution.py -- see
test_simulator.py's module docstring for why.
"""
import numpy as np
import pytest
import jax
from dense_evolution import Chunk, DenseSVSimulator
# ─────────────────────────────────────────────────────────────
# CHUNK PUBLIC API
# ─────────────────────────────────────────────────────────────
class TestChunkPublicAPI:
"""Regression guard for audit finding #3: the README's own Anti-OOM
quick-start (`from dense_evolution import Chunk`) raised ImportError —
Chunk was never re-exported from the package root, only reachable via
`dense_evolution.chunk`. Chunk also lacked get_probabilities()/
get_statevector(), unlike DenseSVSimulator, so even the right import
path needed an undocumented `np.abs(sim.sv)**2` workaround."""
def test_chunk_importable_from_package_root(self):
# this is the regression itself: it would have raised ImportError
from dense_evolution import Chunk as ChunkFromRoot
assert ChunkFromRoot is Chunk
def test_chunk_get_probabilities_matches_manual_computation(self):
sim = Chunk(6)
sim.run_chunk([['h', i] for i in range(6)], 500)
probs = np.asarray(sim.get_probabilities())
manual = np.abs(np.asarray(sim.sv)) ** 2
np.testing.assert_allclose(probs, manual, atol=1e-12)
assert abs(probs.sum() - 1.0) < 1e-9
assert np.allclose(probs, 1.0 / 64, atol=1e-9) # uniform after H on all 6 qubits
def test_chunk_sv_setter_num_chunks_1_round_trips_through_inner_sim(self):
# sv's setter, num_chunks==1 branch (self._chunk_sims is None):
# writes straight through to the physical inner simulator instead of
# splitting across chunks -- never exercised by any existing test,
# which only ever reads sim.sv, never assigns to it.
sim = Chunk(6)
assert sim._chunk_sims is None # confirms this test is on the branch it targets
new_sv = np.zeros(2 ** 6, dtype=sim.sv.dtype)
new_sv[5] = 1.0 # an arbitrary basis state, distinct from the |000000> default
sim.sv = new_sv
np.testing.assert_allclose(np.asarray(sim.sv), new_sv, atol=1e-12)
np.testing.assert_allclose(np.asarray(sim._inner_sim.sv), new_sv, atol=1e-12)
class TestChunkMultiPiece:
"""Chunk(n_qubits) beyond the RAM-safe budget used to silently simulate
FEWER qubits than requested (inner simulator sized to
min(n_qubits, chunk_size_bits)) instead of real multi-chunk splitting —
num_chunks/chunk_dim were computed but never acted on. Found testing
Chunk(n_qubits=28) directly: get_probabilities() returned 2**27 elements,
not 2**28.
These tests force a small chunk_size_bits via monkeypatching
get_dynamic_chunk (so num_chunks>1 is cheap to test) and cross-check the
multi-chunk dispatch against a plain DenseSVSimulator(n_qubits) running
the identical circuit — the only real correctness bar here, since this
is bit-manipulation-heavy code where a plausible-looking-but-wrong
formula is easy to miss by inspection alone."""
@pytest.fixture
def force_chunk_bits(self, monkeypatch):
"""Returns a function to force MemoryChunker's safe budget to a
fixed small value, so num_chunks>1 can be tested without needing
real multi-GB allocations."""
import dense_evolution.chunk as chunk_mod
def _force(bits):
monkeypatch.setattr(chunk_mod, "get_dynamic_chunk", lambda dtype_target: bits)
return _force
def _compare_to_reference(self, n_qubits, circuit):
c = Chunk(n_qubits)
c.run_chunk(circuit)
sv_chunk = np.asarray(c.get_statevector())
ref = DenseSVSimulator(n_qubits)
ref.run_circuit(circuit, transpile=True)
sv_ref = np.asarray(ref.get_statevector())
return sv_chunk, sv_ref
def test_empty_circuit_canary(self, force_chunk_bits):
# cheapest way to catch "every chunk seeded its own |0...0>" in
# isolation, before it's buried in a larger circuit's diff
force_chunk_bits(4)
c = Chunk(6) # chunk_size_bits=4 -> num_chunks=4
assert c.num_chunks == 4
probs = np.asarray(c.get_probabilities())
assert probs.shape == (64,)
assert abs(probs.sum() - 1.0) < 1e-9
assert abs(probs[0] - 1.0) < 1e-9
ref = DenseSVSimulator(6)
np.testing.assert_allclose(np.asarray(c.get_statevector()), ref.get_statevector(), atol=1e-12)
@pytest.mark.parametrize("qubit", [3]) # local qubit (m=2 for n=6,bits=4)
def test_1q_local(self, force_chunk_bits, qubit):
force_chunk_bits(4)
sv_chunk, sv_ref = self._compare_to_reference(6, [('h', qubit)])
np.testing.assert_allclose(sv_chunk, sv_ref, atol=1e-9)
@pytest.mark.parametrize("qubit", [0, 1]) # chunk-select: MSB (0) and non-MSB (1)
def test_1q_chunk_select(self, force_chunk_bits, qubit):
force_chunk_bits(4)
sv_chunk, sv_ref = self._compare_to_reference(6, [('h', qubit)])
np.testing.assert_allclose(sv_chunk, sv_ref, atol=1e-9)
def test_2q_local_local(self, force_chunk_bits):
force_chunk_bits(4)
sv_chunk, sv_ref = self._compare_to_reference(6, [('h', 3), ('cx', 3, 4)])
np.testing.assert_allclose(sv_chunk, sv_ref, atol=1e-9)
@pytest.mark.parametrize("gate", ["cx", "cz", "cy"])
def test_2q_control_chunk_select_target_local(self, force_chunk_bits, gate):
force_chunk_bits(4)
sv_chunk, sv_ref = self._compare_to_reference(6, [('h', 0), (gate, 0, 3)])
np.testing.assert_allclose(sv_chunk, sv_ref, atol=1e-9)
@pytest.mark.parametrize("gate", ["cx", "cz", "cy"])
def test_2q_control_local_target_chunk_select(self, force_chunk_bits, gate):
force_chunk_bits(4)
sv_chunk, sv_ref = self._compare_to_reference(6, [('h', 3), (gate, 3, 0)])
np.testing.assert_allclose(sv_chunk, sv_ref, atol=1e-9)
@pytest.mark.parametrize("gate", ["cx", "cz", "cy"])
def test_2q_control_chunk_select_target_chunk_select(self, force_chunk_bits, gate):
force_chunk_bits(4)
sv_chunk, sv_ref = self._compare_to_reference(6, [('h', 0), ('h', 1), (gate, 0, 1)])
np.testing.assert_allclose(sv_chunk, sv_ref, atol=1e-9)
def test_parametric_2q_gates_all_four_locations(self, force_chunk_bits):
# cp/crz aren't in GATE_IDS (run_circuit_jit's table) —
# canary for silently dropping them via the wrong dispatch table
force_chunk_bits(4)
cases = [
[('h', 0), ('cp', 0, 3, 0.7)], # ctrl chunk-select, tgt local
[('h', 3), ('crz', 3, 0, 1.1)], # ctrl local, tgt chunk-select
[('h', 0), ('h', 1), ('cp', 0, 1, 0.9)], # both chunk-select
[('h', 3), ('h', 4), ('crz', 3, 4, 0.4)], # both local
]
for circuit in cases:
sv_chunk, sv_ref = self._compare_to_reference(6, circuit)
np.testing.assert_allclose(sv_chunk, sv_ref, atol=1e-9)
def test_random_mixed_circuits_num_chunks_8(self, force_chunk_bits):
# num_chunks=8 (m=3) specifically exercises the middle chunk-select
# bit (index 1 of 3), not just the most-significant selector bit —
# catches formulas that hardcode the full-register bit position
# instead of the chunk-index-local one.
force_chunk_bits(4)
n = 7
rng = np.random.default_rng(1234)
gates_1q = ['h', 'x', 'y', 'z', 's', 'sdg', 't', 'tdg']
gates_1q_param = ['rx', 'ry', 'rz', 'p']
gates_2q = ['cx', 'cz', 'cy']
gates_2q_param = ['cp', 'crz']
for _trial in range(8):
circuit = []
for _ in range(20):
kind = rng.integers(0, 4)
if kind == 0:
circuit.append((rng.choice(gates_1q), int(rng.integers(0, n))))
elif kind == 1:
circuit.append((rng.choice(gates_1q_param), int(rng.integers(0, n)),
float(rng.uniform(-3.14, 3.14))))
elif kind == 2:
q1, q2 = rng.choice(n, size=2, replace=False)
circuit.append((rng.choice(gates_2q), int(q1), int(q2)))
else:
q1, q2 = rng.choice(n, size=2, replace=False)
circuit.append((rng.choice(gates_2q_param), int(q1), int(q2),
float(rng.uniform(-3.14, 3.14))))
sv_chunk, sv_ref = self._compare_to_reference(n, circuit)
np.testing.assert_allclose(sv_chunk, sv_ref, atol=1e-8,
err_msg=f"circuit={circuit}")
def test_num_chunks_1_path_untouched(self, force_chunk_bits):
# sanity: with a budget that covers n_qubits, behaviour must be
# identical to before this feature existed (single inner sim)
force_chunk_bits(16)
c = Chunk(6)
assert c.num_chunks == 1
c.run_chunk([('h', i) for i in range(6)])
probs = np.asarray(c.get_probabilities())
assert np.allclose(probs, 1.0 / 64, atol=1e-9)
def test_chunk_get_statevector_matches_sv(self):
sim = Chunk(4)
sim.run_chunk([['h', 0]], 500)
np.testing.assert_array_equal(np.asarray(sim.get_statevector()), np.asarray(sim.sv))
def test_2q_gate_spanning_first_to_last_qubit(self, force_chunk_bits):
# Explicit long-range case: control on qubit 0 (chunk-select, most
# significant) and target on the LAST local qubit (opposite end of
# the register), not just an adjacent chunk-select/local pair --
# the bit-shift math for chunk index vs local index is most likely
# to have an off-by-one/wrong-stride bug at the extremes.
force_chunk_bits(4) # n=6, chunk_size_bits=4 -> m=2, local qubits [2,3,4,5]
sv_chunk, sv_ref = self._compare_to_reference(
6, [('h', 0), ('h', 5), ('cx', 0, 5)])
np.testing.assert_allclose(sv_chunk, sv_ref, atol=1e-9)
# ── MemoryPressureError actually firing, not just "doesn't crash" ────
def test_memory_pressure_error_fires_on_insufficient_ram_num_chunks_1(self, monkeypatch):
import dense_evolution.chunk as chunk_mod
class _FakeVM:
total = 8 * 1024 ** 3 # 8 GB
available = 0.05 * 8 * 1024 ** 3 # 5% free -- below any sane threshold
percent = 95.0
monkeypatch.setattr(chunk_mod.psutil, "virtual_memory", lambda: _FakeVM())
with pytest.raises(chunk_mod.MemoryPressureError, match="MEMORIA CRITICA"):
Chunk(10, memory_threshold=0.15)
def test_memory_pressure_error_fires_on_insufficient_ram_num_chunks_gt_1(self, monkeypatch, force_chunk_bits):
import dense_evolution.chunk as chunk_mod
force_chunk_bits(4) # forces num_chunks>1 at small n_qubits
class _FakeVM:
total = 8 * 1024 ** 3
available = 0.05 * 8 * 1024 ** 3
percent = 95.0
monkeypatch.setattr(chunk_mod.psutil, "virtual_memory", lambda: _FakeVM())
with pytest.raises(chunk_mod.MemoryPressureError, match="MEMORIA INSUFFICIENTE"):
Chunk(6, memory_threshold=0.15)
def test_memory_pressure_error_not_raised_with_ample_ram(self, monkeypatch, force_chunk_bits):
# Negative control: the same fake-psutil machinery, but with
# generous available memory, must NOT raise -- confirms the two
# tests above are catching a real threshold check, not e.g. an
# unconditional raise or a monkeypatch that broke construction
# entirely.
import dense_evolution.chunk as chunk_mod
force_chunk_bits(4)
class _FakeVM:
total = 8 * 1024 ** 3
available = 0.90 * 8 * 1024 ** 3
percent = 10.0
monkeypatch.setattr(chunk_mod.psutil, "virtual_memory", lambda: _FakeVM())
c = Chunk(6, memory_threshold=0.15)
assert c.num_chunks == 4
class TestChunkMultiPieceJIT:
"""dense_evolution.chunk's multi-chunk dispatch (num_chunks>1) used to
apply gates one at a time via a Python loop calling
sim.apply_gate_1q/apply_gate_2q (neither @jax.jit'd, no jax.lax.scan) —
6x slower than run_circuit_jit on an identical workload
(10 qubits/200 gates/4 forced chunks: 2.2s vs 0.37s). Replaced with a
single jax.lax.scan over the whole circuit, operating on the stacked
(num_chunks, chunk_dim) representation directly (never materializing a
(2**n_qubits,) array — the anti-OOM property Chunk exists for). The 6
gate/qubit-location cases were ported formula-for-formula from the old
Python-loop implementation (dense_evolution/chunk.py git history) and
verified against it case-by-case before it was removed — TestChunkMultiPiece
above (unchanged, all 18 tests still pass against the new kernel) is
that cross-check. This class covers what's new: dtype and the actual
measured speedup."""
@pytest.fixture
def force_chunk_bits(self, monkeypatch):
import dense_evolution.chunk as chunk_mod
def _force(bits):
monkeypatch.setattr(chunk_mod, "get_dynamic_chunk", lambda dtype_target: bits)
return _force
@pytest.mark.parametrize("use_float32", [False, True])
def test_dtype_consistency_across_all_cases(self, force_chunk_bits, use_float32):
# Every constant in the new kernel must derive from the stacked
# array's own dtype, never a hardcoded complex128 — the exact
# mistake that once broke beast-mode's use_float32 path (a
# lax.cond/where branch mismatch that surfaces at trace time, not
# silently), so this exercises all 6 cases under complex64 too.
force_chunk_bits(4)
n = 6
circuit = [
('h', 3), ('rx', 4, 0.6), # 1q local
('h', 0), ('ry', 1, 0.4), # 1q chunk-select
('cx', 3, 4), # 2q local-local
('cp', 0, 4, 0.7), # ctrl chunk, tgt local
('crz', 4, 1, 1.1), # ctrl local, tgt chunk
('cy', 0, 1), # both chunk-select
]
chunk_sim = Chunk(n, use_float32=use_float32)
chunk_sim.run_chunk(circuit)
ref = DenseSVSimulator(n, use_float32=use_float32)
ref.run_circuit(circuit, transpile=True)
atol = 1e-5 if use_float32 else 1e-9
np.testing.assert_allclose(
np.asarray(chunk_sim.get_statevector()), np.asarray(ref.get_statevector()), atol=atol)
def test_measured_speedup_over_python_loop_dispatch(self, force_chunk_bits, capsys):
# Not a strict pass/fail bound (timing is noisy) — reports the real
# measured number, same discipline as the donate_argnums RSS
# measurement: honest data, not a claim asserted on faith. The
# comparison point (2.2s on this exact benchmark, pre-fix) is
# recorded in the changelog, not re-measured here (the old
# implementation no longer exists to compare against directly).
import time
force_chunk_bits(8)
n_qubits = 10
circuit = ([('h', i % n_qubits) for i in range(100)]
+ [('cx', i % (n_qubits - 1), (i % (n_qubits - 1)) + 1) for i in range(100)])
chunk_sim = Chunk(n_qubits=n_qubits, memory_threshold=0.01)
assert chunk_sim.num_chunks == 4
t0 = time.perf_counter()
chunk_sim.run_chunk(circuit)
elapsed = time.perf_counter() - t0
with capsys.disabled():
print(f"\n[multi-chunk JIT speed] n_qubits={n_qubits} num_chunks=4 "
f"200 gates: {elapsed:.4f}s (pre-fix Python-loop dispatch: ~2.2s)")
class TestChunkUtilities:
"""Coverage-driven tests for chunk.py's smaller utility surfaces --
get_dynamic_chunk's non-default dtype branches, SafeMemoryGuard/
MemoryChunker's __repr__/geometry/validation, _compile_multi_chunk_ops's
unknown-gate-skip and empty-circuit paths, CircuitChunker's no-simulator
guard, and Chunk's property forwarders in num_chunks>1 mode (never
exercised by TestChunkMultiPiece's own tests, which only ever compare
statevectors/probabilities against DenseSVSimulator, not these
introspection properties directly)."""
def test_get_dynamic_chunk_numpy_complex128(self):
from dense_evolution.chunk import get_dynamic_chunk
bits = get_dynamic_chunk(np.complex128)
assert 16 <= bits <= 27
def test_get_dynamic_chunk_other_dtype(self):
from dense_evolution.chunk import get_dynamic_chunk
bits = get_dynamic_chunk(np.float32)
assert 16 <= bits <= 27
def test_safe_memory_guard_rejects_invalid_threshold(self):
from dense_evolution.chunk import SafeMemoryGuard
with pytest.raises(ValueError):
SafeMemoryGuard(threshold_pct=0.0)
with pytest.raises(ValueError):
SafeMemoryGuard(threshold_pct=1.0)
with pytest.raises(ValueError):
SafeMemoryGuard(threshold_pct=-0.1)
def test_safe_memory_guard_repr(self):
from dense_evolution.chunk import SafeMemoryGuard
guard = SafeMemoryGuard(threshold_pct=0.15)
r = repr(guard)
assert "SafeMemoryGuard" in r and "threshold=15%" in r
def test_memory_chunker_geometry_and_repr(self):
from dense_evolution.chunk import MemoryChunker
mc = MemoryChunker(n_qubits=4)
num_chunks, chunk_dim, chunk_size_bits = mc.geometry()
assert num_chunks == mc.num_chunks
assert chunk_dim == mc.chunk_dim
assert chunk_size_bits == mc.chunk_size_bits
r = repr(mc)
assert "MemoryChunker" in r and "num_chunks=" in r
def test_compile_multi_chunk_ops_skips_unknown_gate(self):
from dense_evolution.chunk import _compile_multi_chunk_ops
# 'not_a_real_gate' isn't in GATE_IDS -- must be silently skipped
# (same documented behavior as beast-mode's own GATE_IDS lookup),
# 'h' on qubit 0 must still be compiled.
rows = _compile_multi_chunk_ops([('not_a_real_gate', 0), ('h', 0)])
rows_np = np.asarray(rows)
assert rows_np.shape[0] == 1
def test_compile_multi_chunk_ops_empty_circuit(self):
from dense_evolution.chunk import _compile_multi_chunk_ops
rows = _compile_multi_chunk_ops([])
rows_np = np.asarray(rows)
assert rows_np.shape == (0, 4)
def test_circuit_chunker_requires_simulator_instance(self):
from dense_evolution.chunk import CircuitChunker
chunker = CircuitChunker() # no simulator_instance
with pytest.raises(RuntimeError, match="no simulator instance"):
chunker.split_circuit([('h', 0)])
def test_chunk_repr(self):
c = Chunk(n_qubits=3)
r = repr(c)
assert "Chunk(" in r and "num_chunks=" in r
def test_chunk_multi_piece_property_forwarding(self, monkeypatch):
# Same force_chunk_bits pattern as TestChunkMultiPiece, but this
# time actually touching the properties themselves (chunk_size_bits,
# chunk_dim, dtype, memory_geometry, the sv setter, memory_mb) in
# num_chunks>1 mode, which no existing test does directly.
import dense_evolution.chunk as chunk_mod
monkeypatch.setattr(chunk_mod, "get_dynamic_chunk", lambda dtype_target: 4)
c = Chunk(n_qubits=6) # chunk_size_bits=4 -> num_chunks=4
assert c.num_chunks == 4
assert c.chunk_size_bits == 4
assert c.chunk_dim == 2 ** 4
assert c.dtype is not None
assert c.memory_geometry.num_chunks == 4
assert c.memory_mb() > 0
# sv getter/setter round-trip through the multi-chunk split path
original_sv = np.asarray(c.sv).copy()
c.sv = original_sv # exercises the setter's num_chunks>1 branch
np.testing.assert_allclose(np.asarray(c.sv), original_sv, atol=1e-12)
class TestChunkDistributed:
"""Chunk.run_chunk_distributed (issue #1): dispatches the multi-chunk
kernel across a real JAX device mesh (jax.shard_map + jax.lax.ppermute
for the cross-chunk edge exchange) instead of one process's RAM, one
physical chunk per device (v1 scope). JAX's device count is fixed at
process start, so these tests need >= 8 devices to exercise the
interesting (num_chunks>1) cases and are skipped otherwise -- run
with XLA_FLAGS=--xla_force_host_platform_device_count=8 (or more) to
actually execute them; see CI, which sets this for a dedicated step.
Correctness bar is the same one TestChunkMultiPiece already
established: cross-check against a plain DenseSVSimulator running the
identical circuit -- not against the single-process multi-chunk path,
to avoid two implementations of the same bug looking like agreement."""
MIN_DEVICES = 8
@pytest.fixture(autouse=True)
def _require_devices(self):
if jax.device_count() < self.MIN_DEVICES:
pytest.skip(
f"needs >= {self.MIN_DEVICES} JAX devices, only "
f"{jax.device_count()} available -- run with XLA_FLAGS="
f"--xla_force_host_platform_device_count={self.MIN_DEVICES}"
)
@pytest.fixture
def force_chunk_bits(self, monkeypatch):
import dense_evolution.chunk as chunk_mod
def _force(bits):
monkeypatch.setattr(chunk_mod, "get_dynamic_chunk", lambda dtype_target: bits)
return _force
def _compare_to_reference(self, n_qubits, circuit):
c = Chunk(n_qubits)
c.run_chunk_distributed(circuit)
sv_dist = np.asarray(c.get_statevector())
ref = DenseSVSimulator(n_qubits)
ref.run_circuit(circuit, transpile=True)
sv_ref = np.asarray(ref.get_statevector())
return sv_dist, sv_ref
def test_1q_local(self, force_chunk_bits):
force_chunk_bits(3) # n_qubits=6 -> num_chunks=8, m=3
sv_dist, sv_ref = self._compare_to_reference(6, [('h', 3), ('rx', 4, 0.4), ('rz', 5, 0.9)])
np.testing.assert_allclose(sv_dist, sv_ref, atol=1e-9)
def test_1q_chunk_select(self, force_chunk_bits):
force_chunk_bits(3)
sv_dist, sv_ref = self._compare_to_reference(6, [('h', 0), ('h', 1), ('h', 2)])
np.testing.assert_allclose(sv_dist, sv_ref, atol=1e-9)
def test_2q_local_local(self, force_chunk_bits):
force_chunk_bits(3)
sv_dist, sv_ref = self._compare_to_reference(6, [('h', 3), ('cx', 3, 4), ('cx', 4, 5)])
np.testing.assert_allclose(sv_dist, sv_ref, atol=1e-9)
@pytest.mark.parametrize("gate", ["cx", "cz", "cy"])
def test_2q_control_chunk_select_target_local(self, force_chunk_bits, gate):
force_chunk_bits(3)
circuit = [('h', 0), ('h', 1), ('h', 2), ('h', 5), (gate, 0, 5)]
sv_dist, sv_ref = self._compare_to_reference(6, circuit)
np.testing.assert_allclose(sv_dist, sv_ref, atol=1e-9)
@pytest.mark.parametrize("gate", ["cx", "cz", "cy"])
def test_2q_control_local_target_chunk_select(self, force_chunk_bits, gate):
force_chunk_bits(3)
circuit = [('h', 5), ('h', 0), (gate, 5, 0)]
sv_dist, sv_ref = self._compare_to_reference(6, circuit)
np.testing.assert_allclose(sv_dist, sv_ref, atol=1e-9)
@pytest.mark.parametrize("gate", ["cx", "cz", "cy"])
def test_2q_control_chunk_select_target_chunk_select(self, force_chunk_bits, gate):
force_chunk_bits(3)
circuit = [('h', 0), ('h', 1), (gate, 0, 1)]
sv_dist, sv_ref = self._compare_to_reference(6, circuit)
np.testing.assert_allclose(sv_dist, sv_ref, atol=1e-9)
def test_parametric_2q_gates(self, force_chunk_bits):
force_chunk_bits(3)
circuit = [('h', q) for q in range(6)] + [('crz', 0, 1, 0.5), ('cp', 3, 4, 0.7)]
sv_dist, sv_ref = self._compare_to_reference(6, circuit)
np.testing.assert_allclose(sv_dist, sv_ref, atol=1e-9)
def test_random_mixed_circuit(self, force_chunk_bits):
force_chunk_bits(3)
rng = np.random.default_rng(11)
pool_1q, pool_2q = ['h', 'x', 'y', 'z', 's'], ['cx', 'cz', 'cy']
circuit = []
for _ in range(30):
if rng.random() < 0.5:
circuit.append((rng.choice(pool_1q), int(rng.integers(0, 6))))
else:
q1, q2 = rng.choice(6, size=2, replace=False)
circuit.append((rng.choice(pool_2q), int(q1), int(q2)))
sv_dist, sv_ref = self._compare_to_reference(6, circuit)
np.testing.assert_allclose(sv_dist, sv_ref, atol=1e-9)
def test_dtype_float32(self, force_chunk_bits):
force_chunk_bits(2) # n_qubits=4 -> num_chunks=4, m=2
circuit = [('h', 0), ('h', 1), ('cx', 0, 2), ('rz', 3, 0.6)]
c = Chunk(4, use_float32=True)
c.run_chunk_distributed(circuit)
sv_dist = np.asarray(c.get_statevector())
ref = DenseSVSimulator(4, use_float32=True)
ref.run_circuit(circuit, transpile=True)
sv_ref = np.asarray(ref.get_statevector())
np.testing.assert_allclose(sv_dist, sv_ref, atol=1e-4)
assert sv_dist.dtype == np.complex64
def test_num_chunks_1_raises_clear_error(self):
# dispatch_distributed only makes sense for num_chunks>1; a Chunk
# that fits in a single chunk must raise, not silently no-op or
# fall back to the single-process path.
c = Chunk(2) # tiny -- always fits in one chunk
assert c.num_chunks == 1
with pytest.raises(RuntimeError, match="num_chunks"):
c.run_chunk_distributed([('h', 0)])
def test_insufficient_devices_raises_clear_error(self, force_chunk_bits, monkeypatch):
# force num_chunks to exceed the actual device count, even though
# we have >= MIN_DEVICES for other tests in this class
force_chunk_bits(3)
c = Chunk(6) # num_chunks=8
monkeypatch.setattr(jax, "device_count", lambda: 4)
with pytest.raises(RuntimeError, match="devices"):
c.run_chunk_distributed([('h', 0)])