|
13 | 13 | # limitations under the License.
|
14 | 14 |
|
15 | 15 | from collections.abc import Sequence
|
16 |
| -from typing import Any, Literal |
| 16 | +from typing import Any |
17 | 17 |
|
18 | 18 | import cirq
|
19 | 19 | import numpy as np
|
| 20 | +from cirq.ops import SingleQubitCliffordGate |
20 | 21 | from cirq.transformers import transformer_api
|
21 | 22 |
|
22 | 23 |
|
23 | 24 | @transformer_api.transformer
|
24 | 25 | class RandomizedMeasurements:
|
25 |
| - """A transformer that appends a moment of random rotations to map qubits to |
26 |
| - random pauli bases.""" |
| 26 | + """A transformer that appends a moment of random rotations from a given unitary ensemble (pauli, |
| 27 | + clifford, cue)""" |
27 | 28 |
|
28 | 29 | def __init__(self, subsystem: Sequence[int] | None = None):
|
29 | 30 | """Class structure for performing and analyzing a general randomized measurement protocol.
|
30 | 31 | For more details on the randomized measurement toolbox see https://arxiv.org/abs/2203.11374
|
31 | 32 |
|
32 | 33 | Args:
|
33 | 34 | subsystem: The specific subsystem (e.g qubit index) to measure in random basis
|
| 35 | + rest of the qubits are measured in the computational basis |
34 | 36 | """
|
35 | 37 | self.subsystem = subsystem
|
36 | 38 |
|
37 | 39 | def __call__(
|
38 | 40 | self,
|
39 |
| - circuit: 'cirq.AbstractCircuit', |
| 41 | + circuit: "cirq.AbstractCircuit", |
| 42 | + unitary_ensemble: str = "pauli", |
40 | 43 | rng: np.random.Generator | None = None,
|
41 | 44 | *,
|
42 | 45 | context: transformer_api.TransformerContext | None = None,
|
43 |
| - ): |
| 46 | + ) -> "cirq.Circuit": |
44 | 47 | """Apply the transformer to the given circuit. Given an input circuit returns
|
45 |
| - a list of circuits with the pre-measurement unitaries. If no arguments are specified, |
46 |
| - it will default to computing the entropy of the entire circuit. |
| 48 | + a new circuit with the pre-measurement unitaries and measurements gates added. |
| 49 | + to the qubits in the subsystem provided.If no subsystem is specified in the |
| 50 | + construction of this class it defaults to measuring all the qubits in the |
| 51 | + randomized bases. |
47 | 52 |
|
48 | 53 | Args:
|
49 | 54 | circuit: The circuit to add randomized measurements to.
|
50 |
| - rng: Random number generator. |
| 55 | + unitary_ensemble: Choice of unitary ensemble (pauli/clifford/cue(circular |
| 56 | + unitary ensemble)) |
51 | 57 | context: Not used; to satisfy transformer API.
|
| 58 | + rng: Random number generator. |
52 | 59 |
|
53 | 60 | Returns:
|
54 |
| - List of circuits with pre-measurement unitaries and measurements added |
| 61 | + A circuit with pre-measurement unitaries and measurements added |
55 | 62 | """
|
| 63 | + |
| 64 | + all_qubits = sorted(circuit.all_qubits()) |
| 65 | + if self.subsystem is None: |
| 66 | + subsystem_qubits = all_qubits |
| 67 | + else: |
| 68 | + subsystem_qubits = [all_qubits[s] for s in self.subsystem] |
56 | 69 | if rng is None:
|
57 | 70 | rng = np.random.default_rng()
|
58 | 71 |
|
59 |
| - qubits = sorted(circuit.all_qubits()) |
60 |
| - num_qubits = len(qubits) |
61 |
| - |
62 |
| - pre_measurement_unitaries_list = self._generate_unitaries_list(rng, num_qubits) |
63 |
| - pre_measurement_moment = self.unitaries_to_moment(pre_measurement_unitaries_list, qubits) |
| 72 | + pre_measurement_moment = self.random_single_qubit_unitary_moment( |
| 73 | + unitary_ensemble, subsystem_qubits, rng |
| 74 | + ) |
64 | 75 |
|
65 | 76 | return cirq.Circuit.from_moments(
|
66 |
| - *circuit.moments, pre_measurement_moment, cirq.M(*qubits, key='m') |
| 77 | + *circuit.moments, pre_measurement_moment, cirq.M(*subsystem_qubits, key="m") |
67 | 78 | )
|
68 | 79 |
|
69 |
| - def _generate_unitaries_list(self, rng: np.random.Generator, num_qubits: int) -> Sequence[Any]: |
70 |
| - """Generates a list of pre-measurement unitaries.""" |
71 |
| - |
72 |
| - pauli_strings = rng.choice(["X", "Y", "Z"], size=num_qubits) |
73 |
| - |
74 |
| - if self.subsystem is not None: |
75 |
| - for i in range(pauli_strings.shape[0]): |
76 |
| - if i not in self.subsystem: |
77 |
| - pauli_strings[i] = np.array("Z") |
78 |
| - |
79 |
| - return pauli_strings.tolist() |
80 |
| - |
81 |
| - def unitaries_to_moment( |
82 |
| - self, unitaries: Sequence[Literal["X", "Y", "Z"]], qubits: Sequence[Any] |
83 |
| - ) -> 'cirq.Moment': |
| 80 | + def random_single_qubit_unitary_moment( |
| 81 | + self, unitary_ensemble: str, qubits: Sequence[Any], rng: np.random.Generator |
| 82 | + ) -> "cirq.Moment": |
84 | 83 | """Outputs the cirq moment associated with the pre-measurement rotations.
|
| 84 | +
|
85 | 85 | Args:
|
86 |
| - unitaries: List of pre-measurement unitaries |
| 86 | + unitary_ensemble: clifford, pauli, cue |
87 | 87 | qubits: List of qubits
|
| 88 | + rng: Random number generator to be used in sampling. |
88 | 89 |
|
89 |
| - Returns: The cirq moment associated with the pre-measurement rotations |
| 90 | + Returns: |
| 91 | + The cirq moment associated with the pre-measurement rotations |
| 92 | +
|
| 93 | + Raises: |
| 94 | + ValueError: When unitary_ensemble is not one of "cue", "pauli" or "clifford" |
90 | 95 | """
|
| 96 | + |
| 97 | + if unitary_ensemble.lower() == "pauli": |
| 98 | + unitaries = [_pauli_basis_rotation(rng) for _ in range(len(qubits))] |
| 99 | + |
| 100 | + elif unitary_ensemble.lower() == "clifford": |
| 101 | + unitaries = [_single_qubit_clifford(rng) for _ in range(len(qubits))] |
| 102 | + |
| 103 | + elif unitary_ensemble.lower() == "cue": |
| 104 | + unitaries = [_single_qubit_cue(rng) for _ in range(len(qubits))] |
| 105 | + |
| 106 | + else: |
| 107 | + raise ValueError("Only pauli, clifford and cue unitaries are available") |
| 108 | + |
91 | 109 | op_list: list[cirq.Operation] = []
|
92 |
| - for idx, pauli in enumerate(unitaries): |
93 |
| - op_list.append(_pauli_basis_rotation(pauli).on(qubits[idx])) |
| 110 | + |
| 111 | + for idx, unitary in enumerate(unitaries): |
| 112 | + op_list.append(unitary.on(qubits[idx])) |
94 | 113 |
|
95 | 114 | return cirq.Moment.from_ops(*op_list)
|
96 | 115 |
|
97 | 116 |
|
98 |
| -def _pauli_basis_rotation(basis: Literal["X", "Y", "Z"]) -> 'cirq.Gate': |
99 |
| - """Given a measurement basis returns the associated rotation. |
| 117 | +def _pauli_basis_rotation(rng: np.random.Generator) -> "cirq.Gate": |
| 118 | + """Randomly generate a Pauli basis rotation. |
| 119 | +
|
100 | 120 | Args:
|
101 |
| - basis: Measurement basis |
102 |
| - Returns: The cirq gate for associated with measurement basis |
| 121 | + rng: Random number generator |
| 122 | +
|
| 123 | + Returns: |
| 124 | + cirq gate |
| 125 | + """ |
| 126 | + basis_idx = rng.choice(np.arange(3)) |
| 127 | + |
| 128 | + if basis_idx == 0: |
| 129 | + gate: "cirq.Gate" = cirq.Ry(rads=-np.pi / 2) |
| 130 | + elif basis_idx == 1: |
| 131 | + gate = cirq.Rx(rads=np.pi / 2) |
| 132 | + else: |
| 133 | + gate = cirq.I |
| 134 | + return gate |
| 135 | + |
| 136 | + |
| 137 | +def _single_qubit_clifford(rng: np.random.Generator) -> "cirq.Gate": |
| 138 | + """Randomly generate a single-qubit Clifford rotation. |
| 139 | +
|
| 140 | + Args: |
| 141 | + rng: Random number generator |
| 142 | +
|
| 143 | + Returns: |
| 144 | + cirq gate |
| 145 | + """ |
| 146 | + |
| 147 | + # there are 24 distinct single-qubit Clifford gates |
| 148 | + clifford_idx = rng.choice(np.arange(24)) |
| 149 | + |
| 150 | + return SingleQubitCliffordGate.to_phased_xz_gate( |
| 151 | + SingleQubitCliffordGate.all_single_qubit_cliffords[clifford_idx] |
| 152 | + ) |
| 153 | + |
| 154 | + |
| 155 | +def _single_qubit_cue(rng: np.random.Generator) -> "cirq.Gate": |
| 156 | + """Randomly generate a CUE gate. |
| 157 | +
|
| 158 | + Args: |
| 159 | + rng: Random number generator |
| 160 | +
|
| 161 | + Returns: |
| 162 | + cirq gate |
103 | 163 | """
|
104 |
| - if basis == "X": |
105 |
| - return cirq.Ry(rads=-np.pi / 2) |
106 |
| - elif basis == "Y": |
107 |
| - return cirq.Rx(rads=np.pi / 2) |
108 |
| - elif basis == "Z": |
109 |
| - return cirq.I |
| 164 | + |
| 165 | + # phasedxz parameters are distinct between -1 and +1 |
| 166 | + x_exponent, z_exponent, axis_phase_exponent = 1 - 2 * rng.random(size=3) |
| 167 | + |
| 168 | + return cirq.PhasedXZGate( |
| 169 | + x_exponent=x_exponent, z_exponent=z_exponent, axis_phase_exponent=axis_phase_exponent |
| 170 | + ) |
0 commit comments