Skip to content

Added random single-qubit CUE and Clifford gates #6670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 105 additions & 42 deletions cirq-core/cirq/transformers/randomized_measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,97 +13,160 @@
# limitations under the License.

from collections.abc import Sequence
from typing import Any, Literal
from typing import Any

import cirq
import numpy as np
from cirq.ops import SingleQubitCliffordGate
from cirq.transformers import transformer_api


@transformer_api.transformer
class RandomizedMeasurements:
"""A transformer that appends a moment of random rotations to map qubits to
random pauli bases."""
"""A transformer that appends a moment of random rotations from a given unitary ensemble (pauli,
clifford, cue)"""

def __init__(self, subsystem: Sequence[int] | None = None):
"""Class structure for performing and analyzing a general randomized measurement protocol.
For more details on the randomized measurement toolbox see https://arxiv.org/abs/2203.11374

Args:
subsystem: The specific subsystem (e.g qubit index) to measure in random basis
rest of the qubits are measured in the computational basis
"""
self.subsystem = subsystem

def __call__(
self,
circuit: 'cirq.AbstractCircuit',
circuit: "cirq.AbstractCircuit",
unitary_ensemble: str = "pauli",
rng: np.random.Generator | None = None,
*,
context: transformer_api.TransformerContext | None = None,
):
) -> "cirq.Circuit":
"""Apply the transformer to the given circuit. Given an input circuit returns
a list of circuits with the pre-measurement unitaries. If no arguments are specified,
it will default to computing the entropy of the entire circuit.
a new circuit with the pre-measurement unitaries and measurements gates added.
to the qubits in the subsystem provided.If no subsystem is specified in the
construction of this classit defaults to measuring all the qubits in the
randomized bases.

Args:
circuit: The circuit to add randomized measurements to.
rng: Random number generator.
unitary_ensemble: Choice of unitary ensemble (pauli/clifford/cue)
context: Not used; to satisfy transformer API.

Returns:
List of circuits with pre-measurement unitaries and measurements added
A circuit with pre-measurement unitaries and measurements added
"""

if rng is None:
rng = np.random.default_rng()

qubits = sorted(circuit.all_qubits())
num_qubits = len(qubits)
all_qubits = sorted(circuit.all_qubits())
if self.subsystem is None:
subsys_qubits = all_qubits
else:
subsys_qubits = [all_qubits[s] for s in self.subsystem]

pre_measurement_unitaries_list = self._generate_unitaries_list(rng, num_qubits)
pre_measurement_moment = self.unitaries_to_moment(pre_measurement_unitaries_list, qubits)
pre_measurement_moment = self.random_single_qubit_unitary_moment(
unitary_ensemble, subsys_qubits, rng
)

return cirq.Circuit.from_moments(
*circuit.moments, pre_measurement_moment, cirq.M(*qubits, key='m')
*circuit.moments, pre_measurement_moment, cirq.M(*subsys_qubits, key="m")
)

def _generate_unitaries_list(self, rng: np.random.Generator, num_qubits: int) -> Sequence[Any]:
"""Generates a list of pre-measurement unitaries."""

pauli_strings = rng.choice(["X", "Y", "Z"], size=num_qubits)

if self.subsystem is not None:
for i in range(pauli_strings.shape[0]):
if i not in self.subsystem:
pauli_strings[i] = np.array("Z")

return pauli_strings.tolist()

def unitaries_to_moment(
self, unitaries: Sequence[Literal["X", "Y", "Z"]], qubits: Sequence[Any]
) -> 'cirq.Moment':
def random_single_qubit_unitary_moment(
self, unitary_ensemble: str, qubits: Sequence[Any], rng: np.random.Generator | None = None
) -> "cirq.Moment":
"""Outputs the cirq moment associated with the pre-measurement rotations.

Args:
unitaries: List of pre-measurement unitaries
unitary_ensemble: clifford, Pauli, cue
qubits: List of qubits

Returns: The cirq moment associated with the pre-measurement rotations
Returns:
The cirq moment associated with the pre-measurement rotations
"""

if unitary_ensemble == "pauli":
unitaries = [_pauli_basis_rotation(rng) for _ in range(len(qubits))]

elif unitary_ensemble == "clifford":
unitaries = [_single_qubit_clifford(rng) for _ in range(len(qubits))]

elif unitary_ensemble == "cue":
unitaries = [_single_qubit_cue(rng) for _ in range(len(qubits))]

else:
raise ValueError("Only pauli, clifford and cue unitaries are available")

op_list: list[cirq.Operation] = []
for idx, pauli in enumerate(unitaries):
op_list.append(_pauli_basis_rotation(pauli).on(qubits[idx]))

for idx, unitary in enumerate(unitaries):
op_list.append(unitary.on(qubits[idx]))

