From af13dd2a9e479669f2eea8abc884efd2f1958698 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 27 Dec 2024 16:36:39 -0500 Subject: [PATCH 01/25] Initial: Register Infrastructure in Rust --- crates/circuit/src/bit.rs | 27 ++++++++ crates/circuit/src/lib.rs | 2 + crates/circuit/src/register.rs | 118 +++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 crates/circuit/src/bit.rs create mode 100644 crates/circuit/src/register.rs diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs new file mode 100644 index 000000000000..98758fc87356 --- /dev/null +++ b/crates/circuit/src/bit.rs @@ -0,0 +1,27 @@ +use pyo3::prelude::*; + +use crate::register::{RegisterAsKey, RegistryIndex}; + +#[pyclass(name = "Qubit")] +pub struct PyBit { + reg_ref: RegisterAsKey, // Register identifier + reg_idx: u32, // Index within Register +} + +#[pymethods] +impl PyBit { + #[new] + pub fn new(reg_ref: RegisterAsKey, index: u32) -> Self { + Self { + reg_ref, + reg_idx: index, + } + } + + +} + +pub struct BitInfo { + register_idx: RegistryIndex, + index: u32, +} diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index a4064d44b917..0486cf75a150 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,6 +25,7 @@ pub mod interner; pub mod operations; pub mod packed_instruction; pub mod parameter_table; +pub mod register; pub mod slice; pub mod util; diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs new file mode 100644 index 000000000000..794ad2b44cd6 --- /dev/null +++ b/crates/circuit/src/register.rs @@ -0,0 +1,118 @@ +use std::{hash::{DefaultHasher, Hash, Hasher}, ops::Index}; +use indexmap::IndexSet; +use pyo3::{intern, types::PyAnyMethods, FromPyObject}; + +use crate::{Clbit, Qubit}; + +/// This represents the hash value of a Register according to the register's +/// name and number of qubits. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct RegisterAsKey(u64); + +impl RegisterAsKey { + pub fn new(name: Option<&str>, num_qubits: u32) -> Self { + let mut hasher = DefaultHasher::default(); + (name, num_qubits).hash(&mut hasher); + Self(hasher.finish()) + } +} + +impl<'py> FromPyObject<'py> for RegisterAsKey { + fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult { + let (name, num_qubits) = ( + ob.getattr(intern!(ob.py(), "name"))? + .extract::>()?, + ob.getattr(intern!(ob.py(), "num_qubits"))?.extract()?, + ); + Ok(RegisterAsKey::new( + name.as_ref().map(|x| x.as_str()), + num_qubits, + )) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct RegistryIndex(u32); + +impl From for RegistryIndex { + fn from(value: usize) -> Self { + Self(value.try_into().expect("Index falls out of range")) + } +} + +impl From for RegistryIndex { + fn from(value: u32) -> Self { + Self(value) + } +} + +/// Represents a collection of registers of a certain type within a circuit. +#[derive(Debug, Clone)] +pub(crate) struct CircuitRegistry { + registry: IndexSet, +} + +impl Index for CircuitRegistry { + type Output = T; + + fn index(&self, index: RegistryIndex) -> &Self::Output { + &self.registry[index.0 as usize] + } +} + +/// 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 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; +} + +macro_rules! create_register { + ($name:ident, $bit:ty) => { + #[derive(Debug, Clone)] + pub struct $name { + register: IndexSet<<$name as Register>::Bit>, + name: Option, + } + + impl $name { + pub fn new(size: usize, name: Option) -> Self { + Self { + register: (0..size).map(|bit| <$bit>::new(bit)).collect(), + name, + } + } + } + + impl Register for $name { + type Bit = $bit; + + fn len(&self) -> usize { + self.register.len() + } + + fn contains(&self, bit: Self::Bit) -> bool { + self.register.contains(&bit) + } + + fn find_index(&self, bit: Self::Bit) -> Option { + self.register.get_index_of(&bit).map(|idx| idx as u32) + } + } + + impl Hash for $name { + fn hash(&self, state: &mut H) { + (self.name.as_ref(), self.len()).hash(state); + } + } + }; +} + +create_register!(QuantumRegister, Qubit); +create_register!(ClassicalRegister, Clbit); From cf673db3526e3c02ad758f9d2328f377fe3fa78e Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:32:56 -0500 Subject: [PATCH 02/25] Add: `bits` method for register. - Add missing traits on `BitInfo` --- crates/circuit/src/bit.rs | 5 ++- crates/circuit/src/register.rs | 81 ++++++++++++++++++++-------------- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs index 98758fc87356..26be3ecd2619 100644 --- a/crates/circuit/src/bit.rs +++ b/crates/circuit/src/bit.rs @@ -2,7 +2,7 @@ use pyo3::prelude::*; use crate::register::{RegisterAsKey, RegistryIndex}; -#[pyclass(name = "Qubit")] +#[pyclass(name = "Bit")] pub struct PyBit { reg_ref: RegisterAsKey, // Register identifier reg_idx: u32, // Index within Register @@ -17,10 +17,11 @@ impl PyBit { reg_idx: index, } } - } +/// Keeps information about where a qubit is located within the circuit. +#[derive(Debug, Clone, Copy, Hash)] pub struct BitInfo { register_idx: RegistryIndex, index: u32, diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs index 794ad2b44cd6..3d9a380ca9f1 100644 --- a/crates/circuit/src/register.rs +++ b/crates/circuit/src/register.rs @@ -7,7 +7,7 @@ use crate::{Clbit, Qubit}; /// This represents the hash value of a Register according to the register's /// name and number of qubits. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct RegisterAsKey(u64); +pub struct RegisterAsKey(u64); impl RegisterAsKey { pub fn new(name: Option<&str>, num_qubits: u32) -> Self { @@ -30,36 +30,6 @@ impl<'py> FromPyObject<'py> for RegisterAsKey { )) } } - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct RegistryIndex(u32); - -impl From for RegistryIndex { - fn from(value: usize) -> Self { - Self(value.try_into().expect("Index falls out of range")) - } -} - -impl From for RegistryIndex { - fn from(value: u32) -> Self { - Self(value) - } -} - -/// Represents a collection of registers of a certain type within a circuit. -#[derive(Debug, Clone)] -pub(crate) struct CircuitRegistry { - registry: IndexSet, -} - -impl Index for CircuitRegistry { - type Output = T; - - fn index(&self, index: RegistryIndex) -> &Self::Output { - &self.registry[index.0 as usize] - } -} - /// Described the desired behavior of a Register. pub trait Register { /// The type of bit stored by the [Register] @@ -71,11 +41,13 @@ pub trait 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; + /// Return an iterator over all the bits in the register + fn bits(&self) -> impl ExactSizeIterator; } macro_rules! create_register { ($name:ident, $bit:ty) => { - #[derive(Debug, Clone)] + #[derive(Debug, Clone, Eq)] pub struct $name { register: IndexSet<<$name as Register>::Bit>, name: Option, @@ -104,6 +76,10 @@ macro_rules! create_register { fn find_index(&self, bit: Self::Bit) -> Option { self.register.get_index_of(&bit).map(|idx| idx as u32) } + + fn bits(&self) -> impl ExactSizeIterator { + self.register.iter().copied() + } } impl Hash for $name { @@ -111,8 +87,49 @@ macro_rules! create_register { (self.name.as_ref(), self.len()).hash(state); } } + + impl PartialEq for $name { + fn eq(&self, other: &Self) -> bool { + self.register.len() == other.register.len() && self.name == other.name + } + } }; } create_register!(QuantumRegister, Qubit); create_register!(ClassicalRegister, Clbit); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RegistryIndex(u32); + +impl From for RegistryIndex { + fn from(value: usize) -> Self { + Self(value.try_into().expect("Index falls out of range")) + } +} + +impl From for RegistryIndex { + fn from(value: u32) -> Self { + Self(value) + } +} +/// Represents a collection of registers of a certain type within a circuit. +#[derive(Debug, Clone)] +pub(crate) struct CircuitRegistry { + registry: IndexSet, +} + +impl Index for CircuitRegistry { + type Output = T; + + fn index(&self, index: RegistryIndex) -> &Self::Output { + &self.registry[index.0 as usize] + } +} + +impl CircuitRegistry { + /// Retreives the index of a register if it exists within a registry. + pub fn find_index(&self, register: &T) -> Option { + self.registry.get_index_of(register).map(RegistryIndex::from) + } +} From 00eeb7dbd302a68bd0d8b608207c317e8649b2a7 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:57:18 -0500 Subject: [PATCH 03/25] Use interners for registry --- crates/circuit/src/bit.rs | 46 ++++++++++++++++++------ crates/circuit/src/interner.rs | 19 ++++++++++ crates/circuit/src/register.rs | 64 ++++++++++++++-------------------- 3 files changed, 82 insertions(+), 47 deletions(-) diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs index 26be3ecd2619..e5535741559a 100644 --- a/crates/circuit/src/bit.rs +++ b/crates/circuit/src/bit.rs @@ -1,28 +1,54 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + use pyo3::prelude::*; -use crate::register::{RegisterAsKey, RegistryIndex}; +use crate::{ + interner::Interned, + register::{Register, RegisterAsKey}, +}; +/// Object representing a Python bit, that allows us to keep backwards compatibility +/// with the previous structure. #[pyclass(name = "Bit")] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PyBit { - reg_ref: RegisterAsKey, // Register identifier - reg_idx: u32, // Index within Register + register: Option, // Register identifier + index: Option, // Index within Register } #[pymethods] impl PyBit { #[new] - pub fn new(reg_ref: RegisterAsKey, index: u32) -> Self { + #[pyo3(signature=(register=None, index=None))] + pub fn new(register: Option, index: Option) -> Self { Self { - reg_ref, - reg_idx: index, + register, + index, + } + } + + fn __eq__<'py>(slf: Bound<'py, Self>, other: Bound<'py, Self>) -> bool { + let borrowed = slf.borrow(); + let other_borrowed = other.borrow(); + if borrowed.register.is_some() && borrowed.index.is_some() { + return borrowed.register == other_borrowed.register + && borrowed.index == other_borrowed.index; } + + slf.is(&other) + } + + fn __hash__(slf: Bound<'_, Self>) -> PyResult { + let borrowed = slf.borrow(); + let mut hasher = DefaultHasher::new(); + borrowed.hash(&mut hasher); + Ok(hasher.finish() as isize) } - } /// Keeps information about where a qubit is located within the circuit. -#[derive(Debug, Clone, Copy, Hash)] -pub struct BitInfo { - register_idx: RegistryIndex, +#[derive(Debug, Clone)] +pub struct BitInfo { + register_idx: Interned, index: u32, } diff --git a/crates/circuit/src/interner.rs b/crates/circuit/src/interner.rs index a72efb037afb..b42683fe32c5 100644 --- a/crates/circuit/src/interner.rs +++ b/crates/circuit/src/interner.rs @@ -196,6 +196,25 @@ where _type: PhantomData, } } + + /// Checks whether an item is already packed in the interner + pub fn contains(&self, value: &T) -> bool + where + T: Hash + Eq, + { + self.0.contains(value) + } + + /// Retrieves interner index of anitem if it exists + pub fn get_interned(&self, value: &T) -> Option> + where + T: Hash + Eq, + { + self.0.get_index_of(value).map(|index| Interned { + index: index as u32, + _type: PhantomData, + }) + } } #[cfg(test)] diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs index 3d9a380ca9f1..3e3a826fdb00 100644 --- a/crates/circuit/src/register.rs +++ b/crates/circuit/src/register.rs @@ -1,8 +1,11 @@ -use std::{hash::{DefaultHasher, Hash, Hasher}, ops::Index}; use indexmap::IndexSet; use pyo3::{intern, types::PyAnyMethods, FromPyObject}; +use std::hash::{DefaultHasher, Hash, Hasher}; -use crate::{Clbit, Qubit}; +use crate::{ + interner::{Interned, Interner}, + Clbit, Qubit, +}; /// This represents the hash value of a Register according to the register's /// name and number of qubits. @@ -25,7 +28,7 @@ impl<'py> FromPyObject<'py> for RegisterAsKey { ob.getattr(intern!(ob.py(), "num_qubits"))?.extract()?, ); Ok(RegisterAsKey::new( - name.as_ref().map(|x| x.as_str()), + name.as_deref(), num_qubits, )) } @@ -34,7 +37,7 @@ impl<'py> FromPyObject<'py> for RegisterAsKey { 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 a bit exists within the [Register]. @@ -42,7 +45,7 @@ pub trait Register { /// Finds the local index of a certain bit within [Register]. fn find_index(&self, bit: Self::Bit) -> Option; /// Return an iterator over all the bits in the register - fn bits(&self) -> impl ExactSizeIterator; + fn bits(&self) -> impl ExactSizeIterator; } macro_rules! create_register { @@ -52,7 +55,7 @@ macro_rules! create_register { register: IndexSet<<$name as Register>::Bit>, name: Option, } - + impl $name { pub fn new(size: usize, name: Option) -> Self { Self { @@ -61,27 +64,27 @@ macro_rules! create_register { } } } - + impl Register for $name { type Bit = $bit; - + fn len(&self) -> usize { self.register.len() } - + fn contains(&self, bit: Self::Bit) -> bool { self.register.contains(&bit) } - + fn find_index(&self, bit: Self::Bit) -> Option { self.register.get_index_of(&bit).map(|idx| idx as u32) } - fn bits(&self) -> impl ExactSizeIterator { + fn bits(&self) -> impl ExactSizeIterator { self.register.iter().copied() } } - + impl Hash for $name { fn hash(&self, state: &mut H) { (self.name.as_ref(), self.len()).hash(state); @@ -99,37 +102,24 @@ macro_rules! create_register { create_register!(QuantumRegister, Qubit); create_register!(ClassicalRegister, Clbit); -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RegistryIndex(u32); - -impl From for RegistryIndex { - fn from(value: usize) -> Self { - Self(value.try_into().expect("Index falls out of range")) - } -} - -impl From for RegistryIndex { - fn from(value: u32) -> Self { - Self(value) - } -} /// Represents a collection of registers of a certain type within a circuit. #[derive(Debug, Clone)] -pub(crate) struct CircuitRegistry { - registry: IndexSet, +pub(crate) struct CircuitRegistry { + registry: Interner, } -impl Index for CircuitRegistry { - type Output = T; - - fn index(&self, index: RegistryIndex) -> &Self::Output { - &self.registry[index.0 as usize] +impl CircuitRegistry { + pub fn add_register(&mut self, register: T) -> Interned { + self.registry.insert_owned(register) } -} -impl CircuitRegistry { /// Retreives the index of a register if it exists within a registry. - pub fn find_index(&self, register: &T) -> Option { - self.registry.get_index_of(register).map(RegistryIndex::from) + pub fn find_index(&self, register: &T) -> Option> { + self.registry.get_interned(register) + } + + /// Checks if a register exists within a circuit + pub fn contains(&self, register: &T) -> bool { + self.registry.contains(register) } } From bfa73899e27df6690769d3b471861d8b24023e51 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:41:03 -0500 Subject: [PATCH 04/25] Fix: Add functional python `Bits` --- crates/circuit/src/bit.rs | 110 ++++++++++++++++++++++++++++++--- crates/circuit/src/lib.rs | 3 + crates/circuit/src/register.rs | 48 +++++++++----- 3 files changed, 135 insertions(+), 26 deletions(-) diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs index e5535741559a..65c7252ede83 100644 --- a/crates/circuit/src/bit.rs +++ b/crates/circuit/src/bit.rs @@ -1,16 +1,17 @@ use std::hash::{DefaultHasher, Hash, Hasher}; -use pyo3::prelude::*; +use pyo3::{prelude::*, types::PyDict}; use crate::{ + circuit_data::CircuitError, interner::Interned, register::{Register, RegisterAsKey}, }; /// Object representing a Python bit, that allows us to keep backwards compatibility /// with the previous structure. -#[pyclass(name = "Bit")] -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[pyclass(name = "Bit", module = "qiskit._accelerate.bit", subclass)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PyBit { register: Option, // Register identifier index: Option, // Index within Register @@ -20,10 +21,13 @@ pub struct PyBit { impl PyBit { #[new] #[pyo3(signature=(register=None, index=None))] - pub fn new(register: Option, index: Option) -> Self { - Self { - register, - index, + pub fn new(register: Option, index: Option) -> PyResult { + match (®ister, index) { + (None, None) => Ok(Self { register, index }), + (Some(_), Some(_)) => Ok(Self { register, index }), + _ => Err(CircuitError::new_err( + "You should provide both an index and a register, not just one of them.", + )), } } @@ -38,17 +42,103 @@ impl PyBit { slf.is(&other) } - fn __hash__(slf: Bound<'_, Self>) -> PyResult { - let borrowed = slf.borrow(); + fn __hash__(&self, py: Python<'_>) -> PyResult { + if let (Some(reg), Some(idx)) = (self.register.as_ref(), self.index) { + return (reg.reduce(), idx).to_object(py).bind(py).hash(); + } + + // If registers are unavailable, hash by pointer value. let mut hasher = DefaultHasher::new(); - borrowed.hash(&mut hasher); + let pointer_val = self as *const Self; + pointer_val.hash(&mut hasher); Ok(hasher.finish() as isize) } + + fn __copy__(slf: Bound) -> Bound { + slf + } + + #[pyo3(signature = (_memo=None))] + fn __deepcopy__<'py>( + slf: Bound<'py, Self>, + _memo: Option>, + ) -> PyResult> { + let borrowed: PyRef = slf.borrow(); + if borrowed.index.is_none() && borrowed.register.is_none() { + return Ok(slf); + } + let copy = slf + .get_type() + .call_method1("__new__", (slf.get_type(),))? + .downcast_into::()?; + let mut copy_mut = copy.borrow_mut(); + copy_mut.register = borrowed.register.clone(); + copy_mut.index = borrowed.index; + Ok(copy) + } + + fn __getstate__(slf: PyRef<'_, Self>) -> (Option<(String, u32)>, Option) { + ( + slf.register.as_ref().map(|reg| { + let (name, num_qubits) = reg.reduce(); + (name.to_string(), num_qubits) + }), + slf.index.as_ref().copied(), + ) + } + + fn __setstate__(mut slf: PyRefMut<'_, Self>, state: (Option<(String, u32)>, Option)) { + slf.register = state + .0 + .map(|(name, num_qubits)| RegisterAsKey::new(name.as_str(), num_qubits)); + slf.index = state.1; + } } +macro_rules! create_py_bit { + ($name:ident, $pyname:literal, $module:literal) => { + #[pyclass(name=$pyname, extends=PyBit, subclass, module=$module)] + #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] + pub struct $name(); + + #[pymethods] + impl $name { + #[new] + #[pyo3(signature = (register=None, index=None))] + pub fn py_new( + register: Option, + index: Option, + ) -> PyResult<(Self, PyBit)> { + Ok((Self(), PyBit::new(register, index)?)) + } + } + }; +} + +// Create python instances +create_py_bit! {PyQubit, "Qubit", "qiskit._accelerate.bit"} +create_py_bit! {PyClbit, "Clbit", "qiskit._accelerate.bit"} + /// Keeps information about where a qubit is located within the circuit. #[derive(Debug, Clone)] pub struct BitInfo { register_idx: Interned, index: u32, } + +impl BitInfo { + pub fn new(register_idx: Interned, index: u32) -> Self { + Self { + register_idx, + index, + } + } + + pub fn register_index(&self) -> Interned { + self.register_idx + } + + pub fn index(&self) -> u32 { + self.index + } +} diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 0486cf75a150..619cd3ec885e 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -125,6 +125,9 @@ impl From for BitType { } pub fn circuit(m: &Bound) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs index 3e3a826fdb00..6d383df036be 100644 --- a/crates/circuit/src/register.rs +++ b/crates/circuit/src/register.rs @@ -1,36 +1,40 @@ use indexmap::IndexSet; use pyo3::{intern, types::PyAnyMethods, FromPyObject}; -use std::hash::{DefaultHasher, Hash, Hasher}; +use std::{ + hash::{Hash, Hasher}, + sync::Mutex, +}; use crate::{ interner::{Interned, Interner}, Clbit, Qubit, }; +static REGISTER_INSTANCE_COUNTER: Mutex = Mutex::new(0); +static PREFIX: &str = "reg"; + /// This represents the hash value of a Register according to the register's /// name and number of qubits. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RegisterAsKey(u64); +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RegisterAsKey(String, u32); impl RegisterAsKey { - pub fn new(name: Option<&str>, num_qubits: u32) -> Self { - let mut hasher = DefaultHasher::default(); - (name, num_qubits).hash(&mut hasher); - Self(hasher.finish()) + pub fn new(name: &str, num_qubits: u32) -> Self { + Self(name.to_string(), num_qubits) + } + + pub fn reduce(&self) -> (&str, u32) { + (self.0.as_str(), self.1) } } impl<'py> FromPyObject<'py> for RegisterAsKey { fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult { let (name, num_qubits) = ( - ob.getattr(intern!(ob.py(), "name"))? - .extract::>()?, - ob.getattr(intern!(ob.py(), "num_qubits"))?.extract()?, + ob.getattr(intern!(ob.py(), "name"))?.extract()?, + ob.len()? as u32, ); - Ok(RegisterAsKey::new( - name.as_deref(), - num_qubits, - )) + Ok(RegisterAsKey(name, num_qubits)) } } /// Described the desired behavior of a Register. @@ -53,11 +57,23 @@ macro_rules! create_register { #[derive(Debug, Clone, Eq)] pub struct $name { register: IndexSet<<$name as Register>::Bit>, - name: Option, + name: String, } impl $name { pub fn new(size: usize, name: Option) -> Self { + let name = if let Some(name) = name { + name + } else { + let count = if let Ok(ref mut count) = REGISTER_INSTANCE_COUNTER.try_lock() { + let curr = **count; + **count += 1; + curr + } else { + panic!("Could not access register counter.") + }; + format!("{}{}", PREFIX, count) + }; Self { register: (0..size).map(|bit| <$bit>::new(bit)).collect(), name, @@ -87,7 +103,7 @@ macro_rules! create_register { impl Hash for $name { fn hash(&self, state: &mut H) { - (self.name.as_ref(), self.len()).hash(state); + (self.name.as_str(), self.len()).hash(state); } } From db42bab8ee2e5476a6aaf3c9d2308a320bd4ac1c Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 3 Jan 2025 01:06:11 -0500 Subject: [PATCH 05/25] Initial Python registers --- crates/circuit/src/register.rs | 92 +++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs index 6d383df036be..1bfc0de27342 100644 --- a/crates/circuit/src/register.rs +++ b/crates/circuit/src/register.rs @@ -1,11 +1,18 @@ use indexmap::IndexSet; -use pyo3::{intern, types::PyAnyMethods, FromPyObject}; +use pyo3::{ + intern, + prelude::*, + types::{PyAnyMethods, PySet}, + FromPyObject, +}; use std::{ hash::{Hash, Hasher}, sync::Mutex, }; use crate::{ + bit::PyBit, + circuit_data::CircuitError, interner::{Interned, Interner}, Clbit, Qubit, }; @@ -53,7 +60,9 @@ pub trait Register { } macro_rules! create_register { - ($name:ident, $bit:ty) => { + ($name:ident, $bit:ty, $counter:ident, $prefix:literal) => { + static $counter: Mutex = Mutex::new(0); + #[derive(Debug, Clone, Eq)] pub struct $name { register: IndexSet<<$name as Register>::Bit>, @@ -65,14 +74,14 @@ macro_rules! create_register { let name = if let Some(name) = name { name } else { - let count = if let Ok(ref mut count) = REGISTER_INSTANCE_COUNTER.try_lock() { + 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) + format!("{}{}", $prefix, count) }; Self { register: (0..size).map(|bit| <$bit>::new(bit)).collect(), @@ -115,8 +124,8 @@ macro_rules! create_register { }; } -create_register!(QuantumRegister, Qubit); -create_register!(ClassicalRegister, Clbit); +create_register!(QuantumRegister, Qubit, QREG_COUNTER, "qr"); +create_register!(ClassicalRegister, Clbit, CREG_COUNTER, "cr"); /// Represents a collection of registers of a certain type within a circuit. #[derive(Debug, Clone)] @@ -139,3 +148,74 @@ impl CircuitRegistry { self.registry.contains(register) } } + +/// Python representation of a generic register +#[derive(Debug, Clone)] +#[pyclass(name = "Register", module = "qiskit.circuit.register", subclass)] +pub struct PyRegister { + /// Bits are stored in Python-space. + bits: Vec>, + /// Name of the register in question + name: String, + /// Size of the register + size: u32, +} + +#[pymethods] +impl PyRegister { + #[new] + pub fn new( + py: Python, + mut size: Option, + mut name: Option, + bits: Option>>, + ) -> PyResult { + if (size.is_none(), bits.is_none()) == (false, false) || (size.is_some() && bits.is_some()) + { + return Err( + CircuitError::new_err( + format!("Exactly one of the size or bits arguments can be provided. Provided size={:?} bits={:?}.", size, bits) + ) + ); + } + if let Some(bits) = bits.as_ref() { + size = Some(bits.len() as u32); + } + if name.is_none() { + let count = if let Ok(ref mut count) = REGISTER_INSTANCE_COUNTER.try_lock() { + let curr = **count; + **count += 1; + curr + } else { + panic!("Could not access register counter.") + }; + name = Some(format!("{}{}", "reg", count)); + } + if let Some(bits) = bits { + if size != Some(PySet::new_bound(py, bits.iter())?.len() as u32) { + return Err(CircuitError::new_err(format!( + "Register bits must not be duplicated. bits={:?}", + bits + ))); + } + Ok(Self { + bits, + name: name.unwrap(), + size: size.unwrap(), + }) + } else { + let name = name.unwrap(); + let size = size.unwrap(); + let bits = (0..size) + .map(|idx| { + Py::new( + py, + PyBit::new(Some(RegisterAsKey(name.clone(), size)), Some(idx)).unwrap(), + ) + .unwrap() + }) + .collect(); + Ok(Self { bits, name, size }) + } + } +} From 20f0ebfdec7301ae2aa2a2a4649b1732fbf46e7e Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 3 Jan 2025 14:01:04 -0500 Subject: [PATCH 06/25] Finalize Python Registers, prepare for replacements. --- crates/circuit/src/bit.rs | 40 +++- crates/circuit/src/lib.rs | 3 + crates/circuit/src/register.rs | 377 +++++++++++++++++++++++++++++++-- 3 files changed, 397 insertions(+), 23 deletions(-) diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs index 65c7252ede83..307214d31d67 100644 --- a/crates/circuit/src/bit.rs +++ b/crates/circuit/src/bit.rs @@ -1,6 +1,6 @@ use std::hash::{DefaultHasher, Hash, Hasher}; -use pyo3::{prelude::*, types::PyDict}; +use pyo3::{exceptions::PyTypeError, prelude::*, types::PyDict}; use crate::{ circuit_data::CircuitError, @@ -90,13 +90,33 @@ impl PyBit { fn __setstate__(mut slf: PyRefMut<'_, Self>, state: (Option<(String, u32)>, Option)) { slf.register = state .0 - .map(|(name, num_qubits)| RegisterAsKey::new(name.as_str(), num_qubits)); + .map(|(name, num_qubits)| RegisterAsKey::Register((name, num_qubits))); slf.index = state.1; } + + fn __repr__(slf: Bound) -> PyResult { + let borrowed = slf.borrow(); + if borrowed.register.is_none() && borrowed.index.is_none() { + return Ok(slf.py_super()?.repr()?.to_string()); + } + let reg = borrowed.register.as_ref().unwrap(); + Ok(format!( + "{}({}({:?}, {}), {})", + slf.get_type().name()?, + reg.type_identifier(), + reg.name(), + reg.index(), + borrowed.index.unwrap() + )) + } + + pub fn is_new(&self) -> bool { + self.index.is_none() && self.register.is_none() + } } macro_rules! create_py_bit { - ($name:ident, $pyname:literal, $module:literal) => { + ($name:ident, $pyname:literal, $reg_type:pat, $module:literal) => { #[pyclass(name=$pyname, extends=PyBit, subclass, module=$module)] #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct $name(); @@ -109,15 +129,23 @@ macro_rules! create_py_bit { register: Option, index: Option, ) -> PyResult<(Self, PyBit)> { - Ok((Self(), PyBit::new(register, index)?)) + if register.is_none() || matches!(register, Some($reg_type)) { + Ok((Self(), PyBit::new(register, index)?)) + } else { + Err(PyTypeError::new_err(format!( + "The incorrect register was assigned. Bit type {}, Register type {}", + $pyname, + register.unwrap().type_identifier() + ))) + } } } }; } // Create python instances -create_py_bit! {PyQubit, "Qubit", "qiskit._accelerate.bit"} -create_py_bit! {PyClbit, "Clbit", "qiskit._accelerate.bit"} +create_py_bit! {PyQubit, "Qubit", RegisterAsKey::Quantum(_), "qiskit._accelerate.bit"} +create_py_bit! {PyClbit, "Clbit", RegisterAsKey::Classical(_), "qiskit._accelerate.bit"} /// Keeps information about where a qubit is located within the circuit. #[derive(Debug, Clone)] diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 619cd3ec885e..a48cf2fc2f40 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -128,6 +128,9 @@ pub fn circuit(m: &Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs index 1bfc0de27342..48f3e399bebc 100644 --- a/crates/circuit/src/register.rs +++ b/crates/circuit/src/register.rs @@ -1,9 +1,11 @@ +use hashbrown::HashMap; use indexmap::IndexSet; use pyo3::{ + exceptions::{PyTypeError, PyValueError}, intern, prelude::*, - types::{PyAnyMethods, PySet}, - FromPyObject, + types::{PyAnyMethods, PyList, PySet}, + FromPyObject, PyTypeInfo, }; use std::{ hash::{Hash, Hasher}, @@ -11,37 +13,80 @@ use std::{ }; use crate::{ - bit::PyBit, + bit::{PyBit, PyClbit, PyQubit}, circuit_data::CircuitError, interner::{Interned, Interner}, + slice::PySequenceIndex, Clbit, Qubit, }; static REGISTER_INSTANCE_COUNTER: Mutex = Mutex::new(0); -static PREFIX: &str = "reg"; /// 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 struct RegisterAsKey(String, u32); +pub enum RegisterAsKey { + Register((String, u32)), + Quantum((String, u32)), + Classical((String, u32)), +} impl RegisterAsKey { - pub fn new(name: &str, num_qubits: u32) -> Self { - Self(name.to_string(), num_qubits) + #[inline] + pub fn reduce(&self) -> (&str, u32) { + match self { + RegisterAsKey::Register((name, num_qubits)) => (name.as_str(), *num_qubits), + RegisterAsKey::Quantum((name, num_qubits)) => (name.as_str(), *num_qubits), + RegisterAsKey::Classical((name, num_qubits)) => (name.as_str(), *num_qubits), + } } - pub fn reduce(&self) -> (&str, u32) { - (self.0.as_str(), self.1) + #[inline] + pub fn name(&self) -> &str { + match self { + RegisterAsKey::Register((name, _)) => name.as_str(), + RegisterAsKey::Quantum((name, _)) => name.as_str(), + RegisterAsKey::Classical((name, _)) => name.as_str(), + } + } + + #[inline] + pub fn index(&self) -> u32 { + match self { + RegisterAsKey::Register((_, idx)) => *idx, + RegisterAsKey::Quantum((_, idx)) => *idx, + RegisterAsKey::Classical((_, idx)) => *idx, + } + } + + #[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 { - let (name, num_qubits) = ( - ob.getattr(intern!(ob.py(), "name"))?.extract()?, - ob.len()? as u32, - ); - Ok(RegisterAsKey(name, num_qubits)) + if ob.is_instance(&PyRegister::type_object_bound(ob.py()))? { + let (name, num_qubits) = ( + ob.getattr(intern!(ob.py(), "name"))?.extract()?, + ob.len()? as u32, + ); + if ob.downcast::().is_ok() { + return Ok(RegisterAsKey::Classical((name, num_qubits))); + } else if ob.downcast::().is_ok() { + 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. @@ -149,6 +194,12 @@ impl CircuitRegistry { } } +#[derive(FromPyObject)] +enum SliceOrInt<'py> { + Slice(PySequenceIndex<'py>), + List(Vec), +} + /// Python representation of a generic register #[derive(Debug, Clone)] #[pyclass(name = "Register", module = "qiskit.circuit.register", subclass)] @@ -156,9 +207,13 @@ pub struct PyRegister { /// Bits are stored in Python-space. bits: Vec>, /// Name of the register in question + #[pyo3(get)] name: String, /// Size of the register + #[pyo3(get)] size: u32, + /// Mapping of the hash value of each bit and their index in the register. + bit_indices: HashMap, } #[pymethods] @@ -198,24 +253,312 @@ impl PyRegister { bits ))); } + let bit_indices: HashMap = bits + .iter() + .enumerate() + .flat_map(|(idx, obj)| -> PyResult<(isize, u32)> { + Ok((obj.bind(py).hash()?, idx as u32)) + }) + .collect(); Ok(Self { bits, name: name.unwrap(), size: size.unwrap(), + bit_indices, }) } else { let name = name.unwrap(); let size = size.unwrap(); - let bits = (0..size) + let bits: Vec> = (0..size) .map(|idx| { Py::new( py, - PyBit::new(Some(RegisterAsKey(name.clone(), size)), Some(idx)).unwrap(), + PyBit::new( + Some(RegisterAsKey::Register((name.clone(), size))), + Some(idx), + ) + .unwrap(), ) .unwrap() }) .collect(); - Ok(Self { bits, name, size }) + let bit_indices: HashMap = bits + .iter() + .enumerate() + .flat_map(|(idx, obj)| -> PyResult<(isize, u32)> { + Ok((obj.bind(py).hash()?, idx as u32)) + }) + .collect(); + Ok(Self { + bits, + name, + size, + bit_indices, + }) + } + } + + fn __repr__(slf: Bound) -> PyResult { + let borrowed = slf.borrow(); + Ok(format!( + "{}({:?}, {})", + slf.get_type().name()?, + borrowed.name, + borrowed.size + )) + } + + fn __len__(&self) -> usize { + self.size as usize + } + + fn __getitem__(&self, py: Python, key: SliceOrInt) -> PyResult { + match key { + SliceOrInt::Slice(py_sequence_index) => { + let sequence = py_sequence_index.with_len(self.size.try_into().unwrap())?; + match sequence { + crate::slice::SequenceIndex::Int(idx) => { + Ok(self.bits[idx].clone_ref(py).into_any()) + } + _ => Ok(PyList::new_bound( + py, + sequence.iter().map(|idx| self.bits[idx].clone_ref(py)), + ) + .into()), + } + } + SliceOrInt::List(vec) => { + if vec.iter().max() < Some(&(self.size as usize)) { + Ok( + PyList::new_bound(py, vec.iter().map(|idx| self.bits[*idx].clone_ref(py))) + .into(), + ) + } else { + Err(CircuitError::new_err("Register index is our of range")) + } + } + } + } + + fn __contains__(&self, bit: &Bound) -> PyResult { + Ok(self.bit_indices.contains_key(&bit.hash()?)) + } + + fn index(slf: Bound, bit: &Bound) -> PyResult { + let borrowed = slf.borrow(); + if borrowed.__contains__(bit)? { + Ok(borrowed.bit_indices[&bit.hash()?]) + } else { + Err(PyValueError::new_err(format!( + "Bit {} not found in Register {}.", + bit.repr()?, + slf.repr()?, + ))) + } + } + + fn __iter__<'py>(&'py self, py: Python<'py>) -> PyResult> { + PyList::new_bound(py, self.bits.iter().map(|obj| obj.clone_ref(py))) + .into_any() + .iter() + } + + fn __getstate__(&self, py: Python) -> (String, u32, PyObject) { + ( + self.name.clone(), + self.size, + self.bits + .iter() + .map(|bit| bit.clone_ref(py)) + .collect::>() + .into_py(py), + ) + } + + fn __setstate__(&mut self, py: Python, state: (String, u32, PyObject)) -> PyResult<()> { + self.name = state.0; + self.size = state.1; + self.bits = state.2.extract(py)?; + self.bit_indices = self + .bits + .iter() + .enumerate() + .flat_map(|(idx, obj)| -> PyResult<(isize, u32)> { + Ok((obj.bind(py).hash()?, idx as u32)) + }) + .collect(); + Ok(()) + } + + fn __eq__(slf: Bound, other: Bound) -> PyResult { + if slf.is(&other) { + return Ok(true); + } + + let self_borrowed = slf.borrow(); + let other_borrowed = other.borrow(); + + Ok(slf.get_type().eq(other.get_type())? + && slf.repr()?.to_string() == other.repr()?.to_string() + && self_borrowed + .bits + .iter() + .zip(other_borrowed.bits.iter()) + .filter_map(|(bit, other)| -> Option { + let borrowed_bit = bit.borrow(slf.py()); + let borrowed_other = other.borrow(slf.py()); + match (borrowed_bit.is_new(), borrowed_other.is_new()) { + (false, false) => None, + _ => Some(bit.is(other)), + } + }) + .all(|bool| bool)) + } +} + +#[derive(Debug, Clone)] +#[pyclass(name="QuantumRegister", extends=PyRegister)] +pub struct PyQuantumRegister(); + +#[pymethods] +impl PyQuantumRegister { + #[new] + pub fn new( + py: Python, + mut size: Option, + mut name: Option, + mut bits: Option>>, + ) -> PyResult<(Self, PyRegister)> { + if (size.is_none(), bits.is_none()) == (false, false) || (size.is_some() && bits.is_some()) + { + return Err( + CircuitError::new_err( + format!("Exactly one of the size or bits arguments can be provided. Provided size={:?} bits={:?}.", size, bits) + ) + ); + } + if name.is_none() { + // This line is the reason we cannot turn this into a macro-rule + let count = if let Ok(ref mut count) = QREG_COUNTER.try_lock() { + let curr = **count; + **count += 1; + curr + } else { + panic!("Could not access register counter.") + }; + name = Some(format!("{}{}", "qr", count)); + } + if bits.is_none() && size.is_some() { + bits = Some( + (0..size.unwrap()) + .map(|idx| { + Py::new( + py, + PyQubit::py_new( + Some(RegisterAsKey::Quantum(( + name.clone().unwrap(), + size.unwrap(), + ))), + Some(idx), + ) + .unwrap(), + ) + .unwrap() + }) + .collect(), + ); + size = None; + } + Ok(( + Self(), + PyRegister::new( + py, + size, + name, + bits.map(|vec| { + vec.into_iter() + .map(|ob| { + as Clone>::clone(&ob.into_bound(py)) + .downcast_into() + .unwrap() + .into() + }) + .collect() + }), + )?, + )) + } +} + +#[derive(Debug, Clone)] +#[pyclass(name="ClassicalRegister", extends=PyRegister)] +pub struct PyClassicalRegister(); + +#[pymethods] +impl PyClassicalRegister { + #[new] + pub fn new( + py: Python, + mut size: Option, + mut name: Option, + mut bits: Option>>, + ) -> PyResult<(Self, PyRegister)> { + if (size.is_none(), bits.is_none()) == (false, false) || (size.is_some() && bits.is_some()) + { + return Err( + CircuitError::new_err( + format!("Exactly one of the size or bits arguments can be provided. Provided size={:?} bits={:?}.", size, bits) + ) + ); + } + if bits.is_none() && size.is_some() { + bits = Some( + (0..size.unwrap()) + .map(|idx| { + Py::new( + py, + PyClbit::py_new( + Some(RegisterAsKey::Quantum(( + name.clone().unwrap(), + size.unwrap(), + ))), + Some(idx), + ) + .unwrap(), + ) + .unwrap() + }) + .collect(), + ); + size = None; + } + if name.is_none() { + let count = if let Ok(ref mut count) = CREG_COUNTER.try_lock() { + let curr = **count; + **count += 1; + curr + } else { + panic!("Could not access register counter.") + }; + name = Some(format!("{}{}", "cr", count)); } + Ok(( + Self(), + PyRegister::new( + py, + size, + name, + bits.map(|vec| { + vec.into_iter() + .map(|ob| { + as Clone>::clone(&ob.into_bound(py)) + .downcast_into() + .unwrap() + .into() + }) + .collect() + }), + )?, + )) } } From 66bee69fbbf73044e3a0bc37c89001f9fd8c96bc Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Sat, 4 Jan 2025 12:49:16 -0500 Subject: [PATCH 07/25] Fix serialization in registers - Fix bit display property. --- crates/circuit/src/bit.rs | 4 ++-- crates/circuit/src/register.rs | 32 +++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs index 307214d31d67..469bf22a340f 100644 --- a/crates/circuit/src/bit.rs +++ b/crates/circuit/src/bit.rs @@ -101,11 +101,11 @@ impl PyBit { } let reg = borrowed.register.as_ref().unwrap(); Ok(format!( - "{}({}({:?}, {}), {})", + "{}({}({}, '{}'), {})", slf.get_type().name()?, reg.type_identifier(), - reg.name(), reg.index(), + reg.name(), borrowed.index.unwrap() )) } diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs index 48f3e399bebc..0ec7903a39e6 100644 --- a/crates/circuit/src/register.rs +++ b/crates/circuit/src/register.rs @@ -9,7 +9,7 @@ use pyo3::{ }; use std::{ hash::{Hash, Hasher}, - sync::Mutex, + sync::{Mutex, OnceLock}, }; use crate::{ @@ -174,11 +174,13 @@ create_register!(ClassicalRegister, Clbit, CREG_COUNTER, "cr"); /// Represents a collection of registers of a certain type within a circuit. #[derive(Debug, Clone)] -pub(crate) struct CircuitRegistry { +pub(crate) struct CircuitRegistry { registry: Interner, + /// Python cache for registers + python_cache: HashMap, OnceLock>>, } -impl CircuitRegistry { +impl CircuitRegistry { pub fn add_register(&mut self, register: T) -> Interned { self.registry.insert_owned(register) } @@ -301,10 +303,10 @@ impl PyRegister { fn __repr__(slf: Bound) -> PyResult { let borrowed = slf.borrow(); Ok(format!( - "{}({:?}, {})", + "{}({}, '{}')", slf.get_type().name()?, + borrowed.size, borrowed.name, - borrowed.size )) } @@ -363,6 +365,18 @@ impl PyRegister { .iter() } + fn __getnewargs__(&self, py: Python) -> (Option, String, PyObject) { + ( + None, + self.name.clone(), + self.bits + .iter() + .map(|bit| bit.clone_ref(py)) + .collect::>() + .into_py(py), + ) + } + fn __getstate__(&self, py: Python) -> (String, u32, PyObject) { ( self.name.clone(), @@ -417,7 +431,7 @@ impl PyRegister { } #[derive(Debug, Clone)] -#[pyclass(name="QuantumRegister", extends=PyRegister)] +#[pyclass(name="QuantumRegister", module="qiskit.circuit.quantumregister", extends=PyRegister)] pub struct PyQuantumRegister(); #[pymethods] @@ -446,7 +460,7 @@ impl PyQuantumRegister { } else { panic!("Could not access register counter.") }; - name = Some(format!("{}{}", "qr", count)); + name = Some(format!("{}{}", "q", count)); } if bits.is_none() && size.is_some() { bits = Some( @@ -491,7 +505,7 @@ impl PyQuantumRegister { } #[derive(Debug, Clone)] -#[pyclass(name="ClassicalRegister", extends=PyRegister)] +#[pyclass(name="ClassicalRegister", module="qiskit.circuit.classicalregister", extends=PyRegister)] pub struct PyClassicalRegister(); #[pymethods] @@ -540,7 +554,7 @@ impl PyClassicalRegister { } else { panic!("Could not access register counter.") }; - name = Some(format!("{}{}", "cr", count)); + name = Some(format!("{}{}", "c", count)); } Ok(( Self(), From 3a0c5bb6b41e56fa914580612f62652d7b2bebfb Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Thu, 16 Jan 2025 13:35:07 -0500 Subject: [PATCH 08/25] `BitData` refactor: Store Registers --- crates/circuit/src/bit.rs | 161 +---------- crates/circuit/src/bit_data.rs | 399 +++++++++++++++++++++++++- crates/circuit/src/imports.rs | 2 + crates/circuit/src/lib.rs | 28 +- crates/circuit/src/register.rs | 505 ++++++--------------------------- 5 files changed, 514 insertions(+), 581 deletions(-) diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs index 469bf22a340f..8580c7651934 100644 --- a/crates/circuit/src/bit.rs +++ b/crates/circuit/src/bit.rs @@ -1,168 +1,21 @@ -use std::hash::{DefaultHasher, Hash, Hasher}; - -use pyo3::{exceptions::PyTypeError, prelude::*, types::PyDict}; - -use crate::{ - circuit_data::CircuitError, - interner::Interned, - register::{Register, RegisterAsKey}, -}; - -/// Object representing a Python bit, that allows us to keep backwards compatibility -/// with the previous structure. -#[pyclass(name = "Bit", module = "qiskit._accelerate.bit", subclass)] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PyBit { - register: Option, // Register identifier - index: Option, // Index within Register -} - -#[pymethods] -impl PyBit { - #[new] - #[pyo3(signature=(register=None, index=None))] - pub fn new(register: Option, index: Option) -> PyResult { - match (®ister, index) { - (None, None) => Ok(Self { register, index }), - (Some(_), Some(_)) => Ok(Self { register, index }), - _ => Err(CircuitError::new_err( - "You should provide both an index and a register, not just one of them.", - )), - } - } - - fn __eq__<'py>(slf: Bound<'py, Self>, other: Bound<'py, Self>) -> bool { - let borrowed = slf.borrow(); - let other_borrowed = other.borrow(); - if borrowed.register.is_some() && borrowed.index.is_some() { - return borrowed.register == other_borrowed.register - && borrowed.index == other_borrowed.index; - } - - slf.is(&other) - } - - fn __hash__(&self, py: Python<'_>) -> PyResult { - if let (Some(reg), Some(idx)) = (self.register.as_ref(), self.index) { - return (reg.reduce(), idx).to_object(py).bind(py).hash(); - } - - // If registers are unavailable, hash by pointer value. - let mut hasher = DefaultHasher::new(); - let pointer_val = self as *const Self; - pointer_val.hash(&mut hasher); - Ok(hasher.finish() as isize) - } - - fn __copy__(slf: Bound) -> Bound { - slf - } - - #[pyo3(signature = (_memo=None))] - fn __deepcopy__<'py>( - slf: Bound<'py, Self>, - _memo: Option>, - ) -> PyResult> { - let borrowed: PyRef = slf.borrow(); - if borrowed.index.is_none() && borrowed.register.is_none() { - return Ok(slf); - } - let copy = slf - .get_type() - .call_method1("__new__", (slf.get_type(),))? - .downcast_into::()?; - let mut copy_mut = copy.borrow_mut(); - copy_mut.register = borrowed.register.clone(); - copy_mut.index = borrowed.index; - Ok(copy) - } - - fn __getstate__(slf: PyRef<'_, Self>) -> (Option<(String, u32)>, Option) { - ( - slf.register.as_ref().map(|reg| { - let (name, num_qubits) = reg.reduce(); - (name.to_string(), num_qubits) - }), - slf.index.as_ref().copied(), - ) - } - - fn __setstate__(mut slf: PyRefMut<'_, Self>, state: (Option<(String, u32)>, Option)) { - slf.register = state - .0 - .map(|(name, num_qubits)| RegisterAsKey::Register((name, num_qubits))); - slf.index = state.1; - } - - fn __repr__(slf: Bound) -> PyResult { - let borrowed = slf.borrow(); - if borrowed.register.is_none() && borrowed.index.is_none() { - return Ok(slf.py_super()?.repr()?.to_string()); - } - let reg = borrowed.register.as_ref().unwrap(); - Ok(format!( - "{}({}({}, '{}'), {})", - slf.get_type().name()?, - reg.type_identifier(), - reg.index(), - reg.name(), - borrowed.index.unwrap() - )) - } - - pub fn is_new(&self) -> bool { - self.index.is_none() && self.register.is_none() - } -} - -macro_rules! create_py_bit { - ($name:ident, $pyname:literal, $reg_type:pat, $module:literal) => { - #[pyclass(name=$pyname, extends=PyBit, subclass, module=$module)] - #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] - pub struct $name(); - - #[pymethods] - impl $name { - #[new] - #[pyo3(signature = (register=None, index=None))] - pub fn py_new( - register: Option, - index: Option, - ) -> PyResult<(Self, PyBit)> { - if register.is_none() || matches!(register, Some($reg_type)) { - Ok((Self(), PyBit::new(register, index)?)) - } else { - Err(PyTypeError::new_err(format!( - "The incorrect register was assigned. Bit type {}, Register type {}", - $pyname, - register.unwrap().type_identifier() - ))) - } - } - } - }; -} - -// Create python instances -create_py_bit! {PyQubit, "Qubit", RegisterAsKey::Quantum(_), "qiskit._accelerate.bit"} -create_py_bit! {PyClbit, "Clbit", RegisterAsKey::Classical(_), "qiskit._accelerate.bit"} +use std::fmt::Debug; /// Keeps information about where a qubit is located within the circuit. -#[derive(Debug, Clone)] -pub struct BitInfo { - register_idx: Interned, +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Hash)] +pub struct BitInfo { + register_idx: u32, index: u32, } -impl BitInfo { - pub fn new(register_idx: Interned, index: u32) -> Self { +impl BitInfo { + pub fn new(register_idx: u32, index: u32) -> Self { Self { register_idx, index, } } - pub fn register_index(&self) -> Interned { + pub fn register_index(&self) -> u32 { self.register_idx } diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 56a2560385f2..c56b1003e5ee 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; +use crate::imports::{CLASSICAL_REGISTER, QUANTUM_REGISTER, REGISTER}; +use crate::register::{Register, RegisterAsKey}; +use crate::{BitType, ToPyBit}; use hashbrown::HashMap; +use indexmap::{Equivalent, IndexSet}; use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; use pyo3::prelude::*; -use pyo3::types::PyList; +use pyo3::types::{PyDict, PyList}; +use std::borrow::Borrow; use std::fmt::Debug; use std::hash::{Hash, Hasher}; +use std::sync::OnceLock; /// 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,390 @@ where self.bits.clear(); } } + +#[derive(Clone, Debug)] +pub struct NewBitData, R: Register + Hash + Eq> { + /// The public field name (i.e. `qubits` or `clbits`). + description: String, + /// Registered Python bits. + bits: Vec>, + /// Maps Python bits to native type. + indices: HashMap, + /// Mapping between bit index and its register info + bit_info: Vec>, + /// Registers in the circuit + registry: IndexSet, + /// Registers in Python + registers: Vec>, +} + +impl NewBitData +where + T: From + Copy + Debug + ToPyBit, + R: Register + + Equivalent + + for<'a> Borrow<&'a RegisterAsKey> + + Hash + + Eq + + From<(usize, Option)> + + for<'a> From<&'a [T]> + + for<'a> From<(&'a [T], Option)>, + BitType: From, +{ + pub fn new(description: String) -> Self { + NewBitData { + description, + bits: Vec::new(), + indices: HashMap::new(), + bit_info: Vec::new(), + registry: IndexSet::new(), + registers: Vec::new(), + } + } + + pub fn with_capacity(description: String, capacity: usize) -> Self { + NewBitData { + description, + bits: Vec::with_capacity(capacity), + indices: HashMap::with_capacity(capacity), + bit_info: Vec::with_capacity(capacity), + registry: IndexSet::with_capacity(capacity), + registers: Vec::with_capacity(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() + } + + /// Gets a reference to the underlying vector of Python bits. + #[inline] + pub fn bits(&self) -> &Vec> { + &self.bits + } + + /// Adds a register onto the [BitData] of the circuit. + pub fn add_register( + &mut self, + name: Option, + size: Option, + bits: Option<&[T]>, + ) -> u32 { + match (size, bits) { + (None, None) => panic!("You should at least provide either a size or the bit indices."), + (None, Some(bits)) => { + let reg: R = (bits, name).into(); + let idx = self.registry.len().try_into().unwrap_or_else(|_| { + panic!( + "The {} registry in this circuit has reached its maximum capacity.", + self.description + ) + }); + // Add register info cancel if any qubit is duplicated + for (bit_idx, bit) in bits.iter().enumerate() { + let bit_info = &mut self.bit_info[BitType::from(*bit) as usize]; + if bit_info.is_some() { + panic!( + "The bit {:?} is currently assigned to another register.", + bit + ) + } else { + *bit_info = Some(BitInfo::new( + idx, + bit_idx.try_into().unwrap_or_else(|_| { + panic!( + "The current register exceeds its capacity limit. Bits {}", + reg.len() + ) + }), + )) + } + } + self.registry.insert(reg); + self.registers.push(OnceLock::new()); + idx + } + (Some(size), None) => { + let bits: Vec = (0..size).map(|_| self.add_bit()).collect(); + let reg = (bits.as_slice(), name).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.registry.insert(reg); + self.registers.push(OnceLock::new()); + 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 { + 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(None); + self.bits.push(OnceLock::new()); + idx.into() + } + + /// Retrieves a register by its index within the circuit + pub fn get_register(&self, index: u32) -> Option<&R> { + self.registry.get_index(index as usize) + } + + pub fn get_register_by_key(&self, key: &RegisterAsKey) -> Option<&R> { + self.registry.get(&key) + } + + // ======================= + // PyMethods + // ======================= + + /// Finds the native bit index of the given Python bit. + #[inline] + pub fn py_find_bit(&self, bit: &Bound) -> Option { + self.indices.get(&BitAsKey::new(bit)).copied() + } + + /// 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>( + &self, + bits: impl IntoIterator>, + ) -> PyResult> { + let v: Result, _> = bits + .into_iter() + .map(|b| { + self.indices + .get(&BitAsKey::new(&b)) + .copied() + .ok_or_else(|| { + PyKeyError::new_err(format!( + "Bit {:?} has not been added to this circuit.", + b + )) + }) + }) + .collect(); + v.map(|x| x.into_iter()) + } + + /// Gets the Python bit corresponding to the given native + /// bit index. + #[inline] + pub fn py_get_bit(&mut self, py: Python, index: T) -> PyResult> { + /* + For this method we want to make sure a couple of things are done first: + + - First off, the method needs mutable access to all of `BitData` for a couple + of reasons, but mainly because it needs to be able to initialize the `OnceCell` + for both the `Bit` instance as well as the register. + + There is a problem with this as it could cause two mutable references to `BitData`. + How do we solve this? I guess we solved it LOL + */ + 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] { + // If it is not initalized and has a register, initialize the 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.bits[index_as_usize] + .set(res.into()) + .map_err(|_| PyRuntimeError::new_err("Could not set the OnceCell correctly"))?; + return Ok(self.bits[index_as_usize].get()); + } + // If it is initialized, just retrieve. + else { + return Ok(self.bits[index_as_usize].get()); + } + } else if let Some(bit) = self.bits[index_as_usize].get() { + Ok(Some(bit)) + } else { + self.bits[index_as_usize] + .set(T::to_py_bit(py)?) + .map_err(|_| PyRuntimeError::new_err("Could not set the OnceCell correctly"))?; + Ok(self.bits[index_as_usize].get()) + } + } + + /// Retrieves a register instance from Python based on the rust description. + pub fn py_get_register(&mut self, py: Python, index: u32) -> PyResult> { + 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 any indices have been initialized, if such is the case + // Treat the rest of indices as new `Bits`` + if register + .bits() + .any(|bit| self.bits[BitType::from(bit) as usize].get().is_some()) + { + let bits: Vec = register + .bits() + .map(|bit| -> PyResult { + 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::>()?; + + // Extract kwargs + let kwargs = PyDict::new_bound(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 { + 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 { + 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, strict: bool) -> PyResult { + let py: Python<'_> = bit.py(); + + 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_insert(BitAsKey::new(bit), idx.into()) + .is_ok() + { + self.bit_info.push(None); + self.bits.push(bit.into_py(py).into()); + // self.cached.bind(py).append(bit)?; + } else if strict { + return Err(PyValueError::new_err(format!( + "Existing bit {:?} cannot be re-added in strict mode.", + bit + ))); + } + Ok(idx.into()) + } + + pub fn py_add_register(&mut self, register: &Bound) -> PyResult { + // let index: 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 = register + .iter()? + .map(|bit| -> PyResult { + let bit = bit?; + if let Some(idx) = self.indices.get(&BitAsKey::new(&bit)) { + Ok(*idx) + } else { + self.py_add_bit(&bit, true) + } + }) + .collect::>()?; + + let name: String = register.getattr("name")?.extract()?; + self.registers.push(register.clone().unbind().into()); + Ok(self.add_register(Some(name), None, Some(&bits))) + } + + pub fn py_remove_bit_indices(&mut self, py: Python, indices: I) -> PyResult<()> + where + I: IntoIterator, + { + let mut indices_sorted: Vec = indices + .into_iter() + .map(|i| >::from(i) as usize) + .collect(); + indices_sorted.sort(); + + for index in indices_sorted.into_iter().rev() { + // self.cached.bind(py).del_item(index)?; + let bit = self + .py_get_bit(py, (index as BitType).into())? + .unwrap() + .clone_ref(py); + self.indices.remove(&BitAsKey::new(bit.bind(py))); + 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() + .clone_ref(py); + self.indices + .insert(BitAsKey::new(bit.bind(py)), (i as BitType).into()); + } + Ok(()) + } + + /// Called during Python garbage collection, only!. + /// Note: INVALIDATES THIS INSTANCE. + pub fn dispose(&mut self) { + self.indices.clear(); + self.bits.clear(); + self.registers.clear(); + self.bit_info.clear(); + self.registry.clear(); + } +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 67ae9f85897f..b3a4aa90b55c 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 a48cf2fc2f40..af83cd29a3c4 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -31,6 +31,7 @@ pub mod util; mod rustworkx_core_vnext; +use imports::{CLBIT, QUBIT}; use pyo3::prelude::*; use pyo3::types::{PySequence, PyTuple}; @@ -124,13 +125,28 @@ impl From 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; +} + +impl ToPyBit for Qubit { + fn to_py_bit(py: Python) -> PyResult { + QUBIT.get_bound(py).call0().map(|bit| bit.into()) + } +} + +impl ToPyBit for Clbit { + fn to_py_bit(py: Python) -> PyResult { + CLBIT.get_bound(py).call0().map(|bit| bit.into()) + } +} + pub fn circuit(m: &Bound) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs index 0ec7903a39e6..78ad590d70ba 100644 --- a/crates/circuit/src/register.rs +++ b/crates/circuit/src/register.rs @@ -1,27 +1,17 @@ -use hashbrown::HashMap; +use hashbrown::Equivalent; use indexmap::IndexSet; -use pyo3::{ - exceptions::{PyTypeError, PyValueError}, - intern, - prelude::*, - types::{PyAnyMethods, PyList, PySet}, - FromPyObject, PyTypeInfo, -}; +use pyo3::{exceptions::PyTypeError, intern, types::PyAnyMethods, FromPyObject}; use std::{ hash::{Hash, Hasher}, - sync::{Mutex, OnceLock}, + ops::Index, + sync::Mutex, }; use crate::{ - bit::{PyBit, PyClbit, PyQubit}, - circuit_data::CircuitError, - interner::{Interned, Interner}, - slice::PySequenceIndex, + imports::{CLASSICAL_REGISTER, QUANTUM_REGISTER, REGISTER}, Clbit, Qubit, }; -static REGISTER_INSTANCE_COUNTER: Mutex = Mutex::new(0); - /// 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)] @@ -33,11 +23,11 @@ pub enum RegisterAsKey { impl RegisterAsKey { #[inline] - pub fn reduce(&self) -> (&str, u32) { + pub fn reduce(&self) -> (u32, &str) { match self { - RegisterAsKey::Register((name, num_qubits)) => (name.as_str(), *num_qubits), - RegisterAsKey::Quantum((name, num_qubits)) => (name.as_str(), *num_qubits), - RegisterAsKey::Classical((name, num_qubits)) => (name.as_str(), *num_qubits), + RegisterAsKey::Register((name, num_qubits)) => (*num_qubits, name.as_str()), + RegisterAsKey::Quantum((name, num_qubits)) => (*num_qubits, name.as_str()), + RegisterAsKey::Classical((name, num_qubits)) => (*num_qubits, name.as_str()), } } @@ -71,14 +61,14 @@ impl RegisterAsKey { impl<'py> FromPyObject<'py> for RegisterAsKey { fn extract_bound(ob: &pyo3::Bound<'py, pyo3::PyAny>) -> pyo3::PyResult { - if ob.is_instance(&PyRegister::type_object_bound(ob.py()))? { + 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.downcast::().is_ok() { + if ob.is_instance(CLASSICAL_REGISTER.get_bound(ob.py()))? { return Ok(RegisterAsKey::Classical((name, num_qubits))); - } else if ob.downcast::().is_ok() { + } 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))); @@ -96,16 +86,22 @@ pub trait Register { /// 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; /// Return an iterator over all the bits in the register fn bits(&self) -> impl ExactSizeIterator; + /// Returns the register as a Key + fn as_key(&self) -> RegisterAsKey; } macro_rules! create_register { - ($name:ident, $bit:ty, $counter:ident, $prefix:literal) => { + ($name:ident, $bit:ty, $counter:ident, $prefix:literal, $key:expr) => { static $counter: Mutex = Mutex::new(0); #[derive(Debug, Clone, Eq)] @@ -115,7 +111,14 @@ macro_rules! create_register { } impl $name { - pub fn new(size: usize, name: Option) -> Self { + pub fn new(size: Option, name: Option, bits: Option<&[$bit]>) -> Self { + let register = if let Some(size) = size { + (0..size).map(|bit| <$bit>::new(bit)).collect() + } else if let Some(bits) = bits { + bits.iter().copied().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 { @@ -128,10 +131,7 @@ macro_rules! create_register { }; format!("{}{}", $prefix, count) }; - Self { - register: (0..size).map(|bit| <$bit>::new(bit)).collect(), - name, - } + Self { register, name } } } @@ -142,6 +142,14 @@ macro_rules! create_register { self.register.len() } + fn is_empty(&self) -> bool { + self.register.is_empty() + } + + fn name(&self) -> &str { + self.name.as_str() + } + fn contains(&self, bit: Self::Bit) -> bool { self.register.contains(&bit) } @@ -153,6 +161,10 @@ macro_rules! create_register { fn bits(&self) -> impl ExactSizeIterator { self.register.iter().copied() } + + fn as_key(&self) -> RegisterAsKey { + $key((self.name.clone(), self.len().try_into().unwrap())) + } } impl Hash for $name { @@ -166,413 +178,70 @@ macro_rules! create_register { self.register.len() == other.register.len() && self.name == other.name } } - }; -} - -create_register!(QuantumRegister, Qubit, QREG_COUNTER, "qr"); -create_register!(ClassicalRegister, Clbit, CREG_COUNTER, "cr"); - -/// Represents a collection of registers of a certain type within a circuit. -#[derive(Debug, Clone)] -pub(crate) struct CircuitRegistry { - registry: Interner, - /// Python cache for registers - python_cache: HashMap, OnceLock>>, -} - -impl CircuitRegistry { - pub fn add_register(&mut self, register: T) -> Interned { - self.registry.insert_owned(register) - } - - /// Retreives the index of a register if it exists within a registry. - pub fn find_index(&self, register: &T) -> Option> { - self.registry.get_interned(register) - } - /// Checks if a register exists within a circuit - pub fn contains(&self, register: &T) -> bool { - self.registry.contains(register) - } -} - -#[derive(FromPyObject)] -enum SliceOrInt<'py> { - Slice(PySequenceIndex<'py>), - List(Vec), -} + impl Index for $name { + type Output = $bit; -/// Python representation of a generic register -#[derive(Debug, Clone)] -#[pyclass(name = "Register", module = "qiskit.circuit.register", subclass)] -pub struct PyRegister { - /// Bits are stored in Python-space. - bits: Vec>, - /// Name of the register in question - #[pyo3(get)] - name: String, - /// Size of the register - #[pyo3(get)] - size: u32, - /// Mapping of the hash value of each bit and their index in the register. - bit_indices: HashMap, -} - -#[pymethods] -impl PyRegister { - #[new] - pub fn new( - py: Python, - mut size: Option, - mut name: Option, - bits: Option>>, - ) -> PyResult { - if (size.is_none(), bits.is_none()) == (false, false) || (size.is_some() && bits.is_some()) - { - return Err( - CircuitError::new_err( - format!("Exactly one of the size or bits arguments can be provided. Provided size={:?} bits={:?}.", size, bits) - ) - ); - } - if let Some(bits) = bits.as_ref() { - size = Some(bits.len() as u32); - } - if name.is_none() { - let count = if let Ok(ref mut count) = REGISTER_INSTANCE_COUNTER.try_lock() { - let curr = **count; - **count += 1; - curr - } else { - panic!("Could not access register counter.") - }; - name = Some(format!("{}{}", "reg", count)); - } - if let Some(bits) = bits { - if size != Some(PySet::new_bound(py, bits.iter())?.len() as u32) { - return Err(CircuitError::new_err(format!( - "Register bits must not be duplicated. bits={:?}", - bits - ))); + fn index(&self, index: usize) -> &Self::Output { + self.register.index(index) } - let bit_indices: HashMap = bits - .iter() - .enumerate() - .flat_map(|(idx, obj)| -> PyResult<(isize, u32)> { - Ok((obj.bind(py).hash()?, idx as u32)) - }) - .collect(); - Ok(Self { - bits, - name: name.unwrap(), - size: size.unwrap(), - bit_indices, - }) - } else { - let name = name.unwrap(); - let size = size.unwrap(); - let bits: Vec> = (0..size) - .map(|idx| { - Py::new( - py, - PyBit::new( - Some(RegisterAsKey::Register((name.clone(), size))), - Some(idx), - ) - .unwrap(), - ) - .unwrap() - }) - .collect(); - let bit_indices: HashMap = bits - .iter() - .enumerate() - .flat_map(|(idx, obj)| -> PyResult<(isize, u32)> { - Ok((obj.bind(py).hash()?, idx as u32)) - }) - .collect(); - Ok(Self { - bits, - name, - size, - bit_indices, - }) } - } - - fn __repr__(slf: Bound) -> PyResult { - let borrowed = slf.borrow(); - Ok(format!( - "{}({}, '{}')", - slf.get_type().name()?, - borrowed.size, - borrowed.name, - )) - } - - fn __len__(&self) -> usize { - self.size as usize - } - fn __getitem__(&self, py: Python, key: SliceOrInt) -> PyResult { - match key { - SliceOrInt::Slice(py_sequence_index) => { - let sequence = py_sequence_index.with_len(self.size.try_into().unwrap())?; - match sequence { - crate::slice::SequenceIndex::Int(idx) => { - Ok(self.bits[idx].clone_ref(py).into_any()) - } - _ => Ok(PyList::new_bound( - py, - sequence.iter().map(|idx| self.bits[idx].clone_ref(py)), - ) - .into()), - } - } - SliceOrInt::List(vec) => { - if vec.iter().max() < Some(&(self.size as usize)) { - Ok( - PyList::new_bound(py, vec.iter().map(|idx| self.bits[*idx].clone_ref(py))) - .into(), - ) - } else { - Err(CircuitError::new_err("Register index is our of range")) - } + impl From<(usize, Option)> for $name { + fn from(value: (usize, Option)) -> Self { + Self::new(Some(value.0), value.1, None) } } - } - - fn __contains__(&self, bit: &Bound) -> PyResult { - Ok(self.bit_indices.contains_key(&bit.hash()?)) - } - fn index(slf: Bound, bit: &Bound) -> PyResult { - let borrowed = slf.borrow(); - if borrowed.__contains__(bit)? { - Ok(borrowed.bit_indices[&bit.hash()?]) - } else { - Err(PyValueError::new_err(format!( - "Bit {} not found in Register {}.", - bit.repr()?, - slf.repr()?, - ))) + impl From<&[$bit]> for $name { + fn from(value: &[$bit]) -> Self { + Self::new(None, None, Some(value)) + } } - } - - fn __iter__<'py>(&'py self, py: Python<'py>) -> PyResult> { - PyList::new_bound(py, self.bits.iter().map(|obj| obj.clone_ref(py))) - .into_any() - .iter() - } - fn __getnewargs__(&self, py: Python) -> (Option, String, PyObject) { - ( - None, - self.name.clone(), - self.bits - .iter() - .map(|bit| bit.clone_ref(py)) - .collect::>() - .into_py(py), - ) - } - - fn __getstate__(&self, py: Python) -> (String, u32, PyObject) { - ( - self.name.clone(), - self.size, - self.bits - .iter() - .map(|bit| bit.clone_ref(py)) - .collect::>() - .into_py(py), - ) - } - - fn __setstate__(&mut self, py: Python, state: (String, u32, PyObject)) -> PyResult<()> { - self.name = state.0; - self.size = state.1; - self.bits = state.2.extract(py)?; - self.bit_indices = self - .bits - .iter() - .enumerate() - .flat_map(|(idx, obj)| -> PyResult<(isize, u32)> { - Ok((obj.bind(py).hash()?, idx as u32)) - }) - .collect(); - Ok(()) - } - - fn __eq__(slf: Bound, other: Bound) -> PyResult { - if slf.is(&other) { - return Ok(true); + impl From<(&[$bit], String)> for $name { + fn from(value: (&[$bit], String)) -> Self { + Self::new(None, Some(value.1), Some(value.0)) + } } - - let self_borrowed = slf.borrow(); - let other_borrowed = other.borrow(); - - Ok(slf.get_type().eq(other.get_type())? - && slf.repr()?.to_string() == other.repr()?.to_string() - && self_borrowed - .bits - .iter() - .zip(other_borrowed.bits.iter()) - .filter_map(|(bit, other)| -> Option { - let borrowed_bit = bit.borrow(slf.py()); - let borrowed_other = other.borrow(slf.py()); - match (borrowed_bit.is_new(), borrowed_other.is_new()) { - (false, false) => None, - _ => Some(bit.is(other)), - } - }) - .all(|bool| bool)) - } + }; } -#[derive(Debug, Clone)] -#[pyclass(name="QuantumRegister", module="qiskit.circuit.quantumregister", extends=PyRegister)] -pub struct PyQuantumRegister(); - -#[pymethods] -impl PyQuantumRegister { - #[new] - pub fn new( - py: Python, - mut size: Option, - mut name: Option, - mut bits: Option>>, - ) -> PyResult<(Self, PyRegister)> { - if (size.is_none(), bits.is_none()) == (false, false) || (size.is_some() && bits.is_some()) - { - return Err( - CircuitError::new_err( - format!("Exactly one of the size or bits arguments can be provided. Provided size={:?} bits={:?}.", size, bits) - ) - ); - } - if name.is_none() { - // This line is the reason we cannot turn this into a macro-rule - let count = if let Ok(ref mut count) = QREG_COUNTER.try_lock() { - let curr = **count; - **count += 1; - curr - } else { - panic!("Could not access register counter.") - }; - name = Some(format!("{}{}", "q", count)); - } - if bits.is_none() && size.is_some() { - bits = Some( - (0..size.unwrap()) - .map(|idx| { - Py::new( - py, - PyQubit::py_new( - Some(RegisterAsKey::Quantum(( - name.clone().unwrap(), - size.unwrap(), - ))), - Some(idx), - ) - .unwrap(), - ) - .unwrap() - }) - .collect(), - ); - size = None; +create_register!( + QuantumRegister, + Qubit, + QREG_COUNTER, + "qr", + RegisterAsKey::Quantum +); + +create_register!( + ClassicalRegister, + Clbit, + CREG_COUNTER, + "cr", + RegisterAsKey::Classical +); + +// Add equivalencies between Keys and Registers +impl Equivalent for RegisterAsKey { + fn equivalent(&self, key: &QuantumRegister) -> bool { + match self { + Self::Quantum((name, length)) => { + name == &key.name && *length == key.len().try_into().unwrap() + } + _ => false, } - Ok(( - Self(), - PyRegister::new( - py, - size, - name, - bits.map(|vec| { - vec.into_iter() - .map(|ob| { - as Clone>::clone(&ob.into_bound(py)) - .downcast_into() - .unwrap() - .into() - }) - .collect() - }), - )?, - )) } } -#[derive(Debug, Clone)] -#[pyclass(name="ClassicalRegister", module="qiskit.circuit.classicalregister", extends=PyRegister)] -pub struct PyClassicalRegister(); - -#[pymethods] -impl PyClassicalRegister { - #[new] - pub fn new( - py: Python, - mut size: Option, - mut name: Option, - mut bits: Option>>, - ) -> PyResult<(Self, PyRegister)> { - if (size.is_none(), bits.is_none()) == (false, false) || (size.is_some() && bits.is_some()) - { - return Err( - CircuitError::new_err( - format!("Exactly one of the size or bits arguments can be provided. Provided size={:?} bits={:?}.", size, bits) - ) - ); - } - if bits.is_none() && size.is_some() { - bits = Some( - (0..size.unwrap()) - .map(|idx| { - Py::new( - py, - PyClbit::py_new( - Some(RegisterAsKey::Quantum(( - name.clone().unwrap(), - size.unwrap(), - ))), - Some(idx), - ) - .unwrap(), - ) - .unwrap() - }) - .collect(), - ); - size = None; - } - if name.is_none() { - let count = if let Ok(ref mut count) = CREG_COUNTER.try_lock() { - let curr = **count; - **count += 1; - curr - } else { - panic!("Could not access register counter.") - }; - name = Some(format!("{}{}", "c", count)); +impl Equivalent for RegisterAsKey { + fn equivalent(&self, key: &ClassicalRegister) -> bool { + match self { + Self::Classical((name, length)) => { + name == &key.name && *length == key.len().try_into().unwrap() + } + _ => false, } - Ok(( - Self(), - PyRegister::new( - py, - size, - name, - bits.map(|vec| { - vec.into_iter() - .map(|ob| { - as Clone>::clone(&ob.into_bound(py)) - .downcast_into() - .unwrap() - .into() - }) - .collect() - }), - )?, - )) } } From f8bdfa62cb8cc436d38a91b36fffa5968be3c1f0 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 17 Jan 2025 15:46:47 -0500 Subject: [PATCH 09/25] Add new `BitData` to `CircuitData` --- crates/circuit/src/bit_data.rs | 198 +++++++++++++++++++++++++---- crates/circuit/src/circuit_data.rs | 141 +++++++++++++------- crates/circuit/src/converters.rs | 18 ++- crates/circuit/src/dag_circuit.rs | 12 +- crates/circuit/src/register.rs | 64 ++++------ 5 files changed, 308 insertions(+), 125 deletions(-) diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index c56b1003e5ee..4e68883ca80e 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -15,11 +15,9 @@ use crate::imports::{CLASSICAL_REGISTER, QUANTUM_REGISTER, REGISTER}; use crate::register::{Register, RegisterAsKey}; use crate::{BitType, ToPyBit}; use hashbrown::HashMap; -use indexmap::{Equivalent, IndexSet}; use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList}; -use std::borrow::Borrow; use std::fmt::Debug; use std::hash::{Hash, Hasher}; use std::sync::OnceLock; @@ -245,25 +243,29 @@ pub struct NewBitData, R: Register + Hash + Eq> { bits: Vec>, /// Maps Python bits to native type. indices: HashMap, + /// Maps Register keys to indices + reg_keys: HashMap, /// Mapping between bit index and its register info bit_info: Vec>, /// Registers in the circuit - registry: IndexSet, + registry: Vec, /// Registers in Python registers: Vec>, + /// Cached Python bits + cached_py_bits: OnceLock>, + /// Cached Python registers + cached_py_regs: OnceLock>, } impl NewBitData where T: From + Copy + Debug + ToPyBit, R: Register - + Equivalent - + for<'a> Borrow<&'a RegisterAsKey> + Hash + Eq + From<(usize, Option)> + for<'a> From<&'a [T]> - + for<'a> From<(&'a [T], Option)>, + + for<'a> From<(&'a [T], String)>, BitType: From, { pub fn new(description: String) -> Self { @@ -272,19 +274,25 @@ where bits: Vec::new(), indices: HashMap::new(), bit_info: Vec::new(), - registry: IndexSet::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, capacity: usize) -> Self { + pub fn with_capacity(description: String, bit_capacity: usize, reg_capacity: usize) -> Self { NewBitData { description, - bits: Vec::with_capacity(capacity), - indices: HashMap::with_capacity(capacity), - bit_info: Vec::with_capacity(capacity), - registry: IndexSet::with_capacity(capacity), - registers: Vec::with_capacity(capacity), + bits: Vec::with_capacity(bit_capacity), + indices: HashMap::with_capacity(bit_capacity), + 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), } } @@ -302,12 +310,6 @@ where self.bits.is_empty() } - /// Gets a reference to the underlying vector of Python bits. - #[inline] - pub fn bits(&self) -> &Vec> { - &self.bits - } - /// Adds a register onto the [BitData] of the circuit. pub fn add_register( &mut self, @@ -318,7 +320,11 @@ where match (size, bits) { (None, None) => panic!("You should at least provide either a size or the bit indices."), (None, Some(bits)) => { - let reg: R = (bits, name).into(); + let reg: R = if let Some(name) = name { + (bits, name).into() + } else { + bits.into() + }; let idx = self.registry.len().try_into().unwrap_or_else(|_| { panic!( "The {} registry in this circuit has reached its maximum capacity.", @@ -345,20 +351,26 @@ where )) } } - self.registry.insert(reg); + self.reg_keys.insert(reg.as_key().clone(), idx); + self.registry.push(reg); self.registers.push(OnceLock::new()); idx } (Some(size), None) => { let bits: Vec = (0..size).map(|_| self.add_bit()).collect(); - let reg = (bits.as_slice(), name).into(); + let reg: R = if let Some(name) = name { + (bits.as_slice(), name).into() + } else { + bits.as_slice().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.registry.insert(reg); + self.reg_keys.insert(reg.as_key().clone(), idx); + self.registry.push(reg); self.registers.push(OnceLock::new()); idx } @@ -383,13 +395,33 @@ where 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) -> Option<&BitInfo> { + self.bit_info[BitType::from(index) as usize].as_ref() + } + /// Retrieves a register by its index within the circuit + #[inline] pub fn get_register(&self, index: u32) -> Option<&R> { - self.registry.get_index(index as usize) + self.registry.get(index as usize) } + #[inline] pub fn get_register_by_key(&self, key: &RegisterAsKey) -> Option<&R> { - self.registry.get(&key) + 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) } // ======================= @@ -402,6 +434,34 @@ where self.indices.get(&BitAsKey::new(bit)).copied() } + /// Gets a reference to the cached Python list, maintained by + /// this instance. + #[inline] + pub fn py_cached_bits(&self, py: Python) -> &Py { + self + .cached_py_bits + .get_or_init(|| PyList::empty_bound(py).into()) + } + + /// Gets a reference to the underlying vector of Python bits. + #[inline] + pub fn py_bits(&self, py: Python) -> PyResult> { + (0..self.len()) + .map(|idx| { + self.py_get_bit(py, (idx as u32).into()) + .map(|bit| bit.unwrap()) + }) + .collect::>() + } + + /// Gets a reference to the underlying vector of Python registers. + #[inline] + pub fn py_registers(&self, py: Python) -> PyResult> { + (0..self.len_regs() as u32) + .map(|idx| self.py_get_register(py, idx).map(|reg| reg.unwrap())) + .collect::>() + } + /// 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>( @@ -425,10 +485,25 @@ where 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( + &mut self, + py: Python, + bits: &[T], + ) -> PyResult>> { + let v: Vec<_> = bits + .iter() + .map(|i| -> PyResult<&PyObject> { Ok(self.py_get_bit(py, *i)?.unwrap()) }) + .collect::>()?; + Ok(v.into_iter()) + } + /// Gets the Python bit corresponding to the given native /// bit index. #[inline] - pub fn py_get_bit(&mut self, py: Python, index: T) -> PyResult> { + pub fn py_get_bit(&self, py: Python, index: T) -> PyResult> { /* For this method we want to make sure a couple of things are done first: @@ -472,7 +547,7 @@ where } /// Retrieves a register instance from Python based on the rust description. - pub fn py_get_register(&mut self, py: Python, index: u32) -> PyResult> { + pub fn py_get_register(&self, py: Python, index: u32) -> PyResult> { 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() { @@ -533,6 +608,18 @@ where pub fn py_add_bit(&mut self, bit: &Bound, strict: bool) -> PyResult { let py: Python<'_> = bit.py(); + if self.bits.len() + != self + .cached_py_bits + .get_or_init(|| PyList::empty_bound(py).into()) + .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", @@ -546,6 +633,10 @@ where { self.bit_info.push(None); self.bits.push(bit.into_py(py).into()); + self.cached_py_bits + .get_or_init(|| PyList::empty_bound(py).into()) + .bind(py) + .append(bit)?; // self.cached.bind(py).append(bit)?; } else if strict { return Err(PyValueError::new_err(format!( @@ -557,6 +648,19 @@ where } pub fn py_add_register(&mut self, register: &Bound) -> PyResult { + let py = register.py(); + if self.registers.len() + != self + .cached_py_regs + .get_or_init(|| PyList::empty_bound(py).into()) + .bind(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 index: u32 = self.registers.len().try_into().map_err(|_| { // PyRuntimeError::new_err(format!( // "The number of {} registers in the circuit has exceeded the maximum capacity", @@ -578,6 +682,10 @@ where let name: String = register.getattr("name")?.extract()?; self.registers.push(register.clone().unbind().into()); + self.cached_py_regs + .get_or_init(|| PyList::empty_bound(py).into()) + .bind(py) + .append(register)?; Ok(self.add_register(Some(name), None, Some(&bits))) } @@ -613,6 +721,22 @@ where Ok(()) } + pub(crate) fn py_bits_raw(&self) -> &[OnceLock] { + &self.bits + } + + pub(crate) fn py_bits_cached_raw(&self) -> Option<&Py> { + self.cached_py_bits.get() + } + + pub(crate) fn py_regs_raw(&self) -> &[OnceLock] { + &self.bits + } + + pub(crate) fn py_regs_cached_raw(&self) -> Option<&Py> { + self.cached_py_bits.get() + } + /// Called during Python garbage collection, only!. /// Note: INVALIDATES THIS INSTANCE. pub fn dispose(&mut self) { @@ -622,4 +746,24 @@ where self.bit_info.clear(); self.registry.clear(); } + + /// 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) -> 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(), + reg_keys: HashMap::new(), + bit_info: (0..bit_data.len()).map(|_| None).collect(), + registry: Vec::new(), + registers: Vec::new(), + cached_py_bits: bit_data.cached().clone_ref(py).into(), + cached_py_regs: OnceLock::new(), + } + } } diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index fa36d8b841fb..ff35290343be 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -13,7 +13,7 @@ #[cfg(feature = "cache_pygates")] use std::sync::OnceLock; -use crate::bit_data::BitData; +use crate::bit_data::NewBitData; use crate::circuit_instruction::{ CircuitInstruction, ExtraInstructionAttributes, OperationFromPython, }; @@ -23,6 +23,7 @@ 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}; @@ -101,9 +102,9 @@ pub struct CircuitData { /// The cache used to intern instruction bits. cargs_interner: Interner<[Clbit]>, /// Qubits registered in the circuit. - qubits: BitData, + qubits: NewBitData, /// Clbits registered in the circuit. - clbits: BitData, + clbits: NewBitData, param_table: ParameterTable, #[pyo3(get)] global_phase: Param, @@ -125,20 +126,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.iter()? { - self_.add_qubit(py, &bit?, true)?; + self_.add_qubit(&bit?, true)?; } } if let Some(clbits) = clbits { for bit in clbits.iter()? { - self_.add_clbit(py, &bit?, true)?; + self_.add_clbit(&bit?, true)?; } } if let Some(data) = data { @@ -153,8 +154,8 @@ impl CircuitData { 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(), @@ -174,7 +175,7 @@ impl CircuitData { /// list(:class:`.Qubit`): The current sequence of registered qubits. #[getter("qubits")] pub fn py_qubits(&self, py: Python<'_>) -> Py { - self.qubits.cached().clone_ref(py) + self.qubits.py_cached_bits(py).clone_ref(py) } /// Return the number of qubits. This is equivalent to the length of the list returned by @@ -199,7 +200,7 @@ impl CircuitData { /// list(:class:`.Clbit`): The current sequence of registered clbits. #[getter("clbits")] pub fn py_clbits(&self, py: Python<'_>) -> Py { - self.clbits.cached().clone_ref(py) + self.clbits.py_cached_bits(py).clone_ref(py) } /// Return the number of clbits. This is equivalent to the length of the list returned by @@ -257,8 +258,18 @@ impl CircuitData { /// 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, strict: bool) -> PyResult<()> { - self.qubits.add(py, bit, strict)?; + pub fn add_qubit(&mut self, bit: &Bound, 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(signature = (register, *,))] + pub fn add_qreg(&mut self, register: &Bound) -> PyResult<()> { + self.qubits.py_add_register(register)?; Ok(()) } @@ -272,8 +283,18 @@ impl CircuitData { /// 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, strict: bool) -> PyResult<()> { - self.clbits.add(py, bit, strict)?; + pub fn add_clbit(&mut self, bit: &Bound, 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(signature = (register, *,))] + pub fn add_creg(&mut self, register: &Bound) -> PyResult<()> { + self.qubits.py_add_register(register)?; Ok(()) } @@ -285,8 +306,8 @@ impl CircuitData { pub fn copy(&self, py: Python<'_>, copy_instructions: bool, deepcopy: bool) -> PyResult { let mut res = CircuitData::new( py, - Some(self.qubits.cached().bind(py)), - Some(self.clbits.cached().bind(py)), + Some(self.qubits.py_cached_bits(py).bind(py)), + Some(self.clbits.py_cached_bits(py).bind(py)), None, self.data.len(), self.global_phase.clone(), @@ -341,15 +362,15 @@ impl CircuitData { /// /// Returns: /// tuple[set[:class:`.Qubit`], set[:class:`.Clbit`]]: The active qubits and clbits. - pub fn active_bits(&self, py: Python<'_>) -> PyResult> { + pub fn active_bits(&mut self, py: Python<'_>) -> PyResult> { let qubits = PySet::empty_bound(py)?; let clbits = PySet::empty_bound(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))?; } } @@ -501,26 +522,32 @@ impl CircuitData { } // Note: we also rely on this to make us iterable! - pub fn __getitem__(&self, py: Python, index: PySequenceIndex) -> PyResult { + pub fn __getitem__(&mut self, py: Python, index: PySequenceIndex) -> PyResult { // Get a single item, assuming the index is validated as in bounds. - let get_single = |index: usize| { + let mut get_single = |index: usize| -> PyResult { let inst = &self.data[index]; let qubits = self.qargs_interner.get(inst.qubits); let clbits = self.cargs_interner.get(inst.clbits); - CircuitInstruction { + Ok(CircuitInstruction { operation: inst.op.clone(), - qubits: PyTuple::new_bound(py, self.qubits.map_indices(qubits)).unbind(), - clbits: PyTuple::new_bound(py, self.clbits.map_indices(clbits)).unbind(), + qubits: PyTuple::new_bound(py, self.qubits.py_map_indices(py, qubits)?).unbind(), + clbits: PyTuple::new_bound(py, self.clbits.py_map_indices(py, clbits)?).unbind(), params: inst.params_view().iter().cloned().collect(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] py_op: inst.py_op.clone(), } - .into_py(py) + .into_py(py)) }; match index.with_len(self.data.len())? { - SequenceIndex::Int(index) => Ok(get_single(index)), - indices => Ok(PyList::new_bound(py, indices.iter().map(get_single)).into_py(py)), + SequenceIndex::Int(index) => Ok(get_single(index)?), + indices => { + let result: Vec = indices + .iter() + .map(|index| -> PyResult { get_single(index) }) + .collect::>()?; + Ok(PyList::new_bound(py, result).into_py(py)) + } } } @@ -657,7 +684,7 @@ impl CircuitData { pub fn extend(&mut self, py: Python<'_>, itr: &Bound) -> PyResult<()> { if let Ok(other) = itr.downcast::() { - 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() { @@ -668,7 +695,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::>>()?; @@ -679,7 +706,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::>>()?; @@ -818,16 +845,40 @@ 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(()) } @@ -979,8 +1030,8 @@ impl CircuitData { /// * global_phase: The global phase value to use for the new circuit. pub fn from_packed_instructions( py: Python, - qubits: BitData, - clbits: BitData, + qubits: NewBitData, + clbits: NewBitData, qargs_interner: Interner<[Qubit]>, cargs_interner: Interner<[Clbit]>, instructions: I, @@ -1076,8 +1127,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::new("qubits".to_string()), + clbits: NewBitData::new("clbits".to_string()), param_table: ParameterTable::new(), global_phase: Param::Float(0.0), }; @@ -1090,14 +1141,14 @@ impl CircuitData { 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(&bit, true)?; } } 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(&bit, true)?; } } Ok(res) @@ -1205,10 +1256,10 @@ impl CircuitData { fn pack(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult { 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, @@ -1285,12 +1336,12 @@ impl CircuitData { } /// Returns an immutable view of the Qubits registered in the circuit - pub fn qubits(&self) -> &BitData { + pub fn qubits(&self) -> &NewBitData { &self.qubits } /// Returns an immutable view of the Classical bits registered in the circuit - pub fn clbits(&self) -> &BitData { + pub fn clbits(&self) -> &NewBitData { &self.clbits } diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs index 030a582bdad7..430afec85fd0 100644 --- a/crates/circuit/src/converters.rs +++ b/crates/circuit/src/converters.rs @@ -20,9 +20,9 @@ use pyo3::{ types::{PyDict, PyList}, }; -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. @@ -99,10 +99,10 @@ pub fn dag_to_circuit( dag: &DAGCircuit, copy_operations: bool, ) -> PyResult { - 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 +133,15 @@ pub fn dag_to_circuit( } }), dag.get_global_phase(), - ) + )?; + // Manually add qregs and cregs + for reg in dag.qregs.bind(py).values() { + circuit.add_qreg(®)?; + } + for reg in dag.cregs.bind(py).values() { + circuit.add_creg(®)?; + } + Ok(circuit) } pub fn converters(m: &Bound) -> PyResult<()> { diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 9b2a78127826..6ec6ff827b48 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -191,9 +191,9 @@ pub struct DAGCircuit { dag: StableDiGraph, #[pyo3(get)] - qregs: Py, + pub qregs: Py, #[pyo3(get)] - cregs: Py, + pub cregs: Py, /// The cache used to intern instruction qargs. pub qargs_interner: Interner<[Qubit]>, @@ -6617,7 +6617,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(()) })?; @@ -6625,7 +6625,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))?; @@ -6646,7 +6646,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(()) })?; @@ -6654,7 +6654,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))?; diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs index 78ad590d70ba..5a491384c338 100644 --- a/crates/circuit/src/register.rs +++ b/crates/circuit/src/register.rs @@ -1,4 +1,3 @@ -use hashbrown::Equivalent; use indexmap::IndexSet; use pyo3::{exceptions::PyTypeError, intern, types::PyAnyMethods, FromPyObject}; use std::{ @@ -25,27 +24,27 @@ impl RegisterAsKey { #[inline] pub fn reduce(&self) -> (u32, &str) { match self { - RegisterAsKey::Register((name, num_qubits)) => (*num_qubits, name.as_str()), - RegisterAsKey::Quantum((name, num_qubits)) => (*num_qubits, name.as_str()), - RegisterAsKey::Classical((name, num_qubits)) => (*num_qubits, name.as_str()), + 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((name, _)) => name.as_str(), - RegisterAsKey::Quantum((name, _)) => name.as_str(), - RegisterAsKey::Classical((name, _)) => name.as_str(), + RegisterAsKey::Register(key) => key.0.as_str(), + RegisterAsKey::Quantum(key) => key.0.as_str(), + RegisterAsKey::Classical(key) => key.0.as_str(), } } #[inline] pub fn index(&self) -> u32 { match self { - RegisterAsKey::Register((_, idx)) => *idx, - RegisterAsKey::Quantum((_, idx)) => *idx, - RegisterAsKey::Classical((_, idx)) => *idx, + RegisterAsKey::Register(key) => key.1, + RegisterAsKey::Quantum(key) => key.1, + RegisterAsKey::Classical(key) => key.1, } } @@ -97,7 +96,7 @@ pub trait Register { /// Return an iterator over all the bits in the register fn bits(&self) -> impl ExactSizeIterator; /// Returns the register as a Key - fn as_key(&self) -> RegisterAsKey; + fn as_key(&self) -> &RegisterAsKey; } macro_rules! create_register { @@ -107,12 +106,12 @@ macro_rules! create_register { #[derive(Debug, Clone, Eq)] pub struct $name { register: IndexSet<<$name as Register>::Bit>, - name: String, + key: RegisterAsKey, } impl $name { pub fn new(size: Option, name: Option, bits: Option<&[$bit]>) -> Self { - let register = if let Some(size) = size { + 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 { bits.iter().copied().collect() @@ -131,7 +130,11 @@ macro_rules! create_register { }; format!("{}{}", $prefix, count) }; - Self { register, name } + let length: u32 = register.len().try_into().unwrap(); + Self { + register, + key: $key((name, length)), + } } } @@ -147,7 +150,7 @@ macro_rules! create_register { } fn name(&self) -> &str { - self.name.as_str() + self.key.name() } fn contains(&self, bit: Self::Bit) -> bool { @@ -162,20 +165,20 @@ macro_rules! create_register { self.register.iter().copied() } - fn as_key(&self) -> RegisterAsKey { - $key((self.name.clone(), self.len().try_into().unwrap())) + fn as_key(&self) -> &RegisterAsKey { + &self.key } } impl Hash for $name { fn hash(&self, state: &mut H) { - (self.name.as_str(), self.len()).hash(state); + (self.key).hash(state); } } impl PartialEq for $name { fn eq(&self, other: &Self) -> bool { - self.register.len() == other.register.len() && self.name == other.name + self.register.len() == other.register.len() && self.key == other.key } } @@ -222,26 +225,3 @@ create_register!( "cr", RegisterAsKey::Classical ); - -// Add equivalencies between Keys and Registers -impl Equivalent for RegisterAsKey { - fn equivalent(&self, key: &QuantumRegister) -> bool { - match self { - Self::Quantum((name, length)) => { - name == &key.name && *length == key.len().try_into().unwrap() - } - _ => false, - } - } -} - -impl Equivalent for RegisterAsKey { - fn equivalent(&self, key: &ClassicalRegister) -> bool { - match self { - Self::Classical((name, length)) => { - name == &key.name && *length == key.len().try_into().unwrap() - } - _ => false, - } - } -} From 53a7979283317d47b5ebe31c0fd1ec29c59bdff4 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Tue, 21 Jan 2025 15:21:25 -0500 Subject: [PATCH 10/25] Add more rust native connections to `CircuitData` and `QuantumCircuit`. --- crates/circuit/src/bit_data.rs | 150 ++++++++++++++---------- crates/circuit/src/circuit_data.rs | 177 ++++++++++++++++++++++++++--- crates/circuit/src/converters.rs | 4 +- qiskit/circuit/quantumcircuit.py | 37 ++++-- 4 files changed, 281 insertions(+), 87 deletions(-) diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 4e68883ca80e..36d0761ed969 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -246,7 +246,7 @@ pub struct NewBitData, R: Register + Hash + Eq> { /// Maps Register keys to indices reg_keys: HashMap, /// Mapping between bit index and its register info - bit_info: Vec>, + bit_info: Vec>, /// Registers in the circuit registry: Vec, /// Registers in Python @@ -334,22 +334,15 @@ where // Add register info cancel if any qubit is duplicated for (bit_idx, bit) in bits.iter().enumerate() { let bit_info = &mut self.bit_info[BitType::from(*bit) as usize]; - if bit_info.is_some() { - panic!( - "The bit {:?} is currently assigned to another register.", - bit - ) - } else { - *bit_info = Some(BitInfo::new( - idx, - bit_idx.try_into().unwrap_or_else(|_| { - panic!( - "The current register exceeds its capacity limit. Bits {}", - reg.len() - ) - }), - )) - } + bit_info.push(BitInfo::new( + idx, + bit_idx.try_into().unwrap_or_else(|_| { + panic!( + "The current register exceeds its capacity limit. Bits {}", + reg.len() + ) + }), + )) } self.reg_keys.insert(reg.as_key().clone(), idx); self.registry.push(reg); @@ -390,13 +383,13 @@ where self.description ) }); - self.bit_info.push(None); + self.bit_info.push(vec![]); 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) -> Option<&BitInfo> { + pub fn get_bit_info(&self, index: T) -> &[BitInfo] { self.bit_info[BitType::from(index) as usize].as_ref() } @@ -434,13 +427,30 @@ where self.indices.get(&BitAsKey::new(bit)).copied() } - /// Gets a reference to the cached Python list, maintained by + /// Gets a reference to the cached Python list, with the bits maintained by /// this instance. #[inline] pub fn py_cached_bits(&self, py: Python) -> &Py { - self - .cached_py_bits - .get_or_init(|| PyList::empty_bound(py).into()) + self.cached_py_bits.get_or_init(|| { + PyList::new_bound( + py, + (0..self.len()).map(|idx| self.py_get_bit(py, (idx as u32).into()).unwrap()), + ) + .into() + }) + } + + /// Gets a reference to the cached Python list, with the registers maintained by + /// this instance. + #[inline] + pub fn py_cached_regs(&self, py: Python) -> &Py { + self.cached_py_regs.get_or_init(|| { + PyList::new_bound( + py, + (0..self.len_regs()).map(|idx| self.py_get_register(py, idx as u32).unwrap()), + ) + .into() + }) } /// Gets a reference to the underlying vector of Python bits. @@ -454,6 +464,24 @@ where .collect::>() } + /// Gets the location of a bit within the circuit + pub fn py_get_bit_location(&self, bit: &Bound) -> PyResult> { + 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 + )))?; + self.get_bit_info(index) + .iter() + .map(|info| -> PyResult<(u32, &PyObject)> { + Ok(( + info.index(), + self.py_get_register(py, info.register_index())?.unwrap(), + )) + }) + .collect::>>() + } + /// Gets a reference to the underlying vector of Python registers. #[inline] pub fn py_registers(&self, py: Python) -> PyResult> { @@ -520,7 +548,7 @@ where 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] { + else if let Some(bit_info) = self.bit_info[index_as_usize].first() { // If it is not initalized and has a register, initialize the register // and retrieve it from there the first time if self.bits[index_as_usize].get().is_none() { @@ -608,13 +636,7 @@ where pub fn py_add_bit(&mut self, bit: &Bound, strict: bool) -> PyResult { let py: Python<'_> = bit.py(); - if self.bits.len() - != self - .cached_py_bits - .get_or_init(|| PyList::empty_bound(py).into()) - .bind(bit.py()) - .len() - { + 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) )); @@ -631,12 +653,9 @@ where .try_insert(BitAsKey::new(bit), idx.into()) .is_ok() { - self.bit_info.push(None); + self.py_cached_bits(py).bind(py).append(bit)?; + self.bit_info.push(vec![]); self.bits.push(bit.into_py(py).into()); - self.cached_py_bits - .get_or_init(|| PyList::empty_bound(py).into()) - .bind(py) - .append(bit)?; // self.cached.bind(py).append(bit)?; } else if strict { return Err(PyValueError::new_err(format!( @@ -647,26 +666,21 @@ where Ok(idx.into()) } + /// Adds new register from Python. pub fn py_add_register(&mut self, register: &Bound) -> PyResult { let py = register.py(); - if self.registers.len() - != self - .cached_py_regs - .get_or_init(|| PyList::empty_bound(py).into()) - .bind(py) - .len() - { + if self.registers.len() != self.py_cached_regs(py).bind(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) + format!("This circuit's {} register list has become out of sync with the circuit data. Did something modify it?", self.description) )); } - // let index: 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 _: 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 = register .iter()? @@ -681,14 +695,36 @@ where .collect::>()?; let name: String = register.getattr("name")?.extract()?; + self.py_cached_regs(py).bind(py).append(register)?; self.registers.push(register.clone().unbind().into()); - self.cached_py_regs - .get_or_init(|| PyList::empty_bound(py).into()) - .bind(py) - .append(register)?; Ok(self.add_register(Some(name), None, Some(&bits))) } + /// 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) -> PyResult<()> { + // First invalidate everything related to registers + // This is done to ensure we regenerate the lost information + let current_length = self.len(); + self.bit_info = (0..current_length).map(|_| vec![]).collect(); + self.bits.iter_mut().for_each(|cell| { + cell.take(); + }); + self.cached_py_bits.take(); + + 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(&mut self, py: Python, indices: I) -> PyResult<()> where I: IntoIterator, @@ -700,7 +736,7 @@ where indices_sorted.sort(); for index in indices_sorted.into_iter().rev() { - // self.cached.bind(py).del_item(index)?; + self.py_cached_bits(py).bind(py).del_item(index)?; let bit = self .py_get_bit(py, (index as BitType).into())? .unwrap() @@ -759,10 +795,10 @@ where .collect(), indices: bit_data.indices.clone(), reg_keys: HashMap::new(), - bit_info: (0..bit_data.len()).map(|_| None).collect(), + bit_info: (0..bit_data.len()).map(|_| vec![]).collect(), registry: Vec::new(), registers: Vec::new(), - cached_py_bits: bit_data.cached().clone_ref(py).into(), + cached_py_bits: OnceLock::new(), cached_py_regs: OnceLock::new(), } } diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index ff35290343be..866b115d61ca 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -13,6 +13,7 @@ #[cfg(feature = "cache_pygates")] use std::sync::OnceLock; +use crate::bit::BitInfo; use crate::bit_data::NewBitData; use crate::circuit_instruction::{ CircuitInstruction, ExtraInstructionAttributes, OperationFromPython, @@ -114,7 +115,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>, clbits: Option<&Bound>, @@ -134,12 +135,12 @@ impl CircuitData { self_.set_global_phase(py, global_phase)?; if let Some(qubits) = qubits { for bit in qubits.iter()? { - self_.add_qubit(&bit?, true)?; + self_.py_add_qubit(&bit?, true)?; } } if let Some(clbits) = clbits { for bit in clbits.iter()? { - self_.add_clbit(&bit?, true)?; + self_.py_add_clbit(&bit?, true)?; } } if let Some(data) = data { @@ -164,6 +165,33 @@ impl CircuitData { Ok((ty, args, None::<()>, self_.iter()?).into_py(py)) } + /// Returns the current mapping of registered :class:`.QuantumRegisters` names and instances + /// as a dict. + /// + /// .. 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<'_>) -> Py { + self.qubits.py_cached_regs(py).clone_ref(py) + } + + /// Setter for registers, in case of forced updates. + #[setter("qregs")] + pub fn py_qreg_set(&mut self, other: &Bound) -> 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(&self, bit: &Bound) -> PyResult> { + self.qubits.py_get_bit_location(bit) + } + /// Returns the current sequence of registered :class:`.Qubit` instances as a list. /// /// .. warning:: @@ -188,6 +216,33 @@ impl CircuitData { self.qubits.len() } + /// Returns the current mapping of registered :class:`.QuantumRegisters` names and instances + /// as a dict. + /// + /// .. 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("cregs")] + pub fn py_cregs(&self, py: Python<'_>) -> Py { + self.clbits.py_cached_regs(py).clone_ref(py) + } + + /// Setter for registers, in case of forced updates. + #[setter("cregs")] + pub fn py_creg_set(&mut self, other: &Bound) -> 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(&self, bit: &Bound) -> PyResult> { + self.qubits.py_get_bit_location(bit) + } + /// Returns the current sequence of registered :class:`.Clbit` /// instances as a list. /// @@ -257,8 +312,8 @@ 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, bit: &Bound, strict: bool) -> PyResult<()> { + #[pyo3(name="add_qubit", signature = (bit, *, strict=true))] + pub fn py_add_qubit(&mut self, bit: &Bound, strict: bool) -> PyResult<()> { self.qubits.py_add_bit(bit, strict)?; Ok(()) } @@ -267,8 +322,8 @@ impl CircuitData { /// /// Args: /// bit (:class:`.QuantumRegister`): The register to add. - #[pyo3(signature = (register, *,))] - pub fn add_qreg(&mut self, register: &Bound) -> PyResult<()> { + #[pyo3(name="add_qreg", signature = (register, *,))] + pub fn py_add_qreg(&mut self, register: &Bound) -> PyResult<()> { self.qubits.py_add_register(register)?; Ok(()) } @@ -282,8 +337,8 @@ 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, bit: &Bound, strict: bool) -> PyResult<()> { + #[pyo3(name="add_clbit", signature = (bit, *, strict=true))] + pub fn py_add_clbit(&mut self, bit: &Bound, strict: bool) -> PyResult<()> { self.clbits.py_add_bit(bit, strict)?; Ok(()) } @@ -292,9 +347,9 @@ impl CircuitData { /// /// Args: /// bit (:class:`.ClassicalRegister`): The register to add. - #[pyo3(signature = (register, *,))] - pub fn add_creg(&mut self, register: &Bound) -> PyResult<()> { - self.qubits.py_add_register(register)?; + #[pyo3(name="add_creg", signature = (register, *,))] + pub fn py_add_creg(&mut self, register: &Bound) -> PyResult<()> { + self.clbits.py_add_register(register)?; Ok(()) } @@ -304,7 +359,7 @@ 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 { - let mut res = CircuitData::new( + 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)), @@ -316,6 +371,13 @@ impl CircuitData { res.cargs_interner = self.cargs_interner.clone(); res.param_table.clone_from(&self.param_table); + 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)?; + } + if deepcopy { let memo = PyDict::new_bound(py); for inst in &self.data { @@ -493,7 +555,7 @@ impl CircuitData { qubits: Option<&Bound>, clbits: Option<&Bound>, ) -> 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())?; if qubits.is_some() { if temp.num_qubits() < self.num_qubits() { return Err(PyValueError::new_err(format!( @@ -938,6 +1000,89 @@ 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."), + 0, + ), + clbits: NewBitData::with_capacity( + "clbits".to_owned(), + num_clbits + .try_into() + .expect("The number of clbits provided exceeds the limit for a circuit."), + 0, + ), + param_table: ParameterTable::new(), + global_phase, + }; + // Add all the bits into a register + if add_qreg { + let indices: Vec = (0..data.qubits.len() as u32).map(Qubit).collect(); + data.add_qreg(Some("q".to_string()), None, Some(&indices)); + } + // Add all the bits into a register + if add_creg { + let indices: Vec = (0..data.clbits.len() as u32).map(Clbit).collect(); + data.add_creg(Some("c".to_string()), None, Some(&indices)); + } + 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) -> &[BitInfo] { + 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, + num_qubits: Option, + bits: Option<&[Qubit]>, + ) -> u32 { + self.qubits.add_register(name, num_qubits, bits) + } + + /// Adds a generic qubit 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, + num_qubits: Option, + bits: Option<&[Clbit]>, + ) -> u32 { + self.clbits.add_register(name, num_qubits, bits) + } + + /// Get qubit location in the circuit + pub fn get_clbit_location(&self, clbit: Clbit) -> &[BitInfo] { + self.clbits.get_bit_info(clbit) + } + /// 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. @@ -1141,14 +1286,14 @@ impl CircuitData { let qubit_cls = QUBIT.get_bound(py); for _i in 0..num_qubits { let bit = qubit_cls.call0()?; - res.add_qubit(&bit, true)?; + res.py_add_qubit(&bit, true)?; } } 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(&bit, true)?; + res.py_add_clbit(&bit, true)?; } } Ok(res) diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs index 430afec85fd0..a83611f35df3 100644 --- a/crates/circuit/src/converters.rs +++ b/crates/circuit/src/converters.rs @@ -136,10 +136,10 @@ pub fn dag_to_circuit( )?; // Manually add qregs and cregs for reg in dag.qregs.bind(py).values() { - circuit.add_qreg(®)?; + circuit.py_add_qreg(®)?; } for reg in dag.cregs.bind(py).values() { - circuit.add_creg(®)?; + circuit.py_add_creg(®)?; } Ok(circuit) } diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index ab0ad758efb9..83542fdc01f6 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1091,13 +1091,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 @@ -1169,25 +1162,25 @@ 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) + bit: BitLocations(index, [data.get_qubit_location(bit)]) + for index, bit in enumerate(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) + bit: BitLocations(index, [data.get_clbit_location(bit)]) + for index, bit in enumerate(data.clbits) } out._data = data @@ -2232,6 +2225,26 @@ 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 + + @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 + @property def ancillas(self) -> list[AncillaQubit]: """A list of :class:`AncillaQubit`\\ s in the order that they were added. You should not @@ -3087,7 +3100,7 @@ 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: From 8b807386b39a8358f411357c0b64da7897de167b Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Tue, 21 Jan 2025 16:24:57 -0500 Subject: [PATCH 11/25] Fix: Do not re-add bits when adding registers. --- qiskit/circuit/quantumcircuit.py | 33 +++++++++----------------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 83542fdc01f6..537885ee0759 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1159,29 +1159,15 @@ def _from_circuit_data( """A private constructor from rust space circuit data.""" out = QuantumCircuit(name=name) - if data.num_qubits > 0: - if add_regs: - qr = QuantumRegister(name="q", bits=data.qubits) - out._qubit_indices = { - bit: BitLocations(index, [(qr, index)]) for index, bit in enumerate(data.qubits) - } - else: - out._qubit_indices = { - bit: BitLocations(index, [data.get_qubit_location(bit)]) - for index, bit in enumerate(data.qubits) - } - - if data.num_clbits > 0: - if add_regs: - cr = ClassicalRegister(name="c", bits=data.clbits) - out._clbit_indices = { - bit: BitLocations(index, [(cr, index)]) for index, bit in enumerate(data.clbits) - } - else: - out._clbit_indices = { - bit: BitLocations(index, [data.get_clbit_location(bit)]) - for index, bit in enumerate(data.clbits) - } + out._qubit_indices = { + bit: BitLocations(index, data.get_qubit_location(bit)) + for index, bit in enumerate(data.qubits) + } + + out._clbit_indices = { + bit: BitLocations(index, data.get_clbit_location(bit)) + for index, bit in enumerate(data.clbits) + } out._data = data @@ -3106,7 +3092,6 @@ def _add_qreg(self, qreg: QuantumRegister) -> None: 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)]) def add_bits(self, bits: Iterable[Bit]) -> None: From ff9958f4f3d0273e4b49f2a3dce6074c176f2133 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Tue, 21 Jan 2025 20:56:19 -0500 Subject: [PATCH 12/25] Fix: Incorrect addition of register cache --- crates/circuit/src/bit_data.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 36d0761ed969..8b605db90de7 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -11,6 +11,7 @@ // that they have been altered from the originals. use crate::bit::BitInfo; +use crate::circuit_data::CircuitError; use crate::imports::{CLASSICAL_REGISTER, QUANTUM_REGISTER, REGISTER}; use crate::register::{Register, RegisterAsKey}; use crate::{BitType, ToPyBit}; @@ -674,6 +675,14 @@ where 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 _: u32 = self.registers.len().try_into().map_err(|_| { PyRuntimeError::new_err(format!( @@ -694,10 +703,11 @@ where }) .collect::>()?; - let name: String = register.getattr("name")?.extract()?; + let name: String = key.name().to_string(); self.py_cached_regs(py).bind(py).append(register)?; - self.registers.push(register.clone().unbind().into()); - Ok(self.add_register(Some(name), None, Some(&bits))) + let idx = self.add_register(Some(name), None, Some(&bits)); + self.registers[idx as usize] = register.clone().unbind().into(); + Ok(idx) } /// Works as a setter for Python registers when the circuit needs to discard old data. @@ -706,13 +716,9 @@ where pub fn py_set_registers(&mut self, other: &Bound) -> PyResult<()> { // First invalidate everything related to registers // This is done to ensure we regenerate the lost information - let current_length = self.len(); - self.bit_info = (0..current_length).map(|_| vec![]).collect(); - self.bits.iter_mut().for_each(|cell| { - cell.take(); - }); - self.cached_py_bits.take(); + // Do not touch qubits. + self.reg_keys.clear(); self.registers.clear(); self.registry.clear(); self.cached_py_regs.take(); From 8c6c2ea411ed175506c212dfc7e1dddef33c9c93 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 22 Jan 2025 13:05:27 -0500 Subject: [PATCH 13/25] Fix: Incorrect serialization and mutability - Some functions in `CircuitData` had become mutable unnecessarily due to erroneous assumptions on the `BitData` `OnceCell` usage. - Fix incorrect serialization to now include registers in `CircuitData::__reduce__`. - Also add `CircuitData::__setstate__` to deal with the new register state. --- crates/circuit/src/bit_data.rs | 24 ++---------- crates/circuit/src/circuit_data.rs | 60 ++++++++++++++++++++++++------ crates/circuit/src/converters.rs | 15 +------- crates/circuit/src/dag_circuit.rs | 14 ++----- qiskit/circuit/quantumcircuit.py | 2 + 5 files changed, 59 insertions(+), 56 deletions(-) diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 8b605db90de7..64593d94f69b 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -518,7 +518,7 @@ where /// bit instances. /// Panics if any of the indices are out of range. pub fn py_map_indices( - &mut self, + &self, py: Python, bits: &[T], ) -> PyResult>> { @@ -533,16 +533,6 @@ where /// bit index. #[inline] pub fn py_get_bit(&self, py: Python, index: T) -> PyResult> { - /* - For this method we want to make sure a couple of things are done first: - - - First off, the method needs mutable access to all of `BitData` for a couple - of reasons, but mainly because it needs to be able to initialize the `OnceCell` - for both the `Bit` instance as well as the register. - - There is a problem with this as it could cause two mutable references to `BitData`. - How do we solve this? I guess we solved it LOL - */ 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() { @@ -550,7 +540,7 @@ where } // 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].first() { - // If it is not initalized and has a register, initialize the register + // If it is not initalized and has a register, initialize the first 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`. @@ -743,20 +733,14 @@ where for index in indices_sorted.into_iter().rev() { self.py_cached_bits(py).bind(py).del_item(index)?; - let bit = self - .py_get_bit(py, (index as BitType).into())? - .unwrap() - .clone_ref(py); + let bit = self.py_get_bit(py, (index as BitType).into())?.unwrap(); self.indices.remove(&BitAsKey::new(bit.bind(py))); 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() - .clone_ref(py); + let bit = self.py_get_bit(py, (i as BitType).into())?.unwrap(); self.indices .insert(BitAsKey::new(bit.bind(py)), (i as BitType).into()); } diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 866b115d61ca..dfa3b10b492f 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -150,7 +150,8 @@ impl CircuitData { Ok(self_) } - pub fn __reduce__(self_: &Bound, py: Python<'_>) -> PyResult { + pub fn __reduce__(self_: &Bound) -> PyResult { + let py = self_.py(); let ty: Bound = self_.get_type(); let args = { let self_ = self_.borrow(); @@ -162,7 +163,28 @@ impl CircuitData { self_.global_phase.clone(), ) }; - Ok((ty, args, None::<()>, self_.iter()?).into_py(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), + ) + }; + Ok((ty, args, state, self_.iter()?).into_py(py)) + } + + pub fn __setstate__( + self_: &Bound, + state: (Bound, Bound), + ) -> 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 current mapping of registered :class:`.QuantumRegisters` names and instances @@ -176,8 +198,8 @@ impl CircuitData { /// Returns: /// dict(:class:`.QuantumRegister`): The current sequence of registered qubits. #[getter("qregs")] - pub fn py_qregs(&self, py: Python<'_>) -> Py { - self.qubits.py_cached_regs(py).clone_ref(py) + pub fn py_qregs(&self, py: Python<'_>) -> &Py { + self.qubits.py_cached_regs(py) } /// Setter for registers, in case of forced updates. @@ -227,8 +249,8 @@ impl CircuitData { /// Returns: /// dict(:class:`.QuantumRegister`): The current sequence of registered qubits. #[getter("cregs")] - pub fn py_cregs(&self, py: Python<'_>) -> Py { - self.clbits.py_cached_regs(py).clone_ref(py) + pub fn py_cregs(&self, py: Python<'_>) -> &Py { + self.clbits.py_cached_regs(py) } /// Setter for registers, in case of forced updates. @@ -254,8 +276,8 @@ impl CircuitData { /// Returns: /// list(:class:`.Clbit`): The current sequence of registered clbits. #[getter("clbits")] - pub fn py_clbits(&self, py: Python<'_>) -> Py { - self.clbits.py_cached_bits(py).clone_ref(py) + pub fn py_clbits(&self, py: Python<'_>) -> &Py { + self.clbits.py_cached_bits(py) } /// Return the number of clbits. This is equivalent to the length of the list returned by @@ -424,7 +446,7 @@ impl CircuitData { /// /// Returns: /// tuple[set[:class:`.Qubit`], set[:class:`.Clbit`]]: The active qubits and clbits. - pub fn active_bits(&mut self, py: Python<'_>) -> PyResult> { + pub fn active_bits(&self, py: Python<'_>) -> PyResult> { let qubits = PySet::empty_bound(py)?; let clbits = PySet::empty_bound(py)?; for inst in self.data.iter() { @@ -548,14 +570,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>, clbits: Option<&Bound>, + qregs: Option<&Bound>, + cregs: Option<&Bound>, ) -> PyResult<()> { 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.iter()? { + temp.py_add_qreg(&qreg?)?; + } + } + // Add cregs if provided. + if let Some(cregs) = cregs { + for creg in cregs.iter()? { + temp.py_add_creg(&creg?)?; + } + } if qubits.is_some() { if temp.num_qubits() < self.num_qubits() { return Err(PyValueError::new_err(format!( @@ -584,9 +620,9 @@ impl CircuitData { } // Note: we also rely on this to make us iterable! - pub fn __getitem__(&mut self, py: Python, index: PySequenceIndex) -> PyResult { + pub fn __getitem__(&self, py: Python, index: PySequenceIndex) -> PyResult { // Get a single item, assuming the index is validated as in bounds. - let mut get_single = |index: usize| -> PyResult { + let get_single = |index: usize| -> PyResult { let inst = &self.data[index]; let qubits = self.qargs_interner.get(inst.qubits); let clbits = self.cargs_interner.get(inst.clbits); diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs index a83611f35df3..422b946e4e1c 100644 --- a/crates/circuit/src/converters.rs +++ b/crates/circuit/src/converters.rs @@ -15,10 +15,7 @@ use std::sync::OnceLock; use ::pyo3::prelude::*; use hashbrown::HashMap; -use pyo3::{ - intern, - types::{PyDict, PyList}, -}; +use pyo3::{intern, types::PyDict}; use crate::dag_circuit::{DAGCircuit, NodeType}; use crate::packed_instruction::PackedInstruction; @@ -32,8 +29,6 @@ pub struct QuantumCircuitData<'py> { pub name: Option>, pub calibrations: Option>>, pub metadata: Option>, - pub qregs: Option>, - pub cregs: Option>, pub input_vars: Vec>, pub captured_vars: Vec>, pub declared_vars: Vec>, @@ -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"))? .iter()? diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 6ec6ff827b48..01cdcf1f07f5 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -6677,16 +6677,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)?; } // Pre-process and re-intern all indices again. @@ -6754,8 +6750,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/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 537885ee0759..b3fc4467ef0f 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1419,6 +1419,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 From e5ebe22c6f0dd8c03cb0e137119afb5d748ba995 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:25:30 -0500 Subject: [PATCH 14/25] Refactor `BitInfo` to account for bits added from registers. - Fix bug with `BlueprintCircuit` that discarded most instances of `QuantumRegister`. - Rename `RegisterAsKey::index()` to size, since it was erroneously named. --- crates/circuit/src/bit.rs | 60 ++++++++++--- crates/circuit/src/bit_data.rs | 99 +++++++++++++++------- crates/circuit/src/circuit_data.rs | 6 +- crates/circuit/src/register.rs | 2 +- qiskit/circuit/library/blueprintcircuit.py | 11 ++- qiskit/circuit/quantumcircuit.py | 37 +++++--- 6 files changed, 159 insertions(+), 56 deletions(-) diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs index 8580c7651934..b749e8a2cd42 100644 --- a/crates/circuit/src/bit.rs +++ b/crates/circuit/src/bit.rs @@ -1,25 +1,65 @@ use std::fmt::Debug; -/// Keeps information about where a qubit is located within the circuit. -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Hash)] +/// Keeps informatyion 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 { - register_idx: u32, - index: u32, + added_by_reg: bool, + registers: Vec, } impl BitInfo { - pub fn new(register_idx: u32, index: u32) -> Self { - Self { - register_idx, - index, + 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.register_idx + self.0 } pub fn index(&self) -> u32 { - self.index + self.1 } } diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 9260e040a6e9..ae0f52b4b711 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -10,7 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use crate::bit::BitInfo; +use crate::bit::{BitInfo, BitLocation}; use crate::circuit_data::CircuitError; use crate::imports::{CLASSICAL_REGISTER, QUANTUM_REGISTER, REGISTER}; use crate::register::{Register, RegisterAsKey}; @@ -247,7 +247,7 @@ pub struct NewBitData, R: Register + Hash + Eq> { /// Maps Register keys to indices reg_keys: HashMap, /// Mapping between bit index and its register info - bit_info: Vec>, + bit_info: Vec, /// Registers in the circuit registry: Vec, /// Registers in Python @@ -312,12 +312,20 @@ where } /// Adds a register onto the [BitData] of the circuit. + /// + /// _**Note:** If providing the ``bits`` argument, the bits must exist in the circuit._ pub fn add_register( &mut self, name: Option, size: Option, bits: Option<&[T]>, ) -> 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)) => { @@ -326,24 +334,19 @@ where } else { bits.into() }; - let idx = self.registry.len().try_into().unwrap_or_else(|_| { - panic!( - "The {} registry in this circuit has reached its maximum capacity.", - self.description - ) - }); // Add register info cancel if any qubit is duplicated for (bit_idx, bit) in bits.iter().enumerate() { let bit_info = &mut self.bit_info[BitType::from(*bit) as usize]; - bit_info.push(BitInfo::new( + bit_info.add_register( idx, bit_idx.try_into().unwrap_or_else(|_| { panic!( - "The current register exceeds its capacity limit. Bits {}", + "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); @@ -351,7 +354,20 @@ where idx } (Some(size), None) => { - let bits: Vec = (0..size).map(|_| self.add_bit()).collect(); + let bits: Vec = (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 = if let Some(name) = name { (bits.as_slice(), name).into() } else { @@ -378,20 +394,24 @@ where /// /// _**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(vec![]); + 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) -> &[BitInfo] { - self.bit_info[BitType::from(index) as usize].as_ref() + 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 @@ -417,11 +437,20 @@ where pub fn contains_register_by_key(&self, reg: &RegisterAsKey) -> bool { self.reg_keys.contains_key(reg) } +} - // ======================= - // PyMethods - // ======================= - +// PyMethods +impl NewBitData +where + T: From + Copy + Debug + ToPyBit, + R: Register + + Hash + + Eq + + From<(usize, Option)> + + for<'a> From<&'a [T]> + + for<'a> From<(&'a [T], String)>, + BitType: From, +{ /// Finds the native bit index of the given Python bit. #[inline] pub fn py_find_bit(&self, bit: &Bound) -> Option { @@ -541,8 +570,8 @@ where 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].first() { - // If it is not initalized and has a register, initialize the first register + 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`. @@ -647,7 +676,7 @@ where .is_ok() { self.py_cached_bits(py).bind(py).append(bit)?; - self.bit_info.push(vec![]); + self.bit_info.push(BitInfo::new(None)); self.bits.push(bit.clone().unbind().into()); // self.cached.bind(py).append(bit)?; } else if strict { @@ -676,7 +705,7 @@ where ))); } - let _: u32 = self.registers.len().try_into().map_err(|_| { + 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 @@ -685,13 +714,23 @@ where let bits: Vec = register .try_iter()? - .map(|bit| -> PyResult { + .enumerate() + .map(|(bit_index, bit)| -> PyResult { + 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?; - if let Some(idx) = self.indices.get(&BitAsKey::new(&bit)) { - Ok(*idx) + let index = if let Some(idx) = self.indices.get(&BitAsKey::new(&bit)) { + *idx } else { - self.py_add_bit(&bit, true) - } + self.py_add_bit(&bit, true)? + }; + self.bit_info[BitType::from(index) as usize].add_register(idx, bit_index); + Ok(index) }) .collect::>()?; @@ -708,7 +747,7 @@ where pub fn py_set_registers(&mut self, other: &Bound) -> PyResult<()> { // First invalidate everything related to registers // This is done to ensure we regenerate the lost information - // Do not touch qubits. + // self.bit_info.clear() self.reg_keys.clear(); self.registers.clear(); @@ -787,7 +826,7 @@ where .collect(), indices: bit_data.indices.clone(), reg_keys: HashMap::new(), - bit_info: (0..bit_data.len()).map(|_| vec![]).collect(), + bit_info: (0..bit_data.len()).map(|_| BitInfo::new(None)).collect(), registry: Vec::new(), registers: Vec::new(), cached_py_bits: OnceLock::new(), diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 36cb5d398814..a8d47f95a954 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -13,7 +13,7 @@ #[cfg(feature = "cache_pygates")] use std::sync::OnceLock; -use crate::bit::BitInfo; +use crate::bit::BitLocation; use crate::bit_data::NewBitData; use crate::circuit_instruction::{ CircuitInstruction, ExtraInstructionAttributes, OperationFromPython, @@ -1089,7 +1089,7 @@ impl CircuitData { } /// Get qubit location in the circuit - pub fn get_qubit_location(&self, qubit: Qubit) -> &[BitInfo] { + pub fn get_qubit_location(&self, qubit: Qubit) -> &[BitLocation] { self.qubits.get_bit_info(qubit) } @@ -1119,7 +1119,7 @@ impl CircuitData { } /// Get qubit location in the circuit - pub fn get_clbit_location(&self, clbit: Clbit) -> &[BitInfo] { + pub fn get_clbit_location(&self, clbit: Clbit) -> &[BitLocation] { self.clbits.get_bit_info(clbit) } diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs index 5a491384c338..dd9623fc1496 100644 --- a/crates/circuit/src/register.rs +++ b/crates/circuit/src/register.rs @@ -40,7 +40,7 @@ impl RegisterAsKey { } #[inline] - pub fn index(&self) -> u32 { + pub fn size(&self) -> u32 { match self { RegisterAsKey::Register(key) => key.1, RegisterAsKey::Quantum(key) => key.1, diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py index 90550a18948e..d49c738841e4 100644 --- a/qiskit/circuit/library/blueprintcircuit.py +++ b/qiskit/circuit/library/blueprintcircuit.py @@ -67,14 +67,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 b3fc4467ef0f..92b4123d6f33 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1159,15 +1159,31 @@ def _from_circuit_data( """A private constructor from rust space circuit data.""" out = QuantumCircuit(name=name) - out._qubit_indices = { - bit: BitLocations(index, data.get_qubit_location(bit)) - for index, bit in enumerate(data.qubits) - } - - out._clbit_indices = { - bit: BitLocations(index, data.get_clbit_location(bit)) - for index, bit in enumerate(data.clbits) - } + 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, data.get_qubit_location(bit)) + for index, bit in enumerate(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, data.get_clbit_location(bit)) + for index, bit in enumerate(data.clbits) + } out._data = data @@ -3071,13 +3087,12 @@ 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)] ) From 2162f1f8f4b16091f9590047f0ccb50a8c784708 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:32:46 -0500 Subject: [PATCH 15/25] Fix: Resolve strange behavior of `BitLocations`. - Fix `BitLocation` updates when a bit or a register is added to use data directly from `CircuitData`. - Fix incorrect register re-assignment in `QuantumCircuit._from_circuit_data`. - Re-organize `BitData::py_get_bit_location` to behave more closely to python. --- crates/circuit/src/bit_data.rs | 31 ++++++++++-------- crates/circuit/src/circuit_data.rs | 12 +++++-- qiskit/circuit/quantumcircuit.py | 52 ++++++++++++------------------ 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index ae0f52b4b711..053198b65c75 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -497,21 +497,27 @@ where } /// Gets the location of a bit within the circuit - pub fn py_get_bit_location(&self, bit: &Bound) -> PyResult> { + pub fn py_get_bit_location( + &self, + bit: &Bound, + ) -> 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 )))?; - self.get_bit_info(index) - .iter() - .map(|info| -> PyResult<(u32, &PyObject)> { - Ok(( - info.index(), - self.py_get_register(py, info.register_index())?.unwrap(), - )) - }) - .collect::>>() + 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::>>()?, + )) } /// Gets a reference to the underlying vector of Python registers. @@ -705,7 +711,7 @@ where ))); } - let idx: u32 = self.registers.len().try_into().map_err(|_| { + let _: 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 @@ -716,7 +722,7 @@ where .try_iter()? .enumerate() .map(|(bit_index, bit)| -> PyResult { - let bit_index: u32 = bit_index.try_into().map_err(|_| { + let _: u32 = bit_index.try_into().map_err(|_| { CircuitError::new_err(format!( "The current register exceeds its capacity limit. Number of {} : {}", self.description, @@ -729,7 +735,6 @@ where } else { self.py_add_bit(&bit, true)? }; - self.bit_info[BitType::from(index) as usize].add_register(idx, bit_index); Ok(index) }) .collect::>()?; diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index a8d47f95a954..685b7ac614e9 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -211,7 +211,10 @@ impl CircuitData { /// Gets the location of the bit inside of the circuit #[pyo3(name = "get_qubit_location")] - pub fn py_get_qubit_location(&self, bit: &Bound) -> PyResult> { + pub fn py_get_qubit_location( + &self, + bit: &Bound, + ) -> PyResult<(u32, Vec<(&PyObject, u32)>)> { self.qubits.py_get_bit_location(bit) } @@ -262,8 +265,11 @@ impl CircuitData { /// Gets the location of the bit inside of the circuit #[pyo3(name = "get_clbit_location")] - pub fn py_get_clbit_location(&self, bit: &Bound) -> PyResult> { - self.qubits.py_get_bit_location(bit) + pub fn py_get_clbit_location( + &self, + bit: &Bound, + ) -> PyResult<(u32, Vec<(&PyObject, u32)>)> { + self.clbits.py_get_bit_location(bit) } /// Returns the current sequence of registered :class:`.Clbit` diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 92b4123d6f33..c478ac1494be 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1162,28 +1162,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, data.get_qubit_location(bit)) - 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, data.get_clbit_location(bit)) - 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 @@ -3089,13 +3079,8 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: elif isinstance(register, ClassicalRegister): 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._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) @@ -3105,11 +3090,8 @@ def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: def _add_qreg(self, qreg: QuantumRegister) -> None: 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._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.""" @@ -3124,10 +3106,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 " @@ -3769,6 +3751,12 @@ def copy_empty_like( self._data.qubits, self._data.clbits, global_phase=self._data.global_phase ) + for qreg in self.qregs: + cpy.add_register(qreg) + + for creg in self.cregs: + cpy.add_register(creg) + if name: cpy.name = name return cpy From 03fbbc20a8a6f38dc174549bd597f1b4fececdac Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Tue, 28 Jan 2025 07:16:04 -0500 Subject: [PATCH 16/25] Update bit.rs Co-authored-by: Kento Ueda <38037695+to24toro@users.noreply.github.com> --- crates/circuit/src/bit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs index b749e8a2cd42..d5c2ed926699 100644 --- a/crates/circuit/src/bit.rs +++ b/crates/circuit/src/bit.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -/// Keeps informatyion about where a bit is located within the circuit. +/// 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. From 2cd46a539da354f2b4b691efe1b9d69fe9b89425 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Tue, 28 Jan 2025 11:48:25 -0500 Subject: [PATCH 17/25] Fix: Add registers correctly in `circuit_to_instruction`. - Add additional checks to `find_bit()`, to make sure bit exists in the `CircuitData` instance. --- qiskit/circuit/quantumcircuit.py | 4 ++++ qiskit/converters/circuit_to_instruction.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index c478ac1494be..89bcf6ae25c8 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -3169,8 +3169,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)}") diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index bd7d720a5dd0..e643ab1e3f30 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 From 82833702d6a867a4e4727cb758be43f1c41cdf64 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Tue, 28 Jan 2025 15:23:38 -0500 Subject: [PATCH 18/25] Fix: Remaining tests (-1) - Separate python `add_register` from native method due to conflicting implementation. - Fix register setter in `NewBitData` to discard old `BitLocations`. - Update `BitLocations` when setting a new register in `QuantumCircuit`. - Modify test to include registers when comparing to an equivalency from the `EquivalenceLibrary`. --- crates/circuit/src/bit_data.rs | 28 +++++++++++++------- qiskit/circuit/quantumcircuit.py | 11 ++++++++ test/python/circuit/test_gate_definitions.py | 4 +-- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 053198b65c75..124bfe9418eb 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -313,7 +313,7 @@ where /// Adds a register onto the [BitData] of the circuit. /// - /// _**Note:** If providing the ``bits`` argument, the bits must exist in the circuit._ + /// _**Note:** If providing the ``bits`` argument, the bits must already exist in the circuit._ pub fn add_register( &mut self, name: Option, @@ -711,7 +711,7 @@ where ))); } - let _: u32 = self.registers.len().try_into().map_err(|_| { + 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 @@ -722,7 +722,7 @@ where .try_iter()? .enumerate() .map(|(bit_index, bit)| -> PyResult { - let _: u32 = bit_index.try_into().map_err(|_| { + 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, @@ -730,19 +730,28 @@ where )) })?; let bit = bit?; - let index = if let Some(idx) = self.indices.get(&BitAsKey::new(&bit)) { - *idx + let index = if let Some(index) = self.indices.get(&BitAsKey::new(&bit)) { + let bit_info = &mut self.bit_info[BitType::from(*index) as usize]; + bit_info.add_register(idx, bit_index); + *index } else { - self.py_add_bit(&bit, true)? + 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::>()?; + // Create the native register let name: String = key.name().to_string(); + let reg: R = (bits.as_slice(), name).into(); + self.py_cached_regs(py).bind(py).append(register)?; - let idx = self.add_register(Some(name), None, Some(&bits)); - self.registers[idx as usize] = register.clone().unbind().into(); + self.reg_keys.insert(reg.as_key().clone(), idx); + self.registry.push(reg); + self.registers.push(register.clone().unbind().into()); Ok(idx) } @@ -752,7 +761,8 @@ where pub fn py_set_registers(&mut self, other: &Bound) -> PyResult<()> { // First invalidate everything related to registers // This is done to ensure we regenerate the lost information - // self.bit_info.clear() + // 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(); diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 89bcf6ae25c8..c804c4500c22 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2228,6 +2228,8 @@ def qregs(self) -> list[QuantumRegister]: @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]: @@ -2238,6 +2240,8 @@ def cregs(self) -> list[ClassicalRegister]: @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]: @@ -4073,11 +4077,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/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 1b78861c6c00..fca8a1131bf0 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -453,8 +453,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) From a3533349a4c7703421051cb9224cfdd1abd26d22 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Tue, 28 Jan 2025 16:24:09 -0500 Subject: [PATCH 19/25] Fix: incorrect circuit initalization on the rust-side. - Add one test case to test initialization. --- crates/circuit/src/circuit_data.rs | 50 +++++++++++++++++++-- qiskit/converters/circuit_to_instruction.py | 2 +- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 685b7ac614e9..0a4be8426d31 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -1064,27 +1064,35 @@ impl CircuitData { num_qubits .try_into() .expect("The number of qubits provided exceeds the limit for a circuit."), - 0, + 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."), - 0, + if add_creg { 1 } else { 0 }, ), param_table: ParameterTable::new(), global_phase, }; // Add all the bits into a register if add_qreg { - let indices: Vec = (0..data.qubits.len() as u32).map(Qubit).collect(); + let indices: Vec = (0..num_qubits).map(|_| data.add_qubit()).collect(); data.add_qreg(Some("q".to_string()), None, Some(&indices)); + } else { + (0..num_qubits).for_each(|_| { + data.add_qubit(); + }); } // Add all the bits into a register if add_creg { - let indices: Vec = (0..data.clbits.len() as u32).map(Clbit).collect(); + let indices: Vec = (0..num_clbits).map(|_| data.add_clbit()).collect(); data.add_creg(Some("c".to_string()), None, Some(&indices)); + } else { + (0..num_clbits).for_each(|_| { + data.add_qubit(); + }); } data } @@ -1109,6 +1117,11 @@ impl CircuitData { self.qubits.add_register(name, num_qubits, bits) } + /// Returns an iterator with all the QuantumRegisters in the circuit + pub fn qregs(&self) -> impl Iterator { + (0..self.qubits.len_regs()).flat_map(|index| self.qubits.get_register(index as u32)) + } + /// Adds a generic qubit to a circuit pub fn add_clbit(&mut self) -> Clbit { self.clbits.add_bit() @@ -1124,6 +1137,11 @@ impl CircuitData { self.clbits.add_register(name, num_qubits, bits) } + /// Returns an iterator with all the QuantumRegisters in the circuit + pub fn cregs(&self) -> impl Iterator { + (0..self.clbits.len_regs()).flat_map(|index| self.clbits.get_register(index as u32)) + } + /// Get qubit location in the circuit pub fn get_clbit_location(&self, clbit: Clbit) -> &[BitLocation] { self.clbits.get_bit_info(clbit) @@ -1827,3 +1845,27 @@ impl<'py> FromPyObject<'py> for AssignParam { Ok(Self(Param::extract_no_coerce(ob)?)) } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_circuit_construction() { + let circuit_data = CircuitData::new(4, 3, Param::Float(0.0), true, true); + let qregs: Vec<&QuantumRegister> = circuit_data.qregs().collect(); + let cregs: Vec<&ClassicalRegister> = circuit_data.cregs().collect(); + + // Expected qregs + let example_qreg = QuantumRegister::new(Some(4), Some("q".to_owned()), None); + let expected_qregs: Vec<&QuantumRegister> = vec![&example_qreg]; + + assert_eq!(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!(cregs, expected_cregs) + } +} diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index e643ab1e3f30..b6c3cee06edf 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -152,7 +152,7 @@ def fix_condition(op): # Re-add the registers. for reg in regs: qc.add_register(reg) - + if circuit.global_phase: qc.global_phase = circuit.global_phase From 6be25c5b271f37827b52db353febf7e31a90ec22 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 29 Jan 2025 13:49:52 -0500 Subject: [PATCH 20/25] Fix: Regenerate cache if any rust bits are missing. - Modify the getter methods for the cached lists to correct the cache if it becomes necessary. This to avoid the uninitialization of bits when rust bits are added after requesting access from Python. These methods are now fallible, so a PyResult will always be returned. - Adapt `CircuitData` and `DAGCircuit` to these changes. --- crates/circuit/src/bit_data.rs | 57 +++++++++++++++++++++++------- crates/circuit/src/circuit_data.rs | 26 +++++++------- crates/circuit/src/dag_circuit.rs | 4 +-- 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 124bfe9418eb..560f46773b2f 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -460,29 +460,61 @@ where /// Gets a reference to the cached Python list, with the bits maintained by /// this instance. #[inline] - pub fn py_cached_bits(&self, py: Python) -> &Py { - self.cached_py_bits.get_or_init(|| { + pub fn py_cached_bits(&self, py: Python) -> PyResult<&Py> { + 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, rebuild cache + let res_as_bound = res.bind(py); + if res_as_bound.len() != self.len() { + let current_length = res_as_bound.len(); + for index in 0..self.len() { + if index < current_length { + res_as_bound.set_item(index, self.py_get_bit(py, (index as u32).into())?)? + } else { + res_as_bound.append(self.py_get_bit(py, (index as u32).into())?)? + } + } + Ok(res) + } else { + 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) -> &Py { - self.cached_py_regs.get_or_init(|| { + pub fn py_cached_regs(&self, py: Python) -> PyResult<&Py> { + 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 bits, 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 0..self.len_regs() { + if index < current_length { + res_as_bound.set_item(index, self.py_get_register(py, index as u32)?)? + } else { + res_as_bound.append(self.py_get_register(py, index as u32)?)? + } + } + Ok(res) + } else { + Ok(res) + } } /// Gets a reference to the underlying vector of Python bits. @@ -664,7 +696,7 @@ where pub fn py_add_bit(&mut self, bit: &Bound, strict: bool) -> PyResult { let py: Python<'_> = bit.py(); - if self.bits.len() != self.py_cached_bits(py).bind(bit.py()).len() { + 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) )); @@ -681,10 +713,10 @@ where .try_insert(BitAsKey::new(bit), idx.into()) .is_ok() { - self.py_cached_bits(py).bind(py).append(bit)?; + // 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()); - // self.cached.bind(py).append(bit)?; } else if strict { return Err(PyValueError::new_err(format!( "Existing bit {:?} cannot be re-added in strict mode.", @@ -697,7 +729,7 @@ where /// Adds new register from Python. pub fn py_add_register(&mut self, register: &Bound) -> PyResult { let py = register.py(); - if self.registers.len() != self.py_cached_regs(py).bind(py).len() { + 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) )); @@ -748,7 +780,8 @@ where let name: String = key.name().to_string(); let reg: R = (bits.as_slice(), name).into(); - self.py_cached_regs(py).bind(py).append(register)?; + // Append to cache before bits 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()); @@ -788,7 +821,7 @@ where indices_sorted.sort(); for index in indices_sorted.into_iter().rev() { - self.py_cached_bits(py).bind(py).del_item(index)?; + self.py_cached_bits(py)?.bind(py).del_item(index)?; let bit = self.py_get_bit(py, (index as BitType).into())?.unwrap(); self.indices.remove(&BitAsKey::new(bit.bind(py))); self.bits.remove(index); diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 0a4be8426d31..ae05954b1e76 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -157,8 +157,8 @@ impl CircuitData { let args = { let self_ = self_.borrow(); ( - self_.qubits.py_cached_bits(py).clone_ref(py), - self_.clbits.py_cached_bits(py).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(), @@ -167,8 +167,8 @@ impl CircuitData { let state = { let borrowed = self_.borrow(); ( - borrowed.qubits.py_cached_regs(py).clone_ref(py), - borrowed.clbits.py_cached_regs(py).clone_ref(py), + 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) @@ -199,7 +199,7 @@ impl CircuitData { /// Returns: /// dict(:class:`.QuantumRegister`): The current sequence of registered qubits. #[getter("qregs")] - pub fn py_qregs(&self, py: Python<'_>) -> &Py { + pub fn py_qregs(&self, py: Python<'_>) -> PyResult<&Py> { self.qubits.py_cached_regs(py) } @@ -228,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 { - self.qubits.py_cached_bits(py).clone_ref(py) + pub fn py_qubits(&self, py: Python<'_>) -> PyResult<&Py> { + self.qubits.py_cached_bits(py) } /// Return the number of qubits. This is equivalent to the length of the list returned by @@ -253,7 +253,7 @@ impl CircuitData { /// Returns: /// dict(:class:`.QuantumRegister`): The current sequence of registered qubits. #[getter("cregs")] - pub fn py_cregs(&self, py: Python<'_>) -> &Py { + pub fn py_cregs(&self, py: Python<'_>) -> PyResult<&Py> { self.clbits.py_cached_regs(py) } @@ -283,7 +283,7 @@ impl CircuitData { /// Returns: /// list(:class:`.Clbit`): The current sequence of registered clbits. #[getter("clbits")] - pub fn py_clbits(&self, py: Python<'_>) -> &Py { + pub fn py_clbits(&self, py: Python<'_>) -> PyResult<&Py> { self.clbits.py_cached_bits(py) } @@ -390,8 +390,8 @@ impl CircuitData { pub fn copy(&self, py: Python<'_>, copy_instructions: bool, deepcopy: bool) -> PyResult { 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)), + Some(self.qubits.py_cached_bits(py)?.bind(py)), + Some(self.clbits.py_cached_bits(py)?.bind(py)), None, self.data.len(), self.global_phase.clone(), @@ -400,10 +400,10 @@ impl CircuitData { res.cargs_interner = self.cargs_interner.clone(); res.param_table.clone_from(&self.param_table); - for qreg in self.py_qregs(py).bind(py).iter() { + 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() { + for creg in self.py_cregs(py)?.bind(py).iter() { res.py_add_creg(&creg)?; } diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index c04ed8c35c9c..70621c8dcb5f 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -6643,11 +6643,11 @@ impl DAGCircuit { } // Add all the registers - for qreg in qc_data.py_qregs(py).bind(py).iter() { + for qreg in qc_data.py_qregs(py)?.bind(py).iter() { new_dag.add_qreg(py, &qreg)?; } - for creg in qc_data.py_cregs(py).bind(py).iter() { + for creg in qc_data.py_cregs(py)?.bind(py).iter() { new_dag.add_creg(py, &creg)?; } From 5169fc68c4018e4fbbd17fe596ffabbbf5a1a5b9 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:27:59 -0500 Subject: [PATCH 21/25] Lint: Remove unused `Qubit` import --- test/python/circuit/test_gate_definitions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index fca8a1131bf0..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 ( From c2461baac895223a428c7c84d0f3d25215a7dab1 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:25:36 -0500 Subject: [PATCH 22/25] Docs: Add header strings in `bit.rs` and `register.rs` --- crates/circuit/src/bit.rs | 12 ++++++++++++ crates/circuit/src/register.rs | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/crates/circuit/src/bit.rs b/crates/circuit/src/bit.rs index d5c2ed926699..86df610a7547 100644 --- a/crates/circuit/src/bit.rs +++ b/crates/circuit/src/bit.rs @@ -1,3 +1,15 @@ +// 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. diff --git a/crates/circuit/src/register.rs b/crates/circuit/src/register.rs index dd9623fc1496..9e06595c7b29 100644 --- a/crates/circuit/src/register.rs +++ b/crates/circuit/src/register.rs @@ -1,3 +1,15 @@ +// 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::{ From f1ffc16ea4b3665319e908d6e72b7d85206d57c7 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez Date: Thu, 30 Jan 2025 13:51:34 -0500 Subject: [PATCH 23/25] Fix: Remove changes to the `Interner` - Remove stale methods `contains` and `get_interned` from `Interner` in favor of the changes introduced by #13752. --- crates/circuit/src/interner.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/crates/circuit/src/interner.rs b/crates/circuit/src/interner.rs index ae28c2119cae..17065c5c92e1 100644 --- a/crates/circuit/src/interner.rs +++ b/crates/circuit/src/interner.rs @@ -391,25 +391,6 @@ where _type: PhantomData, } } - - /// Checks whether an item is already packed in the interner - pub fn contains(&self, value: &T) -> bool - where - T: Hash + Eq, - { - self.0.contains(value) - } - - /// Retrieves interner index of anitem if it exists - pub fn get_interned(&self, value: &T) -> Option> - where - T: Hash + Eq, - { - self.0.get_index_of(value).map(|index| Interned { - index: index as u32, - _type: PhantomData, - }) - } } impl Interner From ba8bcf89503d8ea7ad2d95caeda06efb5721afab Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 31 Jan 2025 20:09:40 -0500 Subject: [PATCH 24/25] Tests: Add rust-side python testing for rust-native circuit creation - Temporarily fix incorrect null assignment of `Bit` object to indices. A different solution can be achieved if more time is spent. - Incorrect python circuit creation in Python due to misusage of the bit's `BitInfo` property inside of `BitData`. - Fix incorrect register and bit creation from `CircuitData::new()` - Add provisional `qubits_mut()` and `clbits_mut()` methods while we address the issue with `indices`. --- crates/circuit/src/bit_data.rs | 61 ++++++++--- crates/circuit/src/circuit_data.rs | 157 +++++++++++++++++++++++++++-- crates/circuit/src/dag_circuit.rs | 6 +- 3 files changed, 196 insertions(+), 28 deletions(-) diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 560f46773b2f..f66cd5cafc50 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -453,8 +453,36 @@ where { /// Finds the native bit index of the given Python bit. #[inline] - pub fn py_find_bit(&self, bit: &Bound) -> Option { - self.indices.get(&BitAsKey::new(bit)).copied() + pub fn py_find_bit(&mut self, bit: &Bound) -> PyResult> { + let key = &BitAsKey::new(bit); + if let Some(value) = self.indices.get(key).copied() { + Ok(Some(value)) + } else if self.indices.len() != self.len() { + let py = bit.py(); + // TODO: Make sure all bits have been mapped during addition. + // The only case in which bits may not be mapped is when a circuit + // is created entirely from Rust. For which case the Python representations + // of the bits still don't exist. + // Ideally, we'd want to initialize the mapping with a value representing the + // future bit, but it is hard to come up with a way to represent an object + // that doesn't exist yet. + // So for now, perform a check if the length of the mapped indices differs + // from the number of bits available. + for index in 0..self.len() { + let index = T::from(index.try_into().map_err(|_| { + CircuitError::new_err(format!( + "This circuit's {} has exceeded its length limit", + self.description + )) + })?); + let bit = self.py_get_bit(py, index)?.unwrap(); + let bit_as_key = BitAsKey::new(bit.bind(py)); + self.indices.entry(bit_as_key).or_insert(index); + } + Ok(self.indices.get(key).copied()) + } else { + Ok(None) + } } /// Gets a reference to the cached Python list, with the bits maintained by @@ -530,11 +558,11 @@ where /// Gets the location of a bit within the circuit pub fn py_get_bit_location( - &self, + &mut self, bit: &Bound, ) -> PyResult<(u32, Vec<(&PyObject, u32)>)> { let py = bit.py(); - let index = self.py_find_bit(bit).ok_or(PyKeyError::new_err(format!( + let index = self.py_find_bit(bit)?.ok_or(PyKeyError::new_err(format!( "The provided {} is not part of this circuit", self.description )))?; @@ -649,12 +677,19 @@ where RegisterAsKey::Quantum(_) => QUANTUM_REGISTER.get_bound(py), RegisterAsKey::Classical(_) => CLASSICAL_REGISTER.get_bound(py), }; - // Check if any indices have been initialized, if such is the case - // Treat the rest of indices as new `Bits`` - if register - .bits() - .any(|bit| self.bits[BitType::from(bit) as usize].get().is_some()) - { + // 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 = register .bits() .map(|bit| -> PyResult { @@ -677,12 +712,6 @@ where .set(reg.into()) .map_err(|_| PyRuntimeError::new_err("Could not set the OnceCell correctly"))?; Ok(self.registers[index_as_usize].get()) - } else { - 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 { Ok(self.registers[index_as_usize].get()) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index ae05954b1e76..dfa6fbc90a8e 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -212,7 +212,7 @@ impl CircuitData { /// Gets the location of the bit inside of the circuit #[pyo3(name = "get_qubit_location")] pub fn py_get_qubit_location( - &self, + &mut self, bit: &Bound, ) -> PyResult<(u32, Vec<(&PyObject, u32)>)> { self.qubits.py_get_bit_location(bit) @@ -266,7 +266,7 @@ impl CircuitData { /// Gets the location of the bit inside of the circuit #[pyo3(name = "get_clbit_location")] pub fn py_get_clbit_location( - &self, + &mut self, bit: &Bound, ) -> PyResult<(u32, Vec<(&PyObject, u32)>)> { self.clbits.py_get_bit_location(bit) @@ -799,7 +799,7 @@ impl CircuitData { .map(|b| { Ok(self .qubits - .py_find_bit(other.qubits.py_get_bit(py, *b)?.unwrap().bind(py)) + .py_find_bit(other.qubits.py_get_bit(py, *b)?.unwrap().bind(py))? .unwrap()) }) .collect::>>()?; @@ -810,7 +810,7 @@ impl CircuitData { .map(|b| { Ok(self .clbits - .py_find_bit(other.clbits.py_get_bit(py, *b)?.unwrap().bind(py)) + .py_find_bit(other.clbits.py_get_bit(py, *b)?.unwrap().bind(py))? .unwrap()) }) .collect::>>()?; @@ -1078,8 +1078,15 @@ impl CircuitData { }; // Add all the bits into a register if add_qreg { - let indices: Vec = (0..num_qubits).map(|_| data.add_qubit()).collect(); - data.add_qreg(Some("q".to_string()), None, Some(&indices)); + 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(); @@ -1087,11 +1094,18 @@ impl CircuitData { } // Add all the bits into a register if add_creg { - let indices: Vec = (0..num_clbits).map(|_| data.add_clbit()).collect(); - data.add_creg(Some("c".to_string()), None, Some(&indices)); + data.add_creg( + Some("c".to_string()), + Some( + num_clbits + .try_into() + .expect("The number of qubits provided exceeds the limit for a circuit."), + ), + None, + ); } else { (0..num_clbits).for_each(|_| { - data.add_qubit(); + data.add_clbit(); }); } data @@ -1554,6 +1568,18 @@ impl CircuitData { &self.clbits } + // TODO: Remove + /// Returns a mutable view of the Qubits registered in the circuit + pub fn qubits_mut(&mut self) -> &mut NewBitData { + &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 { + &mut self.clbits + } + /// Unpacks from interned value to `[Qubit]` pub fn get_qargs(&self, index: Interned<[Qubit]>) -> &[Qubit] { self.qargs_interner().get(index) @@ -1869,3 +1895,116 @@ mod test { assert_eq!(cregs, expected_cregs) } } + +#[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 resgisters 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 { + 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")?; + println!("{}", converted_qregs); + assert!(converted_qregs.is_instance(&PyList::type_object(py))?); + assert!( + converted_qregs.downcast::()?.len() == 0, + "The quantum registers list returned a non-empty value" + ); + + let converted_qubits = converted_circuit.getattr("qubits")?; + println!("{:?}", converted_qubits); + assert!(converted_qubits.is_instance(&PyList::type_object(py))?); + assert!( + converted_qubits.downcast::()?.len() == (num_qubits as usize), + "The qubits has the wrong length" + ); + + let converted_qregs = converted_circuit.getattr("qregs")?; + println!("{}", converted_qregs); + assert!(converted_qregs.is_instance(&PyList::type_object(py))?); + assert!( + converted_qregs.downcast::()?.len() == 0, + "The classical registers list returned a non-empty value" + ); + + let converted_clbits = converted_circuit.getattr("clbits")?; + println!("{:?}", converted_clbits); + assert!(converted_clbits.is_instance(&PyList::type_object(py))?); + assert!( + converted_clbits.downcast::()?.len() == (num_clbits as usize), + "The clbits has the wrong length" + ); + + Ok(true) + }) + .is_ok_and(|res| res); + assert!(result); + } + + #[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 { + let quantum_circuit = QUANTUM_CIRCUIT.get_bound(py).clone(); + + let converted_circuit = + quantum_circuit.call_method1("_from_circuit_data", (circuit_data,))?; + let expected_circuit = quantum_circuit.call1((num_qubits, num_clbits))?; + + let converted_qregs = converted_circuit.getattr("qregs")?; + let expected_qregs = expected_circuit.getattr("qregs")?; + + println!("{:?} vs {:?}", converted_qregs, expected_qregs); + + assert!(converted_qregs.eq(expected_qregs)?); + + let converted_cregs = converted_circuit.getattr("cregs")?; + let expected_cregs = expected_circuit.getattr("cregs")?; + + println!("{:?} vs {:?}", converted_cregs, expected_cregs); + + assert!(converted_cregs.eq(expected_cregs)?); + + let converted_qubits = converted_circuit.getattr("qubits")?; + let expected_qubits = expected_circuit.getattr("qubits")?; + println!("{:?} vs {:?}", converted_qubits, expected_qubits); + assert!(converted_qubits.eq(&expected_qubits)?); + + let converted_clbits = converted_circuit.getattr("clbits")?; + let expected_clbits = expected_circuit.getattr("clbits")?; + println!("{:?} vs {:?}", converted_clbits, expected_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")?; + println!( + "{:?} vs {:?}", + converted_global_phase, expected_global_phase + ); + assert!(converted_global_phase.eq(&expected_global_phase)?); + + // TODO: Figure out why this fails + // converted_circuit.eq(expected_circuit) + + Ok(true) + }) + .is_ok_and(|res| res); + assert!(result); + } +} diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 318d148f1704..af110010eb73 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -6624,7 +6624,7 @@ impl DAGCircuit { clbit_order: Option>>, ) -> PyResult { // Extract necessary attributes - let qc_data = qc.data; + let mut qc_data = qc.data; let num_qubits = qc_data.num_qubits(); let num_clbits = qc_data.num_clbits(); let num_ops = qc_data.__len__(); @@ -6668,7 +6668,7 @@ impl DAGCircuit { &qubit ))); } - let qubit_index = qc_data.qubits().py_find_bit(&qubit).unwrap(); + let qubit_index = qc_data.qubits_mut().py_find_bit(&qubit)?.unwrap(); ordered_vec[qubit_index.index()] = new_dag.add_qubit_unchecked(py, &qubit)?; Ok(()) })?; @@ -6701,7 +6701,7 @@ impl DAGCircuit { &clbit ))); }; - let clbit_index = qc_data.clbits().py_find_bit(&clbit).unwrap(); + let clbit_index = qc_data.clbits_mut().py_find_bit(&clbit)?.unwrap(); ordered_vec[clbit_index.index()] = new_dag.add_clbit_unchecked(py, &clbit)?; Ok(()) })?; From 7690065fc974f24251dadabadc354b7cdc7b8b26 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Sun, 2 Feb 2025 16:17:27 -0500 Subject: [PATCH 25/25] Add; Default to rust bits in rust circuit-generating functions. - Fix error where rust bits wouldn't be initialized in the cache. - Provisionally fix error in which rust bits wouldn't have valid mapping to python bits. Bits need to be generated before being able to map them to a rust native index. The current solution implements said changes during the `find_bit` function, in which there is an extra validation step. However, this is certain to consume more resources. The ideal solution would be to generate said mapping when obtaining the Python bit with something similar to a `RefCell`. However, `RefCells` are not thread safe and could cause several mutable references to coexist. --- crates/circuit/src/bit_data.rs | 50 +++++++++++++++--------------- crates/circuit/src/circuit_data.rs | 38 ++++++++++++++++------- 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index f66cd5cafc50..62e4d2b35f85 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -457,7 +457,7 @@ where let key = &BitAsKey::new(bit); if let Some(value) = self.indices.get(key).copied() { Ok(Some(value)) - } else if self.indices.len() != self.len() { + } else if self.indices.len() < self.len() { let py = bit.py(); // TODO: Make sure all bits have been mapped during addition. // The only case in which bits may not be mapped is when a circuit @@ -500,7 +500,7 @@ where // If the length is different from the stored bits, rebuild cache let res_as_bound = res.bind(py); - if res_as_bound.len() != self.len() { + if res_as_bound.len() < self.len() { let current_length = res_as_bound.len(); for index in 0..self.len() { if index < current_length { @@ -509,7 +509,7 @@ where res_as_bound.append(self.py_get_bit(py, (index as u32).into())?)? } } - Ok(res) + Ok(self.cached_py_bits.get().unwrap()) } else { Ok(res) } @@ -528,9 +528,9 @@ where .into() }); - // If the length is different from the stored bits, rebuild cache + // 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() { + if res_as_bound.len() < self.len_regs() { let current_length = res_as_bound.len(); for index in 0..self.len_regs() { if index < current_length { @@ -539,7 +539,14 @@ where res_as_bound.append(self.py_get_register(py, index as u32)?)? } } - Ok(res) + let trimmed = res_as_bound.get_slice(0, self.len_regs()).unbind(); + self.cached_py_regs.set(trimmed).map_err(|_| { + PyRuntimeError::new_err(format!( + "Tried to initialized {} register cache while another thread was initializing", + self.description + )) + })?; + Ok(self.cached_py_regs.get().unwrap()) } else { Ok(res) } @@ -591,21 +598,15 @@ where /// 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>( - &self, + &mut self, bits: impl IntoIterator>, ) -> PyResult> { let v: Result, _> = bits .into_iter() .map(|b| { - self.indices - .get(&BitAsKey::new(&b)) - .copied() - .ok_or_else(|| { - PyKeyError::new_err(format!( - "Bit {:?} has not been added to this circuit.", - 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()) @@ -646,12 +647,9 @@ where self.bits[index_as_usize] .set(res.into()) .map_err(|_| PyRuntimeError::new_err("Could not set the OnceCell correctly"))?; - return Ok(self.bits[index_as_usize].get()); } // If it is initialized, just retrieve. - else { - return Ok(self.bits[index_as_usize].get()); - } + Ok(self.bits[index_as_usize].get()) } else if let Some(bit) = self.bits[index_as_usize].get() { Ok(Some(bit)) } else { @@ -746,13 +744,15 @@ where 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()); } - Ok(idx.into()) } /// Adds new register from Python. @@ -791,10 +791,10 @@ where )) })?; let bit = bit?; - let index = if let Some(index) = self.indices.get(&BitAsKey::new(&bit)) { - let bit_info = &mut self.bit_info[BitType::from(*index) as usize]; + 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 + index } else { let index = self.py_add_bit(&bit, true)?; self.bit_info[BitType::from(index) as usize] = @@ -809,7 +809,7 @@ where let name: String = key.name().to_string(); let reg: R = (bits.as_slice(), name).into(); - // Append to cache before bits to avoid rebuilding cache. + // 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); diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index dfa6fbc90a8e..f8fe8a34efb4 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -19,7 +19,7 @@ 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}; @@ -1074,8 +1074,11 @@ impl CircuitData { if add_creg { 1 } else { 0 }, ), param_table: ParameterTable::new(), - global_phase, + 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( @@ -1136,7 +1139,7 @@ impl CircuitData { (0..self.qubits.len_regs()).flat_map(|index| self.qubits.get_register(index as u32)) } - /// Adds a generic qubit to a circuit + /// Adds a generic clbit to a circuit pub fn add_clbit(&mut self) -> Clbit { self.clbits.add_bit() } @@ -1161,6 +1164,23 @@ impl CircuitData { 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. @@ -1350,8 +1370,8 @@ impl CircuitData { data: Vec::with_capacity(instruction_capacity), qargs_interner: Interner::new(), cargs_interner: Interner::new(), - qubits: NewBitData::new("qubits".to_string()), - clbits: NewBitData::new("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), }; @@ -1361,17 +1381,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.py_add_qubit(&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.py_add_clbit(&bit, true)?; + res.add_clbit(); } } Ok(res)