| """
|
| Unit tests for dense_evolution/mps.py -- the Matrix Product State
|
| simulator, ported from a private prototype after independently verifying
|
| that its plain SVD truncation (no Lloyd-Max quantization, which measured
|
| a real ~0.5% error against DenseSVSimulator for no bond-dimension benefit
|
| and was dropped in this port) reproduces DenseSVSimulator exactly.
|
|
|
| Cross-checks against the real DenseSVSimulator on entangling circuits are
|
| the primary correctness signal here, not just internal self-consistency.
|
| """
|
|
|
| import numpy as np
|
| import pytest
|
|
|
| import jax
|
| import jax.numpy as jnp
|
|
|
| import dense_evolution as de
|
| from dense_evolution.mps import (
|
| MPSSimulator, _jsd_vectors, _vectorized_chi_search, _expand_nonlocal_2q_positions,
|
| )
|
|
|
| INV2 = 1.0 / np.sqrt(2.0)
|
| H_GATE = INV2 * np.array([[1, 1], [1, -1]], dtype=complex)
|
|
|
|
|
| def _entangling_circuit_probs_dense(n, seed=7, layers=4):
|
| sim = de.DenseSVSimulator(n_qubits=n, use_gpu=False, use_float32=False)
|
| ops = [["h", q, -1] for q in range(n)]
|
| rng = np.random.default_rng(seed)
|
| for _ in range(layers):
|
| for q in range(0, n - 1, 2):
|
| ops.append(["cx", q + 1, q])
|
| for q in range(n):
|
| ops.append(["rz", q, float(rng.uniform(0.1, 1.5))])
|
| for q in range(1, n - 1, 2):
|
| ops.append(["cx", q + 1, q])
|
| sim.run_circuit_jit_beast_mode(ops)
|
| return np.array(sim.get_probabilities())
|
|
|
|
|
| def _entangling_circuit_probs_mps(n, seed=7, layers=4, **mps_kwargs):
|
| mps = MPSSimulator(n_qubits=n, **mps_kwargs)
|
| for q in range(n):
|
| mps.apply_gate_1q(H_GATE, q)
|
| rng = np.random.default_rng(seed)
|
| for _ in range(layers):
|
| for q in range(0, n - 1, 2):
|
| mps.apply_cx(q + 1, q)
|
| for q in range(n):
|
| theta = float(rng.uniform(0.1, 1.5))
|
| rz = np.array([[np.exp(-1j * theta / 2), 0], [0, np.exp(1j * theta / 2)]], dtype=complex)
|
| mps.apply_gate_1q(rz, q)
|
| for q in range(1, n - 1, 2):
|
| mps.apply_cx(q + 1, q)
|
| sv = mps.contract_to_statevector()
|
| return np.abs(sv) ** 2, mps
|
|
|
|
|
|
|
|
|
| def test_jsd_identical_distributions_is_zero():
|
| p = np.array([0.5, 0.3, 0.2])
|
| assert _jsd_vectors(p, p) == pytest.approx(0.0, abs=1e-9)
|
|
|
|
|
| def test_jsd_handles_zero_entries_without_warning():
|
| p = np.array([1.0, 0.0, 0.0])
|
| q = np.array([0.5, 0.5, 0.0])
|
| with np.errstate(all="raise"):
|
| val = _jsd_vectors(p, q)
|
| assert np.isfinite(val)
|
|
|
|
|
|
|
|
|
| def test_bell_state():
|
| mps = MPSSimulator(n_qubits=2, max_bond=8)
|
| mps.apply_gate_1q(H_GATE, 0)
|
| mps.apply_cx(0, 1)
|
| sv = mps.contract_to_statevector()
|
| prob = np.abs(sv) ** 2
|
| assert prob[0] == pytest.approx(0.5, abs=1e-9)
|
| assert prob[3] == pytest.approx(0.5, abs=1e-9)
|
| assert prob[1] == pytest.approx(0.0, abs=1e-9)
|
| assert prob[2] == pytest.approx(0.0, abs=1e-9)
|
|
|
|
|
| def test_ghz_chain_n_qubits():
|
| n = 6
|
| mps = MPSSimulator(n_qubits=n, max_bond=8)
|
| mps.apply_gate_1q(H_GATE, 0)
|
| for q in range(n - 1):
|
| mps.apply_cx(q, q + 1)
|
| sv = mps.contract_to_statevector()
|
| prob = np.abs(sv) ** 2
|
| assert prob[0] == pytest.approx(0.5, abs=1e-9)
|
| assert prob[-1] == pytest.approx(0.5, abs=1e-9)
|
| assert prob.sum() == pytest.approx(1.0, abs=1e-9)
|
|
|
|
|
| def test_statevector_stays_normalized_after_many_gates():
|
| n = 5
|
| mps = MPSSimulator(n_qubits=n, max_bond=16)
|
| rng = np.random.default_rng(3)
|
| for q in range(n):
|
| mps.apply_gate_1q(H_GATE, q)
|
| for _ in range(3):
|
| for q in range(n - 1):
|
| mps.apply_cx(q, q + 1)
|
| sv = mps.contract_to_statevector()
|
| assert np.linalg.norm(sv) == pytest.approx(1.0, abs=1e-9)
|
|
|
|
|
| def test_nonlocal_2q_gate_via_swap_chain():
|
| n = 4
|
| mps = MPSSimulator(n_qubits=n, max_bond=16)
|
| mps.apply_gate_1q(H_GATE, 0)
|
| mps.apply_cx(0, 3)
|
| sv = mps.contract_to_statevector()
|
| prob = np.abs(sv) ** 2
|
|
|
| assert prob[0b0000] == pytest.approx(0.5, abs=1e-9)
|
| assert prob[0b1001] == pytest.approx(0.5, abs=1e-9)
|
|
|
|
|
| def test_toffoli_matches_classical_truth_table():
|
|
|
| mps = MPSSimulator(n_qubits=3, max_bond=8)
|
| x = np.array([[0, 1], [1, 0]], dtype=complex)
|
| mps.apply_gate_1q(x, 0)
|
| mps.apply_gate_1q(x, 1)
|
| mps.apply_ccx(0, 1, 2)
|
| sv = mps.contract_to_statevector()
|
| prob = np.abs(sv) ** 2
|
| assert prob[0b111] == pytest.approx(1.0, abs=1e-6)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def test_nonlocal_2q_gate_ctrl_greater_than_tgt():
|
| n = 4
|
| mps = MPSSimulator(n_qubits=n, max_bond=16)
|
| mps.apply_gate_1q(H_GATE, 3)
|
| mps.apply_cx(3, 0)
|
| sv = mps.contract_to_statevector()
|
| prob = np.abs(sv) ** 2
|
|
|
| assert prob[0b0000] == pytest.approx(0.5, abs=1e-9)
|
| assert prob[0b1001] == pytest.approx(0.5, abs=1e-9)
|
|
|
|
|
| def test_nonlocal_2q_gate_ctrl_greater_than_tgt_matches_dense_simulator():
|
| n = 5
|
| ops = [["h", 0, -1], ["h", 2, -1],
|
| ["cx", 4, 0],
|
| ["cx", 3, 1],
|
| ["cx", 0, 4]]
|
| sim = de.DenseSVSimulator(n_qubits=n, use_gpu=False, use_float32=False)
|
| sim.run_circuit_jit_beast_mode(ops)
|
| prob_dense = np.array(sim.get_probabilities())
|
|
|
| mps = MPSSimulator(n_qubits=n, max_bond=16)
|
| mps.apply_gate_1q(H_GATE, 0)
|
| mps.apply_gate_1q(H_GATE, 2)
|
| mps.apply_cx(4, 0)
|
| mps.apply_cx(3, 1)
|
| mps.apply_cx(0, 4)
|
| prob_mps = np.abs(mps.contract_to_statevector()) ** 2
|
|
|
| tvd = 0.5 * np.sum(np.abs(prob_dense - prob_mps))
|
| assert tvd < 1e-6, f"TVD={tvd} -- MPS diverged from DenseSVSimulator on ctrl>tgt non-adjacent gates"
|
|
|
|
|
| def test_ghz_via_out_of_order_nonlocal_cnots():
|
|
|
|
|
| n = 4
|
| mps = MPSSimulator(n_qubits=n, jsd_budget=1e-6)
|
| mps.apply_gate_1q(H_GATE, 0)
|
| mps.apply_cx(0, 3)
|
| mps.apply_cx(3, 1)
|
| mps.apply_cx(1, 2)
|
| prob = np.abs(mps.contract_to_statevector()) ** 2
|
| assert prob[0b0000] == pytest.approx(0.5, abs=1e-6)
|
| assert prob[0b1111] == pytest.approx(0.5, abs=1e-6)
|
| assert np.sum(prob) == pytest.approx(1.0, abs=1e-6)
|
|
|
|
|
| @pytest.mark.parametrize("use_float64", [False, True])
|
| def test_nonlocal_2q_gate_ctrl_greater_than_tgt_both_dtypes(use_float64):
|
| import jax
|
| previous = jax.config.jax_enable_x64
|
| jax.config.update("jax_enable_x64", use_float64)
|
| try:
|
| n = 4
|
| mps = MPSSimulator(n_qubits=n, max_bond=16)
|
| mps.apply_gate_1q(H_GATE, 3)
|
| mps.apply_cx(3, 0)
|
| prob = np.abs(mps.contract_to_statevector()) ** 2
|
| assert prob[0b0000] == pytest.approx(0.5, abs=1e-6)
|
| assert prob[0b1001] == pytest.approx(0.5, abs=1e-6)
|
| finally:
|
| jax.config.update("jax_enable_x64", previous)
|
|
|
|
|
| def test_toffoli_non_adjacent_unordered_controls():
|
|
|
|
|
|
|
| n = 4
|
| mps = MPSSimulator(n_qubits=n, max_bond=16)
|
| x = np.array([[0, 1], [1, 0]], dtype=complex)
|
| mps.apply_gate_1q(x, 3)
|
| mps.apply_gate_1q(x, 0)
|
| mps.apply_ccx(3, 0, 1)
|
| prob = np.abs(mps.contract_to_statevector()) ** 2
|
|
|
| assert prob[0b1101] == pytest.approx(1.0, abs=1e-6)
|
|
|
|
|
|
|
|
|
| def test_matches_dense_simulator_on_entangling_circuit():
|
| n = 8
|
| prob_dense = _entangling_circuit_probs_dense(n)
|
| prob_mps, mps = _entangling_circuit_probs_mps(n, max_bond=64, jsd_budget=1e-5)
|
| tvd = 0.5 * np.sum(np.abs(prob_dense - prob_mps))
|
| assert tvd < 1e-9, f"TVD={tvd} -- MPS diverged from DenseSVSimulator"
|
| assert mps.max_bond_used() > 1, "test circuit should produce real entanglement"
|
|
|
|
|
| def test_matches_dense_simulator_smaller_bond_still_exact_here():
|
|
|
|
|
|
|
| n = 8
|
| prob_dense = _entangling_circuit_probs_dense(n)
|
| prob_mps, mps = _entangling_circuit_probs_mps(n, max_bond=32, jsd_budget=1e-5)
|
| tvd = 0.5 * np.sum(np.abs(prob_dense - prob_mps))
|
| assert tvd < 1e-9
|
|
|
|
|
|
|
|
|
| def test_budget_violations_flagged_when_max_bond_too_small():
|
|
|
|
|
|
|
|
|
|
|
| n = 8
|
| prob_dense = _entangling_circuit_probs_dense(n, layers=15)
|
| with pytest.warns(UserWarning, match="jsd_budget.*not honored"):
|
| prob_mps, mps = _entangling_circuit_probs_mps(n, layers=15, max_bond=2, jsd_budget=1e-5)
|
| tvd = 0.5 * np.sum(np.abs(prob_dense - prob_mps))
|
| assert tvd > 0.5, "sanity: this max_bond should produce a badly wrong result"
|
| assert mps.budget_violations > 0
|
| assert "budget_violations=" in mps.summary()
|
|
|
|
|
| def test_budget_violations_zero_when_max_bond_is_adequate():
|
|
|
|
|
|
|
| n = 8
|
| import warnings
|
| with warnings.catch_warnings(record=True) as caught:
|
| warnings.simplefilter("always")
|
| _, mps = _entangling_circuit_probs_mps(n, max_bond=64, jsd_budget=1e-5)
|
| user_warnings = [w for w in caught if issubclass(w.category, UserWarning)]
|
| assert user_warnings == []
|
| assert mps.budget_violations == 0
|
|
|
|
|
|
|
|
|
| def test_sampled_probabilities_low_entanglement_large_n():
|
|
|
|
|
| n = 40
|
| mps = MPSSimulator(n_qubits=n, max_bond=8)
|
| mps.apply_gate_1q(H_GATE, 0)
|
| for q in range(n - 1):
|
| mps.apply_cx(q, q + 1)
|
| assert mps.max_bond_used() <= 2
|
|
|
| dist = mps.get_probabilities_sampled(n_samples=500, seed=1)
|
| zeros = "0" * n
|
| ones = "1" * n
|
| total = sum(dist.values())
|
| assert total == pytest.approx(1.0, abs=1e-9)
|
|
|
| assert set(dist.keys()) <= {zeros, ones}
|
| assert zeros in dist and ones in dist
|
|
|
|
|
| def test_contract_to_statevector_raises_above_24_qubits():
|
| mps = MPSSimulator(n_qubits=25, max_bond=4)
|
| with pytest.raises(MemoryError):
|
| mps.contract_to_statevector()
|
|
|
|
|
|
|
| def test_dashboard_run_simulation_mps_matches_dense_bell():
|
| import dashboard_core as dc
|
|
|
| qasm_bell = ('OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; creg c[2]; '
|
| 'h q[0]; cx q[0],q[1]; measure q -> c;')
|
|
|
| res_dense = dc.run_simulation(
|
| 'Custom Workspace', 'Custom Workspace', qasm_bell,
|
| 'ideal', 0.0, 100, 42, use_float32=True, engine='dense',
|
| )
|
| res_mps = dc.run_simulation(
|
| 'Custom Workspace', 'Custom Workspace', qasm_bell,
|
| 'ideal', 0.0, 100, 42, use_float32=True, engine='mps',
|
| )
|
| assert np.allclose(res_dense['prob'], res_mps['prob'], atol=1e-9)
|
|
|
|
|
|
|
| assert type(res_mps['sim']).__name__ == 'DenseSVSimulator'
|
|
|
|
|
| def test_dashboard_run_simulation_mps_matches_dense_entangling():
|
| import dashboard_core as dc
|
|
|
| n = 8
|
| gates = ["h q[{}];".format(i) for i in range(n)]
|
| gates += ["cx q[{}],q[{}];".format(i, i + 1) for i in range(0, n - 1, 2)]
|
| gates += ["rz(0.7) q[{}];".format(i) for i in range(n)]
|
| gates += ["cx q[{}],q[{}];".format(i, i + 1) for i in range(1, n - 1, 2)]
|
| qasm = (f'OPENQASM 2.0; include "qelib1.inc"; qreg q[{n}]; creg c[{n}]; '
|
| + " ".join(gates) + " measure q -> c;")
|
|
|
| res_dense = dc.run_simulation(
|
| 'Custom Workspace', 'Custom Workspace', qasm,
|
| 'ideal', 0.0, 100, 42, use_float32=False, engine='dense',
|
| )
|
| res_mps = dc.run_simulation(
|
| 'Custom Workspace', 'Custom Workspace', qasm,
|
| 'ideal', 0.0, 100, 42, use_float32=False, engine='mps',
|
| )
|
| tvd = 0.5 * np.sum(np.abs(res_dense['prob'] - res_mps['prob']))
|
| assert tvd < 1e-9
|
|
|
|
|
| def test_dashboard_run_simulation_mps_rejects_over_24_qubits():
|
| import dashboard_core as dc
|
|
|
| qasm = 'OPENQASM 2.0; include "qelib1.inc"; qreg q[30]; creg c[30]; h q[0]; measure q -> c;'
|
| with pytest.raises(ValueError, match="24 qubit"):
|
| dc.run_simulation(
|
| 'Custom Workspace', 'Custom Workspace', qasm,
|
| 'ideal', 0.0, 100, 42, use_float32=True, engine='mps',
|
| )
|
|
|
|
|
|
|
| def test_top_k_probability_values_are_exact():
|
|
|
|
|
| n = 8
|
| prob_dense, _ = _entangling_circuit_probs_mps(n, max_bond=64, jsd_budget=1e-5)
|
| mps = MPSSimulator(n_qubits=n, max_bond=64, jsd_budget=1e-5)
|
| for q in range(n):
|
| mps.apply_gate_1q(H_GATE, q)
|
| rng = np.random.default_rng(7)
|
| for _ in range(4):
|
| for q in range(0, n - 1, 2):
|
| mps.apply_cx(q + 1, q)
|
| for q in range(n):
|
| theta = float(rng.uniform(0.1, 1.5))
|
| rz = np.array([[np.exp(-1j * theta / 2), 0], [0, np.exp(1j * theta / 2)]], dtype=complex)
|
| mps.apply_gate_1q(rz, q)
|
| for q in range(1, n - 1, 2):
|
| mps.apply_cx(q + 1, q)
|
|
|
| idx_k, prob_k = mps.get_top_k_probable_states(k=32)
|
| for i, p in zip(idx_k, prob_k):
|
| assert p == pytest.approx(prob_dense[i], abs=1e-9)
|
|
|
|
|
| def test_top_k_recall_improves_with_beam_width():
|
| n = 10
|
| mps_ref = MPSSimulator(n_qubits=n, max_bond=64, jsd_budget=1e-5)
|
| rng = np.random.default_rng(3)
|
| for q in range(n):
|
| mps_ref.apply_gate_1q(H_GATE, q)
|
| for _ in range(3):
|
| for q in range(0, n - 1, 2):
|
| mps_ref.apply_cx(q + 1, q)
|
| for q in range(n):
|
| theta = float(rng.uniform(0.1, 1.5))
|
| rz = np.array([[np.exp(-1j * theta / 2), 0], [0, np.exp(1j * theta / 2)]], dtype=complex)
|
| mps_ref.apply_gate_1q(rz, q)
|
| for q in range(1, n - 1, 2):
|
| mps_ref.apply_cx(q + 1, q)
|
| sv_exact = mps_ref.contract_to_statevector()
|
| prob_exact = np.abs(sv_exact) ** 2
|
| true_top = set(np.argsort(-prob_exact)[:8].tolist())
|
|
|
| def build():
|
| m = MPSSimulator(n_qubits=n, max_bond=64, jsd_budget=1e-5)
|
| rng2 = np.random.default_rng(3)
|
| for q in range(n):
|
| m.apply_gate_1q(H_GATE, q)
|
| for _ in range(3):
|
| for q in range(0, n - 1, 2):
|
| m.apply_cx(q + 1, q)
|
| for q in range(n):
|
| theta = float(rng2.uniform(0.1, 1.5))
|
| rz = np.array([[np.exp(-1j * theta / 2), 0], [0, np.exp(1j * theta / 2)]], dtype=complex)
|
| m.apply_gate_1q(rz, q)
|
| for q in range(1, n - 1, 2):
|
| m.apply_cx(q + 1, q)
|
| return m
|
|
|
| idx_small, _ = build().get_top_k_probable_states(k=8)
|
| idx_large, _ = build().get_top_k_probable_states(k=64)
|
| hits_small = len(set(idx_small.tolist()[:8]) & true_top)
|
| hits_large = len(set(idx_large.tolist()[:8]) & true_top)
|
| assert hits_large >= hits_small
|
|
|
|
|
| def test_top_k_never_touches_full_statevector_at_large_n():
|
|
|
|
|
| n = 30
|
| mps = MPSSimulator(n_qubits=n, max_bond=8)
|
| mps.apply_gate_1q(H_GATE, 0)
|
| for q in range(n - 1):
|
| mps.apply_cx(q, q + 1)
|
| idx_k, prob_k = mps.get_top_k_probable_states(k=8)
|
| assert prob_k.sum() <= 1.0 + 1e-9
|
| assert len(idx_k) > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def _capture_real_svd_truncate_calls(n_qubits, max_bond, jsd_budget, seed, layers):
|
| """Runs a real entangling circuit through MPSSimulator, capturing the
|
| TRUE full (pre-truncation) singular-value spectrum _svd_truncate saw
|
| internally for every 2-qubit gate, plus what the real while loop
|
| decided (chi_new, jsd_val) -- so _vectorized_chi_search can be checked
|
| against real data, not synthetic arrays."""
|
| captured = []
|
| orig_svd_truncate = MPSSimulator._svd_truncate
|
|
|
| def spy(self, theta_mat):
|
| full_S = np.array(jnp.linalg.svd(theta_mat, full_matrices=False)[1])
|
| U, S, Vh, trunc_err, jsd_val = orig_svd_truncate(self, theta_mat)
|
| captured.append((full_S, self.eps, self.jsd_budget, self.chi, U.shape[1], jsd_val))
|
| return U, S, Vh, trunc_err, jsd_val
|
|
|
| MPSSimulator._svd_truncate = spy
|
| try:
|
| rng = np.random.default_rng(seed)
|
| mps = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| cx = jnp.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]],
|
| dtype=complex).reshape(2, 2, 2, 2)
|
| mps.apply_gate_1q(H_GATE, 0)
|
| for _ in range(layers):
|
| for i in range(n_qubits):
|
| theta = float(rng.uniform(0.1, 1.5))
|
| rx = jnp.array([[jnp.cos(theta / 2), -1j * jnp.sin(theta / 2)],
|
| [-1j * jnp.sin(theta / 2), jnp.cos(theta / 2)]], dtype=complex)
|
| mps.apply_gate_1q(rx, i)
|
| for i in range(0, n_qubits - 1, 2):
|
| mps.apply_gate_2q(cx, i, i + 1)
|
| for i in range(1, n_qubits - 1, 2):
|
| mps.apply_gate_2q(cx, i, i + 1)
|
| finally:
|
| MPSSimulator._svd_truncate = orig_svd_truncate
|
| return captured
|
|
|
|
|
| @pytest.mark.parametrize("n_qubits,max_bond,jsd_budget,seed,layers", [
|
| (14, 32, 1e-5, 0, 6),
|
| (10, 8, 1e-3, 1, 4),
|
| (20, 64, 1e-6, 2, 5),
|
| (6, 4, 1e-2, 3, 8),
|
| ])
|
| def test_vectorized_chi_search_matches_real_while_loop(n_qubits, max_bond, jsd_budget, seed, layers):
|
| captured = _capture_real_svd_truncate_calls(n_qubits, max_bond, jsd_budget, seed, layers)
|
| assert len(captured) > 0, "expected at least one real _svd_truncate call"
|
| for full_S, eps, budget, chi, real_chi_new, real_jsd_val in captured:
|
| vec_chi_new, vec_jsd_val = _vectorized_chi_search(jnp.array(full_S), eps, budget, chi)
|
| assert vec_chi_new == real_chi_new
|
| assert vec_jsd_val == pytest.approx(real_jsd_val, abs=1e-9)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.mark.parametrize("n_qubits", [6, 10, 15])
|
| def test_expand_nonlocal_2q_positions_matches_real_dispatch_sequence(n_qubits):
|
| """Exhaustive: every (q1, q2) pair for this n_qubits must produce the
|
| exact same sequence of adjacent-pair apply_gate_2q calls the real
|
| runtime SWAP-chain dispatch produces."""
|
| CNOT = jnp.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]],
|
| dtype=complex).reshape(2, 2, 2, 2)
|
|
|
| def real_sequence(q1, q2):
|
| calls = []
|
| orig_apply_gate_2q = MPSSimulator.apply_gate_2q
|
|
|
| def spy(self, gate_2q, a, b):
|
| calls.append((a, b))
|
| return orig_apply_gate_2q(self, gate_2q, a, b)
|
|
|
| MPSSimulator.apply_gate_2q = spy
|
| try:
|
| mps = MPSSimulator(n_qubits=n_qubits, max_bond=8)
|
| mps._apply_nonlocal_2q(CNOT, q1, q2)
|
| finally:
|
| MPSSimulator.apply_gate_2q = orig_apply_gate_2q
|
| return calls
|
|
|
| for q1 in range(n_qubits):
|
| for q2 in range(n_qubits):
|
| if q1 == q2:
|
| continue
|
| real_seq = real_sequence(q1, q2)
|
| pred_seq, gate_index, needs_transpose = _expand_nonlocal_2q_positions(q1, q2)
|
| assert pred_seq == real_seq, f"(q1={q1}, q2={q2}): predicted {pred_seq} != real {real_seq}"
|
| assert needs_transpose == (q1 > q2)
|
|
|
|
|
| def test_expand_nonlocal_2q_positions_replay_matches_final_state():
|
| """End-to-end, not just index bookkeeping: replaying the predicted
|
| sequence by hand (SWAP at every position except gate_index, the real
|
| gate -- transposed if needs_transpose -- at gate_index) through the
|
| existing eager apply_gate_2q must produce the identical final quantum
|
| state as calling apply_gate_2q(gate, q1, q2) directly (which routes
|
| through the real runtime _apply_nonlocal_2q dispatch internally)."""
|
| n_qubits = 8
|
| swap = jnp.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]],
|
| dtype=complex).reshape(2, 2, 2, 2)
|
| CNOT = jnp.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]],
|
| dtype=complex).reshape(2, 2, 2, 2)
|
|
|
| for q1, q2 in [(0, 3), (3, 0), (2, 7), (7, 2), (1, 6)]:
|
|
|
| mps_real = MPSSimulator(n_qubits=n_qubits, max_bond=16)
|
| mps_real.apply_gate_1q(H_GATE, 0)
|
| mps_real.apply_gate_1q(H_GATE, 4)
|
| mps_real.apply_gate_2q(CNOT, q1, q2)
|
| sv_real = np.asarray(mps_real.contract_to_statevector())
|
|
|
|
|
|
|
| mps_replay = MPSSimulator(n_qubits=n_qubits, max_bond=16)
|
| mps_replay.apply_gate_1q(H_GATE, 0)
|
| mps_replay.apply_gate_1q(H_GATE, 4)
|
| seq, gate_index, needs_transpose = _expand_nonlocal_2q_positions(q1, q2)
|
| gate_to_apply = jnp.transpose(CNOT, (1, 0, 3, 2)) if needs_transpose else CNOT
|
| for i, (a, b) in enumerate(seq):
|
| mps_replay.apply_gate_2q(gate_to_apply if i == gate_index else swap, a, b)
|
| sv_replay = np.asarray(mps_replay.contract_to_statevector())
|
|
|
| fidelity = np.abs(np.vdot(sv_real, sv_replay)) ** 2
|
| assert fidelity == pytest.approx(1.0, abs=1e-9), f"(q1={q1}, q2={q2}): fidelity={fidelity}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def test_run_circuit_jit_bell_state():
|
| mps = MPSSimulator(n_qubits=2, max_bond=8)
|
| mps.run_circuit_jit([["h", 0], ["cx", 0, 1]])
|
| prob = np.abs(np.asarray(mps.contract_to_statevector())) ** 2
|
| assert prob[0b00] == pytest.approx(0.5, abs=1e-9)
|
| assert prob[0b11] == pytest.approx(0.5, abs=1e-9)
|
|
|
|
|
| def test_run_circuit_jit_ghz_chain():
|
| n = 6
|
| ops = [["h", 0]] + [["cx", q, q + 1] for q in range(n - 1)]
|
| mps = MPSSimulator(n_qubits=n, max_bond=8)
|
| mps.run_circuit_jit(ops)
|
| prob = np.abs(np.asarray(mps.contract_to_statevector())) ** 2
|
| assert prob[0] == pytest.approx(0.5, abs=1e-9)
|
| assert prob[2 ** n - 1] == pytest.approx(0.5, abs=1e-9)
|
|
|
|
|
| def test_run_circuit_jit_nonlocal_ctrl_greater_than_tgt_ghz():
|
|
|
|
|
|
|
| n = 4
|
| mps = MPSSimulator(n_qubits=n, max_bond=16)
|
| mps.run_circuit_jit([["h", 0], ["cx", 0, 3], ["cx", 3, 1], ["cx", 1, 2]])
|
| sv = np.asarray(mps.contract_to_statevector())
|
| exact = np.zeros(2 ** n, dtype=complex)
|
| exact[0] = exact[-1] = 1 / np.sqrt(2)
|
| fidelity = np.abs(np.vdot(sv, exact)) ** 2
|
| assert fidelity == pytest.approx(1.0, abs=1e-9)
|
|
|
|
|
| def test_run_circuit_jit_toffoli_matches_classical_truth_table():
|
| mps = MPSSimulator(n_qubits=3, max_bond=8)
|
| mps.run_circuit_jit([["x", 0], ["x", 1], ["ccx", 0, 1, 2]])
|
|
|
|
|
| prob = np.abs(np.asarray(mps.contract_to_statevector())) ** 2
|
| assert prob[0b111] == pytest.approx(1.0, abs=1e-6)
|
|
|
|
|
| @pytest.mark.parametrize("n_qubits,max_bond,jsd_budget,seed,layers", [
|
| (12, 64, 1e-6, 5, 4),
|
| (16, 32, 1e-5, 7, 3),
|
| ])
|
| def test_run_circuit_jit_matches_dense_simulator(n_qubits, max_bond, jsd_budget, seed, layers):
|
| rng = np.random.default_rng(seed)
|
| ops = [["h", q] for q in range(n_qubits)]
|
| for _ in range(layers):
|
| for q in range(0, n_qubits - 1, 2):
|
| ops.append(["cx", q + 1, q])
|
| for q in range(n_qubits):
|
| ops.append(["rz", q, float(rng.uniform(0.1, 1.5))])
|
| for q in range(1, n_qubits - 1, 2):
|
| ops.append(["cx", q + 1, q])
|
|
|
| sim_dense = de.DenseSVSimulator(n_qubits=n_qubits, use_gpu=False, use_float32=False)
|
| sim_dense.run_circuit_jit_beast_mode(ops)
|
| prob_dense = np.array(sim_dense.get_probabilities())
|
|
|
| mps = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| mps.run_circuit_jit(ops)
|
| prob_mps = np.abs(np.asarray(mps.contract_to_statevector())) ** 2
|
|
|
| tvd = 0.5 * np.sum(np.abs(prob_dense - prob_mps))
|
| assert tvd < 1e-6, f"TVD={tvd} -- run_circuit_jit diverged from DenseSVSimulator"
|
|
|
|
|
| def test_run_circuit_jit_matches_eager_even_under_budget_violation():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| n_qubits, max_bond, jsd_budget, seed, layers = 10, 8, 1e-3, 6, 5
|
| rng = np.random.default_rng(seed)
|
| ops = [["h", q] for q in range(n_qubits)]
|
| for _ in range(layers):
|
| for q in range(0, n_qubits - 1, 2):
|
| ops.append(["cx", q + 1, q])
|
| for q in range(n_qubits):
|
| ops.append(["rz", q, float(rng.uniform(0.1, 1.5))])
|
| for q in range(1, n_qubits - 1, 2):
|
| ops.append(["cx", q + 1, q])
|
|
|
| mps_eager = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| rz_cache = {}
|
| for op in ops:
|
| if op[0] == "h":
|
| mps_eager.apply_gate_1q(H_GATE, op[1])
|
| elif op[0] == "cx":
|
| mps_eager.apply_cx(op[1], op[2])
|
| elif op[0] == "rz":
|
| theta = op[2]
|
| if theta not in rz_cache:
|
| rz_cache[theta] = jnp.array(
|
| [[jnp.exp(-1j * theta / 2), 0], [0, jnp.exp(1j * theta / 2)]], dtype=complex)
|
| mps_eager.apply_gate_1q(rz_cache[theta], op[1])
|
| assert mps_eager.budget_violations > 0, "test setup should genuinely stress max_bond"
|
| sv_eager = np.asarray(mps_eager.contract_to_statevector())
|
|
|
| mps_fused = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| mps_fused.run_circuit_jit(ops)
|
| sv_fused = np.asarray(mps_fused.contract_to_statevector())
|
|
|
| fidelity = np.abs(np.vdot(sv_eager, sv_fused)) ** 2
|
| assert fidelity == pytest.approx(1.0, abs=1e-9)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def test_run_circuit_jit_bookkeeping_matches_eager():
|
| n_qubits, max_bond, jsd_budget, seed, layers = 10, 8, 1e-3, 6, 5
|
| rng = np.random.default_rng(seed)
|
| ops = [["h", q] for q in range(n_qubits)]
|
| for _ in range(layers):
|
| for q in range(0, n_qubits - 1, 2):
|
| ops.append(["cx", q + 1, q])
|
| for q in range(n_qubits):
|
| ops.append(["rz", q, float(rng.uniform(0.1, 1.5))])
|
| for q in range(1, n_qubits - 1, 2):
|
| ops.append(["cx", q + 1, q])
|
|
|
| mps_eager = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| rz_cache = {}
|
| for op in ops:
|
| if op[0] == "h":
|
| mps_eager.apply_gate_1q(H_GATE, op[1])
|
| elif op[0] == "cx":
|
| mps_eager.apply_cx(op[1], op[2])
|
| elif op[0] == "rz":
|
| theta = op[2]
|
| if theta not in rz_cache:
|
| rz_cache[theta] = jnp.array(
|
| [[jnp.exp(-1j * theta / 2), 0], [0, jnp.exp(1j * theta / 2)]], dtype=complex)
|
| mps_eager.apply_gate_1q(rz_cache[theta], op[1])
|
|
|
| mps_fused = MPSSimulator(n_qubits=n_qubits, max_bond=max_bond, jsd_budget=jsd_budget)
|
| mps_fused.run_circuit_jit(ops)
|
|
|
| assert mps_fused._bond_history == mps_eager._bond_history
|
| assert mps_fused.budget_violations == mps_eager.budget_violations
|
| assert len(mps_fused.jsd_per_bond) == len(mps_eager.jsd_per_bond)
|
| for fused_v, eager_v in zip(mps_fused.jsd_per_bond, mps_eager.jsd_per_bond):
|
| assert fused_v == pytest.approx(eager_v, abs=1e-9)
|
| for fused_v, eager_v in zip(mps_fused.truncation_errors, mps_eager.truncation_errors):
|
| assert fused_v == pytest.approx(eager_v, abs=1e-6)
|
| assert np.max(np.abs(mps_fused.entanglement_entropy - mps_eager.entanglement_entropy)) < 1e-9
|
| assert mps_eager.budget_violations > 0, "test setup should genuinely stress max_bond"
|
|
|
|
|
| @pytest.mark.parametrize("use_float64", [False, True])
|
| def test_run_circuit_jit_both_dtypes(use_float64):
|
| previous = jax.config.jax_enable_x64
|
| jax.config.update("jax_enable_x64", use_float64)
|
| try:
|
| mps = MPSSimulator(n_qubits=4, max_bond=8)
|
| mps.run_circuit_jit([["h", 0], ["cx", 0, 3], ["cx", 3, 1], ["cx", 1, 2]])
|
| sv = np.asarray(mps.contract_to_statevector())
|
| exact = np.zeros(16, dtype=complex)
|
| exact[0] = exact[-1] = 1 / np.sqrt(2)
|
| fidelity = np.abs(np.vdot(sv, exact)) ** 2
|
| assert fidelity == pytest.approx(1.0, abs=1e-6)
|
| finally:
|
| jax.config.update("jax_enable_x64", previous)
|
|
|
|
|
| def test_run_circuit_jit_rejects_unknown_gate():
|
| mps = MPSSimulator(n_qubits=2, max_bond=4)
|
| with pytest.raises(ValueError):
|
| mps.run_circuit_jit([["not_a_real_gate", 0]])
|
|
|
|
|