Skip to content

Commit 7be2dd6

Browse files
committed
Support multi moment gauge compiling
1 parent 3d4f6f1 commit 7be2dd6

File tree

5 files changed

+628
-0
lines changed

5 files changed

+628
-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: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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

Comments
 (0)