Skip to content

Commit e2a53bc

Browse files
Merge branch 'main' into rot_vis
2 parents 3d57c7a + 4a0ff59 commit e2a53bc

15 files changed

Lines changed: 361 additions & 29 deletions

qualtran/rotation_synthesis/__init__.py

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

1515
from qualtran.rotation_synthesis.math_config import NumpyConfig, with_dps
1616
from qualtran.rotation_synthesis.matrix import to_cirq, to_quirk, to_sequence
17-
from qualtran.rotation_synthesis.protocols.clifford_t_synthesis import (
17+
from qualtran.rotation_synthesis.protocols import (
1818
diagonal_unitary_approx,
1919
fallback_protocol,
20+
magnitude_approx,
2021
mixed_diagonal_protocol,
2122
mixed_fallback_protocol,
2223
)

qualtran/rotation_synthesis/channels/channel.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
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
2121
import cirq
22+
import numpy as np
2223

2324
import qualtran.rotation_synthesis._typing as rst
2425
import qualtran.rotation_synthesis.math_config as mc
@@ -35,7 +36,7 @@ def expected_num_ts(self, config: mc.MathConfig) -> rst.Real: ...
3536

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

4041

4142
@attrs.frozen
@@ -147,6 +148,44 @@ def to_quirk(self, fmt: str = "xz") -> str:
147148
cols = '[' + ','.join(f'[{g}]' for g in gates) + ']'
148149
return "https://algassert.com/quirk#circuit={\"cols\":%s}" % cols
149150

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

151190
@attrs.frozen
152191
class ProjectiveChannel(Channel):

qualtran/rotation_synthesis/channels/channel_test.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ def test_unitary_from_sequence(gates):
4848
def test_diamond_distance_for_unitary(gates, theta, distance):
4949
c = ch.UnitaryChannel.from_sequence(gates)
5050
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)
5155

5256

5357
@pytest.mark.parametrize(

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.float128,
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
)
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)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
import pytest
17+
from scipy import stats
18+
19+
import qualtran.rotation_synthesis.math_config as mc
20+
from qualtran.rotation_synthesis.matrix import analytical_decomposition as ad
21+
22+
23+
def _random_angles(n, seed):
24+
rng = np.random.default_rng(seed)
25+
for _ in range(n):
26+
yield rng.random(3) * 2 * np.pi
27+
28+
29+
def _random_su2(n, seed):
30+
for u in stats.unitary_group(2).rvs(n, seed):
31+
yield u / np.linalg.det(u) ** 0.5
32+
33+
34+
@pytest.mark.parametrize(["theta1", "phi", "theta2"], _random_angles(100, seed=0))
35+
def test_decomposition_round_trip(theta1, phi, theta2):
36+
u = ad.unitary_from_zxz(theta1, phi, theta2, mc.NumpyConfig)
37+
angles = ad.su_unitary_to_zxz_angles(u, config=mc.NumpyConfig)
38+
v = ad.unitary_from_zxz(*angles, config=mc.NumpyConfig)
39+
np.testing.assert_allclose(actual=u, desired=v)
40+
41+
42+
@pytest.mark.parametrize("u", _random_su2(100, seed=0))
43+
def test_decomposition_round_trip_start_with_unitary(u):
44+
angles = ad.su_unitary_to_zxz_angles(u, config=mc.NumpyConfig)
45+
v = ad.unitary_from_zxz(*angles, config=mc.NumpyConfig)
46+
np.testing.assert_allclose(actual=u, desired=v)

qualtran/rotation_synthesis/matrix/generation_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ def test_generated_rotations_determinant():
2929
all_rotations = generate_rotations(5)
3030
for n in range(len(all_rotations)):
3131
assert np.allclose(
32-
[abs(np.linalg.det(r.numpy())) for r in all_rotations[n]], 2 * (2 + np.sqrt(2)) ** n
32+
[abs(np.linalg.det(r.matrix.astype(complex))) for r in all_rotations[n]],
33+
2 * (2 + np.sqrt(2)) ** n,
3334
)
3435

3536

qualtran/rotation_synthesis/matrix/su2_ct.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import attrs
2020
import numpy as np
2121

22+
import qualtran.rotation_synthesis.math_config as mc
2223
from qualtran.rotation_synthesis.rings import zsqrt2, zw
2324

2425

@@ -81,8 +82,23 @@ def __hash__(self):
8182
def __eq__(self, other):
8283
return np.all(self.matrix == other.matrix)
8384

84-
def numpy(self) -> np.ndarray:
85-
return self.matrix.astype(complex)
85+
def numpy(self, config: Optional[mc.MathConfig] = None) -> np.ndarray:
86+
"""Returns the numpy representation of the unitary.
87+
Args:
88+
config: An optional MathConfig used to convert the matrix entries to complex
89+
numbers and normalize the result. If not given numpy methods are used.
90+
"""
91+
if config is None:
92+
result = self.matrix.astype(complex)
93+
result = result / np.linalg.det(result) ** 0.5
94+
return result
95+
result = np.zeros((2, 2)) + 1j * config.zero
96+
sqrt_det = config.sqrt(self.det().value(config.sqrt2))
97+
for i in range(2):
98+
for j in range(2):
99+
result[i, j] = self.matrix[i, j].value(config.sqrt2)
100+
result = result / sqrt_det
101+
return result
86102

87103
def adjoint(self) -> "SU2CliffordT":
88104
return SU2CliffordT(self.matrix.T.conj())
@@ -194,16 +210,32 @@ def is_valid(self) -> bool:
194210
_, _, n1 = self.matrix[1, 0].to_zsqrt2()
195211
return n0 == n1
196212

213+
def rescale(self) -> 'SU2CliffordT':
214+
r"""Rescales the matrix such that its determinant is minimized.
197215
198-
_KEY_MAP = {
199-
(0, 0, 0, 0): "Tx",
200-
(0, 0, 1, 0): "Tz",
201-
(0, 1, 0, 0): "Ty",
202-
(0, 1, 1, 0): "Tx",
203-
(1, 0, 1, 0): "Tx",
204-
(1, 1, 0, 0): "Tx",
205-
(1, 1, 1, 0): "Tz",
206-
}
216+
The determinant of the unitary can be written as $2\lambda^n$ where $\lambda=2+\sqrt{l}$
217+
and $n$ is the number of $T$ gates needed to synthesize the matrix. When all entries of
218+
the matrix are divisible by $\lambda$ then we can divide through by $\lambda$ to reduce $n$
219+
"""
220+
u = self
221+
while u.det() > 2 * zsqrt2.LAMBDA_KLIUCHNIKOV:
222+
if not all(a.is_divisible_by(zw.LAMBDA_KLIUCHNIKOV) for a in u.matrix.flat):
223+
break
224+
u = SU2CliffordT(
225+
[[x // zw.LAMBDA_KLIUCHNIKOV for x in row] for row in u.matrix], u.gates
226+
)
227+
return u
228+
229+
def num_t_gates(self) -> int:
230+
"""Returns the number of T gates needed to synthesize the matrix."""
231+
det = self.det()
232+
x = zsqrt2.ZSqrt2(2)
233+
n = 0
234+
while x < det:
235+
x = x * zsqrt2.LAMBDA_KLIUCHNIKOV
236+
n += 1
237+
assert x == det
238+
return n
207239

208240

209241
def _gate_from_name(gate_name: str) -> SU2CliffordT:

0 commit comments

Comments
 (0)