Skip to content

Commit 2bc120c

Browse files
committed
Ensure output is sorted lexicographically
1 parent 3d477f6 commit 2bc120c

1 file changed

Lines changed: 43 additions & 45 deletions

File tree

crates/accelerate/src/sparse_pauli_op.rs

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ pub fn decompose_dense(
107107
operator.shape()
108108
)));
109109
}
110-
let (stack, mut out_stack, mut scratch) =
110+
let (stack, mut out_list, mut scratch) =
111111
decompose_first_level(operator.as_array(), num_qubits);
112-
decompose_middle_levels(stack, &mut out_stack, &mut scratch, num_qubits);
113-
let out = decompose_last_level(&mut out_stack, &scratch, num_qubits, tolerance)?;
112+
decompose_middle_levels(stack, &mut out_list, &mut scratch, num_qubits);
113+
let out = decompose_last_level(&mut out_list, &scratch, num_qubits, tolerance)?;
114114
Ok(ZXPaulis {
115115
z: PyArray1::from_vec(py, out.z)
116116
.reshape([out.phases.len(), num_qubits])?
@@ -135,15 +135,15 @@ fn decompose_first_level(
135135
let side = 1 << num_qubits;
136136
let zero = Complex64::new(0.0, 0.0);
137137
let mut stack = Vec::<PauliLocation>::with_capacity(4);
138-
let mut out_stack = Vec::<PauliLocation>::new();
138+
let mut out_list = Vec::<PauliLocation>::new();
139139
let mut scratch = Vec::<Complex64>::with_capacity(side * side);
140140
match num_qubits {
141141
0 => {}
142142
1 => {
143143
// If we've only got one qubit, we just want to copy the data over in the correct
144144
// continuity and let the base case of the iteration take care of outputting it.
145145
scratch.extend(in_op.iter());
146-
out_stack.push(PauliLocation::begin(num_qubits));
146+
out_list.push(PauliLocation::begin(num_qubits));
147147
}
148148
_ => {
149149
unsafe { scratch.set_len(side * side) };
@@ -214,33 +214,31 @@ fn decompose_first_level(
214214
z_nonzero = z_nonzero || (value != zero);
215215
}
216216
}
217-
let target_stack = if cur_qubit == 1 {
218-
&mut out_stack
217+
// The middle-levels `stack` is a LIFO, so if we push in this order, we'll consider the
218+
// Pauli terms in lexicographical order, which is the canonical order from
219+
// `SparsePauliOp.sort`. Populating the `out_list` (an initially empty `Vec`) effectively
220+
// reverses the stack, so we want to push its elements in the IXYZ order.
221+
if loc.qubit() == 1 {
222+
i_nonzero.then(|| out_list.push(loc.push_i()));
223+
x_nonzero.then(|| out_list.push(loc.push_x()));
224+
y_nonzero.then(|| out_list.push(loc.push_y()));
225+
z_nonzero.then(|| out_list.push(loc.push_z()));
219226
} else {
220-
&mut stack
221-
};
222-
if i_nonzero {
223-
target_stack.push(loc.push_i());
224-
}
225-
if x_nonzero {
226-
target_stack.push(loc.push_x());
227-
}
228-
if y_nonzero {
229-
target_stack.push(loc.push_y());
230-
}
231-
if z_nonzero {
232-
target_stack.push(loc.push_z());
227+
z_nonzero.then(|| stack.push(loc.push_z()));
228+
y_nonzero.then(|| stack.push(loc.push_y()));
229+
x_nonzero.then(|| stack.push(loc.push_x()));
230+
i_nonzero.then(|| stack.push(loc.push_i()));
233231
}
234232
}
235233
}
236-
(stack, out_stack, scratch)
234+
(stack, out_list, scratch)
237235
}
238236

239237
/// Iteratively decompose the matrix at all levels other than the first and last. This populates
240238
/// the `out_stack` with locations
241239
fn decompose_middle_levels(
242240
mut stack: Vec<PauliLocation>,
243-
mut out_stack: &mut Vec<PauliLocation>,
241+
out_list: &mut Vec<PauliLocation>,
244242
scratch: &mut [Complex64],
245243
num_qubits: usize,
246244
) {
@@ -256,11 +254,6 @@ fn decompose_middle_levels(
256254
// for X and Y) so we can re-use their scratch space and avoid re-allocating. We're doing
257255
// the multiple assignment `(I, Z) = (I + Z, I - Z)`.
258256
let mid = 1 << loc.qubit();
259-
let target_stack = if loc.qubit() == 1 {
260-
&mut out_stack
261-
} else {
262-
&mut stack
263-
};
264257
let mut i_nonzero = false;
265258
let mut z_nonzero = false;
266259
let i_row_0 = loc.row();
@@ -302,23 +295,26 @@ fn decompose_middle_levels(
302295
y_nonzero = y_nonzero || (sub != zero);
303296
}
304297
}
305-
if i_nonzero {
306-
target_stack.push(loc.push_i());
307-
}
308-
if x_nonzero {
309-
target_stack.push(loc.push_x());
310-
}
311-
if y_nonzero {
312-
target_stack.push(loc.push_y());
313-
}
314-
if z_nonzero {
315-
target_stack.push(loc.push_z());
298+
// The middle-levels `stack` is a LIFO, so if we push in this order, we'll consider the
299+
// Pauli terms in lexicographical order, which is the canonical order from
300+
// `SparsePauliOp.sort`. Populating the `out_list` (an initially empty `Vec`) effectively
301+
// reverses the stack, so we want to push its elements in the IXYZ order.
302+
if loc.qubit() == 1 {
303+
i_nonzero.then(|| out_list.push(loc.push_i()));
304+
x_nonzero.then(|| out_list.push(loc.push_x()));
305+
y_nonzero.then(|| out_list.push(loc.push_y()));
306+
z_nonzero.then(|| out_list.push(loc.push_z()));
307+
} else {
308+
z_nonzero.then(|| stack.push(loc.push_z()));
309+
y_nonzero.then(|| stack.push(loc.push_y()));
310+
x_nonzero.then(|| stack.push(loc.push_x()));
311+
i_nonzero.then(|| stack.push(loc.push_i()));
316312
}
317313
}
318314
}
319315

320316
fn decompose_last_level(
321-
out_stack: &mut Vec<PauliLocation>,
317+
out_list: &mut Vec<PauliLocation>,
322318
scratch: &[Complex64],
323319
num_qubits: usize,
324320
tolerance: f64,
@@ -329,16 +325,16 @@ fn decompose_last_level(
329325
// don't really pay much cost if we overallocate, but underallocating means that all four
330326
// outputs have to copy their data across to a new allocation.
331327
let mut out = DecomposeOut {
332-
z: Vec::with_capacity(4 * num_qubits * out_stack.len()),
333-
x: Vec::with_capacity(4 * num_qubits * out_stack.len()),
334-
phases: Vec::with_capacity(4 * out_stack.len()),
335-
coeffs: Vec::with_capacity(4 * out_stack.len()),
328+
z: Vec::with_capacity(4 * num_qubits * out_list.len()),
329+
x: Vec::with_capacity(4 * num_qubits * out_list.len()),
330+
phases: Vec::with_capacity(4 * out_list.len()),
331+
coeffs: Vec::with_capacity(4 * out_list.len()),
336332
scale,
337333
tol: (tolerance * tolerance) / (scale * scale),
338334
num_qubits,
339335
};
340336

341-
for loc in out_stack.drain(..) {
337+
for loc in out_list.drain(..) {
342338
let row = loc.row();
343339
let col = loc.col();
344340
let base = row * side + col;
@@ -350,10 +346,12 @@ fn decompose_last_level(
350346
let x = row ^ col;
351347
let z = row;
352348
let phase = (x & z).count_ones() as u8;
349+
// ... and pushing the last Pauli onto the chain happens forwards, since this is the
350+
// construction of the final object.
353351
push_pauli_if_nonzero(x, z, phase, i_value, &mut out);
354-
push_pauli_if_nonzero(x, z | 1, phase, z_value, &mut out);
355352
push_pauli_if_nonzero(x | 1, z, phase, x_value, &mut out);
356353
push_pauli_if_nonzero(x | 1, z | 1, phase + 1, y_value, &mut out);
354+
push_pauli_if_nonzero(x, z | 1, phase, z_value, &mut out);
357355
}
358356
// If we _wildly_ overallocated, then shrink back to a sensible size to avoid tying up too much
359357
// memory as we return to Python space.

0 commit comments

Comments
 (0)