|
| 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