mirror of https://github.com/Qiskit/qiskit.git
Fix qpy support for Annotated Operations (#11505)
* clifford qpy support + test * release notes * cleanup: checking if instruction is of type Instruction * qpy support for annotated operations * tests and release notes * changing qpy format for floating-point numbers; updating tests * minor * fixes * documenting qpy version 11 changes related to annotated ops * bumping qpy version to 11 * changing to version 11 for annotated operations * Update qiskit/qpy/binary_io/circuits.py Co-authored-by: Matthew Treinish <mtreinish@kortar.org> * exporting AnnotatedOperation + modifiers from qiskit.circuit * minor * qpy_compat test * release notes fix? --------- Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
This commit is contained in:
parent
6141d74b34
commit
86a707e8a9
|
@ -397,3 +397,5 @@ from .controlflow import (
|
|||
BreakLoopOp,
|
||||
ContinueLoopOp,
|
||||
)
|
||||
|
||||
from .annotated_operation import AnnotatedOperation, InverseModifier, ControlModifier, PowerModifier
|
||||
|
|
|
@ -167,10 +167,41 @@ circuits in the data.
|
|||
Version 11
|
||||
==========
|
||||
|
||||
Version 11 is identical to Version 10 except that for names in the CUSTOM_INSTRUCTION blocks
|
||||
Version 11 is identical to Version 10 except for the following.
|
||||
First, the names in the CUSTOM_INSTRUCTION blocks
|
||||
have a suffix of the form ``"_{uuid_hex}"`` where ``uuid_hex`` is a uuid
|
||||
hexadecimal string such as returned by :attr:`.UUID.hex`. For example:
|
||||
``"b3ecab5b4d6a4eb6bc2b2dbf18d83e1e"``.
|
||||
Second, it adds support for :class:`.AnnotatedOperation`
|
||||
objects. The base operation of an annotated operation is stored using the INSTRUCTION block,
|
||||
and an additional ``type`` value ``'a'``is added to indicate that the custom instruction is an
|
||||
annotated operation. The list of modifiers are stored as instruction parameters using INSTRUCTION_PARAM,
|
||||
with an additional value ``'m'`` is added to indicate that the parameter is of type
|
||||
:class:`~qiskit.circuit.annotated_operation.Modifier`. Each modifier is stored using the
|
||||
MODIFIER struct.
|
||||
|
||||
.. _modifier_qpy:
|
||||
|
||||
MODIFIER
|
||||
--------
|
||||
|
||||
This represents :class:`~qiskit.circuit.annotated_operation.Modifier`
|
||||
|
||||
.. code-block:: c
|
||||
|
||||
struct {
|
||||
char type;
|
||||
uint32_t num_ctrl_qubits;
|
||||
uint32_t ctrl_state;
|
||||
double power;
|
||||
}
|
||||
|
||||
This is sufficient to store different types of modifiers required for serializing objects
|
||||
of type :class:`.AnnotatedOperation`.
|
||||
The field ``type`` is either ``'i'``, ``'c'`` or ``'p'``, representing whether the modifier
|
||||
is respectively an inverse modifier, a control modifier or a power modifier. In the second
|
||||
case, the fields ``num_ctrl_qubits`` and ``ctrl_state`` specify the control logic of the base
|
||||
operation, and in the third case the field ``power`` represents the power of the base operation.
|
||||
|
||||
.. _qpy_version_10:
|
||||
|
||||
|
|
|
@ -30,6 +30,13 @@ from qiskit.circuit.classicalregister import ClassicalRegister, Clbit
|
|||
from qiskit.circuit.gate import Gate
|
||||
from qiskit.circuit.singleton import SingletonInstruction, SingletonGate
|
||||
from qiskit.circuit.controlledgate import ControlledGate
|
||||
from qiskit.circuit.annotated_operation import (
|
||||
AnnotatedOperation,
|
||||
Modifier,
|
||||
InverseModifier,
|
||||
ControlModifier,
|
||||
PowerModifier,
|
||||
)
|
||||
from qiskit.circuit.instruction import Instruction
|
||||
from qiskit.circuit.quantumcircuit import QuantumCircuit
|
||||
from qiskit.circuit.quantumregister import QuantumRegister, Qubit
|
||||
|
@ -130,6 +137,8 @@ def _loads_instruction_parameter(
|
|||
):
|
||||
if type_key == type_keys.Program.CIRCUIT:
|
||||
param = common.data_from_binary(data_bytes, read_circuit, version=version)
|
||||
elif type_key == type_keys.Value.MODIFIER:
|
||||
param = common.data_from_binary(data_bytes, _read_modifier)
|
||||
elif type_key == type_keys.Container.RANGE:
|
||||
data = formats.RANGE._make(struct.unpack(formats.RANGE_PACK, data_bytes))
|
||||
param = range(data.start, data.stop, data.step)
|
||||
|
@ -408,6 +417,14 @@ def _parse_custom_operation(
|
|||
inst_obj.definition = definition
|
||||
return inst_obj
|
||||
|
||||
if version >= 11 and type_key == type_keys.CircuitInstruction.ANNOTATED_OPERATION:
|
||||
with io.BytesIO(base_gate_raw) as base_gate_obj:
|
||||
base_gate = _read_instruction(
|
||||
base_gate_obj, None, registers, custom_operations, version, vectors, use_symengine
|
||||
)
|
||||
inst_obj = AnnotatedOperation(base_op=base_gate, modifiers=params)
|
||||
return inst_obj
|
||||
|
||||
if type_key == type_keys.CircuitInstruction.PAULI_EVOL_GATE:
|
||||
return definition
|
||||
|
||||
|
@ -453,6 +470,25 @@ def _read_pauli_evolution_gate(file_obj, version, vectors):
|
|||
return return_gate
|
||||
|
||||
|
||||
def _read_modifier(file_obj):
|
||||
modifier = formats.MODIFIER_DEF._make(
|
||||
struct.unpack(
|
||||
formats.MODIFIER_DEF_PACK,
|
||||
file_obj.read(formats.MODIFIER_DEF_SIZE),
|
||||
)
|
||||
)
|
||||
if modifier.type == b"i":
|
||||
return InverseModifier()
|
||||
elif modifier.type == b"c":
|
||||
return ControlModifier(
|
||||
num_ctrl_qubits=modifier.num_ctrl_qubits, ctrl_state=modifier.ctrl_state
|
||||
)
|
||||
elif modifier.type == b"p":
|
||||
return PowerModifier(power=modifier.power)
|
||||
else:
|
||||
raise TypeError("Unsupported modifier.")
|
||||
|
||||
|
||||
def _read_custom_operations(file_obj, version, vectors):
|
||||
custom_operations = {}
|
||||
custom_definition_header = formats.CUSTOM_CIRCUIT_DEF_HEADER._make(
|
||||
|
@ -547,6 +583,9 @@ def _dumps_instruction_parameter(param, index_map, use_symengine):
|
|||
if isinstance(param, QuantumCircuit):
|
||||
type_key = type_keys.Program.CIRCUIT
|
||||
data_bytes = common.data_to_binary(param, write_circuit)
|
||||
elif isinstance(param, Modifier):
|
||||
type_key = type_keys.Value.MODIFIER
|
||||
data_bytes = common.data_to_binary(param, _write_modifier)
|
||||
elif isinstance(param, range):
|
||||
type_key = type_keys.Container.RANGE
|
||||
data_bytes = struct.pack(formats.RANGE_PACK, param.start, param.stop, param.step)
|
||||
|
@ -606,8 +645,8 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map, use_
|
|||
custom_operations[gate_class_name] = instruction.operation
|
||||
custom_operations_list.append(gate_class_name)
|
||||
|
||||
elif gate_class_name == "ControlledGate":
|
||||
# controlled gates can have the same name but different parameter
|
||||
elif gate_class_name in {"ControlledGate", "AnnotatedOperation"}:
|
||||
# controlled or annotated gates can have the same name but different parameter
|
||||
# values, the uuid is appended to avoid storing a single definition
|
||||
# in circuits with multiple controlled gates.
|
||||
gate_class_name = instruction.operation.name + "_" + str(uuid.uuid4())
|
||||
|
@ -646,8 +685,10 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map, use_
|
|||
]
|
||||
elif isinstance(instruction.operation, Clifford):
|
||||
instruction_params = [instruction.operation.tableau]
|
||||
elif isinstance(instruction.operation, AnnotatedOperation):
|
||||
instruction_params = instruction.operation.modifiers
|
||||
else:
|
||||
instruction_params = instruction.operation.params
|
||||
instruction_params = getattr(instruction.operation, "params", [])
|
||||
|
||||
num_ctrl_qubits = getattr(instruction.operation, "num_ctrl_qubits", 0)
|
||||
ctrl_state = getattr(instruction.operation, "ctrl_state", 0)
|
||||
|
@ -729,6 +770,31 @@ def _write_pauli_evolution_gate(file_obj, evolution_gate):
|
|||
file_obj.write(synth_data)
|
||||
|
||||
|
||||
def _write_modifier(file_obj, modifier):
|
||||
if isinstance(modifier, InverseModifier):
|
||||
type_key = b"i"
|
||||
num_ctrl_qubits = 0
|
||||
ctrl_state = 0
|
||||
power = 0.0
|
||||
elif isinstance(modifier, ControlModifier):
|
||||
type_key = b"c"
|
||||
num_ctrl_qubits = modifier.num_ctrl_qubits
|
||||
ctrl_state = modifier.ctrl_state
|
||||
power = 0.0
|
||||
elif isinstance(modifier, PowerModifier):
|
||||
type_key = b"p"
|
||||
num_ctrl_qubits = 0
|
||||
ctrl_state = 0
|
||||
power = modifier.power
|
||||
else:
|
||||
raise TypeError("Unsupported modifier.")
|
||||
|
||||
modifier_data = struct.pack(
|
||||
formats.MODIFIER_DEF_PACK, type_key, num_ctrl_qubits, ctrl_state, power
|
||||
)
|
||||
file_obj.write(modifier_data)
|
||||
|
||||
|
||||
def _write_custom_operation(file_obj, name, operation, custom_operations, use_symengine, version):
|
||||
type_key = type_keys.CircuitInstruction.assign(operation)
|
||||
has_definition = False
|
||||
|
@ -759,6 +825,9 @@ def _write_custom_operation(file_obj, name, operation, custom_operations, use_sy
|
|||
num_ctrl_qubits = operation.num_ctrl_qubits
|
||||
ctrl_state = operation.ctrl_state
|
||||
base_gate = operation.base_gate
|
||||
elif type_key == type_keys.CircuitInstruction.ANNOTATED_OPERATION:
|
||||
has_definition = False
|
||||
base_gate = operation.base_op
|
||||
elif operation.definition is not None:
|
||||
has_definition = True
|
||||
data = common.data_to_binary(operation.definition, write_circuit)
|
||||
|
|
|
@ -139,6 +139,11 @@ PAULI_EVOLUTION_DEF = namedtuple(
|
|||
PAULI_EVOLUTION_DEF_PACK = "!Q?1cQQ"
|
||||
PAULI_EVOLUTION_DEF_SIZE = struct.calcsize(PAULI_EVOLUTION_DEF_PACK)
|
||||
|
||||
# Modifier
|
||||
MODIFIER_DEF = namedtuple("MODIFIER_DEF", ["type", "num_ctrl_qubits", "ctrl_state", "power"])
|
||||
MODIFIER_DEF_PACK = "!1cIId"
|
||||
MODIFIER_DEF_SIZE = struct.calcsize(MODIFIER_DEF_PACK)
|
||||
|
||||
# CUSTOM_CIRCUIT_DEF_HEADER
|
||||
CUSTOM_CIRCUIT_DEF_HEADER = namedtuple("CUSTOM_CIRCUIT_DEF_HEADER", ["size"])
|
||||
CUSTOM_CIRCUIT_DEF_HEADER_PACK = "!Q"
|
||||
|
|
|
@ -30,6 +30,7 @@ from qiskit.circuit import (
|
|||
Clbit,
|
||||
ClassicalRegister,
|
||||
)
|
||||
from qiskit.circuit.annotated_operation import AnnotatedOperation, Modifier
|
||||
from qiskit.circuit.classical import expr, types
|
||||
from qiskit.circuit.library import PauliEvolutionGate
|
||||
from qiskit.circuit.parameter import Parameter
|
||||
|
@ -113,6 +114,7 @@ class Value(TypeKeyBase):
|
|||
STRING = b"s"
|
||||
NULL = b"z"
|
||||
EXPRESSION = b"x"
|
||||
MODIFIER = b"m"
|
||||
|
||||
@classmethod
|
||||
def assign(cls, obj):
|
||||
|
@ -140,6 +142,8 @@ class Value(TypeKeyBase):
|
|||
return cls.CASE_DEFAULT
|
||||
if isinstance(obj, expr.Expr):
|
||||
return cls.EXPRESSION
|
||||
if isinstance(obj, Modifier):
|
||||
return cls.MODIFIER
|
||||
|
||||
raise exceptions.QpyError(
|
||||
f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace."
|
||||
|
@ -191,6 +195,7 @@ class CircuitInstruction(TypeKeyBase):
|
|||
GATE = b"g"
|
||||
PAULI_EVOL_GATE = b"p"
|
||||
CONTROLLED_GATE = b"c"
|
||||
ANNOTATED_OPERATION = b"a"
|
||||
|
||||
@classmethod
|
||||
def assign(cls, obj):
|
||||
|
@ -198,6 +203,8 @@ class CircuitInstruction(TypeKeyBase):
|
|||
return cls.PAULI_EVOL_GATE
|
||||
if isinstance(obj, ControlledGate):
|
||||
return cls.CONTROLLED_GATE
|
||||
if isinstance(obj, AnnotatedOperation):
|
||||
return cls.ANNOTATED_OPERATION
|
||||
if isinstance(obj, Gate):
|
||||
return cls.GATE
|
||||
if isinstance(obj, Instruction):
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
fixes:
|
||||
- |
|
||||
QPY (using :func:`.qpy.dump` and :func:`.qpy.load`) will now correctly serialize
|
||||
and deserialize quantum circuits with annotated operations
|
||||
(:class:`.AnnotatedOperation`).
|
|
@ -30,6 +30,7 @@ from qiskit.circuit.random import random_circuit
|
|||
from qiskit.circuit.gate import Gate
|
||||
from qiskit.circuit.library import (
|
||||
XGate,
|
||||
CXGate,
|
||||
RYGate,
|
||||
QFT,
|
||||
QAOAAnsatz,
|
||||
|
@ -46,6 +47,12 @@ from qiskit.circuit.library import (
|
|||
UnitaryGate,
|
||||
DiagonalGate,
|
||||
)
|
||||
from qiskit.circuit.annotated_operation import (
|
||||
AnnotatedOperation,
|
||||
InverseModifier,
|
||||
ControlModifier,
|
||||
PowerModifier,
|
||||
)
|
||||
from qiskit.circuit.instruction import Instruction
|
||||
from qiskit.circuit.parameter import Parameter
|
||||
from qiskit.circuit.parametervector import ParameterVector
|
||||
|
@ -1725,6 +1732,40 @@ class TestLoadFromQPY(QiskitTestCase):
|
|||
new_circuit = load(fptr)[0]
|
||||
self.assertEqual(circuit, new_circuit)
|
||||
|
||||
def test_annotated_operations(self):
|
||||
"""Test that circuits with annotated operations can be saved and retrieved correctly."""
|
||||
op1 = AnnotatedOperation(
|
||||
CXGate(), [InverseModifier(), ControlModifier(1), PowerModifier(1.4), InverseModifier()]
|
||||
)
|
||||
op2 = AnnotatedOperation(XGate(), InverseModifier())
|
||||
|
||||
circuit = QuantumCircuit(6, 1)
|
||||
circuit.cx(0, 1)
|
||||
circuit.append(op1, [0, 1, 2])
|
||||
circuit.h(4)
|
||||
circuit.append(op2, [1])
|
||||
|
||||
with io.BytesIO() as fptr:
|
||||
dump(circuit, fptr)
|
||||
fptr.seek(0)
|
||||
new_circuit = load(fptr)[0]
|
||||
self.assertEqual(circuit, new_circuit)
|
||||
|
||||
def test_annotated_operations_iterative(self):
|
||||
"""Test that circuits with iterative annotated operations can be saved and
|
||||
retrieved correctly.
|
||||
"""
|
||||
op = AnnotatedOperation(AnnotatedOperation(XGate(), InverseModifier()), ControlModifier(1))
|
||||
circuit = QuantumCircuit(4)
|
||||
circuit.h(0)
|
||||
circuit.append(op, [0, 2])
|
||||
circuit.cx(2, 3)
|
||||
with io.BytesIO() as fptr:
|
||||
dump(circuit, fptr)
|
||||
fptr.seek(0)
|
||||
new_circuit = load(fptr)[0]
|
||||
self.assertEqual(circuit, new_circuit)
|
||||
|
||||
|
||||
class TestSymengineLoadFromQPY(QiskitTestCase):
|
||||
"""Test use of symengine in qpy set of methods."""
|
||||
|
|
|
@ -652,6 +652,23 @@ def generate_clifford_circuits():
|
|||
return [qc]
|
||||
|
||||
|
||||
def generate_annotated_circuits():
|
||||
"""Test qpy circuits with annotated operations."""
|
||||
from qiskit.circuit import AnnotatedOperation, ControlModifier, InverseModifier, PowerModifier
|
||||
from qiskit.circuit.library import XGate, CXGate
|
||||
|
||||
op1 = AnnotatedOperation(
|
||||
CXGate(), [InverseModifier(), ControlModifier(1), PowerModifier(1.4), InverseModifier()]
|
||||
)
|
||||
op2 = AnnotatedOperation(XGate(), InverseModifier())
|
||||
qc = QuantumCircuit(6, 1)
|
||||
qc.cx(0, 1)
|
||||
qc.append(op1, [0, 1, 2])
|
||||
qc.h(4)
|
||||
qc.append(op2, [1])
|
||||
return [qc]
|
||||
|
||||
|
||||
def generate_control_flow_expr():
|
||||
"""`IfElseOp`, `WhileLoopOp` and `SwitchCaseOp` with `Expr` nodes in their discriminators."""
|
||||
from qiskit.circuit.classical import expr, types
|
||||
|
@ -783,6 +800,8 @@ def generate_circuits(version_parts):
|
|||
output_circuits["control_flow_expr.qpy"] = generate_control_flow_expr()
|
||||
if version_parts >= (0, 45, 2):
|
||||
output_circuits["clifford.qpy"] = generate_clifford_circuits()
|
||||
if version_parts >= (1, 0, 0):
|
||||
output_circuits["annotated.qpy"] = generate_annotated_circuits()
|
||||
return output_circuits
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue