diff --git a/crates/accelerate/src/circuit_library/blocks.rs b/crates/accelerate/src/circuit_library/blocks.rs new file mode 100644 index 000000000000..80add611abb1 --- /dev/null +++ b/crates/accelerate/src/circuit_library/blocks.rs @@ -0,0 +1,173 @@ +// 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::*, + types::{PyList, PyTuple}, +}; +use qiskit_circuit::{ + circuit_instruction::OperationFromPython, + operations::{Operation, Param, StandardGate}, + packed_instruction::PackedOperation, +}; +use smallvec::SmallVec; + +use crate::{circuit_library::entanglement::get_entanglement, QiskitError}; + +#[derive(Debug, Clone)] +pub enum BlockOperation { + Standard { gate: StandardGate }, + PyCustom { builder: Py }, +} + +impl BlockOperation { + pub fn assign_parameters( + &self, + py: Python, + params: &[&Param], + ) -> PyResult<(PackedOperation, SmallVec<[Param; 3]>)> { + match self { + Self::Standard { gate } => Ok(( + (*gate).into(), + SmallVec::from_iter(params.iter().map(|&p| p.clone())), + )), + Self::PyCustom { builder } => { + // the builder returns a Python operation plus the bound parameters + let py_params = + PyList::new_bound(py, params.iter().map(|&p| p.clone().into_py(py))).into_any(); + + let job = builder.call1(py, (py_params,))?; + let result = job.downcast_bound::(py)?; + + let operation: OperationFromPython = result.get_item(0)?.extract()?; + let bound_params = result + .get_item(1)? + .iter()? + .map(|ob| Param::extract_no_coerce(&ob?)) + .collect::>>()?; + + Ok((operation.operation, bound_params)) + } + } + } +} + +#[derive(Debug, Clone)] +#[pyclass] +pub struct Block { + pub operation: BlockOperation, + pub num_qubits: u32, + pub num_parameters: usize, +} + +#[pymethods] +impl Block { + #[staticmethod] + #[pyo3(signature = (gate,))] + pub fn from_standard_gate(gate: StandardGate) -> Self { + Block { + operation: BlockOperation::Standard { gate }, + num_qubits: gate.num_qubits(), + num_parameters: gate.num_params() as usize, + } + } + + #[staticmethod] + #[pyo3(signature = (num_qubits, num_parameters, builder,))] + pub fn from_callable( + py: Python, + num_qubits: i64, + num_parameters: i64, + builder: &Bound, + ) -> PyResult { + if !builder.is_callable() { + return Err(QiskitError::new_err( + "builder must be a callable: parameters->(bound gate, bound gate params)", + )); + } + let block = Block { + operation: BlockOperation::PyCustom { + builder: builder.to_object(py), + }, + num_qubits: num_qubits as u32, + num_parameters: num_parameters as usize, + }; + + Ok(block) + } +} + +// We introduce typedefs to make the types more legible. We can understand the hierarchy +// as follows: +// Connection: Vec -- indices that the multi-qubit gate acts on +// BlockEntanglement: Vec -- entanglement for single block +// LayerEntanglement: Vec -- entanglements for all blocks in the layer +// Entanglement: Vec -- entanglement for every layer +type BlockEntanglement = Vec>; +pub(super) type LayerEntanglement = Vec; + +/// Represent the entanglement in an n-local circuit. +pub struct Entanglement { + // Possible optimization in future: This eagerly expands the full entanglement for every layer. + // This could be done more efficiently, e.g., by creating entanglement objects that store + // their underlying representation (e.g. a string or a list of connections) and returning + // these when given a layer-index. + entanglement_vec: Vec, +} + +impl Entanglement { + /// Create an entanglement from the input of an n_local circuit. + pub fn from_py( + num_qubits: u32, + reps: usize, + entanglement: &Bound, + entanglement_blocks: &[&Block], + ) -> PyResult { + let entanglement_vec = (0..reps) + .map(|layer| -> PyResult { + if entanglement.is_callable() { + let as_any = entanglement.call1((layer,))?; + let as_list = as_any.downcast::()?; + unpack_entanglement(num_qubits, layer, as_list, entanglement_blocks) + } else { + let as_list = entanglement.downcast::()?; + unpack_entanglement(num_qubits, layer, as_list, entanglement_blocks) + } + }) + .collect::>()?; + + Ok(Self { entanglement_vec }) + } + + pub fn get_layer(&self, layer: usize) -> &LayerEntanglement { + &self.entanglement_vec[layer] + } + + pub fn iter(&self) -> impl Iterator { + self.entanglement_vec.iter() + } +} + +fn unpack_entanglement( + num_qubits: u32, + layer: usize, + entanglement: &Bound, + entanglement_blocks: &[&Block], +) -> PyResult { + entanglement_blocks + .iter() + .zip(entanglement.iter()) + .map(|(block, ent)| -> PyResult>> { + get_entanglement(num_qubits, block.num_qubits, &ent, layer)?.collect() + }) + .collect() +} diff --git a/crates/accelerate/src/circuit_library/entanglement.rs b/crates/accelerate/src/circuit_library/entanglement.rs index fbfb5c0193f1..2168414cc4b0 100644 --- a/crates/accelerate/src/circuit_library/entanglement.rs +++ b/crates/accelerate/src/circuit_library/entanglement.rs @@ -14,7 +14,7 @@ use itertools::Itertools; use pyo3::prelude::*; use pyo3::types::PyDict; use pyo3::{ - types::{PyAnyMethods, PyInt, PyList, PyListMethods, PyString, PyTuple}, + types::{PyAnyMethods, PyList, PyListMethods, PyString, PyTuple}, Bound, PyAny, PyResult, }; @@ -194,12 +194,7 @@ fn _check_entanglement_list<'a>( block_size: u32, ) -> PyResult>> + 'a>> { let entanglement_iter = list.iter().map(move |el| { - let connections = el - .downcast::()? - // .expect("Entanglement must be list of tuples") // clearer error message than `?` - .iter() - .map(|index| index.downcast::()?.extract()) - .collect::, _>>()?; + let connections: Vec = el.extract()?; if connections.len() != block_size as usize { return Err(QiskitError::new_err(format!( diff --git a/crates/accelerate/src/circuit_library/mod.rs b/crates/accelerate/src/circuit_library/mod.rs index d2e8755b2d90..2e087b442a9f 100644 --- a/crates/accelerate/src/circuit_library/mod.rs +++ b/crates/accelerate/src/circuit_library/mod.rs @@ -12,8 +12,11 @@ use pyo3::prelude::*; +mod blocks; mod entanglement; mod iqp; +mod multi_local; +mod parameter_ledger; mod pauli_evolution; mod pauli_feature_map; mod quantum_volume; @@ -25,5 +28,8 @@ pub fn circuit_library(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(iqp::py_iqp))?; m.add_wrapped(wrap_pyfunction!(iqp::py_random_iqp))?; m.add_wrapped(wrap_pyfunction!(quantum_volume::quantum_volume))?; + m.add_wrapped(wrap_pyfunction!(multi_local::py_n_local))?; + m.add_class::()?; + Ok(()) } diff --git a/crates/accelerate/src/circuit_library/multi_local.rs b/crates/accelerate/src/circuit_library/multi_local.rs new file mode 100644 index 000000000000..dd75ebbcc5e8 --- /dev/null +++ b/crates/accelerate/src/circuit_library/multi_local.rs @@ -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 std::ops::Deref; + +use hashbrown::HashSet; +use pyo3::prelude::*; +use pyo3::types::PyString; +use qiskit_circuit::packed_instruction::PackedOperation; +use smallvec::{smallvec, SmallVec}; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::operations::{Param, PyInstruction}; +use qiskit_circuit::{imports, Clbit, Qubit}; + +use itertools::izip; + +use super::blocks::{Block, Entanglement, LayerEntanglement}; +use super::parameter_ledger::{LayerParameters, LayerType, ParameterLedger}; + +type Instruction = ( + PackedOperation, + SmallVec<[Param; 3]>, + Vec, + Vec, +); + +/// Construct a rotation layer. +/// +/// # Arguments +/// +/// - `num_qubits`: The number of qubits in the circuit. +/// - `rotation_blocks`: A reference to a vector containing the instructions to insert. +/// This is a vector (sind we can have multiple rotations operations per layer), with +/// 3-tuple elements containing (packed_operation, num_qubits, num_params). +/// - `parameters`: The set of parameter objects to use for the operations. This is a 3x nested +/// vector, organized as operation -> block -> param. That means that for operation `i` +/// and block `j`, the parameters are given by `parameters[i][j]`. +/// - skipped_qubits: A hash-set containing which qubits are skipped in the rotation layer. +/// +/// # Returns +/// +/// An iterator for the rotation instructions. +fn rotation_layer<'a>( + py: Python<'a>, + num_qubits: u32, + rotation_blocks: &'a [&'a Block], + parameters: Vec>>, + skipped_qubits: &'a HashSet, +) -> impl Iterator> + 'a { + rotation_blocks + .iter() + .zip(parameters) + .flat_map(move |(block, block_params)| { + (0..num_qubits) + .step_by(block.num_qubits as usize) + .filter(move |start_idx| { + skipped_qubits.is_disjoint(&HashSet::from_iter( + *start_idx..(*start_idx + block.num_qubits), + )) + }) + .zip(block_params) + .map(move |(start_idx, params)| { + let (bound_op, bound_params) = block + .operation + .assign_parameters(py, ¶ms) + .expect("Failed to rebind"); + Ok(( + bound_op, + bound_params, + (0..block.num_qubits) + .map(|i| Qubit(start_idx + i)) + .collect(), + vec![] as Vec, + )) + }) + }) +} + +/// Construct an entanglement layer. +/// +/// # Arguments +/// +/// - `entanglement`: The entanglement structure in this layer. Given as 3x nested vector, which +/// for each entanglement block contains a vector of connections, where each connection +/// is a vector of indices. +/// - `entanglement_blocks`: A reference to a vector containing the instructions to insert. +/// This is a vector (sind we can have multiple entanglement operations per layer), with +/// 3-tuple elements containing (packed_operation, num_qubits, num_params). +/// - `parameters`: The set of parameter objects to use for the operations. This is a 3x nested +/// vector, organized as operation -> block -> param. That means that for operation `i` +/// and block `j`, the parameters are given by `parameters[i][j]`. +/// +/// # Returns +/// +/// An iterator for the entanglement instructions. +fn entanglement_layer<'a>( + py: Python<'a>, + entanglement: &'a LayerEntanglement, + entanglement_blocks: &'a [&'a Block], + parameters: LayerParameters<'a>, +) -> impl Iterator> + 'a { + let zipped = izip!(entanglement_blocks, parameters, entanglement); + zipped.flat_map(move |(block, block_params, block_entanglement)| { + block_entanglement + .iter() + .zip(block_params) + .map(move |(indices, params)| { + let (bound_op, bound_params) = block + .operation + .assign_parameters(py, ¶ms) + .expect("Failed to rebind"); + Ok(( + bound_op, + bound_params, + indices.iter().map(|i| Qubit(*i)).collect(), + vec![] as Vec, + )) + }) + }) +} + +/// # Arguments +/// +/// - `num_qubits`: The number of qubits of the circuit. +/// - `rotation_blocks`: The blocks used in the rotation layers. If multiple are passed, +/// these will be applied one after another (like new sub-layers). +/// - `entanglement_blocks`: The blocks used in the entanglement layers. If multiple are passed, +/// these will be applied one after another. +/// - `entanglement`: The indices specifying on which qubits the input blocks act. This is +/// specified by string describing an entanglement strategy (see the additional info) +/// or a list of qubit connections. +/// If a list of entanglement blocks is passed, different entanglement for each block can +/// be specified by passing a list of entanglements. To specify varying entanglement for +/// each repetition, pass a callable that takes as input the layer and returns the +/// entanglement for that layer. +/// Defaults to ``"full"``, meaning an all-to-all entanglement structure. +/// - `reps`: Specifies how often the rotation blocks and entanglement blocks are repeated. +/// - `insert_barriers`: If ``True``, barriers are inserted in between each layer. If ``False``, +/// no barriers are inserted. +/// - `parameter_prefix`: The prefix used if default parameters are generated. +/// - `skip_final_rotation_layer`: Whether a final rotation layer is added to the circuit. +/// - `skip_unentangled_qubits`: If ``True``, the rotation gates act only on qubits that +/// are entangled. If ``False``, the rotation gates act on all qubits. +/// +/// # Returns +/// +/// An N-local circuit. +#[allow(clippy::too_many_arguments)] +pub fn n_local( + py: Python, + num_qubits: u32, + rotation_blocks: &[&Block], + entanglement_blocks: &[&Block], + entanglement: &Entanglement, + reps: usize, + insert_barriers: bool, + parameter_prefix: &String, + skip_final_rotation_layer: bool, + skip_unentangled_qubits: bool, +) -> PyResult { + // Construct the parameter ledger, which will define all free parameters and provide + // access to them, given an index for a layer and the current gate to implement. + let ledger = ParameterLedger::from_nlocal( + py, + num_qubits, + reps, + entanglement, + rotation_blocks, + entanglement_blocks, + skip_final_rotation_layer, + parameter_prefix, + )?; + + // Compute the qubits that are skipped in the rotation layer. If this is set, + // we skip qubits that do not appear in any of the entanglement layers. + let skipped_qubits = if skip_unentangled_qubits { + let active: HashSet<&u32> = + HashSet::from_iter(entanglement.iter().flatten().flatten().flatten()); + HashSet::from_iter((0..num_qubits).filter(|i| !active.contains(i))) + } else { + HashSet::new() + }; + + // This struct can be used to yield barrier if insert_barriers is true, otherwise + // it returns an empty iterator. For conveniently injecting barriers in-between operations. + let maybe_barrier = MaybeBarrier::new(py, num_qubits, insert_barriers)?; + + let packed_insts = (0..reps).flat_map(|layer| { + rotation_layer( + py, + num_qubits, + rotation_blocks, + ledger.get_parameters(LayerType::Rotation, layer), + &skipped_qubits, + ) + .chain(maybe_barrier.get()) + .chain(entanglement_layer( + py, + entanglement.get_layer(layer), + entanglement_blocks, + ledger.get_parameters(LayerType::Entangle, layer), + )) + .chain(maybe_barrier.get()) + }); + if !skip_final_rotation_layer { + let packed_insts = packed_insts.chain(rotation_layer( + py, + num_qubits, + rotation_blocks, + ledger.get_parameters(LayerType::Rotation, reps), + &skipped_qubits, + )); + CircuitData::from_packed_operations(py, num_qubits, 0, packed_insts, Param::Float(0.0)) + } else { + CircuitData::from_packed_operations(py, num_qubits, 0, packed_insts, Param::Float(0.0)) + } +} + +#[pyfunction] +#[pyo3(signature = (num_qubits, rotation_blocks, entanglement_blocks, entanglement, reps, insert_barriers, parameter_prefix, skip_final_rotation_layer, skip_unentangled_qubits))] +#[allow(clippy::too_many_arguments)] +pub fn py_n_local( + py: Python, + num_qubits: u32, + rotation_blocks: Vec>, + entanglement_blocks: Vec>, + entanglement: &Bound, + reps: usize, + insert_barriers: bool, + parameter_prefix: &Bound, + skip_final_rotation_layer: bool, + skip_unentangled_qubits: bool, +) -> PyResult { + // Normalize the Python data. + let parameter_prefix = parameter_prefix.to_string(); + let rotation_blocks: Vec<&Block> = rotation_blocks + .iter() + .map(|py_block| py_block.deref()) + .collect(); + let entanglement_blocks: Vec<&Block> = entanglement_blocks + .iter() + .map(|py_block| py_block.deref()) + .collect(); + + // Expand the entanglement. This will (currently) eagerly expand the entanglement for each + // circuit layer. + let entanglement = Entanglement::from_py(num_qubits, reps, entanglement, &entanglement_blocks)?; + + n_local( + py, + num_qubits, + &rotation_blocks, + &entanglement_blocks, + &entanglement, + reps, + insert_barriers, + ¶meter_prefix, + skip_final_rotation_layer, + skip_unentangled_qubits, + ) +} + +/// A convenient struct to optionally yield barriers to inject in-between circuit layers. +/// +/// If constructed with ``insert_barriers=false``, then the method ``.get`` yields empty iterators, +/// otherwise it will yield a barrier. This is a struct such that the call to Python that +/// creates the Barrier object can be done a single time, but barriers can be yielded multiple times. +struct MaybeBarrier { + barrier: Option, +} + +impl MaybeBarrier { + fn new(py: Python, num_qubits: u32, insert_barriers: bool) -> PyResult { + if !insert_barriers { + Ok(Self { barrier: None }) + } else { + let barrier_cls = imports::BARRIER.get_bound(py); + let py_barrier = barrier_cls.call1((num_qubits,))?; + let py_inst = PyInstruction { + qubits: num_qubits, + clbits: 0, + params: 0, + op_name: "barrier".to_string(), + control_flow: false, + instruction: py_barrier.into(), + }; + + let inst = ( + py_inst.into(), + smallvec![], + (0..num_qubits).map(Qubit).collect(), + vec![] as Vec, + ); + + Ok(Self { + barrier: Some(inst), + }) + } + } + + fn get(&self) -> Box>> { + match &self.barrier { + None => Box::new(std::iter::empty()), + Some(inst) => Box::new(std::iter::once(Ok(inst.clone()))), + } + } +} diff --git a/crates/accelerate/src/circuit_library/parameter_ledger.rs b/crates/accelerate/src/circuit_library/parameter_ledger.rs new file mode 100644 index 000000000000..457034850196 --- /dev/null +++ b/crates/accelerate/src/circuit_library/parameter_ledger.rs @@ -0,0 +1,174 @@ +// 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 qiskit_circuit::{imports, operations::Param}; + +use super::blocks::{Block, Entanglement}; + +/// Enum to determine the type of circuit layer. +pub(super) enum LayerType { + Rotation, + Entangle, +} + +type BlockParameters<'a> = Vec>; // parameters for each gate in the block +pub(super) type LayerParameters<'a> = Vec>; // parameter in a layer + +/// The ParameterLedger stores the parameter objects contained in the n-local circuit. +/// +/// Internally, the parameters are stored in a 1-D vector and the ledger keeps track of +/// which indices belong to which layer. For example, a 2-qubit circuit where both the +/// rotation and entanglement layer have 1 block with 2 parameters each, we would store +/// +/// [x0 x1 x2 x3 x4 x5 x6 x7 ....] +/// ----- ----- ----- ----- +/// rep0 rep0 rep1 rep2 +/// rot ent rot ent +/// +/// This allows accessing the parameters by index of the rotation or entanglement layer by means +/// of the ``get_parameters`` method, e.g. as +/// +/// let layer: usize = 4; +/// let params_in_that_layer: LayerParameters = +/// ledger.get_parameter(LayerType::Rotation, layer); +/// +pub(super) struct ParameterLedger { + parameter_vector: Vec, // all parameters + rotation_indices: Vec, // indices where rotation blocks start + entangle_indices: Vec, + rotations: Vec<(u32, usize)>, // (#blocks per layer, #params per block) for each block + entanglements: Vec>, // this might additionally change per layer +} + +impl ParameterLedger { + /// Initialize the ledger n-local input data. This will call Python to create a new + /// ``ParameterVector`` of adequate size and compute all required indices to access + /// parameter of a specific layer. + #[allow(clippy::too_many_arguments)] + pub(super) fn from_nlocal( + py: Python, + num_qubits: u32, + reps: usize, + entanglement: &Entanglement, + rotation_blocks: &[&Block], + entanglement_blocks: &[&Block], + skip_final_rotation_layer: bool, + parameter_prefix: &String, + ) -> PyResult { + // if we keep the final layer (i.e. skip=false), add parameters on the final layer + let final_layer_rep = match skip_final_rotation_layer { + true => 0, + false => 1, + }; + + // compute the number of parameters used for the rotation layers + let mut num_rotation_params_per_layer: usize = 0; + let mut rotations: Vec<(u32, usize)> = Vec::new(); + + for block in rotation_blocks { + let num_blocks = num_qubits / block.num_qubits; + rotations.push((num_blocks, block.num_parameters)); + num_rotation_params_per_layer += (num_blocks as usize) * block.num_parameters; + } + + // compute the number of parameters used for the entanglement layers + let mut num_entangle_params_per_layer: Vec = Vec::with_capacity(reps); + let mut entanglements: Vec> = Vec::with_capacity(reps); + for this_entanglement in entanglement.iter() { + let mut this_entanglements: Vec<(u32, usize)> = Vec::new(); + let mut this_num_params: usize = 0; + for (block, block_entanglement) in entanglement_blocks.iter().zip(this_entanglement) { + let num_blocks = block_entanglement.len(); + this_num_params += num_blocks * block.num_parameters; + this_entanglements.push((num_blocks as u32, block.num_parameters)); + } + num_entangle_params_per_layer.push(this_num_params); + entanglements.push(this_entanglements); + } + + let num_rotation_params: usize = (reps + final_layer_rep) * num_rotation_params_per_layer; + let num_entangle_params: usize = num_entangle_params_per_layer.iter().sum(); + + // generate a ParameterVector Python-side, containing all parameters, and then + // map it onto Rust-space parameters + let num_parameters = num_rotation_params + num_entangle_params; + let parameter_vector: Vec = imports::PARAMETER_VECTOR + .get_bound(py) + .call1((parameter_prefix, num_parameters))? // get the Python ParameterVector + .iter()? // iterate over the elements and cast them to Rust Params + .map(|ob| Param::extract_no_coerce(&ob?)) + .collect::>()?; + + // finally, distribute the parameters onto the repetitions and blocks for each + // rotation layer and entanglement layer + let mut rotation_indices: Vec = Vec::with_capacity(reps + final_layer_rep); + let mut entangle_indices: Vec = Vec::with_capacity(reps); + let mut index: usize = 0; + for num_entangle in num_entangle_params_per_layer { + rotation_indices.push(index); + index += num_rotation_params_per_layer; + entangle_indices.push(index); + index += num_entangle; + } + if !skip_final_rotation_layer { + rotation_indices.push(index); + } + + Ok(ParameterLedger { + parameter_vector, + rotation_indices, + entangle_indices, + rotations, + entanglements, + }) + } + + /// Get the parameters in the rotation or entanglement layer. + pub(super) fn get_parameters(&self, kind: LayerType, layer: usize) -> LayerParameters { + let (mut index, blocks) = match kind { + LayerType::Rotation => ( + *self + .rotation_indices + .get(layer) + .expect("Out of bounds in rotation_indices."), + &self.rotations, + ), + LayerType::Entangle => ( + *self + .entangle_indices + .get(layer) + .expect("Out of bounds in entangle_indices."), + &self.entanglements[layer], + ), + }; + + let mut parameters: LayerParameters = Vec::new(); + for (num_blocks, num_params) in blocks { + let mut per_block: BlockParameters = Vec::new(); + for _ in 0..*num_blocks { + let gate_params: Vec<&Param> = (index..index + num_params) + .map(|i| { + self.parameter_vector + .get(i) + .expect("Ran out of parameters!") + }) + .collect(); + index += num_params; + per_block.push(gate_params); + } + parameters.push(per_block); + } + + parameters + } +} diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index 8ca693d15d3f..d477f9614c54 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -364,6 +364,11 @@ :toctree: ../stubs/ :template: autosummary/class_no_inherited_members.rst + n_local + efficient_su2 + real_amplitudes + pauli_two_design + excitation_preserving NLocal TwoLocal PauliTwoDesign @@ -582,12 +587,17 @@ ) from .n_local import ( + n_local, NLocal, TwoLocal, + pauli_two_design, PauliTwoDesign, + real_amplitudes, RealAmplitudes, + efficient_su2, EfficientSU2, EvolvedOperatorAnsatz, + excitation_preserving, ExcitationPreserving, QAOAAnsatz, ) diff --git a/qiskit/circuit/library/data_preparation/pauli_feature_map.py b/qiskit/circuit/library/data_preparation/pauli_feature_map.py index 1bc14d169273..b511edd9513e 100644 --- a/qiskit/circuit/library/data_preparation/pauli_feature_map.py +++ b/qiskit/circuit/library/data_preparation/pauli_feature_map.py @@ -160,9 +160,9 @@ def pauli_feature_map( data_map_func=data_map_func, alpha=alpha, insert_barriers=insert_barriers, - ) + ), + name=name, ) - circuit.name = name return circuit diff --git a/qiskit/circuit/library/n_local/__init__.py b/qiskit/circuit/library/n_local/__init__.py index 4a238dcedbb8..c0170600a391 100644 --- a/qiskit/circuit/library/n_local/__init__.py +++ b/qiskit/circuit/library/n_local/__init__.py @@ -12,22 +12,27 @@ """The circuit library module containing N-local circuits.""" -from .n_local import NLocal +from .n_local import NLocal, n_local from .two_local import TwoLocal -from .pauli_two_design import PauliTwoDesign -from .real_amplitudes import RealAmplitudes -from .efficient_su2 import EfficientSU2 +from .pauli_two_design import PauliTwoDesign, pauli_two_design +from .real_amplitudes import RealAmplitudes, real_amplitudes +from .efficient_su2 import EfficientSU2, efficient_su2 from .evolved_operator_ansatz import EvolvedOperatorAnsatz -from .excitation_preserving import ExcitationPreserving +from .excitation_preserving import ExcitationPreserving, excitation_preserving from .qaoa_ansatz import QAOAAnsatz __all__ = [ + "n_local", "NLocal", "TwoLocal", + "real_amplitudes", "RealAmplitudes", + "pauli_two_design", "PauliTwoDesign", + "efficient_su2", "EfficientSU2", "EvolvedOperatorAnsatz", + "excitation_preserving", "ExcitationPreserving", "QAOAAnsatz", ] diff --git a/qiskit/circuit/library/n_local/efficient_su2.py b/qiskit/circuit/library/n_local/efficient_su2.py index 69398dca1e90..53698a3e18a6 100644 --- a/qiskit/circuit/library/n_local/efficient_su2.py +++ b/qiskit/circuit/library/n_local/efficient_su2.py @@ -14,18 +14,121 @@ from __future__ import annotations import typing -from collections.abc import Callable +from collections.abc import Callable, Iterable from numpy import pi -from qiskit.circuit import QuantumCircuit +from qiskit.circuit import QuantumCircuit, Gate from qiskit.circuit.library.standard_gates import RYGate, RZGate, CXGate +from qiskit.utils.deprecation import deprecate_func +from .n_local import n_local, BlockEntanglement from .two_local import TwoLocal if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import +def efficient_su2( + num_qubits: int, + su2_gates: str | Gate | Iterable[str | Gate] | None = None, + entanglement: ( + BlockEntanglement + | Iterable[BlockEntanglement] + | Callable[[int], BlockEntanglement | Iterable[BlockEntanglement]] + ) = "reverse_linear", + reps: int = 3, + skip_unentangled_qubits: bool = False, + skip_final_rotation_layer: bool = False, + parameter_prefix: str = "θ", + insert_barriers: bool = False, + name: str = "EfficientSU2", +): + r"""The hardware-efficient :math:`SU(2)` 2-local circuit. + + The ``efficient_su2`` circuit consists of layers of single qubit operations spanned by + :math:`SU(2)` and CX entanglements. This is a heuristic pattern that can be used to prepare trial + wave functions for variational quantum algorithms or classification circuit for machine learning. + + :math:`SU(2)` is the special unitary group of degree 2, its elements are :math:`2 \times 2` + unitary matrices with determinant 1, such as the Pauli rotation gates. + + On 3 qubits and using the Pauli :math:`Y` and :math:`Z` rotations as single qubit gates, the + this circuit is represented by: + + .. parsed-literal:: + + ┌──────────┐┌──────────┐ ░ ░ ░ ┌───────────┐┌───────────┐ + ┤ RY(θ[0]) ├┤ RZ(θ[3]) ├─░────────■───░─ ... ─░─┤ RY(θ[12]) ├┤ RZ(θ[15]) ├ + ├──────────┤├──────────┤ ░ ┌─┴─┐ ░ ░ ├───────────┤├───────────┤ + ┤ RY(θ[1]) ├┤ RZ(θ[4]) ├─░───■──┤ X ├─░─ ... ─░─┤ RY(θ[13]) ├┤ RZ(θ[16]) ├ + ├──────────┤├──────────┤ ░ ┌─┴─┐└───┘ ░ ░ ├───────────┤├───────────┤ + ┤ RY(θ[2]) ├┤ RZ(θ[5]) ├─░─┤ X ├──────░─ ... ─░─┤ RY(θ[14]) ├┤ RZ(θ[17]) ├ + └──────────┘└──────────┘ ░ └───┘ ░ ░ └───────────┘└───────────┘ + + Examples: + + Per default, the ``"reverse_linear"`` entanglement is used, which, in the case of + CX gates, is equivalent to an all-to-all entanglement: + + .. plot:: + :include-source: + :context: + + from qiskit.circuit.library import efficient_su2 + + circuit = efficient_su2(3, reps=1) + circuit.draw("mpl") + + To specify which SU(2) gates should be used in the rotation layer, we can set the + ``su2_gates`` argument. In addition, we can change the entanglement structure. + For example: + + .. plot:: + :include-source: + :context: + + circuit = efficient_su2(4, su2_gates=["rx", "y"], entanglement="circular", reps=1) + circuit.draw("mpl") + + Args: + num_qubits: The number of qubits. + su2_gates: The :math:`SU(2)` single qubit gates to apply in single qubit gate layers. + If only one gate is provided, the same gate is applied to each qubit. + If a list of gates is provided, all gates are applied to each qubit in the provided + order. + reps: Specifies how often the structure of a rotation layer followed by an entanglement + layer is repeated. + entanglement: The indices specifying on which qubits the input blocks act. + See :func:`.n_local` for detailed information. + skip_final_rotation_layer: Whether a final rotation layer is added to the circuit. + skip_unentangled_qubits: If ``True``, the rotation gates act only on qubits that + are entangled. If ``False``, the rotation gates act on all qubits. + parameter_prefix: The name of the free parameters. + insert_barriers: If True, barriers are inserted in between each layer. If False, + no barriers are inserted. + name: The name of the circuit. + + Returns: + An efficient-SU(2) circuit. + """ + if su2_gates is None: + su2_gates = ["ry", "rz"] + + return n_local( + num_qubits, + su2_gates, + ["cx"], + entanglement, + reps, + insert_barriers, + parameter_prefix, + True, + skip_final_rotation_layer, + skip_unentangled_qubits, + name, + ) + + class EfficientSU2(TwoLocal): r"""The hardware efficient SU(2) 2-local circuit. @@ -55,7 +158,7 @@ class EfficientSU2(TwoLocal): Examples: >>> circuit = EfficientSU2(3, reps=1) - >>> print(circuit) + >>> print(circuit.decompose()) ┌──────────┐┌──────────┐ ┌──────────┐┌──────────┐ q_0: ┤ RY(θ[0]) ├┤ RZ(θ[3]) ├──■────■──┤ RY(θ[6]) ├┤ RZ(θ[9]) ├───────────── ├──────────┤├──────────┤┌─┴─┐ │ └──────────┘├──────────┤┌───────────┐ @@ -64,7 +167,8 @@ class EfficientSU2(TwoLocal): q_2: ┤ RY(θ[2]) ├┤ RZ(θ[5]) ├─────┤ X ├───┤ X ├────┤ RY(θ[8]) ├┤ RZ(θ[11]) ├ └──────────┘└──────────┘ └───┘ └───┘ └──────────┘└───────────┘ - >>> ansatz = EfficientSU2(4, su2_gates=['rx', 'y'], entanglement='circular', reps=1) + >>> ansatz = EfficientSU2(4, su2_gates=['rx', 'y'], entanglement='circular', reps=1, + ... flatten=True) >>> qc = QuantumCircuit(4) # create a circuit and append the RY variational form >>> qc.compose(ansatz, inplace=True) >>> qc.draw() @@ -78,8 +182,17 @@ class EfficientSU2(TwoLocal): q_3: ┤ RX(θ[3]) ├┤ Y ├──■──────────────────────┤ X ├────┤ RX(θ[7]) ├┤ Y ├ └──────────┘└───┘ └───┘ └──────────┘└───┘ + .. seealso:: + + The :func:`.efficient_su2` function constructs a functionally equivalent circuit, but faster. + """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.efficient_su2 instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, diff --git a/qiskit/circuit/library/n_local/excitation_preserving.py b/qiskit/circuit/library/n_local/excitation_preserving.py index 4a3e7445fa10..49bfdb07f017 100644 --- a/qiskit/circuit/library/n_local/excitation_preserving.py +++ b/qiskit/circuit/library/n_local/excitation_preserving.py @@ -13,14 +13,127 @@ """The ExcitationPreserving 2-local circuit.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Iterable from numpy import pi from qiskit.circuit import QuantumCircuit, Parameter from qiskit.circuit.library.standard_gates import RZGate +from qiskit.utils.deprecation import deprecate_func +from .n_local import n_local, BlockEntanglement from .two_local import TwoLocal +def excitation_preserving( + num_qubits: int, + mode: str = "iswap", + entanglement: ( + BlockEntanglement + | Iterable[BlockEntanglement] + | Callable[[int], BlockEntanglement | Iterable[BlockEntanglement]] + ) = "full", + reps: int = 3, + skip_unentangled_qubits: bool = False, + skip_final_rotation_layer: bool = False, + parameter_prefix: str = "θ", + insert_barriers: bool = False, + name: str = "ExcitationPreserving", +): + r"""The heuristic excitation-preserving wave function ansatz. + + The ``excitation_preserving`` circuit preserves the ratio of :math:`|00\rangle`, + :math:`|01\rangle + |10\rangle` and :math:`|11\rangle` states. To this end, this circuit + uses two-qubit interactions of the form + + .. math:: + + \newcommand{\rotationangle}{\theta/2} + + \begin{pmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \cos\left(\rotationangle\right) & -i\sin\left(\rotationangle\right) & 0 \\ + 0 & -i\sin\left(\rotationangle\right) & \cos\left(\rotationangle\right) & 0 \\ + 0 & 0 & 0 & e^{-i\phi} + \end{pmatrix} + + for the mode ``"fsim"`` or with :math:`e^{-i\phi} = 1` for the mode ``"iswap"``. + + Note that other wave functions, such as UCC-ansatzes, are also excitation preserving. + However these can become complex quickly, while this heuristically motivated circuit follows + a simpler pattern. + + This trial wave function consists of layers of :math:`Z` rotations with 2-qubit entanglements. + The entangling is creating using :math:`XX+YY` rotations and optionally a controlled-phase + gate for the mode ``"fsim"``. + + Examples: + + With linear entanglement, this circuit is given by: + + .. plot:: + :include-source: + :context: + + from qiskit.circuit.library import excitation_preserving + + ansatz = excitation_preserving(3, reps=1, insert_barriers=True, entanglement="linear") + ansatz.draw("mpl") + + The entanglement structure can be explicitly specified with the ``entanglement`` + argument. The ``"fsim"`` mode includes an additional parameterized :class:`.CPhaseGate` + in each block: + + .. plot:: + :include-source: + :context: + + ansatz = excitation_preserving(3, reps=1, mode="fsim", entanglement=[[0, 2]]) + ansatz.draw("mpl") + + Args: + num_qubits: The number of qubits. + mode: Choose the entangler mode, can be `"iswap"` or `"fsim"`. + reps: Specifies how often the structure of a rotation layer followed by an entanglement + layer is repeated. + entanglement: The indices specifying on which qubits the input blocks act. + See :func:`.n_local` for detailed information. + skip_final_rotation_layer: Whether a final rotation layer is added to the circuit. + skip_unentangled_qubits: If ``True``, the rotation gates act only on qubits that + are entangled. If ``False``, the rotation gates act on all qubits. + parameter_prefix: The name of the free parameters. + insert_barriers: If True, barriers are inserted in between each layer. If False, + no barriers are inserted. + name: The name of the circuit. + + Returns: + An excitation-preserving circuit. + """ + supported_modes = ["iswap", "fsim"] + if mode not in supported_modes: + raise ValueError(f"Unsupported mode {mode}, choose one of {supported_modes}") + + theta = Parameter("θ") + swap = QuantumCircuit(2, name="Interaction") + swap.rxx(theta, 0, 1) + swap.ryy(theta, 0, 1) + if mode == "fsim": + phi = Parameter("φ") + swap.cp(phi, 0, 1) + + return n_local( + num_qubits, + ["rz"], + [swap.to_gate()], + entanglement, + reps, + insert_barriers, + parameter_prefix, + True, + skip_final_rotation_layer, + skip_unentangled_qubits, + name, + ) + + class ExcitationPreserving(TwoLocal): r"""The heuristic excitation-preserving wave function ansatz. @@ -57,7 +170,7 @@ class ExcitationPreserving(TwoLocal): Examples: >>> ansatz = ExcitationPreserving(3, reps=1, insert_barriers=True, entanglement='linear') - >>> print(ansatz) # show the circuit + >>> print(ansatz.decompose()) # show the circuit ┌──────────┐ ░ ┌────────────┐┌────────────┐ ░ ┌──────────┐ q_0: ┤ RZ(θ[0]) ├─░─┤0 ├┤0 ├─────────────────────────────░─┤ RZ(θ[5]) ├ ├──────────┤ ░ │ RXX(θ[3]) ││ RYY(θ[3]) │┌────────────┐┌────────────┐ ░ ├──────────┤ @@ -66,10 +179,10 @@ class ExcitationPreserving(TwoLocal): q_2: ┤ RZ(θ[2]) ├─░─────────────────────────────┤1 ├┤1 ├─░─┤ RZ(θ[7]) ├ └──────────┘ ░ └────────────┘└────────────┘ ░ └──────────┘ - >>> ansatz = ExcitationPreserving(2, reps=1) + >>> ansatz = ExcitationPreserving(2, reps=1, flatten=True) >>> qc = QuantumCircuit(2) # create a circuit and append the RY variational form >>> qc.cry(0.2, 0, 1) # do some previous operation - >>> qc.compose(ansatz, inplace=True) # add the swaprz + >>> qc.compose(ansatz, inplace=True) # add the excitation-preserving >>> qc.draw() ┌──────────┐┌────────────┐┌────────────┐┌──────────┐ q_0: ─────■─────┤ RZ(θ[0]) ├┤0 ├┤0 ├┤ RZ(θ[3]) ├ @@ -78,8 +191,8 @@ class ExcitationPreserving(TwoLocal): └─────────┘└──────────┘└────────────┘└────────────┘└──────────┘ >>> ansatz = ExcitationPreserving(3, reps=1, mode='fsim', entanglement=[[0,2]], - ... insert_barriers=True) - >>> print(ansatz) + ... insert_barriers=True, flatten=True) + >>> print(ansatz.decompose()) ┌──────────┐ ░ ┌────────────┐┌────────────┐ ░ ┌──────────┐ q_0: ┤ RZ(θ[0]) ├─░─┤0 ├┤0 ├─■──────░─┤ RZ(θ[5]) ├ ├──────────┤ ░ │ ││ │ │ ░ ├──────────┤ @@ -87,8 +200,19 @@ class ExcitationPreserving(TwoLocal): ├──────────┤ ░ │ ││ │ │θ[4] ░ ├──────────┤ q_2: ┤ RZ(θ[2]) ├─░─┤1 ├┤1 ├─■──────░─┤ RZ(θ[7]) ├ └──────────┘ ░ └────────────┘└────────────┘ ░ └──────────┘ + + .. seealso:: + + The :func:`.excitation_preserving` function constructs a functionally equivalent circuit, + but faster. + """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.excitation_preserving instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index 9eb79d367f3b..8c0b4d285086 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -17,21 +17,28 @@ import collections import itertools import typing -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Callable, Mapping, Sequence, Iterable import numpy -from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.gate import Gate +from qiskit.circuit.quantumcircuit import QuantumCircuit, ParameterValueType +from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit import ( Instruction, Parameter, - ParameterVector, ParameterExpression, CircuitInstruction, ) from qiskit.exceptions import QiskitError from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping -from qiskit._accelerate.circuit_library import get_entangler_map as fast_entangler_map +from qiskit.utils.deprecation import deprecate_func + +from qiskit._accelerate.circuit_library import ( + Block, + py_n_local, + get_entangler_map as fast_entangler_map, +) from ..blueprintcircuit import BlueprintCircuit @@ -39,6 +46,219 @@ if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import +# entanglement for an individual block, e.g. if the block is CXGate() and we have +# 3 qubits, this could be [(0, 1), (1, 2), (2, 0)] +BlockEntanglement = typing.Union[str, Iterable[Iterable[int]]] + + +def n_local( + num_qubits: int, + rotation_blocks: str | Gate | Iterable[str | Gate], + entanglement_blocks: str | Gate | Iterable[str | Gate], + entanglement: ( + BlockEntanglement + | Iterable[BlockEntanglement] + | Callable[[int], BlockEntanglement | Iterable[BlockEntanglement]] + ) = "full", + reps: int = 3, + insert_barriers: bool = False, + parameter_prefix: str = "θ", + overwrite_block_parameters: bool = True, + skip_final_rotation_layer: bool = False, + skip_unentangled_qubits: bool = False, + name: str | None = "nlocal", +) -> QuantumCircuit: + r"""Construct an n-local variational circuit. + + The structure of the n-local circuit are alternating rotation and entanglement layers. + In both layers, parameterized circuit-blocks act on the circuit in a defined way. + In the rotation layer, the blocks are applied stacked on top of each other, while in the + entanglement layer according to the ``entanglement`` strategy. + The circuit blocks can have arbitrary sizes (smaller equal to the number of qubits in the + circuit). Each layer is repeated ``reps`` times, and by default a final rotation layer is + appended. + + For instance, a rotation block on 2 qubits and an entanglement block on 4 qubits using + ``"linear"`` entanglement yields the following circuit. + + .. parsed-literal:: + + ┌──────┐ ░ ┌──────┐ ░ ┌──────┐ + ┤0 ├─░─┤0 ├──────────────── ... ─░─┤0 ├ + │ Rot │ ░ │ │┌──────┐ ░ │ Rot │ + ┤1 ├─░─┤1 ├┤0 ├──────── ... ─░─┤1 ├ + ├──────┤ ░ │ Ent ││ │┌──────┐ ░ ├──────┤ + ┤0 ├─░─┤2 ├┤1 ├┤0 ├ ... ─░─┤0 ├ + │ Rot │ ░ │ ││ Ent ││ │ ░ │ Rot │ + ┤1 ├─░─┤3 ├┤2 ├┤1 ├ ... ─░─┤1 ├ + ├──────┤ ░ └──────┘│ ││ Ent │ ░ ├──────┤ + ┤0 ├─░─────────┤3 ├┤2 ├ ... ─░─┤0 ├ + │ Rot │ ░ └──────┘│ │ ░ │ Rot │ + ┤1 ├─░─────────────────┤3 ├ ... ─░─┤1 ├ + └──────┘ ░ └──────┘ ░ └──────┘ + + | | + +---------------------------------+ + repeated reps times + + Entanglement: + + The entanglement describes the connections of the gates in the entanglement layer. + For a two-qubit gate for example, the entanglement contains pairs of qubits on which the + gate should acts, e.g. ``[[ctrl0, target0], [ctrl1, target1], ...]``. + A set of default entanglement strategies is provided and can be selected by name: + + * ``"full"`` entanglement is each qubit is entangled with all the others. + * ``"linear"`` entanglement is qubit :math:`i` entangled with qubit :math:`i + 1`, + for all :math:`i \in \{0, 1, ... , n - 2\}`, where :math:`n` is the total number of qubits. + * ``"reverse_linear"`` entanglement is qubit :math:`i` entangled with qubit :math:`i + 1`, + for all :math:`i \in \{n-2, n-3, ... , 1, 0\}`, where :math:`n` is the total number of qubits. + Note that if ``entanglement_blocks=="cx"`` then this option provides the same unitary as + ``"full"`` with fewer entangling gates. + * ``"pairwise"`` entanglement is one layer where qubit :math:`i` is entangled with qubit + :math:`i + 1`, for all even values of :math:`i`, and then a second layer where qubit :math:`i` + is entangled with qubit :math:`i + 1`, for all odd values of :math:`i`. + * ``"circular"`` entanglement is linear entanglement but with an additional entanglement of the + first and last qubit before the linear part. + * ``"sca"`` (shifted-circular-alternating) entanglement is a generalized and modified version + of the proposed circuit 14 in `Sim et al. `__. + It consists of circular entanglement where the "long" entanglement connecting the first with + the last qubit is shifted by one each block. Furthermore the role of control and target + qubits are swapped every block (therefore alternating). + + If an entanglement layer contains multiple blocks, then the entanglement should be + given as list of entanglements for each block. For example:: + + entanglement_blocks = ["rxx", "ryy"] + entanglement = ["full", "linear"] # full for rxx and linear for ryy + + or:: + + structure_rxx = [[0, 1], [2, 3]] + structure_ryy = [[0, 2]] + entanglement = [structure_rxx, structure_ryy] + + Finally, the entanglement can vary in each repetition of the circuit. For this, we + support passing a callable that takes as input the layer index and returns the entanglement + for the layer in the above format. See the examples below for a concrete example. + + Examples: + + The rotation and entanglement gates can be specified via single strings, if they + are made up of a single block per layer: + + .. plot:: + :include-source: + :context: + + from qiskit.circuit.library import n_local + + circuit = n_local(3, "ry", "cx", "linear", reps=2, insert_barriers=True) + circuit.draw("mpl") + + Multiple gates per layer can be set by passing a list. Here, for example, we use + Pauli-Y and Pauli-Z rotations in the rotation layer: + + .. plot:: + :include-source: + :context: + + circuit = n_local(3, ["ry", "rz"], "cz", "full", reps=1, insert_barriers=True) + circuit.draw("mpl") + + To omit rotation or entanglement layers, the block can be set to an empty list: + + .. plot:: + :include-source: + :context: + + circuit = n_local(4, [], "cry", reps=2) + circuit.draw("mpl") + + The entanglement can be set explicitly via the ``entanglement`` argument: + + .. plot:: + :include-source: + :context: + + entangler_map = [[0, 1], [2, 0]] + circuit = n_local(3, "x", "crx", entangler_map, reps=2) + circuit.draw("mpl") + + We can set different entanglements per layer, by specifing a callable that takes + as input the current layer index, and returns the entanglement structure. For example, + the following uses different entanglements for odd and even layers: + + .. plot: + :include-source: + :context: + + def entanglement(layer_index): + if layer_index % 2 == 0: + return [[0, 1], [0, 2]] + return [[1, 2]] + + circuit = n_local(3, "x", "cx", entanglement, reps=3, insert_barriers=True) + circuit.draw("mpl") + + + Args: + num_qubits: The number of qubits of the circuit. + rotation_blocks: The blocks used in the rotation layers. If multiple are passed, + these will be applied one after another (like new sub-layers). + entanglement_blocks: The blocks used in the entanglement layers. If multiple are passed, + these will be applied one after another. + entanglement: The indices specifying on which qubits the input blocks act. This is + specified by string describing an entanglement strategy (see the additional info) + or a list of qubit connections. + If a list of entanglement blocks is passed, different entanglement for each block can + be specified by passing a list of entanglements. To specify varying entanglement for + each repetition, pass a callable that takes as input the layer and returns the + entanglement for that layer. + Defaults to ``"full"``, meaning an all-to-all entanglement structure. + reps: Specifies how often the rotation blocks and entanglement blocks are repeated. + insert_barriers: If ``True``, barriers are inserted in between each layer. If ``False``, + no barriers are inserted. + parameter_prefix: The prefix used if default parameters are generated. + overwrite_block_parameters: If the parameters in the added blocks should be overwritten. + If ``False``, the parameters in the blocks are not changed. + skip_final_rotation_layer: Whether a final rotation layer is added to the circuit. + skip_unentangled_qubits: If ``True``, the rotation gates act only on qubits that + are entangled. If ``False``, the rotation gates act on all qubits. + name: The name of the circuit. + + Returns: + An n-local circuit. + """ + if reps < 0: + # this is an important check, since we cast this to an unsigned integer Rust-side + raise ValueError(f"reps must be non-negative, but is {reps}") + + supported_gates = get_standard_gate_name_mapping() + rotation_blocks = _normalize_blocks( + rotation_blocks, supported_gates, overwrite_block_parameters + ) + entanglement_blocks = _normalize_blocks( + entanglement_blocks, supported_gates, overwrite_block_parameters + ) + + entanglement = _normalize_entanglement(entanglement, len(entanglement_blocks)) + + data = py_n_local( + num_qubits=num_qubits, + rotation_blocks=rotation_blocks, + entanglement_blocks=entanglement_blocks, + entanglement=entanglement, + reps=reps, + insert_barriers=insert_barriers, + parameter_prefix=parameter_prefix, + skip_final_rotation_layer=skip_final_rotation_layer, + skip_unentangled_qubits=skip_unentangled_qubits, + ) + circuit = QuantumCircuit._from_circuit_data(data, add_regs=True, name=name) + + return circuit + class NLocal(BlueprintCircuit): """The n-local circuit class. @@ -76,8 +296,18 @@ class NLocal(BlueprintCircuit): If specified, barriers can be inserted in between every block. If an initial state object is provided, it is added in front of the NLocal. + + .. seealso:: + + The :func:`.n_local` function constructs a functionally equivalent circuit, but faster. + """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.n_local instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, @@ -1069,3 +1299,174 @@ def _stdlib_gate_from_simple_block(block: QuantumCircuit) -> _StdlibGateResult | ): return None return _StdlibGateResult(instruction.operation.base_class, len(instruction.operation.params)) + + +def _normalize_entanglement( + entanglement: ( + BlockEntanglement + | Iterable[BlockEntanglement] + | Callable[[int], BlockEntanglement | Iterable[BlockEntanglement]] + ), + num_entanglement_blocks: int, +) -> list[str | list[tuple[int]]] | Callable[[int], list[str | list[tuple[int]]]]: + """If the entanglement is Iterable[Iterable], normalize to list[tuple].""" + if isinstance(entanglement, str): + return [entanglement] * num_entanglement_blocks + + if callable(entanglement): + return lambda offset: _normalize_entanglement(entanglement(offset), num_entanglement_blocks) + + # here, entanglement is an Iterable + if len(entanglement) == 0: + # handle edge cases when entanglement is set to an empty list + return [[]] + + # if the entanglement is Iterable[Iterable[int]], normalize to Iterable[Iterable[Iterable[int]]] + try: + # if users e.g. gave Iterable[int] this in invalid and will raise a TypeError + if isinstance(entanglement[0][0], (int, numpy.integer)): + entanglement = [entanglement] + except TypeError as exc: + raise TypeError(f"Invalid entanglement type: {entanglement}.") from exc + + # ensure the number of block entanglements matches the number of blocks + if len(entanglement) != num_entanglement_blocks: + raise QiskitError( + f"Number of block-entanglements ({len(entanglement)}) must match number of " + f"entanglement blocks ({num_entanglement_blocks})!" + ) + + # normalize the data: str remains, and Iterable[Iterable[int]] becomes list[tuple[int]] + normalized = [] + for block in entanglement: + if isinstance(block, str): + normalized.append(block) + else: + normalized.append([tuple(connections) for connections in block]) + + return normalized + + +def _normalize_blocks( + blocks: str | Gate | Iterable[str | Gate], + supported_gates: dict[str, Gate], + overwrite_block_parameters: bool, +) -> list[Block]: + # normalize the input into an iterable -- we add an extra check for a circuit as + # courtesy to the users, since the NLocal class used to accept circuits + if isinstance(blocks, (str, Gate, QuantumCircuit)): + blocks = [blocks] + + normalized = [] + for block in blocks: + # since the NLocal circuit accepted circuits as inputs, we raise a warning here + # to simplify the transition (even though, strictly speaking, quantum circuits are + # not a supported input type) + if isinstance(block, QuantumCircuit): + raise ValueError( + "The blocks should be of type Gate or str, but you passed a QuantumCircuit. " + "You can call .to_gate() on the circuit to turn it into a Gate object." + ) + + is_standard = False + if isinstance(block, str): + if block not in supported_gates: + raise ValueError(f"Unsupported gate: {block}") + block = supported_gates[block] + is_standard = True + elif isinstance(block, Gate) and getattr(block, "_standard_gate", None) is not None: + if len(block.params) == 0: + is_standard = True + # the fast path will always overwrite block parameters + elif overwrite_block_parameters: + # if all parameters are plain Parameter objects, this is a plain + # standard gate we do not need to propagate parameterizations for + is_standard = all(isinstance(p, Parameter) for p in block.params) + + if is_standard: + block = Block.from_standard_gate(block._standard_gate) + else: + if overwrite_block_parameters: + num_parameters, builder = _get_gate_builder(block) + else: + num_parameters, builder = _trivial_builder(block) + + block = Block.from_callable(block.num_qubits, num_parameters, builder) + + normalized.append(block) + + return normalized + + +def _trivial_builder( + gate: Gate, +) -> tuple[int, Callable[list[Parameter], tuple[Gate, list[ParameterValueType]]]]: + + def builder(_): + copied = gate.copy() + return copied, copied.params + + return 0, builder + + +def _get_gate_builder( + gate: Gate, +) -> tuple[int, Callable[list[Parameter], tuple[Gate, list[ParameterValueType]]]]: + """Construct a callable that handles parameter-rebinding. + + For a given gate, this return the number of free parameters and a callable that can be + used to obtain a re-parameterized version of the gate. For example:: + + x, y = Parameter("x"), Parameter("y") + gate = CUGate(x, 2 * y, 0.5, 0.) + + num_parameters, builder = _build_gate(gate) + print(num_parameters) # prints 2 + + a, b = Parameter("a"), Parameter("b") + new_gate, new_params = builder([a, b]) + print(new_gate) # CUGate(a, 2 * b, 0.5, 0) + print(new_params) # [a, 2 * b, 0.5, 0] + + """ + free_parameters = set() + for p in gate.params: + if isinstance(p, ParameterExpression): + free_parameters |= set(p.parameters) + + num_parameters = len(free_parameters) + + sorted_parameters = _sort_parameters(free_parameters) + + def builder(new_parameters): + out = gate.copy() + + # re-bind the ``Gate.params`` attribute + param_dict = dict(zip(sorted_parameters, new_parameters)) + bound_params = gate.params.copy() + for i, expr in enumerate(gate.params): + if isinstance(expr, ParameterExpression): + for parameter in expr.parameters: + expr = expr.assign(parameter, param_dict[parameter]) + bound_params[i] = expr + + out.params = bound_params + + # if the definition exists, rebind it + if out._definition is not None: + out._definition.assign_parameters(param_dict, inplace=True) + + return out, bound_params + + return num_parameters, builder + + +def _sort_parameters(parameters): + """Sort a list of Parameter objects.""" + + def key(parameter): + if isinstance(parameter, ParameterVectorElement): + return (parameter.vector.name, parameter.index) + return (parameter.name,) + + return sorted(parameters, key=key) diff --git a/qiskit/circuit/library/n_local/pauli_two_design.py b/qiskit/circuit/library/n_local/pauli_two_design.py index 8bb003f4f690..f0deeb68b287 100644 --- a/qiskit/circuit/library/n_local/pauli_two_design.py +++ b/qiskit/circuit/library/n_local/pauli_two_design.py @@ -16,9 +16,104 @@ import numpy as np from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library.standard_gates import RXGate, RYGate, RZGate, CZGate +from qiskit.utils.deprecation import deprecate_func +from qiskit._accelerate.circuit_library import Block, py_n_local +from .two_local import TwoLocal -from .two_local import TwoLocal +def pauli_two_design( + num_qubits: int, + reps: int = 3, + seed: int | None = None, + insert_barriers: bool = False, + parameter_prefix: str = "θ", + name: str = "PauliTwoDesign", +) -> QuantumCircuit: + r"""Construct a Pauli 2-design ansatz. + + This class implements a particular form of a 2-design circuit [1], which is frequently studied + in quantum machine learning literature, such as, e.g., the investigation of Barren plateaus in + variational algorithms [2]. + + The circuit consists of alternating rotation and entanglement layers with + an initial layer of :math:`\sqrt{H} = RY(\pi/4)` gates. + The rotation layers contain single qubit Pauli rotations, where the axis is chosen uniformly + at random to be X, Y or Z. The entanglement layers is compromised of pairwise CZ gates + with a total depth of 2. + + For instance, the circuit could look like this: + + .. parsed-literal:: + + ┌─────────┐┌──────────┐ ░ ┌──────────┐ ░ ┌──────────┐ + q_0: ┤ RY(π/4) ├┤ RZ(θ[0]) ├─■─────░─┤ RY(θ[4]) ├─■─────░──┤ RZ(θ[8]) ├ + ├─────────┤├──────────┤ │ ░ ├──────────┤ │ ░ ├──────────┤ + q_1: ┤ RY(π/4) ├┤ RZ(θ[1]) ├─■──■──░─┤ RY(θ[5]) ├─■──■──░──┤ RX(θ[9]) ├ + ├─────────┤├──────────┤ │ ░ ├──────────┤ │ ░ ┌┴──────────┤ + q_2: ┤ RY(π/4) ├┤ RX(θ[2]) ├─■──■──░─┤ RY(θ[6]) ├─■──■──░─┤ RX(θ[10]) ├ + ├─────────┤├──────────┤ │ ░ ├──────────┤ │ ░ ├───────────┤ + q_3: ┤ RY(π/4) ├┤ RZ(θ[3]) ├─■─────░─┤ RX(θ[7]) ├─■─────░─┤ RY(θ[11]) ├ + └─────────┘└──────────┘ ░ └──────────┘ ░ └───────────┘ + + Examples: + + .. plot:: + :include-source: + + from qiskit.circuit.library import pauli_two_design + circuit = pauli_two_design(4, reps=2, seed=5, insert_barriers=True) + circuit.draw("mpl") + + Args: + num_qubits: The number of qubits of the Pauli Two-Design circuit. + reps: Specifies how often a block consisting of a rotation layer and entanglement + layer is repeated. + seed: The seed for randomly choosing the axes of the Pauli rotations. + parameter_prefix: The prefix used for the rotation parameters. + insert_barriers: If ``True``, barriers are inserted in between each layer. If ``False``, + no barriers are inserted. Defaults to ``False``. + name: The circuit name. + + Returns: + A Pauli 2-design circuit. + + References: + + [1]: Nakata et al., Unitary 2-designs from random X- and Z-diagonal unitaries. + `arXiv:1502.07514 `_ + + [2]: McClean et al., Barren plateaus in quantum neural network training landscapes. + `arXiv:1803.11173 `_ + """ + rng = np.random.default_rng(seed) + random_block = Block.from_callable(1, 1, lambda params: _random_pauli_builder(params, rng)) + cz_block = Block.from_standard_gate(CZGate._standard_gate) + + data = py_n_local( + num_qubits=num_qubits, + reps=reps, + rotation_blocks=[random_block], + entanglement_blocks=[cz_block], + entanglement=["pairwise"], + insert_barriers=insert_barriers, + skip_final_rotation_layer=False, + skip_unentangled_qubits=False, + parameter_prefix=parameter_prefix, + ) + two_design = QuantumCircuit._from_circuit_data(data) + + circuit = QuantumCircuit(num_qubits, name=name) + circuit.ry(np.pi / 4, circuit.qubits) + circuit.compose(two_design, inplace=True, copy=False) + + return circuit + + +def _random_pauli_builder(params, rng): + gate_cls = rng.choice([RXGate, RYGate, RZGate]) + gate = gate_cls(params[0]) + return gate, gate.params class PauliTwoDesign(TwoLocal): @@ -58,6 +153,10 @@ class PauliTwoDesign(TwoLocal): circuit = PauliTwoDesign(4, reps=2, seed=5, insert_barriers=True) circuit.draw('mpl') + .. seealso:: + + The :func:`.pauli_two_design` function constructs the functionally same circuit, but faster. + References: [1]: Nakata et al., Unitary 2-designs from random X- and Z-diagonal unitaries. @@ -67,6 +166,11 @@ class PauliTwoDesign(TwoLocal): `arXiv:1803.11173 `_ """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.pauli_two_design instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, @@ -85,8 +189,6 @@ def __init__( no barriers are inserted. Defaults to ``False``. """ - from qiskit.circuit.library import RYGate # pylint: disable=cyclic-import - # store a random number generator self._seed = seed self._rng = np.random.default_rng(seed) diff --git a/qiskit/circuit/library/n_local/real_amplitudes.py b/qiskit/circuit/library/n_local/real_amplitudes.py index 2b18bac0eb3d..f1ba10604e9d 100644 --- a/qiskit/circuit/library/n_local/real_amplitudes.py +++ b/qiskit/circuit/library/n_local/real_amplitudes.py @@ -13,15 +13,123 @@ """The real-amplitudes 2-local circuit.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Iterable import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.circuit.library.standard_gates import RYGate, CXGate +from qiskit.utils.deprecation import deprecate_func +from .n_local import n_local, BlockEntanglement from .two_local import TwoLocal +def real_amplitudes( + num_qubits: int, + entanglement: ( + BlockEntanglement + | Iterable[BlockEntanglement] + | Callable[[int], BlockEntanglement | Iterable[BlockEntanglement]] + ) = "reverse_linear", + reps: int = 3, + skip_unentangled_qubits: bool = False, + skip_final_rotation_layer: bool = False, + parameter_prefix: str = "θ", + insert_barriers: bool = False, + name: str = "RealAmplitudes", +) -> QuantumCircuit: + r"""Construct a real-amplitudes 2-local circuit. + + This circuit is a heuristic trial wave function used, e.g., as ansatz in chemistry, optimization + or machine learning applications. The circuit consists of alternating layers of :math:`Y` + rotations and :math:`CX` entanglements. The entanglement pattern can be user-defined or selected + from a predefined set. This circuit is "real amplitudes" since the prepared quantum states will + only have real amplitudes. + + For example a ``real_amplitudes`` circuit with 2 repetitions on 3 qubits with ``"reverse_linear"`` + entanglement is + + .. parsed-literal:: + + ┌──────────┐ ░ ░ ┌──────────┐ ░ ░ ┌──────────┐ + ┤ Ry(θ[0]) ├─░────────■───░─┤ Ry(θ[3]) ├─░────────■───░─┤ Ry(θ[6]) ├ + ├──────────┤ ░ ┌─┴─┐ ░ ├──────────┤ ░ ┌─┴─┐ ░ ├──────────┤ + ┤ Ry(θ[1]) ├─░───■──┤ X ├─░─┤ Ry(θ[4]) ├─░───■──┤ X ├─░─┤ Ry(θ[7]) ├ + ├──────────┤ ░ ┌─┴─┐└───┘ ░ ├──────────┤ ░ ┌─┴─┐└───┘ ░ ├──────────┤ + ┤ Ry(θ[2]) ├─░─┤ X ├──────░─┤ Ry(θ[5]) ├─░─┤ X ├──────░─┤ Ry(θ[8]) ├ + └──────────┘ ░ └───┘ ░ └──────────┘ ░ └───┘ ░ └──────────┘ + + The entanglement can be set using the ``entanglement`` keyword as string or a list of + index-pairs. See the documentation of :func:`.n_local`. Additional options that can be set include + the number of repetitions, skipping rotation gates on qubits that are not entangled, leaving out + the final rotation layer and inserting barriers in between the rotation and entanglement + layers. + + Examples: + + .. plot:: + :include-source: + :context: + + from qiskit.circuit.library import real_amplitudes + + ansatz = real_amplitudes(3, reps=2) # create the circuit on 3 qubits + ansatz.draw("mpl") + + .. plot:: + :include-source: + :context: + + ansatz = real_amplitudes(3, entanglement="full", reps=2) # it is the same unitary as above + ansatz.draw("mpl") + + .. plot:: + :include-source: + :context: + + ansatz = real_amplitudes(3, entanglement="linear", reps=2, insert_barriers=True) + ansatz.draw("mpl") + + .. plot:: + :include-source: + :context: + + ansatz = real_amplitudes(4, reps=2, entanglement=[[0,3], [0,2]], skip_unentangled_qubits=True) + ansatz.draw("mpl") + + Args: + num_qubits: The number of qubits of the RealAmplitudes circuit. + reps: Specifies how often the structure of a rotation layer followed by an entanglement + layer is repeated. + entanglement: The indices specifying on which qubits the input blocks act. + See :func:`.n_local` for detailed information. + skip_final_rotation_layer: Whether a final rotation layer is added to the circuit. + skip_unentangled_qubits: If ``True``, the rotation gates act only on qubits that + are entangled. If ``False``, the rotation gates act on all qubits. + parameter_prefix: The name of the free parameters. + insert_barriers: If True, barriers are inserted in between each layer. If False, + no barriers are inserted. + name: The name of the circuit. + + Returns: + A real-amplitudes circuit. + """ + + return n_local( + num_qubits, + ["ry"], + ["cx"], + entanglement, + reps, + insert_barriers, + parameter_prefix, + True, + skip_final_rotation_layer, + skip_unentangled_qubits, + name, + ) + + class RealAmplitudes(TwoLocal): r"""The real-amplitudes 2-local circuit. @@ -59,7 +167,7 @@ class RealAmplitudes(TwoLocal): Examples: >>> ansatz = RealAmplitudes(3, reps=2) # create the circuit on 3 qubits - >>> print(ansatz) + >>> print(ansatz.decompose()) ┌──────────┐ ┌──────────┐ ┌──────────┐ q_0: ┤ Ry(θ[0]) ├──────────■──────┤ Ry(θ[3]) ├──────────■──────┤ Ry(θ[6]) ├ ├──────────┤ ┌─┴─┐ ├──────────┤ ┌─┴─┐ ├──────────┤ @@ -68,7 +176,7 @@ class RealAmplitudes(TwoLocal): q_2: ┤ Ry(θ[2]) ├┤ X ├┤ Ry(θ[5]) ├────────────┤ X ├┤ Ry(θ[8]) ├──────────── └──────────┘└───┘└──────────┘ └───┘└──────────┘ - >>> ansatz = RealAmplitudes(3, entanglement='full', reps=2) # it is the same unitary as above + >>> ansatz = RealAmplitudes(3, entanglement='full', reps=2, flatten=True) >>> print(ansatz) ┌──────────┐ ┌──────────┐ ┌──────────┐ q_0: ┤ RY(θ[0]) ├──■────■──┤ RY(θ[3]) ├──────────────■────■──┤ RY(θ[6]) ├──────────── @@ -78,7 +186,8 @@ class RealAmplitudes(TwoLocal): q_2: ┤ RY(θ[2]) ├─────┤ X ├───┤ X ├────┤ RY(θ[5]) ├─────┤ X ├───┤ X ├────┤ RY(θ[8]) ├ └──────────┘ └───┘ └───┘ └──────────┘ └───┘ └───┘ └──────────┘ - >>> ansatz = RealAmplitudes(3, entanglement='linear', reps=2, insert_barriers=True) + >>> ansatz = RealAmplitudes(3, entanglement='linear', reps=2, insert_barriers=True, + ... flatten=True) >>> qc = QuantumCircuit(3) # create a circuit and append the RY variational form >>> qc.compose(ansatz, inplace=True) >>> qc.draw() @@ -90,7 +199,8 @@ class RealAmplitudes(TwoLocal): q_2: ┤ RY(θ[2]) ├─░──────┤ X ├─░─┤ RY(θ[5]) ├─░──────┤ X ├─░─┤ RY(θ[8]) ├ └──────────┘ ░ └───┘ ░ └──────────┘ ░ └───┘ ░ └──────────┘ - >>> ansatz = RealAmplitudes(4, reps=1, entanglement='circular', insert_barriers=True) + >>> ansatz = RealAmplitudes(4, reps=1, entanglement='circular', insert_barriers=True, + ... flatten=True) >>> print(ansatz) ┌──────────┐ ░ ┌───┐ ░ ┌──────────┐ q_0: ┤ RY(θ[0]) ├─░─┤ X ├──■─────────────░─┤ RY(θ[4]) ├ @@ -103,7 +213,7 @@ class RealAmplitudes(TwoLocal): └──────────┘ ░ └───┘ ░ └──────────┘ >>> ansatz = RealAmplitudes(4, reps=2, entanglement=[[0,3], [0,2]], - ... skip_unentangled_qubits=True) + ... skip_unentangled_qubits=True, flatten=True) >>> print(ansatz) ┌──────────┐ ┌──────────┐ ┌──────────┐ q_0: ┤ RY(θ[0]) ├──■───────■──────┤ RY(θ[3]) ├──■───────■──────┤ RY(θ[6]) ├ @@ -115,8 +225,17 @@ class RealAmplitudes(TwoLocal): q_3: ┤ RY(θ[2]) ├┤ X ├┤ RY(θ[5]) ├────────────┤ X ├┤ RY(θ[8]) ├──────────── └──────────┘└───┘└──────────┘ └───┘└──────────┘ + .. seealso:: + + The :func:`.real_amplitudes` function constructs a functionally equivalent circuit, but faster. + """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.real_amplitudes instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, diff --git a/qiskit/circuit/library/n_local/two_local.py b/qiskit/circuit/library/n_local/two_local.py index f3822d53243f..fcbc5e30bb8d 100644 --- a/qiskit/circuit/library/n_local/two_local.py +++ b/qiskit/circuit/library/n_local/two_local.py @@ -18,6 +18,7 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit import Gate, Instruction +from qiskit.utils.deprecation import deprecate_func from .n_local import NLocal from ..standard_gates import get_standard_gate_name_mapping @@ -76,7 +77,7 @@ class TwoLocal(NLocal): Examples: >>> two = TwoLocal(3, 'ry', 'cx', 'linear', reps=2, insert_barriers=True) - >>> print(two) # decompose the layers into standard gates + >>> print(two.decompose()) # decompose the layers into standard gates ┌──────────┐ ░ ░ ┌──────────┐ ░ ░ ┌──────────┐ q_0: ┤ Ry(θ[0]) ├─░───■────────░─┤ Ry(θ[3]) ├─░───■────────░─┤ Ry(θ[6]) ├ ├──────────┤ ░ ┌─┴─┐ ░ ├──────────┤ ░ ┌─┴─┐ ░ ├──────────┤ @@ -85,10 +86,10 @@ class TwoLocal(NLocal): q_2: ┤ Ry(θ[2]) ├─░──────┤ X ├─░─┤ Ry(θ[5]) ├─░──────┤ X ├─░─┤ Ry(θ[8]) ├ └──────────┘ ░ └───┘ ░ └──────────┘ ░ └───┘ ░ └──────────┘ - >>> two = TwoLocal(3, ['ry','rz'], 'cz', 'full', reps=1, insert_barriers=True) + >>> two = TwoLocal(3, ['ry','rz'], 'cz', 'full', reps=1, insert_barriers=True, flatten=True) >>> qc = QuantumCircuit(3) >>> qc &= two - >>> print(qc.decompose().draw()) + >>> print(qc.draw()) ┌──────────┐┌──────────┐ ░ ░ ┌──────────┐ ┌──────────┐ q_0: ┤ Ry(θ[0]) ├┤ Rz(θ[3]) ├─░──■──■─────░─┤ Ry(θ[6]) ├─┤ Rz(θ[9]) ├ ├──────────┤├──────────┤ ░ │ │ ░ ├──────────┤┌┴──────────┤ @@ -98,7 +99,7 @@ class TwoLocal(NLocal): └──────────┘└──────────┘ ░ ░ └──────────┘└───────────┘ >>> entangler_map = [[0, 1], [1, 2], [2, 0]] # circular entanglement for 3 qubits - >>> two = TwoLocal(3, 'x', 'crx', entangler_map, reps=1) + >>> two = TwoLocal(3, 'x', 'crx', entangler_map, reps=1, flatten=True) >>> print(two) # note: no barriers inserted this time! ┌───┐ ┌──────────┐┌───┐ q_0: |0>┤ X ├─────■───────────────────────┤ Rx(θ[2]) ├┤ X ├ @@ -109,9 +110,9 @@ class TwoLocal(NLocal): └───┘ └──────────┘ └───┘ >>> entangler_map = [[0, 3], [0, 2]] # entangle the first and last two-way - >>> two = TwoLocal(4, [], 'cry', entangler_map, reps=1) + >>> two = TwoLocal(4, [], 'cry', entangler_map, reps=1, flatten=True) >>> circuit = two.compose(two) - >>> print(circuit.decompose().draw()) # note, that the parameters are the same! + >>> print(circuit.draw()) # note, that the parameters are the same! q_0: ─────■───────────■───────────■───────────■────── │ │ │ │ q_1: ─────┼───────────┼───────────┼───────────┼────── @@ -123,7 +124,8 @@ class TwoLocal(NLocal): >>> layer_1 = [(0, 1), (0, 2)] >>> layer_2 = [(1, 2)] - >>> two = TwoLocal(3, 'x', 'cx', [layer_1, layer_2], reps=2, insert_barriers=True) + >>> two = TwoLocal(3, 'x', 'cx', [layer_1, layer_2], reps=2, insert_barriers=True, + ... flatten=True) >>> print(two) ┌───┐ ░ ░ ┌───┐ ░ ░ ┌───┐ q_0: ┤ X ├─░───■────■───░─┤ X ├─░───────░─┤ X ├ @@ -135,6 +137,11 @@ class TwoLocal(NLocal): """ + @deprecate_func( + since="1.3", + additional_msg="Use the function qiskit.circuit.library.n_local instead.", + pending=True, + ) def __init__( self, num_qubits: int | None = None, diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 75bd85d88ce9..2c96ec533f3f 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1177,9 +1177,11 @@ def unit(self, value): self._unit = value @classmethod - def _from_circuit_data(cls, data: CircuitData, add_regs: bool = False) -> typing.Self: + def _from_circuit_data( + cls, data: CircuitData, add_regs: bool = False, name: str | None = None + ) -> typing.Self: """A private constructor from rust space circuit data.""" - out = QuantumCircuit() + out = QuantumCircuit(name=name) if data.num_qubits > 0: if add_regs: diff --git a/qiskit/synthesis/clifford/clifford_decompose_bm.py b/qiskit/synthesis/clifford/clifford_decompose_bm.py index 5ead6945eba2..7c7857e8dcd1 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_bm.py +++ b/qiskit/synthesis/clifford/clifford_decompose_bm.py @@ -41,7 +41,6 @@ def synth_clifford_bm(clifford: Clifford) -> QuantumCircuit: `arXiv:2003.09412 [quant-ph] `_ """ circuit = QuantumCircuit._from_circuit_data( - synth_clifford_bm_inner(clifford.tableau.astype(bool)), add_regs=True + synth_clifford_bm_inner(clifford.tableau.astype(bool)), add_regs=True, name=str(clifford) ) - circuit.name = str(clifford) return circuit diff --git a/qiskit/synthesis/clifford/clifford_decompose_greedy.py b/qiskit/synthesis/clifford/clifford_decompose_greedy.py index 9766efe4aa8f..9d5181f3e9a9 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_greedy.py +++ b/qiskit/synthesis/clifford/clifford_decompose_greedy.py @@ -51,7 +51,8 @@ def synth_clifford_greedy(clifford: Clifford) -> QuantumCircuit: `arXiv:2105.02291 [quant-ph] `_ """ circuit = QuantumCircuit._from_circuit_data( - synth_clifford_greedy_inner(clifford.tableau.astype(bool)), add_regs=True + synth_clifford_greedy_inner(clifford.tableau.astype(bool)), + add_regs=True, + name=str(clifford), ) - circuit.name = str(clifford) return circuit diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py index dd39aa61ba61..753feff490b1 100644 --- a/test/python/circuit/library/test_nlocal.py +++ b/test/python/circuit/library/test_nlocal.py @@ -20,12 +20,18 @@ from ddt import ddt, data, unpack from qiskit import transpile -from qiskit.circuit import QuantumCircuit, Parameter, ParameterVector, ParameterExpression +from qiskit.circuit import QuantumCircuit, Parameter, ParameterVector, ParameterExpression, Gate from qiskit.circuit.library import ( + n_local, + efficient_su2, + real_amplitudes, + excitation_preserving, + pauli_two_design, NLocal, TwoLocal, RealAmplitudes, ExcitationPreserving, + HGate, XGate, CRXGate, CCXGate, @@ -45,10 +51,23 @@ from qiskit.exceptions import QiskitError from qiskit._accelerate.circuit_library import get_entangler_map as fast_entangler_map +from qiskit._accelerate.circuit_library import Block from test import QiskitTestCase # pylint: disable=wrong-import-order +class Gato(Gate): + """A custom gate.""" + + def __init__(self, x, y): + super().__init__("meow", 1, [x, y]) + + def _define(self): + x, y = self.params + self.definition = QuantumCircuit(1) + self.definition.p(x + y, 0) + + @ddt class TestNLocal(QiskitTestCase): """Test the n-local circuit class.""" @@ -470,6 +489,344 @@ def test_initial_state_as_circuit_object(self): self.assertCircuitEqual(ref, expected) +@ddt +class TestNLocalFunction(QiskitTestCase): + """Test the n_local circuit library function.""" + + def test_empty_blocks(self): + """Test passing no rotation and entanglement blocks.""" + circuit = n_local(2, rotation_blocks=[], entanglement_blocks=[]) + expected = QuantumCircuit(2) + + self.assertEqual(expected, circuit) + + def test_invalid_custom_block(self): + """Test constructing a block from callable but not with a callable.""" + my_block = QuantumCircuit(2) + with self.assertRaises(QiskitError): + _ = Block.from_callable(2, 0, my_block) + + def test_str_blocks(self): + """Test passing blocks as strings.""" + circuit = n_local(2, "h", "ecr", reps=2) + expected = QuantumCircuit(2) + for _ in range(2): + expected.h([0, 1]) + expected.ecr(0, 1) + expected.h([0, 1]) + + self.assertEqual(expected, circuit) + + def test_stdgate_blocks(self): + """Test passing blocks as standard gates.""" + circuit = n_local(2, HGate(), CRXGate(Parameter("x")), reps=2) + + param_iter = iter(circuit.parameters) + expected = QuantumCircuit(2) + for _ in range(2): + expected.h([0, 1]) + expected.crx(next(param_iter), 0, 1) + expected.h([0, 1]) + + self.assertEqual(expected, circuit) + + def test_invalid_str_blocks(self): + """Test passing blocks as invalid string raises.""" + with self.assertRaises(ValueError): + _ = n_local(2, "h", "iamnotanexisting2qgateeventhoughiwanttobe") + + def test_gate_blocks(self): + """Test passing blocks as gates.""" + x = ParameterVector("x", 2) + my_gate = Gato(*x) + + circuit = n_local(4, my_gate, "cx", "linear", reps=3) + + expected_cats = 4 * (3 + 1) # num_qubits * (reps + 1) + expected_cx = 3 * 3 # gates per block * reps + expected_num_params = expected_cats * 2 + + self.assertEqual(expected_cats, circuit.count_ops().get("meow", 0)) + self.assertEqual(expected_cx, circuit.count_ops().get("cx", 0)) + self.assertEqual(expected_num_params, circuit.num_parameters) + + def test_gate_lists(self): + """Test passing a list of strings and gates.""" + reps = 2 + circuit = n_local(4, [XGate(), "ry", SXGate()], ["ryy", CCXGate()], "full", reps) + expected_1q = 4 * (reps + 1) # num_qubits * (reps + 1) + expected_2q = 4 * 3 / 2 * reps # 4 choose 2 * reps + expected_3q = 4 * reps # 4 choose 3 * reps + + ops = circuit.count_ops() + for gate in ["x", "ry", "sx"]: + with self.subTest(gate=gate): + self.assertEqual(expected_1q, ops.get(gate, 0)) + + with self.subTest(gate="ryy"): + self.assertEqual(expected_2q, ops.get("ryy", 0)) + + with self.subTest(gate="ccx"): + self.assertEqual(expected_3q, ops.get("ccx", 0)) + + def test_reps(self): + """Test setting the repetitions.""" + all_reps = [0, 1, 2, 10] + for reps in all_reps: + circuit = n_local(2, rotation_blocks="rx", entanglement_blocks="cz", reps=reps) + expected_rx = (reps + 1) * 2 + expected_cz = reps + + with self.subTest(reps=reps): + self.assertEqual(expected_rx, circuit.count_ops().get("rx", 0)) + self.assertEqual(expected_cz, circuit.count_ops().get("cz", 0)) + + def test_negative_reps(self): + """Test negative reps raises.""" + with self.assertRaises(ValueError): + _ = n_local(1, [], [], reps=-1) + + def test_barrier(self): + """Test setting barriers.""" + circuit = n_local(2, "ry", "cx", reps=2, insert_barriers=True) + values = np.ones(circuit.num_parameters) + + expected = QuantumCircuit(2) + expected.ry(1, [0, 1]) + expected.barrier() + expected.cx(0, 1) + expected.barrier() + expected.ry(1, [0, 1]) + expected.barrier() + expected.cx(0, 1) + expected.barrier() + expected.ry(1, [0, 1]) + + self.assertEqual(expected, circuit.assign_parameters(values)) + + def test_parameter_prefix(self): + """Test setting the parameter prefix.""" + circuit = n_local(2, "h", "crx", parameter_prefix="x") + prefixes = [p.name[0] for p in circuit.parameters] + self.assertTrue(all(prefix == "x" for prefix in prefixes)) + + @data(True, False) + def test_overwrite_block_parameters(self, overwrite): + """Test overwriting the block parameters.""" + x = Parameter("x") + block = QuantumCircuit(2) + block.rxx(x, 0, 1) + + reps = 3 + circuit = n_local( + 4, [], [block.to_gate()], "linear", reps, overwrite_block_parameters=overwrite + ) + + expected_num_params = reps * 3 if overwrite else 1 + self.assertEqual(expected_num_params, circuit.num_parameters) + + @data(True, False) + def test_skip_final_rotation_layer(self, skip): + """Test skipping the final rotation layer.""" + reps = 5 + num_qubits = 2 + circuit = n_local(num_qubits, "rx", "ch", reps=reps, skip_final_rotation_layer=skip) + expected_rx = num_qubits * (reps + (0 if skip else 1)) + + self.assertEqual(expected_rx, circuit.count_ops().get("rx", 0)) + + def test_skip_unentangled_qubits(self): + """Test skipping the unentangled qubits.""" + num_qubits = 6 + entanglement_1 = [[0, 1, 3], [1, 3, 5], [0, 1, 5]] + skipped_1 = [2, 4] + + def entanglement_2(layer): + return entanglement_1 if layer % 2 == 0 else [[0, 1, 2], [2, 3, 5]] + + skipped_2 = [4] + + for entanglement, skipped in zip([entanglement_1, entanglement_2], [skipped_1, skipped_2]): + with self.subTest(entanglement=entanglement, skipped=skipped): + nlocal = n_local( + num_qubits, + rotation_blocks=XGate(), + entanglement_blocks=CCXGate(), + entanglement=entanglement, + reps=3, + skip_unentangled_qubits=True, + ) + + skipped_set = {nlocal.qubits[i] for i in skipped} + dag = circuit_to_dag(nlocal) + idle = set(dag.idle_wires()) + self.assertEqual(skipped_set, idle) + + def test_empty_entanglement(self): + """Test passing an empty list as entanglement.""" + circuit = n_local(3, "h", "cx", entanglement=[], reps=1) + self.assertEqual(6, circuit.count_ops().get("h", 0)) + self.assertEqual(0, circuit.count_ops().get("cx", 0)) + + def test_entanglement_list_of_str(self): + """Test different entanglement strings per entanglement block.""" + circuit = n_local(3, [], ["cx", "cz"], entanglement=["reverse_linear", "full"], reps=1) + self.assertEqual(2, circuit.count_ops().get("cx", 0)) + self.assertEqual(3, circuit.count_ops().get("cz", 0)) + + def test_invalid_entanglement_list(self): + """Test passing an invalid list.""" + with self.assertRaises(TypeError): + _ = n_local(3, "h", "cx", entanglement=[0, 1]) # should be [(0, 1)] + + def test_mismatching_entanglement_blocks_str(self): + """Test an error is raised if the number of entanglements does not match the blocks.""" + entanglement = ["full", "linear", "pairwise"] + blocks = ["ryy", "iswap"] + + with self.assertRaises(QiskitError): + _ = n_local(3, [], blocks, entanglement=entanglement) + + def test_mismatching_entanglement_blocks_indices(self): + """Test an error is raised if the number of entanglements does not match the blocks.""" + ent1 = [(0, 1), (1, 2)] + ent2 = [(0, 2)] + blocks = ["ryy", "iswap"] + + with self.assertRaises(QiskitError): + _ = n_local(3, [], blocks, entanglement=[ent1, ent1, ent2]) + + def test_mismatching_entanglement_indices(self): + """Test an error is raised if the entanglement does not match the blocksize.""" + entanglement = [[0, 1], [2]] + + with self.assertRaises(QiskitError): + _ = n_local(3, "ry", "cx", entanglement) + + def test_entanglement_by_callable(self): + """Test setting the entanglement by callable. + + This is the circuit we test (times 2, with final X layer) + ┌───┐ ┌───┐┌───┐ ┌───┐ + q_0: |0>┤ X ├──■────■───┤ X ├┤ X ├──■─── .. ┤ X ├ + ├───┤ │ │ ├───┤└─┬─┘ │ ├───┤ + q_1: |0>┤ X ├──■────┼───┤ X ├──■────┼─── .. ┤ X ├ + ├───┤┌─┴─┐ │ ├───┤ │ │ x2 ├───┤ + q_2: |0>┤ X ├┤ X ├──■───┤ X ├──■────■─── .. ┤ X ├ + ├───┤└───┘┌─┴─┐ ├───┤ ┌─┴─┐ ├───┤ + q_3: |0>┤ X ├─────┤ X ├─┤ X ├─────┤ X ├─ .. ┤ X ├ + └───┘ └───┘ └───┘ └───┘ └───┘ + """ + circuit = QuantumCircuit(4) + for _ in range(2): + circuit.x([0, 1, 2, 3]) + circuit.barrier() + circuit.ccx(0, 1, 2) + circuit.ccx(0, 2, 3) + circuit.barrier() + circuit.x([0, 1, 2, 3]) + circuit.barrier() + circuit.ccx(2, 1, 0) + circuit.ccx(0, 2, 3) + circuit.barrier() + circuit.x([0, 1, 2, 3]) + + layer_1 = [(0, 1, 2), (0, 2, 3)] + layer_2 = [(2, 1, 0), (0, 2, 3)] + + entanglement = lambda offset: layer_1 if offset % 2 == 0 else layer_2 + + nlocal = QuantumCircuit(4) + nlocal.compose( + n_local( + 4, + rotation_blocks=XGate(), + entanglement_blocks=CCXGate(), + reps=4, + entanglement=entanglement, + insert_barriers=True, + ), + inplace=True, + ) + + self.assertEqual(nlocal, circuit) + + def test_nice_error_if_circuit_passed(self): + """Check the transition-helper error.""" + block = QuantumCircuit(1) + + with self.assertRaisesRegex(ValueError, "but you passed a QuantumCircuit"): + _ = n_local(3, block, "cz") + + +@ddt +class TestNLocalFamily(QiskitTestCase): + """Test the derived circuit functions.""" + + def test_real_amplitudes(self): + """Test the real amplitudes circuit.""" + circuit = real_amplitudes(4) + expected = n_local(4, "ry", "cx", "reverse_linear", reps=3) + self.assertEqual(expected.assign_parameters(circuit.parameters), circuit) + + def test_efficient_su2(self): + """Test the efficient SU(2) circuit.""" + circuit = efficient_su2(4) + expected = n_local(4, ["ry", "rz"], "cx", "reverse_linear", reps=3) + self.assertEqual(expected.assign_parameters(circuit.parameters), circuit) + + @data("fsim", "iswap") + def test_excitation_preserving(self, mode): + """Test the excitation preserving circuit.""" + circuit = excitation_preserving(4, mode=mode) + + x = Parameter("x") + block = QuantumCircuit(2) + block.rxx(x, 0, 1) + block.ryy(x, 0, 1) + if mode == "fsim": + y = Parameter("y") + block.cp(y, 0, 1) + + expected = n_local(4, "rz", block.to_gate(), "full", reps=3) + self.assertEqual( + expected.assign_parameters(circuit.parameters).decompose(), circuit.decompose() + ) + + def test_excitation_preserving_invalid_mode(self): + """Test an error is raised for an invalid mode.""" + with self.assertRaises(ValueError): + _ = excitation_preserving(2, mode="Fsim") + + with self.assertRaises(ValueError): + _ = excitation_preserving(2, mode="swaip") + + def test_two_design(self): + """Test the Pauli 2-design circuit.""" + circuit = pauli_two_design(3) + expected_ops = {"rx", "ry", "rz", "cz"} + circuit_ops = set(circuit.count_ops().keys()) + + self.assertTrue(circuit_ops.issubset(expected_ops)) + + def test_two_design_seed(self): + """Test the seed""" + seed1 = 123 + seed2 = 321 + + with self.subTest(msg="same circuit with same seed"): + first = pauli_two_design(3, seed=seed1) + second = pauli_two_design(3, seed=seed1) + + self.assertEqual(first.assign_parameters(second.parameters), second) + + with self.subTest(msg="different circuit with different seed"): + first = pauli_two_design(3, seed=seed1) + second = pauli_two_design(3, seed=seed2) + + self.assertNotEqual(first.assign_parameters(second.parameters), second) + + @ddt class TestTwoLocal(QiskitTestCase): """Tests for the TwoLocal circuit.""" @@ -872,6 +1229,14 @@ def test_fsim_circuit(self): self.assertCircuitEqual(library, expected) + def test_excitation_preserving_invalid_mode(self): + """Test an error is raised for an invalid mode.""" + with self.assertRaises(ValueError): + _ = ExcitationPreserving(2, mode="Fsim") + + with self.assertRaises(ValueError): + _ = ExcitationPreserving(2, mode="swaip") + def test_circular_on_same_block_and_circuit_size(self): """Test circular entanglement works correctly if the circuit and block sizes match."""