From 39fbb39d409f8f48a5927529d055d892f2de59a7 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 6 Jul 2023 03:10:13 +0100 Subject: [PATCH 1/3] Add `Expr` support to QPY for conditions and targets This adds support to QPY for the current `Expr` nodes, for *all* instruction parameters and conditions. The `Expr` tree is written out to the file in a sort of forwards Polish notation; each node has a type code and header, followed by a type-code-specific number of `Expr` children. While only `IfElseOp.condition`, `WhileLoopOp.condition` and `SwitchCaseOp.target` are allowed to have these nodes in Terra's data model right now, the QPY serialisation does not need to have this arbitrary restriction, and it's much easier just to write the general case. The backwards-compatibility guarantees of QPY are now brought to bear on the `Unary.Op` and `Binary.Op` enumeration values. They were already marked in the source code as their values needing to be part of the stable public interface, and their use in things like QPY is the reason why. --- qiskit/qpy/__init__.py | 141 +++++++++++ qiskit/qpy/binary_io/circuits.py | 49 ++-- qiskit/qpy/binary_io/value.py | 229 +++++++++++++++++- qiskit/qpy/common.py | 2 +- qiskit/qpy/formats.py | 51 +++- qiskit/qpy/type_keys.py | 103 +++++++- .../circuit/test_circuit_load_from_qpy.py | 185 ++++++++++++++ test/qpy_compat/test_qpy.py | 87 ++++++- 8 files changed, 812 insertions(+), 35 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 2ef008485a75..a9de7bce0222 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -126,6 +126,145 @@ by ``num_circuits`` in the file header). There is no padding between the circuits in the data. + +.. _qpy_version_9: + +Version 9 +========= + +Version 9 addds support for classical :class:`~.expr.Expr` nodes and their associated +:class:`~.types.Type`\\ s. + + +EXPRESSION +---------- + +An :class:`~.expr.Expr` node is represented by a stream of variable-width data. A node itself is +represented by a type code discriminator, followed by an EXPR_TYPE, followed by a type-code-specific +additional payload, followed by a type-code-specific number of child EXPRESSION payloads (this +number is not stored in the QPY file). These are described in the following table: + +====================== ========= ======================================================= ======== +Qiskit class Type code Payload Children +====================== ========= ======================================================= ======== +:class:`~.expr.Var` ``x`` One EXPR_VAR. 0 + +:class:`~.expr.Value` ``v`` One EXPR_VALUE. 0 + +:class:`~.expr.Cast` ``c`` One ``_Bool`` that corresponds to the value of 1 + ``implicit``. + +:class:`~.expr.Unary` ``u`` One ``uint8_t`` with the same numeric value as the 1 + :class:`.Unary.Op`. + +:class:`~.expr.Binary` ``b`` One ``uint8_t`` with the same numeric value as the 2 + :class:`.Binary.Op`. +====================== ========= ======================================================= ======== + + +EXPR_TYPE +--------- + +A :class:`~.types.Type` is encoded by a single-byte ASCII ``char`` that encodes the kind of type, +followed by a payload that varies depending on the type. The defined codes are: + +====================== ========= ================================================================= +Qiskit class Type code Payload +====================== ========= ================================================================= +:class:`~.types.Bool` ``b`` None. + +:class:`~.types.Uint` ``u`` One ``uint32_t width``. +====================== ========= ================================================================= + + +EXPR_VAR +-------- + +This represents a runtime variable of a :class:`~.expr.Var` node. These are a type code, followed +by a type-code-specific payload: + +=========================== ========= ============================================================ +Python class Type code Payload +=========================== ========= ============================================================ +:class:`.Clbit` ``C`` One ``uint32_t index`` that is the index of the + :class:`.Clbit` in the containing circuit. + +:class:`.ClassicalRegister` ``R`` One ``uint16_t reg_name_size``, followed by that many bytes + of UTF-8 string data of the register name. +=========================== ========= ============================================================ + + +EXPR_VALUE +---------- + +This represents a literal object in the classical type system, such as an integer. Currently there +are very few such literals. These are encoded as a type code, followed by a type-code-specific +payload. + +=========== ========= ============================================================================ +Python type Type code Payload +=========== ========= ============================================================================ +``bool`` ``b`` One ``_Bool value``. + +``int`` ``i`` One ``uint8_t num_bytes``, followed by the integer encoded into that many + many bytes (network order) in a two's complement representation. +=========== ========= ============================================================================ + + + +Changes to INSTRUCTION +---------------------- + +To support the use of :class:`~.expr.Expr` nodes in the fields :attr:`.IfElseOp.condition`, +:attr:`.WhileLoopOp.condition` and :attr:`.SwitchCaseOp.target`, the INSTRUCTION struct is changed +in an ABI compatible-manner to :ref:`its previous definition `. The new struct +is the C struct: + +.. code-block:: c + + struct { + uint16_t name_size; + uint16_t label_size; + uint16_t num_parameters; + uint32_t num_qargs; + uint32_t num_cargs; + uint8_t conditional_key; + uint16_t conditional_reg_name_size; + int64_t conditional_value; + uint32_t num_ctrl_qubits; + uint32_t ctrl_state; + } + +where the only change is that a ``uint8_t conditional_key`` entry has replaced ``_Bool +has_conditional``. This new ``conditional_key`` takes the following numeric values, with these +effects: + +===== ============================================================================================= +Value Effects +===== ============================================================================================= +0 The instruction has its ``.condition`` field set to ``None``. The + ``conditional_reg_name_size`` and ``conditional_value`` fields should be ignored. + +1 The instruction has its ``.condition`` field set to a 2-tuple of either a :class:`.Clbit` + or a :class:`.ClassicalRegister`, and a integer of value ``conditional_value``. The + INSTRUCTION payload, including its trailing data is parsed exactly as it would be in QPY + versions less than 8. + +2 The instruction has its ``.condition`` field set to a :class:`~.expr.Expr` node. The + ``conditional_reg_name_size`` and ``conditional_value`` fields should be ignored. The data + following the struct is followed (as in QPY versions less than 8) by ``name_size`` bytes of + UTF-8 string data for the class name and ``label_size`` bytes of UTF-8 string data for the + label (if any). Then, there is one INSTRUCTION_PARAM, which will contain an EXPRESSION. After + that, parsing continues with the INSTRUCTION_ARG structs, as in previous versions of QPY. +===== ============================================================================================= + + +Changes to INSTRUCTION_PARAM +---------------------------- + +A new type code ``x`` is added that defines an EXPRESSION parameter. + + .. _qpy_version_8: Version 8 @@ -525,6 +664,8 @@ only :class:`~.ScheduleBlock` payload is supported. Finally, :ref:`qpy_schedule_block` payload is packed for each CALIBRATION_DEF entry. +.. _qpy_instruction_v5: + INSTRUCTION ----------- diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 1172aa065a39..26425238fce3 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -26,6 +26,7 @@ from qiskit import circuit as circuit_mod from qiskit import extensions from qiskit.circuit import library, controlflow, CircuitInstruction +from qiskit.circuit.classical import expr from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.circuit.gate import Gate from qiskit.circuit.controlledgate import ControlledGate @@ -149,7 +150,9 @@ def _loads_instruction_parameter(type_key, data_bytes, version, vectors, registe elif type_key == type_keys.Value.REGISTER: param = _loads_register_param(data_bytes.decode(common.ENCODE), circuit, registers) else: - param = value.loads_value(type_key, data_bytes, version, vectors) + param = value.loads_value( + type_key, data_bytes, version, vectors, clbits=circuit.clbits, cregs=registers["c"] + ) return param @@ -183,12 +186,18 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, qargs = [] cargs = [] params = [] - condition_tuple = None - if instruction.has_condition: - condition_tuple = ( + condition = None + if (version < 5 and instruction.has_condition) or ( + version >= 5 and instruction.conditional_key == type_keys.Condition.TWO_TUPLE + ): + condition = ( _loads_register_param(condition_register, circuit, registers), instruction.condition_value, ) + elif version >= 5 and instruction.conditional_key == type_keys.Condition.EXPRESSION: + condition = value.read_value( + file_obj, version, vectors, clbits=circuit.clbits, cregs=registers["c"] + ) if circuit is not None: qubit_indices = dict(enumerate(circuit.qubits)) clbit_indices = dict(enumerate(circuit.clbits)) @@ -232,7 +241,7 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, inst_obj = _parse_custom_operation( custom_operations, gate_name, params, version, vectors, registers ) - inst_obj.condition = condition_tuple + inst_obj.condition = condition if instruction.label_size > 0: inst_obj.label = label if circuit is None: @@ -243,7 +252,7 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, inst_obj = _parse_custom_operation( custom_operations, gate_name, params, version, vectors, registers ) - inst_obj.condition = condition_tuple + inst_obj.condition = condition if instruction.label_size > 0: inst_obj.label = label if circuit is None: @@ -264,7 +273,7 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, raise AttributeError("Invalid instruction type: %s" % gate_name) if gate_name in {"IfElseOp", "WhileLoopOp"}: - gate = gate_class(condition_tuple, *params) + gate = gate_class(condition, *params) elif version >= 5 and issubclass(gate_class, ControlledGate): if gate_name in { "MCPhaseGate", @@ -279,7 +288,7 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, gate = gate_class(*params) gate.num_ctrl_qubits = instruction.num_ctrl_qubits gate.ctrl_state = instruction.ctrl_state - gate.condition = condition_tuple + gate.condition = condition else: if gate_name in { "Initialize", @@ -296,7 +305,7 @@ def _read_instruction(file_obj, circuit, registers, custom_operations, version, elif gate_name in {"BreakLoopOp", "ContinueLoopOp"}: params = [len(qargs), len(cargs)] gate = gate_class(*params) - gate.condition = condition_tuple + gate.condition = condition if instruction.label_size > 0: gate.label = label if circuit is None: @@ -512,7 +521,7 @@ def _dumps_instruction_parameter(param, index_map): type_key = type_keys.Value.REGISTER data_bytes = _dumps_register(param, index_map) else: - type_key, data_bytes = value.dumps_value(param) + type_key, data_bytes = value.dumps_value(param, index_map=index_map) return type_key, data_bytes @@ -544,13 +553,16 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map): custom_operations[gate_class_name] = instruction.operation custom_operations_list.append(gate_class_name) - has_condition = False + condition_type = type_keys.Condition.NONE condition_register = b"" condition_value = 0 - if getattr(instruction.operation, "condition", None): - has_condition = True - condition_register = _dumps_register(instruction.operation.condition[0], index_map) - condition_value = int(instruction.operation.condition[1]) + if (op_condition := getattr(instruction.operation, "condition", None)) is not None: + if isinstance(op_condition, expr.Expr): + condition_type = type_keys.Condition.EXPRESSION + else: + condition_type = type_keys.Condition.TWO_TUPLE + condition_register = _dumps_register(instruction.operation.condition[0], index_map) + condition_value = int(instruction.operation.condition[1]) gate_class_name = gate_class_name.encode(common.ENCODE) label = getattr(instruction.operation, "label") @@ -578,7 +590,7 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map): len(instruction_params), instruction.operation.num_qubits, instruction.operation.num_clbits, - has_condition, + condition_type.value, len(condition_register), condition_value, num_ctrl_qubits, @@ -587,7 +599,10 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map): file_obj.write(instruction_raw) file_obj.write(gate_class_name) file_obj.write(label_raw) - file_obj.write(condition_register) + if condition_type is type_keys.Condition.EXPRESSION: + value.write_value(file_obj, op_condition, index_map=index_map) + else: + file_obj.write(condition_register) # Encode instruciton args for qbit in instruction.qubits: instruction_arg_raw = struct.pack( diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 968d7e471826..aadbdcf42757 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -12,12 +12,16 @@ """Binary IO for any value objects, such as numbers, string, parameters.""" +from __future__ import annotations + +import collections.abc import struct import uuid import numpy as np -from qiskit.circuit import CASE_DEFAULT +from qiskit.circuit import CASE_DEFAULT, Clbit, ClassicalRegister +from qiskit.circuit.classical import expr, types from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement @@ -82,6 +86,106 @@ def _write_parameter_expression(file_obj, obj): file_obj.write(value_data) +class _ExprWriter(expr.ExprVisitor[None]): + __slots__ = ("file_obj", "clbit_indices") + + def __init__(self, file_obj, clbit_indices): + self.file_obj = file_obj + self.clbit_indices = clbit_indices + + def visit_generic(self, node, /): + raise exceptions.QpyError(f"unhandled Expr object '{node}'") + + def visit_var(self, node, /): + self.file_obj.write(type_keys.Expression.VAR) + _write_expr_type(self.file_obj, node.type) + if isinstance(node.var, Clbit): + self.file_obj.write(type_keys.ExprVar.CLBIT) + self.file_obj.write( + struct.pack( + formats.EXPR_VAR_CLBIT_PACK, + *formats.EXPR_VAR_CLBIT(self.clbit_indices[node.var]), + ) + ) + elif isinstance(node.var, ClassicalRegister): + self.file_obj.write(type_keys.ExprVar.REGISTER) + self.file_obj.write( + struct.pack( + formats.EXPR_VAR_REGISTER_PACK, *formats.EXPR_VAR_REGISTER(len(node.var.name)) + ) + ) + self.file_obj.write(node.var.name.encode(common.ENCODE)) + else: + raise exceptions.QpyError(f"unhandled Var object '{node.var}'") + + def visit_value(self, node, /): + self.file_obj.write(type_keys.Expression.VALUE) + _write_expr_type(self.file_obj, node.type) + if node.value is True or node.value is False: + self.file_obj.write(type_keys.ExprValue.BOOL) + self.file_obj.write( + struct.pack(formats.EXPR_VALUE_BOOL_PACK, *formats.EXPR_VALUE_BOOL(node.value)) + ) + elif isinstance(node.value, int): + self.file_obj.write(type_keys.ExprValue.INT) + if node.value == 0: + num_bytes = 0 + buffer = b"" + else: + # This wastes a byte for `-(2 ** (8*n - 1))` for natural `n`, but they'll still + # decode fine so it's not worth another special case. They'll encode to + # b"\xff\x80\x00\x00...", but we could encode them to b"\x80\x00\x00...". + num_bytes = (node.value.bit_length() // 8) + 1 + buffer = node.value.to_bytes(num_bytes, "big", signed=True) + self.file_obj.write( + struct.pack(formats.EXPR_VALUE_INT_PACK, *formats.EXPR_VALUE_INT(num_bytes)) + ) + self.file_obj.write(buffer) + else: + raise exceptions.QpyError(f"unhandled Value object '{node.value}'") + + def visit_cast(self, node, /): + self.file_obj.write(type_keys.Expression.CAST) + _write_expr_type(self.file_obj, node.type) + self.file_obj.write( + struct.pack(formats.EXPRESSION_CAST_PACK, *formats.EXPRESSION_CAST(node.implicit)) + ) + node.operand.accept(self) + + def visit_unary(self, node, /): + self.file_obj.write(type_keys.Expression.UNARY) + _write_expr_type(self.file_obj, node.type) + self.file_obj.write( + struct.pack(formats.EXPRESSION_UNARY_PACK, *formats.EXPRESSION_UNARY(node.op.value)) + ) + node.operand.accept(self) + + def visit_binary(self, node, /): + self.file_obj.write(type_keys.Expression.BINARY) + _write_expr_type(self.file_obj, node.type) + self.file_obj.write( + struct.pack(formats.EXPRESSION_BINARY_PACK, *formats.EXPRESSION_UNARY(node.op.value)) + ) + node.left.accept(self) + node.right.accept(self) + + +def _write_expr(file_obj, node: expr.Expr, clbit_indices: collections.abc.Mapping[Clbit, int]): + node.accept(_ExprWriter(file_obj, clbit_indices)) + + +def _write_expr_type(file_obj, type_: types.Type): + if type_.kind is types.Bool: + file_obj.write(type_keys.ExprType.BOOL) + elif type_.kind is types.Uint: + file_obj.write(type_keys.ExprType.UINT) + file_obj.write( + struct.pack(formats.EXPR_TYPE_UINT_PACK, *formats.EXPR_TYPE_UINT(type_.width)) + ) + else: + raise exceptions.QpyError(f"unhandled Type object '{type_};") + + def _read_parameter(file_obj): data = formats.PARAMETER( *struct.unpack(formats.PARAMETER_PACK, file_obj.read(formats.PARAMETER_SIZE)) @@ -123,9 +227,9 @@ def _read_parameter_expression(file_obj): if _optional.HAS_SYMENGINE: import symengine - expr = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))) + expr_ = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))) else: - expr = parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)) + expr_ = parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)) symbol_map = {} for _ in range(data.map_elements): elem_data = formats.PARAM_EXPR_MAP_ELEM( @@ -152,7 +256,7 @@ def _read_parameter_expression(file_obj): raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) symbol_map[symbol] = value - return ParameterExpression(symbol_map, expr) + return ParameterExpression(symbol_map, expr_) def _read_parameter_expression_v3(file_obj, vectors): @@ -164,9 +268,9 @@ def _read_parameter_expression_v3(file_obj, vectors): if _optional.HAS_SYMENGINE: import symengine - expr = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))) + expr_ = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))) else: - expr = parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)) + expr_ = parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)) symbol_map = {} for _ in range(data.map_elements): elem_data = formats.PARAM_EXPR_MAP_ELEM_V3( @@ -202,14 +306,101 @@ def _read_parameter_expression_v3(file_obj, vectors): raise exceptions.QpyError("Invalid parameter expression map type: %s" % elem_key) symbol_map[symbol] = value - return ParameterExpression(symbol_map, expr) + return ParameterExpression(symbol_map, expr_) + + +def _read_expr( + file_obj, + clbits: collections.abc.Sequence[Clbit], + cregs: collections.abc.Mapping[str, ClassicalRegister], +) -> expr.Expr: + # pylint: disable=too-many-return-statements + type_key = file_obj.read(1) + type_ = _read_expr_type(file_obj) + if type_key == type_keys.Expression.VAR: + var_type_key = file_obj.read(1) + if var_type_key == type_keys.ExprVar.CLBIT: + payload = formats.EXPR_VAR_CLBIT._make( + struct.unpack( + formats.EXPR_VAR_CLBIT_PACK, file_obj.read(formats.EXPR_VAR_CLBIT_SIZE) + ) + ) + return expr.Var(clbits[payload.index], type_) + if var_type_key == type_keys.ExprVar.REGISTER: + payload = formats.EXPR_VAR_REGISTER._make( + struct.unpack( + formats.EXPR_VAR_REGISTER_PACK, file_obj.read(formats.EXPR_VAR_REGISTER_SIZE) + ) + ) + name = file_obj.read(payload.reg_name_size).decode(common.ENCODE) + return expr.Var(cregs[name], type_) + raise exceptions.QpyError("Invalid classical-expression Var key '{var_type_key}'") + if type_key == type_keys.Expression.VALUE: + value_type_key = file_obj.read(1) + if value_type_key == type_keys.ExprValue.BOOL: + payload = formats.EXPR_VALUE_BOOL._make( + struct.unpack( + formats.EXPR_VALUE_BOOL_PACK, file_obj.read(formats.EXPR_VALUE_BOOL_SIZE) + ) + ) + return expr.Value(payload.value, type_) + if value_type_key == type_keys.ExprValue.INT: + payload = formats.EXPR_VALUE_INT._make( + struct.unpack( + formats.EXPR_VALUE_INT_PACK, file_obj.read(formats.EXPR_VALUE_INT_SIZE) + ) + ) + return expr.Value( + int.from_bytes(file_obj.read(payload.num_bytes), "big", signed=True), type_ + ) + raise exceptions.QpyError("Invalid classical-expression Value key '{value_type_key}'") + if type_key == type_keys.Expression.CAST: + payload = formats.EXPRESSION_CAST._make( + struct.unpack(formats.EXPRESSION_CAST_PACK, file_obj.read(formats.EXPRESSION_CAST_SIZE)) + ) + return expr.Cast(_read_expr(file_obj, clbits, cregs), type_, implicit=payload.implicit) + if type_key == type_keys.Expression.UNARY: + payload = formats.EXPRESSION_UNARY._make( + struct.unpack( + formats.EXPRESSION_UNARY_PACK, file_obj.read(formats.EXPRESSION_UNARY_SIZE) + ) + ) + return expr.Unary(expr.Unary.Op(payload.opcode), _read_expr(file_obj, clbits, cregs), type_) + if type_key == type_keys.Expression.BINARY: + payload = formats.EXPRESSION_BINARY._make( + struct.unpack( + formats.EXPRESSION_BINARY_PACK, file_obj.read(formats.EXPRESSION_BINARY_SIZE) + ) + ) + return expr.Binary( + expr.Binary.Op(payload.opcode), + _read_expr(file_obj, clbits, cregs), + _read_expr(file_obj, clbits, cregs), + type_, + ) + raise exceptions.QpyError("Invalid classical-expression Expr key '{type_key}'") + + +def _read_expr_type(file_obj) -> types.Type: + type_key = file_obj.read(1) + if type_key == type_keys.ExprType.BOOL: + return types.Bool() + if type_key == type_keys.ExprType.UINT: + elem = formats.EXPR_TYPE_UINT._make( + struct.unpack(formats.EXPR_TYPE_UINT_PACK, file_obj.read(formats.EXPR_TYPE_UINT_SIZE)) + ) + return types.Uint(elem.width) + raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") -def dumps_value(obj): +def dumps_value(obj, *, index_map=None): """Serialize input value object. Args: obj (any): Arbitrary value object to serialize. + index_map (dict): Dictionary with two keys, "q" and "c". Each key has a value that is a + dictionary mapping :class:`.Qubit` or :class:`.Clbit` instances (respectively) to their + integer indices. Returns: tuple: TypeKey and binary data. @@ -237,24 +428,30 @@ def dumps_value(obj): binary_data = common.data_to_binary(obj, _write_parameter) elif type_key == type_keys.Value.PARAMETER_EXPRESSION: binary_data = common.data_to_binary(obj, _write_parameter_expression) + elif type_key == type_keys.Value.EXPRESSION: + clbit_indices = {} if index_map is None else index_map["c"] + binary_data = common.data_to_binary(obj, _write_expr, clbit_indices=clbit_indices) else: raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") return type_key, binary_data -def write_value(file_obj, obj): +def write_value(file_obj, obj, *, index_map=None): """Write a value to the file like object. Args: file_obj (File): A file like object to write data. obj (any): Value to write. + index_map (dict): Dictionary with two keys, "q" and "c". Each key has a value that is a + dictionary mapping :class:`.Qubit` or :class:`.Clbit` instances (respectively) to their + integer indices. """ - type_key, data = dumps_value(obj) + type_key, data = dumps_value(obj, index_map=index_map) common.write_generic_typed_data(file_obj, type_key, data) -def loads_value(type_key, binary_data, version, vectors): +def loads_value(type_key, binary_data, version, vectors, *, clbits=(), cregs=None): """Deserialize input binary data to value object. Args: @@ -262,6 +459,8 @@ def loads_value(type_key, binary_data, version, vectors): binary_data (bytes): Data to deserialize. version (int): QPY version. vectors (dict): ParameterVector in current scope. + clbits (Sequence[Clbit]): Clbits in the current scope. + cregs (Mapping[str, ClassicalRegister]): Classical registers in the current scope. Returns: any: Deserialized value object. @@ -299,21 +498,25 @@ def loads_value(type_key, binary_data, version, vectors): return common.data_from_binary( binary_data, _read_parameter_expression_v3, vectors=vectors ) + if type_key == type_keys.Value.EXPRESSION: + return common.data_from_binary(binary_data, _read_expr, clbits=clbits, cregs=cregs or {}) raise exceptions.QpyError(f"Serialization for {type_key} is not implemented in value I/O.") -def read_value(file_obj, version, vectors): +def read_value(file_obj, version, vectors, *, clbits=(), cregs=None): """Read a value from the file like object. Args: file_obj (File): A file like object to write data. version (int): QPY version. vectors (dict): ParameterVector in current scope. + clbits (Sequence[Clbit]): Clbits in the current scope. + cregs (Mapping[str, ClassicalRegister]): Classical registers in the current scope. Returns: any: Deserialized value object. """ type_key, data = common.read_generic_typed_data(file_obj) - return loads_value(type_key, data, version, vectors) + return loads_value(type_key, data, version, vectors, clbits=clbits, cregs=cregs) diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 9c8376b4bf81..89eb1c7644b5 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -20,7 +20,7 @@ from qiskit.qpy import formats -QPY_VERSION = 8 +QPY_VERSION = 9 ENCODE = "utf8" diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index e442adfafbb7..959798aab053 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -94,14 +94,14 @@ "num_parameters", "num_qargs", "num_cargs", - "has_condition", + "conditional_key", "condition_register_size", "condition_value", "num_ctrl_qubits", "ctrl_state", ], ) -CIRCUIT_INSTRUCTION_V2_PACK = "!HHHII?HqII" +CIRCUIT_INSTRUCTION_V2_PACK = "!HHHIIBHqII" CIRCUIT_INSTRUCTION_V2_SIZE = struct.calcsize(CIRCUIT_INSTRUCTION_V2_PACK) @@ -272,3 +272,50 @@ INITIAL_LAYOUT_BIT = namedtuple("INITIAL_LAYOUT_BIT", ["index", "register_size"]) INITIAL_LAYOUT_BIT_PACK = "!ii" INITIAL_LAYOUT_BIT_SIZE = struct.calcsize(INITIAL_LAYOUT_BIT_PACK) + +# EXPRESSION + +EXPRESSION_CAST = namedtuple("EXPRESSION_CAST", ["implicit"]) +EXPRESSION_CAST_PACK = "!?" +EXPRESSION_CAST_SIZE = struct.calcsize(EXPRESSION_CAST_PACK) + +EXPRESSION_UNARY = namedtuple("EXPRESSION_UNARY", ["opcode"]) +EXPRESSION_UNARY_PACK = "!B" +EXPRESSION_UNARY_SIZE = struct.calcsize(EXPRESSION_UNARY_PACK) + +EXPRESSION_BINARY = namedtuple("EXPRESSION_BINARY", ["opcode"]) +EXPRESSION_BINARY_PACK = "!B" +EXPRESSION_BINARY_SIZE = struct.calcsize(EXPRESSION_BINARY_PACK) + + +# EXPR_TYPE + +EXPR_TYPE_BOOL = namedtuple("EXPR_TYPE_BOOL", []) +EXPR_TYPE_BOOL_PACK = "!" +EXPR_TYPE_BOOL_SIZE = struct.calcsize(EXPR_TYPE_BOOL_PACK) + +EXPR_TYPE_UINT = namedtuple("EXPR_TYPE_UINT", ["width"]) +EXPR_TYPE_UINT_PACK = "!L" +EXPR_TYPE_UINT_SIZE = struct.calcsize(EXPR_TYPE_UINT_PACK) + + +# EXPR_VAR + +EXPR_VAR_CLBIT = namedtuple("EXPR_VAR_CLBIT", ["index"]) +EXPR_VAR_CLBIT_PACK = "!L" +EXPR_VAR_CLBIT_SIZE = struct.calcsize(EXPR_VAR_CLBIT_PACK) + +EXPR_VAR_REGISTER = namedtuple("EXPR_VAR_REGISTER", ["reg_name_size"]) +EXPR_VAR_REGISTER_PACK = "!H" +EXPR_VAR_REGISTER_SIZE = struct.calcsize(EXPR_VAR_REGISTER_PACK) + + +# EXPR_VALUE + +EXPR_VALUE_BOOL = namedtuple("EXPR_VALUE_BOOL", ["value"]) +EXPR_VALUE_BOOL_PACK = "!?" +EXPR_VALUE_BOOL_SIZE = struct.calcsize(EXPR_VALUE_BOOL_PACK) + +EXPR_VALUE_INT = namedtuple("EXPR_VALUE_INT", ["num_bytes"]) +EXPR_VALUE_INT_PACK = "!B" +EXPR_VALUE_INT_SIZE = struct.calcsize(EXPR_VALUE_INT_PACK) diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index db99dcdf8f29..01995bb63db2 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -17,7 +17,7 @@ """ from abc import abstractmethod -from enum import Enum +from enum import Enum, IntEnum import numpy as np @@ -30,6 +30,7 @@ Clbit, ClassicalRegister, ) +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import PauliEvolutionGate from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterExpression @@ -110,6 +111,7 @@ class Value(TypeKeyBase): PARAMETER_EXPRESSION = b"e" STRING = b"s" NULL = b"z" + EXPRESSION = b"x" @classmethod def assign(cls, obj): @@ -135,6 +137,8 @@ def assign(cls, obj): return cls.NULL if obj is CASE_DEFAULT: return cls.CASE_DEFAULT + if isinstance(obj, expr.Expr): + return cls.EXPRESSION raise exceptions.QpyError( f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." @@ -145,6 +149,18 @@ def retrieve(cls, type_key): raise NotImplementedError +class Condition(IntEnum): + """Type keys for the ``conditional_key`` field of the INSTRUCTION struct.""" + + # This class is deliberately raw integers and not in terms of ASCII characters for backwards + # compatiblity in the form as an old Boolean value was expanded; `NONE` and `TWO_TUPLE` must + # have the enumeration values 0 and 1. + + NONE = 0 + TWO_TUPLE = 1 + EXPRESSION = 2 + + class Container(TypeKeyBase): """Typle key enum for container-like object.""" @@ -420,3 +436,88 @@ def assign(cls, obj): @classmethod def retrieve(cls, type_key): raise NotImplementedError + + +class Expression(TypeKeyBase): + """Type keys for the ``EXPRESSION`` QPY item.""" + + VAR = b"x" + VALUE = b"v" + CAST = b"c" + UNARY = b"u" + BINARY = b"b" + + @classmethod + def assign(cls, obj): + if ( + isinstance(obj, expr.Expr) + and (key := getattr(cls, obj.__class__.__name__.upper(), None)) is not None + ): + return key + raise exceptions.QpyError(f"Object '{obj}' is not supported in {cls.__name__} namespace.") + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError + + +class ExprType(TypeKeyBase): + """Type keys for the ``EXPR_TYPE`` QPY item.""" + + BOOL = b"b" + UINT = b"u" + + @classmethod + def assign(cls, obj): + if ( + isinstance(obj, types.Type) + and (key := getattr(cls, obj.__class__.__name__.upper(), None)) is not None + ): + return key + raise exceptions.QpyError(f"Object '{obj}' is not supported in {cls.__name__} namespace.") + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError + + +class ExprVar(TypeKeyBase): + """Type keys for the ``EXPR_VAR`` QPY item.""" + + CLBIT = b"C" + REGISTER = b"R" + + @classmethod + def assign(cls, obj): + if isinstance(obj, Clbit): + return cls.CLBIT + if isinstance(obj, ClassicalRegister): + return cls.REGISTER + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError + + +class ExprValue(TypeKeyBase): + """Type keys for the ``EXPR_VALUE`` QPY item.""" + + BOOL = b"b" + INT = b"i" + + @classmethod + def assign(cls, obj): + if isinstance(obj, bool): + return cls.BOOL + if isinstance(obj, int): + return cls.INT + raise exceptions.QpyError( + f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." + ) + + @classmethod + def retrieve(cls, type_key): + raise NotImplementedError diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 8f7fa8f751af..c740587467d9 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -17,10 +17,12 @@ import json import random +import ddt import numpy as np from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, pulse from qiskit.circuit import CASE_DEFAULT +from qiskit.circuit.classical import expr, types from qiskit.circuit.classicalregister import Clbit from qiskit.circuit.quantumregister import Qubit from qiskit.circuit.random import random_circuit @@ -49,6 +51,7 @@ from qiskit.circuit.controlledgate import ControlledGate +@ddt.ddt class TestLoadFromQPY(QiskitTestCase): """Test circuit.from_qasm_* set of methods.""" @@ -1423,6 +1426,188 @@ def test_diagonal_gate(self): self.assertEqual(qc.data[0].operation.params, new_circuit.data[0].operation.params) self.assertDeprecatedBitProperties(qc, new_circuit) + @ddt.data(QuantumCircuit.if_test, QuantumCircuit.while_loop) + def test_if_else_while_expr_simple(self, control_flow): + """Test that `IfElseOp` and `WhileLoopOp` can have an `Expr` node as their `condition`, and + that this round-trips through QPY.""" + body = QuantumCircuit(1) + qr = QuantumRegister(2, "q1") + cr = ClassicalRegister(2, "c1") + qc = QuantumCircuit(qr, cr) + control_flow(qc, expr.equal(cr, 3), body.copy(), [0], []) + control_flow(qc, expr.lift(qc.clbits[0]), body.copy(), [0], []) + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_circuit = load(fptr)[0] + self.assertEqual(qc, new_circuit) + self.assertEqual(qc.qregs, new_circuit.qregs) + self.assertEqual(qc.cregs, new_circuit.cregs) + self.assertDeprecatedBitProperties(qc, new_circuit) + + @ddt.data(QuantumCircuit.if_test, QuantumCircuit.while_loop) + def test_if_else_while_expr_nested(self, control_flow): + """Test that `IfElseOp` and `WhileLoopOp` can have an `Expr` node as their `condition`, and + that this round-trips through QPY.""" + inner = QuantumCircuit(1) + outer = QuantumCircuit(1, 1) + control_flow(outer, expr.lift(outer.clbits[0]), inner.copy(), [0], []) + + qr = QuantumRegister(2, "q1") + cr = ClassicalRegister(2, "c1") + qc = QuantumCircuit(qr, cr) + control_flow(qc, expr.equal(cr, 3), outer.copy(), [1], [1]) + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_circuit = load(fptr)[0] + self.assertEqual(qc, new_circuit) + self.assertEqual(qc.qregs, new_circuit.qregs) + self.assertEqual(qc.cregs, new_circuit.cregs) + self.assertDeprecatedBitProperties(qc, new_circuit) + + def test_if_else_expr_stress(self): + """Stress-test the `Expr` handling in the condition of an `IfElseOp`. This should hit on + every aspect of the `Expr` tree.""" + inner = QuantumCircuit(1) + inner.x(0) + + outer = QuantumCircuit(1, 1) + outer.if_test(expr.cast(outer.clbits[0], types.Bool()), inner.copy(), [0], []) + + # Register whose size is deliberately larger that one byte. + cr1 = ClassicalRegister(256, "c1") + cr2 = ClassicalRegister(4, "c2") + loose = Clbit() + qc = QuantumCircuit([Qubit(), Qubit(), loose], cr1, cr2) + qc.rz(1.0, 0) + qc.if_test( + expr.logic_and( + expr.logic_and( + expr.logic_or( + expr.cast( + expr.less(expr.bit_and(cr1, 0x0F), expr.bit_not(cr1)), + types.Bool(), + ), + expr.cast( + expr.less_equal(expr.bit_or(cr2, 7), expr.bit_xor(cr2, 7)), + types.Bool(), + ), + ), + expr.logic_and( + expr.logic_or(expr.equal(cr2, 2), expr.logic_not(expr.not_equal(cr2, 3))), + expr.logic_or( + expr.greater(cr2, 3), + expr.greater_equal(cr2, 3), + ), + ), + ), + expr.logic_not(loose), + ), + outer.copy(), + [1], + [0], + ) + qc.rz(1.0, 0) + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_circuit = load(fptr)[0] + self.assertEqual(qc, new_circuit) + self.assertEqual(qc.qregs, new_circuit.qregs) + self.assertEqual(qc.cregs, new_circuit.cregs) + self.assertDeprecatedBitProperties(qc, new_circuit) + + def test_switch_expr_simple(self): + """Test that `SwitchCaseOp` can have an `Expr` node as its `target`, and that this + round-trips through QPY.""" + body = QuantumCircuit(1) + qr = QuantumRegister(2, "q1") + cr = ClassicalRegister(2, "c1") + qc = QuantumCircuit(qr, cr) + qc.switch(expr.bit_and(cr, 3), [(1, body.copy())], [0], []) + qc.switch(expr.logic_not(qc.clbits[0]), [(False, body.copy())], [0], []) + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_circuit = load(fptr)[0] + self.assertEqual(qc, new_circuit) + self.assertEqual(qc.qregs, new_circuit.qregs) + self.assertEqual(qc.cregs, new_circuit.cregs) + self.assertDeprecatedBitProperties(qc, new_circuit) + + def test_switch_expr_nested(self): + """Test that `SwitchCaseOp` can have an `Expr` node as its `target`, and that this + round-trips through QPY.""" + inner = QuantumCircuit(1) + outer = QuantumCircuit(1, 1) + outer.switch(expr.lift(outer.clbits[0]), [(False, inner.copy())], [0], []) + + qr = QuantumRegister(2, "q1") + cr = ClassicalRegister(2, "c1") + qc = QuantumCircuit(qr, cr) + qc.switch(expr.lift(cr), [(3, outer.copy())], [1], [1]) + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_circuit = load(fptr)[0] + self.assertEqual(qc, new_circuit) + self.assertEqual(qc.qregs, new_circuit.qregs) + self.assertEqual(qc.cregs, new_circuit.cregs) + self.assertDeprecatedBitProperties(qc, new_circuit) + + def test_switch_expr_stress(self): + """Stress-test the `Expr` handling in the target of a `SwitchCaseOp`. This should hit on + every aspect of the `Expr` tree.""" + inner = QuantumCircuit(1) + inner.x(0) + + outer = QuantumCircuit(1, 1) + outer.switch(expr.cast(outer.clbits[0], types.Bool()), [(True, inner.copy())], [0], []) + + # Register whose size is deliberately larger that one byte. + cr1 = ClassicalRegister(256, "c1") + cr2 = ClassicalRegister(4, "c2") + loose = Clbit() + qc = QuantumCircuit([Qubit(), Qubit(), loose], cr1, cr2) + qc.rz(1.0, 0) + qc.switch( + expr.logic_and( + expr.logic_and( + expr.logic_or( + expr.cast( + expr.less(expr.bit_and(cr1, 0x0F), expr.bit_not(cr1)), + types.Bool(), + ), + expr.cast( + expr.less_equal(expr.bit_or(cr2, 7), expr.bit_xor(cr2, 7)), + types.Bool(), + ), + ), + expr.logic_and( + expr.logic_or(expr.equal(cr2, 2), expr.logic_not(expr.not_equal(cr2, 3))), + expr.logic_or( + expr.greater(cr2, 3), + expr.greater_equal(cr2, 3), + ), + ), + ), + expr.logic_not(loose), + ), + [(False, outer.copy())], + [1], + [0], + ) + qc.rz(1.0, 0) + with io.BytesIO() as fptr: + dump(qc, fptr) + fptr.seek(0) + new_circuit = load(fptr)[0] + self.assertEqual(qc, new_circuit) + self.assertEqual(qc.qregs, new_circuit.qregs) + self.assertEqual(qc.cregs, new_circuit.cregs) + self.assertDeprecatedBitProperties(qc, new_circuit) + def test_qpy_deprecation(self): """Test the old import path's deprecations fire.""" with self.assertWarnsRegex(DeprecationWarning, "is deprecated"): diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 7a2b23fa8403..fc63533565c3 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -595,6 +595,89 @@ def generate_layout_circuits(): return [qc] +def generate_control_flow_expr(): + """`IfElseOp`, `WhileLoopOp` and `SwitchCaseOp` with `Expr` nodes in their discriminators.""" + from qiskit.circuit.classical import expr, types + + body1 = QuantumCircuit(1) + body1.x(0) + qr1 = QuantumRegister(2, "q1") + cr1 = ClassicalRegister(2, "c1") + qc1 = QuantumCircuit(qr1, cr1) + qc1.if_test(expr.equal(cr1, 3), body1.copy(), [0], []) + qc1.while_loop(expr.logic_not(cr1[1]), body1.copy(), [0], []) + + inner2 = QuantumCircuit(1) + inner2.x(0) + outer2 = QuantumCircuit(1, 1) + outer2.if_test(expr.logic_not(outer2.clbits[0]), inner2, [0], []) + qr2 = QuantumRegister(2, "q2") + cr1_2 = ClassicalRegister(3, "c1") + cr2_2 = ClassicalRegister(3, "c2") + qc2 = QuantumCircuit(qr2, cr1_2, cr2_2) + qc2.if_test(expr.logic_or(expr.less(cr1_2, cr2_2), cr1_2[1]), outer2, [1], [1]) + + inner3 = QuantumCircuit(1) + inner3.x(0) + outer3 = QuantumCircuit(1, 1) + outer3.switch(expr.logic_not(outer2.clbits[0]), [(False, inner2)], [0], []) + qr3 = QuantumRegister(2, "q2") + cr1_3 = ClassicalRegister(3, "c1") + cr2_3 = ClassicalRegister(3, "c2") + qc3 = QuantumCircuit(qr3, cr1_3, cr2_3) + qc3.switch(expr.bit_xor(cr1_3, cr2_3), [(0, outer2)], [1], [1]) + + cr1_4 = ClassicalRegister(256, "c1") + cr2_4 = ClassicalRegister(4, "c2") + cr3_4 = ClassicalRegister(4, "c3") + inner4 = QuantumCircuit(1) + inner4.x(0) + outer_loose = Clbit() + outer4 = QuantumCircuit(QuantumRegister(2, "q_outer"), cr2_4, [outer_loose], cr1_4) + outer4.if_test( + expr.logic_and( + expr.logic_or( + expr.greater(expr.bit_or(cr2_4, 7), 10), + expr.equal(expr.bit_and(cr1_4, cr1_4), expr.bit_not(cr1_4)), + ), + expr.logic_or( + outer_loose, + expr.cast(cr1_4, types.Bool()), + ), + ), + inner4, + [0], + [], + ) + qc4_loose = Clbit() + qc4 = QuantumCircuit(QuantumRegister(2, "qr4"), cr1_4, cr2_4, cr3_4, [qc4_loose]) + qc4.rz(np.pi, 0) + qc4.switch( + expr.logic_and( + expr.logic_or( + expr.logic_or( + expr.less(cr2_4, cr3_4), + expr.logic_not(expr.greater_equal(cr3_4, cr2_4)), + ), + expr.logic_or( + expr.logic_not(expr.less_equal(cr3_4, cr2_4)), + expr.greater(cr2_4, cr3_4), + ), + ), + expr.logic_and( + expr.equal(cr3_4, 2), + expr.not_equal(expr.bit_xor(cr1_4, 0x0F), 0x0F), + ), + ), + [(False, outer4)], + [1, 0], + list(cr2_4) + [qc4_loose] + list(cr1_4), + ) + qc4.rz(np.pi, 0) + + return [qc1, qc2, qc3, qc4] + + def generate_circuits(version_parts): """Generate reference circuits.""" output_circuits = { @@ -632,8 +715,10 @@ def generate_circuits(version_parts): if version_parts >= (0, 24, 1): output_circuits["open_controlled_gates.qpy"] = generate_open_controlled_gates() output_circuits["controlled_gates.qpy"] = generate_controlled_gates() - if version_parts > (0, 24, 2): + if version_parts >= (0, 24, 2): output_circuits["layout.qpy"] = generate_layout_circuits() + if version_parts >= (0, 25, 0): + output_circuits["control_flow_expr.qpy"] = generate_control_flow_expr() return output_circuits From 912c1e30796bee37dcf7639cd88b46c3832dd2e3 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 19 Jul 2023 20:06:39 +0100 Subject: [PATCH 2/3] Improve documentation of `EXPRESSION` payload --- qiskit/qpy/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index a9de7bce0222..631b6865fea1 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -140,9 +140,15 @@ ---------- An :class:`~.expr.Expr` node is represented by a stream of variable-width data. A node itself is -represented by a type code discriminator, followed by an EXPR_TYPE, followed by a type-code-specific -additional payload, followed by a type-code-specific number of child EXPRESSION payloads (this -number is not stored in the QPY file). These are described in the following table: +represented by (in order in the byte stream): + +#. a one-byte type code discriminator; +#. an EXPR_TYPE object; +#. a type-code-specific additional payload; +#. a type-code-specific number of child EXPRESSION payloads (the number of these is implied by the + type code and not explicitly stored). + +Each of these are described in the following table: ====================== ========= ======================================================= ======== Qiskit class Type code Payload Children From 11e2eaa1a5b95de75568dd60d26287feca69fbac Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 19 Jul 2023 20:08:29 +0100 Subject: [PATCH 3/3] Factor out magic numbers from discriminator sizes --- qiskit/qpy/binary_io/value.py | 8 ++++---- qiskit/qpy/formats.py | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index aadbdcf42757..2edac6c81b52 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -315,10 +315,10 @@ def _read_expr( cregs: collections.abc.Mapping[str, ClassicalRegister], ) -> expr.Expr: # pylint: disable=too-many-return-statements - type_key = file_obj.read(1) + type_key = file_obj.read(formats.EXPRESSION_DISCRIMINATOR_SIZE) type_ = _read_expr_type(file_obj) if type_key == type_keys.Expression.VAR: - var_type_key = file_obj.read(1) + var_type_key = file_obj.read(formats.EXPR_VAR_DISCRIMINATOR_SIZE) if var_type_key == type_keys.ExprVar.CLBIT: payload = formats.EXPR_VAR_CLBIT._make( struct.unpack( @@ -336,7 +336,7 @@ def _read_expr( return expr.Var(cregs[name], type_) raise exceptions.QpyError("Invalid classical-expression Var key '{var_type_key}'") if type_key == type_keys.Expression.VALUE: - value_type_key = file_obj.read(1) + value_type_key = file_obj.read(formats.EXPR_VALUE_DISCRIMINATOR_SIZE) if value_type_key == type_keys.ExprValue.BOOL: payload = formats.EXPR_VALUE_BOOL._make( struct.unpack( @@ -382,7 +382,7 @@ def _read_expr( def _read_expr_type(file_obj) -> types.Type: - type_key = file_obj.read(1) + type_key = file_obj.read(formats.EXPR_TYPE_DISCRIMINATOR_SIZE) if type_key == type_keys.ExprType.BOOL: return types.Bool() if type_key == type_keys.ExprType.UINT: diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 959798aab053..64e34d9754a2 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -275,6 +275,8 @@ # EXPRESSION +EXPRESSION_DISCRIMINATOR_SIZE = 1 + EXPRESSION_CAST = namedtuple("EXPRESSION_CAST", ["implicit"]) EXPRESSION_CAST_PACK = "!?" EXPRESSION_CAST_SIZE = struct.calcsize(EXPRESSION_CAST_PACK) @@ -290,6 +292,8 @@ # EXPR_TYPE +EXPR_TYPE_DISCRIMINATOR_SIZE = 1 + EXPR_TYPE_BOOL = namedtuple("EXPR_TYPE_BOOL", []) EXPR_TYPE_BOOL_PACK = "!" EXPR_TYPE_BOOL_SIZE = struct.calcsize(EXPR_TYPE_BOOL_PACK) @@ -301,6 +305,8 @@ # EXPR_VAR +EXPR_VAR_DISCRIMINATOR_SIZE = 1 + EXPR_VAR_CLBIT = namedtuple("EXPR_VAR_CLBIT", ["index"]) EXPR_VAR_CLBIT_PACK = "!L" EXPR_VAR_CLBIT_SIZE = struct.calcsize(EXPR_VAR_CLBIT_PACK) @@ -312,6 +318,8 @@ # EXPR_VALUE +EXPR_VALUE_DISCRIMINATOR_SIZE = 1 + EXPR_VALUE_BOOL = namedtuple("EXPR_VALUE_BOOL", ["value"]) EXPR_VALUE_BOOL_PACK = "!?" EXPR_VALUE_BOOL_SIZE = struct.calcsize(EXPR_VALUE_BOOL_PACK)