Skip to content

(PoC) Add Primitives V2 + ISA circuit support #623

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .pylintdict
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ aer's
al
annealers
ansatz
ansatzes
apidocs
applegate
args
Expand All @@ -16,6 +17,7 @@ backend
backends
barkoutsos
benchmarking
bfgs
bitstring
bitstrings
bixby
Expand Down Expand Up @@ -65,12 +67,14 @@ farhi
fmin
formatter
func
functools
fred
fval
fx
gambella
geq
getter
getters
glover
goemans
goldstone
Expand All @@ -81,6 +85,7 @@ gurobi
gurobioptimizer
gurobipy
gutmann
hadfield
hamilton
hamiltonian
hamiltonians
Expand All @@ -99,6 +104,7 @@ iprint
ising
iter
iteratively
jac
july
karimi
kirkpatrick
Expand All @@ -115,6 +121,7 @@ lp
lucas
macos
makefile
marecek
masahito
matplotlib
maxcut
Expand Down Expand Up @@ -149,8 +156,10 @@ optimizationresultstatus
optimizers
panchenko
param
parameterizations
params
parikh
passmanager
pauli
paulis
peleato
Expand Down Expand Up @@ -206,6 +215,7 @@ simonetto
slsqp
smode
smoothen
spedalieri
spsa
src
statevector
Expand Down Expand Up @@ -248,6 +258,7 @@ wecker
whitespace
wiesner
williamson
woerner
xs
ys
zemlin
Expand Down
29 changes: 21 additions & 8 deletions qiskit_optimization/algorithms/grover_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
import numpy as np
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.circuit.library import QuadraticForm
from qiskit.primitives import BaseSampler
from qiskit.passmanager import BasePassManager
from qiskit.primitives import BaseSamplerV1, BaseSamplerV2, SamplerResult
from qiskit_algorithms import AmplificationProblem
from qiskit_algorithms.amplitude_amplifiers.grover import Grover
from qiskit_algorithms.utils import algorithm_globals
Expand Down Expand Up @@ -49,7 +50,8 @@ def __init__(
Union[QuadraticProgramConverter, List[QuadraticProgramConverter]]
] = None,
penalty: Optional[float] = None,
sampler: Optional[BaseSampler] = None,
sampler: Optional[Union[BaseSamplerV1, BaseSamplerV2]] = None,
passmanager: Optional[BasePassManager] = None,
) -> None:
"""
Args:
Expand All @@ -62,6 +64,7 @@ def __init__(
penalty: The penalty factor used in the default
:class:`~qiskit_optimization.converters.QuadraticProgramToQubo` converter
sampler: A Sampler to use for sampling the results of the circuits.
passmanager: A pass manager to use to transpile the circuits

Raises:
ValueError: If both a quantum instance and sampler are set.
Expand All @@ -73,6 +76,7 @@ def __init__(
self._circuit_results = {} # type: dict
self._converters = self._prepare_converters(converters, penalty)
self._sampler = sampler
self._passmanager = passmanager

def get_compatibility_msg(self, problem: QuadraticProgram) -> str:
"""Checks whether a given problem can be solved with this optimizer.
Expand Down Expand Up @@ -294,19 +298,28 @@ def _measure(self, circuit: QuantumCircuit) -> str:

def _get_prob_dist(self, qc: QuantumCircuit) -> Dict[str, float]:
"""Gets probabilities from a given backend."""
# Transpile the circuit
if self._passmanager:
qc = self._passmanager.run(qc)

# Execute job and filter results.
job = self._sampler.run([qc])

try:
result = job.result()
except Exception as exc:
raise QiskitOptimizationError("Sampler job failed.") from exc
quasi_dist = result.quasi_dists[0]
raw_prob_dist = {
k: v
for k, v in quasi_dist.binary_probabilities(qc.num_qubits).items()
if v >= self._MIN_PROBABILITY
}

if isinstance(result, SamplerResult):
# SamplerV1
prob_dist = result.quasi_dists[0].binary_probabilities(qc.num_qubits)
else:
# SamplerV2
counts = getattr(result[0].data, qc.cregs[0].name).get_counts()
shots = sum(counts.values())
prob_dist = {k: v / shots for k, v in counts.items()}

raw_prob_dist = {k: v for k, v in prob_dist.items() if v >= self._MIN_PROBABILITY}
prob_dist = {k[::-1]: v for k, v in raw_prob_dist.items()}
self._circuit_results = {i: v**0.5 for i, v in raw_prob_dist.items()}
return prob_dist
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2023.
# (C) Copyright IBM 2023, 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
Expand All @@ -14,7 +14,8 @@

from __future__ import annotations

from qiskit.primitives import BaseEstimator
from qiskit.passmanager import BasePassManager
from qiskit.primitives import BaseEstimatorV1, BaseEstimatorV2

from qiskit_optimization.exceptions import QiskitOptimizationError

Expand All @@ -24,14 +25,21 @@
class EncodingCommutationVerifier:
"""Class for verifying that the relaxation commutes with the objective function."""

def __init__(self, encoding: QuantumRandomAccessEncoding, estimator: BaseEstimator):
def __init__(
self,
encoding: QuantumRandomAccessEncoding,
estimator: BaseEstimatorV1 | BaseEstimatorV2,
passmanager: BasePassManager | None = None,
):
"""
Args:
encoding: The encoding to verify.
estimator: The estimator to use for the verification.
passmanager: The pass manager to transpile the circuits
"""
self._encoding = encoding
self._estimator = estimator
self._passmanager = passmanager

