Pauli exponentials

Download Notebook - pauli_exponential.ipynb

  • A Pauli exponential rotates a register about a Pauli string \(P\), such as \(Z_0X_1Y_2\).

  • angle(theta) uses half-turns, so the implemented unitary is

\[ U_P(\theta)=e^{-i\pi\theta P/2} =\cos(\pi\theta/2)I-i\sin(\pi\theta/2)P. \]
  • These rotations form the steps of Trotterized Hamiltonian simulation. For \(H=\sum_j a_jP_j\), the library’s time convention gives

\[ e^{-i\pi tH/2}\approx\left[\prod_j U_{P_j}(ta_j/r)\right]^r. \]
  • This notebook builds a rotation, checks its matrix, and substitutes a different \(R_z\) implementation.

  • Run from a source checkout with development dependencies installed. The repository default is little endian.

Hide code cell source

import numpy as np
import zixy.qubit.pauli as zqp
from scipy.linalg import expm
from guppylang import guppy
from guppylang.std.builtins import array
from guppylang.std.quantum import rz, qubit
from guppylang.std.angles import angle
from guppyalgos.primitives.pauli.pauli_exp import pauli_exp
from guppyalgos.primitives.subroutines.ladders import CXLadderLinear
from guppyalgos.tests.helpers import get_unitary, assert_allclose_ignorephase

1. Build the rotation

  • Describe the operator with zqp.String; use zqp.RealTermSum for a weighted Hamiltonian.

  • pauli_exp builds a guppy function for that string. Its angle is supplied when the function is called.

  • The circuit changes to the Z basis, collects parity with a CX ladder, applies \(R_z\), then reverses the ladder and basis changes.

  • Here we choose CXLadderLinear. Omitting the optional arguments uses CXLadderLog and rz.

n_state_qubits = 3
pauli_string = zqp.String.from_str("Z0 X1 Y2", n_state_qubits)
cx_ladder_method = CXLadderLinear
pauli_gadget = pauli_exp(pauli_string, n_state_qubits, cx_ladder_method, rz)
theta = 0.7


@guppy
def rotation(qreg: array[qubit, n_state_qubits]) -> None:
    pauli_gadget(qreg, angle(theta))

2. Check the full unitary

  • get_unitary simulates each computational-basis input and collects the output columns.

  • Compare with the matrix exponential above, allowing an overall global phase. This checks relative phases as well as probabilities.

guppy_u = get_unitary(rotation, n_state_qubits)
pauli_mat = np.asarray(pauli_string.to_sparse_matrix(True).todense())
u_mat = expm(-0.5j * np.pi * theta * pauli_mat)
assert_allclose_ignorephase(u_mat, guppy_u)
print(f"All {2**n_state_qubits} basis inputs match the Pauli exponential, up to global phase.")
All 8 basis inputs match the Pauli exponential, up to global phase.

3. Swap in a repeat-until-success rotation

  • rz_method accepts another function with the same qubit-and-angle interface. The surrounding Pauli gadget stays unchanged.

  • repeat_until_success_rz uses a resource qubit and measurement feedback. This demonstration uses dummy_theta_resource_state, which prepares its resource with a direct rotation; it does not demonstrate a rotation-synthesis resource saving.

  • Replay three failures followed by success and verify the output for \(|+\rangle^{\otimes3}\):

\[ |\psi_{\mathrm{out}}\rangle=U_P(0.7)|+\rangle^{\otimes3}. \]
from guppyalgos.primitives.rotations import repeat_until_success_rz, dummy_theta_resource_state
from guppyalgos.utils import qarray, transversal
from guppylang.std.debug import state_output
from guppylang.std.quantum import discard_array, h
from selene_sim import QuantumReplay, Quest
rus_rz = repeat_until_success_rz(dummy_theta_resource_state)

rus_gadget = pauli_exp(pauli_string, n_state_qubits, cx_ladder_method, rus_rz)

theta = 0.7

@guppy
def main() -> None:
    qreg = qarray(n_state_qubits)
    transversal(h, qreg)
    rus_gadget(qreg, angle(theta))
    state_output("result_state", qreg)
    discard_array(qreg)

n_repeats = 3
# fail 3 times and then succeed
desired_rus_measurements = [[False] * n_repeats + [True]]

rus_replay_sim = QuantumReplay(simulator=Quest(), measurements=desired_rus_measurements)
em_result = (
    main.emulator(n_state_qubits+1).with_simulator(rus_replay_sim).with_shots(1).run()
)
expected_state = u_mat @ (np.ones(2**n_state_qubits) / np.sqrt(2**n_state_qubits))
for shot_result in em_result.results:
    actual_state = Quest.extract_states_dict(shot_result)["result_state"].get_single_state()
    assert_allclose_ignorephase(expected_state, actual_state)
print("Three forced failures followed by success: output state matches the target.")
Three forced failures followed by success: output state matches the target.