Skip to content

Commit

Permalink
Fix qpy support for Annotated Operations (#11505)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
alexanderivrii and mtreinish committed Feb 1, 2024
1 parent 6141d74 commit 86a707e
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 4 deletions.
2 changes: 2 additions & 0 deletions qiskit/circuit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,3 +397,5 @@
BreakLoopOp,
ContinueLoopOp,
)

from .annotated_operation import AnnotatedOperation, InverseModifier, ControlModifier, PowerModifier
33 changes: 32 additions & 1 deletion qiskit/qpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,41 @@
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:
Expand Down
75 changes: 72 additions & 3 deletions qiskit/qpy/binary_io/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -729,6 +770,31 @@ def _write_elem(buffer, op):
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
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions qiskit/qpy/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@
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"
Expand Down
7 changes: 7 additions & 0 deletions qiskit/qpy/type_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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
Expand Down Expand Up @@ -113,6 +114,7 @@ class Value(TypeKeyBase):
STRING = b"s"
NULL = b"z"
EXPRESSION = b"x"
MODIFIER = b"m"

@classmethod
def assign(cls, obj):
Expand Down Expand Up @@ -140,6 +142,8 @@ def assign(cls, obj):
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."
Expand Down Expand Up @@ -191,13 +195,16 @@ class CircuitInstruction(TypeKeyBase):
GATE = b"g"
PAULI_EVOL_GATE = b"p"
CONTROLLED_GATE = b"c"
ANNOTATED_OPERATION = b"a"

@classmethod
def assign(cls, obj):
if isinstance(obj, PauliEvolutionGate):
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):
Expand Down
6 changes: 6 additions & 0 deletions releasenotes/notes/fix-annotated-qpy-6503362c79f29838.yaml
Original file line number Diff line number Diff line change
@@ -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`).
41 changes: 41 additions & 0 deletions test/python/circuit/test_circuit_load_from_qpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from qiskit.circuit.gate import Gate
from qiskit.circuit.library import (
XGate,
CXGate,
RYGate,
QFT,
QAOAAnsatz,
Expand All @@ -46,6 +47,12 @@
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
Expand Down Expand Up @@ -1725,6 +1732,40 @@ def test_clifford(self):
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."""
Expand Down
19 changes: 19 additions & 0 deletions test/qpy_compat/test_qpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down

0 comments on commit 86a707e

Please sign in to comment.