diff --git a/qiskit_ibm_runtime/qpy/__init__.py b/qiskit_ibm_runtime/qpy/__init__.py index 14c75d674..bcf3b5c99 100644 --- a/qiskit_ibm_runtime/qpy/__init__.py +++ b/qiskit_ibm_runtime/qpy/__init__.py @@ -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 ( diff --git a/qiskit_ibm_runtime/qpy/binary_io/circuits.py b/qiskit_ibm_runtime/qpy/binary_io/circuits.py index f51f48b7d..299473a77 100644 --- a/qiskit_ibm_runtime/qpy/binary_io/circuits.py +++ b/qiskit_ibm_runtime/qpy/binary_io/circuits.py @@ -30,10 +30,17 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.singleton import SingletonInstruction, SingletonGate from qiskit.circuit.controlledgate import ControlledGate +from qiskit.circuit.annotated_operation import ( + AnnotatedOperation, + Modifier, + InverseModifier, + ControlModifier, + PowerModifier, +) from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister, Qubit -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 @@ -130,6 +137,8 @@ def _loads_instruction_parameter( # type: ignore[no-untyped-def] ): if type_key == type_keys.Program.CIRCUIT: param = common.data_from_binary(data_bytes, read_circuit, version=version) + elif type_key == type_keys.Value.MODIFIER: + param = common.data_from_binary(data_bytes, _read_modifier) elif type_key == type_keys.Container.RANGE: data = formats.RANGE._make(struct.unpack(formats.RANGE_PACK, data_bytes)) param = range(data.start, data.stop, data.step) @@ -301,6 +310,8 @@ def _read_instruction( # type: ignore[no-untyped-def] gate_class = getattr(circuit_mod, 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) @@ -389,6 +400,9 @@ def _parse_custom_operation( # type: ignore[no-untyped-def] ) = custom_operations[gate_name] else: type_str, num_qubits, num_clbits, definition = custom_operations[gate_name] + # Strip the trailing "_{uuid}" from the gate name if the version >=11 + if version >= 11: + gate_name = "_".join(gate_name.split("_")[:-1]) type_key = type_keys.CircuitInstruction(type_str) if type_key == type_keys.CircuitInstruction.INSTRUCTION: @@ -427,6 +441,14 @@ def _parse_custom_operation( # type: ignore[no-untyped-def] inst_obj.definition = definition return inst_obj + if version >= 11 and type_key == type_keys.CircuitInstruction.ANNOTATED_OPERATION: + with io.BytesIO(base_gate_raw) as base_gate_obj: + base_gate = _read_instruction( + base_gate_obj, None, registers, custom_operations, version, vectors, use_symengine + ) + inst_obj = AnnotatedOperation(base_op=base_gate, modifiers=params) + return inst_obj + if type_key == type_keys.CircuitInstruction.PAULI_EVOL_GATE: return definition @@ -473,6 +495,25 @@ def _read_pauli_evolution_gate(file_obj, version, vectors): # type: ignore[no-u return return_gate +def _read_modifier(file_obj): # type: ignore[no-untyped-def] + modifier = formats.MODIFIER_DEF._make( + struct.unpack( + formats.MODIFIER_DEF_PACK, + file_obj.read(formats.MODIFIER_DEF_SIZE), + ) + ) + if modifier.type == b"i": + return InverseModifier() + elif modifier.type == b"c": + return ControlModifier( + num_ctrl_qubits=modifier.num_ctrl_qubits, ctrl_state=modifier.ctrl_state + ) + elif modifier.type == b"p": + return PowerModifier(power=modifier.power) + else: + raise TypeError("Unsupported modifier.") + + def _read_custom_operations(file_obj, version, vectors): # type: ignore[no-untyped-def] custom_operations = {} custom_definition_header = formats.CUSTOM_CIRCUIT_DEF_HEADER._make( @@ -580,6 +621,9 @@ def _dumps_instruction_parameter(param, index_map, use_symengine): # type: igno if isinstance(param, QuantumCircuit): type_key = type_keys.Program.CIRCUIT data_bytes = common.data_to_binary(param, write_circuit) + elif isinstance(param, Modifier): + type_key = type_keys.Value.MODIFIER + data_bytes = common.data_to_binary(param, _write_modifier) elif isinstance(param, range): type_key = type_keys.Container.RANGE data_bytes = struct.pack(formats.RANGE_PACK, param.start, param.stop, param.step) @@ -612,15 +656,19 @@ def _dumps_instruction_parameter(param, index_map, use_symengine): # type: igno # pylint: disable=too-many-boolean-expressions def _write_instruction( # type: ignore[no-untyped-def] - file_obj, instruction, custom_operations, index_map, use_symengine + file_obj, instruction, custom_operations, index_map, use_symengine, version ): - 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(controlflow, gate_class_name) + and gate_class_name != "Clifford" ) or gate_class_name == "Gate" or gate_class_name == "Instruction" @@ -630,18 +678,21 @@ def _write_instruction( # type: ignore[no-untyped-def] custom_operations[instruction.operation.name] = instruction.operation custom_operations_list.append(instruction.operation.name) gate_class_name = instruction.operation.name + + if version >= 11: + # Assign a uuid to each instance of a custom operation + gate_class_name = f"{gate_class_name}_{uuid.uuid4().hex}" # ucr*_dg gates can have different numbers of parameters, # the uuid is appended to avoid storing a single definition # in circuits with multiple ucr*_dg gates. - if instruction.operation.name in ["ucrx_dg", "ucry_dg", "ucrz_dg"]: - gate_class_name += "_" + str(uuid.uuid4()) + elif instruction.operation.name in {"ucrx_dg", "ucry_dg", "ucrz_dg"}: + gate_class_name = f"{gate_class_name}_{uuid.uuid4()}" - if gate_class_name not in custom_operations: - custom_operations[gate_class_name] = instruction.operation - custom_operations_list.append(gate_class_name) + custom_operations[gate_class_name] = instruction.operation + custom_operations_list.append(gate_class_name) - elif gate_class_name == "ControlledGate": - # controlled gates can have the same name but different parameter + elif gate_class_name in {"ControlledGate", "AnnotatedOperation"}: + # controlled or annotated gates can have the same name but different parameter # values, the uuid is appended to avoid storing a single definition # in circuits with multiple controlled gates. gate_class_name = instruction.operation.name + "_" + str(uuid.uuid4()) @@ -665,7 +716,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: @@ -678,8 +729,12 @@ 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] + elif isinstance(instruction.operation, AnnotatedOperation): + instruction_params = instruction.operation.modifiers else: - instruction_params = instruction.operation.params + instruction_params = getattr(instruction.operation, "params", []) num_ctrl_qubits = getattr(instruction.operation, "num_ctrl_qubits", 0) ctrl_state = getattr(instruction.operation, "ctrl_state", 0) @@ -761,8 +816,33 @@ def _write_elem(buffer, op): # type: ignore[no-untyped-def] file_obj.write(synth_data) +def _write_modifier(file_obj, modifier): # type: ignore[no-untyped-def] + if isinstance(modifier, InverseModifier): + type_key = b"i" + num_ctrl_qubits = 0 + ctrl_state = 0 + power = 0.0 + elif isinstance(modifier, ControlModifier): + type_key = b"c" + num_ctrl_qubits = modifier.num_ctrl_qubits + ctrl_state = modifier.ctrl_state + power = 0.0 + elif isinstance(modifier, PowerModifier): + type_key = b"p" + num_ctrl_qubits = 0 + ctrl_state = 0 + power = modifier.power + else: + raise TypeError("Unsupported modifier.") + + modifier_data = struct.pack( + formats.MODIFIER_DEF_PACK, type_key, num_ctrl_qubits, ctrl_state, power + ) + file_obj.write(modifier_data) + + def _write_custom_operation( # type: ignore[no-untyped-def] - file_obj, name, operation, custom_operations, use_symengine + file_obj, name, operation, custom_operations, use_symengine, version ): type_key = type_keys.CircuitInstruction.assign(operation) has_definition = False @@ -793,6 +873,9 @@ def _write_custom_operation( # type: ignore[no-untyped-def] num_ctrl_qubits = operation.num_ctrl_qubits ctrl_state = operation.ctrl_state base_gate = operation.base_gate + elif type_key == type_keys.CircuitInstruction.ANNOTATED_OPERATION: + has_definition = False + base_gate = operation.base_op elif operation.definition is not None: has_definition = True data = common.data_to_binary(operation.definition, write_circuit) @@ -807,6 +890,7 @@ def _write_custom_operation( # type: ignore[no-untyped-def] custom_operations, {}, use_symengine, + version, ) base_gate_raw = base_gate_buffer.getvalue() name_raw = name.encode(common.ENCODE) @@ -1045,7 +1129,7 @@ def _read_layout_v2(file_obj, circuit): # type: ignore[no-untyped-def] def write_circuit( # type: ignore[no-untyped-def] - file_obj, circuit, metadata_serializer=None, use_symengine=False + file_obj, circuit, metadata_serializer=None, use_symengine=False, version=common.QPY_VERSION ): """Write a single QuantumCircuit object in the file like object. @@ -1060,6 +1144,7 @@ def write_circuit( # type: ignore[no-untyped-def] native mechanism. This is a faster serialization alternative, 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. + version (int): The QPY format version to use for serializing this circuit """ metadata_raw = json.dumps( circuit.metadata, separators=(",", ":"), cls=metadata_serializer @@ -1100,7 +1185,7 @@ def write_circuit( # type: ignore[no-untyped-def] index_map["c"] = {bit: index for index, bit in enumerate(circuit.clbits)} for instruction in circuit.data: _write_instruction( - instruction_buffer, instruction, custom_operations, index_map, use_symengine + instruction_buffer, instruction, custom_operations, index_map, use_symengine, version ) with io.BytesIO() as custom_operations_buffer: @@ -1117,6 +1202,7 @@ def write_circuit( # type: ignore[no-untyped-def] operation, custom_operations, use_symengine, + version, ) ) diff --git a/qiskit_ibm_runtime/qpy/binary_io/schedules.py b/qiskit_ibm_runtime/qpy/binary_io/schedules.py index 183bb50df..6a1499868 100644 --- a/qiskit_ibm_runtime/qpy/binary_io/schedules.py +++ b/qiskit_ibm_runtime/qpy/binary_io/schedules.py @@ -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, {}) @@ -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] @@ -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" @@ -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, @@ -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 @@ -602,8 +598,8 @@ def read_schedule_block( # type: ignore[no-untyped-def] def write_schedule_block( # type: ignore[no-untyped-def] - file_obj, block, metadata_serializer=None, use_symengine=False -): + file_obj, block, metadata_serializer=None, use_symengine=False, version=common.QPY_VERSION +): # pylint: disable=unused-argument """Write a single ScheduleBlock object in the file like object. Args: @@ -618,7 +614,7 @@ def write_schedule_block( # type: ignore[no-untyped-def] native mechanism. This is a faster serialization alternative, 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. - + version (int): The QPY format version to use for serializing this circuit block Raises: TypeError: If any of the instructions is invalid data format. """ diff --git a/qiskit_ibm_runtime/qpy/binary_io/value.py b/qiskit_ibm_runtime/qpy/binary_io/value.py index ca89d7ab3..dd21b7370 100644 --- a/qiskit_ibm_runtime/qpy/binary_io/value.py +++ b/qiskit_ibm_runtime/qpy/binary_io/value.py @@ -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) @@ -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, obj._index, ) ) @@ -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 @@ -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] @@ -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( @@ -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( diff --git a/qiskit_ibm_runtime/qpy/common.py b/qiskit_ibm_runtime/qpy/common.py index dce023f93..a2573da88 100644 --- a/qiskit_ibm_runtime/qpy/common.py +++ b/qiskit_ibm_runtime/qpy/common.py @@ -23,6 +23,7 @@ from . import formats QPY_VERSION = 10 +QPY_COMPATIBILITY_VERSION = 10 ENCODE = "utf8" diff --git a/qiskit_ibm_runtime/qpy/exceptions.py b/qiskit_ibm_runtime/qpy/exceptions.py index a537ed9a1..e1847e871 100644 --- a/qiskit_ibm_runtime/qpy/exceptions.py +++ b/qiskit_ibm_runtime/qpy/exceptions.py @@ -12,6 +12,11 @@ """Exception for errors raised by the pulse module.""" from typing import Any + +try: + from qiskit.exceptions import QiskitWarning +except ImportError: + QiskitWarning = UserWarning from qiskit.qpy.exceptions import QpyError from ..exceptions import IBMError @@ -27,3 +32,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.""" diff --git a/qiskit_ibm_runtime/qpy/formats.py b/qiskit_ibm_runtime/qpy/formats.py index 133a931b4..e7a287c63 100644 --- a/qiskit_ibm_runtime/qpy/formats.py +++ b/qiskit_ibm_runtime/qpy/formats.py @@ -147,6 +147,11 @@ PAULI_EVOLUTION_DEF_PACK = "!Q?1cQQ" PAULI_EVOLUTION_DEF_SIZE = struct.calcsize(PAULI_EVOLUTION_DEF_PACK) +# Modifier +MODIFIER_DEF = namedtuple("MODIFIER_DEF", ["type", "num_ctrl_qubits", "ctrl_state", "power"]) +MODIFIER_DEF_PACK = "!1cIId" +MODIFIER_DEF_SIZE = struct.calcsize(MODIFIER_DEF_PACK) + # CUSTOM_CIRCUIT_DEF_HEADER CUSTOM_CIRCUIT_DEF_HEADER = namedtuple("CUSTOM_CIRCUIT_DEF_HEADER", ["size"]) CUSTOM_CIRCUIT_DEF_HEADER_PACK = "!Q" diff --git a/qiskit_ibm_runtime/qpy/interface.py b/qiskit_ibm_runtime/qpy/interface.py index 6a77697d6..c90fe528a 100644 --- a/qiskit_ibm_runtime/qpy/interface.py +++ b/qiskit_ibm_runtime/qpy/interface.py @@ -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 @@ -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 @@ -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] @@ -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], @@ -177,6 +204,7 @@ def dump( # type: ignore[no-untyped-def] program, metadata_serializer=metadata_serializer, use_symengine=use_symengine, + version=version, ) diff --git a/qiskit_ibm_runtime/qpy/type_keys.py b/qiskit_ibm_runtime/qpy/type_keys.py index 4c5b424d5..c250cc8db 100644 --- a/qiskit_ibm_runtime/qpy/type_keys.py +++ b/qiskit_ibm_runtime/qpy/type_keys.py @@ -32,6 +32,7 @@ ) from qiskit.circuit.library import PauliEvolutionGate from qiskit.circuit.parameter import Parameter +from qiskit.circuit.annotated_operation import AnnotatedOperation, Modifier from qiskit.circuit.classical import expr, types from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.parametervector import ParameterVectorElement @@ -107,6 +108,7 @@ class Value(TypeKeyBase): STRING = b"s" NULL = b"z" EXPRESSION = b"x" + MODIFIER = b"m" CASE_DEFAULT = b"d" REGISTER = b"R" @@ -136,6 +138,8 @@ def assign(cls, obj): # type: ignore[no-untyped-def] return cls.CASE_DEFAULT if isinstance(obj, expr.Expr): return cls.EXPRESSION + if isinstance(obj, Modifier): + return cls.MODIFIER raise exceptions.QpyError( f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." @@ -187,6 +191,7 @@ class CircuitInstruction(TypeKeyBase): GATE = b"g" PAULI_EVOL_GATE = b"p" CONTROLLED_GATE = b"c" + ANNOTATED_OPERATION = b"a" @classmethod def assign(cls, obj): # type: ignore[no-untyped-def] @@ -194,6 +199,8 @@ def assign(cls, obj): # type: ignore[no-untyped-def] return cls.PAULI_EVOL_GATE if isinstance(obj, ControlledGate): return cls.CONTROLLED_GATE + if isinstance(obj, AnnotatedOperation): + return cls.ANNOTATED_OPERATION if isinstance(obj, Gate): return cls.GATE if isinstance(obj, Instruction): diff --git a/requirements-dev.txt b/requirements-dev.txt index 03f4af164..97a774089 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,6 +14,7 @@ coverage>=6.3 pylatexenc scikit-learn ddt>=1.2.0,!=1.4.0,!=1.4.3 +symengine>=0.11 # Documentation nbsphinx