Skip to content

Commit

Permalink
Add version flag to qpy dump (#11644)
Browse files Browse the repository at this point in the history
* Add version flag to qpy dump

This commit adds a new keyword argument to the qpy.dump() function which
is used to specify a compatibility version of QPY which is the latest
version for the 0.45 and 0.46 version. This basically adds a compatibility
mode for generating QPY files that returns a fixed QPY version that
enables the 0.45 and 0.46 release to load the QPY files generated with
the 1.0 release. The default output doesn't change and dump() will
always return the latest QPY format version by default as this is the
version that gets all the bugfixes and is kept up to date with the
QuantumCircuit class.

As of this commit the compatibility version and the latest version are
the same, both at version 10. However, this flag is being added so that
the interface exists for when we inevitably need to update the QPY
version field for any release in the 1.x major version series. There are
potential PRs in the 1.0 release series that will bump the latest QPY
version to 11 which will change this and those PRs will need to factor
this in when they change the format.

* Export current and compatibility version from qiskit.qpy

* Update test to use QPY_COMPATIBILITY_VERSION

Co-authored-by: Will Shanks <willshanks@us.ibm.com>

* Revise support policy to be a range not just min/max versions

* Add attribute documentation

* Remove hardcoded reference to current QPY_VERSION

---------

Co-authored-by: Will Shanks <willshanks@us.ibm.com>
  • Loading branch information
mtreinish and wshanks authored Jan 31, 2024
1 parent 82f15d7 commit 3b82fea
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 4 deletions.
14 changes: 14 additions & 0 deletions qiskit/qpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@
.. autoexception:: QpyError
Attributes:
QPY_VERSION (int): The current QPY format version as of this release. This
is the default value of the ``version`` keyword argument on
:func:`.qpy.dump` and also the upper bound for accepted values for
the same argument. This is also the upper bond on the versions supported
by :func:`.qpy.load`.
QPY_COMPATIBILITY_VERSION (int): The current minimum compatibility QPY
format version. This is the minimum version that :func:`.qpy.dump`
will accept for the ``version`` keyword argument. :func:`.qpy.load`
will be able to load all released format versions of QPY (up until
``QPY_VERSION``).
QPY Compatibility
=================
Expand Down Expand Up @@ -1352,3 +1365,4 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom
_read_parameter_expression,
_read_parameter_expression_v3,
)
from .common import QPY_VERSION, QPY_COMPATIBILITY_VERSION
1 change: 1 addition & 0 deletions qiskit/qpy/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from qiskit.qpy import formats

QPY_VERSION = 10
QPY_COMPATIBILITY_VERSION = 10
ENCODE = "utf8"


Expand Down
30 changes: 29 additions & 1 deletion qiskit/qpy/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

"""User interface of qpy serializer."""

from __future__ import annotations

