Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QPY Qiskit 1.0 updates #1365

Closed
wants to merge 15 commits into from
1 change: 1 addition & 0 deletions qiskit_ibm_runtime/qpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"""

from .interface import dump, load
from .common import QPY_VERSION, QPY_COMPATIBILITY_VERSION

# For backward compatibility. Provide, Runtime, Experiment call these private functions.
from .binary_io import (
Expand Down
22 changes: 11 additions & 11 deletions qiskit_ibm_runtime/qpy/binary_io/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import numpy as np

from qiskit import circuit as circuit_mod
from qiskit import extensions
from qiskit.circuit import library, controlflow, CircuitInstruction, ControlFlowOp
from qiskit.circuit.classical import expr
from qiskit.circuit.classicalregister import ClassicalRegister, Clbit
Expand All @@ -34,8 +33,7 @@
from qiskit.circuit.instruction import Instruction
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.circuit.quantumregister import QuantumRegister, Qubit
from qiskit.extensions import quantum_initializer
from qiskit.quantum_info.operators import SparsePauliOp
from qiskit.quantum_info.operators import SparsePauliOp, Clifford
from qiskit.synthesis import evolution as evo_synth
from qiskit.transpiler.layout import Layout, TranspileLayout
from .. import common, formats, type_keys
Expand Down Expand Up @@ -301,12 +299,10 @@ def _read_instruction( # type: ignore[no-untyped-def]
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)
elif gate_name == "Clifford":
gate_class = Clifford
else:
raise AttributeError("Invalid instruction type: %s" % gate_name)

Expand Down Expand Up @@ -620,15 +616,17 @@ def _dumps_instruction_parameter(param, index_map, use_symengine): # type: igno
def _write_instruction( # type: ignore[no-untyped-def]
file_obj, instruction, custom_operations, index_map, use_symengine
):
gate_class_name = instruction.operation.base_class.__name__
if isinstance(instruction.operation, Instruction):
gate_class_name = instruction.operation.base_class.__name__
else:
gate_class_name = instruction.operation.__class__.__name__
custom_operations_list = []
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)
and not hasattr(controlflow, gate_class_name)
and gate_class_name != "Clifford"
)
or gate_class_name == "Gate"
or gate_class_name == "Instruction"
Expand Down Expand Up @@ -673,7 +671,7 @@ def _write_instruction( # type: ignore[no-untyped-def]
condition_value = int(instruction.operation.condition[1])

gate_class_name = gate_class_name.encode(common.ENCODE)
label = getattr(instruction.operation, "label")
label = getattr(instruction.operation, "label", None)
if label:
label_raw = label.encode(common.ENCODE)
else:
Expand All @@ -686,6 +684,8 @@ def _write_instruction( # type: ignore[no-untyped-def]
instruction.operation.target,
tuple(instruction.operation.cases_specifier()),
]
elif isinstance(instruction.operation, Clifford):
instruction_params = [instruction.operation.tableau]
else:
instruction_params = instruction.operation.params

Expand Down
56 changes: 26 additions & 30 deletions qiskit_ibm_runtime/qpy/binary_io/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,19 @@
from io import BytesIO
import numpy as np

import symengine as sym
from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module
load_basic,
)

from qiskit.pulse import library, channels, instructions
from qiskit.pulse.schedule import ScheduleBlock
from qiskit.utils import optionals as _optional
from qiskit.pulse.configuration import Kernel, Discriminator
from .. import formats, common, type_keys
from ..exceptions import QpyError
from . import value


if _optional.HAS_SYMENGINE:
import symengine as sym
else:
import sympy as sym


def _read_channel(file_obj, version): # type: ignore[no-untyped-def]
type_key = common.read_type_key(file_obj)
index = value.read_value(file_obj, version, {})
Expand Down Expand Up @@ -113,25 +111,17 @@ def _read_discriminator(file_obj, version): # type: ignore[no-untyped-def]
def _loads_symbolic_expr(expr_bytes, use_symengine=False): # type: ignore[no-untyped-def]
if expr_bytes == b"":
return None

expr_bytes = zlib.decompress(expr_bytes)
if use_symengine:
_optional.HAS_SYMENGINE.require_now("load a symengine expression")
from symengine.lib.symengine_wrapper import ( # pylint: disable=import-outside-toplevel, no-name-in-module
load_basic,
)

expr = load_basic(zlib.decompress(expr_bytes))
return load_basic(expr_bytes)

else:
from sympy import parse_expr # pylint: disable=import-outside-toplevel

expr_txt = zlib.decompress(expr_bytes).decode(common.ENCODE)
expr = parse_expr(expr_txt)
if _optional.HAS_SYMENGINE:
from symengine import sympify # pylint: disable=import-outside-toplevel

return sympify(expr)
return expr
return expr


def _read_symbolic_pulse(file_obj, version): # type: ignore[no-untyped-def]
Expand Down Expand Up @@ -167,21 +157,15 @@ def _read_symbolic_pulse(file_obj, version): # type: ignore[no-untyped-def]
class_name = "SymbolicPulse" # Default class name, if not in the library

if pulse_type in legacy_library_pulses:
# Once complex amp support will be deprecated we will need:
# parameters["angle"] = np.angle(parameters["amp"])
# parameters["amp"] = np.abs(parameters["amp"])

# In the meanwhile we simply add:
parameters["angle"] = 0
parameters["angle"] = np.angle(parameters["amp"])
parameters["amp"] = np.abs(parameters["amp"])
_amp, _angle = sym.symbols("amp, angle")
envelope = envelope.subs(_amp, _amp * sym.exp(sym.I * _angle))

# And warn that this will change in future releases:
warnings.warn(
"Complex amp support for symbolic library pulses will be deprecated. "
"Once deprecated, library pulses loaded from old QPY files (Terra version < 0.23),"
" will be converted automatically to float (amp,angle) representation.",
PendingDeprecationWarning,
f"Library pulses with complex amp are no longer supported. "
f"{pulse_type} with complex amp was converted to (amp,angle) representation.",
UserWarning,
)
class_name = "ScalableSymbolicPulse"

Expand Down Expand Up @@ -256,6 +240,19 @@ def _read_symbolic_pulse_v6(file_obj, version, use_symengine): # type: ignore[n
valid_amp_conditions=valid_amp_conditions,
)
elif class_name == "ScalableSymbolicPulse":
# Between Qiskit 0.40 and 0.46, the (amp, angle) representation was present,
# but complex amp was still allowed. In Qiskit 1.0 and beyond complex amp
# is no longer supported and so the amp needs to be checked and converted.
# Once QPY version is bumped, a new reader function can be introduced without
# this check.
if isinstance(parameters["amp"], complex):
parameters["angle"] = np.angle(parameters["amp"])
parameters["amp"] = np.abs(parameters["amp"])
warnings.warn(
f"ScalableSymbolicPulse with complex amp are no longer supported. "
f"{pulse_type} with complex amp was converted to (amp,angle) representation.",
UserWarning,
)
return library.ScalableSymbolicPulse(
pulse_type=pulse_type,
duration=duration,
Expand Down Expand Up @@ -424,7 +421,6 @@ def _dumps_symbolic_expr(expr, use_symengine): # type: ignore[no-untyped-def]
return b""

if use_symengine:
_optional.HAS_SYMENGINE.require_now("dump a symengine expression")
expr_bytes = expr.__reduce__()[1][0]
else:
from sympy import srepr, sympify # pylint: disable=import-outside-toplevel
Expand Down
39 changes: 12 additions & 27 deletions qiskit_ibm_runtime/qpy/binary_io/value.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,23 @@
from typing import Any
import numpy as np

import symengine
from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module
load_basic,
)

from qiskit.circuit import CASE_DEFAULT, Clbit, ClassicalRegister
from qiskit.circuit.classical import expr, types
from qiskit.circuit.parameter import Parameter
from qiskit.circuit.parameterexpression import ParameterExpression
from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement
from qiskit.utils import optionals as _optional

from .. import common, formats, exceptions, type_keys


def _write_parameter(file_obj, obj): # type: ignore[no-untyped-def]
name_bytes = obj._name.encode(common.ENCODE)
file_obj.write(struct.pack(formats.PARAMETER_PACK, len(name_bytes), obj._uuid.bytes))
name_bytes = obj.name.encode(common.ENCODE)
file_obj.write(struct.pack(formats.PARAMETER_PACK, len(name_bytes), obj.uuid.bytes))
file_obj.write(name_bytes)


Expand All @@ -44,7 +48,7 @@ def _write_parameter_vec(file_obj, obj): # type: ignore[no-untyped-def]
formats.PARAMETER_VECTOR_ELEMENT_PACK,
len(name_bytes),
obj._vector._size,
obj._uuid.bytes,
obj.uuid.bytes,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtreinish to be compatible with 0.45, this is causing an error

AttributeError: 'ParameterVectorElement' object has no attribute 'uuid'

Because uuid is added as an attribute to Parameter here, only in 1.0

How should this be handled here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fully porting the qpy changes from qiskit 1.0.0rc1 will inherently be unsound for use with qiskit 0.45.0 or 0.46.0. The internals of the qpy module are updated in lock-step with the QuantumCircuit class. This is basically the same underlying issue we hit that needed: Qiskit/qiskit-ibm-provider#757

This will also start emitting qpy version 11 which will cause a version mismatch with what the server side can parse until it is upgraded to qiskit 1.0.0. I pushed up: #1377 as an alternative to this which will let you do the upgrade in a manner which is compatible with both versions so you can publish a release that is both 0.45.x/0.46.x and 1.0.0 compatible. It also removes the vendored fork completely as it is not needed anymore (and luckily has never been released either, so there is no backwards compatibility issue).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thank you so much for taking care of this!

I'll close this PR

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll create a PR to do the same in qiskit-ibm-provider (but we can't remove the fork completely yet)

obj._index,
)
)
Expand All @@ -53,7 +57,6 @@ def _write_parameter_vec(file_obj, obj): # type: ignore[no-untyped-def]

def _write_parameter_expression(file_obj, obj, use_symengine): # type: ignore[no-untyped-def]
if use_symengine:
_optional.HAS_SYMENGINE.require_now("write_parameter_expression")
expr_bytes = obj._symbol_expr.__reduce__()[1][0]
else:
from sympy import srepr, sympify # pylint: disable=import-outside-toplevel
Expand Down Expand Up @@ -220,7 +223,7 @@ def _read_parameter_vec(file_obj, vectors): # type: ignore[no-untyped-def]
if name not in vectors:
vectors[name] = (ParameterVector(name, data.vector_size), set())
vector = vectors[name][0]
if vector[data.index]._uuid != param_uuid:
if vector[data.index].uuid != param_uuid:
vectors[name][1].add(data.index)
vector._params[data.index] = ParameterVectorElement(vector, data.index, uuid=param_uuid)
return vector[data.index]
Expand All @@ -233,12 +236,7 @@ def _read_parameter_expression(file_obj): # type: ignore[no-untyped-def]
# pylint: disable=import-outside-toplevel
from sympy.parsing.sympy_parser import parse_expr

if _optional.HAS_SYMENGINE:
from symengine import sympify # pylint: disable=import-outside-toplevel

expr_ = sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)))
else:
expr_ = parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))
expr_ = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)))
symbol_map = {}
for _ in range(data.map_elements):
elem_data = formats.PARAM_EXPR_MAP_ELEM(
Expand Down Expand Up @@ -369,26 +367,13 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): # type: ig
data = formats.PARAMETER_EXPR(
*struct.unpack(formats.PARAMETER_EXPR_PACK, file_obj.read(formats.PARAMETER_EXPR_SIZE))
)
# pylint: disable=import-outside-toplevel
from sympy.parsing.sympy_parser import parse_expr

# pylint: disable=import-outside-toplevel

payload = file_obj.read(data.expr_size)
if use_symengine:
_optional.HAS_SYMENGINE.require_now("read_parameter_expression_v3")
from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module
load_basic,
)

expr_ = load_basic(payload)
else:
if _optional.HAS_SYMENGINE:
from symengine import sympify
from sympy.parsing.sympy_parser import parse_expr # pylint: disable=import-outside-toplevel

expr_ = sympify(parse_expr(payload.decode(common.ENCODE)))
else:
expr_ = parse_expr(payload.decode(common.ENCODE))
expr_ = symengine.sympify(parse_expr(payload.decode(common.ENCODE)))
symbol_map = {}
for _ in range(data.map_elements):
elem_data = formats.PARAM_EXPR_MAP_ELEM_V3(
Expand Down
1 change: 1 addition & 0 deletions qiskit_ibm_runtime/qpy/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from . import formats

QPY_VERSION = 10
QPY_COMPATIBILITY_VERSION = 10
ENCODE = "utf8"


Expand Down
6 changes: 6 additions & 0 deletions qiskit_ibm_runtime/qpy/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

"""Exception for errors raised by the pulse module."""
from typing import Any
from qiskit.exceptions import QiskitWarning
kt474 marked this conversation as resolved.
Show resolved Hide resolved
from qiskit.qpy.exceptions import QpyError
from ..exceptions import IBMError

Expand All @@ -27,3 +28,8 @@ def __init__(self, *message: Any):
def __str__(self) -> str:
"""Return the message."""
return repr(self.message)


class QPYLoadingDeprecatedFeatureWarning(QiskitWarning):
"""Visible deprecation warning for QPY loading functions without
a stable point in the call stack."""
31 changes: 29 additions & 2 deletions qiskit_ibm_runtime/qpy/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# that they have been altered from the originals.
"""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,7 +77,8 @@ def dump( # type: ignore[no-untyped-def]
programs: Union[List[QPY_SUPPORTED_TYPES], QPY_SUPPORTED_TYPES],
file_obj: BinaryIO,
metadata_serializer: Optional[Type[JSONEncoder]] = None,
use_symengine: bool = False,
use_symengine: bool = True,
version: int = common.QPY_VERSION,
):
"""Write QPY binary data to a file

Expand Down Expand Up @@ -127,10 +129,26 @@ def dump( # type: ignore[no-untyped-def]
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 @@ -155,13 +173,22 @@ def dump( # type: ignore[no-untyped-def]
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) # type: ignore[no-untyped-call]
header = struct.pack(
formats.FILE_HEADER_V10_PACK, # type: ignore[attr-defined]
b"QISKIT",
common.QPY_VERSION,
version,
version_parts[0],
version_parts[1],
version_parts[2],
Expand Down
Loading