diff --git a/qiskit/providers/fake_provider/__init__.py b/qiskit/providers/fake_provider/__init__.py index 446f7fabe37f..7738c7ff244a 100644 --- a/qiskit/providers/fake_provider/__init__.py +++ b/qiskit/providers/fake_provider/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 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 @@ -20,10 +20,11 @@ Overview ======== -The fake provider module contains fake providers and fake backends classes. The fake backends are -built to mimic the behaviors of IBM Quantum systems using system snapshots. The system snapshots -contain important information about the quantum system such as coupling map, basis gates, qubit -properties (T1, T2, error rate, etc.) which are useful for testing the transpiler and performing +The fake provider module contains fake providers, fake backends and other simulated backend +implementations. The fake backends are built to mimic the behaviors of IBM Quantum systems +using system snapshots. The system snapshots contain important information about the quantum +system such as coupling map, basis gates, qubit properties (T1, T2, error rate, etc.) which +are useful for testing the transpiler and performing noisy simulation of the system. Example Usage @@ -210,6 +211,7 @@ FakeBackendV2 FakeBackend5QV2 FakeMumbaiFractionalCX + GenericBackendV2 Fake Backend Base Classes ========================= @@ -241,10 +243,11 @@ # Standard fake backends with IBM Quantum systems snapshots from .backends import * -# Special fake backends for special testing perpurposes +# Special fake backends for special testing purposes from .fake_qasm_simulator import FakeQasmSimulator from .fake_openpulse_2q import FakeOpenPulse2Q from .fake_openpulse_3q import FakeOpenPulse3Q from .fake_1q import Fake1Q from .fake_backend_v2 import FakeBackendV2, FakeBackend5QV2 from .fake_mumbai_v2 import FakeMumbaiFractionalCX +from .generic_backend_v2 import GenericBackendV2 diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py new file mode 100644 index 000000000000..33f69a2ddaba --- /dev/null +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -0,0 +1,548 @@ +# 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. + +"""Generic BackendV2 class that with a simulated ``run``.""" + +from __future__ import annotations +import warnings + +from collections.abc import Iterable +import numpy as np + +from qiskit import pulse +from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap +from qiskit.circuit import QuantumCircuit, Instruction +from qiskit.circuit.controlflow import ( + IfElseOp, + WhileLoopOp, + ForLoopOp, + SwitchCaseOp, + BreakLoopOp, + ContinueLoopOp, +) +from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping +from qiskit.exceptions import QiskitError +from qiskit.transpiler import CouplingMap, Target, InstructionProperties, QubitProperties +from qiskit.providers import Options +from qiskit.providers.basicaer import BasicAer +from qiskit.providers.backend import BackendV2 +from qiskit.providers.models import ( + PulseDefaults, + Command, +) +from qiskit.qobj import PulseQobjInstruction, PulseLibraryItem +from qiskit.utils import optionals as _optionals + +# Noise default values/ranges for duration and error of supported +# instructions. There are two possible formats: +# - (min_duration, max_duration, min_error, max_error), +# if the defaults are ranges. +# - (duration, error), if the defaults are fixed values. +_NOISE_DEFAULTS = { + "cx": (1e-8, 9e-7, 1e-5, 5e-3), + "ecr": (1e-8, 9e-7, 1e-5, 5e-3), + "cz": (1e-8, 9e-7, 1e-5, 5e-3), + "id": (3e-8, 4e-8, 9e-5, 1e-4), + "rz": (0.0, 0.0), + "sx": (1e-8, 9e-7, 1e-5, 5e-3), + "x": (1e-8, 9e-7, 1e-5, 5e-3), + "measure": (1e-8, 9e-7, 1e-5, 5e-3), + "delay": (None, None), + "reset": (None, None), +} + +# Fallback values for gates with unknown noise default ranges. +_NOISE_DEFAULTS_FALLBACK = (1e-8, 9e-7, 1e-5, 5e-3) + +# Ranges to sample qubit properties from. +_QUBIT_PROPERTIES = { + "dt": 0.222e-9, + "t1": (100e-6, 200e-6), + "t2": (100e-6, 200e-6), + "frequency": (5e9, 5.5e9), +} + +# The number of samples determines the pulse durations of the corresponding +# instructions. This default defines pulses with durations in multiples of +# 16 dt for consistency with the pulse granularity of real IBM devices, but +# keeps the number smaller than what would be realistic for +# manageability. If needed, more realistic durations could be added in the +# future (order of 160dt for 1q gates, 1760dt for 2q gates and measure). +_PULSE_LIBRARY = [ + PulseLibraryItem(name="pulse_1", samples=np.linspace(0, 1.0, 16, dtype=np.complex128)), # 16dt + PulseLibraryItem(name="pulse_2", samples=np.linspace(0, 1.0, 32, dtype=np.complex128)), # 32dt + PulseLibraryItem(name="pulse_3", samples=np.linspace(0, 1.0, 64, dtype=np.complex128)), # 64dt +] + + +class GenericBackendV2(BackendV2): + """Generic :class:`~.BackendV2` implementation with a configurable constructor. This class will + return a :class:`~.BackendV2` instance that runs on a local simulator (in the spirit of fake + backends) and contains all the necessary information to test backend-interfacing components, such + as the transpiler. A :class:`.GenericBackendV2` instance can be constructed from as little as a + specified ``num_qubits``, but users can additionally configure the basis gates, coupling map, + ability to run dynamic circuits (control flow instructions), instruction calibrations and dtm. + The remainder of the backend properties are generated by randomly sampling + from default ranges extracted from historical IBM backend data. The seed for this random + generation can be fixed to ensure the reproducibility of the backend output. + This backend only supports gates in the standard library, if you need a more flexible backend, + there is always the option to directly instantiate a :class:`.Target` object to use for + transpilation. + """ + + def __init__( + self, + num_qubits: int, + basis_gates: list[str] | None = None, + *, + coupling_map: list[list[int]] | CouplingMap | None = None, + control_flow: bool = False, + calibrate_instructions: bool | InstructionScheduleMap | None = None, + dtm: float | None = None, + seed: int | None = None, + ): + """ + Args: + num_qubits: Number of qubits that will be used to construct the backend's target. + Note that, while there is no limit in the size of the target that can be + constructed, this backend runs on local noisy simulators, and these might + present limitations in the number of qubits that can be simulated. + + basis_gates: List of basis gate names to be supported by + the target. These must be part of the standard qiskit circuit library. + The default set of basis gates is ``["id", "rz", "sx", "x", "cx"]`` + The ``"reset"``, ``"delay"``, and ``"measure"`` instructions are + always supported by default, even if not specified via ``basis_gates``. + + coupling_map: Optional coupling map + for the backend. Multiple formats are supported: + + #. :class:`~.CouplingMap` instance + #. List, must be given as an edge list representing the two qubit interactions + supported by the backend, for example: + ``[[0, 1], [0, 3], [1, 2], [1, 5], [2, 5], [4, 1], [5, 3]]`` + + If ``coupling_map`` is specified, it must match the number of qubits + specified in ``num_qubits``. If ``coupling_map`` is not specified, + a fully connected coupling map will be generated with ``num_qubits`` + qubits. + + control_flow: Flag to enable control flow directives on the target + (defaults to False). + + calibrate_instructions: Instruction calibration settings, this argument + supports both boolean and :class:`.InstructionScheduleMap` as + input types, and is ``None`` by default: + + #. If ``calibrate_instructions==None``, no calibrations will be added to the target. + #. If ``calibrate_instructions==True``, all gates will be calibrated for all + qubits using the default pulse schedules generated internally. + #. If ``calibrate_instructions==False``, all gates will be "calibrated" for + all qubits with an empty pulse schedule. + #. If an :class:`.InstructionScheduleMap` instance is given, the calibrations + in this instruction schedule map will be appended to the target + instead of the default pulse schedules (this allows for custom calibrations). + + dtm: System time resolution of output signals in nanoseconds. + None by default. + + seed: Optional seed for generation of default values. + """ + + super().__init__( + provider=None, + name=f"generic_backend_{num_qubits}q", + description=f"This is a device with {num_qubits} qubits and generic settings.", + backend_version="", + ) + + self._sim = None + self._rng = np.random.default_rng(seed=seed) + self._dtm = dtm + self._num_qubits = num_qubits + self._control_flow = control_flow + self._calibrate_instructions = calibrate_instructions + self._supported_gates = get_standard_gate_name_mapping() + + if coupling_map is None: + self._coupling_map = CouplingMap().from_full(num_qubits) + else: + if isinstance(coupling_map, CouplingMap): + self._coupling_map = coupling_map + else: + self._coupling_map = CouplingMap(coupling_map) + + if num_qubits != self._coupling_map.size(): + raise QiskitError( + f"The number of qubits (got {num_qubits}) must match " + f"the size of the provided coupling map (got {coupling_map.size()})." + ) + + self._basis_gates = ( + basis_gates if basis_gates is not None else ["cx", "id", "rz", "sx", "x"] + ) + for name in ["reset", "delay", "measure"]: + if name not in self._basis_gates: + self._basis_gates.append(name) + + self._build_generic_target() + self._build_default_channels() + + @property + def target(self): + return self._target + + @property + def max_circuits(self): + return None + + @property + def dtm(self) -> float: + """Return the system time resolution of output signals""" + # converting `dtm` from nanoseconds to seconds + return self._dtm * 1e-9 if self._dtm is not None else None + + @property + def meas_map(self) -> list[list[int]]: + return self._target.concurrent_measurements + + def _build_default_channels(self) -> None: + channels_map = { + "acquire": {(i,): [pulse.AcquireChannel(i)] for i in range(self.num_qubits)}, + "drive": {(i,): [pulse.DriveChannel(i)] for i in range(self.num_qubits)}, + "measure": {(i,): [pulse.MeasureChannel(i)] for i in range(self.num_qubits)}, + "control": { + (edge): [pulse.ControlChannel(i)] for i, edge in enumerate(self._coupling_map) + }, + } + setattr(self, "channels_map", channels_map) + + def _get_noise_defaults(self, name: str) -> tuple: + """Return noise default values/ranges for duration and error of supported + instructions. There are two possible formats: + - (min_duration, max_duration, min_error, max_error), + if the defaults are ranges. + - (duration, error), if the defaults are fixed values. + """ + return _NOISE_DEFAULTS.get(name, (1e-8, 9e-7, 1e-5, 5e-3)) + + def _get_calibration_sequence( + self, inst: str, num_qubits: int, qargs: tuple[int] + ) -> list[PulseQobjInstruction]: + """Return calibration pulse sequence for given instruction (defined by name and num_qubits) + acting on qargs. + """ + + pulse_library = _PULSE_LIBRARY + # Note that the calibration pulses are different for + # 1q gates vs 2q gates vs measurement instructions. + if inst == "measure": + sequence = [ + PulseQobjInstruction( + name="acquire", + duration=1792, + t0=0, + qubits=list(range(num_qubits)), + memory_slot=list(range(num_qubits)), + ) + ] + [PulseQobjInstruction(name=pulse_library[1], ch=f"m{i}", t0=0) for i in qargs] + return sequence + if num_qubits == 1: + return [ + PulseQobjInstruction(name="fc", ch=f"u{qargs}", t0=0, phase="-P0"), + PulseQobjInstruction(name=pulse_library[0].name, ch=f"d{qargs}", t0=0), + ] + return [ + PulseQobjInstruction(name=pulse_library[1].name, ch=f"d{qargs[0]}", t0=0), + PulseQobjInstruction(name=pulse_library[2].name, ch=f"u{qargs[0]}", t0=0), + PulseQobjInstruction(name=pulse_library[1].name, ch=f"d{qargs[1]}", t0=0), + PulseQobjInstruction(name="fc", ch=f"d{qargs[1]}", t0=0, phase=2.1), + ] + + def _generate_calibration_defaults(self) -> PulseDefaults: + """Generate pulse calibration defaults as specified with `self._calibrate_instructions`. + If `self._calibrate_instructions` is True, the pulse schedules will be generated from + a series of default calibration sequences. If `self._calibrate_instructions` is False, + the pulse schedules will contain empty calibration sequences, but still be generated and + added to the target. + """ + + # If self._calibrate_instructions==True, this method + # will generate default pulse schedules for all gates in self._basis_gates, + # except for `delay` and `reset`. + calibration_buffer = self._basis_gates.copy() + for inst in ["delay", "reset"]: + calibration_buffer.remove(inst) + + # List of calibration commands (generated from sequences of PulseQobjInstructions) + # corresponding to each calibrated instruction. Note that the calibration pulses + # are different for 1q gates vs 2q gates vs measurement instructions. + cmd_def = [] + for inst in calibration_buffer: + num_qubits = self._supported_gates[inst].num_qubits + qarg_set = self._coupling_map if num_qubits > 1 else list(range(self.num_qubits)) + if inst == "measure": + cmd_def.append( + Command( + name=inst, + qubits=qarg_set, + sequence=( + self._get_calibration_sequence(inst, num_qubits, qarg_set) + if self._calibrate_instructions + else [] + ), + ) + ) + else: + for qarg in qarg_set: + qubits = [qarg] if num_qubits == 1 else qarg + cmd_def.append( + Command( + name=inst, + qubits=qubits, + sequence=( + self._get_calibration_sequence(inst, num_qubits, qubits) + if self._calibrate_instructions + else [] + ), + ) + ) + + qubit_freq_est = np.random.normal(4.8, scale=0.01, size=self.num_qubits).tolist() + meas_freq_est = np.linspace(6.4, 6.6, self.num_qubits).tolist() + return PulseDefaults( + qubit_freq_est=qubit_freq_est, + meas_freq_est=meas_freq_est, + buffer=0, + pulse_library=_PULSE_LIBRARY, + cmd_def=cmd_def, + ) + + def _build_generic_target(self): + """This method generates a :class:`~.Target` instance with + default qubit, instruction and calibration properties. + """ + # the qubit properties are sampled from default ranges + properties = _QUBIT_PROPERTIES + self._target = Target( + description=f"Generic Target with {self._num_qubits} qubits", + num_qubits=self._num_qubits, + dt=properties["dt"], + qubit_properties=[ + QubitProperties( + t1=self._rng.uniform(properties["t1"][0], properties["t1"][1]), + t2=self._rng.uniform(properties["t2"][0], properties["t2"][1]), + frequency=self._rng.uniform( + properties["frequency"][0], properties["frequency"][1] + ), + ) + for _ in range(self._num_qubits) + ], + concurrent_measurements=[list(range(self._num_qubits))], + ) + + # Generate instruction schedule map with calibrations to add to target. + calibration_inst_map = None + if self._calibrate_instructions is not None: + if isinstance(self._calibrate_instructions, InstructionScheduleMap): + calibration_inst_map = self._calibrate_instructions + else: + defaults = self._generate_calibration_defaults() + calibration_inst_map = defaults.instruction_schedule_map + + # Iterate over gates, generate noise params from defaults, + # and add instructions, noise and calibrations to target. + for name in self._basis_gates: + if name not in self._supported_gates: + raise QiskitError( + f"Provided basis gate {name} is not an instruction " + f"in the standard qiskit circuit library." + ) + gate = self._supported_gates[name] + noise_params = self._get_noise_defaults(name) + self._add_noisy_instruction_to_target(gate, noise_params, calibration_inst_map) + + if self._control_flow: + self._target.add_instruction(IfElseOp, name="if_else") + self._target.add_instruction(WhileLoopOp, name="while_loop") + self._target.add_instruction(ForLoopOp, name="for_loop") + self._target.add_instruction(SwitchCaseOp, name="switch_case") + self._target.add_instruction(BreakLoopOp, name="break") + self._target.add_instruction(ContinueLoopOp, name="continue") + + def _add_noisy_instruction_to_target( + self, + instruction: Instruction, + noise_params: tuple[float, ...] | None, + calibration_inst_map: InstructionScheduleMap | None, + ) -> None: + """Add instruction properties to target for specified instruction. + + Args: + instruction: Instance of instruction to be added to the target + noise_params: Error and duration noise values/ranges to + include in instruction properties. + calibration_inst_map: Instruction schedule map with calibration defaults + """ + qarg_set = self._coupling_map if instruction.num_qubits > 1 else range(self.num_qubits) + props = {} + for qarg in qarg_set: + try: + qargs = tuple(qarg) + except TypeError: + qargs = (qarg,) + duration, error = ( + noise_params + if len(noise_params) == 2 + else (self._rng.uniform(*noise_params[:2]), self._rng.uniform(*noise_params[2:])) + ) + if ( + calibration_inst_map is not None + and instruction.name not in ["reset", "delay"] + and qarg in calibration_inst_map.qubits_with_instruction(instruction.name) + ): + # Do NOT call .get method. This parses Qobj immediately. + # This operation is computationally expensive and should be bypassed. + calibration_entry = calibration_inst_map._get_calibration_entry( + instruction.name, qargs + ) + else: + calibration_entry = None + props.update({qargs: InstructionProperties(duration, error, calibration_entry)}) + self._target.add_instruction(instruction, props) + + # The "measure" instruction calibrations need to be added qubit by qubit, once the + # instruction has been added to the target. + if calibration_inst_map is not None and instruction.name == "measure": + for qarg in calibration_inst_map.qubits_with_instruction(instruction.name): + try: + qargs = tuple(qarg) + except TypeError: + qargs = (qarg,) + # Do NOT call .get method. This parses Qobj immediately. + # This operation is computationally expensive and should be bypassed. + calibration_entry = calibration_inst_map._get_calibration_entry( + instruction.name, qargs + ) + for qubit in qargs: + self._target[instruction.name][(qubit,)].calibration = calibration_entry + + def run(self, run_input, **options): + """Run on the backend using a simulator. + + This method runs circuit jobs (an individual or a list of :class:`~.QuantumCircuit` + ) and pulse jobs (an individual or a list of :class:`~.Schedule` or + :class:`~.ScheduleBlock`) using :class:`~.BasicAer` or Aer simulator and returns a + :class:`~qiskit.providers.Job` object. + + If qiskit-aer is installed, jobs will be run using the ``AerSimulator`` with + noise model of the backend. Otherwise, jobs will be run using the + ``BasicAer`` simulator without noise. + + Noisy simulations of pulse jobs are not yet supported in :class:`~.GenericBackendV2`. + + Args: + run_input (QuantumCircuit or Schedule or ScheduleBlock or list): An + individual or a list of + :class:`~qiskit.circuit.QuantumCircuit`, + :class:`~qiskit.pulse.ScheduleBlock`, or + :class:`~qiskit.pulse.Schedule` objects to run on the backend. + options: Any kwarg options to pass to the backend for running the + config. If a key is also present in the options + attribute/object, then the expectation is that the value + specified will be used instead of what's set in the options + object. + + Returns: + Job: The job object for the run + + Raises: + QiskitError: If a pulse job is supplied and qiskit_aer is not installed. + """ + circuits = run_input + pulse_job = None + if isinstance(circuits, (pulse.Schedule, pulse.ScheduleBlock)): + pulse_job = True + elif isinstance(circuits, QuantumCircuit): + pulse_job = False + elif isinstance(circuits, list): + if circuits: + if all(isinstance(x, (pulse.Schedule, pulse.ScheduleBlock)) for x in circuits): + pulse_job = True + elif all(isinstance(x, QuantumCircuit) for x in circuits): + pulse_job = False + if pulse_job is None: # submitted job is invalid + raise QiskitError( + "Invalid input object %s, must be either a " + "QuantumCircuit, Schedule, or a list of either" % circuits + ) + if pulse_job: # pulse job + raise QiskitError("Pulse simulation is currently not supported for V2 backends.") + # circuit job + if not _optionals.HAS_AER: + warnings.warn("Aer not found using BasicAer and no noise", RuntimeWarning) + if self._sim is None: + self._setup_sim() + self._sim._options = self._options + job = self._sim.run(circuits, **options) + return job + + def _setup_sim(self) -> None: + if _optionals.HAS_AER: + from qiskit_aer import AerSimulator + from qiskit_aer.noise import NoiseModel + + self._sim = AerSimulator() + noise_model = NoiseModel.from_backend(self) + self._sim.set_options(noise_model=noise_model) + # Update backend default too to avoid overwriting + # it when run() is called + self.set_options(noise_model=noise_model) + else: + self._sim = BasicAer.get_backend("qasm_simulator") + + @classmethod + def _default_options(cls) -> Options: + if _optionals.HAS_AER: + from qiskit_aer import AerSimulator + + return AerSimulator._default_options() + else: + return BasicAer.get_backend("qasm_simulator")._default_options() + + def drive_channel(self, qubit: int): + drive_channels_map = getattr(self, "channels_map", {}).get("drive", {}) + qubits = (qubit,) + if qubits in drive_channels_map: + return drive_channels_map[qubits][0] + return None + + def measure_channel(self, qubit: int): + measure_channels_map = getattr(self, "channels_map", {}).get("measure", {}) + qubits = (qubit,) + if qubits in measure_channels_map: + return measure_channels_map[qubits][0] + return None + + def acquire_channel(self, qubit: int): + acquire_channels_map = getattr(self, "channels_map", {}).get("acquire", {}) + qubits = (qubit,) + if qubits in acquire_channels_map: + return acquire_channels_map[qubits][0] + return None + + def control_channel(self, qubits: Iterable[int]): + control_channels_map = getattr(self, "channels_map", {}).get("control", {}) + qubits = tuple(qubits) + if qubits in control_channels_map: + return control_channels_map[qubits] + return [] diff --git a/releasenotes/notes/add-generic-fake-backend-c1434e0c5c413935.yaml b/releasenotes/notes/add-generic-fake-backend-c1434e0c5c413935.yaml new file mode 100644 index 000000000000..00f7eeea6800 --- /dev/null +++ b/releasenotes/notes/add-generic-fake-backend-c1434e0c5c413935.yaml @@ -0,0 +1,63 @@ +--- +features: + - | + A new class, :class:`.GenericBackendV2` has been added to the :mod:`qiskit.providers.fake_provider` + module. This class is configurable, and builds a :class:`~.BackendV2` backend instance that can + be run locally (in the spirit of fake backends). Users can configure the number of qubits, basis gates, + coupling map, ability to run dynamic circuits (control flow instructions), instruction calibrations and + dtm of the backend without having to deal with manual target construction. + Qubit and gate properties are generated by randomly sampling from default ranges. The seed for this + random generation can be fixed to ensure the reproducibility of the backend output. + It's important to note that this backend only supports gates in the standard + library. If you need a more flexible backend, there is always the option to directly instantiate a + :class:`.Target` object to use for transpilation. + + Example usage 1:: + + from qiskit import QuantumCircuit, transpile + from qiskit.providers.fake_provider import GenericBackendV2 + + # Create a simple circuit + circuit = QuantumCircuit(3) + circuit.h(0) + circuit.cx(0,1) + circuit.cx(0,2) + circuit.measure_all() + circuit.draw('mpl') + + # Define backend with 3 qubits + backend = GenericBackendV2(num_qubits=3) + + # Transpile and run + transpiled_circuit = transpile(circuit, backend) + result = backend.run(transpiled_circuit).result() + + Example usage 2:: + + from qiskit import QuantumCircuit, ClassicalRegister, transpile + from qiskit.providers.fake_provider import GenericBackendV2 + + # Create a circuit with classical control + creg = ClassicalRegister(19) + qc = QuantumCircuit(25) + qc.add_register(creg) + qc.h(0) + for i in range(18): + qc.cx(0, i + 1) + for i in range(18): + qc.measure(i, creg[i]) + qc.ecr(20, 21).c_if(creg, 0) + + # Define backend with custom basis gates and control flow instructions + backend = GenericBackendV2(num_qubits=25, basis_gates = ["ecr","id","rz","sx","x"], control_flow=True) + + #Transpile + transpiled_qc = transpile(qc, backend) + + .. note:: + + The noise properties generated by these class do not mimic any concrete quantum device, and should + not be used to measure any concrete behaviors. They are "reasonable defaults" that can be used to + test backend-interfacing functionality not tied specific noise values of real quantum systems. + For a more accurate simulation of existing devices, you can manually build a noise model from the + real backend using the functionality offered in ``qiskit-aer``. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 6390a6e1c85b..1aa34edbb50c 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2019. +# (C) Copyright IBM 2017, 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 @@ -36,7 +36,6 @@ IfElseOp, Parameter, Qubit, - Reset, SwitchCaseOp, WhileLoopOp, ) @@ -48,6 +47,8 @@ ) from qiskit.circuit.classical import expr from qiskit.circuit.delay import Delay +from qiskit.circuit.measure import Measure +from qiskit.circuit.reset import Reset from qiskit.circuit.library import ( CXGate, CZGate, @@ -60,25 +61,20 @@ SXGate, SXdgGate, SdgGate, - U1Gate, U2Gate, UGate, XGate, ZGate, ) -from qiskit.circuit.measure import Measure from qiskit.compiler import transpile from qiskit.converters import circuit_to_dag from qiskit.dagcircuit import DAGOpNode, DAGOutNode from qiskit.exceptions import QiskitError from qiskit.providers.backend import BackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.providers.fake_provider import ( - FakeBoeblingen, FakeMelbourne, - FakeMumbaiV2, - FakeNairobiV2, FakeRueschlikon, - FakeSherbrooke, FakeVigo, ) from qiskit.providers.options import Options @@ -160,7 +156,7 @@ def test_num_processes_kwarg_concurrent_default(self, num_processes): qc.h(0) qc.cx(0, 1) qc.measure_all() - target = FakeMumbaiV2().target + target = GenericBackendV2(num_qubits=27).target res = transpile([qc] * 3, target=target, num_processes=num_processes) self.assertIsInstance(res, list) for circ in res: @@ -206,6 +202,49 @@ def test_transpile_non_adjacent_layout(self): 13 - 12 - 11 - 10 - 9 - 8 - 7 """ + cmap = [ + [0, 1], + [0, 14], + [1, 0], + [1, 2], + [1, 13], + [2, 1], + [2, 3], + [2, 12], + [3, 2], + [3, 4], + [3, 11], + [4, 3], + [4, 5], + [4, 10], + [5, 4], + [5, 6], + [5, 9], + [6, 5], + [6, 8], + [7, 8], + [8, 6], + [8, 7], + [8, 9], + [9, 5], + [9, 8], + [9, 10], + [10, 4], + [10, 9], + [10, 11], + [11, 3], + [11, 10], + [11, 12], + [12, 2], + [12, 11], + [12, 13], + [13, 1], + [13, 12], + [13, 14], + [14, 0], + [14, 13], + ] + qr = QuantumRegister(4, "qr") circuit = QuantumCircuit(qr) circuit.h(qr[0]) @@ -213,14 +252,15 @@ def test_transpile_non_adjacent_layout(self): circuit.cx(qr[1], qr[2]) circuit.cx(qr[2], qr[3]) - coupling_map = FakeMelbourne().configuration().coupling_map - basis_gates = FakeMelbourne().configuration().basis_gates + backend = GenericBackendV2( + num_qubits=15, basis_gates=["ecr", "id", "rz", "sx", "x"], coupling_map=cmap + ) initial_layout = [None, qr[0], qr[1], qr[2], None, qr[3]] new_circuit = transpile( circuit, - basis_gates=basis_gates, - coupling_map=coupling_map, + basis_gates=backend.operation_names, + coupling_map=backend.coupling_map, initial_layout=initial_layout, ) @@ -228,10 +268,33 @@ def test_transpile_non_adjacent_layout(self): for instruction in new_circuit.data: if isinstance(instruction.operation, CXGate): - self.assertIn([qubit_indices[x] for x in instruction.qubits], coupling_map) + self.assertIn([qubit_indices[x] for x in instruction.qubits], backend.coupling_map) def test_transpile_qft_grid(self): """Transpile pipeline can handle 8-qubit QFT on 14-qubit grid.""" + + coupling_map = [ + [1, 0], + [1, 2], + [2, 3], + [4, 3], + [4, 10], + [5, 4], + [5, 6], + [5, 9], + [6, 8], + [7, 8], + [9, 8], + [9, 10], + [11, 3], + [11, 10], + [11, 12], + [12, 2], + [13, 1], + [13, 12], + ] + basis_gates = ["cx", "id", "rz", "sx", "x"] + qr = QuantumRegister(8) circuit = QuantumCircuit(qr) for i, _ in enumerate(qr): @@ -239,8 +302,6 @@ def test_transpile_qft_grid(self): circuit.cp(math.pi / float(2 ** (i - j)), qr[i], qr[j]) circuit.h(qr[i]) - coupling_map = FakeMelbourne().configuration().coupling_map - basis_gates = FakeMelbourne().configuration().basis_gates new_circuit = transpile(circuit, basis_gates=basis_gates, coupling_map=coupling_map) qubit_indices = {bit: idx for idx, bit in enumerate(new_circuit.qubits)} @@ -254,9 +315,34 @@ def test_already_mapped_1(self): See: https://github.com/Qiskit/qiskit-terra/issues/342 """ - backend = FakeRueschlikon() - coupling_map = backend.configuration().coupling_map - basis_gates = backend.configuration().basis_gates + cmap = [ + [1, 0], + [1, 2], + [2, 3], + [3, 4], + [3, 14], + [5, 4], + [6, 5], + [6, 7], + [6, 11], + [7, 10], + [8, 7], + [9, 8], + [9, 10], + [11, 10], + [12, 5], + [12, 11], + [12, 13], + [13, 4], + [13, 14], + [15, 0], + [15, 2], + [15, 14], + ] + + backend = GenericBackendV2(num_qubits=16, coupling_map=cmap) + coupling_map = backend.coupling_map + basis_gates = backend.operation_names qr = QuantumRegister(16, "qr") cr = ClassicalRegister(16, "cr") @@ -520,7 +606,7 @@ def test_transpile_singleton(self): def test_mapping_correction(self): """Test mapping works in previous failed case.""" - backend = FakeRueschlikon() + backend = GenericBackendV2(num_qubits=12) qr = QuantumRegister(name="qr", size=11) cr = ClassicalRegister(name="qc", size=11) circuit = QuantumCircuit(qr, cr) @@ -635,7 +721,7 @@ def test_transpiler_layout_from_intlist(self): def test_mapping_multi_qreg(self): """Test mapping works for multiple qregs.""" - backend = FakeRueschlikon() + backend = GenericBackendV2(num_qubits=8) qr = QuantumRegister(3, name="qr") qr2 = QuantumRegister(1, name="qr2") qr3 = QuantumRegister(4, name="qr3") @@ -652,7 +738,7 @@ def test_mapping_multi_qreg(self): def test_transpile_circuits_diff_registers(self): """Transpile list of circuits with different qreg names.""" - backend = FakeRueschlikon() + backend = GenericBackendV2(num_qubits=4) circuits = [] for _ in range(2): qr = QuantumRegister(2) @@ -668,7 +754,7 @@ def test_transpile_circuits_diff_registers(self): def test_wrong_initial_layout(self): """Test transpile with a bad initial layout.""" - backend = FakeMelbourne() + backend = GenericBackendV2(num_qubits=4) qubit_reg = QuantumRegister(2, name="q") clbit_reg = ClassicalRegister(2, name="c") @@ -706,15 +792,18 @@ def test_parameterized_circuit_for_device(self): qc = QuantumCircuit(qr) theta = Parameter("theta") - qc.rz(theta, qr[0]) + qc.p(theta, qr[0]) + backend = GenericBackendV2(num_qubits=4) transpiled_qc = transpile( - qc, backend=FakeMelbourne(), initial_layout=Layout.generate_trivial_layout(qr) + qc, + backend=backend, + initial_layout=Layout.generate_trivial_layout(qr), ) - qr = QuantumRegister(14, "q") - expected_qc = QuantumCircuit(qr, global_phase=-1 * theta / 2.0) - expected_qc.append(U1Gate(theta), [qr[0]]) + qr = QuantumRegister(backend.num_qubits, "q") + expected_qc = QuantumCircuit(qr, global_phase=theta / 2.0) + expected_qc.append(RZGate(theta), [qr[0]]) self.assertEqual(expected_qc, transpiled_qc) @@ -743,14 +832,17 @@ def test_parameter_expression_circuit_for_device(self): theta = Parameter("theta") square = theta * theta qc.rz(square, qr[0]) + backend = GenericBackendV2(num_qubits=4) transpiled_qc = transpile( - qc, backend=FakeMelbourne(), initial_layout=Layout.generate_trivial_layout(qr) + qc, + backend=backend, + initial_layout=Layout.generate_trivial_layout(qr), ) - qr = QuantumRegister(14, "q") - expected_qc = QuantumCircuit(qr, global_phase=-1 * square / 2.0) - expected_qc.append(U1Gate(square), [qr[0]]) + qr = QuantumRegister(backend.num_qubits, "q") + expected_qc = QuantumCircuit(qr) + expected_qc.append(RZGate(square), [qr[0]]) self.assertEqual(expected_qc, transpiled_qc) def test_final_measurement_barrier_for_devices(self): @@ -759,10 +851,35 @@ def test_final_measurement_barrier_for_devices(self): circ = QuantumCircuit.from_qasm_file(os.path.join(qasm_dir, "example.qasm")) layout = Layout.generate_trivial_layout(*circ.qregs) orig_pass = BarrierBeforeFinalMeasurements() + coupling_map = [ + [1, 0], + [1, 2], + [2, 3], + [3, 4], + [3, 14], + [5, 4], + [6, 5], + [6, 7], + [6, 11], + [7, 10], + [8, 7], + [9, 8], + [9, 10], + [11, 10], + [12, 5], + [12, 11], + [12, 13], + [13, 4], + [13, 14], + [15, 0], + [15, 2], + [15, 14], + ] + with patch.object(BarrierBeforeFinalMeasurements, "run", wraps=orig_pass.run) as mock_pass: transpile( circ, - coupling_map=FakeRueschlikon().configuration().coupling_map, + coupling_map=coupling_map, initial_layout=layout, ) self.assertTrue(mock_pass.called) @@ -773,7 +890,7 @@ def test_do_not_run_gatedirection_with_symmetric_cm(self): circ = QuantumCircuit.from_qasm_file(os.path.join(qasm_dir, "example.qasm")) layout = Layout.generate_trivial_layout(*circ.qregs) coupling_map = [] - for node1, node2 in FakeRueschlikon().configuration().coupling_map: + for node1, node2 in GenericBackendV2(num_qubits=16).coupling_map: coupling_map.append([node1, node2]) coupling_map.append([node2, node1]) @@ -832,8 +949,7 @@ def test_pass_manager_empty(self): def test_move_measurements(self): """Measurements applied AFTER swap mapping.""" - backend = FakeRueschlikon() - cmap = backend.configuration().coupling_map + cmap = GenericBackendV2(num_qubits=16).coupling_map qasm_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "qasm") circ = QuantumCircuit.from_qasm_file(os.path.join(qasm_dir, "move_measurements.qasm")) @@ -876,7 +992,7 @@ def test_initialize_FakeMelbourne(self): qc = QuantumCircuit(qr) qc.initialize(desired_vector, [qr[0], qr[1], qr[2]]) - out = transpile(qc, backend=FakeMelbourne()) + out = transpile(qc, backend=GenericBackendV2(num_qubits=4)) out_dag = circuit_to_dag(out) reset_nodes = out_dag.named_nodes("reset") @@ -1175,10 +1291,9 @@ def test_transpiled_custom_gates_calibration(self): circ.add_calibration(custom_180, [0], q0_x180) circ.add_calibration(custom_90, [1], q1_y90) - backend = FakeBoeblingen() transpiled_circuit = transpile( circ, - backend=backend, + backend=GenericBackendV2(num_qubits=4), layout_method="trivial", ) self.assertEqual(transpiled_circuit.calibrations, circ.calibrations) @@ -1196,10 +1311,9 @@ def test_transpiled_basis_gates_calibrations(self): # Add calibration circ.add_calibration("h", [0], q0_x180) - backend = FakeBoeblingen() transpiled_circuit = transpile( circ, - backend=backend, + backend=GenericBackendV2(num_qubits=4), ) self.assertEqual(transpiled_circuit.calibrations, circ.calibrations) @@ -1216,9 +1330,12 @@ def test_transpile_calibrated_custom_gate_on_diff_qubit(self): # Add calibration circ.add_calibration(custom_180, [1], q0_x180) - backend = FakeBoeblingen() with self.assertRaises(QiskitError): - transpile(circ, backend=backend, layout_method="trivial") + transpile( + circ, + backend=GenericBackendV2(num_qubits=4), + layout_method="trivial", + ) def test_transpile_calibrated_nonbasis_gate_on_diff_qubit(self): """Test if the non-basis gates are transpiled if they are on different qubit that @@ -1233,13 +1350,12 @@ def test_transpile_calibrated_nonbasis_gate_on_diff_qubit(self): # Add calibration circ.add_calibration("h", [1], q0_x180) - backend = FakeBoeblingen() transpiled_circuit = transpile( circ, - backend=backend, + backend=GenericBackendV2(num_qubits=4), ) self.assertEqual(transpiled_circuit.calibrations, circ.calibrations) - self.assertEqual(set(transpiled_circuit.count_ops().keys()), {"u2", "h"}) + self.assertEqual(set(transpiled_circuit.count_ops().keys()), {"rz", "sx", "h"}) def test_transpile_subset_of_calibrated_gates(self): """Test transpiling a circuit with both basis gate (not-calibrated) and @@ -1257,8 +1373,12 @@ def test_transpile_subset_of_calibrated_gates(self): circ.add_calibration(x_180, [0], q0_x180) circ.add_calibration("h", [1], q0_x180) # 'h' is calibrated on qubit 1 - transpiled_circ = transpile(circ, FakeBoeblingen(), layout_method="trivial") - self.assertEqual(set(transpiled_circ.count_ops().keys()), {"u2", "mycustom", "h"}) + transpiled_circ = transpile( + circ, + backend=GenericBackendV2(num_qubits=4), + layout_method="trivial", + ) + self.assertEqual(set(transpiled_circ.count_ops().keys()), {"rz", "sx", "mycustom", "h"}) def test_parameterized_calibrations_transpile(self): """Check that gates can be matched to their calibrations before and after parameter @@ -1274,10 +1394,18 @@ def q0_rxt(tau): circ.add_calibration("rxt", [0], q0_rxt(tau), [2 * 3.14 * tau]) - transpiled_circ = transpile(circ, FakeBoeblingen(), layout_method="trivial") + transpiled_circ = transpile( + circ, + backend=GenericBackendV2(num_qubits=4), + layout_method="trivial", + ) self.assertEqual(set(transpiled_circ.count_ops().keys()), {"rxt"}) circ = circ.assign_parameters({tau: 1}) - transpiled_circ = transpile(circ, FakeBoeblingen(), layout_method="trivial") + transpiled_circ = transpile( + circ, + backend=GenericBackendV2(num_qubits=4), + layout_method="trivial", + ) self.assertEqual(set(transpiled_circ.count_ops().keys()), {"rxt"}) def test_inst_durations_from_calibrations(self): @@ -1304,8 +1432,9 @@ def test_multiqubit_gates_calibrations(self, opt_level): custom_gate = Gate("my_custom_gate", 5, []) circ.append(custom_gate, [0, 1, 2, 3, 4]) circ.measure_all() - backend = FakeBoeblingen() - with pulse.build(backend, name="custom") as my_schedule: + backend = GenericBackendV2(num_qubits=6) + + with pulse.build(backend=backend, name="custom") as my_schedule: pulse.play( pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(0) ) @@ -1334,7 +1463,9 @@ def test_multiqubit_gates_calibrations(self, opt_level): pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(4) ) circ.add_calibration("my_custom_gate", [0, 1, 2, 3, 4], my_schedule, []) - trans_circ = transpile(circ, backend, optimization_level=opt_level, layout_method="trivial") + trans_circ = transpile( + circ, backend=backend, optimization_level=opt_level, layout_method="trivial" + ) self.assertEqual({"measure": 5, "my_custom_gate": 1, "barrier": 1}, trans_circ.count_ops()) @data(0, 1, 2, 3) @@ -1361,8 +1492,8 @@ def test_delay_converts_to_dt(self): qc = QuantumCircuit(2) qc.delay(1000, [0], unit="us") - backend = FakeRueschlikon() - backend.configuration().dt = 0.5e-6 + backend = GenericBackendV2(num_qubits=4) + backend.target.dt = 0.5e-6 out = transpile([qc, qc], backend) self.assertEqual(out[0].data[0].operation.unit, "dt") self.assertEqual(out[1].data[0].operation.unit, "dt") @@ -1377,8 +1508,11 @@ def test_scheduling_backend_v2(self): qc.cx(0, 1) qc.measure_all() - backend = FakeMumbaiV2() - out = transpile([qc, qc], backend, scheduling_method="alap") + out = transpile( + [qc, qc], + backend=GenericBackendV2(num_qubits=4), + scheduling_method="alap", + ) self.assertIn("delay", out[0].count_ops()) self.assertIn("delay", out[1].count_ops()) @@ -1458,10 +1592,11 @@ def test_transpile_optional_registers(self, optimization_level): qc.cx(1, 2) qc.measure(qubits, clbits) + backend = GenericBackendV2(num_qubits=4) - out = transpile(qc, FakeBoeblingen(), optimization_level=optimization_level) + out = transpile(qc, backend=backend, optimization_level=optimization_level) - self.assertEqual(len(out.qubits), FakeBoeblingen().configuration().num_qubits) + self.assertEqual(len(out.qubits), backend.num_qubits) self.assertEqual(len(out.clbits), len(clbits)) @data(0, 1, 2, 3) @@ -1582,11 +1717,7 @@ def test_target_ideal_gates(self, opt_level): @data(0, 1, 2, 3) def test_transpile_with_custom_control_flow_target(self, opt_level): """Test transpile() with a target and constrol flow ops.""" - target = FakeMumbaiV2().target - target.add_instruction(ForLoopOp, name="for_loop") - target.add_instruction(WhileLoopOp, name="while_loop") - target.add_instruction(IfElseOp, name="if_else") - target.add_instruction(SwitchCaseOp, name="switch_case") + target = GenericBackendV2(num_qubits=8, control_flow=True).target circuit = QuantumCircuit(6, 1) circuit.h(0) @@ -1877,7 +2008,7 @@ def test_qpy_roundtrip(self, optimization_level): """Test that the output of a transpiled circuit can be round-tripped through QPY.""" transpiled = transpile( self._regular_circuit(), - backend=FakeMelbourne(), + backend=GenericBackendV2(num_qubits=8), optimization_level=optimization_level, seed_transpiler=2022_10_17, ) @@ -1894,7 +2025,7 @@ def test_qpy_roundtrip_backendv2(self, optimization_level): """Test that the output of a transpiled circuit can be round-tripped through QPY.""" transpiled = transpile( self._regular_circuit(), - backend=FakeMumbaiV2(), + backend=GenericBackendV2(num_qubits=8), optimization_level=optimization_level, seed_transpiler=2022_10_17, ) @@ -1918,12 +2049,11 @@ def test_qpy_roundtrip_control_flow(self, optimization_level): "See #10345 for more details." ) - backend = FakeMelbourne() + backend = GenericBackendV2(num_qubits=8, control_flow=True) transpiled = transpile( self._control_flow_circuit(), backend=backend, - basis_gates=backend.configuration().basis_gates - + ["if_else", "for_loop", "while_loop", "switch_case"], + basis_gates=backend.operation_names, optimization_level=optimization_level, seed_transpiler=2022_10_17, ) @@ -1939,14 +2069,9 @@ def test_qpy_roundtrip_control_flow(self, optimization_level): def test_qpy_roundtrip_control_flow_backendv2(self, optimization_level): """Test that the output of a transpiled circuit with control flow can be round-tripped through QPY.""" - backend = FakeMumbaiV2() - backend.target.add_instruction(IfElseOp, name="if_else") - backend.target.add_instruction(ForLoopOp, name="for_loop") - backend.target.add_instruction(WhileLoopOp, name="while_loop") - backend.target.add_instruction(SwitchCaseOp, name="switch_case") transpiled = transpile( self._control_flow_circuit(), - backend=backend, + backend=GenericBackendV2(num_qubits=8, control_flow=True), optimization_level=optimization_level, seed_transpiler=2022_10_17, ) @@ -1967,11 +2092,11 @@ def test_qpy_roundtrip_control_flow_expr(self, optimization_level): "This test case triggers a bug in the eigensolver routine on windows. " "See #10345 for more details." ) - backend = FakeMelbourne() + backend = GenericBackendV2(num_qubits=16) transpiled = transpile( self._control_flow_expr_circuit(), backend=backend, - basis_gates=backend.configuration().basis_gates + basis_gates=backend.operation_names + ["if_else", "for_loop", "while_loop", "switch_case"], optimization_level=optimization_level, seed_transpiler=2023_07_26, @@ -1986,7 +2111,7 @@ def test_qpy_roundtrip_control_flow_expr(self, optimization_level): def test_qpy_roundtrip_control_flow_expr_backendv2(self, optimization_level): """Test that the output of a transpiled circuit with control flow including `Expr` nodes can be round-tripped through QPY.""" - backend = FakeMumbaiV2() + backend = GenericBackendV2(num_qubits=27) backend.target.add_instruction(IfElseOp, name="if_else") backend.target.add_instruction(ForLoopOp, name="for_loop") backend.target.add_instruction(WhileLoopOp, name="while_loop") @@ -2021,14 +2146,9 @@ def test_qasm3_output(self, optimization_level): def test_qasm3_output_control_flow(self, optimization_level): """Test that the output of a transpiled circuit with control flow can be dumped into OpenQASM 3.""" - backend = FakeMumbaiV2() - backend.target.add_instruction(IfElseOp, name="if_else") - backend.target.add_instruction(ForLoopOp, name="for_loop") - backend.target.add_instruction(WhileLoopOp, name="while_loop") - backend.target.add_instruction(SwitchCaseOp, name="switch_case") transpiled = transpile( self._control_flow_circuit(), - backend=backend, + backend=GenericBackendV2(num_qubits=8, control_flow=True), optimization_level=optimization_level, seed_transpiler=2022_10_17, ) @@ -2044,14 +2164,9 @@ def test_qasm3_output_control_flow(self, optimization_level): def test_qasm3_output_control_flow_expr(self, optimization_level): """Test that the output of a transpiled circuit with control flow and `Expr` nodes can be dumped into OpenQASM 3.""" - backend = FakeMumbaiV2() - backend.target.add_instruction(IfElseOp, name="if_else") - backend.target.add_instruction(ForLoopOp, name="for_loop") - backend.target.add_instruction(WhileLoopOp, name="while_loop") - backend.target.add_instruction(SwitchCaseOp, name="switch_case") transpiled = transpile( self._control_flow_circuit(), - backend=backend, + backend=GenericBackendV2(num_qubits=27, control_flow=True), optimization_level=optimization_level, seed_transpiler=2023_07_26, ) @@ -2204,8 +2319,7 @@ def test_parallel_multiprocessing(self, opt_level): qc.h(0) qc.cx(0, 1) qc.measure_all() - backend = FakeMumbaiV2() - pm = generate_preset_pass_manager(opt_level, backend) + pm = generate_preset_pass_manager(opt_level, backend=GenericBackendV2(num_qubits=4)) res = pm.run([qc, qc]) for circ in res: self.assertIsInstance(circ, QuantumCircuit) @@ -2217,7 +2331,7 @@ def test_parallel_with_target(self, opt_level): qc.h(0) qc.cx(0, 1) qc.measure_all() - target = FakeMumbaiV2().target + target = GenericBackendV2(num_qubits=4).target res = transpile([qc] * 3, target=target, optimization_level=opt_level) self.assertIsInstance(res, list) for circ in res: @@ -2230,7 +2344,7 @@ def test_parallel_num_processes_kwarg(self, num_processes): qc.h(0) qc.cx(0, 1) qc.measure_all() - target = FakeMumbaiV2().target + target = GenericBackendV2(num_qubits=27).target res = transpile([qc] * 3, target=target, num_processes=num_processes) self.assertIsInstance(res, list) for circ in res: @@ -2277,9 +2391,13 @@ def run(self, dag): ) return dag - backend = FakeMumbaiV2() + # Create backend with empty calibrations (PulseQobjEntries) + backend = GenericBackendV2( + num_qubits=4, + calibrate_instructions=False, + ) - # This target has PulseQobj entries that provides a serialized schedule data + # This target has PulseQobj entries that provide a serialized schedule data pass_ = TestAddCalibration(backend.target) pm = PassManager(passes=[pass_]) self.assertIsNone(backend.target["sx"][(0,)]._calibration._definition) @@ -2297,7 +2415,7 @@ def run(self, dag): @data(0, 1, 2, 3) def test_parallel_singleton_conditional_gate(self, opt_level): """Test that singleton mutable instance doesn't lose state in parallel.""" - backend = FakeNairobiV2() + backend = GenericBackendV2(num_qubits=27) circ = QuantumCircuit(2, 1) circ.h(0) circ.measure(0, circ.clbits[0]) @@ -2311,7 +2429,7 @@ def test_parallel_singleton_conditional_gate(self, opt_level): @data(0, 1, 2, 3) def test_backendv2_and_basis_gates(self, opt_level): """Test transpile() with BackendV2 and basis_gates set.""" - backend = FakeNairobiV2() + backend = GenericBackendV2(num_qubits=6) qc = QuantumCircuit(5) qc.h(0) qc.cz(0, 1) @@ -2337,7 +2455,7 @@ def test_backendv2_and_basis_gates(self, opt_level): @data(0, 1, 2, 3) def test_backendv2_and_coupling_map(self, opt_level): """Test transpile() with custom coupling map.""" - backend = FakeNairobiV2() + qc = QuantumCircuit(5) qc.h(0) qc.cz(0, 1) @@ -2348,7 +2466,7 @@ def test_backendv2_and_coupling_map(self, opt_level): cmap = CouplingMap.from_line(5, bidirectional=False) tqc = transpile( qc, - backend=backend, + backend=GenericBackendV2(num_qubits=6), coupling_map=cmap, optimization_level=opt_level, seed_transpiler=12345678942, @@ -2363,7 +2481,7 @@ def test_backendv2_and_coupling_map(self, opt_level): def test_transpile_with_multiple_coupling_maps(self): """Test passing a different coupling map for every circuit""" - backend = FakeNairobiV2() + backend = GenericBackendV2(num_qubits=4) qc = QuantumCircuit(3) qc.cx(0, 2) @@ -2386,7 +2504,10 @@ def test_transpile_with_multiple_coupling_maps(self): @data(0, 1, 2, 3) def test_backend_and_custom_gate(self, opt_level): """Test transpile() with BackendV2, custom basis pulse gate.""" - backend = FakeNairobiV2() + backend = GenericBackendV2( + num_qubits=5, + coupling_map=[[0, 1], [1, 0], [1, 2], [1, 3], [2, 1], [3, 1], [3, 4], [4, 3]], + ) inst_map = InstructionScheduleMap() inst_map.add("newgate", [0, 1], pulse.ScheduleBlock()) newgate = Gate("newgate", 2, []) @@ -2897,7 +3018,7 @@ def _visit_block(circuit, qubit_mapping=None): )[0] # The first node should be a measurement self.assertIsInstance(first_meas_node.op, Measure) - # This shoulde be in the first ocmponent + # This should be in the first component self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) op_node = tqc_dag._multi_graph.find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) @@ -3269,7 +3390,7 @@ def test_transpile_does_not_affect_backend_coupling(self, opt_level): qc = QuantumCircuit(127) for i in range(1, 127): qc.ecr(0, i) - backend = FakeSherbrooke() + backend = GenericBackendV2(num_qubits=130) original_map = copy.deepcopy(backend.coupling_map) transpile(qc, backend, optimization_level=opt_level) self.assertEqual(original_map, backend.coupling_map) diff --git a/test/python/providers/fake_provider/test_fake_generic.py b/test/python/providers/fake_provider/test_fake_generic.py new file mode 100644 index 000000000000..0c9740f3a285 --- /dev/null +++ b/test/python/providers/fake_provider/test_fake_generic.py @@ -0,0 +1,114 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023, 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. + +""" Test of GenericBackendV2 backend""" + +import math + +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister +from qiskit import transpile +from qiskit.providers.fake_provider import GenericBackendV2 +from qiskit.transpiler import CouplingMap +from qiskit.exceptions import QiskitError +from qiskit.test import QiskitTestCase + + +class TestGenericBackendV2(QiskitTestCase): + """Test class for GenericBackendV2 backend""" + + def setUp(self): + super().setUp() + self.cmap = CouplingMap( + [(0, 2), (0, 1), (1, 3), (2, 4), (2, 3), (3, 5), (4, 6), (4, 5), (5, 7), (6, 7)] + ) + + def test_supported_basis_gates(self): + """Test that target raises error if basis_gate not in ``supported_names``.""" + with self.assertRaises(QiskitError): + GenericBackendV2(num_qubits=8, basis_gates=["cx", "id", "rz", "sx", "zz"]) + + def test_operation_names(self): + """Test that target basis gates include "delay", "measure" and "reset" even + if not provided by user.""" + target = GenericBackendV2(num_qubits=8) + op_names = list(target.operation_names) + op_names.sort() + self.assertEqual(op_names, ["cx", "delay", "id", "measure", "reset", "rz", "sx", "x"]) + + target = GenericBackendV2(num_qubits=8, basis_gates=["ecr", "id", "rz", "sx", "x"]) + op_names = list(target.operation_names) + op_names.sort() + self.assertEqual(op_names, ["delay", "ecr", "id", "measure", "reset", "rz", "sx", "x"]) + + def test_incompatible_coupling_map(self): + """Test that the size of the coupling map must match num_qubits.""" + with self.assertRaises(QiskitError): + GenericBackendV2(num_qubits=5, coupling_map=self.cmap) + + def test_control_flow_operation_names(self): + """Test that control flow instructions are added to the target if control_flow is True.""" + target = GenericBackendV2( + num_qubits=8, + basis_gates=["ecr", "id", "rz", "sx", "x"], + coupling_map=self.cmap, + control_flow=True, + ).target + op_names = list(target.operation_names) + op_names.sort() + reference = [ + "break", + "continue", + "delay", + "ecr", + "for_loop", + "id", + "if_else", + "measure", + "reset", + "rz", + "switch_case", + "sx", + "while_loop", + "x", + ] + self.assertEqual(op_names, reference) + + def test_default_coupling_map(self): + """Test that fully-connected coupling map is generated correctly.""" + + # fmt: off + reference_cmap = [(0, 1), (1, 0), (0, 2), (2, 0), (0, 3), (3, 0), (0, 4), (4, 0), (1, 2), (2, 1), + (1, 3), (3, 1), (1, 4), (4, 1), (2, 3), (3, 2), (2, 4), (4, 2), (3, 4), (4, 3)] + # fmt: on + self.assertEqual( + list(GenericBackendV2(num_qubits=5).coupling_map.get_edges()), + reference_cmap, + ) + + def test_run(self): + """Test run method, confirm correct noisy simulation if Aer is installed.""" + + qr = QuantumRegister(5) + cr = ClassicalRegister(5) + qc = QuantumCircuit(qr, cr) + qc.h(qr[0]) + for k in range(1, 4): + qc.cx(qr[0], qr[k]) + qc.measure(qr, cr) + + backend = GenericBackendV2(num_qubits=5, basis_gates=["cx", "id", "rz", "sx", "x"]) + tqc = transpile(qc, backend=backend, optimization_level=3, seed_transpiler=42) + result = backend.run(tqc, seed_simulator=42, shots=1000).result() + counts = result.get_counts() + + self.assertTrue(math.isclose(counts["00000"], 500, rel_tol=0.1)) + self.assertTrue(math.isclose(counts["01111"], 500, rel_tol=0.1)) diff --git a/test/python/pulse/test_macros.py b/test/python/pulse/test_macros.py index 8c82e0c569be..99cb873c67e2 100644 --- a/test/python/pulse/test_macros.py +++ b/test/python/pulse/test_macros.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019. +# (C) Copyright IBM 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 @@ -24,7 +24,7 @@ ) from qiskit.pulse import macros from qiskit.pulse.exceptions import PulseError -from qiskit.providers.fake_provider import FakeOpenPulse2Q, FakeHanoi, FakeHanoiV2 +from qiskit.providers.fake_provider import FakeOpenPulse2Q, FakeHanoi, GenericBackendV2 from qiskit.test import QiskitTestCase @@ -34,9 +34,15 @@ class TestMeasure(QiskitTestCase): def setUp(self): super().setUp() self.backend = FakeOpenPulse2Q() - self.backend_v2 = FakeHanoiV2() + self.backend_v1 = FakeHanoi() self.inst_map = self.backend.defaults().instruction_schedule_map + self.backend_v2 = GenericBackendV2( + basis_gates=["cx", "id", "rz", "sx", "x"], + num_qubits=27, + calibrate_instructions=self.backend_v1.defaults().instruction_schedule_map, + ) + def test_measure(self): """Test macro - measure.""" sched = macros.measure(qubits=[0], backend=self.backend) @@ -149,13 +155,16 @@ def test_multiple_measure_v2(self): def test_output_with_measure_v1_and_measure_v2(self): """Test make outputs of measure_v1 and measure_v2 consistent.""" - sched_measure_v1 = macros.measure(qubits=[0, 1], backend=FakeHanoi()) + sched_measure_v1 = macros.measure(qubits=[0, 1], backend=self.backend_v1) sched_measure_v2 = macros.measure(qubits=[0, 1], backend=self.backend_v2) + self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions) def test_output_with_measure_v1_and_measure_v2_sched_with_qubit_mem_slots(self): """Test make outputs of measure_v1 and measure_v2 with custom qubit_mem_slots consistent.""" - sched_measure_v1 = macros.measure(qubits=[0], backend=FakeHanoi(), qubit_mem_slots={0: 2}) + sched_measure_v1 = macros.measure( + qubits=[0], backend=self.backend_v1, qubit_mem_slots={0: 2} + ) sched_measure_v2 = macros.measure( qubits=[0], backend=self.backend_v2, qubit_mem_slots={0: 2} ) @@ -167,11 +176,11 @@ def test_output_with_measure_v1_and_measure_v2_sched_with_meas_map(self): num_qubits_list_measure_v1 = list(range(FakeHanoi().configuration().num_qubits)) num_qubits_list_measure_v2 = list(range(self.backend_v2.num_qubits)) sched_with_meas_map_list_v1 = macros.measure( - qubits=[0], backend=FakeHanoi(), meas_map=[num_qubits_list_measure_v1] + qubits=[0], backend=self.backend_v1, meas_map=[num_qubits_list_measure_v1] ) sched_with_meas_map_dict_v1 = macros.measure( qubits=[0], - backend=FakeHanoi(), + backend=self.backend_v1, meas_map={0: num_qubits_list_measure_v1, 1: num_qubits_list_measure_v1}, ) sched_with_meas_map_list_v2 = macros.measure( @@ -193,7 +202,7 @@ def test_output_with_measure_v1_and_measure_v2_sched_with_meas_map(self): def test_output_with_multiple_measure_v1_and_measure_v2(self): """Test macro - consistent output of multiple qubit measure with backendV1 and backendV2.""" - sched_measure_v1 = macros.measure(qubits=[0, 1], backend=FakeHanoi()) + sched_measure_v1 = macros.measure(qubits=[0, 1], backend=self.backend_v1) sched_measure_v2 = macros.measure(qubits=[0, 1], backend=self.backend_v2) self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions) @@ -203,29 +212,30 @@ class TestMeasureAll(QiskitTestCase): def setUp(self): super().setUp() - self.backend = FakeOpenPulse2Q() - self.backend_v2 = FakeHanoiV2() - self.inst_map = self.backend.defaults().instruction_schedule_map + self.backend_v1 = FakeOpenPulse2Q() + self.inst_map = self.backend_v1.defaults().instruction_schedule_map + self.backend_v2 = GenericBackendV2( + basis_gates=["cx", "id", "rz", "sx", "x"], + num_qubits=2, + calibrate_instructions=self.backend_v1.defaults().instruction_schedule_map, + ) def test_measure_all(self): """Test measure_all function.""" - sched = macros.measure_all(self.backend) + sched = macros.measure_all(self.backend_v1) expected = Schedule(self.inst_map.get("measure", [0, 1])) self.assertEqual(sched.instructions, expected.instructions) def test_measure_all_v2(self): """Test measure_all function with backendV2.""" - backend_v1 = FakeHanoi() - sched = macros.measure_all(self.backend_v2) + sched = macros.measure_all(self.backend_v1) expected = Schedule( - backend_v1.defaults().instruction_schedule_map.get( - "measure", list(range(backend_v1.configuration().num_qubits)) - ) + self.inst_map.get("measure", list(range(self.backend_v1.configuration().num_qubits))) ) self.assertEqual(sched.instructions, expected.instructions) def test_output_of_measure_all_with_backend_v1_and_v2(self): """Test make outputs of measure_all with backendV1 and backendV2 consistent.""" - sched_measure_v1 = macros.measure_all(backend=FakeHanoi()) + sched_measure_v1 = macros.measure_all(backend=self.backend_v1) sched_measure_v2 = macros.measure_all(backend=self.backend_v2) self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions)