diff --git a/qiskit/pulse/calibration_entries.py b/qiskit/pulse/calibration_entries.py index eec0d99d19c4..8a5ba1b6e3d6 100644 --- a/qiskit/pulse/calibration_entries.py +++ b/qiskit/pulse/calibration_entries.py @@ -13,6 +13,7 @@ """Internal format of calibration data in target.""" from __future__ import annotations import inspect +import warnings from abc import ABCMeta, abstractmethod from collections.abc import Sequence, Callable from enum import IntEnum @@ -22,6 +23,11 @@ from qiskit.pulse.schedule import Schedule, ScheduleBlock from qiskit.qobj.converters import QobjToInstructionConverter from qiskit.qobj.pulse_qobj import PulseQobjInstruction +from qiskit.exceptions import QiskitError + + +IncompletePulseQobj = object() +"""A None-like constant that represents the PulseQobj is incomplete.""" class CalibrationPublisher(IntEnum): @@ -316,11 +322,20 @@ def __init__( def _build_schedule(self): """Build pulse schedule from cmd-def sequence.""" schedule = Schedule(name=self._name) - for qobj_inst in self._source: - for qiskit_inst in self._converter._get_sequences(qobj_inst): - schedule.insert(qobj_inst.t0, qiskit_inst, inplace=True) - self._definition = schedule - self._parse_argument() + try: + for qobj_inst in self._source: + for qiskit_inst in self._converter._get_sequences(qobj_inst): + schedule.insert(qobj_inst.t0, qiskit_inst, inplace=True) + self._definition = schedule + self._parse_argument() + except QiskitError as ex: + # When the play waveform data is missing in pulse_lib we cannot build schedule. + # Instead of raising an error, get_schedule should return None. + warnings.warn( + f"Pulse calibration cannot be built and the entry is ignored: {ex.message}.", + UserWarning, + ) + self._definition = IncompletePulseQobj def define( self, @@ -336,9 +351,11 @@ def get_signature(self) -> inspect.Signature: self._build_schedule() return super().get_signature() - def get_schedule(self, *args, **kwargs) -> Schedule | ScheduleBlock: + def get_schedule(self, *args, **kwargs) -> Schedule | ScheduleBlock | None: if self._definition is None: self._build_schedule() + if self._definition is IncompletePulseQobj: + return None return super().get_schedule(*args, **kwargs) def __eq__(self, other): @@ -356,4 +373,6 @@ def __str__(self): if self._definition is None: # Avoid parsing schedule for pretty print. return "PulseQobj" + if self._definition is IncompletePulseQobj: + return "None" return super().__str__() diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index e99d497dd314..a19d25085369 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -959,8 +959,12 @@ def _convert_generic( yield instructions.Play(waveform, channel) else: + if qubits := getattr(instruction, "qubits", None): + msg = f"qubits {qubits}" + else: + msg = f"channel {instruction.ch}" raise QiskitError( - f"Instruction {instruction.name} on qubit {instruction.qubits} is not found " + f"Instruction {instruction.name} on {msg} is not found " "in Qiskit namespace. This instruction cannot be deserialized." ) diff --git a/releasenotes/notes/fix-missing-pulse-lib-c370f5b9393d0df6.yaml b/releasenotes/notes/fix-missing-pulse-lib-c370f5b9393d0df6.yaml new file mode 100644 index 000000000000..1e67e9c73bbe --- /dev/null +++ b/releasenotes/notes/fix-missing-pulse-lib-c370f5b9393d0df6.yaml @@ -0,0 +1,12 @@ +--- +fixes: + - | + Fixed a bug that results in an error when a user tries to load .calibration + data of a gate in :class:`.Target` in a particular situation. + This occurs when the backend reports only partial calibration data, for + example referencing a waveform pulse in a command definition but not + including that waveform pulse in the pulse library. In this situation, the + Qiskit pulse object cannot be built, resulting in a failure to build the pulse + schedule for the calibration. Now when calibration data is incomplete + the :class:`.Target` treats it as equivalent to no calibration being reported + at all and does not raise an exception. diff --git a/test/python/pulse/test_calibration_entries.py b/test/python/pulse/test_calibration_entries.py index 3259d3b8a6aa..47d0793fc100 100644 --- a/test/python/pulse/test_calibration_entries.py +++ b/test/python/pulse/test_calibration_entries.py @@ -326,6 +326,30 @@ def test_add_qobj(self): ) self.assertEqual(schedule_to_test, schedule_ref) + def test_missing_waveform(self): + """Test incomplete Qobj should raise warning and calibration returns None.""" + serialized_program = [ + PulseQobjInstruction( + name="waveform_123456", + t0=20, + ch="d0", + ), + ] + entry = PulseQobjDef(converter=self.converter, name="my_gate") + entry.define(serialized_program) + + with self.assertWarns( + UserWarning, + msg=( + "Pulse calibration cannot be built and the entry is ignored: " + "Instruction waveform_123456 on channel d0 is not found in Qiskit namespace. " + "This instruction cannot be deserialized." + ), + ): + out = entry.get_schedule() + + self.assertIsNone(out) + def test_parameterized_qobj(self): """Test adding and managing parameterized qobj. @@ -434,3 +458,37 @@ def test_equality_with_schedule(self): entry2.define(program) self.assertEqual(entry1, entry2) + + def test_calibration_missing_waveform(self): + """Test that calibration with missing waveform should become None. + + When a hardware doesn't support waveform payload and Qiskit doesn't have + the corresponding parametric pulse definition, CmdDef with missing waveform + might be input to the QobjConverter. This fails in loading the calibration data + because necessary pulse object cannot be built. + + In this situation, parsed calibration data must become None, + instead of raising an error. + """ + serialized_program = [ + PulseQobjInstruction( + name="SomeMissingPulse", + t0=0, + ch="d0", + ) + ] + entry = PulseQobjDef(name="qobj_entry") + entry.define(serialized_program) + + # This is pulse qobj before parsing it + self.assertEqual(str(entry), "PulseQobj") + + # Actual calibration value is None + parsed_output = entry.get_schedule() + self.assertIsNone(parsed_output) + + # Repr becomes None-like after it finds calibration is incomplete + self.assertEqual(str(entry), "None") + + # Signature is also None + self.assertIsNone(entry.get_signature())