Skip to content
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

Fix QuantumInstance compatibility with BackendV2 #7563

Merged
merged 14 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from 13 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
9 changes: 9 additions & 0 deletions qiskit/providers/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,15 @@ def options(self):
"""
return self._options

@property
def provider(self):
"""Return the backend Provider.

Returns:
Provider: the Provider responsible for the backend.
"""
return self._provider

@abstractmethod
def run(self, run_input, **options):
"""Run on the backend.
Expand Down
41 changes: 41 additions & 0 deletions qiskit/test/mock/fake_backend_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@
UGate,
ECRGate,
RXGate,
SXGate,
XGate,
RZGate,
)
from qiskit.providers.backend import BackendV2, QubitProperties
from qiskit.providers.options import Options
from qiskit.transpiler import Target, InstructionProperties
from qiskit.providers.basicaer.qasm_simulator import QasmSimulatorPy


class FakeBackendV2(BackendV2):
Expand Down Expand Up @@ -176,3 +180,40 @@ def qubit_properties(self, qubit):
if isinstance(qubit, int):
return self._qubit_properties[qubit]
return [self._qubit_properties[i] for i in qubit]


class FakeBackendSimple(BackendV2):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this class a temporary solution for while FakeBackendV2 doesn't have .run method implemented? What prevents us to implemented .run method to FakeBackendV2 now? Is it blocked by #7391 that I am working on?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is just to have something until #7391 is implemented. But also because the goal of this PR is to fix support I wanted to have something smaller to backport than what #7391 will end up being. To test the quantum instance works with BackendV2 there needs to be a backend implementing the v2 interface with run() defined. So I threw this together so we can have a minimal backend that does this.

"""A fake simple backend that wraps BasicAer to implement run()."""

def __init__(self):
super().__init__(
None,
name="FakeSimpleV2",
description="A fake simple BackendV2 example",
online_date=datetime.datetime.utcnow(),
backend_version="0.0.1",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit confused. There seem to be two different backend_version. One is the integer-only versioned backend which is later referenced as backend_interface_version: 0 for LegacyBackend, 1 for BackendV1 and 2 for BackendV2. the other one is the backend_version has SemVer format like 0.0.1 that comes from conf.json from real backends? Am I right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It is explained pretty clearly in a docstring, but I forgot which and just now I was unable to locate it quickly. As I was digging through the code it became clear that this is what's happening even before I found the docstring. The semvar-ish version is for the particular backends, i.e. child classes. The integer version is for the parent classes of these.

It would be nice to use a completely different word than "version" for one of these, to avoid confusion. But, @mtreinish pointed out that that would require a deprecation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you thinking of the backend_version entry in the BackendV1->V2 migration guide: https://qiskit.org/documentation/apidoc/providers.html#migrating-between-backend-api-versions it might be somewhere else too.

But yeah, @jlapeyre is correct as to the difference. The version attribute is set in the abstract class definition as a class variable and is used to indicate which version of that interface a particular backend object implements. While the backend_version attribute is arbitrary metadata that a backend can optionally supply when it's implemented to indicate any version information. The semver like string is just what IBM happen to uses for its backends (and that comes from the backend configuration json payload), but it can be any string as it's up to each backend. I agree the dual use of version can be confusing, but it is something that's not easy to change since this is part of a defined interface and it would require a deprecation cycle to change (and honestly I think it'll just be better to put it on the list for the inevitable BackendV3 and wait for that)

)
self._lam = Parameter("lambda")
self._target = Target(num_qubits=20)
self._target.add_instruction(SXGate())
self._target.add_instruction(XGate())
self._target.add_instruction(RZGate(self._lam))
self._target.add_instruction(CXGate())
self._target.add_instruction(Measure())
self._runner = QasmSimulatorPy()

@property
def target(self):
return self._target

@property
def max_circuits(self):
return None

@classmethod
def _default_options(cls):
return QasmSimulatorPy._default_options()

def run(self, run_input, **options):
self._runner._options = self._options
return self._runner.run(run_input, **options)
43 changes: 37 additions & 6 deletions qiskit/utils/backend_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ def __init__(self) -> None:
_PROVIDER_CHECK = ProviderCheck()


def _get_backend_interface_version(backend):
"""Get the backend version int."""
backend_interface_version = getattr(backend, "version", None)
# Handle deprecated BaseBackend based backends which have a version()
# method
if not isinstance(backend_interface_version, int):
backend_interface_version = 0
return backend_interface_version


def _get_backend_provider(backend):
backend_interface_version = _get_backend_interface_version(backend)
if backend_interface_version > 1:
provider = backend.provider
else:
provider = backend.provider()
return provider


def has_ibmq():
"""Check if IBMQ is installed"""
if not _PROVIDER_CHECK.checked_ibmq:
Expand Down Expand Up @@ -78,7 +97,7 @@ def is_aer_provider(backend):
if has_aer():
from qiskit.providers.aer import AerProvider

if isinstance(backend.provider(), AerProvider):
if isinstance(_get_backend_provider(backend), AerProvider):
return True
from qiskit.providers.aer.backends.aerbackend import AerBackend

