diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs new file mode 100644 index 000000000000..86df610a7547 --- /dev/null +++ b/crates/circuit/src/bit.rs @@ -0,0 +1,77 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use std::fmt::Debug; + +/// Keeps information about where a bit is located within the circuit. +/// +/// This information includes whether the bit was added by a register, +/// which register it belongs to and where it is located within it. +#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)] +pub struct BitInfo { + added_by_reg: bool, + registers: Vec<BitLocation>, +} + +impl BitInfo { + pub fn new(orig_reg: Option<(u32, u32)>) -> Self { + // If the instance was added by a register, add it and prefil its locator + if let Some((reg_idx, idx)) = orig_reg { + Self { + added_by_reg: true, + registers: vec![BitLocation::new(reg_idx, idx)], + } + } else { + Self { + added_by_reg: false, + registers: vec![], + } + } + } + + /// Add a register to the bit instance + pub fn add_register(&mut self, register: u32, index: u32) { + self.registers.push(BitLocation(register, index)) + } + + /// Returns a list with all the [BitLocation] instances + pub fn get_registers(&self) -> &[BitLocation] { + &self.registers + } + + /// Returns the index of the original register if any exists + pub fn orig_register_index(&self) -> Option<&BitLocation> { + if self.added_by_reg { + Some(&self.registers[0]) + } else { + None + } + } +} + +/// Keeps information about where a qubit is located within a register. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Hash)] +pub struct BitLocation(u32, u32); + +impl BitLocation { + pub fn new(register_idx: u32, index: u32) -> Self { + Self(register_idx, index) + } + + pub fn register_index(&self) -> u32 { + self.0 + } + + pub fn index(&self) -> u32 { + self.1 + } +} diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index bf9667e44214..e21d6049adc6 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -10,13 +10,19 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use crate::BitType; +use crate::bit::{BitInfo, BitLocation}; +use crate::circuit_data::CircuitError; +use crate::imports::{CLASSICAL_REGISTER, QUANTUM_REGISTER, REGISTER}; +use crate::register::{Register, RegisterAsKey}; +use crate::{BitType, ToPyBit}; use hashbrown::HashMap; use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; use pyo3::prelude::*; -use pyo3::types::PyList; +use pyo3::types::{PyDict, PyList}; +use std::borrow::Cow; use std::fmt::Debug; use std::hash::{Hash, Hasher}; +use std::sync::{OnceLock, RwLock}; /// Private wrapper for Python-side Bit instances that implements /// [Hash] and [Eq], allowing them to be used in Rust hash-based @@ -29,7 +35,7 @@ use std::hash::{Hash, Hasher}; /// it call `repr()` on both sides, which has a significant /// performance advantage. #[derive(Clone, Debug)] -struct BitAsKey { +pub(crate) struct BitAsKey { /// Python's `hash()` of the wrapped instance. hash: isize, /// The wrapped instance. @@ -230,3 +236,712 @@ where self.bits.clear(); } } + +#[derive(Debug)] +pub struct NewBitData<T: From<BitType>, R: Register + Hash + Eq> { + /// The public field name (i.e. `qubits` or `clbits`). + description: String, + /// Registered Python bits. + bits: Vec<OnceLock<PyObject>>, + /// Maps Python bits to native type, should be modifiable upon + /// retrieval. + indices: RwLock<HashMap<BitAsKey, T>>, + /// Maps Register keys to indices + reg_keys: HashMap<RegisterAsKey, u32>, + /// Mapping between bit index and its register info + bit_info: Vec<BitInfo>, + /// Registers in the circuit + registry: Vec<R>, + /// Registers in Python + registers: Vec<OnceLock<PyObject>>, + /// Cached Python bits + cached_py_bits: OnceLock<Py<PyList>>, + /// Cached Python registers + cached_py_regs: OnceLock<Py<PyList>>, +} + +impl<T, R> NewBitData<T, R> +where + T: From<BitType> + Copy + Debug + ToPyBit, + R: Register<Bit = T> + + Hash + + Eq + + From<(usize, Option<String>)> + + for<'a> From<Cow<'a, [T]>> + + for<'a> From<(Cow<'a, [T]>, Option<String>)>, + BitType: From<T>, +{ + pub fn new(description: String) -> Self { + NewBitData { + description, + bits: Vec::new(), + indices: HashMap::new().into(), + bit_info: Vec::new(), + registry: Vec::new(), + registers: Vec::new(), + cached_py_bits: OnceLock::new(), + cached_py_regs: OnceLock::new(), + reg_keys: HashMap::new(), + } + } + + pub fn with_capacity(description: String, bit_capacity: usize, reg_capacity: usize) -> Self { + NewBitData { + description, + bits: Vec::with_capacity(bit_capacity), + indices: HashMap::with_capacity(bit_capacity).into(), + bit_info: Vec::with_capacity(bit_capacity), + registry: Vec::with_capacity(reg_capacity), + registers: Vec::with_capacity(reg_capacity), + cached_py_bits: OnceLock::new(), + cached_py_regs: OnceLock::new(), + reg_keys: HashMap::with_capacity(reg_capacity), + } + } + + /// Gets the number of bits. + pub fn len(&self) -> usize { + self.bits.len() + } + + /// Gets the number of registers. + pub fn len_regs(&self) -> usize { + self.registry.len() + } + + pub fn is_empty(&self) -> bool { + self.bits.is_empty() + } + + /// Adds a register onto the [BitData] of the circuit. + /// + /// _**Note:** If providing the ``bits`` argument, the bits must already exist in the circuit._ + pub fn add_register( + &mut self, + name: Option<String>, + size: Option<usize>, + bits: Option<Cow<'_, [T]>>, + ) -> Option<u32> { + let idx = self.registry.len().try_into().unwrap_or_else(|_| { + panic!( + "The {} registry in this circuit has reached its maximum capacity.", + self.description + ) + }); + match (size, bits) { + (None, None) => panic!("You should at least provide either a size or the bit indices."), + (None, Some(bits)) => { + if bits.is_empty() { + return None; + } + let reg: R = (bits, name).into(); + if self.reg_keys.contains_key(reg.as_key()) { + return Some(self.reg_keys[reg.as_key()]); + } + // Add register info cancel if any qubit is duplicated + for (bit_idx, bit) in reg.bits().enumerate() { + let bit_info = &mut self.bit_info[BitType::from(bit) as usize]; + bit_info.add_register( + idx, + bit_idx.try_into().unwrap_or_else(|_| { + panic!( + "The current register exceeds its capacity limit. Number of {} : {}", + self.description, + reg.len() + ) + }), + ); + } + self.reg_keys.insert(reg.as_key().clone(), idx); + self.registry.push(reg); + self.registers.push(OnceLock::new()); + Some(idx) + } + (Some(size), None) => { + if size < 1 { + return None; + } + // Check if the register already exists + let reg: R = (size, name).into(); + if let Some(index) = self.reg_keys.get(reg.as_key()) { + return Some(*index); + } + let bits: Vec<T> = (0..size) + .map(|bit| { + self.add_bit_inner(Some(( + idx, + bit.try_into().unwrap_or_else(|_| { + panic!( + "The current register exceeds its capacity limit. Number of {} : {}", + self.description, + size + ) + }), + ))) + }) + .collect(); + let reg: R = (Cow::from(bits), Some(reg.name().to_string())).into(); + let idx = self.registry.len().try_into().unwrap_or_else(|_| { + panic!( + "The {} registry in this circuit has reached its maximum capacity.", + self.description + ) + }); + self.reg_keys.insert(reg.as_key().clone(), idx); + self.registry.push(reg); + self.registers.push(OnceLock::new()); + Some(idx) + } + (Some(_), Some(_)) => { + panic!("You should only provide either a size or the bit indices, not both.") + } + } + } + + /// Adds a bit index into the circuit's [BitData]. + /// + /// _**Note:** You cannot add bits to registers once they are added._ + pub fn add_bit(&mut self) -> T { + self.add_bit_inner(None) + } + + fn add_bit_inner(&mut self, reg: Option<(u32, u32)>) -> T { + let idx: BitType = self.bits.len().try_into().unwrap_or_else(|_| { + panic!( + "The number of {} in the circuit has exceeded the maximum capacity", + self.description + ) + }); + self.bit_info.push(BitInfo::new(reg)); + self.bits.push(OnceLock::new()); + idx.into() + } + + /// Retrieves the register info of a bit. Will panic if the index is out of range. + pub fn get_bit_info(&self, index: T) -> &[BitLocation] { + self.bit_info[BitType::from(index) as usize].get_registers() + } + + /// Retrieves a register by its index within the circuit + #[inline] + pub fn get_register(&self, index: u32) -> Option<&R> { + self.registry.get(index as usize) + } + + /// Returns a slice of the registers in the circuit + pub fn registers(&self) -> &[R] { + &self.registry + } + + #[inline] + pub fn get_register_by_key(&self, key: &RegisterAsKey) -> Option<&R> { + self.reg_keys + .get(key) + .and_then(|idx| self.get_register(*idx)) + } + + /// Checks if a register is in the circuit + #[inline] + pub fn contains_register(&self, reg: &R) -> bool { + self.contains_register_by_key(reg.as_key()) + } + + #[inline] + pub fn contains_register_by_key(&self, reg: &RegisterAsKey) -> bool { + self.reg_keys.contains_key(reg) + } +} + +// PyMethods +impl<T, R> NewBitData<T, R> +where + T: From<BitType> + Copy + Debug + ToPyBit, + R: Register<Bit = T> + + Hash + + Eq + + From<(usize, Option<String>)> + + for<'a> From<Cow<'a, [T]>> + + for<'a> From<(Cow<'a, [T]>, Option<String>)>, + BitType: From<T>, +{ + /// Finds the native bit index of the given Python bit. + #[inline] + pub fn py_find_bit(&self, bit: &Bound<PyAny>) -> PyResult<Option<T>> { + self.indices + .try_read() + .map(|op| op.get(&BitAsKey::new(bit)).copied()) + .map_err(|_| { + PyRuntimeError::new_err(format!( + "Could not map {}. Error accessing index mapping.", + &bit + )) + }) + } + + /// Gets a reference to the cached Python list, with the bits maintained by + /// this instance. + #[inline] + pub fn py_cached_bits(&self, py: Python) -> PyResult<&Py<PyList>> { + let res = self.cached_py_bits.get_or_init(|| { + PyList::new( + py, + (0..self.len()).map(|idx| self.py_get_bit(py, (idx as u32).into()).unwrap()), + ) + .unwrap() + .into() + }); + + // If the length is different from the stored bits, append to cache + // Indices are guaranteed to follow + let res_as_bound = res.bind(py); + if res_as_bound.len() < self.len() { + let current_length = res_as_bound.len(); + for index in current_length.checked_sub(1).unwrap_or_default()..self.len() { + res_as_bound.append(self.py_get_bit(py, (index as u32).into())?)? + } + } + Ok(res) + } + + /// Gets a reference to the cached Python list, with the registers maintained by + /// this instance. + #[inline] + pub fn py_cached_regs(&self, py: Python) -> PyResult<&Py<PyList>> { + // Initialize the list with all the currently available registers + let res = self.cached_py_regs.get_or_init(|| { + PyList::new( + py, + (0..self.len_regs()).map(|idx| self.py_get_register(py, idx as u32).unwrap()), + ) + .unwrap() + .into() + }); + + // If the length is different from the stored registers, rebuild cache + let res_as_bound = res.bind(py); + if res_as_bound.len() < self.len_regs() { + let current_length = res_as_bound.len(); + for index in (current_length - 1)..self.len_regs() { + res_as_bound.append(self.py_get_register(py, index as u32)?)? + } + } + Ok(res) + } + + /// Gets a reference to the underlying vector of Python bits. + #[inline] + pub fn py_bits(&self, py: Python) -> PyResult<Vec<&PyObject>> { + (0..self.len()) + .map(|idx| { + self.py_get_bit(py, (idx as u32).into()) + .map(|bit| bit.unwrap()) + }) + .collect::<PyResult<_>>() + } + + /// Gets the location of a bit within the circuit + pub fn py_get_bit_location( + &mut self, + bit: &Bound<PyAny>, + ) -> PyResult<(u32, Vec<(&PyObject, u32)>)> { + let py = bit.py(); + let index = self.py_find_bit(bit)?.ok_or(PyKeyError::new_err(format!( + "The provided {} is not part of this circuit", + self.description + )))?; + Ok(( + index.into(), + self.get_bit_info(index) + .iter() + .map(|info| -> PyResult<(&PyObject, u32)> { + Ok(( + self.py_get_register(py, info.register_index())?.unwrap(), + info.index(), + )) + }) + .collect::<PyResult<Vec<_>>>()?, + )) + } + + /// Gets a reference to the underlying vector of Python registers. + #[inline] + pub fn py_registers(&self, py: Python) -> PyResult<Vec<&PyObject>> { + (0..self.len_regs() as u32) + .map(|idx| self.py_get_register(py, idx).map(|reg| reg.unwrap())) + .collect::<PyResult<_>>() + } + + /// Map the provided Python bits to their native indices. + /// An error is returned if any bit is not registered. + pub fn py_map_bits<'py>( + &mut self, + bits: impl IntoIterator<Item = Bound<'py, PyAny>>, + ) -> PyResult<impl Iterator<Item = T>> { + let v: Result<Vec<_>, _> = bits + .into_iter() + .map(|b| { + self.py_find_bit(&b)?.ok_or_else(|| { + PyKeyError::new_err(format!("Bit {:?} has not been added to this circuit.", b)) + }) + }) + .collect(); + v.map(|x| x.into_iter()) + } + + /// Map the provided native indices to the corresponding Python + /// bit instances. + /// Panics if any of the indices are out of range. + pub fn py_map_indices( + &self, + py: Python, + bits: &[T], + ) -> PyResult<impl ExactSizeIterator<Item = &Py<PyAny>>> { + let v: Vec<_> = bits + .iter() + .map(|i| -> PyResult<&PyObject> { Ok(self.py_get_bit(py, *i)?.unwrap()) }) + .collect::<PyResult<_>>()?; + Ok(v.into_iter()) + } + + /// Gets the Python bit corresponding to the given native + /// bit index. + #[inline] + pub fn py_get_bit(&self, py: Python, index: T) -> PyResult<Option<&PyObject>> { + let index_as_usize = BitType::from(index) as usize; + // First check if the cell is in range if not, return none + if self.bits.get(index_as_usize).is_none() { + Ok(None) + } + // If the bit has an assigned register, check if it has been initialized. + else if let Some(bit_info) = self.bit_info[index_as_usize].orig_register_index() { + // If it is not initalized and has a register, initialize the original register + // and retrieve it from there the first time + if self.bits[index_as_usize].get().is_none() { + // A register index is guaranteed to exist in the instance of `BitData`. + let py_reg = self.py_get_register(py, bit_info.register_index())?; + let res = py_reg.unwrap().bind(py).get_item(bit_info.index())?; + self.indices + .try_write() + .map(|mut indices| indices.insert(BitAsKey::new(&res), index)) + .map_err(|err| PyRuntimeError::new_err(format!("{:?}", err)))?; + self.bits[index_as_usize].set(res.into()).map_err(|_| { + PyRuntimeError::new_err(format!( + "Error while initializing Python bit at index {} in these circuit's {}", + BitType::from(index), + &self.description + )) + })?; + } + // If it is initialized, just retrieve. + Ok(self.bits[index_as_usize].get()) + } else if let Some(bit) = self.bits[index_as_usize].get() { + Ok(Some(bit)) + } else { + let new_bit = T::to_py_bit(py)?; + // Try and write the bit index into `BitData`. + self.indices + .try_write() + .map(|mut indices| indices.insert(BitAsKey::new(new_bit.bind(py)), index)) + .map_err(|err| PyRuntimeError::new_err(format!("{:?}", err)))?; + self.bits[index_as_usize].set(new_bit).map_err(|_| { + PyRuntimeError::new_err(format!( + "Error while initializing Python bit at index {} in these circuit's {}", + BitType::from(index), + &self.description + )) + })?; + Ok(self.bits[index_as_usize].get()) + } + } + + /// Retrieves a register instance from Python based on the rust description. + pub fn py_get_register(&self, py: Python, index: u32) -> PyResult<Option<&PyObject>> { + let index_as_usize = index as usize; + // First check if the cell is in range if not, return none + if self.registers.get(index_as_usize).is_none() { + Ok(None) + } else if self.registers[index_as_usize].get().is_none() { + let register = &self.registry[index as usize]; + // Decide the register type based on its key + let reg_as_key = register.as_key(); + let reg_type = match reg_as_key { + RegisterAsKey::Register(_) => REGISTER.get_bound(py), + RegisterAsKey::Quantum(_) => QUANTUM_REGISTER.get_bound(py), + RegisterAsKey::Classical(_) => CLASSICAL_REGISTER.get_bound(py), + }; + // Check if all indices have been initialized from this register, if such is the case + // Treat the rest of indices as old `Bits`` + if register.bits().all(|bit| { + self.bit_info[BitType::from(bit) as usize] + .orig_register_index() + .is_some_and(|idx| idx.register_index() == index) + }) { + let reg = reg_type.call1((register.len(), register.name()))?; + self.registers[index_as_usize] + .set(reg.into()) + .map_err(|_| PyRuntimeError::new_err("Could not set the OnceCell correctly"))?; + Ok(self.registers[index_as_usize].get()) + } else { + let bits: Vec<PyObject> = register + .bits() + .map(|bit| -> PyResult<PyObject> { + if let Some(bit_obj) = self.bits[BitType::from(bit) as usize].get() { + Ok(bit_obj.clone_ref(py)) + } else { + T::to_py_bit(py) + } + }) + .collect::<PyResult<_>>()?; + + // Extract kwargs + let kwargs = PyDict::new(py); + kwargs.set_item("name", register.name())?; + kwargs.set_item("bits", bits)?; + + // Create register and assign to OnceCell + let reg = reg_type.call((), Some(&kwargs))?; + self.registers[index_as_usize] + .set(reg.into()) + .map_err(|_| PyRuntimeError::new_err("Could not set the OnceCell correctly"))?; + Ok(self.registers[index_as_usize].get()) + } + } else { + Ok(self.registers[index_as_usize].get()) + } + } + + /// Adds a new Python bit. + /// + /// _**Note:** If this Bit has register information, it will not be reflected unless + /// the Register is also added._ + pub fn py_add_bit(&mut self, bit: &Bound<PyAny>, strict: bool) -> PyResult<T> { + let py: Python<'_> = bit.py(); + + if self.bits.len() != self.py_cached_bits(py)?.bind(bit.py()).len() { + return Err(PyRuntimeError::new_err( + format!("This circuit's {} list has become out of sync with the circuit data. Did something modify it?", self.description) + )); + } + + let idx: BitType = self.bits.len().try_into().map_err(|_| { + PyRuntimeError::new_err(format!( + "The number of {} in the circuit has exceeded the maximum capacity", + self.description + )) + })?; + if self + .indices + .try_write() + .map(|mut res| res.try_insert(BitAsKey::new(bit), idx.into()).is_ok()) + .is_ok_and(|res| res) + { + // Append to cache before bits to avoid rebuilding cache. + self.py_cached_bits(py)?.bind(py).append(bit)?; + self.bit_info.push(BitInfo::new(None)); + self.bits.push(bit.clone().unbind().into()); + Ok(idx.into()) + } else if strict { + return Err(PyValueError::new_err(format!( + "Existing bit {:?} cannot be re-added in strict mode.", + bit + ))); + } else { + return self.py_find_bit(bit).map(|opt| opt.unwrap()); + } + } + + /// Adds new register from Python. + pub fn py_add_register(&mut self, register: &Bound<PyAny>) -> PyResult<u32> { + let py = register.py(); + if self.registers.len() != self.py_cached_regs(py)?.bind(py).len() { + return Err(PyRuntimeError::new_err( + format!("This circuit's {} register list has become out of sync with the circuit data. Did something modify it?", self.description) + )); + } + let key: RegisterAsKey = register.extract()?; + if self.reg_keys.contains_key(&key) { + return Err(CircuitError::new_err(format!( + "A {} register of name {} already exists in the circuit", + &self.description, + key.name() + ))); + } + + let idx: u32 = self.registers.len().try_into().map_err(|_| { + PyRuntimeError::new_err(format!( + "The number of {} registers in the circuit has exceeded the maximum capacity", + self.description + )) + })?; + + let bits: Vec<T> = register + .try_iter()? + .enumerate() + .map(|(bit_index, bit)| -> PyResult<T> { + let bit_index: u32 = bit_index.try_into().map_err(|_| { + CircuitError::new_err(format!( + "The current register exceeds its capacity limit. Number of {} : {}", + self.description, + key.size() + )) + })?; + let bit = bit?; + let index = if let Some(index) = self.py_find_bit(&bit)? { + let bit_info = &mut self.bit_info[BitType::from(index) as usize]; + bit_info.add_register(idx, bit_index); + index + } else { + let index = self.py_add_bit(&bit, true)?; + self.bit_info[BitType::from(index) as usize] = + BitInfo::new(Some((idx, bit_index))); + index + }; + Ok(index) + }) + .collect::<PyResult<_>>()?; + + // Create the native register + let name: String = key.name().to_string(); + let reg: R = (Cow::from(bits), Some(name)).into(); + + // Append to cache before registers to avoid rebuilding cache. + self.py_cached_regs(py)?.bind(py).append(register)?; + self.reg_keys.insert(reg.as_key().clone(), idx); + self.registry.push(reg); + self.registers.push(register.clone().unbind().into()); + Ok(idx) + } + + /// Works as a setter for Python registers when the circuit needs to discard old data. + /// This method discards the current registers and the data associated with them from its + /// respective bits. + pub fn py_set_registers(&mut self, other: &Bound<PyList>) -> PyResult<()> { + // First invalidate everything related to registers + // This is done to ensure we regenerate the lost information + // Clear all information bits may have on registers + self.bit_info = (0..self.len()).map(|_| BitInfo::new(None)).collect(); + + self.reg_keys.clear(); + self.registers.clear(); + self.registry.clear(); + self.cached_py_regs.take(); + + // Re-assign + for reg in other.iter() { + self.py_add_register(®)?; + } + + Ok(()) + } + + pub fn py_remove_bit_indices<I>(&mut self, py: Python, indices: I) -> PyResult<()> + where + I: IntoIterator<Item = T>, + { + let mut indices_sorted: Vec<usize> = indices + .into_iter() + .map(|i| <BitType as From<T>>::from(i) as usize) + .collect(); + indices_sorted.sort(); + + for index in indices_sorted.into_iter().rev() { + self.cached_py_bits.take(); + let bit = self.py_get_bit(py, (index as BitType).into())?.unwrap(); + self.indices + .try_write() + .map(|mut op| op.remove(&BitAsKey::new(bit.bind(py)))) + .map_err(|_| { + PyRuntimeError::new_err("Could not remove bit from cache".to_string()) + })?; + self.bits.remove(index); + self.bit_info.remove(index); + } + // Update indices. + for i in 0..self.bits.len() { + let bit = self.py_get_bit(py, (i as BitType).into())?.unwrap(); + self.indices + .try_write() + .map(|mut op| op.insert(BitAsKey::new(bit.bind(py)), (i as BitType).into())) + .map_err(|_| { + PyRuntimeError::new_err("Could not re-map bit in cache".to_string()) + })?; + } + Ok(()) + } + + pub(crate) fn py_bits_raw(&self) -> &[OnceLock<PyObject>] { + &self.bits + } + + pub(crate) fn py_bits_cached_raw(&self) -> Option<&Py<PyList>> { + self.cached_py_bits.get() + } + + pub(crate) fn py_regs_raw(&self) -> &[OnceLock<PyObject>] { + &self.bits + } + + pub(crate) fn py_regs_cached_raw(&self) -> Option<&Py<PyList>> { + self.cached_py_bits.get() + } + + /// Called during Python garbage collection, only!. + /// Note: INVALIDATES THIS INSTANCE. + pub fn dispose(&mut self) -> PyResult<()> { + self.indices + .try_write() + .map(|mut op| op.clear()) + .map_err(|err| PyRuntimeError::new_err(format!("{:?}", err)))?; + self.bits.clear(); + self.registers.clear(); + self.bit_info.clear(); + self.registry.clear(); + Ok(()) + } + + /// To convert [BitData] into [NewBitData]. If the structure the original comes from contains register + /// info. Make sure to add it manually after. + pub fn from_bit_data(py: Python, bit_data: &BitData<T>) -> Self { + Self { + description: bit_data.description.clone(), + bits: bit_data + .bits + .iter() + .map(|bit| bit.clone_ref(py).into()) + .collect(), + indices: bit_data.indices.clone().into(), + reg_keys: HashMap::new(), + bit_info: (0..bit_data.len()).map(|_| BitInfo::new(None)).collect(), + registry: Vec::new(), + registers: Vec::new(), + cached_py_bits: OnceLock::new(), + cached_py_regs: OnceLock::new(), + } + } +} + +// Custom implementation of Clone due to RWLock usage. +impl<T: Clone, R: Clone> Clone for NewBitData<T, R> +where + T: From<u32> + Copy, + R: Register + Hash + Eq, +{ + fn clone(&self) -> Self { + Self { + description: self.description.clone(), + bits: self.bits.clone(), + indices: self + .indices + .try_read() + .map(|indices| indices.clone()) + .unwrap_or_default() + .into(), + reg_keys: self.reg_keys.clone(), + bit_info: self.bit_info.clone(), + registry: self.registry.clone(), + registers: self.registers.clone(), + cached_py_bits: self.cached_py_bits.clone(), + cached_py_regs: self.cached_py_regs.clone(), + } + } +} diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index be36aed73f6b..ce470364382e 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -10,19 +10,22 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use std::borrow::Cow; #[cfg(feature = "cache_pygates")] use std::sync::OnceLock; -use crate::bit_data::BitData; +use crate::bit::BitLocation; +use crate::bit_data::NewBitData; use crate::circuit_instruction::{ CircuitInstruction, ExtraInstructionAttributes, OperationFromPython, }; use crate::dag_circuit::add_global_phase; -use crate::imports::{ANNOTATED_OPERATION, CLBIT, QUANTUM_CIRCUIT, QUBIT}; +use crate::imports::{ANNOTATED_OPERATION, QUANTUM_CIRCUIT}; use crate::interner::{Interned, Interner}; use crate::operations::{Operation, OperationRef, Param, StandardGate}; use crate::packed_instruction::{PackedInstruction, PackedOperation}; use crate::parameter_table::{ParameterTable, ParameterTableError, ParameterUse, ParameterUuid}; +use crate::register::{ClassicalRegister, QuantumRegister}; use crate::slice::{PySequenceIndex, SequenceIndex}; use crate::{Clbit, Qubit}; @@ -102,9 +105,9 @@ pub struct CircuitData { /// The cache used to intern instruction bits. cargs_interner: Interner<[Clbit]>, /// Qubits registered in the circuit. - qubits: BitData<Qubit>, + qubits: NewBitData<Qubit, QuantumRegister>, /// Clbits registered in the circuit. - clbits: BitData<Clbit>, + clbits: NewBitData<Clbit, ClassicalRegister>, param_table: ParameterTable, #[pyo3(get)] global_phase: Param, @@ -114,7 +117,7 @@ pub struct CircuitData { impl CircuitData { #[new] #[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0, global_phase=Param::Float(0.0)))] - pub fn new( + pub fn py_new( py: Python<'_>, qubits: Option<&Bound<PyAny>>, clbits: Option<&Bound<PyAny>>, @@ -126,20 +129,20 @@ impl CircuitData { data: Vec::new(), qargs_interner: Interner::new(), cargs_interner: Interner::new(), - qubits: BitData::new(py, "qubits".to_string()), - clbits: BitData::new(py, "clbits".to_string()), + qubits: NewBitData::new("qubits".to_string()), + clbits: NewBitData::new("clbits".to_string()), param_table: ParameterTable::new(), global_phase: Param::Float(0.), }; self_.set_global_phase(py, global_phase)?; if let Some(qubits) = qubits { for bit in qubits.try_iter()? { - self_.add_qubit(py, &bit?, true)?; + self_.py_add_qubit(&bit?, true)?; } } if let Some(clbits) = clbits { for bit in clbits.try_iter()? { - self_.add_clbit(py, &bit?, true)?; + self_.py_add_clbit(&bit?, true)?; } } if let Some(data) = data { @@ -149,19 +152,70 @@ impl CircuitData { Ok(self_) } - pub fn __reduce__(self_: &Bound<CircuitData>, py: Python<'_>) -> PyResult<PyObject> { + pub fn __reduce__(self_: &Bound<CircuitData>) -> PyResult<PyObject> { + let py = self_.py(); let ty: Bound<PyType> = self_.get_type(); let args = { let self_ = self_.borrow(); ( - self_.qubits.cached().clone_ref(py), - self_.clbits.cached().clone_ref(py), + self_.qubits.py_cached_bits(py)?.clone_ref(py), + self_.clbits.py_cached_bits(py)?.clone_ref(py), None::<()>, self_.data.len(), self_.global_phase.clone(), ) }; - (ty, args, None::<()>, self_.try_iter()?).into_py_any(py) + let state = { + let borrowed = self_.borrow(); + ( + borrowed.qubits.py_cached_regs(py)?.clone_ref(py), + borrowed.clbits.py_cached_regs(py)?.clone_ref(py), + ) + }; + (ty, args, state, self_.try_iter()?).into_py_any(py) + } + + pub fn __setstate__( + self_: &Bound<CircuitData>, + state: (Bound<PyList>, Bound<PyList>), + ) -> PyResult<()> { + let mut borrowed_mut = self_.borrow_mut(); + for qreg in state.0.iter() { + borrowed_mut.py_add_qreg(&qreg)?; + } + for creg in state.1.iter() { + borrowed_mut.py_add_creg(&creg)?; + } + Ok(()) + } + + /// Returns the a list of registered :class:`.QuantumRegisters` instances. + /// + /// .. warning:: + /// + /// Do not modify this list yourself. It will invalidate the :class:`CircuitData` data + /// structures. + /// + /// Returns: + /// dict(:class:`.QuantumRegister`): The current sequence of registered qubits. + #[getter("qregs")] + pub fn py_qregs(&self, py: Python<'_>) -> PyResult<&Py<PyList>> { + self.qubits.py_cached_regs(py) + } + + /// Setter for registers, in case of forced updates. + #[setter("qregs")] + pub fn py_qreg_set(&mut self, other: &Bound<PyList>) -> PyResult<()> { + self.qubits.py_set_registers(other) + } + + /// Gets the location of the bit inside of the circuit + #[pyo3(name = "get_qubit_location")] + pub fn py_get_qubit_location( + &mut self, + bit: &Bound<PyAny>, + ) -> PyResult<(u32, Vec<(&PyObject, u32)>)> { + self.qubits.py_get_bit_location(bit) } /// Returns the current sequence of registered :class:`.Qubit` instances as a list. @@ -174,8 +228,8 @@ impl CircuitData { /// Returns: /// list(:class:`.Qubit`): The current sequence of registered qubits. #[getter("qubits")] - pub fn py_qubits(&self, py: Python<'_>) -> Py<PyList> { - self.qubits.cached().clone_ref(py) + pub fn py_qubits(&self, py: Python<'_>) -> PyResult<&Py<PyList>> { + self.qubits.py_cached_bits(py) } /// Return the number of qubits. This is equivalent to the length of the list returned by @@ -188,8 +242,36 @@ impl CircuitData { self.qubits.len() } - /// Returns the current sequence of registered :class:`.Clbit` - /// instances as a list. + /// Returns the a list of registered :class:`.ClassicalRegisters` instances. + /// + /// .. warning:: + /// + /// Do not modify this list yourself. It will invalidate the :class:`CircuitData` data + /// structures. + /// + /// Returns: + /// dict(:class:`.ClassicalRegister`): The current sequence of registered qubits. + #[getter("cregs")] + pub fn py_cregs(&self, py: Python<'_>) -> PyResult<&Py<PyList>> { + self.clbits.py_cached_regs(py) + } + + /// Setter for registers, in case of forced updates. + #[setter("cregs")] + pub fn py_creg_set(&mut self, other: &Bound<PyList>) -> PyResult<()> { + self.clbits.py_set_registers(other) + } + + /// Gets the location of the bit inside of the circuit + #[pyo3(name = "get_clbit_location")] + pub fn py_get_clbit_location( + &mut self, + bit: &Bound<PyAny>, + ) -> PyResult<(u32, Vec<(&PyObject, u32)>)> { + self.clbits.py_get_bit_location(bit) + } + + /// Returns the current sequence of registered :class:`.Clbit` instances as a list. /// /// .. warning:: /// @@ -199,8 +281,8 @@ impl CircuitData { /// Returns: /// list(:class:`.Clbit`): The current sequence of registered clbits. #[getter("clbits")] - pub fn py_clbits(&self, py: Python<'_>) -> Py<PyList> { - self.clbits.cached().clone_ref(py) + pub fn py_clbits(&self, py: Python<'_>) -> PyResult<&Py<PyList>> { + self.clbits.py_cached_bits(py) } /// Return the number of clbits. This is equivalent to the length of the list returned by @@ -257,9 +339,19 @@ impl CircuitData { /// Raises: /// ValueError: The specified ``bit`` is already present and flag ``strict`` /// was provided. - #[pyo3(signature = (bit, *, strict=true))] - pub fn add_qubit(&mut self, py: Python, bit: &Bound<PyAny>, strict: bool) -> PyResult<()> { - self.qubits.add(py, bit, strict)?; + #[pyo3(name="add_qubit", signature = (bit, *, strict=true))] + pub fn py_add_qubit(&mut self, bit: &Bound<PyAny>, strict: bool) -> PyResult<()> { + self.qubits.py_add_bit(bit, strict)?; + Ok(()) + } + + /// Registers a :class:`.QuantumRegister` instance. + /// + /// Args: + /// bit (:class:`.QuantumRegister`): The register to add. + #[pyo3(name="add_qreg", signature = (register, *,))] + pub fn py_add_qreg(&mut self, register: &Bound<PyAny>) -> PyResult<()> { + self.qubits.py_add_register(register)?; Ok(()) } @@ -272,9 +364,19 @@ impl CircuitData { /// Raises: /// ValueError: The specified ``bit`` is already present and flag ``strict`` /// was provided. - #[pyo3(signature = (bit, *, strict=true))] - pub fn add_clbit(&mut self, py: Python, bit: &Bound<PyAny>, strict: bool) -> PyResult<()> { - self.clbits.add(py, bit, strict)?; + #[pyo3(name="add_clbit", signature = (bit, *, strict=true))] + pub fn py_add_clbit(&mut self, bit: &Bound<PyAny>, strict: bool) -> PyResult<()> { + self.clbits.py_add_bit(bit, strict)?; + Ok(()) + } + + /// Registers a :class:`.ClassicalRegister` instance. + /// + /// Args: + /// bit (:class:`.ClassicalRegister`): The register to add. + #[pyo3(name="add_creg", signature = (register, *,))] + pub fn py_add_creg(&mut self, register: &Bound<PyAny>) -> PyResult<()> { + self.clbits.py_add_register(register)?; Ok(()) } @@ -284,18 +386,8 @@ impl CircuitData { /// CircuitData: The shallow copy. #[pyo3(signature = (copy_instructions=true, deepcopy=false))] pub fn copy(&self, py: Python<'_>, copy_instructions: bool, deepcopy: bool) -> PyResult<Self> { - let mut res = CircuitData::new( - py, - Some(self.qubits.cached().bind(py)), - Some(self.clbits.cached().bind(py)), - None, - self.data.len(), - self.global_phase.clone(), - )?; - res.qargs_interner = self.qargs_interner.clone(); - res.cargs_interner = self.cargs_interner.clone(); + let mut res = self.copy_empty_like(py, Some(self.data().len()))?; res.param_table.clone_from(&self.param_table); - if deepcopy { let memo = PyDict::new(py); for inst in &self.data { @@ -327,6 +419,32 @@ impl CircuitData { Ok(res) } + /// Performs an empty-like shallow copy. + /// + /// Returns: + /// CircuitData: The shallow copy. + #[pyo3(signature = (reserve = None,))] + pub fn copy_empty_like(&self, py: Python<'_>, reserve: Option<usize>) -> PyResult<Self> { + let mut res = CircuitData::py_new( + py, + Some(self.qubits.py_cached_bits(py)?.bind(py)), + Some(self.clbits.py_cached_bits(py)?.bind(py)), + None, + reserve.unwrap_or_default(), + self.global_phase.clone(), + )?; + res.qargs_interner = self.qargs_interner.clone(); + res.cargs_interner = self.cargs_interner.clone(); + + for qreg in self.py_qregs(py)?.bind(py).iter() { + res.py_add_qreg(&qreg)?; + } + for creg in self.py_cregs(py)?.bind(py).iter() { + res.py_add_creg(&creg)?; + } + Ok(res) + } + /// Reserves capacity for at least ``additional`` more /// :class:`.CircuitInstruction` instances to be added to this container. /// @@ -347,10 +465,10 @@ impl CircuitData { let clbits = PySet::empty(py)?; for inst in self.data.iter() { for b in self.qargs_interner.get(inst.qubits) { - qubits.add(self.qubits.get(*b).unwrap().clone_ref(py))?; + qubits.add(self.qubits.py_get_bit(py, *b)?.unwrap().clone_ref(py))?; } for b in self.cargs_interner.get(inst.clbits) { - clbits.add(self.clbits.get(*b).unwrap().clone_ref(py))?; + clbits.add(self.clbits.py_get_bit(py, *b)?.unwrap().clone_ref(py))?; } } @@ -466,14 +584,28 @@ impl CircuitData { /// CircuitInstruction(XGate(), [qr[1]], []), /// CircuitInstruction(XGate(), [qr[0]], []), /// ]) - #[pyo3(signature = (qubits=None, clbits=None))] + #[pyo3(signature = (qubits=None, clbits=None, qregs=None, cregs=None))] pub fn replace_bits( &mut self, py: Python<'_>, qubits: Option<&Bound<PyAny>>, clbits: Option<&Bound<PyAny>>, + qregs: Option<&Bound<PyAny>>, + cregs: Option<&Bound<PyAny>>, ) -> PyResult<()> { - let mut temp = CircuitData::new(py, qubits, clbits, None, 0, self.global_phase.clone())?; + let mut temp = CircuitData::py_new(py, qubits, clbits, None, 0, self.global_phase.clone())?; + // Add qregs if provided. + if let Some(qregs) = qregs { + for qreg in qregs.try_iter()? { + temp.py_add_qreg(&qreg?)?; + } + } + // Add cregs if provided. + if let Some(cregs) = cregs { + for creg in cregs.try_iter()? { + temp.py_add_creg(&creg?)?; + } + } if qubits.is_some() { if temp.num_qubits() < self.num_qubits() { return Err(PyValueError::new_err(format!( @@ -504,16 +636,16 @@ impl CircuitData { // Note: we also rely on this to make us iterable! pub fn __getitem__(&self, py: Python, index: PySequenceIndex) -> PyResult<PyObject> { // Get a single item, assuming the index is validated as in bounds. - let get_single = |index: usize| { + let get_single = |index: usize| -> PyObject { let inst = &self.data[index]; let qubits = self.qargs_interner.get(inst.qubits); let clbits = self.cargs_interner.get(inst.clbits); CircuitInstruction { operation: inst.op.clone(), - qubits: PyTuple::new(py, self.qubits.map_indices(qubits)) + qubits: PyTuple::new(py, self.qubits.py_map_indices(py, qubits).unwrap()) .unwrap() .unbind(), - clbits: PyTuple::new(py, self.clbits.map_indices(clbits)) + clbits: PyTuple::new(py, self.clbits.py_map_indices(py, clbits).unwrap()) .unwrap() .unbind(), params: inst.params_view().iter().cloned().collect(), @@ -663,7 +795,7 @@ impl CircuitData { pub fn extend(&mut self, py: Python<'_>, itr: &Bound<PyAny>) -> PyResult<()> { if let Ok(other) = itr.downcast::<CircuitData>() { - let other = other.borrow(); + let other = other.borrow_mut(); // Fast path to avoid unnecessary construction of CircuitInstruction instances. self.data.reserve(other.data.len()); for inst in other.data.iter() { @@ -674,7 +806,7 @@ impl CircuitData { .map(|b| { Ok(self .qubits - .find(other.qubits.get(*b).unwrap().bind(py)) + .py_find_bit(other.qubits.py_get_bit(py, *b)?.unwrap().bind(py))? .unwrap()) }) .collect::<PyResult<Vec<Qubit>>>()?; @@ -685,7 +817,7 @@ impl CircuitData { .map(|b| { Ok(self .clbits - .find(other.clbits.get(*b).unwrap().bind(py)) + .py_find_bit(other.clbits.py_get_bit(py, *b)?.unwrap().bind(py))? .unwrap()) }) .collect::<PyResult<Vec<Clbit>>>()?; @@ -824,26 +956,51 @@ impl CircuitData { } fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - for bit in self.qubits.bits().iter().chain(self.clbits.bits().iter()) { + for bit in self + .qubits + .py_bits_raw() + .iter() + .chain(self.clbits.py_bits_raw().iter()) + .filter_map(|cell| cell.get()) + { visit.call(bit)?; } - + for register in self + .qubits + .py_regs_raw() + .iter() + .chain(self.clbits.py_regs_raw().iter()) + .filter_map(|cell| cell.get()) + { + visit.call(register)?; + } // 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.cached())?; - visit.call(self.clbits.cached())?; + if let Some(bits) = self.qubits.py_bits_cached_raw() { + visit.call(bits)?; + } + if let Some(registers) = self.qubits.py_regs_cached_raw() { + visit.call(registers)?; + } + if let Some(bits) = self.clbits.py_bits_cached_raw() { + visit.call(bits)?; + } + if let Some(registers) = self.clbits.py_regs_cached_raw() { + visit.call(registers)?; + } self.param_table.py_gc_traverse(&visit)?; Ok(()) } - fn __clear__(&mut self) { + fn __clear__(&mut self) -> PyResult<()> { // Clear anything that could have a reference cycle. self.data.clear(); - self.qubits.dispose(); - self.clbits.dispose(); + self.qubits.dispose()?; + self.clbits.dispose()?; self.param_table.clear(); + Ok(()) } /// Set the global phase of the circuit. @@ -897,6 +1054,141 @@ impl CircuitData { } impl CircuitData { + /// Rust native constructor for [CircuitData]. Builds a new instance without + /// any python initialization. + pub fn new( + num_qubits: u32, + num_clbits: u32, + global_phase: Param, + add_qreg: bool, + add_creg: bool, + ) -> Self { + let mut data = Self { + data: vec![], + qargs_interner: Interner::new(), + cargs_interner: Interner::new(), + qubits: NewBitData::with_capacity( + "qubits".to_owned(), + num_qubits + .try_into() + .expect("The number of qubits provided exceeds the limit for a circuit."), + if add_qreg { 1 } else { 0 }, + ), + clbits: NewBitData::with_capacity( + "clbits".to_owned(), + num_clbits + .try_into() + .expect("The number of clbits provided exceeds the limit for a circuit."), + if add_creg { 1 } else { 0 }, + ), + param_table: ParameterTable::new(), + global_phase: Param::Float(0.), + }; + + // Set the global phase using internal setter. + data._set_global_phase_float(global_phase); + // Add all the bits into a register + if add_qreg { + data.add_qreg( + Some("q".to_string()), + Some( + num_qubits + .try_into() + .expect("The number of qubits provided exceeds the limit for a circuit."), + ), + None, + ); + } else { + (0..num_qubits).for_each(|_| { + data.add_qubit(); + }); + } + // Add all the bits into a register + if add_creg { + data.add_creg( + Some("c".to_string()), + Some( + num_clbits + .try_into() + .expect("The number of clbits provided exceeds the limit for a circuit."), + ), + None, + ); + } else { + (0..num_clbits).for_each(|_| { + data.add_clbit(); + }); + } + data + } + + /// Adds a generic qubit to a circuit + pub fn add_qubit(&mut self) -> Qubit { + self.qubits.add_bit() + } + + /// Get qubit location in the circuit + pub fn get_qubit_location(&self, qubit: Qubit) -> &[BitLocation] { + self.qubits.get_bit_info(qubit) + } + + /// Adds either a generic register with new bits, or uses existing bit indices. + pub fn add_qreg( + &mut self, + name: Option<String>, + num_qubits: Option<usize>, + bits: Option<Cow<'_, [Qubit]>>, + ) -> Option<u32> { + self.qubits.add_register(name, num_qubits, bits) + } + + /// Returns an iterator with all the QuantumRegisters in the circuit + pub fn qregs(&self) -> &[QuantumRegister] { + self.qubits.registers() + } + + /// Adds a generic clbit to a circuit + pub fn add_clbit(&mut self) -> Clbit { + self.clbits.add_bit() + } + + /// Adds either a generic register with new bits, or uses existing bit indices. + pub fn add_creg( + &mut self, + name: Option<String>, + num_qubits: Option<usize>, + bits: Option<Cow<'_, [Clbit]>>, + ) -> Option<u32> { + self.clbits.add_register(name, num_qubits, bits) + } + + /// Returns an iterator with all the QuantumRegisters in the circuit + pub fn cregs(&self) -> &[ClassicalRegister] { + self.clbits.registers() + } + + /// Get qubit location in the circuit + pub fn get_clbit_location(&self, clbit: Clbit) -> &[BitLocation] { + self.clbits.get_bit_info(clbit) + } + + /// Set the global phase of the circuit using a float, without needing a + /// `py` token. + /// + /// _**Note:** for development purposes only. Should be removed after + /// [#13278](https://github.com/Qiskit/qiskit/pull/13278)._ + fn _set_global_phase_float(&mut self, angle: Param) { + match angle { + Param::Float(angle) => { + self.global_phase = Param::Float(angle.rem_euclid(2. * std::f64::consts::PI)); + } + _ => panic!( + "Could not set the parameter {:?}. Parameter was not a float.", + &angle + ), + } + } + /// An alternate constructor to build a new `CircuitData` from an iterator /// of packed operations. This can be used to build a circuit from a sequence /// of `PackedOperation` without needing to involve Python. @@ -989,8 +1281,8 @@ impl CircuitData { /// * global_phase: The global phase value to use for the new circuit. pub fn from_packed_instructions<I>( py: Python, - qubits: BitData<Qubit>, - clbits: BitData<Clbit>, + qubits: NewBitData<Qubit, QuantumRegister>, + clbits: NewBitData<Clbit, ClassicalRegister>, qargs_interner: Interner<[Qubit]>, cargs_interner: Interner<[Clbit]>, instructions: I, @@ -1086,8 +1378,8 @@ impl CircuitData { data: Vec::with_capacity(instruction_capacity), qargs_interner: Interner::new(), cargs_interner: Interner::new(), - qubits: BitData::new(py, "qubits".to_string()), - clbits: BitData::new(py, "clbits".to_string()), + qubits: NewBitData::with_capacity("qubits".to_string(), num_qubits as usize, 0), + clbits: NewBitData::with_capacity("clbits".to_string(), num_clbits as usize, 0), param_table: ParameterTable::new(), global_phase: Param::Float(0.0), }; @@ -1097,17 +1389,13 @@ impl CircuitData { res.set_global_phase(py, global_phase)?; if num_qubits > 0 { - let qubit_cls = QUBIT.get_bound(py); for _i in 0..num_qubits { - let bit = qubit_cls.call0()?; - res.add_qubit(py, &bit, true)?; + res.add_qubit(); } } if num_clbits > 0 { - let clbit_cls = CLBIT.get_bound(py); for _i in 0..num_clbits { - let bit = clbit_cls.call0()?; - res.add_clbit(py, &bit, true)?; + res.add_clbit(); } } Ok(res) @@ -1215,10 +1503,10 @@ impl CircuitData { fn pack(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult<PackedInstruction> { let qubits = self .qargs_interner - .insert_owned(self.qubits.map_bits(inst.qubits.bind(py))?.collect()); + .insert_owned(self.qubits.py_map_bits(inst.qubits.bind(py))?.collect()); let clbits = self .cargs_interner - .insert_owned(self.clbits.map_bits(inst.clbits.bind(py))?.collect()); + .insert_owned(self.clbits.py_map_bits(inst.clbits.bind(py))?.collect()); Ok(PackedInstruction { op: inst.operation.clone(), qubits, @@ -1295,15 +1583,27 @@ impl CircuitData { } /// Returns an immutable view of the Qubits registered in the circuit - pub fn qubits(&self) -> &BitData<Qubit> { + pub fn qubits(&self) -> &NewBitData<Qubit, QuantumRegister> { &self.qubits } /// Returns an immutable view of the Classical bits registered in the circuit - pub fn clbits(&self) -> &BitData<Clbit> { + pub fn clbits(&self) -> &NewBitData<Clbit, ClassicalRegister> { &self.clbits } + // TODO: Remove + /// Returns a mutable view of the Qubits registered in the circuit + pub fn qubits_mut(&mut self) -> &mut NewBitData<Qubit, QuantumRegister> { + &mut self.qubits + } + + // TODO: Remove + /// Returns a mutable view of the Classical bits registered in the circuit + pub fn clbits_mut(&mut self) -> &mut NewBitData<Clbit, ClassicalRegister> { + &mut self.clbits + } + /// Unpacks from interned value to `[Qubit]` pub fn get_qargs(&self, index: Interned<[Qubit]>) -> &[Qubit] { self.qargs_interner().get(index) @@ -1595,3 +1895,265 @@ impl<'py> FromPyObject<'py> for AssignParam { Ok(Self(Param::extract_no_coerce(ob)?)) } } + +#[cfg(test)] +mod test { + use crate::register::Register; + + use super::*; + + #[test] + fn test_circuit_construction() { + let num_qubits = 4; + let num_clbits = 3; + let circuit_data = CircuitData::new(num_qubits, num_clbits, Param::Float(0.0), true, true); + + // Expected qregs + let example_qreg = QuantumRegister::new(Some(4), Some("q".to_owned()), None); + let expected_qregs: Vec<QuantumRegister> = vec![example_qreg]; + + assert_eq!(circuit_data.qregs(), &expected_qregs); + + // Expected cregs + let example_creg = ClassicalRegister::new(Some(3), Some("c".to_owned()), None); + let expected_cregs: Vec<ClassicalRegister> = vec![example_creg]; + assert_eq!(circuit_data.cregs(), &expected_cregs) + } + + #[test] + fn test_circuit_construction_no_regs() { + let num_qubits = 4; + let num_clbits = 3; + let circuit_data = + CircuitData::new(num_qubits, num_clbits, Param::Float(0.0), false, false); + + // Register lists should be empty + assert!( + circuit_data.qregs().is_empty(), + "There are quantum registers in the circuit!" + ); + assert!( + circuit_data.cregs().is_empty(), + "There are classical registers in the circuit!" + ); + + for qubit in 0..num_qubits { + assert!( + circuit_data.qubits().get_bit_info(qubit.into()).is_empty(), + "A qubit seems to have a register assigned, even when none exist yet!" + ) + } + + for clbit in 0..num_clbits { + assert!(circuit_data.clbits().get_bit_info(clbit.into()).is_empty()) + } + } + + #[test] + fn test_circuit_bit_multiple_registers() { + let mut circuit: CircuitData = CircuitData::new(0, 0, 0.0.into(), false, false); + assert_eq!( + circuit.num_qubits(), + 0, + "The circuit says it contains bits, even when they don't exist!" + ); + + circuit.add_qreg(None, Some(3), None); + assert_eq!( + circuit.num_qubits(), + 3, + "The qubits were not properly added!" + ); + assert_eq!( + circuit.qregs().len(), + 1, + "The qreg was either not added or mishandled!" + ); + + circuit.add_qreg(None, None, Some(vec![2.into()].into())); + assert_eq!(circuit.num_qubits(), 3, "The number of qubits changed!"); + assert_eq!( + circuit.qregs().len(), + 2, + "The qreg was either not added or mishandled!" + ); + + let locations: &[BitLocation] = circuit.get_qubit_location(2.into()); + assert_eq!( + locations.len(), + 2, + "The new register was not assigned to the bit" + ); + assert_eq!( + locations.first(), + Some(&BitLocation::new(0, 2)), + "Incorrect register assigned as original." + ); + assert_eq!( + locations.last(), + Some(&BitLocation::new(1, 0)), + "Incorrect register assigned as secondary." + ); + + // Check if the registers contain the index in question + for reg in circuit.qregs() { + assert!(reg.contains(2.into())) + } + } + + #[test] + fn test_duplicate_q_register() { + // The circuit will be initialized by default with qregs ['q'] and cregs ['c'] + // Should not add repeated instances of registers with the same hash value. + // + let mut circ = CircuitData::new(3, 0, 0.0.into(), true, false); + + // Should not panic + circ.add_qreg( + Some("q".to_string()), + None, + Some((1..3).map(Qubit).collect::<Vec<_>>().into()), + ); + + assert_eq!( + circ.get_qubit_location(2.into()), + &[BitLocation::new(0, 2), BitLocation::new(1, 1)] + ); + + // When trying to add a register with the same name and length, return the original instance. + assert_eq!(circ.add_qreg(Some("q".to_string()), Some(3), None), Some(0)); + + // When trying to add a register with the same name and length, return the original instance. + assert_eq!(circ.add_qreg(Some("q".to_string()), Some(2), None), Some(1)); + } + + #[test] + fn test_duplicate_c_register() { + // The circuit will be initialized by default with qregs ['q'] and cregs ['c'] + // Should not add repeated instances of registers with the same hash value. + // + let mut circ = CircuitData::new(0, 3, 0.0.into(), false, true); + + // Should not panic + circ.add_creg( + Some("c".to_string()), + None, + Some((1..3).map(Clbit).collect::<Vec<_>>().into()), + ); + + assert_eq!( + circ.get_clbit_location(2.into()), + &[BitLocation::new(0, 2), BitLocation::new(1, 1)] + ); + + // When trying to add a register with the same name and length, return the original instance. + assert_eq!(circ.add_creg(Some("c".to_string()), Some(3), None), Some(0)); + + // When trying to add a register with the same name and length, return the original instance. + assert_eq!(circ.add_creg(Some("c".to_string()), Some(2), None), Some(1)); + } +} + +#[cfg(all(test, not(miri)))] +// #[cfg(all(test))] +mod pytest { + use pyo3::PyTypeInfo; + + use super::*; + + // Test Rust native circuit construction when accessed through Python, without + // adding registers to the circuit. + #[test] + fn test_circuit_construction_py_no_regs() { + let num_qubits = 4; + let num_clbits = 3; + let circuit_data = + CircuitData::new(num_qubits, num_clbits, Param::Float(0.0), false, false); + let result = Python::with_gil(|py| -> PyResult<bool> { + let quantum_circuit = QUANTUM_CIRCUIT.get_bound(py).clone(); + + let converted_circuit = + quantum_circuit.call_method1("_from_circuit_data", (circuit_data,))?; + + let converted_qregs = converted_circuit.getattr("qregs")?; + assert!(converted_qregs.is_instance(&PyList::type_object(py))?); + assert!( + converted_qregs.downcast::<PyList>()?.len() == 0, + "The quantum registers list returned a non-empty value" + ); + + let converted_qubits = converted_circuit.getattr("qubits")?; + assert!(converted_qubits.is_instance(&PyList::type_object(py))?); + assert!( + converted_qubits.downcast::<PyList>()?.len() == (num_qubits as usize), + "The qubits has the wrong length" + ); + + let converted_qregs = converted_circuit.getattr("qregs")?; + assert!(converted_qregs.is_instance(&PyList::type_object(py))?); + assert!( + converted_qregs.downcast::<PyList>()?.len() == 0, + "The classical registers list returned a non-empty value" + ); + + let converted_clbits = converted_circuit.getattr("clbits")?; + assert!(converted_clbits.is_instance(&PyList::type_object(py))?); + assert!( + converted_clbits.downcast::<PyList>()?.len() == (num_clbits as usize), + "The clbits has the wrong length" + ); + + Ok(true) + }) + .is_ok_and(|res| res); + assert!(result); + } + + // Test Rust native circuit construction when accessed through Python. + #[test] + fn test_circuit_construction() { + let num_qubits = 4; + let num_clbits = 3; + let circuit_data = CircuitData::new(num_qubits, num_clbits, Param::Float(0.0), true, true); + let result = Python::with_gil(|py| -> PyResult<bool> { + let quantum_circuit = QUANTUM_CIRCUIT.get_bound(py).clone(); + + let converted_circuit = quantum_circuit.call_method1( + "_from_circuit_data", + (circuit_data.clone().into_pyobject(py)?,), + )?; + let expected_circuit = quantum_circuit.call((num_qubits, num_clbits), None)?; + + let converted_qregs = converted_circuit.getattr("qregs")?; + let expected_qregs = expected_circuit.getattr("qregs")?; + + assert!(converted_qregs.eq(expected_qregs)?); + + let converted_cregs = converted_circuit.getattr("cregs")?; + let expected_cregs = expected_circuit.getattr("cregs")?; + + assert!(converted_cregs.eq(expected_cregs)?); + + let converted_qubits = converted_circuit.getattr("qubits")?; + let expected_qubits = expected_circuit.getattr("qubits")?; + assert!(converted_qubits.eq(&expected_qubits)?); + + let converted_clbits = converted_circuit.getattr("clbits")?; + let expected_clbits = expected_circuit.getattr("clbits")?; + assert!(converted_clbits.eq(&expected_clbits)?); + + let converted_global_phase = converted_circuit.getattr("global_phase")?; + let expected_global_phase = expected_circuit.getattr("global_phase")?; + + assert!(converted_global_phase.eq(&expected_global_phase)?); + + // TODO: Figure out why this fails + // println!("{:?}", expected_circuit.eq(&converted_circuit)); + + // Return true due to being unable to extract `CircuitData` from python + // due to conflics between cargo and python binaries. + Ok(true) + }); + assert!(result.is_ok_and(|result| result)) + } +} diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs index b9b7b433eec8..32f45ba2e416 100644 --- a/crates/circuit/src/converters.rs +++ b/crates/circuit/src/converters.rs @@ -15,14 +15,11 @@ use std::sync::OnceLock; use hashbrown::HashMap; use pyo3::prelude::*; -use pyo3::{ - intern, - types::{PyDict, PyList}, -}; +use pyo3::{intern, types::PyDict}; -use crate::circuit_data::CircuitData; use crate::dag_circuit::{DAGCircuit, NodeType}; use crate::packed_instruction::PackedInstruction; +use crate::{bit_data::NewBitData, circuit_data::CircuitData}; /// An extractable representation of a QuantumCircuit reserved only for /// conversion purposes. @@ -32,8 +29,6 @@ pub struct QuantumCircuitData<'py> { pub name: Option<Bound<'py, PyAny>>, pub calibrations: Option<HashMap<String, Py<PyDict>>>, pub metadata: Option<Bound<'py, PyAny>>, - pub qregs: Option<Bound<'py, PyList>>, - pub cregs: Option<Bound<'py, PyList>>, pub input_vars: Vec<Bound<'py, PyAny>>, pub captured_vars: Vec<Bound<'py, PyAny>>, pub declared_vars: Vec<Bound<'py, PyAny>>, @@ -52,14 +47,6 @@ impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> { .extract() .ok(), metadata: ob.getattr(intern!(py, "metadata")).ok(), - qregs: ob - .getattr(intern!(py, "qregs")) - .map(|ob| ob.downcast_into())? - .ok(), - cregs: ob - .getattr(intern!(py, "cregs")) - .map(|ob| ob.downcast_into())? - .ok(), input_vars: ob .call_method0(intern!(py, "iter_input_vars"))? .try_iter()? @@ -99,10 +86,10 @@ pub fn dag_to_circuit( dag: &DAGCircuit, copy_operations: bool, ) -> PyResult<CircuitData> { - CircuitData::from_packed_instructions( + let mut circuit = CircuitData::from_packed_instructions( py, - dag.qubits().clone(), - dag.clbits().clone(), + NewBitData::from_bit_data(py, dag.qubits()), + NewBitData::from_bit_data(py, dag.clbits()), dag.qargs_interner().clone(), dag.cargs_interner().clone(), dag.topological_op_nodes()?.map(|node_index| { @@ -133,7 +120,15 @@ pub fn dag_to_circuit( } }), dag.get_global_phase(), - ) + )?; + // Manually add qregs and cregs + for reg in dag.qregs.bind(py).values() { + circuit.py_add_qreg(®)?; + } + for reg in dag.cregs.bind(py).values() { + circuit.py_add_creg(®)?; + } + Ok(circuit) } pub fn converters(m: &Bound<PyModule>) -> PyResult<()> { diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 42b334e88207..90b7beab1903 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -193,9 +193,9 @@ pub struct DAGCircuit { dag: StableDiGraph<NodeType, Wire>, #[pyo3(get)] - qregs: Py<PyDict>, + pub qregs: Py<PyDict>, #[pyo3(get)] - cregs: Py<PyDict>, + pub cregs: Py<PyDict>, /// The cache used to intern instruction qargs. pub qargs_interner: Interner<[Qubit]>, @@ -6729,7 +6729,7 @@ impl DAGCircuit { &qubit ))); } - let qubit_index = qc_data.qubits().find(&qubit).unwrap(); + let qubit_index = qc_data.qubits().py_find_bit(&qubit)?.unwrap(); ordered_vec[qubit_index.index()] = new_dag.add_qubit_unchecked(py, &qubit)?; Ok(()) })?; @@ -6741,7 +6741,7 @@ impl DAGCircuit { } else { qc_data .qubits() - .bits() + .py_bits(py)? .iter() .try_for_each(|qubit| -> PyResult<_> { new_dag.add_qubit_unchecked(py, qubit.bind(py))?; @@ -6762,7 +6762,7 @@ impl DAGCircuit { &clbit ))); }; - let clbit_index = qc_data.clbits().find(&clbit).unwrap(); + let clbit_index = qc_data.clbits().py_find_bit(&clbit)?.unwrap(); ordered_vec[clbit_index.index()] = new_dag.add_clbit_unchecked(py, &clbit)?; Ok(()) })?; @@ -6774,7 +6774,7 @@ impl DAGCircuit { } else { qc_data .clbits() - .bits() + .py_bits(py)? .iter() .try_for_each(|clbit| -> PyResult<()> { new_dag.add_clbit_unchecked(py, clbit.bind(py))?; @@ -6797,16 +6797,12 @@ impl DAGCircuit { } // Add all the registers - if let Some(qregs) = qc.qregs { - for qreg in qregs.iter() { - new_dag.add_qreg(py, &qreg)?; - } + for qreg in qc_data.py_qregs(py)?.bind(py).iter() { + new_dag.add_qreg(py, &qreg)?; } - if let Some(cregs) = qc.cregs { - for creg in cregs.iter() { - new_dag.add_creg(py, &creg)?; - } + for creg in qc_data.py_cregs(py)?.bind(py).iter() { + new_dag.add_creg(py, &creg)?; } new_dag.try_extend( @@ -6841,8 +6837,6 @@ impl DAGCircuit { name: None, calibrations: None, metadata: None, - qregs: None, - cregs: None, input_vars: Vec::new(), captured_vars: Vec::new(), declared_vars: Vec::new(), diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index a591dfb1569e..933a715e54d1 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -66,10 +66,12 @@ pub static CONTROL_FLOW_OP: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.controlflow", "ControlFlowOp"); pub static QUBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.quantumregister", "Qubit"); pub static CLBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classicalregister", "Clbit"); +pub static BIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.bit", "Bit"); pub static QUANTUM_REGISTER: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.quantumregister", "QuantumRegister"); pub static CLASSICAL_REGISTER: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classicalregister", "ClassicalRegister"); +pub static REGISTER: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.register", "Register"); pub static PARAMETER_EXPRESSION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression"); pub static PARAMETER_VECTOR: ImportOnceCell = diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 5138438dec33..69e8cbf290b1 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -10,6 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +pub mod bit; pub mod bit_data; pub mod circuit_data; pub mod circuit_instruction; @@ -24,11 +25,13 @@ pub mod interner; pub mod operations; pub mod packed_instruction; pub mod parameter_table; +pub mod register; pub mod slice; pub mod util; mod rustworkx_core_vnext; +use imports::{CLBIT, QUBIT}; use pyo3::prelude::*; use pyo3::types::{PySequence, PyTuple}; @@ -122,6 +125,27 @@ impl From<Clbit> for BitType { } } +/// **For development purposes only.** This ensures we convert to the correct Bit +/// type in Python since [BitData] does not know what its types are inherently. +pub trait ToPyBit { + /// Creates an empty bit from a rust bit instance of the correct type. + /// + /// _**Note:** Should only be used when dealing with fully opaque bits._ + fn to_py_bit(py: Python) -> PyResult<PyObject>; +} + +impl ToPyBit for Qubit { + fn to_py_bit(py: Python) -> PyResult<PyObject> { + QUBIT.get_bound(py).call0().map(|bit| bit.into()) + } +} + +impl ToPyBit for Clbit { + fn to_py_bit(py: Python) -> PyResult<PyObject> { + CLBIT.get_bound(py).call0().map(|bit| bit.into()) + } +} + /// Implement `IntoPyObject` for the reference to a struct or enum declared as `#[pyclass]` that is /// also `Copy`. /// diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs new file mode 100644 index 000000000000..e86d1eed8783 --- /dev/null +++ b/crates/circuit/src/register.rs @@ -0,0 +1,247 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// 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 indexmap::IndexSet; +use pyo3::{exceptions::PyTypeError, intern, types::PyAnyMethods, FromPyObject}; +use std::{ + borrow::Cow, + hash::{Hash, Hasher}, + ops::Index, + sync::Mutex, +}; + +use crate::{ + imports::{CLASSICAL_REGISTER, QUANTUM_REGISTER, REGISTER}, + Clbit, Qubit, +}; + +/// This represents the hash value of a Register according to the register's +/// name and number of qubits. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RegisterAsKey { + Register((String, u32)), + Quantum((String, u32)), + Classical((String, u32)), +} + +impl RegisterAsKey { + #[inline] + pub fn reduce(&self) -> (u32, &str) { + match self { + RegisterAsKey::Register(key) => (key.1, key.0.as_str()), + RegisterAsKey::Quantum(key) => (key.1, key.0.as_str()), + RegisterAsKey::Classical(key) => (key.1, key.0.as_str()), + } + } + + #[inline] + pub fn name(&self) -> &str { + match self { + RegisterAsKey::Register(key) => key.0.as_str(), + RegisterAsKey::Quantum(key) => key.0.as_str(), + RegisterAsKey::Classical(key) => key.0.as_str(), + } + } + + #[inline] + pub fn size(&self) -> u32 { + match self { + RegisterAsKey::Register(key) => key.1, + RegisterAsKey::Quantum(key) => key.1, + RegisterAsKey::Classical(key) => key.1, + } + } + + #[inline] + pub fn type_identifier(&self) -> &str { + match self { + RegisterAsKey::Register(_) => "Register", + RegisterAsKey::Quantum(_) => "QuantumRegister", + RegisterAsKey::Classical(_) => "ClassicalRegister", + } + } +} + +impl<'py> FromPyObject<'py> for RegisterAsKey { + fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult<Self> { + if ob.is_instance(REGISTER.get_bound(ob.py()))? { + let (name, num_qubits) = ( + ob.getattr(intern!(ob.py(), "name"))?.extract()?, + ob.len()? as u32, + ); + if ob.is_instance(CLASSICAL_REGISTER.get_bound(ob.py()))? { + return Ok(RegisterAsKey::Classical((name, num_qubits))); + } else if ob.is_instance(QUANTUM_REGISTER.get_bound(ob.py()))? { + return Ok(RegisterAsKey::Quantum((name, num_qubits))); + } else { + return Ok(RegisterAsKey::Register((name, num_qubits))); + } + } + Err(PyTypeError::new_err( + "The provided argument was not a register.", + )) + } +} +/// Described the desired behavior of a Register. +pub trait Register { + /// The type of bit stored by the [Register] + type Bit; + + /// Returns the size of the [Register]. + fn len(&self) -> usize; + /// Checks if the [Register] is empty. + fn is_empty(&self) -> bool; + /// Returns the name of the [Register]. + fn name(&self) -> &str; + /// Checks if a bit exists within the [Register]. + fn contains(&self, bit: Self::Bit) -> bool; + /// Finds the local index of a certain bit within [Register]. + fn find_index(&self, bit: Self::Bit) -> Option<u32>; + /// Return an iterator over all the bits in the register + fn bits(&self) -> impl ExactSizeIterator<Item = Self::Bit>; + /// Returns the register as a Key + fn as_key(&self) -> &RegisterAsKey; +} + +macro_rules! create_register { + ($name:ident, $bit:ty, $counter:ident, $prefix:literal, $key:expr) => { + static $counter: Mutex<u32> = Mutex::new(0); + + #[derive(Debug, Clone, Eq)] + pub struct $name { + register: IndexSet<<$name as Register>::Bit>, + key: RegisterAsKey, + } + + impl $name { + pub fn new( + size: Option<usize>, + name: Option<String>, + bits: Option<Cow<'_, [$bit]>>, + ) -> Self { + let register: IndexSet<<$name as Register>::Bit> = if let Some(size) = size { + (0..size).map(|bit| <$bit>::new(bit)).collect() + } else if let Some(bits) = bits { + match bits { + Cow::Borrowed(borrowed) => borrowed.iter().copied().collect(), + Cow::Owned(owned) => owned.into_iter().collect(), + } + } else { + panic!("You should only provide either a size or the bit indices, not both.") + }; + let name = if let Some(name) = name { + name + } else { + let count = if let Ok(ref mut count) = $counter.try_lock() { + let curr = **count; + **count += 1; + curr + } else { + panic!("Could not access register counter.") + }; + format!("{}{}", $prefix, count) + }; + let length: u32 = register.len().try_into().unwrap(); + Self { + register, + key: $key((name, length)), + } + } + } + + impl Register for $name { + type Bit = $bit; + + fn len(&self) -> usize { + self.register.len() + } + + fn is_empty(&self) -> bool { + self.register.is_empty() + } + + fn name(&self) -> &str { + self.key.name() + } + + fn contains(&self, bit: Self::Bit) -> bool { + self.register.contains(&bit) + } + + fn find_index(&self, bit: Self::Bit) -> Option<u32> { + self.register.get_index_of(&bit).map(|idx| idx as u32) + } + + fn bits(&self) -> impl ExactSizeIterator<Item = Self::Bit> { + self.register.iter().copied() + } + + fn as_key(&self) -> &RegisterAsKey { + &self.key + } + } + + impl Hash for $name { + fn hash<H: Hasher>(&self, state: &mut H) { + (self.key).hash(state); + } + } + + impl PartialEq for $name { + fn eq(&self, other: &Self) -> bool { + self.register.len() == other.register.len() && self.key == other.key + } + } + + impl Index<usize> for $name { + type Output = $bit; + + fn index(&self, index: usize) -> &Self::Output { + self.register.index(index) + } + } + + impl From<(usize, Option<String>)> for $name { + fn from(value: (usize, Option<String>)) -> Self { + Self::new(Some(value.0), value.1, None) + } + } + + impl From<Cow<'_, [$bit]>> for $name { + fn from(value: Cow<'_, [$bit]>) -> Self { + Self::new(None, None, Some(value)) + } + } + + impl From<(Cow<'_, [$bit]>, Option<String>)> for $name { + fn from(value: (Cow<'_, [$bit]>, Option<String>)) -> Self { + Self::new(None, value.1, Some(value.0)) + } + } + }; +} + +create_register!( + QuantumRegister, + Qubit, + QREG_COUNTER, + "qr", + RegisterAsKey::Quantum +); + +create_register!( + ClassicalRegister, + Clbit, + CREG_COUNTER, + "cr", + RegisterAsKey::Classical +); diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py index 8e4276c16229..c32c030c3938 100644 --- a/qiskit/circuit/library/blueprintcircuit.py +++ b/qiskit/circuit/library/blueprintcircuit.py @@ -69,14 +69,23 @@ def _build(self) -> None: def _invalidate(self) -> None: """Invalidate the current circuit build.""" + # Take out the registers before invalidating + qregs = self._data.qregs + cregs = self._data.cregs self._data = CircuitData(self._data.qubits, self._data.clbits) + for qreg in qregs: + self._data.add_qreg(qreg) + for creg in cregs: + self._data.add_creg(creg) self.global_phase = 0 self._is_built = False @property def qregs(self): """A list of the quantum registers associated with the circuit.""" - return self._qregs + if not self._is_initialized: + return self._qregs + return super().qregs @qregs.setter def qregs(self, qregs): diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 7ab1bae5d8cd..2af597188371 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1096,13 +1096,6 @@ def __init__( "qiskit.circuit.controlflow.builder.ControlFlowBuilderBlock" ] = [] - self.qregs: list[QuantumRegister] = [] - """A list of the :class:`QuantumRegister`\\ s in this circuit. You should not mutate - this.""" - self.cregs: list[ClassicalRegister] = [] - """A list of the :class:`ClassicalRegister`\\ s in this circuit. You should not mutate - this.""" - # Dict mapping Qubit or Clbit instances to tuple comprised of 0) the # corresponding index in circuit.{qubits,clbits} and 1) a list of # Register-int pairs for each Register containing the Bit and its index @@ -1174,26 +1167,18 @@ def _from_circuit_data( if data.num_qubits > 0: if add_regs: qr = QuantumRegister(name="q", bits=data.qubits) - out.qregs = [qr] - out._qubit_indices = { - bit: BitLocations(index, [(qr, index)]) for index, bit in enumerate(data.qubits) - } - else: - out._qubit_indices = { - bit: BitLocations(index, []) for index, bit in enumerate(data.qubits) - } + data.qregs = [qr] + out._qubit_indices = { + bit: BitLocations(*data.get_qubit_location(bit)) for bit in data.qubits + } if data.num_clbits > 0: if add_regs: cr = ClassicalRegister(name="c", bits=data.clbits) - out.cregs = [cr] - out._clbit_indices = { - bit: BitLocations(index, [(cr, index)]) for index, bit in enumerate(data.clbits) - } - else: - out._clbit_indices = { - bit: BitLocations(index, []) for index, bit in enumerate(data.clbits) - } + data.cregs = [cr] + out._clbit_indices = { + bit: BitLocations(*data.get_clbit_location(bit)) for bit in data.clbits + } out._data = data @@ -1445,6 +1430,8 @@ def __deepcopy__(self, memo=None): result._data.replace_bits( qubits=_copy.deepcopy(self._data.qubits, memo), clbits=_copy.deepcopy(self._data.clbits, memo), + qregs=_copy.deepcopy(self._data.qregs, memo), + cregs=_copy.deepcopy(self._data.cregs, memo), ) return result @@ -2237,6 +2224,30 @@ def clbits(self) -> list[Clbit]: this.""" return self._data.clbits + @property + def qregs(self) -> list[QuantumRegister]: + """A list of :class:`Qubit`\\ s in the order that they were added. You should not mutate + this.""" + return self._data.qregs + + @qregs.setter + def qregs(self, other: list[QuantumRegister]): + self._data.qregs = other + for qubit in self.qubits: + self._qubit_indices[qubit] = BitLocations(*self._data.get_qubit_location(qubit)) + + @property + def cregs(self) -> list[ClassicalRegister]: + """A list of :class:`Clbit`\\ s in the order that they were added. You should not mutate + this.""" + return self._data.cregs + + @cregs.setter + def cregs(self, other: list[ClassicalRegister]): + self._data.cregs = other + for clbit in self.clbits: + self._clbit_indices[clbit] = BitLocations(*self._data.get_clbit_location(clbit)) + @property def ancillas(self) -> list[AncillaQubit]: """A list of :class:`AncillaQubit`\\ s in the order that they were added. You should not @@ -3075,16 +3086,10 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: self._add_qreg(register) elif isinstance(register, ClassicalRegister): - self.cregs.append(register) + self._data.add_creg(register) - for idx, bit in enumerate(register): - if bit in self._clbit_indices: - self._clbit_indices[bit].registers.append((register, idx)) - else: - self._data.add_clbit(bit) - self._clbit_indices[bit] = BitLocations( - self._data.num_clbits - 1, [(register, idx)] - ) + for bit in register: + self._clbit_indices[bit] = BitLocations(*self._data.get_clbit_location(bit)) elif isinstance(register, list): self.add_bits(register) @@ -3092,14 +3097,10 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: raise CircuitError("expected a register") def _add_qreg(self, qreg: QuantumRegister) -> None: - self.qregs.append(qreg) + self._data.add_qreg(qreg) - for idx, bit in enumerate(qreg): - if bit in self._qubit_indices: - self._qubit_indices[bit].registers.append((qreg, idx)) - else: - self._data.add_qubit(bit) - self._qubit_indices[bit] = BitLocations(self._data.num_qubits - 1, [(qreg, idx)]) + for bit in qreg: + self._qubit_indices[bit] = BitLocations(*self._data.get_qubit_location(bit)) def add_bits(self, bits: Iterable[Bit]) -> None: """Add Bits to the circuit.""" @@ -3114,10 +3115,10 @@ def add_bits(self, bits: Iterable[Bit]) -> None: self._ancillas.append(bit) if isinstance(bit, Qubit): self._data.add_qubit(bit) - self._qubit_indices[bit] = BitLocations(self._data.num_qubits - 1, []) + self._qubit_indices[bit] = BitLocations(*self._data.get_qubit_location(bit)) elif isinstance(bit, Clbit): self._data.add_clbit(bit) - self._clbit_indices[bit] = BitLocations(self._data.num_clbits - 1, []) + self._clbit_indices[bit] = BitLocations(*self._data.get_clbit_location(bit)) else: raise CircuitError( "Expected an instance of Qubit, Clbit, or " @@ -3177,8 +3178,12 @@ def find_bit(self, bit: Bit) -> BitLocations: try: if isinstance(bit, Qubit): + if bit not in self._qubit_indices: + self._qubit_indices[bit] = BitLocations(*self._data.get_qubit_location(bit)) return self._qubit_indices[bit] elif isinstance(bit, Clbit): + if bit not in self._clbit_indices: + self._clbit_indices[bit] = BitLocations(*self._data.get_clbit_location(bit)) return self._clbit_indices[bit] else: raise CircuitError(f"Could not locate bit of unknown type: {type(bit)}") @@ -3698,8 +3703,16 @@ def copy(self, name: str | None = None) -> typing.Self: Returns: QuantumCircuit: a deepcopy of the current circuit, with the specified name """ - cpy = self.copy_empty_like(name) + if not (name is None or isinstance(name, str)): + raise TypeError( + f"invalid name for a circuit: '{name}'. The name must be a string or 'None'." + ) + + cpy = _copy.copy(self) + _copy_metadata(self, cpy, "alike") cpy._data = self._data.copy() + if name is not None: + cpy.name = name return cpy def copy_empty_like( @@ -3755,9 +3768,7 @@ def copy_empty_like( _copy_metadata(self, cpy, vars_mode) - cpy._data = CircuitData( - self._data.qubits, self._data.clbits, global_phase=self._data.global_phase - ) + cpy._data = self._data.copy_empty_like() if name: cpy.name = name @@ -4071,11 +4082,18 @@ def remove_final_measurements(self, inplace: bool = True) -> Optional["QuantumCi circ.cregs = [] circ._clbit_indices = {} + # Save the old qregs + old_qregs = circ.qregs + # Clear instruction info circ._data = CircuitData( qubits=circ._data.qubits, reserve=len(circ._data), global_phase=circ.global_phase ) + # Re-add old registers + for qreg in old_qregs: + circ.add_register(qreg) + # 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. diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index bd7d720a5dd0..b6c3cee06edf 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -146,9 +146,13 @@ def fix_condition(op): data.replace_bits(qubits=qreg, clbits=creg) data.map_nonstandard_ops(fix_condition) - qc = QuantumCircuit(*regs, name=out_instruction.name) + qc = QuantumCircuit(name=out_instruction.name) qc._data = data + # Re-add the registers. + for reg in regs: + qc.add_register(reg) + if circuit.global_phase: qc.global_phase = circuit.global_phase diff --git a/releasenotes/notes/add_rust_native_bits_and_registers-3691716ca96194c2.yaml b/releasenotes/notes/add_rust_native_bits_and_registers-3691716ca96194c2.yaml new file mode 100644 index 000000000000..613bb4cadad1 --- /dev/null +++ b/releasenotes/notes/add_rust_native_bits_and_registers-3691716ca96194c2.yaml @@ -0,0 +1,13 @@ +--- +features_circuits: + - | + Added rust representation of the :class:`.QuantumRegister` and :class:`.ClassicalRegister` + classes. +upgrade_circuits: + - | + Reformulate :class:`.CircuitData` to use its native representations of :class:`.Bit`, and + :class:`.Register` as the source of truth. With this there is no longer a need to initialize + a circuit from rust with a `py` token. + - | + Refactor :class:`.CircuitData`'s ``BitData`` to store registers and enables users to use + stores bits before initializing them with python counterparts. diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 1b78861c6c00..60951182be02 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -21,7 +21,6 @@ from qiskit import QuantumCircuit, QuantumRegister from qiskit.quantum_info import Operator from qiskit.circuit import ParameterVector, Gate, ControlledGate -from qiskit.circuit.quantumregister import Qubit from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate from qiskit.circuit.library import standard_gates from qiskit.circuit.library import ( @@ -453,8 +452,8 @@ def test_definition_parameters(self, gate_class): self.assertGreaterEqual(len(param_entry), 1) self.assertGreaterEqual(len(float_entry), 1) - param_qc = QuantumCircuit([Qubit() for _ in range(param_gate.num_qubits)]) - float_qc = QuantumCircuit([Qubit() for _ in range(float_gate.num_qubits)]) + param_qc = QuantumCircuit(param_gate.num_qubits) + float_qc = QuantumCircuit(float_gate.num_qubits) param_qc.append(param_gate, param_qc.qubits) float_qc.append(float_gate, float_qc.qubits)