From 54ac8f17517759e5ae591fe36a73a5ee3ffda615 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 26 Aug 2024 09:35:41 +0200 Subject: [PATCH] Move the circuit library's entanglement logic to Rust (#12950) * Move ``get_entangler_map`` to Rust * add reno * add docs, better arg order * clippy * add docs to make reviewers happier * use Itertools::combinations thanks @alexanderivrii for the hint! * avoid usage of convert_idx * implement reviews * implement review comments * rm unused import --- .../src/circuit_library/entanglement.rs | 245 ++++++++++++++++++ crates/accelerate/src/circuit_library/mod.rs | 21 ++ crates/accelerate/src/lib.rs | 1 + crates/pyext/src/lib.rs | 17 +- qiskit/__init__.py | 1 + qiskit/circuit/library/n_local/n_local.py | 52 +--- ...ircular-entanglement-5aadd5adf75c0c13.yaml | 16 ++ test/python/circuit/library/test_nlocal.py | 157 ++++++++++- 8 files changed, 456 insertions(+), 54 deletions(-) create mode 100644 crates/accelerate/src/circuit_library/entanglement.rs create mode 100644 crates/accelerate/src/circuit_library/mod.rs create mode 100644 releasenotes/notes/fix-circular-entanglement-5aadd5adf75c0c13.yaml diff --git a/crates/accelerate/src/circuit_library/entanglement.rs b/crates/accelerate/src/circuit_library/entanglement.rs new file mode 100644 index 000000000000..db602ef717fa --- /dev/null +++ b/crates/accelerate/src/circuit_library/entanglement.rs @@ -0,0 +1,245 @@ +// 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::{PyAnyMethods, PyInt, PyList, PyListMethods, PyString, PyTuple}, + Bound, PyAny, PyResult, +}; + +use crate::QiskitError; + +/// Get all-to-all entanglement. For 4 qubits and block size 2 we have: +/// [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] +fn full(num_qubits: u32, block_size: u32) -> impl Iterator> { + (0..num_qubits).combinations(block_size as usize) +} + +/// Get a linear entanglement structure. For ``n`` qubits and block size ``m`` we have: +/// [(0..m-1), (1..m), (2..m+1), ..., (n-m..n-1)] +fn linear(num_qubits: u32, block_size: u32) -> impl DoubleEndedIterator> { + (0..num_qubits - block_size + 1) + .map(move |start_index| (start_index..start_index + block_size).collect()) +} + +/// Get a reversed linear entanglement. This is like linear entanglement but in reversed order: +/// [(n-m..n-1), ..., (1..m), (0..m-1)] +/// This is particularly interesting, as CX+"full" uses n(n-1)/2 gates, but operationally equals +/// CX+"reverse_linear", which needs only n-1 gates. +fn reverse_linear(num_qubits: u32, block_size: u32) -> impl Iterator> { + linear(num_qubits, block_size).rev() +} + +/// Return the qubit indices for circular entanglement. This is defined as tuples of length ``m`` +/// starting at each possible index ``(0..n)``. Historically, Qiskit starts with index ``n-m+1``. +/// This is probably easiest understood for a concerete example of 4 qubits and block size 3: +/// [(2,3,0), (3,0,1), (0,1,2), (1,2,3)] +fn circular(num_qubits: u32, block_size: u32) -> Box>> { + if block_size == 1 || num_qubits == block_size { + Box::new(linear(num_qubits, block_size)) + } else { + let historic_offset = num_qubits - block_size + 1; + Box::new((0..num_qubits).map(move |start_index| { + (0..block_size) + .map(|i| (historic_offset + start_index + i) % num_qubits) + .collect() + })) + } +} + +/// Get pairwise entanglement. This is typically used on 2 qubits and only has a depth of 2, as +/// first all odd pairs, and then even pairs are entangled. For example on 6 qubits: +/// [(0, 1), (2, 3), (4, 5), /* now the even pairs */ (1, 2), (3, 4)] +fn pairwise(num_qubits: u32) -> impl Iterator> { + // for Python-folks (like me): pairwise is equal to linear[::2] + linear[1::2] + linear(num_qubits, 2) + .step_by(2) + .chain(linear(num_qubits, 2).skip(1).step_by(2)) +} + +/// The shifted, circular, alternating (sca) entanglement is motivated from circuits 14/15 of +/// https://arxiv.org/abs/1905.10876. It corresponds to circular entanglement, with the difference +/// that in each layer (controlled by ``offset``) the entanglement gates are shifted by one, plus +/// in each second layer, the entanglement gate is turned upside down. +/// Offset 0 -> [(2,3,0), (3,0,1), (0,1,2), (1,2,3)] +/// Offset 1 -> [(3,2,1), (0,3,2), (1,0,3), (2,1,0)] +/// Offset 2 -> [(0,1,2), (1,2,3), (2,3,0), (3,0,1)] +/// ... +fn shift_circular_alternating( + num_qubits: u32, + block_size: u32, + offset: usize, +) -> Box>> { + // index at which we split the circular iterator -- we use Python-like indexing here, + // and define ``split`` as equivalent to a Python index of ``-offset`` + let split = (num_qubits - (offset as u32 % num_qubits)) % num_qubits; + let shifted = circular(num_qubits, block_size) + .skip(split as usize) + .chain(circular(num_qubits, block_size).take(split as usize)); + if offset % 2 == 0 { + Box::new(shifted) + } else { + // if the offset is odd, reverse the indices inside the qubit block (e.g. turn CX + // gates upside down) + Box::new(shifted.map(|indices| indices.into_iter().rev().collect())) + } +} + +/// Get an entangler map for an arbitrary number of qubits. +/// +/// Args: +/// num_qubits: The number of qubits of the circuit. +/// block_size: The number of qubits of the entangling block. +/// entanglement: The entanglement strategy as string. +/// offset: The block offset, can be used if the entanglements differ per block, +/// for example used in the "sca" mode. +/// +/// Returns: +/// The entangler map using mode ``entanglement`` to scatter a block of ``block_size`` +/// qubits on ``num_qubits`` qubits. +pub fn get_entanglement_from_str( + num_qubits: u32, + block_size: u32, + entanglement: &str, + offset: usize, +) -> PyResult>>> { + if num_qubits == 0 || block_size == 0 { + return Ok(Box::new(std::iter::empty())); + } + + if block_size > num_qubits { + return Err(QiskitError::new_err(format!( + "block_size ({}) cannot be larger than number of qubits ({})", + block_size, num_qubits + ))); + } + + match (entanglement, block_size) { + ("full", _) => Ok(Box::new(full(num_qubits, block_size))), + ("linear", _) => Ok(Box::new(linear(num_qubits, block_size))), + ("reverse_linear", _) => Ok(Box::new(reverse_linear(num_qubits, block_size))), + ("sca", _) => Ok(shift_circular_alternating(num_qubits, block_size, offset)), + ("circular", _) => Ok(circular(num_qubits, block_size)), + ("pairwise", 1) => Ok(Box::new(linear(num_qubits, 1))), + ("pairwise", 2) => Ok(Box::new(pairwise(num_qubits))), + ("pairwise", _) => Err(QiskitError::new_err(format!( + "block_size ({}) can be at most 2 for pairwise entanglement", + block_size + ))), + _ => Err(QiskitError::new_err(format!( + "Unsupported entanglement: {}", + entanglement + ))), + } +} + +/// Get an entangler map for an arbitrary number of qubits. +/// +/// Args: +/// num_qubits: The number of qubits of the circuit. +/// block_size: The number of qubits of the entangling block. +/// entanglement: The entanglement strategy. +/// offset: The block offset, can be used if the entanglements differ per block, +/// for example used in the "sca" mode. +/// +/// Returns: +/// The entangler map using mode ``entanglement`` to scatter a block of ``block_size`` +/// qubits on ``num_qubits`` qubits. +pub fn get_entanglement<'a>( + num_qubits: u32, + block_size: u32, + entanglement: &'a Bound, + offset: usize, +) -> PyResult>> + 'a>> { + // unwrap the callable, if it is one + let entanglement = if entanglement.is_callable() { + entanglement.call1((offset,))? + } else { + entanglement.to_owned() + }; + + if let Ok(strategy) = entanglement.downcast::() { + let as_str = strategy.to_string(); + return Ok(Box::new( + get_entanglement_from_str(num_qubits, block_size, as_str.as_str(), offset)?.map(Ok), + )); + } else if let Ok(list) = entanglement.downcast::() { + 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::, _>>()?; + + if connections.len() != block_size as usize { + return Err(QiskitError::new_err(format!( + "Entanglement {:?} does not match block size {}", + connections, block_size + ))); + } + Ok(connections) + }); + return Ok(Box::new(entanglement_iter)); + } + Err(QiskitError::new_err( + "Entanglement must be a string or list of qubit indices.", + )) +} + +/// Get the entanglement for given number of qubits and block size. +/// +/// Args: +/// num_qubits: The number of qubits to entangle. +/// block_size: The entanglement block size (e.g. 2 for CX or 3 for CCX). +/// entanglement: The entanglement strategy. This can be one of: +/// +/// * string: Available options are ``"full"``, ``"linear"``, ``"pairwise"`` +/// ``"reverse_linear"``, ``"circular"``, or ``"sca"``. +/// * list of tuples: A list of entanglements given as tuple, e.g. [(0, 1), (1, 2)]. +/// * callable: A callable that takes as input an offset as ``int`` (usually the layer +/// in the variational circuit) and returns a string or list of tuples to use as +/// entanglement in this layer. +/// +/// offset: An offset used by certain entanglement strategies (e.g. ``"sca"``) or if the +/// entanglement is given as callable. This is typically used to have different +/// entanglement structures in different layers of variational quantum circuits. +/// +/// Returns: +/// The entanglement as list of tuples. +/// +/// Raises: +/// QiskitError: In case the entanglement is invalid. +#[pyfunction] +#[pyo3(signature = (num_qubits, block_size, entanglement, offset=0))] +pub fn get_entangler_map<'py>( + py: Python<'py>, + num_qubits: u32, + block_size: u32, + entanglement: &Bound, + offset: usize, +) -> PyResult>> { + // The entanglement is Result>>>, so there's two + // levels of errors we must handle: the outer error is handled by the outer match statement, + // and the inner (Result>) is handled upon the PyTuple creation. + match get_entanglement(num_qubits, block_size, entanglement, offset) { + Ok(entanglement) => entanglement + .into_iter() + .map(|vec| match vec { + Ok(vec) => Ok(PyTuple::new_bound(py, vec)), + Err(e) => Err(e), + }) + .collect::, _>>(), + Err(e) => Err(e), + } +} diff --git a/crates/accelerate/src/circuit_library/mod.rs b/crates/accelerate/src/circuit_library/mod.rs new file mode 100644 index 000000000000..d0444c484dc8 --- /dev/null +++ b/crates/accelerate/src/circuit_library/mod.rs @@ -0,0 +1,21 @@ +// 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::*; + +mod entanglement; + +#[pymodule] +pub fn circuit_library(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(entanglement::get_entangler_map))?; + Ok(()) +} diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 4e079ea84b57..5414183d22cc 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -14,6 +14,7 @@ use std::env; use pyo3::import_exception; +pub mod circuit_library; pub mod convert_2q_block_matrix; pub mod dense_layout; pub mod edge_collections; diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 04b0c0609347..e8971bc87629 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -13,14 +13,14 @@ use pyo3::prelude::*; use qiskit_accelerate::{ - convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, - error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer, - isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, - pauli_exp_val::pauli_expval, results::results, sabre::sabre, sampled_exp_val::sampled_exp_val, - sparse_pauli_op::sparse_pauli_op, star_prerouting::star_prerouting, - stochastic_swap::stochastic_swap, synthesis::synthesis, target_transpiler::target, - two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, - vf2_layout::vf2_layout, + circuit_library::circuit_library, convert_2q_block_matrix::convert_2q_block_matrix, + dense_layout::dense_layout, error_map::error_map, + euler_one_qubit_decomposer::euler_one_qubit_decomposer, isometry::isometry, nlayout::nlayout, + optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval, results::results, + sabre::sabre, sampled_exp_val::sampled_exp_val, sparse_pauli_op::sparse_pauli_op, + star_prerouting::star_prerouting, stochastic_swap::stochastic_swap, synthesis::synthesis, + target_transpiler::target, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, + utils::utils, vf2_layout::vf2_layout, }; #[inline(always)] @@ -39,6 +39,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, qiskit_circuit::circuit, "circuit")?; add_submodule(m, qiskit_qasm2::qasm2, "qasm2")?; add_submodule(m, qiskit_qasm3::qasm3, "qasm3")?; + add_submodule(m, circuit_library, "circuit_library")?; add_submodule(m, convert_2q_block_matrix, "convert_2q_block_matrix")?; add_submodule(m, dense_layout, "dense_layout")?; add_submodule(m, error_map, "error_map")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 6a8df393307e..33933fd8fd7e 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -60,6 +60,7 @@ # We manually define them on import so people can directly import qiskit._accelerate.* submodules # and not have to rely on attribute access. No action needed for top-level extension packages. sys.modules["qiskit._accelerate.circuit"] = _accelerate.circuit +sys.modules["qiskit._accelerate.circuit_library"] = _accelerate.circuit_library sys.modules["qiskit._accelerate.convert_2q_block_matrix"] = _accelerate.convert_2q_block_matrix sys.modules["qiskit._accelerate.dense_layout"] = _accelerate.dense_layout sys.modules["qiskit._accelerate.error_map"] = _accelerate.error_map diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index 2a750195dab3..f948a458ad7b 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -31,9 +31,11 @@ ) 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 ..blueprintcircuit import BlueprintCircuit + if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import @@ -1037,51 +1039,11 @@ def get_entangler_map( Raises: ValueError: If the entanglement mode ist not supported. """ - n, m = num_circuit_qubits, num_block_qubits - if m > n: - raise ValueError( - "The number of block qubits must be smaller or equal to the number of " - "qubits in the circuit." - ) - - if entanglement == "pairwise" and num_block_qubits > 2: - raise ValueError("Pairwise entanglement is not defined for blocks with more than 2 qubits.") - - if entanglement == "full": - return list(itertools.combinations(list(range(n)), m)) - elif entanglement == "reverse_linear": - # reverse linear connectivity. In the case of m=2 and the entanglement_block='cx' - # then it's equivalent to 'full' entanglement - reverse = [tuple(range(n - i - m, n - i)) for i in range(n - m + 1)] - return reverse - elif entanglement in ["linear", "circular", "sca", "pairwise"]: - linear = [tuple(range(i, i + m)) for i in range(n - m + 1)] - # if the number of block qubits is 1, we don't have to add the 'circular' part - if entanglement == "linear" or m == 1: - return linear - - if entanglement == "pairwise": - return linear[::2] + linear[1::2] - - # circular equals linear plus top-bottom entanglement (if there's space for it) - if n > m: - circular = [tuple(range(n - m + 1, n)) + (0,)] + linear - else: - circular = linear - if entanglement == "circular": - return circular - - # sca is circular plus shift and reverse - shifted = circular[-offset:] + circular[:-offset] - if offset % 2 == 1: # if odd, reverse the qubit indices - sca = [ind[::-1] for ind in shifted] - else: - sca = shifted - - return sca - - else: - raise ValueError(f"Unsupported entanglement type: {entanglement}") + try: + return fast_entangler_map(num_circuit_qubits, num_block_qubits, entanglement, offset) + except Exception as exc: + # need this as Rust is now raising a QiskitError, where this function was raising ValueError + raise ValueError("Something went wrong in Rust space, here's the error:") from exc _StdlibGateResult = collections.namedtuple("_StdlibGateResult", ("gate", "num_params")) diff --git a/releasenotes/notes/fix-circular-entanglement-5aadd5adf75c0c13.yaml b/releasenotes/notes/fix-circular-entanglement-5aadd5adf75c0c13.yaml new file mode 100644 index 000000000000..fbede3d31756 --- /dev/null +++ b/releasenotes/notes/fix-circular-entanglement-5aadd5adf75c0c13.yaml @@ -0,0 +1,16 @@ +fixes: + - | + Fixed a bug with the ``"circular"`` and ``"sca"`` entanglement for + :class:`.NLocal` circuits and its derivatives. For entanglement blocks + of more than 2 qubits, the circular entanglement was previously missing + some connections. For example, for 4 qubits and a block size of 3 the + code previously used:: + + [(2, 3, 0), (0, 1, 2), (1, 2, 3)] + + but now is correctly adding the ``(3, 0, 1)`` connections, that is:: + + [(2, 3, 0), (3, 0, 1), (0, 1, 2), (1, 2, 3)] + + As such, the ``"circular"`` and ``"sca"`` entanglements use ``num_qubits`` + entangling blocks per layer. diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py index 2e308af48ff4..dd39aa61ba61 100644 --- a/test/python/circuit/library/test_nlocal.py +++ b/test/python/circuit/library/test_nlocal.py @@ -42,6 +42,10 @@ from qiskit.circuit.random.utils import random_circuit from qiskit.converters.circuit_to_dag import circuit_to_dag from qiskit.quantum_info import Operator +from qiskit.exceptions import QiskitError + +from qiskit._accelerate.circuit_library import get_entangler_map as fast_entangler_map + from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -339,7 +343,7 @@ def get_expected_entangler_map(rep_num, mode): (2, 3, 4), ] else: - circular = [(3, 4, 0), (0, 1, 2), (1, 2, 3), (2, 3, 4)] + circular = [(3, 4, 0), (4, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 4)] if mode == "circular": return circular sca = circular[-rep_num:] + circular[:-rep_num] @@ -928,5 +932,156 @@ def test_full_vs_reverse_linear(self, num_qubits): self.assertEqual(Operator(full), Operator(reverse)) +@ddt +class TestEntanglement(QiskitTestCase): + """Test getting the entanglement structure.""" + + @data( + ("linear", [(0, 1), (1, 2), (2, 3)]), + ("reverse_linear", [(2, 3), (1, 2), (0, 1)]), + ("pairwise", [(0, 1), (2, 3), (1, 2)]), + ("circular", [(3, 0), (0, 1), (1, 2), (2, 3)]), + ("full", [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), + ) + @unpack + def test_2q_str(self, strategy, expected): + """Test getting by string.""" + entanglement = fast_entangler_map( + num_qubits=4, block_size=2, entanglement=strategy, offset=0 + ) + self.assertEqual(expected, entanglement) + + @data( + ("linear", [(0, 1, 2), (1, 2, 3)]), + ("reverse_linear", [(1, 2, 3), (0, 1, 2)]), + ("circular", [(2, 3, 0), (3, 0, 1), (0, 1, 2), (1, 2, 3)]), + ("full", [(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)]), + ) + @unpack + def test_3q_str(self, strategy, expected): + """Test getting by string.""" + entanglement = fast_entangler_map( + num_qubits=4, block_size=3, entanglement=strategy, offset=0 + ) + self.assertEqual(expected, entanglement) + + def test_2q_sca(self): + """Test shift, circular, alternating on 2-qubit blocks.""" + expected = { # offset: result + 0: [(3, 0), (0, 1), (1, 2), (2, 3)], + 1: [(3, 2), (0, 3), (1, 0), (2, 1)], + 2: [(1, 2), (2, 3), (3, 0), (0, 1)], + 3: [(1, 0), (2, 1), (3, 2), (0, 3)], + } + for offset in range(8): + with self.subTest(offset=offset): + entanglement = fast_entangler_map( + num_qubits=4, block_size=2, entanglement="sca", offset=offset + ) + self.assertEqual(expected[offset % 4], entanglement) + + def test_3q_sca(self): + """Test shift, circular, alternating on 3-qubit blocks.""" + circular = [(2, 3, 0), (3, 0, 1), (0, 1, 2), (1, 2, 3)] + for offset in range(8): + expected = circular[-(offset % 4) :] + circular[: -(offset % 4)] + if offset % 2 == 1: + expected = [tuple(reversed(indices)) for indices in expected] + with self.subTest(offset=offset): + entanglement = fast_entangler_map( + num_qubits=4, block_size=3, entanglement="sca", offset=offset + ) + self.assertEqual(expected, entanglement) + + @data("full", "reverse_linear", "linear", "circular", "sca", "pairwise") + def test_0q(self, entanglement): + """Test the corner case of a single qubit block.""" + entanglement = fast_entangler_map( + num_qubits=3, block_size=0, entanglement=entanglement, offset=0 + ) + expect = [] + self.assertEqual(entanglement, expect) + + @data("full", "reverse_linear", "linear", "circular", "sca", "pairwise") + def test_1q(self, entanglement): + """Test the corner case of a single qubit block.""" + entanglement = fast_entangler_map( + num_qubits=3, block_size=1, entanglement=entanglement, offset=0 + ) + expect = [(i,) for i in range(3)] + + self.assertEqual(set(entanglement), set(expect)) # order does not matter for 1 qubit + + @data("full", "reverse_linear", "linear", "circular", "sca") + def test_full_block(self, entanglement): + """Test the corner case of the block size equal the number of qubits.""" + entanglement = fast_entangler_map( + num_qubits=5, block_size=5, entanglement=entanglement, offset=0 + ) + expect = [tuple(range(5))] + + self.assertEqual(entanglement, expect) + + def test_pairwise_limit(self): + """Test pairwise raises an error above 2 qubits.""" + _ = fast_entangler_map(num_qubits=4, block_size=1, entanglement="pairwise", offset=0) + _ = fast_entangler_map(num_qubits=4, block_size=2, entanglement="pairwise", offset=0) + with self.assertRaises(QiskitError): + _ = fast_entangler_map(num_qubits=4, block_size=3, entanglement="pairwise", offset=0) + + def test_invalid_blocksize(self): + """Test the block size being too large.""" + with self.assertRaises(QiskitError): + _ = fast_entangler_map(num_qubits=2, block_size=3, entanglement="linear", offset=0) + + def test_invalid_entanglement_str(self): + """Test invalid entanglement string.""" + with self.assertRaises(QiskitError): + _ = fast_entangler_map(num_qubits=4, block_size=2, entanglement="lniaer", offset=0) + + def test_as_list(self): + """Test passing a list just returns the list.""" + expected = [(0, 1), (1, 10), (2, 10)] + out = fast_entangler_map(num_qubits=20, block_size=2, entanglement=expected, offset=0) + self.assertEqual(expected, out) + + def test_invalid_list(self): + """Test passing a list that does not match the block size.""" + + # TODO this test fails, somehow the error is not propagated correctly! + expected = [(0, 1), (1, 2, 10)] + with self.assertRaises(QiskitError): + _ = fast_entangler_map(num_qubits=20, block_size=2, entanglement=expected, offset=0) + + def test_callable_list(self): + """Test using a callable.""" + + def my_entanglement(offset): + return [(0, 1)] if offset % 2 == 0 else [(1, 2)] + + for offset in range(3): + with self.subTest(offset=offset): + expect = my_entanglement(offset) + result = fast_entangler_map( + num_qubits=3, block_size=2, entanglement=my_entanglement, offset=offset + ) + self.assertEqual(expect, result) + + def test_callable_str(self): + """Test using a callable.""" + + def my_entanglement(offset): + return "linear" if offset % 2 == 0 else "pairwise" + + expected = {"linear": [(0, 1), (1, 2), (2, 3)], "pairwise": [(0, 1), (2, 3), (1, 2)]} + + for offset in range(3): + with self.subTest(offset=offset): + result = fast_entangler_map( + num_qubits=4, block_size=2, entanglement=my_entanglement, offset=offset + ) + self.assertEqual(expected["linear" if offset % 2 == 0 else "pairwise"], result) + + if __name__ == "__main__": unittest.main()