From 16e8647733b86191b2f6fd1faf5e71851c168542 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 7 Dec 2021 19:14:14 -0500 Subject: [PATCH 01/17] WIP: Add qpy serialization for PauliEvolutionGate This commit adds serialization for the PauliEvolutionGate class so that we can exactly reproduce a PauliEvolutionGate over QPY. This works by bumping the qpy format version and adding new structs to represent the PauliEvolutionGate and all it's child attribute types. --- qiskit/circuit/qpy_serialization.py | 238 +++++++++++++++++- .../circuit/test_circuit_load_from_qpy.py | 15 +- 2 files changed, 240 insertions(+), 13 deletions(-) diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index 3785f4d433d6..60da01b10aa5 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -100,6 +100,76 @@ by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _version_3: + +Version 3 +========= + +Version 3 of the QPY format is identical to :ref:`version_2` except that it defines +a struct format to represent a :class:`~qiskit.circuit.library.PauliEvolutionGate` +natively in QPY. To accomplish this the :ref:`custom_definition` struct now supports +a new type value ``'p'`` to represent a :class:`~qiskit.circuit.library.PauliEvolutionGate`. +Enties in the custom instructions tables have unique name generated that start with the +string ``"###PauliEvolutionGate_"`` followed by a uuid string. This gate name is reservered +in QPY and if you have a custom :class:`~qiskit.circuit.Instruction` object with a definition +set and that name prefix it will error. If it's of type ``'p'`` the data payload is defined +as follows: + +PAULI_EVOLUTION +--------------- + +This represents the high level :class:`~qiskit.circuit.library.PauliEvolutionGate` + +.. code-block:: c + + struct { + uint64_t operator_size; + char time_type; + uint64_t time_size; + uint64_t synthesis_size; + } + +This is immediatedly followed by ``operator_size`` bytes represented by the :ref:`pauli_sum_op` +data which is then followed by the :ref:`evolution_synthesis` data. Following that we have +``time_size`` bytes representing the ``time`` attribute. The encoding of these bytes is determined +by the value of ``time_type``. Possible values of ``time_type`` are ``'f'``, ``'p'``, and ``'e'``. +If ``time_type`` is ``'f'`` it's a double, ``'p'`` defines a :class:`~qiskit.circuit.Parameter` +object which is represented by a :ref:`param_struct`, ``e`` defines a +:class:`~qiskit.circuit.ParameterExpression` object (that's not a +:class:`~qiskit.circuit.Parameter`) which is represented by a :ref:`param_expr`. Following that is +``synthesis_size`` bytes. These are define + +.. pauli_sum_op + +PAULI_SUM_OP +------------ + +This represents an instance of :class:`~qiskit.opflow.PauliSumOp`. + + +.. code-block: c + + struct { + uint32_t pauli_list_size; + char coeff_type; + uint64_t coeff_size; + char grouping_type; + } + +which is immediately followed by ``pauli_list_size`` bytes which are .npy format [#f2]_ +data which represents the pauli list. +:ref:`pauli_list_elements`. Then following that will be ``coeff_size`` bytes that +represent the data for the coefficient. The data in these bytes are based on the +value of ``coeff_type``. Possible values for ``coeff_type`` are +``'f'``, ``'p'``, ``'e'``, ``'s'``, or ``'c'`` which dictate the format. For ``'f'`` +it's a double, ``'c'`` is a complex and the data is represented by the struct +format in the :ref:`param_expr` section. ``'p'`` defines a +:class:`~qiskit.circuit.Parameter` object which is represented by a +:ref:`param_struct`, ``e`` defines a :class:`~qiskit.circuit.ParameterExpression` +object (that's not a :class:`~qiskit.circuit.Parameter`) which is represented by a +:ref:`param_expr`. The ``grouping_type`` can either be of value ``'n'`` to represent +``"None"`` or ``'t'`` to represent ``"TPB"``. + .. _version_2: Version 2 @@ -213,6 +283,8 @@ ``qr`` would have ``standalone`` set to ``False``. +.. custom_definition: + CUSTOM_DEFINITIONS ------------------ @@ -244,7 +316,10 @@ If ``custom_definition`` is ``True`` that means that the immediately following ``size`` bytes contains a QPY circuit data which can be used for the custom definition of that gate. If ``custom_definition`` is ``False`` then the -instruction can be considered opaque (ie no definition). +instruction can be considered opaque (ie no definition). The ``type`` field +determines what type of object will get created with the custom definition. +If it's ``'g'`` it will be a :class:`~qiskit.circuit.Gate` object, ``'i'`` +it will be a :class:`~qiskit.circuit.Instruction` object. INSTRUCTIONS ------------ @@ -316,6 +391,8 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom (see below), and ``'n'`` represents an object from numpy (either an ``ndarray`` or a numpy type) which means the data is .npy format [#f2]_ data. +.. param_struct: + PARAMETER --------- @@ -407,6 +484,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom from qiskit.extensions import quantum_initializer from qiskit.version import __version__ from qiskit.exceptions import QiskitError +from qiskit.quantum_info.operators import SparsePauliOp try: import symengine @@ -518,6 +596,18 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom COMPLEX = namedtuple("COMPLEX", ["real", "imag"]) COMPLEX_PACK = "!dd" COMPLEX_SIZE = struct.calcsize(COMPLEX_PACK) +# Pauli Evolution Gate +PAULI_EVOLUTION_DEF = namedtuple( + "PAULI_EVOLUTION_DEF", ["operator_size", "time_type", "time_size", "synth_method_size"] +) +PAULI_EVOLUTION_DEF_PACK = "!Q1cQQ" +PAULI_EVOLUTION_DEF_SIZE = struct.calcsize(PAULI_EVOLUTION_DEF_PACK) +# PauliSumOp +PAULI_SUM_OP = namedtuple( + "PAULI_SUM_OP", ["pauli_list_size", "coeff_type", "coeff_size", "grouping_type"] +) +PAULI_SUM_OP_PACK = "!I1cQ1c" +PAULI_SUM_OP_SIZE = struct.calcsize(PAULI_EVOLUTION_DEF_PACK) def _read_header_v2(file_obj): @@ -751,6 +841,8 @@ def _parse_custom_instruction(custom_instructions, gate_name, params): elif type_str == "g": inst_obj = Gate(gate_name, num_qubits, params) inst_obj.definition = definition + elif type_str == "p": + inst_obj = definition else: raise ValueError("Invalid custom instruction type '%s'" % type_str) return inst_obj @@ -779,7 +871,10 @@ def _read_custom_instructions(file_obj, version): definition_circuit = None if has_custom_definition: definition_buffer = io.BytesIO(file_obj.read(size)) - definition_circuit = _read_circuit(definition_buffer, version) + if version < 3 or not name.startswith(r"###PauliEvolutionGate"): + definition_circuit = _read_circuit(definition_buffer, version) + else: + definition_circuit = _read_pauli_evolution_gate(definition_buffer, version) custom_instructions[name] = (type_str, num_qubits, num_clbits, definition_circuit) return custom_instructions @@ -842,12 +937,16 @@ def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_m ) or gate_class_name == "Gate" or gate_class_name == "Instruction" - or isinstance(instruction_tuple[0], (library.BlueprintCircuit, library.PauliEvolutionGate)) + or isinstance(instruction_tuple[0], (library.BlueprintCircuit)) ): if instruction_tuple[0].name not in custom_instructions: custom_instructions[instruction_tuple[0].name] = instruction_tuple[0] gate_class_name = instruction_tuple[0].name + elif isinstance(instruction_tuple[0], library.PauliEvolutionGate): + evolutation_gate_str = r"###PauliEvolutionGate_" + str(uuid.uuid4()) + custom_instructions[evolutation_gate_str] = instruction_tuple[0] + has_condition = False condition_register = b"" condition_value = 0 @@ -936,9 +1035,135 @@ def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_m container.close() +def _write_pauli_sum_op(file_obj, sum_op): + buf = io.BytesIO() + pauli_list = sum_op.to_list(array=True) + np.save(buf, pauli_list) + buf.seek(0) + pauli_list_data = container.read() + pauli_list_size = len(data) + coeff = sum_op.coeff + if isinstance(coeff, float): + coeff_type = "f" + coeff_data = struct.pack("!d", coeff) + coeff_size = struct.calcsize("!d") + elif isinstance(param, Parameter): + coeff_type = "p" + _write_parameter(container, coeff) + container.seek(0) + coeff_data = container.read() + coeff_size = len(data) + elif isinstance(param, ParameterExpression): + coeff_type = "e" + _write_parameter_expression(container, coeff) + container.seek(0) + coeff_data = container.read() + coeff_size = len(data) + elif isinstance(param, complex): + coeff_type = "c" + coeff_data = struct.pack(COMPLEX_PACK, coeff.real, coeff.imag) + coeff_size = struct.calcsize(COMPLEX_PACK) + else: + raise TypeError(f"Invalid coefficient type {coeff} for PauliSumOp") + if sum_op.grouping_type == "None": + group_type = "n" + elif sum_op.grouping_type == "TPB": + group_type = "t" + else: + raise ValueError(f"Unknown grouping_type {sum_op.grouping_type} for PauliSumOp") + pauli_sum_op_raw = struct.pack( + PAULI_SUM_OP_PACK, pauli_list_size, coeff_type, coeff_size, group_type + ) + file_obj.write(pauli_sum_op_raw) + file_obj.write(pauli_list_data) + file_obj.write(coeff_data) + + +def _read_pauli_sum_op(file_obj): + sum_op_raw = struct.unpack(PAULI_SUM_OP_PACK, file_obj.read(PAULI_SUM_OP_SIZE)) + pauli_list_data = io.BytesIO(file_obj.read(sum_op_raw[0])) + primitive = SparsePauliOp.from_list(np.load(pauli_list_data)) + coeff_type = sum_op_raw[1] + coeff_data = file_obj.read(sum_op_raw[2]) + if coeff_type == "f": + coeff = struct.unpack("!d", coeff_data) + elif coeff_type == "p": + buf = io.BytesIO(coeff_data) + coeff = _read_parameter(buf) + elif coeff_type == "e": + buf = io.BytesIO(coeff_data) + coeff = _read_parameter_expression(buf) + elif coeff_type == "c": + value = complex(*struct.unpack(COMPLEX_PACK, coeff_data)) + if sum_op_raw[3] == "n": + grouping_type = "None" + elif sum_op_raw[3] == "t": + grouping_type = "TPB" + return PauliSumOp(primitive=primitive, coeff=coeff, grouping_type=grouping_type) + + +def _write_pauli_evolution_gate(file_obj, evolution_gate): + pauli_sum_buf = io.BytesIO() + _write_pauli_sum_op(pauli_sum_buf, evolution_gate.operator) + pauli_sum_data = pauli_sum_buf.getvalue() + pauli_sum_size = len(pauli_sum_data) + time = evolution_gate.time + if isinstance(time, float): + time_type = "f" + time_data = struct.pack("!d", time) + time_size = struct.calcsize("!d") + elif isinstance(param, Parameter): + time_type = "p" + _write_parameter(container, time) + container.seek(0) + time_data = container.read() + time_size = len(data) + elif isinstance(param, ParameterExpression): + time_type = "e" + _write_parameter_expression(container, time) + container.seek(0) + time_data = container.read() + time_size = len(data) + else: + raise TypeError(f"Invalid coefficient type {coeff} for PauliSumOp") + + # TODO: Add synthesis class to format (will need a new pack format and settings for classes) + synth_size = 0 + synth_data = b"" + pauli_evolution_raw = struct.pack( + PAULI_EVOLUTION_DEF_PACK, pauli_sum_size, time_type, time_size, synth_size + ) + file_obj.write(pauli_evolution_raw) + file_obj.write(pauli_sum_data) + file_obj.write(time_data) + file_obj.write(synth_data) + + +def _read_pauli_evolution_gate(file_obj): + pauli_evolution_raw = struct.unpack( + PAULI_EVOLUTION_DEF_PACK, file_obj.read(PAULI_EVOLUTION_DEF_SIZE) + ) + pauli_sum = _read_pauli_sum_op(io.BytesIO(file_obj.read(pauli_evolution_raw[0]))) + time_type = pauli_evolution_raw[1] + time_data = file_obj.read(file_obj.read(pauli_evolution_raw[2])) + if time_type == "f": + time = struct.unpack("!d", time_data) + elif time_type == "p": + buf = io.BytesIO(time_data) + time = _read_parameter(buf) + elif time_type == "e": + buf = io.BytesIO(time_data) + time = _read_parameter_expression(buf) + # TODO: Read synthesis class + synthesis = None + return PauliEvolutionGate(pauli_sum, time=time, synthesis=None) + + def _write_custom_instruction(file_obj, name, instruction): if isinstance(instruction, Gate): type_str = b"g" + elif isinstance(instruction, PauliEvolutionGate): + type_str = b"p" else: type_str = b"i" has_definition = False @@ -949,7 +1174,10 @@ def _write_custom_instruction(file_obj, name, instruction): if instruction.definition: has_definition = True definition_buffer = io.BytesIO() - _write_circuit(definition_buffer, instruction.definition) + if isinstance(instruction, PauliEvolutionGate): + _write_pauli_evolution_gate(definition_buffer, instruction) + else: + _write_circuit(definition_buffer, instruction.definition) definition_buffer.seek(0) data = definition_buffer.read() definition_buffer.close() @@ -1019,7 +1247,7 @@ def dump(circuits, file_obj): header = struct.pack( FILE_HEADER_PACK, b"QISKIT", - 2, + 3, version_parts[0], version_parts[1], version_parts[2], diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 53dbe21e45db..9e29958d1fd4 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -536,8 +536,8 @@ def test_qaoa(self): def test_evolutiongate(self): """Test loading a circuit with evolution gate works.""" - synthesis = LieTrotter(reps=2) - evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=0.2, synthesis=synthesis) +# synthesis = LieTrotter(reps=2) + evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=0.2, synthesis=None) qc = QuantumCircuit(2) qc.append(evo, range(2)) qpy_file = io.BytesIO() @@ -546,17 +546,16 @@ def test_evolutiongate(self): new_circ = load(qpy_file)[0] # remove wrapping of instructions - qc = qc.decompose().decompose() - new_circ = new_circ.decompose().decompose() +# qc = qc.decompose().decompose() +# new_circ = new_circ.decompose().decompose() self.assertEqual(qc, new_circ) self.assertEqual([x[0].label for x in qc.data], [x[0].label for x in new_circ.data]) - # enable these tests once we can can serialize allPauliEvolutionGate parameters such as - # new_evo = new_circ.data[0][0] + new_evo = new_circ.data[0][0] # SparsePauliOp and EvolutionSynthesis - # self.assertIsInstance(new_evo,PauliEvolutionGate) - # self.assertIsInstance(new_evo.synthesis, LieTrotter) + self.assertIsInstance(new_evo,PauliEvolutionGate) +# self.assertIsInstance(new_evo.synthesis, LieTrotter) def test_parameter_expression_global_phase(self): """Test a circuit with a parameter expression global_phase.""" From e8bd3a02aebad1b21ba7811b260bb778650c29bf Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 08:20:37 -0500 Subject: [PATCH 02/17] Fix handling of operator in PauliEvolutionGate This commit fixes the handling of the operator attribute in the PauliEvolutionGate. With this commit we can serialize a PauliEvolutionGate correctly with the exception of it's synthesis class which still needs to be supported. --- qiskit/circuit/qpy_serialization.py | 207 +++++++----------- .../circuit/test_circuit_load_from_qpy.py | 25 ++- 2 files changed, 97 insertions(+), 135 deletions(-) diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index 60da01b10aa5..fc713741e88f 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -123,26 +123,28 @@ .. code-block:: c struct { - uint64_t operator_size; + uint64_t operator_count; + _Bool standalone_op; char time_type; uint64_t time_size; uint64_t synthesis_size; } -This is immediatedly followed by ``operator_size`` bytes represented by the :ref:`pauli_sum_op` -data which is then followed by the :ref:`evolution_synthesis` data. Following that we have -``time_size`` bytes representing the ``time`` attribute. The encoding of these bytes is determined -by the value of ``time_type``. Possible values of ``time_type`` are ``'f'``, ``'p'``, and ``'e'``. -If ``time_type`` is ``'f'`` it's a double, ``'p'`` defines a :class:`~qiskit.circuit.Parameter` -object which is represented by a :ref:`param_struct`, ``e`` defines a -:class:`~qiskit.circuit.ParameterExpression` object (that's not a -:class:`~qiskit.circuit.Parameter`) which is represented by a :ref:`param_expr`. Following that is -``synthesis_size`` bytes. These are define +This is immediately followed by ``operator_count`` elements defined by the :ref:`pauli_sum_op` +payload. Following that we have `time_size`` bytes representing the ``time`` attribute. If +``standalone_op`` is ``True`` then there must only be a single operator. The +encoding of these bytes is determined by the value of ``time_type``. Possible values of +``time_type`` are ``'f'``, ``'p'``, and ``'e'``. If ``time_type`` is ``'f'`` it's a double, +``'p'`` defines a :class:`~qiskit.circuit.Parameter` object which is represented by a +:ref:`param_struct`, ``e`` defines a :class:`~qiskit.circuit.ParameterExpression` object +(that's not a :class:`~qiskit.circuit.Parameter`) which is represented by a :ref:`param_expr`. +Following that is ``synthesis_size`` bytes which is a utf8 encoded json payload representing +the :class:`.EvolutionSynthesis` class used by the gate. .. pauli_sum_op -PAULI_SUM_OP ------------- +SPARSE_PAULI_OP_LIST_ELEM +------------------------- This represents an instance of :class:`~qiskit.opflow.PauliSumOp`. @@ -150,25 +152,11 @@ .. code-block: c struct { - uint32_t pauli_list_size; - char coeff_type; - uint64_t coeff_size; - char grouping_type; + uint32_t pauli_op_size; } -which is immediately followed by ``pauli_list_size`` bytes which are .npy format [#f2]_ -data which represents the pauli list. -:ref:`pauli_list_elements`. Then following that will be ``coeff_size`` bytes that -represent the data for the coefficient. The data in these bytes are based on the -value of ``coeff_type``. Possible values for ``coeff_type`` are -``'f'``, ``'p'``, ``'e'``, ``'s'``, or ``'c'`` which dictate the format. For ``'f'`` -it's a double, ``'c'`` is a complex and the data is represented by the struct -format in the :ref:`param_expr` section. ``'p'`` defines a -:class:`~qiskit.circuit.Parameter` object which is represented by a -:ref:`param_struct`, ``e`` defines a :class:`~qiskit.circuit.ParameterExpression` -object (that's not a :class:`~qiskit.circuit.Parameter`) which is represented by a -:ref:`param_expr`. The ``grouping_type`` can either be of value ``'n'`` to represent -``"None"`` or ``'t'`` to represent ``"TPB"``. +which is immediately followed by ``pauli_op_size`` bytes which are .npy format [#f2]_ +data which represents the :class:`~qiskit.quantum_info.SparsePauliOp`. .. _version_2: @@ -600,14 +588,12 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom PAULI_EVOLUTION_DEF = namedtuple( "PAULI_EVOLUTION_DEF", ["operator_size", "time_type", "time_size", "synth_method_size"] ) -PAULI_EVOLUTION_DEF_PACK = "!Q1cQQ" +PAULI_EVOLUTION_DEF_PACK = "!Q?1cQQ" PAULI_EVOLUTION_DEF_SIZE = struct.calcsize(PAULI_EVOLUTION_DEF_PACK) -# PauliSumOp -PAULI_SUM_OP = namedtuple( - "PAULI_SUM_OP", ["pauli_list_size", "coeff_type", "coeff_size", "grouping_type"] -) -PAULI_SUM_OP_PACK = "!I1cQ1c" -PAULI_SUM_OP_SIZE = struct.calcsize(PAULI_EVOLUTION_DEF_PACK) +# SparsePauliOp List +SPARSE_PAULI_OP_LIST_ELEM = namedtuple("SPARSE_PAULI_OP_LIST_ELEMENT", ["size"]) +SPARSE_PAULI_OP_LIST_ELEM_PACK = "!Q" +SPARSE_PAULI_OP_LIST_ELEM_SIZE = struct.calcsize(SPARSE_PAULI_OP_LIST_ELEM_PACK) def _read_header_v2(file_obj): @@ -873,8 +859,8 @@ def _read_custom_instructions(file_obj, version): definition_buffer = io.BytesIO(file_obj.read(size)) if version < 3 or not name.startswith(r"###PauliEvolutionGate"): definition_circuit = _read_circuit(definition_buffer, version) - else: - definition_circuit = _read_pauli_evolution_gate(definition_buffer, version) + elif name.startswith(r"###PauliEvolutionGate"): + definition_circuit = _read_pauli_evolution_gate(definition_buffer) custom_instructions[name] = (type_str, num_qubits, num_clbits, definition_circuit) return custom_instructions @@ -944,8 +930,8 @@ def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_m gate_class_name = instruction_tuple[0].name elif isinstance(instruction_tuple[0], library.PauliEvolutionGate): - evolutation_gate_str = r"###PauliEvolutionGate_" + str(uuid.uuid4()) - custom_instructions[evolutation_gate_str] = instruction_tuple[0] + gate_class_name = r"###PauliEvolutionGate_" + str(uuid.uuid4()) + custom_instructions[gate_class_name] = instruction_tuple[0] has_condition = False condition_register = b"" @@ -1035,91 +1021,39 @@ def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_m container.close() -def _write_pauli_sum_op(file_obj, sum_op): - buf = io.BytesIO() - pauli_list = sum_op.to_list(array=True) - np.save(buf, pauli_list) - buf.seek(0) - pauli_list_data = container.read() - pauli_list_size = len(data) - coeff = sum_op.coeff - if isinstance(coeff, float): - coeff_type = "f" - coeff_data = struct.pack("!d", coeff) - coeff_size = struct.calcsize("!d") - elif isinstance(param, Parameter): - coeff_type = "p" - _write_parameter(container, coeff) - container.seek(0) - coeff_data = container.read() - coeff_size = len(data) - elif isinstance(param, ParameterExpression): - coeff_type = "e" - _write_parameter_expression(container, coeff) - container.seek(0) - coeff_data = container.read() - coeff_size = len(data) - elif isinstance(param, complex): - coeff_type = "c" - coeff_data = struct.pack(COMPLEX_PACK, coeff.real, coeff.imag) - coeff_size = struct.calcsize(COMPLEX_PACK) - else: - raise TypeError(f"Invalid coefficient type {coeff} for PauliSumOp") - if sum_op.grouping_type == "None": - group_type = "n" - elif sum_op.grouping_type == "TPB": - group_type = "t" - else: - raise ValueError(f"Unknown grouping_type {sum_op.grouping_type} for PauliSumOp") - pauli_sum_op_raw = struct.pack( - PAULI_SUM_OP_PACK, pauli_list_size, coeff_type, coeff_size, group_type - ) - file_obj.write(pauli_sum_op_raw) - file_obj.write(pauli_list_data) - file_obj.write(coeff_data) - - -def _read_pauli_sum_op(file_obj): - sum_op_raw = struct.unpack(PAULI_SUM_OP_PACK, file_obj.read(PAULI_SUM_OP_SIZE)) - pauli_list_data = io.BytesIO(file_obj.read(sum_op_raw[0])) - primitive = SparsePauliOp.from_list(np.load(pauli_list_data)) - coeff_type = sum_op_raw[1] - coeff_data = file_obj.read(sum_op_raw[2]) - if coeff_type == "f": - coeff = struct.unpack("!d", coeff_data) - elif coeff_type == "p": - buf = io.BytesIO(coeff_data) - coeff = _read_parameter(buf) - elif coeff_type == "e": - buf = io.BytesIO(coeff_data) - coeff = _read_parameter_expression(buf) - elif coeff_type == "c": - value = complex(*struct.unpack(COMPLEX_PACK, coeff_data)) - if sum_op_raw[3] == "n": - grouping_type = "None" - elif sum_op_raw[3] == "t": - grouping_type = "TPB" - return PauliSumOp(primitive=primitive, coeff=coeff, grouping_type=grouping_type) - - def _write_pauli_evolution_gate(file_obj, evolution_gate): - pauli_sum_buf = io.BytesIO() - _write_pauli_sum_op(pauli_sum_buf, evolution_gate.operator) - pauli_sum_data = pauli_sum_buf.getvalue() - pauli_sum_size = len(pauli_sum_data) + operator_list = evolution_gate.operator + standalone = False + if not isinstance(operator_list, list): + operator_list = [operator_list] + standalone = True + num_operators = len(operator_list) + pauli_data_buf = io.BytesIO() + for operator in operator_list: + element_buf = io.BytesIO() + buf = io.BytesIO() + pauli_list = operator.to_list(array=True) + np.save(buf, pauli_list) + data = buf.getvalue() + element_metadata = struct.pack(SPARSE_PAULI_OP_LIST_ELEM_PACK, len(data)) + element_buf.write(element_metadata) + element_buf.write(data) + pauli_data_buf.write(element_buf.getvalue()) + pauli_list_data = buf.getvalue() + pauli_list_size = len(pauli_list_data) time = evolution_gate.time if isinstance(time, float): - time_type = "f" + time_type = b"f" time_data = struct.pack("!d", time) time_size = struct.calcsize("!d") elif isinstance(param, Parameter): - time_type = "p" + time_type = b"p" _write_parameter(container, time) container.seek(0) time_data = container.read() time_size = len(data) elif isinstance(param, ParameterExpression): - time_type = "e" + time_type = b"e" _write_parameter_expression(container, time) container.seek(0) time_data = container.read() @@ -1131,10 +1065,10 @@ def _write_pauli_evolution_gate(file_obj, evolution_gate): synth_size = 0 synth_data = b"" pauli_evolution_raw = struct.pack( - PAULI_EVOLUTION_DEF_PACK, pauli_sum_size, time_type, time_size, synth_size + PAULI_EVOLUTION_DEF_PACK, num_operators, standalone, time_type, time_size, synth_size ) file_obj.write(pauli_evolution_raw) - file_obj.write(pauli_sum_data) + file_obj.write(pauli_data_buf.getvalue()) file_obj.write(time_data) file_obj.write(synth_data) @@ -1143,27 +1077,42 @@ def _read_pauli_evolution_gate(file_obj): pauli_evolution_raw = struct.unpack( PAULI_EVOLUTION_DEF_PACK, file_obj.read(PAULI_EVOLUTION_DEF_SIZE) ) - pauli_sum = _read_pauli_sum_op(io.BytesIO(file_obj.read(pauli_evolution_raw[0]))) - time_type = pauli_evolution_raw[1] - time_data = file_obj.read(file_obj.read(pauli_evolution_raw[2])) - if time_type == "f": - time = struct.unpack("!d", time_data) - elif time_type == "p": + if pauli_evolution_raw[0] != 1 and pauli_evolution_raw[1]: + raise ValueError( + "Can't have a standalone operator with {pauli_evolution_raw[0]} operators in the payload" + ) + operator_list = [] + for _ in range(pauli_evolution_raw[0]): + op_size = struct.unpack( + SPARSE_PAULI_OP_LIST_ELEM_PACK, file_obj.read(SPARSE_PAULI_OP_LIST_ELEM_SIZE) + )[0] + operator_list.append(SparsePauliOp.from_list(np.load(io.BytesIO(file_obj.read(op_size))))) + if pauli_evolution_raw[1]: + pauli_op = operator_list[0] + else: + pauli_op = operator_list + + time_type = pauli_evolution_raw[2] + time_data = file_obj.read(pauli_evolution_raw[3]) + if time_type == b"f": + time = struct.unpack("!d", time_data)[0] + elif time_type == b"p": buf = io.BytesIO(time_data) time = _read_parameter(buf) - elif time_type == "e": + elif time_type == b"e": buf = io.BytesIO(time_data) time = _read_parameter_expression(buf) # TODO: Read synthesis class synthesis = None - return PauliEvolutionGate(pauli_sum, time=time, synthesis=None) + return_gate = library.PauliEvolutionGate(pauli_op, time=time, synthesis=None) + return return_gate def _write_custom_instruction(file_obj, name, instruction): - if isinstance(instruction, Gate): - type_str = b"g" - elif isinstance(instruction, PauliEvolutionGate): + if isinstance(instruction, library.PauliEvolutionGate): type_str = b"p" + elif isinstance(instruction, Gate): + type_str = b"g" else: type_str = b"i" has_definition = False @@ -1171,10 +1120,10 @@ def _write_custom_instruction(file_obj, name, instruction): data = None num_qubits = instruction.num_qubits num_clbits = instruction.num_clbits - if instruction.definition: + if instruction.definition or type_str == b"p": has_definition = True definition_buffer = io.BytesIO() - if isinstance(instruction, PauliEvolutionGate): + if type_str == b"p": _write_pauli_evolution_gate(definition_buffer, instruction) else: _write_circuit(definition_buffer, instruction.definition) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 9e29958d1fd4..8c0994d4e36c 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -536,7 +536,7 @@ def test_qaoa(self): def test_evolutiongate(self): """Test loading a circuit with evolution gate works.""" -# synthesis = LieTrotter(reps=2) + # synthesis = LieTrotter(reps=2) evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=0.2, synthesis=None) qc = QuantumCircuit(2) qc.append(evo, range(2)) @@ -545,17 +545,30 @@ def test_evolutiongate(self): qpy_file.seek(0) new_circ = load(qpy_file)[0] - # remove wrapping of instructions -# qc = qc.decompose().decompose() -# new_circ = new_circ.decompose().decompose() + self.assertEqual(qc, new_circ) + self.assertEqual([x[0].label for x in qc.data], [x[0].label for x in new_circ.data]) + + new_evo = new_circ.data[0][0] + # SparsePauliOp and EvolutionSynthesis + self.assertIsInstance(new_evo, PauliEvolutionGate) + + def test_op_list_evolutiongate(self): + """Test loading a circuit with evolution gate works.""" + # synthesis = LieTrotter(reps=2) + evo = PauliEvolutionGate([(Z ^ I) + (I ^ Z)] * 5, time=0.2, synthesis=None) + qc = QuantumCircuit(2) + qc.append(evo, range(2)) + qpy_file = io.BytesIO() + dump(qc, qpy_file) + qpy_file.seek(0) + new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) self.assertEqual([x[0].label for x in qc.data], [x[0].label for x in new_circ.data]) new_evo = new_circ.data[0][0] # SparsePauliOp and EvolutionSynthesis - self.assertIsInstance(new_evo,PauliEvolutionGate) -# self.assertIsInstance(new_evo.synthesis, LieTrotter) + self.assertIsInstance(new_evo, PauliEvolutionGate) def test_parameter_expression_global_phase(self): """Test a circuit with a parameter expression global_phase.""" From fbc00b73dc484c5f24d2721676d239f9f42825ad Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 8 Dec 2021 15:44:48 +0100 Subject: [PATCH 03/17] settings for synth --- .../evolution/evolution_synthesis.py | 15 ++++++++++ qiskit/synthesis/evolution/lie_trotter.py | 23 ++++++++++++++- qiskit/synthesis/evolution/product_formula.py | 29 ++++++++++++++++++- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/qiskit/synthesis/evolution/evolution_synthesis.py b/qiskit/synthesis/evolution/evolution_synthesis.py index b97be353c2ca..f904f457e49b 100644 --- a/qiskit/synthesis/evolution/evolution_synthesis.py +++ b/qiskit/synthesis/evolution/evolution_synthesis.py @@ -13,6 +13,7 @@ """Evolution synthesis.""" from abc import ABC, abstractmethod +from typing import Any, Dict class EvolutionSynthesis(ABC): @@ -29,3 +30,17 @@ def synthesize(self, evolution): QuantumCircuit: A circuit implementing the evolution. """ raise NotImplementedError + + @property + def settings(self) -> Dict[str, Any]: + """Return the settings in a dictionary, which can be used to reconstruct the object. + + Returns: + A dictionary containing the settings of this product formula. + + Raises: + NotImplementedError: The interface does not implement this method. + """ + raise NotImplementedError( + "The settings property is not implemented for the base interface." + ) diff --git a/qiskit/synthesis/evolution/lie_trotter.py b/qiskit/synthesis/evolution/lie_trotter.py index ddcd11b8f2bf..2e9242f3801d 100644 --- a/qiskit/synthesis/evolution/lie_trotter.py +++ b/qiskit/synthesis/evolution/lie_trotter.py @@ -12,7 +12,7 @@ """The Lie-Trotter product formula.""" -from typing import Callable, Optional, Union +from typing import Callable, Optional, Union, Dict, Any import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info.operators import SparsePauliOp, Pauli @@ -97,3 +97,24 @@ def synthesize(self, evolution): ) return evolution_circuit + + @property + def settings(self) -> Dict[str, Any]: + """Return the settings in a dictionary, which can be used to reconstruct the object. + + Returns: + A dictionary containing the settings of this product formula. + + Raises: + NotImplementedError: If a custom atomic evolution is set, which cannot be serialized. + """ + if self._atomic_evolution is not None: + raise NotImplementedError( + "Cannot serialize a product formula with a custom atomic evolution." + ) + + return { + "reps": self.reps, + "insert_barries": self.insert_barriers, + "cx_structure": self._cx_structure, + } diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index db5007f8fa2b..3080ec1f5538 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -12,7 +12,7 @@ """A product formula base for decomposing non-commuting operator exponentials.""" -from typing import Callable, Optional, Union +from typing import Callable, Optional, Union, Any, Dict from functools import partial import numpy as np from qiskit.circuit.parameterexpression import ParameterExpression @@ -55,11 +55,38 @@ def __init__( self.reps = reps self.insert_barriers = insert_barriers + # user-provided atomic evolution, stored for serialization + self._atomic_evolution = atomic_evolution + self._cx_structure = cx_structure + + # if atomic evolution is not provided, set a default if atomic_evolution is None: atomic_evolution = partial(_default_atomic_evolution, cx_structure=cx_structure) self.atomic_evolution = atomic_evolution + @property + def settings(self) -> Dict[str, Any]: + """Return the settings in a dictionary, which can be used to reconstruct the object. + + Returns: + A dictionary containing the settings of this product formula. + + Raises: + NotImplementedError: If a custom atomic evolution is set, which cannot be serialized. + """ + if self._atomic_evolution is not None: + raise NotImplementedError( + "Cannot serialize a product formula with a custom atomic evolution." + ) + + return { + "order": self.order, + "reps": self.reps, + "insert_barries": self.insert_barriers, + "cx_structure": self._cx_structure, + } + def evolve_pauli( pauli: Pauli, From 71b6ee449b3fed476d7e434c7545ec888669f541 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 10:59:14 -0500 Subject: [PATCH 04/17] Add support for custom synthesis classes --- qiskit/circuit/qpy_serialization.py | 38 +++++++++---------- qiskit/synthesis/evolution/lie_trotter.py | 2 +- qiskit/synthesis/evolution/product_formula.py | 2 +- qiskit/test/base.py | 1 + .../circuit/test_circuit_load_from_qpy.py | 26 ++++++++++--- 5 files changed, 42 insertions(+), 27 deletions(-) diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index fc713741e88f..213b6e1d55b1 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -473,6 +473,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom from qiskit.version import __version__ from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import SparsePauliOp +from qiskit.synthesis import evolution as evo_synth try: import symengine @@ -1039,31 +1040,30 @@ def _write_pauli_evolution_gate(file_obj, evolution_gate): element_buf.write(element_metadata) element_buf.write(data) pauli_data_buf.write(element_buf.getvalue()) - pauli_list_data = buf.getvalue() - pauli_list_size = len(pauli_list_data) time = evolution_gate.time if isinstance(time, float): time_type = b"f" time_data = struct.pack("!d", time) time_size = struct.calcsize("!d") - elif isinstance(param, Parameter): + elif isinstance(time, Parameter): time_type = b"p" - _write_parameter(container, time) - container.seek(0) - time_data = container.read() - time_size = len(data) - elif isinstance(param, ParameterExpression): + buf = io.BytesIO() + _write_parameter(buf, time) + time_data = buf.getvalue() + time_size = len(time_data) + elif isinstance(time, ParameterExpression): time_type = b"e" - _write_parameter_expression(container, time) - container.seek(0) - time_data = container.read() - time_size = len(data) + buf = io.BytesIO() + _write_parameter_expression(buf, time) + time_data = buf.getvalue() + time_size = len(time_data) else: - raise TypeError(f"Invalid coefficient type {coeff} for PauliSumOp") + raise TypeError(f"Invalid time type {time} for PauliEvolutionGate") - # TODO: Add synthesis class to format (will need a new pack format and settings for classes) - synth_size = 0 - synth_data = b"" + synth_class = str(type(evolution_gate.synthesis).__name__) + settings_dict = evolution_gate.synthesis.settings + synth_data = json.dumps({"class": synth_class, "settings": settings_dict}).encode("utf8") + synth_size = len(synth_data) pauli_evolution_raw = struct.pack( PAULI_EVOLUTION_DEF_PACK, num_operators, standalone, time_type, time_size, synth_size ) @@ -1102,9 +1102,9 @@ def _read_pauli_evolution_gate(file_obj): elif time_type == b"e": buf = io.BytesIO(time_data) time = _read_parameter_expression(buf) - # TODO: Read synthesis class - synthesis = None - return_gate = library.PauliEvolutionGate(pauli_op, time=time, synthesis=None) + synth_data = json.loads(file_obj.read(pauli_evolution_raw[4])) + synthesis = getattr(evo_synth, synth_data["class"])(**synth_data["settings"]) + return_gate = library.PauliEvolutionGate(pauli_op, time=time, synthesis=synthesis) return return_gate diff --git a/qiskit/synthesis/evolution/lie_trotter.py b/qiskit/synthesis/evolution/lie_trotter.py index 2e9242f3801d..c71932dcaa57 100644 --- a/qiskit/synthesis/evolution/lie_trotter.py +++ b/qiskit/synthesis/evolution/lie_trotter.py @@ -115,6 +115,6 @@ def settings(self) -> Dict[str, Any]: return { "reps": self.reps, - "insert_barries": self.insert_barriers, + "insert_barriers": self.insert_barriers, "cx_structure": self._cx_structure, } diff --git a/qiskit/synthesis/evolution/product_formula.py b/qiskit/synthesis/evolution/product_formula.py index 3080ec1f5538..eda3ff938ffc 100644 --- a/qiskit/synthesis/evolution/product_formula.py +++ b/qiskit/synthesis/evolution/product_formula.py @@ -83,7 +83,7 @@ def settings(self) -> Dict[str, Any]: return { "order": self.order, "reps": self.reps, - "insert_barries": self.insert_barriers, + "insert_barriers": self.insert_barriers, "cx_structure": self._cx_structure, } diff --git a/qiskit/test/base.py b/qiskit/test/base.py index 7cbd80d73431..e6814a89d1e7 100644 --- a/qiskit/test/base.py +++ b/qiskit/test/base.py @@ -62,6 +62,7 @@ class BaseTestCase(testtools.TestCase): assertRaises = unittest.TestCase.assertRaises assertEqual = unittest.TestCase.assertEqual + else: class BaseTestCase(unittest.TestCase): diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 8c0994d4e36c..bc2adc9ec9b6 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -26,7 +26,7 @@ from qiskit.circuit.library import XGate, QFT, QAOAAnsatz, PauliEvolutionGate from qiskit.circuit.instruction import Instruction from qiskit.circuit.parameter import Parameter -from qiskit.synthesis import LieTrotter +from qiskit.synthesis import LieTrotter, SuzukiTrotter from qiskit.extensions import UnitaryGate from qiskit.opflow import I, X, Y, Z from qiskit.test import QiskitTestCase @@ -536,8 +536,8 @@ def test_qaoa(self): def test_evolutiongate(self): """Test loading a circuit with evolution gate works.""" - # synthesis = LieTrotter(reps=2) - evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=0.2, synthesis=None) + synthesis = LieTrotter(reps=2) + evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=0.2, synthesis=synthesis) qc = QuantumCircuit(2) qc.append(evo, range(2)) qpy_file = io.BytesIO() @@ -549,12 +549,10 @@ def test_evolutiongate(self): self.assertEqual([x[0].label for x in qc.data], [x[0].label for x in new_circ.data]) new_evo = new_circ.data[0][0] - # SparsePauliOp and EvolutionSynthesis self.assertIsInstance(new_evo, PauliEvolutionGate) def test_op_list_evolutiongate(self): """Test loading a circuit with evolution gate works.""" - # synthesis = LieTrotter(reps=2) evo = PauliEvolutionGate([(Z ^ I) + (I ^ Z)] * 5, time=0.2, synthesis=None) qc = QuantumCircuit(2) qc.append(evo, range(2)) @@ -567,7 +565,23 @@ def test_op_list_evolutiongate(self): self.assertEqual([x[0].label for x in qc.data], [x[0].label for x in new_circ.data]) new_evo = new_circ.data[0][0] - # SparsePauliOp and EvolutionSynthesis + self.assertIsInstance(new_evo, PauliEvolutionGate) + + def test_op_evolution_gate_suzuki_trotter(self): + """Test qpy path with a suzuki trotter synthesis method on an evolution gate.""" + synthesis = SuzukiTrotter() + evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=0.2, synthesis=synthesis) + qc = QuantumCircuit(2) + qc.append(evo, range(2)) + qpy_file = io.BytesIO() + dump(qc, qpy_file) + qpy_file.seek(0) + new_circ = load(qpy_file)[0] + + self.assertEqual(qc, new_circ) + self.assertEqual([x[0].label for x in qc.data], [x[0].label for x in new_circ.data]) + + new_evo = new_circ.data[0][0] self.assertIsInstance(new_evo, PauliEvolutionGate) def test_parameter_expression_global_phase(self): From a326154608b78456fcb5f325aa705fa8c627eb1b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 11:17:35 -0500 Subject: [PATCH 05/17] Expand test coverage --- .../circuit/test_circuit_load_from_qpy.py | 36 +++++++++++++++++++ test/qpy_compat/test_qpy.py | 21 +++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index bc2adc9ec9b6..423a99d344ef 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -551,6 +551,42 @@ def test_evolutiongate(self): new_evo = new_circ.data[0][0] self.assertIsInstance(new_evo, PauliEvolutionGate) + def test_evolutiongate_param_time(self): + """Test loading a circuit with an evolution gate that has a parameter for time.""" + synthesis = LieTrotter(reps=2) + time = Parameter("t") + evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=time, synthesis=synthesis) + qc = QuantumCircuit(2) + qc.append(evo, range(2)) + qpy_file = io.BytesIO() + dump(qc, qpy_file) + qpy_file.seek(0) + new_circ = load(qpy_file)[0] + + self.assertEqual(qc, new_circ) + self.assertEqual([x[0].label for x in qc.data], [x[0].label for x in new_circ.data]) + + new_evo = new_circ.data[0][0] + self.assertIsInstance(new_evo, PauliEvolutionGate) + + def test_evolutiongate_param_expr_time(self): + """Test loading a circuit with an evolution gate that has a parameter for time.""" + synthesis = LieTrotter(reps=2) + time = Parameter("t") + evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=time * time, synthesis=synthesis) + qc = QuantumCircuit(2) + qc.append(evo, range(2)) + qpy_file = io.BytesIO() + dump(qc, qpy_file) + qpy_file.seek(0) + new_circ = load(qpy_file)[0] + + self.assertEqual(qc, new_circ) + self.assertEqual([x[0].label for x in qc.data], [x[0].label for x in new_circ.data]) + + new_evo = new_circ.data[0][0] + self.assertIsInstance(new_evo, PauliEvolutionGate) + def test_op_list_evolutiongate(self): """Test loading a circuit with evolution gate works.""" evo = PauliEvolutionGate([(Z ^ I) + (I ^ Z)] * 5, time=0.2, synthesis=None) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index fba61507f302..11b58c5d2f90 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -24,7 +24,7 @@ from qiskit.circuit.quantumregister import Qubit from qiskit.circuit.parameter import Parameter from qiskit.circuit.qpy_serialization import dump, load -from qiskit.opflow import X, Y, Z +from qiskit.opflow import X, Y, Z, I from qiskit.quantum_info.random import random_unitary from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, QFT @@ -230,7 +230,8 @@ def generate_param_phase(): return output_circuits -def generate_single_clbit_condition_teleportation(): +def generate_single_clbit_condition_teleportation(): # pylint: disable=invalid-name + """Generate single clbit condition teleportation circuit.""" qr = QuantumRegister(1) cr = ClassicalRegister(2, name="name") teleport_qc = QuantumCircuit(qr, cr, name="Reset Test") @@ -241,6 +242,19 @@ def generate_single_clbit_condition_teleportation(): return teleport_qc +def generate_evolution_gate(): + """Generate a circuit with a pauli evolution gate.""" + # Runtime import since this only exists in terra 0.19.0 + from qiskit.circuit.library import PauliEvolutionGate + from qiskit.synthesis import SuzukiTrotter + + synthesis = SuzukiTrotter() + evo = PauliEvolutionGate([(Z ^ I) + (I ^ Z)] * 5, time=2.0, synthesis=synthesis) + qc = QuantumCircuit(2) + qc.append(evo, range(2)) + return qc + + def generate_circuits(version_str=None): """Generate reference circuits.""" version_parts = None @@ -265,6 +279,9 @@ def generate_circuits(version_str=None): if version_parts >= (0, 19, 0): output_circuits["param_phase.qpy"] = generate_param_phase() + if version_parts >= (0, 19, 1): + output_circuits["pauli_evo.qpy"] = [generate_evolution_gate()] + return output_circuits From 10531f60c1032ea7d45459e048cc8f95f522f67d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 11:28:07 -0500 Subject: [PATCH 06/17] Add release notes --- ...pauli-evolution-gate-bf85592f0f8f0ba7.yaml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml diff --git a/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml b/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml new file mode 100644 index 000000000000..8cbe117c52c5 --- /dev/null +++ b/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml @@ -0,0 +1,20 @@ +--- +fixes: + - | + Fixed the :mod:`~qiskit.circuit.qpy_serialization` support for serializing + a :class:`~qiskit.circuit.library.PauliEvolutionGate` object. Previously, + the :class:`~qiskit.circuit.library.PauliEvolutionGate` was treated as + a custom gate for serialization and would be deserialized as a + :class:`~qiskit.circuit.Gate` object that had the same definition and + name as the original :class:`~qiskit.circuit.library.PauliEvolutionGate`. + However, this would lose the original state from the + :class:`~qiskit.circuit.library.PauliEvolutionGate`. This has been fixed + so that starting in this release a + :class:`~qiskit.circuit.library.PauliEvolutionGate` in the circuit will + be preserved 1:1 across QPY serialization now. + + To fix this the requires a new QPY format version, :ref:`version_3`, which + contains a representation of the + :class:`~qiskit.circuit.library.PauliEvolutionGate` class which is + described in the `~qiskit.circuit.qpy_serialization` documentation at + :ref:`pauli_evo_qpy`. From 79e38709d362afaf8f26b23f8f1aef13803443a1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 11:31:21 -0500 Subject: [PATCH 07/17] Update release note --- qiskit/circuit/qpy_serialization.py | 2 ++ .../notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index 213b6e1d55b1..ab6d4c6bac8f 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -115,6 +115,8 @@ set and that name prefix it will error. If it's of type ``'p'`` the data payload is defined as follows: +.. pauli_evo_qpy: + PAULI_EVOLUTION --------------- diff --git a/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml b/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml index 8cbe117c52c5..4a8f49db2f49 100644 --- a/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml +++ b/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml @@ -11,7 +11,11 @@ fixes: :class:`~qiskit.circuit.library.PauliEvolutionGate`. This has been fixed so that starting in this release a :class:`~qiskit.circuit.library.PauliEvolutionGate` in the circuit will - be preserved 1:1 across QPY serialization now. + be preserved 1:1 across QPY serialization now. The only limitation with + this is that it does not support custom + :class:`~qiskit.synthesis.EvolutionSynthesis` classes. Only the classes + available from :mod:`qiskit.synthesis` can be used with a + :class:`~qiskit.circuit.library.PauliEvolutionGate` for qpy serialization. To fix this the requires a new QPY format version, :ref:`version_3`, which contains a representation of the From 4cc86bd956109916e4159c126979fc9f8ef22305 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 8 Dec 2021 18:44:10 +0100 Subject: [PATCH 08/17] fix param binding in PauliEvo --- qiskit/circuit/library/pauli_evolution.py | 19 ++++++++++++++++++- .../evolutions/pauli_trotter_evolution.py | 2 +- .../circuit/test_circuit_load_from_qpy.py | 5 +---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/qiskit/circuit/library/pauli_evolution.py b/qiskit/circuit/library/pauli_evolution.py index 2711d81cbacf..eb903cb044f1 100644 --- a/qiskit/circuit/library/pauli_evolution.py +++ b/qiskit/circuit/library/pauli_evolution.py @@ -79,10 +79,27 @@ def __init__( num_qubits = operator[0].num_qubits if isinstance(operator, list) else operator.num_qubits super().__init__(name=name, num_qubits=num_qubits, params=[time], label=label) - self.time = time self.operator = operator self.synthesis = synthesis + @property + def time(self) -> Union[float, ParameterExpression]: + """Return the evolution time as stored in the gate parameters. + + Returns: + The evolution time. + """ + return self.params[0] + + @time.setter + def time(self, time: Union[float, ParameterExpression]) -> None: + """Set the evolution time. + + Args: + time: The evolution time. + """ + self.params = [time] + def _define(self): """Unroll, where the default synthesis is matrix based.""" self.definition = self.synthesis.synthesize(self) diff --git a/qiskit/opflow/evolutions/pauli_trotter_evolution.py b/qiskit/opflow/evolutions/pauli_trotter_evolution.py index 45f7b2d8f502..5521ba3f86a3 100644 --- a/qiskit/opflow/evolutions/pauli_trotter_evolution.py +++ b/qiskit/opflow/evolutions/pauli_trotter_evolution.py @@ -119,7 +119,7 @@ def _recursive_convert(self, operator: OperatorBase) -> OperatorBase: evo = PauliEvolutionGate( pauli, time=time, synthesis=self._get_evolution_synthesis() ) - return CircuitOp(evo.definition) + return CircuitOp(evo) # operator = EvolvedOp(operator.primitive.to_pauli_op(), coeff=operator.coeff) if not {"Pauli"} == operator.primitive_strings(): logger.warning( diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 423a99d344ef..631ea507d6a9 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -523,14 +523,11 @@ def test_single_bit_teleportation(self): def test_qaoa(self): """Test loading a QAOA circuit works.""" cost_operator = Z ^ I ^ I ^ Z - qaoa = QAOAAnsatz(cost_operator) + qaoa = QAOAAnsatz(cost_operator, reps=2) qpy_file = io.BytesIO() dump(qaoa, qpy_file) qpy_file.seek(0) new_circ = load(qpy_file)[0] - # decompose to check the circuits, not their labels - qaoa = qaoa.decompose().decompose() - new_circ = new_circ.decompose().decompose() self.assertEqual(qaoa, new_circ) self.assertEqual([x[0].label for x in qaoa.data], [x[0].label for x in new_circ.data]) From 5a286268628e1a5d80d963d6b71ea5abf80f449f Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 8 Dec 2021 18:50:36 +0100 Subject: [PATCH 09/17] allow time as an int --- qiskit/circuit/library/pauli_evolution.py | 11 ++++++++++- test/python/circuit/test_circuit_load_from_qpy.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/qiskit/circuit/library/pauli_evolution.py b/qiskit/circuit/library/pauli_evolution.py index eb903cb044f1..dad68239f090 100644 --- a/qiskit/circuit/library/pauli_evolution.py +++ b/qiskit/circuit/library/pauli_evolution.py @@ -51,7 +51,7 @@ class PauliEvolutionGate(Gate): def __init__( self, operator, - time: Union[float, ParameterExpression] = 1.0, + time: Union[int, float, ParameterExpression] = 1.0, label: Optional[str] = None, synthesis: Optional[EvolutionSynthesis] = None, ) -> None: @@ -107,6 +107,15 @@ def _define(self): def inverse(self) -> "PauliEvolutionGate": return PauliEvolutionGate(operator=self.operator, time=-self.time, synthesis=self.synthesis) + def validate_parameter( + self, parameter: Union[int, float, ParameterExpression] + ) -> Union[float, ParameterExpression]: + """Gate parameters should be int, float, or ParameterExpression""" + if isinstance(parameter, int): + parameter = float(parameter) + + return super().validate_parameter(parameter) + def _to_sparse_pauli_op(operator): """Cast the operator to a SparsePauliOp. diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 631ea507d6a9..9e98ee23564d 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -534,7 +534,7 @@ def test_qaoa(self): def test_evolutiongate(self): """Test loading a circuit with evolution gate works.""" synthesis = LieTrotter(reps=2) - evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=0.2, synthesis=synthesis) + evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=2, synthesis=synthesis) qc = QuantumCircuit(2) qc.append(evo, range(2)) qpy_file = io.BytesIO() From f59887ed900c0ce081966b203615704bef3642dc Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 13:00:18 -0500 Subject: [PATCH 10/17] Apply suggestions from code review Co-authored-by: Jake Lishman --- qiskit/circuit/qpy_serialization.py | 16 ++++++++-------- ...ix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index ab6d4c6bac8f..c4a3702b0548 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -115,7 +115,7 @@ set and that name prefix it will error. If it's of type ``'p'`` the data payload is defined as follows: -.. pauli_evo_qpy: +.. _pauli_evo_qpy: PAULI_EVOLUTION --------------- @@ -133,7 +133,7 @@ } This is immediately followed by ``operator_count`` elements defined by the :ref:`pauli_sum_op` -payload. Following that we have `time_size`` bytes representing the ``time`` attribute. If +payload. Following that we have ``time_size`` bytes representing the ``time`` attribute. If ``standalone_op`` is ``True`` then there must only be a single operator. The encoding of these bytes is determined by the value of ``time_type``. Possible values of ``time_type`` are ``'f'``, ``'p'``, and ``'e'``. If ``time_type`` is ``'f'`` it's a double, @@ -143,15 +143,15 @@ Following that is ``synthesis_size`` bytes which is a utf8 encoded json payload representing the :class:`.EvolutionSynthesis` class used by the gate. -.. pauli_sum_op +.. _pauli_sum_op: SPARSE_PAULI_OP_LIST_ELEM ------------------------- -This represents an instance of :class:`~qiskit.opflow.PauliSumOp`. +This represents an instance of :class:`.PauliSumOp`. -.. code-block: c +.. code-block:: c struct { uint32_t pauli_op_size; @@ -273,7 +273,7 @@ ``qr`` would have ``standalone`` set to ``False``. -.. custom_definition: +.. _custom_definition: CUSTOM_DEFINITIONS ------------------ @@ -381,7 +381,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom (see below), and ``'n'`` represents an object from numpy (either an ``ndarray`` or a numpy type) which means the data is .npy format [#f2]_ data. -.. param_struct: +.. _param_struct: PARAMETER --------- @@ -926,7 +926,7 @@ def _write_instruction(file_obj, instruction_tuple, custom_instructions, index_m ) or gate_class_name == "Gate" or gate_class_name == "Instruction" - or isinstance(instruction_tuple[0], (library.BlueprintCircuit)) + or isinstance(instruction_tuple[0], library.BlueprintCircuit) ): if instruction_tuple[0].name not in custom_instructions: custom_instructions[instruction_tuple[0].name] = instruction_tuple[0] diff --git a/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml b/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml index 4a8f49db2f49..9478dc144ddb 100644 --- a/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml +++ b/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml @@ -20,5 +20,5 @@ fixes: To fix this the requires a new QPY format version, :ref:`version_3`, which contains a representation of the :class:`~qiskit.circuit.library.PauliEvolutionGate` class which is - described in the `~qiskit.circuit.qpy_serialization` documentation at + described in the :mod:`~qiskit.circuit.qpy_serialization` documentation at :ref:`pauli_evo_qpy`. From 30349229be85521f88a0bd91a63bff03cd201242 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 13:03:33 -0500 Subject: [PATCH 11/17] Update qiskit/circuit/qpy_serialization.py Co-authored-by: Jake Lishman --- qiskit/circuit/qpy_serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index c4a3702b0548..b64063e7b149 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -589,7 +589,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom COMPLEX_SIZE = struct.calcsize(COMPLEX_PACK) # Pauli Evolution Gate PAULI_EVOLUTION_DEF = namedtuple( - "PAULI_EVOLUTION_DEF", ["operator_size", "time_type", "time_size", "synth_method_size"] + "PAULI_EVOLUTION_DEF", ["operator_size", "standalone_op", "time_type", "time_size", "synth_method_size"] ) PAULI_EVOLUTION_DEF_PACK = "!Q?1cQQ" PAULI_EVOLUTION_DEF_SIZE = struct.calcsize(PAULI_EVOLUTION_DEF_PACK) From 44d36df7272ccac2d3d1fba8f45be53da421d9b3 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 13:20:24 -0500 Subject: [PATCH 12/17] Update qiskit/circuit/qpy_serialization.py --- qiskit/circuit/qpy_serialization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index b64063e7b149..21a6d2d6768f 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -860,9 +860,9 @@ def _read_custom_instructions(file_obj, version): definition_circuit = None if has_custom_definition: definition_buffer = io.BytesIO(file_obj.read(size)) - if version < 3 or not name.startswith(r"###PauliEvolutionGate"): + if version < 3 or not name.startswith(r"###PauliEvolutionGate_"): definition_circuit = _read_circuit(definition_buffer, version) - elif name.startswith(r"###PauliEvolutionGate"): + elif name.startswith(r"###PauliEvolutionGate_"): definition_circuit = _read_pauli_evolution_gate(definition_buffer) custom_instructions[name] = (type_str, num_qubits, num_clbits, definition_circuit) return custom_instructions From babce2e65e881772d10f5912448fd919039704a7 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 13:27:51 -0500 Subject: [PATCH 13/17] Rerun with latest black --- qiskit/test/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/test/base.py b/qiskit/test/base.py index e6814a89d1e7..7cbd80d73431 100644 --- a/qiskit/test/base.py +++ b/qiskit/test/base.py @@ -62,7 +62,6 @@ class BaseTestCase(testtools.TestCase): assertRaises = unittest.TestCase.assertRaises assertEqual = unittest.TestCase.assertEqual - else: class BaseTestCase(unittest.TestCase): From 82d3576f13866eb006314ce338da7ef54b0053d6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 13:34:33 -0500 Subject: [PATCH 14/17] Close buffers when finished --- qiskit/circuit/qpy_serialization.py | 43 +++++++++++++++-------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index 21a6d2d6768f..8fc789dff064 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -1033,15 +1033,15 @@ def _write_pauli_evolution_gate(file_obj, evolution_gate): num_operators = len(operator_list) pauli_data_buf = io.BytesIO() for operator in operator_list: - element_buf = io.BytesIO() - buf = io.BytesIO() - pauli_list = operator.to_list(array=True) - np.save(buf, pauli_list) - data = buf.getvalue() - element_metadata = struct.pack(SPARSE_PAULI_OP_LIST_ELEM_PACK, len(data)) - element_buf.write(element_metadata) - element_buf.write(data) - pauli_data_buf.write(element_buf.getvalue()) + with io.BytesIO() as element_buf: + with io.BytesIO() as buf: + pauli_list = operator.to_list(array=True) + np.save(buf, pauli_list) + data = buf.getvalue() + element_metadata = struct.pack(SPARSE_PAULI_OP_LIST_ELEM_PACK, len(data)) + element_buf.write(element_metadata) + element_buf.write(data) + pauli_data_buf.write(element_buf.getvalue()) time = evolution_gate.time if isinstance(time, float): time_type = b"f" @@ -1049,16 +1049,16 @@ def _write_pauli_evolution_gate(file_obj, evolution_gate): time_size = struct.calcsize("!d") elif isinstance(time, Parameter): time_type = b"p" - buf = io.BytesIO() - _write_parameter(buf, time) - time_data = buf.getvalue() - time_size = len(time_data) + with io.BytesIO() as buf: + _write_parameter(buf, time) + time_data = buf.getvalue() + time_size = len(time_data) elif isinstance(time, ParameterExpression): time_type = b"e" - buf = io.BytesIO() - _write_parameter_expression(buf, time) - time_data = buf.getvalue() - time_size = len(time_data) + with io.BytesIO() as buf: + _write_parameter_expression(buf, time) + time_data = buf.getvalue() + time_size = len(time_data) else: raise TypeError(f"Invalid time type {time} for PauliEvolutionGate") @@ -1071,6 +1071,7 @@ def _write_pauli_evolution_gate(file_obj, evolution_gate): ) file_obj.write(pauli_evolution_raw) file_obj.write(pauli_data_buf.getvalue()) + pauli_data_buf.close() file_obj.write(time_data) file_obj.write(synth_data) @@ -1099,11 +1100,11 @@ def _read_pauli_evolution_gate(file_obj): if time_type == b"f": time = struct.unpack("!d", time_data)[0] elif time_type == b"p": - buf = io.BytesIO(time_data) - time = _read_parameter(buf) + with io.BytesIO(time_data) as buf: + time = _read_parameter(buf) elif time_type == b"e": - buf = io.BytesIO(time_data) - time = _read_parameter_expression(buf) + with io.BytesIO(time_data) as buf: + time = _read_parameter_expression(buf) synth_data = json.loads(file_obj.read(pauli_evolution_raw[4])) synthesis = getattr(evo_synth, synth_data["class"])(**synth_data["settings"]) return_gate = library.PauliEvolutionGate(pauli_op, time=time, synthesis=synthesis) From 7852fc9761b225e27c6a8a3213d8fdc692a7e298 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 13:38:33 -0500 Subject: [PATCH 15/17] Fix release note wording --- .../notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml b/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml index 9478dc144ddb..79b27f4a06c0 100644 --- a/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml +++ b/releasenotes/notes/fix-pauli-evolution-gate-bf85592f0f8f0ba7.yaml @@ -17,8 +17,8 @@ fixes: available from :mod:`qiskit.synthesis` can be used with a :class:`~qiskit.circuit.library.PauliEvolutionGate` for qpy serialization. - To fix this the requires a new QPY format version, :ref:`version_3`, which - contains a representation of the + To fix this issue a new QPY format version, :ref:`version_3`, was required. + This new format version includes a representation of the :class:`~qiskit.circuit.library.PauliEvolutionGate` class which is described in the :mod:`~qiskit.circuit.qpy_serialization` documentation at :ref:`pauli_evo_qpy`. From 05359d2a23160e83348e73d098a290f11be817de Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 14:08:55 -0500 Subject: [PATCH 16/17] Fix lint --- qiskit/circuit/qpy_serialization.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index 8fc789dff064..0e63e46a37ec 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -589,7 +589,8 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom COMPLEX_SIZE = struct.calcsize(COMPLEX_PACK) # Pauli Evolution Gate PAULI_EVOLUTION_DEF = namedtuple( - "PAULI_EVOLUTION_DEF", ["operator_size", "standalone_op", "time_type", "time_size", "synth_method_size"] + "PAULI_EVOLUTION_DEF", + ["operator_size", "standalone_op", "time_type", "time_size", "synth_method_size"], ) PAULI_EVOLUTION_DEF_PACK = "!Q?1cQQ" PAULI_EVOLUTION_DEF_SIZE = struct.calcsize(PAULI_EVOLUTION_DEF_PACK) @@ -1104,7 +1105,7 @@ def _read_pauli_evolution_gate(file_obj): time = _read_parameter(buf) elif time_type == b"e": with io.BytesIO(time_data) as buf: - time = _read_parameter_expression(buf) + time = _read_parameter_expression(buf) synth_data = json.loads(file_obj.read(pauli_evolution_raw[4])) synthesis = getattr(evo_synth, synth_data["class"])(**synth_data["settings"]) return_gate = library.PauliEvolutionGate(pauli_op, time=time, synthesis=synthesis) From 523150bc5bda17353a503ed24c2ca82440be5b5d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Dec 2021 15:39:02 -0500 Subject: [PATCH 17/17] Adjust tests around extra layer of gates --- test/python/circuit/library/test_evolved_op_ansatz.py | 2 +- test/python/circuit/library/test_qaoa_ansatz.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/python/circuit/library/test_evolved_op_ansatz.py b/test/python/circuit/library/test_evolved_op_ansatz.py index 4d14b9c03a02..93b489c7dc82 100644 --- a/test/python/circuit/library/test_evolved_op_ansatz.py +++ b/test/python/circuit/library/test_evolved_op_ansatz.py @@ -37,7 +37,7 @@ def test_evolved_op_ansatz(self): for string, time in zip(strings, parameters): reference.compose(evolve(string, time), inplace=True) - self.assertEqual(evo.decompose(), reference) + self.assertEqual(evo.decompose().decompose(), reference) def test_custom_evolution(self): """Test using another evolution than the default (e.g. matrix evolution).""" diff --git a/test/python/circuit/library/test_qaoa_ansatz.py b/test/python/circuit/library/test_qaoa_ansatz.py index 840c691f8ded..564f329e0db0 100644 --- a/test/python/circuit/library/test_qaoa_ansatz.py +++ b/test/python/circuit/library/test_qaoa_ansatz.py @@ -35,7 +35,7 @@ def test_default_qaoa(self): circuit = circuit.decompose() self.assertEqual(1, len(parameters)) self.assertIsInstance(circuit.data[0][0], HGate) - self.assertIsInstance(circuit.data[1][0], RXGate) + self.assertIsInstance(circuit.decompose().data[1][0], RXGate) def test_custom_initial_state(self): """Test circuit with a custom initial state.""" @@ -47,7 +47,7 @@ def test_custom_initial_state(self): circuit = circuit.decompose() self.assertEqual(1, len(parameters)) self.assertIsInstance(circuit.data[0][0], YGate) - self.assertIsInstance(circuit.data[1][0], RXGate) + self.assertIsInstance(circuit.decompose().data[1][0], RXGate) def test_invalid_reps(self): """Test negative reps."""