from json import JSONEncoder, JSONDecoder
from typing import Union, List, BinaryIO, Type, Optional
from collections.abc import Iterable
Expand Down Expand Up @@ -76,6 +78,7 @@ def dump(
file_obj: BinaryIO,
metadata_serializer: Optional[Type[JSONEncoder]] = None,
use_symengine: bool = True,
version: int = common.QPY_VERSION,
):
"""Write QPY binary data to a file
Expand Down Expand Up @@ -126,9 +129,25 @@ def dump(
but not supported in all platforms. Please check that your target platform is supported
by the symengine library before setting this option, as it will be required by qpy to
deserialize the payload. For this reason, the option defaults to False.
version: The QPY format version to emit. By default this defaults to
the latest supported format of :attr:`~.qpy.QPY_VERSION`, however for
compatibility reasons if you need to load the generated QPY payload with an older
version of Qiskit you can also select an older QPY format version down to the minimum
supported export version, which only can change during a Qiskit major version release,
to generate an older QPY format version. You can access the current QPY version and
minimum compatible version with :attr:`.qpy.QPY_VERSION` and
:attr:`.qpy.QPY_COMPATIBILITY_VERSION` respectively.
.. note::
If specified with an older version of QPY the limitations and potential bugs stemming
from the QPY format at that version will persist. This should only be used if
compatibility with loading the payload with an older version of Qiskit is necessary.
Raises:
QpyError: When multiple data format is mixed in the output.
TypeError: When invalid data type is input.
ValueError: When an unsupported version number is passed in for the ``version`` argument
"""
if not isinstance(programs, Iterable):
programs = [programs]
Expand All @@ -153,13 +172,22 @@ def dump(
else:
raise TypeError(f"'{program_type}' is not supported data type.")

if version is None:
version = common.QPY_VERSION
elif common.QPY_COMPATIBILITY_VERSION > version or version > common.QPY_VERSION:
raise ValueError(
f"The specified QPY version {version} is not support for dumping with this version, "
f"of Qiskit. The only supported versions between {common.QPY_COMPATIBILITY_VERSION} and "
f"{common.QPY_VERSION}"
)

version_match = VERSION_PATTERN_REGEX.search(__version__)
version_parts = [int(x) for x in version_match.group("release").split(".")]
encoding = type_keys.SymExprEncoding.assign(use_symengine)
header = struct.pack(
formats.FILE_HEADER_V10_PACK,
b"QISKIT",
common.QPY_VERSION,
version,
version_parts[0],
version_parts[1],
version_parts[2],
Expand Down
15 changes: 15 additions & 0 deletions releasenotes/notes/add-qpy-version-flag-6bb1756e671fde55.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
features:
- |
Added a new flag, ``version``, to the :func:`.qpy.dump` function which
optionally takes an integer value for the :ref:`qpy_format` version to
emit from the dump function. This is useful if you need to generate a QPY
file that will be loaded by an older version of Qiskit. However, the
supported versions to emit are limited, only versions between the latest
QPY version (which is the default), and the compatibility QPY version
which is :ref:`qpy_version_10` (which was introduced in Qiskit 0.45.0) can
be used. The compatibility version will remain fixed for the the entire
1.x.y major version release series. This does not change the backwards
compatibility guarantees of the QPY format when calling :func:`.qpy.load`,
it just enables users to emit an older version of QPY to maintain
compatibility and interoperability between the 0.x and 1.x release series.
28 changes: 25 additions & 3 deletions test/python/qpy/test_circuit_load_from_qpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit
from qiskit.providers.fake_provider import FakeHanoi, FakeSherbrooke
from qiskit.exceptions import QiskitError
from qiskit.qpy import dump, load, formats
from qiskit.qpy import dump, load, formats, QPY_COMPATIBILITY_VERSION
from qiskit.qpy.common import QPY_VERSION
from qiskit.test import QiskitTestCase
from qiskit.transpiler import PassManager, TranspileLayout
Expand All @@ -31,15 +31,23 @@
class QpyCircuitTestCase(QiskitTestCase):
"""QPY schedule testing platform."""

def assert_roundtrip_equal(self, circuit):
def assert_roundtrip_equal(self, circuit, version=None):
"""QPY roundtrip equal test."""
qpy_file = io.BytesIO()
dump(circuit, qpy_file)
dump(circuit, qpy_file, version=version)
qpy_file.seek(0)
new_circuit = load(qpy_file)[0]

self.assertEqual(circuit, new_circuit)
self.assertEqual(circuit.layout, new_circuit.layout)
if version is not None:
qpy_file.seek(0)
file_version = struct.unpack("!6sB", qpy_file.read(7))[1]
self.assertEqual(
version,
file_version,
f"Generated QPY file version {file_version} does not match request version {version}",
)


@ddt
Expand Down Expand Up @@ -185,3 +193,17 @@ def test_no_register(self, opt_level):
list(new_circuit.layout.input_qubit_mapping.values()),
)
self.assertEqual(tqc.layout.final_layout, new_circuit.layout.final_layout)

def test_invalid_version_value(self):
"""Assert we raise an error with an invalid version request."""
qc = QuantumCircuit(2)
with self.assertRaises(ValueError):
dump(qc, io.BytesIO(), version=3)

def test_compatibility_version_roundtrip(self):
"""Test the version is set correctly when specified."""
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
self.assert_roundtrip_equal(qc, version=QPY_COMPATIBILITY_VERSION)

0 comments on commit 3b82fea

Please sign in to comment.