Skip to content

Commit

Permalink
fixes after refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
alexanderivrii committed Oct 31, 2024
1 parent 397e0d6 commit e283496
Show file tree
Hide file tree
Showing 4 changed files with 359 additions and 7 deletions.
2 changes: 1 addition & 1 deletion crates/accelerate/src/circuit_library/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
use pyo3::prelude::*;

mod entanglement;
mod pauli_evolution;
mod iqp;
mod pauli_evolution;
mod pauli_feature_map;
mod quantum_volume;

Expand Down
330 changes: 330 additions & 0 deletions crates/accelerate/src/circuit_library/pauli_evolution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2024
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

use pyo3::prelude::*;
use pyo3::types::{PyList, PyString, PyTuple};
use qiskit_circuit::circuit_data::CircuitData;
use qiskit_circuit::operations::{multiply_param, radd_param, Param, PyInstruction, StandardGate};
use qiskit_circuit::packed_instruction::PackedOperation;
use qiskit_circuit::{imports, Clbit, Qubit};
use smallvec::{smallvec, SmallVec};

// custom types for a more readable code
type StandardInstruction = (StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>);
type Instruction = (
PackedOperation,
SmallVec<[Param; 3]>,
Vec<Qubit>,
Vec<Clbit>,
);

/// Return instructions (using only StandardGate operations) to implement a Pauli evolution
/// of a given Pauli string over a given time (as Param).
///
/// Args:
/// pauli: The Pauli string, e.g. "IXYZ".
/// indices: The qubit indices the Pauli acts on, e.g. if given as [0, 1, 2, 3] with the
/// Pauli "IXYZ", then the correspondence is I_0 X_1 Y_2 Z_3.
/// time: The rotation angle. Note that this will directly be used as input of the
/// rotation gate and not be multiplied by a factor of 2 (that should be done before so
/// that this function can remain Rust-only).
/// phase_gate: If ``true``, use the ``PhaseGate`` instead of ``RZGate`` as single-qubit rotation.
/// do_fountain: If ``true``, implement the CX propagation as "fountain" shape, where each
/// CX uses the top qubit as target. If ``false``, uses a "chain" shape, where CX in between
/// neighboring qubits are used.
///
/// Returns:
/// A pointer to an iterator over standard instructions.
pub fn pauli_evolution<'a>(
pauli: &'a str,
indices: Vec<u32>,
time: Param,
phase_gate: bool,
do_fountain: bool,
) -> Box<dyn Iterator<Item = StandardInstruction> + 'a> {
// ensure the Pauli has no identity terms
let binding = pauli.to_lowercase(); // lowercase for convenience
let active = binding
.as_str()
.chars()
.zip(indices)
.filter(|(pauli, _)| *pauli != 'i');
let (paulis, indices): (Vec<char>, Vec<u32>) = active.unzip();

match (phase_gate, indices.len()) {
(_, 0) => Box::new(std::iter::empty()),
(false, 1) => Box::new(single_qubit_evolution(paulis[0], indices[0], time)),
(false, 2) => two_qubit_evolution(paulis, indices, time),
_ => Box::new(multi_qubit_evolution(
paulis,
indices,
time,
phase_gate,
do_fountain,
)),
}
}

/// Implement a single-qubit Pauli evolution of a Pauli given as char, on a given index and
/// for given time. Note that the time here equals the angle of the rotation and is not
/// multiplied by a factor of 2.
fn single_qubit_evolution(
pauli: char,
index: u32,
time: Param,
) -> impl Iterator<Item = StandardInstruction> {
let qubit: SmallVec<[Qubit; 2]> = smallvec![Qubit(index)];
let param: SmallVec<[Param; 3]> = smallvec![time];

std::iter::once(match pauli {
'x' => (StandardGate::RXGate, param, qubit),
'y' => (StandardGate::RYGate, param, qubit),
'z' => (StandardGate::RZGate, param, qubit),
_ => unreachable!("Unsupported Pauli, at this point we expected one of x, y, z."),
})
}

