Skip to content

Commit

Permalink
local mode support for v2 (#1533)
Browse files Browse the repository at this point in the history
  • Loading branch information
jyu00 authored Mar 19, 2024
1 parent 83a39ad commit dc294e2
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 96 deletions.
2 changes: 1 addition & 1 deletion qiskit_ibm_runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import Session
from qiskit_ibm_runtime import Sampler
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
# Bell Circuit
Expand Down
59 changes: 36 additions & 23 deletions qiskit_ibm_runtime/base_primitive.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from .ibm_backend import IBMBackend
from .utils.default_session import get_cm_session
from .utils.deprecation import issue_deprecation_msg
from .utils.utils import validate_isa_circuits
from .utils.utils import validate_isa_circuits, is_simulator
from .constants import DEFAULT_DECODERS
from .qiskit_runtime_service import QiskitRuntimeService
from .fake_provider.local_service import QiskitRuntimeLocalService
Expand All @@ -56,15 +56,15 @@ class BasePrimitiveV2(ABC, Generic[OptionsT]):

def __init__(
self,
backend: Optional[Union[str, IBMBackend]] = None,
session: Optional[Union[Session, str, IBMBackend]] = None,
backend: Optional[Union[str, BackendV1, BackendV2]] = None,
session: Optional[Session] = None,
options: Optional[Union[Dict, OptionsT]] = None,
):
"""Initializes the primitive.
Args:
backend: Backend to run the primitive. This can be a backend name or an :class:`IBMBackend`
backend: Backend to run the primitive. This can be a backend name or a ``Backend``
instance. If a name is specified, the default account (e.g. ``QiskitRuntimeService()``)
is used.
Expand All @@ -75,31 +75,33 @@ def __init__(
:class:`qiskit_ibm_runtime.Session` context manager, then the session is used.
Otherwise if IBM Cloud channel is used, a default backend is selected.
options: Primitive options, see :class:`Options` for detailed description.
The ``backend`` keyword is still supported but is deprecated.
options: Primitive options, see :class:`qiskit_ibm_runtime.options.EstimatorOptions`
and :class:`qiskit_ibm_runtime.options.SamplerOptions` for detailed description
on estimator and sampler options, respectively.
Raises:
ValueError: Invalid arguments are given.
"""
self._session: Optional[Session] = None
self._service: QiskitRuntimeService = None
self._backend: Optional[IBMBackend] = None
self._service: QiskitRuntimeService | QiskitRuntimeLocalService = None
self._backend: Optional[BackendV1 | BackendV2] = None

self._set_options(options)

if isinstance(session, Session):
self._session = session
self._service = self._session.service
self._backend = self._service.backend(
name=self._session.backend(), instance=self._session._instance
)
self._backend = self._session._backend
return
elif session is not None:
elif session is not None: # type: ignore[unreachable]
raise ValueError("session must be of type Session or None")

if isinstance(backend, IBMBackend):
if isinstance(backend, IBMBackend): # type: ignore[unreachable]
self._service = backend.service
self._backend = backend
elif isinstance(backend, (BackendV1, BackendV2)):
self._service = QiskitRuntimeLocalService()
self._backend = backend
elif isinstance(backend, str):
self._service = (
QiskitRuntimeService()
Expand All @@ -123,6 +125,12 @@ def __init__(
raise ValueError(
"A backend or session must be specified when not using ibm_cloud channel."
)
issue_deprecation_msg(
"Not providing a backend is deprecated",
"0.22.0",
"Passing in a backend will be required, please provide a backend.",
3,
)

def _run(self, pubs: Union[list[EstimatorPub], list[SamplerPub]]) -> RuntimeJobV2:
"""Run the primitive.
Expand All @@ -142,13 +150,11 @@ def _run(self, pubs: Union[list[EstimatorPub], list[SamplerPub]]) -> RuntimeJobV

if self._backend:
for pub in pubs:
if (
getattr(self._backend, "target", None)
and not self._backend.configuration().simulator
):
if getattr(self._backend, "target", None) and not is_simulator(self._backend):
validate_isa_circuits([pub.circuit], self._backend.target)

self._backend.check_faulty(pub.circuit)
if isinstance(self._backend, IBMBackend):
self._backend.check_faulty(pub.circuit)

logger.info("Submitting job using options %s", primitive_options)

Expand All @@ -162,16 +168,23 @@ def _run(self, pubs: Union[list[EstimatorPub], list[SamplerPub]]) -> RuntimeJobV
)

if self._backend:
runtime_options["backend"] = self._backend.name
if "instance" not in runtime_options:
runtime_options["backend"] = self._backend
if "instance" not in runtime_options and isinstance(self._backend, IBMBackend):
runtime_options["instance"] = self._backend._instance

if isinstance(self._service, QiskitRuntimeService):
return self._service.run(
program_id=self._program_id(),
options=runtime_options,
inputs=primitive_inputs,
callback=options_dict.get("environment", {}).get("callback", None),
result_decoder=DEFAULT_DECODERS.get(self._program_id()),
)

return self._service.run(
program_id=self._program_id(),
program_id=self._program_id(), # type: ignore[arg-type]
options=runtime_options,
inputs=primitive_inputs,
callback=options_dict.get("environment", {}).get("callback", None),
result_decoder=DEFAULT_DECODERS.get(self._program_id()),
)

@property
Expand Down
3 changes: 1 addition & 2 deletions qiskit_ibm_runtime/estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ def __init__(
:class:`qiskit_ibm_runtime.Session` context manager, then the session is used.
Otherwise if IBM Cloud channel is used, a default backend is selected.
options: Primitive options, see :class:`Options` for detailed description.
The ``backend`` keyword is still supported but is deprecated.
options: Estimator options, see :class:`EstimatorOptions` for detailed description.
Raises:
NotImplementedError: If "q-ctrl" channel strategy is used.
Expand Down
90 changes: 68 additions & 22 deletions qiskit_ibm_runtime/fake_provider/local_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import logging
import copy
from typing import Dict, Union, Literal
import warnings
from dataclasses import asdict

from qiskit.utils import optionals
from qiskit.providers.backend import BackendV1, BackendV2
Expand All @@ -25,6 +27,7 @@

from ..runtime_options import RuntimeOptions
from ..ibm_backend import IBMBackend
from ..qiskit.primitives import BackendEstimatorV2, BackendSamplerV2 # type: ignore[attr-defined]

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -66,19 +69,20 @@ def run(
ValueError: If input is invalid.
NotImplementedError: If using V2 primitives.
"""
# qrt_options: RuntimeOptions = options
if isinstance(options, Dict):
qrt_options = RuntimeOptions(**options)
qrt_options = copy.deepcopy(options)
else:
qrt_options = options
qrt_options = asdict(options)

backend = qrt_options.pop("backend", None)

if program_id not in ["sampler", "estimator"]:
raise ValueError("Only sampler and estimator are supported in local testing mode.")
if isinstance(qrt_options.backend, IBMBackend):
if isinstance(backend, IBMBackend):
raise ValueError(
"Local testing mode is not supported when a cloud-based backend is used."
)
if isinstance(qrt_options.backend, str):
if isinstance(backend, str):
raise ValueError(
"Passing a backend name is not supported in local testing mode. "
"Please pass a backend instance."
Expand All @@ -94,25 +98,30 @@ def run(
if program_id == "estimator":
primitive_inputs["observables"] = inputs.pop("observables")
inputs.pop("parameters", None)

if optionals.HAS_AER:
# pylint: disable=import-outside-toplevel
from qiskit_aer.backends.aerbackend import AerBackend

if isinstance(backend, AerBackend):
return self._run_aer_primitive_v1(
primitive=program_id, options=inputs, inputs=primitive_inputs
)

return self._run_backend_primitive_v1(
backend=backend,
primitive=program_id,
options=inputs,
inputs=primitive_inputs,
)
else:
primitive_inputs = {"pubs": inputs.pop("pubs")}
raise NotImplementedError("V2 primitives are not supported in local mode.")

if optionals.HAS_AER:
# pylint: disable=import-outside-toplevel
from qiskit_aer.backends.aerbackend import AerBackend

if isinstance(qrt_options.backend, AerBackend):
return self._run_aer_primitive_v1(
primitive=program_id, options=inputs, inputs=primitive_inputs
)

return self._run_backend_primitive_v1(
backend=qrt_options.backend,
primitive=program_id,
options=inputs,
inputs=primitive_inputs,
)
return self._run_backend_primitive_v2(
backend=backend,
primitive=program_id,
options=inputs.get("options", {}),
inputs=primitive_inputs,
)

def _run_aer_primitive_v1(
self, primitive: Literal["sampler", "estimator"], options: dict, inputs: dict
Expand Down Expand Up @@ -197,3 +206,40 @@ def _run_backend_primitive_v1(

primitive_inst.set_transpile_options(**transpilation_options)
return primitive_inst.run(**inputs, **run_options)

def _run_backend_primitive_v2(
self,
backend: BackendV1 | BackendV2,
primitive: Literal["sampler", "estimator"],
options: dict,
inputs: dict,
) -> PrimitiveJob:
"""Run V2 backend primitive.
Args:
backend: The backend to run the primitive on.
primitive: Name of the primitive.
options: Primitive options to use.
inputs: Primitive inputs.
Returns:
The job object of the result of the primitive.
"""
options_copy = copy.deepcopy(options)

prim_options = {}
if seed_simulator := options_copy.pop("simulator", {}).pop("seed_simulator", None):
prim_options["seed_simulator"] = seed_simulator
if primitive == "sampler":
if default_shots := options_copy.pop("default_shots", None):
prim_options["default_shots"] = default_shots
primitive_inst = BackendSamplerV2(backend=backend, options=prim_options)
else:
if default_precision := options_copy.pop("default_precision", None):
prim_options["default_precision"] = default_precision
primitive_inst = BackendEstimatorV2(backend=backend, options=prim_options)

if options_copy:
warnings.warn(f"Options {options_copy} have no effect in local testing mode.")

return primitive_inst.run(**inputs)
5 changes: 2 additions & 3 deletions qiskit_ibm_runtime/options/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from qiskit.providers.backend import Backend

from ..utils.utils import is_simulator

if TYPE_CHECKING:
from ..options.options import BaseOptions
Expand All @@ -47,9 +48,7 @@ def set_default_error_levels(
Returns:
options with correct error level defaults.
"""
is_sim = False
if hasattr(backend, "configuration"):
is_sim = getattr(backend.configuration(), "simulator", False)
is_sim = is_simulator(backend)

if options.get("optimization_level") is None:
if is_sim and not options.get("simulator", {}).get("noise_model"):
Expand Down
3 changes: 1 addition & 2 deletions qiskit_ibm_runtime/sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ def __init__(
:class:`qiskit_ibm_runtime.Session` context manager, then the session is used.
Otherwise if IBM Cloud channel is used, a default backend is selected.
options: Primitive options, see :class:`Options` for detailed description.
The ``backend`` keyword is still supported but is deprecated.
options: Sampler options, see :class:`SamplerOptions` for detailed description.
Raises:
NotImplementedError: If "q-ctrl" channel strategy is used.
Expand Down
25 changes: 17 additions & 8 deletions qiskit_ibm_runtime/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
# that they have been altered from the originals.

"""General utility functions."""

from __future__ import annotations
import copy
import keyword
import logging
import os
import re
import hashlib
from queue import Queue
from threading import Condition
from typing import List, Optional, Any, Dict, Union, Tuple, Sequence
Expand All @@ -29,9 +30,24 @@
from ibm_platform_services import ResourceControllerV2 # pylint: disable=import-error
from qiskit.circuit import QuantumCircuit
from qiskit.transpiler import Target
from qiskit.providers.backend import BackendV1, BackendV2
from qiskit_ibm_runtime.exceptions import IBMInputValueError


def is_simulator(backend: BackendV1 | BackendV2) -> bool:
"""Return true if the backend is a simulator.
Args:
backend: Backend to check.
Returns:
True if backend is a simulator.
"""
if hasattr(backend, "configuration"):
return getattr(backend.configuration(), "simulator", False)
return getattr(backend, "simulator", False)


def is_isa_circuit(circuit: QuantumCircuit, target: Target) -> str:
"""Checks if the circuit is an ISA circuit, meaning that it has a layout and that it
only uses instructions that exist in the target.
Expand Down Expand Up @@ -310,13 +326,6 @@ def _filter_value(data: Dict[str, Any], filter_keys: List[Union[str, Tuple[str,
_filter_value(value, filter_keys)


def _hash(hash_str: str) -> str:
"""Hashes and returns a digest.
blake2s is supposedly faster than SHAs.
"""
return hashlib.blake2s(hash_str.encode()).hexdigest()


class RefreshQueue(Queue):
"""A queue that replaces the oldest item with the new item being added when full.
Expand Down
10 changes: 3 additions & 7 deletions test/unit/test_estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@

"""Tests for estimator class."""

from unittest.mock import MagicMock

import numpy as np
from ddt import data, ddt

Expand All @@ -28,7 +26,6 @@
from ..ibm_test_case import IBMTestCase
from ..utils import (
get_mocked_backend,
MockSession,
dict_paritally_equal,
transpile_pubs,
get_primitive_inputs,
Expand Down Expand Up @@ -115,11 +112,10 @@ def test_unsupported_values_for_estimator_options(self):

def test_pec_simulator(self):
"""Test error is raised when using pec on simulator without coupling map."""
backend = get_mocked_backend()
backend.configuration().simulator = True

session = MagicMock(spec=MockSession)
session.service.backend().configuration().simulator = True

inst = EstimatorV2(session=session, options={"resilience": {"pec_mitigation": True}})
inst = EstimatorV2(backend=backend, options={"resilience": {"pec_mitigation": True}})
with self.assertRaises(ValueError) as exc:
inst.run(**get_primitive_inputs(inst))
self.assertIn("coupling map", str(exc.exception))
Expand Down
Loading

0 comments on commit dc294e2

Please sign in to comment.