Skip to content

Commit aa79bd8

Browse files
authored
SK Model QAOA application benchmark (#290)
1 parent 22a1a75 commit aa79bd8

File tree

6 files changed

+303
-2
lines changed

6 files changed

+303
-2
lines changed

recirq/qaoa/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,30 @@
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+
15+
from functools import lru_cache
16+
from typing import Optional
17+
18+
from cirq.protocols.json_serialization import ObjectFactory, DEFAULT_RESOLVERS
19+
from .sk_model import (
20+
SKModelQAOASpec,
21+
)
22+
23+
24+
@lru_cache()
25+
def _resolve_json(cirq_type: str) -> Optional[ObjectFactory]:
26+
"""Resolve the types of `recirq.qaoa.` json objects.
27+
28+
This is a Cirq JSON resolver suitable for appending to
29+
`cirq.protocols.json_serialization.DEFAULT_RESOLVERS`.
30+
"""
31+
if not cirq_type.startswith('recirq.qaoa.'):
32+
return None
33+
34+
cirq_type = cirq_type[len('recirq.qaoa.'):]
35+
return {k.__name__: k for k in [
36+
SKModelQAOASpec,
37+
]}.get(cirq_type, None)
38+
39+
40+
DEFAULT_RESOLVERS.append(_resolve_json)

recirq/qaoa/classical_angle_optimization.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
from timeit import default_timer as timer
16+
from typing import List
1617

1718
import networkx as nx
1819
import numpy as np
@@ -53,7 +54,7 @@ def optimize_instance_interp_heuristic(graph: nx.Graph,
5354
param_guess_at_p1=None,
5455
node_to_index_map=None,
5556
dtype=np.complex128,
56-
verbose=False):
57+
verbose=False) -> List[OptimizationResult]:
5758
r"""
5859
Given a graph, find QAOA parameters that minimizes C=\sum_{<ij>} w_{ij} Z_i Z_j
5960

recirq/qaoa/problem_circuits.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
compile_to_non_negligible,
1515
validate_well_structured,
1616
compile_problem_unitary_to_swap_network, compile_swap_network_to_zzswap,
17-
measure_with_final_permutation, compile_problem_unitary_to_arbitrary_zz)
17+
measure_with_final_permutation, compile_problem_unitary_to_arbitrary_zz, ZZSwap)
1818
from recirq.qaoa.placement import place_on_device
1919
from recirq.qaoa.problems import HardwareGridProblem, SKProblem, ThreeRegularProblem
2020

@@ -122,10 +122,17 @@ def get_routed_sk_model_circuit(
122122
qubits: List[cirq.Qid],
123123
gammas: Sequence[float],
124124
betas: Sequence[float],
125+
*,
126+
keep_zzswap_as_one_op=True,
125127
) -> cirq.Circuit:
126128
"""Get a QAOA circuit for a fully-connected problem using the linear swap
127129
network.
128130
131+
This leaves the circuit in an "uncompiled" form, using ZZ, Swap, and/or ZZSwap gates
132+
for the two-qubit gate and will append a permutation gate for odd p depths. The former
133+
should be compiled to hardware-native two-qubit gates; the latter can be absorbed into
134+
analysis routines.
135+
129136
See Also:
130137
:py:func:`get_compiled_sk_model_circuit`
131138
@@ -135,11 +142,18 @@ def get_routed_sk_model_circuit(
135142
qubits: The qubits to use in construction of the circuit.
136143
gammas: Gamma angles to use as parameters for problem unitaries
137144
betas: Beta angles to use as parameters for driver unitaries
145+
keep_zzswap_as_one_op: If True, use `recirq.qaoa.gates_and_compilation.ZZSwap`
146+
custom, composite gate. This is required for using `get_compiled_sk_model_circuit`
147+
and `compile_to_syc`. Otherwise, decompose each ZZSwap operation into a ZZPowGate
148+
and SWAP gate. This is useful if you plan to use vanilla Cirq transformers.
138149
"""
139150
circuit = get_generic_qaoa_circuit(problem_graph, qubits, gammas, betas)
140151
circuit = compile_problem_unitary_to_swap_network(circuit)
141152
circuit = compile_swap_network_to_zzswap(circuit)
142153
circuit = compile_driver_unitary_to_rx(circuit)
154+
if not keep_zzswap_as_one_op:
155+
circuit = cirq.expand_composite(
156+
circuit, no_decomp=lambda op: not isinstance(op.gate, ZZSwap))
143157
return circuit
144158

145159

recirq/qaoa/sk_model/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .sk_model import *

recirq/qaoa/sk_model/sk_model.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Copyright 2022 Google
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 itertools
16+
from dataclasses import dataclass
17+
from typing import List, Tuple, Iterable, Sequence
18+
19+
import networkx as nx
20+
import numpy as np
21+
22+
import cirq
23+
from cirq.protocols import dataclass_json_dict
24+
from cirq_google.workflow import QuantumExecutable, BitstringsMeasurement, QuantumExecutableGroup, \
25+
ExecutableSpec
26+
from recirq.qaoa.classical_angle_optimization import optimize_instance_interp_heuristic
27+
from recirq.qaoa.problem_circuits import get_routed_sk_model_circuit
28+
29+
30+
def _graph_from_row_major_upper_triangular(
31+
all_to_all_couplings: Sequence[float], *, n: int
32+
) -> nx.Graph:
33+
"""Get `all_to_all_couplings` in the form of a NetworkX graph."""
34+
if not len(all_to_all_couplings) == n * (n - 1) / 2:
35+
raise ValueError("Number of couplings does not match the number of nodes.")
36+
37+
g = nx.Graph()
38+
for (u, v), coupling in zip(itertools.combinations(range(n), r=2), all_to_all_couplings):
39+
g.add_edge(u, v, weight=coupling)
40+
return g
41+
42+
43+
def _all_to_all_couplings_from_graph(graph: nx.Graph) -> Tuple[int, ...]:
44+
"""Given a networkx graph, turn it into a tuple of all-to-all couplings."""
45+
n = graph.number_of_nodes()
46+
if not sorted(graph.nodes) == sorted(range(n)):
47+
raise ValueError("Nodes must be contiguous and zero-indexed.")
48+
49+
edges = graph.edges
50+
return tuple(edges[u, v]['weight'] for u, v in itertools.combinations(range(n), r=2))
51+
52+
53+
@dataclass(frozen=True)
54+
class SKModelQAOASpec(ExecutableSpec):
55+
"""ExecutableSpec for running SK-model QAOA.
56+
57+
QAOA uses alternating applications of a problem-specific entangling unitary and a
58+
problem-agnostic driver unitary. It is a variational algorithm, but for this spec
59+
we rely on optimizing the angles via classical simulation.
60+
61+
The SK model is an all-to-all 2-body spin problem that we can route using the
62+
"swap network" to require only linear connectivity (but circuit depth scales with problem
63+
size)
64+
65+
Args:
66+
n_nodes: The number of nodes in the SK problem. This is equal to the number of qubits.
67+
all_to_all_couplings: The n(n-1)/2 pairwise coupling constants that defines the problem
68+
as a serializable tuple of the row-major upper triangular coupling matrix.
69+
p_depth: The depth hyperparemeter that presecribes the number of U_problem * U_driver
70+
repetitions.
71+
n_repetitions: The number of shots to take when running the circuits.
72+
executable_family: `recirq.qaoa.sk_model`.
73+
74+
"""
75+
76+
n_nodes: int
77+
all_to_all_couplings: Tuple[int, ...]
78+
p_depth: int
79+
n_repetitions: int
80+
executable_family: str = 'recirq.qaoa.sk_model'
81+
82+
def __post_init__(self):
83+
object.__setattr__(self, 'all_to_all_couplings', tuple(self.all_to_all_couplings))
84+
85+
def get_graph(self) -> nx.Graph:
86+
"""Get `all_to_all_couplings` in the form of a NetworkX graph."""
87+
return _graph_from_row_major_upper_triangular(self.all_to_all_couplings, n=self.n_nodes)
88+
89+
@staticmethod
90+
def get_all_to_all_couplings_from_graph(graph: nx.Graph) -> Tuple[int, ...]:
91+
"""Given a networkx graph, turn it into a tuple of all-to-all couplings."""
92+
return _all_to_all_couplings_from_graph(graph)
93+
94+
@classmethod
95+
def _json_namespace_(cls):
96+
return 'recirq.qaoa'
97+
98+
def _json_dict_(self):
99+
return dataclass_json_dict(self, namespace=self._json_namespace_())
100+
101+
102+
def _classically_optimize_qaoa_parameters(graph: nx.Graph, *, n: int, p_depth: int):
103+
param_guess = [
104+
np.arccos(np.sqrt((1 + np.sqrt((n - 2) / (n - 1))) / 2)),
105+
-np.pi / 8
106+
]
107+
108+
optima = optimize_instance_interp_heuristic(
109+
graph=graph,
110+
# Potential performance improvement: To optimize for a given p_depth,
111+
# we also find the optima for lower p values.
112+
# You could cache these instead of re-finding for each executable.
113+
p_max=p_depth,
114+
param_guess_at_p1=param_guess,
115+
verbose=True,
116+
)
117+
# The above returns a list, but since we asked for p_max = spec.p_depth,
118+
# we always want the last one.
119+
optimum = optima[-1]
120+
assert optimum.p == p_depth
121+
return optimum
122+
123+
124+
def sk_model_qaoa_spec_to_exe(
125+
spec: SKModelQAOASpec,
126+
) -> QuantumExecutable:
127+
"""Create a full `QuantumExecutable` from a given `SKModelQAOASpec`
128+
129+
Args:
130+
spec: The spec
131+
132+
Returns:
133+
a QuantumExecutable corresponding to the input specification.
134+
"""
135+
n = spec.n_nodes
136+
graph = spec.get_graph()
137+
138+
# Get params
139+
optimum = _classically_optimize_qaoa_parameters(graph, n=n, p_depth=spec.p_depth)
140+
141+
# Make the circuit
142+
qubits = cirq.LineQubit.range(n)
143+
circuit = get_routed_sk_model_circuit(
144+
graph, qubits, optimum.gammas, optimum.betas, keep_zzswap_as_one_op=False)
145+
146+
# QAOA code optionally finishes with a QubitPermutationGate, which we want to
147+
# absorb into measurement. Maybe at some point this can be part of
148+
# `cg.BitstringsMeasurement`, but for now we'll do it implicitly in the analysis code.
149+
if spec.p_depth % 2 == 1:
150+
assert len(circuit[-1]) == 1
151+
permute_op, = circuit[-1]
152+
assert isinstance(permute_op.gate, cirq.QubitPermutationGate)
153+
circuit = circuit[:-1]
154+
155+
# Measure
156+
circuit += cirq.measure(*qubits, key='z')
157+
158+
return QuantumExecutable(
159+
spec=spec,
160+
problem_topology=cirq.LineTopology(n),
161+
circuit=circuit,
162+
measurement=BitstringsMeasurement(spec.n_repetitions),
163+
)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Copyright 2022 Google
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 networkx as nx
16+
import numpy as np
17+
import pytest
18+
19+
import cirq
20+
from recirq.qaoa.sk_model.sk_model import _graph_from_row_major_upper_triangular, \
21+
_all_to_all_couplings_from_graph, SKModelQAOASpec, sk_model_qaoa_spec_to_exe
22+
23+
24+
def test_graph_from_row_major_upper_triangular():
25+
couplings = np.array([
26+
[0, 1, 2, 3],
27+
[0, 0, 4, 5],
28+
[0, 0, 0, 6],
29+
[0, 0, 0, 0],
30+
])
31+
flat_couplings = np.arange(1, 6 + 1)
32+
33+
graph1 = nx.from_numpy_array(couplings)
34+
graph2 = _graph_from_row_major_upper_triangular(flat_couplings, n=4)
35+
assert sorted(graph1.nodes) == sorted(graph2.nodes)
36+
assert sorted(graph1.edges) == sorted(graph2.edges)
37+
38+
for u, v, w in graph1.edges.data('weight'):
39+
assert w == graph2.edges[u, v]['weight']
40+
41+
42+
def test_graph_from_row_major_upper_triangular_bad():
43+
with pytest.raises(ValueError):
44+
_graph_from_row_major_upper_triangular([1, 2, 3, 4], n=2)
45+
46+
47+
def test_all_to_all_couplings_from_graph():
48+
g = nx.Graph()
49+
g.add_edge(0, 1, weight=1)
50+
g.add_edge(0, 2, weight=2)
51+
g.add_edge(1, 2, weight=3)
52+
couplings = _all_to_all_couplings_from_graph(g)
53+
assert couplings == (1, 2, 3)
54+
55+
56+
def test_all_to_all_couplings_from_graph_missing():
57+
g = nx.Graph()
58+
g.add_edge(0, 1, weight=1)
59+
# g.add_edge(0, 2, weight=2)
60+
g.add_edge(1, 2, weight=3)
61+
with pytest.raises(KeyError):
62+
_ = _all_to_all_couplings_from_graph(g)
63+
64+
65+
def test_all_to_all_couplings_from_graph_bad_nodes():
66+
g = nx.Graph()
67+
g.add_edge(10, 11, weight=1)
68+
g.add_edge(10, 12, weight=2)
69+
g.add_edge(11, 12, weight=3)
70+
with pytest.raises(ValueError):
71+
_ = _all_to_all_couplings_from_graph(g)
72+
73+
74+
@pytest.mark.parametrize('n', [5, 6])
75+
def test_graph_round_trip(n):
76+
couplings = tuple(np.random.choice([0, 1], size=n * (n - 1) // 2))
77+
assert couplings == _all_to_all_couplings_from_graph(
78+
_graph_from_row_major_upper_triangular(couplings, n=n))
79+
80+
81+
def test_spec_to_exe():
82+
spec = SKModelQAOASpec(
83+
n_nodes=3, all_to_all_couplings=[1, -1, 1], p_depth=1, n_repetitions=1_000
84+
)
85+
assert isinstance(spec.all_to_all_couplings, tuple)
86+
assert hash(spec) is not None
87+
exe = sk_model_qaoa_spec_to_exe(spec)
88+
init_hadamard_depth = 1
89+
zz_swap_depth = 2 # zz + swap
90+
driver_depth = 1
91+
measure_depth = 1
92+
assert len(
93+
exe.circuit) == init_hadamard_depth + zz_swap_depth * 3 + driver_depth + measure_depth
94+
assert exe.spec == spec
95+
assert exe.problem_topology == cirq.LineTopology(3)

0 commit comments

Comments
 (0)