diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index e56e22efd178..7294854132f8 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -126,6 +126,144 @@ by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _qpy_version_8: + +Version 8 +========= + +Version 8 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_7: Version 7 @@ -468,6 +606,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 95adba8fdbd1..b0af39bf7e93 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -25,6 +25,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 @@ -147,7 +148,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 @@ -181,12 +184,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)) @@ -230,7 +239,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: @@ -241,7 +250,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: @@ -262,7 +271,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", @@ -277,7 +286,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", "StatePreparation", "UCRXGate", "UCRYGate", "UCRZGate"}: gate = gate_class(params) @@ -287,7 +296,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: @@ -503,7 +512,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 @@ -535,13 +544,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") @@ -569,7 +581,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, @@ -578,7 +590,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/formats.py b/qiskit/qpy/formats.py index 31cc32a9c405..4316ed21bdd0 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) @@ -261,3 +261,51 @@ MAP_ITEM = namedtuple("MAP_ITEM", ["key_size", "type", "size"]) MAP_ITEM_PACK = "!H1cH" MAP_ITEM_SIZE = struct.calcsize(MAP_ITEM_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 331c30471032..c18ff8a6516d 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.""" @@ -1408,6 +1411,188 @@ def test_incomplete_owned_bits(self): self.assertEqual(qc, new_circuit) 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 2a4b112e1508..fcdafae7f2b0 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -574,6 +574,89 @@ def generate_open_controlled_gates(): return circuits +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 = { @@ -611,6 +694,7 @@ def generate_circuits(version_parts): if version_parts >= (0, 25, 0): output_circuits["open_controlled_gates.qpy"] = generate_open_controlled_gates() output_circuits["controlled_gates.qpy"] = generate_controlled_gates() + output_circuits["control_flow_expr.qpy"] = generate_control_flow_expr() return output_circuits