diff --git a/docs/apidocs/qpy.rst b/docs/apidocs/qpy.rst index 524d2488c031..0bb00a640357 100644 --- a/docs/apidocs/qpy.rst +++ b/docs/apidocs/qpy.rst @@ -1,6 +1,6 @@ -.. _qiskit-circuit-qpy_serialization: +.. _qiskit-qpy: -.. automodule:: qiskit.circuit.qpy_serialization +.. automodule:: qiskit.qpy :no-members: :no-inherited-members: :no-special-members: diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index e6134050df93..d20acbc51b15 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -10,1913 +10,18 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# pylint: disable=invalid-name,too-many-boolean-expressions +# pylint: disable=wrong-import-position, unused-import -""" -########################################################### -QPY serialization (:mod:`qiskit.circuit.qpy_serialization`) -########################################################### +"""Alias for Qiskit QPY import.""" -.. currentmodule:: qiskit.circuit.qpy_serialization +# TODO deprecate this in 0.21.0 +from qiskit.qpy import dump, load -********* -Using QPY -********* - -Using QPY is defined to be straightforward and mirror the user API of the -serializers in Python's standard library, ``pickle`` and ``json``. There are -2 user facing functions: :func:`qiskit.circuit.qpy_serialization.dump` and -:func:`qiskit.circuit.qpy_serialization.load` which are used to dump QPY data -to a file object and load circuits from QPY data in a file object respectively. -For example:: - - from qiskit.circuit import QuantumCircuit - from qiskit.circuit import qpy_serialization - - qc = QuantumCircuit(2, name='Bell', metadata={'test': True}) - qc.h(0) - qc.cx(0, 1) - qc.measure_all() - - with open('bell.qpy', 'wb') as fd: - qpy_serialization.dump(qc, fd) - - with open('bell.qpy', 'rb') as fd: - new_qc = qpy_serialization.load(fd)[0] - -API documentation -================= - -.. autosummary:: - :toctree: ../stubs/ - - load - dump - -QPY Compatibility -================= - -The QPY format is designed to be backwards compatible moving forward. This means -you should be able to load a QPY with any newer Qiskit version than the one -that generated it. However, loading a QPY file with an older Qiskit version is -not supported and may not work. - -For example, if you generated a QPY file using qiskit-terra 0.18.1 you could -load that QPY file with qiskit-terra 0.19.0 and a hypothetical qiskit-terra -0.29.0. However, loading that QPY file with 0.18.0 is not supported and may not -work. - -********** -QPY Format -********** - -The QPY serialization format is a portable cross-platform binary -serialization format for :class:`~qiskit.circuit.QuantumCircuit` objects in Qiskit. The basic -file format is as follows: - -A QPY file (or memory object) always starts with the following 7 -byte UTF8 string: ``QISKIT`` which is immediately followed by the overall -file header. The contents of the file header as defined as a C struct are: - -.. code-block:: c - - struct { - uint8_t qpy_version; - uint8_t qiskit_major_version; - uint8_t qiskit_minor_version; - uint8_t qiskit_patch_version; - uint64_t num_circuits; - } - -All values use network byte order [#f1]_ (big endian) for cross platform -compatibility. - -The file header is immediately followed by the circuit payloads. -Each individual circuit is composed of the following parts: - -``HEADER | METADATA | REGISTERS | CUSTOM_DEFINITIONS | INSTRUCTIONS`` - -There is a circuit payload for each circuit (where the total number is dictated -by ``num_circuits`` in the file header). There is no padding between the -circuits in the data. - -.. _qpy_version_4: - -Version 4 -========= - -Version 4 is identical to :ref:`qpy_version_3` except that it adds 2 new type strings -to the INSTRUCTION_PARAM struct, ``z`` to represent ``None`` (which is encoded as -no data), ``q`` to represent a :class:`.QuantumCircuit` (which is encoded as -a QPY circuit), ``r`` to represent a ``range`` of integers (which is encoded as -a :ref:`qpy_range_pack`), and ``t`` to represent a ``tuple`` (which is encoded as -defined by :ref:`qpy_tuple`). Additionally, version 4 changes the type of register -index mapping array from ``uint32_t`` to ``int64_t``. If the values of any of the -array elements are negative they represent a register bit that is not present in the -circuit. - -The :ref:`qpy_registers` header format has also been updated to - -.. code-block:: c - - struct { - char type; - _Bool standalone; - uint32_t size; - uint16_t name_size; - _bool in_circuit; - } - -which just adds the ``in_circuit`` field which represents whether the register is -part of the circuit or not. - -.. _qpy_range_pack: - -RANGE ------ - -A RANGE is a representation of a ``range`` object. It is defined as: - -.. code-block:: c - - struct { - int64_t start; - int64_t stop; - int64_t step; - } - -.. _qpy_tuple: - -TUPLE ------ - -A TUPLE is a reprentation of a ``tuple`` object. As tuples are just fixed length -containers of arbitrary python objects their QPY can't fully represent any tuple, -but as long as the contents in a tuple are other QPY serializable types for -the INSTRUCTION_PARAM payload the ``tuple`` object can be serialized. - -A tuple instruction parameter starts with a header defined as: - -.. code-block:: c - - struct { - uint64_t size; - } - -followed by ``size`` elements that are INSTRUCTION_PARAM payloads, where each of -these define an element in the tuple. - -.. _qpy_version_3: - -Version 3 -========= - -Version 3 of the QPY format is identical to :ref:`qpy_version_2` except that it defines -a struct format to represent a :class:`~qiskit.circuit.library.PauliEvolutionGate` -natively in QPY. To accomplish this the :ref:`qpy_custom_definition` struct now supports -a new type value ``'p'`` to represent a :class:`~qiskit.circuit.library.PauliEvolutionGate`. -Enties in the custom instructions tables have unique name generated that start with the -string ``"###PauliEvolutionGate_"`` followed by a uuid string. This gate name is reservered -in QPY and if you have a custom :class:`~qiskit.circuit.Instruction` object with a definition -set and that name prefix it will error. If it's of type ``'p'`` the data payload is defined -as follows: - -.. _pauli_evo_qpy: - -PAULI_EVOLUTION ---------------- - -This represents the high level :class:`~qiskit.circuit.library.PauliEvolutionGate` - -.. code-block:: c - - struct { - uint64_t operator_count; - _Bool standalone_op; - char time_type; - uint64_t time_size; - uint64_t synthesis_size; - } - -This is immediately followed by ``operator_count`` elements defined by the :ref:`qpy_pauli_sum_op` -payload. Following that we have ``time_size`` bytes representing the ``time`` attribute. If -``standalone_op`` is ``True`` then there must only be a single operator. The -encoding of these bytes is determined by the value of ``time_type``. Possible values of -``time_type`` are ``'f'``, ``'p'``, and ``'e'``. If ``time_type`` is ``'f'`` it's a double, -``'p'`` defines a :class:`~qiskit.circuit.Parameter` object which is represented by a -:ref:`qpy_param_struct`, ``e`` defines a :class:`~qiskit.circuit.ParameterExpression` object -(that's not a :class:`~qiskit.circuit.Parameter`) which is represented by a :ref:`qpy_param_expr`. -Following that is ``synthesis_size`` bytes which is a utf8 encoded json payload representing -the :class:`.EvolutionSynthesis` class used by the gate. - -.. _qpy_pauli_sum_op: - -SPARSE_PAULI_OP_LIST_ELEM -------------------------- - -This represents an instance of :class:`.PauliSumOp`. - - -.. code-block:: c - - struct { - uint32_t pauli_op_size; - } - -which is immediately followed by ``pauli_op_size`` bytes which are .npy format [#f2]_ -data which represents the :class:`~qiskit.quantum_info.SparsePauliOp`. - -Version 3 of the QPY format also defines a struct format to represent a -:class:`~qiskit.circuit.ParameterVectorElement` as a distinct subclass from -a :class:`~qiskit.circuit.Parameter`. This adds a new parameter type char ``'v'`` -to represent a :class:`~qiskit.circuit.ParameterVectorElement` which is now -supported as a type string value for an INSTRUCTION_PARAM. The payload for these -parameters are defined below as :ref:`qpy_param_vector`. - -.. _qpy_param_vector: - - -PARAMETER_VECTOR_ELEMENT ------------------------- - -A PARAMETER_VECTOR_ELEMENT represents a :class:`~qiskit.circuit.ParameterVectorElement` -object the data for a INSTRUCTION_PARAM. The contents of the PARAMETER_VECTOR_ELEMENT are -defined as: - -.. code-block:: c - - struct { - uint16_t vector_name_size; - uint64_t vector_size; - char uuid[16]; - uint64_t index; - } - -which is immediately followed by ``vector_name_size`` utf8 bytes representing -the parameter's vector name. - -.. _qpy_param_expr_v3: - - -PARAMETER_EXPR --------------- - -Additionally, since QPY format version v3 distinguishes between a -:class:`~qiskit.circuit.Parameter` and :class:`~qiskit.circuit.ParameterVectorElement` -the payload for a :class:`~qiskit.circuit.ParameterExpression` needs to be updated -to distinguish between the types. The following is the modified payload format -which is mostly identical to the format in Version 1 and :ref:`qpy_version_2` but just -modifies the ``map_elements`` struct to include a symbol type field. - -A PARAMETER_EXPR represents a :class:`~qiskit.circuit.ParameterExpression` -object that the data for an INSTRUCTION_PARAM. The contents of a PARAMETER_EXPR -are defined as: - -.. code-block:: c - - struct { - uint64_t map_elements; - uint64_t expr_size; - } - -Immediately following the header is ``expr_size`` bytes of utf8 data containing -the expression string, which is the sympy srepr of the expression for the -parameter expression. Following that is a symbol map which contains -``map_elements`` elements with the format - -.. code-block:: c - - struct { - char symbol_type; - char type; - uint64_t size; - } - -The ``symbol_type`` key determines the payload type of the symbol representation -for the element. If it's ``p`` it represents a :class:`~qiskit.circuit.Parameter` -and if it's ``v`` it represents a :class:`~qiskit.circuit.ParameterVectorElement`. -The map element struct is immediately followed by the symbol map key payload, if -``symbol_type`` is ``p`` then it is followed immediately by a :ref:`qpy_param_struct` -object (both the struct and utf8 name bytes) and if ``symbol_type`` is ``v`` -then the struct is imediately followed by :ref:`qpy_param_vector` (both the struct -and utf8 name bytes). That is followed by ``size`` bytes for the -data of the symbol. The data format is dependent on the value of ``type``. If -``type`` is ``p`` then it represents a :class:`~qiskit.circuit.Parameter` and -size will be 0, the value will just be the same as the key. Similarly if the -``type`` is ``v`` then it represents a :class:`~qiskit.circuit.ParameterVectorElement` -and size will be 0 as the value will just be the same as the key. If -``type`` is ``f`` then it represents a double precision float. If ``type`` is -``c`` it represents a double precision complex, which is represented by the -:ref:`qpy_complex`. Finally, if type is ``i`` it represents an integer which is an -``int64_t``. - -.. _qpy_version_2: - -Version 2 -========= - -Version 2 of the QPY format is identical to version 1 except for the HEADER -section is slightly different. You can refer to the :ref:`qpy_version_1` section -for the details on the rest of the payload format. - -HEADER ------- - -The contents of HEADER are defined as a C struct are: - -.. code-block:: c - - struct { - uint16_t name_size; - char global_phase_type; - uint16_t global_phase_size; - uint32_t num_qubits; - uint32_t num_clbits; - uint64_t metadata_size; - uint32_t num_registers; - uint64_t num_instructions; - uint64_t num_custom_gates; - } - -This is immediately followed by ``name_size`` bytes of utf8 data for the name -of the circuit. Following this is immediately ``global_phase_size`` bytes -representing the global phase. The content of that data is dictated by the -value of ``global_phase_type``. If it's ``'f'`` the data is a float and is the -size of a ``double``. If it's ``'p'`` defines a :class:`~qiskit.circuit.Parameter` -object which is represented by a PARAM struct (see below), ``e`` defines a -:class:`~qiskit.circuit.ParameterExpression` object (that's not a -:class:`~qiskit.circuit.Parameter`) which is represented by a PARAM_EXPR struct -(see below). - -.. _qpy_version_1: - -Version 1 -========= - -HEADER ------- - -The contents of HEADER as defined as a C struct are: - -.. code-block:: c - - struct { - uint16_t name_size; - double global_phase; - uint32_t num_qubits; - uint32_t num_clbits; - uint64_t metadata_size; - uint32_t num_registers; - uint64_t num_instructions; - uint64_t num_custom_gates; - } - -This is immediately followed by ``name_size`` bytes of utf8 data for the name -of the circuit. - -METADATA --------- - -The METADATA field is a UTF8 encoded JSON string. After reading the HEADER -(which is a fixed size at the start of the QPY file) and the ``name`` string -you then read the ``metadata_size`` number of bytes and parse the JSON to get -the metadata for the circuit. - -.. _qpy_registers: - -REGISTERS ---------- - -The contents of REGISTERS is a number of REGISTER object. If num_registers is -> 0 then after reading METADATA you read that number of REGISTER structs defined -as: - -.. code-block:: c - - struct { - char type; - _Bool standalone; - uint32_t size; - uint16_t name_size; - } - -``type`` can be ``'q'`` or ``'c'``. - -Immediately following the REGISTER struct is the utf8 encoded register name of -size ``name_size``. After the ``name`` utf8 bytes there is then an array of -int64_t values of size ``size`` that contains a map of the register's index to -the circuit's qubit index. For example, array element 0's value is the index -of the ``register[0]``'s position in the containing circuit's qubits list. - -.. note:: - - Prior to QPY :ref:`qpy_version_4` the type of array elements was uint32_t. This was changed - to enable negative values which represent bits in the array not present in the - circuit - -The standalone boolean determines whether the register is constructed as a -standalone register that was added to the circuit or was created from existing -bits. A register is considered standalone if it has bits constructed solely -as part of it, for example:: - - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - -the register ``qr`` would be a standalone register. While something like:: - - bits = [Qubit(), Qubit()] - qr = QuantumRegister(bits=bits) - qc = QuantumCircuit(bits=bits) - -``qr`` would have ``standalone`` set to ``False``. - - -.. _qpy_custom_definition: - -CUSTOM_DEFINITIONS ------------------- - -This section specifies custom definitions for any of the instructions in the circuit. - -CUSTOM_DEFINITION_HEADER contents are defined as: - -.. code-block:: c - - struct { - uint64_t size; - } - -If size is greater than 0 that means the circuit contains custom instruction(s). -Each custom instruction is defined with a CUSTOM_INSTRUCTION block defined as: - -.. code-block:: c - - struct { - uint16_t name_size; - char type; - _Bool custom_definition; - uint64_t size; - } - -Immediately following the CUSTOM_INSTRUCTION struct is the utf8 encoded name -of size ``name_size``. - -If ``custom_definition`` is ``True`` that means that the immediately following -``size`` bytes contains a QPY circuit data which can be used for the custom -definition of that gate. If ``custom_definition`` is ``False`` then the -instruction can be considered opaque (ie no definition). The ``type`` field -determines what type of object will get created with the custom definition. -If it's ``'g'`` it will be a :class:`~qiskit.circuit.Gate` object, ``'i'`` -it will be a :class:`~qiskit.circuit.Instruction` object. - -INSTRUCTIONS ------------- - -The contents of INSTRUCTIONS is a list of INSTRUCTION metadata objects - -.. code-block:: c - - struct { - uint16_t name_size; - uint16_t label_size; - uint16_t num_parameters; - uint32_t num_qargs; - uint32_t num_cargs; - _Bool has_conditional; - uint16_t conditional_reg_name_size; - int64_t conditional_value; - } - -This metadata object is immediately followed by ``name_size`` bytes of utf8 bytes -for the ``name``. ``name`` here is the Qiskit class name for the Instruction -class if it's defined in Qiskit. Otherwise it falls back to the custom -instruction name. Following the ``name`` bytes there are ``label_size`` bytes of -utf8 data for the label if one was set on the instruction. Following the label -bytes if ``has_conditional`` is ``True`` then there are -``conditional_reg_name_size`` bytes of utf8 data for the name of the conditional -register name. In case of single classical bit conditions the register name -utf8 data will be prefixed with a null character "\\x00" and then a utf8 string -integer representing the classical bit index in the circuit that the condition -is on. - -This is immediately followed by the INSTRUCTION_ARG structs for the list of -arguments of that instruction. These are in the order of all quantum arguments -(there are num_qargs of these) followed by all classical arguments (num_cargs -of these). - -The contents of each INSTRUCTION_ARG is: - -.. code-block:: c - - struct { - char type; - uint32_t index; - } - -``type`` can be ``'q'`` or ``'c'``. - -After all arguments for an instruction the parameters are specified with -``num_parameters`` INSTRUCTION_PARAM structs. - -The contents of each INSTRUCTION_PARAM is: - -.. code-block:: c - - struct { - char type; - uint64_t size; - } - -After each INSTRUCTION_PARAM the next ``size`` bytes are the parameter's data. -The ``type`` field can be ``'i'``, ``'f'``, ``'p'``, ``'e'``, ``'s'``, ``'c'`` -or ``'n'`` which dictate the format. For ``'i'`` it's an integer, ``'f'`` it's -a double, ``'s'`` if it's a string (encoded as utf8), ``'c'`` is a complex and -the data is represented by the struct format in the :ref:`qpy_param_expr` section. -``'p'`` defines a :class:`~qiskit.circuit.Parameter` object which is -represented by a :ref:`qpy_param_struct` struct, ``e`` defines a -:class:`~qiskit.circuit.ParameterExpression` object (that's not a -:class:`~qiskit.circuit.Parameter`) which is represented by a :ref:`qpy_param_expr` -struct (on QPY format :ref:`qpy_version_3` the format is tweak slightly see: -:ref:`qpy_param_expr_v3`), ``'n'`` represents an object from numpy (either an -``ndarray`` or a numpy type) which means the data is .npy format [#f2]_ data, -and in QPY :ref:`qpy_version_3` ``'v'`` represents a -:class:`~qiskit.circuit.ParameterVectorElement` which is represented by a -:ref:`qpy_param_vector` struct. - -.. _qpy_param_struct: - -PARAMETER ---------- - -A PARAMETER represents a :class:`~qiskit.circuit.Parameter` object the data for -a INSTRUCTION_PARAM. The contents of the PARAMETER are defined as: - -.. code-block:: c - - struct { - uint16_t name_size; - char uuid[16]; - } - -which is immediately followed by ``name_size`` utf8 bytes representing the -parameter name. - -.. _qpy_param_expr: - -PARAMETER_EXPR --------------- - -A PARAMETER_EXPR represents a :class:`~qiskit.circuit.ParameterExpression` -object that the data for an INSTRUCTION_PARAM. The contents of a PARAMETER_EXPR -are defined as: - -The PARAMETER_EXPR data starts with a header: - -.. code-block:: c - - struct { - uint64_t map_elements; - uint64_t expr_size; - } - -Immediately following the header is ``expr_size`` bytes of utf8 data containing -the expression string, which is the sympy srepr of the expression for the -parameter expression. Follwing that is a symbol map which contains -``map_elements`` elements with the format - -.. code-block:: c - - struct { - char type; - uint64_t size; - } - -Which is followed immediately by ``PARAMETER`` object (both the struct and utf8 -name bytes) for the symbol map key. That is followed by ``size`` bytes for the -data of the symbol. The data format is dependent on the value of ``type``. If -``type`` is ``p`` then it represents a :class:`~qiskit.circuit.Parameter` and -size will be 0, the value will just be the same as the key. If -``type`` is ``f`` then it represents a double precision float. If ``type`` is -``c`` it represents a double precision complex, which is represented by :ref:`qpy_complex`. -Finally, if type is ``i`` it represents an integer which is an ``int64_t``. - -.. _qpy_complex: - -COMPLEX -------- - -When representing a double precision complex value in QPY the following -struct is used: - - -.. code-block:: c - - struct { - double real; - double imag; - } - -this matches the internal C representation of Python's complex type. [#f3]_ - - -.. [#f1] https://tools.ietf.org/html/rfc1700 -.. [#f2] https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html -.. [#f3] https://docs.python.org/3/c-api/complex.html#c.Py_complex -""" -from collections import namedtuple -import io -import json -import struct -import uuid -import warnings - -import numpy as np - -from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.circuit.quantumregister import QuantumRegister, Qubit -from qiskit.circuit.classicalregister import ClassicalRegister, Clbit -from qiskit.circuit.parameter import Parameter -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement -from qiskit.circuit.gate import Gate -from qiskit.circuit.instruction import Instruction -from qiskit.circuit import library -from qiskit import circuit as circuit_mod -from qiskit import extensions -from qiskit.circuit import controlflow -from qiskit.extensions import quantum_initializer -from qiskit.version import __version__ -from qiskit.exceptions import QiskitError -from qiskit.quantum_info.operators import SparsePauliOp -from qiskit.synthesis import evolution as evo_synth -from qiskit.utils import optionals as _optionals - - -# v1 Binary Format -# ---------------- -# FILE_HEADER -FILE_HEADER = namedtuple( - "FILE_HEADER", - ["preface", "qpy_version", "major_version", "minor_version", "patch_version", "num_circuits"], +# For backward compatibility. Provide, Runtime, Experiment call these private functions. +from qiskit.qpy import ( + _write_instruction, + _read_instruction, + _write_parameter_expression, + _read_parameter_expression, + _read_parameter_expression_v3, ) -FILE_HEADER_PACK = "!6sBBBBQ" -FILE_HEADER_SIZE = struct.calcsize(FILE_HEADER_PACK) - -# HEADER binary format -HEADER_V2 = namedtuple( - "HEADER", - [ - "name_size", - "global_phase_type", - "global_phase_size", - "num_qubits", - "num_clbits", - "metadata_size", - "num_registers", - "num_instructions", - ], -) -HEADER_V2_PACK = "!H1cHIIQIQ" -HEADER_V2_SIZE = struct.calcsize(HEADER_V2_PACK) - -HEADER = namedtuple( - "HEADER", - [ - "name_size", - "global_phase", - "num_qubits", - "num_clbits", - "metadata_size", - "num_registers", - "num_instructions", - ], -) -HEADER_PACK = "!HdIIQIQ" -HEADER_SIZE = struct.calcsize(HEADER_PACK) - -# CUSTOM_DEFINITIONS -# CUSTOM DEFINITION HEADER -CUSTOM_DEFINITION_HEADER = namedtuple("CUSTOM_DEFINITION_HEADER", ["size"]) -CUSTOM_DEFINITION_HEADER_PACK = "!Q" -CUSTOM_DEFINITION_HEADER_SIZE = struct.calcsize(CUSTOM_DEFINITION_HEADER_PACK) - -# CUSTOM_DEFINITION -CUSTOM_DEFINITION = namedtuple( - "CUSTOM_DEFINITON", - ["gate_name_size", "type", "num_qubits", "num_clbits", "custom_definition", "size"], -) -CUSTOM_DEFINITION_PACK = "!H1cII?Q" -CUSTOM_DEFINITION_SIZE = struct.calcsize(CUSTOM_DEFINITION_PACK) - - -# REGISTER binary format -REGISTER_V4 = namedtuple("REGISTER", ["type", "standalone", "size", "name_size", "in_circuit"]) -REGISTER_V4_PACK = "!1c?IH?" -REGISTER_V4_SIZE = struct.calcsize(REGISTER_V4_PACK) -REGISTER = namedtuple("REGISTER", ["type", "standalone", "size", "name_size"]) -REGISTER_PACK = "!1c?IH" -REGISTER_SIZE = struct.calcsize(REGISTER_PACK) - - -# INSTRUCTION binary format -INSTRUCTION = namedtuple( - "INSTRUCTION", - [ - "name_size", - "label_size", - "num_parameters", - "num_qargs", - "num_cargs", - "has_condition", - "condition_register_size", - "value", - ], -) -INSTRUCTION_PACK = "!HHHII?Hq" -INSTRUCTION_SIZE = struct.calcsize(INSTRUCTION_PACK) -# Instruction argument format -INSTRUCTION_ARG = namedtuple("INSTRUCTION_ARG", ["type", "size"]) -INSTRUCTION_ARG_PACK = "!1cI" -INSTRUCTION_ARG_SIZE = struct.calcsize(INSTRUCTION_ARG_PACK) -# INSTRUCTION parameter format -INSTRUCTION_PARAM = namedtuple("INSTRUCTION_PARAM", ["type", "size"]) -INSTRUCTION_PARAM_PACK = "!1cQ" -INSTRUCTION_PARAM_SIZE = struct.calcsize(INSTRUCTION_PARAM_PACK) -# PARAMETER -PARAMETER = namedtuple("PARAMETER", ["name_size", "uuid"]) -PARAMETER_PACK = "!H16s" -PARAMETER_SIZE = struct.calcsize(PARAMETER_PACK) -# PARAMETER_EXPR -PARAMETER_EXPR = namedtuple("PARAMETER_EXPR", ["map_elements", "expr_size"]) -PARAMETER_EXPR_PACK = "!QQ" -PARAMETER_EXPR_SIZE = struct.calcsize(PARAMETER_EXPR_PACK) -# PARAMETER_EXPR_MAP_ELEM -PARAM_EXPR_MAP_ELEM = namedtuple("PARAMETER_EXPR_MAP_ELEM", ["type", "size"]) -PARAM_EXPR_MAP_ELEM_PACK = "!cQ" -PARAM_EXPR_MAP_ELEM_SIZE = struct.calcsize(PARAM_EXPR_MAP_ELEM_PACK) -# PARAMETER_EXPR_MAP_ELEM_V3 -PARAM_EXPR_MAP_ELEM_V3 = namedtuple("PARAMETER_EXPR_MAP_ELEM", ["symbol_type", "type", "size"]) -PARAM_EXPR_MAP_ELEM_PACK_V3 = "!ccQ" -PARAM_EXPR_MAP_ELEM_SIZE_V3 = struct.calcsize(PARAM_EXPR_MAP_ELEM_PACK_V3) -# Complex -COMPLEX = namedtuple("COMPLEX", ["real", "imag"]) -COMPLEX_PACK = "!dd" -COMPLEX_SIZE = struct.calcsize(COMPLEX_PACK) -# PARAMETER_VECTOR_ELEMENT -PARAMETER_VECTOR_ELEMENT = namedtuple( - "PARAMETER_VECTOR_ELEMENT", ["vector_name_size", "vector_size", "uuid", "index"] -) -PARAMETER_VECTOR_ELEMENT_PACK = "!HQ16sQ" -PARAMETER_VECTOR_ELEMENT_SIZE = struct.calcsize(PARAMETER_VECTOR_ELEMENT_PACK) -# Pauli Evolution Gate -PAULI_EVOLUTION_DEF = namedtuple( - "PAULI_EVOLUTION_DEF", - ["operator_size", "standalone_op", "time_type", "time_size", "synth_method_size"], -) -PAULI_EVOLUTION_DEF_PACK = "!Q?1cQQ" -PAULI_EVOLUTION_DEF_SIZE = struct.calcsize(PAULI_EVOLUTION_DEF_PACK) -# SparsePauliOp List -SPARSE_PAULI_OP_LIST_ELEM = namedtuple("SPARSE_PAULI_OP_LIST_ELEMENT", ["size"]) -SPARSE_PAULI_OP_LIST_ELEM_PACK = "!Q" -SPARSE_PAULI_OP_LIST_ELEM_SIZE = struct.calcsize(SPARSE_PAULI_OP_LIST_ELEM_PACK) -# Range -RANGE = namedtuple("RANGE", ["start", "stop", "step"]) -RANGE_PACK = "!qqq" -RANGE_PACK_SIZE = struct.calcsize(RANGE_PACK) -# Tuple -TUPLE = namedtuple("TUPLE", ["num_elements"]) -TUPLE_PACK = "!Q" -TUPLE_PACK_SIZE = struct.calcsize(TUPLE_PACK) - - -def _read_header_v2(file_obj, version, vectors): - header_raw = struct.unpack(HEADER_V2_PACK, file_obj.read(HEADER_V2_SIZE)) - header_tuple = HEADER_V2._make(header_raw) - name = file_obj.read(header_tuple[0]).decode("utf8") - global_phase_type_str = header_tuple[1].decode("utf8") - data = file_obj.read(header_tuple[2]) - if global_phase_type_str == "f": - global_phase = struct.unpack("!d", data)[0] - elif global_phase_type_str == "i": - global_phase = struct.unpack("!q", data)[0] - elif global_phase_type_str == "p": - with io.BytesIO(data) as container: - global_phase = _read_parameter(container) - elif global_phase_type_str == "v": - with io.BytesIO(data) as container: - global_phase = _read_parameter_vec(container, vectors) - elif global_phase_type_str == "e": - with io.BytesIO(data) as container: - if version < 3: - global_phase = _read_parameter_expression(container) - else: - global_phase = _read_parameter_expression_v3(container, vectors) - else: - raise TypeError("Invalid global phase type: %s" % global_phase_type_str) - header = { - "global_phase": global_phase, - "num_qubits": header_tuple[3], - "num_clbits": header_tuple[4], - "num_registers": header_tuple[6], - "num_instructions": header_tuple[7], - } - metadata_raw = file_obj.read(header_tuple[5]) - metadata = json.loads(metadata_raw) - return header, name, metadata - - -def _read_header(file_obj): - header_raw = struct.unpack(HEADER_PACK, file_obj.read(HEADER_SIZE)) - header_tuple = HEADER._make(header_raw) - name = file_obj.read(header_tuple[0]).decode("utf8") - header = { - "global_phase": header_tuple[1], - "num_qubits": header_tuple[2], - "num_clbits": header_tuple[3], - "num_registers": header_tuple[5], - "num_instructions": header_tuple[6], - } - metadata_raw = file_obj.read(header_tuple[4]) - metadata = json.loads(metadata_raw) - return header, name, metadata - - -def _read_registers(file_obj, num_registers): - registers = {"q": {}, "c": {}} - for _reg in range(num_registers): - register_raw = file_obj.read(REGISTER_SIZE) - register = struct.unpack(REGISTER_PACK, register_raw) - name = file_obj.read(register[3]).decode("utf8") - standalone = register[1] - REGISTER_ARRAY_PACK = "!%sI" % register[2] - bit_indices_raw = file_obj.read(struct.calcsize(REGISTER_ARRAY_PACK)) - bit_indices = list(struct.unpack(REGISTER_ARRAY_PACK, bit_indices_raw)) - if register[0].decode("utf8") == "q": - registers["q"][name] = (standalone, bit_indices, True) - else: - registers["c"][name] = (standalone, bit_indices, True) - return registers - - -def _read_registers_v4(file_obj, num_registers): - registers = {"q": {}, "c": {}} - for _reg in range(num_registers): - register_raw = file_obj.read(REGISTER_V4_SIZE) - register = struct.unpack(REGISTER_V4_PACK, register_raw) - name = file_obj.read(register[3]).decode("utf8") - standalone = register[1] - REGISTER_ARRAY_PACK = "!%sq" % register[2] - in_circuit = register[4] - bit_indices_raw = file_obj.read(struct.calcsize(REGISTER_ARRAY_PACK)) - bit_indices = list(struct.unpack(REGISTER_ARRAY_PACK, bit_indices_raw)) - if register[0].decode("utf8") == "q": - registers["q"][name] = (standalone, bit_indices, in_circuit) - else: - registers["c"][name] = (standalone, bit_indices, in_circuit) - return registers - - -def _read_parameter(file_obj): - param_raw = struct.unpack(PARAMETER_PACK, file_obj.read(PARAMETER_SIZE)) - name_size = param_raw[0] - param_uuid = uuid.UUID(bytes=param_raw[1]) - name = file_obj.read(name_size).decode("utf8") - param = Parameter.__new__(Parameter, name, uuid=param_uuid) - param.__init__(name) - return param - - -def _read_parameter_vec(file_obj, vectors): - param_raw = struct.unpack( - PARAMETER_VECTOR_ELEMENT_PACK, file_obj.read(PARAMETER_VECTOR_ELEMENT_SIZE) - ) - vec_name_size = param_raw[0] - param_uuid = uuid.UUID(bytes=param_raw[2]) - param_index = param_raw[3] - name = file_obj.read(vec_name_size).decode("utf8") - if name not in vectors: - vectors[name] = (ParameterVector(name, param_raw[1]), set()) - vector = vectors[name][0] - if vector[param_index]._uuid != param_uuid: - vectors[name][1].add(param_index) - vector._params[param_index] = ParameterVectorElement.__new__( - ParameterVectorElement, vector, param_index, uuid=param_uuid - ) - vector._params[param_index].__init__(vector, param_index) - return vector[param_index] - - -def _read_parameter_expression_v3(file_obj, vectors): - param_expr_raw = struct.unpack(PARAMETER_EXPR_PACK, file_obj.read(PARAMETER_EXPR_SIZE)) - map_elements = param_expr_raw[0] - from sympy.parsing.sympy_parser import parse_expr - - if _optionals.HAS_SYMENGINE: - import symengine - - expr = symengine.sympify(parse_expr(file_obj.read(param_expr_raw[1]).decode("utf8"))) - else: - expr = parse_expr(file_obj.read(param_expr_raw[1]).decode("utf8")) - symbol_map = {} - for _ in range(map_elements): - elem_raw = file_obj.read(PARAM_EXPR_MAP_ELEM_SIZE_V3) - elem = struct.unpack(PARAM_EXPR_MAP_ELEM_PACK_V3, elem_raw) - symbol_type = elem[0].decode("utf8") - if symbol_type == "p": - param = _read_parameter(file_obj) - elif symbol_type == "v": - param = _read_parameter_vec(file_obj, vectors) - elem_type = elem[1].decode("utf8") - elem_data = file_obj.read(elem[2]) - if elem_type == "f": - value = struct.unpack("!d", elem_data) - elif elem_type == "i": - value = struct.unpack("!q", elem_data) - elif elem_type == "c": - value = complex(*struct.unpack(COMPLEX_PACK, elem_data)) - elif elem_type in ("p", "v"): - value = param._symbol_expr - elif elem_type == "e": - value = _read_parameter_expression_v3(io.BytesIO(elem_data), vectors) - else: - raise TypeError("Invalid parameter expression map type: %s" % elem_type) - symbol_map[param] = value - return ParameterExpression(symbol_map, expr) - - -def _read_parameter_expression(file_obj): - param_expr_raw = struct.unpack(PARAMETER_EXPR_PACK, file_obj.read(PARAMETER_EXPR_SIZE)) - map_elements = param_expr_raw[0] - from sympy.parsing.sympy_parser import parse_expr - - if _optionals.HAS_SYMENGINE: - import symengine - - expr = symengine.sympify(parse_expr(file_obj.read(param_expr_raw[1]).decode("utf8"))) - else: - expr = parse_expr(file_obj.read(param_expr_raw[1]).decode("utf8")) - symbol_map = {} - for _ in range(map_elements): - elem_raw = file_obj.read(PARAM_EXPR_MAP_ELEM_SIZE) - elem = struct.unpack(PARAM_EXPR_MAP_ELEM_PACK, elem_raw) - param = _read_parameter(file_obj) - elem_type = elem[0].decode("utf8") - elem_data = file_obj.read(elem[1]) - if elem_type == "f": - value = struct.unpack("!d", elem_data) - elif elem_type == "i": - value = struct.unpack("!q", elem_data) - elif elem_type == "c": - value = complex(*struct.unpack(COMPLEX_PACK, elem_data)) - elif elem_type == "p": - value = param._symbol_expr - elif elem_type == "e": - value = _read_parameter_expression(io.BytesIO(elem_data)) - else: - raise TypeError("Invalid parameter expression map type: %s" % elem_type) - symbol_map[param] = value - return ParameterExpression(symbol_map, expr) - - -def _read_instruction_param(file_obj, version, vectors): - param_raw = file_obj.read(INSTRUCTION_PARAM_SIZE) - param = struct.unpack(INSTRUCTION_PARAM_PACK, param_raw) - data = file_obj.read(param[1]) - type_str = param[0].decode("utf8") - param = None - if type_str == "i": - param = struct.unpack(" 0: - inst_obj.label = label - circuit._append(inst_obj, qargs, cargs) - return - elif gate_name in custom_instructions: - inst_obj = _parse_custom_instruction(custom_instructions, gate_name, params) - inst_obj.condition = condition_tuple - if label_size > 0: - inst_obj.label = label - circuit._append(inst_obj, qargs, cargs) - return - elif hasattr(library, gate_name): - gate_class = getattr(library, gate_name) - elif hasattr(circuit_mod, gate_name): - gate_class = getattr(circuit_mod, gate_name) - elif hasattr(extensions, gate_name): - gate_class = getattr(extensions, gate_name) - elif hasattr(quantum_initializer, gate_name): - gate_class = getattr(quantum_initializer, gate_name) - elif hasattr(controlflow, gate_name): - gate_class = getattr(controlflow, gate_name) - else: - raise AttributeError("Invalid instruction type: %s" % gate_name) - if gate_name in {"IfElseOp", "WhileLoopOp"}: - gate = gate_class(condition_tuple, *params) - else: - if gate_name == "Initialize": - gate = gate_class(params) - else: - if gate_name == "Barrier": - params = [len(qargs)] - elif gate_name in {"BreakLoopOp", "ContinueLoopOp"}: - params = [len(qargs), len(cargs)] - gate = gate_class(*params) - gate.condition = condition_tuple - if label_size > 0: - gate.label = label - if not isinstance(gate, Instruction): - circuit.append(gate, qargs, cargs) - else: - circuit._append(gate, qargs, cargs) - - -def _parse_custom_instruction(custom_instructions, gate_name, params): - (type_str, num_qubits, num_clbits, definition) = custom_instructions[gate_name] - if type_str == "i": - inst_obj = Instruction(gate_name, num_qubits, num_clbits, params) - if definition is not None: - inst_obj.definition = definition - elif type_str == "g": - inst_obj = Gate(gate_name, num_qubits, params) - inst_obj.definition = definition - elif type_str == "p": - inst_obj = definition - else: - raise ValueError("Invalid custom instruction type '%s'" % type_str) - return inst_obj - - -def _read_custom_instructions(file_obj, version, vectors): - custom_instructions = {} - custom_definition_header_raw = file_obj.read(CUSTOM_DEFINITION_HEADER_SIZE) - custom_definition_header = struct.unpack( - CUSTOM_DEFINITION_HEADER_PACK, custom_definition_header_raw - ) - if custom_definition_header[0] > 0: - for _ in range(custom_definition_header[0]): - custom_definition_raw = file_obj.read(CUSTOM_DEFINITION_SIZE) - custom_definition = struct.unpack(CUSTOM_DEFINITION_PACK, custom_definition_raw) - ( - name_size, - type_str, - num_qubits, - num_clbits, - has_custom_definition, - size, - ) = custom_definition - name = file_obj.read(name_size).decode("utf8") - type_str = type_str.decode("utf8") - definition_circuit = None - if has_custom_definition: - definition_buffer = io.BytesIO(file_obj.read(size)) - if version < 3 or not name.startswith(r"###PauliEvolutionGate_"): - definition_circuit = _read_circuit(definition_buffer, version) - elif name.startswith(r"###PauliEvolutionGate_"): - definition_circuit = _read_pauli_evolution_gate(definition_buffer, vectors) - custom_instructions[name] = (type_str, num_qubits, num_clbits, definition_circuit) - return custom_instructions - - -def _write_parameter(file_obj, param): - name_bytes = param._name.encode("utf8") - file_obj.write(struct.pack(PARAMETER_PACK, len(name_bytes), param._uuid.bytes)) - file_obj.write(name_bytes) - - -def _write_parameter_vec(file_obj, param): - name_bytes = param._vector._name.encode("utf8") - file_obj.write( - struct.pack( - PARAMETER_VECTOR_ELEMENT_PACK, - len(name_bytes), - param._vector._size, - param._uuid.bytes, - param._index, - ) - ) - file_obj.write(name_bytes) - - -def _write_parameter_expression(file_obj, param): - from sympy import srepr, sympify - - expr_bytes = srepr(sympify(param._symbol_expr)).encode("utf8") - param_expr_header_raw = struct.pack( - PARAMETER_EXPR_PACK, len(param._parameter_symbols), len(expr_bytes) - ) - file_obj.write(param_expr_header_raw) - file_obj.write(expr_bytes) - for parameter, value in param._parameter_symbols.items(): - with io.BytesIO() as parameter_container: - if isinstance(parameter, ParameterVectorElement): - symbol_type_str = "v" - _write_parameter_vec(parameter_container, parameter) - else: - symbol_type_str = "p" - _write_parameter(parameter_container, parameter) - parameter_data = parameter_container.getvalue() - if isinstance(value, float): - type_str = "f" - data = struct.pack("!d", value) - elif isinstance(value, complex): - type_str = "c" - data = struct.pack(COMPLEX_PACK, value.real, value.imag) - elif isinstance(value, int): - type_str = "i" - data = struct.pack("!q", value) - elif value == parameter._symbol_expr: - type_str = symbol_type_str - data = bytes() - elif isinstance(value, ParameterExpression): - type_str = "e" - container = io.BytesIO() - _write_parameter_expression(container, value) - container.seek(0) - data = container.read() - else: - raise TypeError(f"Invalid expression type in symbol map for {param}: {type(value)}") - - elem_header = struct.pack( - PARAM_EXPR_MAP_ELEM_PACK_V3, - symbol_type_str.encode("utf8"), - type_str.encode("utf8"), - len(data), - ) - file_obj.write(elem_header) - file_obj.write(parameter_data) - file_obj.write(data) - - -def _write_instruction_parameter(file_obj, param): - container = io.BytesIO() - if isinstance(param, int): - type_key = "i" - data = struct.pack("= 0: - processed_qubit_indices.add(bit_index) - bit_indices.append(bit_index) - buf.write(struct.pack(REGISTER_ARRAY_PACK, *bit_indices)) - - def process_cregs(buf, cregs, in_circuit=True): - for reg in cregs: - standalone = all(bit._register is reg for bit in reg) - reg_name = reg.name.encode("utf8") - buf.write( - struct.pack(REGISTER_V4_PACK, b"c", standalone, reg.size, len(reg_name), in_circuit) - ) - buf.write(reg_name) - REGISTER_ARRAY_PACK = "!%sq" % reg.size - bit_indices = [] - for bit in reg: - bit_index = clbit_indices.get(bit, -1) - # If index already in circuit treat it as non-existent (since it indicates dual - # register membership and the bit is already being reconstructed - if bit_index in processed_clbit_indices: - bit_index = -1 - if bit_index >= 0: - processed_clbit_indices.add(bit_index) - bit_indices.append(bit_index) - buf.write(struct.pack(REGISTER_ARRAY_PACK, *bit_indices)) - - with io.BytesIO() as reg_buf: - if num_registers > 0: - process_qregs(reg_buf, circuit.qregs) - process_cregs(reg_buf, circuit.cregs) - # process dangling registers (those not added to circuit) - qreg_set = set() - for bit in circuit.qubits: - if bit._register is not None and bit._register not in circuit.qregs: - qreg_set.add(bit._register) - process_qregs(reg_buf, qreg_set, in_circuit=False) - num_registers += len(qreg_set) - creg_set = set() - for bit in circuit.clbits: - if bit._register is not None and bit._register not in circuit.cregs: - creg_set.add(bit._register) - process_cregs(reg_buf, creg_set, in_circuit=False) - num_registers += len(creg_set) - # Write circuit header - header_raw = HEADER_V2( - name_size=len(circuit_name), - global_phase_type=global_phase_type, - global_phase_size=len(global_phase_data), - num_qubits=circuit.num_qubits, - num_clbits=circuit.num_clbits, - metadata_size=metadata_size, - num_registers=num_registers, - num_instructions=num_instructions, - ) - header = struct.pack(HEADER_V2_PACK, *header_raw) - file_obj.write(header) - file_obj.write(circuit_name) - file_obj.write(global_phase_data) - file_obj.write(metadata_raw) - # Write header payload - file_obj.write(reg_buf.getvalue()) - - instruction_buffer = io.BytesIO() - custom_instructions = {} - index_map = {} - index_map["q"] = qubit_indices - index_map["c"] = clbit_indices - for instruction in circuit.data: - _write_instruction(instruction_buffer, instruction, custom_instructions, index_map) - file_obj.write(struct.pack(CUSTOM_DEFINITION_HEADER_PACK, len(custom_instructions))) - - for name, instruction in custom_instructions.items(): - _write_custom_instruction(file_obj, name, instruction) - - instruction_buffer.seek(0) - file_obj.write(instruction_buffer.read()) - instruction_buffer.close() - - -def load(file_obj): - """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. - For example: - - .. code-block:: python - - from qiskit.circuit import qpy_serialization - - with open('bell.qpy', 'rb') as fd: - circuits = qpy_serialization.load(fd) - - or with a gzip compressed file: - - .. code-block:: python - - import gzip - from qiskit.circuit import qpy_serialization - - with gzip.open('bell.qpy.gz', 'rb') as fd: - circuits = qpy_serialization.load(fd) - - which will read the contents of the qpy and return a list of - :class:`~qiskit.circuit.QuantumCircuit` objects from the file. - - Args: - file_obj (File): A file like object that contains the QPY binary - data for a circuit - 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. - Raises: - QiskitError: if ``file_obj`` is not a valid QPY file - """ - file_header_raw = file_obj.read(FILE_HEADER_SIZE) - file_header = struct.unpack(FILE_HEADER_PACK, file_header_raw) - if file_header[0].decode("utf8") != "QISKIT": - raise QiskitError("Input file is not a valid QPY file") - qpy_version = file_header[1] - version_parts = [int(x) for x in __version__.split(".")[0:3]] - header_version_parts = [file_header[2], file_header[3], file_header[4]] - if ( - version_parts[0] < header_version_parts[0] - or ( - version_parts[0] == header_version_parts[0] - and header_version_parts[1] > version_parts[1] - ) - or ( - version_parts[0] == header_version_parts[0] - and header_version_parts[1] == version_parts[1] - and header_version_parts[2] > version_parts[2] - ) - ): - warnings.warn( - "The qiskit version used to generate the provided QPY " - "file, %s, is newer than the current qiskit version %s. " - "This may result in an error if the QPY file uses " - "instructions not present in this current qiskit " - "version" % (".".join([str(x) for x in header_version_parts]), __version__) - ) - circuits = [] - for _ in range(file_header[5]): - circuits.append(_read_circuit(file_obj, qpy_version)) - return circuits - - -def _read_circuit(file_obj, version): - vectors = {} - if version < 2: - header, name, metadata = _read_header(file_obj) - else: - header, name, metadata = _read_header_v2(file_obj, version, vectors) - global_phase = header["global_phase"] - num_qubits = header["num_qubits"] - num_clbits = header["num_clbits"] - num_registers = header["num_registers"] - num_instructions = header["num_instructions"] - out_registers = {"q": {}, "c": {}} - if num_registers > 0: - circ = QuantumCircuit(name=name, global_phase=global_phase, metadata=metadata) - if version < 4: - registers = _read_registers(file_obj, num_registers) - else: - registers = _read_registers_v4(file_obj, num_registers) - - for bit_type_label, bit_type, reg_type in [ - ("q", Qubit, QuantumRegister), - ("c", Clbit, ClassicalRegister), - ]: - register_bits = set() - # Add quantum registers and bits - for register_name in registers[bit_type_label]: - standalone, indices, in_circuit = registers[bit_type_label][register_name] - indices_defined = [x for x in indices if x >= 0] - # If a register has no bits in the circuit skip it - if not indices_defined: - continue - if standalone: - start = min(indices_defined) - count = start - out_of_order = False - for index in indices: - if index < 0: - out_of_order = True - continue - if not out_of_order and index != count: - out_of_order = True - count += 1 - if index in register_bits: - # If we have a bit in the position already it's been - # added by an earlier register in the circuit - # otherwise it's invalid qpy - if not in_circuit: - continue - raise QiskitError("Duplicate register bits found") - - register_bits.add(index) - - num_reg_bits = len(indices) - # Create a standlone register of the appropriate length (from - # the number of indices in the qpy data) and add it to the circuit - reg = reg_type(num_reg_bits, register_name) - # If any bits from qreg are out of order in the circuit handle - # is case - if out_of_order or not in_circuit: - for index, pos in sorted( - enumerate(x for x in indices if x >= 0), key=lambda x: x[1] - ): - if bit_type_label == "q": - bit_len = len(circ.qubits) - else: - bit_len = len(circ.clbits) - if pos < bit_len: - # If we have a bit in the position already it's been - # added by an earlier register in the circuit - # otherwise it's invalid qpy - if not in_circuit: - continue - raise QiskitError("Duplicate register bits found") - # Fill any holes between the current register bit and the - # next one - if pos > bit_len: - bits = [bit_type() for _ in range(pos - bit_len)] - circ.add_bits(bits) - circ.add_bits([reg[index]]) - if in_circuit: - circ.add_register(reg) - else: - if bit_type_label == "q": - bit_len = len(circ.qubits) - else: - bit_len = len(circ.clbits) - # If there is a hole between the start of the register and the - # current bits and standalone bits to fill the gap. - if start > len(circ.qubits): - bits = [bit_type() for _ in range(start - bit_len)] - circ.add_bits(bit_len) - if in_circuit: - circ.add_register(reg) - out_registers[bit_type_label][register_name] = reg - else: - for index in indices: - if bit_type_label == "q": - bit_len = len(circ.qubits) - else: - bit_len = len(circ.clbits) - # Add any missing bits - bits = [bit_type() for _ in range(index + 1 - bit_len)] - circ.add_bits(bits) - if index in register_bits: - raise QiskitError("Duplicate register bits found") - register_bits.add(index) - if bit_type_label == "q": - bits = [circ.qubits[i] for i in indices] - else: - bits = [circ.clbits[i] for i in indices] - reg = reg_type(name=register_name, bits=bits) - if in_circuit: - circ.add_register(reg) - out_registers[bit_type_label][register_name] = reg - # If we don't have sufficient bits in the circuit after adding - # all the registers add more bits to fill the circuit - if len(circ.qubits) < num_qubits: - qubits = [Qubit() for _ in range(num_qubits - len(circ.qubits))] - circ.add_bits(qubits) - if len(circ.clbits) < num_clbits: - clbits = [Clbit() for _ in range(num_qubits - len(circ.clbits))] - circ.add_bits(clbits) - else: - circ = QuantumCircuit( - num_qubits, - num_clbits, - name=name, - global_phase=global_phase, - metadata=metadata, - ) - custom_instructions = _read_custom_instructions(file_obj, version, vectors) - for _instruction in range(num_instructions): - _read_instruction(file_obj, circ, out_registers, custom_instructions, version, vectors) - for vec_name, (vector, initialized_params) in vectors.items(): - if len(initialized_params) != len(vector): - warnings.warn( - f"The ParameterVector: '{vec_name}' is not fully identical to its " - "pre-serialization state. Elements " - f"{', '.join([str(x) for x in set(range(len(vector))) - initialized_params])} " - "in the ParameterVector will be not equal to the pre-serialized ParameterVector " - f"as they weren't used in the circuit: {circ.name}", - UserWarning, - ) - - return circ diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py new file mode 100644 index 000000000000..2ede88a790eb --- /dev/null +++ b/qiskit/qpy/__init__.py @@ -0,0 +1,635 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 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. + +""" +########################################################### +QPY serialization (:mod:`qiskit.qpy`) +########################################################### + +.. currentmodule:: qiskit.qpy + +********* +Using QPY +********* + +Using QPY is defined to be straightforward and mirror the user API of the +serializers in Python's standard library, ``pickle`` and ``json``. There are +2 user facing functions: :func:`qiskit.circuit.qpy_serialization.dump` and +:func:`qiskit.circuit.qpy_serialization.load` which are used to dump QPY data +to a file object and load circuits from QPY data in a file object respectively. +For example:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit import qpy_serialization + + qc = QuantumCircuit(2, name='Bell', metadata={'test': True}) + qc.h(0) + qc.cx(0, 1) + qc.measure_all() + + with open('bell.qpy', 'wb') as fd: + qpy_serialization.dump(qc, fd) + + with open('bell.qpy', 'rb') as fd: + new_qc = qpy_serialization.load(fd)[0] + +API documentation +================= + +.. autosummary:: + :toctree: ../stubs/ + + load + dump + +QPY Compatibility +================= + +The QPY format is designed to be backwards compatible moving forward. This means +you should be able to load a QPY with any newer Qiskit version than the one +that generated it. However, loading a QPY file with an older Qiskit version is +not supported and may not work. + +For example, if you generated a QPY file using qiskit-terra 0.18.1 you could +load that QPY file with qiskit-terra 0.19.0 and a hypothetical qiskit-terra +0.29.0. However, loading that QPY file with 0.18.0 is not supported and may not +work. + +********** +QPY Format +********** + +The QPY serialization format is a portable cross-platform binary +serialization format for :class:`~qiskit.circuit.QuantumCircuit` objects in Qiskit. The basic +file format is as follows: + +A QPY file (or memory object) always starts with the following 7 +byte UTF8 string: ``QISKIT`` which is immediately followed by the overall +file header. The contents of the file header as defined as a C struct are: + +.. code-block:: c + + struct { + uint8_t qpy_version; + uint8_t qiskit_major_version; + uint8_t qiskit_minor_version; + uint8_t qiskit_patch_version; + uint64_t num_circuits; + } + +All values use network byte order [#f1]_ (big endian) for cross platform +compatibility. + +The file header is immediately followed by the circuit payloads. +Each individual circuit is composed of the following parts: + +``HEADER | METADATA | REGISTERS | CUSTOM_DEFINITIONS | INSTRUCTIONS`` + +There is a circuit payload for each circuit (where the total number is dictated +by ``num_circuits`` in the file header). There is no padding between the +circuits in the data. + +.. _qpy_version_4: + +Version 4 +========= + +Version 4 is identical to :ref:`qpy_version_3` except that it adds 2 new type strings +to the INSTRUCTION_PARAM struct, ``z`` to represent ``None`` (which is encoded as +no data), ``q`` to represent a :class:`.QuantumCircuit` (which is encoded as +a QPY circuit), ``r`` to represent a ``range`` of integers (which is encoded as +a :ref:`qpy_range_pack`), and ``t`` to represent a ``sequence`` (which is encoded as +defined by :ref:`qpy_sequence`). Additionally, version 4 changes the type of register +index mapping array from ``uint32_t`` to ``int64_t``. If the values of any of the +array elements are negative they represent a register bit that is not present in the +circuit. + +The :ref:`qpy_registers` header format has also been updated to + +.. code-block:: c + + struct { + char type; + _Bool standalone; + uint32_t size; + uint16_t name_size; + _bool in_circuit; + } + +which just adds the ``in_circuit`` field which represents whether the register is +part of the circuit or not. + +.. _qpy_range_pack: + +RANGE +----- + +A RANGE is a representation of a ``range`` object. It is defined as: + +.. code-block:: c + + struct { + int64_t start; + int64_t stop; + int64_t step; + } + +.. _qpy_sequence: + +SEQUENCE +-------- + +A SEQUENCE is a reprentation of a arbitrary sequence object. As sequence are just fixed length +containers of arbitrary python objects their QPY can't fully represent any sequence, +but as long as the contents in a sequence are other QPY serializable types for +the INSTRUCTION_PARAM payload the ``sequence`` object can be serialized. + +A sequence instruction parameter starts with a header defined as: + +.. code-block:: c + + struct { + uint64_t size; + } + +followed by ``size`` elements that are INSTRUCTION_PARAM payloads, where each of +these define an element in the sequence. The sequence object will be typecasted +into proper type, e.g. ``tuple``, afterwards. + +.. _qpy_version_3: + +Version 3 +========= + +Version 3 of the QPY format is identical to :ref:`qpy_version_2` except that it defines +a struct format to represent a :class:`~qiskit.circuit.library.PauliEvolutionGate` +natively in QPY. To accomplish this the :ref:`qpy_custom_definition` struct now supports +a new type value ``'p'`` to represent a :class:`~qiskit.circuit.library.PauliEvolutionGate`. +Enties in the custom instructions tables have unique name generated that start with the +string ``"###PauliEvolutionGate_"`` followed by a uuid string. This gate name is reservered +in QPY and if you have a custom :class:`~qiskit.circuit.Instruction` object with a definition +set and that name prefix it will error. If it's of type ``'p'`` the data payload is defined +as follows: + +.. _pauli_evo_qpy: + +PAULI_EVOLUTION +--------------- + +This represents the high level :class:`~qiskit.circuit.library.PauliEvolutionGate` + +.. code-block:: c + + struct { + uint64_t operator_count; + _Bool standalone_op; + char time_type; + uint64_t time_size; + uint64_t synthesis_size; + } + +This is immediately followed by ``operator_count`` elements defined by the :ref:`qpy_pauli_sum_op` +payload. Following that we have ``time_size`` bytes representing the ``time`` attribute. If +``standalone_op`` is ``True`` then there must only be a single operator. The +encoding of these bytes is determined by the value of ``time_type``. Possible values of +``time_type`` are ``'f'``, ``'p'``, and ``'e'``. If ``time_type`` is ``'f'`` it's a double, +``'p'`` defines a :class:`~qiskit.circuit.Parameter` object which is represented by a +:ref:`qpy_param_struct`, ``e`` defines a :class:`~qiskit.circuit.ParameterExpression` object +(that's not a :class:`~qiskit.circuit.Parameter`) which is represented by a :ref:`qpy_param_expr`. +Following that is ``synthesis_size`` bytes which is a utf8 encoded json payload representing +the :class:`.EvolutionSynthesis` class used by the gate. + +.. _qpy_pauli_sum_op: + +SPARSE_PAULI_OP_LIST_ELEM +------------------------- + +This represents an instance of :class:`.PauliSumOp`. + + +.. code-block:: c + + struct { + uint32_t pauli_op_size; + } + +which is immediately followed by ``pauli_op_size`` bytes which are .npy format [#f2]_ +data which represents the :class:`~qiskit.quantum_info.SparsePauliOp`. + +Version 3 of the QPY format also defines a struct format to represent a +:class:`~qiskit.circuit.ParameterVectorElement` as a distinct subclass from +a :class:`~qiskit.circuit.Parameter`. This adds a new parameter type char ``'v'`` +to represent a :class:`~qiskit.circuit.ParameterVectorElement` which is now +supported as a type string value for an INSTRUCTION_PARAM. The payload for these +parameters are defined below as :ref:`qpy_param_vector`. + +.. _qpy_param_vector: + +PARAMETER_VECTOR_ELEMENT +------------------------ + +A PARAMETER_VECTOR_ELEMENT represents a :class:`~qiskit.circuit.ParameterVectorElement` +object the data for a INSTRUCTION_PARAM. The contents of the PARAMETER_VECTOR_ELEMENT are +defined as: + +.. code-block:: c + + struct { + uint16_t vector_name_size; + uint64_t vector_size; + char uuid[16]; + uint64_t index; + } + +which is immediately followed by ``vector_name_size`` utf8 bytes representing +the parameter's vector name. + +.. _qpy_param_expr_v3: + + +PARAMETER_EXPR +-------------- + +Additionally, since QPY format version v3 distinguishes between a +:class:`~qiskit.circuit.Parameter` and :class:`~qiskit.circuit.ParameterVectorElement` +the payload for a :class:`~qiskit.circuit.ParameterExpression` needs to be updated +to distinguish between the types. The following is the modified payload format +which is mostly identical to the format in Version 1 and :ref:`qpy_version_2` but just +modifies the ``map_elements`` struct to include a symbol type field. + +A PARAMETER_EXPR represents a :class:`~qiskit.circuit.ParameterExpression` +object that the data for an INSTRUCTION_PARAM. The contents of a PARAMETER_EXPR +are defined as: + +.. code-block:: c + + struct { + uint64_t map_elements; + uint64_t expr_size; + } + +Immediately following the header is ``expr_size`` bytes of utf8 data containing +the expression string, which is the sympy srepr of the expression for the +parameter expression. Following that is a symbol map which contains +``map_elements`` elements with the format + +.. code-block:: c + + struct { + char symbol_type; + char type; + uint64_t size; + } + +The ``symbol_type`` key determines the payload type of the symbol representation +for the element. If it's ``p`` it represents a :class:`~qiskit.circuit.Parameter` +and if it's ``v`` it represents a :class:`~qiskit.circuit.ParameterVectorElement`. +The map element struct is immediately followed by the symbol map key payload, if +``symbol_type`` is ``p`` then it is followed immediately by a :ref:`qpy_param_struct` +object (both the struct and utf8 name bytes) and if ``symbol_type`` is ``v`` +then the struct is imediately followed by :ref:`qpy_param_vector` (both the struct +and utf8 name bytes). That is followed by ``size`` bytes for the +data of the symbol. The data format is dependent on the value of ``type``. If +``type`` is ``p`` then it represents a :class:`~qiskit.circuit.Parameter` and +size will be 0, the value will just be the same as the key. Similarly if the +``type`` is ``v`` then it represents a :class:`~qiskit.circuit.ParameterVectorElement` +and size will be 0 as the value will just be the same as the key. If +``type`` is ``f`` then it represents a double precision float. If ``type`` is +``c`` it represents a double precision complex, which is represented by the +:ref:`qpy_complex`. Finally, if type is ``i`` it represents an integer which is an +``int64_t``. + +.. _qpy_version_2: + +Version 2 +========= + +Version 2 of the QPY format is identical to version 1 except for the HEADER +section is slightly different. You can refer to the :ref:`qpy_version_1` section +for the details on the rest of the payload format. + +HEADER +------ + +The contents of HEADER are defined as a C struct are: + +.. code-block:: c + + struct { + uint16_t name_size; + char global_phase_type; + uint16_t global_phase_size; + uint32_t num_qubits; + uint32_t num_clbits; + uint64_t metadata_size; + uint32_t num_registers; + uint64_t num_instructions; + uint64_t num_custom_gates; + } + +This is immediately followed by ``name_size`` bytes of utf8 data for the name +of the circuit. Following this is immediately ``global_phase_size`` bytes +representing the global phase. The content of that data is dictated by the +value of ``global_phase_type``. If it's ``'f'`` the data is a float and is the +size of a ``double``. If it's ``'p'`` defines a :class:`~qiskit.circuit.Parameter` +object which is represented by a PARAM struct (see below), ``e`` defines a +:class:`~qiskit.circuit.ParameterExpression` object (that's not a +:class:`~qiskit.circuit.Parameter`) which is represented by a PARAM_EXPR struct +(see below). + +.. _qpy_version_1: + +Version 1 +========= + +HEADER +------ + +The contents of HEADER as defined as a C struct are: + +.. code-block:: c + + struct { + uint16_t name_size; + double global_phase; + uint32_t num_qubits; + uint32_t num_clbits; + uint64_t metadata_size; + uint32_t num_registers; + uint64_t num_instructions; + uint64_t num_custom_gates; + } + +This is immediately followed by ``name_size`` bytes of utf8 data for the name +of the circuit. + +METADATA +-------- + +The METADATA field is a UTF8 encoded JSON string. After reading the HEADER +(which is a fixed size at the start of the QPY file) and the ``name`` string +you then read the ``metadata_size`` number of bytes and parse the JSON to get +the metadata for the circuit. + +.. _qpy_registers: + +REGISTERS +--------- + +The contents of REGISTERS is a number of REGISTER object. If num_registers is +> 0 then after reading METADATA you read that number of REGISTER structs defined +as: + +.. code-block:: c + + struct { + char type; + _Bool standalone; + uint32_t size; + uint16_t name_size; + } + +``type`` can be ``'q'`` or ``'c'``. + +Immediately following the REGISTER struct is the utf8 encoded register name of +size ``name_size``. After the ``name`` utf8 bytes there is then an array of +int64_t values of size ``size`` that contains a map of the register's index to +the circuit's qubit index. For example, array element 0's value is the index +of the ``register[0]``'s position in the containing circuit's qubits list. + +.. note:: + + Prior to QPY :ref:`qpy_version_4` the type of array elements was uint32_t. This was changed + to enable negative values which represent bits in the array not present in the + circuit + + +The standalone boolean determines whether the register is constructed as a +standalone register that was added to the circuit or was created from existing +bits. A register is considered standalone if it has bits constructed solely +as part of it, for example:: + + qr = QuantumRegister(2) + qc = QuantumCircuit(qr) + +the register ``qr`` would be a standalone register. While something like:: + + bits = [Qubit(), Qubit()] + qr = QuantumRegister(bits=bits) + qc = QuantumCircuit(bits=bits) + +``qr`` would have ``standalone`` set to ``False``. + + +.. _qpy_custom_definition: + +CUSTOM_DEFINITIONS +------------------ + +This section specifies custom definitions for any of the instructions in the circuit. + +CUSTOM_DEFINITION_HEADER contents are defined as: + +.. code-block:: c + + struct { + uint64_t size; + } + +If size is greater than 0 that means the circuit contains custom instruction(s). +Each custom instruction is defined with a CUSTOM_INSTRUCTION block defined as: + +.. code-block:: c + + struct { + uint16_t name_size; + char type; + _Bool custom_definition; + uint64_t size; + } + +Immediately following the CUSTOM_INSTRUCTION struct is the utf8 encoded name +of size ``name_size``. + +If ``custom_definition`` is ``True`` that means that the immediately following +``size`` bytes contains a QPY circuit data which can be used for the custom +definition of that gate. If ``custom_definition`` is ``False`` then the +instruction can be considered opaque (ie no definition). The ``type`` field +determines what type of object will get created with the custom definition. +If it's ``'g'`` it will be a :class:`~qiskit.circuit.Gate` object, ``'i'`` +it will be a :class:`~qiskit.circuit.Instruction` object. + +INSTRUCTIONS +------------ + +The contents of INSTRUCTIONS is a list of INSTRUCTION metadata objects + +.. code-block:: c + + struct { + uint16_t name_size; + uint16_t label_size; + uint16_t num_parameters; + uint32_t num_qargs; + uint32_t num_cargs; + _Bool has_conditional; + uint16_t conditional_reg_name_size; + int64_t conditional_value; + } + +This metadata object is immediately followed by ``name_size`` bytes of utf8 bytes +for the ``name``. ``name`` here is the Qiskit class name for the Instruction +class if it's defined in Qiskit. Otherwise it falls back to the custom +instruction name. Following the ``name`` bytes there are ``label_size`` bytes of +utf8 data for the label if one was set on the instruction. Following the label +bytes if ``has_conditional`` is ``True`` then there are +``conditional_reg_name_size`` bytes of utf8 data for the name of the conditional +register name. In case of single classical bit conditions the register name +utf8 data will be prefixed with a null character "\\x00" and then a utf8 string +integer representing the classical bit index in the circuit that the condition +is on. + +This is immediately followed by the INSTRUCTION_ARG structs for the list of +arguments of that instruction. These are in the order of all quantum arguments +(there are num_qargs of these) followed by all classical arguments (num_cargs +of these). + +The contents of each INSTRUCTION_ARG is: + +.. code-block:: c + + struct { + char type; + uint32_t index; + } + +``type`` can be ``'q'`` or ``'c'``. + +After all arguments for an instruction the parameters are specified with +``num_parameters`` INSTRUCTION_PARAM structs. + +The contents of each INSTRUCTION_PARAM is: + +.. code-block:: c + + struct { + char type; + uint64_t size; + } + +After each INSTRUCTION_PARAM the next ``size`` bytes are the parameter's data. +The ``type`` field can be ``'i'``, ``'f'``, ``'p'``, ``'e'``, ``'s'``, ``'c'`` +or ``'n'`` which dictate the format. For ``'i'`` it's an integer, ``'f'`` it's +a double, ``'s'`` if it's a string (encoded as utf8), ``'c'`` is a complex and +the data is represented by the struct format in the :ref:`qpy_param_expr` section. +``'p'`` defines a :class:`~qiskit.circuit.Parameter` object which is +represented by a :ref:`qpy_param_struct` struct, ``e`` defines a +:class:`~qiskit.circuit.ParameterExpression` object (that's not a +:class:`~qiskit.circuit.Parameter`) which is represented by a :ref:`qpy_param_expr` +struct (on QPY format :ref:`qpy_version_3` the format is tweak slightly see: +:ref:`qpy_param_expr_v3`), ``'n'`` represents an object from numpy (either an +``ndarray`` or a numpy type) which means the data is .npy format [#f2]_ data, +and in QPY :ref:`qpy_version_3` ``'v'`` represents a +:class:`~qiskit.circuit.ParameterVectorElement` which is represented by a +:ref:`qpy_param_vector` struct. + +.. _qpy_param_struct: + +PARAMETER +--------- + +A PARAMETER represents a :class:`~qiskit.circuit.Parameter` object the data for +a INSTRUCTION_PARAM. The contents of the PARAMETER are defined as: + +.. code-block:: c + + struct { + uint16_t name_size; + char uuid[16]; + } + +which is immediately followed by ``name_size`` utf8 bytes representing the +parameter name. + +.. _qpy_param_expr: + +PARAMETER_EXPR +-------------- + +A PARAMETER_EXPR represents a :class:`~qiskit.circuit.ParameterExpression` +object that the data for an INSTRUCTION_PARAM. The contents of a PARAMETER_EXPR +are defined as: + +The PARAMETER_EXPR data starts with a header: + +.. code-block:: c + + struct { + uint64_t map_elements; + uint64_t expr_size; + } + +Immediately following the header is ``expr_size`` bytes of utf8 data containing +the expression string, which is the sympy srepr of the expression for the +parameter expression. Follwing that is a symbol map which contains +``map_elements`` elements with the format + +.. code-block:: c + + struct { + char type; + uint64_t size; + } + +Which is followed immediately by ``PARAMETER`` object (both the struct and utf8 +name bytes) for the symbol map key. That is followed by ``size`` bytes for the +data of the symbol. The data format is dependent on the value of ``type``. If +``type`` is ``p`` then it represents a :class:`~qiskit.circuit.Parameter` and +size will be 0, the value will just be the same as the key. If +``type`` is ``f`` then it represents a double precision float. If ``type`` is +``c`` it represents a double precision complex, which is represented by :ref:`qpy_complex`. +Finally, if type is ``i`` it represents an integer which is an ``int64_t``. + +.. _qpy_complex: + +COMPLEX +------- + +When representing a double precision complex value in QPY the following +struct is used: + + +.. code-block:: c + + struct { + double real; + double imag; + } + +this matches the internal C representation of Python's complex type. [#f3]_ + + +.. [#f1] https://tools.ietf.org/html/rfc1700 +.. [#f2] https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html +.. [#f3] https://docs.python.org/3/c-api/complex.html#c.Py_complex +""" + +from .interface import dump, load + +# For backward compatibility. Provide, Runtime, Experiment call these private functions. +from .binary_io import ( + _write_instruction, + _read_instruction, + _write_parameter_expression, + _read_parameter_expression, + _read_parameter_expression_v3, +) diff --git a/qiskit/qpy/binary_io/__init__.py b/qiskit/qpy/binary_io/__init__.py new file mode 100644 index 000000000000..69ae4b7d651e --- /dev/null +++ b/qiskit/qpy/binary_io/__init__.py @@ -0,0 +1,30 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 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. + +"""Read and write QPY-serializable objects.""" + +from .value import ( + dumps_value, + loads_value, + # for backward compatibility; provider, runtime, experiment call this private methods. + _write_parameter_expression, + _read_parameter_expression, + _read_parameter_expression_v3, +) + +from .circuits import ( + write_circuit, + read_circuit, + # for backward compatibility; provider calls this private methods. + _write_instruction, + _read_instruction, +) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py new file mode 100644 index 000000000000..ee5efb5dc450 --- /dev/null +++ b/qiskit/qpy/binary_io/circuits.py @@ -0,0 +1,776 @@ +# 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 + +"""Binary IO for circuit objects.""" + +import io +import json +import struct +import uuid +import warnings + +import numpy as np + +from qiskit import circuit as circuit_mod +from qiskit import extensions +from qiskit.circuit import library, controlflow +from qiskit.circuit.classicalregister import ClassicalRegister, Clbit +from qiskit.circuit.gate import Gate +from qiskit.circuit.instruction import Instruction +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.quantum_info.operators import SparsePauliOp +from qiskit.synthesis import evolution as evo_synth + + +def _read_header_v2(file_obj, version, vectors): + data = formats.CIRCUIT_HEADER_V2._make( + struct.unpack( + formats.CIRCUIT_HEADER_V2_PACK, + file_obj.read(formats.CIRCUIT_HEADER_V2_SIZE), + ) + ) + name = file_obj.read(data.name_size).decode(common.ENCODE) + global_phase = value.loads_value( + data.global_phase_type, + file_obj.read(data.global_phase_size), + version=version, + vectors=vectors, + ) + header = { + "global_phase": global_phase, + "num_qubits": data.num_qubits, + "num_clbits": data.num_clbits, + "num_registers": data.num_registers, + "num_instructions": data.num_instructions, + } + metadata_raw = file_obj.read(data.metadata_size) + metadata = json.loads(metadata_raw) + return header, name, metadata + + +def _read_header(file_obj): + data = formats.CIRCUIT_HEADER._make( + struct.unpack(formats.CIRCUIT_HEADER_PACK, file_obj.read(formats.CIRCUIT_HEADER_SIZE)) + ) + name = file_obj.read(data.name_size).decode(common.ENCODE) + header = { + "global_phase": data.global_phase, + "num_qubits": data.num_qubits, + "num_clbits": data.num_clbits, + "num_registers": data.num_registers, + "num_instructions": data.num_instructions, + } + metadata_raw = file_obj.read(data.metadata_size) + metadata = json.loads(metadata_raw) + return header, name, metadata + + +def _read_registers_v4(file_obj, num_registers): + registers = {"q": {}, "c": {}} + for _reg in range(num_registers): + data = formats.REGISTER_V4._make( + struct.unpack( + formats.REGISTER_V4_PACK, + file_obj.read(formats.REGISTER_V4_SIZE), + ) + ) + name = file_obj.read(data.name_size).decode("utf8") + REGISTER_ARRAY_PACK = "!%sq" % data.size + bit_indices_raw = file_obj.read(struct.calcsize(REGISTER_ARRAY_PACK)) + bit_indices = list(struct.unpack(REGISTER_ARRAY_PACK, bit_indices_raw)) + if data.type.decode("utf8") == "q": + registers["q"][name] = (data.standalone, bit_indices, data.in_circuit) + else: + registers["c"][name] = (data.standalone, bit_indices, data.in_circuit) + return registers + + +def _read_registers(file_obj, num_registers): + registers = {"q": {}, "c": {}} + for _reg in range(num_registers): + data = formats.REGISTER._make( + struct.unpack( + formats.REGISTER_PACK, + file_obj.read(formats.REGISTER_SIZE), + ) + ) + name = file_obj.read(data.name_size).decode("utf8") + REGISTER_ARRAY_PACK = "!%sI" % data.size + bit_indices_raw = file_obj.read(struct.calcsize(REGISTER_ARRAY_PACK)) + bit_indices = list(struct.unpack(REGISTER_ARRAY_PACK, bit_indices_raw)) + if data.type.decode("utf8") == "q": + registers["q"][name] = (data.standalone, bit_indices, True) + else: + registers["c"][name] = (data.standalone, bit_indices, True) + 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)) + param = range(data.start, data.stop, data.step) + elif type_key == common.ContainerTypeKey.TUPLE: + param = tuple( + common.sequence_from_binary( + bin_data, + _read_instruction_parameter, + version=version, + vectors=vectors, + ) + ) + elif type_key == common.ValueTypeKey.INTEGER: + # TODO This uses little endian. Should be fixed in the next QPY version. + param = struct.unpack(" 0: + inst_obj.label = label + circuit._append(inst_obj, qargs, cargs) + return + elif gate_name in custom_instructions: + inst_obj = _parse_custom_instruction(custom_instructions, gate_name, params) + inst_obj.condition = condition_tuple + if instruction.label_size > 0: + inst_obj.label = label + circuit._append(inst_obj, qargs, cargs) + return + elif hasattr(library, gate_name): + gate_class = getattr(library, gate_name) + elif hasattr(circuit_mod, gate_name): + gate_class = getattr(circuit_mod, gate_name) + elif hasattr(extensions, gate_name): + gate_class = getattr(extensions, gate_name) + elif hasattr(quantum_initializer, gate_name): + gate_class = getattr(quantum_initializer, gate_name) + elif hasattr(controlflow, gate_name): + gate_class = getattr(controlflow, gate_name) + else: + raise AttributeError("Invalid instruction type: %s" % gate_name) + + if gate_name in {"IfElseOp", "WhileLoopOp"}: + gate = gate_class(condition_tuple, *params) + else: + if gate_name == "Initialize": + gate = gate_class(params) + else: + if gate_name == "Barrier": + params = [len(qargs)] + elif gate_name in {"BreakLoopOp", "ContinueLoopOp"}: + params = [len(qargs), len(cargs)] + gate = gate_class(*params) + gate.condition = condition_tuple + if instruction.label_size > 0: + gate.label = label + if not isinstance(gate, Instruction): + circuit.append(gate, qargs, cargs) + else: + circuit._append(gate, qargs, cargs) + + +def _parse_custom_instruction(custom_instructions, gate_name, params): + type_str, num_qubits, num_clbits, definition = custom_instructions[gate_name] + type_key = common.CircuitInstructionTypeKey(type_str) + + if type_key == common.CircuitInstructionTypeKey.INSTRUCTION: + inst_obj = Instruction(gate_name, num_qubits, num_clbits, params) + if definition is not None: + inst_obj.definition = definition + return inst_obj + + if type_key == common.CircuitInstructionTypeKey.GATE: + inst_obj = Gate(gate_name, num_qubits, params) + inst_obj.definition = definition + return inst_obj + + if type_key == common.CircuitInstructionTypeKey.PAULI_EVOL_GATE: + return definition + + raise ValueError("Invalid custom instruction type '%s'" % type_str) + + +def _read_pauli_evolution_gate(file_obj, version, vectors): + pauli_evolution_def = formats.PAULI_EVOLUTION_DEF._make( + struct.unpack( + formats.PAULI_EVOLUTION_DEF_PACK, file_obj.read(formats.PAULI_EVOLUTION_DEF_SIZE) + ) + ) + if pauli_evolution_def.operator_size != 1 and pauli_evolution_def.standalone_op: + raise ValueError( + "Can't have a standalone operator with {pauli_evolution_raw[0]} operators in the payload" + ) + + operator_list = [] + for _ in range(pauli_evolution_def.operator_size): + op_elem = formats.SPARSE_PAULI_OP_LIST_ELEM._make( + struct.unpack( + formats.SPARSE_PAULI_OP_LIST_ELEM_PACK, + file_obj.read(formats.SPARSE_PAULI_OP_LIST_ELEM_SIZE), + ) + ) + op_raw_data = common.data_from_binary(file_obj.read(op_elem.size), np.load) + operator_list.append(SparsePauliOp.from_list(op_raw_data)) + + if pauli_evolution_def.standalone_op: + pauli_op = operator_list[0] + else: + pauli_op = operator_list + + time = value.loads_value( + common.ValueTypeKey(pauli_evolution_def.time_type), + file_obj.read(pauli_evolution_def.time_size), + version=version, + vectors=vectors, + ) + synth_data = json.loads(file_obj.read(pauli_evolution_def.synth_method_size)) + synthesis = getattr(evo_synth, synth_data["class"])(**synth_data["settings"]) + return_gate = library.PauliEvolutionGate(pauli_op, time=time, synthesis=synthesis) + return return_gate + + +def _read_custom_instructions(file_obj, version, vectors): + custom_instructions = {} + custom_definition_header = formats.CUSTOM_CIRCUIT_DEF_HEADER._make( + struct.unpack( + formats.CUSTOM_CIRCUIT_DEF_HEADER_PACK, + file_obj.read(formats.CUSTOM_CIRCUIT_DEF_HEADER_SIZE), + ) + ) + if custom_definition_header.size > 0: + for _ in range(custom_definition_header.size): + data = formats.CUSTOM_CIRCUIT_INST_DEF._make( + struct.unpack( + formats.CUSTOM_CIRCUIT_INST_DEF_PACK, + file_obj.read(formats.CUSTOM_CIRCUIT_INST_DEF_SIZE), + ) + ) + name = file_obj.read(data.gate_name_size).decode(common.ENCODE) + type_str = data.type + definition_circuit = None + if data.custom_definition: + def_binary = file_obj.read(data.size) + if version < 3 or not name.startswith(r"###PauliEvolutionGate_"): + definition_circuit = common.data_from_binary( + def_binary, read_circuit, version=version + ) + elif name.startswith(r"###PauliEvolutionGate_"): + definition_circuit = common.data_from_binary( + def_binary, _read_pauli_evolution_gate, version=version, vectors=vectors + ) + custom_instructions[name] = ( + type_str, + data.num_qubits, + data.num_clbits, + definition_circuit, + ) + return custom_instructions + + +def _write_instruction_parameter(file_obj, param): + if isinstance(param, QuantumCircuit): + type_key = common.ProgramTypeKey.CIRCUIT + data = 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) + elif isinstance(param, tuple): + type_key = common.ContainerTypeKey.TUPLE + data = common.sequence_to_binary(param, _write_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("= 0: + processed_indices.add(bit_index) + bit_indices.append(bit_index) + file_obj.write(struct.pack(REGISTER_ARRAY_PACK, *bit_indices)) + + return len(in_circ_regs) + len(out_circ_regs) + + +def write_circuit(file_obj, circuit): + """Write a single QuantumCircuit object in the file like object. + + Args: + file_obj (FILE): The file like object to write the circuit data in. + circuit (QuantumCircuit): The circuit data to write. + """ + metadata_raw = json.dumps(circuit.metadata, separators=(",", ":")).encode(common.ENCODE) + metadata_size = len(metadata_raw) + num_instructions = len(circuit) + circuit_name = circuit.name.encode(common.ENCODE) + global_phase_type, global_phase_data = value.dumps_value(circuit.global_phase) + + with io.BytesIO() as reg_buf: + num_qregs = _write_registers(reg_buf, circuit.qregs, circuit.qubits) + num_cregs = _write_registers(reg_buf, circuit.cregs, circuit.clbits) + registers_raw = reg_buf.getvalue() + num_registers = num_qregs + num_cregs + + # Write circuit header + header_raw = formats.CIRCUIT_HEADER_V2( + name_size=len(circuit_name), + global_phase_type=global_phase_type, + global_phase_size=len(global_phase_data), + num_qubits=circuit.num_qubits, + num_clbits=circuit.num_clbits, + metadata_size=metadata_size, + num_registers=num_registers, + num_instructions=num_instructions, + ) + header = struct.pack(formats.CIRCUIT_HEADER_V2_PACK, *header_raw) + file_obj.write(header) + file_obj.write(circuit_name) + file_obj.write(global_phase_data) + file_obj.write(metadata_raw) + # Write header payload + file_obj.write(registers_raw) + instruction_buffer = io.BytesIO() + custom_instructions = {} + index_map = {} + index_map["q"] = {bit: index for index, bit in enumerate(circuit.qubits)} + index_map["c"] = {bit: index for index, bit in enumerate(circuit.clbits)} + for instruction in circuit.data: + _write_instruction(instruction_buffer, instruction, custom_instructions, index_map) + file_obj.write(struct.pack(formats.CUSTOM_CIRCUIT_DEF_HEADER_PACK, len(custom_instructions))) + + for name, instruction in custom_instructions.items(): + _write_custom_instruction(file_obj, name, instruction) + + instruction_buffer.seek(0) + file_obj.write(instruction_buffer.read()) + instruction_buffer.close() + + +def read_circuit(file_obj, version): + """Read a single QuantumCircuit object from the file like object. + + Args: + file_obj (FILE): The file like object to read the circuit data from. + version (int): QPY version. + + Returns: + QuantumCircuit: The circuit object from the file. + + Raises: + QpyError: Invalid register. + """ + vectors = {} + if version < 2: + header, name, metadata = _read_header(file_obj) + else: + header, name, metadata = _read_header_v2(file_obj, version, vectors) + + global_phase = header["global_phase"] + num_qubits = header["num_qubits"] + num_clbits = header["num_clbits"] + num_registers = header["num_registers"] + num_instructions = header["num_instructions"] + out_registers = {"q": {}, "c": {}} + if num_registers > 0: + circ = QuantumCircuit(name=name, global_phase=global_phase, metadata=metadata) + if version < 4: + registers = _read_registers(file_obj, num_registers) + else: + registers = _read_registers_v4(file_obj, num_registers) + + for bit_type_label, bit_type, reg_type in [ + ("q", Qubit, QuantumRegister), + ("c", Clbit, ClassicalRegister), + ]: + register_bits = set() + # Add quantum registers and bits + for register_name in registers[bit_type_label]: + standalone, indices, in_circuit = registers[bit_type_label][register_name] + indices_defined = [x for x in indices if x >= 0] + # If a register has no bits in the circuit skip it + if not indices_defined: + continue + if standalone: + start = min(indices_defined) + count = start + out_of_order = False + for index in indices: + if index < 0: + out_of_order = True + continue + if not out_of_order and index != count: + out_of_order = True + count += 1 + if index in register_bits: + # If we have a bit in the position already it's been + # added by an earlier register in the circuit + # otherwise it's invalid qpy + if not in_circuit: + continue + raise exceptions.QpyError("Duplicate register bits found") + + register_bits.add(index) + + num_reg_bits = len(indices) + # Create a standlone register of the appropriate length (from + # the number of indices in the qpy data) and add it to the circuit + reg = reg_type(num_reg_bits, register_name) + # If any bits from qreg are out of order in the circuit handle + # is case + if out_of_order or not in_circuit: + for index, pos in sorted( + enumerate(x for x in indices if x >= 0), key=lambda x: x[1] + ): + if bit_type_label == "q": + bit_len = len(circ.qubits) + else: + bit_len = len(circ.clbits) + if pos < bit_len: + # If we have a bit in the position already it's been + # added by an earlier register in the circuit + # otherwise it's invalid qpy + if not in_circuit: + continue + raise exceptions.QpyError("Duplicate register bits found") + # Fill any holes between the current register bit and the + # next one + if pos > bit_len: + bits = [bit_type() for _ in range(pos - bit_len)] + circ.add_bits(bits) + circ.add_bits([reg[index]]) + if in_circuit: + circ.add_register(reg) + else: + if bit_type_label == "q": + bit_len = len(circ.qubits) + else: + bit_len = len(circ.clbits) + # If there is a hole between the start of the register and the + # current bits and standalone bits to fill the gap. + if start > len(circ.qubits): + bits = [bit_type() for _ in range(start - bit_len)] + circ.add_bits(bit_len) + if in_circuit: + circ.add_register(reg) + out_registers[bit_type_label][register_name] = reg + else: + for index in indices: + if bit_type_label == "q": + bit_len = len(circ.qubits) + else: + bit_len = len(circ.clbits) + # Add any missing bits + bits = [bit_type() for _ in range(index + 1 - bit_len)] + circ.add_bits(bits) + if index in register_bits: + raise exceptions.QpyError("Duplicate register bits found") + register_bits.add(index) + if bit_type_label == "q": + bits = [circ.qubits[i] for i in indices] + else: + bits = [circ.clbits[i] for i in indices] + reg = reg_type(name=register_name, bits=bits) + if in_circuit: + circ.add_register(reg) + out_registers[bit_type_label][register_name] = reg + # If we don't have sufficient bits in the circuit after adding + # all the registers add more bits to fill the circuit + if len(circ.qubits) < num_qubits: + qubits = [Qubit() for _ in range(num_qubits - len(circ.qubits))] + circ.add_bits(qubits) + if len(circ.clbits) < num_clbits: + clbits = [Clbit() for _ in range(num_qubits - len(circ.clbits))] + circ.add_bits(clbits) + else: + circ = QuantumCircuit( + num_qubits, + num_clbits, + name=name, + global_phase=global_phase, + metadata=metadata, + ) + custom_instructions = _read_custom_instructions(file_obj, version, vectors) + for _instruction in range(num_instructions): + _read_instruction(file_obj, circ, out_registers, custom_instructions, version, vectors) + for vec_name, (vector, initialized_params) in vectors.items(): + if len(initialized_params) != len(vector): + warnings.warn( + f"The ParameterVector: '{vec_name}' is not fully identical to its " + "pre-serialization state. Elements " + f"{', '.join([str(x) for x in set(range(len(vector))) - initialized_params])} " + "in the ParameterVector will be not equal to the pre-serialized ParameterVector " + f"as they weren't used in the circuit: {circ.name}", + UserWarning, + ) + + return circ diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py new file mode 100644 index 000000000000..937082a562a6 --- /dev/null +++ b/qiskit/qpy/binary_io/value.py @@ -0,0 +1,290 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 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. + +"""Binary IO for any value objects, such as numbers, string, parameters.""" + +import struct +import uuid + +import numpy as np + +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.utils import optionals as _optional + + +def _write_parameter(file_obj, obj): + name_bytes = obj._name.encode("utf8") + 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) + file_obj.write( + struct.pack( + formats.PARAMETER_VECTOR_ELEMENT_PACK, + len(name_bytes), + obj._vector._size, + obj._uuid.bytes, + obj._index, + ) + ) + file_obj.write(name_bytes) + + +def _write_parameter_expression(file_obj, obj): + from sympy import srepr, sympify + + expr_bytes = srepr(sympify(obj._symbol_expr)).encode(ENCODE) + param_expr_header_raw = struct.pack( + formats.PARAMETER_EXPR_PACK, len(obj._parameter_symbols), len(expr_bytes) + ) + file_obj.write(param_expr_header_raw) + file_obj.write(expr_bytes) + + for symbol, value in obj._parameter_symbols.items(): + symbol_key = TypeKey.assign(symbol) + + # serialize key + if symbol_key == TypeKey.PARAMETER_VECTOR: + symbol_data = common.data_to_binary(symbol, _write_parameter_vec) + else: + symbol_data = common.data_to_binary(symbol, _write_parameter) + + # serialize value + if value == symbol._symbol_expr: + value_key = symbol_key + value_data = bytes() + else: + value_key, value_data = dumps_value(value) + + elem_header = struct.pack( + formats.PARAM_EXPR_MAP_ELEM_V3_PACK, + symbol_key, + value_key, + len(value_data), + ) + file_obj.write(elem_header) + file_obj.write(symbol_data) + file_obj.write(value_data) + + +def _read_parameter(file_obj): + data = formats.PARAMETER( + *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) + param = Parameter.__new__(Parameter, name, uuid=param_uuid) + param.__init__(name) + return param + + +def _read_parameter_vec(file_obj, vectors): + data = formats.PARAMETER_VECTOR_ELEMENT( + *struct.unpack( + formats.PARAMETER_VECTOR_ELEMENT_PACK, + file_obj.read(formats.PARAMETER_VECTOR_ELEMENT_SIZE), + ), + ) + param_uuid = uuid.UUID(bytes=data.uuid) + name = file_obj.read(data.vector_name_size).decode(ENCODE) + if name not in vectors: + vectors[name] = (ParameterVector(name, data.vector_size), set()) + vector = vectors[name][0] + if vector[data.index]._uuid != param_uuid: + vectors[name][1].add(data.index) + vector._params[data.index] = ParameterVectorElement.__new__( + ParameterVectorElement, vector, data.index, uuid=param_uuid + ) + vector._params[data.index].__init__(vector, data.index) + return vector[data.index] + + +def _read_parameter_expression(file_obj): + data = formats.PARAMETER_EXPR( + *struct.unpack(formats.PARAMETER_EXPR_PACK, file_obj.read(formats.PARAMETER_EXPR_SIZE)) + ) + from sympy.parsing.sympy_parser import parse_expr + + if _optional.HAS_SYMENGINE: + import symengine + + expr = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(ENCODE))) + else: + expr = parse_expr(file_obj.read(data.expr_size).decode(ENCODE)) + symbol_map = {} + for _ in range(data.map_elements): + elem_data = formats.PARAM_EXPR_MAP_ELEM( + *struct.unpack( + formats.PARAM_EXPR_MAP_ELEM_PACK, + file_obj.read(formats.PARAM_EXPR_MAP_ELEM_SIZE), + ) + ) + symbol = _read_parameter(file_obj) + + elem_key = TypeKey(elem_data.type) + binary_data = file_obj.read(elem_data.size) + if elem_key == TypeKey.INTEGER: + value = struct.unpack("!q", binary_data) + elif elem_key == TypeKey.FLOAT: + value = struct.unpack("!d", binary_data) + elif elem_key == TypeKey.COMPLEX: + value = complex(*struct.unpack(formats.COMPLEX_PACK, binary_data)) + elif elem_key == TypeKey.PARAMETER: + value = symbol._symbol_expr + elif elem_key == TypeKey.PARAMETER_EXPRESSION: + value = common.data_from_binary(binary_data, _read_parameter_expression) + else: + raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) + symbol_map[symbol] = value + + return ParameterExpression(symbol_map, expr) + + +def _read_parameter_expression_v3(file_obj, vectors): + data = formats.PARAMETER_EXPR( + *struct.unpack(formats.PARAMETER_EXPR_PACK, file_obj.read(formats.PARAMETER_EXPR_SIZE)) + ) + from sympy.parsing.sympy_parser import parse_expr + + if _optional.HAS_SYMENGINE: + import symengine + + expr = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(ENCODE))) + else: + expr = parse_expr(file_obj.read(data.expr_size).decode(ENCODE)) + symbol_map = {} + for _ in range(data.map_elements): + elem_data = formats.PARAM_EXPR_MAP_ELEM_V3( + *struct.unpack( + formats.PARAM_EXPR_MAP_ELEM_V3_PACK, + file_obj.read(formats.PARAM_EXPR_MAP_ELEM_V3_SIZE), + ) + ) + symbol_key = TypeKey(elem_data.symbol_type) + + if symbol_key == TypeKey.PARAMETER: + symbol = _read_parameter(file_obj) + elif symbol_key == TypeKey.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) + binary_data = file_obj.read(elem_data.size) + if elem_key == TypeKey.INTEGER: + value = struct.unpack("!q", binary_data) + elif elem_key == TypeKey.FLOAT: + value = struct.unpack("!d", binary_data) + elif elem_key == TypeKey.COMPLEX: + value = complex(*struct.unpack(formats.COMPLEX_PACK, binary_data)) + elif elem_key in (TypeKey.PARAMETER, TypeKey.PARAMETER_VECTOR): + value = symbol._symbol_expr + elif elem_key == TypeKey.PARAMETER_EXPRESSION: + value = common.data_from_binary( + binary_data, _read_parameter_expression_v3, vectors=vectors + ) + else: + raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) + symbol_map[symbol] = value + + return ParameterExpression(symbol_map, expr) + + +def dumps_value(obj): + """Serialize input value object. + + Args: + obj (any): Arbitrary value object to serialize. + + Returns: + tuple: TypeKey and binary data. + + Raises: + QpyError: Serializer for given format is not ready. + """ + type_key = TypeKey.assign(obj) + + if type_key == TypeKey.INTEGER: + binary_data = struct.pack("!q", obj) + elif type_key == TypeKey.FLOAT: + binary_data = struct.pack("!d", obj) + elif type_key == TypeKey.COMPLEX: + binary_data = struct.pack(formats.COMPLEX_PACK, obj.real, obj.imag) + elif type_key == TypeKey.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: + binary_data = b"" + elif type_key == TypeKey.PARAMETER_VECTOR: + binary_data = common.data_to_binary(obj, _write_parameter_vec) + elif type_key == TypeKey.PARAMETER: + binary_data = common.data_to_binary(obj, _write_parameter) + elif type_key == TypeKey.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.") + + return type_key, binary_data + + +def loads_value(type_key, binary_data, version, vectors): + """Deserialize input binary data to value object. + + Args: + type_key (ValueTypeKey): Type enum information. + binary_data (bytes): Data to deserialize. + version (int): QPY version. + vectors (dict): ParameterVector in current scope. + + Returns: + any: Deserialized value object. + + Raises: + QpyError: Serializer for given format is not ready. + """ + 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: + if version < 3: + obj = common.data_from_binary(binary_data, _read_parameter_expression) + else: + obj = 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 diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py new file mode 100644 index 000000000000..048cd4c0fb50 --- /dev/null +++ b/qiskit/qpy/common.py @@ -0,0 +1,284 @@ +# 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 + +""" +Common functions across several serialization and deserialization modules. +""" + +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 +from qiskit.qpy import formats, exceptions + +QPY_VERSION = 4 +ENCODE = "utf8" + + +class CircuitInstructionTypeKey(bytes, Enum): + """Type key enum for circuit instruction object.""" + + INSTRUCTION = b"i" + GATE = b"g" + PAULI_EVOL_GATE = b"p" + + @classmethod + def assign(cls, obj): + """Assign type key to given object. + + Args: + obj (any): Arbitrary object to evaluate. + + 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, Gate): + return cls.GATE + if isinstance(obj, CircuitInstruction): + return cls.INSTRUCTION + + raise exceptions.QpyError( + f"Object type {type(obj)} is not supported in {cls.__name__} namespace." + ) + + +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." + ) + + +class ContainerTypeKey(bytes, Enum): + """Typle key enum for container-like object.""" + + RANGE = b"r" + TUPLE = b"t" + + @classmethod + def assign(cls, obj): + """Assign type key to given object. + + Args: + obj (any): Arbitrary object to evaluate. + + Returns: + ContainerTypeKey: Corresponding key object. + + 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 + + raise exceptions.QpyError( + f"Object type {type(obj)} is not supported in {cls.__name__} namespace." + ) + + +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: + ContainerTypeKey: 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. + + 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. + """ + 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) + + +def write_instruction_param(file_obj, type_key, data_binary): + """Write statically typed binary data to the file like object. + + Args: + file_obj (File): A file like object to write data. + type_key (Enum): Object type of the data. + data_binary (bytes): Binary data to write. + """ + data_header = struct.pack(formats.INSTRUCTION_PARAM_PACK, type_key, len(data_binary)) + file_obj.write(data_header) + file_obj.write(data_binary) + + +def data_to_binary(obj, serializer, **kwargs): + """Convert object into binary data with specified serializer. + + Args: + obj (any): Object to serialize. + serializer (Callable): Serializer callback that can handle input object type. + kwargs: Options set to the serializer. + + Returns: + bytes: Binary data. + """ + with io.BytesIO() as container: + serializer(container, obj, **kwargs) + binary_data = container.getvalue() + + return binary_data + + +def sequence_to_binary(sequence, serializer, **kwargs): + """Convert sequence into binary data with specified serializer. + + Args: + sequence (Sequence): Object to serialize. + serializer (Callable): Serializer callback that can handle input object type. + kwargs: Options set to the serializer. + + Returns: + bytes: Binary data. + """ + num_elements = len(sequence) + + with io.BytesIO() as container: + container.write(struct.pack(formats.SEQUENCE_PACK, num_elements)) + for datum in sequence: + serializer(container, datum, **kwargs) + binary_data = container.getvalue() + + return binary_data + + +def data_from_binary(binary_data, deserializer, **kwargs): + """Load object from binary data with specified deserializer. + + Args: + binary_data (bytes): Binary data to deserialize. + deserializer (Callable): Deserializer callback that can handle input object type. + kwargs: Options set to the deserializer. + + Returns: + any: Deserialized object. + """ + with io.BytesIO(binary_data) as container: + obj = deserializer(container, **kwargs) + return obj + + +def sequence_from_binary(binary_data, deserializer, **kwargs): + """Load object from binary sequence with specified deserializer. + + Args: + binary_data (bytes): Binary data to deserialize. + deserializer (Callable): Deserializer callback that can handle input object type. + 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)) + + return sequence diff --git a/qiskit/qpy/exceptions.py b/qiskit/qpy/exceptions.py new file mode 100644 index 000000000000..527681a266d1 --- /dev/null +++ b/qiskit/qpy/exceptions.py @@ -0,0 +1,27 @@ +# 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. + +"""Exception for errors raised by the pulse module.""" +from qiskit.exceptions import QiskitError + + +class QpyError(QiskitError): + """Errors raised by the qpy module.""" + + def __init__(self, *message): + """Set the error message.""" + super().__init__(*message) + self.message = " ".join(message) + + def __str__(self): + """Return the message.""" + return repr(self.message) diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py new file mode 100644 index 000000000000..ba8f900a1c9e --- /dev/null +++ b/qiskit/qpy/formats.py @@ -0,0 +1,164 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 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. + +# pylint: disable=invalid-name + +"""Binary format definition.""" + +import struct +from collections import namedtuple + + +# FILE_HEADER +FILE_HEADER = namedtuple( + "FILE_HEADER", + ["preface", "qpy_version", "major_version", "minor_version", "patch_version", "num_circuits"], +) +FILE_HEADER_PACK = "!6sBBBBQ" +FILE_HEADER_SIZE = struct.calcsize(FILE_HEADER_PACK) + +# CIRCUIT_HEADER_V2 +CIRCUIT_HEADER_V2 = namedtuple( + "HEADER", + [ + "name_size", + "global_phase_type", + "global_phase_size", + "num_qubits", + "num_clbits", + "metadata_size", + "num_registers", + "num_instructions", + ], +) +CIRCUIT_HEADER_V2_PACK = "!H1cHIIQIQ" +CIRCUIT_HEADER_V2_SIZE = struct.calcsize(CIRCUIT_HEADER_V2_PACK) + +# CIRCUIT_HEADER +CIRCUIT_HEADER = namedtuple( + "HEADER", + [ + "name_size", + "global_phase", + "num_qubits", + "num_clbits", + "metadata_size", + "num_registers", + "num_instructions", + ], +) +CIRCUIT_HEADER_PACK = "!HdIIQIQ" +CIRCUIT_HEADER_SIZE = struct.calcsize(CIRCUIT_HEADER_PACK) + +# REGISTER +REGISTER_V4 = namedtuple("REGISTER", ["type", "standalone", "size", "name_size", "in_circuit"]) +REGISTER_V4_PACK = "!1c?IH?" +REGISTER_V4_SIZE = struct.calcsize(REGISTER_V4_PACK) + +REGISTER = namedtuple("REGISTER", ["type", "standalone", "size", "name_size"]) +REGISTER_PACK = "!1c?IH" +REGISTER_SIZE = struct.calcsize(REGISTER_PACK) + +# CIRCUIT_INSTRUCTION +CIRCUIT_INSTRUCTION = namedtuple( + "CIRCUIT_INSTRUCTION", + [ + "name_size", + "label_size", + "num_parameters", + "num_qargs", + "num_cargs", + "has_condition", + "condition_register_size", + "condition_value", + ], +) +CIRCUIT_INSTRUCTION_PACK = "!HHHII?Hq" +CIRCUIT_INSTRUCTION_SIZE = struct.calcsize(CIRCUIT_INSTRUCTION_PACK) + +# CIRCUIT_INSTRUCTION_ARG +CIRCUIT_INSTRUCTION_ARG = namedtuple("CIRCUIT_INSTRUCTION_ARG", ["type", "size"]) +CIRCUIT_INSTRUCTION_ARG_PACK = "!1cI" +CIRCUIT_INSTRUCTION_ARG_SIZE = struct.calcsize(CIRCUIT_INSTRUCTION_ARG_PACK) + +# SparsePauliOp List +SPARSE_PAULI_OP_LIST_ELEM = namedtuple("SPARSE_PAULI_OP_LIST_ELEMENT", ["size"]) +SPARSE_PAULI_OP_LIST_ELEM_PACK = "!Q" +SPARSE_PAULI_OP_LIST_ELEM_SIZE = struct.calcsize(SPARSE_PAULI_OP_LIST_ELEM_PACK) + +# Pauli Evolution Gate +PAULI_EVOLUTION_DEF = namedtuple( + "PAULI_EVOLUTION_DEF", + ["operator_size", "standalone_op", "time_type", "time_size", "synth_method_size"], +) +PAULI_EVOLUTION_DEF_PACK = "!Q?1cQQ" +PAULI_EVOLUTION_DEF_SIZE = struct.calcsize(PAULI_EVOLUTION_DEF_PACK) + +# CUSTOM_CIRCUIT_DEF_HEADER +CUSTOM_CIRCUIT_DEF_HEADER = namedtuple("CUSTOM_CIRCUIT_DEF_HEADER", ["size"]) +CUSTOM_CIRCUIT_DEF_HEADER_PACK = "!Q" +CUSTOM_CIRCUIT_DEF_HEADER_SIZE = struct.calcsize(CUSTOM_CIRCUIT_DEF_HEADER_PACK) + +# CUSTOM_CIRCUIT_INST_DEF +CUSTOM_CIRCUIT_INST_DEF = namedtuple( + "CUSTOM_CIRCUIT_INST_DEF", + ["gate_name_size", "type", "num_qubits", "num_clbits", "custom_definition", "size"], +) +CUSTOM_CIRCUIT_INST_DEF_PACK = "!H1cII?Q" +CUSTOM_CIRCUIT_INST_DEF_SIZE = struct.calcsize(CUSTOM_CIRCUIT_INST_DEF_PACK) + +# INSTRUCTION_PARAM +INSTRUCTION_PARAM = namedtuple("TYPED_OBJECT", ["type", "size"]) +INSTRUCTION_PARAM_PACK = "!1cQ" +INSTRUCTION_PARAM_SIZE = struct.calcsize(INSTRUCTION_PARAM_PACK) + +# PARAMETER +PARAMETER = namedtuple("PARAMETER", ["name_size", "uuid"]) +PARAMETER_PACK = "!H16s" +PARAMETER_SIZE = struct.calcsize(PARAMETER_PACK) + +# COMPLEX +COMPLEX = namedtuple("COMPLEX", ["real", "imag"]) +COMPLEX_PACK = "!dd" +COMPLEX_SIZE = struct.calcsize(COMPLEX_PACK) + +# PARAMETER_VECTOR_ELEMENT +PARAMETER_VECTOR_ELEMENT = namedtuple( + "PARAMETER_VECTOR_ELEMENT", ["vector_name_size", "vector_size", "uuid", "index"] +) +PARAMETER_VECTOR_ELEMENT_PACK = "!HQ16sQ" +PARAMETER_VECTOR_ELEMENT_SIZE = struct.calcsize(PARAMETER_VECTOR_ELEMENT_PACK) + +# PARAMETER_EXPR +PARAMETER_EXPR = namedtuple("PARAMETER_EXPR", ["map_elements", "expr_size"]) +PARAMETER_EXPR_PACK = "!QQ" +PARAMETER_EXPR_SIZE = struct.calcsize(PARAMETER_EXPR_PACK) + +# PARAMETER_EXPR_MAP_ELEM_V3 +PARAM_EXPR_MAP_ELEM_V3 = namedtuple("PARAMETER_EXPR_MAP_ELEM", ["symbol_type", "type", "size"]) +PARAM_EXPR_MAP_ELEM_V3_PACK = "!ccQ" +PARAM_EXPR_MAP_ELEM_V3_SIZE = struct.calcsize(PARAM_EXPR_MAP_ELEM_V3_PACK) + +# PARAMETER_EXPR_MAP_ELEM +PARAM_EXPR_MAP_ELEM = namedtuple("PARAMETER_EXPR_MAP_ELEM", ["type", "size"]) +PARAM_EXPR_MAP_ELEM_PACK = "!cQ" +PARAM_EXPR_MAP_ELEM_SIZE = struct.calcsize(PARAM_EXPR_MAP_ELEM_PACK) + +# RANGE +RANGE = namedtuple("RANGE", ["start", "stop", "step"]) +RANGE_PACK = "!qqq" +RANGE_SIZE = struct.calcsize(RANGE_PACK) + +# SEQUENCE +SEQUENCE = namedtuple("SEQUENCE", ["num_elements"]) +SEQUENCE_PACK = "!Q" +SEQUENCE_SIZE = struct.calcsize(SEQUENCE_PACK) diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py new file mode 100644 index 000000000000..cf4fb8f6a898 --- /dev/null +++ b/qiskit/qpy/interface.py @@ -0,0 +1,156 @@ +# 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. + +"""User interface of qpy serializer.""" + +import struct +import warnings + +from qiskit.circuit import QuantumCircuit +from qiskit.exceptions import QiskitError +from qiskit.qpy import formats, common, binary_io +from qiskit.version import __version__ + + +def dump(circuits, file_obj): + """Write QPY binary data to a file + + This function is used to save a circuit to a file for later use or transfer + between machines. The QPY format is backwards compatible and can be + loaded with future versions of Qiskit. + + For example: + + .. code-block:: python + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit import qpy_serialization + + qc = QuantumCircuit(2, name='Bell', metadata={'test': True}) + qc.h(0) + qc.cx(0, 1) + qc.measure_all() + + from this you can write the qpy data to a file: + + .. code-block:: python + + with open('bell.qpy', 'wb') as fd: + qpy_serialization.dump(qc, fd) + + or a gzip compressed file: + + .. code-block:: python + + import gzip + + with gzip.open('bell.qpy.gz', 'wb') as fd: + qpy_serialization.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 + """ + if isinstance(circuits, QuantumCircuit): + circuits = [circuits] + version_parts = [int(x) for x in __version__.split(".")[0:3]] + header = struct.pack( + formats.FILE_HEADER_PACK, + b"QISKIT", + common.QPY_VERSION, + version_parts[0], + version_parts[1], + version_parts[2], + len(circuits), + ) + file_obj.write(header) + for circuit in circuits: + binary_io.write_circuit(file_obj, circuit) + + +def load(file_obj): + """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. + For example: + + .. code-block:: python + + from qiskit.circuit import qpy_serialization + + with open('bell.qpy', 'rb') as fd: + circuits = qpy_serialization.load(fd) + + or with a gzip compressed file: + + .. code-block:: python + + import gzip + from qiskit.circuit import qpy_serialization + + with gzip.open('bell.qpy.gz', 'rb') as fd: + circuits = qpy_serialization.load(fd) + + which will read the contents of the qpy and return a list of + :class:`~qiskit.circuit.QuantumCircuit` objects from the file. + + Args: + file_obj (File): A file like object that contains the QPY binary + data for a circuit + 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. + Raises: + QiskitError: if ``file_obj`` is not a valid QPY file + """ + data = formats.FILE_HEADER._make( + struct.unpack( + formats.FILE_HEADER_PACK, + file_obj.read(formats.FILE_HEADER_SIZE), + ) + ) + if data.preface.decode(common.ENCODE) != "QISKIT": + raise QiskitError("Input file is not a valid QPY file") + version_parts = [int(x) for x in __version__.split(".")[0:3]] + header_version_parts = [data.major_version, data.minor_version, data.patch_version] + + # pylint: disable=too-many-boolean-expressions + if ( + version_parts[0] < header_version_parts[0] + or ( + version_parts[0] == header_version_parts[0] + and header_version_parts[1] > version_parts[1] + ) + or ( + version_parts[0] == header_version_parts[0] + and header_version_parts[1] == version_parts[1] + and header_version_parts[2] > version_parts[2] + ) + ): + warnings.warn( + "The qiskit version used to generate the provided QPY " + "file, %s, is newer than the current qiskit version %s. " + "This may result in an error if the QPY file uses " + "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)) + return circuits diff --git a/releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml b/releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml new file mode 100644 index 000000000000..d5e7c7f12d30 --- /dev/null +++ b/releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + New module :mod:`qiskit.qpy` has beend added. This functionality for + saving and loading :class:`QuantumCircuit` has been existing + in :mod:`qiskit.circuit.qpy_serialization` but now code is moved to its own module. + This change is aiming at QPY serializing different data types that Qiskit generates, + such as pulse schedules attached to the pulse gates.