Sync test_mps.py from v8.1.35
Browse files- test_mps.py +334 -1
test_mps.py
CHANGED
|
@@ -12,8 +12,13 @@ the primary correctness signal here, not just internal self-consistency.
|
|
| 12 |
import numpy as np
|
| 13 |
import pytest
|
| 14 |
|
|
|
|
|
|
|
|
|
|
| 15 |
import dense_evolution as de
|
| 16 |
-
from dense_evolution.mps import
|
|
|
|
|
|
|
| 17 |
|
| 18 |
INV2 = 1.0 / np.sqrt(2.0)
|
| 19 |
H_GATE = INV2 * np.array([[1, 1], [1, -1]], dtype=complex)
|
|
@@ -435,3 +440,331 @@ def test_top_k_never_touches_full_statevector_at_large_n():
|
|
| 435 |
assert prob_k.sum() <= 1.0 + 1e-9
|
| 436 |
assert len(idx_k) > 0
|
| 437 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
import numpy as np
|
| 13 |
import pytest
|
| 14 |
|
| 15 |
+
import jax
|
| 16 |
+
import jax.numpy as jnp
|
| 17 |
+
|
| 18 |
import dense_evolution as de
|
| 19 |
+
from dense_evolution.mps import (
|
| 20 |
+
MPSSimulator, _jsd_vectors, _vectorized_chi_search, _expand_nonlocal_2q_positions,
|
| 21 |
+
)
|
| 22 |
|
| 23 |
INV2 = 1.0 / np.sqrt(2.0)
|
| 24 |
H_GATE = INV2 * np.array([[1, 1], [1, -1]], dtype=complex)
|
|
|
|
| 440 |
assert prob_k.sum() <= 1.0 + 1e-9
|
| 441 |
assert len(idx_k) > 0
|
| 442 |
|
| 443 |
+
|
| 444 |
+
# ββ _vectorized_chi_search: JIT-fusion groundwork (issue: MPS is ~140x ββ
|
| 445 |
+
# slower than Qiskit Aer's MPS backend on a 60-qubit stress circuit,
|
| 446 |
+
# measured 88.9s vs 0.64s -- root cause: zero @jax.jit anywhere in this
|
| 447 |
+
# file, including a host-syncing Python `while` loop in _svd_truncate to
|
| 448 |
+
# search for the truncated bond dimension. _vectorized_chi_search replaces
|
| 449 |
+
# that loop with one vectorized pass; this test is the correctness gate
|
| 450 |
+
# before it's wired into _svd_truncate itself (a later step) -- it must
|
| 451 |
+
# reproduce the while loop's (chi_new, jsd_val) exactly, not approximately,
|
| 452 |
+
# across real _svd_truncate calls captured from real entangled circuits.
|
| 453 |
+
|
| 454 |
+
def _capture_real_svd_truncate_calls(n_qubits, max_bond, jsd_budget, seed, layers):
|
| 455 |
+
"""Runs a real entangling circuit through MPSSimulator, capturing the
|
| 456 |
+
TRUE full (pre-truncation) singular-value spectrum _svd_truncate saw
|
| 457 |
+
internally for every 2-qubit gate, plus what the real while loop
|
| 458 |
+
decided (chi_new, jsd_val) -- so _vectorized_chi_search can be checked
|
| 459 |
+
against real data, not synthetic arrays."""
|
| 460 |
+
captured = []
|
| 461 |
+
orig_svd_truncate = MPSSimulator._svd_truncate
|
| 462 |
+
|
| 463 |
+
def spy(self, theta_mat):
|
| 464 |
+
full_S = np.array(jnp.linalg.svd(theta_mat, full_matrices=False)[1])
|
| 465 |
+
U, S, Vh, trunc_err, jsd_val = orig_svd_truncate(self, theta_mat)
|
| 466 |
+
captured.append((full_S, self.eps, self.jsd_budget, self.chi, U.shape[1], jsd_val))
|
| 467 |
+
return U, S, Vh, trunc_err, jsd_val
|
| 468 |
+
|
| 469 |
+
MPSSimulator._svd_truncate = spy
|
| 470 |
+
try:
|
| 471 |
+
rng = np.random.default_rng(seed)
|
| 472 |
+
mps = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| 473 |
+
cx = jnp.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]],
|
| 474 |
+
dtype=complex).reshape(2, 2, 2, 2)
|
| 475 |
+
mps.apply_gate_1q(H_GATE, 0)
|
| 476 |
+
for _ in range(layers):
|
| 477 |
+
for i in range(n_qubits):
|
| 478 |
+
theta = float(rng.uniform(0.1, 1.5))
|
| 479 |
+
rx = jnp.array([[jnp.cos(theta / 2), -1j * jnp.sin(theta / 2)],
|
| 480 |
+
[-1j * jnp.sin(theta / 2), jnp.cos(theta / 2)]], dtype=complex)
|
| 481 |
+
mps.apply_gate_1q(rx, i)
|
| 482 |
+
for i in range(0, n_qubits - 1, 2):
|
| 483 |
+
mps.apply_gate_2q(cx, i, i + 1)
|
| 484 |
+
for i in range(1, n_qubits - 1, 2):
|
| 485 |
+
mps.apply_gate_2q(cx, i, i + 1)
|
| 486 |
+
finally:
|
| 487 |
+
MPSSimulator._svd_truncate = orig_svd_truncate
|
| 488 |
+
return captured
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
@pytest.mark.parametrize("n_qubits,max_bond,jsd_budget,seed,layers", [
|
| 492 |
+
(14, 32, 1e-5, 0, 6), # the config that first validated this during design
|
| 493 |
+
(10, 8, 1e-3, 1, 4), # small max_bond -- more likely to hit budget violations
|
| 494 |
+
(20, 64, 1e-6, 2, 5), # larger, tighter budget
|
| 495 |
+
(6, 4, 1e-2, 3, 8), # tiny bond, many layers -- stresses the violation fallback
|
| 496 |
+
])
|
| 497 |
+
def test_vectorized_chi_search_matches_real_while_loop(n_qubits, max_bond, jsd_budget, seed, layers):
|
| 498 |
+
captured = _capture_real_svd_truncate_calls(n_qubits, max_bond, jsd_budget, seed, layers)
|
| 499 |
+
assert len(captured) > 0, "expected at least one real _svd_truncate call"
|
| 500 |
+
for full_S, eps, budget, chi, real_chi_new, real_jsd_val in captured:
|
| 501 |
+
vec_chi_new, vec_jsd_val = _vectorized_chi_search(jnp.array(full_S), eps, budget, chi)
|
| 502 |
+
assert vec_chi_new == real_chi_new
|
| 503 |
+
assert vec_jsd_val == pytest.approx(real_jsd_val, abs=1e-9)
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
# ββ _expand_nonlocal_2q_positions: JIT-fusion groundwork, step 2 ββββββββ
|
| 507 |
+
# Moves the SWAP-chain expansion _apply_nonlocal_2q does at runtime to a
|
| 508 |
+
# pre-compile-time Python function, so a future jax.lax.scan-based kernel
|
| 509 |
+
# can consume a flat, pre-expanded op list instead of dispatching SWAPs
|
| 510 |
+
# inside the traced region.
|
| 511 |
+
|
| 512 |
+
@pytest.mark.parametrize("n_qubits", [6, 10, 15])
|
| 513 |
+
def test_expand_nonlocal_2q_positions_matches_real_dispatch_sequence(n_qubits):
|
| 514 |
+
"""Exhaustive: every (q1, q2) pair for this n_qubits must produce the
|
| 515 |
+
exact same sequence of adjacent-pair apply_gate_2q calls the real
|
| 516 |
+
runtime SWAP-chain dispatch produces."""
|
| 517 |
+
CNOT = jnp.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]],
|
| 518 |
+
dtype=complex).reshape(2, 2, 2, 2)
|
| 519 |
+
|
| 520 |
+
def real_sequence(q1, q2):
|
| 521 |
+
calls = []
|
| 522 |
+
orig_apply_gate_2q = MPSSimulator.apply_gate_2q
|
| 523 |
+
|
| 524 |
+
def spy(self, gate_2q, a, b):
|
| 525 |
+
calls.append((a, b))
|
| 526 |
+
return orig_apply_gate_2q(self, gate_2q, a, b)
|
| 527 |
+
|
| 528 |
+
MPSSimulator.apply_gate_2q = spy
|
| 529 |
+
try:
|
| 530 |
+
mps = MPSSimulator(n_qubits=n_qubits, max_bond=8)
|
| 531 |
+
mps._apply_nonlocal_2q(CNOT, q1, q2)
|
| 532 |
+
finally:
|
| 533 |
+
MPSSimulator.apply_gate_2q = orig_apply_gate_2q
|
| 534 |
+
return calls
|
| 535 |
+
|
| 536 |
+
for q1 in range(n_qubits):
|
| 537 |
+
for q2 in range(n_qubits):
|
| 538 |
+
if q1 == q2:
|
| 539 |
+
continue
|
| 540 |
+
real_seq = real_sequence(q1, q2)
|
| 541 |
+
pred_seq, gate_index, needs_transpose = _expand_nonlocal_2q_positions(q1, q2)
|
| 542 |
+
assert pred_seq == real_seq, f"(q1={q1}, q2={q2}): predicted {pred_seq} != real {real_seq}"
|
| 543 |
+
assert needs_transpose == (q1 > q2)
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
def test_expand_nonlocal_2q_positions_replay_matches_final_state():
|
| 547 |
+
"""End-to-end, not just index bookkeeping: replaying the predicted
|
| 548 |
+
sequence by hand (SWAP at every position except gate_index, the real
|
| 549 |
+
gate -- transposed if needs_transpose -- at gate_index) through the
|
| 550 |
+
existing eager apply_gate_2q must produce the identical final quantum
|
| 551 |
+
state as calling apply_gate_2q(gate, q1, q2) directly (which routes
|
| 552 |
+
through the real runtime _apply_nonlocal_2q dispatch internally)."""
|
| 553 |
+
n_qubits = 8
|
| 554 |
+
swap = jnp.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]],
|
| 555 |
+
dtype=complex).reshape(2, 2, 2, 2)
|
| 556 |
+
CNOT = jnp.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]],
|
| 557 |
+
dtype=complex).reshape(2, 2, 2, 2)
|
| 558 |
+
|
| 559 |
+
for q1, q2 in [(0, 3), (3, 0), (2, 7), (7, 2), (1, 6)]:
|
| 560 |
+
# Reference: real dispatch, via the public API exactly as used elsewhere.
|
| 561 |
+
mps_real = MPSSimulator(n_qubits=n_qubits, max_bond=16)
|
| 562 |
+
mps_real.apply_gate_1q(H_GATE, 0)
|
| 563 |
+
mps_real.apply_gate_1q(H_GATE, 4)
|
| 564 |
+
mps_real.apply_gate_2q(CNOT, q1, q2)
|
| 565 |
+
sv_real = np.asarray(mps_real.contract_to_statevector())
|
| 566 |
+
|
| 567 |
+
# Replay: predicted sequence, executed by hand through the same
|
| 568 |
+
# eager apply_gate_2q primitive, no call into _apply_nonlocal_2q.
|
| 569 |
+
mps_replay = MPSSimulator(n_qubits=n_qubits, max_bond=16)
|
| 570 |
+
mps_replay.apply_gate_1q(H_GATE, 0)
|
| 571 |
+
mps_replay.apply_gate_1q(H_GATE, 4)
|
| 572 |
+
seq, gate_index, needs_transpose = _expand_nonlocal_2q_positions(q1, q2)
|
| 573 |
+
gate_to_apply = jnp.transpose(CNOT, (1, 0, 3, 2)) if needs_transpose else CNOT
|
| 574 |
+
for i, (a, b) in enumerate(seq):
|
| 575 |
+
mps_replay.apply_gate_2q(gate_to_apply if i == gate_index else swap, a, b)
|
| 576 |
+
sv_replay = np.asarray(mps_replay.contract_to_statevector())
|
| 577 |
+
|
| 578 |
+
fidelity = np.abs(np.vdot(sv_real, sv_replay)) ** 2
|
| 579 |
+
assert fidelity == pytest.approx(1.0, abs=1e-9), f"(q1={q1}, q2={q2}): fidelity={fidelity}"
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
# ββ run_circuit_jit: JIT-fusion groundwork, step 3 βββββββββββββββββββββββ
|
| 583 |
+
# The fused whole-circuit jax.lax.scan path. Correctness bar is the same
|
| 584 |
+
# one this file already established for the eager path: cross-validation
|
| 585 |
+
# against DenseSVSimulator on real entangling circuits, not just internal
|
| 586 |
+
# self-consistency. The eager apply_gate_1q/apply_gate_2q/_apply_nonlocal_2q
|
| 587 |
+
# methods are untouched by this -- run_circuit_jit is a new, additional
|
| 588 |
+
# entry point.
|
| 589 |
+
|
| 590 |
+
def test_run_circuit_jit_bell_state():
|
| 591 |
+
mps = MPSSimulator(n_qubits=2, max_bond=8)
|
| 592 |
+
mps.run_circuit_jit([["h", 0], ["cx", 0, 1]])
|
| 593 |
+
prob = np.abs(np.asarray(mps.contract_to_statevector())) ** 2
|
| 594 |
+
assert prob[0b00] == pytest.approx(0.5, abs=1e-9)
|
| 595 |
+
assert prob[0b11] == pytest.approx(0.5, abs=1e-9)
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
def test_run_circuit_jit_ghz_chain():
|
| 599 |
+
n = 6
|
| 600 |
+
ops = [["h", 0]] + [["cx", q, q + 1] for q in range(n - 1)]
|
| 601 |
+
mps = MPSSimulator(n_qubits=n, max_bond=8)
|
| 602 |
+
mps.run_circuit_jit(ops)
|
| 603 |
+
prob = np.abs(np.asarray(mps.contract_to_statevector())) ** 2
|
| 604 |
+
assert prob[0] == pytest.approx(0.5, abs=1e-9)
|
| 605 |
+
assert prob[2 ** n - 1] == pytest.approx(0.5, abs=1e-9)
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
def test_run_circuit_jit_nonlocal_ctrl_greater_than_tgt_ghz():
|
| 609 |
+
# The exact scenario the MPS ctrl/tgt inversion bug was found on
|
| 610 |
+
# earlier this session (H, CX(0,3), CX(3,1) non-adjacent+ctrl>tgt,
|
| 611 |
+
# CX(1,2)) -- must stay correct through the fused path too.
|
| 612 |
+
n = 4
|
| 613 |
+
mps = MPSSimulator(n_qubits=n, max_bond=16)
|
| 614 |
+
mps.run_circuit_jit([["h", 0], ["cx", 0, 3], ["cx", 3, 1], ["cx", 1, 2]])
|
| 615 |
+
sv = np.asarray(mps.contract_to_statevector())
|
| 616 |
+
exact = np.zeros(2 ** n, dtype=complex)
|
| 617 |
+
exact[0] = exact[-1] = 1 / np.sqrt(2)
|
| 618 |
+
fidelity = np.abs(np.vdot(sv, exact)) ** 2
|
| 619 |
+
assert fidelity == pytest.approx(1.0, abs=1e-9)
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
def test_run_circuit_jit_toffoli_matches_classical_truth_table():
|
| 623 |
+
mps = MPSSimulator(n_qubits=3, max_bond=8)
|
| 624 |
+
mps.run_circuit_jit([["x", 0], ["x", 1], ["ccx", 0, 1, 2]])
|
| 625 |
+
# MPSSimulator has no run_circuit_jit-native ccx decomposition -- use
|
| 626 |
+
# the transpiler's own, same source of truth compiler.py/chunk.py use.
|
| 627 |
+
prob = np.abs(np.asarray(mps.contract_to_statevector())) ** 2
|
| 628 |
+
assert prob[0b111] == pytest.approx(1.0, abs=1e-6)
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
@pytest.mark.parametrize("n_qubits,max_bond,jsd_budget,seed,layers", [
|
| 632 |
+
(12, 64, 1e-6, 5, 4), # generous max_bond -- exact match expected
|
| 633 |
+
(16, 32, 1e-5, 7, 3),
|
| 634 |
+
])
|
| 635 |
+
def test_run_circuit_jit_matches_dense_simulator(n_qubits, max_bond, jsd_budget, seed, layers):
|
| 636 |
+
rng = np.random.default_rng(seed)
|
| 637 |
+
ops = [["h", q] for q in range(n_qubits)]
|
| 638 |
+
for _ in range(layers):
|
| 639 |
+
for q in range(0, n_qubits - 1, 2):
|
| 640 |
+
ops.append(["cx", q + 1, q])
|
| 641 |
+
for q in range(n_qubits):
|
| 642 |
+
ops.append(["rz", q, float(rng.uniform(0.1, 1.5))])
|
| 643 |
+
for q in range(1, n_qubits - 1, 2):
|
| 644 |
+
ops.append(["cx", q + 1, q])
|
| 645 |
+
|
| 646 |
+
sim_dense = de.DenseSVSimulator(n_qubits=n_qubits, use_gpu=False, use_float32=False)
|
| 647 |
+
sim_dense.run_circuit_jit_beast_mode(ops)
|
| 648 |
+
prob_dense = np.array(sim_dense.get_probabilities())
|
| 649 |
+
|
| 650 |
+
mps = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| 651 |
+
mps.run_circuit_jit(ops)
|
| 652 |
+
prob_mps = np.abs(np.asarray(mps.contract_to_statevector())) ** 2
|
| 653 |
+
|
| 654 |
+
tvd = 0.5 * np.sum(np.abs(prob_dense - prob_mps))
|
| 655 |
+
assert tvd < 1e-6, f"TVD={tvd} -- run_circuit_jit diverged from DenseSVSimulator"
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
def test_run_circuit_jit_matches_eager_even_under_budget_violation():
|
| 659 |
+
# max_bond=8 is deliberately too small for this circuit's real
|
| 660 |
+
# entanglement (triggers real budget_violations, same as
|
| 661 |
+
# test_budget_violations_flagged_when_max_bond_too_small above) --
|
| 662 |
+
# DenseSVSimulator is NOT the right correctness bar here (neither
|
| 663 |
+
# MPS path can match it at this max_bond, by construction, that's
|
| 664 |
+
# what "budget violation" means). What must hold: the fused path's
|
| 665 |
+
# lossy truncation must match the eager path's own lossy truncation
|
| 666 |
+
# almost exactly -- fusion must not introduce error *beyond* what the
|
| 667 |
+
# eager implementation itself already accepts at this max_bond.
|
| 668 |
+
n_qubits, max_bond, jsd_budget, seed, layers = 10, 8, 1e-3, 6, 5
|
| 669 |
+
rng = np.random.default_rng(seed)
|
| 670 |
+
ops = [["h", q] for q in range(n_qubits)]
|
| 671 |
+
for _ in range(layers):
|
| 672 |
+
for q in range(0, n_qubits - 1, 2):
|
| 673 |
+
ops.append(["cx", q + 1, q])
|
| 674 |
+
for q in range(n_qubits):
|
| 675 |
+
ops.append(["rz", q, float(rng.uniform(0.1, 1.5))])
|
| 676 |
+
for q in range(1, n_qubits - 1, 2):
|
| 677 |
+
ops.append(["cx", q + 1, q])
|
| 678 |
+
|
| 679 |
+
mps_eager = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| 680 |
+
rz_cache = {}
|
| 681 |
+
for op in ops:
|
| 682 |
+
if op[0] == "h":
|
| 683 |
+
mps_eager.apply_gate_1q(H_GATE, op[1])
|
| 684 |
+
elif op[0] == "cx":
|
| 685 |
+
mps_eager.apply_cx(op[1], op[2])
|
| 686 |
+
elif op[0] == "rz":
|
| 687 |
+
theta = op[2]
|
| 688 |
+
if theta not in rz_cache:
|
| 689 |
+
rz_cache[theta] = jnp.array(
|
| 690 |
+
[[jnp.exp(-1j * theta / 2), 0], [0, jnp.exp(1j * theta / 2)]], dtype=complex)
|
| 691 |
+
mps_eager.apply_gate_1q(rz_cache[theta], op[1])
|
| 692 |
+
assert mps_eager.budget_violations > 0, "test setup should genuinely stress max_bond"
|
| 693 |
+
sv_eager = np.asarray(mps_eager.contract_to_statevector())
|
| 694 |
+
|
| 695 |
+
mps_fused = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| 696 |
+
mps_fused.run_circuit_jit(ops)
|
| 697 |
+
sv_fused = np.asarray(mps_fused.contract_to_statevector())
|
| 698 |
+
|
| 699 |
+
fidelity = np.abs(np.vdot(sv_eager, sv_fused)) ** 2
|
| 700 |
+
assert fidelity == pytest.approx(1.0, abs=1e-9)
|
| 701 |
+
|
| 702 |
+
|
| 703 |
+
# ββ bookkeeping parity: JIT-fusion groundwork, step 4 ββββββββββββββββββββ
|
| 704 |
+
# entanglement_entropy/_bond_history/jsd_per_bond/truncation_errors/
|
| 705 |
+
# budget_violations must end up populated the same way after
|
| 706 |
+
# run_circuit_jit as after the equivalent eager apply_gate_1q/apply_cx
|
| 707 |
+
# calls -- same information, populated from jax.lax.scan's stacked
|
| 708 |
+
# per-step diagnostics instead of Python list.append()s inside a loop.
|
| 709 |
+
|
| 710 |
+
def test_run_circuit_jit_bookkeeping_matches_eager():
|
| 711 |
+
n_qubits, max_bond, jsd_budget, seed, layers = 10, 8, 1e-3, 6, 5
|
| 712 |
+
rng = np.random.default_rng(seed)
|
| 713 |
+
ops = [["h", q] for q in range(n_qubits)]
|
| 714 |
+
for _ in range(layers):
|
| 715 |
+
for q in range(0, n_qubits - 1, 2):
|
| 716 |
+
ops.append(["cx", q + 1, q])
|
| 717 |
+
for q in range(n_qubits):
|
| 718 |
+
ops.append(["rz", q, float(rng.uniform(0.1, 1.5))])
|
| 719 |
+
for q in range(1, n_qubits - 1, 2):
|
| 720 |
+
ops.append(["cx", q + 1, q])
|
| 721 |
+
|
| 722 |
+
mps_eager = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| 723 |
+
rz_cache = {}
|
| 724 |
+
for op in ops:
|
| 725 |
+
if op[0] == "h":
|
| 726 |
+
mps_eager.apply_gate_1q(H_GATE, op[1])
|
| 727 |
+
elif op[0] == "cx":
|
| 728 |
+
mps_eager.apply_cx(op[1], op[2])
|
| 729 |
+
elif op[0] == "rz":
|
| 730 |
+
theta = op[2]
|
| 731 |
+
if theta not in rz_cache:
|
| 732 |
+
rz_cache[theta] = jnp.array(
|
| 733 |
+
[[jnp.exp(-1j * theta / 2), 0], [0, jnp.exp(1j * theta / 2)]], dtype=complex)
|
| 734 |
+
mps_eager.apply_gate_1q(rz_cache[theta], op[1])
|
| 735 |
+
|
| 736 |
+
mps_fused = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| 737 |
+
mps_fused.run_circuit_jit(ops)
|
| 738 |
+
|
| 739 |
+
assert mps_fused._bond_history == mps_eager._bond_history
|
| 740 |
+
assert mps_fused.budget_violations == mps_eager.budget_violations
|
| 741 |
+
assert len(mps_fused.jsd_per_bond) == len(mps_eager.jsd_per_bond)
|
| 742 |
+
for fused_v, eager_v in zip(mps_fused.jsd_per_bond, mps_eager.jsd_per_bond):
|
| 743 |
+
assert fused_v == pytest.approx(eager_v, abs=1e-9)
|
| 744 |
+
for fused_v, eager_v in zip(mps_fused.truncation_errors, mps_eager.truncation_errors):
|
| 745 |
+
assert fused_v == pytest.approx(eager_v, abs=1e-6)
|
| 746 |
+
assert np.max(np.abs(mps_fused.entanglement_entropy - mps_eager.entanglement_entropy)) < 1e-9
|
| 747 |
+
assert mps_eager.budget_violations > 0, "test setup should genuinely stress max_bond"
|
| 748 |
+
|
| 749 |
+
|
| 750 |
+
@pytest.mark.parametrize("use_float64", [False, True])
|
| 751 |
+
def test_run_circuit_jit_both_dtypes(use_float64):
|
| 752 |
+
previous = jax.config.jax_enable_x64
|
| 753 |
+
jax.config.update("jax_enable_x64", use_float64)
|
| 754 |
+
try:
|
| 755 |
+
mps = MPSSimulator(n_qubits=4, max_bond=8)
|
| 756 |
+
mps.run_circuit_jit([["h", 0], ["cx", 0, 3], ["cx", 3, 1], ["cx", 1, 2]])
|
| 757 |
+
sv = np.asarray(mps.contract_to_statevector())
|
| 758 |
+
exact = np.zeros(16, dtype=complex)
|
| 759 |
+
exact[0] = exact[-1] = 1 / np.sqrt(2)
|
| 760 |
+
fidelity = np.abs(np.vdot(sv, exact)) ** 2
|
| 761 |
+
assert fidelity == pytest.approx(1.0, abs=1e-6)
|
| 762 |
+
finally:
|
| 763 |
+
jax.config.update("jax_enable_x64", previous)
|
| 764 |
+
|
| 765 |
+
|
| 766 |
+
def test_run_circuit_jit_rejects_unknown_gate():
|
| 767 |
+
mps = MPSSimulator(n_qubits=2, max_bond=4)
|
| 768 |
+
with pytest.raises(ValueError):
|
| 769 |
+
mps.run_circuit_jit([["not_a_real_gate", 0]])
|
| 770 |
+
|