diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 9111f932e270..63b1a5f756be 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -31,6 +31,7 @@ pub mod isometry; pub mod nlayout; pub mod optimize_1q_gates; pub mod pauli_exp_val; +pub mod qubit_tracker; pub mod remove_diagonal_gates_before_measure; pub mod results; pub mod sabre; diff --git a/crates/accelerate/src/qubit_tracker.rs b/crates/accelerate/src/qubit_tracker.rs new file mode 100644 index 000000000000..0a77e026ccbb --- /dev/null +++ b/crates/accelerate/src/qubit_tracker.rs @@ -0,0 +1,233 @@ +// 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 crate::QiskitError; +use hashbrown::HashMap; +use hashbrown::HashSet; +use pyo3::prelude::*; +use qiskit_circuit::Qubit; + +/// Track qubits by their state +#[pyclass] +pub struct QubitTracker { + qubits: Vec, + clean: HashSet, + dirty: HashSet, +} + +#[pymethods] +impl QubitTracker { + #[new] + pub fn new(qubits: Vec, clean: HashSet, dirty: HashSet) -> Self { + QubitTracker { + qubits, + clean, + dirty, + } + } + + /// Return the number of clean qubits, excluding `active_qubits` + #[pyo3(signature = (/, active_qubits=None))] + pub fn num_clean(&self, active_qubits: Option>) -> usize { + if let Some(active_qubits) = active_qubits { + let active_qubits_as_set: HashSet = active_qubits.into_iter().collect(); + self.clean.difference(&active_qubits_as_set).count() + } else { + self.clean.len() + } + } + + /// Return the number of dirty qubits, excluding `active_qubits` + #[pyo3(signature = (/, active_qubits=None))] + pub fn num_dirty(&self, active_qubits: Option>) -> usize { + if let Some(active_qubits) = active_qubits { + let active_qubits_as_set: HashSet = active_qubits.into_iter().collect(); + self.dirty.difference(&active_qubits_as_set).count() + } else { + self.dirty.len() + } + } + + /// Set the state of `qubits` to used. + /// Returns an error when a qubit in `qubits` is untracked. + #[pyo3(signature = (qubits, /, check=false))] + pub fn used(&mut self, qubits: Vec, check: bool) -> PyResult<()> { + if check { + for q in &qubits { + if !self.qubits.contains(q) { + return Err(QiskitError::new_err(format!( + "Setting state of an untracked qubit: {}", + q.0 + ))); + } + } + } + + for q in qubits { + self.clean.remove(&q); + self.dirty.insert(q); + } + Ok(()) + } + + /// Set the state of `qubits` to `0`. + /// Returns an error when a qubit in `qubits` is untracked. + #[pyo3(signature = (qubits, /, check=false))] + pub fn reset(&mut self, qubits: Vec, check: bool) -> PyResult<()> { + if check { + for q in &qubits { + if !self.qubits.contains(q) { + return Err(QiskitError::new_err(format!( + "Setting state of an untracked qubit: {}", + q.0 + ))); + } + } + } + + for q in qubits { + self.dirty.remove(&q); + self.clean.insert(q); + } + Ok(()) + } + + /// Drops `qubits` from the tracker, making these qubits no longer available. + /// Returns an error when a qubit in `qubits` is untracked. + #[pyo3(signature = (qubits, /, check=false))] + pub fn drop(&mut self, qubits: Vec, check: bool) -> PyResult<()> { + if check { + for q in &qubits { + if !self.qubits.contains(q) { + return Err(QiskitError::new_err(format!( + "Dropping an untracked qubit: {}", + q.0 + ))); + } + } + } + + for q in qubits { + self.qubits.retain(|&x| x != q); + self.dirty.remove(&q); + self.clean.remove(&q); + } + Ok(()) + } + + /// Get `num_qubits` qubits, excluding `active_qubits`. + /// `active_qubits` may include qubits that are not part of the tracker. + #[pyo3(signature = (num_qubits, /, active_qubits=None))] + fn borrow(&self, num_qubits: usize, active_qubits: Option>) -> PyResult> { + // Compute the set of tracked active qubits + let tracked_active_qubits: HashSet = if let Some(active_qubits) = active_qubits { + active_qubits + .iter() + .filter(|q| self.qubits.contains(*q)) + .copied() + .collect() + } else { + HashSet::new() + }; + + let num_available = self.qubits.len() - tracked_active_qubits.len(); + if num_available < num_qubits { + return Err(QiskitError::new_err(format!( + "Cannot borrow {} qubits, only {} available", + num_qubits, num_available + ))); + } + + let clean_qubits = self + .qubits + .iter() + .filter(|q| !tracked_active_qubits.contains(*q)) + .filter(|q| self.clean.contains(*q)); + let dirty_qubits = self + .qubits + .iter() + .filter(|q| !tracked_active_qubits.contains(*q)) + .filter(|q| self.dirty.contains(*q)); + + let borrowed_qubits = clean_qubits + .chain(dirty_qubits) + .take(num_qubits) + .map(|q| q.0 as usize) + .collect(); + Ok(borrowed_qubits) + } + + pub fn copy( + &self, + qubit_map: Option>, + drop: Option>, + ) -> PyResult { + let (qubits, clean, dirty) = if qubit_map.is_none() && drop.is_none() { + // Copy everything + (self.qubits.clone(), self.clean.clone(), self.dirty.clone()) + } else if qubit_map.is_some() { + // Filter based on the map + let qubit_map = qubit_map.unwrap(); + let mut qubits: Vec = Vec::with_capacity(self.qubits.len()); + let mut clean: HashSet = HashSet::with_capacity(self.clean.len()); + let mut dirty: HashSet = HashSet::with_capacity(self.dirty.len()); + for (old_index, new_index) in qubit_map.iter() { + qubits.push(*new_index); + if self.clean.contains(old_index) { + clean.insert(*new_index); + } else if self.dirty.contains(old_index) { + dirty.insert(*new_index); + } else { + return Err(QiskitError::new_err(format!( + "Unknown old qubit index: {}.", + old_index.0 as usize + ))); + } + } + (qubits, clean, dirty) + } else { + // Filter based on drop + let drop = drop.unwrap(); + let mut qubits = self.qubits.clone(); + let mut clean = self.clean.clone(); + let mut dirty = self.dirty.clone(); + qubits.retain(|q| !drop.contains(q)); + clean.retain(|q| !drop.contains(q)); + dirty.retain(|q| !drop.contains(q)); + (qubits, clean, dirty) + }; + + Ok(QubitTracker { + qubits, + clean, + dirty, + }) + } + + pub fn __str__(&self) -> String { + format!( + "QubitTracker({}), clean: {}, dirty: {}\n\ + \tclean: {:?}\n\ + \tdirty: {:?}", + self.qubits.len(), + self.clean.len(), + self.dirty.len(), + self.clean, + self.dirty + ) + } +} + +pub fn qubit_tracker_mod(m: &Bound) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 6033c7c47e49..d5b209611aac 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -20,7 +20,7 @@ use qiskit_accelerate::{ euler_one_qubit_decomposer::euler_one_qubit_decomposer, filter_op_nodes::filter_op_nodes_mod, gate_direction::gate_direction, inverse_cancellation::inverse_cancellation_mod, isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, - pauli_exp_val::pauli_expval, + pauli_exp_val::pauli_expval, qubit_tracker::qubit_tracker_mod, remove_diagonal_gates_before_measure::remove_diagonal_gates_before_measure, results::results, sabre::sabre, sampled_exp_val::sampled_exp_val, sparse_pauli_op::sparse_pauli_op, split_2q_unitaries::split_2q_unitaries_mod, star_prerouting::star_prerouting, @@ -58,6 +58,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, nlayout, "nlayout")?; add_submodule(m, optimize_1q_gates, "optimize_1q_gates")?; add_submodule(m, pauli_expval, "pauli_expval")?; + add_submodule(m, qubit_tracker_mod, "qubit_tracker")?; add_submodule(m, synthesis, "synthesis")?; add_submodule( m, diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 25137d7a5918..e22cce05d98b 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -91,6 +91,7 @@ sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes +sys.modules["qiskit._accelerate.qubit_tracker"] = _accelerate.qubit_tracker from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 31036de69645..4958fa6e3fca 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -41,9 +41,9 @@ ControlModifier, PowerModifier, ) - +from qiskit._accelerate.qubit_tracker import QubitTracker from .plugin import HighLevelSynthesisPluginManager -from .qubit_tracker import QubitTracker + if typing.TYPE_CHECKING: from qiskit.dagcircuit import DAGOpNode diff --git a/qiskit/transpiler/passes/synthesis/qubit_tracker.py b/qiskit/transpiler/passes/synthesis/qubit_tracker.py deleted file mode 100644 index f3dd34b7df31..000000000000 --- a/qiskit/transpiler/passes/synthesis/qubit_tracker.py +++ /dev/null @@ -1,132 +0,0 @@ -# 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. - -"""A qubit state tracker for synthesizing operations with auxiliary qubits.""" - -from __future__ import annotations -from collections.abc import Iterable -from dataclasses import dataclass - - -@dataclass -class QubitTracker: - """Track qubits (by global index) and their state. - - The states are distinguished into clean (meaning in state :math:`|0\rangle`) or dirty (an - unknown state). - """ - - # This could in future be extended to track different state types, if necessary. - # However, using sets of integers here is much faster than e.g. storing a dictionary with - # {index: state} entries. - qubits: tuple[int] - clean: set[int] - dirty: set[int] - - def num_clean(self, active_qubits: Iterable[int] | None = None): - """Return the number of clean qubits, not considering the active qubits.""" - # this could be cached if getting the set length becomes a performance bottleneck - return len(self.clean.difference(active_qubits or set())) - - def num_dirty(self, active_qubits: Iterable[int] | None = None): - """Return the number of dirty qubits, not considering the active qubits.""" - return len(self.dirty.difference(active_qubits or set())) - - def borrow(self, num_qubits: int, active_qubits: Iterable[int] | None = None) -> list[int]: - """Get ``num_qubits`` qubits, excluding ``active_qubits``.""" - active_qubits = set(active_qubits or []) - available_qubits = [qubit for qubit in self.qubits if qubit not in active_qubits] - - if num_qubits > (available := len(available_qubits)): - raise RuntimeError(f"Cannot borrow {num_qubits} qubits, only {available} available.") - - # for now, prioritize returning clean qubits - available_clean = [qubit for qubit in available_qubits if qubit in self.clean] - available_dirty = [qubit for qubit in available_qubits if qubit in self.dirty] - - borrowed = available_clean[:num_qubits] - return borrowed + available_dirty[: (num_qubits - len(borrowed))] - - def used(self, qubits: Iterable[int], check: bool = True) -> None: - """Set the state of ``qubits`` to used (i.e. False).""" - qubits = set(qubits) - - if check: - if len(untracked := qubits.difference(self.qubits)) > 0: - raise ValueError(f"Setting state of untracked qubits: {untracked}. Tracker: {self}") - - self.clean -= qubits - self.dirty |= qubits - - def reset(self, qubits: Iterable[int], check: bool = True) -> None: - """Set the state of ``qubits`` to 0 (i.e. True).""" - qubits = set(qubits) - - if check: - if len(untracked := qubits.difference(self.qubits)) > 0: - raise ValueError(f"Setting state of untracked qubits: {untracked}. Tracker: {self}") - - self.clean |= qubits - self.dirty -= qubits - - def drop(self, qubits: Iterable[int], check: bool = True) -> None: - """Drop qubits from the tracker, meaning that they are no longer available.""" - qubits = set(qubits) - - if check: - if len(untracked := qubits.difference(self.qubits)) > 0: - raise ValueError(f"Dropping untracked qubits: {untracked}. Tracker: {self}") - - self.qubits = tuple(qubit for qubit in self.qubits if qubit not in qubits) - self.clean -= qubits - self.dirty -= qubits - - def copy( - self, qubit_map: dict[int, int] | None = None, drop: Iterable[int] | None = None - ) -> "QubitTracker": - """Copy self. - - Args: - qubit_map: If provided, apply the mapping ``{old_qubit: new_qubit}`` to - the qubits in the tracker. Only those old qubits in the mapping will be - part of the new one. - drop: If provided, drop these qubits in the copied tracker. This argument is ignored - if ``qubit_map`` is given, since the qubits can then just be dropped in the map. - """ - if qubit_map is None and drop is not None: - remaining_qubits = [qubit for qubit in self.qubits if qubit not in drop] - qubit_map = dict(zip(remaining_qubits, remaining_qubits)) - - if qubit_map is None: - clean = self.clean.copy() - dirty = self.dirty.copy() - qubits = self.qubits # tuple is immutable, no need to copy - else: - clean, dirty = set(), set() - for old_index, new_index in qubit_map.items(): - if old_index in self.clean: - clean.add(new_index) - elif old_index in self.dirty: - dirty.add(new_index) - else: - raise ValueError(f"Unknown old qubit index: {old_index}. Tracker: {self}") - - qubits = tuple(qubit_map.values()) - - return QubitTracker(qubits, clean=clean, dirty=dirty) - - def __str__(self) -> str: - return ( - f"QubitTracker({len(self.qubits)}, clean: {self.num_clean()}, dirty: {self.num_dirty()})" - + f"\n\tclean: {self.clean}" - + f"\n\tdirty: {self.dirty}" - ) diff --git a/test/python/transpiler/test_qubit_tracker.py b/test/python/transpiler/test_qubit_tracker.py new file mode 100644 index 000000000000..21bfeef11523 --- /dev/null +++ b/test/python/transpiler/test_qubit_tracker.py @@ -0,0 +1,81 @@ +# 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. + +"""Test QubitTracker functionality.""" + +import unittest + +from qiskit.exceptions import QiskitError +from qiskit._accelerate.qubit_tracker import QubitTracker + +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +class TestQubitTracker(QiskitTestCase): + """Test QubitTracker functionality.""" + + def test_basic_usage(self): + """Test basic usage: defining the tracker, calling `used` and `reset`, + querying the number of clean and dirty ancilla qubits. + """ + qubits = range(11) + clean = {0, 2, 4, 6, 8, 10} + dirty = {1, 3, 5, 7, 9} + tracker = QubitTracker(qubits, clean, dirty) + self.assertEqual(tracker.num_clean(), 6) + self.assertEqual(tracker.num_dirty(), 5) + self.assertEqual(tracker.num_clean(active_qubits=[2, 3, 4, 5]), 4) + self.assertEqual(tracker.num_dirty(active_qubits=[2, 3, 4, 5]), 3) + tracker.used([4, 3, 0]) # clean becomes {2, 6, 8, 10}, dirty becomes {0, 1, 3, 4, 5, 7, 9} + self.assertEqual(tracker.num_clean(), 4) + self.assertEqual(tracker.num_dirty(), 7) + tracker.reset([2, 5, 9]) # clean becomes {2, 5, 6, 8, 9, 10}, dirty becomes {0, 1, 3, 4, 7} + self.assertEqual(tracker.num_clean(), 6) + self.assertEqual(tracker.num_dirty(), 5) + tracker.drop([10, 0, 1, 5]) # clean becomes {2, 6, 8, 9}, dirty becomes {3, 4, 7} + self.assertEqual(tracker.num_clean(), 4) + self.assertEqual(tracker.num_dirty(), 3) + self.assertEqual(set(tracker.borrow(3)), {2, 6, 8}) + self.assertEqual(set(tracker.borrow(5)), {2, 6, 8, 9, 3}) + self.assertEqual(set(tracker.borrow(2, active_qubits=[8, 3, 2])), {6, 9}) + self.assertEqual(set(tracker.borrow(3, active_qubits=[8, 3, 2])), {6, 9, 4}) + + def test_borrow_raises(self): + """Test that `borrow` raises an exception when there are not enough available qubits.""" + tracker = QubitTracker(range(5), {0, 1, 2}, {3, 4}) + with self.assertRaises(QiskitError): + tracker.borrow(3, [0, 1, 2]) + + def test_copy(self): + """Test `copy` method.""" + tracker = QubitTracker(range(11), {0, 2, 4, 6, 8, 10}, {1, 3, 5, 7, 9}) + + with self.subTest("copy without qubit_map and without drop"): + tracker1 = tracker.copy() + self.assertEqual(tracker1.num_clean(), 6) + self.assertEqual(tracker1.num_dirty(), 5) + + with self.subTest("copy with qubit_map and without drop"): + tracker2 = tracker.copy( + qubit_map={2: 11, 4: 13, 5: 5, 7: 2} + ) # clean: {11, 13}, dirty: {2, 5} + self.assertEqual(tracker2.num_clean(), 2) + self.assertEqual(tracker2.num_dirty(), 2) + + with self.subTest("copy without qubit_map and with drop"): + tracker3 = tracker.copy(drop=[10, 2, 6, 7, 5, 3]) # clean: {0, 4, 8}, dirty: {1, 9} + self.assertEqual(tracker3.num_clean(), 3) + self.assertEqual(tracker3.num_dirty(), 2) + + +if __name__ == "__main__": + unittest.main()