diff --git a/qiskit_ibm_runtime/models/__init__.py b/qiskit_ibm_runtime/models/__init__.py new file mode 100644 index 000000000..38b120b35 --- /dev/null +++ b/qiskit_ibm_runtime/models/__init__.py @@ -0,0 +1,15 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""IBM custom backend model.""" + +from .backend_configuration import IBMBackendConfiguration diff --git a/qiskit_ibm_runtime/models/backend_configuration.py b/qiskit_ibm_runtime/models/backend_configuration.py new file mode 100644 index 000000000..68b914061 --- /dev/null +++ b/qiskit_ibm_runtime/models/backend_configuration.py @@ -0,0 +1,230 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""IBM configuration model.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any, Literal + +from qiskit.circuit.parameter import Parameter + +from pydantic import BaseModel, ConfigDict, Field, field_validator + + +class GateConfig(BaseModel): + """Schema of gate configuration""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + name: str + """QASM instruction name""" + parameters: list[Parameter | float] + """Gate parameters""" + qasm_def: str | None = None + """QASM representation""" + coupling_map: list[tuple[int, ...]] = Field(default_factory=list) + """Set of qubits this gate is applied to""" + label: str | None = None + """Unique identifier of this entry""" + + @field_validator("parameters", mode="before") + @classmethod + def _format_parameters(cls, params): + if params is None: + return [] + qk_params = [] + for param in params: + try: + qk_params.append(float(param)) + except ValueError: + qk_params.append(Parameter(param)) + return qk_params + + +class ProcessorType(BaseModel): + """Schema of processor type information""" + + family: str + """Processor family of this backend""" + revision: str + """Revision version of this processor""" + segment: str | None = None + """Segment this processor belongs to within a larger chip""" + + @field_validator("revision", mode="before") + @classmethod + def _to_string(cls, value): + return str(value) + + +class ChannelSpec(BaseModel): + """Schema of hardware channel specification""" + + operates: dict[str, list[int]] + """Hardware components that this channels act on""" + purpose: str + """Purpose of this channel""" + type: Literal["drive", "measure", "control", "acquire"] + """Qiskit Pulse channel type identifier""" + + +class TimingConstraints(BaseModel): + """Schema of instruction timing constraints""" + + pulse_alignment: int = Field(ge=1) + """Alignment interval of gate instructions""" + acquire_alignment: int = Field(ge=1) + """Alignment interval of acquisition trigger""" + granularity: int = Field(ge=1) + """granularity of pulse waveform""" + min_length: int = Field(get=1) + """Minimum pulse waveform samples""" + + +class IBMBackendConfiguration(BaseModel): + """Schema of backend configuration""" + + backend_name: str + """The backend name""" + backend_version: str + """The backend version in the form X.Y.Z""" + n_qubits: int = Field(ge=1) + """The number of qubits for the backend""" + basis_gates: list[str] + """The list of strings for the basis gates of the backends""" + gates: list[GateConfig] + """The list of GateConfig objects for the basis gates of the backend""" + local: bool + """True if the backend is local or False if remote""" + simulator: bool + """True if the backend is a simulator""" + conditional: bool + """True if the backend supports conditional operations""" + open_pulse: bool + """True if the backend supports Qiskit pulse gate feature""" + memory: bool + """True if the backend supports memory""" + max_shots: int = Field(gt=0) + """The maximum number of shots allowed on the backend""" + coupling_map: list[list[int]] + """The coupling map for the device""" + dt: float = Field(ge=0) + """Qubit drive channel timestep in nanoseconds""" + dtm: float = Field(ge=0) + """Measurement drive channel timestep in nanoseconds""" + supported_instructions: list[str] = Field(default_factory=list) + """Instructions supported by the backend""" + dynamic_reprate_enabled: bool = False + """Whether delay between programs can be set dynamically""" + rep_delay_range: list[float] = Field(default_factory=list) + """Range of idle time between circuits in units of microseconds""" + default_rep_delay: float | None = None + """Default value for idle time between circuits in units of microseconds""" + rep_times: list[float] = Field(default_factory=list) + """Available repetition rates of shots""" + max_experiments: int | None = None + """The maximum number of experiments per job""" + sample_name: str | None = None + """Sample name for the backend""" + credits_required: bool | None = None + """True if backend requires credits to run a job""" + online_date: datetime | None = None + """The date that the device went online""" + description: str | None = None + """A description for the backend""" + processor_type: ProcessorType | None = None + """Processor type for this backend""" + parametric_pulses: list[str] = Field(default_factory=list) + """A list of pulse shapes which are supported on the backend""" + qubit_lo_range: list[tuple[float, float]] = Field(default_factory=list) + """Range of measurement frequency for qubits.""" + meas_lo_range: list[tuple[float, float]] = Field(default_factory=list) + """Range of measurement frequency for qubits.""" + meas_kernels: list[str] = Field(default_factory=list) + """Supported measurement kernels""" + discriminators: list[str] = Field(default_factory=list) + """Supported discriminators""" + acquisition_latency: list[float] = Field(default_factory=list) + """Latency (in units of dt) to write a measurement result from qubit n into register slot m""" + conditional_latency: list[float] = Field(default_factory=list) + """Latency (in units of dt) to do a conditional operation on channel n from register slot m""" + meas_map: list[list[int]] = Field(default_factory=list) + """Grouping of measurement which are multiplexed""" + channels: dict[str, ChannelSpec] = Field(default_factory=dict) + """Information of each hardware channel""" + uchannels_enabled: bool = False + """True when control for u-channels are allowed""" + n_uchannels: int | None = None + """Number of u-channels""" + u_channel_lo: list[list[dict]] = Field(default_factory=list) + """U-channel relationship on device los""" + meas_levels: list[int] = Field(default_factory=list) + """Supported measurement levels""" + hamiltonian: dict = Field(default_factory=dict) + """Dictionary with fields characterizing the system hamiltonian""" + supported_features: list[str] = Field(default_factory=list) + """List of supported features.""" + timing_constraints: TimingConstraints | None = None + """Constraints of instruction timing on hardware controller""" + + # TODO add more fields? Or remove fields that are never used practically? + + @field_validator("dt", "dtm", mode="before") + @classmethod + def _format_dts(cls, value): + return _apply_prefix_recursive(value, 1e-9) + + @field_validator("qubit_lo_range", "meas_lo_range", mode="before") + @classmethod + def _format_los(cls, value): + return _apply_prefix_recursive(value, 1e9) + + @field_validator( + "rep_delay_range", + "default_rep_delay", + "default_rep_delay", + "rep_times", + mode="before", + ) + @classmethod + def _format_reptimes(cls, value): + return _apply_prefix_recursive(value, 1e-6) + + @field_validator("u_channel_lo", mode="before") + @classmethod + def _format_u_channel_lo(cls, value): + if value is None: + return None + for uchannel_lo in value: + for lo_spec in uchannel_lo: + scale = lo_spec["scale"] + if not isinstance(scale, complex): + try: + lo_spec["scale"] = complex(*scale) + except TypeError: + raise TypeError(f"{scale} is not in a valid complex number format.") + return value + + @property + def num_qubits(self) -> int: + """Alias of n_qubits""" + return self.n_qubits + + +def _apply_prefix_recursive(value: Any, prefix: float): + """Helper function to apply prefix recursively.""" + try: + return prefix * float(value) + except TypeError: + return [_apply_prefix_recursive(v, prefix) for v in value] diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index 7f22b817d..1b826d325 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -23,12 +23,12 @@ from qiskit.providers.backend import BackendV2 as Backend from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit.providers.providerutils import filter_backends +from pydantic import ValidationError from qiskit_ibm_runtime import ibm_backend from .proxies import ProxyConfiguration from .utils.deprecation import issue_deprecation_msg, deprecate_function from .utils.hgp import to_instance_format, from_instance_format -from .utils.backend_decoder import configuration_from_server_data from .utils.utils import validate_job_tags @@ -47,6 +47,7 @@ from .api.client_parameters import ClientParameters from .runtime_options import RuntimeOptions from .ibm_backend import IBMBackend +from .models.backend_configuration import IBMBackendConfiguration logger = logging.getLogger(__name__) @@ -580,44 +581,56 @@ def _create_backend_obj( Raises: QiskitBackendNotFoundError: if the backend is not in the hgp passed in. """ - if config := configuration_from_server_data( - raw_config=self._api_client.backend_configuration(backend_name), - instance=instance, - ): - if self._channel == "ibm_quantum": - if not instance: - for hgp in list(self._hgps.values()): - if config.backend_name in hgp.backends: - instance = to_instance_format(hgp._hub, hgp._group, hgp._project) - break - - elif config.backend_name not in self._get_hgp(instance=instance).backends: - hgps_with_backend = [] - for hgp in list(self._hgps.values()): - if config.backend_name in hgp.backends: - hgps_with_backend.append( - to_instance_format(hgp._hub, hgp._group, hgp._project) - ) - raise QiskitBackendNotFoundError( - f"Backend {config.backend_name} is not in " - f"{instance}. Please try a different instance. " - f"{config.backend_name} is in the following instances you have access to: " - f"{hgps_with_backend}" - ) - return ibm_backend.IBMBackend( - instance=instance, - configuration=config, - service=self, - api_client=self._api_client, - ) - else: - # cloud backend doesn't set hgp instance - return ibm_backend.IBMBackend( - configuration=config, - service=self, - api_client=self._api_client, + raw_config = self._api_client.backend_configuration(backend_name) + if not isinstance(raw_config, dict): + logger.warning( # type: ignore[unreachable] + "An error occurred when retrieving backend " + "information. Some backends might not be available." + ) + return None + try: + config = IBMBackendConfiguration(**raw_config) + except ValidationError: + logger.warning( + 'Remote backend "%s" for service instance %s could not be instantiated due ' + "to an invalid server-side configuration", + raw_config.get("backend_name", raw_config.get("name", "unknown")), + repr(instance), + ) + logger.debug("Invalid device configuration: %s", traceback.format_exc()) + return None + if self._channel == "ibm_quantum": + if not instance: + for hgp in list(self._hgps.values()): + if config.backend_name in hgp.backends: + instance = to_instance_format(hgp._hub, hgp._group, hgp._project) + break + + elif config.backend_name not in self._get_hgp(instance=instance).backends: + hgps_with_backend = [] + for hgp in list(self._hgps.values()): + if config.backend_name in hgp.backends: + hgps_with_backend.append( + to_instance_format(hgp._hub, hgp._group, hgp._project) + ) + raise QiskitBackendNotFoundError( + f"Backend {config.backend_name} is not in " + f"{instance}. Please try a different instance. " + f"{config.backend_name} is in the following instances you have access to: " + f"{hgps_with_backend}" ) - return None + return ibm_backend.IBMBackend( + instance=instance, + configuration=config, + service=self, + api_client=self._api_client, + ) + # cloud backend doesn't set hgp instance + return ibm_backend.IBMBackend( + configuration=config, + service=self, + api_client=self._api_client, + ) def active_account(self) -> Optional[Dict[str, str]]: """Return the IBM Quantum account currently in use for the session. diff --git a/qiskit_ibm_runtime/utils/backend_converter.py b/qiskit_ibm_runtime/utils/backend_converter.py index 94c969357..154b818ce 100644 --- a/qiskit_ibm_runtime/utils/backend_converter.py +++ b/qiskit_ibm_runtime/utils/backend_converter.py @@ -16,7 +16,9 @@ import logging import warnings -from typing import Any, Dict, List +from dataclasses import dataclass +from typing import Any, Dict, Type +from collections import defaultdict from qiskit.circuit.controlflow import ( CONTROL_FLOW_OP_NAMES, @@ -32,20 +34,30 @@ U1Gate, PhaseGate, ) +from qiskit.circuit.instruction import Instruction from qiskit.circuit.delay import Delay -from qiskit.circuit.parameter import Parameter from qiskit.providers.backend import QubitProperties from qiskit.providers.exceptions import BackendPropertyError -from qiskit.providers.models.backendconfiguration import BackendConfiguration from qiskit.providers.models.backendproperties import BackendProperties from qiskit.providers.models.pulsedefaults import PulseDefaults from qiskit.transpiler.target import InstructionProperties, Target +from qiskit_ibm_runtime.models.backend_configuration import IBMBackendConfiguration + logger = logging.getLogger(__name__) +@dataclass +class InstructionEntry: + """A collection of information to populate Qiskit target""" + + qiskit_instruction: Instruction | Type[Instruction] + properties: dict[tuple[int, ...], InstructionProperties] | None = None + name: str | None = None + + def convert_to_target( - configuration: BackendConfiguration, + configuration: IBMBackendConfiguration, properties: BackendProperties = None, defaults: PulseDefaults = None, *, @@ -59,7 +71,7 @@ def convert_to_target( These objects are usually components of the legacy :class:`.BackendV1` model. Args: - configuration: Backend configuration as ``BackendConfiguration`` + configuration: Backend configuration as ``IBMBackendConfiguration`` properties: Backend property dictionary or ``BackendProperties`` defaults: Backend pulse defaults dictionary or ``PulseDefaults`` include_control_flow: Set True to include control flow instructions. @@ -68,9 +80,6 @@ def convert_to_target( Returns: A ``Target`` instance. """ - add_delay = True - filter_faulty = True - required = ["measure", "delay", "reset"] # Load Qiskit object representation @@ -83,31 +92,30 @@ def convert_to_target( "switch_case": SwitchCaseOp, } - in_data = {"num_qubits": configuration.n_qubits} - - # Parse global configuration properties - if hasattr(configuration, "dt"): - in_data["dt"] = configuration.dt - if hasattr(configuration, "timing_constraints"): - in_data.update(configuration.timing_constraints) + in_data = { + "num_qubits": configuration.n_qubits, + "dt": configuration.dt, + } + if configuration.timing_constraints: + in_data.update(configuration.timing_constraints.model_dump()) # Create instruction property placeholder from backend configuration - basis_gates = set(getattr(configuration, "basis_gates", [])) - supported_instructions = set(getattr(configuration, "supported_instructions", [])) - gate_configs = {gate.name: gate for gate in configuration.gates} + basis_gates = set(configuration.basis_gates) + supported_instructions = set(configuration.supported_instructions) + gate_configs = defaultdict(list) + for gate in configuration.gates: + gate_configs[gate.name].append(gate) all_instructions = set.union( basis_gates, set(required), supported_instructions.intersection(CONTROL_FLOW_OP_NAMES) ) - inst_name_map = {} - faulty_ops = set() faulty_qubits = set() - unsupported_instructions = [] + entries: list[InstructionEntry] = [] # Create name to Qiskit instruction object repr mapping for name in all_instructions: - if name in qiskit_control_flow_mapping: + if qiskit_control_flow_op := qiskit_control_flow_mapping.get(name, None): if not include_control_flow: # Remove name if this is control flow and dynamic circuits feature is disabled. logger.info( @@ -115,10 +123,16 @@ def convert_to_target( "This instruction is excluded from the backend Target.", name, ) - unsupported_instructions.append(name) - continue - if name in qiskit_inst_mapping: - qiskit_gate = qiskit_inst_mapping[name] + continue + entries.append( + InstructionEntry( + qiskit_instruction=qiskit_control_flow_op, + properties=None, + name=name, + ) + ) + elif qiskit_gate := qiskit_inst_mapping.get(name, None): + # Standard Qiskit gates if (not include_fractional_gates) and is_fractional_gate(qiskit_gate): # Remove name if this is fractional gate and fractional gate feature is disabled. logger.info( @@ -126,22 +140,47 @@ def convert_to_target( "This gate is excluded from the backend Target.", name, ) - unsupported_instructions.append(name) continue - inst_name_map[name] = qiskit_gate + if name in gate_configs: + # Respect gate configuration from the backend + for sub_gate in gate_configs[name]: + # Respect operational qubits that gate configuration defines + # This ties instruction to particular qubits even without properties information. + # Note that each instruction is considered to be ideal unless + # its spec (e.g. error, duration) is bound by the properties object. + entries.append( + InstructionEntry( + qiskit_instruction=qiskit_gate.base_class(*sub_gate.parameters), + properties=dict.fromkeys(sub_gate.coupling_map), + name=sub_gate.label or sub_gate.name, + ) + ) + else: + entries.append( + InstructionEntry( + qiskit_instruction=qiskit_gate, + properties=None, + name=qiskit_gate.name, + ) + ) elif name in gate_configs: # GateConfig model is a translator of QASM opcode. # This doesn't have quantum definition, so Qiskit transpiler doesn't perform # any optimization in quantum domain. # Usually GateConfig counterpart should exist in Qiskit namespace so this is rarely called. - this_config = gate_configs[name] - params = list(map(Parameter, getattr(this_config, "parameters", []))) - coupling_map = getattr(this_config, "coupling_map", []) - inst_name_map[name] = Gate( - name=name, - num_qubits=len(coupling_map[0]) if coupling_map else 0, - params=params, - ) + for sub_gate in gate_configs[name]: + opaque_gate = Gate( + name=name, + num_qubits=len(sub_gate.coupling_map[0]) if sub_gate.coupling_map else 0, + params=sub_gate.parameters, + ) + entries.append( + InstructionEntry( + qiskit_instruction=opaque_gate, + properties=dict.fromkeys(sub_gate.coupling_map), + name=sub_gate.label or sub_gate.name, + ) + ) else: warnings.warn( f"No gate definition for {name} can be found and is being excluded " @@ -149,25 +188,6 @@ def convert_to_target( "a definition for this operation.", RuntimeWarning, ) - unsupported_instructions.append(name) - - for name in unsupported_instructions: - all_instructions.remove(name) - - # Create inst properties placeholder - # Without any assignment, properties value is None, - # which defines a global instruction that can be applied to any qubit(s). - # The None value behaves differently from an empty dictionary. - # See API doc of Target.add_instruction for details. - prop_name_map = dict.fromkeys(all_instructions) - for name in all_instructions: - if name in gate_configs: - if coupling_map := getattr(gate_configs[name], "coupling_map", None): - # Respect operational qubits that gate configuration defines - # This ties instruction to particular qubits even without properties information. - # Note that each instruction is considered to be ideal unless - # its spec (e.g. error, duration) is bound by the properties object. - prop_name_map[name] = dict.fromkeys(map(tuple, coupling_map)) # Populate instruction properties if properties: @@ -199,152 +219,93 @@ def _get_value(prop_dict: Dict, prop_name: str) -> Any: ) in_data["qubit_properties"] = qubit_properties - for name in all_instructions: - try: - for qubits, param_dict in properties.gate_property(name).items(): - if filter_faulty and ( - set.intersection(faulty_qubits, qubits) - or not properties.is_gate_operational(name, qubits) - ): - try: - # Qubits might be pre-defined by the gate config - # However properties objects says the qubits is non-operational - del prop_name_map[name][qubits] - except KeyError: - pass - faulty_ops.add((name, qubits)) + for entry in entries: + if entry.name in required and entry.properties is None: + entry.properties = { + (q,): None for q in range(configuration.num_qubits) if q not in faulty_qubits + } + if entry.name == "measure": + # Measure instruction property is stored in qubit property + for qubit_idx in range(configuration.num_qubits): + if qubit_idx in faulty_qubits: continue - if prop_name_map[name] is None: - # This instruction is tied to particular qubits - # i.e. gate config is not provided, and instruction has been globally defined. - prop_name_map[name] = {} - prop_name_map[name][qubits] = InstructionProperties( - error=_get_value(param_dict, "gate_error"), - duration=_get_value(param_dict, "gate_length"), + qubit_prop = properties.qubit_property(qubit_idx) + entry.properties[(qubit_idx,)] = InstructionProperties( + error=_get_value(qubit_prop, "readout_error"), + duration=_get_value(qubit_prop, "readout_length"), ) - if isinstance(prop_name_map[name], dict) and any( - v is None for v in prop_name_map[name].values() - ): - # Properties provides gate properties only for subset of qubits - # Associated qubit set might be defined by the gate config here - logger.info( - "Gate properties of instruction %s are not provided for every qubits. " - "This gate is ideal for some qubits and the rest is with finite error. " - "Created backend target may confuse error-aware circuit optimization.", - name, - ) - except BackendPropertyError: - # This gate doesn't report any property - continue - - # Measure instruction property is stored in qubit property - prop_name_map["measure"] = {} - - for qubit_idx in range(configuration.num_qubits): - if filter_faulty and (qubit_idx in faulty_qubits): - continue - qubit_prop = properties.qubit_property(qubit_idx) - prop_name_map["measure"][(qubit_idx,)] = InstructionProperties( - error=_get_value(qubit_prop, "readout_error"), - duration=_get_value(qubit_prop, "readout_length"), - ) - - for op in required: - # Map required ops to each operational qubit - if prop_name_map[op] is None: - prop_name_map[op] = { - (q,): None - for q in range(configuration.num_qubits) - if not filter_faulty or (q not in faulty_qubits) - } + else: + try: + for qubits, param_dict in properties.gate_property(entry.name).items(): + if entry.properties is None: + # This instruction is tied to particular qubits + # i.e. gate config is not provided, + # and instruction has been globally defined. + entry.properties = {} + if set.intersection( + faulty_qubits, qubits + ) or not properties.is_gate_operational(entry.name, qubits): + try: + # Qubits might be pre-defined by the gate config + # However properties objects says the qubits is non-operational + del entry.properties[qubits] + except KeyError: + pass + faulty_ops.add((entry.name, qubits)) + continue + entry.properties[qubits] = InstructionProperties( + error=_get_value(param_dict, "gate_error"), + duration=_get_value(param_dict, "gate_length"), + ) + except BackendPropertyError: + # This gate doesn't report any property + pass + + if entry.properties is not None and None in entry.properties.values(): + # Properties provides gate properties only for subset of qubits + # Associated qubit set might be defined by the gate config here + logger.info( + "Gate properties of instruction %s are not provided for every qubits. " + "This gate is ideal for some qubits and the rest is with finite error. " + "Created backend target may confuse error-aware circuit optimization.", + entry.name, + ) if defaults: inst_sched_map = defaults.instruction_schedule_map - for name in inst_sched_map.instructions: - for qubits in inst_sched_map.qubits_with_instruction(name): - - if not isinstance(qubits, tuple): - qubits = (qubits,) - - if ( - name not in all_instructions - or name not in prop_name_map - or prop_name_map[name] is None - or qubits not in prop_name_map[name] - ): - logger.info( - "Gate calibration for instruction %s on qubits %s is found " - "in the PulseDefaults payload. However, this entry is not defined in " - "the gate mapping of Target. This calibration is ignored.", - name, - qubits, - ) + for entry in entries: + if entry.properties is None: + continue + for qubits, inst_properties in entry.properties.items(): + # We assume calibration is provided with the sub gate name, e.g. rx_30. + # If we assume parameterized schedule and bind parameter immediately + # to get sub gate schedule, this causes Qobj parsing overhead. + # We perfer lazy calibration parsing. + name_qubits = entry.name, qubits + if name_qubits in faulty_ops: continue - - if (name, qubits) in faulty_ops: + if not inst_sched_map.has(*name_qubits): continue - - entry = inst_sched_map._get_calibration_entry(name, qubits) - + calibration = inst_sched_map._get_calibration_entry(*name_qubits) try: - prop_name_map[name][qubits].calibration = entry + inst_properties.calibration = calibration except AttributeError: logger.info( "The PulseDefaults payload received contains an instruction %s on " "qubits %s which is not present in the configuration or properties payload.", - name, - qubits, + *name_qubits, ) # Add parsed properties to target target = Target(**in_data) - for inst_name in all_instructions: - if inst_name == "delay" and not add_delay: - continue - if inst_name in qiskit_control_flow_mapping: - # Control flow operator doesn't have gate property. - target.add_instruction( - instruction=qiskit_control_flow_mapping[inst_name], - name=inst_name, - ) - else: - target.add_instruction( - instruction=inst_name_map[inst_name], - properties=prop_name_map.get(inst_name, None), - name=inst_name, - ) - return target - - -def qubit_props_list_from_props( - properties: BackendProperties, -) -> List[QubitProperties]: - """Uses BackendProperties to construct - and return a list of QubitProperties. - """ - qubit_props: List[QubitProperties] = [] - for qubit, _ in enumerate(properties.qubits): - try: - t_1 = properties.t1(qubit) - except BackendPropertyError: - t_1 = None - try: - t_2 = properties.t2(qubit) - except BackendPropertyError: - t_2 = None - try: - frequency = properties.frequency(qubit) - except BackendPropertyError: - frequency = None - qubit_props.append( - QubitProperties( # type: ignore[no-untyped-call] - t1=t_1, - t2=t_2, - frequency=frequency, - ) + for entry in entries: + target.add_instruction( + instruction=entry.qiskit_instruction, + properties=entry.properties, + name=entry.name, ) - return qubit_props + return target def is_fractional_gate(gate: Gate) -> bool: diff --git a/qiskit_ibm_runtime/utils/backend_decoder.py b/qiskit_ibm_runtime/utils/backend_decoder.py index 369be0791..2f83287a1 100644 --- a/qiskit_ibm_runtime/utils/backend_decoder.py +++ b/qiskit_ibm_runtime/utils/backend_decoder.py @@ -12,60 +12,17 @@ """Utilities for working with IBM Quantum backends.""" -from typing import List, Dict, Union, Optional +from typing import List, Dict, Union import logging -import traceback import dateutil.parser -from qiskit.providers.models import ( - BackendProperties, - PulseDefaults, - PulseBackendConfiguration, - QasmBackendConfiguration, -) +from qiskit.providers.models import BackendProperties, PulseDefaults from .converters import utc_to_local_all logger = logging.getLogger(__name__) -def configuration_from_server_data( - raw_config: Dict, - instance: str = "", -) -> Optional[Union[QasmBackendConfiguration, PulseBackendConfiguration]]: - """Create an IBMBackend instance from raw server data. - - Args: - raw_config: Raw configuration. - instance: Service instance. - - Returns: - Backend configuration. - """ - # Make sure the raw_config is of proper type - if not isinstance(raw_config, dict): - logger.warning( # type: ignore[unreachable] - "An error occurred when retrieving backend " - "information. Some backends might not be available." - ) - return None - try: - _decode_backend_configuration(raw_config) - try: - return PulseBackendConfiguration.from_dict(raw_config) - except (KeyError, TypeError): - return QasmBackendConfiguration.from_dict(raw_config) - except Exception: # pylint: disable=broad-except - logger.warning( - 'Remote backend "%s" for service instance %s could not be instantiated due ' - "to an invalid server-side configuration", - raw_config.get("backend_name", raw_config.get("name", "unknown")), - repr(instance), - ) - logger.debug("Invalid device configuration: %s", traceback.format_exc()) - return None - - def defaults_from_server_data(defaults: Dict) -> PulseDefaults: """Decode pulse defaults data. @@ -110,21 +67,6 @@ def properties_from_server_data(properties: Dict) -> BackendProperties: return BackendProperties.from_dict(properties) -def _decode_backend_configuration(config: Dict) -> None: - """Decode backend configuration. - - Args: - config: A ``QasmBackendConfiguration`` or ``PulseBackendConfiguration`` - in dictionary format. - """ - config["online_date"] = dateutil.parser.isoparse(config["online_date"]) - - if "u_channel_lo" in config: - for u_channel_list in config["u_channel_lo"]: - for u_channel_lo in u_channel_list: - u_channel_lo["scale"] = _to_complex(u_channel_lo["scale"]) - - def _to_complex(value: Union[List[float], complex]) -> complex: """Convert the input value to type ``complex``.