diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index d00d17644b83..9e7d4cbddf72 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -24,6 +24,7 @@ mod euler_one_qubit_decomposer; mod nlayout; mod optimize_1q_gates; mod pauli_exp_val; +mod quantum_circuit; mod results; mod sabre_layout; mod sabre_swap; @@ -52,6 +53,7 @@ fn _accelerate(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(sabre_swap::sabre_swap))?; m.add_wrapped(wrap_pymodule!(pauli_exp_val::pauli_expval))?; m.add_wrapped(wrap_pymodule!(dense_layout::dense_layout))?; + m.add_wrapped(wrap_pymodule!(quantum_circuit::quantum_circuit))?; m.add_wrapped(wrap_pymodule!(error_map::error_map))?; m.add_wrapped(wrap_pymodule!(sparse_pauli_op::sparse_pauli_op))?; m.add_wrapped(wrap_pymodule!(results::results))?; diff --git a/crates/accelerate/src/quantum_circuit/circuit_data.rs b/crates/accelerate/src/quantum_circuit/circuit_data.rs new file mode 100644 index 000000000000..9b7011cbc682 --- /dev/null +++ b/crates/accelerate/src/quantum_circuit/circuit_data.rs @@ -0,0 +1,657 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2023 +// +// 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::quantum_circuit::circuit_instruction::CircuitInstruction; +use crate::quantum_circuit::intern_context::{BitType, IndexType, InternContext}; +use crate::quantum_circuit::py_ext; +use hashbrown::HashMap; +use pyo3::exceptions::{PyIndexError, PyKeyError, PyRuntimeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::{PyIterator, PyList, PySlice, PyTuple, PyType}; +use pyo3::{PyObject, PyResult, PyTraverseError, PyVisit}; +use std::hash::{Hash, Hasher}; + +/// Private type used to store instructions with interned arg lists. +#[derive(Clone, Debug)] +struct PackedInstruction { + /// The Python-side operation instance. + op: PyObject, + /// The index under which the interner has stored `qubits`. + qubits_id: IndexType, + /// The index under which the interner has stored `clbits`. + clbits_id: IndexType, +} + +/// Private wrapper for Python-side Bit instances that implements +/// [Hash] and [Eq], allowing them to be used in Rust hash-based +/// sets and maps. +/// +/// Python's `hash()` is called on the wrapped Bit instance during +/// construction and returned from Rust's [Hash] trait impl. +/// The impl of [PartialEq] first compares the native Py pointers +/// to determine equality. If these are not equal, only then does +/// it call `repr()` on both sides, which has a significant +/// performance advantage. +#[derive(Clone, Debug)] +struct BitAsKey { + /// Python's `hash()` of the wrapped instance. + hash: isize, + /// The wrapped instance. + bit: PyObject, +} + +impl BitAsKey { + fn new(bit: &PyAny) -> PyResult { + Ok(BitAsKey { + hash: bit.hash()?, + bit: bit.into_py(bit.py()), + }) + } +} + +impl Hash for BitAsKey { + fn hash(&self, state: &mut H) { + state.write_isize(self.hash); + } +} + +impl PartialEq for BitAsKey { + fn eq(&self, other: &Self) -> bool { + self.bit.is(&other.bit) + || Python::with_gil(|py| { + self.bit + .as_ref(py) + .repr() + .unwrap() + .eq(other.bit.as_ref(py).repr().unwrap()) + .unwrap() + }) + } +} + +impl Eq for BitAsKey {} + +/// A container for :class:`.QuantumCircuit` instruction listings that stores +/// :class:`.CircuitInstruction` instances in a packed form by interning +/// their :attr:`~.CircuitInstruction.qubits` and +/// :attr:`~.CircuitInstruction.clbits` to native vectors of indices. +/// +/// Before adding a :class:`.CircuitInstruction` to this container, its +/// :class:`.Qubit` and :class:`.Clbit` instances MUST be registered via the +/// constructor or via :meth:`.CircuitData.add_qubit` and +/// :meth:`.CircuitData.add_clbit`. This is because the order in which +/// bits of the same type are added to the container determines their +/// associated indices used for storage and retrieval. +/// +/// Once constructed, this container behaves like a Python list of +/// :class:`.CircuitInstruction` instances. However, these instances are +/// created and destroyed on the fly, and thus should be treated as ephemeral. +/// +/// For example, +/// +/// .. code-block:: +/// +/// qubits = [Qubit()] +/// data = CircuitData(qubits) +/// data.append(CircuitInstruction(XGate(), (qubits[0],), ())) +/// assert(data[0] == data[0]) # => Ok. +/// assert(data[0] is data[0]) # => PANICS! +/// +/// .. warning:: +/// +/// This is an internal interface and no part of it should be relied upon +/// outside of Qiskit. +/// +/// Args: +/// qubits (Iterable[:class:`.Qubit`] | None): The initial sequence of +/// qubits, used to map :class:`.Qubit` instances to and from its +/// indices. +/// clbits (Iterable[:class:`.Clbit`] | None): The initial sequence of +/// clbits, used to map :class:`.Clbit` instances to and from its +/// indices. +/// data (Iterable[:class:`.CircuitInstruction`]): An initial instruction +/// listing to add to this container. All bits appearing in the +/// instructions in this iterable must also exist in ``qubits`` and +/// ``clbits``. +/// reserve (int): The container's initial capacity. This is reserved +/// before copying instructions into the container when ``data`` +/// is provided, so the initialized container's unused capacity will +/// be ``max(0, reserve - len(data))``. +/// +/// Raises: +/// KeyError: if ``data`` contains a reference to a bit that is not present +/// in ``qubits`` or ``clbits``. +#[pyclass(sequence, module = "qiskit._accelerate.quantum_circuit")] +#[derive(Clone, Debug)] +pub struct CircuitData { + /// The packed instruction listing. + data: Vec, + /// The intern context used to intern instruction bits. + intern_context: InternContext, + /// The qubits registered (e.g. through :meth:`~.CircuitData.add_qubit`). + qubits_native: Vec, + /// The clbits registered (e.g. through :meth:`~.CircuitData.add_clbit`). + clbits_native: Vec, + /// Map of :class:`.Qubit` instances to their index in + /// :attr:`.CircuitData.qubits`. + qubit_indices_native: HashMap, + /// Map of :class:`.Clbit` instances to their index in + /// :attr:`.CircuitData.clbits`. + clbit_indices_native: HashMap, + /// The qubits registered, cached as a ``list[Qubit]``. + qubits: Py, + /// The clbits registered, cached as a ``list[Clbit]``. + clbits: Py, +} + +/// A private enumeration type used to extract arguments to pymethods +/// that may be either an index or a slice. +#[derive(FromPyObject)] +pub enum SliceOrInt<'a> { + Slice(&'a PySlice), + Int(isize), +} + +#[pymethods] +impl CircuitData { + #[new] + #[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0))] + pub fn new( + py: Python<'_>, + qubits: Option<&PyAny>, + clbits: Option<&PyAny>, + data: Option<&PyAny>, + reserve: usize, + ) -> PyResult { + let mut self_ = CircuitData { + data: Vec::new(), + intern_context: InternContext::new(), + qubits_native: Vec::new(), + clbits_native: Vec::new(), + qubit_indices_native: HashMap::new(), + clbit_indices_native: HashMap::new(), + qubits: PyList::empty(py).into_py(py), + clbits: PyList::empty(py).into_py(py), + }; + if let Some(qubits) = qubits { + for bit in qubits.iter()? { + self_.add_qubit(py, bit?)?; + } + } + if let Some(clbits) = clbits { + for bit in clbits.iter()? { + self_.add_clbit(py, bit?)?; + } + } + if let Some(data) = data { + self_.reserve(py, reserve); + self_.extend(py, data)?; + } + Ok(self_) + } + + pub fn __reduce__(self_: &PyCell, py: Python<'_>) -> PyResult { + let ty: &PyType = self_.get_type(); + let args = { + let self_ = self_.borrow(); + ( + self_.qubits.clone_ref(py), + self_.clbits.clone_ref(py), + None::<()>, + self_.data.len(), + ) + }; + Ok((ty, args, None::<()>, self_.iter()?).into_py(py)) + } + + /// Returns the current sequence of registered :class:`.Qubit` + /// instances as a list. + /// + /// .. note:: + /// + /// This list is not kept in sync with the container. + /// + /// Returns: + /// list(:class:`.Qubit`): The current sequence of registered qubits. + #[getter] + pub fn qubits(&self, py: Python<'_>) -> PyObject { + PyList::new(py, self.qubits.as_ref(py)).into_py(py) + } + + /// Returns the current sequence of registered :class:`.Clbit` + /// instances as a list. + /// + /// .. note:: + /// + /// This list is not kept in sync with the container. + /// + /// Returns: + /// list(:class:`.Clbit`): The current sequence of registered clbits. + #[getter] + pub fn clbits(&self, py: Python<'_>) -> PyObject { + PyList::new(py, self.clbits.as_ref(py)).into_py(py) + } + + /// Registers a :class:`.Qubit` instance. + /// + /// Args: + /// bit (:class:`.Qubit`): The qubit to register. + pub fn add_qubit(&mut self, py: Python<'_>, bit: &PyAny) -> PyResult<()> { + let idx: BitType = self.qubits_native.len().try_into().map_err(|_| { + PyRuntimeError::new_err( + "The number of qubits in the circuit has exceeded the maximum capacity", + ) + })?; + self.qubit_indices_native.insert(BitAsKey::new(bit)?, idx); + self.qubits_native.push(bit.into_py(py)); + self.qubits = PyList::new(py, &self.qubits_native).into_py(py); + Ok(()) + } + + /// Registers a :class:`.Clbit` instance. + /// + /// Args: + /// bit (:class:`.Clbit`): The clbit to register. + pub fn add_clbit(&mut self, py: Python<'_>, bit: &PyAny) -> PyResult<()> { + let idx: BitType = self.clbits_native.len().try_into().map_err(|_| { + PyRuntimeError::new_err( + "The number of clbits in the circuit has exceeded the maximum capacity", + ) + })?; + self.clbit_indices_native.insert(BitAsKey::new(bit)?, idx); + self.clbits_native.push(bit.into_py(py)); + self.clbits = PyList::new(py, &self.clbits_native).into_py(py); + Ok(()) + } + + /// Performs a shallow copy. + /// + /// Returns: + /// CircuitData: The shallow copy. + pub fn copy(&self, py: Python<'_>) -> PyResult { + Ok(CircuitData { + data: self.data.clone(), + intern_context: self.intern_context.clone(), + qubits_native: self.qubits_native.clone(), + clbits_native: self.clbits_native.clone(), + qubit_indices_native: self.qubit_indices_native.clone(), + clbit_indices_native: self.clbit_indices_native.clone(), + qubits: PyList::new(py, &self.qubits_native).into_py(py), + clbits: PyList::new(py, &self.clbits_native).into_py(py), + }) + } + + /// Reserves capacity for at least ``additional`` more + /// :class:`.CircuitInstruction` instances to be added to this container. + /// + /// Args: + /// additional (int): The additional capacity to reserve. If the + /// capacity is already sufficient, does nothing. + pub fn reserve(&mut self, _py: Python<'_>, additional: usize) { + self.data.reserve(additional); + } + + pub fn __len__(&self) -> usize { + self.data.len() + } + + // Note: we also rely on this to make us iterable! + pub fn __getitem__<'py>(&self, py: Python<'py>, index: &PyAny) -> PyResult { + // Internal helper function to get a specific + // instruction by index. + fn get_at( + self_: &CircuitData, + py: Python<'_>, + index: isize, + ) -> PyResult> { + let index = self_.convert_py_index(index)?; + if let Some(inst) = self_.data.get(index) { + self_.unpack(py, inst) + } else { + Err(PyIndexError::new_err(format!( + "No element at index {:?} in circuit data", + index + ))) + } + } + + if index.is_exact_instance_of::() { + let slice = self.convert_py_slice(index.downcast_exact::()?)?; + let result = slice + .into_iter() + .map(|i| get_at(self, py, i)) + .collect::>>()?; + Ok(result.into_py(py)) + } else { + Ok(get_at(self, py, index.extract()?)?.into_py(py)) + } + } + + pub fn __delitem__(&mut self, py: Python<'_>, index: SliceOrInt) -> PyResult<()> { + match index { + SliceOrInt::Slice(slice) => { + let slice = { + let mut s = self.convert_py_slice(slice)?; + if s.len() > 1 && s.first().unwrap() < s.last().unwrap() { + // Reverse the order so we're sure to delete items + // at the back first (avoids messing up indices). + s.reverse() + } + s + }; + for i in slice.into_iter() { + self.__delitem__(py, SliceOrInt::Int(i))?; + } + Ok(()) + } + SliceOrInt::Int(index) => { + let index = self.convert_py_index(index)?; + if self.data.get(index).is_some() { + self.data.remove(index); + Ok(()) + } else { + Err(PyIndexError::new_err(format!( + "No element at index {:?} in circuit data", + index + ))) + } + } + } + } + + pub fn __setitem__( + &mut self, + py: Python<'_>, + index: SliceOrInt, + value: &PyAny, + ) -> PyResult<()> { + match index { + SliceOrInt::Slice(slice) => { + let indices = slice.indices(self.data.len().try_into().unwrap())?; + let slice = self.convert_py_slice(slice)?; + let values = value.iter()?.collect::>>()?; + if indices.step != 1 && slice.len() != values.len() { + // A replacement of a different length when step isn't exactly '1' + // would result in holes. + return Err(PyValueError::new_err(format!( + "attempt to assign sequence of size {:?} to extended slice of size {:?}", + values.len(), + slice.len(), + ))); + } + + for (i, v) in slice.iter().zip(values.iter()) { + self.__setitem__(py, SliceOrInt::Int(*i), *v)?; + } + + if slice.len() > values.len() { + // Delete any extras. + let slice = PySlice::new( + py, + indices.start + values.len() as isize, + indices.stop, + 1isize, + ); + self.__delitem__(py, SliceOrInt::Slice(slice))?; + } else { + // Insert any extra values. + for v in values.iter().skip(slice.len()).rev() { + let v: PyRef = v.extract()?; + self.insert(py, indices.stop, v)?; + } + } + + Ok(()) + } + SliceOrInt::Int(index) => { + let index = self.convert_py_index(index)?; + let value: PyRef = value.extract()?; + let mut packed = self.pack(py, value)?; + std::mem::swap(&mut packed, &mut self.data[index]); + Ok(()) + } + } + } + + pub fn insert( + &mut self, + py: Python<'_>, + index: isize, + value: PyRef, + ) -> PyResult<()> { + let index = self.convert_py_index_clamped(index); + let packed = self.pack(py, value)?; + self.data.insert(index, packed); + Ok(()) + } + + pub fn pop(&mut self, py: Python<'_>, index: Option) -> PyResult { + let index = + index.unwrap_or_else(|| std::cmp::max(0, self.data.len() as isize - 1).into_py(py)); + let item = self.__getitem__(py, index.as_ref(py))?; + self.__delitem__(py, index.as_ref(py).extract()?)?; + Ok(item) + } + + pub fn append(&mut self, py: Python<'_>, value: PyRef) -> PyResult<()> { + let packed = self.pack(py, value)?; + self.data.push(packed); + Ok(()) + } + + // To prevent the entire iterator from being loaded into memory, + // we create a `GILPool` for each iteration of the loop, which + // ensures that the `CircuitInstruction` returned by the call + // to `next` is dropped before the next iteration. + pub fn extend(&mut self, py: Python<'_>, itr: &PyAny) -> PyResult<()> { + // To ensure proper lifetime management, we explicitly store + // the result of calling `iter(itr)` as a GIL-independent + // reference that we access only with the most recent GILPool. + // It would be dangerous to access the original `itr` or any + // GIL-dependent derivatives of it after creating the new pool. + let itr: Py = itr.iter()?.into_py(py); + loop { + // Create a new pool, so that PyO3 can clear memory at + // the end of the loop. + let pool = unsafe { py.new_pool() }; + + // It is recommended to *always* immediately set py to the pool's + // Python, to help avoid creating references with invalid lifetimes. + let py = pool.python(); + + // Access the iterator using the new pool. + match itr.as_ref(py).next() { + None => { + break; + } + Some(v) => { + self.append(py, v?.extract()?)?; + } + } + // The GILPool is dropped here, which cleans up the ref + // returned from `next` as well as any resources used by + // `self.append`. + } + Ok(()) + } + + pub fn clear(&mut self, _py: Python<'_>) -> PyResult<()> { + std::mem::take(&mut self.data); + Ok(()) + } + + // Marks this pyclass as NOT hashable. + #[classattr] + const __hash__: Option> = None; + + fn __eq__(slf: &PyCell, other: &PyAny) -> PyResult { + let slf: &PyAny = slf; + if slf.is(other) { + return Ok(true); + } + if slf.len()? != other.len()? { + return Ok(false); + } + // Implemented using generic iterators on both sides + // for simplicity. + let mut ours_itr = slf.iter()?; + let mut theirs_itr = other.iter()?; + loop { + match (ours_itr.next(), theirs_itr.next()) { + (Some(ours), Some(theirs)) => { + if !ours?.eq(theirs?)? { + return Ok(false); + } + } + (None, None) => { + return Ok(true); + } + _ => { + return Ok(false); + } + } + } + } + + fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + for packed in self.data.iter() { + visit.call(&packed.op)?; + } + for bit in self.qubits_native.iter().chain(self.clbits_native.iter()) { + visit.call(bit)?; + } + + // Note: + // There's no need to visit the native Rust data + // structures used for internal tracking: the only Python + // references they contain are to the bits in these lists! + visit.call(&self.qubits)?; + visit.call(&self.clbits)?; + Ok(()) + } + + fn __clear__(&mut self) { + // Clear anything that could have a reference cycle. + self.data.clear(); + self.qubits_native.clear(); + self.clbits_native.clear(); + self.qubit_indices_native.clear(); + self.clbit_indices_native.clear(); + } +} + +impl CircuitData { + /// Converts a Python slice to a `Vec` of indices into + /// the instruction listing, [CircuitData.data]. + fn convert_py_slice(&self, slice: &PySlice) -> PyResult> { + let indices = slice.indices(self.data.len().try_into().unwrap())?; + if indices.step > 0 { + Ok((indices.start..indices.stop) + .step_by(indices.step as usize) + .collect()) + } else { + let mut out = Vec::with_capacity(indices.slicelength as usize); + let mut x = indices.start; + while x > indices.stop { + out.push(x); + x += indices.step; + } + Ok(out) + } + } + + /// Converts a Python index to an index into the instruction listing, + /// or one past its end. + /// If the resulting index would be < 0, clamps to 0. + /// If the resulting index would be > len(data), clamps to len(data). + fn convert_py_index_clamped(&self, index: isize) -> usize { + let index = if index < 0 { + index + self.data.len() as isize + } else { + index + }; + std::cmp::min(std::cmp::max(0, index), self.data.len() as isize) as usize + } + + /// Converts a Python index to an index into the instruction listing. + fn convert_py_index(&self, index: isize) -> PyResult { + let index = if index < 0 { + index + self.data.len() as isize + } else { + index + }; + + if index < 0 || index >= self.data.len() as isize { + return Err(PyIndexError::new_err(format!( + "Index {:?} is out of bounds.", + index, + ))); + } + Ok(index as usize) + } + + /// Returns a [PackedInstruction] containing the original operation + /// of `elem` and [InternContext] indices of its `qubits` and `clbits` + /// fields. + fn pack( + &mut self, + py: Python<'_>, + inst: PyRef, + ) -> PyResult { + let mut interned_bits = + |indices: &HashMap, bits: &PyTuple| -> PyResult { + let args = bits + .into_iter() + .map(|b| { + let key = BitAsKey::new(b)?; + indices.get(&key).copied().ok_or_else(|| { + PyKeyError::new_err(format!( + "Bit {:?} has not been added to this circuit.", + b + )) + }) + }) + .collect::>>()?; + self.intern_context.intern(args) + }; + Ok(PackedInstruction { + op: inst.operation.clone_ref(py), + qubits_id: interned_bits(&self.qubit_indices_native, inst.qubits.as_ref(py))?, + clbits_id: interned_bits(&self.clbit_indices_native, inst.clbits.as_ref(py))?, + }) + } + + fn unpack(&self, py: Python<'_>, inst: &PackedInstruction) -> PyResult> { + Py::new( + py, + CircuitInstruction { + operation: inst.op.clone_ref(py), + qubits: py_ext::tuple_new( + py, + self.intern_context + .lookup(inst.qubits_id) + .iter() + .map(|i| self.qubits_native[*i as usize].clone_ref(py)) + .collect(), + ), + clbits: py_ext::tuple_new( + py, + self.intern_context + .lookup(inst.clbits_id) + .iter() + .map(|i| self.clbits_native[*i as usize].clone_ref(py)) + .collect(), + ), + }, + ) + } +} diff --git a/crates/accelerate/src/quantum_circuit/circuit_instruction.rs b/crates/accelerate/src/quantum_circuit/circuit_instruction.rs new file mode 100644 index 000000000000..0bf84a362c3f --- /dev/null +++ b/crates/accelerate/src/quantum_circuit/circuit_instruction.rs @@ -0,0 +1,254 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2023 +// +// 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::quantum_circuit::py_ext; +use pyo3::basic::CompareOp; +use pyo3::prelude::*; +use pyo3::types::{PyList, PyTuple}; +use pyo3::{PyObject, PyResult}; + +/// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and +/// various operands. +/// +/// .. note:: +/// +/// There is some possible confusion in the names of this class, :class:`~.circuit.Instruction`, +/// and :class:`~.circuit.Operation`, and this class's attribute :attr:`operation`. Our +/// preferred terminology is by analogy to assembly languages, where an "instruction" is made up +/// of an "operation" and its "operands". +/// +/// Historically, :class:`~.circuit.Instruction` came first, and originally contained the qubits +/// it operated on and any parameters, so it was a true "instruction". Over time, +/// :class:`.QuantumCircuit` became responsible for tracking qubits and clbits, and the class +/// became better described as an "operation". Changing the name of such a core object would be +/// a very unpleasant API break for users, and so we have stuck with it. +/// +/// This class was created to provide a formal "instruction" context object in +/// :class:`.QuantumCircuit.data`, which had long been made of ad-hoc tuples. With this, and +/// the advent of the :class:`~.circuit.Operation` interface for adding more complex objects to +/// circuits, we took the opportunity to correct the historical naming. For the time being, +/// this leads to an awkward case where :attr:`.CircuitInstruction.operation` is often an +/// :class:`~.circuit.Instruction` instance (:class:`~.circuit.Instruction` implements the +/// :class:`.Operation` interface), but as the :class:`.Operation` interface gains more use, +/// this confusion will hopefully abate. +/// +/// .. warning:: +/// +/// This is a lightweight internal class and there is minimal error checking; you must respect +/// the type hints when using it. It is the user's responsibility to ensure that direct +/// mutations of the object do not invalidate the types, nor the restrictions placed on it by +/// its context. Typically this will mean, for example, that :attr:`qubits` must be a sequence +/// of distinct items, with no duplicates. +#[pyclass( + freelist = 20, + sequence, + get_all, + module = "qiskit._accelerate.quantum_circuit" +)] +#[derive(Clone, Debug)] +pub struct CircuitInstruction { + /// The logical operation that this instruction represents an execution of. + pub operation: PyObject, + /// A sequence of the qubits that the operation is applied to. + pub qubits: Py, + /// A sequence of the classical bits that this operation reads from or writes to. + pub clbits: Py, +} + +#[pymethods] +impl CircuitInstruction { + #[new] + pub fn new( + py: Python<'_>, + operation: PyObject, + qubits: Option<&PyAny>, + clbits: Option<&PyAny>, + ) -> PyResult { + fn as_tuple(py: Python<'_>, seq: Option<&PyAny>) -> PyResult> { + match seq { + None => Ok(py_ext::tuple_new_empty(py)), + Some(seq) => { + if seq.is_instance_of::() { + Ok(seq.downcast_exact::()?.into_py(py)) + } else if seq.is_instance_of::() { + let seq = seq.downcast_exact::()?; + Ok(py_ext::tuple_from_list(seq)) + } else { + // New tuple from iterable. + Ok(py_ext::tuple_new( + py, + seq.iter()? + .map(|o| Ok(o?.into_py(py))) + .collect::>>()?, + )) + } + } + } + } + + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + }) + } + + /// Returns a shallow copy. + /// + /// Returns: + /// CircuitInstruction: The shallow copy. + pub fn copy(&self) -> Self { + self.clone() + } + + /// Creates a shallow copy with the given fields replaced. + /// + /// Returns: + /// CircuitInstruction: A new instance with the given fields replaced. + pub fn replace( + &self, + py: Python<'_>, + operation: Option, + qubits: Option<&PyAny>, + clbits: Option<&PyAny>, + ) -> PyResult { + CircuitInstruction::new( + py, + operation.unwrap_or_else(|| self.operation.clone_ref(py)), + Some(qubits.unwrap_or_else(|| self.qubits.as_ref(py))), + Some(clbits.unwrap_or_else(|| self.clbits.as_ref(py))), + ) + } + + fn __getstate__(&self, py: Python<'_>) -> PyObject { + ( + self.operation.as_ref(py), + self.qubits.as_ref(py), + self.clbits.as_ref(py), + ) + .into_py(py) + } + + fn __setstate__(&mut self, _py: Python<'_>, state: &PyTuple) -> PyResult<()> { + self.operation = state.get_item(0)?.extract()?; + self.qubits = state.get_item(1)?.extract()?; + self.clbits = state.get_item(2)?.extract()?; + Ok(()) + } + + pub fn __getnewargs__(&self, py: Python<'_>) -> PyResult { + Ok(( + self.operation.as_ref(py), + self.qubits.as_ref(py), + self.clbits.as_ref(py), + ) + .into_py(py)) + } + + pub fn __repr__(self_: &PyCell, py: Python<'_>) -> PyResult { + let type_name = self_.get_type().name()?; + let r = self_.try_borrow()?; + Ok(format!( + "{}(\ + operation={}\ + , qubits={}\ + , clbits={}\ + )", + type_name, + r.operation.as_ref(py).repr()?, + r.qubits.as_ref(py).repr()?, + r.clbits.as_ref(py).repr()? + )) + } + + // Legacy tuple-like interface support. + // + // For a best attempt at API compatibility during the transition to using this new class, we need + // the interface to behave exactly like the old 3-tuple `(inst, qargs, cargs)` if it's treated + // like that via unpacking or similar. That means that the `parameters` field is completely + // absent, and the qubits and clbits must be converted to lists. + pub fn _legacy_format(&self, py: Python<'_>) -> PyObject { + PyTuple::new( + py, + [ + self.operation.as_ref(py), + self.qubits.as_ref(py).to_list(), + self.clbits.as_ref(py).to_list(), + ], + ) + .into_py(py) + } + + pub fn __getitem__(&self, py: Python<'_>, key: &PyAny) -> PyResult { + Ok(self + ._legacy_format(py) + .as_ref(py) + .get_item(key)? + .into_py(py)) + } + + pub fn __iter__(&self, py: Python<'_>) -> PyResult { + Ok(self._legacy_format(py).as_ref(py).iter()?.into_py(py)) + } + + pub fn __len__(&self) -> usize { + 3 + } + + pub fn __richcmp__( + self_: &PyCell, + other: &PyAny, + op: CompareOp, + py: Python<'_>, + ) -> PyResult { + fn eq( + py: Python<'_>, + self_: &PyCell, + other: &PyAny, + ) -> PyResult> { + if self_.is(other) { + return Ok(Some(true)); + } + + let self_ = self_.try_borrow()?; + if other.is_instance_of::() { + let other: PyResult<&PyCell> = other.extract(); + return other.map_or(Ok(Some(false)), |v| { + let v = v.try_borrow()?; + Ok(Some( + self_.clbits.as_ref(py).eq(v.clbits.as_ref(py))? + && self_.qubits.as_ref(py).eq(v.qubits.as_ref(py))? + && self_.operation.as_ref(py).eq(v.operation.as_ref(py))?, + )) + }); + } + + if other.is_instance_of::() { + return Ok(Some(self_._legacy_format(py).as_ref(py).eq(other)?)); + } + + Ok(None) + } + + match op { + CompareOp::Eq => eq(py, self_, other).map(|r| { + r.map(|b| b.into_py(py)) + .unwrap_or_else(|| py.NotImplemented()) + }), + CompareOp::Ne => eq(py, self_, other).map(|r| { + r.map(|b| (!b).into_py(py)) + .unwrap_or_else(|| py.NotImplemented()) + }), + _ => Ok(py.NotImplemented()), + } + } +} diff --git a/crates/accelerate/src/quantum_circuit/intern_context.rs b/crates/accelerate/src/quantum_circuit/intern_context.rs new file mode 100644 index 000000000000..0c8b596e6dd8 --- /dev/null +++ b/crates/accelerate/src/quantum_circuit/intern_context.rs @@ -0,0 +1,71 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2023 +// +// 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 hashbrown::HashMap; +use pyo3::exceptions::PyRuntimeError; +use pyo3::PyResult; +use std::sync::Arc; + +pub type IndexType = u32; +pub type BitType = u32; + +/// A Rust-only data structure (not a pyclass!) for interning +/// `Vec`. +/// +/// Takes ownership of vectors given to [InternContext.intern] +/// and returns an [IndexType] index that can be used to look up +/// an _equivalent_ sequence by reference via [InternContext.lookup]. +#[derive(Clone, Debug)] +pub struct InternContext { + slots: Vec>>, + slot_lookup: HashMap>, IndexType>, +} + +impl InternContext { + pub fn new() -> Self { + InternContext { + slots: Vec::new(), + slot_lookup: HashMap::new(), + } + } + + /// Takes `args` by reference and returns an index that can be used + /// to obtain a reference to an equivalent sequence of `BitType` by + /// calling [CircuitData.lookup]. + pub fn intern(&mut self, args: Vec) -> PyResult { + if let Some(slot_idx) = self.slot_lookup.get(&args) { + return Ok(*slot_idx); + } + + let args = Arc::new(args); + let slot_idx: IndexType = self + .slots + .len() + .try_into() + .map_err(|_| PyRuntimeError::new_err("InternContext capacity exceeded!"))?; + self.slots.push(args.clone()); + self.slot_lookup.insert_unique_unchecked(args, slot_idx); + Ok(slot_idx) + } + + /// Returns the sequence corresponding to `slot_idx`, which must + /// be a value returned by [InternContext.intern]. + pub fn lookup(&self, slot_idx: IndexType) -> &[BitType] { + self.slots.get(slot_idx as usize).unwrap() + } +} + +impl Default for InternContext { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/accelerate/src/quantum_circuit/mod.rs b/crates/accelerate/src/quantum_circuit/mod.rs new file mode 100644 index 000000000000..4f4b56865034 --- /dev/null +++ b/crates/accelerate/src/quantum_circuit/mod.rs @@ -0,0 +1,25 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2023 +// +// 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. + +pub mod circuit_data; +pub mod circuit_instruction; +pub mod intern_context; +mod py_ext; + +use pyo3::prelude::*; + +#[pymodule] +pub fn quantum_circuit(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/crates/accelerate/src/quantum_circuit/py_ext.rs b/crates/accelerate/src/quantum_circuit/py_ext.rs new file mode 100644 index 000000000000..da27764a7f4e --- /dev/null +++ b/crates/accelerate/src/quantum_circuit/py_ext.rs @@ -0,0 +1,45 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2023 +// +// 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. +//! Contains helper functions for creating [Py] (GIL-independent) +//! objects without creating an intermediate owned reference. These functions +//! are faster than PyO3's list and tuple factory methods when the caller +//! doesn't need to dereference the newly constructed object (i.e. if the +//! resulting [Py] will simply be stored in a Rust struct). +//! +//! The reason this is faster is because PyO3 tracks owned references and +//! will perform deallocation when the active [GILPool] goes out of scope. +//! If we don't need to dereference the [Py], then we can skip the +//! tracking and deallocation. + +use pyo3::ffi::Py_ssize_t; +use pyo3::prelude::*; +use pyo3::types::{PyList, PyTuple}; +use pyo3::{ffi, AsPyPointer, PyNativeType}; + +pub fn tuple_new(py: Python<'_>, items: Vec) -> Py { + unsafe { + let ptr = ffi::PyTuple_New(items.len() as Py_ssize_t); + let tup: Py = Py::from_owned_ptr(py, ptr); + for (i, obj) in items.into_iter().enumerate() { + ffi::PyTuple_SetItem(ptr, i as Py_ssize_t, obj.into_ptr()); + } + tup + } +} + +pub fn tuple_new_empty(py: Python<'_>) -> Py { + unsafe { Py::from_owned_ptr(py, ffi::PyTuple_New(0)) } +} + +pub fn tuple_from_list(list: &PyList) -> Py { + unsafe { Py::from_owned_ptr(list.py(), ffi::PyList_AsTuple(list.as_ptr())) } +} diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 20f2e3c80851..92b376a478c7 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -26,6 +26,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.nlayout"] = qiskit._accelerate.nlayout +sys.modules["qiskit._accelerate.quantum_circuit"] = qiskit._accelerate.quantum_circuit sys.modules["qiskit._accelerate.stochastic_swap"] = qiskit._accelerate.stochastic_swap sys.modules["qiskit._accelerate.sabre_swap"] = qiskit._accelerate.sabre_swap sys.modules["qiskit._accelerate.sabre_layout"] = qiskit._accelerate.sabre_layout diff --git a/qiskit/circuit/controlflow/if_else.py b/qiskit/circuit/controlflow/if_else.py index 60d18cacbbc5..775432fc671e 100644 --- a/qiskit/circuit/controlflow/if_else.py +++ b/qiskit/circuit/controlflow/if_else.py @@ -448,7 +448,7 @@ def __enter__(self): raise CircuitError("Cannot attach an 'else' to a broadcasted 'if' block.") appended = appended_instructions[0] instruction = circuit._peek_previous_instruction_in_scope() - if appended is not instruction: + if appended.operation is not instruction.operation: raise CircuitError( "The 'if' block is not the most recent instruction in the circuit." f" Expected to find: {appended!r}, but instead found: {instruction!r}." diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index 2b1a3b756de6..cf3954ba4894 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -15,6 +15,8 @@ """ from __future__ import annotations + +from collections.abc import MutableSequence from typing import Callable from qiskit.circuit.exceptions import CircuitError @@ -52,7 +54,9 @@ def __init__( # pylint: disable=bad-docstring-quotes used. It may throw an error if the resource is not valid for usage. """ - self._instructions: list[CircuitInstruction] = [] + self._instructions: list[ + CircuitInstruction | (MutableSequence[CircuitInstruction], int) + ] = [] self._requester = resource_requester def __len__(self): @@ -61,7 +65,11 @@ def __len__(self): def __getitem__(self, i): """Return instruction at index""" - return self._instructions[i] + inst = self._instructions[i] + if isinstance(inst, CircuitInstruction): + return inst + data, idx = inst + return data[idx] def add(self, instruction, qargs=None, cargs=None): """Add an instruction and its context (where it is attached).""" @@ -73,10 +81,22 @@ def add(self, instruction, qargs=None, cargs=None): instruction = CircuitInstruction(instruction, tuple(qargs), tuple(cargs)) self._instructions.append(instruction) + def _add_ref(self, data: MutableSequence[CircuitInstruction], pos: int): + """Add a reference to an instruction and its context within a mutable sequence. + Updates to the instruction set will modify the specified sequence in place.""" + self._instructions.append((data, pos)) + def inverse(self): """Invert all instructions.""" for i, instruction in enumerate(self._instructions): - self._instructions[i] = instruction.replace(operation=instruction.operation.inverse()) + if isinstance(instruction, CircuitInstruction): + self._instructions[i] = instruction.replace( + operation=instruction.operation.inverse() + ) + else: + data, idx = instruction + instruction = data[idx] + data[idx] = instruction.replace(operation=instruction.operation.inverse()) return self def c_if(self, classical: Clbit | ClassicalRegister | int, val: int) -> "InstructionSet": @@ -132,26 +152,40 @@ def c_if(self, classical: Clbit | ClassicalRegister | int, val: int) -> "Instruc if self._requester is not None: classical = self._requester(classical) for instruction in self._instructions: - instruction.operation = instruction.operation.c_if(classical, val) + if isinstance(instruction, CircuitInstruction): + updated = instruction.operation.c_if(classical, val) + if updated is not instruction.operation: + raise CircuitError( + "SingletonGate instances can only be added to InstructionSet via _add_ref" + ) + else: + data, idx = instruction + instruction = data[idx] + data[idx] = instruction.replace( + operation=instruction.operation.c_if(classical, val) + ) return self # Legacy support for properties. Added in Terra 0.21 to support the internal switch in # `QuantumCircuit.data` from the 3-tuple to `CircuitInstruction`. + def _instructions_iter(self): + return (i if isinstance(i, CircuitInstruction) else i[0][i[1]] for i in self._instructions) + @property def instructions(self): """Legacy getter for the instruction components of an instruction set. This does not support mutation.""" - return [instruction.operation for instruction in self._instructions] + return [instruction.operation for instruction in self._instructions_iter()] @property def qargs(self): """Legacy getter for the qargs components of an instruction set. This does not support mutation.""" - return [list(instruction.qubits) for instruction in self._instructions] + return [list(instruction.qubits) for instruction in self._instructions_iter()] @property def cargs(self): """Legacy getter for the cargs components of an instruction set. This does not support mutation.""" - return [list(instruction.clbits) for instruction in self._instructions] + return [list(instruction.clbits) for instruction in self._instructions_iter()] diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py index 415ae8ecd117..6006fdb6ae9d 100644 --- a/qiskit/circuit/library/blueprintcircuit.py +++ b/qiskit/circuit/library/blueprintcircuit.py @@ -15,6 +15,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from qiskit._accelerate.quantum_circuit import CircuitData from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.circuit.parametertable import ParameterTable, ParameterView @@ -32,12 +33,13 @@ class BlueprintCircuit(QuantumCircuit, ABC): def __init__(self, *regs, name: str | None = None) -> None: """Create a new blueprint circuit.""" + self._is_initialized = False super().__init__(*regs, name=name) self._qregs: list[QuantumRegister] = [] self._cregs: list[ClassicalRegister] = [] - self._qubits = [] self._qubit_indices = {} self._is_built = False + self._is_initialized = True @abstractmethod def _check_configuration(self, raise_on_failure: bool = True) -> bool: @@ -65,7 +67,7 @@ def _build(self) -> None: def _invalidate(self) -> None: """Invalidate the current circuit build.""" - self._data = [] + self._data = CircuitData(self._data.qubits, self._data.clbits) self._parameter_table = ParameterTable() self.global_phase = 0 self._is_built = False @@ -78,13 +80,19 @@ def qregs(self): @qregs.setter def qregs(self, qregs): """Set the quantum registers associated with the circuit.""" + if not self._is_initialized: + # Workaround to ignore calls from QuantumCircuit.__init__() which + # doesn't expect 'qregs' to be an overridden property! + return self._qregs = [] - self._qubits = [] self._ancillas = [] self._qubit_indices = {} + self._data = CircuitData(clbits=self._data.clbits) + self._parameter_table = ParameterTable() + self.global_phase = 0 + self._is_built = False self.add_register(*qregs) - self._invalidate() @property def data(self): diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 76dc11ea55b6..8668abf6f738 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -37,6 +37,7 @@ overload, ) import numpy as np +from qiskit._accelerate.quantum_circuit import CircuitData from qiskit.exceptions import QiskitError from qiskit.utils.multiprocessing import is_main_process from qiskit.circuit.instruction import Instruction @@ -229,9 +230,6 @@ def __init__( self.name = name self._increment_instances() - # Data contains a list of instructions and their contexts, - # in the order they were applied. - self._data: list[CircuitInstruction] = [] self._op_start_times = None # A stack to hold the instruction sets that are being built up during for-, if- and @@ -246,8 +244,6 @@ def __init__( self.qregs: list[QuantumRegister] = [] self.cregs: list[ClassicalRegister] = [] - self._qubits: list[Qubit] = [] - self._clbits: list[Clbit] = [] # Dict mapping Qubit or Clbit instances to tuple comprised of 0) the # corresponding index in circuit.{qubits,clbits} and 1) a list of @@ -256,6 +252,10 @@ def __init__( self._qubit_indices: dict[Qubit, BitLocations] = {} self._clbit_indices: dict[Clbit, BitLocations] = {} + # Data contains a list of instructions and their contexts, + # in the order they were applied. + self._data: CircuitData = CircuitData() + self._ancillas: list[AncillaQubit] = [] self._calibrations: DefaultDict[str, dict[tuple, Any]] = defaultdict(dict) self.add_register(*regs) @@ -386,8 +386,11 @@ def data(self, data_input: Iterable): """ # If data_input is QuantumCircuitData(self), clearing self._data # below will also empty data_input, so make a shallow copy first. - data_input = list(data_input) - self._data = [] + if isinstance(data_input, CircuitData): + data_input = data_input.copy() + else: + data_input = list(data_input) + self._data.clear() self._parameter_table = ParameterTable() if not data_input: return @@ -501,6 +504,28 @@ def __eq__(self, other) -> bool: other, copy_operations=False ) + def __deepcopy__(self, memo=None): + # This is overridden to minimize memory pressure when we don't + # actually need to pickle (i.e. the typical deepcopy case). + # Note: + # This is done here instead of in CircuitData since PyO3 + # doesn't include a native way to recursively call + # copy.deepcopy(memo). + cls = self.__class__ + result = cls.__new__(cls) + for k in self.__dict__.keys() - {"_data"}: + setattr(result, k, copy.deepcopy(self.__dict__[k], memo)) + + # Avoids pulling self._data into a Python list + # like we would when pickling. + result._data = CircuitData( + copy.deepcopy(self._data.qubits, memo), + copy.deepcopy(self._data.clbits, memo), + (i.replace(operation=copy.deepcopy(i.operation, memo)) for i in self._data), + reserve=len(self._data), + ) + return result + @classmethod def _increment_instances(cls): cls.instances += 1 @@ -905,7 +930,7 @@ def compose( clbits = self.clbits[: other.num_clbits] if front: # Need to keep a reference to the data for use after we've emptied it. - old_data = list(dest.data) + old_data = dest._data.copy() dest.clear() dest.append(other, qubits, clbits) for instruction in old_data: @@ -948,7 +973,7 @@ def compose( variable_mapper = _classical_resource_map.VariableMapper( dest.cregs, edge_map, dest.add_register ) - mapped_instrs: list[CircuitInstruction] = [] + mapped_instrs: CircuitData = CircuitData(dest.qubits, dest.clbits, reserve=len(other.data)) for instr in other.data: n_qargs: list[Qubit] = [edge_map[qarg] for qarg in instr.qubits] n_cargs: list[Clbit] = [edge_map[carg] for carg in instr.clbits] @@ -961,7 +986,7 @@ def compose( if front: # adjust new instrs before original ones and update all parameters - mapped_instrs += dest.data + mapped_instrs.extend(dest._data) dest.clear() append = dest._control_flow_scopes[-1].append if dest._control_flow_scopes else dest._append for instr in mapped_instrs: @@ -1072,14 +1097,14 @@ def qubits(self) -> list[Qubit]: """ Returns a list of quantum bits in the order that the registers were added. """ - return self._qubits + return self._data.qubits @property def clbits(self) -> list[Clbit]: """ Returns a list of classical bits in the order that the registers were added. """ - return self._clbits + return self._data.clbits @property def ancillas(self) -> list[AncillaQubit]: @@ -1195,7 +1220,7 @@ def _resolve_classical_resource(self, specifier): return specifier if isinstance(specifier, int): try: - return self._clbits[specifier] + return self._data.clbits[specifier] except IndexError: raise CircuitError(f"Classical bit index {specifier} is out-of-range.") from None raise CircuitError(f"Unknown classical resource specifier: '{specifier}'.") @@ -1274,27 +1299,29 @@ def append( expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []] if self._control_flow_scopes: + circuit_data = self._control_flow_scopes[-1].instructions appender = self._control_flow_scopes[-1].append requester = self._control_flow_scopes[-1].request_classical_resource else: + circuit_data = self._data appender = self._append requester = self._resolve_classical_resource instructions = InstructionSet(resource_requester=requester) if isinstance(operation, Instruction): for qarg, carg in operation.broadcast_arguments(expanded_qargs, expanded_cargs): self._check_dups(qarg) - instruction = CircuitInstruction(operation, qarg, carg) - appender(instruction) - instructions.add(instruction) + data_idx = len(circuit_data) + appender(CircuitInstruction(operation, qarg, carg)) + instructions._add_ref(circuit_data, data_idx) else: # For Operations that are non-Instructions, we use the Instruction's default method for qarg, carg in Instruction.broadcast_arguments( operation, expanded_qargs, expanded_cargs ): self._check_dups(qarg) - instruction = CircuitInstruction(operation, qarg, carg) - appender(instruction) - instructions.add(instruction) + data_idx = len(circuit_data) + appender(CircuitInstruction(operation, qarg, carg)) + instructions._add_ref(circuit_data, data_idx) return instructions # Preferred new style. @@ -1431,9 +1458,9 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: if bit in self._qubit_indices: self._qubit_indices[bit].registers.append((register, idx)) else: - self._qubits.append(bit) + self._data.add_qubit(bit) self._qubit_indices[bit] = BitLocations( - len(self._qubits) - 1, [(register, idx)] + len(self._data.qubits) - 1, [(register, idx)] ) elif isinstance(register, ClassicalRegister): @@ -1443,9 +1470,9 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: if bit in self._clbit_indices: self._clbit_indices[bit].registers.append((register, idx)) else: - self._clbits.append(bit) + self._data.add_clbit(bit) self._clbit_indices[bit] = BitLocations( - len(self._clbits) - 1, [(register, idx)] + len(self._data.clbits) - 1, [(register, idx)] ) elif isinstance(register, list): @@ -1463,11 +1490,11 @@ def add_bits(self, bits: Iterable[Bit]) -> None: if isinstance(bit, AncillaQubit): self._ancillas.append(bit) if isinstance(bit, Qubit): - self._qubits.append(bit) - self._qubit_indices[bit] = BitLocations(len(self._qubits) - 1, []) + self._data.add_qubit(bit) + self._qubit_indices[bit] = BitLocations(len(self._data.qubits) - 1, []) elif isinstance(bit, Clbit): - self._clbits.append(bit) - self._clbit_indices[bit] = BitLocations(len(self._clbits) - 1, []) + self._data.add_clbit(bit) + self._clbit_indices[bit] = BitLocations(len(self._data.clbits) - 1, []) else: raise CircuitError( "Expected an instance of Qubit, Clbit, or " @@ -2096,10 +2123,11 @@ def copy(self, name: str | None = None) -> "QuantumCircuit": } ) - cpy._data = [ + cpy._data.reserve(len(self._data)) + cpy._data.extend( instruction.replace(operation=operation_copies[id(instruction.operation)]) for instruction in self._data - ] + ) return cpy @@ -2125,14 +2153,12 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": # copy registers correctly, in copy.copy they are only copied via reference cpy.qregs = self.qregs.copy() cpy.cregs = self.cregs.copy() - cpy._qubits = self._qubits.copy() cpy._ancillas = self._ancillas.copy() - cpy._clbits = self._clbits.copy() cpy._qubit_indices = self._qubit_indices.copy() cpy._clbit_indices = self._clbit_indices.copy() cpy._parameter_table = ParameterTable() - cpy._data = [] + cpy._data = CircuitData(self._data.qubits, self._data.clbits) cpy._calibrations = copy.deepcopy(self._calibrations) cpy._metadata = copy.deepcopy(self._metadata) @@ -2365,13 +2391,16 @@ def remove_final_measurements(self, inplace: bool = True) -> Optional["QuantumCi # Filter only cregs/clbits still in new DAG, preserving original circuit order cregs_to_add = [creg for creg in circ.cregs if creg in kept_cregs] - clbits_to_add = [clbit for clbit in circ._clbits if clbit in kept_clbits] + clbits_to_add = [clbit for clbit in circ._data.clbits if clbit in kept_clbits] # Clear cregs and clbits circ.cregs = [] - circ._clbits = [] circ._clbit_indices = {} + # Clear instruction info + circ._data = CircuitData(qubits=circ._data.qubits, reserve=len(circ._data)) + circ._parameter_table.clear() + # We must add the clbits first to preserve the original circuit # order. This way, add_register never adds clbits and just # creates registers that point to them. @@ -2379,10 +2408,6 @@ def remove_final_measurements(self, inplace: bool = True) -> Optional["QuantumCi for creg in cregs_to_add: circ.add_register(creg) - # Clear instruction info - circ.data.clear() - circ._parameter_table.clear() - # Set circ instructions to match the new DAG for node in new_dag.topological_op_nodes(): # Get arguments for classical condition (if any) diff --git a/qiskit/circuit/quantumcircuitdata.py b/qiskit/circuit/quantumcircuitdata.py index e1e61873bed6..9500cdf9425d 100644 --- a/qiskit/circuit/quantumcircuitdata.py +++ b/qiskit/circuit/quantumcircuitdata.py @@ -14,131 +14,15 @@ QuantumCircuit.data while maintaining the interface of a python list.""" from collections.abc import MutableSequence -from typing import Tuple, Iterable, Optional + +import qiskit._accelerate.quantum_circuit from .exceptions import CircuitError from .instruction import Instruction from .operation import Operation -from .quantumregister import Qubit -from .classicalregister import Clbit - - -class CircuitInstruction: - """A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and - various operands. - - .. note:: - - There is some possible confusion in the names of this class, :class:`~.circuit.Instruction`, - and :class:`~.circuit.Operation`, and this class's attribute :attr:`operation`. Our - preferred terminology is by analogy to assembly languages, where an "instruction" is made up - of an "operation" and its "operands". - - Historically, :class:`~.circuit.Instruction` came first, and originally contained the qubits - it operated on and any parameters, so it was a true "instruction". Over time, - :class:`.QuantumCircuit` became responsible for tracking qubits and clbits, and the class - became better described as an "operation". Changing the name of such a core object would be - a very unpleasant API break for users, and so we have stuck with it. - - This class was created to provide a formal "instruction" context object in - :class:`.QuantumCircuit.data`, which had long been made of ad-hoc tuples. With this, and - the advent of the :class:`~.circuit.Operation` interface for adding more complex objects to - circuits, we took the opportunity to correct the historical naming. For the time being, - this leads to an awkward case where :attr:`.CircuitInstruction.operation` is often an - :class:`~.circuit.Instruction` instance (:class:`~.circuit.Instruction` implements the - :class:`.Operation` interface), but as the :class:`.Operation` interface gains more use, - this confusion will hopefully abate. - - .. warning:: - - This is a lightweight internal class and there is minimal error checking; you must respect - the type hints when using it. It is the user's responsibility to ensure that direct - mutations of the object do not invalidate the types, nor the restrictions placed on it by - its context. Typically this will mean, for example, that :attr:`qubits` must be a sequence - of distinct items, with no duplicates. - """ - - __slots__ = ("operation", "qubits", "clbits") - - operation: Operation - """The logical operation that this instruction represents an execution of.""" - qubits: Tuple[Qubit, ...] - """A sequence of the qubits that the operation is applied to.""" - clbits: Tuple[Clbit, ...] - """A sequence of the classical bits that this operation reads from or writes to.""" - - def __init__( - self, - operation: Operation, - qubits: Iterable[Qubit] = (), - clbits: Iterable[Clbit] = (), - ): - self.operation = operation - self.qubits = tuple(qubits) - self.clbits = tuple(clbits) - - def copy(self) -> "CircuitInstruction": - """Return a shallow copy of the :class:`CircuitInstruction`.""" - return self.__class__( - operation=self.operation, - qubits=self.qubits, - clbits=self.clbits, - ) - - def replace( - self, - operation: Optional[Operation] = None, - qubits: Optional[Iterable[Qubit]] = None, - clbits: Optional[Iterable[Clbit]] = None, - ) -> "CircuitInstruction": - """Return a new :class:`CircuitInstruction` with the given fields replaced.""" - return self.__class__( - operation=self.operation if operation is None else operation, - qubits=self.qubits if qubits is None else qubits, - clbits=self.clbits if clbits is None else clbits, - ) - - def __repr__(self): - return ( - f"{type(self).__name__}(" - f"operation={self.operation!r}" - f", qubits={self.qubits!r}" - f", clbits={self.clbits!r}" - ")" - ) - - def __eq__(self, other): - if isinstance(other, type(self)): - # Ordered from fastest comparisons to slowest. - return ( - self.clbits == other.clbits - and self.qubits == other.qubits - and self.operation == other.operation - ) - if isinstance(other, tuple): - return self._legacy_format() == other - return NotImplemented - # Legacy tuple-like interface support. - # - # For a best attempt at API compatibility during the transition to using this new class, we need - # the interface to behave exactly like the old 3-tuple `(inst, qargs, cargs)` if it's treated - # like that via unpacking or similar. That means that the `parameters` field is completely - # absent, and the qubits and clbits must be converted to lists. - def _legacy_format(self): - # The qubits and clbits were generally stored as lists in the old format, and various - # places assume that they will certainly be lists. - return (self.operation, list(self.qubits), list(self.clbits)) - - def __getitem__(self, key): - return self._legacy_format()[key] - - def __iter__(self): - return iter(self._legacy_format()) - - def __len__(self): - return 3 +CircuitInstruction = qiskit._accelerate.quantum_circuit.CircuitInstruction class QuantumCircuitData(MutableSequence): @@ -192,7 +76,7 @@ def _resolve_legacy_value(self, operation, qargs, cargs) -> CircuitInstruction: return CircuitInstruction(operation, tuple(qargs), tuple(cargs)) def insert(self, index, value): - self._circuit._data.insert(index, None) + self._circuit._data.insert(index, CircuitInstruction(None, (), ())) try: self[index] = value except CircuitError: @@ -209,42 +93,46 @@ def __len__(self): return len(self._circuit._data) def __cast(self, other): - return other._circuit._data if isinstance(other, QuantumCircuitData) else other + return list(other._circuit._data) if isinstance(other, QuantumCircuitData) else other def __repr__(self): - return repr(self._circuit._data) + return repr(list(self._circuit._data)) def __lt__(self, other): - return self._circuit._data < self.__cast(other) + return list(self._circuit._data) < self.__cast(other) def __le__(self, other): - return self._circuit._data <= self.__cast(other) + return list(self._circuit._data) <= self.__cast(other) def __eq__(self, other): return self._circuit._data == self.__cast(other) def __gt__(self, other): - return self._circuit._data > self.__cast(other) + return list(self._circuit._data) > self.__cast(other) def __ge__(self, other): - return self._circuit._data >= self.__cast(other) + return list(self._circuit._data) >= self.__cast(other) def __add__(self, other): - return self._circuit._data + self.__cast(other) + return list(self._circuit._data) + self.__cast(other) def __radd__(self, other): - return self.__cast(other) + self._circuit._data + return self.__cast(other) + list(self._circuit._data) def __mul__(self, n): - return self._circuit._data * n + return list(self._circuit._data) * n def __rmul__(self, n): - return n * self._circuit._data + return n * list(self._circuit._data) def sort(self, *args, **kwargs): """In-place stable sort. Accepts arguments of list.sort.""" - self._circuit._data.sort(*args, **kwargs) + data = list(self._circuit._data) + data.sort(*args, **kwargs) + self._circuit._data.clear() + self._circuit._data.reserve(len(data)) + self._circuit._data.extend(data) def copy(self): """Returns a shallow copy of instruction list.""" - return self._circuit._data.copy() + return list(self._circuit._data) diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index 793362281b5e..58323e07d0f6 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -100,32 +100,29 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None qubit_map = {bit: q[idx] for idx, bit in enumerate(circuit.qubits)} clbit_map = {bit: c[idx] for idx, bit in enumerate(circuit.clbits)} - definition = [ - instruction.replace( + qc = QuantumCircuit(*regs, name=out_instruction.name) + qc._data.reserve(len(target.data)) + for instruction in target._data: + rule = instruction.replace( qubits=[qubit_map[y] for y in instruction.qubits], clbits=[clbit_map[y] for y in instruction.clbits], ) - for instruction in target.data - ] - # fix condition - for rule in definition: + # fix condition condition = getattr(rule.operation, "condition", None) if condition: reg, val = condition if isinstance(reg, Clbit): - rule.operation = rule.operation.c_if(clbit_map[reg], val) + rule = rule.replace(operation=rule.operation.c_if(clbit_map[reg], val)) elif reg.size == c.size: - rule.operation = rule.operation.c_if(c, val) + rule = rule.replace(operation=rule.operation.c_if(c, val)) else: raise QiskitError( "Cannot convert condition in circuit with " "multiple classical registers to instruction" ) + qc._append(rule) - qc = QuantumCircuit(*regs, name=out_instruction.name) - for instruction in definition: - qc._append(instruction) if circuit.global_phase: qc.global_phase = circuit.global_phase diff --git a/qiskit/opflow/gradients/circuit_gradients/lin_comb.py b/qiskit/opflow/gradients/circuit_gradients/lin_comb.py index cd658837331c..44394b874618 100644 --- a/qiskit/opflow/gradients/circuit_gradients/lin_comb.py +++ b/qiskit/opflow/gradients/circuit_gradients/lin_comb.py @@ -533,7 +533,8 @@ def apply_grad_gate( qr_superpos_qubits = tuple(qr_superpos) # copy the input circuit taking the gates by reference out = QuantumCircuit(*circuit.qregs) - out._data = circuit._data.copy() + out._data.reserve(len(circuit._data)) + out._data.extend(circuit._data) out._parameter_table = ParameterTable( {param: values.copy() for param, values in circuit._parameter_table.items()} ) diff --git a/releasenotes/notes/bit-interning-35da0aaa76aa7fc5.yaml b/releasenotes/notes/bit-interning-35da0aaa76aa7fc5.yaml new file mode 100644 index 000000000000..bb1c83f76f33 --- /dev/null +++ b/releasenotes/notes/bit-interning-35da0aaa76aa7fc5.yaml @@ -0,0 +1,17 @@ +--- +upgrade: + - | + To support a more compact in-memory representation, the + :class:`.QuantumCircuit` class is now limited to supporting + a maximum of ``2^32 (=4,294,967,296)`` qubits and clbits, + for each of these two bit types (the limit is not combined). + The number of unique sequences of indices used in + :attr:`.CircuitInstruction.qubits` and + :attr:`.CircuitInstruction.clbits` is also limited to ``2^32`` + for instructions added to a single circuit. +other: + - | + The :class:`.QuantumCircuit` class now performs interning for the + ``qubits`` and ``clbits`` of the :class:`.CircuitInstruction` + instances that it stores, resulting in a potentially significant + reduction in memory footprint, especially for large circuits. diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index d90d953902f7..45f2c1639c40 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -11,14 +11,178 @@ # that they have been altered from the originals. """Test operations on circuit.data.""" - -from qiskit.circuit import QuantumCircuit, QuantumRegister, Parameter, CircuitInstruction, Operation +import ddt +from qiskit._accelerate.quantum_circuit import CircuitData + +from qiskit.circuit import ( + ClassicalRegister, + QuantumCircuit, + QuantumRegister, + Parameter, + CircuitInstruction, + Operation, + Qubit, +) from qiskit.circuit.library import HGate, XGate, CXGate, RXGate from qiskit.test import QiskitTestCase from qiskit.circuit.exceptions import CircuitError +@ddt.ddt +class TestQuantumCircuitData(QiskitTestCase): + """CircuitData (Rust) operation tests.""" + + @ddt.data( + slice(0, 5, 1), # Get everything. + slice(-1, -6, -1), # Get everything, reversed. + slice(0, 4, 1), # Get subslice. + slice(0, 5, 2), # Get every other. + slice(-1, -6, -2), # Get every other, reversed. + slice(2, 2, 1), # Get nothing. + slice(2, 3, 1), # Get at index 2. + slice(4, 10, 1), # Get index 4 to end, using excessive upper bound. + slice(5, 0, -2), # Get every other, reversed, excluding index 0. + slice(-10, -5, 1), # Get nothing. + slice(0, 10, 1), # Get everything. + ) + def test_getitem_slice(self, sli): + """Test that __getitem__ with slice is equivalent to that of list.""" + qr = QuantumRegister(5) + data_list = [ + CircuitInstruction(XGate(), [qr[0]], []), + CircuitInstruction(XGate(), [qr[1]], []), + CircuitInstruction(XGate(), [qr[2]], []), + CircuitInstruction(XGate(), [qr[3]], []), + CircuitInstruction(XGate(), [qr[4]], []), + ] + data = CircuitData(qubits=list(qr), data=data_list) + self.assertEqual(data[sli], data_list[sli]) + + @ddt.data( + slice(0, 5, 1), # Delete everything. + slice(-1, -6, -1), # Delete everything, reversed. + slice(0, 4, 1), # Delete subslice. + slice(0, 5, 2), # Delete every other. + slice(-1, -6, -2), # Delete every other, reversed. + slice(2, 2, 1), # Delete nothing. + slice(2, 3, 1), # Delete at index 2. + slice(4, 10, 1), # Delete index 4 to end, excessive upper bound. + slice(5, 0, -2), # Delete every other, reversed, excluding index 0. + slice(-10, -5, 1), # Delete nothing. + slice(0, 10, 1), # Delete everything, excessive upper bound. + ) + def test_delitem_slice(self, sli): + """Test that __delitem__ with slice is equivalent to that of list.""" + qr = QuantumRegister(5) + data_list = [ + CircuitInstruction(XGate(), [qr[0]], []), + CircuitInstruction(XGate(), [qr[1]], []), + CircuitInstruction(XGate(), [qr[2]], []), + CircuitInstruction(XGate(), [qr[3]], []), + CircuitInstruction(XGate(), [qr[4]], []), + ] + data = CircuitData(qubits=list(qr), data=data_list) + + del data_list[sli] + del data[sli] + if data_list[sli] != data[sli]: + print(f"data_list: {data_list}") + print(f"data: {list(data)}") + + self.assertEqual(data[sli], data_list[sli]) + + @ddt.data( + (slice(0, 5, 1), 5), # Replace entire slice. + (slice(-1, -6, -1), 5), # Replace entire slice, reversed. + (slice(0, 4, 1), 4), # Replace subslice. + (slice(0, 4, 1), 10), # Replace subslice with bigger sequence. + (slice(0, 5, 2), 3), # Replace every other. + (slice(-1, -6, -2), 3), # Replace every other, reversed. + (slice(2, 2, 1), 1), # Insert at index 2. + (slice(2, 3, 1), 1), # Replace at index 2. + (slice(2, 3, 1), 10), # Replace at index 2 with bigger sequence. + (slice(4, 10, 1), 2), # Replace index 4 with bigger sequence, excessive upper bound. + (slice(5, 10, 1), 10), # Append sequence. + (slice(4, 0, -1), 4), # Replace subslice at end, reversed. + ) + @ddt.unpack + def test_setitem_slice(self, sli, value_length): + """Test that __setitem__ with slice is equivalent to that of list.""" + reg_size = 20 + assert value_length <= reg_size + qr = QuantumRegister(reg_size) + default_bit = Qubit() + data_list = [ + CircuitInstruction(XGate(), [default_bit], []), + CircuitInstruction(XGate(), [default_bit], []), + CircuitInstruction(XGate(), [default_bit], []), + CircuitInstruction(XGate(), [default_bit], []), + CircuitInstruction(XGate(), [default_bit], []), + ] + data = CircuitData(qubits=list(qr) + [default_bit], data=data_list) + + value = [CircuitInstruction(XGate(), [qr[i]]) for i in range(value_length)] + data_list[sli] = value + data[sli] = value + self.assertEqual(data, data_list) + + @ddt.data( + (slice(0, 5, 2), 2), # Replace smaller, with gaps. + (slice(0, 5, 2), 4), # Replace larger, with gaps. + (slice(4, 0, -1), 10), # Replace larger, reversed. + (slice(-1, -6, -1), 6), # Replace larger, reversed, negative notation. + (slice(4, 3, -1), 10), # Replace at index 4 with bigger sequence, reversed. + ) + @ddt.unpack + def test_setitem_slice_negative(self, sli, value_length): + """Test that __setitem__ with slice is equivalent to that of list.""" + reg_size = 20 + assert value_length <= reg_size + qr = QuantumRegister(reg_size) + default_bit = Qubit() + data_list = [ + CircuitInstruction(XGate(), [default_bit], []), + CircuitInstruction(XGate(), [default_bit], []), + CircuitInstruction(XGate(), [default_bit], []), + CircuitInstruction(XGate(), [default_bit], []), + CircuitInstruction(XGate(), [default_bit], []), + ] + data = CircuitData(qubits=list(qr) + [default_bit], data=data_list) + + value = [CircuitInstruction(XGate(), [qr[i]]) for i in range(value_length)] + with self.assertRaises(ValueError): + data_list[sli] = value + with self.assertRaises(ValueError): + data[sli] = value + self.assertEqual(data, data_list) + + def test_unregistered_bit_error_new(self): + """Test using foreign bits is not allowed.""" + qr = QuantumRegister(1) + cr = ClassicalRegister(1) + with self.assertRaisesRegex(KeyError, "not been added to this circuit"): + CircuitData(qr, cr, [CircuitInstruction(XGate(), [Qubit()], [])]) + + def test_unregistered_bit_error_append(self): + """Test using foreign bits is not allowed.""" + qr = QuantumRegister(1) + cr = ClassicalRegister(1) + data = CircuitData(qr, cr) + with self.assertRaisesRegex(KeyError, "not been added to this circuit"): + qr_foreign = QuantumRegister(1) + data.append(CircuitInstruction(XGate(), [qr_foreign[0]], [])) + + def test_unregistered_bit_error_set(self): + """Test using foreign bits is not allowed.""" + qr = QuantumRegister(1) + cr = ClassicalRegister(1) + data = CircuitData(qr, cr, [CircuitInstruction(XGate(), [qr[0]], [])]) + with self.assertRaisesRegex(KeyError, "not been added to this circuit"): + qr_foreign = QuantumRegister(1) + data[0] = CircuitInstruction(XGate(), [qr_foreign[0]], []) + + class TestQuantumCircuitInstructionData(QiskitTestCase): """QuantumCircuit.data operation tests.""" diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 11770d896bc6..fc5229349700 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -1287,11 +1287,11 @@ def test_pop_previous_instruction_removes_parameters(self): x, y = Parameter("x"), Parameter("y") test = QuantumCircuit(1, 1) test.rx(y, 0) - last_instructions = test.u(x, y, 0, 0) + last_instructions = list(test.u(x, y, 0, 0)) self.assertEqual({x, y}, set(test.parameters)) instruction = test._pop_previous_instruction_in_scope() - self.assertEqual(list(last_instructions), [instruction]) + self.assertEqual(last_instructions, [instruction]) self.assertEqual({y}, set(test.parameters)) def test_decompose_gate_type(self):