|
| 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 | + ) |
0 commit comments