Skip to content

Commit f1729db

Browse files
eliarbelraynelfssmtreinish
authored
Remove pulse support in QPY in 2.0 (Qiskit#13814)
* Handle ScheduleBlock and pulse gates loading * Add documentation and remove redundant code * Limit QPY version when generating circuits for compatibility test Fix some doc issues * Handle QPY compatibility testing. Misc other fixes * Update qiskit/qpy/binary_io/circuits.py Co-authored-by: Raynel Sanchez <[email protected]> * Avoid generating pulse circuits in load_qpy & version >= 2.0 * Raise QpyError when loading ScheduleBlock payloads * Fix lint * Apply review comments * import from qiskit.qpy only in load command (i.e. dev version) * Fix lint --------- Co-authored-by: Raynel Sanchez <[email protected]> Co-authored-by: Matthew Treinish <[email protected]>
1 parent ddb7670 commit f1729db

File tree

11 files changed

+215
-1312
lines changed

11 files changed

+215
-1312
lines changed

qiskit/qpy/__init__.py

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717
1818
.. currentmodule:: qiskit.qpy
1919
20-
QPY is a binary serialization format for :class:`~.QuantumCircuit` and
21-
:class:`~.ScheduleBlock` objects that is designed to be cross-platform,
22-
Python version agnostic, and backwards compatible moving forward. QPY should
23-
be used if you need a mechanism to save or copy between systems a
24-
:class:`~.QuantumCircuit` or :class:`~.ScheduleBlock` that preserves the full
25-
Qiskit object structure (except for custom attributes defined outside of
26-
Qiskit code). This differs from other serialization formats like
20+
QPY is a binary serialization format for :class:`~.QuantumCircuit`
21+
objects that is designed to be cross-platform, Python version agnostic,
22+
and backwards compatible moving forward. QPY should be used if you need
23+
a mechanism to save or copy between systems a :class:`~.QuantumCircuit`
24+
that preserves the full Qiskit object structure (except for custom attributes
25+
defined outside of Qiskit code). This differs from other serialization formats like
2726
`OpenQASM <https://github.com/openqasm/openqasm>`__ (2.0 or 3.0) which has a
2827
different abstraction model and can result in a loss of information contained
2928
in the original circuit (or is unable to represent some aspects of the
@@ -170,6 +169,14 @@ def open(*args):
170169
it to QPY setting ``use_symengine=False``. The resulting file can then be loaded by any later
171170
version of Qiskit.
172171
172+
.. note::
173+
174+
Starting with Qiskit version 2.0.0, which removed the Pulse module from the library, QPY provides
175+
limited support for loading payloads that include pulse data. Loading a ``ScheduleBlock`` payload,
176+
a :class:`.QpyError` exception will be raised. Loading a payload for a circuit that contained pulse
177+
gates, the output circuit will contain custom instructions **without** calibration data attached
178+
for each pulse gate, leaving them undefined.
179+
173180
QPY format version history
174181
--------------------------
175182
@@ -902,7 +909,7 @@ def open(*args):
902909
---------
903910
904911
Version 7 adds support for :class:`.~Reference` instruction and serialization of
905-
a :class:`.~ScheduleBlock` program while keeping its reference to subroutines::
912+
a ``ScheduleBlock`` program while keeping its reference to subroutines::
906913
907914
from qiskit import pulse
908915
from qiskit import qpy
@@ -974,12 +981,12 @@ def open(*args):
974981
Version 5
975982
---------
976983
977-
Version 5 changes from :ref:`qpy_version_4` by adding support for :class:`.~ScheduleBlock`
984+
Version 5 changes from :ref:`qpy_version_4` by adding support for ``ScheduleBlock``
978985
and changing two payloads the INSTRUCTION metadata payload and the CUSTOM_INSTRUCTION block.
979986
These now have new fields to better account for :class:`~.ControlledGate` objects in a circuit.
980987
In addition, new payload MAP_ITEM is defined to implement the :ref:`qpy_mapping` block.
981988
982-
With the support of :class:`.~ScheduleBlock`, now :class:`~.QuantumCircuit` can be
989+
With the support of ``ScheduleBlock``, now :class:`~.QuantumCircuit` can be
983990
serialized together with :attr:`~.QuantumCircuit.calibrations`, or
984991
`Pulse Gates <https://docs.quantum.ibm.com/guides/pulse>`_.
985992
In QPY version 5 and above, :ref:`qpy_circuit_calibrations` payload is
@@ -996,7 +1003,7 @@ def open(*args):
9961003
immediately follows the file header block to represent the program type stored in the file.
9971004
9981005
- When ``type==c``, :class:`~.QuantumCircuit` payload follows
999-
- When ``type==s``, :class:`~.ScheduleBlock` payload follows
1006+
- When ``type==s``, ``ScheduleBlock`` payload follows
10001007
10011008
.. note::
10021009
@@ -1009,12 +1016,10 @@ def open(*args):
10091016
SCHEDULE_BLOCK
10101017
~~~~~~~~~~~~~~
10111018
1012-
:class:`~.ScheduleBlock` is first supported in QPY Version 5. This allows
1019+
``ScheduleBlock`` is first supported in QPY Version 5. This allows
10131020
users to save pulse programs in the QPY binary format as follows:
10141021
1015-
.. plot::
1016-
:include-source:
1017-
:nofigs:
1022+
.. code-block:: python
10181023
10191024
from qiskit import pulse, qpy
10201025
@@ -1027,13 +1032,6 @@ def open(*args):
10271032
with open('schedule.qpy', 'rb') as fd:
10281033
new_schedule = qpy.load(fd)[0]
10291034
1030-
.. plot::
1031-
:nofigs:
1032-
1033-
# This block is hidden from readers. It's cleanup code.
1034-
from pathlib import Path
1035-
Path("schedule.qpy").unlink()
1036-
10371035
Note that circuit and schedule block are serialized and deserialized through
10381036
the same QPY interface. Input data type is implicitly analyzed and
10391037
no extra option is required to save the schedule block.
@@ -1043,7 +1041,7 @@ def open(*args):
10431041
SCHEDULE_BLOCK_HEADER
10441042
~~~~~~~~~~~~~~~~~~~~~
10451043
1046-
:class:`~.ScheduleBlock` block starts with the following header:
1044+
``ScheduleBlock`` block starts with the following header:
10471045
10481046
.. code-block:: c
10491047
@@ -1243,8 +1241,8 @@ def open(*args):
12431241
and ``num_params`` length of INSTRUCTION_PARAM payload for parameters
12441242
associated to the custom instruction.
12451243
The ``type`` indicates the class of pulse program which is either, in principle,
1246-
:class:`~.ScheduleBlock` or :class:`~.Schedule`. As of QPY Version 5,
1247-
only :class:`~.ScheduleBlock` payload is supported.
1244+
``ScheduleBlock`` or :class:`~.Schedule`. As of QPY Version 5,
1245+
only ``ScheduleBlock`` payload is supported.
12481246
Finally, :ref:`qpy_schedule_block` payload is packed for each CALIBRATION_DEF entry.
12491247
12501248
.. _qpy_instruction_v5:

qiskit/qpy/binary_io/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,5 @@
3131
_read_instruction,
3232
)
3333
from .schedules import (
34-
write_schedule_block,
3534
read_schedule_block,
3635
)

qiskit/qpy/binary_io/circuits.py

Lines changed: 20 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -648,8 +648,7 @@ def _read_custom_operations(file_obj, version, vectors):
648648

649649

650650
def _read_calibrations(file_obj, version, vectors, metadata_deserializer):
651-
calibrations = {}
652-
651+
"""Consume calibrations data, make the file handle point to the next section"""
653652
header = formats.CALIBRATION._make(
654653
struct.unpack(formats.CALIBRATION_PACK, file_obj.read(formats.CALIBRATION_SIZE))
655654
)
@@ -658,21 +657,20 @@ def _read_calibrations(file_obj, version, vectors, metadata_deserializer):
658657
struct.unpack(formats.CALIBRATION_DEF_PACK, file_obj.read(formats.CALIBRATION_DEF_SIZE))
659658
)
660659
name = file_obj.read(defheader.name_size).decode(common.ENCODE)
661-
qubits = tuple(
662-
struct.unpack("!q", file_obj.read(struct.calcsize("!q")))[0]
663-
for _ in range(defheader.num_qubits)
664-
)
665-
params = tuple(
666-
value.read_value(file_obj, version, vectors) for _ in range(defheader.num_params)
667-
)
668-
schedule = schedules.read_schedule_block(file_obj, version, metadata_deserializer)
660+
if name:
661+
warnings.warn(
662+
category=UserWarning,
663+
message="Support for loading pulse gates has been removed in Qiskit 2.0. "
664+
f"If `{name}` is in the circuit it will be left as an opaque instruction.",
665+
)
669666

