Skip to content

Commit c182892

Browse files
committed
Support multi moment gauge compiling
1 parent 11f6238 commit c182892

File tree

5 files changed

+608
-0
lines changed

5 files changed

+608
-0
lines changed

cirq-core/cirq/transformers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
SpinInversionGaugeTransformer as SpinInversionGaugeTransformer,
151151
SqrtCZGaugeTransformer as SqrtCZGaugeTransformer,
152152
SqrtISWAPGaugeTransformer as SqrtISWAPGaugeTransformer,
153+
CPhaseGaugeTransformerMM as CPhaseGaugeTransformerMM,
153154
)
154155

155156
from cirq.transformers.randomized_measurements import (

cirq-core/cirq/transformers/gauge_compiling/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,11 @@
4242
from cirq.transformers.gauge_compiling.cphase_gauge import (
4343
CPhaseGaugeTransformer as CPhaseGaugeTransformer,
4444
)
45+
46+
from cirq.transformers.gauge_compiling.multi_moment_gauge_compiling import (
47+
MultiMomentGaugeTransformer as MultiMomentGaugeTransformer,
48+
)
49+
50+
from cirq.transformers.gauge_compiling.multi_moment_cphase_gauge import (
51+
CPhaseGaugeTransformerMM as CPhaseGaugeTransformerMM,
52+
)
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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 after_Rz(self, Rz: ops.Rz) -> tuple[ops.ZPowGate, _PauliAndZPow]:
125+
"""Calculates ─self─Rz─ ==> ─Rz'─output─."""
126+
if self.pauli in self.commuting_gates:
127+
return ops.Rz(rads=Rz._rads), self
128+
else:
129+
return ops.Rz(rads=-Rz._rads), self
130+
131+
def __str__(self) -> str:
132+
return f"─{self.pauli}──{self.zpow}─"
133+
134+
def to_single_qubit_gate(self) -> ops.PhasedXZGate | ops.ZPowGate | ops.IdentityGate:
135+
"""Converts the _PhasedXYAndRz to a single-qubit gate."""
136+
exp = self.zpow.exponent
137+
match self.pauli:
138+
case ops.I:
139+
if exp % 2 == 0:
140+
return ops.I
141+
return self.zpow
142+
case ops.X:
143+
return ops.PhasedXZGate(x_exponent=1, z_exponent=exp, axis_phase_exponent=0)
144+
case ops.Y:
145+
return ops.PhasedXZGate(x_exponent=1, z_exponent=exp - 1, axis_phase_exponent=0)
146+
case _: # ops.Z
147+
if (exp + 1) % 2 == 0:
148+
return ops.I
149+
return ops.ZPowGate(exponent=1 + exp)
150+
151+
152+
def _pull_through_single_cphase(
153+
cphase: ops.CZPowGate, input0: _PauliAndZPow, input1: _PauliAndZPow
154+
) -> tuple[ops.CZPowGate, _PauliAndZPow, _PauliAndZPow]:
155+
"""Pulls input0 and input1 through a CZPowGate.
156+
Input:
157+
0: ─(input0)─@─────
158+
159+
1: ─(input1)─@^exp─
160+
Output:
161+
0: ─@────────(output0)─
162+
163+
1: ─@^+/-exp─(output1)─
164+
"""
165+
166+
# Step 1; pull input0 through CZPowGate.
167+
# 0: ─input0─@───── 0: ────────@─────────output0─
168+
# │ ==> │
169+
# 1: ─input1─@^exp─ 1: ─input1─@^+/-exp──output1─
170+
output_cphase, output0, output1 = input0.after_cphase(cphase)
171+
172+
# Step 2; similar to step 1, pull input1 through CZPowGate.
173+
# 0: ─@──────────pulled0────output0─ 0: ─@────────output0─
174+
# ==> │ ==> │
175+
# 1: ─@^+/-exp───pulled1────output1─ 1: ─@^+/-exp─output1─
176+
output_cphase, pulled1, pulled0 = input1.after_cphase(output_cphase)
177+
output0.merge_left(pulled0)
178+
output1.merge_left(pulled1)
179+
180+
return output_cphase, output0, output1
181+
182+
183+
_TARGET_GATESET: ops.Gateset = ops.Gateset(ops.CZPowGate)
184+
_SUPPORTED_GATESET: ops.Gateset = ops.Gateset(ops.Pauli, ops.IdentityGate, ops.Rz)
185+
186+
187+
class CPhaseGaugeTransformerMM(MultiMomentGaugeTransformer):
188+
189+
def __init__(self, supported_gates=_SUPPORTED_GATESET):
190+
super().__init__(target=_TARGET_GATESET, supported_gates=supported_gates)
191+
192+
def sample_left_moment(self, active_qubits) -> circuits.Moment:
193+
return circuits.Moment(
194+
[
195+
self.rng.choice(
196+
np.array([ops.I, ops.X, ops.Y, ops.Z], dtype=ops.Gate),
197+
p=[0.25, 0.25, 0.25, 0.25],
198+
).on(q)
199+
for q in active_qubits
200+
]
201+
)
202+
203+
def gauge_on_moments(self, moments_to_gauge) -> list[circuits.Moment]:
204+
active_qubits = circuits.Circuit.from_moments(*moments_to_gauge).all_qubits()
205+
left_moment = self.sample_left_moment(active_qubits)
206+
pulled: dict[ops.Qid, _PauliAndZPow] = {
207+
op.qubits[0]: _PauliAndZPow(pauli=cast(ops.Pauli | ops.IdentityGate, op.gate))
208+
for op in left_moment
209+
if op.gate
210+
}
211+
ret: list[circuits.Moment] = [left_moment]
212+
for moment in moments_to_gauge:
213+
# Calc --prev--moment-- ==> --updated_momment--pulled--
214+
prev = pulled
215+
pulled = {}
216+
ops_at_updated_moment: list[ops.Operation] = []
217+
for op in moment:
218+
# Pull prev through ops at the moment.
219+
if op.gate:
220+
match op.gate:
221+
case ops.CZPowGate():
222+
q0, q1 = op.qubits
223+
new_gate, pulled[q0], pulled[q1] = _pull_through_single_cphase(
224+
op.gate, prev[q0], prev[q1]
225+
)
226+
ops_at_updated_moment.append(new_gate.on(q0, q1))
227+
case ops.Pauli() | ops.IdentityGate():
228+
q = op.qubits[0]
229+
ops_at_updated_moment.append(op)
230+
pulled[q] = prev[q].after_pauli(op.gate)
231+
case ops.ZPowGate():
232+
q = op.qubits[0]
233+
new_zpow, pulled[q] = prev[q].after_zpow(cast(ops.ZPowGate, op.gate))
234+
ops_at_updated_moment.append(new_zpow.on(q))
235+
case ops.Rz():
236+
q = op.qubits[0]
237+
new_Rz, pulled[q] = prev[q].after_Rz(op.gate)
238+
ops_at_updated_moment.append(new_Rz.on(q))
239+
case _:
240+
raise ValueError(f"Gate type {type(op.gate)} is not supported.")
241+
# Keep the other ops of prev
242+
for q, gate in prev.items():
243+
if q not in pulled:
244+
pulled[q] = gate
245+
ret.append(circuits.Moment(ops_at_updated_moment))
246+
last_moment = circuits.Moment(
247+
[gate.to_single_qubit_gate().on(q) for q, gate in pulled.items()]
248+
)
249+
ret.append(last_moment)
250+
return ret

0 commit comments

Comments
 (0)