/// Implement a 2-qubit Pauli evolution of a Pauli string, on a given indices and
/// for given time. Note that the time here equals the angle of the rotation and is not
/// multiplied by a factor of 2.
///
/// If possible, Qiskit's native 2-qubit Pauli rotations are used. Otherwise, the general
/// multi-qubit evolution is called.
fn two_qubit_evolution<'a>(
pauli: Vec<char>,
indices: Vec<u32>,
time: Param,
) -> Box<dyn Iterator<Item = StandardInstruction> + 'a> {
let qubits: SmallVec<[Qubit; 2]> = smallvec![Qubit(indices[0]), Qubit(indices[1])];
let param: SmallVec<[Param; 3]> = smallvec![time.clone()];
let paulistring: String = pauli.iter().collect();

match paulistring.as_str() {
"xx" => Box::new(std::iter::once((StandardGate::RXXGate, param, qubits))),
"zx" => Box::new(std::iter::once((StandardGate::RZXGate, param, qubits))),
"yy" => Box::new(std::iter::once((StandardGate::RYYGate, param, qubits))),
"zz" => Box::new(std::iter::once((StandardGate::RZZGate, param, qubits))),
// Note: the CX modes (do_fountain=true/false) give the same circuit for a 2-qubit
// Pauli, so we just set it to false here
_ => Box::new(multi_qubit_evolution(pauli, indices, time, false, false)),
}
}

/// Implement a multi-qubit Pauli evolution. See ``pauli_evolution`` detailed docs.
fn multi_qubit_evolution(
pauli: Vec<char>,
indices: Vec<u32>,
time: Param,
phase_gate: bool,
do_fountain: bool,
) -> impl Iterator<Item = StandardInstruction> {
let active_paulis: Vec<(char, Qubit)> = pauli
.into_iter()
.zip(indices.into_iter().map(Qubit))
.collect();

// get the basis change: x -> HGate, y -> SXdgGate, z -> nothing
let basis_change: Vec<StandardInstruction> = active_paulis
.iter()
.filter(|(p, _)| *p != 'z')
.map(|(p, q)| match p {
'x' => (StandardGate::HGate, smallvec![], smallvec![*q]),
'y' => (StandardGate::SXdgGate, smallvec![], smallvec![*q]),
_ => unreachable!("Invalid Pauli string."), // "z" and "i" have been filtered out
})
.collect();

// get the inverse basis change
let inverse_basis_change: Vec<StandardInstruction> = basis_change
.iter()
.map(|(gate, _, qubit)| match gate {
StandardGate::HGate => (StandardGate::HGate, smallvec![], qubit.clone()),
StandardGate::SXdgGate => (StandardGate::SXGate, smallvec![], qubit.clone()),
_ => unreachable!("Invalid basis-changing Clifford."),
})
.collect();

// get the CX propagation up to the first qubit, and down
let (chain_up, chain_down) = match do_fountain {
true => (
cx_fountain(active_paulis.clone()),
cx_fountain(active_paulis.clone()).rev(),
),
false => (
cx_chain(active_paulis.clone()),
cx_chain(active_paulis.clone()).rev(),
),
};

// get the RZ gate on the first qubit
let first_qubit = active_paulis.first().unwrap().1;
let z_rotation = std::iter::once((
if phase_gate {
StandardGate::PhaseGate
} else {
StandardGate::RZGate
},
smallvec![time],
smallvec![first_qubit],
));

// and finally chain everything together
basis_change
.into_iter()
.chain(chain_down)
.chain(z_rotation)
.chain(chain_up)
.chain(inverse_basis_change)
}