670-
if name not in calibrations:
671-
calibrations[name] = {(qubits, params): schedule}
672-
else:
673-
calibrations[name][(qubits, params)] = schedule
667+
for _ in range(defheader.num_qubits): # read qubits info
668+
file_obj.read(struct.calcsize("!q"))
669+
670+
for _ in range(defheader.num_params): # read params info
671+
value.read_value(file_obj, version, vectors)
674672

675-
return calibrations
673+
schedules.read_schedule_block(file_obj, version, metadata_deserializer)
676674

677675

678676
def _dumps_register(register, index_map):
@@ -1003,34 +1001,6 @@ def _write_custom_operation(
10031001
return new_custom_instruction
10041002

10051003

1006-
def _write_calibrations(file_obj, calibrations, metadata_serializer, version):
1007-
flatten_dict = {}
1008-
for gate, caldef in calibrations.items():
1009-
for (qubits, params), schedule in caldef.items():
1010-
key = (gate, qubits, params)
1011-
flatten_dict[key] = schedule
1012-
header = struct.pack(formats.CALIBRATION_PACK, len(flatten_dict))
1013-
file_obj.write(header)
1014-
for (name, qubits, params), schedule in flatten_dict.items():
1015-
# In principle ScheduleBlock and Schedule can be supported.
1016-
# As of version 5 only ScheduleBlock is supported.
1017-
name_bytes = name.encode(common.ENCODE)
1018-
defheader = struct.pack(
1019-
formats.CALIBRATION_DEF_PACK,
1020-
len(name_bytes),
1021-
len(qubits),
1022-
len(params),
1023-
type_keys.Program.assign(schedule),
1024-
)
1025-
file_obj.write(defheader)
1026-
file_obj.write(name_bytes)
1027-
for qubit in qubits:
1028-
file_obj.write(struct.pack("!q", qubit))
1029-
for param in params:
1030-
value.write_value(file_obj, param, version=version)
1031-
schedules.write_schedule_block(file_obj, schedule, metadata_serializer, version=version)
1032-
1033-
10341004
def _write_registers(file_obj, in_circ_regs, full_bits):
10351005
bitmap = {bit: index for index, bit in enumerate(full_bits)}
10361006

@@ -1331,8 +1301,11 @@ def write_circuit(
13311301
file_obj.write(instruction_buffer.getvalue())
13321302
instruction_buffer.close()
13331303

1334-
# Write calibrations
1335-
_write_calibrations(file_obj, circuit._calibrations_prop, metadata_serializer, version=version)
1304+
# Pulse has been removed in Qiskit 2.0. As long as we keep QPY at version 13,
1305+
# we need to write an empty calibrations header since read_circuit expects it
1306+
header = struct.pack(formats.CALIBRATION_PACK, 0)
1307+
file_obj.write(header)
1308+
13361309
_write_layout(file_obj, circuit)
13371310

13381311

@@ -1460,11 +1433,9 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa
14601433
standalone_var_indices,
14611434
)
14621435

1463-
# Read calibrations
1436+
# Consume calibrations, but don't use them since pulse gates are not supported as of Qiskit 2.0
14641437
if version >= 5:
1465-
circ._calibrations_prop = _read_calibrations(
1466-
file_obj, version, vectors, metadata_deserializer
1467-
)
1438+
_read_calibrations(file_obj, version, vectors, metadata_deserializer)
14681439

14691440
for vec_name, (vector, initialized_params) in vectors.items():
14701441
if len(initialized_params) != len(vector):

0 commit comments

Comments
 (0)