Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 8 additions & 8 deletions crates/qpy/src/formats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,21 +506,21 @@ pub struct SparsePauliOpListElemPack {
#[derive(Debug)]
pub struct SparsePauliObservableElemPack {
pub num_qubits: u32,
#[bw(calc = coeff_data.len() as u64)]
#[bw(calc = (coeff_data.len() * std::mem::size_of::<f64>()) as u64)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Aren't the coeffs Complex64? Or why is f64 the correct size?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Look at _write_elem_sparse in the python code (the source of the format, in this case). coeffs are stored using

coeff_data = struct.pack(
            f"!{len(coeffs)*2}d", *(val for coeff in coeffs for val in (coeff.real, coeff.imag))
        )

So each coeff is stored using two consecutive f64 values comprising the original Complex64.

In the rust code we do the same, on pack_sparse_pauli_op:

let coeff_data = sparse_observable
            .coeffs()
            .iter()
            .flat_map(|coeff| [coeff.re, coeff.im])
            .collect();

pub coeff_data_size: u64,
#[bw(calc = bitterm_data.len() as u64)]
#[bw(calc = (bitterm_data.len() * std::mem::size_of::<u16>()) as u64)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think there's something I'm not understanding, I thought this would correspond to the size of BitTerm which is u8 -- could you explain where these sizes are coming from?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a bug in the original python implementation, and is actually one of the things we're gonna fix in QPY18, you can see it on the list at #15524. Even more, this bug was found as part of the fix...

The python code used bitterm_data = struct.pack(f"!{len(bitterms)}H", *bitterms); instead of H it should have been a B.

pub bitterm_data_size: u64,
#[bw(calc = inds_data.len() as u64)]
#[bw(calc = (inds_data.len() * std::mem::size_of::<u32>()) as u64)]
pub inds_data_size: u64,
#[bw(calc = bounds_data.len() as u64)]
#[bw(calc = (bounds_data.len() * std::mem::size_of::<u64>()) as u64)]
pub bounds_data_size: u64,
#[br(count = coeff_data_size)]
#[br(count = coeff_data_size / std::mem::size_of::<f64>() as u64)]
pub coeff_data: Vec<f64>, // complex numbers stored in format [re1, im1, re2, im2,...]
#[br(count = bitterm_data_size)]
#[br(count = bitterm_data_size / std::mem::size_of::<u16>() as u64)]
pub bitterm_data: Vec<u16>,
#[br(count = inds_data_size)]
#[br(count = inds_data_size / std::mem::size_of::<u32>() as u64)]
pub inds_data: Vec<u32>,
#[br(count = bounds_data_size)]
#[br(count = bounds_data_size / std::mem::size_of::<u64>() as u64)]
pub bounds_data: Vec<u64>,
}

Expand Down
6 changes: 6 additions & 0 deletions releasenotes/notes/fix-qpy-sparse_pauli-095b5c3e6ef4f5e8.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
fixes:
- |
Fixed a bug with the QPY serialization and deserialization of instructions
containing :class:`.SparseObservable` data.
Fixed `#16722 <https://github.com/Qiskit/qiskit/issues/16722>`__.
15 changes: 14 additions & 1 deletion test/python/qpy/test_roundtrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from qiskit.circuit.random import random_circuit
from qiskit.circuit.parameter import Parameter
from qiskit.circuit.parametervector import ParameterVector
from qiskit.quantum_info import SparsePauliOp
from qiskit.quantum_info import SparsePauliOp, SparseObservable
from qiskit.circuit.classical import expr
from qiskit.synthesis import LieTrotter
from qiskit.qpy.common import QPY_RUST_READ_MIN_VERSION, QPY_RUST_WRITE_MIN_VERSION, QPY_VERSION
Expand Down Expand Up @@ -283,3 +283,16 @@ def test_literal_integers_in_for(self, version, write_with, read_with):
with qc.for_loop((2, 5, (1 << 60))) as _:
qc.x(0)
self.assert_roundtrip_equal(qc, version=version, read_with=read_with, write_with=write_with)

@all_qpy_combinations(17)
def test_evolutiongate_sparse_observable(self, version, write_with, read_with):
"""Test loading a circuit with an evolution gate over a SparseObservable works.

``SparseObservable`` support was added to QPY in version 17.
"""
op = SparseObservable.from_list([("XIX", 0.1), ("ZIZ", 0.3)])
evo = PauliEvolutionGate(op, time=2, synthesis=LieTrotter(reps=2))

qc = QuantumCircuit(op.num_qubits)
qc.append(evo, range(op.num_qubits))
self.assert_roundtrip_equal(qc, version=version, read_with=read_with, write_with=write_with)