"""Correctness checks for the Vidal/TEBD MPS fast path. The cases here intentionally cover more than the benchmark ring-XZ observable: different nearest-neighbor gate orientations and several Pauli-sum observables. Run serially to compare qibojit/statevector vs Vidal, or under MPI to compare the segmented Vidal executor. """ from __future__ import annotations import argparse import math import time import numpy as np import torch from qibo import Circuit, gates from qibotn.backends.vidal_mpi_segment import SegmentVidalMPIExecutor from qibotn.backends.vidal_tebd import VidalTEBDExecutor def build_circuit(kind, nqubits, nlayers, seed): 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))) if kind == "rx_ry_cz": circuit.add(gates.RX(q, theta=rng.uniform(-math.pi, math.pi))) if kind in ("brickwall", "reversed_cnot"): for q in range(0, nqubits - 1, 2): if kind == "reversed_cnot" and (layer % 2): circuit.add(gates.CNOT(q + 1, q)) else: circuit.add(gates.CNOT(q, q + 1)) for q in range(1, nqubits - 1, 2): if kind == "reversed_cnot" and not (layer % 2): circuit.add(gates.CNOT(q + 1, q)) else: circuit.add(gates.CNOT(q, q + 1)) elif kind == "rx_ry_cz": for q in range(layer % 2, nqubits - 1, 2): circuit.add(gates.CZ(q, q + 1)) else: raise ValueError(f"Unknown circuit kind {kind!r}.") return circuit def observable_terms(kind, nqubits): if kind == "ring_xz": return [ (0.5, (("X", site), ("Z", (site + 1) % nqubits))) for site in range(nqubits) ] if kind == "open_zz": return [ (1.0 / (nqubits - 1), (("Z", site), ("Z", site + 1))) for site in range(nqubits - 1) ] if kind == "mixed_local": terms = [(0.25, (("X", 0),)), (-0.5, (("Z", nqubits - 1),))] terms += [ (0.125, (("Y", site), ("Y", site + 1))) for site in range(0, nqubits - 1, 3) ] return terms raise ValueError(f"Unknown observable kind {kind!r}.") def exact_pauli_sum(circuit, terms, nqubits): state = circuit().state(numpy=True).reshape(-1) indices = np.arange(state.size, dtype=np.int64) value = 0.0 + 0.0j for coeff, ops in terms: flipped = indices.copy() phase = np.ones(state.size, dtype=np.complex128) for name, site in ops: shift = nqubits - 1 - site bit = (indices >> shift) & 1 name = name.upper() if name == "X": flipped ^= 1 << shift elif name == "Y": flipped ^= 1 << shift phase *= 1j * (1 - 2 * bit) elif name == "Z": phase *= 1 - 2 * bit elif name != "I": raise ValueError(f"Unsupported Pauli {name!r}.") value += coeff * np.vdot(state[flipped], phase * state) return float(value.real) def run_vidal(circuit, terms, nqubits, bond, tensor_module): executor = VidalTEBDExecutor( nqubits=nqubits, max_bond=bond, cut_ratio=1e-12, tensor_module=tensor_module, ) executor.run_circuit(circuit) return float(executor.expectation_pauli_sum(terms)) def run_segment_mpi(circuit, terms, nqubits, bond, tensor_module, comm): executor = SegmentVidalMPIExecutor( nqubits=nqubits, max_bond=bond, cut_ratio=1e-12, tensor_module=tensor_module, comm=comm, ) executor.run_circuit(circuit) return executor.expectation_pauli_sum_root(terms) def main(): parser = argparse.ArgumentParser() parser.add_argument("--nqubits", type=int, default=16) parser.add_argument("--nlayers", type=int, default=6) parser.add_argument("--bond", "--bonds", dest="bond", type=int, default=512) parser.add_argument("--seed", type=int, default=42) parser.add_argument("--tensor-module", choices=("torch", "numpy"), default="torch") parser.add_argument("--torch-threads", type=int, default=32) parser.add_argument("--mpi", action="store_true") parser.add_argument( "--circuits", nargs="+", default=("brickwall", "reversed_cnot", "rx_ry_cz"), ) parser.add_argument( "--observables", nargs="+", default=("ring_xz", "open_zz", "mixed_local"), ) args = parser.parse_args() torch.set_num_threads(args.torch_threads) comm = None rank = 0 size = 1 if args.mpi: from mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.Get_rank() size = comm.Get_size() if rank == 0: mode = f"vidal-segment-mpi/{size}" if args.mpi else "vidal" print( f"mode={mode} nqubits={args.nqubits} nlayers={args.nlayers} " f"bond={args.bond} tensor_module={args.tensor_module}" ) print("circuit observable exact value abs_error seconds") for circuit_kind in args.circuits: circuit = build_circuit(circuit_kind, args.nqubits, args.nlayers, args.seed) exact = None if rank == 0: exact_values = { obs: exact_pauli_sum( circuit, observable_terms(obs, args.nqubits), args.nqubits ) for obs in args.observables } else: exact_values = None if comm is not None: exact_values = comm.bcast(exact_values, root=0) for obs_kind in args.observables: terms = observable_terms(obs_kind, args.nqubits) start = time.perf_counter() if args.mpi: value = run_segment_mpi( circuit, terms, args.nqubits, args.bond, args.tensor_module, comm, ) else: value = run_vidal( circuit, terms, args.nqubits, args.bond, args.tensor_module ) if rank != 0: continue elapsed = time.perf_counter() - start exact = exact_values[obs_kind] print( f"{circuit_kind} {obs_kind} {exact:.16e} {value:.16e} " f"{abs(value - exact):.6e} {elapsed:.3f}" ) if __name__ == "__main__": main()