Skip to content

Commit

Permalink
Update v2 transpilation options (#1373)
Browse files Browse the repository at this point in the history
* transpilation options

* Update test/unit/test_ibm_primitives_v2.py

Co-authored-by: Kevin Tian <kt474@cornell.edu>

---------

Co-authored-by: Kevin Tian <kt474@cornell.edu>
  • Loading branch information
jyu00 and kt474 authored Feb 5, 2024
1 parent 1bae150 commit f9bcb47
Show file tree
Hide file tree
Showing 10 changed files with 81 additions and 113 deletions.
2 changes: 1 addition & 1 deletion qiskit_ibm_runtime/estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def _validate_options(self, options: dict) -> None:
# TODO: Server should have different optimization/resilience levels for simulator

if (
options["resilience_level"] == 3
options.get("resilience", {}).get("pec_mitigation", False) is True
and self._backend is not None
and self._backend.configuration().simulator is True
and not options["simulator"]["coupling_map"]
Expand Down
39 changes: 7 additions & 32 deletions qiskit_ibm_runtime/options/estimator_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
skip_unset_validation,
)
from .execution_options import ExecutionOptionsV2
from .transpilation_options import TranspilationOptions
from .transpilation_options import TranspilationOptionsV2
from .resilience_options import ResilienceOptionsV2
from .twirling_options import TwirlingOptions
from .options import OptionsV2
Expand All @@ -32,24 +32,14 @@
from ..qiskit.primitives.options import primitive_dataclass

DDSequenceType = Literal["XX", "XpXm", "XY4"]
MAX_RESILIENCE_LEVEL: int = 2


@primitive_dataclass
class EstimatorOptions(OptionsV2):
"""Options for EstimatorV2.
Args:
optimization_level: How much optimization to perform on the circuits.
Higher levels generate more optimized circuits,
at the expense of longer transpilation times. This is based on the
``optimization_level`` parameter in qiskit-terra but may include
backend-specific optimization. Default: 1.
* 0: no optimization
* 1: light optimization
* 2: heavy optimization
* 3: even heavier optimization
resilience_level: How much resilience to build against errors.
Higher levels generate more accurate results,
at the expense of longer processing times. Default: 1.
Expand Down Expand Up @@ -88,40 +78,25 @@ class EstimatorOptions(OptionsV2):
"""

_MAX_OPTIMIZATION_LEVEL: int = Field(3, frozen=True) # pylint: disable=invalid-name
_MAX_RESILIENCE_LEVEL: int = Field(3, frozen=True) # pylint: disable=invalid-name

# Sadly we cannot use pydantic's built in validation because it won't work on Unset.
optimization_level: Union[UnsetType, int] = Unset
resilience_level: Union[UnsetType, int] = Unset
dynamical_decoupling: Union[UnsetType, DDSequenceType] = Unset
seed_estimator: Union[UnsetType, int] = Unset
transpilation: Union[TranspilationOptions, Dict] = Field(default_factory=TranspilationOptions)
transpilation: Union[TranspilationOptionsV2, Dict] = Field(
default_factory=TranspilationOptionsV2
)
resilience: Union[ResilienceOptionsV2, Dict] = Field(default_factory=ResilienceOptionsV2)
execution: Union[ExecutionOptionsV2, Dict] = Field(default_factory=ExecutionOptionsV2)
twirling: Union[TwirlingOptions, Dict] = Field(default_factory=TwirlingOptions)
experimental: Union[UnsetType, dict] = Unset

@field_validator("optimization_level")
@classmethod
@skip_unset_validation
def _validate_optimization_level(cls, optimization_level: int) -> int:
"""Validate optimization_leve."""
if not 0 <= optimization_level <= EstimatorOptions._MAX_OPTIMIZATION_LEVEL:
raise ValueError(
"Invalid optimization_level. Valid range is "
f"0-{EstimatorOptions._MAX_OPTIMIZATION_LEVEL}"
)
return optimization_level

@field_validator("resilience_level")
@classmethod
@skip_unset_validation
def _validate_resilience_level(cls, resilience_level: int) -> int:
"""Validate resilience_level."""
if not 0 <= resilience_level <= EstimatorOptions._MAX_RESILIENCE_LEVEL:
if not 0 <= resilience_level <= MAX_RESILIENCE_LEVEL:
raise ValueError(
"Invalid optimization_level. Valid range is "
f"0-{EstimatorOptions._MAX_RESILIENCE_LEVEL}"
"Invalid optimization_level. Valid range is " f"0-{MAX_RESILIENCE_LEVEL}"
)
return resilience_level
1 change: 0 additions & 1 deletion qiskit_ibm_runtime/options/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ def _set_if_exists(name: str, _inputs: dict, _options: dict) -> None:
coupling_map = list(map(list, coupling_map.get_edges()))
inputs["transpilation"].update(
{
"optimization_level": options_copy.get("optimization_level", Unset),
"coupling_map": coupling_map,
"basis_gates": sim_options.get("basis_gates", Unset),
}
Expand Down
35 changes: 5 additions & 30 deletions qiskit_ibm_runtime/options/sampler_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@

from typing import Union, Literal

from pydantic import Field, field_validator
from pydantic import Field

from .utils import (
Dict,
Unset,
UnsetType,
skip_unset_validation,
)
from .execution_options import ExecutionOptionsV2
from .transpilation_options import TranspilationOptions
from .transpilation_options import TranspilationOptionsV2
from .twirling_options import TwirlingOptions
from .options import OptionsV2

Expand All @@ -38,17 +37,6 @@ class SamplerOptions(OptionsV2):
"""Options for v2 Sampler.
Args:
optimization_level: How much optimization to perform on the circuits.
Higher levels generate more optimized circuits,
at the expense of longer transpilation times. This is based on the
``optimization_level`` parameter in qiskit-terra but may include
backend-specific optimization. Default: 1.
* 0: no optimization
* 1: light optimization
* 2: heavy optimization
* 3: even heavier optimization
dynamical_decoupling: Optional, specify a dynamical decoupling sequence to use.
Allowed values are ``"XX"``, ``"XpXm"``, ``"XY4"``.
Default: None
Expand All @@ -68,24 +56,11 @@ class SamplerOptions(OptionsV2):
"""

_MAX_OPTIMIZATION_LEVEL: int = Field(3, frozen=True) # pylint: disable=invalid-name

# Sadly we cannot use pydantic's built in validation because it won't work on Unset.
optimization_level: Union[UnsetType, int] = Unset
dynamical_decoupling: Union[UnsetType, DDSequenceType] = Unset
transpilation: Union[TranspilationOptions, Dict] = Field(default_factory=TranspilationOptions)
transpilation: Union[TranspilationOptionsV2, Dict] = Field(
default_factory=TranspilationOptionsV2
)
execution: Union[ExecutionOptionsV2, Dict] = Field(default_factory=ExecutionOptionsV2)
twirling: Union[TwirlingOptions, Dict] = Field(default_factory=TwirlingOptions)
experimental: Union[UnsetType, dict] = Unset

@field_validator("optimization_level")
@classmethod
@skip_unset_validation
def _validate_optimization_level(cls, optimization_level: int) -> int:
"""Validate optimization_leve."""
if not 0 <= optimization_level <= SamplerOptions._MAX_OPTIMIZATION_LEVEL:
raise ValueError(
"Invalid optimization_level. Valid range is "
f"0-{SamplerOptions._MAX_OPTIMIZATION_LEVEL}"
)
return optimization_level
32 changes: 32 additions & 0 deletions qiskit_ibm_runtime/options/transpilation_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"sabre",
"none",
]
MAX_OPTIMIZATION_LEVEL: int = 1


@primitive_dataclass
Expand Down Expand Up @@ -74,3 +75,34 @@ def _validate_approximation_degree(cls, degree: float) -> float:
"and 1.0 (no approximation)"
)
return degree


