Skip to content

Commit 2436c55

Browse files
[Rotation Synthesis] add Tx/Tz/Tx*/Tz* representation and methods to export to Cirq and Quirk (#1751)
This PR adds 1. a method to represent an $SU(2)$ matrix as a string of $Tx, Tx^\dagger, Tz, Tz^\dagger$ 2. a method that exports the representation as a cirq circuit 3. a method that exports the representatoin as quirk link
1 parent 4a0ff59 commit 2436c55

11 files changed

Lines changed: 438 additions & 60 deletions

File tree

qualtran/rotation_synthesis/README.md

Lines changed: 26 additions & 1 deletion
Large diffs are not rendered by default.

qualtran/rotation_synthesis/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
from qualtran.rotation_synthesis.math_config import NumpyConfig, with_dps
16+
from qualtran.rotation_synthesis.matrix import to_cirq, to_quirk, to_sequence
1617
from qualtran.rotation_synthesis.protocols import (
1718
diagonal_unitary_approx,
1819
fallback_protocol,

qualtran/rotation_synthesis/channels/channel.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
from typing import Optional, Sequence, Union
1919

2020
import attrs
21+
import cirq
2122
import numpy as np
2223

2324
import qualtran.rotation_synthesis._typing as rst
2425
import qualtran.rotation_synthesis.math_config as mc
26+
import qualtran.rotation_synthesis.matrix.clifford_t_repr as ctr
2527
import qualtran.rotation_synthesis.rings as rings
2628
from qualtran.rotation_synthesis.matrix import su2_ct
2729
from qualtran.rotation_synthesis.rings import zsqrt2
@@ -111,6 +113,41 @@ def from_sequence(cls, seq: Sequence[str], twirl: bool = False) -> UnitaryChanne
111113
n = sum(g.startswith("T") for g in seq)
112114
return UnitaryChannel(u.matrix[0, 0], u.matrix[1, 0], n, twirl)
113115

116+
def to_cirq(self, fmt: str = "xz", qs: Optional[Sequence[cirq.Qid]] = None) -> cirq.Circuit:
117+
"""Retruns a representation of the channel as a cirq circuit.
118+
119+
Args:
120+
fmt: The gates to use (see the documentation of to_sequence).
121+
qs: Optional qubits to operate on.
122+
Returns:
123+
A cirq circuit
124+
Raises:
125+
ValueError: If twirl=True
126+
"""
127+
if self.twirl:
128+
raise ValueError("to_cirq is not supported when twirl=True")
129+
if qs:
130+
(q,) = qs
131+
else:
132+
q = cirq.q(0)
133+
return cirq.Circuit(ctr.to_cirq(self.to_matrix(), fmt, q))
134+
135+
def to_quirk(self, fmt: str = "xz") -> str:
136+
"""Retruns a quirk link representing the channel operation.
137+
138+
Args:
139+
fmt: The gates to use (see the documentation of to_sequence).
140+
Returns:
141+
A quirk link.
142+
Raises:
143+
ValueError: If twirl=True
144+
"""
145+
if self.twirl:
146+
raise ValueError("to_quirk is not supported when twirl=True")
147+
gates = ctr.to_quirk(self.to_matrix(), fmt)
148+
cols = '[' + ','.join(f'[{g}]' for g in gates) + ']'
149+
return "https://algassert.com/quirk#circuit={\"cols\":%s}" % cols
150+
114151
def diamond_norm_distance_to_unitary(
115152
self, unitary: np.ndarray, config: mc.MathConfig
116153
) -> rst.Real:
@@ -212,6 +249,71 @@ def diamond_norm_distance_to_rz(self, theta: rst.Real, config: mc.MathConfig) ->
212249
theta - self.failure_angle(config), config
213250
)
214251

252+
def to_cirq(self, fmt: str = "xz", qs: Optional[Sequence[cirq.Qid]] = None) -> cirq.Circuit:
253+
"""Retruns a representation of the channel as a cirq circuit.
254+
255+
Args:
256+
fmt: The gates to use (see the documentation of to_sequence).
257+
qs: Optional qubits to operate on.
258+
Returns:
259+
A cirq circuit
260+
Raises:
261+
ValueError: If the correction channel is not a UnitaryChannel.
262+
"""
263+
if qs:
264+
q0, q1 = qs
265+
else:
266+
q0, q1 = cirq.LineQubit.range(2)
267+
correction = self.correction
268+
if not isinstance(correction, UnitaryChannel):
269+
raise ValueError('to_cirq does not support a non unitary correction')
270+
return cirq.Circuit(
271+
cirq.CNOT(q0, q1),
272+
cirq.CircuitOperation(self.rotation.to_cirq(fmt, (q0,)).freeze()),
273+
cirq.CNOT(q0, q1),
274+
cirq.measure(q1, key='m'),
275+
cirq.CircuitOperation(correction.to_cirq(fmt, (q0,)).freeze()).with_classical_controls(
276+
'm'
277+
),
278+
)
279+
280+
def to_quirk(self, fmt: str = "xz") -> str:
281+
"""Retruns a quirk link representing the channel operation.
282+
283+
Args:
284+
fmt: The gates to use (see the documentation of to_sequence).
285+
Returns:
286+
A quirk link.
287+
"""
288+
correction = self.correction
289+
if not isinstance(correction, UnitaryChannel):
290+
raise ValueError(f"to_quirk is not supported for correction of type {type(correction)}")
291+
rot = ctr.to_quirk(self.rotation.to_matrix(), fmt)
292+
cor = ctr.to_quirk(correction.to_matrix(), fmt)
293+
first_row = []
294+
second_row = []
295+
# CNOT
296+
first_row.append("\"\"")
297+
second_row.append("\"X\"")
298+
# rotation
299+
first_row.extend(rot)
300+
second_row.extend("1" for _ in rot)
301+
# CNOT
302+
first_row.append("\"\"")
303+
second_row.append("\"X\"")
304+
# measure
305+
first_row.append("1")
306+
second_row.append("\"Measure\"")
307+
# correction
308+
first_row.extend(cor)
309+
second_row.extend("\"\"" for _ in cor)
310+
cols = (
311+
'['
312+
+ ','.join(f'[{g1},{g2}]' for g1, g2 in zip(first_row, second_row, strict=True))
313+
+ ']'
314+
)
315+
return "https://algassert.com/quirk#circuit={\"cols\":%s}" % cols
316+
215317

216318
@attrs.frozen
217319
class ProbabilisticChannel(Channel):

qualtran/rotation_synthesis/channels/channel_test.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import cirq
1516
import numpy as np
1617
import pytest
1718

1819
import qualtran.rotation_synthesis.channels as ch
1920
import qualtran.rotation_synthesis.math_config as mc
21+
import qualtran.rotation_synthesis.matrix.clifford_t_repr as ctr
2022
import qualtran.rotation_synthesis.matrix.su2_ct as su2_ct
2123

2224

@@ -310,3 +312,38 @@ def test_diamond_distance_for_mixed_fallback(
310312
np.testing.assert_allclose(
311313
float(c.diamond_norm_distance_to_rz(theta, mc.NumpyConfig)), distance, atol=3e-5
312314
)
315+
316+
317+
@pytest.mark.parametrize(
318+
"gates",
319+
[["I", "Z", "I", "Tz"], ["I", "S", "Tz"], ["I", "S", "Tz"], ["I", "Z", "S", "Tz"], ["I", "S"]],
320+
)
321+
@pytest.mark.parametrize("fmt", ["xz", "xyz"])
322+
def test_unitary_to_cirq(gates, fmt):
323+
u = ch.UnitaryChannel.from_sequence(gates)
324+
assert u.to_cirq(fmt) == cirq.Circuit(ctr.to_cirq(u.to_matrix(), fmt))
325+
326+
327+
@pytest.mark.parametrize(
328+
"gates1",
329+
[["I", "Z", "I", "Tz"], ["I", "S", "Tz"], ["I", "S", "Tz"], ["I", "Z", "S", "Tz"], ["I", "S"]],
330+
)
331+
@pytest.mark.parametrize(
332+
"gates2",
333+
[["I", "Z", "I", "Tz"], ["I", "S", "Tz"], ["I", "S", "Tz"], ["I", "Z", "S", "Tz"], ["I", "S"]],
334+
)
335+
@pytest.mark.parametrize("fmt", ["xz", "xyz"])
336+
def test_fallback_to_cirq(gates1, gates2, fmt):
337+
c = ch.ProjectiveChannel(
338+
ch.UnitaryChannel.from_sequence(gates1), ch.UnitaryChannel.from_sequence(gates2)
339+
)
340+
q0, q1 = cirq.LineQubit.range(2)
341+
assert c.to_cirq(fmt) == cirq.Circuit(
342+
cirq.CNOT(q0, q1),
343+
cirq.CircuitOperation(cirq.FrozenCircuit(ctr.to_cirq(c.rotation.to_matrix(), fmt, q0))),
344+
cirq.CNOT(q0, q1),
345+
cirq.measure(q1, key='m'),
346+
cirq.CircuitOperation(
347+
cirq.FrozenCircuit(ctr.to_cirq(c.correction.to_matrix(), fmt, q0)) # type: ignore[attr-defined]
348+
).with_classical_controls('m'),
349+
)

qualtran/rotation_synthesis/matrix/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
r"""A submodule for compiling $\mathbb{Z}[e^{i \pi/4}]$ matrices to Clifford+T as well as generating them."""
1616

17+
from qualtran.rotation_synthesis.matrix.clifford_t_repr import to_cirq, to_quirk, to_sequence
1718
from qualtran.rotation_synthesis.matrix.generation import (
19+
generate_cliffords,
1820
generate_rotations,
1921
generate_rotations_iter,
2022
)
21-
from qualtran.rotation_synthesis.matrix.su2_ct import generate_cliffords, SU2CliffordT
23+
from qualtran.rotation_synthesis.matrix.su2_ct import SU2CliffordT
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Copyright 2025 Google LLC
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+
import math
16+
from typing import cast, Mapping, Optional
17+
18+
import cirq
19+
20+
import qualtran.rotation_synthesis.matrix.generation as ctg
21+
import qualtran.rotation_synthesis.matrix.su2_ct as su2_ct
22+
import qualtran.rotation_synthesis.rings.zsqrt2 as zsqrt2
23+
24+
_TWO = zsqrt2.ZSqrt2(2)
25+
26+
27+
def clifford(matrix: su2_ct.SU2CliffordT) -> tuple[str, ...]:
28+
assert matrix.det() == 2
29+
cliffords = ctg.generate_cliffords()
30+
if matrix in cliffords:
31+
return cliffords[matrix]
32+
return ("Z", "X", "Z", "X") + cliffords[-matrix]
33+
34+
35+
def _xyz_sequence(matrix: su2_ct.SU2CliffordT) -> tuple[str, ...]:
36+
seq = []
37+
while matrix.det() > _TWO:
38+
t = su2_ct._key_map()[matrix._key]
39+
nxt = (su2_ct.GATE_MAP[t].adjoint() @ matrix).scale_down()
40+
assert nxt is not None
41+
assert nxt.det() < matrix.det()
42+
seq.append(t)
43+
matrix = nxt
44+
return clifford(matrix) + tuple(seq[::-1])
45+
46+
47+
_T_list = [
48+
('Tz', su2_ct.Tz),
49+
('Tx', su2_ct.Tx),
50+
('Tz*', su2_ct.Tz.adjoint()),
51+
('Tx*', su2_ct.Tx.adjoint()),
52+
]
53+
54+
55+
def _xz_sequence(matrix: su2_ct.SU2CliffordT, use_hs: bool = True) -> Optional[tuple[str, ...]]:
56+
if matrix.det() == 2:
57+
return clifford(matrix)
58+
cliffords = [su2_ct.ISqrt2]
59+
if use_hs:
60+
cliffords.append(su2_ct.HSqrt2)
61+
cliffords.append(su2_ct.HSqrt2 @ su2_ct.SSqrt2)
62+
candidates = []
63+
for name, t in _T_list:
64+
for c in cliffords:
65+
new = c.adjoint() @ matrix
66+
new = t.adjoint() @ new
67+
new = new.scale_down()
68+
if new is None or not new.is_valid():
69+
continue
70+
seq = _xz_sequence(new, False)
71+
if seq is None:
72+
continue
73+
gates = cast(tuple[str, ...], c.gates)
74+
candidates.append(seq + (name,) + gates)
75+
if not candidates:
76+
return None
77+
return min(candidates, key=len)
78+
79+
80+
def to_sequence(matrix: su2_ct.SU2CliffordT, fmt: str = 'xyz') -> tuple[str, ...]:
81+
r"""Returns a sequence of Clifford+T that produces the given matrix.
82+
83+
Args:
84+
matrix: The matrix to represent.
85+
fmt: A string from the set {'xz', 'xyz'} representing the allowed T gates where
86+
- 'xyz' uses Tx, Ty, Tz gates.
87+
- 'xz' uses $Tx, Tz, Tx^\dagger, Tz^\dagger$
88+
Returns:
89+
A tuple of strings representing the gates.
90+
Raises:
91+
ValueError: If `fmt` is not supported.
92+
"""
93+
if fmt == 'xyz':
94+
return _xyz_sequence(matrix)
95+
if fmt == 'xz':
96+
return cast(tuple[str, ...], _xz_sequence(matrix))
97+
raise ValueError(f'{type=} is not supported')
98+
99+
100+
_CIRQ_GATE_MAP: Mapping[str, cirq.Gate] = {
101+
"I": cirq.I,
102+
"H": cirq.H,
103+
"S": cirq.S,
104+
"X": cirq.X,
105+
"Y": cirq.Y,
106+
"Z": cirq.Z,
107+
"Tx": cirq.rx(math.pi / 4),
108+
"Ty": cirq.ry(math.pi / 4),
109+
"Tz": cirq.rz(math.pi / 4),
110+
"Tx*": cirq.rx(-math.pi / 4),
111+
"Ty*": cirq.ry(-math.pi / 4),
112+
"Tz*": cirq.rz(-math.pi / 4),
113+
}
114+
115+
116+
def _to_quirk_name(name: str) -> str:
117+
if name == "I":
118+
return "1"
119+
if name in ("X", "Y", "Z", "H"):
120+
return "\"" + name + "\""
121+
if name == "S":
122+
return "\"Z^½\""
123+
if name == "S*":
124+
return "\"Z^-½\""
125+
if name.startswith("T"):
126+
if name.endswith("*"):
127+
return "\"" + name[1].upper() + "^-¼" + "\""
128+
return "\"" + name[1].upper() + "^¼" + "\""
129+
raise ValueError(f"{name=} is not supported")
130+
131+
132+
def to_cirq(
133+
matrix: su2_ct.SU2CliffordT, fmt: str, q: Optional[cirq.Qid] = None
134+
) -> tuple[cirq.Operation]:
135+
"""Retruns a representation of the matrix as a sequence of Cirq operations.
136+
137+
Args:
138+
matrix: The matrix to represent.
139+
fmt: The gates to use (see the documentation of to_sequence).
140+
q: The qubit to use.
141+
Returns:
142+
A tuple of Cirq operations.
143+
"""
144+
q = q or cirq.q(0)
145+
return tuple(_CIRQ_GATE_MAP[g](q) for g in to_sequence(matrix, fmt))
146+
147+
148+
def to_quirk(matrix: su2_ct.SU2CliffordT, fmt: str) -> tuple[str, ...]:
149+
"""Retruns a representation of the matrix as a sequence of quirk symbols.
150+
151+
Args:
152+
matrix: The matrix to represent.
153+
fmt: The gates to use (see the documentation of to_sequence).
154+
Returns:
155+
A tuple quirk symbols.
156+
"""
157+
return tuple(_to_quirk_name(name) for name in to_sequence(matrix, fmt))

0 commit comments

Comments
 (0)