/// Implement a Pauli evolution circuit.
///
/// The Pauli evolution is implemented as a basis transformation to the Pauli-Z basis,
/// followed by a CX-chain and then a single Pauli-Z rotation on the last qubit. Then the CX-chain
/// is uncomputed and the inverse basis transformation applied. E.g. for the evolution under the
/// Pauli string XIYZ we have the circuit
/// ┌───┐┌───────┐┌───┐
/// 0: ─────────────┤ X ├┤ Rz(2) ├┤ X ├───────────
/// ┌──────┐┌───┐└─┬─┘└───────┘└─┬─┘┌───┐┌────┐
/// 1: ┤ √Xdg ├┤ X ├──■─────────────■──┤ X ├┤ √X ├
/// └──────┘└─┬─┘ └─┬─┘└────┘
/// 2: ──────────┼───────────────────────┼────────
/// ┌───┐ │ │ ┌───┐
/// 3: ─┤ H ├────■───────────────────────■──┤ H ├─
/// └───┘ └───┘
///
/// Args:
/// num_qubits: The number of qubits in the Hamiltonian.
/// sparse_paulis: The Paulis to implement. Given in a sparse-list format with elements
/// ``(pauli_string, qubit_indices, coefficient)``. An element of the form
/// ``("IXYZ", [0,1,2,3], 0.2)``, for example, is interpreted in terms of qubit indices as
/// I_q0 X_q1 Y_q2 Z_q3 and will use a RZ rotation angle of 0.4.
/// insert_barriers: If ``true``, insert a barrier in between the evolution of individual
/// Pauli terms.
/// do_fountain: If ``true``, implement the CX propagation as "fountain" shape, where each
/// CX uses the top qubit as target. If ``false``, uses a "chain" shape, where CX in between
/// neighboring qubits are used.
///
/// Returns:
/// Circuit data for to implement the evolution.
#[pyfunction]
#[pyo3(name = "pauli_evolution", signature = (num_qubits, sparse_paulis, insert_barriers=false, do_fountain=false))]
pub fn py_pauli_evolution(
num_qubits: i64,
sparse_paulis: &Bound<PyList>,
insert_barriers: bool,
do_fountain: bool,
) -> PyResult<CircuitData> {
let py = sparse_paulis.py();
let num_paulis = sparse_paulis.len();
let mut paulis: Vec<String> = Vec::with_capacity(num_paulis);
let mut indices: Vec<Vec<u32>> = Vec::with_capacity(num_paulis);
let mut times: Vec<Param> = Vec::with_capacity(num_paulis);
let mut global_phase = Param::Float(0.0);
let mut modified_phase = false; // keep track of whether we modified the phase

for el in sparse_paulis.iter() {
let tuple = el.downcast::<PyTuple>()?;
let pauli = tuple.get_item(0)?.downcast::<PyString>()?.to_string();
let time = Param::extract_no_coerce(&tuple.get_item(2)?)?;

if pauli.as_str().chars().all(|p| p == 'i') {
global_phase = radd_param(global_phase, time, py);
modified_phase = true;
continue;
}

paulis.push(pauli);
times.push(time); // note we do not multiply by 2 here, this is done Python side!
indices.push(tuple.get_item(1)?.extract::<Vec<u32>>()?)
}

let barrier = get_barrier(py, num_qubits as u32);

let evos = paulis.iter().enumerate().zip(indices).zip(times).flat_map(
|(((i, pauli), qubits), time)| {
let as_packed = pauli_evolution(pauli, qubits, time, false, do_fountain).map(
|(gate, params, qubits)| -> PyResult<Instruction> {
Ok((
gate.into(),
params,
Vec::from_iter(qubits.into_iter()),
Vec::new(),
))
},
);

// this creates an iterator containing a barrier only if required, otherwise it is empty
let maybe_barrier = (insert_barriers && i < (num_paulis - 1))
.then_some(Ok(barrier.clone()))
.into_iter();
as_packed.chain(maybe_barrier)
},
);

// When handling all-identity Paulis above, we added the time as global phase.
// However, the all-identity Paulis should add a negative phase, as they implement
// exp(-i t I). We apply the negative sign here, to only do a single (-1) multiplication,
// instead of doing it every time we find an all-identity Pauli.
if modified_phase {
global_phase = multiply_param(&global_phase, -1.0, py);
}

CircuitData::from_packed_operations(py, num_qubits as u32, 0, evos, global_phase)
}

