Skip to content

Commit

Permalink
Fix a bug of missing pulse library entry in PulseQobj parsing (backport
Browse files Browse the repository at this point in the history
#11397) (#11573)

* Fix a bug of missing pulse library entry in PulseQobj parsing (#11397)

* Fix missing pulse lib bug

* Update releasenotes/notes/fix-missing-pulse-lib-c370f5b9393d0df6.yaml

Co-authored-by: Will Shanks <[email protected]>

* Add user warning

---------

Co-authored-by: Will Shanks <[email protected]>
(cherry picked from commit a9e9e95)

# Conflicts:
#	qiskit/pulse/calibration_entries.py

* Fix merge conflict

---------

Co-authored-by: Naoki Kanazawa <[email protected]>
Co-authored-by: Elena Peña Tapia <[email protected]>
  • Loading branch information
3 people authored Jan 17, 2024
1 parent 0055340 commit 92d589f
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 7 deletions.
31 changes: 25 additions & 6 deletions qiskit/pulse/calibration_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

"""Internal format of calibration data in target."""
import inspect
import warnings
from abc import ABCMeta, abstractmethod
from enum import IntEnum
from typing import Callable, List, Union, Optional, Sequence, Any
Expand All @@ -20,6 +21,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):
Expand Down Expand Up @@ -314,11 +320,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,
Expand All @@ -334,9 +349,11 @@ def get_signature(self) -> inspect.Signature:
self._build_schedule()
return super().get_signature()

def get_schedule(self, *args, **kwargs) -> Union[Schedule, ScheduleBlock]:
def get_schedule(self, *args, **kwargs) -> Optional[Union[Schedule, ScheduleBlock]]:
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):
Expand All @@ -354,4 +371,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__()
6 changes: 5 additions & 1 deletion qiskit/qobj/converters/pulse_instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)

Expand Down
12 changes: 12 additions & 0 deletions releasenotes/notes/fix-missing-pulse-lib-c370f5b9393d0df6.yaml
Original file line number Diff line number Diff line change
@@ -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.
58 changes: 58 additions & 0 deletions test/python/pulse/test_calibration_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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())

0 comments on commit 92d589f

Please sign in to comment.