|
| 1 | +# Copyright 2025 The Cirq Developers |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +"""A Multi-Moment Gauge Transformer for the cphase gate.""" |
| 16 | + |
| 17 | +from __future__ import annotations |
| 18 | + |
| 19 | +from typing import cast |
| 20 | + |
| 21 | +import numpy as np |
| 22 | + |
| 23 | +from cirq import circuits, ops |
| 24 | +from cirq.transformers.gauge_compiling.multi_moment_gauge_compiling import ( |
| 25 | + MultiMomentGaugeTransformer, |
| 26 | +) |
| 27 | + |
| 28 | + |
| 29 | +class _PauliAndZPow: |
| 30 | + """In pulling through, one qubit gate can be represented by a Pauli and an Rz gate. |
| 31 | + The order is --Pauli--ZPowGate--. |
| 32 | + """ |
| 33 | + |
| 34 | + pauli: ops.Pauli | ops.IdentityGate = ops.I |
| 35 | + zpow: ops.ZPowGate = ops.ZPowGate(exponent=0) |
| 36 | + |
| 37 | + commuting_gates = {ops.I, ops.Z} # I,Z Commute with ZPowGate and CZPowGate; X,Y anti-commute. |
| 38 | + |
| 39 | + def __init__( |
| 40 | + self, |
| 41 | + pauli: ops.Pauli | ops.IdentityGate = ops.I, |
| 42 | + zpow: ops.ZPowGate = ops.ZPowGate(exponent=0), |
| 43 | + ) -> None: |
| 44 | + self.pauli = pauli |
| 45 | + self.zpow = zpow |
| 46 | + |
| 47 | + def _merge_left_zpow(self, left: ops.ZPowGate): |
| 48 | + """Merges ZPowGate from left.""" |
| 49 | + if self.pauli in self.commuting_gates: |
| 50 | + self.zpow = ops.ZPowGate(exponent=left.exponent + self.zpow.exponent) |
| 51 | + else: |
| 52 | + self.zpow = ops.ZPowGate(exponent=-left.exponent + self.zpow.exponent) |
| 53 | + |
| 54 | + def _merge_right_zpow(self, right: ops.ZPowGate): |
| 55 | + """Merges ZPowGate from right.""" |
| 56 | + self.zpow = ops.ZPowGate(exponent=right.exponent + self.zpow.exponent) |
| 57 | + |
| 58 | + def _merge_left_pauli(self, left: ops.Pauli): |
| 59 | + """Merges --left_pauli--self--.""" |
| 60 | + if self.pauli == ops.I: |
| 61 | + self.pauli = left |
| 62 | + else: |
| 63 | + self.pauli = left.phased_pauli_product(self.pauli)[1] |
| 64 | + |
| 65 | + def _merge_right_pauli(self, right: ops.Pauli): |
| 66 | + """Merges --self--right_pauli--.""" |
| 67 | + if self.pauli == ops.I: |
| 68 | + self.pauli = right |
| 69 | + else: |
| 70 | + self.pauli = right.phased_pauli_product(self.pauli)[1] |
| 71 | + if right not in self.commuting_gates: |
| 72 | + self.zpow = ops.ZPowGate(exponent=-self.zpow.exponent) |
| 73 | + |
| 74 | + def merge_left(self, left: _PauliAndZPow) -> None: |
| 75 | + """Inplace merge other from left.""" |
| 76 | + self._merge_left_zpow(left.zpow) |
| 77 | + if left.pauli != ops.I: |
| 78 | + self._merge_left_pauli(cast(ops.Pauli, left.pauli)) |
| 79 | + |
| 80 | + def merge_right(self, right: _PauliAndZPow) -> None: |
| 81 | + """Inplace merge other from right.""" |
| 82 | + if right.pauli != ops.I: |
| 83 | + self._merge_right_pauli(cast(ops.Pauli, right.pauli)) |
| 84 | + self._merge_right_zpow(right.zpow) |
| 85 | + |
| 86 | + def after_cphase( |
| 87 | + self, cphase: ops.CZPowGate |
| 88 | + ) -> tuple[ops.CZPowGate, _PauliAndZPow, _PauliAndZPow]: |
| 89 | + """Pull self through cphase. |
| 90 | +
|
| 91 | + Returns: |
| 92 | + A tuple of |
| 93 | + (updated cphase gate, pull_through of this qubit, pull_through of the other qubit). |
| 94 | + """ |
| 95 | + if self.pauli in self.commuting_gates: |
| 96 | + return cphase, self, _PauliAndZPow() |
| 97 | + else: |
| 98 | + # Taking self.pauli==X gate as an example: |
| 99 | + # 0: ─X─Z^t──@────── 0: ─X──@─────Z^t─ 0: ─@──────X──Z^t── |
| 100 | + # │ ==> │ ==> │ |
| 101 | + # 1: ────────@^exp── 1: ────@^exp───── 1: ─@^-exp─Z^exp─── |
| 102 | + # Similarly for X|Y on qubit 0/1, the result is always flipping cphase and |
| 103 | + # add an extra Rz rotation on the other qubit. |
| 104 | + return ( |
| 105 | + cast(ops.CZPowGate, cphase**-1), |
| 106 | + self, |
| 107 | + _PauliAndZPow(zpow=ops.ZPowGate(exponent=cphase.exponent)), |
| 108 | + ) |
| 109 | + |
| 110 | + def after_pauli(self, pauli: ops.Pauli | ops.IdentityGate) -> _PauliAndZPow: |
| 111 | + """Calculates ─self─pauli─ ==> ─pauli─output─.""" |
| 112 | + if pauli in self.commuting_gates: |
| 113 | + return _PauliAndZPow(self.pauli, self.zpow) |
| 114 | + else: |
| 115 | + return _PauliAndZPow(self.pauli, ops.ZPowGate(exponent=-self.zpow.exponent)) |
| 116 | + |
| 117 | + def after_zpow(self, zpow: ops.ZPowGate) -> tuple[ops.ZPowGate, _PauliAndZPow]: |
| 118 | + """Calculates ─self─zpow─ ==> ─zpow'─output─.""" |
| 119 | + if self.pauli in self.commuting_gates: |
| 120 | + return zpow, self |
| 121 | + else: |
| 122 | + return ops.ZPowGate(exponent=-zpow.exponent), self |
| 123 | + |
| 124 | + def __str__(self) -> str: |
| 125 | + return f"─{self.pauli}──{self.zpow}─" |
| 126 | + |
| 127 | + def to_single_qubit_gate(self) -> ops.PhasedXZGate | ops.ZPowGate | ops.IdentityGate: |
| 128 | + """Converts the _PhasedXYAndRz to a single-qubit gate.""" |
| 129 | + exp = self.zpow.exponent |
| 130 | + match self.pauli: |
| 131 | + case ops.I: |
| 132 | + if exp % 2 == 0: |
| 133 | + return ops.I |
| 134 | + return self.zpow |
| 135 | + case ops.X: |
| 136 | + return ops.PhasedXZGate(x_exponent=1, z_exponent=exp, axis_phase_exponent=0) |
| 137 | + case ops.Y: |
| 138 | + return ops.PhasedXZGate(x_exponent=1, z_exponent=exp - 1, axis_phase_exponent=0) |
| 139 | + case _: # ops.Z |
| 140 | + if (exp + 1) % 2 == 0: |
| 141 | + return ops.I |
| 142 | + return ops.ZPowGate(exponent=1 + exp) |
| 143 | + |
| 144 | + |
| 145 | +def _pull_through_single_cphase( |
| 146 | + cphase: ops.CZPowGate, input0: _PauliAndZPow, input1: _PauliAndZPow |
| 147 | +) -> tuple[ops.CZPowGate, _PauliAndZPow, _PauliAndZPow]: |
| 148 | + """Pulls input0 and input1 through a CZPowGate. |
| 149 | + Input: |
| 150 | + 0: ─(input0)─@───── |
| 151 | + │ |
| 152 | + 1: ─(input1)─@^exp─ |
| 153 | + Output: |
| 154 | + 0: ─@────────(output0)─ |
| 155 | + │ |
| 156 | + 1: ─@^+/-exp─(output1)─ |
| 157 | + """ |
| 158 | + |
| 159 | + # Step 1; pull input0 through CZPowGate. |
| 160 | + # 0: ─input0─@───── 0: ────────@─────────output0─ |
| 161 | + # │ ==> │ |
| 162 | + # 1: ─input1─@^exp─ 1: ─input1─@^+/-exp──output1─ |
| 163 | + output_cphase, output0, output1 = input0.after_cphase(cphase) |
| 164 | + |
| 165 | + # Step 2; similar to step 1, pull input1 through CZPowGate. |
| 166 | + # 0: ─@──────────pulled0────output0─ 0: ─@────────output0─ |
| 167 | + # ==> │ ==> │ |
| 168 | + # 1: ─@^+/-exp───pulled1────output1─ 1: ─@^+/-exp─output1─ |
| 169 | + output_cphase, pulled1, pulled0 = input1.after_cphase(output_cphase) |
| 170 | + output0.merge_left(pulled0) |
| 171 | + output1.merge_left(pulled1) |
| 172 | + |
| 173 | + return output_cphase, output0, output1 |
| 174 | + |
| 175 | + |
| 176 | +_TARGET_GATESET: ops.Gateset = ops.Gateset(ops.CZPowGate) |
| 177 | +_SUPPORTED_GATESET: ops.Gateset = ops.Gateset(ops.Pauli, ops.IdentityGate, ops.Rz, ops.ZPowGate) |
| 178 | + |
| 179 | + |
| 180 | +class CPhaseGaugeTransformerMM(MultiMomentGaugeTransformer): |
| 181 | + |
| 182 | + def __init__(self, supported_gates=_SUPPORTED_GATESET): |
| 183 | + super().__init__(target=_TARGET_GATESET, supported_gates=supported_gates) |
| 184 | + |
| 185 | + def sample_left_moment(self, active_qubits: frozenset[ops.Qid]) -> circuits.Moment: |
| 186 | + return circuits.Moment( |
| 187 | + [ |
| 188 | + self.rng.choice( |
| 189 | + np.array([ops.I, ops.X, ops.Y, ops.Z], dtype=ops.Gate), |
| 190 | + p=[0.25, 0.25, 0.25, 0.25], |
| 191 | + ).on(q) |
| 192 | + for q in active_qubits |
| 193 | + ] |
| 194 | + ) |
| 195 | + |
| 196 | + def gauge_on_moments(self, moments_to_gauge) -> list[circuits.Moment]: |
| 197 | + active_qubits = circuits.Circuit.from_moments(*moments_to_gauge).all_qubits() |
| 198 | + left_moment = self.sample_left_moment(active_qubits) |
| 199 | + pulled: dict[ops.Qid, _PauliAndZPow] = { |
| 200 | + op.qubits[0]: _PauliAndZPow(pauli=cast(ops.Pauli | ops.IdentityGate, op.gate)) |
| 201 | + for op in left_moment |
| 202 | + if op.gate |
| 203 | + } |
| 204 | + ret: list[circuits.Moment] = [left_moment] |
| 205 | + # The loop iterates through each moment of the target block, propagating |
| 206 | + # the `pulled` gauge from left to right. In each iteration, `prev` holds |
| 207 | + # the gauge to the left of the current `moment`, and the loop computes |
| 208 | + # the transformed `moment` and the new `pulled` gauge to its right. |
| 209 | + for moment in moments_to_gauge: |
| 210 | + # Calculate --prev--moment-- ==> --updated_momment--pulled-- |
| 211 | + prev = pulled |
| 212 | + pulled = {} |
| 213 | + ops_at_updated_moment: list[ops.Operation] = [] |
| 214 | + for op in moment: |
| 215 | + # Pull prev through ops at the moment. |
| 216 | + if op.gate: |
| 217 | + match op.gate: |
| 218 | + case ops.CZPowGate(): |
| 219 | + q0, q1 = op.qubits |
| 220 | + new_gate, pulled[q0], pulled[q1] = _pull_through_single_cphase( |
| 221 | + op.gate, prev[q0], prev[q1] |
| 222 | + ) |
| 223 | + ops_at_updated_moment.append(new_gate.on(q0, q1)) |
| 224 | + case ops.Pauli() | ops.IdentityGate(): |
| 225 | + q = op.qubits[0] |
| 226 | + ops_at_updated_moment.append(op) |
| 227 | + pulled[q] = prev[q].after_pauli(op.gate) |
| 228 | + case ops.ZPowGate(): |
| 229 | + q = op.qubits[0] |
| 230 | + new_zpow, pulled[q] = prev[q].after_zpow(op.gate) |
| 231 | + ops_at_updated_moment.append(new_zpow.on(q)) |
| 232 | + case _: |
| 233 | + raise ValueError(f"Gate type {type(op.gate)} is not supported.") |
| 234 | + # Keep the other ops of prev |
| 235 | + for q, gate in prev.items(): |
| 236 | + if q not in pulled: |
| 237 | + pulled[q] = gate |
| 238 | + ret.append(circuits.Moment(ops_at_updated_moment)) |
| 239 | + last_moment = circuits.Moment( |
| 240 | + [gate.to_single_qubit_gate().on(q) for q, gate in pulled.items()] |
| 241 | + ) |
| 242 | + ret.append(last_moment) |
| 243 | + return ret |
0 commit comments