Skip to content

Commit f047aaf

Browse files
Fix QPY loading delay integers durations incorrectly (#16076)
* Fix QPY loading delay integers durations incorrectly This commit fixes an issue with parsing QPY payloads which have circuits that contain delays with duration of units dt. Durations of dt are integers and that is preserved in the QPY data. However our rust data model doesn't have support for an integer in a Rust PackedInstruction's parameters (this is an inconsistency which we should arguably fix but that is separate from this bugfix). To workaround this the QPY reader in rust was sticking the integer into a ParameterExpression as a constant without any symbols. This resulted in storing the integer in Rust but treating the object as a ParameterExpression and not an int, which in Qiskit's rust data model is mapped to a Param::Obj (indicating a Python object parameter). This mismatch in types was not really noticeable to Python because the ParameterExpression with the constant integer was coerced to an integer when it's passed to Python. However, this would break underlying assumptions for Rust code that is interacting with the delay. For example, the experimental rust qasm3 exporter would encounter the ParameterExpression on the delay and error because it can't handle parameter expressions yet. However fundamentally it could because this is just an integer. The reproducer for this failure is: ```python import io from qiskit import qpy, qasm3, QuantumCircuit qc = QuantumCircuit(1) qc.delay(1, 0) qasm3.dumps_experimental(qc) with io.BytesIO() as fptr: qpy.dump(qc, fptr) fptr.seek(0) qc2 = qpy.load(fptr)[0] qasm3.dumps_experimental(qc2) ``` The OQ3 experimental exporter is wrong not to handle `ParameterExpression::try_as_value` returning an `int`, but it's _more_ wrong that QPY is producing a `ParameterExpression` on deserialisation in the first place. To fix this issue this commit removes the conversion of the `int` in the qpy payload into a ParameterExpression and just retains an integer until we write out the Delay's PackedInstruction where we convert that rust int into a python int for the parameter. Doing this unraveled a deeper issue in how endianess is handled in QPY. In general everything in QPY is supposed to be encoded using network byte order (i.e. big endian). However, in the case of instructions' parameters there was a mistake made in QPY where the integer and float values for an instruction's parameters were encoded in little endian. All other uses of floats or ints are correctly big endian. When the raw int was returned to Python it was incorrectly assuming all integers were a big endian bytes value. To fix this an endian arg is added to the function which is converting the bytes arrays into a `GenericValue` enum for floats and ints. Then the callers of this function in circuit_reader are updated to explicitly assert what endianess the data in the circuit payload is if there are any floats or ints. This is `Endian::Little` for any instruction params that are in the parameters list explicitly and `Endian::Big` everywhere else. At the same time the handling of flipping the endianess of value in several places was fixed because this was no longer necessary as the data was read now using the correct byte order. This fundamentally stems from all the base values being stored in a single binrw generic value pack which is trying to encode all the primitive types in a single place. Ideally we should be handling the primitive types explicitly for each data pack field. But this was not changed to keep the diff minimal for backport Co-authored-by: Jake Lishman <jake.lishman@ibm.com> * Fix typo in qpy compat test * Fix tests again * Fix delay stretch expr test typo * Add more round-trip tests * Remove dead control-flow switching logic All the entries in this list either take zero parameters, or would have caused program control flow to enter `unpack_control_flow` and _not_ `unpack_py_instruction`, so this switching logic is dead. * Remove endianness switching from `BigUint` parsing The `BigUint` keys are only valid in contexts where they are guaranteed to be in network order. The `pack_biguint` function knew this, but `unpack_biguint` mistakenly gained an `endian` argument, which was required to always be `Big`. * Reword release note --------- Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
1 parent 56beeb8 commit f047aaf

8 files changed

Lines changed: 175 additions & 64 deletions

File tree

crates/qpy/src/circuit_reader.rs

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
// Ideally, serialization is done by packing in a binrw-enhanced struct and using the
2020
// `write` method into a `Cursor` buffer, but there might be exceptions.
2121

22-
use binrw::Endian::Little;
22+
use binrw::Endian;
2323
use hashbrown::HashMap;
2424
use num_bigint::BigUint;
2525
use num_complex::Complex64;
@@ -48,7 +48,6 @@ use qiskit_circuit::operations::{
4848
};
4949
use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation};
5050
use qiskit_circuit::parameter::parameter_expression::ParameterExpression;
51-
use qiskit_circuit::parameter::symbol_expr;
5251
use qiskit_circuit::var_stretch_container::{StretchType, VarType};
5352
use qiskit_circuit::{Block, classical, imports};
5453
use qiskit_circuit::{Clbit, Qubit};
@@ -153,7 +152,7 @@ fn unpack_condition(
153152
match &condition_pack.data {
154153
ConditionData::None => Ok(None),
155154
ConditionData::Expression(exp_pack) => {
156-
let exp_value = unpack_generic_value(exp_pack, qpy_data)?;
155+
let exp_value = unpack_generic_value(exp_pack, qpy_data, Endian::Big)?;
157156
match exp_value {
158157
GenericValue::Expression(exp) => Ok(Some(Condition::Expr(exp.clone()))),
159158
_ => Err(QpyError::InvalidExpression(
@@ -243,13 +242,16 @@ fn get_instruction_bits(
243242
fn get_instruction_values(
244243
instruction: &formats::CircuitInstructionV2Pack,
245244
qpy_data: &mut QPYReadData,
245+
endian: Endian,
246246
) -> Result<Vec<GenericValue>, QpyError> {
247247
// note that numbers are not read correctly - they are read in big endian, but for instruction parameters, due to historical reasons,
248248
// they are stored in little endian
249249
let inst_params: Vec<GenericValue> = instruction
250250
.params
251251
.iter()
252-
.map(|packed_param: &formats::GenericDataPack| unpack_generic_value(packed_param, qpy_data))
252+
.map(|packed_param: &formats::GenericDataPack| {
253+
unpack_generic_value(packed_param, qpy_data, endian)
254+
})
253255
.collect::<Result<_, QpyError>>()?;
254256
Ok(inst_params)
255257
}
@@ -287,18 +289,10 @@ pub fn instruction_values_to_params(
287289
} else {
288290
// params
289291
let inst_params: Vec<Param> = values
290-
.iter()
292+
.into_iter()
291293
.map(|value| -> Result<_, QpyError> {
292-
match value.as_le() {
293-
// TODO: is the "as_le" here enough to solve the "params are little endian" problem?
294+
match value {
294295
GenericValue::Float64(float) => Ok(Param::Float(float)),
295-
GenericValue::Int64(int) => {
296-
let value_expression =
297-
symbol_expr::SymbolExpr::Value(symbol_expr::Value::Int(int));
298-
Ok(Param::ParameterExpression(Arc::new(
299-
ParameterExpression::from_symbol_expr(value_expression),
300-
)))
301-
}
302296
GenericValue::ParameterExpression(exp) => Ok(Param::ParameterExpression(exp)),
303297
GenericValue::ParameterExpressionSymbol(symbol) => {
304298
Ok(Param::ParameterExpression(Arc::new(
@@ -310,7 +304,7 @@ pub fn instruction_values_to_params(
310304
ParameterExpression::from_symbol(symbol),
311305
)))
312306
}
313-
_ => Ok(Param::Obj(py_convert_from_generic_value(value)?)),
307+
_ => Ok(Param::Obj(py_convert_from_generic_value(&value)?)),
314308
}
315309
})
316310
.collect::<Result<_, QpyError>>()?;
@@ -403,7 +397,7 @@ fn unpack_standard_gate(
403397
instruction.gate_class_name
404398
)));
405399
};
406-
let param_values = get_instruction_values(instruction, qpy_data)?;
400+
let param_values = get_instruction_values(instruction, qpy_data, Endian::Little)?;
407401
Ok((op, param_values))
408402
}
409403

@@ -420,7 +414,7 @@ fn unpack_standard_instruction(
420414
instruction.gate_class_name
421415
)));
422416
};
423-
let param_values = get_instruction_values(instruction, qpy_data)?;
417+
let param_values = get_instruction_values(instruction, qpy_data, Endian::Little)?;
424418
Ok((op, param_values))
425419
}
426420

@@ -433,21 +427,21 @@ fn unpack_pauli_product_measurement(
433427
"Pauli Product Measurement should have exactly 3 parameters".to_string(),
434428
));
435429
}
436-
let z = unpack_generic_value(&instruction.params[0], qpy_data)?
430+
let z = unpack_generic_value(&instruction.params[0], qpy_data, Endian::Little)?
437431
.as_typed::<Vec<bool>>()
438432
.ok_or_else(|| {
439433
QpyError::InvalidParameter(
440434
"Pauli product measurement z parameter should be a boolean vector".to_string(),
441435
)
442436
})?;
443-
let x = unpack_generic_value(&instruction.params[1], qpy_data)?
437+
let x = unpack_generic_value(&instruction.params[1], qpy_data, Endian::Little)?
444438
.as_typed::<Vec<bool>>()
445439
.ok_or_else(|| {
446440
QpyError::InvalidParameter(
447441
"Pauli product measurement x parameter should be a boolean vector".to_string(),
448442
)
449443
})?;
450-
let neg = unpack_generic_value(&instruction.params[2], qpy_data)?
444+
let neg = unpack_generic_value(&instruction.params[2], qpy_data, Endian::Little)?
451445
.as_typed::<bool>()
452446
.ok_or_else(|| {
453447
QpyError::InvalidParameter(
@@ -470,22 +464,22 @@ fn unpack_pauli_product_rotation(
470464
"No matrix for unitary op".to_string(),
471465
));
472466
}
473-
let z = unpack_generic_value(&instruction.params[0], qpy_data)?
467+
let z = unpack_generic_value(&instruction.params[0], qpy_data, Endian::Little)?
474468
.as_typed::<Vec<bool>>()
475469
.ok_or_else(|| {
476470
QpyError::InvalidParameter(
477471
"Pauli product measurement z parameter should be a boolean vector".to_string(),
478472
)
479473
})?;
480-
let x = unpack_generic_value(&instruction.params[1], qpy_data)?
474+
let x = unpack_generic_value(&instruction.params[1], qpy_data, Endian::Little)?
481475
.as_typed::<Vec<bool>>()
482476
.ok_or_else(|| {
483477
QpyError::InvalidParameter(
484478
"Pauli product measurement x parameter should be a boolean vector".to_string(),
485479
)
486480
})?;
487-
let angle_value = unpack_generic_value(&instruction.params[2], qpy_data)?;
488-
let angle = generic_value_to_param(&angle_value, Little)?;
481+
let angle_value = unpack_generic_value(&instruction.params[2], qpy_data, Endian::Little)?;
482+
let angle = generic_value_to_param(&angle_value)?;
489483
let rotation = PauliProductRotation { z, x, angle };
490484
let pbc = Box::new(PauliBased::PauliProductRotation(rotation));
491485
let op = PackedOperation::from_pauli_based(pbc);
@@ -498,7 +492,7 @@ fn unpack_unitary(
498492
qpy_data: &mut QPYReadData,
499493
) -> Result<(PackedOperation, Vec<GenericValue>), QpyError> {
500494
let GenericValue::NumpyObject(py_matrix) =
501-
unpack_generic_value(&instruction.params[0], qpy_data)?
495+
unpack_generic_value(&instruction.params[0], qpy_data, Endian::Little)?
502496
else {
503497
return Err(QpyError::InvalidParameter(
504498
"No matrix for unitary op".to_string(),
@@ -543,7 +537,7 @@ fn unpack_control_flow(
543537
.params
544538
.iter()
545539
.skip(1)
546-
.map(|param| unpack_generic_value(param, qpy_data))
540+
.map(|param| unpack_generic_value(param, qpy_data, Endian::Little))
547541
.collect::<Result<_, QpyError>>()?;
548542
let duration_value = if let Some(duration_pack) = instruction.params.first() {
549543
unpack_duration_value(duration_pack, qpy_data)?
@@ -566,7 +560,8 @@ fn unpack_control_flow(
566560
ControlFlowType::BreakLoop => ControlFlow::BreakLoop,
567561
ControlFlowType::ContinueLoop => ControlFlow::ContinueLoop,
568562
ControlFlowType::ForLoop => {
569-
let mut instruction_values = get_instruction_values(instruction, qpy_data)?;
563+
let mut instruction_values =
564+
get_instruction_values(instruction, qpy_data, Endian::Big)?;
570565
param_values = instruction_values.split_off(2);
571566
let mut iter = instruction_values.into_iter();
572567
let (mut collection_value_pack, loop_param_value_pack) =
@@ -590,17 +585,18 @@ fn unpack_control_flow(
590585
ControlFlowType::IfElse => {
591586
let condition = unpack_condition(&instruction.condition, qpy_data)?
592587
.ok_or_else(|| QpyError::MissingData("if else condition is missing".to_string()))?;
593-
param_values = get_instruction_values(instruction, qpy_data)?;
588+
param_values = get_instruction_values(instruction, qpy_data, Endian::Big)?;
594589
ControlFlow::IfElse { condition }
595590
}
596591
ControlFlowType::WhileLoop => {
597592
let condition = unpack_condition(&instruction.condition, qpy_data)?
598593
.ok_or_else(|| QpyError::MissingData("if else condition is missing".to_string()))?;
599-
param_values = get_instruction_values(instruction, qpy_data)?;
594+
param_values = get_instruction_values(instruction, qpy_data, Endian::Big)?;
600595
ControlFlow::While { condition }
601596
}
602597
ControlFlowType::SwitchCase => {
603-
let mut instruction_values = get_instruction_values(instruction, qpy_data)?;
598+
let mut instruction_values =
599+
get_instruction_values(instruction, qpy_data, Endian::Big)?;
604600
let (target_value, case_label_list) = if instruction_values.len() < 3 {
605601
// we follow the python way of storing switch params
606602
// the first param is the target, the next param is the cases specifier
@@ -708,12 +704,12 @@ fn unpack_py_instruction(
708704
qpy_data: &mut QPYReadData,
709705
) -> Result<(PackedOperation, Vec<GenericValue>), QpyError> {
710706
let name = instruction.gate_class_name.clone();
711-
let mut instruction_values = get_instruction_values(instruction, qpy_data)?;
707+
let mut instruction_values = get_instruction_values(instruction, qpy_data, Endian::Little)?;
712708
Python::attach(|py| -> Result<_, QpyError> {
713709
let mut py_params: Vec<Bound<PyAny>> = instruction_values
714710
.iter()
715711
.map(|value| -> Result<_, QpyError> {
716-
generic_value_to_param(value, binrw::Endian::Little)?
712+
generic_value_to_param(value)?
717713
.into_pyobject(py)
718714
.map_err(QpyError::from)
719715
})
@@ -872,12 +868,12 @@ fn unpack_custom_instruction(
872868
let custom_instruction = custom_instructions_map.get(&name).ok_or_else(|| {
873869
QpyError::MissingData("Custom instruction data not found for {name}".to_string())
874870
})?;
875-
let instruction_values = get_instruction_values(instruction, qpy_data)?;
871+
let instruction_values = get_instruction_values(instruction, qpy_data, Endian::Little)?;
876872
Python::attach(|py| -> Result<_, QpyError> {
877873
let py_params: Vec<Bound<PyAny>> = instruction_values
878874
.iter()
879875
.map(|value| -> Result<_, QpyError> {
880-
generic_value_to_param(value, binrw::Endian::Little)?
876+
generic_value_to_param(value)?
881877
.into_pyobject(py)
882878
.map_err(QpyError::from)
883879
})
@@ -1171,8 +1167,12 @@ fn deserialize_pauli_evolution_gate(
11711167
sparse_pauli_op_pack,
11721168
)) => {
11731169
// formats::PauliDataPack::SparsePauliOp(sparse_pauli_op_pack) => {
1174-
let data =
1175-
load_value(ValueType::NumpyObject, &sparse_pauli_op_pack.data, qpy_data)?;
1170+
let data = load_value(
1171+
ValueType::NumpyObject,
1172+
&sparse_pauli_op_pack.data,
1173+
qpy_data,
1174+
Endian::Big,
1175+
)?;
11761176
if let GenericValue::NumpyObject(op_raw_data) = data {
11771177
Ok(imports::SPARSE_PAULI_OP
11781178
.get_bound(py)
@@ -1193,7 +1193,12 @@ fn deserialize_pauli_evolution_gate(
11931193
};
11941194
// time is of type ParameterValueType = Union[ParameterExpression, float]
11951195
// we don't have a rust PauliEvolutionGate so we'll convert the time to python
1196-
let time = load_value(packed_data.time_type, &packed_data.time_data, qpy_data)?;
1196+
let time = load_value(
1197+
packed_data.time_type,
1198+
&packed_data.time_data,
1199+
qpy_data,
1200+
Endian::Big,
1201+
)?;
11971202
let py_time: Py<PyAny> = match time {
11981203
GenericValue::Float64(value) => value.into_py_any(py)?,
11991204
GenericValue::ParameterExpression(exp) => exp.as_ref().clone().into_py_any(py)?,
@@ -1499,14 +1504,12 @@ pub(crate) fn unpack_circuit(
14991504
.annotation_handler
15001505
.load_deserializers(annotation_deserializers_data)?;
15011506
}
1502-
let global_phase = generic_value_to_param(
1503-
&load_value(
1504-
packed_circuit.header.global_phase_type,
1505-
&packed_circuit.header.global_phase_data,
1506-
&mut qpy_data,
1507-
)?,
1508-
binrw::Endian::Big,
1509-
)?;
1507+
let global_phase = generic_value_to_param(&load_value(
1508+
packed_circuit.header.global_phase_type,
1509+
&packed_circuit.header.global_phase_data,
1510+
&mut qpy_data,
1511+
Endian::Big,
1512+
)?)?;
15101513
qpy_data.circuit_data.set_global_phase_param(global_phase)?;
15111514
add_standalone_vars(packed_circuit, &mut qpy_data)?;
15121515
add_registers_and_bits(packed_circuit, &mut qpy_data)?;

crates/qpy/src/params.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ pub(crate) fn unpack_parameter_expression(
451451
item.item_type,
452452
&item.item_bytes,
453453
qpy_data,
454+
Endian::Big,
454455
)?)?;
455456
Ok((sym, replacement))
456457
})
@@ -628,14 +629,7 @@ pub(crate) fn pack_param_obj(
628629
})
629630
}
630631

631-
pub(crate) fn generic_value_to_param(
632-
value: &GenericValue,
633-
endian: Endian,
634-
) -> Result<Param, QpyError> {
635-
let value = match endian {
636-
Endian::Big => value,
637-
Endian::Little => &value.as_le(),
638-
};
632+
pub(crate) fn generic_value_to_param(value: &GenericValue) -> Result<Param, QpyError> {
639633
match value {
640634
GenericValue::Float64(float_val) => Ok(Param::Float(*float_val)),
641635
GenericValue::ParameterExpressionSymbol(symbol) => {

crates/qpy/src/value.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -405,10 +405,25 @@ impl<T: FromGenericValue> FromGenericValue for Vec<T> {
405405
}
406406
}
407407

408+
/// Load a bytes array value of a specified type
409+
///
410+
/// # Args
411+
///
412+
/// * `type_key` - The type of the data
413+
/// * `bytes` - The raw data as a u8 array
414+
/// * `qpy_data` - QPY reader metadata
415+
/// * `endian` - The endianess of the data used only for reading
416+
/// `ValueType::Integer` and `ValueType::Float` primitive types
417+
/// (this applies recursively for `ValueType::Tuple` too). All other
418+
/// data is big endian per the QPY format documentation. The use of
419+
/// little endian data for floats and integers in instruction parameter
420+
/// contexts only is an oversight/mistake in the QPY implementation for
421+
/// format versions <=17. All data is supposed to be network byte order.
408422
pub(crate) fn load_value(
409423
type_key: ValueType,
410424
bytes: &Bytes,
411425
qpy_data: &mut QPYReadData,
426+
endian: Endian,
412427
) -> Result<GenericValue, QpyError> {
413428
match type_key {
414429
ValueType::Bool => {
@@ -417,15 +432,21 @@ pub(crate) fn load_value(
417432
}
418433
ValueType::Integer => {
419434
// a little tricky since this can be either i64 or biguint
420-
let result = bytes.try_into();
421-
if let Ok(value) = result {
422-
Ok(GenericValue::Int64(value))
435+
if bytes.len() <= 8 {
436+
let mut bytes_array: [u8; 8] = [0; 8];
437+
for (idx, byte) in bytes.iter().enumerate() {
438+
bytes_array[idx] = *byte;
439+
}
440+
match endian {
441+
Endian::Little => Ok(GenericValue::Int64(i64::from_le_bytes(bytes_array))),
442+
Endian::Big => Ok(GenericValue::Int64(i64::from_be_bytes(bytes_array))),
443+
}
423444
} else {
424445
load_biguint_value(bytes)
425446
}
426447
}
427448
ValueType::Float => {
428-
let value: f64 = bytes.try_into()?;
449+
let value: f64 = bytes.try_to_f64(endian)?;
429450
Ok(GenericValue::Float64(value))
430451
}
431452
ValueType::Complex => {
@@ -465,7 +486,7 @@ pub(crate) fn load_value(
465486
}
466487
ValueType::Tuple => {
467488
let (elements_pack, _) = deserialize::<GenericDataSequencePack>(bytes)?;
468-
let values = unpack_generic_value_sequence(elements_pack, qpy_data)?;
489+
let values = unpack_generic_value_sequence(elements_pack, qpy_data, endian)?;
469490
Ok(GenericValue::Tuple(values))
470491
}
471492
ValueType::NumpyObject => {
@@ -619,8 +640,9 @@ pub(crate) fn pack_generic_value(
619640
pub(crate) fn unpack_generic_value(
620641
value_pack: &GenericDataPack,
621642
qpy_data: &mut QPYReadData,
643+
endian: Endian,
622644
) -> Result<GenericValue, QpyError> {
623-
let result = load_value(value_pack.type_key, &value_pack.data, qpy_data)?;
645+
let result = load_value(value_pack.type_key, &value_pack.data, qpy_data, endian)?;
624646
Ok(result)
625647
}
626648

@@ -636,7 +658,7 @@ pub(crate) fn unpack_duration_value(
636658
let duration = unpack_duration(deserialize::<DurationPack>(&value_pack.data)?.0);
637659
Ok(GenericValue::Duration(duration))
638660
}
639-
_ => unpack_generic_value(value_pack, qpy_data), // fallback (duration can also be expression)
661+
_ => unpack_generic_value(value_pack, qpy_data, Endian::Little), // fallback (duration can also be expression)
640662
}
641663
}
642664

@@ -690,11 +712,12 @@ pub(crate) fn pack_generic_value_sequence(
690712
pub(crate) fn unpack_generic_value_sequence(
691713
value_seqeunce_pack: GenericDataSequencePack,
692714
qpy_data: &mut QPYReadData,
715+
endian: Endian,
693716
) -> Result<Vec<GenericValue>, QpyError> {
694717
value_seqeunce_pack
695718
.elements
696719
.iter()
697-
.map(|data_pack| unpack_generic_value(data_pack, qpy_data))
720+
.map(|data_pack| unpack_generic_value(data_pack, qpy_data, endian))
698721
.collect()
699722
}
700723

0 commit comments

Comments
 (0)