From 6c925819a3b314e7353701d8c3f8f4484f552d60 Mon Sep 17 00:00:00 2001 From: Jun Doi Date: Wed, 14 Aug 2024 10:59:46 +0900 Subject: [PATCH] add configuration and properties in Aer --- qiskit_aer/backends/aer_simulator.py | 69 +- qiskit_aer/backends/aerbackend.py | 268 ++++++-- qiskit_aer/backends/backendconfiguration.py | 399 ++++++++++++ qiskit_aer/backends/backendproperties.py | 589 ++++++++++++++++++ qiskit_aer/backends/name_mapping.py | 2 + qiskit_aer/backends/qasm_simulator.py | 58 +- qiskit_aer/backends/statevector_simulator.py | 12 +- qiskit_aer/backends/unitary_simulator.py | 12 +- .../backends/aer_simulator/test_options.py | 11 +- 9 files changed, 1315 insertions(+), 105 deletions(-) create mode 100644 qiskit_aer/backends/backendconfiguration.py create mode 100644 qiskit_aer/backends/backendproperties.py diff --git a/qiskit_aer/backends/aer_simulator.py b/qiskit_aer/backends/aer_simulator.py index c773db8fb2..dc5310240d 100644 --- a/qiskit_aer/backends/aer_simulator.py +++ b/qiskit_aer/backends/aer_simulator.py @@ -22,6 +22,8 @@ from ..version import __version__ from .aerbackend import AerBackend, AerError +from .backendconfiguration import AerBackendConfiguration +from .backendproperties import AerBackendProperties, target_to_backend_properties from .backend_utils import ( cpp_execute_circuits, cpp_execute_qobj, @@ -681,6 +683,10 @@ class AerSimulator(AerBackend): "backend_version": __version__, "n_qubits": MAX_QUBITS_STATEVECTOR, "url": "https://github.com/Qiskit/qiskit-aer", + "simulator": True, + "local": True, + "conditional": True, + "memory": True, "max_shots": int(1e6), "description": "A C++ QasmQobj simulator with noise", "coupling_map": None, @@ -707,7 +713,9 @@ class AerSimulator(AerBackend): _AVAILABLE_DEVICES = None - def __init__(self, configuration=None, provider=None, target=None, **backend_options): + def __init__( + self, configuration=None, properties=None, provider=None, target=None, **backend_options + ): self._controller = aer_controller_execute() # Update available methods and devices for class @@ -720,11 +728,10 @@ def __init__(self, configuration=None, provider=None, target=None, **backend_opt # Default configuration if configuration is None: - configuration = AerSimulator._DEFAULT_CONFIGURATION + configuration = AerBackendConfiguration.from_dict(AerSimulator._DEFAULT_CONFIGURATION) # set backend name from method and device in option - backend_name = configuration["backend_name"] - if "from" not in backend_name: + if "from" not in configuration.backend_name: method = "automatic" device = "CPU" for key, value in backend_options.items(): @@ -733,10 +740,9 @@ def __init__(self, configuration=None, provider=None, target=None, **backend_opt if key == "device": device = value if method not in [None, "automatic"]: - backend_name += f"_{method}" + configuration.backend_name += f"_{method}" if device not in [None, "CPU"]: - backend_name += f"_{device}".lower() - configuration["backend_name"] = backend_name + configuration.backend_name += f"_{device}".lower() # Cache basis gates since computing the intersection # of noise model, method, and config gates is expensive. @@ -744,6 +750,7 @@ def __init__(self, configuration=None, provider=None, target=None, **backend_opt super().__init__( configuration, + properties=properties, provider=provider, target=target, backend_options=backend_options, @@ -839,18 +846,18 @@ def from_backend(cls, backend, **options): else: description = backend.description - configuration = { - "backend_name": f"aer_simulator_from({backend.name})", - "backend_version": backend.backend_version, - "n_qubits": backend.num_qubits, - "basis_gates": backend.operation_names, - "max_shots": int(1e6), - "coupling_map": ( - None if backend.coupling_map is None else list(backend.coupling_map.get_edges()) - ), - "max_experiments": backend.max_circuits, - "description": description, - } + configuration = AerBackendConfiguration( + backend_name=f"aer_simulator_from({backend.name})", + backend_version=backend.backend_version, + n_qubits=backend.num_qubits, + basis_gates=backend.operation_names, + gates=[], + max_shots=int(1e6), + coupling_map=list(backend.coupling_map.get_edges()), + max_experiments=backend.max_circuits, + description=description, + ) + properties = target_to_backend_properties(backend.target) target = backend.target elif isinstance(backend, BackendV1): # BackendV1 will be removed in Qiskit 2.0, so we will remove this soon @@ -862,15 +869,14 @@ def from_backend(cls, backend, **options): stacklevel=2, ) # Get configuration and properties from backend - config = backend.configuration() + configuration = backend.configuration() properties = copy.copy(backend.properties()) # Customize configuration name - name = config.backend_name - config.backend_name = f"aer_simulator_from({name})" + name = configuration.backend_name + configuration.backend_name = f"aer_simulator_from({name})" target = convert_to_target(config, properties, None, NAME_MAPPING) - configuration = config.to_dict() else: raise TypeError( "The backend argument requires a BackendV2 or BackendV1 object, " @@ -887,7 +893,7 @@ def from_backend(cls, backend, **options): options["noise_model"] = noise_model # Initialize simulator - sim = cls(configuration=configuration, target=target, **options) + sim = cls(configuration=configuration, properties=properties, target=target, **options) return sim def available_methods(self): @@ -906,15 +912,16 @@ def configuration(self): Returns: BackendConfiguration: the configuration for the backend. """ - config = self._configuration.copy() - config.update(self._options_configuration) + config = copy.copy(self._configuration) + for key, val in self._options_configuration.items(): + setattr(config, key, val) method = getattr(self.options, "method", "automatic") # Update basis gates based on custom options, config, method, # and noise model - config["custom_instructions"] = self._CUSTOM_INSTR[method] - config["basis_gates"] = self._cached_basis_gates + config["custom_instructions"] + config.custom_instructions = self._CUSTOM_INSTR[method] + config.basis_gates = self._cached_basis_gates + config.custom_instructions return config def _execute_circuits(self, aer_circuits, noise_model, config): @@ -1002,7 +1009,7 @@ def _basis_gates(self): # Compute intersection with method basis gates method = getattr(self._options, "method", "automatic") method_gates = self._BASIS_GATES[method] - config_gates = self._configuration.get("basis_gates", []) + config_gates = self._configuration.basis_gates if config_gates: basis_gates = set(config_gates).intersection(method_gates) else: @@ -1056,8 +1063,8 @@ def _set_method_config(self, method=None): description = None n_qubits = None - if self._configuration.get("coupling_map", None) is not None: - n_qubits = max(list(map(max, self._configuration["coupling_map"]))) + 1 + if self._configuration.coupling_map: + n_qubits = max(list(map(max, self._configuration.coupling_map))) + 1 self._set_configuration_option("description", description) self._set_configuration_option("n_qubits", n_qubits) diff --git a/qiskit_aer/backends/aerbackend.py b/qiskit_aer/backends/aerbackend.py index 5f15bba9e5..b3c7b84780 100644 --- a/qiskit_aer/backends/aerbackend.py +++ b/qiskit_aer/backends/aerbackend.py @@ -18,6 +18,7 @@ import logging import time import uuid +import warnings from abc import ABC, abstractmethod from qiskit.circuit import QuantumCircuit, ParameterExpression, Delay @@ -29,7 +30,6 @@ from qiskit.result import Result from qiskit.transpiler import CouplingMap from qiskit.transpiler.target import Target -from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping from ..aererror import AerError from ..jobs import AerJob, AerJobSet, split_qobj from ..noise.noise_model import NoiseModel, QuantumErrorLocation @@ -48,7 +48,9 @@ class AerBackend(Backend, ABC): """Aer Backend class.""" - def __init__(self, configuration, provider=None, target=None, backend_options=None): + def __init__( + self, configuration, properties=None, provider=None, target=None, backend_options=None + ): """Aer class for backends. This method should initialize the module and its configuration, and @@ -56,7 +58,8 @@ def __init__(self, configuration, provider=None, target=None, backend_options=No not available. Args: - configuration (dict): backend configuration. + configuration (AerBackendConfiguration): backend configuration. + properties (AerBackendProperties or None): Optional, backend properties. provider (Provider): Optional, provider responsible for this backend. target (Target): initial target for backend backend_options (dict or None): Optional set custom backend options. @@ -67,16 +70,18 @@ def __init__(self, configuration, provider=None, target=None, backend_options=No # Init configuration and provider in Backend super().__init__( provider=provider, - name=configuration["backend_name"], - description=configuration["description"], - backend_version=configuration["backend_version"], + name=configuration.backend_name, + description=configuration.description, + backend_version=configuration.backend_version, ) # Initialize backend configuration + self._properties = properties self._configuration = configuration # Custom option values for config self._options_configuration = {} + self._options_properties = {} self._target = target self._mapping = NAME_MAPPING if target is not None: @@ -89,8 +94,8 @@ def __init__(self, configuration, provider=None, target=None, backend_options=No self.set_options(**backend_options) # build coupling map - if self.configuration()["coupling_map"] is not None: - self._coupling_map = CouplingMap(self.configuration()["coupling_map"]) + if self.configuration().coupling_map is not None: + self._coupling_map = CouplingMap(self.configuration().coupling_map) def _convert_circuit_binds(self, circuit, binds, idx_map): parameterizations = [] @@ -248,17 +253,31 @@ def configuration(self): Returns: BackendConfiguration: the configuration for the backend. """ - config = self._configuration.copy() + config = copy.copy(self._configuration) + for key, val in self._options_configuration.items(): + setattr(config, key, val) # If config has custom instructions add them to # basis gates to include them for the qiskit transpiler - if "custom_instructions" in config: - config["basis_gates"] = config["basis_gates"] + config["custom_instructions"] + if hasattr(config, "custom_instructions"): + config.basis_gates = config.basis_gates + config.custom_instructions return config + def properties(self): + """Return the simulator backend properties if set. + + Returns: + BackendProperties: The backend properties or ``None`` if the + backend does not have properties set. + """ + properties = copy.copy(self._properties) + for key, val in self._options_properties.items(): + setattr(properties, key, val) + return properties + @property def max_circuits(self): - if "max_experiments" in self.configuration(): - return self.configuration()["max_experiments"] + if hasattr(self.configuration(), "max_experiments"): + return self.configuration().max_experiments else: return None @@ -268,28 +287,197 @@ def target(self): return self._target # make target for AerBackend + + # importing packages where they are needed, to avoid cyclic-import. + # pylint: disable=cyclic-import + from qiskit.transpiler.target import InstructionProperties + from qiskit.circuit.controlflow import ForLoopOp, IfElseOp, SwitchCaseOp, WhileLoopOp + from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping + from qiskit.circuit.parameter import Parameter + from qiskit.circuit.gate import Gate + from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES + required = ["measure", "delay"] + configuration = self.configuration() + properties = self.properties() + + # Load Qiskit object representation qiskit_inst_mapping = get_standard_gate_name_mapping() qiskit_inst_mapping.update(NAME_MAPPING) - num_qubits = self.configuration()["n_qubits"] - in_data = {"num_qubits": num_qubits} - basis_gates = set.union(set(required), set(self.configuration().get("basis_gates", []))) + qiskit_control_flow_mapping = { + "if_else": IfElseOp, + "while_loop": WhileLoopOp, + "for_loop": ForLoopOp, + "switch_case": SwitchCaseOp, + } + in_data = {"num_qubits": configuration.num_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) + + # 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} + all_instructions = set.union( + basis_gates, set(required), supported_instructions.intersection(CONTROL_FLOW_OP_NAMES) + ) + inst_name_map = {} # type: Dict[str, Instruction] + + faulty_ops = set() + faulty_qubits = set() + unsupported_instructions = [] + + # Create name to Qiskit instruction object repr mapping + for name in all_instructions: + if name in qiskit_control_flow_mapping: + continue + if name in qiskit_inst_mapping: + inst_name_map[name] = qiskit_inst_mapping[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, + ) + else: + warnings.warn( + f"No gate definition for {name} can be found and is being excluded " + "from the generated target.", + 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: + + def _get_value(prop_dict, prop_name): + if ndval := prop_dict.get(prop_name, None): + return ndval[0] + return None + + # is_qubit_operational is a bit of expensive operation so precache the value + faulty_qubits = { + q for q in range(configuration.num_qubits) if not properties.is_qubit_operational(q) + } + + qubit_properties = [] + for qi in range(0, configuration.num_qubits): + # TODO faulty qubit handling might be needed since + # faulty qubit reporting qubit properties doesn't make sense. + try: + prop_dict = properties.qubit_property(qubit=qi) + except KeyError: + continue + qubit_properties.append( + QubitProperties( + t1=prop_dict.get("T1", (None, None))[0], + t2=prop_dict.get("T2", (None, None))[0], + frequency=prop_dict.get("frequency", (None, None))[0], + ) + ) + in_data["qubit_properties"] = qubit_properties + + for name in all_instructions: + try: + for qubits, params in properties.gate_property(name).items(): + if 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)) + 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(params, "gate_error"), + duration=_get_value(params, "gate_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 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 q not in faulty_qubits + } + # Add parsed properties to target target = Target(**in_data) - for name in basis_gates: - if name in required: + for inst_name in all_instructions: + if inst_name in qiskit_control_flow_mapping: + # Control flow operator doesn't have gate property. target.add_instruction( - instruction=qiskit_inst_mapping[name], - properties={(q,): None for q in range(num_qubits)}, - name=name, + instruction=qiskit_control_flow_mapping[inst_name], + name=inst_name, ) - elif name in qiskit_inst_mapping: + else: target.add_instruction( - instruction=qiskit_inst_mapping[name], - properties=None, - name=name, + instruction=inst_name_map[inst_name], + properties=prop_name_map.get(inst_name, None), + name=inst_name, ) if self._coupling_map is not None: @@ -298,14 +486,14 @@ def target(self): def set_max_qubits(self, max_qubits): """Set maximun number of qubits to be used for this backend.""" - if not self._from_backend: - self._configuration["n_qubits"] = max_qubits - self._set_configuration_option("n_qubits", max_qubits) + if self._target is None: + self._configuration.n_qubits = max_qubits def clear_options(self): """Reset the simulator options to default values.""" self._options = self._default_options() self._options_configuration = {} + self._options_properties = {} def status(self): """Return backend status. @@ -315,7 +503,7 @@ def status(self): """ return BackendStatus( backend_name=self.name, - backend_version=self.configuration()["backend_version"], + backend_version=self.configuration().backend_version, operational=True, pending_jobs=0, status_msg="", @@ -361,7 +549,7 @@ def _execute_qobj_job(self, qobj, job_id="", format_result=True): output["job_id"] = job_id output["date"] = datetime.datetime.now().isoformat() output["backend_name"] = self.name - output["backend_version"] = self.configuration()["backend_version"] + output["backend_version"] = self.configuration().backend_version # Push metadata to experiment headers for result in output["results"]: @@ -397,9 +585,7 @@ def _execute_circuits_job( circuits, noise_model = self._compile(circuits, **run_options) if self._target is not None: - aer_circuits, idx_maps = assemble_circuits( - circuits, self.configuration()["basis_gates"] - ) + aer_circuits, idx_maps = assemble_circuits(circuits, self.configuration().basis_gates) else: aer_circuits, idx_maps = assemble_circuits(circuits) if parameter_binds: @@ -432,7 +618,7 @@ def _execute_circuits_job( output["job_id"] = job_id output["date"] = datetime.datetime.now().isoformat() output["backend_name"] = self.name - output["backend_version"] = self.configuration()["backend_version"] + output["backend_version"] = self.configuration().backend_version # Push metadata to experiment headers for result in output["results"]: @@ -643,8 +829,13 @@ def set_option(self, key, value): AerError: if key is 'method' and val isn't in available methods. """ # Add all other options to the options dict - if key in self._configuration: + # TODO: in the future this could be replaced with an options class + # for the simulators like configuration/properties to show all + # available options + if hasattr(self._configuration, key): self._set_configuration_option(key, value) + elif hasattr(self._properties, key): + self._set_properties_option(key, value) else: if not hasattr(self._options, key): raise AerError(f"Invalid option {key}") @@ -669,6 +860,13 @@ def _set_configuration_option(self, key, value): elif key in self._options_configuration: self._options_configuration.pop(key) + def _set_properties_option(self, key, value): + """Special handling for setting backend properties options.""" + if value is not None: + self._options_properties[key] = value + elif key in self._options_properties: + self._options_properties.pop(key) + def __repr__(self): """String representation of an AerBackend.""" name = self.__class__.__name__ diff --git a/qiskit_aer/backends/backendconfiguration.py b/qiskit_aer/backends/backendconfiguration.py new file mode 100644 index 0000000000..02abb55e4e --- /dev/null +++ b/qiskit_aer/backends/backendconfiguration.py @@ -0,0 +1,399 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2019, 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. +""" +Aer backend configuration +""" +import copy +import numbers +from typing import Dict, List, Any, Iterable, Tuple, Union +from qiskit.exceptions import QiskitError +from qiskit.providers.exceptions import BackendConfigurationError + + +class GateConfig: + """Class representing a Gate Configuration + + Attributes: + name: the gate name as it will be referred to in OpenQASM. + parameters: variable names for the gate parameters (if any). + qasm_def: definition of this gate in terms of OpenQASM 2 primitives U + and CX. + """ + + def __init__( + self, + name, + parameters, + qasm_def, + coupling_map=None, + latency_map=None, + conditional=None, + description=None, + ): + """Initialize a GateConfig object + + Args: + name (str): the gate name as it will be referred to in OpenQASM. + parameters (list): variable names for the gate parameters (if any) + as a list of strings. + qasm_def (str): definition of this gate in terms of OpenQASM 2 primitives U and CX. + coupling_map (list): An optional coupling map for the gate. In + the form of a list of lists of integers representing the qubit + groupings which are coupled by this gate. + latency_map (list): An optional map of latency for the gate. In the + the form of a list of lists of integers of either 0 or 1 + representing an array of dimension + len(coupling_map) X n_registers that specifies the register + latency (1: fast, 0: slow) conditional operations on the gate + conditional (bool): Optionally specify whether this gate supports + conditional operations (true/false). If this is not specified, + then the gate inherits the conditional property of the backend. + description (str): Description of the gate operation + """ + + self.name = name + self.parameters = parameters + self.qasm_def = qasm_def + # coupling_map with length 0 is invalid + if coupling_map: + self.coupling_map = coupling_map + # latency_map with length 0 is invalid + if latency_map: + self.latency_map = latency_map + if conditional is not None: + self.conditional = conditional + if description is not None: + self.description = description + + @classmethod + def from_dict(cls, data): + """Create a new GateConfig object from a dictionary. + + Args: + data (dict): A dictionary representing the GateConfig to create. + It will be in the same format as output by + :func:`to_dict`. + + Returns: + GateConfig: The GateConfig from the input dictionary. + """ + return cls(**data) + + def to_dict(self): + """Return a dictionary format representation of the GateConfig. + + Returns: + dict: The dictionary form of the GateConfig. + """ + out_dict = { + "name": self.name, + "parameters": self.parameters, + "qasm_def": self.qasm_def, + } + if hasattr(self, "coupling_map"): + out_dict["coupling_map"] = self.coupling_map + if hasattr(self, "latency_map"): + out_dict["latency_map"] = self.latency_map + if hasattr(self, "conditional"): + out_dict["conditional"] = self.conditional + if hasattr(self, "description"): + out_dict["description"] = self.description + return out_dict + + def __eq__(self, other): + if isinstance(other, GateConfig): + if self.to_dict() == other.to_dict(): + return True + return False + + def __repr__(self): + out_str = f"GateConfig({self.name}, {self.parameters}, {self.qasm_def}" + for i in ["coupling_map", "latency_map", "conditional", "description"]: + if hasattr(self, i): + out_str += ", " + repr(getattr(self, i)) + out_str += ")" + return out_str + + +class AerBackendConfiguration: + """Class representing an Aer Backend Configuration. + + Attributes: + backend_name: backend name. + backend_version: backend version in the form X.Y.Z. + n_qubits: number of qubits. + basis_gates: list of basis gates names on the backend. + gates: list of basis gates on the backend. + max_shots: maximum number of shots supported. + """ + + _data = {} + + def __init__( + self, + backend_name, + backend_version, + n_qubits, + basis_gates, + gates, + max_shots, + coupling_map, + supported_instructions=None, + dynamic_reprate_enabled=False, + rep_delay_range=None, + default_rep_delay=None, + max_experiments=None, + sample_name=None, + n_registers=None, + register_map=None, + configurable=None, + credits_required=None, + online_date=None, + display_name=None, + description=None, + tags=None, + dt=None, + dtm=None, + processor_type=None, + parametric_pulses=None, + **kwargs, + ): + """Initialize a AerBackendConfiguration Object + + Args: + backend_name (str): The backend name + backend_version (str): The backend version in the form X.Y.Z + n_qubits (int): the number of qubits for the backend + basis_gates (list): The list of strings for the basis gates of the + backends + gates (list): The list of GateConfig objects for the basis gates of + the backend + max_shots (int): The maximum number of shots allowed on the backend + coupling_map (list): The coupling map for the device + supported_instructions (List[str]): Instructions supported by the backend. + dynamic_reprate_enabled (bool): whether delay between programs can be set dynamically + (ie via ``rep_delay``). Defaults to False. + rep_delay_range (List[float]): 2d list defining supported range of repetition + delays for backend in μs. First entry is lower end of the range, second entry is + higher end of the range. Optional, but will be specified when + ``dynamic_reprate_enabled=True``. + default_rep_delay (float): Value of ``rep_delay`` if not specified by user and + ``dynamic_reprate_enabled=True``. + max_experiments (int): The maximum number of experiments per job + sample_name (str): Sample name for the backend + n_registers (int): Number of register slots available for feedback + (if conditional is True) + register_map (list): An array of dimension n_qubits X + n_registers that specifies whether a qubit can store a + measurement in a certain register slot. + configurable (bool): True if the backend is configurable, if the + backend is a simulator + credits_required (bool): True if backend requires credits to run a + job. + online_date (datetime.datetime): The date that the device went online + display_name (str): Alternate name field for the backend + description (str): A description for the backend + tags (list): A list of string tags to describe the backend + dt (float): Qubit drive channel timestep in nanoseconds. + dtm (float): Measurement drive channel timestep in nanoseconds. + processor_type (dict): Processor type for this backend. A dictionary of the + form ``{"family": , "revision": , segment: }`` such as + ``{"family": "Canary", "revision": "1.0", segment: "A"}``. + + - family: Processor family of this backend. + - revision: Revision version of this processor. + - segment: Segment this processor belongs to within a larger chip. + + **kwargs: optional fields + """ + self._data = {} + + self.backend_name = backend_name + self.backend_version = backend_version + self.n_qubits = n_qubits + self.basis_gates = basis_gates + self.gates = gates + self.local = True + self.simulator = True + self.conditional = True + self.memory = True + self.max_shots = max_shots + self.coupling_map = coupling_map + if supported_instructions: + self.supported_instructions = supported_instructions + + self.dynamic_reprate_enabled = dynamic_reprate_enabled + if rep_delay_range: + self.rep_delay_range = [_rd * 1e-6 for _rd in rep_delay_range] # convert to sec + if default_rep_delay is not None: + self.default_rep_delay = default_rep_delay * 1e-6 # convert to sec + + # max_experiments must be >=1 + if max_experiments: + self.max_experiments = max_experiments + if sample_name is not None: + self.sample_name = sample_name + # n_registers must be >=1 + if n_registers: + self.n_registers = 1 + # register_map must have at least 1 entry + if register_map: + self.register_map = register_map + if configurable is not None: + self.configurable = configurable + if credits_required is not None: + self.credits_required = credits_required + if online_date is not None: + self.online_date = online_date + if display_name is not None: + self.display_name = display_name + if description is not None: + self.description = description + if tags is not None: + self.tags = tags + # Add pulse properties here because some backends do not + # fit within the Qasm / Pulse backend partitioning in Qiskit + if dt is not None: + self.dt = dt * 1e-9 + if dtm is not None: + self.dtm = dtm * 1e-9 + if processor_type is not None: + self.processor_type = processor_type + + # convert lo range from GHz to Hz + if "qubit_lo_range" in kwargs: + kwargs["qubit_lo_range"] = [ + [min_range * 1e9, max_range * 1e9] + for (min_range, max_range) in kwargs["qubit_lo_range"] + ] + + if "meas_lo_range" in kwargs: + kwargs["meas_lo_range"] = [ + [min_range * 1e9, max_range * 1e9] + for (min_range, max_range) in kwargs["meas_lo_range"] + ] + + # convert rep_times from μs to sec + if "rep_times" in kwargs: + kwargs["rep_times"] = [_rt * 1e-6 for _rt in kwargs["rep_times"]] + + self._data.update(kwargs) + + def __getattr__(self, name): + try: + return self._data[name] + except KeyError as ex: + raise AttributeError(f"Attribute {name} is not defined") from ex + + @classmethod + def from_dict(cls, data): + """Create a new GateConfig object from a dictionary. + + Args: + data (dict): A dictionary representing the GateConfig to create. + It will be in the same format as output by + :func:`to_dict`. + Returns: + GateConfig: The GateConfig from the input dictionary. + """ + in_data = copy.copy(data) + gates = [GateConfig.from_dict(x) for x in in_data.pop("gates")] + in_data["gates"] = gates + return cls(**in_data) + + def to_dict(self): + """Return a dictionary format representation of the GateConfig. + + Returns: + dict: The dictionary form of the GateConfig. + """ + out_dict = { + "backend_name": self.backend_name, + "backend_version": self.backend_version, + "n_qubits": self.n_qubits, + "basis_gates": self.basis_gates, + "gates": [x.to_dict() for x in self.gates], + "local": self.local, + "simulator": self.simulator, + "conditional": self.conditional, + "memory": self.memory, + "max_shots": self.max_shots, + "coupling_map": self.coupling_map, + "dynamic_reprate_enabled": self.dynamic_reprate_enabled, + } + + if hasattr(self, "supported_instructions"): + out_dict["supported_instructions"] = self.supported_instructions + + if hasattr(self, "rep_delay_range"): + out_dict["rep_delay_range"] = [_rd * 1e6 for _rd in self.rep_delay_range] + if hasattr(self, "default_rep_delay"): + out_dict["default_rep_delay"] = self.default_rep_delay * 1e6 + + for kwarg in [ + "max_experiments", + "sample_name", + "n_registers", + "register_map", + "configurable", + "credits_required", + "online_date", + "display_name", + "description", + "tags", + "dt", + "dtm", + "processor_type", + ]: + if hasattr(self, kwarg): + out_dict[kwarg] = getattr(self, kwarg) + + out_dict.update(self._data) + + if "dt" in out_dict: + out_dict["dt"] *= 1e9 + if "dtm" in out_dict: + out_dict["dtm"] *= 1e9 + + # Use GHz in dict + if "qubit_lo_range" in out_dict: + out_dict["qubit_lo_range"] = [ + [min_range * 1e-9, max_range * 1e-9] + for (min_range, max_range) in out_dict["qubit_lo_range"] + ] + + if "meas_lo_range" in out_dict: + out_dict["meas_lo_range"] = [ + [min_range * 1e-9, max_range * 1e-9] + for (min_range, max_range) in out_dict["meas_lo_range"] + ] + + return out_dict + + @property + def num_qubits(self): + """Returns the number of qubits. + + In future, `n_qubits` should be replaced in favor of `num_qubits` for consistent use + throughout Qiskit. Until this is properly refactored, this property serves as intermediate + solution. + """ + return self.n_qubits + + def __eq__(self, other): + if isinstance(other, AerBackendConfiguration): + if self.to_dict() == other.to_dict(): + return True + return False + + def __contains__(self, item): + return item in self.__dict__ diff --git a/qiskit_aer/backends/backendproperties.py b/qiskit_aer/backends/backendproperties.py new file mode 100644 index 0000000000..07c829004f --- /dev/null +++ b/qiskit_aer/backends/backendproperties.py @@ -0,0 +1,589 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2019, 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. +""" +Aer backend properties +""" +import copy +import datetime +from typing import Any, Iterable, Tuple, Union, Dict +from qiskit.providers.exceptions import BackendPropertyError +from qiskit.utils.units import apply_prefix +from qiskit.transpiler.target import Target + + +PropertyT = Tuple[Any, datetime.datetime] + + +class Nduv: + """Class representing name-date-unit-value + + Attributes: + date: date. + name: name. + unit: unit. + value: value. + """ + + def __init__(self, date, name, unit, value): + """Initialize a new name-date-unit-value object + + Args: + date (datetime.datetime): Date field + name (str): Name field + unit (str): Nduv unit + value (float): The value of the Nduv + """ + self.date = date + self.name = name + self.unit = unit + self.value = value + + @classmethod + def from_dict(cls, data): + """Create a new Nduv object from a dictionary. + + Args: + data (dict): A dictionary representing the Nduv to create. + It will be in the same format as output by + :func:`to_dict`. + + Returns: + Nduv: The Nduv from the input dictionary. + """ + return cls(**data) + + def to_dict(self): + """Return a dictionary format representation of the object. + + Returns: + dict: The dictionary form of the Nduv. + """ + out_dict = { + "date": self.date, + "name": self.name, + "unit": self.unit, + "value": self.value, + } + return out_dict + + def __eq__(self, other): + if isinstance(other, Nduv): + if self.to_dict() == other.to_dict(): + return True + return False + + def __repr__(self): + return f"Nduv({repr(self.date)}, {self.name}, {self.unit}, {self.value})" + + +class GateProperties: + """Class representing a gate's properties + + Attributes: + qubits: qubits. + gate: gate. + parameters: parameters. + """ + + _data = {} + + def __init__(self, qubits, gate, parameters, **kwargs): + """Initialize a new :class:`GateProperties` object + + Args: + qubits (list): A list of integers representing qubits + gate (str): The gates name + parameters (list): List of :class:`Nduv` objects for the + name-date-unit-value for the gate + kwargs: Optional additional fields + """ + self._data = {} + self.qubits = qubits + self.gate = gate + self.parameters = parameters + self._data.update(kwargs) + + def __getattr__(self, name): + try: + return self._data[name] + except KeyError as ex: + raise AttributeError(f"Attribute {name} is not defined") from ex + + @classmethod + def from_dict(cls, data): + """Create a new Gate object from a dictionary. + + Args: + data (dict): A dictionary representing the Gate to create. + It will be in the same format as output by + :func:`to_dict`. + + Returns: + GateProperties: The Nduv from the input dictionary. + """ + in_data = {} + for key, value in data.items(): + if key == "parameters": + in_data[key] = list(map(Nduv.from_dict, value)) + else: + in_data[key] = value + return cls(**in_data) + + def to_dict(self): + """Return a dictionary format representation of the BackendStatus. + + Returns: + dict: The dictionary form of the Gate. + """ + out_dict = {} + out_dict["qubits"] = self.qubits + out_dict["gate"] = self.gate + out_dict["parameters"] = [x.to_dict() for x in self.parameters] + out_dict.update(self._data) + return out_dict + + def __eq__(self, other): + if isinstance(other, GateProperties): + if self.to_dict() == other.to_dict(): + return True + return False + + +class AerBackendProperties: + """Class representing Aer backend properties + + This holds backend properties measured by the provider. All properties + which are provided optionally. These properties may describe qubits, gates, + or other general properties of the backend. + """ + + _data = {} + + def __init__( + self, backend_name, backend_version, last_update_date, qubits, gates, general, **kwargs + ): + """Initialize a BackendProperties instance. + + Args: + backend_name (str): Backend name. + backend_version (str): Backend version in the form X.Y.Z. + last_update_date (datetime.datetime or str): Last date/time that a property was + updated. If specified as a ``str``, it must be in ISO format. + qubits (list): System qubit parameters as a list of lists of + :class:`Nduv` objects + gates (list): System gate parameters as a list of :class:`GateProperties` + objects + general (list): General parameters as a list of :class:`Nduv` + objects + kwargs: optional additional fields + """ + self._data = {} + self.backend_name = backend_name + self.backend_version = backend_version + if isinstance(last_update_date, str): + last_update_date = dateutil.parser.isoparse(last_update_date) + self.last_update_date = last_update_date + self.general = general + self.qubits = qubits + self.gates = gates + + self._qubits = {} + for qubit, props in enumerate(qubits): + formatted_props = {} + for prop in props: + value = self._apply_prefix(prop.value, prop.unit) + formatted_props[prop.name] = (value, prop.date) + self._qubits[qubit] = formatted_props + + self._gates = {} + for gate in gates: + if gate.gate not in self._gates: + self._gates[gate.gate] = {} + formatted_props = {} + for param in gate.parameters: + value = self._apply_prefix(param.value, param.unit) + formatted_props[param.name] = (value, param.date) + self._gates[gate.gate][tuple(gate.qubits)] = formatted_props + self._data.update(kwargs) + + def __getattr__(self, name): + try: + return self._data[name] + except KeyError as ex: + raise AttributeError(f"Attribute {name} is not defined") from ex + + @classmethod + def from_dict(cls, data): + """Create a new BackendProperties object from a dictionary. + + Args: + data (dict): A dictionary representing the BackendProperties to create. It will be in + the same format as output by :meth:`to_dict`. + + Returns: + BackendProperties: The BackendProperties from the input dictionary. + """ + in_data = copy.copy(data) + backend_name = in_data.pop("backend_name") + backend_version = in_data.pop("backend_version") + last_update_date = in_data.pop("last_update_date") + qubits = [] + for qubit in in_data.pop("qubits"): + nduvs = [] + for nduv in qubit: + nduvs.append(Nduv.from_dict(nduv)) + qubits.append(nduvs) + gates = [GateProperties.from_dict(x) for x in in_data.pop("gates")] + general = [Nduv.from_dict(x) for x in in_data.pop("general")] + + return cls( + backend_name, backend_version, last_update_date, qubits, gates, general, **in_data + ) + + def to_dict(self): + """Return a dictionary format representation of the BackendProperties. + + Returns: + dict: The dictionary form of the BackendProperties. + """ + out_dict = { + "backend_name": self.backend_name, + "backend_version": self.backend_version, + "last_update_date": self.last_update_date, + } + out_dict["qubits"] = [] + for qubit in self.qubits: + qubit_props = [] + for item in qubit: + qubit_props.append(item.to_dict()) + out_dict["qubits"].append(qubit_props) + out_dict["gates"] = [x.to_dict() for x in self.gates] + out_dict["general"] = [x.to_dict() for x in self.general] + out_dict.update(self._data) + return out_dict + + def __eq__(self, other): + if isinstance(other, BackendProperties): + if self.to_dict() == other.to_dict(): + return True + return False + + def gate_property( + self, + gate: str, + qubits: Union[int, Iterable[int]] = None, + name: str = None, + ) -> Union[ + Dict[Tuple[int, ...], Dict[str, PropertyT]], + Dict[str, PropertyT], + PropertyT, + ]: + """ + Return the property of the given gate. + + Args: + gate: Name of the gate. + qubits: The qubit to find the property for. + name: Optionally used to specify which gate property to return. + + Returns: + Gate property as a tuple of the value and the time it was measured. + + Raises: + BackendPropertyError: If the property is not found or name is + specified but qubit is not. + """ + try: + result = self._gates[gate] + if qubits is not None: + if isinstance(qubits, int): + qubits = (qubits,) + result = result[tuple(qubits)] + if name: + result = result[name] + elif name: + raise BackendPropertyError(f"Provide qubits to get {name} of {gate}") + except KeyError as ex: + raise BackendPropertyError(f"Could not find the desired property for {gate}") from ex + return result + + def faulty_qubits(self): + """Return a list of faulty qubits.""" + faulty = [] + for qubit in self._qubits: + if not self.is_qubit_operational(qubit): + faulty.append(qubit) + return faulty + + def faulty_gates(self): + """Return a list of faulty gates.""" + faulty = [] + for gate in self.gates: + if not self.is_gate_operational(gate.gate, gate.qubits): + faulty.append(gate) + return faulty + + def is_gate_operational(self, gate: str, qubits: Union[int, Iterable[int]] = None) -> bool: + """ + Return the operational status of the given gate. + + Args: + gate: Name of the gate. + qubits: The qubit to find the operational status for. + + Returns: + bool: Operational status of the given gate. True if the gate is operational, + False otherwise. + """ + properties = self.gate_property(gate, qubits) + if "operational" in properties: + return bool(properties["operational"][0]) + return True # if property operational not existent, then True. + + def gate_error(self, gate: str, qubits: Union[int, Iterable[int]]) -> float: + """ + Return gate error estimates from backend properties. + + Args: + gate: The gate for which to get the error. + qubits: The specific qubits for the gate. + + Returns: + Gate error of the given gate and qubit(s). + """ + return self.gate_property(gate, qubits, "gate_error")[0] # Throw away datetime at index 1 + + def gate_length(self, gate: str, qubits: Union[int, Iterable[int]]) -> float: + """ + Return the duration of the gate in units of seconds. + + Args: + gate: The gate for which to get the duration. + qubits: The specific qubits for the gate. + + Returns: + Gate length of the given gate and qubit(s). + """ + return self.gate_property(gate, qubits, "gate_length")[0] # Throw away datetime at index 1 + + def qubit_property( + self, + qubit: int, + name: str = None, + ) -> Union[ + Dict[str, PropertyT], + PropertyT, + ]: + """ + Return the property of the given qubit. + + Args: + qubit: The property to look for. + name: Optionally used to specify within the hierarchy which property to return. + + Returns: + Qubit property as a tuple of the value and the time it was measured. + + Raises: + BackendPropertyError: If the property is not found. + """ + try: + result = self._qubits[qubit] + if name is not None: + result = result[name] + except KeyError as ex: + formatted_name = "y '" + name + "'" if name else "ies" + raise BackendPropertyError( + f"Couldn't find the propert{formatted_name} for qubit {qubit}." + ) from ex + return result + + def t1(self, qubit: int) -> float: # pylint: disable=invalid-name + """ + Return the T1 time of the given qubit. + + Args: + qubit: Qubit for which to return the T1 time of. + + Returns: + T1 time of the given qubit. + """ + return self.qubit_property(qubit, "T1")[0] # Throw away datetime at index 1 + + def t2(self, qubit: int) -> float: # pylint: disable=invalid-name + """ + Return the T2 time of the given qubit. + + Args: + qubit: Qubit for which to return the T2 time of. + + Returns: + T2 time of the given qubit. + """ + return self.qubit_property(qubit, "T2")[0] # Throw away datetime at index 1 + + def frequency(self, qubit: int) -> float: + """ + Return the frequency of the given qubit. + + Args: + qubit: Qubit for which to return frequency of. + + Returns: + Frequency of the given qubit. + """ + return self.qubit_property(qubit, "frequency")[0] # Throw away datetime at index 1 + + def readout_error(self, qubit: int) -> float: + """ + Return the readout error of the given qubit. + + Args: + qubit: Qubit for which to return the readout error of. + + Return: + Readout error of the given qubit. + """ + return self.qubit_property(qubit, "readout_error")[0] # Throw away datetime at index 1 + + def readout_length(self, qubit: int) -> float: + """ + Return the readout length [sec] of the given qubit. + + Args: + qubit: Qubit for which to return the readout length of. + + Return: + Readout length of the given qubit. + """ + return self.qubit_property(qubit, "readout_length")[0] # Throw away datetime at index 1 + + def is_qubit_operational(self, qubit: int) -> bool: + """ + Return the operational status of the given qubit. + + Args: + qubit: Qubit for which to return operational status of. + + Returns: + Operational status of the given qubit. + """ + properties = self.qubit_property(qubit) + if "operational" in properties: + return bool(properties["operational"][0]) + return True # if property operational not existent, then True. + + def _apply_prefix(self, value: float, unit: str) -> float: + """ + Given a SI unit prefix and value, apply the prefix to convert to + standard SI unit. + + Args: + value: The number to apply prefix to. + unit: String prefix. + + Returns: + Converted value. + + Raises: + BackendPropertyError: If the units aren't recognized. + """ + try: + return apply_prefix(value, unit) + except Exception as ex: + raise BackendPropertyError(f"Could not understand units: {unit}") from ex + + +def target_to_backend_properties(target: Target): + """Convert a :class:`qiskit.transpiler.Target` object into a legacy :class:`~.AerBackendProperties`""" + + properties_dict: dict[str, Any] = { + "backend_name": "", + "backend_version": "", + "last_update_date": None, + "general": [], + } + gates = [] + qubits = [] + for gate, qargs_list in target.items(): + if gate != "measure": + for qargs, props in qargs_list.items(): + property_list = [] + if getattr(props, "duration", None) is not None: + property_list.append( + { + "date": datetime.datetime.now(datetime.timezone.utc), + "name": "gate_length", + "unit": "s", + "value": props.duration, + } + ) + if getattr(props, "error", None) is not None: + property_list.append( + { + "date": datetime.datetime.now(datetime.timezone.utc), + "name": "gate_error", + "unit": "", + "value": props.error, + } + ) + if property_list: + gates.append( + { + "gate": gate, + "qubits": list(qargs), + "parameters": property_list, + "name": gate + "_".join([str(x) for x in qargs]), + } + ) + else: + qubit_props: dict[int, Any] = {} + if target.num_qubits is not None: + qubit_props = {x: None for x in range(target.num_qubits)} + for qargs, props in qargs_list.items(): + if qargs is None: + continue + qubit = qargs[0] + props_list = [] + if getattr(props, "error", None) is not None: + props_list.append( + { + "date": datetime.datetime.now(datetime.timezone.utc), + "name": "readout_error", + "unit": "", + "value": props.error, + } + ) + if getattr(props, "duration", None) is not None: + props_list.append( + { + "date": datetime.datetime.now(datetime.timezone.utc), + "name": "readout_length", + "unit": "s", + "value": props.duration, + } + ) + if not props_list: + qubit_props = {} + break + qubit_props[qubit] = props_list + if qubit_props and all(x is not None for x in qubit_props.values()): + qubits = [qubit_props[i] for i in range(target.num_qubits)] + if gates or qubits: + properties_dict["gates"] = gates + properties_dict["qubits"] = qubits + with warnings.catch_warnings(): + # This raises BackendProperties internally + warnings.filterwarnings("ignore", category=DeprecationWarning) + return AerBackendProperties.from_dict(properties_dict) + else: + return None diff --git a/qiskit_aer/backends/name_mapping.py b/qiskit_aer/backends/name_mapping.py index 5af58e3b07..b9dde39b7b 100644 --- a/qiskit_aer/backends/name_mapping.py +++ b/qiskit_aer/backends/name_mapping.py @@ -16,6 +16,7 @@ """ from qiskit.circuit import ControlledGate, Parameter from qiskit.circuit.reset import Reset +from qiskit.circuit.store import Store from qiskit.circuit.library import ( U2Gate, RGate, @@ -301,4 +302,5 @@ def __init__(self, num_ctrl_qubits, ctrl_state=None): "save_probs_ket": SaveProbabilitiesDict, "save_probs": SaveProbabilities, "reset": Reset(), + "store": Store, } diff --git a/qiskit_aer/backends/qasm_simulator.py b/qiskit_aer/backends/qasm_simulator.py index e40602da12..14e8533b14 100644 --- a/qiskit_aer/backends/qasm_simulator.py +++ b/qiskit_aer/backends/qasm_simulator.py @@ -23,6 +23,8 @@ from ..version import __version__ from ..aererror import AerError from .aerbackend import AerBackend +from .backendconfiguration import AerBackendConfiguration +from .backendproperties import AerBackendProperties, target_to_backend_properties from .backend_utils import ( cpp_execute_qobj, cpp_execute_circuits, @@ -401,7 +403,6 @@ class QasmSimulator(AerBackend): "simulator": True, "local": True, "conditional": True, - "open_pulse": False, "memory": True, "max_shots": int(1e6), "description": "A C++ QasmQobj simulator with noise", @@ -432,7 +433,7 @@ class QasmSimulator(AerBackend): _AVAILABLE_DEVICES = None - def __init__(self, configuration=None, provider=None, **backend_options): + def __init__(self, configuration=None, properties=None, provider=None, **backend_options): warn( "The `QasmSimulator` backend will be deprecated in the" " future. It has been superseded by the `AerSimulator`" @@ -451,13 +452,17 @@ def __init__(self, configuration=None, provider=None, **backend_options): # Default configuration if configuration is None: - configuration = QasmSimulator._DEFAULT_CONFIGURATION + configuration = AerBackendConfiguration.from_dict(QasmSimulator._DEFAULT_CONFIGURATION) + else: + configuration.open_pulse = False # Cache basis gates since computing the intersection # of noise model, method, and config gates is expensive. self._cached_basis_gates = self._DEFAULT_BASIS_GATES - super().__init__(configuration, provider=provider, backend_options=backend_options) + super().__init__( + configuration, properties, provider=provider, backend_options=backend_options + ) def __repr__(self): """String representation of an AerBackend.""" @@ -532,22 +537,22 @@ def from_backend(cls, backend, **options): """Initialize simulator from backend.""" if isinstance(backend, BackendV2): if backend.description is None: - description = "created by QasmSimulator.from_backend" + description = "created by AerSimulator.from_backend" else: description = backend.description - configuration = { - "backend_name": f"aer_simulator_from({backend.name})", - "backend_version": backend.backend_version, - "n_qubits": backend.num_qubits, - "basis_gates": backend.operation_names, - "max_shots": int(1e6), - "coupling_map": ( - None if backend.coupling_map is None else list(backend.coupling_map.get_edges()) - ), - "max_experiments": backend.max_circuits, - "description": description, - } + configuration = AerBackendConfiguration( + backend_name=f"aer_simulator_from({backend.name})", + backend_version=backend.backend_version, + n_qubits=backend.num_qubits, + basis_gates=backend.operation_names, + gates=[], + max_shots=int(1e6), + coupling_map=list(backend.coupling_map.get_edges()), + max_experiments=backend.max_circuits, + description=description, + ) + properties = target_to_backend_properties(backend.target) target = backend.target elif isinstance(backend, BackendV1): # BackendV1 will be removed in Qiskit 2.0, so we will remove this soon @@ -559,15 +564,14 @@ def from_backend(cls, backend, **options): stacklevel=2, ) # Get configuration and properties from backend - config = backend.configuration() + configuration = backend.configuration() properties = copy.copy(backend.properties()) # Customize configuration name - name = config.backend_name - config.backend_name = f"aer_simulator_from({name})" + name = configuration.backend_name + configuration.backend_name = f"aer_simulator_from({name})" target = convert_to_target(config, properties, None, NAME_MAPPING) - configuration = config.to_dict() else: raise TypeError( "The backend argument requires a BackendV2 or BackendV1 object, " @@ -584,7 +588,7 @@ def from_backend(cls, backend, **options): options["noise_model"] = noise_model # Initialize simulator - sim = cls(configuration=configuration, target=target, **options) + sim = cls(configuration=configuration, properties=properties, target=target, **options) return sim def configuration(self): @@ -593,13 +597,13 @@ def configuration(self): Returns: BackendConfiguration: the configuration for the backend. """ - config = self._configuration.copy() + config = copy.copy(self._configuration) for key, val in self._options_configuration.items(): - config[key] = val + setattr(config, key, val) # Update basis gates based on custom options, config, method, # and noise model - config["custom_instructions"] = self._custom_instructions() - config["basis_gates"] = self._cached_basis_gates + config["custom_instructions"] + config.custom_instructions = self._custom_instructions() + config.basis_gates = self._cached_basis_gates + config.custom_instructions return config def available_methods(self): @@ -681,7 +685,7 @@ def _basis_gates(self): # Compute intersection with method basis gates method_gates = self._method_basis_gates() - config_gates = self.configuration()["basis_gates"] + config_gates = self._configuration.basis_gates if config_gates: basis_gates = set(config_gates).intersection(method_gates) else: diff --git a/qiskit_aer/backends/statevector_simulator.py b/qiskit_aer/backends/statevector_simulator.py index 179512c92f..9860b6e91b 100644 --- a/qiskit_aer/backends/statevector_simulator.py +++ b/qiskit_aer/backends/statevector_simulator.py @@ -241,7 +241,7 @@ class StatevectorSimulator(AerBackend): _AVAILABLE_DEVICES = None - def __init__(self, configuration=None, provider=None, **backend_options): + def __init__(self, configuration=None, properties=None, provider=None, **backend_options): warn( "The `StatevectorSimulator` backend will be deprecated in the" " future. It has been superseded by the `AerSimulator`" @@ -257,9 +257,15 @@ def __init__(self, configuration=None, provider=None, **backend_options): StatevectorSimulator._AVAILABLE_DEVICES = available_devices(self._controller) if configuration is None: - configuration = StatevectorSimulator._DEFAULT_CONFIGURATION + configuration = QasmBackendConfiguration.from_dict( + StatevectorSimulator._DEFAULT_CONFIGURATION + ) + else: + configuration.open_pulse = False - super().__init__(configuration, provider=provider, backend_options=backend_options) + super().__init__( + configuration, properties=properties, provider=provider, backend_options=backend_options + ) @classmethod def _default_options(cls): diff --git a/qiskit_aer/backends/unitary_simulator.py b/qiskit_aer/backends/unitary_simulator.py index 21dcbb725e..ae85fabbbb 100644 --- a/qiskit_aer/backends/unitary_simulator.py +++ b/qiskit_aer/backends/unitary_simulator.py @@ -227,7 +227,7 @@ class UnitarySimulator(AerBackend): _AVAILABLE_DEVICES = None - def __init__(self, configuration=None, provider=None, **backend_options): + def __init__(self, configuration=None, properties=None, provider=None, **backend_options): warn( "The `UnitarySimulator` backend will be deprecated in the" " future. It has been superseded by the `AerSimulator`" @@ -243,9 +243,15 @@ def __init__(self, configuration=None, provider=None, **backend_options): UnitarySimulator._AVAILABLE_DEVICES = available_devices(self._controller) if configuration is None: - configuration = UnitarySimulator._DEFAULT_CONFIGURATION + configuration = QasmBackendConfiguration.from_dict( + UnitarySimulator._DEFAULT_CONFIGURATION + ) + else: + configuration.open_pulse = False - super().__init__(configuration, provider=provider, backend_options=backend_options) + super().__init__( + configuration, properties=properties, provider=provider, backend_options=backend_options + ) @classmethod def _default_options(cls): diff --git a/test/terra/backends/aer_simulator/test_options.py b/test/terra/backends/aer_simulator/test_options.py index 41210fcb74..49c9e3cec5 100644 --- a/test/terra/backends/aer_simulator/test_options.py +++ b/test/terra/backends/aer_simulator/test_options.py @@ -150,12 +150,11 @@ def test_option_basis_gates(self, method): noise_gates = ["id", "sx", "x", "cx"] noise_model = NoiseModel(basis_gates=noise_gates) target_gates = ( - sorted(set(config["basis_gates"]).intersection(noise_gates)) - + config["custom_instructions"] + sorted(set(config.basis_gates).intersection(noise_gates)) + config.custom_instructions ) sim = self.backend(method=method, noise_model=noise_model) - basis_gates = sim.configuration()["basis_gates"] + basis_gates = sim.configuration().basis_gates self.assertEqual(sorted(basis_gates), sorted(target_gates)) @data( @@ -170,9 +169,9 @@ def test_option_order_basis_gates(self, method): """Test order of setting method and noise model gives same basis gates""" noise_model = NoiseModel(basis_gates=["id", "sx", "x", "cx"]) sim1 = self.backend(method=method, noise_model=noise_model) - basis_gates1 = sim1.configuration()["basis_gates"] + basis_gates1 = sim1.configuration().basis_gates sim2 = self.backend(noise_model=noise_model, method=method) - basis_gates2 = sim2.configuration()["basis_gates"] + basis_gates2 = sim2.configuration().basis_gates self.assertEqual(sorted(basis_gates1), sorted(basis_gates2)) @supported_methods( @@ -306,7 +305,7 @@ def test_num_qubits(self, method): num_qubits = 20 fake_backend = GenericBackendV2(num_qubits=num_qubits) backend = AerSimulator.from_backend(fake_backend, method=method) - self.assertGreaterEqual(backend.configuration()["n_qubits"], num_qubits) + self.assertGreaterEqual(backend.configuration().num_qubits, num_qubits) def test_mps_svd_method(self): """Test env. variabe to change MPS SVD method"""