fn cx_chain(
active_paulis: Vec<(char, Qubit)>,
) -> Box<dyn DoubleEndedIterator<Item = StandardInstruction>> {
let num_terms = active_paulis.len();
Box::new(
(0..num_terms - 1)
.map(move |i| (active_paulis[i].1, active_paulis[i + 1].1))
.map(|(target, ctrl)| (StandardGate::CXGate, smallvec![], smallvec![ctrl, target])),
)
}

fn cx_fountain(
active_paulis: Vec<(char, Qubit)>,
) -> Box<dyn DoubleEndedIterator<Item = StandardInstruction>> {
let num_terms = active_paulis.len();
let first_qubit = active_paulis[0].1;
Box::new((1..num_terms).rev().map(move |i| {
let ctrl = active_paulis[i].1;
(
StandardGate::CXGate,
smallvec![],
smallvec![ctrl, first_qubit],
)
}))
}

fn get_barrier(py: Python, num_qubits: u32) -> Instruction {
let barrier_cls = imports::BARRIER.get_bound(py);
let barrier = barrier_cls
.call1((num_qubits,))
.expect("Could not create Barrier Python-side");
let barrier_inst = PyInstruction {
qubits: num_qubits,
clbits: 0,
params: 0,
op_name: "barrier".to_string(),
control_flow: false,
instruction: barrier.into(),
};
(
barrier_inst.into(),
smallvec![],
(0..num_qubits).map(Qubit).collect(),
vec![],
)
}
7 changes: 3 additions & 4 deletions crates/accelerate/src/synthesis/evolution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,10 @@ fn qiskit_rotation_gate(py: Python, paulis: &PauliSet, i: usize, angle: &Param)
'Z' => StandardGate::RZGate,
_ => panic!(),
};
// Due to Rustiq's inner workings, we need to multiply the angle by 2.
// We also need to negate the angle when there is a phase.
// We need to negate the angle when there is a phase.
let param = match phase {
false => multiply_param(angle, 2.0, py),
true => multiply_param(angle, -2.0, py),
false => angle.clone(),
true => multiply_param(angle, -1.0, py),
};
return (standard_gate, smallvec![param], smallvec![Qubit(q as u32)]);
}
Expand Down
27 changes: 25 additions & 2 deletions test/python/transpiler/test_high_level_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@
MCXGate,
SGate,
)
from qiskit.circuit.library.generalized_gates import LinearFunction
from qiskit.quantum_info import Clifford, Operator, Statevector
from qiskit.circuit.library import LinearFunction, PauliEvolutionGate
from qiskit.quantum_info import Clifford, Operator, Statevector, SparsePauliOp
from qiskit.synthesis.linear import random_invertible_binary_matrix
from qiskit.compiler import transpile
from qiskit.exceptions import QiskitError
Expand Down Expand Up @@ -2601,5 +2601,28 @@ def test_annotated_mcx(self):
self.assertEqual(Operator(qc), Operator(qct))


@ddt
class TestPauliEvolutionSynthesisPlugins(QiskitTestCase):
"""Tests related to plugins for PauliEvolutionGate."""

def test_supported_names(self):
"""Test that "default" and "rustiq" plugins do exist."""
supported_plugin_names = high_level_synthesis_plugin_names("PauliEvolution")
self.assertIn("default", supported_plugin_names)
self.assertIn("rustiq", supported_plugin_names)

@data("default", "rustiq")
def test_correctness(self, plugin_name):
"""Test that all plugins return a correct Operator"""
op = SparsePauliOp(["XXX", "YYY", "IZZ"], coeffs=[1, 2, 3])
qc = QuantumCircuit(6)
qc.append(PauliEvolutionGate(op), [1, 2, 4])
hls_config = HLSConfig(PauliEvolution=[plugin_name])
hls_pass = HighLevelSynthesis(hls_config=hls_config)
qct = hls_pass(qc)
# self.assertEqual(Operator(qc), Operator(qct))
self.assertTrue(Operator(qc).equiv(Operator(qct)))


if __name__ == "__main__":
unittest.main()

0 comments on commit e283496

Please sign in to comment.