Download tests/unit/test_chunk.py from Tatopenn/dense-Evolution: direct link, hf CLI and curl.
- Browser
- Download file 27.4 kB
-
https://huggingface.co/Tatopenn/dense-Evolution/resolve/main/tests/unit/test_chunk.py
- Command line
-
hf download hf://Tatopenn/dense-Evolution/tests/unit/test_chunk.py
-
curl -L -o test_chunk.py https://huggingface.co/Tatopenn/dense-Evolution/resolve/main/tests/unit/test_chunk.py
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.""" | |
| 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) | |
| # 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) | |
| # 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) | |
| 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) | |
| 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) | |
| 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.""" | |
| 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 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 | |
| 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}" | |
| ) | |
| 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) | |
| 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) | |
| 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) | |
| 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)]) | |