From 87e2a936b7b7397aae23a8f20d8df85d9dd3b2ed Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 22 Oct 2024 11:27:11 -0400 Subject: [PATCH] Add Qiskit native QPY ParameterExpression serialization With the release of symengine 0.13.0 we discovered a version dependence on the payload format used for serializing symengine expressions. This was worked around in #13251 but this is not a sustainable solution and only works for symengine 0.11.0 and 0.13.0 (there was no 0.12.0). While there was always the option to use sympy to serialize the underlying symbolic expression (there is a `use_symengine` flag on `qpy.dumps` you can set to `False` to do this) the sympy serialzation has several tradeoffs most importantly is much higher runtime overhead. To solve the issue moving forward a qiskit native representation of the parameter expression object is necessary for serialization. This commit bumps the QPY format version to 13 and adds a new serialization format for ParameterExpression objects. This new format is a serialization of the API calls made to ParameterExpression that resulted in the creation of the underlying object. To facilitate this the ParameterExpression class is expanded to store an internal "replay" record of the API calls used to construct the ParameterExpression object. This internal list is what gets serialized by QPY and then on deserialization the "replay" is replayed to reconstruct the expression object. This is a different approach to the previous QPY representations of the ParameterExpression objects which instead represented the internal state stored in the ParameterExpression object with the symbolic expression from symengine (or a sympy copy of the expression). Doing this directly in Qiskit isn't viable though because symengine's internal expression tree is not exposed to Python directly. There isn't any method (private or public) to walk the expression tree to construct a serialization format based off of it. Converting symengine to a sympy expression and then using sympy's API to walk the expression tree is a possibility but that would tie us to sympy which would be problematic for #13267 and #13131, have significant runtime overhead, and it would be just easier to rely on sympy's native serialization tools. The tradeoff with this approach is that it does increase the memory overhead of the `ParameterExpression` class because for each element in the expression we have to store a record of it. Depending on the depth of the expression tree this also could be a lot larger than symengine's internal representation as we store the raw api calls made to create the ParameterExpression but symengine is likely simplifying it's internal representation as it builds it out. But I personally think this tradeoff is worthwhile as it ties the serialization format to the Qiskit objects instead of relying on a 3rd party library. This also gives us the flexibility of changing the internal symbolic expression library internally in the future if we decide to stop using symengine at any point. Fixes #13252 --- qiskit/circuit/parameter.py | 2 + qiskit/circuit/parameterexpression.py | 166 ++++++++++--- qiskit/qpy/binary_io/value.py | 344 +++++++++++++++++++++++++- qiskit/qpy/common.py | 2 +- qiskit/qpy/formats.py | 12 + 5 files changed, 483 insertions(+), 43 deletions(-) diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index c7a8228dd463..8df6f86c8372 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -87,6 +87,7 @@ def __init__( self._hash = hash((self._parameter_keys, self._symbol_expr)) self._parameter_symbols = {self: symbol} self._name_map = None + self._qpy_replay = [] def assign(self, parameter, value): if parameter != self: @@ -172,3 +173,4 @@ def __setstate__(self, state): self._hash = hash((self._parameter_keys, self._symbol_expr)) self._parameter_symbols = {self: self._symbol_expr} self._name_map = None + self._qpy_replay = [] diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 16b691480d26..1cc1155896dc 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -14,6 +14,9 @@ """ from __future__ import annotations + +from dataclasses import dataclass +from enum import IntEnum from typing import Callable, Union import numbers @@ -30,12 +33,79 @@ ParameterValueType = Union["ParameterExpression", float] +class _OPCode(IntEnum): + ADD = 0 + SUB = 1 + MUL = 2 + DIV = 3 + POW = 4 + SIN = 5 + COS = 6 + TAN = 7 + ASIN = 8 + ACOS = 9 + EXP = 10 + LOG = 11 + SIGN = 12 + DERIV = 13 + CONJ = 14 + SUBSTITUTE = 15 + ABS = 16 + ATAN = 17 + + +_OP_CODE_MAP = ( + "__add__", + "__sub__", + "__mul__", + "__truediv__", + "__pow__", + "sin", + "cos", + "tan", + "arcsin", + "arccos", + "exp", + "log", + "sign", + "gradient", + "conjugate", + "subs", + "abs", + "arctan", +) + + +def op_code_to_method(op_code: _OPCode): + """Return the method name for a given op_code.""" + return _OP_CODE_MAP[op_code] + + +@dataclass +class _INSTRUCTION: + op: _OPCode + lhs: ParameterValueType + rhs: ParameterValueType | None = None + + +@dataclass +class _SUBS: + binds: dict + op: _OPCode = _OPCode.SUBSTITUTE + + class ParameterExpression: """ParameterExpression class to enable creating expressions of Parameters.""" - __slots__ = ["_parameter_symbols", "_parameter_keys", "_symbol_expr", "_name_map"] + __slots__ = [ + "_parameter_symbols", + "_parameter_keys", + "_symbol_expr", + "_name_map", + "_qpy_replay", + ] - def __init__(self, symbol_map: dict, expr): + def __init__(self, symbol_map: dict, expr, *, _qpy_replay=None): """Create a new :class:`ParameterExpression`. Not intended to be called directly, but to be instantiated via operations @@ -54,6 +124,10 @@ def __init__(self, symbol_map: dict, expr): self._parameter_keys = frozenset(p._hash_key() for p in self._parameter_symbols) self._symbol_expr = expr self._name_map: dict | None = None + if _qpy_replay is not None: + self._qpy_replay = _qpy_replay + else: + self._qpy_replay = [] @property def parameters(self) -> set: @@ -69,8 +143,11 @@ def _names(self) -> dict: def conjugate(self) -> "ParameterExpression": """Return the conjugate.""" + new_op = _INSTRUCTION(_OPCode.CONJ, self) + new_replay = self._qpy_replay.copy() + new_replay.append(new_op) conjugated = ParameterExpression( - self._parameter_symbols, symengine.conjugate(self._symbol_expr) + self._parameter_symbols, symengine.conjugate(self._symbol_expr), _qpy_replay=new_replay ) return conjugated @@ -117,6 +194,7 @@ def bind( self._raise_if_passed_unknown_parameters(parameter_values.keys()) self._raise_if_passed_nan(parameter_values) + new_op = _SUBS(parameter_values) symbol_values = {} for parameter, value in parameter_values.items(): if (param_expr := self._parameter_symbols.get(parameter)) is not None: @@ -143,7 +221,12 @@ def bind( f"(Expression: {self}, Bindings: {parameter_values})." ) - return ParameterExpression(free_parameter_symbols, bound_symbol_expr) + new_replay = self._qpy_replay.copy() + new_replay.append(new_op) + + return ParameterExpression( + free_parameter_symbols, bound_symbol_expr, _qpy_replay=new_replay + ) def subs( self, parameter_map: dict, allow_unknown_parameters: bool = False @@ -175,6 +258,7 @@ def subs( for p in replacement_expr.parameters } self._raise_if_parameter_names_conflict(inbound_names, parameter_map.keys()) + new_op = _SUBS(parameter_map) # Include existing parameters in self not set to be replaced. new_parameter_symbols = { @@ -192,8 +276,12 @@ def subs( new_parameter_symbols[p] = symbol_type(p.name) substituted_symbol_expr = self._symbol_expr.subs(symbol_map) + new_replay = self._qpy_replay.copy() + new_replay.append(new_op) - return ParameterExpression(new_parameter_symbols, substituted_symbol_expr) + return ParameterExpression( + new_parameter_symbols, substituted_symbol_expr, _qpy_replay=new_replay + ) def _raise_if_passed_unknown_parameters(self, parameters): unknown_parameters = parameters - self.parameters @@ -231,7 +319,11 @@ def _raise_if_parameter_names_conflict(self, inbound_parameters, outbound_parame ) def _apply_operation( - self, operation: Callable, other: ParameterValueType, reflected: bool = False + self, + operation: Callable, + other: ParameterValueType, + reflected: bool = False, + op_code: _OPCode = None, ) -> "ParameterExpression": """Base method implementing math operations between Parameters and either a constant or a second ParameterExpression. @@ -253,7 +345,6 @@ def _apply_operation( A new expression describing the result of the operation. """ self_expr = self._symbol_expr - if isinstance(other, ParameterExpression): self._raise_if_parameter_names_conflict(other._names) parameter_symbols = {**self._parameter_symbols, **other._parameter_symbols} @@ -266,10 +357,14 @@ def _apply_operation( if reflected: expr = operation(other_expr, self_expr) + new_op = _INSTRUCTION(op_code, other, self) else: expr = operation(self_expr, other_expr) + new_op = _INSTRUCTION(op_code, self, other) + new_replay = self._qpy_replay.copy() + new_replay.append(new_op) - out_expr = ParameterExpression(parameter_symbols, expr) + out_expr = ParameterExpression(parameter_symbols, expr, _qpy_replay=new_replay) out_expr._name_map = self._names.copy() if isinstance(other, ParameterExpression): out_expr._names.update(other._names.copy()) @@ -313,81 +408,86 @@ def gradient(self, param) -> Union["ParameterExpression", complex]: return float(expr_grad) def __add__(self, other): - return self._apply_operation(operator.add, other) + return self._apply_operation(operator.add, other, op_code=_OPCode.ADD) def __radd__(self, other): - return self._apply_operation(operator.add, other, reflected=True) + return self._apply_operation(operator.add, other, reflected=True, op_code=_OPCode.ADD) def __sub__(self, other): - return self._apply_operation(operator.sub, other) + return self._apply_operation(operator.sub, other, op_code=_OPCode.SUB) def __rsub__(self, other): - return self._apply_operation(operator.sub, other, reflected=True) + return self._apply_operation(operator.sub, other, reflected=True, op_code=_OPCode.SUB) def __mul__(self, other): - return self._apply_operation(operator.mul, other) + return self._apply_operation(operator.mul, other, op_code=_OPCode.MUL) def __pos__(self): - return self._apply_operation(operator.mul, 1) + return self._apply_operation(operator.mul, 1, op_code=_OPCode.MUL) def __neg__(self): - return self._apply_operation(operator.mul, -1) + return self._apply_operation(operator.mul, -1, op_code=_OPCode.MUL) def __rmul__(self, other): - return self._apply_operation(operator.mul, other, reflected=True) + return self._apply_operation(operator.mul, other, reflected=True, op_code=_OPCode.MUL) def __truediv__(self, other): if other == 0: raise ZeroDivisionError("Division of a ParameterExpression by zero.") - return self._apply_operation(operator.truediv, other) + return self._apply_operation(operator.truediv, other, op_code=_OPCode.DIV) def __rtruediv__(self, other): - return self._apply_operation(operator.truediv, other, reflected=True) + return self._apply_operation(operator.truediv, other, reflected=True, op_code=_OPCode.DIV) def __pow__(self, other): - return self._apply_operation(pow, other) + return self._apply_operation(pow, other, op_code=_OPCode.POW) def __rpow__(self, other): - return self._apply_operation(pow, other, reflected=True) - - def _call(self, ufunc): - return ParameterExpression(self._parameter_symbols, ufunc(self._symbol_expr)) + return self._apply_operation(pow, other, reflected=True, op_code=_OPCode.POW) + + def _call(self, ufunc, op_code): + new_op = _INSTRUCTION(op_code, self) + new_replay = self._qpy_replay.copy() + new_replay.append(new_op) + return ParameterExpression( + self._parameter_symbols, ufunc(self._symbol_expr), _qpy_replay=new_replay + ) def sin(self): """Sine of a ParameterExpression""" - return self._call(symengine.sin) + return self._call(symengine.sin, op_code=_OPCode.SIN) def cos(self): """Cosine of a ParameterExpression""" - return self._call(symengine.cos) + return self._call(symengine.cos, op_code=_OPCode.COS) def tan(self): """Tangent of a ParameterExpression""" - return self._call(symengine.tan) + return self._call(symengine.tan, op_code=_OPCode.TAN) def arcsin(self): """Arcsin of a ParameterExpression""" - return self._call(symengine.asin) + return self._call(symengine.asin, op_code=_OPCode.ASIN) def arccos(self): """Arccos of a ParameterExpression""" - return self._call(symengine.acos) + return self._call(symengine.acos, op_code=_OPCode.ACOS) def arctan(self): """Arctan of a ParameterExpression""" - return self._call(symengine.atan) + return self._call(symengine.atan, op_code=_OPCode.ATAN) def exp(self): """Exponential of a ParameterExpression""" - return self._call(symengine.exp) + return self._call(symengine.exp, op_code=_OPCode.EXP) def log(self): """Logarithm of a ParameterExpression""" - return self._call(symengine.log) + return self._call(symengine.log, op_code=_OPCode.LOG) def sign(self): """Sign of a ParameterExpression""" - return self._call(symengine.sign) + return self._call(symengine.sign, op_code=_OPCode.SIGN) def __repr__(self): return f"{self.__class__.__name__}({str(self)})" @@ -455,7 +555,7 @@ def __deepcopy__(self, memo=None): def __abs__(self): """Absolute of a ParameterExpression""" - return self._call(symengine.Abs) + return self._call(symengine.Abs, _OPCode.ABS) def abs(self): """Absolute of a ParameterExpression""" diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 5b82e14d15cd..819be0161374 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -15,6 +15,7 @@ from __future__ import annotations import collections.abc +import io import struct import uuid @@ -25,7 +26,12 @@ 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.parameterexpression import ( + ParameterExpression, + op_code_to_method, + _OPCode, + _SUBS, +) from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement from qiskit.qpy import common, formats, exceptions, type_keys @@ -50,20 +56,136 @@ def _write_parameter_vec(file_obj, obj): file_obj.write(name_bytes) -def _write_parameter_expression(file_obj, obj, use_symengine, *, version): - if use_symengine: - expr_bytes = obj._symbol_expr.__reduce__()[1][0] +def _encode_replay_entry(inst, expression_tracking, file_obj, version, side=False): + inst_type = None + inst_data = None + if inst is None: + inst_type = "n" + inst_data = b"\x00" + elif isinstance(inst, Parameter): + inst_type = "p" + inst_data = inst.uuid.bytes + elif isinstance(inst, complex): + inst_type = "c" + inst_data = struct.pack("!dd", inst.real, inst.imag) + elif isinstance(inst, (float, int)): + inst_type = "f" + inst_data = struct.pack("!Qd", 0, inst) + elif isinstance(inst, ParameterExpression): + if inst not in expression_tracking: + if not side: + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V4_PACK, + 255, + "s".encode("utf8"), + b"\x00", + "n".encode("utf8"), + b"\x00", + ) + else: + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V4_PACK, + 255, + "n".encode("utf8"), + b"\x00", + "s".encode("utf8"), + b"\x00", + ) + + file_obj.write(entry) + _write_parameter_expression_v13(file_obj, inst, version) + if not side: + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V4_PACK, + 255, + "e".encode("utf8"), + b"\x00", + "n".encode("utf8"), + b"\x00", + ) + else: + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V4_PACK, + 255, + "n".encode("utf8"), + b"\x00", + "e".encode("utf8"), + b"\x00", + ) + inst_type = "n" + inst_data = b"\x00" + else: + inst_type = "n" + inst_data = b"\x00" else: - from sympy import srepr, sympify + raise exceptions.QpyError("Invalid parameter expression type") + return inst_type, inst_data + - expr_bytes = srepr(sympify(obj._symbol_expr)).encode(common.ENCODE) +def _encode_replay_subs(subs, file_obj, version): + with io.BytesIO() as mapping_buf: + subs_dict = {k.name: v for k, v in subs.binds.items()} + common.write_mapping( + mapping_buf, mapping=subs_dict, serializer=dumps_value, version=version + ) + data = mapping_buf.getvalue() + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V4_PACK, + subs.op, + "u".encode("utf8"), + struct.pack("!QQ", len(data), 0), + "n".encode("utf8"), + b"\x00", + ) + file_obj.write(entry) + file_obj.write(data) + return subs.binds + + +def _write_parameter_expression_v13(file_obj, obj, version): + expression_tracking = { + obj, + } + symbol_map = {} + for inst in obj._qpy_replay: + if isinstance(inst, _SUBS): + symbol_map.update(_encode_replay_subs(inst, file_obj, version)) + continue + lhs_type, lhs = _encode_replay_entry(inst.lhs, expression_tracking, file_obj, version) + rhs_type, rhs = _encode_replay_entry(inst.rhs, expression_tracking, file_obj, version, True) + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V4_PACK, + inst.op, + lhs_type.encode("utf8"), + lhs, + rhs_type.encode("utf8"), + rhs, + ) + file_obj.write(entry) + return symbol_map + + +def _write_parameter_expression(file_obj, obj, use_symengine, *, version): + extra_symbols = None + if version < 13: + if use_symengine: + expr_bytes = obj._symbol_expr.__reduce__()[1][0] + else: + from sympy import srepr, sympify + expr_bytes = srepr(sympify(obj._symbol_expr)).encode(common.ENCODE) + else: + with io.BytesIO() as buf: + extra_symbols = _write_parameter_expression_v13(buf, obj, version) + expr_bytes = buf.getvalue() + symbol_table_len = len(obj._parameter_symbols) + if extra_symbols: + symbol_table_len += 2 * len(extra_symbols) param_expr_header_raw = struct.pack( - formats.PARAMETER_EXPR_PACK, len(obj._parameter_symbols), len(expr_bytes) + formats.PARAMETER_EXPR_PACK, symbol_table_len, 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 = type_keys.Value.assign(symbol) @@ -89,6 +211,49 @@ def _write_parameter_expression(file_obj, obj, use_symengine, *, version): file_obj.write(elem_header) file_obj.write(symbol_data) file_obj.write(value_data) + if extra_symbols: + for symbol in extra_symbols: + symbol_key = type_keys.Value.assign(symbol) + # serialize key + if symbol_key == type_keys.Value.PARAMETER_VECTOR: + symbol_data = common.data_to_binary(symbol, _write_parameter_vec) + else: + symbol_data = common.data_to_binary(symbol, _write_parameter) + # serialize value + value_key, value_data = dumps_value( + symbol, version=version, use_symengine=use_symengine + ) + + elem_header = struct.pack( + formats.PARAM_EXPR_MAP_ELEM_V3_PACK, + symbol_key, + value_key, + len(value_data), + ) + file_obj.write(elem_header) + file_obj.write(symbol_data) + file_obj.write(value_data) + for symbol in extra_symbols.values(): + symbol_key = type_keys.Value.assign(symbol) + # serialize key + if symbol_key == type_keys.Value.PARAMETER_VECTOR: + symbol_data = common.data_to_binary(symbol, _write_parameter_vec) + else: + symbol_data = common.data_to_binary(symbol, _write_parameter) + # serialize value + value_key, value_data = dumps_value( + symbol, version=version, use_symengine=use_symengine + ) + + elem_header = struct.pack( + formats.PARAM_EXPR_MAP_ELEM_V3_PACK, + symbol_key, + value_key, + len(value_data), + ) + file_obj.write(elem_header) + file_obj.write(symbol_data) + file_obj.write(value_data) class _ExprWriter(expr.ExprVisitor[None]): @@ -334,6 +499,163 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): return ParameterExpression(symbol_map, expr_) +def _read_parameter_expression_v4(file_obj, vectors, version): + data = formats.PARAMETER_EXPR( + *struct.unpack(formats.PARAMETER_EXPR_PACK, file_obj.read(formats.PARAMETER_EXPR_SIZE)) + ) + + payload = file_obj.read(data.expr_size) + + symbol_map = {} + for _ in range(data.map_elements): + elem_data = formats.PARAM_EXPR_MAP_ELEM_V3( + *struct.unpack( + formats.PARAM_EXPR_MAP_ELEM_V3_PACK, + file_obj.read(formats.PARAM_EXPR_MAP_ELEM_V3_SIZE), + ) + ) + symbol_key = type_keys.Value(elem_data.symbol_type) + + if symbol_key == type_keys.Value.PARAMETER: + symbol = _read_parameter(file_obj) + elif symbol_key == type_keys.Value.PARAMETER_VECTOR: + symbol = _read_parameter_vec(file_obj, vectors) + else: + raise exceptions.QpyError(f"Invalid parameter expression map type: {symbol_key}") + + elem_key = type_keys.Value(elem_data.type) + binary_data = file_obj.read(elem_data.size) + if elem_key == type_keys.Value.INTEGER: + value = struct.unpack("!q", binary_data) + elif elem_key == type_keys.Value.FLOAT: + value = struct.unpack("!d", binary_data) + elif elem_key == type_keys.Value.COMPLEX: + value = complex(*struct.unpack(formats.COMPLEX_PACK, binary_data)) + elif elem_key in (type_keys.Value.PARAMETER, type_keys.Value.PARAMETER_VECTOR): + value = symbol._symbol_expr + elif elem_key == type_keys.Value.PARAMETER_EXPRESSION: + value = common.data_from_binary( + binary_data, + _read_parameter_expression_v4, + vectors=vectors, + ) + else: + raise exceptions.QpyError(f"Invalid parameter expression map type: {elem_key}") + symbol_map[symbol] = value + with io.BytesIO(payload) as buf: + return _read_parameter_expr_v13(buf, symbol_map, version, vectors) + + +def _read_parameter_expr_v13(buf, symbol_map, version, vectors): + param_uuid_map = {symbol.uuid: symbol for symbol in symbol_map if isinstance(symbol, Parameter)} + name_map = {str(v): k for k, v in symbol_map.items()} + expression = None + data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + rhs = None + lhs = None + while data: + expression_data = formats.PARAM_EXPR_ELEM_V4._make( + struct.unpack(formats.PARAM_EXPR_ELEM_V4_PACK, data) + ) + if lhs is None: + if expression_data.LHS_TYPE == b"p": + lhs = param_uuid_map[uuid.UUID(bytes=expression_data.LHS)] + elif expression_data.LHS_TYPE == b"f": + lhs = struct.unpack("!Qd", expression_data.LHS)[1] + elif expression_data.LHS_TYPE == b"n": + lhs = None + elif expression_data.LHS_TYPE == b"c": + lhs = complex(*struct.unpack("!dd", expression_data.LHS)) + elif expression_data.LHS_TYPE == b"s": + lhs = _read_parameter_expr_v13(buf, symbol_map, version, vectors) + data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + if expression is not None: + expression = lhs + continue + elif expression_data.LHS_TYPE == b"e": + return expression + elif expression_data.LHS_TYPE == b"u": + size = struct.unpack_from("!QQ", expression_data.LHS)[0] + subs_map_data = buf.read(size) + with io.BytesIO(subs_map_data) as mapping_buf: + mapping = common.read_mapping( + mapping_buf, deserializer=loads_value, version=version, vectors=vectors + ) + expression = expression.subs( + {name_map[k]: v for k, v in mapping.items()}, allow_unknown_parameters=True + ) + data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + continue + else: + raise exceptions.QpyError( + "Unknown ParameterExpression operation type {expression_data.LHS_TYPE}" + ) + if rhs is None: + if expression_data.RHS_TYPE == b"p": + rhs = param_uuid_map[uuid.UUID(bytes=expression_data.RHS)] + elif expression_data.RHS_TYPE == b"f": + rhs = struct.unpack("!Qd", expression_data.RHS)[1] + elif expression_data.RHS_TYPE == b"n": + rhs = None + elif expression_data.RHS_TYPE == b"c": + rhs = complex(*struct.unpack("!dd", expression_data.LHS)) + elif expression_data.RHS_TYPE == b"s": + rhs = _read_parameter_expr_v13(buf, symbol_map, version, vectors) + data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + if expression is not None: + expression = rhs + continue + elif expression_data.RHS_TYPE == b"e": + return expression + else: + raise exceptions.QpyError( + f"Unknown ParameterExpression operation type {expression_data.RHS_TYPE}" + ) + reverse_op = False + if expression is None: + if isinstance(lhs, ParameterExpression): + expression = lhs + elif isinstance(rhs, ParameterExpression): + expression = rhs + reverse_op = True + rhs = lhs + else: + raise exceptions.QpyError("Invalid ParameterExpression payload construction") + method_str = op_code_to_method(_OPCode(expression_data.OP_CODE)) + # Handle reverse operators + if rhs is None and expression is not None: + reverse_op = True + rhs = lhs + if expression_data.OP_CODE in {0, 1, 2, 3, 4, 13, 15}: + if reverse_op: + # Map arithmetic operators to reverse methods + if expression_data.OP_CODE == 0: + method_str = "__radd__" + elif expression_data.OP_CODE == 1: + method_str = "__rsub__" + elif expression_data.OP_CODE == 2: + method_str = "__rmul__" + elif expression_data.OP_CODE == 3: + method_str = "__rtruediv__" + + expression = getattr(expression, method_str)(rhs) + else: + expression = getattr(expression, method_str)() + lhs = None + rhs = None + data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + if expression is None: + if isinstance(lhs, ParameterExpression): + expression = lhs + elif isinstance(rhs, ParameterExpression): + expression = rhs + reverse_op = True + rhs = lhs + else: + raise exceptions.QpyError("Invalid ParameterExpression payload construction") + return expression + + def _read_expr( file_obj, clbits: collections.abc.Sequence[Clbit], @@ -664,13 +986,17 @@ def loads_value( if type_key == type_keys.Value.PARAMETER_EXPRESSION: if version < 3: return common.data_from_binary(binary_data, _read_parameter_expression) - else: + elif version < 13: return common.data_from_binary( binary_data, _read_parameter_expression_v3, vectors=vectors, use_symengine=use_symengine, ) + else: + return common.data_from_binary( + binary_data, _read_parameter_expression_v4, vectors=vectors, version=version + ) if type_key == type_keys.Value.EXPRESSION: return common.data_from_binary( binary_data, diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 8d8a57b7404f..c2585d5be4d3 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -25,7 +25,7 @@ from qiskit.qpy import formats, exceptions -QPY_VERSION = 12 +QPY_VERSION = 13 QPY_COMPATIBILITY_VERSION = 10 ENCODE = "utf8" diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index a48a9ea777fa..3a4343b684c4 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -259,6 +259,18 @@ PARAMETER_PACK = "!H16s" PARAMETER_SIZE = struct.calcsize(PARAMETER_PACK) +## PARAMETEREXPRESSION +# PARAM_EXPR_V2 = namedtuple("PARAM_EXPR_V2", ["size"]) +# PARAM_EXPR_V2_PACK = "!H" +# PARAM_EXPR_V2_SIZE = struct.calcsize(PARAM_EXPR_V2_PACK) + +# PARAMETEREXPRESSION_ENTRY +PARAM_EXPR_ELEM_V4 = namedtuple( + "PARAM_EXPR_ELEM_V4", ["OP_CODE", "LHS_TYPE", "LHS", "RHS_TYPE", "RHS"] +) +PARAM_EXPR_ELEM_V4_PACK = "!Bc16sc16s" +PARAM_EXPR_ELEM_V4_SIZE = struct.calcsize(PARAM_EXPR_ELEM_V4_PACK) + # COMPLEX COMPLEX = namedtuple("COMPLEX", ["real", "imag"]) COMPLEX_PACK = "!dd"