Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix qpy support for Annotated Operations #11505

Merged
merged 21 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
77 changes: 73 additions & 4 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 @@ -587,7 +626,7 @@ def _write_instruction(file_obj, instruction, custom_operations, index_map, use_
not hasattr(library, gate_class_name)
and not hasattr(circuit_mod, gate_class_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not export AnnotatedOperation from qiskit.circuit? I feel like it should be there as it is a public interface. If we are wouldn't this line pick it up as an attribute of the circuit module?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, that was an omission on my part. 77d044d exports this together with the modifiers. This also removes the need to check the name in the if above.

and not hasattr(controlflow, gate_class_name)
and gate_class_name != "Clifford"
and gate_class_name not in ["Clifford", "AnnotatedOperation"]
alexanderivrii marked this conversation as resolved.
Show resolved Hide resolved
)
or gate_class_name == "Gate"
or gate_class_name == "Instruction"
Expand All @@ -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)
Comment on lines +143 to +145
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this requires bumping the QPY version to 11 as this is a new struct to represent the modifiers. If someone tried to load a file using this with 0.45 which uses the same qpy version this would result in an error when the loader encounters the annotated operation (actually the new type key would trigger this too).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks.

Does this mean that I need to change this line

if version >= 5 and type_key == type_keys.CircuitInstruction.ANNOTATED_OPERATION:

to version >= 11 and ... or does this require more drastic changes? In addition, is this related to #11644?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to change that condition, also bump https://github.com/Qiskit/qiskit/blob/main/qiskit/qpy/common.py#L23 to 11, and also update the format documentation to describe what's new in version 11. I have another PR #11646 that actually is adding version 11 already, you can use this as a model. The two will merge conflict but we can handle that as there will only be version 11 in this release (there is only one format version for each qiskit release).

#11644 will have implications for this, as we'll need to error if someone uses an annotated operation and the version < 11 (this was something I didn't have to worry about in #11646)


# 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
Loading