|
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