Skip to content

Commit 14dd571

Browse files
committed
Improved sample_gates.py implementation and unitary_protocol tests. Also added docstrings
1 parent 88f7f2d commit 14dd571

File tree

6 files changed

+147
-133
lines changed

6 files changed

+147
-133
lines changed

cirq-core/cirq/protocols/apply_unitary_protocol.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,31 @@ def default(
134134
return ApplyUnitaryArgs(state, np.empty_like(state), range(num_qubits))
135135

136136
@classmethod
137-
def for_unitary(cls, qid_shapes: Tuple[int, ...]) -> 'ApplyUnitaryArgs':
138-
state = qis.eye_tensor(qid_shapes, dtype=np.complex128)
139-
buffer = np.empty_like(state)
140-
return ApplyUnitaryArgs(state, buffer, range(len(qid_shapes)))
137+
def for_unitary(
138+
cls, num_qubits: Optional[int] = None, *, qid_shape: Optional[Tuple[int, ...]] = None
139+
) -> 'ApplyUnitaryArgs':
140+
"""A default instance corresponding to an identity matrix.
141+
142+
Specify exactly one argument.
143+
144+
Args:
145+
num_qubits: The number of qubits to make space for in the state.
146+
qid_shape: A tuple representing the number of quantum levels of each
147+
qubit the identity matrix applies to. `qid_shape` is (2, 2, 2) for
148+
a three-qubit identity operation tensor.
149+
150+
Raises:
151+
TypeError: If exactly neither `num_qubits` or `qid_shape` is provided or
152+
both are provided.
153+
"""
154+
if (num_qubits is None) == (qid_shape is None):
155+
raise TypeError('Specify exactly one of num_qubits or qid_shape.')
156+
if num_qubits is not None:
157+
qid_shape = (2,) * num_qubits
158+
qid_shape = cast(Tuple[int, ...], qid_shape) # Satisfy mypy
159+
num_qubits = len(qid_shape)
160+
state = qis.eye_tensor(qid_shape, dtype=np.complex128)
161+
return ApplyUnitaryArgs(state, np.empty_like(state), range(num_qubits))
141162

142163
def with_axes_transposed_to_start(self) -> 'ApplyUnitaryArgs':
143164
"""Returns a transposed view of the same arguments.
@@ -471,7 +492,7 @@ def _strat_apply_unitary_from_decompose(val: Any, args: ApplyUnitaryArgs) -> Opt
471492
ordered_qubits = ancilla + tuple(qubits)
472493
all_qid_shapes = qid_shape_protocol.qid_shape(ordered_qubits)
473494
result = apply_unitaries(
474-
operations, ordered_qubits, ApplyUnitaryArgs.for_unitary(all_qid_shapes), None
495+
operations, ordered_qubits, ApplyUnitaryArgs.for_unitary(qid_shape=all_qid_shapes), None
475496
)
476497
if result is None or result is NotImplemented:
477498
return result

cirq-core/cirq/protocols/unitary_protocol.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def _strat_unitary_from_apply_unitary(val: Any) -> Optional[np.ndarray]:
161161
return NotImplemented
162162

163163
# Apply unitary effect to an identity matrix.
164-
result = method(ApplyUnitaryArgs.for_unitary(val_qid_shape))
164+
result = method(ApplyUnitaryArgs.for_unitary(qid_shape=val_qid_shape))
165165

166166
if result is NotImplemented or result is None:
167167
return result
@@ -185,7 +185,7 @@ def _strat_unitary_from_decompose(val: Any) -> Optional[np.ndarray]:
185185

186186
# Apply sub-operations' unitary effects to an identity matrix.
187187
result = apply_unitaries(
188-
operations, ordered_qubits, ApplyUnitaryArgs.for_unitary(val_qid_shape), None
188+
operations, ordered_qubits, ApplyUnitaryArgs.for_unitary(qid_shape=val_qid_shape), None
189189
)
190190

191191
# Package result.

cirq-core/cirq/protocols/unitary_protocol_test.py

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -189,29 +189,40 @@ def test_has_unitary():
189189
assert not cirq.has_unitary(FullyImplemented(False))
190190

191191

192-
@pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 10))
193-
def test_decompose_gate_that_allocates_qubits(theta: float):
192+
def _test_gate_that_allocates_qubits(gate):
194193
from cirq.protocols.unitary_protocol import _strat_unitary_from_decompose
195194

196-
gate = testing.GateThatAllocatesAQubit(theta)
197-
np.testing.assert_allclose(
198-
cast(np.ndarray, _strat_unitary_from_decompose(gate)), gate.target_unitary()
199-
)
200-
np.testing.assert_allclose(
201-
cast(np.ndarray, _strat_unitary_from_decompose(gate(a))), gate.target_unitary()
202-
)
195+
op = gate.on(*cirq.LineQubit.range(cirq.num_qubits(gate)))
196+
moment = cirq.Moment(op)
197+
circuit = cirq.FrozenCircuit(op)
198+
circuit_op = cirq.CircuitOperation(circuit)
199+
for val in [gate, op, moment, circuit, circuit_op]:
200+
unitary_from_strat = _strat_unitary_from_decompose(val)
201+
assert unitary_from_strat is not None
202+
np.testing.assert_allclose(unitary_from_strat, gate.narrow_unitary())
203203

204204

205205
@pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 10))
206-
@pytest.mark.parametrize('n', [*range(1, 6)])
207-
def test_recusive_decomposition(n: int, theta: float):
208-
from cirq.protocols.unitary_protocol import _strat_unitary_from_decompose
206+
@pytest.mark.parametrize('phase_state', [0, 1])
207+
@pytest.mark.parametrize('target_bitsize', [1, 2, 3])
208+
@pytest.mark.parametrize('ancilla_bitsize', [1, 4])
209+
def test_decompose_gate_that_allocates_clean_qubits(
210+
theta: float, phase_state: int, target_bitsize: int, ancilla_bitsize: int
211+
):
209212

210-
g1 = testing.GateThatDecomposesIntoNGates(n, cirq.H, theta)
211-
g2 = testing.GateThatDecomposesIntoNGates(n, g1, theta)
212-
np.testing.assert_allclose(
213-
cast(np.ndarray, _strat_unitary_from_decompose(g2)), g2.target_unitary()
214-
)
213+
gate = testing.PhaseUsingCleanAncilla(theta, phase_state, target_bitsize, ancilla_bitsize)
214+
_test_gate_that_allocates_qubits(gate)
215+
216+
217+
@pytest.mark.parametrize('phase_state', [0, 1])
218+
@pytest.mark.parametrize('target_bitsize', [1, 2, 3])
219+
@pytest.mark.parametrize('ancilla_bitsize', [1, 4])
220+
def test_decompose_gate_that_allocates_dirty_qubits(
221+
phase_state: int, target_bitsize: int, ancilla_bitsize: int
222+
):
223+
224+
gate = testing.PhaseUsingDirtyAncilla(phase_state, target_bitsize, ancilla_bitsize)
225+
_test_gate_that_allocates_qubits(gate)
215226

216227

217228
def test_decompose_and_get_unitary():
@@ -227,15 +238,6 @@ def test_decompose_and_get_unitary():
227238
np.testing.assert_allclose(_strat_unitary_from_decompose(DummyComposite()), np.eye(1))
228239
np.testing.assert_allclose(_strat_unitary_from_decompose(OtherComposite()), m2)
229240

230-
np.testing.assert_allclose(
231-
_strat_unitary_from_decompose(testing.GateThatAllocatesTwoQubits()),
232-
testing.GateThatAllocatesTwoQubits.target_unitary(),
233-
)
234-
np.testing.assert_allclose(
235-
_strat_unitary_from_decompose(testing.GateThatAllocatesTwoQubits().on(a, b)),
236-
testing.GateThatAllocatesTwoQubits.target_unitary(),
237-
)
238-
239241

240242
def test_decomposed_has_unitary():
241243
# Gates

cirq-core/cirq/testing/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,4 @@
108108

109109
from cirq.testing.sample_circuits import nonoptimal_toffoli_circuit
110110

111-
from cirq.testing.sample_gates import (
112-
GateThatAllocatesAQubit,
113-
GateThatAllocatesTwoQubits,
114-
GateThatDecomposesIntoNGates,
115-
)
111+
from cirq.testing.sample_gates import PhaseUsingCleanAncilla, PhaseUsingDirtyAncilla

cirq-core/cirq/testing/sample_gates.py

Lines changed: 51 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,75 +11,69 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
import functools
15-
import numpy as np
16-
from cirq import ops
17-
18-
19-
class GateThatAllocatesAQubit(ops.Gate):
20-
r"""A gate that applies $Z^\theta$ indirectly through a clean ancilla."""
14+
import dataclasses
2115

22-
def __init__(self, theta: float) -> None:
23-
super().__init__()
24-
self._theta = theta
16+
import cirq
17+
import numpy as np
18+
from cirq import ops, qis
2519

26-
def _num_qubits_(self):
27-
return 1
2820

29-
def _decompose_(self, q):
30-
anc = ops.NamedQubit("anc")
31-
yield ops.CX(*q, anc)
32-
yield (ops.Z**self._theta)(anc)
33-
yield ops.CX(*q, anc)
21+
def _matrix_for_phasing_state(num_qubits, phase_state, phase):
22+
matrix = qis.eye_tensor((2,) * num_qubits, dtype=np.complex128)
23+
matrix = matrix.reshape((2**num_qubits, 2**num_qubits))
24+
matrix[phase_state, phase_state] = phase
25+
print(num_qubits, phase_state, phase)
26+
print(matrix)
27+
return matrix
3428

35-
def target_unitary(self) -> np.ndarray:
36-
return np.array([[1, 0], [0, (-1 + 0j) ** self._theta]])
3729

30+
@dataclasses.dataclass(frozen=True)
31+
class PhaseUsingCleanAncilla(ops.Gate):
32+
r"""Phases the state $|phase_state>$ by $\exp(1j * \pi * \theta)$ using one clean ancilla."""
3833

39-
class GateThatAllocatesTwoQubits(ops.Gate):
40-
r"""A gate that applies $-j Z \otimes Z$ indirectly through two ancillas."""
34+
theta: float
35+
phase_state: int = 1
36+
target_bitsize: int = 1
37+
ancilla_bitsize: int = 1
4138

4239
def _num_qubits_(self):
43-
return 2
40+
return self.target_bitsize
4441

45-
def _decompose_(self, qs):
46-
q0, q1 = qs
47-
anc = ops.NamedQubit.range(2, prefix='two_ancillas_')
42+
def _decompose_(self, qubits):
43+
anc = ops.NamedQubit.range(self.ancilla_bitsize, prefix="anc")
44+
cv = [int(x) for x in f'{self.phase_state:0{self.target_bitsize}b}']
45+
cnot_ladder = [cirq.CNOT(anc[i - 1], anc[i]) for i in range(1, self.ancilla_bitsize)]
4846

49-
yield ops.X(anc[0])
50-
yield ops.CX(q0, anc[0])
51-
yield (ops.Y)(anc[0])
52-
yield ops.CX(q0, anc[0])
47+
yield ops.X(anc[0]).controlled_by(*qubits, control_values=cv)
48+
yield [cnot_ladder, ops.Z(anc[-1]) ** self.theta, reversed(cnot_ladder)]
49+
yield ops.X(anc[0]).controlled_by(*qubits, control_values=cv)
5350

54-
yield ops.CX(q1, anc[1])
55-
yield (ops.Z)(anc[1])
56-
yield ops.CX(q1, anc[1])
51+
def narrow_unitary(self) -> np.ndarray:
52+
"""Narrowed unitary corresponding to the unitary effect applied on target qubits."""
53+
phase = np.exp(1j * np.pi * self.theta)
54+
return _matrix_for_phasing_state(self.target_bitsize, self.phase_state, phase)
5755

58-
@classmethod
59-
def target_unitary(cls) -> np.ndarray:
60-
# Unitary = -j Z \otimes Z
61-
return np.array([[-1j, 0, 0, 0], [0, 1j, 0, 0], [0, 0, 1j, 0], [0, 0, 0, -1j]])
6256

57+
@dataclasses.dataclass(frozen=True)
58+
class PhaseUsingDirtyAncilla(ops.Gate):
59+
r"""Phases the state $|phase_state>$ by -1 using one dirty ancilla."""
6360

64-
class GateThatDecomposesIntoNGates(ops.Gate):
65-
r"""Applies $(Z^\theta)^{\otimes_n}$ on work qubits and `subgate` on $n$ borrowable ancillas."""
61+
phase_state: int = 1
62+
target_bitsize: int = 1
63+
ancilla_bitsize: int = 1
6664

67-
def __init__(self, n: int, subgate: ops.Gate, theta: float) -> None:
68-
super().__init__()
69-
self._n = n
70-
self._subgate = subgate
71-
self._name = str(subgate)
72-
self._theta = theta
73-
74-
def _num_qubits_(self) -> int:
75-
return self._n
76-
77-
def _decompose_(self, qs):
78-
ancilla = ops.NamedQubit.range(self._n, prefix=self._name)
79-
yield self._subgate.on_each(ancilla)
80-
yield (ops.Z**self._theta).on_each(qs)
81-
yield self._subgate.on_each(ancilla)
82-
83-
def target_unitary(self) -> np.ndarray:
84-
U = np.array([[1, 0], [0, (-1 + 0j) ** self._theta]])
85-
return functools.reduce(np.kron, [U] * self._n)
65+
def _num_qubits_(self):
66+
return self.target_bitsize
67+
68+
def _decompose_(self, qubits):
69+
anc = ops.NamedQubit.range(self.ancilla_bitsize, prefix="anc")
70+
cv = [int(x) for x in f'{self.phase_state:0{self.target_bitsize}b}']
71+
cnot_ladder = [cirq.CNOT(anc[i - 1], anc[i]) for i in range(1, self.ancilla_bitsize)]
72+
yield ops.X(anc[0]).controlled_by(*qubits, control_values=cv)
73+
yield [cnot_ladder, ops.Z(anc[-1]), reversed(cnot_ladder)]
74+
yield ops.X(anc[0]).controlled_by(*qubits, control_values=cv)
75+
yield [cnot_ladder, ops.Z(anc[-1]), reversed(cnot_ladder)]
76+
77+
def narrow_unitary(self) -> np.ndarray:
78+
"""Narrowed unitary corresponding to the unitary effect applied on target qubits."""
79+
return _matrix_for_phasing_state(self.target_bitsize, self.phase_state, -1)

cirq-core/cirq/testing/sample_gates_test.py

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,48 +11,49 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
import functools
1514
import pytest
1615

1716
import numpy as np
1817
from cirq.testing import sample_gates
19-
from cirq import protocols, ops
18+
import cirq
2019

2120

2221
@pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 20))
23-
def test_GateThatAllocatesAQubit(theta: float):
24-
g = sample_gates.GateThatAllocatesAQubit(theta)
25-
26-
want = np.array([[1, 0], [0, (-1 + 0j) ** theta]], dtype=np.complex128)
27-
# test unitary
28-
np.testing.assert_allclose(g.target_unitary(), want)
29-
30-
# test decomposition
31-
np.testing.assert_allclose(protocols.unitary(g), g.target_unitary())
32-
33-
34-
def test_GateThatAllocatesTwoQubits():
35-
g = sample_gates.GateThatAllocatesTwoQubits()
36-
37-
Z = np.array([[1, 0], [0, -1]])
38-
want = -1j * np.kron(Z, Z)
39-
# test unitary
40-
np.testing.assert_allclose(g.target_unitary(), want)
41-
42-
# test decomposition
43-
np.testing.assert_allclose(protocols.unitary(g), g.target_unitary())
44-
45-
46-
@pytest.mark.parametrize('n', [*range(1, 6)])
47-
@pytest.mark.parametrize('subgate', [ops.Z, ops.X, ops.Y, ops.T])
48-
@pytest.mark.parametrize('theta', np.linspace(0, 2 * np.pi, 5))
49-
def test_GateThatDecomposesIntoNGates(n: int, subgate: ops.Gate, theta: float):
50-
g = sample_gates.GateThatDecomposesIntoNGates(n, subgate, theta)
51-
52-
U = np.array([[1, 0], [0, (-1 + 0j) ** theta]], dtype=np.complex128)
53-
want = functools.reduce(np.kron, [U] * n)
54-
# test unitary
55-
np.testing.assert_allclose(g.target_unitary(), want)
56-
57-
# test decomposition
58-
np.testing.assert_allclose(protocols.unitary(g), g.target_unitary())
22+
def test_phase_using_clean_ancilla(theta: float):
23+
g = sample_gates.PhaseUsingCleanAncilla(theta)
24+
q = cirq.LineQubit(0)
25+
qubit_order = cirq.QubitOrder.explicit([q], fallback=cirq.QubitOrder.DEFAULT)
26+
decomposed_unitary = cirq.Circuit(cirq.decompose_once(g.on(q))).unitary(qubit_order=qubit_order)
27+
phase = np.exp(1j * np.pi * theta)
28+
np.testing.assert_allclose(g.narrow_unitary(), np.array([[1, 0], [0, phase]]))
29+
np.testing.assert_allclose(
30+
decomposed_unitary,
31+
# fmt: off
32+
np.array(
33+
[
34+
[1 , 0 , 0 , 0],
35+
[0 , phase, 0 , 0],
36+
[0 , 0 , phase, 0],
37+
[0 , 0 , 0 , 1],
38+
]
39+
),
40+
# fmt: on
41+
)
42+
43+
44+
@pytest.mark.parametrize(
45+
'target_bitsize, phase_state', [(1, 0), (1, 1), (2, 0), (2, 1), (2, 2), (2, 3)]
46+
)
47+
@pytest.mark.parametrize('ancilla_bitsize', [1, 4])
48+
def test_phase_using_dirty_ancilla(target_bitsize, phase_state, ancilla_bitsize):
49+
g = sample_gates.PhaseUsingDirtyAncilla(phase_state, target_bitsize, ancilla_bitsize)
50+
q = cirq.LineQubit.range(target_bitsize)
51+
qubit_order = cirq.QubitOrder.explicit(q, fallback=cirq.QubitOrder.DEFAULT)
52+
decomposed_circuit = cirq.Circuit(cirq.decompose_once(g.on(*q)))
53+
decomposed_unitary = decomposed_circuit.unitary(qubit_order=qubit_order)
54+
phase_matrix = np.eye(2**target_bitsize)
55+
phase_matrix[phase_state, phase_state] = -1
56+
np.testing.assert_allclose(g.narrow_unitary(), phase_matrix)
57+
np.testing.assert_allclose(
58+
decomposed_unitary, np.kron(phase_matrix, np.eye(2**ancilla_bitsize)), atol=1e-5
59+
)

0 commit comments

Comments
 (0)