import math import numpy as np from qibo import Circuit, gates, hamiltonians from qibo.symbols import Symbol, X, Y, Z from qibotn.benchmark_cases import exact_pauli_sum from qibotn.backends.vidal import ( VidalBackend, _can_route_non_adjacent, _unsupported_reason, _operator_terms_to_mpo, _symbolic_hamiltonian_to_operator_terms, ) from qibotn.backends.vidal_tebd import ( VidalTEBDExecutor, _route_non_adjacent_gates, _gate_sites, ) def build_local_circuit(nqubits=8, nlayers=3, seed=42): rng = np.random.default_rng(seed) circuit = Circuit(nqubits) for layer in range(nlayers): for q in range(nqubits): circuit.add(gates.RY(q, theta=rng.uniform(-math.pi, math.pi))) circuit.add(gates.RZ(q, theta=rng.uniform(-math.pi, math.pi))) for q in range(layer % 2, nqubits - 1, 2): circuit.add(gates.CNOT(q, q + 1)) return circuit def test_vidal_backend_expectation_matches_statevector(): circuit = build_local_circuit() observable = hamiltonians.SymbolicHamiltonian( form=0.5 * X(0) * Z(1) + 0.25 * Y(2) * Y(3) - 0.7 * Z(7) ) exact = observable.expectation_from_state(circuit().state(numpy=True)) backend = VidalBackend() backend.configure_tn_simulation(max_bond_dimension=128, tensor_module="torch") value = backend.expectation(circuit, observable) np.testing.assert_allclose(value, exact, atol=1e-12) def test_vidal_backend_accepts_unlimited_bond_and_no_cutoff(): circuit = build_local_circuit(nqubits=6, nlayers=2) observable = hamiltonians.SymbolicHamiltonian( form=0.5 * X(0) * Z(1) - 0.7 * Z(5) ) exact = observable.expectation_from_state(circuit().state(numpy=True)) backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=None, cut_ratio=None, tensor_module="torch", fallback=False, ) value = backend.expectation(circuit, observable, preprocess=False) np.testing.assert_allclose(value, exact, atol=1e-12) def test_vidal_backend_fallback_for_non_adjacent_gate(): """compile_circuit=False (default) → falls back to qmatchatea for non-adjacent.""" circuit = Circuit(4) circuit.add(gates.H(0)) circuit.add(gates.CNOT(0, 3)) observable = hamiltonians.SymbolicHamiltonian(form=Z(0) * Z(3)) backend = VidalBackend() backend.configure_tn_simulation(max_bond_dimension=32, tensor_module="torch") value = backend.expectation(circuit, observable) exact = observable.expectation_from_state(circuit().state(numpy=True)) np.testing.assert_allclose(value, exact, atol=1e-12) def test_vidal_backend_routes_non_adjacent_with_compile(): """Non-adjacent gate with compile_circuit=True goes through Vidal SWAP routing.""" circuit = Circuit(4) circuit.add(gates.H(0)) circuit.add(gates.CNOT(0, 3)) observable = hamiltonians.SymbolicHamiltonian(form=Z(0) * Z(3)) backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=32, tensor_module="torch", compile_circuit=True, ) value = backend.expectation(circuit, observable) exact = observable.expectation_from_state(circuit().state(numpy=True)) np.testing.assert_allclose(value, exact, atol=1e-12) def test_can_route_non_adjacent(): """_can_route_non_adjacent correctly identifies routable circuits.""" circuit = Circuit(4) circuit.add(gates.H(0)) circuit.add(gates.CNOT(0, 3)) assert _can_route_non_adjacent(circuit) circuit.add(gates.CNOT(0, 1)) assert _can_route_non_adjacent(circuit) def test_cannot_route_multi_qubit(): """Circuits with 3+ qubit gates cannot be routed.""" circuit = Circuit(3) circuit.add(gates.TOFFOLI(0, 1, 2)) assert not _can_route_non_adjacent(circuit) def test_routing_preserves_adjacent_gates(): """_route_non_adjacent_gates leaves adjacent gates unchanged.""" circuit = build_local_circuit(nqubits=4, nlayers=2) original = list(circuit.queue) routed = _route_non_adjacent_gates(original, 4) # Count 2Q gates — should be more due to inserted SWAPs, so just # check that all 2-site gates ARE adjacent. for gate in routed: sites = _gate_sites(gate) if len(sites) == 2: diff = abs(sites[0] - sites[1]) assert diff == 1, f"Non-adjacent gate after routing: {gate.name} on {sites}" def test_routing_non_adjacent_cnot(): """Manually verify SWAP+CNOT+unSWAP for CNOT(0,3).""" circuit = Circuit(4) circuit.add(gates.H(0)) circuit.add(gates.H(3)) circuit.add(gates.CNOT(0, 3)) routed = _route_non_adjacent_gates(list(circuit.queue), 4) # Expected: H(0), H(3), SWAP(2,3), SWAP(1,2), routed CNOT on (0,1), SWAP(1,2), SWAP(2,3) names = [getattr(g, "name", g.__class__.__name__) for g in routed] assert names == ["h", "h", "swap", "swap", "routed_two_qubit", "swap", "swap"], f"Got {names}" # Verify expectation through full pipeline observable = hamiltonians.SymbolicHamiltonian(form=Z(0) * Z(3)) exact = observable.expectation_from_state(circuit().state(numpy=True)) backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=32, tensor_module="torch", compile_circuit=True, ) value = backend.expectation(circuit, observable) np.testing.assert_allclose(value, exact, atol=1e-12) def test_routing_preserves_reversed_non_adjacent_gate_order(): circuit = Circuit(6) circuit.add(gates.X(5)) circuit.add(gates.H(0)) circuit.add(gates.CNOT(5, 0)) observable = hamiltonians.SymbolicHamiltonian(form=X(0) + Z(5) + Z(0) * Z(5)) exact = observable.expectation_from_state(circuit().state(numpy=True)) backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=64, tensor_module="torch", compile_circuit=True, fallback=False, ) value = backend.expectation(circuit, observable, preprocess=False) np.testing.assert_allclose(value, exact, atol=1e-12) def test_vidal_backend_preprocesses_non_adjacent_circuit(): circuit = Circuit(4) circuit.add(gates.H(0)) circuit.add(gates.CNOT(0, 3)) observable = hamiltonians.SymbolicHamiltonian(form=Z(0) * Z(3)) exact = observable.expectation_from_state(circuit().state(numpy=True)) backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=64, tensor_module="torch", compile_circuit=True, fallback=False, ) value = backend.expectation(circuit, observable, preprocess=True) np.testing.assert_allclose(value, exact, atol=1e-12) def test_vidal_backend_preprocesses_toffoli_locally(): circuit = Circuit(4) circuit.add(gates.H(0)) circuit.add(gates.H(1)) circuit.add(gates.TOFFOLI(0, 1, 3)) observable = hamiltonians.SymbolicHamiltonian(form=Z(0) * Z(3)) exact = observable.expectation_from_state(circuit().state(numpy=True)) backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=128, tensor_module="torch", compile_circuit=True, fallback=False, ) value = backend.expectation(circuit, observable, preprocess=True) np.testing.assert_allclose(value, exact, atol=1e-12) def test_vidal_expectation_preserves_complex_coefficients(): circuit = Circuit(1) observable = hamiltonians.SymbolicHamiltonian(form=(1.0 + 2.0j) * Z(0)) backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=8, tensor_module="torch", fallback=False, ) value = backend.expectation(circuit, observable, preprocess=False) np.testing.assert_allclose(value, 1.0 + 2.0j, atol=1e-12) def test_vidal_expectation_supports_custom_local_symbols(): circuit = build_local_circuit(nqubits=4, nlayers=2) a0 = Symbol(0, np.array([[0.2, 1.0], [1.0, -0.3]], dtype=complex), name="A") b2 = Symbol(2, np.array([[0.7, -0.4j], [0.4j, 0.1]], dtype=complex), name="B") a3 = Symbol(3, np.array([[0.5, 0.2], [0.2, -0.8]], dtype=complex), name="A") observable = hamiltonians.SymbolicHamiltonian(form=0.7 * a0 * b2 - 0.4 * a3) exact = observable.expectation_from_state(circuit().state(numpy=True)) backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=64, tensor_module="torch", fallback=False, ) value = backend.expectation(circuit, observable, preprocess=False) np.testing.assert_allclose(value, exact, atol=1e-12) def test_vidal_executor_mpo_expectation_matches_pauli_sum(): circuit = build_local_circuit(nqubits=4, nlayers=2) executor = VidalTEBDExecutor( nqubits=circuit.nqubits, max_bond=64, tensor_module="torch", ) executor.run_circuit(circuit) x = np.array([[0, 1], [1, 0]], dtype=complex) z = np.array([[1, 0], [0, -1]], dtype=complex) i2 = np.eye(2, dtype=complex) mpo = [ x.reshape(1, 2, 2, 1), z.reshape(1, 2, 2, 1), i2.reshape(1, 2, 2, 1), i2.reshape(1, 2, 2, 1), ] mpo_value = executor.expectation_mpo(mpo) pauli_value = executor.expectation_pauli_sum([(1.0, (("X", 0), ("Z", 1)))]) np.testing.assert_allclose(mpo_value, pauli_value, atol=1e-12) def test_vidal_backend_accepts_mpo_observable_dict(): circuit = build_local_circuit(nqubits=4, nlayers=2) x = np.array([[0, 1], [1, 0]], dtype=complex) z = np.array([[1, 0], [0, -1]], dtype=complex) i2 = np.eye(2, dtype=complex) mpo = [ x.reshape(1, 2, 2, 1), z.reshape(1, 2, 2, 1), i2.reshape(1, 2, 2, 1), i2.reshape(1, 2, 2, 1), ] exact = exact_pauli_sum(circuit, [(1.0, (("X", 0), ("Z", 1)))], 4) backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=64, tensor_module="torch", fallback=False, ) value = backend.expectation(circuit, {"mpo_tensors": mpo}, preprocess=False) np.testing.assert_allclose(value, exact, atol=1e-12) def test_vidal_symbolic_hamiltonian_auto_mpo_matches_operator_sum(): circuit = build_local_circuit(nqubits=5, nlayers=2) observable = hamiltonians.SymbolicHamiltonian( form=0.3 * X(0) * Z(1) - 0.2j * Y(2) + 0.7 * Z(3) * X(4) ) executor = VidalTEBDExecutor( nqubits=circuit.nqubits, max_bond=64, tensor_module="torch", ) executor.run_circuit(circuit) terms = _symbolic_hamiltonian_to_operator_terms(observable) term_value = executor.expectation_operator_sum(terms) mpo_value = executor.expectation_mpo(_operator_terms_to_mpo(terms, circuit.nqubits)) np.testing.assert_allclose(mpo_value, term_value, atol=1e-12) def test_vidal_backend_accepts_dense_two_qubit_observable(): circuit = Circuit(2) circuit.add(gates.H(0)) circuit.add(gates.CNOT(0, 1)) bell = np.zeros((4, 4), dtype=complex) bell[0, 0] = bell[0, 3] = bell[3, 0] = bell[3, 3] = 0.5 observable = {"matrix": bell, "qubits": [0, 1]} backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=16, tensor_module="torch", fallback=False, ) value = backend.expectation(circuit, observable, preprocess=False) np.testing.assert_allclose(value, 1.0, atol=1e-12) def test_vidal_backend_dense_observable_preserves_complex_value(): circuit = Circuit(2) circuit.add(gates.H(0)) circuit.add(gates.H(1)) op = np.zeros((4, 4), dtype=complex) op[0, 3] = 1.0 observable = {"coefficient": 1.0j, "matrix": op, "qubits": [0, 1]} backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=16, tensor_module="torch", fallback=False, ) value = backend.expectation(circuit, observable, preprocess=False) np.testing.assert_allclose(value, 0.25j, atol=1e-12) def test_truncation_error_no_truncation(): """With large bond, truncation error should be essentially zero.""" circuit = build_local_circuit(nqubits=6, nlayers=2) observable = hamiltonians.SymbolicHamiltonian(form=0.5 * X(0) * Z(1)) backend = VidalBackend() backend.configure_tn_simulation(max_bond_dimension=256, tensor_module="torch") value = backend.expectation(circuit, observable) _ = value # ensure computation runs assert backend.last_truncation_error < 1e-14, ( f"Expected near-zero truncation error, got {backend.last_truncation_error}" ) assert backend.last_max_truncation_error < 1e-14, ( "Expected near-zero max truncation error, got " f"{backend.last_max_truncation_error}" ) def test_vidal_backend_matches_statevector_multiterm(): """Multi-term observable with non-adjacent gates, compile_circuit=True.""" circuit = Circuit(5) for q in range(5): circuit.add(gates.RY(q, theta=0.7)) circuit.add(gates.RZ(q, theta=0.3)) circuit.add(gates.CNOT(0, 2)) circuit.add(gates.CNOT(1, 4)) observable = hamiltonians.SymbolicHamiltonian( form=(0.3 * X(0) * Z(2) + 0.7 * Y(1) * Y(4) - 0.5 * Z(0) * X(4)) ) exact_state = circuit().state(numpy=True) exact = observable.expectation_from_state(exact_state) backend = VidalBackend() backend.configure_tn_simulation( max_bond_dimension=64, tensor_module="torch", compile_circuit=True, ) value = backend.expectation(circuit, observable) np.testing.assert_allclose(value, exact, atol=1e-10)