@primitive_dataclass
class TranspilationOptionsV2:
"""Transpilation options for v2 primitives.
Args:
skip_transpilation: Whether to skip transpilation. Default is False.
optimization_level: How much optimization to perform on the circuits.
Higher levels generate more optimized circuits,
at the expense of longer transpilation times.
* 0: no optimization
* 1: light optimization
"""

skip_transpilation: bool = False
optimization_level: Union[UnsetType, int] = Unset

@field_validator("optimization_level")
@classmethod
@skip_unset_validation
def _validate_optimization_level(cls, optimization_level: int) -> int:
"""Validate optimization_leve."""
if not 0 <= optimization_level <= MAX_OPTIMIZATION_LEVEL:
raise ValueError(
"Invalid optimization_level. Valid range is " f"0-{MAX_OPTIMIZATION_LEVEL}"
)
return optimization_level
9 changes: 9 additions & 0 deletions test/unit/mock/fake_runtime_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ def __init__(
self._job_kwargs = job_kwargs or {}
self._channel = channel
self.session_time = 0
self._sessions = set()

# Setup the available backends
if not backend_specs:
Expand Down Expand Up @@ -342,6 +343,8 @@ def program_run(
)
self.session_time = session_time
self._jobs[job_id] = job
if start_session:
self._sessions.add(job_id)
return {"id": job_id, "backend": backend_name}

def job_get(self, job_id: str, exclude_params: bool = True) -> Any:
Expand Down Expand Up @@ -455,6 +458,12 @@ def backend_pulse_defaults(self, backend_name: str) -> Dict[str, Any]:
"""Return the pulse defaults of a backend."""
return self._find_backend(backend_name).defaults

def close_session(self, session_id: str) -> None:
"""Close the session."""
if session_id not in self._sessions:
raise ValueError(f"Session {session_id} not found.")
self._sessions.remove(session_id)

def _find_backend(self, backend_name):
for back in self._backends:
if back.name == backend_name:
Expand Down
18 changes: 7 additions & 11 deletions test/unit/test_estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ def test_run_program_inputs(self, in_pubs):
def test_unsupported_values_for_estimator_options(self):
"""Test exception when options levels are not supported."""
options_bad = [
{"resilience_level": 4, "optimization_level": 3},
{"optimization_level": 4, "resilience_level": 2},
{"resilience_level": 3},
{"resilience_level": 4},
]

with Session(
Expand All @@ -108,13 +108,13 @@ def test_unsupported_values_for_estimator_options(self):
inst.options.update(**bad_opt)
self.assertIn(list(bad_opt.keys())[0], str(exc.exception))

def test_res_level3_simulator(self):
"""Test the correct default error levels are used."""
def test_pec_simulator(self):
"""Test error is raised when using pec on simulator without coupling map."""

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

inst = EstimatorV2(session=session, options={"resilience_level": 3})
inst = EstimatorV2(session=session, options={"resilience": {"pec_mitigation": True}})
with self.assertRaises(ValueError) as exc:
inst.run((self.circuit, self.observables))
self.assertIn("coupling map", str(exc.exception))
Expand All @@ -127,17 +127,13 @@ def test_run_default_options(self):
EstimatorOptions(resilience_level=1), # pylint: disable=unexpected-keyword-arg
{"resilience_level": 1},
),
(
EstimatorOptions(optimization_level=3), # pylint: disable=unexpected-keyword-arg
{"transpilation": {"optimization_level": 3}},
),
(
{
"transpilation": {"initial_layout": [1, 2]},
"transpilation": {"optimization_level": 1},
"execution": {"shots": 100},
},
{
"transpilation": {"initial_layout": [1, 2]},
"transpilation": {"optimization_level": 1},
"execution": {"shots": 100},
},
),
Expand Down
1 change: 0 additions & 1 deletion test/unit/test_estimator_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class TestEStimatorOptions(IBMTestCase):
"""Class for testing the Sampler class."""

@data(
{"optimization_level": 99},
{"resilience_level": 99},
{"dynamical_decoupling": "foo"},
{"transpilation": {"skip_transpilation": "foo"}},
Expand Down
27 changes: 8 additions & 19 deletions test/unit/test_ibm_primitives_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,9 @@ def test_dict_options(self, primitive):
{},
{
"max_execution_time": 100,
"transpilation": {"initial_layout": [1, 2]},
"transpilation": {"optimization_level": 1},
"execution": {"shots": 100, "init_qubits": True},
},
{"optimization_level": 2},
{"transpilation": {}},
]
for options in options_vars:
Expand Down Expand Up @@ -363,17 +362,8 @@ def test_run_updated_options(self, primitive):
({"dynamical_decoupling": "XY4"}, {"dynamical_decoupling": "XY4"}),
({"shots": 200}, {"execution": {"shots": 200}}),
(
{"optimization_level": 3},
{"transpilation": {"optimization_level": 3}},
),
(
{"initial_layout": [1, 2], "optimization_level": 2},
{
"transpilation": {
"optimization_level": 2,
"initial_layout": [1, 2],
}
},
{"optimization_level": 1},
{"transpilation": {"optimization_level": 1}},
),
(
{"skip_transpilation": True},
Expand Down Expand Up @@ -458,14 +448,14 @@ def test_run_same_session(self):
@combine(
primitive=[EstimatorV2],
new_opts=[
{"optimization_level": 2},
{"optimization_level": 3, "shots": 200},
{"optimization_level": 0},
{"optimization_level": 1, "shots": 200},
],
)
def test_set_options(self, primitive, new_opts):
"""Test set options."""
opt_cls = primitive._options_class
options = opt_cls(optimization_level=1, execution={"shots": 100})
options = opt_cls(transpilation={"optimization_level": 1}, execution={"shots": 100})

session = MagicMock(spec=MockSession)
inst = primitive(session=session, options=options)
Expand All @@ -492,17 +482,16 @@ def test_accept_level_1_options(self, primitive):
{"shots": 10},
{"seed_simulator": 123},
{"skip_transpilation": True, "log_level": "ERROR"},
{"initial_layout": [1, 2], "shots": 100, "optimization_level": 2},
{"shots": 100, "optimization_level": 1},
]

expected_list = [opt_cls() for _ in range(len(options_dicts))]
expected_list[1].execution.shots = 10
expected_list[2].simulator.seed_simulator = 123
expected_list[3].transpilation.skip_transpilation = True
expected_list[3].environment.log_level = "ERROR"
expected_list[4].transpilation.initial_layout = [1, 2]
expected_list[4].execution.shots = 100
expected_list[4].optimization_level = 2
expected_list[4].transpilation.optimization_level = 1

session = MagicMock(spec=MockSession)
for opts, expected in zip(options_dicts, expected_list):
Expand Down
Loading

0 comments on commit f9bcb47

Please sign in to comment.