From c650341a52ff485b6f9a5e3fa6153474ecfd6399 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 23 Jun 2022 09:25:30 +0900 Subject: [PATCH] QPY schedule serialization (#7300) * Schedule serialization wip * pulse serialization * move mapping io to separate file * move dump numbers to parameter_values * add alignment context serialization * block serialization * add interface * bug fix * black / lint * documentation * add serialization for kernel and discriminator * migrate circuit qpy to qpy module * keep import path * reno * lint * fix import path * update schedule qpy with cleanup. alignment classes and acquire instruction class are also updated, i.e. remove individual instance members. * move type key enum to type_keys module * add mapping writer (for qpy values) and cleanup sequence serializer to make sure element data is INSTRUCTION_PARAM struct. * compress symbolic pulse expressions with zlib * remove change to existing circuit qpy tests * cleanup for type key * add unittest and fix some bug - small bug fix for wrong class usage - exeption for None symbolic expression - add explicit expand for symengine expression * update documentation * add seek(0) to data_from_binary helper function * remove subclasses and hard coded is_sequential in every subclass * documentation update and type fix Co-authored-by: Matthew Treinish * move test to own module * update typehint * add backward compatibility test * fix merge and cleanup * add circuit calibrations serialization * fix qpy_compat test * remove compatibility test for parameterized program * update reno for calibrations * fix data size Co-authored-by: Matthew Treinish Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/pulse/instructions/acquire.py | 12 +- qiskit/pulse/library/symbolic_pulses.py | 10 +- qiskit/pulse/transforms/alignments.py | 116 ++++-- qiskit/pulse/transforms/dag.py | 3 + qiskit/qpy/__init__.py | 252 +++++++++++- qiskit/qpy/binary_io/__init__.py | 6 + qiskit/qpy/binary_io/circuits.py | 153 +++++-- qiskit/qpy/binary_io/schedules.py | 349 ++++++++++++++++ qiskit/qpy/binary_io/value.py | 152 ++++--- qiskit/qpy/common.py | 329 ++++++++------- qiskit/qpy/formats.py | 51 ++- qiskit/qpy/interface.py | 145 +++++-- qiskit/qpy/type_keys.py | 388 ++++++++++++++++++ ...upgrade-qpy-schedule-f28f6a48a3abb4de.yaml | 32 ++ test/python/pulse/test_instructions.py | 10 +- test/python/qpy/__init__.py | 13 + test/python/qpy/test_block_load_from_qpy.py | 271 ++++++++++++ test/qpy_compat/test_qpy.py | 81 ++++ 18 files changed, 2021 insertions(+), 352 deletions(-) create mode 100644 qiskit/qpy/binary_io/schedules.py create mode 100644 qiskit/qpy/type_keys.py create mode 100644 releasenotes/notes/upgrade-qpy-schedule-f28f6a48a3abb4de.yaml create mode 100644 test/python/qpy/__init__.py create mode 100644 test/python/qpy/test_block_load_from_qpy.py diff --git a/qiskit/pulse/instructions/acquire.py b/qiskit/pulse/instructions/acquire.py index f00e94172ff0..9555e9317938 100644 --- a/qiskit/pulse/instructions/acquire.py +++ b/qiskit/pulse/instructions/acquire.py @@ -75,10 +75,10 @@ def __init__( if not (mem_slot or reg_slot): raise PulseError("Neither MemorySlots nor RegisterSlots were supplied.") - self._kernel = kernel - self._discriminator = discriminator - - super().__init__(operands=(duration, channel, mem_slot, reg_slot), name=name) + super().__init__( + operands=(duration, channel, mem_slot, reg_slot, kernel, discriminator), + name=name, + ) @property def channel(self) -> AcquireChannel: @@ -100,12 +100,12 @@ def duration(self) -> Union[int, ParameterExpression]: @property def kernel(self) -> Kernel: """Return kernel settings.""" - return self._kernel + return self._operands[4] @property def discriminator(self) -> Discriminator: """Return discrimination settings.""" - return self._discriminator + return self._operands[5] @property def acquire(self) -> AcquireChannel: diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 3e163cb236ab..a839dd45ca81 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -69,8 +69,14 @@ def _lifted_gaussian( Returns: Symbolic equation. """ - gauss = sym.exp(-(((t - center) / sigma) ** 2) / 2) - offset = sym.exp(-(((t_zero - center) / sigma) ** 2) / 2) + # Sympy automatically does expand. + # This causes expression inconsistency after qpy round-trip serializing through sympy. + # See issue for details: https://github.com/symengine/symengine.py/issues/409 + t_shifted = (t - center).expand() + t_offset = (t_zero - center).expand() + + gauss = sym.exp(-((t_shifted / sigma) ** 2) / 2) + offset = sym.exp(-((t_offset / sigma) ** 2) / 2) return (gauss - offset) / (1 - offset) diff --git a/qiskit/pulse/transforms/alignments.py b/qiskit/pulse/transforms/alignments.py index f60ab70bf7c1..412a543387d2 100644 --- a/qiskit/pulse/transforms/alignments.py +++ b/qiskit/pulse/transforms/alignments.py @@ -12,24 +12,22 @@ """A collection of passes to reallocate the timeslots of instructions according to context.""" import abc -from typing import Callable, Dict, Any, Union +from typing import Callable, Dict, Any, Union, Tuple import numpy as np -from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType from qiskit.pulse.exceptions import PulseError from qiskit.pulse.schedule import Schedule, ScheduleComponent -from qiskit.pulse.utils import instruction_duration_validation +from qiskit.pulse.utils import instruction_duration_validation, deprecated_functionality class AlignmentKind(abc.ABC): """An abstract class for schedule alignment.""" - is_sequential = None - - def __init__(self): + def __init__(self, context_params: Tuple[ParameterValueType, ...]): """Create new context.""" - self._context_params = tuple() + self._context_params = tuple(context_params) @abc.abstractmethod def align(self, schedule: Schedule) -> Schedule: @@ -46,20 +44,52 @@ def align(self, schedule: Schedule) -> Schedule: """ pass + @deprecated_functionality def to_dict(self) -> Dict[str, Any]: """Returns dictionary to represent this alignment.""" return {"alignment": self.__class__.__name__} + @property + @abc.abstractmethod + def is_sequential(self) -> bool: + """Return ``True`` if this is sequential alignment context. + + This information is used to evaluate DAG equivalency of two :class:`.ScheduleBlock`s. + When the context has two pulses in different channels, + a sequential context subtype intends to return following scheduling outcome. + + .. parsed-literal:: + + ┌────────┐ + D0: ┤ pulse1 ├──────────── + └────────┘ ┌────────┐ + D1: ────────────┤ pulse2 ├ + └────────┘ + + On the other hand, parallel context with ``is_sequential=False`` returns + + .. parsed-literal:: + + ┌────────┐ + D0: ┤ pulse1 ├ + ├────────┤ + D1: ┤ pulse2 ├ + └────────┘ + + All subclasses must implement this method according to scheduling strategy. + """ + pass + def __eq__(self, other): """Check equality of two transforms.""" - return isinstance(other, type(self)) and self.to_dict() == other.to_dict() + if type(self) is not type(other): + return False + if self._context_params != other._context_params: + return False + return True def __repr__(self): - name = self.__class__.__name__ - opts = self.to_dict() - opts.pop("alignment") - opts_str = ", ".join(f"{key}={val}" for key, val in opts.items()) - return f"{name}({opts_str})" + return f"{self.__class__.__name__}({', '.join(self._context_params)})" class AlignLeft(AlignmentKind): @@ -68,7 +98,13 @@ class AlignLeft(AlignmentKind): Instructions are placed at earliest available timeslots. """ - is_sequential = False + def __init__(self): + """Create new left-justified context.""" + super().__init__(context_params=()) + + @property + def is_sequential(self) -> bool: + return False def align(self, schedule: Schedule) -> Schedule: """Reallocate instructions according to the policy. @@ -129,7 +165,13 @@ class AlignRight(AlignmentKind): Instructions are placed at latest available timeslots. """ - is_sequential = False + def __init__(self): + """Create new right-justified context.""" + super().__init__(context_params=()) + + @property + def is_sequential(self) -> bool: + return False def align(self, schedule: Schedule) -> Schedule: """Reallocate instructions according to the policy. @@ -192,7 +234,13 @@ class AlignSequential(AlignmentKind): No buffer time is inserted in between instructions. """ - is_sequential = True + def __init__(self): + """Create new sequential context.""" + super().__init__(context_params=()) + + @property + def is_sequential(self) -> bool: + return True def align(self, schedule: Schedule) -> Schedule: """Reallocate instructions according to the policy. @@ -220,8 +268,6 @@ class AlignEquispaced(AlignmentKind): This alignment is convenient to create dynamical decoupling sequences such as PDD. """ - is_sequential = True - def __init__(self, duration: Union[int, ParameterExpression]): """Create new equispaced context. @@ -231,9 +277,11 @@ def __init__(self, duration: Union[int, ParameterExpression]): no alignment is performed and the input schedule is just returned. This duration can be parametrized. """ - super().__init__() + super().__init__(context_params=(duration,)) - self._context_params = (duration,) + @property + def is_sequential(self) -> bool: + return True @property def duration(self): @@ -281,6 +329,7 @@ def align(self, schedule: Schedule) -> Schedule: return aligned + @deprecated_functionality def to_dict(self) -> Dict[str, Any]: """Returns dictionary to represent this alignment.""" return {"alignment": self.__class__.__name__, "duration": self.duration} @@ -301,9 +350,13 @@ class AlignFunc(AlignmentKind): def udd10_pos(j): return np.sin(np.pi*j/(2*10 + 2))**2 - """ - is_sequential = True + .. note:: + + This context cannot be QPY serialized because of the callable. If you use this context, + your program cannot be saved in QPY format. + + """ def __init__(self, duration: Union[int, ParameterExpression], func: Callable): """Create new equispaced context. @@ -317,16 +370,22 @@ def __init__(self, duration: Union[int, ParameterExpression], func: Callable): fractional coordinate of of that sub-schedule. The returned value should be defined within [0, 1]. The pulse index starts from 1. """ - super().__init__() + super().__init__(context_params=(duration, func)) - self._context_params = (duration,) - self._func = func + @property + def is_sequential(self) -> bool: + return True @property def duration(self): """Return context duration.""" return self._context_params[0] + @property + def func(self): + """Return context alignment function.""" + return self._context_params[1] + def align(self, schedule: Schedule) -> Schedule: """Reallocate instructions according to the policy. @@ -346,7 +405,7 @@ def align(self, schedule: Schedule) -> Schedule: aligned = Schedule.initialize_from(schedule) for ind, (_, child) in enumerate(schedule.children): - _t_center = self.duration * self._func(ind + 1) + _t_center = self.duration * self.func(ind + 1) _t0 = int(_t_center - 0.5 * child.duration) if _t0 < 0 or _t0 > self.duration: PulseError("Invalid schedule position t=%d is specified at index=%d" % (_t0, ind)) @@ -354,6 +413,7 @@ def align(self, schedule: Schedule) -> Schedule: return aligned + @deprecated_functionality def to_dict(self) -> Dict[str, Any]: """Returns dictionary to represent this alignment. @@ -361,6 +421,6 @@ def to_dict(self) -> Dict[str, Any]: """ return { "alignment": self.__class__.__name__, - "duration": self._context_params[0], - "func": self._func.__name__, + "duration": self.duration, + "func": self.func.__name__, } diff --git a/qiskit/pulse/transforms/dag.py b/qiskit/pulse/transforms/dag.py index 4b86c1628787..e533740ece02 100644 --- a/qiskit/pulse/transforms/dag.py +++ b/qiskit/pulse/transforms/dag.py @@ -58,6 +58,9 @@ def block_to_dag(block: ScheduleBlock) -> rx.PyDAG: Returns: Instructions in DAG representation. + + Raises: + PulseError: When the context is invalid subclass. """ if block.alignment_context.is_sequential: return _sequential_allocation(block) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index cd79d0291428..9df5806e5f4e 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -29,7 +29,7 @@ For example:: from qiskit.circuit import QuantumCircuit - from qiskit.circuit import qpy_serialization + from qiskit import qpy qc = QuantumCircuit(2, name='Bell', metadata={'test': True}) qc.h(0) @@ -37,10 +37,10 @@ qc.measure_all() with open('bell.qpy', 'wb') as fd: - qpy_serialization.dump(qc, fd) + qpy.dump(qc, fd) with open('bell.qpy', 'rb') as fd: - new_qc = qpy_serialization.load(fd)[0] + new_qc = qpy.load(fd)[0] API documentation ================= @@ -105,9 +105,249 @@ Version 5 ========= -Version 5 changes from :ref:`qpy_version_4` by changing two payloads the INSTRUCTION metadata -payload and the CUSTOM_INSTRUCTION block. These now have new fields to better account for -:class:`~.ControlledGate` objects in a circuit. +Version 5 changes from :ref:`qpy_version_4` by adding support for :class:`.~ScheduleBlock` +and changing two payloads the INSTRUCTION metadata payload and the CUSTOM_INSTRUCTION block. +These now have new fields to better account for :class:`~.ControlledGate` objects in a circuit. +In addition, new payload MAP_ITEM is defined to implement the :ref:`qpy_mapping` block. + +With the support of :class:`.~ScheduleBlock`, now :class:`~.QuantumCircuit` can be +serialized together with :attr:`~.QuantumCircuit.calibrations`, or +`Pulse Gates `_. +In QPY version 5 and above, :ref:`qpy_circuit_calibrations` payload is +packed after the :ref:`qpy_instructions` block. + +In QPY version 5 and above, + +.. code-block:: c + + struct { + char type; + } + +immediately follows the file header block to represent the program type stored in the file. + +- When ``type==c``, :class:`~.QuantumCircuit` payload follows +- When ``type==s``, :class:`~.ScheduleBlock` payload follows + +.. note:: + + Different programs cannot be packed together in the same file. + You must create different files for different program types. + Multiple objects with the same type can be saved in a single file. + +.. _qpy_schedule_block: + +SCHEDULE_BLOCK +-------------- + +:class:`~.ScheduleBlock` is first supported in QPY Version 5. This allows +users to save pulse programs in the QPY binary format as follows: + +.. code-block:: python + + from qiskit import pulse, qpy + + with pulse.build() as schedule: + pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) + + with open('schedule.qpy', 'wb') as fd: + qpy.dump(qc, fd) + + with open('schedule.qpy', 'rb') as fd: + new_qc = qpy.load(fd)[0] + +Note that circuit and schedule block are serialized and deserialized through +the same QPY interface. Input data type is implicitly analyzed and +no extra option is required to save the schedule block. + +SCHEDULE_BLOCK_HEADER +--------------------- + +:class:`~.ScheduleBlock` block starts with the following header: + +.. code-block:: c + + struct { + uint16_t name_size; + uint64_t metadata_size; + uint16_t num_element; + } + +which is immediately followed by ``name_size`` utf8 bytes of schedule name and +``metadata_size`` utf8 bytes of the JSON serialized metadata dictionary +attached to the schedule. + +Then, alignment context of the schedule block starts with ``char`` +representing the supported context type followed by the :ref:`qpy_sequence` block representing +the parameters associated with the alignment context :attr:`AlignmentKind._context_params`. +The context type char is mapped to each alignment subclass as follows: + +- ``l``: :class:`~.AlignLeft` +- ``r``: :class:`~.AlignRight` +- ``s``: :class:`~.AlignSequential` +- ``e``: :class:`~.AlignEquispaced` + +Note that :class:`~.AlignFunc` context is not supported becasue of the callback function +stored in the context parameters. + +This alignment block is further followed by ``num_element`` length of block elements which may +consist of nested schedule blocks and schedule instructions. +Each schedule instruction starts with ``char`` representing the instruction type +followed by the :ref:`qpy_sequence` block representing the instruction +:attr:`~qiskit.pulse.instructions.Instruction.operands`. +Note that the data structure of pulse :class:`~qiskit.pulse.instructions.Instruction` +is unified so that instance can be uniquely determied by the class and a tuple of operands. +The mapping of type char to the instruction subclass is defined as follows: + +- ``a``: :class:`~qiskit.pulse.instructions.Acquire` instruction +- ``p``: :class:`~qiskit.pulse.instructions.Play` instruction +- ``d``: :class:`~qiskit.pulse.instructions.Delay` instruction +- ``f``: :class:`~qiskit.pulse.instructions.SetFrequency` instruction +- ``g``: :class:`~qiskit.pulse.instructions.ShiftFrequency` instruction +- ``q``: :class:`~qiskit.pulse.instructions.SetPhase` instruction +- ``r``: :class:`~qiskit.pulse.instructions.ShiftPhase` instruction +- ``b``: :class:`~qiskit.pulse.instructions.RelativeBarrier` instruction + +The operands of these instances can be serialized through the standard QPY value serialization +mechanism, however there are special object types that only appear in the schedule operands. +Since the operands are serialized as :ref:`qpy_sequence`, each element must be packed with the +INSTRUCTION_PARAM pack struct, where each payload starts with a header block consists of +the char ``type`` and uint64_t ``size``. +Special objects start with the following type key: + +- ``c``: :class:`~qiskit.pulse.channels.Channel` +- ``w``: :class:`~qiskit.pulse.library.Waveform` +- ``s``: :class:`~qiskit.pulse.library.SymbolicPulse` + +.. _qpy_schedule_channel: + +CHANNEL +------- + +Channel block starts with channel subtype ``char`` that maps an object data to +:class:`~qiskit.pulse.channels.Channel` subclass. Mapping is defined as follows: + +- ``d``: :class:`~qiskit.pulse.channels.DriveChannel` +- ``c``: :class:`~qiskit.pulse.channels.ControlChannel` +- ``m``: :class:`~qiskit.pulse.channels.MeasureChannel` +- ``a``: :class:`~qiskit.pulse.channels.AcquireChannel` +- ``e``: :class:`~qiskit.pulse.channels.MemorySlot` +- ``r``: :class:`~qiskit.pulse.channels.RegisterSlot` + +The key is immediately followed by the channel index serialized as the INSTRUCTION_PARAM. + +.. _qpy_schedule_waveform: + +Waveform +-------- + +Waveform block starts with WAVEFORM header: + +.. code-block:: c + + struct { + double epsilon; + uint32_t data_size; + _bool amp_limited; + } + +which is followed by ``data_size`` bytes of complex ``ndarray`` binary generated by numpy.save_. +This represents the complex IQ data points played on a quantum device. +:attr:`~qiskit.pulse.library.Waveform.name` is saved after the samples in the +INSTRUCTION_PARAM pack struct, which can be string or ``None``. + +.. _numpy.save: https://numpy.org/doc/stable/reference/generated/numpy.save.html + +.. _qpy_schedule_symbolic_pulse: + +SymbolicPulse +------------- + +SymbolicPulse block starts with SYMBOLIC_PULSE header: + +.. code-block:: c + + struct { + uint16_t type_size; + uint16_t envelope_size; + uint16_t constraints_size; + uint16_t valid_amp_conditions_size; + _bool amp_limited; + } + +which is followed by ``type_size`` utf8 bytes of :attr:`.SymbolicPulse.pulse_type` string +that represents a class of waveform, such as "Gaussian" or "GaussianSquare". +Then, ``envelope_size``, ``constraints_size``, ``valid_amp_conditions_size`` utf8 bytes of +serialized symbolic expressions are generated for :attr:`.SymbolicPulse.envelope`, +:attr:`.SymbolicPulse.constraints`, and :attr:`.SymbolicPulse.valid_amp_conditions`, respectively. +Since string representation of these expressions are usually lengthy, +the expression binary is generated by the python zlib_ module with data compression. + +To uniquely specify a pulse instance, we also need to store the associated parameters, +which consist of ``duration`` and the rest of parameters as a dictionary. +Dictionary parameters are first dumped in the :ref:`qpy_mapping` form, and then ``duration`` +is dumped with the INSTRUCTION_PARAM pack struct. +Lastly, :attr:`~qiskit.pulse.library.SymbolicPulse.name` is saved also with the +INSTRUCTION_PARAM pack struct, which can be string or ``None``. + +.. _zlib: https://docs.python.org/3/library/zlib.html + +.. _qpy_mapping: + +MAPPING +------- + +The MAPPING is a representation for arbitrary mapping object. This is a fixed length +:ref:`qpy_sequence` of key-value pair represented by the MAP_ITEM payload. + +A MAP_ITEM starts with a header defined as: + +.. code-block:: c + + struct { + uint16_t key_size; + char type; + uint16_t size; + } + +which is immediately followed by the ``key_size`` utf8 bytes representing +the dictionary key in string and ``size`` utf8 bytes of arbitrary object data of +QPY serializable ``type``. + +.. _qpy_circuit_calibrations: + +CIRCUIT_CALIBRATIONS +-------------------- + +The CIRCUIT_CALIBRATIONS block is a dictionary to define pulse calibrations of the custom +instruction set. This block starts with the following CALIBRATION header: + +.. code-block:: c + + struct { + uint16_t num_cals; + } + +which is followed by the ``num_cals`` length of calibration entries, each starts with +the CALIBRATION_DEF header: + +.. code-block:: c + + struct { + uint16_t name_size; + uint16_t num_qubits; + uint16_t num_params; + char type; + } + +The calibration definition header is then followed by ``name_size`` utf8 bytes of +the gate name, ``num_qubits`` length of integers representing a sequence of qubits, +and ``num_params`` length of INSTRUCTION_PARAM payload for parameters +associated to the custom instruction. +The ``type`` indicates the class of pulse program which is either, in pricinple, +:class:`~.ScheduleBlock` or :class:`~.Schedule`. As of QPY Version 5, +only :class:`~.ScheduleBlock` payload is supported. +Finally, :ref:`qpy_schedule_block` payload is packed for each CALIBRATION_DEF entry. INSTRUCTION ----------- diff --git a/qiskit/qpy/binary_io/__init__.py b/qiskit/qpy/binary_io/__init__.py index 69ae4b7d651e..a5948b7d3f1b 100644 --- a/qiskit/qpy/binary_io/__init__.py +++ b/qiskit/qpy/binary_io/__init__.py @@ -15,6 +15,8 @@ from .value import ( dumps_value, loads_value, + write_value, + read_value, # for backward compatibility; provider, runtime, experiment call this private methods. _write_parameter_expression, _read_parameter_expression, @@ -28,3 +30,7 @@ _write_instruction, _read_instruction, ) +from .schedules import ( + write_schedule_block, + read_schedule_block, +) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 1d70e0fe8d68..f851679179d8 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -32,8 +32,8 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister, Qubit from qiskit.extensions import quantum_initializer -from qiskit.qpy import common, formats, exceptions -from qiskit.qpy.binary_io import value +from qiskit.qpy import common, formats, exceptions, type_keys +from qiskit.qpy.binary_io import value, schedules from qiskit.quantum_info.operators import SparsePauliOp from qiskit.synthesis import evolution as evo_synth @@ -121,31 +121,29 @@ def _read_registers(file_obj, num_registers): return registers -def _read_instruction_parameter(file_obj, version, vectors): - type_key, bin_data = common.read_instruction_param(file_obj) - - if type_key == common.ProgramTypeKey.CIRCUIT: - param = common.data_from_binary(bin_data, read_circuit, version=version) - elif type_key == common.ContainerTypeKey.RANGE: - data = formats.RANGE._make(struct.unpack(formats.RANGE_PACK, bin_data)) +def _loads_instruction_parameter(type_key, data_bytes, version, vectors): + if type_key == type_keys.Program.CIRCUIT: + param = common.data_from_binary(data_bytes, read_circuit, version=version) + elif type_key == type_keys.Container.RANGE: + data = formats.RANGE._make(struct.unpack(formats.RANGE_PACK, data_bytes)) param = range(data.start, data.stop, data.step) - elif type_key == common.ContainerTypeKey.TUPLE: + elif type_key == type_keys.Container.TUPLE: param = tuple( common.sequence_from_binary( - bin_data, - _read_instruction_parameter, + data_bytes, + _loads_instruction_parameter, version=version, vectors=vectors, ) ) - elif type_key == common.ValueTypeKey.INTEGER: + elif type_key == type_keys.Value.INTEGER: # TODO This uses little endian. Should be fixed in the next QPY version. - param = struct.unpack("= 5 and type_key == common.CircuitInstructionTypeKey.CONTROLLED_GATE: + if version >= 5 and type_key == type_keys.CircuitInstruction.CONTROLLED_GATE: with io.BytesIO(base_gate_raw) as base_gate_obj: base_gate = _read_instruction( base_gate_obj, None, registers, custom_operations, version, vectors @@ -332,7 +331,7 @@ def _parse_custom_operation(custom_operations, gate_name, params, version, vecto inst_obj.definition = definition return inst_obj - if type_key == common.CircuitInstructionTypeKey.PAULI_EVOL_GATE: + if type_key == type_keys.CircuitInstruction.PAULI_EVOL_GATE: return definition raise ValueError("Invalid custom instruction type '%s'" % type_str) @@ -366,7 +365,7 @@ def _read_pauli_evolution_gate(file_obj, version, vectors): pauli_op = operator_list time = value.loads_value( - common.ValueTypeKey(pauli_evolution_def.time_type), + pauli_evolution_def.time_type, file_obj.read(pauli_evolution_def.time_size), version=version, vectors=vectors, @@ -432,27 +431,56 @@ def _read_custom_operations(file_obj, version, vectors): return custom_operations -def _write_instruction_parameter(file_obj, param): +def _read_calibrations(file_obj, version, vectors, metadata_deserializer): + calibrations = {} + + header = formats.CALIBRATION._make( + struct.unpack(formats.CALIBRATION_PACK, file_obj.read(formats.CALIBRATION_SIZE)) + ) + for _ in range(header.num_cals): + defheader = formats.CALIBRATION_DEF._make( + struct.unpack(formats.CALIBRATION_DEF_PACK, file_obj.read(formats.CALIBRATION_DEF_SIZE)) + ) + name = file_obj.read(defheader.name_size).decode(common.ENCODE) + qubits = tuple( + struct.unpack("!q", file_obj.read(struct.calcsize("!q")))[0] + for _ in range(defheader.num_qubits) + ) + params = tuple( + value.read_value(file_obj, version, vectors) for _ in range(defheader.num_params) + ) + schedule = schedules.read_schedule_block(file_obj, version, metadata_deserializer) + + if name not in calibrations: + calibrations[name] = {(qubits, params): schedule} + else: + calibrations[name][(qubits, params)] = schedule + + return calibrations + + +def _dumps_instruction_parameter(param): if isinstance(param, QuantumCircuit): - type_key = common.ProgramTypeKey.CIRCUIT - data = common.data_to_binary(param, write_circuit) + type_key = type_keys.Program.CIRCUIT + data_bytes = common.data_to_binary(param, write_circuit) elif isinstance(param, range): - type_key = common.ContainerTypeKey.RANGE - data = struct.pack(formats.RANGE_PACK, param.start, param.stop, param.step) + type_key = type_keys.Container.RANGE + data_bytes = struct.pack(formats.RANGE_PACK, param.start, param.stop, param.step) elif isinstance(param, tuple): - type_key = common.ContainerTypeKey.TUPLE - data = common.sequence_to_binary(param, _write_instruction_parameter) + type_key = type_keys.Container.TUPLE + data_bytes = common.sequence_to_binary(param, _dumps_instruction_parameter) elif isinstance(param, int): # TODO This uses little endian. This should be fixed in next QPY version. - type_key = common.ValueTypeKey.INTEGER - data = struct.pack("= 5: + circ.calibrations = _read_calibrations(file_obj, version, vectors, metadata_deserializer) + for vec_name, (vector, initialized_params) in vectors.items(): if len(initialized_params) != len(vector): warnings.warn( diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py new file mode 100644 index 000000000000..8f4bd0ca471f --- /dev/null +++ b/qiskit/qpy/binary_io/schedules.py @@ -0,0 +1,349 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +# pylint: disable=invalid-name + +"""Read and write schedule and schedule instructions.""" +import json +import struct +import zlib + +import numpy as np + +from qiskit.exceptions import QiskitError +from qiskit.pulse import library, channels +from qiskit.pulse.schedule import ScheduleBlock +from qiskit.qpy import formats, common, type_keys +from qiskit.qpy.binary_io import value +from qiskit.utils import optionals as _optional + + +def _read_channel(file_obj, version): + type_key = common.read_type_key(file_obj) + index = value.read_value(file_obj, version, {}) + + channel_cls = type_keys.ScheduleChannel.retrieve(type_key) + + return channel_cls(index) + + +def _read_waveform(file_obj, version): + header = formats.WAVEFORM._make( + struct.unpack( + formats.WAVEFORM_PACK, + file_obj.read(formats.WAVEFORM_SIZE), + ) + ) + samples_raw = file_obj.read(header.data_size) + samples = common.data_from_binary(samples_raw, np.load) + name = value.read_value(file_obj, version, {}) + + return library.Waveform( + samples=samples, + name=name, + epsilon=header.epsilon, + limit_amplitude=header.amp_limited, + ) + + +def _loads_symbolic_expr(expr_bytes): + from sympy import parse_expr + + if expr_bytes == b"": + return None + + expr_txt = zlib.decompress(expr_bytes).decode(common.ENCODE) + expr = parse_expr(expr_txt) + + if _optional.HAS_SYMENGINE: + from symengine import sympify + + return sympify(expr) + return expr + + +def _read_symbolic_pulse(file_obj, version): + header = formats.SYMBOLIC_PULSE._make( + struct.unpack( + formats.SYMBOLIC_PULSE_PACK, + file_obj.read(formats.SYMBOLIC_PULSE_SIZE), + ) + ) + pulse_type = file_obj.read(header.type_size).decode(common.ENCODE) + envelope = _loads_symbolic_expr(file_obj.read(header.envelope_size)) + constraints = _loads_symbolic_expr(file_obj.read(header.constraints_size)) + valid_amp_conditions = _loads_symbolic_expr(file_obj.read(header.valid_amp_conditions_size)) + parameters = common.read_mapping( + file_obj, + deserializer=value.loads_value, + version=version, + vectors={}, + ) + duration = value.read_value(file_obj, version, {}) + name = value.read_value(file_obj, version, {}) + + # TODO remove this and merge subclasses into a single kind of SymbolicPulse + # We need some refactoring of our codebase, + # mainly removal of isinstance check and name access with self.__class__.__name__. + if pulse_type == "Gaussian": + pulse_cls = library.Gaussian + elif pulse_type == "GaussianSquare": + pulse_cls = library.GaussianSquare + elif pulse_type == "Drag": + pulse_cls = library.Drag + elif pulse_type == "Constant": + pulse_cls = library.Constant + else: + pulse_cls = library.SymbolicPulse + + # Skip calling constructor to absorb signature mismatch in subclass. + instance = object.__new__(pulse_cls) + instance.duration = duration + instance.name = name + instance._limit_amplitude = header.amp_limited + instance._pulse_type = pulse_type + instance._params = parameters + instance._envelope = envelope + instance._constraints = constraints + instance._valid_amp_conditions = valid_amp_conditions + + return instance + + +def _read_alignment_context(file_obj, version): + type_key = common.read_type_key(file_obj) + + context_params = common.read_sequence( + file_obj, + deserializer=value.loads_value, + version=version, + vectors={}, + ) + context_cls = type_keys.ScheduleAlignment.retrieve(type_key) + + instance = object.__new__(context_cls) + instance._context_params = tuple(context_params) + + return instance + + +def _loads_operand(type_key, data_bytes, version): + if type_key == type_keys.ScheduleOperand.WAVEFORM: + return common.data_from_binary(data_bytes, _read_waveform, version=version) + if type_key == type_keys.ScheduleOperand.SYMBOLIC_PULSE: + return common.data_from_binary(data_bytes, _read_symbolic_pulse, version=version) + if type_key == type_keys.ScheduleOperand.CHANNEL: + return common.data_from_binary(data_bytes, _read_channel, version=version) + + return value.loads_value(type_key, data_bytes, version, {}) + + +def _read_element(file_obj, version, metadata_deserializer): + type_key = common.read_type_key(file_obj) + + if type_key == type_keys.Program.SCHEDULE_BLOCK: + return read_schedule_block(file_obj, version, metadata_deserializer) + + operands = common.read_sequence( + file_obj, + deserializer=_loads_operand, + version=version, + ) + name = value.read_value(file_obj, version, {}) + + instance = object.__new__(type_keys.ScheduleInstruction.retrieve(type_key)) + instance._operands = tuple(operands) + instance._name = name + instance._hash = None + + return instance + + +def _write_channel(file_obj, data): + type_key = type_keys.ScheduleChannel.assign(data) + common.write_type_key(file_obj, type_key) + value.write_value(file_obj, data.index) + + +def _write_waveform(file_obj, data): + samples_bytes = common.data_to_binary(data.samples, np.save) + + header = struct.pack( + formats.WAVEFORM_PACK, + data.epsilon, + len(samples_bytes), + data.limit_amplitude, + ) + file_obj.write(header) + file_obj.write(samples_bytes) + value.write_value(file_obj, data.name) + + +def _dumps_symbolic_expr(expr): + from sympy import srepr, sympify + + if expr is None: + return b"" + + expr_bytes = srepr(sympify(expr)).encode(common.ENCODE) + return zlib.compress(expr_bytes) + + +def _write_symbolic_pulse(file_obj, data): + pulse_type_bytes = data.pulse_type.encode(common.ENCODE) + envelope_bytes = _dumps_symbolic_expr(data.envelope) + constraints_bytes = _dumps_symbolic_expr(data.constraints) + valid_amp_conditions_bytes = _dumps_symbolic_expr(data.valid_amp_conditions) + + header_bytes = struct.pack( + formats.SYMBOLIC_PULSE_PACK, + len(pulse_type_bytes), + len(envelope_bytes), + len(constraints_bytes), + len(valid_amp_conditions_bytes), + data.limit_amplitude, + ) + file_obj.write(header_bytes) + file_obj.write(pulse_type_bytes) + file_obj.write(envelope_bytes) + file_obj.write(constraints_bytes) + file_obj.write(valid_amp_conditions_bytes) + common.write_mapping( + file_obj, + mapping=data._params, + serializer=value.dumps_value, + ) + value.write_value(file_obj, data.duration) + value.write_value(file_obj, data.name) + + +def _write_alignment_context(file_obj, context): + type_key = type_keys.ScheduleAlignment.assign(context) + common.write_type_key(file_obj, type_key) + common.write_sequence( + file_obj, + sequence=context._context_params, + serializer=value.dumps_value, + ) + + +def _dumps_operand(operand): + if isinstance(operand, library.Waveform): + type_key = type_keys.ScheduleOperand.WAVEFORM + data_bytes = common.data_to_binary(operand, _write_waveform) + elif isinstance(operand, library.SymbolicPulse): + type_key = type_keys.ScheduleOperand.SYMBOLIC_PULSE + data_bytes = common.data_to_binary(operand, _write_symbolic_pulse) + elif isinstance(operand, channels.Channel): + type_key = type_keys.ScheduleOperand.CHANNEL + data_bytes = common.data_to_binary(operand, _write_channel) + else: + type_key, data_bytes = value.dumps_value(operand) + + return type_key, data_bytes + + +def _write_element(file_obj, element, metadata_serializer): + if isinstance(element, ScheduleBlock): + common.write_type_key(file_obj, type_keys.Program.SCHEDULE_BLOCK) + write_schedule_block(file_obj, element, metadata_serializer) + else: + type_key = type_keys.ScheduleInstruction.assign(element) + common.write_type_key(file_obj, type_key) + common.write_sequence( + file_obj, + sequence=element.operands, + serializer=_dumps_operand, + ) + value.write_value(file_obj, element.name) + + +def read_schedule_block(file_obj, version, metadata_deserializer=None): + """Read a single ScheduleBlock from the file like object. + + Args: + file_obj (File): A file like object that contains the QPY binary data. + version (int): QPY version. + metadata_deserializer (JSONDecoder): An optional JSONDecoder class + that will be used for the ``cls`` kwarg on the internal + ``json.load`` call used to deserialize the JSON payload used for + the :attr:`.ScheduleBlock.metadata` attribute for a schdule block + in the file-like object. If this is not specified the circuit metadata will + be parsed as JSON with the stdlib ``json.load()`` function using + the default ``JSONDecoder`` class. + + Returns: + ScheduleBlock: The schedule block object from the file. + + Raises: + TypeError: If any of the instructions is invalid data format. + QiskitError: QPY version is earlier than block support. + """ + if version < 5: + QiskitError(f"QPY version {version} does not support ScheduleBlock.") + + data = formats.SCHEDULE_BLOCK_HEADER._make( + struct.unpack( + formats.SCHEDULE_BLOCK_HEADER_PACK, + file_obj.read(formats.SCHEDULE_BLOCK_HEADER_SIZE), + ) + ) + name = file_obj.read(data.name_size).decode(common.ENCODE) + metadata_raw = file_obj.read(data.metadata_size) + metadata = json.loads(metadata_raw, cls=metadata_deserializer) + context = _read_alignment_context(file_obj, version) + + block = ScheduleBlock( + name=name, + metadata=metadata, + alignment_context=context, + ) + for _ in range(data.num_elements): + block_elm = _read_element(file_obj, version, metadata_deserializer) + block.append(block_elm, inplace=True) + + return block + + +def write_schedule_block(file_obj, block, metadata_serializer=None): + """Write a single ScheduleBlock object in the file like object. + + Args: + file_obj (File): The file like object to write the circuit data in. + block (ScheduleBlock): A schedule block data to write. + metadata_serializer (JSONEncoder): An optional JSONEncoder class that + will be passed the :attr:`.ScheduleBlock.metadata` dictionary for + ``block`` and will be used as the ``cls`` kwarg + on the ``json.dump()`` call to JSON serialize that dictionary. + + Raises: + TypeError: If any of the instructions is invalid data format. + """ + metadata = json.dumps(block.metadata, separators=(",", ":"), cls=metadata_serializer).encode( + common.ENCODE + ) + block_name = block.name.encode(common.ENCODE) + + # Write schedule block header + header_raw = formats.SCHEDULE_BLOCK_HEADER( + name_size=len(block_name), + metadata_size=len(metadata), + num_elements=len(block), + ) + header = struct.pack(formats.SCHEDULE_BLOCK_HEADER_PACK, *header_raw) + file_obj.write(header) + file_obj.write(block_name) + file_obj.write(metadata) + + _write_alignment_context(file_obj, block.alignment_context) + for block_elm in block.blocks: + _write_element(file_obj, block_elm, metadata_serializer) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 937082a562a6..d47ea7cc9b05 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -20,19 +20,18 @@ from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement -from qiskit.qpy import common, formats, exceptions -from qiskit.qpy.common import ValueTypeKey as TypeKey, ENCODE +from qiskit.qpy import common, formats, exceptions, type_keys from qiskit.utils import optionals as _optional def _write_parameter(file_obj, obj): - name_bytes = obj._name.encode("utf8") + name_bytes = obj._name.encode(common.ENCODE) file_obj.write(struct.pack(formats.PARAMETER_PACK, len(name_bytes), obj._uuid.bytes)) file_obj.write(name_bytes) def _write_parameter_vec(file_obj, obj): - name_bytes = obj._vector._name.encode(ENCODE) + name_bytes = obj._vector._name.encode(common.ENCODE) file_obj.write( struct.pack( formats.PARAMETER_VECTOR_ELEMENT_PACK, @@ -48,7 +47,7 @@ def _write_parameter_vec(file_obj, obj): def _write_parameter_expression(file_obj, obj): from sympy import srepr, sympify - expr_bytes = srepr(sympify(obj._symbol_expr)).encode(ENCODE) + expr_bytes = srepr(sympify(obj._symbol_expr)).encode(common.ENCODE) param_expr_header_raw = struct.pack( formats.PARAMETER_EXPR_PACK, len(obj._parameter_symbols), len(expr_bytes) ) @@ -56,10 +55,10 @@ def _write_parameter_expression(file_obj, obj): file_obj.write(expr_bytes) for symbol, value in obj._parameter_symbols.items(): - symbol_key = TypeKey.assign(symbol) + symbol_key = type_keys.Value.assign(symbol) # serialize key - if symbol_key == TypeKey.PARAMETER_VECTOR: + if symbol_key == type_keys.Value.PARAMETER_VECTOR: symbol_data = common.data_to_binary(symbol, _write_parameter_vec) else: symbol_data = common.data_to_binary(symbol, _write_parameter) @@ -87,7 +86,7 @@ def _read_parameter(file_obj): *struct.unpack(formats.PARAMETER_PACK, file_obj.read(formats.PARAMETER_SIZE)) ) param_uuid = uuid.UUID(bytes=data.uuid) - name = file_obj.read(data.name_size).decode(ENCODE) + name = file_obj.read(data.name_size).decode(common.ENCODE) param = Parameter.__new__(Parameter, name, uuid=param_uuid) param.__init__(name) return param @@ -101,7 +100,7 @@ def _read_parameter_vec(file_obj, vectors): ), ) param_uuid = uuid.UUID(bytes=data.uuid) - name = file_obj.read(data.vector_name_size).decode(ENCODE) + name = file_obj.read(data.vector_name_size).decode(common.ENCODE) if name not in vectors: vectors[name] = (ParameterVector(name, data.vector_size), set()) vector = vectors[name][0] @@ -123,9 +122,9 @@ def _read_parameter_expression(file_obj): if _optional.HAS_SYMENGINE: import symengine - expr = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(ENCODE))) + expr = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))) else: - expr = parse_expr(file_obj.read(data.expr_size).decode(ENCODE)) + expr = parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)) symbol_map = {} for _ in range(data.map_elements): elem_data = formats.PARAM_EXPR_MAP_ELEM( @@ -136,17 +135,17 @@ def _read_parameter_expression(file_obj): ) symbol = _read_parameter(file_obj) - elem_key = TypeKey(elem_data.type) + elem_key = type_keys.Value(elem_data.type) binary_data = file_obj.read(elem_data.size) - if elem_key == TypeKey.INTEGER: + if elem_key == type_keys.Value.INTEGER: value = struct.unpack("!q", binary_data) - elif elem_key == TypeKey.FLOAT: + elif elem_key == type_keys.Value.FLOAT: value = struct.unpack("!d", binary_data) - elif elem_key == TypeKey.COMPLEX: + elif elem_key == type_keys.Value.COMPLEX: value = complex(*struct.unpack(formats.COMPLEX_PACK, binary_data)) - elif elem_key == TypeKey.PARAMETER: + elif elem_key == type_keys.Value.PARAMETER: value = symbol._symbol_expr - elif elem_key == TypeKey.PARAMETER_EXPRESSION: + elif elem_key == type_keys.Value.PARAMETER_EXPRESSION: value = common.data_from_binary(binary_data, _read_parameter_expression) else: raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) @@ -164,9 +163,9 @@ def _read_parameter_expression_v3(file_obj, vectors): if _optional.HAS_SYMENGINE: import symengine - expr = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(ENCODE))) + expr = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))) else: - expr = parse_expr(file_obj.read(data.expr_size).decode(ENCODE)) + expr = parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)) symbol_map = {} for _ in range(data.map_elements): elem_data = formats.PARAM_EXPR_MAP_ELEM_V3( @@ -175,26 +174,26 @@ def _read_parameter_expression_v3(file_obj, vectors): file_obj.read(formats.PARAM_EXPR_MAP_ELEM_V3_SIZE), ) ) - symbol_key = TypeKey(elem_data.symbol_type) + symbol_key = type_keys.Value(elem_data.symbol_type) - if symbol_key == TypeKey.PARAMETER: + if symbol_key == type_keys.Value.PARAMETER: symbol = _read_parameter(file_obj) - elif symbol_key == TypeKey.PARAMETER_VECTOR: + elif symbol_key == type_keys.Value.PARAMETER_VECTOR: symbol = _read_parameter_vec(file_obj, vectors) else: raise exceptions.QpyError("Invalid parameter expression map type: %s" % symbol_key) - elem_key = TypeKey(elem_data.type) + elem_key = type_keys.Value(elem_data.type) binary_data = file_obj.read(elem_data.size) - if elem_key == TypeKey.INTEGER: + if elem_key == type_keys.Value.INTEGER: value = struct.unpack("!q", binary_data) - elif elem_key == TypeKey.FLOAT: + elif elem_key == type_keys.Value.FLOAT: value = struct.unpack("!d", binary_data) - elif elem_key == TypeKey.COMPLEX: + elif elem_key == type_keys.Value.COMPLEX: value = complex(*struct.unpack(formats.COMPLEX_PACK, binary_data)) - elif elem_key in (TypeKey.PARAMETER, TypeKey.PARAMETER_VECTOR): + elif elem_key in (type_keys.Value.PARAMETER, type_keys.Value.PARAMETER_VECTOR): value = symbol._symbol_expr - elif elem_key == TypeKey.PARAMETER_EXPRESSION: + elif elem_key == type_keys.Value.PARAMETER_EXPRESSION: value = common.data_from_binary( binary_data, _read_parameter_expression_v3, vectors=vectors ) @@ -217,25 +216,25 @@ def dumps_value(obj): Raises: QpyError: Serializer for given format is not ready. """ - type_key = TypeKey.assign(obj) + type_key = type_keys.Value.assign(obj) - if type_key == TypeKey.INTEGER: + if type_key == type_keys.Value.INTEGER: binary_data = struct.pack("!q", obj) - elif type_key == TypeKey.FLOAT: + elif type_key == type_keys.Value.FLOAT: binary_data = struct.pack("!d", obj) - elif type_key == TypeKey.COMPLEX: + elif type_key == type_keys.Value.COMPLEX: binary_data = struct.pack(formats.COMPLEX_PACK, obj.real, obj.imag) - elif type_key == TypeKey.NUMPY_OBJ: + elif type_key == type_keys.Value.NUMPY_OBJ: binary_data = common.data_to_binary(obj, np.save) - elif type_key == TypeKey.STRING: - binary_data = obj.encode(ENCODE) - elif type_key == TypeKey.NULL: + elif type_key == type_keys.Value.STRING: + binary_data = obj.encode(common.ENCODE) + elif type_key == type_keys.Value.NULL: binary_data = b"" - elif type_key == TypeKey.PARAMETER_VECTOR: + elif type_key == type_keys.Value.PARAMETER_VECTOR: binary_data = common.data_to_binary(obj, _write_parameter_vec) - elif type_key == TypeKey.PARAMETER: + elif type_key == type_keys.Value.PARAMETER: binary_data = common.data_to_binary(obj, _write_parameter) - elif type_key == TypeKey.PARAMETER_EXPRESSION: + elif type_key == type_keys.Value.PARAMETER_EXPRESSION: binary_data = common.data_to_binary(obj, _write_parameter_expression) else: raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") @@ -243,6 +242,17 @@ def dumps_value(obj): return type_key, binary_data +def write_value(file_obj, obj): + """Write a value to the file like object. + + Args: + file_obj (File): A file like object to write data. + obj (any): Value to write. + """ + type_key, data = dumps_value(obj) + common.write_generic_typed_data(file_obj, type_key, data) + + def loads_value(type_key, binary_data, version, vectors): """Deserialize input binary data to value object. @@ -258,33 +268,49 @@ def loads_value(type_key, binary_data, version, vectors): Raises: QpyError: Serializer for given format is not ready. """ + # pylint: disable=too-many-return-statements + if isinstance(type_key, bytes): - type_key = TypeKey(type_key) - - if type_key == TypeKey.INTEGER: - obj = struct.unpack("!q", binary_data)[0] - elif type_key == TypeKey.FLOAT: - obj = struct.unpack("!d", binary_data)[0] - elif type_key == TypeKey.COMPLEX: - obj = complex(*struct.unpack(formats.COMPLEX_PACK, binary_data)) - elif type_key == TypeKey.NUMPY_OBJ: - obj = common.data_from_binary(binary_data, np.load) - elif type_key == TypeKey.STRING: - obj = binary_data.decode(ENCODE) - elif type_key == TypeKey.NULL: - obj = None - elif type_key == TypeKey.PARAMETER_VECTOR: - obj = common.data_from_binary(binary_data, _read_parameter_vec, vectors=vectors) - elif type_key == TypeKey.PARAMETER: - obj = common.data_from_binary(binary_data, _read_parameter) - elif type_key == TypeKey.PARAMETER_EXPRESSION: + type_key = type_keys.Value(type_key) + + if type_key == type_keys.Value.INTEGER: + return struct.unpack("!q", binary_data)[0] + if type_key == type_keys.Value.FLOAT: + return struct.unpack("!d", binary_data)[0] + if type_key == type_keys.Value.COMPLEX: + return complex(*struct.unpack(formats.COMPLEX_PACK, binary_data)) + if type_key == type_keys.Value.NUMPY_OBJ: + return common.data_from_binary(binary_data, np.load) + if type_key == type_keys.Value.STRING: + return binary_data.decode(common.ENCODE) + if type_key == type_keys.Value.NULL: + return None + if type_key == type_keys.Value.PARAMETER_VECTOR: + return common.data_from_binary(binary_data, _read_parameter_vec, vectors=vectors) + if type_key == type_keys.Value.PARAMETER: + return common.data_from_binary(binary_data, _read_parameter) + if type_key == type_keys.Value.PARAMETER_EXPRESSION: if version < 3: - obj = common.data_from_binary(binary_data, _read_parameter_expression) + return common.data_from_binary(binary_data, _read_parameter_expression) else: - obj = common.data_from_binary( + return common.data_from_binary( binary_data, _read_parameter_expression_v3, vectors=vectors ) - else: - raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") - return obj + raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") + + +def read_value(file_obj, version, vectors): + """Read a value from the file like object. + + Args: + file_obj (File): A file like object to write data. + version (int): QPY version. + vectors (dict): ParameterVector in current scope. + + Returns: + any: Deserialized value object. + """ + type_key, data = common.read_generic_typed_data(file_obj) + + return loads_value(type_key, data, version, vectors) diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index c988034244d3..f20aa2245582 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -18,181 +18,102 @@ import io import struct -from enum import Enum -import numpy as np - -from qiskit.circuit.parameter import Parameter -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.circuit.parametervector import ParameterVectorElement -from qiskit.circuit.library import PauliEvolutionGate -from qiskit.circuit import Gate, Instruction as CircuitInstruction, QuantumCircuit, ControlledGate -from qiskit.qpy import formats, exceptions +from qiskit.qpy import formats QPY_VERSION = 5 ENCODE = "utf8" -class CircuitInstructionTypeKey(bytes, Enum): - """Type key enum for circuit instruction object.""" +def read_generic_typed_data(file_obj): + """Read a single data chunk from the file like object. - INSTRUCTION = b"i" - GATE = b"g" - PAULI_EVOL_GATE = b"p" - CONTROLLED_GATE = b"c" + Args: + file_obj (File): A file like object that contains the QPY binary data. - @classmethod - def assign(cls, obj): - """Assign type key to given object. + Returns: + tuple: Tuple of type key binary and the bytes object of the single data. + """ + data = formats.INSTRUCTION_PARAM._make( + struct.unpack(formats.INSTRUCTION_PARAM_PACK, file_obj.read(formats.INSTRUCTION_PARAM_SIZE)) + ) - Args: - obj (any): Arbitrary object to evaluate. + return data.type, file_obj.read(data.size) - Returns: - CircuitInstructionTypeKey: Corresponding key object. - Raises: - QpyError: if object type is not defined in QPY. Likely not supported. - """ - if isinstance(obj, PauliEvolutionGate): - return cls.PAULI_EVOL_GATE - if isinstance(obj, ControlledGate): - return cls.CONTROLLED_GATE - if isinstance(obj, Gate): - return cls.GATE - if isinstance(obj, CircuitInstruction): - return cls.INSTRUCTION +def read_sequence(file_obj, deserializer, **kwargs): + """Read a sequence of data from the file like object. - raise exceptions.QpyError( - f"Object type {type(obj)} is not supported in {cls.__name__} namespace." - ) + Args: + file_obj (File): A file like object that contains the QPY binary data. + deserializer (Callable): Deserializer callback that can handle input object type. + This must take type key and binary data of the element and return object. + kwargs: Options set to the deserializer. + Returns: + list: Deserialized object. + """ + sequence = [] -class ValueTypeKey(bytes, Enum): - """Type key enum for value object, e.g. numbers, string, null, parameters.""" - - INTEGER = b"i" - FLOAT = b"f" - COMPLEX = b"c" - NUMPY_OBJ = b"n" - PARAMETER = b"p" - PARAMETER_VECTOR = b"v" - PARAMETER_EXPRESSION = b"e" - STRING = b"s" - NULL = b"z" - - @classmethod - def assign(cls, obj): - """Assign type key to given object. - - Args: - obj (any): Arbitrary object to evaluate. - - Returns: - ValueTypeKey: Corresponding key object. - - Raises: - QpyError: if object type is not defined in QPY. Likely not supported. - """ - if isinstance(obj, int): - return cls.INTEGER - if isinstance(obj, float): - return cls.FLOAT - if isinstance(obj, complex): - return cls.COMPLEX - if isinstance(obj, (np.integer, np.floating, np.ndarray, np.complexfloating)): - return cls.NUMPY_OBJ - if isinstance(obj, ParameterVectorElement): - return cls.PARAMETER_VECTOR - if isinstance(obj, Parameter): - return cls.PARAMETER - if isinstance(obj, ParameterExpression): - return cls.PARAMETER_EXPRESSION - if isinstance(obj, str): - return cls.STRING - if obj is None: - return cls.NULL - - raise exceptions.QpyError( - f"Object type {type(obj)} is not supported in {cls.__name__} namespace." - ) + data = formats.SEQUENCE._make( + struct.unpack(formats.SEQUENCE_PACK, file_obj.read(formats.SEQUENCE_SIZE)) + ) + for _ in range(data.num_elements): + type_key, datum_bytes = read_generic_typed_data(file_obj) + sequence.append(deserializer(type_key, datum_bytes, **kwargs)) + return sequence -class ContainerTypeKey(bytes, Enum): - """Typle key enum for container-like object.""" - RANGE = b"r" - TUPLE = b"t" +def read_mapping(file_obj, deserializer, **kwargs): + """Read a mapping from the file like object. - @classmethod - def assign(cls, obj): - """Assign type key to given object. + .. note:: - Args: - obj (any): Arbitrary object to evaluate. + This function must be used to make a binary data of mapping + which include QPY serialized values. + It's easier to use JSON serializer followed by encoding for standard data formats. + This only supports flat dictionary and key must be string. - Returns: - ContainerTypeKey: Corresponding key object. + Args: + file_obj (File): A file like object that contains the QPY binary data. + deserializer (Callable): Deserializer callback that can handle mapping item. + This must take type key and binary data of the mapping value and return object. + kwargs: Options set to the deserializer. - Raises: - QpyError: if object type is not defined in QPY. Likely not supported. - """ - if isinstance(obj, range): - return cls.RANGE - if isinstance(obj, tuple): - return cls.TUPLE + Returns: + dict: Deserialized object. + """ + mapping = {} - raise exceptions.QpyError( - f"Object type {type(obj)} is not supported in {cls.__name__} namespace." + data = formats.SEQUENCE._make( + struct.unpack(formats.SEQUENCE_PACK, file_obj.read(formats.SEQUENCE_SIZE)) + ) + for _ in range(data.num_elements): + map_header = formats.MAP_ITEM._make( + struct.unpack(formats.MAP_ITEM_PACK, file_obj.read(formats.MAP_ITEM_SIZE)) ) + key = file_obj.read(map_header.key_size).decode(ENCODE) + datum = deserializer(map_header.type, file_obj.read(map_header.size), **kwargs) + mapping[key] = datum + return mapping -class ProgramTypeKey(bytes, Enum): - """Typle key enum for program that Qiskit generates.""" - - CIRCUIT = b"q" - - @classmethod - def assign(cls, obj): - """Assign type key to given object. - - Args: - obj (any): Arbitrary object to evaluate. - - Returns: - ProgramTypeKey: Corresponding key object. - - Raises: - QpyError: if object type is not defined in QPY. Likely not supported. - """ - if isinstance(obj, QuantumCircuit): - return cls.CIRCUIT - raise exceptions.QpyError( - f"Object type {type(obj)} is not supported in {cls.__name__} namespace." - ) - - -def read_instruction_param(file_obj): - """Read a single data chunk from the file like object. +def read_type_key(file_obj): + """Read a type key from the file like object. Args: file_obj (File): A file like object that contains the QPY binary data. Returns: - tuple: Tuple of type key binary and the bytes object of the single data. + bytes: Type key. """ - data = formats.INSTRUCTION_PARAM( - *struct.unpack( - formats.INSTRUCTION_PARAM_PACK, - file_obj.read(formats.INSTRUCTION_PARAM_SIZE), - ) - ) - - return data.type, file_obj.read(data.size) + key_size = struct.calcsize("!1c") + return struct.unpack("!1c", file_obj.read(key_size))[0] -def write_instruction_param(file_obj, type_key, data_binary): +def write_generic_typed_data(file_obj, type_key, data_binary): """Write statically typed binary data to the file like object. Args: @@ -205,6 +126,63 @@ def write_instruction_param(file_obj, type_key, data_binary): file_obj.write(data_binary) +def write_sequence(file_obj, sequence, serializer, **kwargs): + """Write a sequence of data in the file like object. + + Args: + file_obj (File): A file like object to write data. + sequence (Sequence): Object to serialize. + serializer (Callable): Serializer callback that can handle input object type. + This must return type key and binary data of each element. + kwargs: Options set to the serializer. + """ + num_elements = len(sequence) + + file_obj.write(struct.pack(formats.SEQUENCE_PACK, num_elements)) + for datum in sequence: + type_key, datum_bytes = serializer(datum, **kwargs) + write_generic_typed_data(file_obj, type_key, datum_bytes) + + +def write_mapping(file_obj, mapping, serializer, **kwargs): + """Write a mapping in the file like object. + + .. note:: + + This function must be used to make a binary data of mapping + which include QPY serialized values. + It's easier to use JSON serializer followed by encoding for standard data formats. + This only supports flat dictionary and key must be string. + + Args: + file_obj (File): A file like object to write data. + mapping (Mapping): Object to serialize. + serializer (Callable): Serializer callback that can handle mapping item. + This must return type key and binary data of the mapping value. + kwargs: Options set to the serializer. + """ + num_elements = len(mapping) + + file_obj.write(struct.pack(formats.SEQUENCE_PACK, num_elements)) + for key, datum in mapping.items(): + key_bytes = key.encode(ENCODE) + type_key, datum_bytes = serializer(datum, **kwargs) + item_header = struct.pack(formats.MAP_ITEM_PACK, len(key_bytes), type_key, len(datum_bytes)) + file_obj.write(item_header) + file_obj.write(key_bytes) + file_obj.write(datum_bytes) + + +def write_type_key(file_obj, type_key): + """Write a type key in the file like object. + + Args: + file_obj (File): A file like object that contains the QPY binary data. + type_key (bytes): Type key to write. + """ + file_obj.write(struct.pack("!1c", type_key)) + + def data_to_binary(obj, serializer, **kwargs): """Convert object into binary data with specified serializer. @@ -229,17 +207,40 @@ def sequence_to_binary(sequence, serializer, **kwargs): Args: sequence (Sequence): Object to serialize. serializer (Callable): Serializer callback that can handle input object type. + This must return type key and binary data of each element. kwargs: Options set to the serializer. Returns: bytes: Binary data. """ - num_elements = len(sequence) + with io.BytesIO() as container: + write_sequence(container, sequence, serializer, **kwargs) + binary_data = container.getvalue() + + return binary_data + + +def mapping_to_binary(mapping, serializer, **kwargs): + """Convert mapping into binary data with specified serializer. + .. note:: + + This function must be used to make a binary data of mapping + which include QPY serialized values. + It's easier to use JSON serializer followed by encoding for standard data formats. + This only supports flat dictionary and key must be string. + + Args: + mapping (Mapping): Object to serialize. + serializer (Callable): Serializer callback that can handle mapping item. + This must return type key and binary data of the mapping value. + kwargs: Options set to the serializer. + + Returns: + bytes: Binary data. + """ with io.BytesIO() as container: - container.write(struct.pack(formats.SEQUENCE_PACK, num_elements)) - for datum in sequence: - serializer(container, datum, **kwargs) + write_mapping(container, mapping, serializer, **kwargs) binary_data = container.getvalue() return binary_data @@ -257,6 +258,7 @@ def data_from_binary(binary_data, deserializer, **kwargs): any: Deserialized object. """ with io.BytesIO(binary_data) as container: + container.seek(0) obj = deserializer(container, **kwargs) return obj @@ -267,21 +269,38 @@ def sequence_from_binary(binary_data, deserializer, **kwargs): Args: binary_data (bytes): Binary data to deserialize. deserializer (Callable): Deserializer callback that can handle input object type. + This must take type key and binary data of the element and return object. kwargs: Options set to the deserializer. Returns: any: Deserialized sequence. """ - sequence = [] - with io.BytesIO(binary_data) as container: - data = formats.SEQUENCE._make( - struct.unpack( - formats.SEQUENCE_PACK, - container.read(formats.SEQUENCE_SIZE), - ) - ) - for _ in range(data.num_elements): - sequence.append(deserializer(container, **kwargs)) + sequence = read_sequence(container, deserializer, **kwargs) return sequence + + +def mapping_from_binary(binary_data, deserializer, **kwargs): + """Load object from binary mapping with specified deserializer. + + .. note:: + + This function must be used to make a binary data of mapping + which include QPY serialized values. + It's easier to use JSON serializer followed by encoding for standard data formats. + This only supports flat dictionary and key must be string. + + Args: + binary_data (bytes): Binary data to deserialize. + deserializer (Callable): Deserializer callback that can handle mapping item. + This must take type key and binary data of the mapping value and return object. + kwargs: Options set to the deserializer. + + Returns: + dict: Deserialized object. + """ + with io.BytesIO(binary_data) as container: + mapping = read_mapping(container, deserializer, **kwargs) + + return mapping diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 6e8f869ecc70..07e121f67705 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -21,7 +21,7 @@ # FILE_HEADER FILE_HEADER = namedtuple( "FILE_HEADER", - ["preface", "qpy_version", "major_version", "minor_version", "patch_version", "num_circuits"], + ["preface", "qpy_version", "major_version", "minor_version", "patch_version", "num_programs"], ) FILE_HEADER_PACK = "!6sBBBBQ" FILE_HEADER_SIZE = struct.calcsize(FILE_HEADER_PACK) @@ -146,7 +146,6 @@ CUSTOM_CIRCUIT_INST_DEF_V2_PACK = "!H1cII?QIIQ" CUSTOM_CIRCUIT_INST_DEF_V2_SIZE = struct.calcsize(CUSTOM_CIRCUIT_INST_DEF_V2_PACK) - # CUSTOM_CIRCUIT_INST_DEF CUSTOM_CIRCUIT_INST_DEF = namedtuple( "CUSTOM_CIRCUIT_INST_DEF", @@ -155,8 +154,49 @@ CUSTOM_CIRCUIT_INST_DEF_PACK = "!H1cII?Q" CUSTOM_CIRCUIT_INST_DEF_SIZE = struct.calcsize(CUSTOM_CIRCUIT_INST_DEF_PACK) +# CALIBRATION +CALIBRATION = namedtuple("CALIBRATION", ["num_cals"]) +CALIBRATION_PACK = "!H" +CALIBRATION_SIZE = struct.calcsize(CALIBRATION_PACK) + +# CALIBRATION_DEF +CALIBRATION_DEF = namedtuple("CALIBRATION_DEF", ["name_size", "num_qubits", "num_params", "type"]) +CALIBRATION_DEF_PACK = "!HHH1c" +CALIBRATION_DEF_SIZE = struct.calcsize(CALIBRATION_DEF_PACK) + +# SCHEDULE_BLOCK binary format +SCHEDULE_BLOCK_HEADER = namedtuple( + "SCHEDULE_BLOCK", + [ + "name_size", + "metadata_size", + "num_elements", + ], +) +SCHEDULE_BLOCK_HEADER_PACK = "!HQH" +SCHEDULE_BLOCK_HEADER_SIZE = struct.calcsize(SCHEDULE_BLOCK_HEADER_PACK) + +# WAVEFORM binary format +WAVEFORM = namedtuple("WAVEFORM", ["epsilon", "data_size", "amp_limited"]) +WAVEFORM_PACK = "!fI?" +WAVEFORM_SIZE = struct.calcsize(WAVEFORM_PACK) + +# SYMBOLIC_PULSE +SYMBOLIC_PULSE = namedtuple( + "SYMBOLIC_PULSE", + [ + "type_size", + "envelope_size", + "constraints_size", + "valid_amp_conditions_size", + "amp_limited", + ], +) +SYMBOLIC_PULSE_PACK = "!HHHH?" +SYMBOLIC_PULSE_SIZE = struct.calcsize(SYMBOLIC_PULSE_PACK) + # INSTRUCTION_PARAM -INSTRUCTION_PARAM = namedtuple("TYPED_OBJECT", ["type", "size"]) +INSTRUCTION_PARAM = namedtuple("INSTRUCTION_PARAM", ["type", "size"]) INSTRUCTION_PARAM_PACK = "!1cQ" INSTRUCTION_PARAM_SIZE = struct.calcsize(INSTRUCTION_PARAM_PACK) @@ -201,3 +241,8 @@ SEQUENCE = namedtuple("SEQUENCE", ["num_elements"]) SEQUENCE_PACK = "!Q" SEQUENCE_SIZE = struct.calcsize(SEQUENCE_PACK) + +# MAP_ITEM +MAP_ITEM = namedtuple("MAP_ITEM", ["key_size", "type", "size"]) +MAP_ITEM_PACK = "!H1cH" +MAP_ITEM_SIZE = struct.calcsize(MAP_ITEM_PACK) diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index 8b2abf1cebdf..02012452abfc 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -12,14 +12,24 @@ """User interface of qpy serializer.""" -import re +from json import JSONEncoder, JSONDecoder +from typing import Union, List, BinaryIO, Type, Optional +from collections.abc import Iterable import struct import warnings +import re from qiskit.circuit import QuantumCircuit +from qiskit.pulse import ScheduleBlock from qiskit.exceptions import QiskitError -from qiskit.qpy import formats, common, binary_io +from qiskit.qpy import formats, common, binary_io, type_keys +from qiskit.qpy.exceptions import QpyError from qiskit.version import __version__ +from qiskit.utils.deprecation import deprecate_arguments + + +# pylint: disable=invalid-name +QPY_SUPPORTED_TYPES = Union[QuantumCircuit, ScheduleBlock] # This version pattern is taken from the pypa packaging project: # https://github.com/pypa/packaging/blob/21.3/packaging/version.py#L223-L254 @@ -61,7 +71,12 @@ ) -def dump(circuits, file_obj, metadata_serializer=None): +@deprecate_arguments({"circuits": "programs"}) +def dump( + programs: Union[List[QPY_SUPPORTED_TYPES], QPY_SUPPORTED_TYPES], + file_obj: BinaryIO, + metadata_serializer: Optional[Type[JSONEncoder]] = None, +): """Write QPY binary data to a file This function is used to save a circuit to a file for later use or transfer @@ -73,7 +88,7 @@ def dump(circuits, file_obj, metadata_serializer=None): .. code-block:: python from qiskit.circuit import QuantumCircuit - from qiskit.circuit import qpy_serialization + from qiskit import qpy qc = QuantumCircuit(2, name='Bell', metadata={'test': True}) qc.h(0) @@ -85,7 +100,7 @@ def dump(circuits, file_obj, metadata_serializer=None): .. code-block:: python with open('bell.qpy', 'wb') as fd: - qpy_serialization.dump(qc, fd) + qpy.dump(qc, fd) or a gzip compressed file: @@ -94,25 +109,48 @@ def dump(circuits, file_obj, metadata_serializer=None): import gzip with gzip.open('bell.qpy.gz', 'wb') as fd: - qpy_serialization.dump(qc, fd) + qpy.dump(qc, fd) Which will save the qpy serialized circuit to the provided file. Args: - circuits (list or QuantumCircuit): The quantum circuit object(s) to - store in the specified file like object. This can either be a - single QuantumCircuit object or a list of QuantumCircuits. - file_obj (file): The file like object to write the QPY data too - metadata_serializer (JSONEncoder): An optional JSONEncoder class that - will be passed the :attr:`.QuantumCircuit.metadata` dictionary for - each circuit in ``circuits`` and will be used as the ``cls`` kwarg - on the ``json.dump()`` call to JSON serialize that dictionary. + programs: QPY supported object(s) to store in the specified file like object. + QPY supports :class:`.QuantumCircuit` and :class:`.ScheduleBlock`. + Different data types must be separately serialized. + file_obj: The file like object to write the QPY data too + metadata_serializer: An optional JSONEncoder class that + will be passed the ``.metadata`` attribute for each program in ``programs`` and will be + used as the ``cls`` kwarg on the `json.dump()`` call to JSON serialize that dictionary. + + Raises: + QpyError: When multiple data format is mixed in the output. + TypeError: When invalid data type is input. """ - if isinstance(circuits, QuantumCircuit): - circuits = [circuits] + if not isinstance(programs, Iterable): + programs = [programs] + + program_types = set() + for program in programs: + program_types.add(type(program)) + + if len(program_types) > 1: + raise QpyError( + "Input programs contain multiple data types. " + "Different data type must be serialized separately." + ) + program_type = next(iter(program_types)) + + if issubclass(program_type, QuantumCircuit): + type_key = type_keys.Program.CIRCUIT + writer = binary_io.write_circuit + elif program_type is ScheduleBlock: + type_key = type_keys.Program.SCHEDULE_BLOCK + writer = binary_io.write_schedule_block + else: + raise TypeError(f"'{program_type}' is not supported data type.") + version_match = re.search(VERSION_PATTERN, __version__, re.VERBOSE | re.IGNORECASE) version_parts = [int(x) for x in version_match.group("release").split(".")] - header = struct.pack( formats.FILE_HEADER_PACK, b"QISKIT", @@ -120,57 +158,65 @@ def dump(circuits, file_obj, metadata_serializer=None): version_parts[0], version_parts[1], version_parts[2], - len(circuits), + len(programs), ) file_obj.write(header) - for circuit in circuits: - binary_io.write_circuit(file_obj, circuit, metadata_serializer=metadata_serializer) + common.write_type_key(file_obj, type_key) + + for program in programs: + writer(file_obj, program, metadata_serializer=metadata_serializer) -def load(file_obj, metadata_deserializer=None): +def load( + file_obj: BinaryIO, + metadata_deserializer: Optional[Type[JSONDecoder]] = None, +) -> List[QPY_SUPPORTED_TYPES]: """Load a QPY binary file - This function is used to load a serialized QPY circuit file and create - :class:`~qiskit.circuit.QuantumCircuit` objects from its contents. + This function is used to load a serialized QPY Qiskit program file and create + :class:`~qiskit.circuit.QuantumCircuit` objects or + :class:`~qiskit.pulse.schedule.ScheduleBlock` objects from its contents. For example: .. code-block:: python - from qiskit.circuit import qpy_serialization + from qiskit import qpy with open('bell.qpy', 'rb') as fd: - circuits = qpy_serialization.load(fd) + circuits = qpy.load(fd) or with a gzip compressed file: .. code-block:: python import gzip - from qiskit.circuit import qpy_serialization + from qiskit import qpy with gzip.open('bell.qpy.gz', 'rb') as fd: - circuits = qpy_serialization.load(fd) + circuits = qpy.load(fd) which will read the contents of the qpy and return a list of - :class:`~qiskit.circuit.QuantumCircuit` objects from the file. + :class:`~qiskit.circuit.QuantumCircuit` objects or + :class:`~qiskit.pulse.schedule.ScheduleBlock` objects from the file. Args: - file_obj (File): A file like object that contains the QPY binary - data for a circuit - metadata_deserializer (JSONDecoder): An optional JSONDecoder class + file_obj: A file like object that contains the QPY binary + data for a circuit or pulse schedule. + metadata_deserializer: An optional JSONDecoder class that will be used for the ``cls`` kwarg on the internal ``json.load`` call used to deserialize the JSON payload used for - the :attr:`.QuantumCircuit.metadata` attribute for any circuits - in the QPY file. If this is not specified the circuit metadata will + the ``.metadata`` attribute for any programs in the QPY file. + If this is not specified the circuit metadata will be parsed as JSON with the stdlib ``json.load()`` function using the default ``JSONDecoder`` class. + Returns: - list: List of ``QuantumCircuit`` - The list of :class:`~qiskit.circuit.QuantumCircuit` objects - contained in the QPY data. A list is always returned, even if there - is only 1 circuit in the QPY data. + The list of Qiskit programs contained in the QPY data. + A list is always returned, even if there is only 1 program in the QPY data. + Raises: QiskitError: if ``file_obj`` is not a valid QPY file + TypeError: When invalid data type is loaded. """ data = formats.FILE_HEADER._make( struct.unpack( @@ -205,11 +251,22 @@ def load(file_obj, metadata_deserializer=None): "instructions not present in this current qiskit " "version" % (".".join([str(x) for x in header_version_parts]), __version__) ) - circuits = [] - for _ in range(data.num_circuits): - circuits.append( - binary_io.read_circuit( - file_obj, data.qpy_version, metadata_deserializer=metadata_deserializer - ) + + if data.qpy_version < 5: + type_key = type_keys.Program.CIRCUIT + else: + type_key = common.read_type_key(file_obj) + + if type_key == type_keys.Program.CIRCUIT: + loader = binary_io.read_circuit + elif type_key == type_keys.Program.SCHEDULE_BLOCK: + loader = binary_io.read_schedule_block + else: + raise TypeError(f"Invalid payload format data kind '{type_key}'.") + + programs = [] + for _ in range(data.num_programs): + programs.append( + loader(file_obj, data.qpy_version, metadata_deserializer=metadata_deserializer) ) - return circuits + return programs diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py new file mode 100644 index 000000000000..38b22addf8d0 --- /dev/null +++ b/qiskit/qpy/type_keys.py @@ -0,0 +1,388 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2019. +# +# 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. + +# pylint: disable=too-many-return-statements + +""" +QPY Type keys for several namespace. +""" + +from abc import abstractmethod +from enum import Enum + +import numpy as np + +from qiskit.circuit import Gate, Instruction, QuantumCircuit, ControlledGate +from qiskit.circuit.library import PauliEvolutionGate +from qiskit.circuit.parameter import Parameter +from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.circuit.parametervector import ParameterVectorElement +from qiskit.pulse.channels import ( + Channel, + DriveChannel, + MeasureChannel, + ControlChannel, + AcquireChannel, + MemorySlot, + RegisterSlot, +) +from qiskit.pulse.instructions import ( + Acquire, + Play, + Delay, + SetFrequency, + ShiftFrequency, + SetPhase, + ShiftPhase, + RelativeBarrier, +) +from qiskit.pulse.library import Waveform, SymbolicPulse +from qiskit.pulse.schedule import ScheduleBlock +from qiskit.pulse.transforms.alignments import ( + AlignLeft, + AlignRight, + AlignSequential, + AlignEquispaced, +) +from qiskit.qpy import exceptions + + +class TypeKeyBase(bytes, Enum): + """Abstract baseclass for type key Enums.""" + + @classmethod + @abstractmethod + def assign(cls, obj): + """Assign type key to given object. + + Args: + obj (any): Arbitrary object to evaluate. + + Returns: + TypeKey: Corresponding key object. + """ + pass + + @classmethod + @abstractmethod + def retrieve(cls, type_key): + """Get a class from given type key. + + Args: + type_key (bytes): Object type key. + + Returns: + any: Corresponding class. + """ + pass + + +class Value(TypeKeyBase): + """Type key enum for value object.""" + + INTEGER = b"i" + FLOAT = b"f" + COMPLEX = b"c" + NUMPY_OBJ = b"n" + PARAMETER = b"p" + PARAMETER_VECTOR = b"v" + PARAMETER_EXPRESSION = b"e" + STRING = b"s" + NULL = b"z" + + @classmethod + def assign(cls, obj): + if isinstance(obj, int): + return cls.INTEGER + if isinstance(obj, float): + return cls.FLOAT + if isinstance(obj, complex): + return cls.COMPLEX + if isinstance(obj, (np.integer, np.floating, np.complexfloating, np.ndarray)): + return cls.NUMPY_OBJ + if isinstance(obj, ParameterVectorElement): + return cls.PARAMETER_VECTOR + if isinstance(obj, Parameter): + return cls.PARAMETER + if isinstance(obj, ParameterExpression): + return cls.PARAMETER_EXPRESSION + if isinstance(obj, str): + return cls.STRING + if obj is None: + return cls.NULL + + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError + + +class Container(TypeKeyBase): + """Typle key enum for container-like object.""" + + RANGE = b"r" + TUPLE = b"t" + + @classmethod + def assign(cls, obj): + if isinstance(obj, range): + return cls.RANGE + if isinstance(obj, tuple): + return cls.TUPLE + + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError + + +class CircuitInstruction(TypeKeyBase): + """Type key enum for circuit instruction object.""" + + INSTRUCTION = b"i" + GATE = b"g" + PAULI_EVOL_GATE = b"p" + CONTROLLED_GATE = b"c" + + @classmethod + def assign(cls, obj): + if isinstance(obj, PauliEvolutionGate): + return cls.PAULI_EVOL_GATE + if isinstance(obj, ControlledGate): + return cls.CONTROLLED_GATE + if isinstance(obj, Gate): + return cls.GATE + if isinstance(obj, Instruction): + return cls.INSTRUCTION + + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError + + +class ScheduleAlignment(TypeKeyBase): + """Type key enum for schedule block alignment context object.""" + + LEFT = b"l" + RIGHT = b"r" + SEQUENTIAL = b"s" + EQUISPACED = b"e" + + # AlignFunc is not serializable due to the callable in context parameter + + @classmethod + def assign(cls, obj): + if isinstance(obj, AlignLeft): + return cls.LEFT + if isinstance(obj, AlignRight): + return cls.RIGHT + if isinstance(obj, AlignSequential): + return cls.SEQUENTIAL + if isinstance(obj, AlignEquispaced): + return cls.EQUISPACED + + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) + + @classmethod + def retrieve(cls, type_key): + if type_key == cls.LEFT: + return AlignLeft + if type_key == cls.RIGHT: + return AlignRight + if type_key == cls.SEQUENTIAL: + return AlignSequential + if type_key == cls.EQUISPACED: + return AlignEquispaced + + raise exceptions.QpyError( + f"A class corresponding to type key '{type_key}' is not found in {cls.__name__} namespace." + ) + + +class ScheduleInstruction(TypeKeyBase): + """Type key enum for schedule instruction object.""" + + ACQUIRE = b"a" + PLAY = b"p" + DELAY = b"d" + SET_FREQUENCY = b"f" + SHIFT_FREQUENCY = b"g" + SET_PHASE = b"q" + SHIFT_PHASE = b"r" + BARRIER = b"b" + + # 's' is reserved by ScheduleBlock, i.e. block can be nested as an element. + # Call instructon is not supported by QPY. + # This instruction is excluded from ScheduleBlock instructions with + # qiskit-terra/#8005 and new instruction Reference will be added instead. + # Call is only applied to Schedule which is not supported by QPY. + # Also snapshot is not suppored because of its limited usecase. + + @classmethod + def assign(cls, obj): + if isinstance(obj, Acquire): + return cls.ACQUIRE + if isinstance(obj, Play): + return cls.PLAY + if isinstance(obj, Delay): + return cls.DELAY + if isinstance(obj, SetFrequency): + return cls.SET_FREQUENCY + if isinstance(obj, ShiftFrequency): + return cls.SHIFT_FREQUENCY + if isinstance(obj, SetPhase): + return cls.SET_PHASE + if isinstance(obj, ShiftPhase): + return cls.SHIFT_PHASE + if isinstance(obj, RelativeBarrier): + return cls.BARRIER + + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) + + @classmethod + def retrieve(cls, type_key): + if type_key == cls.ACQUIRE: + return Acquire + if type_key == cls.PLAY: + return Play + if type_key == cls.DELAY: + return Delay + if type_key == cls.SET_FREQUENCY: + return SetFrequency + if type_key == cls.SHIFT_FREQUENCY: + return ShiftFrequency + if type_key == cls.SET_PHASE: + return SetPhase + if type_key == cls.SHIFT_PHASE: + return ShiftPhase + if type_key == cls.BARRIER: + return RelativeBarrier + + raise exceptions.QpyError( + f"A class corresponding to type key '{type_key}' is not found in {cls.__name__} namespace." + ) + + +class ScheduleOperand(TypeKeyBase): + """Type key enum for schedule instruction operand object.""" + + WAVEFORM = b"w" + SYMBOLIC_PULSE = b"s" + CHANNEL = b"c" + + # Discriminator and Acquire instance are not serialzied. + # Data format of these object is somewhat opaque and not defiend well. + # It's rarely used in the Qiskit experiements. Of course these can be added later. + + @classmethod + def assign(cls, obj): + if isinstance(obj, Waveform): + return cls.WAVEFORM + if isinstance(obj, SymbolicPulse): + return cls.SYMBOLIC_PULSE + if isinstance(obj, Channel): + return cls.CHANNEL + + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError + + +class ScheduleChannel(TypeKeyBase): + """Type key enum for schedule channel object.""" + + DRIVE = b"d" + CONTROL = b"c" + MEASURE = b"m" + ACQURE = b"a" + MEM_SLOT = b"e" + REG_SLOT = b"r" + + # SnapShot channel is not defined because of its limited usecase. + + @classmethod + def assign(cls, obj): + if isinstance(obj, DriveChannel): + return cls.DRIVE + if isinstance(obj, ControlChannel): + return cls.CONTROL + if isinstance(obj, MeasureChannel): + return cls.MEASURE + if isinstance(obj, AcquireChannel): + return cls.ACQURE + if isinstance(obj, MemorySlot): + return cls.MEM_SLOT + if isinstance(obj, RegisterSlot): + return cls.REG_SLOT + + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) + + @classmethod + def retrieve(cls, type_key): + if type_key == cls.DRIVE: + return DriveChannel + if type_key == cls.CONTROL: + return ControlChannel + if type_key == cls.MEASURE: + return MeasureChannel + if type_key == cls.ACQURE: + return AcquireChannel + if type_key == cls.MEM_SLOT: + return MemorySlot + if type_key == cls.REG_SLOT: + return RegisterSlot + + raise exceptions.QpyError( + f"A class corresponding to type key '{type_key}' is not found in {cls.__name__} namespace." + ) + + +class Program(TypeKeyBase): + """Typle key enum for program that QPY supports.""" + + CIRCUIT = b"q" + SCHEDULE_BLOCK = b"s" + + @classmethod + def assign(cls, obj): + if isinstance(obj, QuantumCircuit): + return cls.CIRCUIT + if isinstance(obj, ScheduleBlock): + return cls.SCHEDULE_BLOCK + + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError diff --git a/releasenotes/notes/upgrade-qpy-schedule-f28f6a48a3abb4de.yaml b/releasenotes/notes/upgrade-qpy-schedule-f28f6a48a3abb4de.yaml new file mode 100644 index 000000000000..4b9013e4d163 --- /dev/null +++ b/releasenotes/notes/upgrade-qpy-schedule-f28f6a48a3abb4de.yaml @@ -0,0 +1,32 @@ +--- +features: + - | + QPY serialization has been upgraded to support :class:`.ScheduleBlock`. + Now you can save pulse program in binary and load it at later time as follows. + + .. code-block:: python + + from qiskit import pulse, qpy + + with pulse.build() as schedule: + pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) + + with open('schedule.qpy', 'wb') as fd: + qpy.dump(qc, fd) + + with open('schedule.qpy', 'rb') as fd: + new_qc = qpy.load(fd)[0] + + This uses the QPY interface common to :class:`.QuantumCircuit`. + See :ref:`qpy_schedule_block` for details of data structure. +upgrade: + - | + QPY serialization has been upgraded to serialize :class:`.QuantumCircuit` + with :attr:`.QuantumCircuit.calibrations`. As of QPY Version 5, only calibration + entris in :class:`.ScheduleBlock` type can be serialized. +deprecations: + - | + ``circuits`` argument in :func:`qiskit.qpy.dump` has been deprecated and + replaced with ``programs`` since now QPY supports multiple data types other than circuits. + - | + :meth:`.AlignmentKind.to_dict` method has been deprecated and will be removed. diff --git a/test/python/pulse/test_instructions.py b/test/python/pulse/test_instructions.py index 886bb6e3c86f..a5284c27ed2f 100644 --- a/test/python/pulse/test_instructions.py +++ b/test/python/pulse/test_instructions.py @@ -54,7 +54,15 @@ def test_can_construct_valid_acquire_command(self): self.assertIsInstance(acq.id, int) self.assertEqual(acq.name, "acquire") self.assertEqual( - acq.operands, (10, channels.AcquireChannel(0), channels.MemorySlot(0), None) + acq.operands, + ( + 10, + channels.AcquireChannel(0), + channels.MemorySlot(0), + None, + kernel, + discriminator, + ), ) def test_instructions_hash(self): diff --git a/test/python/qpy/__init__.py b/test/python/qpy/__init__.py new file mode 100644 index 000000000000..80d69c7611d9 --- /dev/null +++ b/test/python/qpy/__init__.py @@ -0,0 +1,13 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2018. +# +# 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. + +"""Qiskit Qpy tests.""" diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py new file mode 100644 index 000000000000..c829307dbb80 --- /dev/null +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -0,0 +1,271 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 cases for the schedule block qpy loading and saving.""" + +import io +from ddt import ddt, data, unpack + +import numpy as np + +from qiskit.pulse import builder +from qiskit.pulse.library import ( + SymbolicPulse, + Gaussian, + GaussianSquare, + Drag, + Constant, +) +from qiskit.pulse.channels import ( + DriveChannel, + ControlChannel, + MeasureChannel, + AcquireChannel, + MemorySlot, + RegisterSlot, +) +from qiskit.circuit import Parameter, QuantumCircuit, Gate +from qiskit.test import QiskitTestCase +from qiskit.qpy import dump, load +from qiskit.utils import optionals as _optional + + +if _optional.HAS_SYMENGINE: + import symengine as sym +else: + import sympy as sym + + +class QpyScheduleTestCase(QiskitTestCase): + """QPY schedule testing platform.""" + + def assert_roundtrip_equal(self, block): + """QPY roundtrip equal test.""" + qpy_file = io.BytesIO() + dump(block, qpy_file) + qpy_file.seek(0) + new_block = load(qpy_file)[0] + + self.assertEqual(block, new_block) + + +@ddt +class TestLoadFromQPY(QpyScheduleTestCase): + """Test loading and saving schedule block to qpy file.""" + + @data( + (Gaussian, DriveChannel, 160, 0.1, 40), + (GaussianSquare, DriveChannel, 800, 0.1, 64, 544), + (Drag, DriveChannel, 160, 0.1, 40, 0.5), + (Constant, DriveChannel, 800, 0.1), + (Constant, ControlChannel, 800, 0.1), + (Constant, MeasureChannel, 800, 0.1), + ) + @unpack + def test_library_pulse_play(self, envelope, channel, *params): + """Test playing standard pulses.""" + with builder.build() as test_sched: + builder.play( + envelope(*params), + channel(0), + ) + self.assert_roundtrip_equal(test_sched) + + def test_playing_custom_symbolic_pulse(self): + """Test playing a custom user pulse.""" + # pylint: disable=invalid-name + t, amp, freq = sym.symbols("t, amp, freq") + sym_envelope = 2 * amp * (freq * t - sym.floor(1 / 2 + freq * t)) + + my_pulse = SymbolicPulse( + pulse_type="Sawtooth", + duration=100, + parameters={"amp": 0.1, "freq": 0.05}, + envelope=sym_envelope, + name="pulse1", + ) + with builder.build() as test_sched: + builder.play(my_pulse, DriveChannel(0)) + self.assert_roundtrip_equal(test_sched) + + def test_playing_waveform(self): + """Test playing waveform.""" + # pylint: disable=invalid-name + t = np.linspace(0, 1, 100) + waveform = 0.1 * np.sin(2 * np.pi * t) + with builder.build() as test_sched: + builder.play(waveform, DriveChannel(0)) + self.assert_roundtrip_equal(test_sched) + + def test_phases(self): + """Test phase.""" + with builder.build() as test_sched: + builder.shift_phase(0.1, DriveChannel(0)) + builder.set_phase(0.4, DriveChannel(1)) + self.assert_roundtrip_equal(test_sched) + + def test_frequencies(self): + """Test frequency.""" + with builder.build() as test_sched: + builder.shift_frequency(10e6, DriveChannel(0)) + builder.set_frequency(5e9, DriveChannel(1)) + self.assert_roundtrip_equal(test_sched) + + def test_delay(self): + """Test delay.""" + with builder.build() as test_sched: + builder.delay(100, DriveChannel(0)) + self.assert_roundtrip_equal(test_sched) + + def test_barrier(self): + """Test barrier.""" + with builder.build() as test_sched: + builder.barrier(DriveChannel(0), DriveChannel(1), ControlChannel(2)) + self.assert_roundtrip_equal(test_sched) + + def test_measure(self): + """Test measurement.""" + with builder.build() as test_sched: + builder.acquire(100, AcquireChannel(0), MemorySlot(0)) + builder.acquire(100, AcquireChannel(1), RegisterSlot(1)) + self.assert_roundtrip_equal(test_sched) + + @data( + (0, Parameter("dur"), 0.1, 40), + (Parameter("ch1"), 160, 0.1, 40), + (Parameter("ch1"), Parameter("dur"), Parameter("amp"), Parameter("sigma")), + (0, 160, Parameter("amp") * np.exp(1j * Parameter("phase")), 40), + ) + @unpack + def test_parameterized(self, channel, *params): + """Test playing parameterized pulse.""" + # pylint: disable=no-value-for-parameter + with builder.build() as test_sched: + builder.play(Gaussian(*params), DriveChannel(channel)) + self.assert_roundtrip_equal(test_sched) + + def test_nested_blocks(self): + """Test nested blocks with different alignment contexts.""" + with builder.build() as test_sched: + with builder.align_equispaced(duration=1200): + with builder.align_left(): + builder.delay(100, DriveChannel(0)) + builder.delay(200, DriveChannel(1)) + with builder.align_right(): + builder.delay(100, DriveChannel(0)) + builder.delay(200, DriveChannel(1)) + with builder.align_sequential(): + builder.delay(100, DriveChannel(0)) + builder.delay(200, DriveChannel(1)) + self.assert_roundtrip_equal(test_sched) + + def test_bell_schedule(self): + """Test complex schedule to create a Bell state.""" + with builder.build() as test_sched: + with builder.align_sequential(): + # H + builder.shift_phase(-1.57, DriveChannel(0)) + builder.play(Drag(160, 0.05, 40, 1.3), DriveChannel(0)) + builder.shift_phase(-1.57, DriveChannel(0)) + # ECR + with builder.align_left(): + builder.play(GaussianSquare(800, 0.05, 64, 544), DriveChannel(1)) + builder.play(GaussianSquare(800, 0.1 - 0.2j, 64, 544), ControlChannel(0)) + builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) + with builder.align_left(): + builder.play(GaussianSquare(800, -0.05, 64, 544), DriveChannel(1)) + builder.play(GaussianSquare(800, -0.1 + 0.2j, 64, 544), ControlChannel(0)) + builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) + # Measure + with builder.align_left(): + builder.play(GaussianSquare(8000, 0.2, 64, 7744), MeasureChannel(0)) + builder.acquire(8000, AcquireChannel(0), MemorySlot(0)) + + self.assert_roundtrip_equal(test_sched) + + +class TestPulseGate(QpyScheduleTestCase): + """Test loading and saving pulse gate attached circuit to qpy file.""" + + def test_1q_gate(self): + """Test for single qubit pulse gate.""" + mygate = Gate("mygate", 1, []) + + with builder.build() as caldef: + builder.play(Constant(100, 0.1), DriveChannel(0)) + + qc = QuantumCircuit(2) + qc.append(mygate, [0]) + qc.add_calibration(mygate, (0,), caldef) + + self.assert_roundtrip_equal(qc) + + def test_2q_gate(self): + """Test for two qubit pulse gate.""" + mygate = Gate("mygate", 2, []) + + with builder.build() as caldef: + builder.play(Constant(100, 0.1), ControlChannel(0)) + + qc = QuantumCircuit(2) + qc.append(mygate, [0, 1]) + qc.add_calibration(mygate, (0, 1), caldef) + + self.assert_roundtrip_equal(qc) + + def test_parameterized_gate(self): + """Test for parameterized pulse gate.""" + amp = Parameter("amp") + angle = Parameter("angle") + mygate = Gate("mygate", 2, [amp, angle]) + + with builder.build() as caldef: + builder.play(Constant(100, amp * np.exp(1j * angle)), ControlChannel(0)) + + qc = QuantumCircuit(2) + qc.append(mygate, [0, 1]) + qc.add_calibration(mygate, (0, 1), caldef) + + self.assert_roundtrip_equal(qc) + + def test_override(self): + """Test for overriding standard gate with pulse gate.""" + amp = Parameter("amp") + + with builder.build() as caldef: + builder.play(Constant(100, amp), ControlChannel(0)) + + qc = QuantumCircuit(2) + qc.rx(amp, 0) + qc.add_calibration("rx", (0,), caldef, [amp]) + + self.assert_roundtrip_equal(qc) + + def test_multiple_calibrations(self): + """Test for circuit with multiple pulse gates.""" + amp1 = Parameter("amp1") + amp2 = Parameter("amp2") + mygate = Gate("mygate", 1, [amp2]) + + with builder.build() as caldef1: + builder.play(Constant(100, amp1), DriveChannel(0)) + + with builder.build() as caldef2: + builder.play(Constant(100, amp2), DriveChannel(1)) + + qc = QuantumCircuit(2) + qc.rx(amp1, 0) + qc.append(mygate, [1]) + qc.add_calibration("rx", (0,), caldef1, [amp1]) + qc.add_calibration(mygate, (1,), caldef2) + + self.assert_roundtrip_equal(qc) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index fcf6fa3b4f1d..8c23af7fdb0f 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -384,6 +384,85 @@ def generate_control_flow_circuits(): return circuits +def generate_schedule_blocks(): + """Standard QPY testcase for schedule blocks.""" + from qiskit.pulse import builder, channels, library + from qiskit.utils import optionals + + # Parameterized schedule test is avoided. + # Generated reference and loaded QPY object may induce parameter uuid mismatch. + # As workaround, we need test with bounded parameters, however, schedule.parameters + # are returned as Set and thus its order is random. + # Since schedule parameters are validated, we cannot assign random numbers. + # We need to upgrade testing framework. + + schedule_blocks = [] + + # Instructions without parameters + with builder.build() as block: + with builder.align_sequential(): + builder.set_frequency(5e9, channels.DriveChannel(0)) + builder.shift_frequency(10e6, channels.DriveChannel(1)) + builder.set_phase(1.57, channels.DriveChannel(0)) + builder.shift_phase(0.1, channels.DriveChannel(1)) + builder.barrier(channels.DriveChannel(0), channels.DriveChannel(1)) + builder.play(library.Gaussian(160, 0.1, 40), channels.DriveChannel(0)) + builder.play(library.GaussianSquare(800, 0.1, 64, 544), channels.ControlChannel(0)) + builder.play(library.Drag(160, 0.1, 40, 1.5), channels.DriveChannel(1)) + builder.play(library.Constant(800, 0.1), channels.MeasureChannel(0)) + builder.acquire(1000, channels.AcquireChannel(0), channels.MemorySlot(0)) + schedule_blocks.append(block) + # Raw symbolic pulse + if optionals.HAS_SYMENGINE: + import symengine as sym + else: + import sympy as sym + duration, amp, t = sym.symbols("duration amp t") # pylint: disable=invalid-name + expr = amp * sym.sin(2 * sym.pi * t / duration) + my_pulse = library.SymbolicPulse( + pulse_type="Sinusoidal", + duration=100, + parameters={"amp": 0.1}, + envelope=expr, + valid_amp_conditions=sym.Abs(amp) <= 1.0, + ) + with builder.build() as block: + builder.play(my_pulse, channels.DriveChannel(0)) + schedule_blocks.append(block) + # Raw waveform + my_waveform = 0.1 * np.sin(2 * np.pi * np.linspace(0, 1, 100)) + with builder.build() as block: + builder.play(my_waveform, channels.DriveChannel(0)) + schedule_blocks.append(block) + + return schedule_blocks + + +def generate_calibrated_circuits(): + """Test for QPY serialization with calibrations.""" + from qiskit.pulse import builder, Constant, DriveChannel + + circuits = [] + + # custom gate + mygate = Gate("mygate", 1, []) + qc = QuantumCircuit(1) + qc.append(mygate, [0]) + with builder.build() as caldef: + builder.play(Constant(100, 0.1), DriveChannel(0)) + qc.add_calibration(mygate, (0,), caldef) + circuits.append(qc) + # override instruction + qc = QuantumCircuit(1) + qc.x(0) + with builder.build() as caldef: + builder.play(Constant(100, 0.1), DriveChannel(0)) + qc.add_calibration("x", (0,), caldef) + circuits.append(qc) + + return circuits + + def generate_controlled_gates(): """Test QPY serialization with custom ControlledGates.""" circuits = [] @@ -444,6 +523,8 @@ def generate_circuits(version_str=None): output_circuits["control_flow.qpy"] = generate_control_flow_circuits() if version_parts >= (0, 21, 0): output_circuits["controlled_gates.qpy"] = generate_controlled_gates() + output_circuits["schedule_blocks.qpy"] = generate_schedule_blocks() + output_circuits["pulse_gates.qpy"] = generate_calibrated_circuits() return output_circuits