From 7a411405ac3b5f2e107eabb8846475c38fdcaa07 Mon Sep 17 00:00:00 2001 From: MozammilQ <54737489+MozammilQ@users.noreply.github.com> Date: Mon, 11 Dec 2023 09:33:37 +0530 Subject: [PATCH] Update backend model up-conversion logic (#11095) * This marks an initiative to replace the contents of method convert_to_target from qiskit.providers.backend_compat.py with contents of method target_from_server_data from qiskit_ibm_provider/utils/json_decoder.py * removed usage of supported_instructions from conf_*.json * fixed lint * added test, to check if the issue has been fixed * added release note * added suggestions and refactored code * removed argument deprecation, removed covert_to_target_legacy * refactor code * Added working add_delay argument in convert_to_target function, and removed source code for convert_to_target_legacy function from backend_compat.py. * refactor code * fixed lint * changed a test * altered release note * applied suggestions * add test * refactor code * altered handling of calibration data * docs * fix docs error * lint * refactor code * refactor code(removed inefficiencies and improved code legibility), applied suggestions, reverted a visualization test to its original form --- qiskit/providers/backend_compat.py | 389 +++++++++++------- .../providers/fake_provider/fake_backend.py | 3 +- ..._up_conversion_logic-75ecc2030a9fe6b1.yaml | 19 + test/python/providers/test_fake_backends.py | 28 ++ test/python/scheduler/test_basic_scheduler.py | 32 ++ 5 files changed, 310 insertions(+), 161 deletions(-) create mode 100644 releasenotes/notes/Update_backend_model_up_conversion_logic-75ecc2030a9fe6b1.yaml diff --git a/qiskit/providers/backend_compat.py b/qiskit/providers/backend_compat.py index fe582540a647..1e75f4a4a8a6 100644 --- a/qiskit/providers/backend_compat.py +++ b/qiskit/providers/backend_compat.py @@ -13,185 +13,256 @@ """Backend abstract interface for providers.""" from __future__ import annotations - -from typing import List, Iterable, Any, Dict, Optional - -from qiskit.exceptions import QiskitError +import logging +from typing import List, Iterable, Any, Dict, Optional, Tuple from qiskit.providers.backend import BackendV1, BackendV2 from qiskit.providers.backend import QubitProperties -from qiskit.utils.units import apply_prefix -from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping -from qiskit.circuit.measure import Measure from qiskit.providers.models.backendconfiguration import BackendConfiguration from qiskit.providers.models.backendproperties import BackendProperties + from qiskit.providers.models.pulsedefaults import PulseDefaults from qiskit.providers.options import Options from qiskit.providers.exceptions import BackendPropertyError +logger = logging.getLogger(__name__) + def convert_to_target( configuration: BackendConfiguration, properties: BackendProperties = None, defaults: PulseDefaults = None, custom_name_mapping: Optional[Dict[str, Any]] = None, - add_delay: bool = False, - filter_faulty: bool = False, + add_delay: bool = True, + filter_faulty: bool = True, ): - """Uses configuration, properties and pulse defaults - to construct and return Target class. - - In order to convert with a ``defaults.instruction_schedule_map``, - which has a custom calibration for an operation, - the operation name must be in ``configuration.basis_gates`` and - ``custom_name_mapping`` must be supplied for the operation. - Otherwise, the operation will be dropped in the resulting ``Target`` object. - - That suggests it is recommended to add custom calibrations **after** creating a target - with this function instead of adding them to ``defaults`` in advance. For example:: - - target.add_instruction(custom_gate, {(0, 1): InstructionProperties(calibration=custom_sched)}) + """Decode transpiler target from backend data set. + + This function generates ``Target`` instance from intermediate + legacy objects such as ``BackendProperties`` and ``PulseDefaults``. + + .. note:: + Passing in legacy objects like BackendProperties as properties and PulseDefaults + as defaults will be deprecated in the future. + + Args: + configuration: Backend configuration as ``BackendConfiguration`` + properties: Backend property dictionary or ``BackendProperties`` + defaults: Backend pulse defaults dictionary or ``PulseDefaults`` + custom_name_mapping: A name mapping must be supplied for the operation + not included in Qiskit Standard Gate name mapping, otherwise the operation + will be dropped in the resulting ``Target`` object. + add_delay: If True, adds delay to the instruction set. + filter_faulty: If True, this filters the non-operational qubits. + + Returns: + A ``Target`` instance. """ + + # importing pacakges where they are needed, to avoid cyclic-import. # pylint: disable=cyclic-import from qiskit.transpiler.target import ( Target, 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 + + required = ["measure", "delay"] + + # Load Qiskit object representation + qiskit_inst_mapping = get_standard_gate_name_mapping() + if custom_name_mapping: + qiskit_inst_mapping.update(custom_name_mapping) + + qiskit_control_flow_mapping = { + "if_else": IfElseOp, + "while_loop": WhileLoopOp, + "for_loop": ForLoopOp, + "switch_case": SwitchCaseOp, + } + + in_data = {"num_qubits": configuration.n_qubits} + + # Parse global configuration properties + if hasattr(configuration, "dt"): + in_data["dt"] = configuration.dt + if hasattr(configuration, "timing_constraints"): + in_data.update(configuration.timing_constraints) + + # Create instruction property placeholder from backend configuration + basis_gates = set(getattr(configuration, "basis_gates", [])) + gate_configs = {gate.name: gate for gate in configuration.gates} + inst_name_map = {} # type: Dict[str, Instruction] + prop_name_map = {} # type: Dict[str, Dict[Tuple[int, ...], InstructionProperties]] + all_instructions = set.union(basis_gates, set(required)) + + faulty_ops = set() + faulty_qubits = [] + 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: + 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: + logger.warning( + "Definition of instruction %s is not found in the Qiskit namespace and " + "GateConfig is not provided by the BackendConfiguration payload. " + "Qiskit Gate model cannot be instantiated for this instruction and " + "this instruction is silently excluded from the Target. " + "Please add new gate class to Qiskit or provide GateConfig for this name.", + name, + ) + unsupported_instructions.append(name) + + for name in unsupported_instructions: + all_instructions.remove(name) + + # Create empty inst properties from gate configs + for name, spec in gate_configs.items(): + if hasattr(spec, "coupling_map"): + coupling_map = spec.coupling_map + prop_name_map[name] = dict.fromkeys(map(tuple, coupling_map)) + else: + prop_name_map[name] = None + + # Populate instruction properties + if properties: + qubit_properties = [ + QubitProperties( + t1=properties.qubit_property(qubit_idx)["T1"][0], + t2=properties.qubit_property(qubit_idx)["T2"][0], + frequency=properties.qubit_property(qubit_idx)["frequency"][0], + ) + for qubit_idx in range(0, configuration.num_qubits) + ] + + in_data["qubit_properties"] = qubit_properties - # Standard gates library mapping, multicontrolled gates not included since they're - # variable width - name_mapping = get_standard_gate_name_mapping() - target = None - if custom_name_mapping is not None: - name_mapping.update(custom_name_mapping) - faulty_qubits = set() - # Parse from properties if it exsits - if properties is not None: if filter_faulty: - faulty_qubits = set(properties.faulty_qubits()) - qubit_properties = qubit_props_list_from_props(properties=properties) - target = Target( - num_qubits=configuration.n_qubits, - qubit_properties=qubit_properties, - concurrent_measurements=getattr(configuration, "meas_map", None), - ) - # Parse instructions - gates: Dict[str, Any] = {} - for gate in properties.gates: - name = gate.gate - if name in name_mapping: - if name not in gates: - gates[name] = {} - else: - raise QiskitError( - f"Operation name {name} does not have a known mapping. Use " - "custom_name_mapping to map this name to an Operation object" - ) - - qubits = tuple(gate.qubits) - if filter_faulty: - if any(not properties.is_qubit_operational(qubit) for qubit in qubits): + faulty_qubits = properties.faulty_qubits() + + for name in prop_name_map.keys(): + for qubits, params in properties.gate_property(name).items(): + in_param = { + "error": params["gate_error"][0] if "gate_error" in params else None, + "duration": params["gate_length"][0] if "gate_length" in params else None, + } + inst_prop = InstructionProperties(**in_param) + + if filter_faulty and ( + (not properties.is_gate_operational(name, qubits)) + or any(not properties.is_qubit_operational(qubit) for qubit in qubits) + ): + faulty_ops.add((name, qubits)) + try: + del prop_name_map[name][qubits] + except KeyError: + pass continue - if not properties.is_gate_operational(name, gate.qubits): + + if prop_name_map[name] is None: + prop_name_map[name] = {} + + prop_name_map[name][qubits] = inst_prop + + # 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) + in_prop = { + "duration": qubit_prop["readout_length"][0] + if "readout_length" in qubit_prop + else None, + "error": qubit_prop["readout_error"][0] if "readout_error" in qubit_prop else None, + } + prop_name_map["measure"][(qubit_idx,)] = InstructionProperties(**in_prop) + + if add_delay and "delay" not in prop_name_map: + prop_name_map["delay"] = { + (q,): None for q in range(configuration.num_qubits) if q not in faulty_qubits + } + + if defaults: + inst_sched_map = defaults.instruction_schedule_map + + for name in inst_sched_map.instructions: + for qubits in inst_sched_map.qubits_with_instruction(name): + + if not isinstance(qubits, tuple): + qubits = (qubits,) + + if ( + name not in all_instructions + or name not in prop_name_map + or qubits not in prop_name_map[name] + ): + logger.info( + "Gate calibration for instruction %s on qubits %s is found " + "in the PulseDefaults payload. However, this entry is not defined in " + "the gate mapping of Target. This calibration is ignored.", + name, + qubits, + ) continue - gate_props = {} - for param in gate.parameters: - if param.name == "gate_error": - gate_props["error"] = param.value - if param.name == "gate_length": - gate_props["duration"] = apply_prefix(param.value, param.unit) - gates[name][qubits] = InstructionProperties(**gate_props) - for gate, props in gates.items(): - inst = name_mapping[gate] - target.add_instruction(inst, props) - # Create measurement instructions: - measure_props = {} - for qubit, _ in enumerate(properties.qubits): - if filter_faulty: - if not properties.is_qubit_operational(qubit): + if (name, qubits) in faulty_ops: continue - try: - duration = properties.readout_length(qubit) - except BackendPropertyError: - duration = None - try: - error = properties.readout_error(qubit) - except BackendPropertyError: - error = None - measure_props[(qubit,)] = InstructionProperties( - duration=duration, - error=error, + + entry = inst_sched_map._get_calibration_entry(name, qubits) + + try: + prop_name_map[name][qubits].calibration = entry + except AttributeError: + logger.info( + "The PulseDefaults payload received contains an instruction %s on " + "qubits %s which is not present in the configuration or properties payload.", + name, + qubits, + ) + + # Remove 'delay' if add_delay is set to False. + if not add_delay: + if "delay" in all_instructions: + all_instructions.remove("delay") + + # Add parsed properties to target + target = Target(**in_data) + 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_control_flow_mapping[inst_name], + name=inst_name, ) - target.add_instruction(Measure(), measure_props) - # Parse from configuration because properties doesn't exist - else: - target = Target( - num_qubits=configuration.n_qubits, - concurrent_measurements=getattr(configuration, "meas_map", None), - ) - for gate in configuration.gates: - name = gate.name - gate_props = ( - {tuple(x): None for x in gate.coupling_map} # type: ignore[misc] - if hasattr(gate, "coupling_map") - else {None: None} + elif properties is None: + target.add_instruction( + instruction=inst_name_map[inst_name], + name=inst_name, ) - if name in name_mapping: - target.add_instruction(name_mapping[name], gate_props) - else: - raise QiskitError( - f"Operation name {name} does not have a known mapping. " - "Use custom_name_mapping to map this name to an Operation object" - ) - target.add_instruction(Measure()) - # parse global configuration properties - if hasattr(configuration, "dt"): - target.dt = configuration.dt - if hasattr(configuration, "timing_constraints"): - target.granularity = configuration.timing_constraints.get("granularity") - target.min_length = configuration.timing_constraints.get("min_length") - target.pulse_alignment = configuration.timing_constraints.get("pulse_alignment") - target.acquire_alignment = configuration.timing_constraints.get("acquire_alignment") - # If a pulse defaults exists use that as the source of truth - if defaults is not None: - inst_map = defaults.instruction_schedule_map - for inst in inst_map.instructions: - for qarg in inst_map.qubits_with_instruction(inst): - try: - qargs = tuple(qarg) - except TypeError: - qargs = (qarg,) - # Do NOT call .get method. This parses Qpbj immediately. - # This operation is computationally expensive and should be bypassed. - calibration_entry = inst_map._get_calibration_entry(inst, qargs) - if inst in target: - if inst == "measure": - for qubit in qargs: - if filter_faulty and qubit in faulty_qubits: - continue - target[inst][(qubit,)].calibration = calibration_entry - elif qargs in target[inst]: - if filter_faulty and any(qubit in faulty_qubits for qubit in qargs): - continue - target[inst][qargs].calibration = calibration_entry - combined_global_ops = set() - if configuration.basis_gates: - combined_global_ops.update(configuration.basis_gates) - for op in combined_global_ops: - if op not in target: - if op in name_mapping: - target.add_instruction(name_mapping[op], name=op) - else: - raise QiskitError( - f"Operation name '{op}' does not have a known mapping. Use " - "custom_name_mapping to map this name to an Operation object" - ) - if add_delay and "delay" not in target: - target.add_instruction( - name_mapping["delay"], - {(bit,): None for bit in range(target.num_qubits) if bit not in faulty_qubits}, - ) + else: + target.add_instruction( + instruction=inst_name_map[inst_name], + properties=prop_name_map.get(inst_name, None), + ) + return target @@ -254,8 +325,8 @@ def __init__( self, backend: BackendV1, name_mapping: Optional[Dict[str, Any]] = None, - add_delay: bool = False, - filter_faulty: bool = False, + add_delay: bool = True, + filter_faulty: bool = True, ): """Initialize a BackendV2 converter instance based on a BackendV1 instance. @@ -286,9 +357,13 @@ def __init__( ) self._options = self._backend._options self._properties = None + self._defaults = None + if hasattr(self._backend, "properties"): self._properties = self._backend.properties() - self._defaults = None + if hasattr(self._backend, "defaults"): + self._defaults = self._backend.defaults() + self._target = None self._name_mapping = name_mapping self._add_delay = add_delay @@ -301,14 +376,10 @@ def target(self): :rtype: Target """ if self._target is None: - if self._defaults is None and hasattr(self._backend, "defaults"): - self._defaults = self._backend.defaults() - if self._properties is None and hasattr(self._backend, "properties"): - self._properties = self._backend.properties() self._target = convert_to_target( - self._config, - self._properties, - self._defaults, + configuration=self._config, + properties=self._properties, + defaults=self._defaults, custom_name_mapping=self._name_mapping, add_delay=self._add_delay, filter_faulty=self._filter_faulty, diff --git a/qiskit/providers/fake_provider/fake_backend.py b/qiskit/providers/fake_provider/fake_backend.py index e1740368884c..35e993185bfe 100644 --- a/qiskit/providers/fake_provider/fake_backend.py +++ b/qiskit/providers/fake_provider/fake_backend.py @@ -177,9 +177,8 @@ def target(self) -> Target: defaults = PulseDefaults.from_dict(self._defs_dict) self._target = convert_to_target( - conf, props, defaults, add_delay=True, filter_faulty=True + configuration=conf, properties=props, defaults=defaults ) - return self._target @property diff --git a/releasenotes/notes/Update_backend_model_up_conversion_logic-75ecc2030a9fe6b1.yaml b/releasenotes/notes/Update_backend_model_up_conversion_logic-75ecc2030a9fe6b1.yaml new file mode 100644 index 000000000000..9080273a97a7 --- /dev/null +++ b/releasenotes/notes/Update_backend_model_up_conversion_logic-75ecc2030a9fe6b1.yaml @@ -0,0 +1,19 @@ +--- +upgrade: + - | + The new logic provides better backend model up-conversion mechanism, and better handling of control flow instructions. +fixes: + - | + Fixes return of improper Schedule by Backend.instruction_schedule_map.get('measure', [0]) + + .. code-block:: python + + #import a fake backend which is a sub-class of BackendV2. + from qiskit.providers.fake_provider import FakePerth + backend = FakePerth() + sched = backend.instruction_schedule_map.get('measure', [0]) + + The issue was that the :code:`sched` contained Schedule for measure operation on + all qubits of the backend instead of having the Schedule for measure operation + on just qubit_0. + diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index 078a5110f818..21a18e635d89 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -530,6 +530,34 @@ def test_filter_faulty_qubits_backend_v2_converter_with_delay(self): for qarg in v2_backend.target.qargs: self.assertNotIn(i, qarg) + def test_backend_v2_converter_without_delay(self): + """Test setting :code:`add_delay`argument of :func:`.BackendV2Converter` + to :code:`False`.""" + + expected = { + (0,), + (0, 1), + (0, 2), + (1,), + (1, 0), + (1, 2), + (2,), + (2, 0), + (2, 1), + (2, 3), + (2, 4), + (3,), + (3, 2), + (3, 4), + (4,), + (4, 2), + (4, 3), + } + + backend = BackendV2Converter(backend=FakeYorktown(), filter_faulty=True, add_delay=False) + + self.assertEqual(backend.target.qargs, expected) + def test_filter_faulty_qubits_and_gates_backend_v2_converter(self): """Test faulty gates and qubits.""" backend = FakeWashington() diff --git a/test/python/scheduler/test_basic_scheduler.py b/test/python/scheduler/test_basic_scheduler.py index 83782243d15f..f26a4fe6c7a8 100644 --- a/test/python/scheduler/test_basic_scheduler.py +++ b/test/python/scheduler/test_basic_scheduler.py @@ -1140,3 +1140,35 @@ def test_schedule_block_in_instmap(self): ref_sched += Play(Gaussian(100, 0.1, 10), DriveChannel(0)) self.assertEqual(sched, ref_sched) + + def test_inst_sched_map_get_measure_0(self): + """Test that Schedule returned by backend.instruction_schedule_map.get('measure', [0]) + is actually Schedule for just qubit_0""" + sched_from_backend = self.backend.instruction_schedule_map.get("measure", [0]) + expected_sched = Schedule( + (0, Acquire(1472, AcquireChannel(0), MemorySlot(0))), + (0, Acquire(1472, AcquireChannel(1), MemorySlot(1))), + (0, Acquire(1472, AcquireChannel(2), MemorySlot(2))), + (0, Acquire(1472, AcquireChannel(3), MemorySlot(3))), + (0, Acquire(1472, AcquireChannel(4), MemorySlot(4))), + (0, Acquire(1472, AcquireChannel(5), MemorySlot(5))), + (0, Acquire(1472, AcquireChannel(6), MemorySlot(6))), + ( + 0, + Play( + GaussianSquare( + duration=1472, + sigma=64, + width=1216, + amp=0.24000000000000002, + angle=-0.24730169436555283, + name="M_m0", + ), + MeasureChannel(0), + name="M_m0", + ), + ), + (1472, Delay(1568, MeasureChannel(0))), + name="measure", + ) + self.assertEqual(sched_from_backend, expected_sched)