From 87e2a936b7b7397aae23a8f20d8df85d9dd3b2ed Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 22 Oct 2024 11:27:11 -0400 Subject: [PATCH 01/14] 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" From 0f84dbafe9b0a617b8fffaee489da6606d1d2d83 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 23 Oct 2024 08:36:15 -0400 Subject: [PATCH 02/14] Remove stray comment --- qiskit/qpy/formats.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 3a4343b684c4..b05d823aa731 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -259,11 +259,6 @@ 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"] From 969e4718d5e136b0f534e87ff7c18968f5873df0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 18:21:53 -0400 Subject: [PATCH 03/14] Add format documentation --- qiskit/qpy/__init__.py | 148 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 950b78fc421d..ad49b5147c0d 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -157,6 +157,24 @@ * - Qiskit (qiskit-terra for < 1.0.0) version - :func:`.dump` format(s) output versions - :func:`.load` maximum supported version (older format versions can always be read) + * - 1.3.0 + - 10, 11, 12, 13 + - 13 + * - 1.2.4 + - 10, 11, 12 + - 12 + * - 1.2.3 (yanked) + - 10, 11, 12 + - 12 + * - 1.2.2 + - 10, 11, 12 + - 12 + * - 1.2.1 + - 10, 11, 12 + - 12 + * - 1.2.0 + - 10, 11, 12 + - 12 * - 1.1.0 - 10, 11, 12 - 12 @@ -322,6 +340,136 @@ by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _qpy_version_13: + +Version 13 +---------- + +Version 13 added a native Qiskit serialization representation for :class:`.ParameterExpression`. +Previous QPY versions relied on either sympy or symengine to serialize the underlying symbolic +expression, while in version 13 QPY now represents the sequence of API calls used to create the +ParameterExpression. + +The first change is the :ref:`qpy_param_expr_v3` payload for the ``expr_size`` bytes following +the header is now comprised of an array of ``PARAM_EXPR_ELEM_V13`` structs. The intent is for the +array of elements to be read one struct at a time that describe the calls to make to reconstruct +the :class:`.ParameterExpression`. + +PARAM_EXPR_ELEM_V13 +~~~~~~~~~~~~~~~~~~~ + +The struct format is defined as: + +.. code-block:: c + + struct { + unsigned char op_code; + char lhs_type; + char lhs[16]; + char rhs_type; + char rhs[16]; + } PARAM_EXPR_ELEM_V13; + +The ``op_code`` field is used to define the operation used for the operation +added to the :class:`.ParameterExpression`. The value can be: + +.. list-table:: PARAM_EXPR_ELEM_V13 op code values + :header-rows: 1 + + * - ``op_code`` + - :class:`.ParameterExpression` method + * - 0 + - :meth:`~.ParameterExpression.__add__` + * - 1 + - :meth:`~.ParameterExpression.__sub__` + * - 2 + - :meth:`~.ParameterExpression.__mul__` + * - 3 + - :meth:`~.ParameterExpression.__truediv__` + * - 4 + - :meth:`~.ParameterExpression.__pow__` + * - 5 + - :meth:`~.ParameterExpression.sin` + * - 6 + - :meth:`~.ParameterExpression.cos` + * - 7 + - :meth:`~.ParameterExpression.tan` + * - 8 + - :meth:`~.ParameterExpression.arcsin` + * - 9 + - :meth:`~.ParameterExpression.arccos` + * - 10 + - :meth:`~.ParameterExpression.exp` + * - 11 + - :meth:`~.ParameterExpression.log` + * - 12 + - :meth:`~.ParameterExpression.sign` + * - 13 + - :meth:`~.ParameterExpression.gradient` + * - 14 + - :meth:`~.ParameterExpression.conjugate` + * - 15 + - :meth:`~.ParameterExpression.subs` + * - 16 + - :meth:`~.ParameterExpression.abs` + * - 17 + - :meth:`~.ParameterExpression.arctan` + * - 255 + - NULL + +The ``NULL`` value of 255 is only used to fill the op code field for +entries that are not actual operations but indicate recursive definitions. +Then the ``lhs_type`` and ``rhs_type`` fields are used to describe +the operand types and can be one of the following utf8 encoded +characters: + +.. list-table:: PARAM_EXPR_ELEM_V13 operand type values + :header-rows: 1 + + * - Value + - Type + * - ``n`` + - ``None`` + * - ``p`` + - :class:`.Parameter` + * - ``f`` + - ``float`` + * - ``c`` + - ``complex`` + * - ``s`` + - Recursive :class:`.ParameterExpression` definition start + * - ``e`` + - Recursive :class:`.ParameterExpression` definition stop + * - ``u`` + - substitution + +if the type value is ``f`` or ``c`` the corresponding ``lhs`` or `rhs`` +field is the literal value with 0 padding for the full 128 bit width in case of a +float. ``n`` is used to represent a ``None`` and typically isn't directly used as +it indicates an argument that's not used. For ``p`` the data is the UUID for the +:class:`.Parameter` which can be looked up in the symbol map described in the +``map_elements`` outer :ref:`qpy_param_expr_v3` payload. If the type value is +``s`` this marks the start of a a new recursive section for a nested +:class:`.ParameterExpression`. For example:: + + from qiskit.circuit import Parameter + + x = Parameter("x") + y = Parameter("y") + z = Parameter("z") + + expr = (x + y) / 2 + final_expr = z**2 + expr + +the inner ``expr`` in ``final_expr``. When ``s`` is encountered this indicates that until +an ``e` struct is reached the next structs are used for a recursive definition. For both +``s`` and ``e`` types the data values are 0 and not used. The type value +of ``u`` is used to represent a substitution call. This only is used for ``lhs_type`` +and is always paired with an ``rhs_type`` of ``n``. The data value is the size in bytes of +a :ref:`qpy_mapping` encoded mapping of :class:`.Parameter` names to their value for the +:meth:`~.ParameterExpression.subs` call. The mapping data is immediately following the +struct, and the next struct starts immediately after the mapping data. + .. _qpy_version_12: Version 12 From 54ef86e54c52311d810a2cfc371c7976760110a0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 24 Oct 2024 18:37:31 -0400 Subject: [PATCH 04/14] Add release note --- qiskit/qpy/__init__.py | 2 ++ qiskit/qpy/interface.py | 6 ++-- .../notes/add-qpy-v13-3b22ae33045af6c1.yaml | 33 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/add-qpy-v13-3b22ae33045af6c1.yaml diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index ad49b5147c0d..4dabba7a89a2 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -97,6 +97,8 @@ will be able to load all released format versions of QPY (up until ``QPY_VERSION``). +.. _qpy_compatibility: + QPY Compatibility ================= diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index 2410e4b0a79d..ef637b6bced5 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -126,9 +126,9 @@ def dump( used as the ``cls`` kwarg on the `json.dump()`` call to JSON serialize that dictionary. use_symengine: If True, all objects containing symbolic expressions will be serialized using symengine's 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. For this reason, the option defaults to False. + but not supported in all platforms. This flag only has an effect if the emitted QPY format + version is 10, 11, or 12. For QPY format version >= 13 (which is the default starting in + Qiskit 1.3.0) this flag is no longer used. 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 diff --git a/releasenotes/notes/add-qpy-v13-3b22ae33045af6c1.yaml b/releasenotes/notes/add-qpy-v13-3b22ae33045af6c1.yaml new file mode 100644 index 000000000000..3cc80adc0b5c --- /dev/null +++ b/releasenotes/notes/add-qpy-v13-3b22ae33045af6c1.yaml @@ -0,0 +1,33 @@ +--- +features_qpy: + - | + Added a new QPY format version 13 that adds a Qiskit native representation + of :class:`.ParameterExpression` objects. +issues: + - | + When using QPY formats 10, 11, or 12 there is a dependency on the version + of ``symengine`` installed in the payload for serialized + :class:`.ParamerExpression` if there is mismatched version of the installed + ``symengine`` package between the environment that generated the payload with + :func:`.qpy.dump` and the installed version that is trying to load the payload + with :func:`.qpy.load`. If this is encountered you will need to install the + symengine version from the error message emitted to load the payload. QPY + format version >= 13 (or < 10) will not have this issue and it is recommended + if you're serializing :class:`.ParameterExpression` objects as part of your + circuit or any :class:`.ScheduleBlock` objects you use version 13 to avoid + this issue in the future. +upgrade_qpy: + - | + The :func:`.qpy.dump` function will now emit format version 13 by default. + This means payloads generated with this function by default will only + be compatible with Qiskit >= 1.3.0. If you need for the payload to be + loaded by a older version of Qiskit you can use the ``version`` flag on + :func:`.qpy.dump` to emit a version compatible with earlier releases of + Qiskit. You can refer to :ref:`qpy_compatibility` for more details on this. +# security: +# - | +# Add security notes here, or remove this section. All of the list items in +# this section are combined when the release notes are rendered, so the text +# needs to be worded so that it does not depend on any information only +# available in another section, such as the prelude. This may mean repeating +# some details. From 2b352d705ab30f2106f5668ec07a3fbe0bbfcd41 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 31 Oct 2024 17:45:43 -0400 Subject: [PATCH 05/14] Add test and fix some issues with recursive expressions --- qiskit/qpy/binary_io/value.py | 5 +---- test/python/qpy/test_circuit_load_from_qpy.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 819be0161374..67d4592714d6 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -112,6 +112,7 @@ def _encode_replay_entry(inst, expression_tracking, file_obj, version, side=Fals "e".encode("utf8"), b"\x00", ) + file_obj.write(entry) inst_type = "n" inst_data = b"\x00" else: @@ -569,8 +570,6 @@ def _read_parameter_expr_v13(buf, symbol_map, version, vectors): 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 @@ -602,8 +601,6 @@ def _read_parameter_expr_v13(buf, symbol_map, version, vectors): 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 diff --git a/test/python/qpy/test_circuit_load_from_qpy.py b/test/python/qpy/test_circuit_load_from_qpy.py index e909f7ced455..1c4792580b9b 100644 --- a/test/python/qpy/test_circuit_load_from_qpy.py +++ b/test/python/qpy/test_circuit_load_from_qpy.py @@ -306,6 +306,28 @@ def test_compatibility_version_roundtrip(self): qc.measure_all() self.assert_roundtrip_equal(qc, version=QPY_COMPATIBILITY_VERSION) + def test_all_the_expression_ops(self): + """Test a circuit with an expression that uses all the ops available.""" + qc = QuantumCircuit(1) + a = Parameter("a") + b = Parameter("b") + c = Parameter("c") + d = Parameter("d") + + expression = (a + b.sin() / 4) * c**2 + final_expr = ( + (expression.cos() + d.arccos() - d.arcsin() + d.arctan() + d.tan()) / d.exp() + + expression.gradient(a) + + expression.log() + - a.sin() + - b.conjugate() + ) + final_expr = final_expr.abs() + final_expr = final_expr.subs({c: a}) + + qc.rx(final_expr, 0) + self.assert_roundtrip_equal(qc) + class TestUseSymengineFlag(QpyCircuitTestCase): """Test that the symengine flag works correctly.""" From 5d7b79addd031ccbc692723867ce0ad0317f1c62 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 31 Oct 2024 17:57:35 -0400 Subject: [PATCH 06/14] Add int type for operands --- qiskit/qpy/__init__.py | 5 ++++- qiskit/qpy/binary_io/value.py | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 4dabba7a89a2..28257c772955 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -438,6 +438,8 @@ - ``float`` * - ``c`` - ``complex`` + * - ``i`` + - ``int`` * - ``s`` - Recursive :class:`.ParameterExpression` definition start * - ``e`` @@ -447,7 +449,8 @@ if the type value is ``f`` or ``c`` the corresponding ``lhs`` or `rhs`` field is the literal value with 0 padding for the full 128 bit width in case of a -float. ``n`` is used to represent a ``None`` and typically isn't directly used as +float. For ``i`` it's encoded as a 64 bit signed integer with 0 padding for the full +128 bit width. ``n`` is used to represent a ``None`` and typically isn't directly used as it indicates an argument that's not used. For ``p`` the data is the UUID for the :class:`.Parameter` which can be looked up in the symbol map described in the ``map_elements`` outer :ref:`qpy_param_expr_v3` payload. If the type value is diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 67d4592714d6..d577a2eb4f80 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -68,9 +68,12 @@ def _encode_replay_entry(inst, expression_tracking, file_obj, version, side=Fals elif isinstance(inst, complex): inst_type = "c" inst_data = struct.pack("!dd", inst.real, inst.imag) - elif isinstance(inst, (float, int)): + elif isinstance(inst, float): inst_type = "f" inst_data = struct.pack("!Qd", 0, inst) + elif isinstance(inst, int): + inst_type = "i" + inst_data = struct.pack("!Qq", 0, inst) elif isinstance(inst, ParameterExpression): if inst not in expression_tracking: if not side: @@ -567,6 +570,8 @@ def _read_parameter_expr_v13(buf, symbol_map, version, vectors): lhs = None elif expression_data.LHS_TYPE == b"c": lhs = complex(*struct.unpack("!dd", expression_data.LHS)) + elif expression_data.LHS_TYPE == b"i": + lhs = struct.unpack("!Qq", expression_data.LHS)[1] 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) @@ -598,6 +603,8 @@ def _read_parameter_expr_v13(buf, symbol_map, version, vectors): rhs = None elif expression_data.RHS_TYPE == b"c": rhs = complex(*struct.unpack("!dd", expression_data.LHS)) + elif expression_data.RHS_TYPE == b"i": + rhs = struct.unpack("!Qq", expression_data.RHS)[1] 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) From 60a21729ef5c102f52d3b4d99e8124699dda28ad Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 31 Oct 2024 18:22:45 -0400 Subject: [PATCH 07/14] Add dedicated subs test --- test/python/qpy/test_circuit_load_from_qpy.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/python/qpy/test_circuit_load_from_qpy.py b/test/python/qpy/test_circuit_load_from_qpy.py index 1c4792580b9b..8726db41c7f3 100644 --- a/test/python/qpy/test_circuit_load_from_qpy.py +++ b/test/python/qpy/test_circuit_load_from_qpy.py @@ -306,6 +306,16 @@ def test_compatibility_version_roundtrip(self): qc.measure_all() self.assert_roundtrip_equal(qc, version=QPY_COMPATIBILITY_VERSION) + def test_nested_params_subs(self): + """Test substitution works.""" + qc = QuantumCircuit(1) + a = Parameter("a") + b = Parameter("b") + expr = a + b + expr = expr.subs({b: a}) + qc.ry(expr, 0) + self.assert_roundtrip_equal(qc) + def test_all_the_expression_ops(self): """Test a circuit with an expression that uses all the ops available.""" qc = QuantumCircuit(1) From a903387f435294cc5a85a2e39d119a83e4864578 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 5 Nov 2024 16:55:17 -0500 Subject: [PATCH 08/14] Pivot to stack based postfix/rpn deserialization This commit changes how the deserialization works to use a postfix stack based approach. Operands are push on the stack and then popped off based on the operation being run. The result of the operation is then pushed on the stack. This handles nested objects much more cleanly than the recursion based approach because we just keep pushing on the stack instead of recursing, making the accounting much simpler. After the expression payload is finished being processed there will be a single value on the stack and that is returned as the final expression. --- qiskit/circuit/parameter.py | 2 + qiskit/circuit/parameterexpression.py | 33 +++- qiskit/qpy/binary_io/value.py | 243 +++++++++++--------------- 3 files changed, 134 insertions(+), 144 deletions(-) diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index 8df6f86c8372..8723445cdef4 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -88,6 +88,7 @@ def __init__( self._parameter_symbols = {self: symbol} self._name_map = None self._qpy_replay = [] + self._standalone_param = True def assign(self, parameter, value): if parameter != self: @@ -174,3 +175,4 @@ def __setstate__(self, state): self._parameter_symbols = {self: self._symbol_expr} self._name_map = None self._qpy_replay = [] + self._standalone_param = True diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 1cc1155896dc..1a09e333bfe7 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -84,7 +84,7 @@ def op_code_to_method(op_code: _OPCode): @dataclass class _INSTRUCTION: op: _OPCode - lhs: ParameterValueType + lhs: ParameterValueType | None rhs: ParameterValueType | None = None @@ -103,6 +103,7 @@ class ParameterExpression: "_symbol_expr", "_name_map", "_qpy_replay", + "_standalone_param", ] def __init__(self, symbol_map: dict, expr, *, _qpy_replay=None): @@ -124,6 +125,7 @@ def __init__(self, symbol_map: dict, expr, *, _qpy_replay=None): self._parameter_keys = frozenset(p._hash_key() for p in self._parameter_symbols) self._symbol_expr = expr self._name_map: dict | None = None + self._standalone_param = False if _qpy_replay is not None: self._qpy_replay = _qpy_replay else: @@ -143,7 +145,10 @@ def _names(self) -> dict: def conjugate(self) -> "ParameterExpression": """Return the conjugate.""" - new_op = _INSTRUCTION(_OPCode.CONJ, self) + if self._standalone_param: + new_op = _INSTRUCTION(_OPCode.CONJ, self) + else: + new_op = _INSTRUCTION(_OPCode.CONJ, None) new_replay = self._qpy_replay.copy() new_replay.append(new_op) conjugated = ParameterExpression( @@ -357,10 +362,16 @@ def _apply_operation( if reflected: expr = operation(other_expr, self_expr) - new_op = _INSTRUCTION(op_code, other, self) + if self._standalone_param: + new_op = _INSTRUCTION(op_code, other, self) + else: + new_op = _INSTRUCTION(op_code, other, None) else: expr = operation(self_expr, other_expr) - new_op = _INSTRUCTION(op_code, self, other) + if self._standalone_param: + new_op = _INSTRUCTION(op_code, self, other) + else: + new_op = _INSTRUCTION(op_code, None, other) new_replay = self._qpy_replay.copy() new_replay.append(new_op) @@ -386,6 +397,13 @@ def gradient(self, param) -> Union["ParameterExpression", complex]: # If it is not contained then return 0 return 0.0 + if self._standalone_param: + new_op = _INSTRUCTION(_OPCode.DERIV, self, param) + else: + new_op = _INSTRUCTION(_OPCode.DERIV, None, param) + qpy_replay = self._qpy_replay.copy() + qpy_replay.append(new_op) + # Compute the gradient of the parameter expression w.r.t. param key = self._parameter_symbols[param] expr_grad = symengine.Derivative(self._symbol_expr, key) @@ -399,7 +417,7 @@ def gradient(self, param) -> Union["ParameterExpression", complex]: parameter_symbols[parameter] = symbol # If the gradient corresponds to a parameter expression then return the new expression. if len(parameter_symbols) > 0: - return ParameterExpression(parameter_symbols, expr=expr_grad) + return ParameterExpression(parameter_symbols, expr=expr_grad, _qpy_replay=qpy_replay) # If no free symbols left, return a complex or float gradient expr_grad_cplx = complex(expr_grad) if expr_grad_cplx.imag != 0: @@ -446,7 +464,10 @@ def __rpow__(self, other): return self._apply_operation(pow, other, reflected=True, op_code=_OPCode.POW) def _call(self, ufunc, op_code): - new_op = _INSTRUCTION(op_code, self) + if self._standalone_param: + new_op = _INSTRUCTION(op_code, self) + else: + new_op = _INSTRUCTION(op_code, None) new_replay = self._qpy_replay.copy() new_replay.append(new_op) return ParameterExpression( diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index d577a2eb4f80..9ddf30c2c254 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -56,7 +56,7 @@ def _write_parameter_vec(file_obj, obj): file_obj.write(name_bytes) -def _encode_replay_entry(inst, expression_tracking, file_obj, version, side=False): +def _encode_replay_entry(inst, file_obj, version, side=False): inst_type = None inst_data = None if inst is None: @@ -75,52 +75,47 @@ def _encode_replay_entry(inst, expression_tracking, file_obj, version, side=Fals inst_type = "i" inst_data = struct.pack("!Qq", 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", - ) - file_obj.write(entry) - inst_type = "n" - inst_data = b"\x00" + 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: - inst_type = "n" - inst_data = b"\x00" + entry = struct.pack( + formats.PARAM_EXPR_ELEM_V4_PACK, + 255, + "n".encode("utf8"), + b"\x00", + "e".encode("utf8"), + b"\x00", + ) + file_obj.write(entry) + inst_type = "n" + inst_data = b"\x00" else: raise exceptions.QpyError("Invalid parameter expression type") return inst_type, inst_data @@ -147,16 +142,13 @@ def _encode_replay_subs(subs, file_obj, version): 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) + lhs_type, lhs = _encode_replay_entry(inst.lhs, file_obj, version) + rhs_type, rhs = _encode_replay_entry(inst.rhs, file_obj, version, True) entry = struct.pack( formats.PARAM_EXPR_ELEM_V4_PACK, inst.op, @@ -553,86 +545,70 @@ def _read_parameter_expression_v4(file_obj, vectors, version): 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 + stack = [] 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"i": - lhs = struct.unpack("!Qq", expression_data.LHS)[1] - 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) - 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"i": - rhs = struct.unpack("!Qq", expression_data.RHS)[1] - 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) - continue - elif expression_data.RHS_TYPE == b"e": - return expression - else: - raise exceptions.QpyError( - f"Unknown ParameterExpression operation type {expression_data.RHS_TYPE}" + # LHS + if expression_data.LHS_TYPE == b"p": + stack.append(param_uuid_map[uuid.UUID(bytes=expression_data.LHS)]) + elif expression_data.LHS_TYPE == b"f": + stack.append(struct.unpack("!Qd", expression_data.LHS)[1]) + elif expression_data.LHS_TYPE == b"n": + pass + elif expression_data.LHS_TYPE == b"c": + stack.append(complex(*struct.unpack("!dd", expression_data.LHS))) + elif expression_data.LHS_TYPE == b"i": + stack.append(struct.unpack("!Qq", expression_data.LHS)[1]) + elif expression_data.LHS_TYPE == b"s": + data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + continue + elif expression_data.LHS_TYPE == b"e": + data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + continue + 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 ) - 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") + stack.append({name_map[k]: v for k, v in mapping.items()}) + else: + raise exceptions.QpyError( + "Unknown ParameterExpression operation type {expression_data.LHS_TYPE}" + ) + # RHS + if expression_data.RHS_TYPE == b"p": + stack.append(param_uuid_map[uuid.UUID(bytes=expression_data.RHS)]) + elif expression_data.RHS_TYPE == b"f": + stack.append(struct.unpack("!Qd", expression_data.RHS)[1]) + elif expression_data.RHS_TYPE == b"n": + pass + elif expression_data.RHS_TYPE == b"c": + stack.append(complex(*struct.unpack("!dd", expression_data.RHS))) + elif expression_data.RHS_TYPE == b"i": + stack.append(struct.unpack("!Qq", expression_data.RHS)[1]) + elif expression_data.RHS_TYPE == b"s": + data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + continue + elif expression_data.RHS_TYPE == b"e": + data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + continue + else: + raise exceptions.QpyError( + f"Unknown ParameterExpression operation type {expression_data.RHS_TYPE}" + ) + if expression_data.OP_CODE == 255: + continue 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 + rhs = stack.pop() + lhs = stack.pop() + # Reverse ops + if not isinstance(lhs, ParameterExpression) and isinstance(rhs, ParameterExpression): if expression_data.OP_CODE == 0: method_str = "__radd__" elif expression_data.OP_CODE == 1: @@ -641,23 +617,14 @@ def _read_parameter_expr_v13(buf, symbol_map, version, vectors): method_str = "__rmul__" elif expression_data.OP_CODE == 3: method_str = "__rtruediv__" - - expression = getattr(expression, method_str)(rhs) + stack.append(getattr(rhs, method_str)(lhs)) + else: + stack.append(getattr(lhs, method_str)(rhs)) else: - expression = getattr(expression, method_str)() - lhs = None - rhs = None + lhs = stack.pop() + stack.append(getattr(lhs, method_str)()) 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 + return stack.pop() def _read_expr( From b72856d3f977a097a6058affed1aa2c4a84a71de Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 6 Nov 2024 11:13:24 -0500 Subject: [PATCH 09/14] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- qiskit/qpy/__init__.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 28257c772955..3e319b74ac9f 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -348,14 +348,13 @@ ---------- Version 13 added a native Qiskit serialization representation for :class:`.ParameterExpression`. -Previous QPY versions relied on either sympy or symengine to serialize the underlying symbolic -expression, while in version 13 QPY now represents the sequence of API calls used to create the -ParameterExpression. +Previous QPY versions relied on either ``sympy`` or ``symengine`` to serialize the underlying symbolic +expression. Starting in Version 13, QPY now represents the sequence of API calls used to create the +:class:`.ParameterExpression`. -The first change is the :ref:`qpy_param_expr_v3` payload for the ``expr_size`` bytes following -the header is now comprised of an array of ``PARAM_EXPR_ELEM_V13`` structs. The intent is for the -array of elements to be read one struct at a time that describe the calls to make to reconstruct -the :class:`.ParameterExpression`. +The main change in the serialization format is in the :ref:`qpy_param_expr_v3` payload. The ``expr_size`` bytes following +the head now contain an array of ``PARAM_EXPR_ELEM_V13`` structs. The intent is for this +array to be read one struct at a time, where each struct describes one of the calls to make to reconstruct the :class:`.ParameterExpression`. PARAM_EXPR_ELEM_V13 ~~~~~~~~~~~~~~~~~~~ @@ -372,8 +371,7 @@ char rhs[16]; } PARAM_EXPR_ELEM_V13; -The ``op_code`` field is used to define the operation used for the operation -added to the :class:`.ParameterExpression`. The value can be: +The ``op_code`` field is used to define the operation added to the :class:`.ParameterExpression`. The value can be: .. list-table:: PARAM_EXPR_ELEM_V13 op code values :header-rows: 1 @@ -422,7 +420,7 @@ The ``NULL`` value of 255 is only used to fill the op code field for entries that are not actual operations but indicate recursive definitions. Then the ``lhs_type`` and ``rhs_type`` fields are used to describe -the operand types and can be one of the following utf8 encoded +the operand types and can be one of the following UTF-8 encoded characters: .. list-table:: PARAM_EXPR_ELEM_V13 operand type values @@ -447,15 +445,17 @@ * - ``u`` - substitution -if the type value is ``f`` or ``c`` the corresponding ``lhs`` or `rhs`` -field is the literal value with 0 padding for the full 128 bit width in case of a -float. For ``i`` it's encoded as a 64 bit signed integer with 0 padding for the full +If the type value is ``f`` ,``c`` or ``i``, the corresponding ``lhs`` or `rhs`` +field widths are 128 bits each. In the case of floats, the literal value is encoded as a double +with 0 padding, while complex numbers are encoded as real part followed by imaginary part, +taking up 64 bits each. For ``i`, the value is encoded as a 64 bit signed integer with 0 padding for the full 128 bit width. ``n`` is used to represent a ``None`` and typically isn't directly used as it indicates an argument that's not used. For ``p`` the data is the UUID for the :class:`.Parameter` which can be looked up in the symbol map described in the ``map_elements`` outer :ref:`qpy_param_expr_v3` payload. If the type value is ``s`` this marks the start of a a new recursive section for a nested -:class:`.ParameterExpression`. For example:: +:class:`.ParameterExpression`. For example, in the following snippet there is an inner ``expr`` +contained in ``final_expr``, constituting a nested expression:: from qiskit.circuit import Parameter @@ -466,10 +466,10 @@ expr = (x + y) / 2 final_expr = z**2 + expr -the inner ``expr`` in ``final_expr``. When ``s`` is encountered this indicates that until -an ``e` struct is reached the next structs are used for a recursive definition. For both -``s`` and ``e`` types the data values are 0 and not used. The type value -of ``u`` is used to represent a substitution call. This only is used for ``lhs_type`` +When ``s`` is encountered, this indicates that until an ``e` struct is reached, the next structs +are used for a recursive definition. For both +``s`` and ``e`` types, the data values are not used, and always set to 0. The type value +of ``u`` is used to represent a substitution call. This is only used for ``lhs_type`` and is always paired with an ``rhs_type`` of ``n``. The data value is the size in bytes of a :ref:`qpy_mapping` encoded mapping of :class:`.Parameter` names to their value for the :meth:`~.ParameterExpression.subs` call. The mapping data is immediately following the From 8ae658932de61ad5c06e70572bb9c8e0d23a5904 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 6 Nov 2024 11:15:17 -0500 Subject: [PATCH 10/14] Change DERIV to GRAD --- qiskit/circuit/parameterexpression.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 1a09e333bfe7..66afed1b904b 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -47,7 +47,7 @@ class _OPCode(IntEnum): EXP = 10 LOG = 11 SIGN = 12 - DERIV = 13 + GRAD = 13 CONJ = 14 SUBSTITUTE = 15 ABS = 16 @@ -398,9 +398,9 @@ def gradient(self, param) -> Union["ParameterExpression", complex]: return 0.0 if self._standalone_param: - new_op = _INSTRUCTION(_OPCode.DERIV, self, param) + new_op = _INSTRUCTION(_OPCode.GRAD, self, param) else: - new_op = _INSTRUCTION(_OPCode.DERIV, None, param) + new_op = _INSTRUCTION(_OPCode.GRAD, None, param) qpy_replay = self._qpy_replay.copy() qpy_replay.append(new_op) From 11f65adbc08690ffe3cb3587814ec1274905b111 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 6 Nov 2024 11:16:06 -0500 Subject: [PATCH 11/14] Change side kwarg to r_side --- qiskit/qpy/binary_io/value.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 9ddf30c2c254..f92ee94016fc 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -56,7 +56,7 @@ def _write_parameter_vec(file_obj, obj): file_obj.write(name_bytes) -def _encode_replay_entry(inst, file_obj, version, side=False): +def _encode_replay_entry(inst, file_obj, version, r_side=False): inst_type = None inst_data = None if inst is None: @@ -75,7 +75,7 @@ def _encode_replay_entry(inst, file_obj, version, side=False): inst_type = "i" inst_data = struct.pack("!Qq", 0, inst) elif isinstance(inst, ParameterExpression): - if not side: + if not r_side: entry = struct.pack( formats.PARAM_EXPR_ELEM_V4_PACK, 255, @@ -95,7 +95,7 @@ def _encode_replay_entry(inst, file_obj, version, side=False): ) file_obj.write(entry) _write_parameter_expression_v13(file_obj, inst, version) - if not side: + if not r_side: entry = struct.pack( formats.PARAM_EXPR_ELEM_V4_PACK, 255, From 6e75ea58fa69df4c17e5add40b86c0ac5d53c899 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 6 Nov 2024 11:20:37 -0500 Subject: [PATCH 12/14] Change all the v4s to v13s --- qiskit/qpy/binary_io/value.py | 34 +++++++++++++++++----------------- qiskit/qpy/formats.py | 8 ++++---- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index f92ee94016fc..fc1554756907 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -77,7 +77,7 @@ def _encode_replay_entry(inst, file_obj, version, r_side=False): elif isinstance(inst, ParameterExpression): if not r_side: entry = struct.pack( - formats.PARAM_EXPR_ELEM_V4_PACK, + formats.PARAM_EXPR_ELEM_V13_PACK, 255, "s".encode("utf8"), b"\x00", @@ -86,7 +86,7 @@ def _encode_replay_entry(inst, file_obj, version, r_side=False): ) else: entry = struct.pack( - formats.PARAM_EXPR_ELEM_V4_PACK, + formats.PARAM_EXPR_ELEM_V13_PACK, 255, "n".encode("utf8"), b"\x00", @@ -97,7 +97,7 @@ def _encode_replay_entry(inst, file_obj, version, r_side=False): _write_parameter_expression_v13(file_obj, inst, version) if not r_side: entry = struct.pack( - formats.PARAM_EXPR_ELEM_V4_PACK, + formats.PARAM_EXPR_ELEM_V13_PACK, 255, "e".encode("utf8"), b"\x00", @@ -106,7 +106,7 @@ def _encode_replay_entry(inst, file_obj, version, r_side=False): ) else: entry = struct.pack( - formats.PARAM_EXPR_ELEM_V4_PACK, + formats.PARAM_EXPR_ELEM_V13_PACK, 255, "n".encode("utf8"), b"\x00", @@ -129,7 +129,7 @@ def _encode_replay_subs(subs, file_obj, version): ) data = mapping_buf.getvalue() entry = struct.pack( - formats.PARAM_EXPR_ELEM_V4_PACK, + formats.PARAM_EXPR_ELEM_V13_PACK, subs.op, "u".encode("utf8"), struct.pack("!QQ", len(data), 0), @@ -150,7 +150,7 @@ def _write_parameter_expression_v13(file_obj, obj, version): lhs_type, lhs = _encode_replay_entry(inst.lhs, file_obj, version) rhs_type, rhs = _encode_replay_entry(inst.rhs, file_obj, version, True) entry = struct.pack( - formats.PARAM_EXPR_ELEM_V4_PACK, + formats.PARAM_EXPR_ELEM_V13_PACK, inst.op, lhs_type.encode("utf8"), lhs, @@ -495,7 +495,7 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): return ParameterExpression(symbol_map, expr_) -def _read_parameter_expression_v4(file_obj, vectors, version): +def _read_parameter_expression_v13(file_obj, vectors, version): data = formats.PARAMETER_EXPR( *struct.unpack(formats.PARAMETER_EXPR_PACK, file_obj.read(formats.PARAMETER_EXPR_SIZE)) ) @@ -532,7 +532,7 @@ def _read_parameter_expression_v4(file_obj, vectors, version): elif elem_key == type_keys.Value.PARAMETER_EXPRESSION: value = common.data_from_binary( binary_data, - _read_parameter_expression_v4, + _read_parameter_expression_v13, vectors=vectors, ) else: @@ -545,11 +545,11 @@ def _read_parameter_expression_v4(file_obj, vectors, version): 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()} - data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) stack = [] while data: - expression_data = formats.PARAM_EXPR_ELEM_V4._make( - struct.unpack(formats.PARAM_EXPR_ELEM_V4_PACK, data) + expression_data = formats.PARAM_EXPR_ELEM_V13._make( + struct.unpack(formats.PARAM_EXPR_ELEM_V13_PACK, data) ) # LHS if expression_data.LHS_TYPE == b"p": @@ -563,10 +563,10 @@ def _read_parameter_expr_v13(buf, symbol_map, version, vectors): elif expression_data.LHS_TYPE == b"i": stack.append(struct.unpack("!Qq", expression_data.LHS)[1]) elif expression_data.LHS_TYPE == b"s": - data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) continue elif expression_data.LHS_TYPE == b"e": - data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) continue elif expression_data.LHS_TYPE == b"u": size = struct.unpack_from("!QQ", expression_data.LHS)[0] @@ -592,10 +592,10 @@ def _read_parameter_expr_v13(buf, symbol_map, version, vectors): elif expression_data.RHS_TYPE == b"i": stack.append(struct.unpack("!Qq", expression_data.RHS)[1]) elif expression_data.RHS_TYPE == b"s": - data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) continue elif expression_data.RHS_TYPE == b"e": - data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) continue else: raise exceptions.QpyError( @@ -623,7 +623,7 @@ def _read_parameter_expr_v13(buf, symbol_map, version, vectors): else: lhs = stack.pop() stack.append(getattr(lhs, method_str)()) - data = buf.read(formats.PARAM_EXPR_ELEM_V4_SIZE) + data = buf.read(formats.PARAM_EXPR_ELEM_V13_SIZE) return stack.pop() @@ -966,7 +966,7 @@ def loads_value( ) else: return common.data_from_binary( - binary_data, _read_parameter_expression_v4, vectors=vectors, version=version + binary_data, _read_parameter_expression_v13, vectors=vectors, version=version ) if type_key == type_keys.Value.EXPRESSION: return common.data_from_binary( diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index b05d823aa731..7696cae94e2b 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -260,11 +260,11 @@ PARAMETER_SIZE = struct.calcsize(PARAMETER_PACK) # PARAMETEREXPRESSION_ENTRY -PARAM_EXPR_ELEM_V4 = namedtuple( - "PARAM_EXPR_ELEM_V4", ["OP_CODE", "LHS_TYPE", "LHS", "RHS_TYPE", "RHS"] +PARAM_EXPR_ELEM_V13 = namedtuple( + "PARAM_EXPR_ELEM_V13", ["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) +PARAM_EXPR_ELEM_V13_PACK = "!Bc16sc16s" +PARAM_EXPR_ELEM_V13_SIZE = struct.calcsize(PARAM_EXPR_ELEM_V13_PACK) # COMPLEX COMPLEX = namedtuple("COMPLEX", ["real", "imag"]) From 8c2205faf7971fe281d4b82100ab8f79c5bb8d70 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 6 Nov 2024 11:45:32 -0500 Subject: [PATCH 13/14] Correctly handle non-commutative operations This commit fixes a bug with handling the operand order of subtraction, division, and exponentiation. These operations are not commutative but the qpy deserialization code was treating them as such. So in cases where the argument order was reversed qpy was trying to flip the operands around for code simplicity and this would result in incorrect behavior. This commit fixes this by adding explicit op codes for the reversed sub, div, and pow and preserving the operand order correctly in these cases. --- qiskit/circuit/parameterexpression.py | 24 ++++++++++---- qiskit/qpy/binary_io/value.py | 17 ++++++---- test/python/qpy/test_circuit_load_from_qpy.py | 33 +++++++++++++++++++ 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 66afed1b904b..fe786762c096 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -52,6 +52,9 @@ class _OPCode(IntEnum): SUBSTITUTE = 15 ABS = 16 ATAN = 17 + RSUB = 18 + RDIV = 19 + RPOW = 20 _OP_CODE_MAP = ( @@ -73,6 +76,9 @@ class _OPCode(IntEnum): "subs", "abs", "arctan", + "__rsub__", + "__rtruediv__", + "__rpow__", ) @@ -362,10 +368,16 @@ def _apply_operation( if reflected: expr = operation(other_expr, self_expr) - if self._standalone_param: - new_op = _INSTRUCTION(op_code, other, self) + if op_code in {_OPCode.RSUB, _OPCode.RDIV, _OPCode.RPOW}: + if self._standalone_param: + new_op = _INSTRUCTION(op_code, self, other) + else: + new_op = _INSTRUCTION(op_code, None, other) else: - new_op = _INSTRUCTION(op_code, other, None) + if self._standalone_param: + new_op = _INSTRUCTION(op_code, other, self) + else: + new_op = _INSTRUCTION(op_code, other, None) else: expr = operation(self_expr, other_expr) if self._standalone_param: @@ -435,7 +447,7 @@ def __sub__(self, 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, op_code=_OPCode.SUB) + return self._apply_operation(operator.sub, other, reflected=True, op_code=_OPCode.RSUB) def __mul__(self, other): return self._apply_operation(operator.mul, other, op_code=_OPCode.MUL) @@ -455,13 +467,13 @@ def __truediv__(self, 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, op_code=_OPCode.DIV) + return self._apply_operation(operator.truediv, other, reflected=True, op_code=_OPCode.RDIV) def __pow__(self, other): return self._apply_operation(pow, other, op_code=_OPCode.POW) def __rpow__(self, other): - return self._apply_operation(pow, other, reflected=True, op_code=_OPCode.POW) + return self._apply_operation(pow, other, reflected=True, op_code=_OPCode.RPOW) def _call(self, ufunc, op_code): if self._standalone_param: diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index fc1554756907..9799fdf3f459 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -604,19 +604,22 @@ def _read_parameter_expr_v13(buf, symbol_map, version, vectors): if expression_data.OP_CODE == 255: continue method_str = op_code_to_method(_OPCode(expression_data.OP_CODE)) - if expression_data.OP_CODE in {0, 1, 2, 3, 4, 13, 15}: + if expression_data.OP_CODE in {0, 1, 2, 3, 4, 13, 15, 18, 19, 20}: rhs = stack.pop() lhs = stack.pop() - # Reverse ops - if not isinstance(lhs, ParameterExpression) and isinstance(rhs, ParameterExpression): + # Reverse ops for commutative ops, which are add, mul (0 and 2 respectively) + # op codes 13 and 15 can never be reversed and 18, 19, 20 + # are the reversed versions of non-commuative operations + # so 1, 3, 4 and 18, 19, 20 handle this explicitly. + if ( + not isinstance(lhs, ParameterExpression) + and isinstance(rhs, ParameterExpression) + and expression_data.OP_CODE in {0, 2} + ): 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__" stack.append(getattr(rhs, method_str)(lhs)) else: stack.append(getattr(lhs, method_str)(rhs)) diff --git a/test/python/qpy/test_circuit_load_from_qpy.py b/test/python/qpy/test_circuit_load_from_qpy.py index 8726db41c7f3..8890a45ffe9e 100644 --- a/test/python/qpy/test_circuit_load_from_qpy.py +++ b/test/python/qpy/test_circuit_load_from_qpy.py @@ -338,6 +338,39 @@ def test_all_the_expression_ops(self): qc.rx(final_expr, 0) self.assert_roundtrip_equal(qc) + def test_rpow(self): + """Test rpow works as expected""" + qc = QuantumCircuit(1) + a = Parameter("A") + b = Parameter("B") + expr = 3.14159**a + expr = expr**b + expr = 1.2345**expr + qc.ry(expr, 0) + self.assert_roundtrip_equal(qc) + + def test_rsub(self): + """Test rsub works as expected""" + qc = QuantumCircuit(1) + a = Parameter("A") + b = Parameter("B") + expr = 3.14159 - a + expr = expr - b + expr = 1.2345 - expr + qc.ry(expr, 0) + self.assert_roundtrip_equal(qc) + + def test_rdiv(self): + """Test rdiv works as expected""" + qc = QuantumCircuit(1) + a = Parameter("A") + b = Parameter("B") + expr = 3.14159 / a + expr = expr / b + expr = 1.2345 / expr + qc.ry(expr, 0) + self.assert_roundtrip_equal(qc) + class TestUseSymengineFlag(QpyCircuitTestCase): """Test that the symengine flag works correctly.""" From 33953fe21ede991104eaa50c3197e1c55b7125a6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 6 Nov 2024 11:48:48 -0500 Subject: [PATCH 14/14] Fix lint --- qiskit/qpy/__init__.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 3e319b74ac9f..b6f7420ccfdf 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -352,9 +352,10 @@ expression. Starting in Version 13, QPY now represents the sequence of API calls used to create the :class:`.ParameterExpression`. -The main change in the serialization format is in the :ref:`qpy_param_expr_v3` payload. The ``expr_size`` bytes following -the head now contain an array of ``PARAM_EXPR_ELEM_V13`` structs. The intent is for this -array to be read one struct at a time, where each struct describes one of the calls to make to reconstruct the :class:`.ParameterExpression`. +The main change in the serialization format is in the :ref:`qpy_param_expr_v3` payload. The +``expr_size`` bytes following the head now contain an array of ``PARAM_EXPR_ELEM_V13`` structs. The +intent is for this array to be read one struct at a time, where each struct describes one of the +calls to make to reconstruct the :class:`.ParameterExpression`. PARAM_EXPR_ELEM_V13 ~~~~~~~~~~~~~~~~~~~ @@ -371,7 +372,8 @@ char rhs[16]; } PARAM_EXPR_ELEM_V13; -The ``op_code`` field is used to define the operation added to the :class:`.ParameterExpression`. The value can be: +The ``op_code`` field is used to define the operation added to the :class:`.ParameterExpression`. +The value can be: .. list-table:: PARAM_EXPR_ELEM_V13 op code values :header-rows: 1 @@ -446,15 +448,15 @@ - substitution If the type value is ``f`` ,``c`` or ``i``, the corresponding ``lhs`` or `rhs`` -field widths are 128 bits each. In the case of floats, the literal value is encoded as a double +field widths are 128 bits each. In the case of floats, the literal value is encoded as a double with 0 padding, while complex numbers are encoded as real part followed by imaginary part, -taking up 64 bits each. For ``i`, the value is encoded as a 64 bit signed integer with 0 padding for the full -128 bit width. ``n`` is used to represent a ``None`` and typically isn't directly used as -it indicates an argument that's not used. For ``p`` the data is the UUID for the +taking up 64 bits each. For ``i`, the value is encoded as a 64 bit signed integer with 0 padding +for the full 128 bit width. ``n`` is used to represent a ``None`` and typically isn't directly used +as it indicates an argument that's not used. For ``p`` the data is the UUID for the :class:`.Parameter` which can be looked up in the symbol map described in the ``map_elements`` outer :ref:`qpy_param_expr_v3` payload. If the type value is ``s`` this marks the start of a a new recursive section for a nested -:class:`.ParameterExpression`. For example, in the following snippet there is an inner ``expr`` +:class:`.ParameterExpression`. For example, in the following snippet there is an inner ``expr`` contained in ``final_expr``, constituting a nested expression:: from qiskit.circuit import Parameter @@ -466,7 +468,7 @@ expr = (x + y) / 2 final_expr = z**2 + expr -When ``s`` is encountered, this indicates that until an ``e` struct is reached, the next structs +When ``s`` is encountered, this indicates that until an ``e` struct is reached, the next structs are used for a recursive definition. For both ``s`` and ``e`` types, the data values are not used, and always set to 0. The type value of ``u`` is used to represent a substitution call. This is only used for ``lhs_type``