Skip to content

QPY Rust and Python compatibility fixes#15847

Draft
gadial wants to merge 9 commits intoQiskit:mainfrom
gadial:qpy_parameter_replay_vector_fix
Draft

QPY Rust and Python compatibility fixes#15847
gadial wants to merge 9 commits intoQiskit:mainfrom
gadial:qpy_parameter_replay_vector_fix

Conversation

@gadial
Copy link
Contributor

@gadial gadial commented Mar 22, 2026

Summary

Fixes several incompatibilities between the Python and Rust file formats; in all cases, the Rust code was changed to conform to the Python expected standard

Details and comments

This PR handles several problems detected with the Rust QPY component, most related to problems arising when an older, Python-based version of the QPY module attempts to read files generated with the Rust module. New tests were added to cover all these cases.

ParameterVector in qpy_reply

In Python, the data type of a ParameterVector inside the qpy_replay object is stored as Parameter (b'p') and not ParameterVector (b'v'). Rust saved them with separate types, resulting in Python failing to load QPY files generated with Rust when the circuits contained parameter expressions involving ParameterVectors.

The fix makes Rust save the data type as Parameter as well. This should not cause problems, as parameters and parameter vectors are treated exactly the same when unpacking a parameter expression:

let lhs: Option<ParameterValueType> = match op.lhs_type {
                ParameterType::Parameter | ParameterType::ParameterVector => {
                    if let Some(value) = param_uuid_map.get(&op.lhs) {
                        Some(parameter_value_type_from_generic_value(value)?)
                    } else {

Note that writing `b'v' here breaks the QPY17 format, meaning that version 2.4.0rc1 does not fully conform to QPY17.

BoxOp duration parameter

In Rust, the BoxOp duration parameter was stored as GenericData which is either duration or expression. This should be the implementation going forward to QPY18 and beyond, but in Python support for duration in instruction parameters was nonexistant, so a hack was used instead, where BoxOp had two parameters, one for the numerical value and the other for the string of the duration's units - as well as "expr" when storing an expression. The current Rust code now conforms to this hack.

Control flow names

Previously, Rust stored the "new style" control flow names, e.g. if_else. This makes Python unable to correctly create those gates as it expects the actual class name. A conversion method from the new to the old style was added.

SwitchCaseOp handling

The way in which SwitchCaseOp parameters are stored in Python QPY is obsolete since the introduction of Rust based control flow ops. However, for now we still need to conform to the old method and the code was changed to reflect this. This allows us to dispose of the specialized treatment for this case in unpack_py_instruction since now we won't read switch instructions via this path.

Since the qpy write code of 2.4.0rc1 already used the new (now reverted) style that does not conform to QPY17 (although control flows are not defined in the spec), but we'll probably transition to it on QPY18, the read code now supports both approaches.

Additional bugfix

As part of the new testing, another bug was discovered in the way condition register is handled - if its value is 0, the program would raise an error. This was fixed.

@gadial gadial requested a review from a team as a code owner March 22, 2026 10:10
@qiskit-bot
Copy link
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core
  • @mtreinish

@ShellyGarion ShellyGarion added the Changelog: Fixed Add a "Fixed" entry in the GitHub Release changelog. label Mar 22, 2026
@ShellyGarion ShellyGarion added this to the 2.4.0 milestone Mar 22, 2026
Copy link
Collaborator

@Cryoris Cryoris left a comment

Choose a reason for hiding this comment

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

I don't quite understand between which versions this PR fixes compatibility issues. Isn't V17 always loaded by Rust already?

@@ -163,9 +163,8 @@ fn pack_condition(
let register_size = bytes.len() as u16;
let data = formats::ConditionData::Register(bytes);
// TODO: this may cause loss of data, but we are constrained by the current qpy format
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this comment still valid or is it resolved with the 0-default value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it's unrelated. The potential loss of data is when the value is too large. We do have BigUint support in the module, it's simply that V17 doesn't use it at this point.

(ParameterType::Parameter, *parameter.symbol.uuid.as_bytes())
}
ParameterValueType::VectorElement(element) => (
ParameterType::ParameterVector,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm a bit surprised the ParameterVector type is not needed for anything -- why did we introduce it then? 🤔 The one additional info that a parameter vector element has is the index, was it connected to that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is needed in general, but in that specific point of the replay there's no real difference between it and Parameter.

num_ctrl_qubits: 0, // standard instructions have no control qubits
ctrl_state: 0,
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
gate_class_name: control_flow_class_name(control_flow_inst.name())?, // need to use the Python class names for backward compatability
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
gate_class_name: control_flow_class_name(control_flow_inst.name())?, // need to use the Python class names for backward compatability
gate_class_name: control_flow_class_name(control_flow_inst.name())?, // need to use the Python class names for backward compatibility

num_ctrl_qubits: 0, // standard instructions have no control qubits
ctrl_state: 0,
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
gate_class_name: control_flow_class_name(control_flow_inst.name())?, // need to use the Python class names for backward compatability
Copy link
Collaborator

Choose a reason for hiding this comment

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

Small nit: why not just pass the enum variant by reference directly? I would expect the enum match to be faster since we don't end up comparing string values 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, will do.

@gadial gadial changed the title Change the encoding of qpy_replay ParameterVector elements QPY Rust and Python compatibility fixes Mar 23, 2026
@gadial gadial marked this pull request as draft March 23, 2026 10:34
@gadial
Copy link
Contributor Author

gadial commented Mar 23, 2026

I don't quite understand between which versions this PR fixes compatibility issues. Isn't V17 always loaded by Rust already?

The problem is when writing V17 via Rust then reading it via Python. This path wasn't tested enough, as opposed to the other direction (writing in Python, reading in Rust). The main problem is regarding how control flows are handled; in Rust the parameters were written differently and Python didn't know how to handle them. This breaks V17 and so must be fixed.

@mtreinish mtreinish added the stable backport potential Make Mergify open a backport PR to the most recent stable branch on merge. label Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Changelog: Fixed Add a "Fixed" entry in the GitHub Release changelog. stable backport potential Make Mergify open a backport PR to the most recent stable branch on merge.

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

5 participants