return cirq.Moment.from_ops(*op_list)


def _pauli_basis_rotation(basis: Literal["X", "Y", "Z"]) -> 'cirq.Gate':
"""Given a measurement basis returns the associated rotation.
def _pauli_basis_rotation(rng: np.random.Generator | None = None) -> "cirq.Gate":
"""Randomly generate a Pauli basis rotation.

Args:
basis: Measurement basis
Returns: The cirq gate for associated with measurement basis
rng: numpy random number generator

Returns:
cirq gate
"""
if basis == "X":
return cirq.Ry(rads=-np.pi / 2)
elif basis == "Y":
return cirq.Rx(rads=np.pi / 2)
elif basis == "Z":
return cirq.I
if rng is None:
rng = np.random.default_rng()
basis_idx = rng.choice(np.arange(3))

if basis_idx == 0:
gate: "cirq.Gate" = cirq.Ry(rads=-np.pi / 2)
elif basis_idx == 1:
gate = cirq.Rx(rads=np.pi / 2)
else:
gate = cirq.I
return gate


def _single_qubit_clifford(rng: np.random.Generator | None = None) -> "cirq.Gate":
"""Randomly generate a single-qubit Clifford rotation.

Args:
rng: numpy random number generator

Returns:
cirq gate
"""
if rng is None:
rng = np.random.default_rng()

# there are 24 distinct single-qubit Clifford gates
clifford_idx = rng.choice(np.arange(24))

return SingleQubitCliffordGate.to_phased_xz_gate(
SingleQubitCliffordGate.all_single_qubit_cliffords[clifford_idx]
)


def _single_qubit_cue(rng: np.random.Generator | None = None) -> "cirq.Gate":
"""Randomly generate a CUE gate.

Args:
rng: numpy random number generator

Returns:
cirq gate
"""
if rng is None:
rng = np.random.default_rng()

# phasedxz parameters are distinct between -1 and +1
x_exponent, z_exponent, axis_phase_exponent = 1 - 2 * rng.random(size=3)

return cirq.PhasedXZGate(
x_exponent=x_exponent, z_exponent=z_exponent, axis_phase_exponent=axis_phase_exponent
)
24 changes: 13 additions & 11 deletions cirq-core/cirq/transformers/randomized_measurements_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@
def test_randomized_measurements_appends_two_moments_on_returned_circuit():
# Create a 4-qubit circuit
q0, q1, q2, q3 = cirq.LineQubit.range(4)
circuit = cirq.Circuit([cirq.H(q0), cirq.CNOT(q0, q1), cirq.CNOT(q1, q2), cirq.CNOT(q2, q3)])
num_moments_pre = len(circuit.moments)
circuit_pre = cirq.Circuit(
[cirq.H(q0), cirq.CNOT(q0, q1), cirq.CNOT(q1, q2), cirq.CNOT(q2, q3)]
)
num_moments_pre = len(circuit_pre.moments)

# Append randomized measurements to subsystem
circuit = rand_meas.RandomizedMeasurements()(circuit)

num_moments_post = len(circuit.moments)
assert num_moments_post == num_moments_pre + 2
unitary_ensembles = ['pauli', 'clifford', 'cue']
for u in unitary_ensembles:
circuit_post = rand_meas.RandomizedMeasurements()(circuit_pre, unitary_ensemble=u)
num_moments_post = len(circuit_post.moments)
assert num_moments_post == num_moments_pre + 2


def test_append_randomized_measurements_leaves_qubits_not_in_specified_subsystem_unchanged():
Expand All @@ -36,10 +39,9 @@ def test_append_randomized_measurements_leaves_qubits_not_in_specified_subsystem

# Append randomized measurements to subsystem
circuit = rand_meas.RandomizedMeasurements(subsystem=(0, 1))(circuit)

# assert latter subsystems were not changed.
assert circuit.operation_at(q2, 4) == cirq.I(q2)
assert circuit.operation_at(q3, 4) == cirq.I(q3)
assert circuit.operation_at(q2, 4) is None
assert circuit.operation_at(q3, 4) is None


def test_append_randomized_measurements_leaves_qubits_not_in_noncontinuous_subsystem_unchanged():
Expand All @@ -51,5 +53,5 @@ def test_append_randomized_measurements_leaves_qubits_not_in_noncontinuous_subsy
circuit = rand_meas.RandomizedMeasurements(subsystem=(0, 2))(circuit)

# assert latter subsystems were not changed.
assert circuit.operation_at(q1, 4) == cirq.I(q1)
assert circuit.operation_at(q3, 4) == cirq.I(q3)
assert circuit.operation_at(q1, 4) is None
assert circuit.operation_at(q3, 4) is None
Loading