def __len__(self) -> int:
return 2**self._encoding.num_vars
Expand All @@ -48,6 +56,8 @@ def __getitem__(self, i: int) -> tuple[str, float, float]:
str_dvars = f"{i:0{encoding.num_vars}b}"
dvars = [int(b) for b in str_dvars]
encoded_bitstr_qc = encoding.state_preparation_circuit(dvars)
if self._passmanager:
encoded_bitstr_qc = self._passmanager.run(encoded_bitstr_qc)

# Evaluate the original objective function
problem = encoding.problem
Expand All @@ -58,13 +68,24 @@ def __getitem__(self, i: int) -> tuple[str, float, float]:
encoded_op = encoding.qubit_op
offset = encoding.offset

job = self._estimator.run([encoded_bitstr_qc], [encoded_op])

try:
encoded_obj_val = job.result().values[0] + offset
except Exception as exc:
raise QiskitOptimizationError(
"The primitive job to verify commutation failed!"
) from exc

return (str_dvars, obj_val, encoded_obj_val)
if isinstance(self._estimator, BaseEstimatorV1):
job = self._estimator.run([encoded_bitstr_qc], [encoded_op])

try:
encoded_obj_val = job.result().values[0] + offset
except Exception as exc:
raise QiskitOptimizationError(
"The primitive job to verify commutation failed!"
) from exc
else: # BaseEstimatorV2
job = self._estimator.run([(encoded_bitstr_qc, encoded_op)])

try:
result = job.result()
encoded_obj_val = result[0].data.evs.item() + offset
except Exception as exc:
raise QiskitOptimizationError(
"The primitive job to verify commutation failed!"
) from exc

return str_dvars, obj_val, encoded_obj_val
44 changes: 36 additions & 8 deletions qiskit_optimization/algorithms/qrao/magic_rounding.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@

import numpy as np
from qiskit import QuantumCircuit
from qiskit.primitives import BaseSampler
from qiskit.passmanager import BasePassManager
from qiskit.primitives import BaseSamplerV1, BaseSamplerV2, SamplerResult
from qiskit.quantum_info import SparsePauliOp
from qiskit_algorithms.exceptions import AlgorithmError

Expand Down Expand Up @@ -58,9 +59,10 @@ class MagicRounding(RoundingScheme):

def __init__(
self,
sampler: BaseSampler,
sampler: BaseSamplerV1 | BaseSamplerV2,
basis_sampling: str = "uniform",
seed: int | None = None,
passmanager: BasePassManager | None = None,
):
"""
Args:
Expand All @@ -76,6 +78,7 @@ def __init__(
sampling.
seed: Seed for random number generator, which is used to sample the
magic bases.
passmanager: Pass manager to transpile the circuits

Raises:
ValueError: If ``basis_sampling`` is not ``"uniform"`` or ``"weighted"``.
Expand All @@ -89,13 +92,23 @@ def __init__(
self._sampler = sampler
self._rng = np.random.default_rng(seed)
self._basis_sampling = basis_sampling
if self._sampler.options.get("shots") is None:
raise ValueError("Magic rounding requires a sampler configured with a number of shots.")
self._shots = sampler.options.shots
self._passmanager = passmanager
if isinstance(self._sampler, BaseSamplerV1):
if self._sampler.options.get("shots") is None:
raise ValueError(
"Magic rounding requires a sampler configured with a number of shots."
)
self._shots = sampler.options.shots
else: # BaseSamplerV2
if self._sampler.default_shots is None:
raise ValueError(
"Magic rounding requires a sampler configured with a number of shots."
)
self._shots = self._sampler.default_shots
super().__init__()

@property
def sampler(self) -> BaseSampler:
def sampler(self) -> BaseSamplerV1 | BaseSamplerV2:
"""Returns the Sampler used to sample the magic bases."""
return self._sampler

Expand Down Expand Up @@ -166,6 +179,8 @@ def _evaluate_magic_bases(
QiskitOptimizationError: If some of the results from the primitive job are not collected.
"""
circuits = self._make_circuits(circuit, bases, vars_per_qubit)
if self._passmanager:
circuits = self._passmanager.run(circuits)
# Execute each of the rotated circuits and collect the results
# Batch the circuits into jobs where each group has the same number of
# shots, so that you can wait for the queue as few times as possible if
Expand All @@ -182,14 +197,27 @@ def _evaluate_magic_bases(
circuit_indices_by_shots[shots].append(i)

for shots, indices in sorted(circuit_indices_by_shots.items(), reverse=True):
circuits_ = [circuits[i] for i in indices]
try:
job = self._sampler.run([circuits[i] for i in indices], shots=shots)
job = self._sampler.run(circuits_, shots=shots)
result = job.result()
except Exception as exc:
raise AlgorithmError(
"The primitive job to evaluate the magic state failed."
) from exc
counts_list = [dist.binary_probabilities() for dist in result.quasi_dists]

if isinstance(result, SamplerResult):
counts_list = [dist.binary_probabilities() for dist in result.quasi_dists]
else:
counts_list = [
getattr(res.data, circ.cregs[0].name).get_counts()
for res, circ in zip(result, circuits_)
]
counts_list = [
{k: v / sum(counts.values()) for k, v in counts.items()}
for counts in counts_list
]

if len(counts_list) != len(indices):
raise QiskitOptimizationError(
"Internal error: The number of circuits and the results from the primitive job "
Expand Down
35 changes: 35 additions & 0 deletions qiskit_optimization/compat/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""
Compatibility module (:mod:`qiskit_optimization.compat`)
=======================================================

Algorithms copied from qiskit-algorithms, which are compatible with Sampler V2.

.. currentmodule:: qiskit_optimization.compat

Algorithms
----------

.. autosummary::
:toctree: ../stubs/
:nosignatures:

SamplingVQE
QAOA

"""

from .qaoa import QAOA
from .sampling_vqe import SamplingVQE

__all__ = ["SamplingVQE", "QAOA"]
Loading
Loading