Skip to content

Commit db04339

Browse files
Prepare PauliEvolutionGate for Rustiq & port it to Rust (#13295)
* py version for expand * expand fully & simplify lie trotter * use examples that actually do not commute * add plugin structure * fix plugin name, rm complex from expand --> expand should return float | ParameterExpression * paulievo in Rust * take care of global phase * support barriers * fix time parameter some things are still upside down and it seems like it would be cleaner to do the time multiplication inside pauli_evolution * fix lint * fix final barrier * fix wrapping * add reno and HLS docs * fix unreachable * fix QPY test & pauli feature map * use SX as basis tranfo * slight performance tweak move the x2 multiplication of the rotation angle to Python, where we can pull it together with other floats and minimize the float * Param multiplications * implement review comments * do_fountain * clippy * review comments * fix the basis trafo! * properly test the evolution basis changes * fix more tests * Shelly's review comments --------- Co-authored-by: Alexander Ivrii <alexi@il.ibm.com>
1 parent 2a1fd08 commit db04339

17 files changed

Lines changed: 778 additions & 479 deletions

File tree

crates/accelerate/src/circuit_library/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ use pyo3::prelude::*;
1414

1515
mod entanglement;
1616
mod iqp;
17+
mod pauli_evolution;
1718
mod pauli_feature_map;
1819
mod quantum_volume;
1920

2021
pub fn circuit_library(m: &Bound<PyModule>) -> PyResult<()> {
22+
m.add_wrapped(wrap_pyfunction!(pauli_evolution::py_pauli_evolution))?;
2123
m.add_wrapped(wrap_pyfunction!(pauli_feature_map::pauli_feature_map))?;
2224
m.add_wrapped(wrap_pyfunction!(entanglement::get_entangler_map))?;
2325
m.add_wrapped(wrap_pyfunction!(iqp::py_iqp))?;
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2024
4+
//
5+
// This code is licensed under the Apache License, Version 2.0. You may
6+
// obtain a copy of this license in the LICENSE.txt file in the root directory
7+
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// Any modifications or derivative works of this code must retain this
10+
// copyright notice, and modified files need to carry a notice indicating
11+
// that they have been altered from the originals.
12+
13+
use pyo3::prelude::*;
14+
use pyo3::types::{PyList, PyString, PyTuple};
15+
use qiskit_circuit::circuit_data::CircuitData;
16+
use qiskit_circuit::operations::{multiply_param, radd_param, Param, PyInstruction, StandardGate};
17+
use qiskit_circuit::packed_instruction::PackedOperation;
18+
use qiskit_circuit::{imports, Clbit, Qubit};
19+
use smallvec::{smallvec, SmallVec};
20+
21+
// custom types for a more readable code
22+
type StandardInstruction = (StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>);
23+
type Instruction = (
24+
PackedOperation,
25+
SmallVec<[Param; 3]>,
26+
Vec<Qubit>,
27+
Vec<Clbit>,
28+
);
29+
30+
/// Return instructions (using only StandardGate operations) to implement a Pauli evolution
31+
/// of a given Pauli string over a given time (as Param).
32+
///
33+
/// Args:
34+
/// pauli: The Pauli string, e.g. "IXYZ".
35+
/// indices: The qubit indices the Pauli acts on, e.g. if given as [0, 1, 2, 3] with the
36+
/// Pauli "IXYZ", then the correspondence is I_0 X_1 Y_2 Z_3.
37+
/// time: The rotation angle. Note that this will directly be used as input of the
38+
/// rotation gate and not be multiplied by a factor of 2 (that should be done before so
39+
/// that this function can remain Rust-only).
40+
/// phase_gate: If ``true``, use the ``PhaseGate`` instead of ``RZGate`` as single-qubit rotation.
41+
/// do_fountain: If ``true``, implement the CX propagation as "fountain" shape, where each
42+
/// CX uses the top qubit as target. If ``false``, uses a "chain" shape, where CX in between
43+
/// neighboring qubits are used.
44+
///
45+
/// Returns:
46+
/// A pointer to an iterator over standard instructions.
47+
pub fn pauli_evolution<'a>(
48+
pauli: &'a str,
49+
indices: Vec<u32>,
50+
time: Param,
51+
phase_gate: bool,
52+
do_fountain: bool,
53+
) -> Box<dyn Iterator<Item = StandardInstruction> + 'a> {
54+
// ensure the Pauli has no identity terms
55+
let binding = pauli.to_lowercase(); // lowercase for convenience
56+
let active = binding
57+
.as_str()
58+
.chars()
59+
.zip(indices)
60+
.filter(|(pauli, _)| *pauli != 'i');
61+
let (paulis, indices): (Vec<char>, Vec<u32>) = active.unzip();
62+
63+
match (phase_gate, indices.len()) {
64+
(_, 0) => Box::new(std::iter::empty()),
65+
(false, 1) => Box::new(single_qubit_evolution(paulis[0], indices[0], time)),
66+
(false, 2) => two_qubit_evolution(paulis, indices, time),
67+
_ => Box::new(multi_qubit_evolution(
68+
paulis,
69+
indices,
70+
time,
71+
phase_gate,
72+
do_fountain,
73+
)),
74+
}
75+
}
76+
77+
/// Implement a single-qubit Pauli evolution of a Pauli given as char, on a given index and
78+
/// for given time. Note that the time here equals the angle of the rotation and is not
79+
/// multiplied by a factor of 2.
80+
fn single_qubit_evolution(
81+
pauli: char,
82+
index: u32,
83+
time: Param,
84+
) -> impl Iterator<Item = StandardInstruction> {
85+
let qubit: SmallVec<[Qubit; 2]> = smallvec![Qubit(index)];
86+
let param: SmallVec<[Param; 3]> = smallvec![time];
87+
88+
std::iter::once(match pauli {
89+
'x' => (StandardGate::RXGate, param, qubit),
90+
'y' => (StandardGate::RYGate, param, qubit),
91+
'z' => (StandardGate::RZGate, param, qubit),
92+
_ => unreachable!("Unsupported Pauli, at this point we expected one of x, y, z."),
93+
})
94+
}
95+
96+
/// Implement a 2-qubit Pauli evolution of a Pauli string, on a given indices and
97+
/// for given time. Note that the time here equals the angle of the rotation and is not
98+
/// multiplied by a factor of 2.
99+
///
100+
/// If possible, Qiskit's native 2-qubit Pauli rotations are used. Otherwise, the general
101+
/// multi-qubit evolution is called.
102+
fn two_qubit_evolution<'a>(
103+
pauli: Vec<char>,
104+
indices: Vec<u32>,
105+
time: Param,
106+
) -> Box<dyn Iterator<Item = StandardInstruction> + 'a> {
107+
let qubits: SmallVec<[Qubit; 2]> = smallvec![Qubit(indices[0]), Qubit(indices[1])];
108+
let param: SmallVec<[Param; 3]> = smallvec![time.clone()];
109+
let paulistring: String = pauli.iter().collect();
110+
111+
match paulistring.as_str() {
112+
"xx" => Box::new(std::iter::once((StandardGate::RXXGate, param, qubits))),
113+
"zx" => Box::new(std::iter::once((StandardGate::RZXGate, param, qubits))),
114+
"yy" => Box::new(std::iter::once((StandardGate::RYYGate, param, qubits))),
115+
"zz" => Box::new(std::iter::once((StandardGate::RZZGate, param, qubits))),
116+
// Note: the CX modes (do_fountain=true/false) give the same circuit for a 2-qubit
117+
// Pauli, so we just set it to false here
118+
_ => Box::new(multi_qubit_evolution(pauli, indices, time, false, false)),
119+
}
120+
}
121+
122+
/// Implement a multi-qubit Pauli evolution. See ``pauli_evolution`` detailed docs.
123+
fn multi_qubit_evolution(
124+
pauli: Vec<char>,
125+
indices: Vec<u32>,
126+
time: Param,
127+
phase_gate: bool,
128+
do_fountain: bool,
129+
) -> impl Iterator<Item = StandardInstruction> {
130+
let active_paulis: Vec<(char, Qubit)> = pauli
131+
.into_iter()
132+
.zip(indices.into_iter().map(Qubit))
133+
.collect();
134+
135+
// get the basis change: x -> HGate, y -> SXdgGate, z -> nothing
136+
let basis_change: Vec<StandardInstruction> = active_paulis
137+
.iter()
138+
.filter(|(p, _)| *p != 'z')
139+
.map(|(p, q)| match p {
140+
'x' => (StandardGate::HGate, smallvec![], smallvec![*q]),
141+
'y' => (StandardGate::SXGate, smallvec![], smallvec![*q]),
142+
_ => unreachable!("Invalid Pauli string."), // "z" and "i" have been filtered out
143+
})
144+
.collect();
145+
146+
// get the inverse basis change
147+
let inverse_basis_change: Vec<StandardInstruction> = basis_change
148+
.iter()
149+
.map(|(gate, _, qubit)| match gate {
150+
StandardGate::HGate => (StandardGate::HGate, smallvec![], qubit.clone()),
151+
StandardGate::SXGate => (StandardGate::SXdgGate, smallvec![], qubit.clone()),
152+
_ => unreachable!("Invalid basis-changing Clifford."),
153+
})
154+
.collect();
155+
156+
// get the CX propagation up to the first qubit, and down
157+
let (chain_up, chain_down) = match do_fountain {
158+
true => (
159+
cx_fountain(active_paulis.clone()),
160+
cx_fountain(active_paulis.clone()).rev(),
161+
),
162+
false => (
163+
cx_chain(active_paulis.clone()),
164+
cx_chain(active_paulis.clone()).rev(),
165+
),
166+
};
167+
168+
// get the RZ gate on the first qubit
169+
let first_qubit = active_paulis.first().unwrap().1;
170+
let z_rotation = std::iter::once((
171+
if phase_gate {
172+
StandardGate::PhaseGate
173+
} else {
174+
StandardGate::RZGate
175+
},
176+
smallvec![time],
177+
smallvec![first_qubit],
178+
));
179+
180+
// and finally chain everything together
181+
basis_change
182+
.into_iter()
183+
.chain(chain_down)
184+
.chain(z_rotation)
185+
.chain(chain_up)
186+
.chain(inverse_basis_change)
187+
}
188+
189+
/// Implement a Pauli evolution circuit.
190+
///
191+
/// The Pauli evolution is implemented as a basis transformation to the Pauli-Z basis,
192+
/// followed by a CX-chain and then a single Pauli-Z rotation on the last qubit. Then the CX-chain
193+
/// is uncomputed and the inverse basis transformation applied. E.g. for the evolution under the
194+
/// Pauli string XIYZ we have the circuit
195+
/// ┌───┐┌───────┐┌───┐
196+
/// 0: ─────────────┤ X ├┤ Rz(2) ├┤ X ├───────────
197+
/// ┌──────┐┌───┐└─┬─┘└───────┘└─┬─┘┌───┐┌────┐
198+
/// 1: ┤ √Xdg ├┤ X ├──■─────────────■──┤ X ├┤ √X ├
199+
/// └──────┘└─┬─┘ └─┬─┘└────┘
200+
/// 2: ──────────┼───────────────────────┼────────
201+
/// ┌───┐ │ │ ┌───┐
202+
/// 3: ─┤ H ├────■───────────────────────■──┤ H ├─
203+
/// └───┘ └───┘
204+
///
205+
/// Args:
206+
/// num_qubits: The number of qubits in the Hamiltonian.
207+
/// sparse_paulis: The Paulis to implement. Given in a sparse-list format with elements
208+
/// ``(pauli_string, qubit_indices, coefficient)``. An element of the form
209+
/// ``("IXYZ", [0,1,2,3], 0.2)``, for example, is interpreted in terms of qubit indices as
210+
/// I_q0 X_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 0.4.
211+
/// insert_barriers: If ``true``, insert a barrier in between the evolution of individual
212+
/// Pauli terms.
213+
/// do_fountain: If ``true``, implement the CX propagation as "fountain" shape, where each
214+
/// CX uses the top qubit as target. If ``false``, uses a "chain" shape, where CX in between
215+
/// neighboring qubits are used.
216+
///
217+
/// Returns:
218+
/// Circuit data for to implement the evolution.
219+
#[pyfunction]
220+
#[pyo3(name = "pauli_evolution", signature = (num_qubits, sparse_paulis, insert_barriers=false, do_fountain=false))]
221+
pub fn py_pauli_evolution(
222+
num_qubits: i64,
223+
sparse_paulis: &Bound<PyList>,
224+
insert_barriers: bool,
225+
do_fountain: bool,
226+
) -> PyResult<CircuitData> {
227+
let py = sparse_paulis.py();
228+
let num_paulis = sparse_paulis.len();
229+
let mut paulis: Vec<String> = Vec::with_capacity(num_paulis);
230+
let mut indices: Vec<Vec<u32>> = Vec::with_capacity(num_paulis);
231+
let mut times: Vec<Param> = Vec::with_capacity(num_paulis);
232+
let mut global_phase = Param::Float(0.0);
233+
let mut modified_phase = false; // keep track of whether we modified the phase
234+
235+
for el in sparse_paulis.iter() {
236+
let tuple = el.downcast::<PyTuple>()?;
237+
let pauli = tuple.get_item(0)?.downcast::<PyString>()?.to_string();
238+
let time = Param::extract_no_coerce(&tuple.get_item(2)?)?;
239+
240+
if pauli.as_str().chars().all(|p| p == 'i') {
241+
global_phase = radd_param(global_phase, time, py);
242+
modified_phase = true;
243+
continue;
244+
}
245+
246+
paulis.push(pauli);
247+
times.push(time); // note we do not multiply by 2 here, this is done Python side!
248+
indices.push(tuple.get_item(1)?.extract::<Vec<u32>>()?)
249+
}
250+
251+
let barrier = get_barrier(py, num_qubits as u32);
252+
253+
let evos = paulis.iter().enumerate().zip(indices).zip(times).flat_map(
254+
|(((i, pauli), qubits), time)| {
255+
let as_packed = pauli_evolution(pauli, qubits, time, false, do_fountain).map(
256+
|(gate, params, qubits)| -> PyResult<Instruction> {
257+
Ok((
258+
gate.into(),
259+
params,
260+
Vec::from_iter(qubits.into_iter()),
261+
Vec::new(),
262+
))
263+
},
264+
);
265+
266+
// this creates an iterator containing a barrier only if required, otherwise it is empty
267+
let maybe_barrier = (insert_barriers && i < (num_paulis - 1))
268+
.then_some(Ok(barrier.clone()))
269+
.into_iter();
270+
as_packed.chain(maybe_barrier)
271+
},
272+
);
273+
274+
// When handling all-identity Paulis above, we added the time as global phase.
275+
// However, the all-identity Paulis should add a negative phase, as they implement
276+
// exp(-i t I). We apply the negative sign here, to only do a single (-1) multiplication,
277+
// instead of doing it every time we find an all-identity Pauli.
278+
if modified_phase {
279+
global_phase = multiply_param(&global_phase, -1.0, py);
280+
}
281+
282+
CircuitData::from_packed_operations(py, num_qubits as u32, 0, evos, global_phase)
283+
}
284+
285+
/// Build a CX chain over the active qubits. E.g. with q_1 inactive, this would return
286+
///
287+
/// ┌───┐
288+
/// q_0: ──────────┤ X ├
289+
/// └─┬─┘
290+
/// q_1: ────────────┼──
291+
/// ┌───┐ │
292+
/// q_2: ─────┤ X ├──■──
293+
/// ┌───┐└─┬─┘
294+
/// q_3: ┤ X ├──■───────
295+
/// └─┬─┘
296+
/// q_4: ──■────────────
297+
///
298+
fn cx_chain(
299+
active_paulis: Vec<(char, Qubit)>,
300+
) -> Box<dyn DoubleEndedIterator<Item = StandardInstruction>> {
301+
let num_terms = active_paulis.len();
302+
Box::new(
303+
(0..num_terms - 1)
304+
.map(move |i| (active_paulis[i].1, active_paulis[i + 1].1))
305+
.map(|(target, ctrl)| (StandardGate::CXGate, smallvec![], smallvec![ctrl, target])),
306+
)
307+
}
308+
309+
/// Build a CX fountain over the active qubits. E.g. with q_1 inactive, this would return
310+
///
311+
//// ┌───┐┌───┐┌───┐
312+
//// q_0: ┤ X ├┤ X ├┤ X ├
313+
//// └─┬─┘└─┬─┘└─┬─┘
314+
//// q_1: ──┼────┼────┼──
315+
//// │ │ │
316+
//// q_2: ──■────┼────┼──
317+
//// │ │
318+
//// q_3: ───────■────┼──
319+
//// │
320+
//// q_4: ────────────■──
321+
///
322+
fn cx_fountain(
323+
active_paulis: Vec<(char, Qubit)>,
324+
) -> Box<dyn DoubleEndedIterator<Item = StandardInstruction>> {
325+
let num_terms = active_paulis.len();
326+
let first_qubit = active_paulis[0].1;
327+
Box::new((1..num_terms).rev().map(move |i| {
328+
let ctrl = active_paulis[i].1;
329+
(
330+
StandardGate::CXGate,
331+
smallvec![],
332+
smallvec![ctrl, first_qubit],
333+
)
334+
}))
335+
}
336+
337+
fn get_barrier(py: Python, num_qubits: u32) -> Instruction {
338+
let barrier_cls = imports::BARRIER.get_bound(py);
339+
let barrier = barrier_cls
340+
.call1((num_qubits,))
341+
.expect("Could not create Barrier Python-side");
342+
let barrier_inst = PyInstruction {
343+
qubits: num_qubits,
344+
clbits: 0,
345+
params: 0,
346+
op_name: "barrier".to_string(),
347+
control_flow: false,
348+
instruction: barrier.into(),
349+
};
350+
(
351+
barrier_inst.into(),
352+
smallvec![],
353+
(0..num_qubits).map(Qubit).collect(),
354+
vec![],
355+
)
356+
}

0 commit comments

Comments
 (0)