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

Add support for noise model and level 1 data to local sampler #1990

Merged
merged 1 commit into from
Nov 19, 2024
Merged
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
37 changes: 36 additions & 1 deletion qiskit_ibm_runtime/fake_provider/local_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,43 @@ def _run_backend_primitive_v2(
options_copy = copy.deepcopy(options)

prim_options = {}
if seed_simulator := options_copy.pop("simulator", {}).pop("seed_simulator", None):
sim_options = options_copy.get("simulator", {})
if seed_simulator := sim_options.pop("seed_simulator", None):
prim_options["seed_simulator"] = seed_simulator
if primitive == "sampler":
# Create a dummy primitive to check which options it supports
dummy_prim = BackendSamplerV2(backend=backend)
use_run_options = hasattr(dummy_prim.options, "run_options")

run_options = {}
if use_run_options and "run_options" in options_copy:
run_options = options_copy.pop("run_options")
if use_run_options and "noise_model" in sim_options:
run_options["noise_model"] = sim_options.pop("noise_model")

if default_shots := options_copy.pop("default_shots", None):
prim_options["default_shots"] = default_shots
if use_run_options and (
meas_type := options_copy.get("execution", {}).pop("meas_type", None)
):
if meas_type == "classified":
run_options["meas_level"] = 2
elif meas_type == "kerneled":
run_options["meas_level"] = 1
run_options["meas_return"] = "single"
elif meas_type == "avg_kerneled":
run_options["meas_level"] = 1
run_options["meas_return"] = "avg"
else:
# Put unexepcted meas_type back so it is in the warning below
options_copy["execution"]["meas_type"] = meas_type

if not options_copy["execution"]:
del options_copy["execution"]

if run_options:
prim_options["run_options"] = run_options

primitive_inst = BackendSamplerV2(backend=backend, options=prim_options)
else:
if default_shots := options_copy.pop("default_shots", None):
Expand All @@ -229,6 +261,9 @@ def _run_backend_primitive_v2(
prim_options["default_precision"] = default_precision
primitive_inst = BackendEstimatorV2(backend=backend, options=prim_options)

if not sim_options:
# Pop to avoid warning below if all contents were popped above
options_copy.pop("simulator", None)
if options_copy:
warnings.warn(f"Options {options_copy} have no effect in local testing mode.")

Expand Down
13 changes: 13 additions & 0 deletions release-notes/unreleased/1990.feat.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Add support for noise model and level 1 data to local sampler

The ``simulator.noise_model`` option of :class:`~.SamplerV2` is now passed
through to the :class:`~qiskit.primitives.BackendSamplerV2` as a `noise_model`
option under `run_options` if the primitive supports the `run_options` option
(support was added in Qiskit 1.3).

Similarly, the ``execution.meas_type`` option of :class:`~.SamplerV2` is now
translated into ``meas_level`` and ``meas_return`` options under
``run_options`` of the :class:`~qiskit.primitives.BackendSamplerV2` if it
supports ``run_options``. This change allows support for level 1 data in local
testing mode, where previously the only level 2 (classified) data was
supported.
113 changes: 113 additions & 0 deletions test/unit/test_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@
from unittest.mock import MagicMock

from ddt import data, ddt, named_data
from packaging.version import Version, parse as parse_version
import numpy as np

from qiskit.version import get_version_info as get_qiskit_version_info
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.primitives.containers.sampler_pub import SamplerPub
from qiskit.circuit import Parameter
from qiskit.circuit.library import RealAmplitudes
from qiskit.providers import BackendV2, Options
from qiskit.result import Result
from qiskit.result.models import ExperimentResult, ExperimentResultData
from qiskit.transpiler import Target
from qiskit_ibm_runtime import Session, SamplerV2, SamplerOptions, IBMInputValueError
from qiskit_ibm_runtime.fake_provider import FakeFractionalBackend, FakeSherbrooke, FakeCusco

Expand Down Expand Up @@ -315,3 +321,110 @@ def test_rzz_validates_only_for_fixed_angles(self):
circ.rzz(2 * param, 0, 1)
# Should run without an error
SamplerV2(backend).run(pubs=[(circ, [0.5])])

@data(
"classified",
"kerneled",
"avg_kerneled",
)
def test_backend_run_options(self, meas_type):
"""Test translation of sampler options into backend run options"""

# This test is checking that meas_level, meas_return, and noise_model
# get through the backend's run() call when SamplerV2 falls back to
# BackendSamplerV2 in local mode. To do this, it creates a dummy
# backend class that returns a result of the right format so that the
# sampler execution completes successfully.

if parse_version(get_qiskit_version_info()) < Version("1.3.0rc1"):
self.skipTest("Feature not supported on this version of Qiskit")

class DummyJob:
"""Enough of a job class to return a result"""

def __init__(self, run_options):
self.run_options = run_options

def result(self):
"""Return result object"""
shots = self.run_options["shots"]

if self.run_options["meas_level"] == 1:
counts = None
if self.run_options["meas_return"] == "single":
memory = [[[0.0, 0.0]] * shots]
else:
memory = [[0.0, 0.0]]
else:
counts = {"0": shots}
memory = ["0"] * shots
result = Result(
backend_name="test_backend",
backend_version="0.0",
qobj_id="xyz",
job_id="123",
success=True,
results=[
ExperimentResult(
shots=100,
success=True,
data=ExperimentResultData(memory=memory, counts=counts),
)
],
)
return result

class DummyBackend(BackendV2):
"""Test backend that saves run options into the result"""

max_circuits = 1
# The backend gets cloned inside of the sampler execution code, so
# it is difficult to get a handle on the actual backend used to run
# the job. Here we save the run options into a class level variable
# that can be checked after run() is called.
used_run_options = {}

def __init__(self, **kwargs):
super().__init__(**kwargs)

self._target = Target()

@classmethod
def _default_options(cls):
return Options()

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

def run(self, run_input, **run_options):
nonlocal used_run_options
DummyBackend.used_run_options = run_options
return DummyJob(run_options)

backend = DummyBackend()

circ = QuantumCircuit(1, 1)
circ.measure(0, 0)

sampler = SamplerV2(mode=backend)
sampler.options.simulator.noise_model = {"name": "some_model"}
sampler.options.execution.meas_type = meas_type

job = sampler.run([circ], shots=100)
result = job.result()

used_run_options = DummyBackend.used_run_options
self.assertDictEqual(used_run_options["noise_model"], {"name": "some_model"})

if meas_type == "classified":
self.assertEqual(used_run_options["meas_level"], 2)
self.assertDictEqual(result[0].data.c.get_counts(), {"0": 100})
elif meas_type == "kerneled":
self.assertEqual(used_run_options["meas_level"], 1)
self.assertEqual(used_run_options["meas_return"], "single")
self.assertTrue(np.array_equal(result[0].data.c, np.zeros((1, 100))))
else: # meas_type == "avg_kerneled"
self.assertEqual(used_run_options["meas_level"], 1)
self.assertEqual(used_run_options["meas_return"], "avg")
self.assertTrue(np.array_equal(result[0].data.c, np.zeros((1,))))