Skip to content

Commit 13b23a3

Browse files
authored
Fix annotation-handling in Rust-space QPY (#15649)
* Fix annotation-handling in Rust-space QPY The Rust-space QPY serialiser was not actually serialising the annotations successfully; it always returned zero annotations due to an accidental overwrite of the `CircuitInstructionV2Pack::annotations` field, which was not updated when control flow operations moved to Rust. Unfortunately, the round-trip tests also did not consider annotations, because annotations, in general, do not need to have equality defined between them. This corrects the annotations implementation (it was mostly already correct), and fixes the equality checks in the tests. * Remove useless f-strings
1 parent c90f59d commit 13b23a3

6 files changed

Lines changed: 62 additions & 44 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/qpy/src/annotations.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ impl<'a> AnnotationHandler<'a> {
110110
}
111111
}
112112
}
113-
Ok((0, Bytes(Vec::new())))
113+
Err(QpyError::AnnotationError(format!(
114+
"No configured annotation serializer could handle {}",
115+
annotation.bind(py).repr()?,
116+
)))
114117
})
115118
}
116119

crates/qpy/src/circuit_writer.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ use crate::formats;
4747
use crate::params::pack_param_obj;
4848
use crate::py_methods::{
4949
PAULI_PRODUCT_MEASUREMENT_GATE_CLASS_NAME, PAULI_PRODUCT_ROTATION_GATE_CLASS_NAME,
50-
UNITARY_GATE_CLASS_NAME, gate_class_name, getattr_or_none, py_get_instruction_annotations,
51-
py_pack_param, py_pack_pauli_evolution_gate, recognize_custom_operation, serialize_metadata,
50+
UNITARY_GATE_CLASS_NAME, gate_class_name, getattr_or_none, py_pack_param,
51+
py_pack_pauli_evolution_gate, recognize_custom_operation, serialize_metadata,
5252
};
5353
use crate::value::{
5454
BitType, CircuitInstructionType, ExpressionVarDeclaration, GenericValue, ParamRegisterValue,
@@ -250,7 +250,6 @@ fn pack_instruction(
250250
instruction_pack.label = label.clone();
251251
}
252252
instruction_pack.bit_data = get_packed_bit_list(instruction, qpy_data.circuit_data);
253-
instruction_pack.annotations = py_get_instruction_annotations(instruction, qpy_data)?;
254253
if let Some(new_name) =
255254
recognize_custom_operation(&instruction.op, &gate_class_name(&instruction.op)?)?
256255
{
@@ -382,8 +381,6 @@ fn pack_control_flow_inst(
382381
) -> Result<formats::CircuitInstructionV2Pack, QpyError> {
383382
let mut packed_annotations = None;
384383
let mut packed_condition: formats::ConditionPack = Default::default();
385-
let mut extras_key = 0; // should contain a combination of condition key and annotations key, if present
386-
387384
let params = match control_flow_inst.control_flow.clone() {
388385
ControlFlow::Box {
389386
duration,
@@ -420,12 +417,10 @@ fn pack_control_flow_inst(
420417
}
421418
ControlFlow::IfElse { condition } => {
422419
packed_condition = pack_condition(condition, qpy_data)?;
423-
extras_key = packed_condition.key() as u8;
424420
pack_instruction_blocks(instruction, qpy_data)?
425421
}
426422
ControlFlow::While { condition } => {
427423
packed_condition = pack_condition(condition, qpy_data)?;
428-
extras_key = packed_condition.key() as u8;
429424
pack_instruction_blocks(instruction, qpy_data)?
430425
}
431426
ControlFlow::Switch {
@@ -467,10 +462,16 @@ fn pack_control_flow_inst(
467462
params
468463
}
469464
};
465+
let annotations_key = if packed_annotations.is_some() {
466+
formats::extras_key_parts::ANNOTATIONS
467+
} else {
468+
0
469+
};
470+
let condition_key = packed_condition.key() as u8;
470471
Ok(formats::CircuitInstructionV2Pack {
471472
num_qargs: control_flow_inst.num_qubits,
472473
num_cargs: control_flow_inst.num_clbits,
473-
extras_key,
474+
extras_key: condition_key | annotations_key,
474475
num_ctrl_qubits: 0, // standard instructions have no control qubits
475476
ctrl_state: 0,
476477
gate_class_name: control_flow_inst.name().to_string(), // this name is NOT a proper python class name, but we don't instantiate from the python class anymore

crates/qpy/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ pub enum QpyError {
132132
#[error("binary parsing error: {0}")]
133133
BinRwError(String),
134134

135+
/// Something went wrong in annotation handling.
136+
#[error("annotation-handling error: {0}")]
137+
AnnotationError(String),
138+
135139
/// Python error that occurred during a Python call
136140
#[error("Python error: {0}")]
137141
PythonError(#[from] PyErr),

crates/qpy/src/py_methods.rs

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ use numpy::Complex64;
1616
use pyo3::IntoPyObjectExt;
1717
use pyo3::intern;
1818
use pyo3::prelude::*;
19-
use pyo3::types::{
20-
PyAny, PyComplex, PyDict, PyFloat, PyInt, PyIterator, PyList, PyString, PyTuple,
21-
};
19+
use pyo3::types::{PyAny, PyComplex, PyDict, PyFloat, PyInt, PyList, PyString, PyTuple};
2220
use qiskit_circuit::classical::expr::Expr;
2321
use std::num::NonZero;
2422
use std::sync::Arc;
@@ -27,7 +25,7 @@ use qiskit_circuit::bit::{ClassicalRegister, ShareableClbit};
2725
use qiskit_circuit::classical;
2826
use qiskit_circuit::imports;
2927
use qiskit_circuit::operations::{Operation, OperationRef, PyRange};
30-
use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation};
28+
use qiskit_circuit::packed_instruction::PackedOperation;
3129
use qiskit_circuit::parameter::parameter_expression::{
3230
PyParameter, PyParameterExpression, PyParameterVectorElement,
3331
};
@@ -513,35 +511,6 @@ pub(crate) fn py_pack_param(
513511
Ok(formats::GenericDataPack { type_key, data })
514512
}
515513

516-
pub(crate) fn py_get_instruction_annotations(
517-
instruction: &PackedInstruction,
518-
qpy_data: &mut QPYWriteData,
519-
) -> Result<Option<formats::InstructionsAnnotationPack>, QpyError> {
520-
Python::attach(|py| {
521-
if let OperationRef::Instruction(inst) = instruction.op.view() {
522-
let op = inst.instruction.bind(py);
523-
if op.is_instance(imports::CONTROL_FLOW_BOX_OP.get_bound(py))? {
524-
let annotations_iter = PyIterator::from_object(&op.getattr("annotations")?)?;
525-
let annotations: Vec<formats::InstructionAnnotationPack> = annotations_iter
526-
.map(|annotation| {
527-
let (namespace_index, payload) = qpy_data
528-
.annotation_handler
529-
.serialize(&annotation?.unbind())?;
530-
Ok(formats::InstructionAnnotationPack {
531-
namespace_index,
532-
payload,
533-
})
534-
})
535-
.collect::<Result<_, QpyError>>()?;
536-
if !annotations.is_empty() {
537-
return Ok(Some(formats::InstructionsAnnotationPack { annotations }));
538-
}
539-
}
540-
}
541-
Ok(None)
542-
})
543-
}
544-
545514
pub(crate) fn py_pack_modifier(modifier: &Py<PyAny>) -> Result<formats::ModifierPack, QpyError> {
546515
Python::attach(|py| {
547516
let modifier = modifier.bind(py);

test/python/qpy/test_circuit_load_from_qpy.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from qiskit.circuit.random import random_circuit
2222
from qiskit.providers.fake_provider import GenericBackendV2
2323
from qiskit.exceptions import QiskitError
24-
from qiskit.qpy import dump, load, formats, get_qpy_version, QPY_COMPATIBILITY_VERSION
24+
from qiskit.qpy import dump, load, formats, get_qpy_version, QPY_COMPATIBILITY_VERSION, QpyError
2525
from qiskit.qpy.common import QPY_VERSION
2626
from qiskit.transpiler import TranspileLayout, CouplingMap
2727
from qiskit.compiler import transpile
@@ -60,6 +60,18 @@ def assert_roundtrip_equal(
6060
file_version,
6161
f"Generated QPY file version {file_version} does not match request version {version}",
6262
)
63+
if annotation_factories is not None:
64+
65+
def flat_annotations(qc):
66+
for inst in qc.data:
67+
op = inst.operation
68+
if inst.name == "box":
69+
yield op.annotations
70+
if inst.is_control_flow():
71+
for block in op.blocks:
72+
yield from flat_annotations(block)
73+
74+
self.assertEqual(list(flat_annotations(circuit)), list(flat_annotations(new_circuit)))
6375

6476

6577
class TestVersions(QpyCircuitTestCase):
@@ -398,6 +410,9 @@ def __eq__(self, other):
398410
and self.value == other.value
399411
)
400412

413+
def __repr__(self):
414+
return f"My({self.namespace!r}, {self.value!r})"
415+
401416
class Serializer(annotation.OpenQASM3Serializer):
402417
def load(self, namespace, payload):
403418
return My(namespace, payload)
@@ -420,6 +435,9 @@ class Dummy(annotation.Annotation):
420435
def __eq__(self, other):
421436
return isinstance(other, Dummy)
422437

438+
def __repr__(self):
439+
return "Dummy()"
440+
423441
class Serializer(annotation.QPYSerializer):
424442
def load_annotation(self, payload):
425443
outer_self.assertEqual(payload, b"SENTINEL")
@@ -446,6 +464,9 @@ def __init__(self, value):
446464
def __eq__(self, other):
447465
return isinstance(other, My) and self.value == other.value
448466

467+
def __repr__(self):
468+
return f"My({self.value!r})"
469+
449470
class Serializer(annotation.QPYSerializer):
450471
def __init__(self):
451472
self.loaded_state = False
@@ -494,12 +515,18 @@ class TypeA(annotation.Annotation):
494515
def __eq__(self, other):
495516
return isinstance(other, TypeA)
496517

518+
def __repr__(self):
519+
return "TypeA()"
520+
497521
class TypeB(annotation.Annotation):
498522
namespace = "b"
499523

500524
def __eq__(self, other):
501525
return isinstance(other, TypeB)
502526

527+
def __repr__(self):
528+
return "TypeB()"
529+
503530
class SerializerA(annotation.QPYSerializer):
504531
def dump_annotation(self, namespace, annotation):
505532
outer_self.assertEqual(namespace, "a")
@@ -558,6 +585,9 @@ def __eq__(self, other):
558585
and self.value == other.value
559586
)
560587

588+
def __repr__(self):
589+
return f"My({self.namespace!r}, {self.value!r})"
590+
561591
triggered_not_implemented = False
562592

563593
class Serializer(annotation.QPYSerializer):
@@ -608,6 +638,17 @@ def load_state(self, namespace, payload):
608638
)
609639
self.assertTrue(triggered_not_implemented)
610640

641+
def test_reject_unknown_namespace(self):
642+
class Unknown(annotation.Annotation):
643+
namespace = "unknown"
644+
645+
qc = QuantumCircuit()
646+
with qc.box([Unknown()]):
647+
pass
648+
649+
with io.BytesIO() as fptr, self.assertRaisesRegex(QpyError, "No configured annotation"):
650+
dump(qc, fptr)
651+
611652

612653
@ddt
613654
class TestOutputStreamProperties(QpyCircuitTestCase):

0 commit comments

Comments
 (0)