Skip to content

Commit f9062a8

Browse files
Merge branch 'main' into replace_npfloat128
2 parents 06f4b93 + 2436c55 commit f9062a8

20 files changed

Lines changed: 769 additions & 85 deletions

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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
# limitations under the License.
1414

1515
from qualtran.rotation_synthesis.math_config import NumpyConfig, with_dps
16-
from qualtran.rotation_synthesis.protocols.clifford_t_synthesis import (
16+
from qualtran.rotation_synthesis.matrix import to_cirq, to_quirk, to_sequence
17+
from qualtran.rotation_synthesis.protocols import (
1718
diagonal_unitary_approx,
1819
fallback_protocol,
20+
magnitude_approx,
1921
mixed_diagonal_protocol,
2022
mixed_fallback_protocol,
2123
)

qualtran/rotation_synthesis/channels/channel.py

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
from __future__ import annotations
1616

1717
import abc
18-
from typing import Optional, Sequence
18+
from typing import Optional, Sequence, Union
1919

2020
import attrs
21+
import cirq
22+
import numpy as np
2123

2224
import qualtran.rotation_synthesis._typing as rst
2325
import qualtran.rotation_synthesis.math_config as mc
26+
import qualtran.rotation_synthesis.matrix.clifford_t_repr as ctr
2427
import qualtran.rotation_synthesis.rings as rings
2528
from qualtran.rotation_synthesis.matrix import su2_ct
2629
from qualtran.rotation_synthesis.rings import zsqrt2
@@ -33,7 +36,7 @@ def expected_num_ts(self, config: mc.MathConfig) -> rst.Real: ...
3336

3437
@abc.abstractmethod
3538
def diamond_norm_distance_to_rz(self, theta: rst.Real, config: mc.MathConfig) -> rst.Real:
36-
r"""Returns the diamond norm distance to $Rz(2\theta)$."""
39+
r"""Returns the diamond norm distance to $e^{i\theta Z}$."""
3740

3841

3942
@attrs.frozen
@@ -110,6 +113,79 @@ def from_sequence(cls, seq: Sequence[str], twirl: bool = False) -> UnitaryChanne
110113
n = sum(g.startswith("T") for g in seq)
111114
return UnitaryChannel(u.matrix[0, 0], u.matrix[1, 0], n, twirl)
112115

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+
151+
def diamond_norm_distance_to_unitary(
152+
self, unitary: np.ndarray, config: mc.MathConfig
153+
) -> rst.Real:
154+
r"""Returns the diamond norm distance between self and the given unitary.
155+
156+
From Theorem B.1 of arxiv:2203.10064, the diamond norm distance between two untiaries
157+
$U, V$ is $|v_1 - v_0|$ where $v_i$ are the eigen values of $V^\dagger U$. Geometrically
158+
this is the diameter of the smallest disc in the complex plane that contains both
159+
eigenvalues.
160+
"""
161+
# W = V^\dagger U
162+
w = self.to_matrix().adjoint().numpy(config) @ unitary
163+
# Compute the eigen values of W
164+
a = config.one
165+
b = -w[0, 0] - w[1, 1]
166+
c = w[0, 0] * w[1, 1] - w[0, 1] * w[1, 0]
167+
d = config.sqrt(b**2 - 4 * a * c)
168+
eigv0, eigv1 = [(-b - d) / (2 * a), (-b + d) / (2 * a)]
169+
# Compute the norm of the difference.
170+
diameter_vec = eigv1 - eigv0
171+
return config.sqrt(diameter_vec.real**2 + diameter_vec.imag**2)
172+
173+
@classmethod
174+
def from_unitaries(
175+
cls, *unitaries: Union[UnitaryChannel, su2_ct.SU2CliffordT]
176+
) -> UnitaryChannel:
177+
if not unitaries:
178+
raise ValueError('at least one unitary should be provided')
179+
180+
unitary = su2_ct.ISqrt2
181+
for u in unitaries:
182+
if isinstance(u, UnitaryChannel):
183+
unitary = unitary @ u.to_matrix()
184+
else:
185+
unitary = unitary @ u
186+
unitary = unitary.rescale()
187+
return UnitaryChannel(unitary.matrix[0, 0], unitary.matrix[1, 0], unitary.num_t_gates())
188+
113189

114190
@attrs.frozen
115191
class ProjectiveChannel(Channel):
@@ -173,6 +249,71 @@ def diamond_norm_distance_to_rz(self, theta: rst.Real, config: mc.MathConfig) ->
173249
theta - self.failure_angle(config), config
174250
)
175251

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+
176317

177318
@attrs.frozen
178319
class ProbabilisticChannel(Channel):

qualtran/rotation_synthesis/channels/channel_test.py

Lines changed: 41 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

