diff --git a/cirq-core/cirq/transformers/randomized_measurements.py b/cirq-core/cirq/transformers/randomized_measurements.py index 30b7f94b91b..ac70d272840 100644 --- a/cirq-core/cirq/transformers/randomized_measurements.py +++ b/cirq-core/cirq/transformers/randomized_measurements.py @@ -13,17 +13,18 @@ # 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. @@ -31,79 +32,139 @@ def __init__(self, subsystem: Sequence[int] | None = None): 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 class it 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(circular + unitary ensemble)) context: Not used; to satisfy transformer API. + rng: Random number generator. Returns: - List of circuits with pre-measurement unitaries and measurements added + A circuit with pre-measurement unitaries and measurements added """ + + all_qubits = sorted(circuit.all_qubits()) + if self.subsystem is None: + subsystem_qubits = all_qubits + else: + subsystem_qubits = [all_qubits[s] for s in self.subsystem] if rng is None: rng = np.random.default_rng() - qubits = sorted(circuit.all_qubits()) - num_qubits = len(qubits) - - 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, subsystem_qubits, rng + ) return cirq.Circuit.from_moments( - *circuit.moments, pre_measurement_moment, cirq.M(*qubits, key='m') + *circuit.moments, pre_measurement_moment, cirq.M(*subsystem_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 + ) -> "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 + rng: Random number generator to be used in sampling. - Returns: The cirq moment associated with the pre-measurement rotations + Returns: + The cirq moment associated with the pre-measurement rotations + + Raises: + ValueError: When unitary_ensemble is not one of "cue", "pauli" or "clifford" """ + + if unitary_ensemble.lower() == "pauli": + unitaries = [_pauli_basis_rotation(rng) for _ in range(len(qubits))] + + elif unitary_ensemble.lower() == "clifford": + unitaries = [_single_qubit_clifford(rng) for _ in range(len(qubits))] + + elif unitary_ensemble.lower() == "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) -> "cirq.Gate": + """Randomly generate a Pauli basis rotation. + Args: - basis: Measurement basis - Returns: The cirq gate for associated with measurement basis + rng: Random number generator + + Returns: + cirq gate + """ + 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) -> "cirq.Gate": + """Randomly generate a single-qubit Clifford rotation. + + Args: + rng: Random number generator + + Returns: + cirq gate + """ + + # 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) -> "cirq.Gate": + """Randomly generate a CUE gate. + + Args: + rng: 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 + + # 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 + ) diff --git a/cirq-core/cirq/transformers/randomized_measurements_test.py b/cirq-core/cirq/transformers/randomized_measurements_test.py index 7ff355eb461..1618ce391c2 100644 --- a/cirq-core/cirq/transformers/randomized_measurements_test.py +++ b/cirq-core/cirq/transformers/randomized_measurements_test.py @@ -14,19 +14,23 @@ import cirq import cirq.transformers.randomized_measurements as rand_meas +import pytest 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(): @@ -36,10 +40,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(): @@ -51,5 +54,14 @@ 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 + + +def test_exception(): + 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)]) + + # Append randomized measurements to subsystem + with pytest.raises(ValueError): + rand_meas.RandomizedMeasurements()(circuit, unitary_ensemble="coe")