From c59aa4cb6bc8ea8eddf3a01bddd35adccd8a3d53 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 9 Feb 2022 14:22:20 +0900 Subject: [PATCH 1/6] Move qpy to own module. qpy_serialization.py is splint into several files for maintenability. This commit also adds several bytes Enum classes for type keys in the header, and provides several helper functions. Some namedtuple class names are updated because, for example, INSTRUCTION will be vague when we add schedule, i.e. it's basically different program and has own instruction that has different data format. Basically CIRCUIT_ prefix is added to them. --- docs/apidocs/qpy.rst | 4 +- qiskit/circuit/qpy_serialization.py | 1912 +---------------- qiskit/qpy/__init__.py | 551 +++++ qiskit/qpy/common.py | 176 ++ qiskit/qpy/formats.py | 150 ++ qiskit/qpy/interface.py | 157 ++ qiskit/qpy/objects/__init__.py | 13 + qiskit/qpy/objects/alphanumeric.py | 284 +++ qiskit/qpy/objects/circuits.py | 658 ++++++ qiskit/test/base.py | 1 + .../notes/qpy-module-c2ff2cc086b52fc6.yaml | 13 + 11 files changed, 2013 insertions(+), 1906 deletions(-) create mode 100644 qiskit/qpy/__init__.py create mode 100644 qiskit/qpy/common.py create mode 100644 qiskit/qpy/formats.py create mode 100644 qiskit/qpy/interface.py create mode 100644 qiskit/qpy/objects/__init__.py create mode 100644 qiskit/qpy/objects/alphanumeric.py create mode 100644 qiskit/qpy/objects/circuits.py create mode 100644 releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml 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..e1e3489d4b9b 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -10,1913 +10,17 @@ # 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 - -********* -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"], -) -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"], +warnings.warn( + "Importing QPY serialization from qiskit.circuit.qpy_serialization " + "has been deprecated. Use new import path qiskit.qpy instead. " + "This import path will be removed after sufficient deprecation period from 0.20 release.", + DeprecationWarning, ) -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 +from qiskit.qpy import dump, load diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py new file mode 100644 index 000000000000..77faae08ce78 --- /dev/null +++ b/qiskit/qpy/__init__.py @@ -0,0 +1,551 @@ +# 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. + +.. _version_3: + +Version 3 +========= + +Version 3 of the QPY format is identical to :ref:`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:`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:`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:`param_struct`, ``e`` defines a :class:`~qiskit.circuit.ParameterExpression` object +(that's not a :class:`~qiskit.circuit.Parameter`) which is represented by a :ref:`param_expr`. +Following that is ``synthesis_size`` bytes which is a utf8 encoded json payload representing +the :class:`.EvolutionSynthesis` class used by the gate. + +.. _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:`param_vector`. + +.. _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. + +.. _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:`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:`param_struct` +object (both the struct and utf8 name bytes) and if ``symbol_type`` is ``v`` +then the struct is imediately followed by :ref:`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:`complex`. Finally, if type is ``i`` it represents an integer which is an +``int64_t``. + +.. _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:`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). + +.. _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. + +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 +uint32_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. + +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``. + + +.. _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:`param_expr` section. +``'p'`` defines a :class:`~qiskit.circuit.Parameter` object which is +represented by a :ref:`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:`param_expr` +struct (on QPY format :ref:`version_3` the format is tweak slightly see: +:ref:`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:`version_3` ``'v'`` represents a +:class:`~qiskit.circuit.ParameterVectorElement` which is represented by a +:ref:`param_vector` struct. + +.. _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. + +.. _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:`complex`. +Finally, if type is ``i`` it represents an integer which is an ``int64_t``. + +.. _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 diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py new file mode 100644 index 000000000000..558791ecdea6 --- /dev/null +++ b/qiskit/qpy/common.py @@ -0,0 +1,176 @@ +# 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 +from qiskit.qpy import formats + +QPY_VERSION = 3 +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: + TypeError: 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 TypeError(f"Object type {type(obj)} is not supported.") + + +class AlphanumericTypeKey(bytes, Enum): + """Type key enum for alphanumeric object.""" + + INTEGER = b"i" + FLOAT = b"f" + COMPLEX = b"c" + NUMPY_OBJ = b"n" + PARAMETER = b"p" + PARAMETER_VECTOR = b"v" + PARAMETER_EXPRESSION = b"e" + STRING = b"s" + + @classmethod + def assign(cls, obj): + """Assign type key to given object. + + Args: + obj (any): Arbitrary object to evaluate. + + Returns: + AlphanumericTypeKey: Corresponding key object. + + Raises: + TypeError: 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 + + raise TypeError(f"Object type {type(obj)} is not supported.") + + +def read_typed_data(file_obj, key_scope): + """Read a single data chunk from the file like object. + + Args: + file_obj (File): A file like object that contains the QPY binary data. + key_scope (EnumMeta): Expected type of this data. + + Returns: + tuple: Tuple of ``TypeKey`` and the bytes object of the single data. + """ + data = formats.TYPED_OBJECT( + *struct.unpack( + formats.TYPED_OBJECT_PACK, + file_obj.read(formats.TYPED_OBJECT_PACK_SIZE), + ) + ) + + return key_scope(data.type), file_obj.read(data.size) + + +def write_typed_data(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.TYPED_OBJECT_PACK, type_key, len(data_binary)) + file_obj.write(data_header) + file_obj.write(data_binary) + + +def 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) + container.seek(0) + binary_data = container.read() + + return binary_data + + +def from_binary(binary_data, deserializer, **kwargs): + """Load object from binary data with specified serializer. + + 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 diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py new file mode 100644 index 000000000000..7744a8fad9aa --- /dev/null +++ b/qiskit/qpy/formats.py @@ -0,0 +1,150 @@ +# 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 binary format +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 binary format +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 binary format +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 binary format +REGISTER = namedtuple("REGISTER", ["type", "standalone", "size", "name_size"]) +REGISTER_PACK = "!1c?IH" +REGISTER_SIZE = struct.calcsize(REGISTER_PACK) + +# CIRCUIT_INSTRUCTION binary format +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 binary format +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 binary format +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 binary format +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 binary format +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) + +# TYPED_OBJECT binary format +TYPED_OBJECT = namedtuple("TYPED_OBJECT", ["type", "size"]) +TYPED_OBJECT_PACK = "!1cQ" +TYPED_OBJECT_PACK_SIZE = struct.calcsize(TYPED_OBJECT_PACK) + +# PARAMETER binary format +PARAMETER = namedtuple("PARAMETER", ["name_size", "uuid"]) +PARAMETER_PACK = "!H16s" +PARAMETER_SIZE = struct.calcsize(PARAMETER_PACK) + +# COMPLEX binary format +COMPLEX = namedtuple("COMPLEX", ["real", "imag"]) +COMPLEX_PACK = "!dd" +COMPLEX_PACK_SIZE = struct.calcsize(COMPLEX_PACK) + +# PARAMETER_VECTOR_ELEMENT binary format +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 binary format +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 binary format +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) + +# PARAMETER_EXPR_MAP_ELEM binary format +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) diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py new file mode 100644 index 000000000000..d36540d74393 --- /dev/null +++ b/qiskit/qpy/interface.py @@ -0,0 +1,157 @@ +# 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 +from qiskit.qpy.objects import circuits as circuits_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", + 3, + version_parts[0], + version_parts[1], + version_parts[2], + len(circuits), + ) + file_obj.write(header) + for circuit in circuits: + circuits_io.write(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(circuits_io.read(file_obj, data.qpy_version)) + return circuits diff --git a/qiskit/qpy/objects/__init__.py b/qiskit/qpy/objects/__init__.py new file mode 100644 index 000000000000..f8a7a9b4dc6e --- /dev/null +++ b/qiskit/qpy/objects/__init__.py @@ -0,0 +1,13 @@ +# 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.""" diff --git a/qiskit/qpy/objects/alphanumeric.py b/qiskit/qpy/objects/alphanumeric.py new file mode 100644 index 000000000000..4c64489540aa --- /dev/null +++ b/qiskit/qpy/objects/alphanumeric.py @@ -0,0 +1,284 @@ +# 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 alphanumeric objects""" + +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 +from qiskit.qpy.common import AlphanumericTypeKey as TypeKey, ENCODE + +try: + import symengine + + HAS_SYMENGINE = True +except ImportError: + HAS_SYMENGINE = False + + +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.to_binary(symbol, _write_parameter_vec) + else: + symbol_data = common.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) + + elem_header = struct.pack( + formats.PARAM_EXPR_MAP_ELEM_PACK_V3, + 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 HAS_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.from_binary(binary_data, _read_parameter_expression) + else: + raise TypeError("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 HAS_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_PACK_V3, + file_obj.read(formats.PARAM_EXPR_MAP_ELEM_SIZE_V3), + ) + ) + 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 TypeError("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.from_binary(binary_data, _read_parameter_expression_v3, vectors=vectors) + else: + raise TypeError("Invalid parameter expression map type: %s" % elem_key) + symbol_map[symbol] = value + + return ParameterExpression(symbol_map, expr) + + +def dumps(obj): + """Serialize input alphanumeric object. + + Args: + obj (any): Arbitrary alphanumeric object to serialize. + + Returns: + tuple: TypeKey and binary data. + + Raises: + NotImplementedError: 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.to_binary(obj, np.save) + elif type_key == TypeKey.STRING: + binary_data = obj.encode(ENCODE) + elif type_key == TypeKey.PARAMETER_VECTOR: + binary_data = common.to_binary(obj, _write_parameter_vec) + elif type_key == TypeKey.PARAMETER: + binary_data = common.to_binary(obj, _write_parameter) + elif type_key == TypeKey.PARAMETER_EXPRESSION: + binary_data = common.to_binary(obj, _write_parameter_expression) + else: + raise NotImplementedError(f"Serialization for {type_key} is not implemented.") + + return type_key, binary_data + + +def loads(type_key, binary_data, version, vectors): + """Deserialize input binary data to alphanumeric 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 alphanumeric object. + + Raises: + NotImplementedError: Serializer for given format is not ready. + """ + if isinstance(type_key, bytes): + type_key = common.AlphanumericTypeKey(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.from_binary(binary_data, np.load) + elif type_key == TypeKey.STRING: + obj = binary_data.decode(ENCODE) + elif type_key == TypeKey.PARAMETER_VECTOR: + obj = common.from_binary(binary_data, _read_parameter_vec, vectors=vectors) + elif type_key == TypeKey.PARAMETER: + obj = common.from_binary(binary_data, _read_parameter) + elif type_key == TypeKey.PARAMETER_EXPRESSION: + if version < 3: + obj = common.from_binary(binary_data, _read_parameter_expression) + else: + obj = common.from_binary(binary_data, _read_parameter_expression_v3, vectors=vectors) + else: + raise NotImplementedError(f"Serialization for {type_key} is not implemented.") + + return obj diff --git a/qiskit/qpy/objects/circuits.py b/qiskit/qpy/objects/circuits.py new file mode 100644 index 000000000000..edd2b2925af7 --- /dev/null +++ b/qiskit/qpy/objects/circuits.py @@ -0,0 +1,658 @@ +# 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 +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.exceptions import QiskitError +from qiskit.extensions import quantum_initializer +from qiskit.qpy import common, formats +from qiskit.qpy.objects import alphanumeric +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 = alphanumeric.loads( + 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(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) + else: + registers["c"][name] = (data.standalone, bit_indices) + return registers + + +def _read_instruction(file_obj, circuit, registers, custom_instructions, version, vectors): + instruction = formats.CIRCUIT_INSTRUCTION._make( + struct.unpack( + formats.CIRCUIT_INSTRUCTION_PACK, + file_obj.read(formats.CIRCUIT_INSTRUCTION_SIZE), + ) + ) + gate_name = file_obj.read(instruction.name_size).decode(common.ENCODE) + label = file_obj.read(instruction.label_size).decode(common.ENCODE) + condition_register = file_obj.read(instruction.condition_register_size).decode(common.ENCODE) + qargs = [] + cargs = [] + params = [] + condition_tuple = None + if instruction.has_condition: + # If an invalid register name is used assume it's a single bit + # condition and treat the register name as a string of the clbit index + if ClassicalRegister.name_format.match(condition_register) is None: + # If invalid register prefixed with null character it's a clbit + # index for single bit condition + if condition_register[0] == "\x00": + conditional_bit = int(condition_register[1:]) + condition_tuple = (circuit.clbits[conditional_bit], instruction.condition_value) + else: + raise ValueError( + f"Invalid register name: {condition_register} for condition register of " + f"instruction: {gate_name}" + ) + else: + condition_tuple = (registers["c"][condition_register], instruction.condition_value) + qubit_indices = dict(enumerate(circuit.qubits)) + clbit_indices = dict(enumerate(circuit.clbits)) + + # Load Arguments + for _qarg in range(instruction.num_qargs): + qarg = formats.CIRCUIT_INSTRUCTION_ARG._make( + struct.unpack( + formats.CIRCUIT_INSTRUCTION_ARG_PACK, + file_obj.read(formats.CIRCUIT_INSTRUCTION_ARG_SIZE), + ) + ) + if qarg.type.decode(common.ENCODE) == "c": + raise TypeError("Invalid input carg prior to all qargs") + qargs.append(qubit_indices[qarg.size]) + for _carg in range(instruction.num_cargs): + carg = formats.CIRCUIT_INSTRUCTION_ARG._make( + struct.unpack( + formats.CIRCUIT_INSTRUCTION_ARG_PACK, + file_obj.read(formats.CIRCUIT_INSTRUCTION_ARG_SIZE), + ) + ) + if carg.type.decode(common.ENCODE) == "q": + raise TypeError("Invalid input qarg after all qargs") + cargs.append(clbit_indices[carg.size]) + + # Load Parameters + for _param in range(instruction.num_parameters): + type_key, data = common.read_typed_data(file_obj, common.AlphanumericTypeKey) + # TODO This uses little endian. Should be fixed in the next QPY version. + if type_key == common.AlphanumericTypeKey.INTEGER: + 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) + else: + raise AttributeError("Invalid instruction type: %s" % gate_name) + if gate_name == "Initialize": + gate = gate_class(params) + else: + if gate_name == "Barrier": + params = [len(qargs)] + 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.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 = alphanumeric.loads( + common.AlphanumericTypeKey(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.from_binary(def_binary, read, version=version) + elif name.startswith(r"###PauliEvolutionGate_"): + definition_circuit = common.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 + + +# pylint: disable=too-many-boolean-expressions +def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_map): + gate_class_name = instruction_tuple[0].__class__.__name__ + if ( + ( + not hasattr(library, gate_class_name) + and not hasattr(circuit_mod, gate_class_name) + and not hasattr(extensions, gate_class_name) + and not hasattr(quantum_initializer, gate_class_name) + ) + or gate_class_name == "Gate" + or gate_class_name == "Instruction" + or isinstance(instruction_tuple[0], library.BlueprintCircuit) + ): + if instruction_tuple[0].name not in custom_instructions: + custom_instructions[instruction_tuple[0].name] = instruction_tuple[0] + gate_class_name = instruction_tuple[0].name + + elif isinstance(instruction_tuple[0], library.PauliEvolutionGate): + gate_class_name = r"###PauliEvolutionGate_" + str(uuid.uuid4()) + custom_instructions[gate_class_name] = instruction_tuple[0] + + has_condition = False + condition_register = b"" + condition_value = 0 + if instruction_tuple[0].condition: + has_condition = True + if isinstance(instruction_tuple[0].condition[0], Clbit): + bit_index = index_map["c"][instruction_tuple[0].condition[0]] + condition_register = b"\x00" + str(bit_index).encode(common.ENCODE) + condition_value = int(instruction_tuple[0].condition[1]) + else: + condition_register = instruction_tuple[0].condition[0].name.encode(common.ENCODE) + condition_value = instruction_tuple[0].condition[1] + + gate_class_name = gate_class_name.encode(common.ENCODE) + label = getattr(instruction_tuple[0], "label") + if label: + label_raw = label.encode(common.ENCODE) + else: + label_raw = b"" + instruction_raw = struct.pack( + formats.CIRCUIT_INSTRUCTION_PACK, + len(gate_class_name), + len(label_raw), + len(instruction_tuple[0].params), + instruction_tuple[0].num_qubits, + instruction_tuple[0].num_clbits, + has_condition, + len(condition_register), + condition_value, + ) + file_obj.write(instruction_raw) + file_obj.write(gate_class_name) + file_obj.write(label_raw) + file_obj.write(condition_register) + # Encode instruciton args + for qbit in instruction_tuple[1]: + instruction_arg_raw = struct.pack( + formats.CIRCUIT_INSTRUCTION_ARG_PACK, b"q", index_map["q"][qbit] + ) + file_obj.write(instruction_arg_raw) + for clbit in instruction_tuple[2]: + instruction_arg_raw = struct.pack( + formats.CIRCUIT_INSTRUCTION_ARG_PACK, b"c", index_map["c"][clbit] + ) + file_obj.write(instruction_arg_raw) + # Encode instruction params + for param in instruction_tuple[0].params: + # TODO This uses little endian. This should be fixed in next QPY version. + if isinstance(param, int): + type_key = common.AlphanumericTypeKey.INTEGER + data = struct.pack(" 0: + for reg in circuit.qregs: + standalone = all(bit._register is reg for bit in reg) + reg_name = reg.name.encode(common.ENCODE) + file_obj.write( + struct.pack(formats.REGISTER_PACK, b"q", standalone, reg.size, len(reg_name)) + ) + file_obj.write(reg_name) + REGISTER_ARRAY_PACK = "!%sI" % reg.size + file_obj.write(struct.pack(REGISTER_ARRAY_PACK, *(qubit_indices[bit] for bit in reg))) + for reg in circuit.cregs: + standalone = all(bit._register is reg for bit in reg) + reg_name = reg.name.encode(common.ENCODE) + file_obj.write( + struct.pack(formats.REGISTER_PACK, b"c", standalone, reg.size, len(reg_name)) + ) + file_obj.write(reg_name) + REGISTER_ARRAY_PACK = "!%sI" % reg.size + file_obj.write(struct.pack(REGISTER_ARRAY_PACK, *(clbit_indices[bit] for bit in reg))) + 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(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(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: + QiskitError: 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) + registers = _read_registers(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 = registers[bit_type_label][register_name] + if standalone: + start = min(indices) + count = start + out_of_order = False + for index in indices: + if not out_of_order and index != count: + out_of_order = True + count += 1 + if index in register_bits: + 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: + sorted_indices = np.argsort(indices) + for index in sorted_indices: + pos = indices[index] + if bit_type_label == "q": + bit_len = len(circ.qubits) + else: + bit_len = len(circ.clbits) + # 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]]) + 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) + 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) + 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/test/base.py b/qiskit/test/base.py index a7765afbc1d2..95c1cae3510a 100644 --- a/qiskit/test/base.py +++ b/qiskit/test/base.py @@ -56,6 +56,7 @@ class BaseTestCase(testtools.TestCase): assertRaises = unittest.TestCase.assertRaises assertEqual = unittest.TestCase.assertEqual + else: class BaseTestCase(unittest.TestCase): diff --git a/releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml b/releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml new file mode 100644 index 000000000000..2812b80b2913 --- /dev/null +++ b/releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml @@ -0,0 +1,13 @@ +--- +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. +deprecations: + - | + Importing QPY :func:`dump` abd :func:`load` from :mod:`qiskit.circuit.qpy_serialization` + has been deprecated. New import path is :mod:`qiskit.qpy`. Old import path + will be removed after sufficient deprecation period from this relase. \ No newline at end of file From b5bd17b06246879e21e3bc2c19d21574fc6604ad Mon Sep 17 00:00:00 2001 From: knzwnao Date: Thu, 27 Jan 2022 01:45:24 +0900 Subject: [PATCH 2/6] manually cherry-pick #7584 with some cleanup - change qiskit.qpy.objects -> qiskit.qpy.binary_io - TUPLE -> SEQUENCE (we may use this for list in future) - add QpyError - add _write_register in circuit io to remove boilerplate code --- qiskit/circuit/qpy_serialization.py | 2 +- qiskit/qpy/__init__.py | 139 ++++++--- qiskit/qpy/{objects => binary_io}/__init__.py | 3 + .../{objects => binary_io}/alphanumeric.py | 62 ++-- qiskit/qpy/{objects => binary_io}/circuits.py | 264 +++++++++++++----- qiskit/qpy/common.py | 148 ++++++++-- qiskit/qpy/exceptions.py | 27 ++ qiskit/qpy/formats.py | 58 ++-- qiskit/qpy/interface.py | 9 +- qiskit/test/base.py | 1 - 10 files changed, 534 insertions(+), 179 deletions(-) rename qiskit/qpy/{objects => binary_io}/__init__.py (77%) rename qiskit/qpy/{objects => binary_io}/alphanumeric.py (80%) rename qiskit/qpy/{objects => binary_io}/circuits.py (74%) create mode 100644 qiskit/qpy/exceptions.py diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index e1e3489d4b9b..5a9df1a72c1c 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -20,7 +20,7 @@ "Importing QPY serialization from qiskit.circuit.qpy_serialization " "has been deprecated. Use new import path qiskit.qpy instead. " "This import path will be removed after sufficient deprecation period from 0.20 release.", - DeprecationWarning, + ImportWarning, ) from qiskit.qpy import dump, load diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 77faae08ce78..6b2f58d2d94f 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -98,14 +98,81 @@ by ``num_circuits`` in the file header). There is no padding between the circuits in the data. -.. _version_3: +.. _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:`version_2` except that it defines +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:`custom_definition` struct now supports +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 @@ -130,18 +197,18 @@ uint64_t synthesis_size; } -This is immediately followed by ``operator_count`` elements defined by the :ref:`pauli_sum_op` +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:`param_struct`, ``e`` defines a :class:`~qiskit.circuit.ParameterExpression` object -(that's not a :class:`~qiskit.circuit.Parameter`) which is represented by a :ref:`param_expr`. +: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. -.. _pauli_sum_op: +.. _qpy_pauli_sum_op: SPARSE_PAULI_OP_LIST_ELEM ------------------------- @@ -163,10 +230,9 @@ 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:`param_vector`. - -.. _param_vector: +parameters are defined below as :ref:`qpy_param_vector`. +.. _qpy_param_vector: PARAMETER_VECTOR_ELEMENT ------------------------ @@ -187,7 +253,7 @@ which is immediately followed by ``vector_name_size`` utf8 bytes representing the parameter's vector name. -.. _param_expr_v3: +.. _qpy_param_expr_v3: PARAMETER_EXPR @@ -197,7 +263,7 @@ :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:`version_2` but just +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` @@ -228,9 +294,9 @@ 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:`param_struct` +``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:`param_vector` (both the struct +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 @@ -239,16 +305,16 @@ 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:`complex`. Finally, if type is ``i`` it represents an integer which is an +:ref:`qpy_complex`. Finally, if type is ``i`` it represents an integer which is an ``int64_t``. -.. _version_2: +.. _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:`version_1` section +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 @@ -280,7 +346,7 @@ :class:`~qiskit.circuit.Parameter`) which is represented by a PARAM_EXPR struct (see below). -.. _version_1: +.. _qpy_version_1: Version 1 ========= @@ -311,9 +377,11 @@ 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 +you then read the ``metadata_size`` number of bytes and parse the JSON to get the metadata for the circuit. +.. _qpy_registers: + REGISTERS --------- @@ -334,10 +402,17 @@ 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 -uint32_t values of size ``size`` that contains a map of the register's index to +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 @@ -355,7 +430,7 @@ ``qr`` would have ``standalone`` set to ``False``. -.. _custom_definition: +.. _qpy_custom_definition: CUSTOM_DEFINITIONS ------------------ @@ -455,19 +530,19 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom 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:`param_expr` section. +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:`param_struct` struct, ``e`` defines a +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:`param_expr` -struct (on QPY format :ref:`version_3` the format is tweak slightly see: -:ref:`param_expr_v3`), ``'n'`` represents an object from numpy (either an +: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:`version_3` ``'v'`` represents a +and in QPY :ref:`qpy_version_3` ``'v'`` represents a :class:`~qiskit.circuit.ParameterVectorElement` which is represented by a -:ref:`param_vector` struct. +:ref:`qpy_param_vector` struct. -.. _param_struct: +.. _qpy_param_struct: PARAMETER --------- @@ -485,7 +560,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom which is immediately followed by ``name_size`` utf8 bytes representing the parameter name. -.. _param_expr: +.. _qpy_param_expr: PARAMETER_EXPR -------------- @@ -521,10 +596,10 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom ``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:`complex`. +``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``. -.. _complex: +.. _qpy_complex: COMPLEX ------- diff --git a/qiskit/qpy/objects/__init__.py b/qiskit/qpy/binary_io/__init__.py similarity index 77% rename from qiskit/qpy/objects/__init__.py rename to qiskit/qpy/binary_io/__init__.py index f8a7a9b4dc6e..53665b10c7a5 100644 --- a/qiskit/qpy/objects/__init__.py +++ b/qiskit/qpy/binary_io/__init__.py @@ -11,3 +11,6 @@ # that they have been altered from the originals. """Read and write QPY-serializable objects.""" + +from .alphanumeric import dumps as dumps_alphanumeric, loads as load_alphanumeric +from .circuits import write as write_circuit, read as read_circuit diff --git a/qiskit/qpy/objects/alphanumeric.py b/qiskit/qpy/binary_io/alphanumeric.py similarity index 80% rename from qiskit/qpy/objects/alphanumeric.py rename to qiskit/qpy/binary_io/alphanumeric.py index 4c64489540aa..1326ca2dca6b 100644 --- a/qiskit/qpy/objects/alphanumeric.py +++ b/qiskit/qpy/binary_io/alphanumeric.py @@ -20,7 +20,7 @@ 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 +from qiskit.qpy import common, formats, exceptions from qiskit.qpy.common import AlphanumericTypeKey as TypeKey, ENCODE try: @@ -66,9 +66,9 @@ def _write_parameter_expression(file_obj, obj): # serialize key if symbol_key == TypeKey.PARAMETER_VECTOR: - symbol_data = common.to_binary(symbol, _write_parameter_vec) + symbol_data = common.data_to_binary(symbol, _write_parameter_vec) else: - symbol_data = common.to_binary(symbol, _write_parameter) + symbol_data = common.data_to_binary(symbol, _write_parameter) # serialize value if value == symbol._symbol_expr: @@ -78,7 +78,7 @@ def _write_parameter_expression(file_obj, obj): value_key, value_data = dumps(value) elem_header = struct.pack( - formats.PARAM_EXPR_MAP_ELEM_PACK_V3, + formats.PARAM_EXPR_MAP_ELEM_V3_PACK, symbol_key, value_key, len(value_data), @@ -151,9 +151,9 @@ def _read_parameter_expression(file_obj): elif elem_key == TypeKey.PARAMETER: value = symbol._symbol_expr elif elem_key == TypeKey.PARAMETER_EXPRESSION: - value = common.from_binary(binary_data, _read_parameter_expression) + value = common.data_from_binary(binary_data, _read_parameter_expression) else: - raise TypeError("Invalid parameter expression map type: %s" % elem_key) + raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) symbol_map[symbol] = value return ParameterExpression(symbol_map, expr) @@ -173,8 +173,8 @@ def _read_parameter_expression_v3(file_obj, vectors): for _ in range(data.map_elements): elem_data = formats.PARAM_EXPR_MAP_ELEM_V3( *struct.unpack( - formats.PARAM_EXPR_MAP_ELEM_PACK_V3, - file_obj.read(formats.PARAM_EXPR_MAP_ELEM_SIZE_V3), + formats.PARAM_EXPR_MAP_ELEM_V3_PACK, + file_obj.read(formats.PARAM_EXPR_MAP_ELEM_V3_SIZE), ) ) symbol_key = TypeKey(elem_data.symbol_type) @@ -184,7 +184,7 @@ def _read_parameter_expression_v3(file_obj, vectors): elif symbol_key == TypeKey.PARAMETER_VECTOR: symbol = _read_parameter_vec(file_obj, vectors) else: - raise TypeError("Invalid parameter expression map type: %s" % symbol_key) + 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) @@ -197,9 +197,11 @@ def _read_parameter_expression_v3(file_obj, vectors): elif elem_key in (TypeKey.PARAMETER, TypeKey.PARAMETER_VECTOR): value = symbol._symbol_expr elif elem_key == TypeKey.PARAMETER_EXPRESSION: - value = common.from_binary(binary_data, _read_parameter_expression_v3, vectors=vectors) + value = common.data_from_binary( + binary_data, _read_parameter_expression_v3, vectors=vectors + ) else: - raise TypeError("Invalid parameter expression map type: %s" % elem_key) + raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) symbol_map[symbol] = value return ParameterExpression(symbol_map, expr) @@ -215,7 +217,7 @@ def dumps(obj): tuple: TypeKey and binary data. Raises: - NotImplementedError: Serializer for given format is not ready. + QpyError: Serializer for given format is not ready. """ type_key = TypeKey.assign(obj) @@ -226,17 +228,21 @@ def dumps(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.to_binary(obj, np.save) + 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.to_binary(obj, _write_parameter_vec) + binary_data = common.data_to_binary(obj, _write_parameter_vec) elif type_key == TypeKey.PARAMETER: - binary_data = common.to_binary(obj, _write_parameter) + binary_data = common.data_to_binary(obj, _write_parameter) elif type_key == TypeKey.PARAMETER_EXPRESSION: - binary_data = common.to_binary(obj, _write_parameter_expression) + binary_data = common.data_to_binary(obj, _write_parameter_expression) else: - raise NotImplementedError(f"Serialization for {type_key} is not implemented.") + raise exceptions.QpyError( + f"Serialization for {type_key} is not implemented in alphanumeric I/O." + ) return type_key, binary_data @@ -254,10 +260,10 @@ def loads(type_key, binary_data, version, vectors): any: Deserialized alphanumeric object. Raises: - NotImplementedError: Serializer for given format is not ready. + QpyError: Serializer for given format is not ready. """ if isinstance(type_key, bytes): - type_key = common.AlphanumericTypeKey(type_key) + type_key = TypeKey(type_key) if type_key == TypeKey.INTEGER: obj = struct.unpack("!q", binary_data)[0] @@ -266,19 +272,25 @@ def loads(type_key, binary_data, version, vectors): elif type_key == TypeKey.COMPLEX: obj = complex(*struct.unpack(formats.COMPLEX_PACK, binary_data)) elif type_key == TypeKey.NUMPY_OBJ: - obj = common.from_binary(binary_data, np.load) + 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.from_binary(binary_data, _read_parameter_vec, vectors=vectors) + obj = common.data_from_binary(binary_data, _read_parameter_vec, vectors=vectors) elif type_key == TypeKey.PARAMETER: - obj = common.from_binary(binary_data, _read_parameter) + obj = common.data_from_binary(binary_data, _read_parameter) elif type_key == TypeKey.PARAMETER_EXPRESSION: if version < 3: - obj = common.from_binary(binary_data, _read_parameter_expression) + obj = common.data_from_binary(binary_data, _read_parameter_expression) else: - obj = common.from_binary(binary_data, _read_parameter_expression_v3, vectors=vectors) + obj = common.data_from_binary( + binary_data, _read_parameter_expression_v3, vectors=vectors + ) else: - raise NotImplementedError(f"Serialization for {type_key} is not implemented.") + raise exceptions.QpyError( + f"Serialization for {type_key} is not implemented in alphanumeric I/O." + ) return obj diff --git a/qiskit/qpy/objects/circuits.py b/qiskit/qpy/binary_io/circuits.py similarity index 74% rename from qiskit/qpy/objects/circuits.py rename to qiskit/qpy/binary_io/circuits.py index edd2b2925af7..94138050226b 100644 --- a/qiskit/qpy/objects/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -24,16 +24,15 @@ from qiskit import circuit as circuit_mod from qiskit import extensions -from qiskit.circuit import library +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.exceptions import QiskitError from qiskit.extensions import quantum_initializer -from qiskit.qpy import common, formats -from qiskit.qpy.objects import alphanumeric +from qiskit.qpy import common, formats, exceptions +from qiskit.qpy.binary_io import alphanumeric from qiskit.quantum_info.operators import SparsePauliOp from qiskit.synthesis import evolution as evo_synth @@ -81,6 +80,26 @@ def _read_header(file_obj): 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): @@ -95,12 +114,41 @@ def _read_registers(file_obj, num_registers): 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) + registers["q"][name] = (data.standalone, bit_indices, True) else: - registers["c"][name] = (data.standalone, bit_indices) + 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, 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.AlphanumericTypeKey.INTEGER: + # TODO This uses little endian. Should be fixed in the next QPY version. + param = struct.unpack(" 0: gate.label = label if not isinstance(gate, Instruction): @@ -248,7 +297,7 @@ def _read_pauli_evolution_gate(file_obj, version, vectors): file_obj.read(formats.SPARSE_PAULI_OP_LIST_ELEM_SIZE), ) ) - op_raw_data = common.from_binary(file_obj.read(op_elem.size), np.load) + 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: @@ -290,9 +339,9 @@ def _read_custom_instructions(file_obj, version, vectors): if data.custom_definition: def_binary = file_obj.read(data.size) if version < 3 or not name.startswith(r"###PauliEvolutionGate_"): - definition_circuit = common.from_binary(def_binary, read, version=version) + definition_circuit = common.data_from_binary(def_binary, read, version=version) elif name.startswith(r"###PauliEvolutionGate_"): - definition_circuit = common.from_binary( + definition_circuit = common.data_from_binary( def_binary, _read_pauli_evolution_gate, version=version, vectors=vectors ) custom_instructions[name] = ( @@ -304,6 +353,29 @@ def _read_custom_instructions(file_obj, version, vectors): 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) + 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.AlphanumericTypeKey.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(file_obj, circuit): """Write a single QuantumCircuit object in the file like object. @@ -466,11 +569,17 @@ def write(file_obj, circuit): """ metadata_raw = json.dumps(circuit.metadata, separators=(",", ":")).encode(common.ENCODE) metadata_size = len(metadata_raw) - num_registers = len(circuit.qregs) + len(circuit.cregs) num_instructions = len(circuit) circuit_name = circuit.name.encode(common.ENCODE) global_phase_type, global_phase_data = alphanumeric.dumps(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, @@ -486,32 +595,13 @@ def write(file_obj, circuit): file_obj.write(circuit_name) file_obj.write(global_phase_data) file_obj.write(metadata_raw) - qubit_indices = {bit: index for index, bit in enumerate(circuit.qubits)} - clbit_indices = {bit: index for index, bit in enumerate(circuit.clbits)} - if num_registers > 0: - for reg in circuit.qregs: - standalone = all(bit._register is reg for bit in reg) - reg_name = reg.name.encode(common.ENCODE) - file_obj.write( - struct.pack(formats.REGISTER_PACK, b"q", standalone, reg.size, len(reg_name)) - ) - file_obj.write(reg_name) - REGISTER_ARRAY_PACK = "!%sI" % reg.size - file_obj.write(struct.pack(REGISTER_ARRAY_PACK, *(qubit_indices[bit] for bit in reg))) - for reg in circuit.cregs: - standalone = all(bit._register is reg for bit in reg) - reg_name = reg.name.encode(common.ENCODE) - file_obj.write( - struct.pack(formats.REGISTER_PACK, b"c", standalone, reg.size, len(reg_name)) - ) - file_obj.write(reg_name) - REGISTER_ARRAY_PACK = "!%sI" % reg.size - file_obj.write(struct.pack(REGISTER_ARRAY_PACK, *(clbit_indices[bit] for bit in reg))) + # Write header payload + file_obj.write(registers_raw) instruction_buffer = io.BytesIO() custom_instructions = {} index_map = {} - index_map["q"] = qubit_indices - index_map["c"] = clbit_indices + 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))) @@ -535,7 +625,7 @@ def read(file_obj, version): QuantumCircuit: The circuit object from the file. Raises: - QiskitError: Invalid register. + QpyError: Invalid register. """ vectors = {} if version < 2: @@ -551,7 +641,10 @@ def read(file_obj, version): out_registers = {"q": {}, "c": {}} if num_registers > 0: circ = QuantumCircuit(name=name, global_phase=global_phase, metadata=metadata) - registers = _read_registers(file_obj, num_registers) + 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), @@ -560,17 +653,30 @@ def read(file_obj, version): register_bits = set() # Add quantum registers and bits for register_name in registers[bit_type_label]: - standalone, indices = registers[bit_type_label][register_name] + 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) + 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: - raise QiskitError("Duplicate register bits found") + # 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) @@ -579,21 +685,29 @@ def read(file_obj, version): 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: - sorted_indices = np.argsort(indices) - for index in sorted_indices: - pos = indices[index] + 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]]) - circ.add_register(reg) + if in_circuit: + circ.add_register(reg) else: if bit_type_label == "q": bit_len = len(circ.qubits) @@ -604,7 +718,8 @@ def read(file_obj, version): if start > len(circ.qubits): bits = [bit_type() for _ in range(start - bit_len)] circ.add_bits(bit_len) - circ.add_register(reg) + if in_circuit: + circ.add_register(reg) out_registers[bit_type_label][register_name] = reg else: for index in indices: @@ -616,14 +731,15 @@ def read(file_obj, version): 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") + 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) - circ.add_register(reg) + 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 diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 558791ecdea6..7c896db01617 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -26,10 +26,10 @@ 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 -from qiskit.qpy import formats +from qiskit.circuit import Gate, Instruction as CircuitInstruction, QuantumCircuit +from qiskit.qpy import formats, exceptions -QPY_VERSION = 3 +QPY_VERSION = 4 ENCODE = "utf8" @@ -51,7 +51,7 @@ def assign(cls, obj): CircuitInstructionTypeKey: Corresponding key object. Raises: - TypeError: if object type is not defined in QPY. Likely not supported. + QpyError: if object type is not defined in QPY. Likely not supported. """ if isinstance(obj, PauliEvolutionGate): return cls.PAULI_EVOL_GATE @@ -60,7 +60,9 @@ def assign(cls, obj): if isinstance(obj, CircuitInstruction): return cls.INSTRUCTION - raise TypeError(f"Object type {type(obj)} is not supported.") + raise exceptions.QpyError( + f"Object type {type(obj)} is not supported in {cls.__name__} namespace." + ) class AlphanumericTypeKey(bytes, Enum): @@ -74,6 +76,7 @@ class AlphanumericTypeKey(bytes, Enum): PARAMETER_VECTOR = b"v" PARAMETER_EXPRESSION = b"e" STRING = b"s" + NULL = b"z" @classmethod def assign(cls, obj): @@ -86,7 +89,7 @@ def assign(cls, obj): AlphanumericTypeKey: Corresponding key object. Raises: - TypeError: if object type is not defined in QPY. Likely not supported. + QpyError: if object type is not defined in QPY. Likely not supported. """ if isinstance(obj, int): return cls.INTEGER @@ -104,31 +107,89 @@ def assign(cls, obj): 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 TypeError(f"Object type {type(obj)} is not supported.") + raise exceptions.QpyError( + f"Object type {type(obj)} is not supported in {cls.__name__} namespace." + ) -def read_typed_data(file_obj, key_scope): +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. - key_scope (EnumMeta): Expected type of this data. Returns: - tuple: Tuple of ``TypeKey`` and the bytes object of the single data. + tuple: Tuple of type key binary and the bytes object of the single data. """ - data = formats.TYPED_OBJECT( + data = formats.INSTRUCTION_PARAM( *struct.unpack( - formats.TYPED_OBJECT_PACK, - file_obj.read(formats.TYPED_OBJECT_PACK_SIZE), + formats.INSTRUCTION_PARAM_PACK, + file_obj.read(formats.INSTRUCTION_PARAM_SIZE), ) ) - return key_scope(data.type), file_obj.read(data.size) + return data.type, file_obj.read(data.size) -def write_typed_data(file_obj, type_key, data_binary): +def write_instruction_param(file_obj, type_key, data_binary): """Write statically typed binary data to the file like object. Args: @@ -136,12 +197,12 @@ def write_typed_data(file_obj, type_key, data_binary): type_key (Enum): Object type of the data. data_binary (bytes): Binary data to write. """ - data_header = struct.pack(formats.TYPED_OBJECT_PACK, type_key, len(data_binary)) + data_header = struct.pack(formats.INSTRUCTION_PARAM_PACK, type_key, len(data_binary)) file_obj.write(data_header) file_obj.write(data_binary) -def to_binary(obj, serializer, **kwargs): +def data_to_binary(obj, serializer, **kwargs): """Convert object into binary data with specified serializer. Args: @@ -160,8 +221,31 @@ def to_binary(obj, serializer, **kwargs): return binary_data -def from_binary(binary_data, deserializer, **kwargs): - """Load object from binary data with specified serializer. +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) + container.seek(0) + binary_data = container.read() + + 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. @@ -174,3 +258,29 @@ def from_binary(binary_data, deserializer, **kwargs): 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 index 7744a8fad9aa..ba8f900a1c9e 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -18,7 +18,7 @@ from collections import namedtuple -# FILE_HEADER binary format +# FILE_HEADER FILE_HEADER = namedtuple( "FILE_HEADER", ["preface", "qpy_version", "major_version", "minor_version", "patch_version", "num_circuits"], @@ -26,7 +26,7 @@ FILE_HEADER_PACK = "!6sBBBBQ" FILE_HEADER_SIZE = struct.calcsize(FILE_HEADER_PACK) -# CIRCUIT_HEADER_V2 binary format +# CIRCUIT_HEADER_V2 CIRCUIT_HEADER_V2 = namedtuple( "HEADER", [ @@ -43,7 +43,7 @@ CIRCUIT_HEADER_V2_PACK = "!H1cHIIQIQ" CIRCUIT_HEADER_V2_SIZE = struct.calcsize(CIRCUIT_HEADER_V2_PACK) -# CIRCUIT_HEADER binary format +# CIRCUIT_HEADER CIRCUIT_HEADER = namedtuple( "HEADER", [ @@ -59,12 +59,16 @@ CIRCUIT_HEADER_PACK = "!HdIIQIQ" CIRCUIT_HEADER_SIZE = struct.calcsize(CIRCUIT_HEADER_PACK) -# REGISTER binary format +# 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 binary format +# CIRCUIT_INSTRUCTION CIRCUIT_INSTRUCTION = namedtuple( "CIRCUIT_INSTRUCTION", [ @@ -81,17 +85,17 @@ CIRCUIT_INSTRUCTION_PACK = "!HHHII?Hq" CIRCUIT_INSTRUCTION_SIZE = struct.calcsize(CIRCUIT_INSTRUCTION_PACK) -# CIRCUIT_INSTRUCTION_ARG binary format +# 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 binary format +# 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 binary format +# Pauli Evolution Gate PAULI_EVOLUTION_DEF = namedtuple( "PAULI_EVOLUTION_DEF", ["operator_size", "standalone_op", "time_type", "time_size", "synth_method_size"], @@ -99,7 +103,7 @@ PAULI_EVOLUTION_DEF_PACK = "!Q?1cQQ" PAULI_EVOLUTION_DEF_SIZE = struct.calcsize(PAULI_EVOLUTION_DEF_PACK) -# CUSTOM_CIRCUIT_DEF_HEADER binary format +# 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) @@ -112,39 +116,49 @@ CUSTOM_CIRCUIT_INST_DEF_PACK = "!H1cII?Q" CUSTOM_CIRCUIT_INST_DEF_SIZE = struct.calcsize(CUSTOM_CIRCUIT_INST_DEF_PACK) -# TYPED_OBJECT binary format -TYPED_OBJECT = namedtuple("TYPED_OBJECT", ["type", "size"]) -TYPED_OBJECT_PACK = "!1cQ" -TYPED_OBJECT_PACK_SIZE = struct.calcsize(TYPED_OBJECT_PACK) +# INSTRUCTION_PARAM +INSTRUCTION_PARAM = namedtuple("TYPED_OBJECT", ["type", "size"]) +INSTRUCTION_PARAM_PACK = "!1cQ" +INSTRUCTION_PARAM_SIZE = struct.calcsize(INSTRUCTION_PARAM_PACK) -# PARAMETER binary format +# PARAMETER PARAMETER = namedtuple("PARAMETER", ["name_size", "uuid"]) PARAMETER_PACK = "!H16s" PARAMETER_SIZE = struct.calcsize(PARAMETER_PACK) -# COMPLEX binary format +# COMPLEX COMPLEX = namedtuple("COMPLEX", ["real", "imag"]) COMPLEX_PACK = "!dd" -COMPLEX_PACK_SIZE = struct.calcsize(COMPLEX_PACK) +COMPLEX_SIZE = struct.calcsize(COMPLEX_PACK) -# PARAMETER_VECTOR_ELEMENT binary format +# 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 binary format +# 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 binary format +# 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) +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 binary format +# 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 index d36540d74393..cf4fb8f6a898 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -17,8 +17,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.exceptions import QiskitError -from qiskit.qpy import formats, common -from qiskit.qpy.objects import circuits as circuits_io +from qiskit.qpy import formats, common, binary_io from qiskit.version import __version__ @@ -71,7 +70,7 @@ def dump(circuits, file_obj): header = struct.pack( formats.FILE_HEADER_PACK, b"QISKIT", - 3, + common.QPY_VERSION, version_parts[0], version_parts[1], version_parts[2], @@ -79,7 +78,7 @@ def dump(circuits, file_obj): ) file_obj.write(header) for circuit in circuits: - circuits_io.write(file_obj, circuit) + binary_io.write_circuit(file_obj, circuit) def load(file_obj): @@ -153,5 +152,5 @@ def load(file_obj): ) circuits = [] for _ in range(data.num_circuits): - circuits.append(circuits_io.read(file_obj, data.qpy_version)) + circuits.append(binary_io.read_circuit(file_obj, data.qpy_version)) return circuits diff --git a/qiskit/test/base.py b/qiskit/test/base.py index 95c1cae3510a..a7765afbc1d2 100644 --- a/qiskit/test/base.py +++ b/qiskit/test/base.py @@ -56,7 +56,6 @@ class BaseTestCase(testtools.TestCase): assertRaises = unittest.TestCase.assertRaises assertEqual = unittest.TestCase.assertEqual - else: class BaseTestCase(unittest.TestCase): From 83e4c43dc04545636cd239303b1005e334f2f7ed Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 16 Feb 2022 04:26:38 +0900 Subject: [PATCH 3/6] respond to review comments - expose several private methods for backward compatibility - use options for symengine - rename alphanumeric -> value - rename write, read methods and remove alias - improve container read --- qiskit/qpy/__init__.py | 9 ++++++ qiskit/qpy/binary_io/__init__.py | 18 ++++++++++-- qiskit/qpy/binary_io/circuits.py | 28 ++++++++++--------- .../binary_io/{alphanumeric.py => value.py} | 24 ++++++++-------- qiskit/qpy/common.py | 6 ++-- 5 files changed, 53 insertions(+), 32 deletions(-) rename qiskit/qpy/binary_io/{alphanumeric.py => value.py} (96%) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 6b2f58d2d94f..2ede88a790eb 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -624,3 +624,12 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom """ 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 index 53665b10c7a5..69ae4b7d651e 100644 --- a/qiskit/qpy/binary_io/__init__.py +++ b/qiskit/qpy/binary_io/__init__.py @@ -12,5 +12,19 @@ """Read and write QPY-serializable objects.""" -from .alphanumeric import dumps as dumps_alphanumeric, loads as load_alphanumeric -from .circuits import write as write_circuit, read as read_circuit +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 index 94138050226b..c8862fe2491f 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -32,7 +32,7 @@ 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 alphanumeric +from qiskit.qpy.binary_io import value from qiskit.quantum_info.operators import SparsePauliOp from qiskit.synthesis import evolution as evo_synth @@ -45,7 +45,7 @@ def _read_header_v2(file_obj, version, vectors): ) ) name = file_obj.read(data.name_size).decode(common.ENCODE) - global_phase = alphanumeric.loads( + global_phase = value.loads_value( data.global_phase_type, file_obj.read(data.global_phase_size), version=version, @@ -124,7 +124,7 @@ 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, version=version) + 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) @@ -144,7 +144,7 @@ def _read_instruction_parameter(file_obj, version, vectors): # TODO This uses little endian. Should be fixed in the next QPY version. param = struct.unpack(" Date: Wed, 16 Feb 2022 05:04:44 +0900 Subject: [PATCH 4/6] remove import warning --- qiskit/circuit/qpy_serialization.py | 10 +--------- releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml | 5 ----- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index 5a9df1a72c1c..c308503d0a16 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -14,13 +14,5 @@ """Alias for Qiskit QPY import.""" -import warnings - -warnings.warn( - "Importing QPY serialization from qiskit.circuit.qpy_serialization " - "has been deprecated. Use new import path qiskit.qpy instead. " - "This import path will be removed after sufficient deprecation period from 0.20 release.", - ImportWarning, -) - +# TODO deprecate this in 0.21.0 from qiskit.qpy import dump, load diff --git a/releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml b/releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml index 2812b80b2913..d5e7c7f12d30 100644 --- a/releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml +++ b/releasenotes/notes/qpy-module-c2ff2cc086b52fc6.yaml @@ -6,8 +6,3 @@ features: 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. -deprecations: - - | - Importing QPY :func:`dump` abd :func:`load` from :mod:`qiskit.circuit.qpy_serialization` - has been deprecated. New import path is :mod:`qiskit.qpy`. Old import path - will be removed after sufficient deprecation period from this relase. \ No newline at end of file From 72a262eebd7807077e78cf624f726341762470db Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 16 Feb 2022 05:10:47 +0900 Subject: [PATCH 5/6] replace alphanumeric with value in comments and messages. --- qiskit/qpy/binary_io/circuits.py | 10 +++++----- qiskit/qpy/binary_io/value.py | 18 +++++++----------- qiskit/qpy/common.py | 6 +++--- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index c8862fe2491f..ee5efb5dc450 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -137,10 +137,10 @@ def _read_instruction_parameter(file_obj, version, vectors): vectors=vectors, ) ) - elif type_key == common.AlphanumericTypeKey.INTEGER: + elif type_key == common.ValueTypeKey.INTEGER: # TODO This uses little endian. Should be fixed in the next QPY version. param = struct.unpack(" Date: Sun, 20 Feb 2022 14:09:51 +0900 Subject: [PATCH 6/6] private functions import --- qiskit/circuit/qpy_serialization.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index c308503d0a16..d20acbc51b15 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -16,3 +16,12 @@ # TODO deprecate this in 0.21.0 from qiskit.qpy import dump, load + +# 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, +)