Skip to content

Commit

Permalink
factor out shared code in qaoa proxies (#1091)
Browse files Browse the repository at this point in the history
most of the code in QAOAFermionicSwapProxy is identical to that of
QAOAVanillaProxy, so instead of repeating it we can inherit
  • Loading branch information
richrines1 authored Oct 14, 2024
1 parent e8751b8 commit 7a7ea26
Show file tree
Hide file tree
Showing 4 changed files with 10 additions and 121 deletions.
2 changes: 1 addition & 1 deletion checks-superstaq/checks_superstaq/checks-pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ enable = [
accept-no-param-doc=false # Do not accept totally missing parameter documentation.
accept-no-raise-doc=false # Do not accept totally missing raises documentation.
accept-no-return-doc=false # Do not accept totally missing return documentation.
accept-no-yield-doc=false # Do not accept missing yields annotations.
accept-no-yields-doc=false # Do not accept missing yields annotations.
default-docstring-type = "google"

# Ignore long lines containing urls or pylint directives.
Expand Down
119 changes: 4 additions & 115 deletions supermarq-benchmarks/supermarq/benchmarks/qaoa_fermionic_swap_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@

from __future__ import annotations

from collections.abc import Mapping

import cirq
import numpy as np
import numpy.typing as npt
import scipy

import supermarq
from supermarq.benchmark import Benchmark
from supermarq.benchmarks.qaoa_vanilla_proxy import QAOAVanillaProxy


class QAOAFermionicSwapProxy(Benchmark):
class QAOAFermionicSwapProxy(QAOAVanillaProxy):
"""Proxy of a full Quantum Approximate Optimization Algorithm (QAOA) benchmark.
This benchmark targets MaxCut on a Sherrington-Kirkpatrick (SK) model. Device
Expand All @@ -34,28 +29,7 @@ class QAOAFermionicSwapProxy(Benchmark):
#. Finding approximately optimal angles (rather than random values)
"""

def __init__(self, num_qubits: int) -> None:
"""Generate a new benchmark instance.
Args:
num_qubits: The number of nodes (qubits) within the SK graph.
"""
self.num_qubits = num_qubits
self.hamiltonian = self._gen_sk_hamiltonian()
self.params = self._gen_angles()

def _gen_sk_hamiltonian(self) -> list[tuple[int, int, float]]:
"""Randomly pick +1 or -1 for each edge weight."""
hamiltonian = []
for i in range(self.num_qubits):
for j in range(i + 1, self.num_qubits):
hamiltonian.append((i, j, np.random.choice([-1, 1])))

np.random.shuffle(hamiltonian)

return hamiltonian

def _gen_swap_network(self, gamma: float, beta: float) -> cirq.Circuit:
def _gen_ansatz(self, gamma: float, beta: float) -> cirq.Circuit:
qubits = cirq.LineQubit.range(self.num_qubits)
circuit = cirq.Circuit()

Expand Down Expand Up @@ -105,90 +79,5 @@ def _gen_swap_network(self, gamma: float, beta: float) -> cirq.Circuit:
return circuit

def _get_energy_for_bitstring(self, bitstring: str) -> float:
energy_val = 0.0
for i, j, weight in self.hamiltonian:
if bitstring[i] == bitstring[j]:
energy_val -= weight # if edge is UNCUT, weight counts against objective
else:
energy_val += weight # if edge is CUT, weight counts towards objective
return energy_val

def _get_expectation_value_from_probs(self, probabilities: Mapping[str, float]) -> float:
expectation_value = 0.0
for bitstring, probability in probabilities.items():
expectation_value += probability * self._get_energy_for_bitstring(bitstring)
return expectation_value

def _get_opt_angles(self) -> tuple[npt.NDArray[np.float_], float]:
def f(params: npt.NDArray[np.float_]) -> float:
"""The objective function to minimize.
Args:
params: The parameters at which to evaluate the objective.
Returns:
Evaluation of objective given parameters.
"""
gamma, beta = params
circ = self._gen_swap_network(gamma, beta)
# Reverse bitstring ordering due to SWAP network
raw_probs = supermarq.simulation.get_ideal_counts(circ)
probs = {bitstring[::-1]: probability for bitstring, probability in raw_probs.items()}
h_expect = self._get_expectation_value_from_probs(probs)

return -h_expect # because we are minimizing instead of maximizing

init_params = [np.random.uniform() * 2 * np.pi, np.random.uniform() * 2 * np.pi]
out = scipy.optimize.minimize(f, init_params, method="COBYLA")

return out["x"], out["fun"]

def _gen_angles(self) -> npt.NDArray[np.float_]:
# Classically simulate the variational optimization 5 times,
# return the parameters from the best performing simulation
best_params, best_cost = np.zeros(2), 0.0
for _ in range(5):
params, cost = self._get_opt_angles()
if cost < best_cost:
best_params = params
best_cost = cost
return best_params

def circuit(self) -> cirq.Circuit:
"""Generate a QAOA circuit for the Sherrington-Kirkpatrick model.
This particular benchmark utilizes a quantum circuit structure called
the fermionic swap network. We restrict the depth of this proxy benchmark
to p=1 to keep the classical simulation scalable.
Returns:
The S-K model QAOA `cirq.Circuit`.
"""
gamma, beta = self.params
return self._gen_swap_network(gamma, beta)

def score(self, counts: Mapping[str, float]) -> float:
"""Compare the experimental output to the output of noiseless simulation.
The implementation here has exponential runtime and would not scale. However, it could in
principle be done efficiently via https://arxiv.org/abs/1706.02998, so we're good.
Args:
counts: A dictionary containing the measurement counts from circuit execution.
Returns:
The QAOA Fermionic SWAP proxy benchmark score.
"""
# Reverse bitstring ordering due to SWAP network
raw_probs = supermarq.simulation.get_ideal_counts(self.circuit())
ideal_counts = {
bitstring[::-1]: probability for bitstring, probability in raw_probs.items()
}
total_shots = sum(counts.values())
# Reverse the order of the bitstrings due to the fermionic swap ansatz
experimental_counts = {k[::-1]: v / total_shots for k, v in counts.items()}

ideal_value = self._get_expectation_value_from_probs(ideal_counts)
experimental_value = self._get_expectation_value_from_probs(experimental_counts)

return 1 - abs(ideal_value - experimental_value) / (2 * ideal_value)
return super()._get_energy_for_bitstring(bitstring[::-1])
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def f(params: npt.NDArray[np.float_]) -> float:
"""The objective function to minimize.
Args:
params: parameters for objective.
params: The parameters at which to evaluate the objective.
Returns:
Evaluation of objective given parameters.
Expand Down Expand Up @@ -137,7 +137,7 @@ def circuit(self) -> cirq.Circuit:
the classical simulation scalable.
Returns:
The S-K model QAOA circuit.
The S-K model QAOA `cirq.Circuit`.
"""
gamma, beta = self.params
return self._gen_ansatz(gamma, beta)
Expand All @@ -149,10 +149,10 @@ def score(self, counts: Mapping[str, float]) -> float:
principle be done efficiently via https://arxiv.org/abs/1706.02998, so we're good.
Args:
counts: A dictionary containing measurement counts from circuit execution.
counts: A dictionary containing the measurement counts from circuit execution.
Returns:
The QAOA Vanilla proxy benchmark score.
The QAOA proxy benchmark score.
"""
ideal_counts = supermarq.simulation.get_ideal_counts(self.circuit())
total_shots = sum(counts.values())
Expand Down
2 changes: 1 addition & 1 deletion supermarq-benchmarks/supermarq/qcvv/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


@pytest.fixture(scope="session", autouse=True)
def patch_tqdm() -> Generator[None, None, None]:
def _patch_tqdm() -> Generator[None, None, None]:
"""Disable progress bars during tests."""
with mock.patch.dict(os.environ, {"TQDM_DISABLE": "1"}):
yield

0 comments on commit 7a7ea26

Please sign in to comment.