@@ -46,6 +48,10 @@ def test_unitary_from_sequence(gates):
4648
def test_diamond_distance_for_unitary(gates, theta, distance):
4749
c = ch.UnitaryChannel.from_sequence(gates)
4850
np.testing.assert_allclose(c.diamond_norm_distance_to_rz(theta, mc.NumpyConfig), distance)
51+
u = np.zeros((2, 2), complex)
52+
u[1, 1] = np.exp(-1j * theta)
53+
u[0, 0] = u[1, 1].conjugate()
54+
np.testing.assert_allclose(c.diamond_norm_distance_to_unitary(u, mc.NumpyConfig), distance)
4955

5056

5157
@pytest.mark.parametrize(
@@ -306,3 +312,38 @@ def test_diamond_distance_for_mixed_fallback(
306312
np.testing.assert_allclose(
307313
float(c.diamond_norm_distance_to_rz(theta, mc.NumpyConfig)), distance, atol=3e-5
308314
)
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/lattice/geometry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def plot(self, ax: Optional[plt.Axes] = None, add_label: bool = True, **patch_ar
213213
fig, ax = plt.subplots(1)
214214

215215
theta = float(self.tilt(mc.NumpyConfig))
216-
e = self.rotate(-theta, mc.NumpyConfig)
216+
e = self.rotate(theta, mc.NumpyConfig)
217217
w = 2 / np.sqrt(float(e.D[0, 0]))
218218
h = 2 / np.sqrt(float(e.D[1, 1]))
219219
c = self.center.astype(float).tolist()

qualtran/rotation_synthesis/math_config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class MathConfig:
4040
_ceil: Callable[[Real], int] = math.ceil
4141
_arctan2: Callable[[Real, Real], Real] = math.atan2
4242
_arcsin: Callable[[Real], Real] = math.asin
43+
_arccos: Callable[[Real], Real] = math.acos
4344
_number: Callable[[Real], Real] = float
4445

4546
def number(self, x) -> Real:
@@ -75,6 +76,9 @@ def __hash__(self) -> int:
7576
def arcsin(self, x: Real) -> Real:
7677
return self._arcsin(x)
7778

79+
def arccos(self, x: Real) -> Real:
80+
return self._arccos(x)
81+
7882

7983
NumpyConfig = MathConfig(
8084
"numpy",
@@ -91,6 +95,7 @@ def arcsin(self, x: Real) -> Real:
9195
lambda x: int(np.ceil(x)),
9296
np.arctan2,
9397
np.arcsin,
98+
np.arccos,
9499
np.longdouble,
95100
)
96101

@@ -124,5 +129,6 @@ def with_dps(dps: int) -> MathConfig:
124129
lambda x: int(mpmath.ceil(x)),
125130
mpmath.atan2,
126131
mpmath.asin,
132+
mpmath.acos,
127133
mpmath.mpf,
128134
)

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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 numpy as np
16+
17+
import qualtran.rotation_synthesis._typing as rst
18+
import qualtran.rotation_synthesis.math_config as mc
19+
20+
21+
def su_unitary_to_zxz_angles(
22+
u: np.ndarray, config: mc.MathConfig
23+
) -> tuple[rst.Real, rst.Real, rst.Real]:
24+
det = u[0, 0] * u[1, 1] - u[0, 1] * u[1, 0]
25+
# print(det)
26+
# assert config.isclose(det, 1)
27+
28+
cos_phi = (u[1, 1] * u[0, 0] + u[1, 0] * u[0, 1]).real
29+
# clip to the range [-1, 1] to ensure numerical stability.
30+
cos_phi = min(1, max(-1, cos_phi))
31+
phi = config.arccos(cos_phi)
32+
33+
sum_half_theta = config.arctan2(u[1, 1].imag, u[1, 1].real)
34+
diff_half_theta = config.arctan2(u[1, 0].imag, u[1, 0].real) - 1.5 * config.pi
35+
36+
theta1 = sum_half_theta + diff_half_theta
37+
theta2 = sum_half_theta - diff_half_theta
38+
39+
return theta1, phi, theta2
40+
41+
42+
def rx(phi: rst.Real, config: mc.MathConfig) -> np.ndarray:
43+
c = config.cos(phi / 2)
44+
s = config.sin(phi / 2)
45+
return np.array([[c, -1j * s], [-1j * s, c]])
46+
47+
48+
def rz(theta: rst.Real, config: mc.MathConfig) -> np.ndarray:
49+
v = config.cos(theta / 2) + 1j * config.sin(theta / 2)
50+
return np.diag([v.conjugate(), v])
51+
52+
53+
def unitary_from_zxz(
54+
theta1: rst.Real, phi: rst.Real, theta2: rst.Real, config: mc.MathConfig
55+
) -> np.ndarray:
56+
return rz(theta1, config) @ rx(phi, config) @ rz(theta2, config)

0 commit comments

Comments
 (0)