Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0698026
add options
jyu00 Sep 8, 2023
c699b59
fix mypy
jyu00 Sep 11, 2023
ba30066
rename meas err mit
jyu00 Sep 11, 2023
a2851a1
Add additional resilience options
chriseclectic Sep 13, 2023
b722c8c
Add additional execution options for twirling
chriseclectic Sep 13, 2023
19dff5b
A twirling strategy option and validation
chriseclectic Sep 13, 2023
7efd82b
handle default resilience options
chriseclectic Sep 13, 2023
5836981
Merge pull request #2 from chriseclectic/new-options-cjw
jyu00 Sep 13, 2023
7265815
add _experimental
jyu00 Sep 13, 2023
228b7ad
Merge branch 'new-options' of https://github.com/jyu00/qiskit-ibm-run…
jyu00 Sep 13, 2023
179678e
Update default resilience options (#1062)
kt474 Sep 12, 2023
5354146
add finalize options
jyu00 Sep 14, 2023
773115a
add tests
jyu00 Sep 15, 2023
d0ae417
Update qiskit_ibm_runtime/options/resilience_options.py
mberna Sep 15, 2023
a8e4a64
add validation
jyu00 Sep 18, 2023
6eff5ea
Merge branch 'new-options' of https://github.com/jyu00/qiskit-ibm-run…
jyu00 Sep 18, 2023
d473619
lint
jyu00 Sep 18, 2023
ba9d05f
lint again
jyu00 Sep 18, 2023
3c76e9a
lint again
jyu00 Sep 18, 2023
2e8ffcc
Allow None values for specific options to be passed through
chriseclectic Sep 18, 2023
77a78e4
Merge pull request #3 from chriseclectic/new-options-cjw
jyu00 Sep 18, 2023
6115261
Fix parameter validation, allow computational basis
chriseclectic Sep 18, 2023
85a0521
Merge pull request #4 from chriseclectic/new-options-cjw
jyu00 Sep 18, 2023
4111e70
black
jyu00 Sep 18, 2023
c885cde
Fix ZneExtrapolatorType validation
mberna Sep 18, 2023
4af1318
lint
jyu00 Sep 20, 2023
5b15018
lint again
jyu00 Sep 20, 2023
6a95a21
fix mypy
jyu00 Sep 20, 2023
502c0e5
Fix ZNE extrapolator default option
chriseclectic Sep 20, 2023
42fa510
fix level options
jyu00 Sep 20, 2023
0d9096e
Merge branch 'new-options' of https://github.com/jyu00/qiskit-ibm-run…
jyu00 Sep 20, 2023
af0c51f
black
jyu00 Sep 20, 2023
72d5d3d
use _isreal
jyu00 Sep 20, 2023
5f4b581
Disable gate twirling for default lvl 1 opts
chriseclectic Sep 20, 2023
372d171
Support for legacy options
mberna Sep 20, 2023
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
3 changes: 3 additions & 0 deletions qiskit_ibm_runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ def result_callback(job_id, result):
"""

import logging
import warnings

from .qiskit_runtime_service import QiskitRuntimeService
from .ibm_backend import IBMBackend
Expand Down Expand Up @@ -203,3 +204,5 @@ def result_callback(job_id, result):
"""The environment variable name that is used to set the level for the IBM Quantum logger."""
QISKIT_IBM_RUNTIME_LOG_FILE = "QISKIT_IBM_RUNTIME_LOG_FILE"
"""The environment variable name that is used to set the file for the IBM Quantum logger."""

warnings.warn("You are using the experimental branch. Stability is not guaranteed.")
46 changes: 22 additions & 24 deletions qiskit_ibm_runtime/base_primitive.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from qiskit.providers.options import Options as TerraOptions

from .options import Options
from .options.utils import set_default_error_levels
from .runtime_job import RuntimeJob
from .ibm_backend import IBMBackend
from .session import get_cm_session
Expand Down Expand Up @@ -75,15 +74,6 @@ def __init__(
self._service: QiskitRuntimeService = None
self._backend: Optional[IBMBackend] = None

if options is None:
self._options = asdict(Options())
elif isinstance(options, Options):
self._options = asdict(copy.deepcopy(options))
else:
options_copy = copy.deepcopy(options)
default_options = asdict(Options())
self._options = Options._merge_options(default_options, options_copy)

if isinstance(session, Session):
self._session = session
self._service = self._session.service
Expand Down Expand Up @@ -148,6 +138,21 @@ def __init__(
raise ValueError(
"A backend or session must be specified when not using ibm_cloud channel."
)
self._simulator_backend = (
self._backend.configuration().simulator if self._backend else False
)

if options is None:
self._options = asdict(Options())
elif isinstance(options, Options):
self._options = asdict(copy.deepcopy(options))
else:
options_copy = copy.deepcopy(options)
default_options = asdict(Options())
self._options = Options._merge_options_with_defaults(
default_options, options_copy, is_simulator=self._simulator_backend
)

# self._first_run = True
# self._circuits_map = {}
# if self.circuits:
Expand All @@ -169,20 +174,11 @@ def _run_primitive(self, primitive_inputs: Dict, user_kwargs: Dict) -> RuntimeJo
Returns:
Submitted job.
"""
combined = Options._merge_options(self._options, user_kwargs)

if self._backend:
combined = set_default_error_levels(
combined,
self._backend,
Options._DEFAULT_OPTIMIZATION_LEVEL,
Options._DEFAULT_RESILIENCE_LEVEL,
)
else:
combined["optimization_level"] = Options._DEFAULT_OPTIMIZATION_LEVEL
combined["resilience_level"] = Options._DEFAULT_RESILIENCE_LEVEL

combined = Options._merge_options_with_defaults(
self._options, user_kwargs, self._simulator_backend
)
self._validate_options(combined)

primitive_inputs.update(Options._get_program_inputs(combined))

if self._backend and combined["transpilation"]["skip_transpilation"]:
Expand Down Expand Up @@ -238,7 +234,9 @@ def set_options(self, **fields: Any) -> None:
Args:
**fields: The fields to update the options
"""
self._options = Options._merge_options(self._options, fields)
self._options = Options._merge_options_with_defaults(
self._options, fields, self._simulator_backend
)

@abstractmethod
def _validate_options(self, options: dict) -> None:
Expand Down
117 changes: 112 additions & 5 deletions qiskit_ibm_runtime/estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,52 @@

from __future__ import annotations
import os
from typing import Optional, Dict, Sequence, Any, Union
from typing import Optional, Dict, Sequence, Any, Union, Mapping
from numbers import Integral
import logging

import numpy as np
from numpy.typing import ArrayLike

from qiskit.circuit import QuantumCircuit
from qiskit.opflow import PauliSumOp
from qiskit.quantum_info.operators.base_operator import BaseOperator
from qiskit.primitives import BaseEstimator
from qiskit.primitives.base.base_primitive import _isreal
from qiskit.quantum_info import SparsePauliOp, Pauli
from qiskit.primitives.utils import init_observable
from qiskit.circuit import Parameter

# TODO import _circuit_key from terra once 0.23 is released
from .runtime_job import RuntimeJob
from .ibm_backend import IBMBackend
from .options import Options
from .base_primitive import BasePrimitive
from .utils.qctrl import validate as qctrl_validate
from .utils.deprecation import issue_deprecation_msg

# pylint: disable=unused-import,cyclic-import
from .session import Session

logger = logging.getLogger(__name__)


BasisObservableLike = Union[str, Pauli, SparsePauliOp, Mapping[Union[str, Pauli], complex]]
"""Types that can be natively used to construct a :const:`BasisObservable`."""

ObservablesArrayLike = Union[ArrayLike, Sequence[BasisObservableLike], BasisObservableLike]

ParameterMappingLike = Mapping[
Parameter, Union[float, np.ndarray, Sequence[float], Sequence[Sequence[float]]]
]
BindingsArrayLike = Union[
float,
np.ndarray,
ParameterMappingLike,
Sequence[Union[float, Sequence[float], np.ndarray, ParameterMappingLike]],
]
"""Parameter types that can be bound to a single circuit."""


class Estimator(BasePrimitive, BaseEstimator):
"""Class for interacting with Qiskit Runtime Estimator primitive service.

Expand Down Expand Up @@ -85,6 +110,7 @@ class Estimator(BasePrimitive, BaseEstimator):
"""

_PROGRAM_ID = "estimator"
_ALLOWED_BASIS: str = "IXYZ01+-rl"

def __init__(
self,
Expand Down Expand Up @@ -119,8 +145,11 @@ def __init__(
def run( # pylint: disable=arguments-differ
self,
circuits: QuantumCircuit | Sequence[QuantumCircuit],
observables: BaseOperator | PauliSumOp | Sequence[BaseOperator | PauliSumOp],
parameter_values: Sequence[float] | Sequence[Sequence[float]] | None = None,
observables: Sequence[ObservablesArrayLike]
| ObservablesArrayLike
| Sequence[BaseOperator]
| BaseOperator,
parameter_values: BindingsArrayLike | Sequence[BindingsArrayLike] | None = None,
**kwargs: Any,
) -> RuntimeJob:
"""Submit a request to the estimator primitive.
Expand Down Expand Up @@ -155,7 +184,7 @@ def run( # pylint: disable=arguments-differ
def _run( # pylint: disable=arguments-differ
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
observables: Sequence[ObservablesArrayLike],
parameter_values: Sequence[Sequence[float]],
**kwargs: Any,
) -> RuntimeJob:
Expand Down Expand Up @@ -220,6 +249,84 @@ def _validate_options(self, options: dict) -> None:
)
Options.validate_options(options)

@staticmethod
def _validate_observables(
observables: Sequence[ObservablesArrayLike] | ObservablesArrayLike,
) -> Sequence[ObservablesArrayLike]:
def _check_and_init(obs: Any) -> Any:
if isinstance(obs, str):
pass
if not all(basis in Estimator._ALLOWED_BASIS for basis in obs):
raise ValueError(
f"Invalid character(s) found in observable string. "
f"Allowed basis are {Estimator._ALLOWED_BASIS}."
)
elif isinstance(obs, Sequence):
return tuple(_check_and_init(obs_) for obs_ in obs)
elif not isinstance(obs, (Pauli, SparsePauliOp)) and isinstance(obs, BaseOperator):
issue_deprecation_msg(
msg="Only Pauli and SparsePauliOp operators can be used as observables.",
version=0.13,
remedy="",
)
return init_observable(obs)
elif isinstance(obs, Mapping):
for key in obs.keys():
_check_and_init(key)

return obs

if isinstance(observables, str) or not isinstance(observables, Sequence):
observables = (observables,)

if len(observables) == 0:
raise ValueError("No observables were provided.")

return tuple(_check_and_init(obs_array) for obs_array in observables)

@staticmethod
def _validate_parameter_values(
parameter_values: BindingsArrayLike | Sequence[BindingsArrayLike] | None,
default: Sequence[Sequence[float]] | Sequence[float] | None = None,
) -> Sequence:

# Allow optional (if default)
if parameter_values is None:
if default is None:
raise ValueError("No default `parameter_values`, optional input disallowed.")
parameter_values = default

# Convert single input types to length-1 lists
if isinstance(parameter_values, Integral):
parameter_values = [[parameter_values]]
elif isinstance(parameter_values, Mapping):
parameter_values = [parameter_values]
elif isinstance(parameter_values, Sequence) and all(
isinstance(item, Integral) for item in parameter_values
):
parameter_values = [parameter_values]
return tuple(parameter_values)

@staticmethod
def _cross_validate_circuits_parameter_values(
circuits: tuple[QuantumCircuit, ...], parameter_values: tuple[tuple[float, ...], ...]
) -> None:
if len(circuits) != len(parameter_values):
raise ValueError(
f"The number of circuits ({len(circuits)}) does not match "
f"the number of parameter value sets ({len(parameter_values)})."
)

@staticmethod
def _cross_validate_circuits_observables(
circuits: tuple[QuantumCircuit, ...], observables: tuple[ObservablesArrayLike, ...]
) -> None:
if len(circuits) != len(observables):
raise ValueError(
f"The number of circuits ({len(circuits)}) does not match "
f"the number of observables ({len(observables)})."
)

@classmethod
def _program_id(cls) -> str:
"""Return the program ID."""
Expand Down
2 changes: 2 additions & 0 deletions qiskit_ibm_runtime/options/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
ExecutionOptions
EnvironmentOptions
SimulatorOptions
TwirlingOptions

"""

Expand All @@ -58,3 +59,4 @@
from .simulator_options import SimulatorOptions
from .transpilation_options import TranspilationOptions
from .resilience_options import ResilienceOptions
from .twirling_options import TwirlingOptions
60 changes: 57 additions & 3 deletions qiskit_ibm_runtime/options/execution_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@
"""Execution options."""

from dataclasses import dataclass
from typing import Literal, get_args
from typing import Literal, get_args, Optional
from numbers import Integral

from .utils import _flexible

ExecutionSupportedOptions = Literal[
"shots",
"init_qubits",
"samples",
"shots_per_sample",
"interleave_samples",
]


Expand All @@ -29,14 +33,34 @@ class ExecutionOptions:
"""Execution options.

Args:
shots: Number of repetitions of each circuit, for sampling. Default: 4000.
shots: Number of repetitions of each circuit, for sampling. Default: 4096.

init_qubits: Whether to reset the qubits to the ground state for each shot.
Default: ``True``.

samples: The number of samples of each measurement circuit to run. This
is used when twirling or resilience levels 1, 2, 3. If None it will
be calculated automatically based on the ``shots`` and
``shots_per_sample`` (if specified).
Default: None

shots_per_sample: The number of shots per sample of each measurement
circuit to run. This is used when twirling or resilience levels 1, 2, 3.
If None it will be calculated automatically based on the ``shots`` and
``samples`` (if specified).
Default: None

interleave_samples: If True interleave samples from different measurement
circuits when running. If False run all samples from each measurement
circuit in order.
Default: False
"""

shots: int = 4000
shots: int = 4096
init_qubits: bool = True
samples: Optional[int] = None
shots_per_sample: Optional[int] = None
interleave_samples: bool = False

@staticmethod
def validate_execution_options(execution_options: dict) -> None:
Expand All @@ -47,3 +71,33 @@ def validate_execution_options(execution_options: dict) -> None:
for opt in execution_options:
if not opt in get_args(ExecutionSupportedOptions):
raise ValueError(f"Unsupported value '{opt}' for execution.")

shots = execution_options.get("shots")
samples = execution_options.get("samples")
shots_per_sample = execution_options.get("shots_per_sample")
if (
shots is not None
and samples is not None
and shots_per_sample is not None
and shots != samples * shots_per_sample
):
raise ValueError(
f"If shots ({shots}) != samples ({samples}) * shots_per_sample ({shots_per_sample})"
)
if shots is not None:
if not isinstance(shots, Integral):
raise ValueError(f"shots must be None or an integer, not {type(shots)}")
if shots < 1:
raise ValueError("shots must be None or >= 1")
if samples is not None:
if not isinstance(samples, Integral):
raise ValueError(f"samples must be None or an integer, not {type(samples)}")
if samples < 1:
raise ValueError("samples must be None or >= 1")
if shots_per_sample is not None:
if not isinstance(shots_per_sample, Integral):
raise ValueError(
f"shots_per_sample must be None or an integer, not {type(shots_per_sample)}"
)
if shots_per_sample < 1:
raise ValueError("shots_per_sample must be None or >= 1")
Loading