Skip to content

Commit 967e1c2

Browse files
authored
feat(circuits): add QAOAPathCircuit class (#14)
1 parent 671fd04 commit 967e1c2

File tree

3 files changed

+199
-0
lines changed

3 files changed

+199
-0
lines changed

quantum_enablement/circuits/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212

1313
"""Quantum circuit library and tools."""
1414

15+
from ._qaoa import QAOAPathCircuit
1516
from ._utils import compute_uncompute
1617

1718
__all__ = [
19+
"QAOAPathCircuit",
1820
"compute_uncompute",
1921
]
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2024.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
"""Quantum Approximate Optimization Algorithm (QAOA) quantum circuits.
14+
15+
[1] Farhi et.al. A Quantum Approximate Optimization Algorithm,
16+
https://arxiv.org/abs/1411.4028
17+
"""
18+
19+
20+
from qiskit import QuantumCircuit
21+
from qiskit.circuit import ParameterVector
22+
23+
24+
# TODO: add graph max-cut weights input arg
25+
class QAOAPathCircuit(QuantumCircuit):
26+
"""Parameterized QAOA acyclic line graph quantum circuit.
27+
28+
The cost parameter-vector is labeled γ, and the mixer
29+
parameter-vector β. Overall, there will be one parameter per
30+
unit of two-qubit depth: half in γ and half in ß [1].
31+
Weights in the generating max-cut graph are all equal to one.
32+
33+
Args:
34+
num_qubits: number of qubits (must be even).
35+
depth: two-qubit depth (must be even).
36+
barriers: if True adds barriers between layers.
37+
measurements: if True adds measurements at the end.
38+
39+
Notes:
40+
[1] Farhi et.al. A Quantum Approximate Optimization Algorithm,
41+
https://arxiv.org/abs/1411.4028
42+
"""
43+
44+
def __init__(
45+
self, num_qubits: int, depth: int, *, barriers: bool = False, measurements: bool = False
46+
) -> None:
47+
num_qubits = _validate_qaoa_num_qubits(num_qubits)
48+
depth = _validate_qaoa_depth(depth)
49+
barriers = bool(barriers)
50+
measurements = bool(measurements)
51+
52+
super().__init__(num_qubits, name=f"QAOAPathCircuit<{num_qubits}, {depth}>")
53+
54+
gammas = ParameterVector("γ", depth // 2)
55+
betas = ParameterVector("β", depth // 2)
56+
57+
self.h(range(num_qubits))
58+
for layer in range(depth // 2):
59+
if barriers:
60+
self.barrier()
61+
for qubit in range(0, num_qubits - 1, 2):
62+
self.rzz(gammas[layer], qubit, qubit + 1)
63+
for qubit in range(1, num_qubits - 1, 2):
64+
self.rzz(gammas[layer], qubit, qubit + 1)
65+
for qubit in range(num_qubits):
66+
self.rx(betas[layer], qubit)
67+
if measurements:
68+
self.measure_all()
69+
70+
71+
def _validate_qaoa_num_qubits(num_qubits: int) -> int:
72+
"""Validate number of qubits for QAOA circuits."""
73+
# pylint: disable=duplicate-code
74+
if not isinstance(num_qubits, int):
75+
raise TypeError(f"Invalid num. qubits type {type(num_qubits)}, expected <int>.")
76+
if num_qubits <= 2:
77+
raise ValueError(f"Number of qubits ({num_qubits}) must be greater than two.")
78+
if num_qubits % 2:
79+
raise ValueError(f"Number of qubits ({num_qubits}) must be even.")
80+
return num_qubits
81+
82+
83+
def _validate_qaoa_depth(depth: int) -> int:
84+
"""Validate depth for QAOA circuits."""
85+
# pylint: disable=duplicate-code
86+
if not isinstance(depth, int):
87+
raise TypeError(f"Invalid depth type {type(depth)}, expected <int>.")
88+
if depth < 0:
89+
raise ValueError(f"Depth ({depth}) must be positive.")
90+
if depth % 2:
91+
raise ValueError(f"Depth ({depth}) must be even.")
92+
return depth

test/unit/circuits/test_qaoa.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2024.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
"""Test QAOA circuits module."""
14+
15+
16+
from pytest import mark, raises # pylint: disable=import-error
17+
from qiskit import QuantumCircuit
18+
19+
from quantum_enablement.circuits._qaoa import QAOAPathCircuit
20+
21+
22+
def two_qubit_depth_filter(instruction):
23+
"""Filter instructions which do not contribute to two-qubit depth."""
24+
num_qubits = instruction.operation.num_qubits
25+
name = instruction.operation.name
26+
return num_qubits > 1 and name != "barrier"
27+
28+
29+
class TestQAOAPathCircuit:
30+
"""Test suite for the QAOAPathCircuit class."""
31+
32+
@mark.parametrize("num_qubits", [4, 6])
33+
@mark.parametrize("depth", [0, 2, 4])
34+
def test_creation(self, num_qubits, depth):
35+
"""Test QAOAPathCircuit creation."""
36+
circuit = QAOAPathCircuit(num_qubits, depth, barriers=False, measurements=False)
37+
op_counts = circuit.count_ops()
38+
assert isinstance(circuit, QuantumCircuit), "QAOAPathCircuit must be a QuantumCircuit."
39+
assert circuit.num_qubits == num_qubits, "Wrong number of qubits."
40+
assert circuit.depth(two_qubit_depth_filter) == depth, "Wrong two-qubit depth."
41+
assert circuit.num_parameters == depth, "Wrong number of parameters."
42+
assert op_counts.get("h", 0) == num_qubits, "Wrong number of H gates."
43+
assert op_counts.get("rzz", 0) == (num_qubits - 1) * (
44+
depth // 2
45+
), "Wrong number of RZZ gates."
46+
assert op_counts.get("rx", 0) == num_qubits * (depth // 2), "Wrong number of RX gates."
47+
assert op_counts.get("barrier", 0) == 0, "Wrong number of barriers."
48+
assert op_counts.get("measure", 0) == 0, "Wrong number of measurements."
49+
50+
def test_defaults(self):
51+
"""Test QAOAPathCircuit defaults."""
52+
circuit = QAOAPathCircuit(4, 4) # Note: default barriers and measurements are False.
53+
op_counts = circuit.count_ops()
54+
assert op_counts.get("barrier", 0) == 0, "By default barriers should not be present."
55+
assert op_counts.get("measure", 0) == 0, "By default measurements should not be present."
56+
57+
@mark.parametrize("num_qubits", [4, 6])
58+
@mark.parametrize("depth", [0, 2, 4])
59+
def test_barriers(self, num_qubits, depth):
60+
"""Test QAOAPathCircuit barriers."""
61+
circuit = QAOAPathCircuit(num_qubits, depth, barriers=True, measurements=False)
62+
op_counts = circuit.count_ops()
63+
assert op_counts.get("barrier", 0) == depth // 2, "Wrong number of barriers."
64+
# circuit.remove_operations("barrier")
65+
# assert circuit == QAOAPathCircuit(num_qubits, depth, measurements=False, barriers=False)
66+
67+
@mark.parametrize("num_qubits", [4, 6])
68+
@mark.parametrize("depth", [0, 2, 4])
69+
def test_measurements(self, num_qubits, depth):
70+
"""Test QAOAPathCircuit measurements."""
71+
circuit = QAOAPathCircuit(num_qubits, depth, barriers=False, measurements=True)
72+
op_counts = circuit.count_ops()
73+
assert op_counts.get("measure", 0) == num_qubits, "Wrong number of measurements."
74+
assert op_counts.get("barrier", 0) == 1, "Measurements should be preceded by a barrier."
75+
# circuit.remove_final_measurements()
76+
# assert circuit == QAOAPathCircuit(num_qubits, depth, measurements=False, barriers=False)
77+
78+
@mark.parametrize("num_qubits", [4, 6])
79+
@mark.parametrize("depth", [0, 2, 4])
80+
def test_name(self, num_qubits, depth):
81+
"""Test QAOAPathCircuit name."""
82+
circuit = QAOAPathCircuit(num_qubits, depth)
83+
assert circuit.name == f"QAOAPathCircuit<{num_qubits}, {depth}>", "Wrong circuit name."
84+
85+
def test_invalid_num_qubits(self):
86+
"""Test QAOAPathCircuit with invalid number of qubits."""
87+
with raises(ValueError, match=r"Number of qubits \(2\) must be greater than two."):
88+
QAOAPathCircuit(2, 2)
89+
90+
with raises(ValueError, match=r"Number of qubits \(3\) must be even\."):
91+
QAOAPathCircuit(3, 2)
92+
93+
with raises(TypeError, match=r"Invalid num\. qubits type .*, expected <int>\."):
94+
QAOAPathCircuit("4", 2) # type: ignore
95+
96+
def test_invalid_depth(self):
97+
"""Test QAOAPathCircuit with invalid depth."""
98+
with raises(ValueError, match=r"Depth \(-1\) must be positive\."):
99+
QAOAPathCircuit(4, -1)
100+
101+
with raises(ValueError, match=r"Depth \(1\) must be even\."):
102+
QAOAPathCircuit(4, 1)
103+
104+
with raises(TypeError, match=r"Invalid depth type .*, expected <int>\."):
105+
QAOAPathCircuit(4, "2") # type: ignore

0 commit comments

Comments
 (0)