-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Oxidize PauliFeatureMap
#13045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Oxidize PauliFeatureMap
#13045
Changes from 6 commits
7436ea2
79a4176
eca9a50
0e17036
9820c6f
75126a0
137873b
71ca90b
c42462e
9a49d2f
9878a05
59113e9
56a9f37
4cc14a5
29fb437
d014852
cf0cce5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,317 @@ | ||
| // 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 itertools::Itertools; | ||
| use pyo3::prelude::*; | ||
| use pyo3::types::PySequence; | ||
| use pyo3::types::PyString; | ||
| use qiskit_circuit::circuit_data::CircuitData; | ||
| use qiskit_circuit::imports; | ||
| use qiskit_circuit::operations::PyInstruction; | ||
| use qiskit_circuit::operations::{add_param, multiply_param, rmultiply_param, Param, StandardGate}; | ||
| use qiskit_circuit::packed_instruction::PackedOperation; | ||
| use qiskit_circuit::{Clbit, Qubit}; | ||
| use smallvec::{smallvec, SmallVec}; | ||
| use std::f64::consts::PI; | ||
|
|
||
| use crate::circuit_library::entanglement; | ||
| use crate::QiskitError; | ||
|
|
||
| // custom math and types for a more readable code | ||
| const PI2: f64 = PI / 2.; | ||
| type Instruction = ( | ||
| PackedOperation, | ||
| SmallVec<[Param; 3]>, | ||
| Vec<Qubit>, | ||
| Vec<Clbit>, | ||
| ); | ||
| type StandardInstruction = (StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>); | ||
|
|
||
| /// Return instructions (using only StandardGate operations) to implement a Pauli evolution | ||
| /// of a given Pauli string over a given time (as Param). | ||
| /// | ||
| /// 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: ┤ Rx(pi/2) ├┤ X ├──■─────────────■──┤ X ├┤ Rx(-pi/2) ├ | ||
| /// └──────────┘└─┬─┘ └─┬─┘└───────────┘ | ||
| /// 2: ──────────────┼───────────────────────┼─────────────── | ||
| /// ┌───┐ │ │ ┌───┐ | ||
| /// 3: ─┤ H ├────────■───────────────────────■──┤ H ├──────── | ||
| /// └───┘ └───┘ | ||
| fn pauli_evolution( | ||
|
alexanderivrii marked this conversation as resolved.
|
||
| pauli: &str, | ||
| indices: Vec<u32>, | ||
| time: Param, | ||
| ) -> impl Iterator<Item = StandardInstruction> + '_ { | ||
| // Get pairs of (pauli, qubit) that are active, i.e. that are not the identity. Note that | ||
| // the rest of the code also works if there are only identities, in which case we will | ||
| // effectively return an empty iterator. | ||
| let qubits = indices.iter().map(|i| Qubit(*i)).collect_vec(); | ||
|
alexanderivrii marked this conversation as resolved.
|
||
| let binding = pauli.to_lowercase(); // lowercase for convenience | ||
| let active_paulis = binding | ||
| .as_str() | ||
| .chars() | ||
| .rev() // reverse due to Qiskit's bit ordering convention | ||
| .zip(qubits) | ||
| .filter(|(p, _)| *p != 'i') | ||
| .collect_vec(); | ||
|
|
||
| // get the basis change: x -> HGate, y -> RXGate(pi/2), z -> nothing | ||
| let basis_change = active_paulis | ||
| .clone() | ||
| .into_iter() | ||
| .filter(|(p, _)| *p != 'z') | ||
| .map(|(p, q)| match p { | ||
| 'x' => (StandardGate::HGate, smallvec![], smallvec![q]), | ||
| 'y' => ( | ||
| StandardGate::RXGate, | ||
| smallvec![Param::Float(PI2)], | ||
| smallvec![q], | ||
| ), | ||
| _ => unreachable!("Invalid Pauli string."), // "z" and "i" have been filtered out | ||
| }); | ||
|
|
||
| // get the inverse basis change | ||
| let inverse_basis_change = basis_change.clone().map(|(gate, _, qubit)| match gate { | ||
| StandardGate::HGate => (gate, smallvec![], qubit), | ||
| StandardGate::RXGate => (gate, smallvec![Param::Float(-PI2)], qubit), | ||
| _ => unreachable!(), | ||
| }); | ||
|
|
||
| // get the CX chain down to the target rotation qubit | ||
| let chain_down = active_paulis | ||
| .clone() | ||
| .into_iter() | ||
| .map(|(_, q)| q) | ||
| .tuple_windows() // iterates over (q[i], q[i+1]) windows | ||
| .map(|(ctrl, target)| (StandardGate::CXGate, smallvec![], smallvec![ctrl, target])); | ||
|
alexanderivrii marked this conversation as resolved.
|
||
|
|
||
| // get the CX chain up (cannot use chain_down.rev since tuple_windows is not double ended) | ||
| let chain_up = active_paulis | ||
| .clone() | ||
| .into_iter() | ||
| .rev() | ||
| .map(|(_, q)| q) | ||
| .tuple_windows() | ||
| .map(|(target, ctrl)| (StandardGate::CXGate, smallvec![], smallvec![ctrl, target])); | ||
|
|
||
| // get the RZ gate on the last qubit | ||
| let last_qubit = active_paulis.last().unwrap().1; | ||
| let z_rotation = std::iter::once(( | ||
| StandardGate::PhaseGate, | ||
| smallvec![time], | ||
| smallvec![last_qubit], | ||
| )); | ||
|
|
||
| // and finally chain everything together | ||
| basis_change | ||
| .chain(chain_down) | ||
| .chain(z_rotation) | ||
| .chain(chain_up) | ||
| .chain(inverse_basis_change) | ||
|
alexanderivrii marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /// Build a Pauli feature map circuit. | ||
| /// | ||
| /// Args: | ||
| /// feature_dimension: The feature dimension (i.e. the number of qubits). | ||
| /// parameters: A parameter vector with ``feature_dimension`` elements. Taken as input | ||
| /// here to avoid a call to Python constructing the vector. | ||
| /// reps: The number of repetitions of Hadamard + evolution layers. | ||
| /// entanglement: The entanglement, given as Python string or None (defaults to "full"). | ||
| /// paulis: The Pauli strings as list of strings or None (default to ["z", "zz"]). | ||
| /// alpha: A float multiplier for rotation angles. | ||
| /// insert_barriers: Whether to insert barriers in between the Hadamard and evolution layers. | ||
| /// data_map_func: An accumulation function that takes as input a vector of parameters the | ||
| /// current gate acts on and returns a scalar. | ||
| /// | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's a bit confusing that both the python and rust functions have the same name
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, the parameters are constructed Python-side because (1) we avoid calling Python once we're in Rust space and (2) I'm not even sure we can create And I gave them the same name because they do the same thing 🙂 I know we often have |
||
| /// Returns: | ||
| /// The ``CircuitData`` to construct the Pauli feature map. | ||
| #[pyfunction] | ||
| #[pyo3(signature = (feature_dimension, parameters, *, reps=1, entanglement=None, paulis=None, alpha=2.0, insert_barriers=false, data_map_func=None))] | ||
| #[allow(clippy::too_many_arguments)] | ||
| pub fn pauli_feature_map( | ||
| py: Python, | ||
| feature_dimension: u32, | ||
| parameters: Bound<PyAny>, | ||
| reps: usize, | ||
| entanglement: Option<&Bound<PyAny>>, | ||
| paulis: Option<&Bound<PySequence>>, | ||
| alpha: f64, | ||
| insert_barriers: bool, | ||
| data_map_func: Option<&Bound<PyAny>>, | ||
| ) -> PyResult<CircuitData> { | ||
| // normalize the Pauli strings | ||
| let pauli_strings = _get_paulis(feature_dimension, paulis)?; | ||
|
|
||
| // set the default value for entanglement | ||
| let default = PyString::new_bound(py, "full"); | ||
| let entanglement = entanglement.unwrap_or(&default); | ||
|
|
||
| // extract the parameters from the input variable ``parameters`` | ||
| let parameter_vector = parameters | ||
| .iter()? | ||
| .map(|el| Param::extract_no_coerce(&el?)) | ||
| .collect::<PyResult<Vec<Param>>>()?; | ||
|
|
||
| // construct a Barrier object Python side to (possibly) add to the circuit | ||
| let packed_barrier = _get_barrier(py, feature_dimension)?; | ||
|
Cryoris marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Main work: construct the circuit instructions as iterator. Each repetition is constituted | ||
| // by a layer of Hadamards and the Pauli evolutions of the specified Paulis. | ||
| // Note that we eagerly trigger errors, since the final CircuitData::from_packed_operations | ||
| // does not allow Result objects in the iterator. | ||
| let packed_insts = (0..reps).flat_map(|rep| { | ||
|
Cryoris marked this conversation as resolved.
Outdated
|
||
| let h_layer = (0..feature_dimension).map(|i| { | ||
| ( | ||
| StandardGate::HGate.into(), | ||
| smallvec![], | ||
| vec![Qubit(i)], | ||
| vec![] as Vec<Clbit>, | ||
| ) | ||
| }); | ||
| let parameter_vector = ¶meter_vector; | ||
| let pauli_strings = &pauli_strings; | ||
| let evo = pauli_strings.iter().flat_map(move |pauli| { | ||
| let block_size = pauli.len() as u32; | ||
| let entanglement = | ||
| entanglement::get_entanglement(feature_dimension, block_size, entanglement, rep) | ||
| .unwrap(); | ||
| // let data_map = &mut data_map; | ||
| entanglement.flat_map(move |indices| { | ||
| // get the parameters the evolution is acting on and the corresponding angle, | ||
| // which is given by the data_map_func (we provide a default if not given) | ||
| let indices = indices.as_ref().unwrap(); | ||
| // let indices = indices.as_ref().unwrap(); | ||
|
Cryoris marked this conversation as resolved.
Outdated
|
||
| let active_parameters: Vec<Param> = indices | ||
| .iter() | ||
| .map(|i| parameter_vector[*i as usize].clone()) | ||
| .collect(); | ||
|
|
||
| let angle = match data_map_func { | ||
| Some(fun) => fun | ||
| .call1((active_parameters,)) | ||
| .expect("Failed running ``data_map_func`` Python-side.") | ||
| .extract() | ||
| .expect("Failed retrieving the Param"), | ||
| None => _default_reduce(py, active_parameters), | ||
| }; | ||
|
|
||
| // Get the pauli evolution and map it into | ||
| // (PackedOperation, SmallVec<[Params; 3]>, Vec<Qubit>, Vec<Clbit>) | ||
| // to call CircuitData::from_packed_operations. This is needed since we might | ||
| // have to interject barriers, which are not a standard gate and prevents us | ||
| // from using CircuitData::from_standard_gates. | ||
| pauli_evolution(pauli, indices.clone(), multiply_param(&angle, alpha, py)).map( | ||
| |(gate, params, qargs)| { | ||
| (gate.into(), params, qargs.to_vec(), vec![] as Vec<Clbit>) | ||
| }, | ||
| ) | ||
| }) | ||
| }); | ||
|
|
||
| // Chain the H layer and evolution together, adding barriers around the evolutions, | ||
| // if necessary. Note that in the last repetition, there's no barrier after the evolution. | ||
| let mut out: Box<dyn Iterator<Item = Instruction>> = Box::new(h_layer); | ||
| if insert_barriers { | ||
| out = Box::new(out.chain(std::iter::once(packed_barrier.clone()))); | ||
| } | ||
| out = Box::new(out.chain(evo)); | ||
| if insert_barriers && rep < reps - 1 { | ||
| out = Box::new(out.chain(std::iter::once(packed_barrier.clone()))); | ||
| } | ||
| out | ||
| }); | ||
|
|
||
| CircuitData::from_packed_operations(py, feature_dimension, 0, packed_insts, Param::Float(0.0)) | ||
| } | ||
|
|
||
| /// The default data_map_func for Pauli feature maps. For a parameter vector (x1, ..., xN), this | ||
| /// implements | ||
| /// (pi - x1) (pi - x2) ... (pi - xN) | ||
| /// unless there is only one parameter, in which case it returns just the value. | ||
| fn _default_reduce(py: Python, parameters: Vec<Param>) -> Param { | ||
| if parameters.len() == 1 { | ||
| parameters[0].clone() | ||
| } else { | ||
| let acc = parameters.iter().fold(Param::Float(1.0), |acc, param| { | ||
| rmultiply_param(acc, add_param(param, -PI, py), py) | ||
| }); | ||
| if parameters.len() % 2 == 0 { | ||
| acc | ||
| } else { | ||
| multiply_param(&acc, -1.0, py) // take care of parity | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Normalize the Pauli strings to a Vec<String>. We first define the default, which is | ||
| /// ["z", "zz"], unless we only have a single qubit, in which case we default to ["z"]. | ||
| /// Then, ``pauli_strings`` is either set to the default, or we try downcasting to a | ||
| /// PyString->String, followed by a check whether the feature dimension is large enough | ||
| /// for the Pauli (e.g. we cannot implement a "zzz" Pauli on a 2 qubit circuit). | ||
| fn _get_paulis( | ||
| feature_dimension: u32, | ||
| paulis: Option<&Bound<PySequence>>, | ||
| ) -> PyResult<Vec<String>> { | ||
| let default_pauli: Vec<String> = if feature_dimension == 1 { | ||
| vec!["z".to_string()] | ||
| } else { | ||
| vec!["z".to_string(), "zz".to_string()] | ||
| }; | ||
|
|
||
| paulis.map_or_else( | ||
| || Ok(default_pauli), // use Ok() since we might raise an error in the other arm | ||
| |v| { | ||
| let v = PySequenceMethods::to_list(v)?; // sequence to list | ||
| v.iter() // iterate over the list of Paulis | ||
| .map(|el| { | ||
| // Get the string and check whether it fits the feature dimension | ||
| let as_string = (*el.downcast::<PyString>()?).to_string(); | ||
| if as_string.len() > feature_dimension as usize { | ||
| Err(QiskitError::new_err(format!( | ||
| "feature_dimension ({}) smaller than the Pauli ({})", | ||
| feature_dimension, as_string | ||
| ))) | ||
| } else { | ||
| Ok(as_string) | ||
| } | ||
| }) | ||
| .collect::<PyResult<Vec<String>>>() | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| /// Get a barrier object from Python space. | ||
| fn _get_barrier(py: Python, feature_dimension: u32) -> PyResult<Instruction> { | ||
| let barrier_cls = imports::BARRIER.get_bound(py); | ||
| let barrier = barrier_cls.call1((feature_dimension,))?; | ||
| let barrier_inst = PyInstruction { | ||
| qubits: feature_dimension, | ||
| clbits: 0, | ||
| params: 0, | ||
| op_name: "barrier".to_string(), | ||
| control_flow: false, | ||
| instruction: barrier.into(), | ||
| }; | ||
| Ok(( | ||
| barrier_inst.into(), | ||
| smallvec![], | ||
| (0..feature_dimension).map(Qubit).collect(), | ||
| vec![] as Vec<Clbit>, | ||
| )) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.