Expand All @@ -97,7 +116,7 @@ def is_basicaer_provider(backend):
"""
from qiskit.providers.basicaer import BasicAerProvider

return isinstance(backend.provider(), BasicAerProvider)
return isinstance(_get_backend_provider(backend), BasicAerProvider)


def is_ibmq_provider(backend):
Expand All @@ -111,7 +130,7 @@ def is_ibmq_provider(backend):
if has_ibmq():
from qiskit.providers.ibmq.accountprovider import AccountProvider

return isinstance(backend.provider(), AccountProvider)
return isinstance(_get_backend_provider(backend), AccountProvider)

return False

Expand Down Expand Up @@ -144,7 +163,13 @@ def is_statevector_backend(backend):
return True
if isinstance(backend, AerSimulator) and backend.name() == "aer_simulator_statevector":
return True
return backend.name().startswith("statevector") if backend is not None else False
if backend is None:
return False
backend_interface_version = _get_backend_interface_version(backend)
if backend_interface_version <= 1:
return backend.name().startswith("statevector")
else:
return backend.name.startswith("statevector")


def is_simulator_backend(backend):
Expand All @@ -156,7 +181,10 @@ def is_simulator_backend(backend):
Returns:
bool: True is a simulator
"""
return backend.configuration().simulator
backend_interface_version = _get_backend_interface_version(backend)
if backend_interface_version <= 1:
return backend.configuration().simulator
return False
Comment on lines -159 to +187
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function (is_simulator_backend) doesn't look especially accurate, but I'm guessing we're not really bothered about that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it's more that BackendV2 doesn't have a simulator bool attribute in the same way as the BackendConfiguration does. It was never really an actionable flag anyway, if simulator is set to True what is a backend consumer to do with it?

That being said the code that uses this in the QuantumInstance just makes incorrect assumptions about it being aer xor basicar if this returns True and either can use a noise model, and also disable retry logic (which also is similarly not-portable).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, let's just leave it as-is then.



def is_local_backend(backend):
Expand All @@ -168,7 +196,10 @@ def is_local_backend(backend):
Returns:
bool: True is a local backend
"""
return backend.configuration().local
backend_interface_version = _get_backend_interface_version(backend)
if backend_interface_version <= 1:
return backend.configuration().local
return False
Comment on lines -171 to +202
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Similar question to is_simulator_backend - the version 2+ check seems inaccurate, but we might not care)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically the same answer as is_simulator_backend , BackendV2 doesn't have a local attribute the same was as BackendConfiguration. It's also not really user actionable, like what does a consumer of backend do with this information? It doesn't really change anything to know if it's a local backend vs a remote one. The closest thing in BackendV2 to this is that the JobV1 class has a provision for async or sync jobs which could be used to influence behavior.

That being said all this does in QuantumInstance is to influence the max # of circuits per job (to fallback to max list size if it's local) because it's making an incorrect assumption that the local backend is basically aer or basic aer and that local backends always accept an arbitrary number of circuits per job. This doesn't make a difference in V2, because the .max_circuits attribute is explicitly defined as returning None if there is no limit to the number of circuits per job, regardless of whether it's None or not.



def is_aer_qasm(backend):
Expand Down
35 changes: 22 additions & 13 deletions qiskit/utils/quantum_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
is_aer_qasm,
is_basicaer_provider,
support_backend_options,
_get_backend_provider,
_get_backend_interface_version,
Comment on lines +35 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're importing them elsewhere, they kind of ought to be public.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mostly so we don't have to backport a new public interface. We can promote it on main if we want

)
from qiskit.utils.mitigation import (
CompleteMeasFitter,
Expand Down Expand Up @@ -239,15 +241,16 @@ def __init__(
QiskitError: set backend_options but the backend does not support that
"""
self._backend = backend
self._backend_interface_version = _get_backend_interface_version(self._backend)
self._pass_manager = pass_manager
self._bound_pass_manager = bound_pass_manager

# if the shots are none, try to get them from the backend
if shots is None:
from qiskit.providers.basebackend import BaseBackend # pylint: disable=cyclic-import
from qiskit.providers.backend import BackendV1 # pylint: disable=cyclic-import
from qiskit.providers.backend import Backend # pylint: disable=cyclic-import

