Skip to content

Commit e6dcafb

Browse files
authored
Add support for the Matsumoto-Amano normal form. (#1801)
Fixes #1762
1 parent af8592b commit e6dcafb

6 files changed

Lines changed: 236 additions & 4 deletions

File tree

qualtran/rotation_synthesis/matrix/_clifford_t_repr.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from typing import cast, Mapping, Optional
1717

1818
import cirq
19+
import numpy as np
1920

2021
import qualtran.rotation_synthesis.matrix._generation as ctg
2122
import qualtran.rotation_synthesis.matrix._su2_ct as _su2_ct
@@ -79,16 +80,72 @@ def _xz_sequence(
7980
return None
8081

8182

83+
def _matsumoto_amano_syllable(matrix: _su2_ct.SU2CliffordT) -> list[str]:
84+
"""Computes the next syllable in the Matsumoto-Amano decomposition for matrix.
85+
86+
Returns:
87+
the next syllable as a list of gates, representing either T, HT, or SHT.
88+
89+
Raises:
90+
ValueError if the parity matrix doesn't match any of the forms in
91+
Lemma 4.10, https://arxiv.org/abs/1312.6584.
92+
"""
93+
parity = matrix.bloch_form_parity()
94+
# Parity matrix must have a 0 column, see Lemma 4.10, https://arxiv.org/abs/1312.6584.
95+
# We move it to be last.
96+
for i in range(2):
97+
if np.all(parity[:, i] == 0):
98+
parity[:, [i, 2]] = parity[:, [2, i]]
99+
break
100+
if np.array_equal(parity, np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]])):
101+
# Leftmost syllable is T
102+
return ['T']
103+
elif np.array_equal(parity, np.array([[0, 0, 0], [1, 1, 0], [1, 1, 0]])):
104+
# Leftmost syllable is HT
105+
return ['H', 'T']
106+
elif np.array_equal(parity, np.array([[1, 1, 0], [0, 0, 0], [1, 1, 0]])):
107+
# Leftmost syllable is SHT
108+
return ['S', 'H', 'T']
109+
else:
110+
raise ValueError(f'Unexpected parity matrix:\n{parity}')
111+
112+
113+
def _matsumoto_amano_sequence(matrix: _su2_ct.SU2CliffordT) -> tuple[str, ...]:
114+
r"""Represents the Clifford+T operator in the Matsumoto-Amano normal form.
115+
116+
Returns:
117+
a list of gates matching the regular expression $(T|\eps)(HT|SHT)^*C$,
118+
where C is a Clifford operator, itself represented as a list of H and S gates.
119+
The gates are returned in the order they need to be applied to generate the
120+
input matrix.
121+
122+
Raises:
123+
ValueError if during the decomposition an invalid SU2CliffordT matrix is created.
124+
"""
125+
gates = []
126+
while matrix.det() != _TWO:
127+
syllable = _matsumoto_amano_syllable(matrix)
128+
gates += syllable
129+
for gate in syllable:
130+
matrix = _su2_ct.GATE_MAP[gate].adjoint() @ matrix
131+
new = matrix.scale_down()
132+
if new is None or not new.is_valid():
133+
raise ValueError('Invalid SU2CliffordT matrix')
134+
matrix = new
135+
return clifford(matrix) + tuple(gates[::-1])
136+
137+
82138
def to_sequence(matrix: _su2_ct.SU2CliffordT, fmt: str = 'xyz') -> tuple[str, ...]:
83139
r"""Returns a sequence of Clifford+T that produces the given matrix.
84140
85141
Allowable format strings are
86142
- 'xyz' uses Tx, Ty, Tz gates.
87143
- 'xz' uses $Tx, Tz, Tx^\dagger, Tz^\dagger$
144+
- 't' uses only Tz gates, and returns the Matsumoto-Amano normal form.
88145
89146
Args:
90147
matrix: The matrix to represent.
91-
fmt: A string from the set {'xz', 'xyz'} representing the allowed T gates described above.
148+
fmt: A string from the set {'xz', 'xyz', 't'} representing the allowed T gates described above.
92149
93150
Returns:
94151
A tuple of strings representing the gates.
@@ -100,6 +157,8 @@ def to_sequence(matrix: _su2_ct.SU2CliffordT, fmt: str = 'xyz') -> tuple[str, ..
100157
return _xyz_sequence(matrix)
101158
if fmt == 'xz':
102159
return cast(tuple[str, ...], _xz_sequence(matrix))
160+
if fmt == 't':
161+
return _matsumoto_amano_sequence(matrix)
103162
raise ValueError(f'{type=} is not supported')
104163

105164

@@ -116,6 +175,7 @@ def to_sequence(matrix: _su2_ct.SU2CliffordT, fmt: str = 'xyz') -> tuple[str, ..
116175
"Tx*": cirq.rx(-math.pi / 4),
117176
"Ty*": cirq.ry(-math.pi / 4),
118177
"Tz*": cirq.rz(-math.pi / 4),
178+
"T": cirq.rz(math.pi / 4),
119179
}
120180

121181

@@ -130,6 +190,8 @@ def _to_quirk_name(name: str, allow_global_phase: bool = False) -> str:
130190
if name == "S*":
131191
return "\"Z^-½\""
132192
if name.startswith("T"):
193+
if name == "T":
194+
return "\"Z^¼\""
133195
if name.endswith("*"):
134196
return "\"" + name[1].upper() + "^-¼" + "\""
135197
return "\"" + name[1].upper() + "^¼" + "\""
@@ -145,6 +207,8 @@ def _to_quirk_name(name: str, allow_global_phase: bool = False) -> str:
145207
if name == "S*":
146208
return '{"id":"Rzft","arg":"-pi/2"}'
147209
if name.startswith("T"):
210+
if name == "T":
211+
return '{"id":"Rzft","arg":"pi/4"}'
148212
angle = ['pi/4', '-pi/4'][name.endswith('*')]
149213
return '{"id":"R%sft","arg":"%s"}' % (name[1].lower(), angle)
150214
raise ValueError(f"{name=} is not supported")

qualtran/rotation_synthesis/matrix/_su2_ct.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,62 @@ def num_t_gates(self) -> int:
242242
assert x == det
243243
return n
244244

245+
def bloch_sphere_form(self) -> tuple[np.ndarray, int]:
246+
r"""Represents the unitary operator as a scaled element of SO(3).
247+
248+
The entries of the returned matrix are in $\mathbb{Z}[\sqrt{2}]$,
249+
scaled by $\frac{1}{2\sqrt{2}(2+\sqrt{2})^n}$. The scaling factor
250+
is implicit. The additional $\sqrt{2}$ factor is needed to ensure
251+
the entries are in $\mathbb{Z}[\sqrt{2}]$.
252+
253+
See Section 4 in https://arxiv.org/abs/1312.6584.
254+
255+
Returns:
256+
the scaled SO(3) matrix and the value n.
257+
"""
258+
u = self.matrix[0, 0]
259+
v = self.matrix[1, 0]
260+
return (
261+
np.array(
262+
[
263+
[
264+
(u**2 - v.conj() ** 2).real_zsqrt2(),
265+
(u**2 - v**2).imag_zsqrt2(),
266+
2 * (u * v.conj()).real_zsqrt2(),
267+
],
268+
[
269+
-(u**2 - v.conj() ** 2).imag_zsqrt2(),
270+
(u**2 + v**2).real_zsqrt2(),
271+
-2 * (u * v.conj()).imag_zsqrt2(),
272+
],
273+
[
274+
-2 * (u * v).real_zsqrt2(),
275+
-2 * (u * v).imag_zsqrt2(),
276+
(u * u.conj() - v * v.conj()).real_zsqrt2(),
277+
],
278+
]
279+
),
280+
self.num_t_gates(),
281+
)
282+
283+
def bloch_form_parity(self) -> np.ndarray:
284+
"""Returns the n-parity of the SO(3) Bloch sphere representation.
285+
286+
See Definition 4.7, https://arxiv.org/abs/1312.6584 for the definition of k-parity.
287+
From Lemma 4.10, the least denominator exponent for the Bloch form equals the T-count,
288+
and so the n-parity is well-defined.
289+
"""
290+
bf, n = self.bloch_sphere_form()
291+
bf = bf * _zsqrt2.SQRT_2**n
292+
scale_factor = _zsqrt2.ZSqrt2(2, 0) * _zsqrt2.SQRT_2 * _zsqrt2.LAMBDA_KLIUCHNIKOV**n
293+
if not all(x.is_divisible_by(scale_factor) for x in bf.flat):
294+
raise ValueError(
295+
"Not all entries of the \\sqrt{2}^n * SO(3) matrix are divisible "
296+
f"by 2\\sqrt{2}(2+\\sqrt{2})^n. The matrix is:\n{bf}"
297+
)
298+
scaled_bf = [[x // scale_factor for x in row] for row in bf]
299+
return np.array([[x.a % 2 for x in row] for row in scaled_bf])
300+
245301

246302
def _gate_from_name(gate_name: str) -> SU2CliffordT:
247303
adjoint = gate_name.count('*') % 2 == 1
@@ -308,6 +364,7 @@ def _key_map():
308364
# Tx, Ty, Tz scaled by sqrt(2*(2+sqrt(2)))
309365
Tx = SU2CliffordT(_I * _zw.SQRT_2 + _I - _X * _zw.J, ("Tx",))
310366
Ty = SU2CliffordT(_I * _zw.SQRT_2 + _I - _Y * _zw.J, ("Ty",))
367+
# Tz = sqrt(2) * (1 + w^*) * T and has determinant = 2(2+sqrt(2))
311368
Tz = SU2CliffordT(_I * _zw.SQRT_2 + _I - _Z * _zw.J, ("Tz",))
312369
Ts = [Tx, Ty, Tz]
313370

@@ -322,6 +379,7 @@ def _key_map():
322379
"X": XSqrt2,
323380
"Y": YSqrt2,
324381
"Z": ZSqrt2,
382+
"T": Tz,
325383
}
326384

327385
PARAMETRIC_FORM_BASES = [

qualtran/rotation_synthesis/matrix/clifford_t_repr_test.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,47 @@ def test_to_xz_seq(g: _su2_ct.SU2CliffordT):
6060

6161

6262
@pytest.mark.parametrize("g", _make_random_su(50, 5, random_cliffords=True, seed=0))
63-
def test_to_cirq(g):
64-
circuit = cirq.Circuit(ctr.to_cirq(g, 'xyz'))
63+
@pytest.mark.parametrize("fmt", ('xyz', 'xz', 't'))
64+
def test_to_cirq(g: _su2_ct.SU2CliffordT, fmt: str):
65+
g = g.rescale()
66+
circuit = cirq.Circuit(ctr.to_cirq(g, fmt))
6567
unitary = cirq.unitary(circuit)
6668
u = g.matrix.astype(complex)
6769
u = u / np.linalg.det(u) ** 0.5
6870
assert cirq.protocols.equal_up_to_global_phase(u, unitary)
71+
72+
73+
@pytest.mark.parametrize(
74+
["g", "fmt", "expected"],
75+
[
76+
[_su2_ct.HSqrt2, "t", ('"Z^½"', '"X"', '"Z^½"', '"X"', '"H"')],
77+
[_su2_ct.SSqrt2, "t", ('{"id":"Rzft","arg":"pi/2"}',)],
78+
[_su2_ct.Tz, "t", ('{"id":"Rzft","arg":"pi/4"}',)],
79+
],
80+
)
81+
def test_to_quirk(g: _su2_ct.SU2CliffordT, fmt: str, expected: tuple[str, ...]):
82+
assert ctr.to_quirk(g, fmt) == expected
83+
84+
85+
@pytest.mark.parametrize("g", _make_random_su(50, 10, random_cliffords=True, seed=0))
86+
def test_to_matsumoto_amano_seq(g: _su2_ct.SU2CliffordT):
87+
g = g.rescale()
88+
seq = ctr.to_sequence(g, 't')
89+
prev_t = -1
90+
# Check that the reversed list matches the regular expression $(T|\eps)(HT|SHT)^*C$.
91+
seq_r = list(reversed(seq))
92+
for i in range(len(seq_r)):
93+
assert seq_r[i] in ('T', 'H', 'S', 'Z', 'X')
94+
if i == 0 and seq_r[0] == 'T':
95+
prev_t = 0
96+
continue
97+
if seq_r[i] == 'T':
98+
# Check that this is one of HT, SHT syllabes
99+
assert prev_t in (i - 2, i - 3)
100+
if prev_t == i - 2:
101+
assert seq_r[i - 1] == 'H'
102+
else:
103+
assert seq_r[i - 2] + seq_r[i - 1] == 'SH'
104+
prev_t = i
105+
got = _su2_ct.SU2CliffordT.from_sequence(seq)
106+
assert got == g

qualtran/rotation_synthesis/matrix/su2_ct_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,55 @@ def test_num_t_gates():
113113

114114
# We need to call .rescale to remove the common factor and reduce the T count.
115115
assert (t @ t.adjoint()).rescale().num_t_gates() == 0
116+
117+
118+
_H_bloch_form_numpy = np.array([[0, 0, 1], [0, -1, 0], [1, 0, 0]])
119+
_S_bloch_form_numpy = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
120+
_T_bloch_form_numpy = np.array([[1, -1, 0], [1, 1, 0], [0, 0, _SQRT2]]) / _SQRT2
121+
122+
123+
@pytest.mark.parametrize(
124+
["g", "bloch_form", "n"],
125+
[
126+
[_su2_ct.HSqrt2, _H_bloch_form_numpy, 0],
127+
[_su2_ct.SSqrt2, _S_bloch_form_numpy, 0],
128+
[_su2_ct.Tz, _T_bloch_form_numpy, 1],
129+
],
130+
)
131+
def test_bloch_sphere_form_generators(g: _su2_ct.SU2CliffordT, bloch_form: np.ndarray, n: int):
132+
g_so3, m = g.bloch_sphere_form()
133+
assert m == n
134+
np.testing.assert_allclose(g_so3.astype(float) / (2 * _SQRT2 * (2 + _SQRT2) ** m), bloch_form)
135+
136+
137+
def _matrix_from_pauli(coords: np.ndarray) -> np.ndarray:
138+
return coords[0] * _X + coords[1] * _Y + coords[2] * _Z
139+
140+
141+
@pytest.mark.parametrize("g", _make_random_su(10, 5, random_cliffords=True, seed=0))
142+
@pytest.mark.parametrize("vector", np.random.choice(2, size=(10, 3)))
143+
def test_bloch_sphere_form_random(g: _su2_ct.SU2CliffordT, vector: np.ndarray):
144+
g_so3, _ = g.bloch_sphere_form()
145+
g_action = (
146+
g.matrix.astype(complex) @ _matrix_from_pauli(vector) @ g.adjoint().matrix.astype(complex)
147+
)
148+
g_so3_action = g_so3.astype(float) @ vector.T / _SQRT2
149+
np.testing.assert_allclose(_matrix_from_pauli(g_so3_action), g_action, atol=1e-7)
150+
151+
152+
_T_parity_numpy = np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]])
153+
_HT_parity_numpy = np.array([[0, 0, 0], [1, 1, 0], [1, 1, 0]])
154+
_SHT_parity_numpy = np.array([[1, 1, 0], [0, 0, 0], [1, 1, 0]])
155+
156+
157+
@pytest.mark.parametrize(
158+
["g", "parity"],
159+
[
160+
[_su2_ct.Tz, _T_parity_numpy],
161+
[_su2_ct.HSqrt2 @ _su2_ct.Tz, _HT_parity_numpy],
162+
[_su2_ct.SSqrt2 @ _su2_ct.HSqrt2 @ _su2_ct.Tz, _SHT_parity_numpy],
163+
],
164+
)
165+
def test_bloch_form_parity(g: _su2_ct.SU2CliffordT, parity: np.ndarray):
166+
g_parity = g.bloch_form_parity()
167+
np.testing.assert_equal(g_parity, parity)

qualtran/rotation_synthesis/rings/_zw.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class ZW:
4242
4343
Elements of $\mathbb{Z}[\omega]$ can be represented by 4 integers as
4444
$$
45-
\sum_{i=0}^4 a_i \omega^i
45+
\sum_{i=0}^3 a_i \omega^i
4646
$$
4747
4848
Attributes:
@@ -206,6 +206,16 @@ def to_zsqrt2(self) -> tuple[_zsqrt2.ZSqrt2, _zsqrt2.ZSqrt2, bool]:
206206
r == 1,
207207
)
208208

209+
def real_zsqrt2(self) -> _zsqrt2.ZSqrt2:
210+
r"""Returns \sqrt{2} * the real part of the element."""
211+
a, _, need_w = self.to_zsqrt2()
212+
return a * _zsqrt2.SQRT_2 + _zsqrt2.ZSqrt2(need_w, 0)
213+
214+
def imag_zsqrt2(self) -> _zsqrt2.ZSqrt2:
215+
r"""Returns \sqrt{2} * the imaginary part of the element."""
216+
_, b, need_w = self.to_zsqrt2()
217+
return b * _zsqrt2.SQRT_2 + _zsqrt2.ZSqrt2(need_w, 0)
218+
209219
def norm(self) -> int:
210220
res = self * self.conj() * self.sqrt2_conj() * self.sqrt2_conj().conj()
211221
assert all(c == 0 for c in res.coords[1:])

qualtran/rotation_synthesis/rings/zw_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,13 @@ def test_factor_prime(p):
138138
else:
139139
assert r.coords[1:] == (0, 0, 0)
140140
assert r.coords[0] % p == 0
141+
142+
143+
@pytest.mark.parametrize("x", _create_random_elements(20))
144+
def test_real_zsqrt2(x: rings.ZW):
145+
np.testing.assert_allclose((x.real_zsqrt2()).value(_SQRT_2), x.value(_SQRT_2).real * _SQRT_2)
146+
147+
148+
@pytest.mark.parametrize("x", _create_random_elements(20))
149+
def test_imag_zsqrt2(x: rings.ZW):
150+
np.testing.assert_allclose((x.imag_zsqrt2()).value(_SQRT_2), x.value(_SQRT_2).imag * _SQRT_2)

0 commit comments

Comments
 (0)