if isinstance(backend, (BaseBackend, BackendV1)):
if isinstance(backend, (BaseBackend, Backend)):
if hasattr(backend, "options"): # should always be true for V1
backend_shots = backend.options.get("shots", 1024)
if shots != backend_shots:
Expand Down Expand Up @@ -280,9 +283,12 @@ def __init__(
self._run_config = run_config

# setup backend config
basis_gates = basis_gates or backend.configuration().basis_gates
coupling_map = coupling_map or getattr(backend.configuration(), "coupling_map", None)
self._backend_config = {"basis_gates": basis_gates, "coupling_map": coupling_map}
if self._backend_interface_version <= 1:
basis_gates = basis_gates or backend.configuration().basis_gates
coupling_map = coupling_map or getattr(backend.configuration(), "coupling_map", None)
self._backend_config = {"basis_gates": basis_gates, "coupling_map": coupling_map}
else:
self._backend_config = {}

# setup compile config
self._compile_config = {
Expand All @@ -306,7 +312,7 @@ def __init__(
"The noise model is not supported "
"on the selected backend {} ({}) "
"only certain backends, such as Aer qasm simulator "
"support noise.".format(self.backend_name, self._backend.provider())
"support noise.".format(self.backend_name, _get_backend_provider(self._backend))
)

# setup backend options for run
Expand Down Expand Up @@ -374,7 +380,7 @@ def __str__(self) -> str:
info = f"\nQiskit Terra version: {terra_version}\n"
info += "Backend: '{} ({})', with following setting:\n{}\n{}\n{}\n{}\n{}\n{}".format(
self.backend_name,
self._backend.provider(),
_get_backend_provider(self._backend),
self._backend_config,
self._compile_config,
self._run_config,
Expand Down Expand Up @@ -505,10 +511,10 @@ def execute(self, circuits, had_transpiled: bool = False):
# transpile here, the method always returns a copied list
circuits = self.transpile(circuits)

from qiskit.providers import BackendV1
from qiskit.providers import Backend

circuit_job = isinstance(self._backend, BackendV1)
if self.is_statevector and self._backend.name() == "aer_simulator_statevector":
circuit_job = isinstance(self._backend, Backend)
if self.is_statevector and self.backend_name == "aer_simulator_statevector":
try:
from qiskit.providers.aer.library import SaveStatevector

Expand Down Expand Up @@ -836,7 +842,7 @@ def set_config(self, **kwargs):
if not support_backend_options(self._backend):
raise QiskitError(
"backend_options can not be used with this backend "
"{} ({}).".format(self.backend_name, self._backend.provider())
"{} ({}).".format(self.backend_name, _get_backend_provider(self._backend))
)

if k in QuantumInstance._BACKEND_OPTIONS_QASM_ONLY and self.is_statevector:
Expand All @@ -853,7 +859,7 @@ def set_config(self, **kwargs):
raise QiskitError(
"The noise model is not supported on the selected backend {} ({}) "
"only certain backends, such as Aer qasm support "
"noise.".format(self.backend_name, self._backend.provider())
"noise.".format(self.backend_name, _get_backend_provider(self._backend))
)

self._noise_config[k] = v
Expand Down Expand Up @@ -967,7 +973,10 @@ def backend(self):
@property
def backend_name(self):
"""Return backend name."""
return self._backend.name()
if self._backend_interface_version <= 1:
return self._backend.name()
else:
return self._backend.name

@property
def is_statevector(self):
Expand Down
26 changes: 21 additions & 5 deletions qiskit/utils/run_circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
is_simulator_backend,
is_local_backend,
is_ibmq_provider,
_get_backend_interface_version,
)

MAX_CIRCUITS_PER_JOB = os.environ.get("QISKIT_AQUA_MAX_CIRCUITS_PER_JOB", None)
Expand Down Expand Up @@ -277,7 +278,11 @@ def run_qobj(
if is_local_backend(backend):
max_circuits_per_job = sys.maxsize
else:
max_circuits_per_job = backend.configuration().max_experiments
backend_interface_version = _get_backend_interface_version(backend)
if backend_interface_version <= 1:
max_circuits_per_job = backend.configuration().max_experiments
else:
max_circuits_per_job = backend.max_circuits

# split qobj if it exceeds the payload of the backend

Expand Down Expand Up @@ -470,18 +475,29 @@ def run_circuits(
Raises:
QiskitError: Any error except for JobError raised by Qiskit Terra
"""
backend_interface_version = _get_backend_interface_version(backend)

backend_options = backend_options or {}
noise_config = noise_config or {}
run_config = run_config or {}
with_autorecover = not is_simulator_backend(backend)
if backend_interface_version <= 1:
with_autorecover = not is_simulator_backend(backend)
else:
with_autorecover = False

if MAX_CIRCUITS_PER_JOB is not None:
max_circuits_per_job = int(MAX_CIRCUITS_PER_JOB)
else:
if is_local_backend(backend):
max_circuits_per_job = sys.maxsize
if backend_interface_version <= 1:
if is_local_backend(backend):
max_circuits_per_job = sys.maxsize
else:
max_circuits_per_job = backend.configuration().max_experiments
else:
max_circuits_per_job = backend.configuration().max_experiments
if backend.max_circuits is not None:
max_circuits_per_job = backend.max_circuits
else:
max_circuits_per_job = sys.maxsize

if len(circuits) > max_circuits_per_job:
jobs = []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
fixes:
- |
Added a missing :attr:`.BackendV2.provider` attribute to implementations
of the :class:`.BackendV2` abstract class. Previously, :class:`.BackendV2`
backends could be initialized with a provider but that was not accesible
to users.
- |
Fixed support for the :class:`.QuantumInstance` class when running with
a :class:`.BackendV2` backend. Previously, attempting to use a
:class:`.QuantumInstance` with a :class:`.BackendV2` would have resulted in
an error.
Loading