Oxidize `QuantumCircuit._data` and intern `CircuitInstruction` args (#10827)

* Initial commit.

* Fix bugs with slicing impl.

* Fix sort, remove dead code.

* Use custom cfg flag for debug.

* Run fmt.

* Add todo.

* Revert utils change, not needed anymore.

* Use CircuitData in compose.

* Revert test stub.

* Remove once_cell. Not needed anymore.

* Run format.

* Fix lint issues.

* Use PyTuple in ElementType.

* Use native list and dict types for lookup tables.

* Implement __traverse__ and __clear__.

* Take iterable for extend. Preallocate.

* Fix insertion indexing behavior.

* Fix typo.

* Avoid possible hash collisions in InternContext.

* Use () instead of None for default iterable.

* Resolve copy share intern context TODO.

* Use () instead of empty list in stubed lists.

* Use u32 for IndexType.

* Resolve TODO in if_else.py.

* Fix Rust lint issues.

* Add unit testing for InternContext.

* Remove print logging.

* Fix bug introduced during print removal.

* Add helper methods for getting the context in CircuitData.

* Fix issue with BlueprintCircuit.

* Workaround for CircuitInstruction operation mutability.

Fix lint.

* Revert "Workaround for CircuitInstruction operation mutability."

This reverts commit 21f3e514ff.

* Add _add_ref to InstructionSet.

Allows in-place update of CircuitInstruction.operation
within a CircuitData.

* Exclude CircuitData::intern_context from GC clear.

* Fix lint.

* Avoid copy into list.

* Override __deepcopy__.

* Override __copy__ to avoid pulling CircuitData into a list.

* Implement copy for CircuitData.

* Port CircuitInstruction to Rust.

* Use Rust CircuitInstruction.

* Optimize circuit_to_instruction.py

* Use freelist for CircuitInstruction class.

* Remove use count, fix extend, InternContext internal.

Previously CircuitData::extend would construct CircuitInstruction instances
in memory for the entire iterable. Now, a GILPool is created for
each iteration of the loop to ensure each instance is dropped
before the next one is created. At most 3 CircuitInstruction
instances are now created during the construction and transpilation
of a QuantumVolume circuit in my testing.

Use count tracking is now removed from InternContext.

InternContext is now no longer exposed through the Python API.

* Revert to using old extraction for now until we move bits inside CircuitData.

* Fix issue with deletion.

* Performance optimization overhaul.

- Switched to native types for qubits, clbits, qubit_indices,
clbit_indices.
- Tweaks to ensure that only 1-3 CircuitInstruction instances
  are ever alive at the same time (e.g. in CircuitData.extend).
- Use py_ext helpers to avoid unnecessary deallocations in PyO3.
- Move pickling for CircuitData from QC to CircuitData itself.
- Add CircuitData.reserve to preallocate space during copy
  scenarios.

* Fix lint.

* Attempt to move docstring for CircuitInstruction.

* Add missing py_ext module file.

* Use full struct for InternedInstruction.

* Remove __copy__ from QuantumCircuit.

* Improve bit key wrapper name.

* Remove opt TODO comment. Will be done in new PR.

* Clean up GC cycle breaking.

* Add separate method convert_py_index_clamped.

* Implement __eq__ instead of __richcmp__.

* Avoid 'use'ing SliceOrInt enum.

* Port slice conversion to pure Rust.

* Clean up InternContext.

* Change import order in quantumcircuitdata.py.

* Use .zip method on iter().

* Rename get_or_cache to intern_instruction.

* Improve error handling.

* Add documentation comments for Rust types.

* Move reserve method.

* Add tests for bit key error.

* Localize BlueprintCircuit workaround.

* Slice refactoring, fixes, tests.

* Fix setitem slice regression, clean up slice testing.

* Use Python docstring form for pymethods.

* Don't use underscore in BitAsKey type name.

* Add release note.

* Add upgrade note.

* Add error messages for exceeded qubits and clbits.

* Use BitType instead of u32.

* Improve code comments for extend.

* Improve readability with PackedInstruction.

InternedInstruction is renamed to PackedInstruction to
make it clearer that the code deals in terms of some
form of a packed instruction, and that interning of
the instruction's qubits and clbits is just an
implementation detail of that.

* Fix reserve issue.

* Use usize for pointer type.

* Use copied instead of cloned on Option.

* Use .is() instead of IDs.

* Convert tuples to list in legacy format.

* Remove redundant parens.

* Add crate comment for py_ext.

* Make CircuitData::qubits and CircuitData::clbits ephemeral.
This commit is contained in:
Kevin Hartman 2023-11-20 12:21:41 -05:00 committed by GitHub
parent 45efe668eb
commit f3857f18d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1386 additions and 197 deletions

View File

@ -24,6 +24,7 @@ mod euler_one_qubit_decomposer;
mod nlayout;
mod optimize_1q_gates;
mod pauli_exp_val;
mod quantum_circuit;
mod results;
mod sabre_layout;
mod sabre_swap;
@ -52,6 +53,7 @@ fn _accelerate(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pymodule!(sabre_swap::sabre_swap))?;
m.add_wrapped(wrap_pymodule!(pauli_exp_val::pauli_expval))?;
m.add_wrapped(wrap_pymodule!(dense_layout::dense_layout))?;
m.add_wrapped(wrap_pymodule!(quantum_circuit::quantum_circuit))?;
m.add_wrapped(wrap_pymodule!(error_map::error_map))?;
m.add_wrapped(wrap_pymodule!(sparse_pauli_op::sparse_pauli_op))?;
m.add_wrapped(wrap_pymodule!(results::results))?;

View File

@ -0,0 +1,657 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2023
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
use crate::quantum_circuit::circuit_instruction::CircuitInstruction;
use crate::quantum_circuit::intern_context::{BitType, IndexType, InternContext};
use crate::quantum_circuit::py_ext;
use hashbrown::HashMap;
use pyo3::exceptions::{PyIndexError, PyKeyError, PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyIterator, PyList, PySlice, PyTuple, PyType};
use pyo3::{PyObject, PyResult, PyTraverseError, PyVisit};
use std::hash::{Hash, Hasher};
/// Private type used to store instructions with interned arg lists.
#[derive(Clone, Debug)]
struct PackedInstruction {
/// The Python-side operation instance.
op: PyObject,
/// The index under which the interner has stored `qubits`.
qubits_id: IndexType,
/// The index under which the interner has stored `clbits`.
clbits_id: IndexType,
}
/// Private wrapper for Python-side Bit instances that implements
/// [Hash] and [Eq], allowing them to be used in Rust hash-based
/// sets and maps.
///
/// Python's `hash()` is called on the wrapped Bit instance during
/// construction and returned from Rust's [Hash] trait impl.
/// The impl of [PartialEq] first compares the native Py pointers
/// to determine equality. If these are not equal, only then does
/// it call `repr()` on both sides, which has a significant
/// performance advantage.
#[derive(Clone, Debug)]
struct BitAsKey {
/// Python's `hash()` of the wrapped instance.
hash: isize,
/// The wrapped instance.
bit: PyObject,
}
impl BitAsKey {
fn new(bit: &PyAny) -> PyResult<Self> {
Ok(BitAsKey {
hash: bit.hash()?,
bit: bit.into_py(bit.py()),
})
}
}
impl Hash for BitAsKey {
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_isize(self.hash);
}
}
impl PartialEq for BitAsKey {
fn eq(&self, other: &Self) -> bool {
self.bit.is(&other.bit)
|| Python::with_gil(|py| {
self.bit
.as_ref(py)
.repr()
.unwrap()
.eq(other.bit.as_ref(py).repr().unwrap())
.unwrap()
})
}
}
impl Eq for BitAsKey {}
/// A container for :class:`.QuantumCircuit` instruction listings that stores
/// :class:`.CircuitInstruction` instances in a packed form by interning
/// their :attr:`~.CircuitInstruction.qubits` and
/// :attr:`~.CircuitInstruction.clbits` to native vectors of indices.
///
/// Before adding a :class:`.CircuitInstruction` to this container, its
/// :class:`.Qubit` and :class:`.Clbit` instances MUST be registered via the
/// constructor or via :meth:`.CircuitData.add_qubit` and
/// :meth:`.CircuitData.add_clbit`. This is because the order in which
/// bits of the same type are added to the container determines their
/// associated indices used for storage and retrieval.
///
/// Once constructed, this container behaves like a Python list of
/// :class:`.CircuitInstruction` instances. However, these instances are
/// created and destroyed on the fly, and thus should be treated as ephemeral.
///
/// For example,
///
/// .. code-block::
///
/// qubits = [Qubit()]
/// data = CircuitData(qubits)
/// data.append(CircuitInstruction(XGate(), (qubits[0],), ()))
/// assert(data[0] == data[0]) # => Ok.
/// assert(data[0] is data[0]) # => PANICS!
///
/// .. warning::
///
/// This is an internal interface and no part of it should be relied upon
/// outside of Qiskit.
///
/// Args:
/// qubits (Iterable[:class:`.Qubit`] | None): The initial sequence of
/// qubits, used to map :class:`.Qubit` instances to and from its
/// indices.
/// clbits (Iterable[:class:`.Clbit`] | None): The initial sequence of
/// clbits, used to map :class:`.Clbit` instances to and from its
/// indices.
/// data (Iterable[:class:`.CircuitInstruction`]): An initial instruction
/// listing to add to this container. All bits appearing in the
/// instructions in this iterable must also exist in ``qubits`` and
/// ``clbits``.
/// reserve (int): The container's initial capacity. This is reserved
/// before copying instructions into the container when ``data``
/// is provided, so the initialized container's unused capacity will
/// be ``max(0, reserve - len(data))``.
///
/// Raises:
/// KeyError: if ``data`` contains a reference to a bit that is not present
/// in ``qubits`` or ``clbits``.
#[pyclass(sequence, module = "qiskit._accelerate.quantum_circuit")]
#[derive(Clone, Debug)]
pub struct CircuitData {
/// The packed instruction listing.
data: Vec<PackedInstruction>,
/// The intern context used to intern instruction bits.
intern_context: InternContext,
/// The qubits registered (e.g. through :meth:`~.CircuitData.add_qubit`).
qubits_native: Vec<PyObject>,
/// The clbits registered (e.g. through :meth:`~.CircuitData.add_clbit`).
clbits_native: Vec<PyObject>,
/// Map of :class:`.Qubit` instances to their index in
/// :attr:`.CircuitData.qubits`.
qubit_indices_native: HashMap<BitAsKey, BitType>,
/// Map of :class:`.Clbit` instances to their index in
/// :attr:`.CircuitData.clbits`.
clbit_indices_native: HashMap<BitAsKey, BitType>,
/// The qubits registered, cached as a ``list[Qubit]``.
qubits: Py<PyList>,
/// The clbits registered, cached as a ``list[Clbit]``.
clbits: Py<PyList>,
}
/// A private enumeration type used to extract arguments to pymethods
/// that may be either an index or a slice.
#[derive(FromPyObject)]
pub enum SliceOrInt<'a> {
Slice(&'a PySlice),
Int(isize),
}
#[pymethods]
impl CircuitData {
#[new]
#[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0))]
pub fn new(
py: Python<'_>,
qubits: Option<&PyAny>,
clbits: Option<&PyAny>,
data: Option<&PyAny>,
reserve: usize,
) -> PyResult<Self> {
let mut self_ = CircuitData {
data: Vec::new(),
intern_context: InternContext::new(),
qubits_native: Vec::new(),
clbits_native: Vec::new(),
qubit_indices_native: HashMap::new(),
clbit_indices_native: HashMap::new(),
qubits: PyList::empty(py).into_py(py),
clbits: PyList::empty(py).into_py(py),
};
if let Some(qubits) = qubits {
for bit in qubits.iter()? {
self_.add_qubit(py, bit?)?;
}
}
if let Some(clbits) = clbits {
for bit in clbits.iter()? {
self_.add_clbit(py, bit?)?;
}
}
if let Some(data) = data {
self_.reserve(py, reserve);
self_.extend(py, data)?;
}
Ok(self_)
}
pub fn __reduce__(self_: &PyCell<CircuitData>, py: Python<'_>) -> PyResult<PyObject> {
let ty: &PyType = self_.get_type();
let args = {
let self_ = self_.borrow();
(
self_.qubits.clone_ref(py),
self_.clbits.clone_ref(py),
None::<()>,
self_.data.len(),
)
};
Ok((ty, args, None::<()>, self_.iter()?).into_py(py))
}
/// Returns the current sequence of registered :class:`.Qubit`
/// instances as a list.
///
/// .. note::
///
/// This list is not kept in sync with the container.
///
/// Returns:
/// list(:class:`.Qubit`): The current sequence of registered qubits.
#[getter]
pub fn qubits(&self, py: Python<'_>) -> PyObject {
PyList::new(py, self.qubits.as_ref(py)).into_py(py)
}
/// Returns the current sequence of registered :class:`.Clbit`
/// instances as a list.
///
/// .. note::
///
/// This list is not kept in sync with the container.
///
/// Returns:
/// list(:class:`.Clbit`): The current sequence of registered clbits.
#[getter]
pub fn clbits(&self, py: Python<'_>) -> PyObject {
PyList::new(py, self.clbits.as_ref(py)).into_py(py)
}
/// Registers a :class:`.Qubit` instance.
///
/// Args:
/// bit (:class:`.Qubit`): The qubit to register.
pub fn add_qubit(&mut self, py: Python<'_>, bit: &PyAny) -> PyResult<()> {
let idx: BitType = self.qubits_native.len().try_into().map_err(|_| {
PyRuntimeError::new_err(
"The number of qubits in the circuit has exceeded the maximum capacity",
)
})?;
self.qubit_indices_native.insert(BitAsKey::new(bit)?, idx);
self.qubits_native.push(bit.into_py(py));
self.qubits = PyList::new(py, &self.qubits_native).into_py(py);
Ok(())
}
/// Registers a :class:`.Clbit` instance.
///
/// Args:
/// bit (:class:`.Clbit`): The clbit to register.
pub fn add_clbit(&mut self, py: Python<'_>, bit: &PyAny) -> PyResult<()> {
let idx: BitType = self.clbits_native.len().try_into().map_err(|_| {
PyRuntimeError::new_err(
"The number of clbits in the circuit has exceeded the maximum capacity",
)
})?;
self.clbit_indices_native.insert(BitAsKey::new(bit)?, idx);
self.clbits_native.push(bit.into_py(py));
self.clbits = PyList::new(py, &self.clbits_native).into_py(py);
Ok(())
}
/// Performs a shallow copy.
///
/// Returns:
/// CircuitData: The shallow copy.
pub fn copy(&self, py: Python<'_>) -> PyResult<Self> {
Ok(CircuitData {
data: self.data.clone(),
intern_context: self.intern_context.clone(),
qubits_native: self.qubits_native.clone(),
clbits_native: self.clbits_native.clone(),
qubit_indices_native: self.qubit_indices_native.clone(),
clbit_indices_native: self.clbit_indices_native.clone(),
qubits: PyList::new(py, &self.qubits_native).into_py(py),
clbits: PyList::new(py, &self.clbits_native).into_py(py),
})
}
/// Reserves capacity for at least ``additional`` more
/// :class:`.CircuitInstruction` instances to be added to this container.
///
/// Args:
/// additional (int): The additional capacity to reserve. If the
/// capacity is already sufficient, does nothing.
pub fn reserve(&mut self, _py: Python<'_>, additional: usize) {
self.data.reserve(additional);
}
pub fn __len__(&self) -> usize {
self.data.len()
}
// Note: we also rely on this to make us iterable!
pub fn __getitem__<'py>(&self, py: Python<'py>, index: &PyAny) -> PyResult<PyObject> {
// Internal helper function to get a specific
// instruction by index.
fn get_at(
self_: &CircuitData,
py: Python<'_>,
index: isize,
) -> PyResult<Py<CircuitInstruction>> {
let index = self_.convert_py_index(index)?;
if let Some(inst) = self_.data.get(index) {
self_.unpack(py, inst)
} else {
Err(PyIndexError::new_err(format!(
"No element at index {:?} in circuit data",
index
)))
}
}
if index.is_exact_instance_of::<PySlice>() {
let slice = self.convert_py_slice(index.downcast_exact::<PySlice>()?)?;
let result = slice
.into_iter()
.map(|i| get_at(self, py, i))
.collect::<PyResult<Vec<_>>>()?;
Ok(result.into_py(py))
} else {
Ok(get_at(self, py, index.extract()?)?.into_py(py))
}
}
pub fn __delitem__(&mut self, py: Python<'_>, index: SliceOrInt) -> PyResult<()> {
match index {
SliceOrInt::Slice(slice) => {
let slice = {
let mut s = self.convert_py_slice(slice)?;
if s.len() > 1 && s.first().unwrap() < s.last().unwrap() {
// Reverse the order so we're sure to delete items
// at the back first (avoids messing up indices).
s.reverse()
}
s
};
for i in slice.into_iter() {
self.__delitem__(py, SliceOrInt::Int(i))?;
}
Ok(())
}
SliceOrInt::Int(index) => {
let index = self.convert_py_index(index)?;
if self.data.get(index).is_some() {
self.data.remove(index);
Ok(())
} else {
Err(PyIndexError::new_err(format!(
"No element at index {:?} in circuit data",
index
)))
}
}
}
}
pub fn __setitem__(
&mut self,
py: Python<'_>,
index: SliceOrInt,
value: &PyAny,
) -> PyResult<()> {
match index {
SliceOrInt::Slice(slice) => {
let indices = slice.indices(self.data.len().try_into().unwrap())?;
let slice = self.convert_py_slice(slice)?;
let values = value.iter()?.collect::<PyResult<Vec<&PyAny>>>()?;
if indices.step != 1 && slice.len() != values.len() {
// A replacement of a different length when step isn't exactly '1'
// would result in holes.
return Err(PyValueError::new_err(format!(
"attempt to assign sequence of size {:?} to extended slice of size {:?}",
values.len(),
slice.len(),
)));
}
for (i, v) in slice.iter().zip(values.iter()) {
self.__setitem__(py, SliceOrInt::Int(*i), *v)?;
}
if slice.len() > values.len() {
// Delete any extras.
let slice = PySlice::new(
py,
indices.start + values.len() as isize,
indices.stop,
1isize,
);
self.__delitem__(py, SliceOrInt::Slice(slice))?;
} else {
// Insert any extra values.
for v in values.iter().skip(slice.len()).rev() {
let v: PyRef<CircuitInstruction> = v.extract()?;
self.insert(py, indices.stop, v)?;
}
}
Ok(())
}
SliceOrInt::Int(index) => {
let index = self.convert_py_index(index)?;
let value: PyRef<CircuitInstruction> = value.extract()?;
let mut packed = self.pack(py, value)?;
std::mem::swap(&mut packed, &mut self.data[index]);
Ok(())
}
}
}
pub fn insert(
&mut self,
py: Python<'_>,
index: isize,
value: PyRef<CircuitInstruction>,
) -> PyResult<()> {
let index = self.convert_py_index_clamped(index);
let packed = self.pack(py, value)?;
self.data.insert(index, packed);
Ok(())
}
pub fn pop(&mut self, py: Python<'_>, index: Option<PyObject>) -> PyResult<PyObject> {
let index =
index.unwrap_or_else(|| std::cmp::max(0, self.data.len() as isize - 1).into_py(py));
let item = self.__getitem__(py, index.as_ref(py))?;
self.__delitem__(py, index.as_ref(py).extract()?)?;
Ok(item)
}
pub fn append(&mut self, py: Python<'_>, value: PyRef<CircuitInstruction>) -> PyResult<()> {
let packed = self.pack(py, value)?;
self.data.push(packed);
Ok(())
}
// To prevent the entire iterator from being loaded into memory,
// we create a `GILPool` for each iteration of the loop, which
// ensures that the `CircuitInstruction` returned by the call
// to `next` is dropped before the next iteration.
pub fn extend(&mut self, py: Python<'_>, itr: &PyAny) -> PyResult<()> {
// To ensure proper lifetime management, we explicitly store
// the result of calling `iter(itr)` as a GIL-independent
// reference that we access only with the most recent GILPool.
// It would be dangerous to access the original `itr` or any
// GIL-dependent derivatives of it after creating the new pool.
let itr: Py<PyIterator> = itr.iter()?.into_py(py);
loop {
// Create a new pool, so that PyO3 can clear memory at
// the end of the loop.
let pool = unsafe { py.new_pool() };
// It is recommended to *always* immediately set py to the pool's
// Python, to help avoid creating references with invalid lifetimes.
let py = pool.python();
// Access the iterator using the new pool.
match itr.as_ref(py).next() {
None => {
break;
}
Some(v) => {
self.append(py, v?.extract()?)?;
}
}
// The GILPool is dropped here, which cleans up the ref
// returned from `next` as well as any resources used by
// `self.append`.
}
Ok(())
}
pub fn clear(&mut self, _py: Python<'_>) -> PyResult<()> {
std::mem::take(&mut self.data);
Ok(())
}
// Marks this pyclass as NOT hashable.
#[classattr]
const __hash__: Option<Py<PyAny>> = None;
fn __eq__(slf: &PyCell<Self>, other: &PyAny) -> PyResult<bool> {
let slf: &PyAny = slf;
if slf.is(other) {
return Ok(true);
}
if slf.len()? != other.len()? {
return Ok(false);
}
// Implemented using generic iterators on both sides
// for simplicity.
let mut ours_itr = slf.iter()?;
let mut theirs_itr = other.iter()?;
loop {
match (ours_itr.next(), theirs_itr.next()) {
(Some(ours), Some(theirs)) => {
if !ours?.eq(theirs?)? {
return Ok(false);
}
}
(None, None) => {
return Ok(true);
}
_ => {
return Ok(false);
}
}
}
}
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
for packed in self.data.iter() {
visit.call(&packed.op)?;
}
for bit in self.qubits_native.iter().chain(self.clbits_native.iter()) {
visit.call(bit)?;
}
// Note:
// There's no need to visit the native Rust data
// structures used for internal tracking: the only Python
// references they contain are to the bits in these lists!
visit.call(&self.qubits)?;
visit.call(&self.clbits)?;
Ok(())
}
fn __clear__(&mut self) {
// Clear anything that could have a reference cycle.
self.data.clear();
self.qubits_native.clear();
self.clbits_native.clear();
self.qubit_indices_native.clear();
self.clbit_indices_native.clear();
}
}
impl CircuitData {
/// Converts a Python slice to a `Vec` of indices into
/// the instruction listing, [CircuitData.data].
fn convert_py_slice(&self, slice: &PySlice) -> PyResult<Vec<isize>> {
let indices = slice.indices(self.data.len().try_into().unwrap())?;
if indices.step > 0 {
Ok((indices.start..indices.stop)
.step_by(indices.step as usize)
.collect())
} else {
let mut out = Vec::with_capacity(indices.slicelength as usize);
let mut x = indices.start;
while x > indices.stop {
out.push(x);
x += indices.step;
}
Ok(out)
}
}
/// Converts a Python index to an index into the instruction listing,
/// or one past its end.
/// If the resulting index would be < 0, clamps to 0.
/// If the resulting index would be > len(data), clamps to len(data).
fn convert_py_index_clamped(&self, index: isize) -> usize {
let index = if index < 0 {
index + self.data.len() as isize
} else {
index
};
std::cmp::min(std::cmp::max(0, index), self.data.len() as isize) as usize
}
/// Converts a Python index to an index into the instruction listing.
fn convert_py_index(&self, index: isize) -> PyResult<usize> {
let index = if index < 0 {
index + self.data.len() as isize
} else {
index
};
if index < 0 || index >= self.data.len() as isize {
return Err(PyIndexError::new_err(format!(
"Index {:?} is out of bounds.",
index,
)));
}
Ok(index as usize)
}
/// Returns a [PackedInstruction] containing the original operation
/// of `elem` and [InternContext] indices of its `qubits` and `clbits`
/// fields.
fn pack(
&mut self,
py: Python<'_>,
inst: PyRef<CircuitInstruction>,
) -> PyResult<PackedInstruction> {
let mut interned_bits =
|indices: &HashMap<BitAsKey, BitType>, bits: &PyTuple| -> PyResult<IndexType> {
let args = bits
.into_iter()
.map(|b| {
let key = BitAsKey::new(b)?;
indices.get(&key).copied().ok_or_else(|| {
PyKeyError::new_err(format!(
"Bit {:?} has not been added to this circuit.",
b
))
})
})
.collect::<PyResult<Vec<BitType>>>()?;
self.intern_context.intern(args)
};
Ok(PackedInstruction {
op: inst.operation.clone_ref(py),
qubits_id: interned_bits(&self.qubit_indices_native, inst.qubits.as_ref(py))?,
clbits_id: interned_bits(&self.clbit_indices_native, inst.clbits.as_ref(py))?,
})
}
fn unpack(&self, py: Python<'_>, inst: &PackedInstruction) -> PyResult<Py<CircuitInstruction>> {
Py::new(
py,
CircuitInstruction {
operation: inst.op.clone_ref(py),
qubits: py_ext::tuple_new(
py,
self.intern_context
.lookup(inst.qubits_id)
.iter()
.map(|i| self.qubits_native[*i as usize].clone_ref(py))
.collect(),
),
clbits: py_ext::tuple_new(
py,
self.intern_context
.lookup(inst.clbits_id)
.iter()
.map(|i| self.clbits_native[*i as usize].clone_ref(py))
.collect(),
),
},
)
}
}

View File

@ -0,0 +1,254 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2023
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
use crate::quantum_circuit::py_ext;
use pyo3::basic::CompareOp;
use pyo3::prelude::*;
use pyo3::types::{PyList, PyTuple};
use pyo3::{PyObject, PyResult};
/// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and
/// various operands.
///
/// .. note::
///
/// There is some possible confusion in the names of this class, :class:`~.circuit.Instruction`,
/// and :class:`~.circuit.Operation`, and this class's attribute :attr:`operation`. Our
/// preferred terminology is by analogy to assembly languages, where an "instruction" is made up
/// of an "operation" and its "operands".
///
/// Historically, :class:`~.circuit.Instruction` came first, and originally contained the qubits
/// it operated on and any parameters, so it was a true "instruction". Over time,
/// :class:`.QuantumCircuit` became responsible for tracking qubits and clbits, and the class
/// became better described as an "operation". Changing the name of such a core object would be
/// a very unpleasant API break for users, and so we have stuck with it.
///
/// This class was created to provide a formal "instruction" context object in
/// :class:`.QuantumCircuit.data`, which had long been made of ad-hoc tuples. With this, and
/// the advent of the :class:`~.circuit.Operation` interface for adding more complex objects to
/// circuits, we took the opportunity to correct the historical naming. For the time being,
/// this leads to an awkward case where :attr:`.CircuitInstruction.operation` is often an
/// :class:`~.circuit.Instruction` instance (:class:`~.circuit.Instruction` implements the
/// :class:`.Operation` interface), but as the :class:`.Operation` interface gains more use,
/// this confusion will hopefully abate.
///
/// .. warning::
///
/// This is a lightweight internal class and there is minimal error checking; you must respect
/// the type hints when using it. It is the user's responsibility to ensure that direct
/// mutations of the object do not invalidate the types, nor the restrictions placed on it by
/// its context. Typically this will mean, for example, that :attr:`qubits` must be a sequence
/// of distinct items, with no duplicates.
#[pyclass(
freelist = 20,
sequence,
get_all,
module = "qiskit._accelerate.quantum_circuit"
)]
#[derive(Clone, Debug)]
pub struct CircuitInstruction {
/// The logical operation that this instruction represents an execution of.
pub operation: PyObject,
/// A sequence of the qubits that the operation is applied to.
pub qubits: Py<PyTuple>,
/// A sequence of the classical bits that this operation reads from or writes to.
pub clbits: Py<PyTuple>,
}
#[pymethods]
impl CircuitInstruction {
#[new]
pub fn new(
py: Python<'_>,
operation: PyObject,
qubits: Option<&PyAny>,
clbits: Option<&PyAny>,
) -> PyResult<Self> {
fn as_tuple(py: Python<'_>, seq: Option<&PyAny>) -> PyResult<Py<PyTuple>> {
match seq {
None => Ok(py_ext::tuple_new_empty(py)),
Some(seq) => {
if seq.is_instance_of::<PyTuple>() {
Ok(seq.downcast_exact::<PyTuple>()?.into_py(py))
} else if seq.is_instance_of::<PyList>() {
let seq = seq.downcast_exact::<PyList>()?;
Ok(py_ext::tuple_from_list(seq))
} else {
// New tuple from iterable.
Ok(py_ext::tuple_new(
py,
seq.iter()?
.map(|o| Ok(o?.into_py(py)))
.collect::<PyResult<Vec<PyObject>>>()?,
))
}
}
}
}
Ok(CircuitInstruction {
operation,
qubits: as_tuple(py, qubits)?,
clbits: as_tuple(py, clbits)?,
})
}
/// Returns a shallow copy.
///
/// Returns:
/// CircuitInstruction: The shallow copy.
pub fn copy(&self) -> Self {
self.clone()
}
/// Creates a shallow copy with the given fields replaced.
///
/// Returns:
/// CircuitInstruction: A new instance with the given fields replaced.
pub fn replace(
&self,
py: Python<'_>,
operation: Option<PyObject>,
qubits: Option<&PyAny>,
clbits: Option<&PyAny>,
) -> PyResult<Self> {
CircuitInstruction::new(
py,
operation.unwrap_or_else(|| self.operation.clone_ref(py)),
Some(qubits.unwrap_or_else(|| self.qubits.as_ref(py))),
Some(clbits.unwrap_or_else(|| self.clbits.as_ref(py))),
)
}
fn __getstate__(&self, py: Python<'_>) -> PyObject {
(
self.operation.as_ref(py),
self.qubits.as_ref(py),
self.clbits.as_ref(py),
)
.into_py(py)
}
fn __setstate__(&mut self, _py: Python<'_>, state: &PyTuple) -> PyResult<()> {
self.operation = state.get_item(0)?.extract()?;
self.qubits = state.get_item(1)?.extract()?;
self.clbits = state.get_item(2)?.extract()?;
Ok(())
}
pub fn __getnewargs__(&self, py: Python<'_>) -> PyResult<PyObject> {
Ok((
self.operation.as_ref(py),
self.qubits.as_ref(py),
self.clbits.as_ref(py),
)
.into_py(py))
}
pub fn __repr__(self_: &PyCell<Self>, py: Python<'_>) -> PyResult<String> {
let type_name = self_.get_type().name()?;
let r = self_.try_borrow()?;
Ok(format!(
"{}(\
operation={}\
, qubits={}\
, clbits={}\
)",
type_name,
r.operation.as_ref(py).repr()?,
r.qubits.as_ref(py).repr()?,
r.clbits.as_ref(py).repr()?
))
}
// Legacy tuple-like interface support.
//
// For a best attempt at API compatibility during the transition to using this new class, we need
// the interface to behave exactly like the old 3-tuple `(inst, qargs, cargs)` if it's treated
// like that via unpacking or similar. That means that the `parameters` field is completely
// absent, and the qubits and clbits must be converted to lists.
pub fn _legacy_format(&self, py: Python<'_>) -> PyObject {
PyTuple::new(
py,
[
self.operation.as_ref(py),
self.qubits.as_ref(py).to_list(),
self.clbits.as_ref(py).to_list(),
],
)
.into_py(py)
}
pub fn __getitem__(&self, py: Python<'_>, key: &PyAny) -> PyResult<PyObject> {
Ok(self
._legacy_format(py)
.as_ref(py)
.get_item(key)?
.into_py(py))
}
pub fn __iter__(&self, py: Python<'_>) -> PyResult<PyObject> {
Ok(self._legacy_format(py).as_ref(py).iter()?.into_py(py))
}
pub fn __len__(&self) -> usize {
3
}
pub fn __richcmp__(
self_: &PyCell<Self>,
other: &PyAny,
op: CompareOp,
py: Python<'_>,
) -> PyResult<PyObject> {
fn eq(
py: Python<'_>,
self_: &PyCell<CircuitInstruction>,
other: &PyAny,
) -> PyResult<Option<bool>> {
if self_.is(other) {
return Ok(Some(true));
}
let self_ = self_.try_borrow()?;
if other.is_instance_of::<CircuitInstruction>() {
let other: PyResult<&PyCell<CircuitInstruction>> = other.extract();
return other.map_or(Ok(Some(false)), |v| {
let v = v.try_borrow()?;
Ok(Some(
self_.clbits.as_ref(py).eq(v.clbits.as_ref(py))?
&& self_.qubits.as_ref(py).eq(v.qubits.as_ref(py))?
&& self_.operation.as_ref(py).eq(v.operation.as_ref(py))?,
))
});
}
if other.is_instance_of::<PyTuple>() {
return Ok(Some(self_._legacy_format(py).as_ref(py).eq(other)?));
}
Ok(None)
}
match op {
CompareOp::Eq => eq(py, self_, other).map(|r| {
r.map(|b| b.into_py(py))
.unwrap_or_else(|| py.NotImplemented())
}),
CompareOp::Ne => eq(py, self_, other).map(|r| {
r.map(|b| (!b).into_py(py))
.unwrap_or_else(|| py.NotImplemented())
}),
_ => Ok(py.NotImplemented()),
}
}
}

View File

@ -0,0 +1,71 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2023
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
use hashbrown::HashMap;
use pyo3::exceptions::PyRuntimeError;
use pyo3::PyResult;
use std::sync::Arc;
pub type IndexType = u32;
pub type BitType = u32;
/// A Rust-only data structure (not a pyclass!) for interning
/// `Vec<BitType>`.
///
/// Takes ownership of vectors given to [InternContext.intern]
/// and returns an [IndexType] index that can be used to look up
/// an _equivalent_ sequence by reference via [InternContext.lookup].
#[derive(Clone, Debug)]
pub struct InternContext {
slots: Vec<Arc<Vec<BitType>>>,
slot_lookup: HashMap<Arc<Vec<BitType>>, IndexType>,
}
impl InternContext {
pub fn new() -> Self {
InternContext {
slots: Vec::new(),
slot_lookup: HashMap::new(),
}
}
/// Takes `args` by reference and returns an index that can be used
/// to obtain a reference to an equivalent sequence of `BitType` by
/// calling [CircuitData.lookup].
pub fn intern(&mut self, args: Vec<BitType>) -> PyResult<IndexType> {
if let Some(slot_idx) = self.slot_lookup.get(&args) {
return Ok(*slot_idx);
}
let args = Arc::new(args);
let slot_idx: IndexType = self
.slots
.len()
.try_into()
.map_err(|_| PyRuntimeError::new_err("InternContext capacity exceeded!"))?;
self.slots.push(args.clone());
self.slot_lookup.insert_unique_unchecked(args, slot_idx);
Ok(slot_idx)
}
/// Returns the sequence corresponding to `slot_idx`, which must
/// be a value returned by [InternContext.intern].
pub fn lookup(&self, slot_idx: IndexType) -> &[BitType] {
self.slots.get(slot_idx as usize).unwrap()
}
}
impl Default for InternContext {
fn default() -> Self {
Self::new()
}
}

View File

@ -0,0 +1,25 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2023
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
pub mod circuit_data;
pub mod circuit_instruction;
pub mod intern_context;
mod py_ext;
use pyo3::prelude::*;
#[pymodule]
pub fn quantum_circuit(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<circuit_data::CircuitData>()?;
m.add_class::<circuit_instruction::CircuitInstruction>()?;
Ok(())
}

View File

@ -0,0 +1,45 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2023
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.
//! Contains helper functions for creating [Py<T>] (GIL-independent)
//! objects without creating an intermediate owned reference. These functions
//! are faster than PyO3's list and tuple factory methods when the caller
//! doesn't need to dereference the newly constructed object (i.e. if the
//! resulting [Py<T>] will simply be stored in a Rust struct).
//!
//! The reason this is faster is because PyO3 tracks owned references and
//! will perform deallocation when the active [GILPool] goes out of scope.
//! If we don't need to dereference the [Py<T>], then we can skip the
//! tracking and deallocation.
use pyo3::ffi::Py_ssize_t;
use pyo3::prelude::*;
use pyo3::types::{PyList, PyTuple};
use pyo3::{ffi, AsPyPointer, PyNativeType};
pub fn tuple_new(py: Python<'_>, items: Vec<PyObject>) -> Py<PyTuple> {
unsafe {
let ptr = ffi::PyTuple_New(items.len() as Py_ssize_t);
let tup: Py<PyTuple> = Py::from_owned_ptr(py, ptr);
for (i, obj) in items.into_iter().enumerate() {
ffi::PyTuple_SetItem(ptr, i as Py_ssize_t, obj.into_ptr());
}
tup
}
}
pub fn tuple_new_empty(py: Python<'_>) -> Py<PyTuple> {
unsafe { Py::from_owned_ptr(py, ffi::PyTuple_New(0)) }
}
pub fn tuple_from_list(list: &PyList) -> Py<PyTuple> {
unsafe { Py::from_owned_ptr(list.py(), ffi::PyList_AsTuple(list.as_ptr())) }
}

View File

@ -25,6 +25,7 @@ import qiskit._accelerate
# We manually define them on import so people can directly import qiskit._accelerate.* submodules
# and not have to rely on attribute access. No action needed for top-level extension packages.
sys.modules["qiskit._accelerate.nlayout"] = qiskit._accelerate.nlayout
sys.modules["qiskit._accelerate.quantum_circuit"] = qiskit._accelerate.quantum_circuit
sys.modules["qiskit._accelerate.stochastic_swap"] = qiskit._accelerate.stochastic_swap
sys.modules["qiskit._accelerate.sabre_swap"] = qiskit._accelerate.sabre_swap
sys.modules["qiskit._accelerate.sabre_layout"] = qiskit._accelerate.sabre_layout

View File

@ -448,7 +448,7 @@ class ElseContext:
raise CircuitError("Cannot attach an 'else' to a broadcasted 'if' block.")
appended = appended_instructions[0]
instruction = circuit._peek_previous_instruction_in_scope()
if appended is not instruction:
if appended.operation is not instruction.operation:
raise CircuitError(
"The 'if' block is not the most recent instruction in the circuit."
f" Expected to find: {appended!r}, but instead found: {instruction!r}."

View File

@ -15,6 +15,8 @@ Instruction collection.
"""
from __future__ import annotations
from collections.abc import MutableSequence
from typing import Callable
from qiskit.circuit.exceptions import CircuitError
@ -52,7 +54,9 @@ class InstructionSet:
used. It may throw an error if the resource is not valid for usage.
"""
self._instructions: list[CircuitInstruction] = []
self._instructions: list[
CircuitInstruction | (MutableSequence[CircuitInstruction], int)
] = []
self._requester = resource_requester
def __len__(self):
@ -61,7 +65,11 @@ class InstructionSet:
def __getitem__(self, i):
"""Return instruction at index"""
return self._instructions[i]
inst = self._instructions[i]
if isinstance(inst, CircuitInstruction):
return inst
data, idx = inst
return data[idx]
def add(self, instruction, qargs=None, cargs=None):
"""Add an instruction and its context (where it is attached)."""
@ -73,10 +81,22 @@ class InstructionSet:
instruction = CircuitInstruction(instruction, tuple(qargs), tuple(cargs))
self._instructions.append(instruction)
def _add_ref(self, data: MutableSequence[CircuitInstruction], pos: int):
"""Add a reference to an instruction and its context within a mutable sequence.
Updates to the instruction set will modify the specified sequence in place."""
self._instructions.append((data, pos))
def inverse(self):
"""Invert all instructions."""
for i, instruction in enumerate(self._instructions):
self._instructions[i] = instruction.replace(operation=instruction.operation.inverse())
if isinstance(instruction, CircuitInstruction):
self._instructions[i] = instruction.replace(
operation=instruction.operation.inverse()
)
else:
data, idx = instruction
instruction = data[idx]
data[idx] = instruction.replace(operation=instruction.operation.inverse())
return self
def c_if(self, classical: Clbit | ClassicalRegister | int, val: int) -> "InstructionSet":
@ -132,26 +152,40 @@ class InstructionSet:
if self._requester is not None:
classical = self._requester(classical)
for instruction in self._instructions:
instruction.operation = instruction.operation.c_if(classical, val)
if isinstance(instruction, CircuitInstruction):
updated = instruction.operation.c_if(classical, val)
if updated is not instruction.operation:
raise CircuitError(
"SingletonGate instances can only be added to InstructionSet via _add_ref"
)
else:
data, idx = instruction
instruction = data[idx]
data[idx] = instruction.replace(
operation=instruction.operation.c_if(classical, val)
)
return self
# Legacy support for properties. Added in Terra 0.21 to support the internal switch in
# `QuantumCircuit.data` from the 3-tuple to `CircuitInstruction`.
def _instructions_iter(self):
return (i if isinstance(i, CircuitInstruction) else i[0][i[1]] for i in self._instructions)
@property
def instructions(self):
"""Legacy getter for the instruction components of an instruction set. This does not
support mutation."""
return [instruction.operation for instruction in self._instructions]
return [instruction.operation for instruction in self._instructions_iter()]
@property
def qargs(self):
"""Legacy getter for the qargs components of an instruction set. This does not support
mutation."""
return [list(instruction.qubits) for instruction in self._instructions]
return [list(instruction.qubits) for instruction in self._instructions_iter()]
@property
def cargs(self):
"""Legacy getter for the cargs components of an instruction set. This does not support
mutation."""
return [list(instruction.clbits) for instruction in self._instructions]
return [list(instruction.clbits) for instruction in self._instructions_iter()]

View File

@ -15,6 +15,7 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from qiskit._accelerate.quantum_circuit import CircuitData
from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.parametertable import ParameterTable, ParameterView
@ -32,12 +33,13 @@ class BlueprintCircuit(QuantumCircuit, ABC):
def __init__(self, *regs, name: str | None = None) -> None:
"""Create a new blueprint circuit."""
self._is_initialized = False
super().__init__(*regs, name=name)
self._qregs: list[QuantumRegister] = []
self._cregs: list[ClassicalRegister] = []
self._qubits = []
self._qubit_indices = {}
self._is_built = False
self._is_initialized = True
@abstractmethod
def _check_configuration(self, raise_on_failure: bool = True) -> bool:
@ -65,7 +67,7 @@ class BlueprintCircuit(QuantumCircuit, ABC):
def _invalidate(self) -> None:
"""Invalidate the current circuit build."""
self._data = []
self._data = CircuitData(self._data.qubits, self._data.clbits)
self._parameter_table = ParameterTable()
self.global_phase = 0
self._is_built = False
@ -78,13 +80,19 @@ class BlueprintCircuit(QuantumCircuit, ABC):
@qregs.setter
def qregs(self, qregs):
"""Set the quantum registers associated with the circuit."""
if not self._is_initialized:
# Workaround to ignore calls from QuantumCircuit.__init__() which
# doesn't expect 'qregs' to be an overridden property!
return
self._qregs = []
self._qubits = []
self._ancillas = []
self._qubit_indices = {}
self._data = CircuitData(clbits=self._data.clbits)
self._parameter_table = ParameterTable()
self.global_phase = 0
self._is_built = False
self.add_register(*qregs)
self._invalidate()
@property
def data(self):

View File

@ -37,6 +37,7 @@ from typing import (
overload,
)
import numpy as np
from qiskit._accelerate.quantum_circuit import CircuitData
from qiskit.exceptions import QiskitError
from qiskit.utils.multiprocessing import is_main_process
from qiskit.circuit.instruction import Instruction
@ -227,9 +228,6 @@ class QuantumCircuit:
self.name = name
self._increment_instances()
# Data contains a list of instructions and their contexts,
# in the order they were applied.
self._data: list[CircuitInstruction] = []
self._op_start_times = None
# A stack to hold the instruction sets that are being built up during for-, if- and
@ -244,8 +242,6 @@ class QuantumCircuit:
self.qregs: list[QuantumRegister] = []
self.cregs: list[ClassicalRegister] = []
self._qubits: list[Qubit] = []
self._clbits: list[Clbit] = []
# Dict mapping Qubit or Clbit instances to tuple comprised of 0) the
# corresponding index in circuit.{qubits,clbits} and 1) a list of
@ -254,6 +250,10 @@ class QuantumCircuit:
self._qubit_indices: dict[Qubit, BitLocations] = {}
self._clbit_indices: dict[Clbit, BitLocations] = {}
# Data contains a list of instructions and their contexts,
# in the order they were applied.
self._data: CircuitData = CircuitData()
self._ancillas: list[AncillaQubit] = []
self._calibrations: DefaultDict[str, dict[tuple, Any]] = defaultdict(dict)
self.add_register(*regs)
@ -384,8 +384,11 @@ class QuantumCircuit:
"""
# If data_input is QuantumCircuitData(self), clearing self._data
# below will also empty data_input, so make a shallow copy first.
data_input = list(data_input)
self._data = []
if isinstance(data_input, CircuitData):
data_input = data_input.copy()
else:
data_input = list(data_input)
self._data.clear()
self._parameter_table = ParameterTable()
if not data_input:
return
@ -499,6 +502,28 @@ class QuantumCircuit:
other, copy_operations=False
)
def __deepcopy__(self, memo=None):
# This is overridden to minimize memory pressure when we don't
# actually need to pickle (i.e. the typical deepcopy case).
# Note:
# This is done here instead of in CircuitData since PyO3
# doesn't include a native way to recursively call
# copy.deepcopy(memo).
cls = self.__class__
result = cls.__new__(cls)
for k in self.__dict__.keys() - {"_data"}:
setattr(result, k, copy.deepcopy(self.__dict__[k], memo))
# Avoids pulling self._data into a Python list
# like we would when pickling.
result._data = CircuitData(
copy.deepcopy(self._data.qubits, memo),
copy.deepcopy(self._data.clbits, memo),
(i.replace(operation=copy.deepcopy(i.operation, memo)) for i in self._data),
reserve=len(self._data),
)
return result
@classmethod
def _increment_instances(cls):
cls.instances += 1
@ -903,7 +928,7 @@ class QuantumCircuit:
clbits = self.clbits[: other.num_clbits]
if front:
# Need to keep a reference to the data for use after we've emptied it.
old_data = list(dest.data)
old_data = dest._data.copy()
dest.clear()
dest.append(other, qubits, clbits)
for instruction in old_data:
@ -946,7 +971,7 @@ class QuantumCircuit:
variable_mapper = _classical_resource_map.VariableMapper(
dest.cregs, edge_map, dest.add_register
)
mapped_instrs: list[CircuitInstruction] = []
mapped_instrs: CircuitData = CircuitData(dest.qubits, dest.clbits, reserve=len(other.data))
for instr in other.data:
n_qargs: list[Qubit] = [edge_map[qarg] for qarg in instr.qubits]
n_cargs: list[Clbit] = [edge_map[carg] for carg in instr.clbits]
@ -959,7 +984,7 @@ class QuantumCircuit:
if front:
# adjust new instrs before original ones and update all parameters
mapped_instrs += dest.data
mapped_instrs.extend(dest._data)
dest.clear()
append = dest._control_flow_scopes[-1].append if dest._control_flow_scopes else dest._append
for instr in mapped_instrs:
@ -1070,14 +1095,14 @@ class QuantumCircuit:
"""
Returns a list of quantum bits in the order that the registers were added.
"""
return self._qubits
return self._data.qubits
@property
def clbits(self) -> list[Clbit]:
"""
Returns a list of classical bits in the order that the registers were added.
"""
return self._clbits
return self._data.clbits
@property
def ancillas(self) -> list[AncillaQubit]:
@ -1193,7 +1218,7 @@ class QuantumCircuit:
return specifier
if isinstance(specifier, int):
try:
return self._clbits[specifier]
return self._data.clbits[specifier]
except IndexError:
raise CircuitError(f"Classical bit index {specifier} is out-of-range.") from None
raise CircuitError(f"Unknown classical resource specifier: '{specifier}'.")
@ -1272,27 +1297,29 @@ class QuantumCircuit:
expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []]
if self._control_flow_scopes:
circuit_data = self._control_flow_scopes[-1].instructions
appender = self._control_flow_scopes[-1].append
requester = self._control_flow_scopes[-1].request_classical_resource
else:
circuit_data = self._data
appender = self._append
requester = self._resolve_classical_resource
instructions = InstructionSet(resource_requester=requester)
if isinstance(operation, Instruction):
for qarg, carg in operation.broadcast_arguments(expanded_qargs, expanded_cargs):
self._check_dups(qarg)
instruction = CircuitInstruction(operation, qarg, carg)
appender(instruction)
instructions.add(instruction)
data_idx = len(circuit_data)
appender(CircuitInstruction(operation, qarg, carg))
instructions._add_ref(circuit_data, data_idx)
else:
# For Operations that are non-Instructions, we use the Instruction's default method
for qarg, carg in Instruction.broadcast_arguments(
operation, expanded_qargs, expanded_cargs
):
self._check_dups(qarg)
instruction = CircuitInstruction(operation, qarg, carg)
appender(instruction)
instructions.add(instruction)
data_idx = len(circuit_data)
appender(CircuitInstruction(operation, qarg, carg))
instructions._add_ref(circuit_data, data_idx)
return instructions
# Preferred new style.
@ -1429,9 +1456,9 @@ class QuantumCircuit:
if bit in self._qubit_indices:
self._qubit_indices[bit].registers.append((register, idx))
else:
self._qubits.append(bit)
self._data.add_qubit(bit)
self._qubit_indices[bit] = BitLocations(
len(self._qubits) - 1, [(register, idx)]
len(self._data.qubits) - 1, [(register, idx)]
)
elif isinstance(register, ClassicalRegister):
@ -1441,9 +1468,9 @@ class QuantumCircuit:
if bit in self._clbit_indices:
self._clbit_indices[bit].registers.append((register, idx))
else:
self._clbits.append(bit)
self._data.add_clbit(bit)
self._clbit_indices[bit] = BitLocations(
len(self._clbits) - 1, [(register, idx)]
len(self._data.clbits) - 1, [(register, idx)]
)
elif isinstance(register, list):
@ -1461,11 +1488,11 @@ class QuantumCircuit:
if isinstance(bit, AncillaQubit):
self._ancillas.append(bit)
if isinstance(bit, Qubit):
self._qubits.append(bit)
self._qubit_indices[bit] = BitLocations(len(self._qubits) - 1, [])
self._data.add_qubit(bit)
self._qubit_indices[bit] = BitLocations(len(self._data.qubits) - 1, [])
elif isinstance(bit, Clbit):
self._clbits.append(bit)
self._clbit_indices[bit] = BitLocations(len(self._clbits) - 1, [])
self._data.add_clbit(bit)
self._clbit_indices[bit] = BitLocations(len(self._data.clbits) - 1, [])
else:
raise CircuitError(
"Expected an instance of Qubit, Clbit, or "
@ -2094,10 +2121,11 @@ class QuantumCircuit:
}
)
cpy._data = [
cpy._data.reserve(len(self._data))
cpy._data.extend(
instruction.replace(operation=operation_copies[id(instruction.operation)])
for instruction in self._data
]
)
return cpy
@ -2123,14 +2151,12 @@ class QuantumCircuit:
# copy registers correctly, in copy.copy they are only copied via reference
cpy.qregs = self.qregs.copy()
cpy.cregs = self.cregs.copy()
cpy._qubits = self._qubits.copy()
cpy._ancillas = self._ancillas.copy()
cpy._clbits = self._clbits.copy()
cpy._qubit_indices = self._qubit_indices.copy()
cpy._clbit_indices = self._clbit_indices.copy()
cpy._parameter_table = ParameterTable()
cpy._data = []
cpy._data = CircuitData(self._data.qubits, self._data.clbits)
cpy._calibrations = copy.deepcopy(self._calibrations)
cpy._metadata = copy.deepcopy(self._metadata)
@ -2367,13 +2393,16 @@ class QuantumCircuit:
# Filter only cregs/clbits still in new DAG, preserving original circuit order
cregs_to_add = [creg for creg in circ.cregs if creg in kept_cregs]
clbits_to_add = [clbit for clbit in circ._clbits if clbit in kept_clbits]
clbits_to_add = [clbit for clbit in circ._data.clbits if clbit in kept_clbits]
# Clear cregs and clbits
circ.cregs = []
circ._clbits = []
circ._clbit_indices = {}
# Clear instruction info
circ._data = CircuitData(qubits=circ._data.qubits, reserve=len(circ._data))
circ._parameter_table.clear()
# We must add the clbits first to preserve the original circuit
# order. This way, add_register never adds clbits and just
# creates registers that point to them.
@ -2381,10 +2410,6 @@ class QuantumCircuit:
for creg in cregs_to_add:
circ.add_register(creg)
# Clear instruction info
circ.data.clear()
circ._parameter_table.clear()
# Set circ instructions to match the new DAG
for node in new_dag.topological_op_nodes():
# Get arguments for classical condition (if any)

View File

@ -14,131 +14,15 @@
QuantumCircuit.data while maintaining the interface of a python list."""
from collections.abc import MutableSequence
from typing import Tuple, Iterable, Optional
import qiskit._accelerate.quantum_circuit
from .exceptions import CircuitError
from .instruction import Instruction
from .operation import Operation
from .quantumregister import Qubit
from .classicalregister import Clbit
class CircuitInstruction:
"""A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and
various operands.
.. note::
There is some possible confusion in the names of this class, :class:`~.circuit.Instruction`,
and :class:`~.circuit.Operation`, and this class's attribute :attr:`operation`. Our
preferred terminology is by analogy to assembly languages, where an "instruction" is made up
of an "operation" and its "operands".
Historically, :class:`~.circuit.Instruction` came first, and originally contained the qubits
it operated on and any parameters, so it was a true "instruction". Over time,
:class:`.QuantumCircuit` became responsible for tracking qubits and clbits, and the class
became better described as an "operation". Changing the name of such a core object would be
a very unpleasant API break for users, and so we have stuck with it.
This class was created to provide a formal "instruction" context object in
:class:`.QuantumCircuit.data`, which had long been made of ad-hoc tuples. With this, and
the advent of the :class:`~.circuit.Operation` interface for adding more complex objects to
circuits, we took the opportunity to correct the historical naming. For the time being,
this leads to an awkward case where :attr:`.CircuitInstruction.operation` is often an
:class:`~.circuit.Instruction` instance (:class:`~.circuit.Instruction` implements the
:class:`.Operation` interface), but as the :class:`.Operation` interface gains more use,
this confusion will hopefully abate.
.. warning::
This is a lightweight internal class and there is minimal error checking; you must respect
the type hints when using it. It is the user's responsibility to ensure that direct
mutations of the object do not invalidate the types, nor the restrictions placed on it by
its context. Typically this will mean, for example, that :attr:`qubits` must be a sequence
of distinct items, with no duplicates.
"""
__slots__ = ("operation", "qubits", "clbits")
operation: Operation
"""The logical operation that this instruction represents an execution of."""
qubits: Tuple[Qubit, ...]
"""A sequence of the qubits that the operation is applied to."""
clbits: Tuple[Clbit, ...]
"""A sequence of the classical bits that this operation reads from or writes to."""
def __init__(
self,
operation: Operation,
qubits: Iterable[Qubit] = (),
clbits: Iterable[Clbit] = (),
):
self.operation = operation
self.qubits = tuple(qubits)
self.clbits = tuple(clbits)
def copy(self) -> "CircuitInstruction":
"""Return a shallow copy of the :class:`CircuitInstruction`."""
return self.__class__(
operation=self.operation,
qubits=self.qubits,
clbits=self.clbits,
)
def replace(
self,
operation: Optional[Operation] = None,
qubits: Optional[Iterable[Qubit]] = None,
clbits: Optional[Iterable[Clbit]] = None,
) -> "CircuitInstruction":
"""Return a new :class:`CircuitInstruction` with the given fields replaced."""
return self.__class__(
operation=self.operation if operation is None else operation,
qubits=self.qubits if qubits is None else qubits,
clbits=self.clbits if clbits is None else clbits,
)
def __repr__(self):
return (
f"{type(self).__name__}("
f"operation={self.operation!r}"
f", qubits={self.qubits!r}"
f", clbits={self.clbits!r}"
")"
)
def __eq__(self, other):
if isinstance(other, type(self)):
# Ordered from fastest comparisons to slowest.
return (
self.clbits == other.clbits
and self.qubits == other.qubits
and self.operation == other.operation
)
if isinstance(other, tuple):
return self._legacy_format() == other
return NotImplemented
# Legacy tuple-like interface support.
#
# For a best attempt at API compatibility during the transition to using this new class, we need
# the interface to behave exactly like the old 3-tuple `(inst, qargs, cargs)` if it's treated
# like that via unpacking or similar. That means that the `parameters` field is completely
# absent, and the qubits and clbits must be converted to lists.
def _legacy_format(self):
# The qubits and clbits were generally stored as lists in the old format, and various
# places assume that they will certainly be lists.
return (self.operation, list(self.qubits), list(self.clbits))
def __getitem__(self, key):
return self._legacy_format()[key]
def __iter__(self):
return iter(self._legacy_format())
def __len__(self):
return 3
CircuitInstruction = qiskit._accelerate.quantum_circuit.CircuitInstruction
class QuantumCircuitData(MutableSequence):
@ -192,7 +76,7 @@ class QuantumCircuitData(MutableSequence):
return CircuitInstruction(operation, tuple(qargs), tuple(cargs))
def insert(self, index, value):
self._circuit._data.insert(index, None)
self._circuit._data.insert(index, CircuitInstruction(None, (), ()))
try:
self[index] = value
except CircuitError:
@ -209,42 +93,46 @@ class QuantumCircuitData(MutableSequence):
return len(self._circuit._data)
def __cast(self, other):
return other._circuit._data if isinstance(other, QuantumCircuitData) else other
return list(other._circuit._data) if isinstance(other, QuantumCircuitData) else other
def __repr__(self):
return repr(self._circuit._data)
return repr(list(self._circuit._data))
def __lt__(self, other):
return self._circuit._data < self.__cast(other)
return list(self._circuit._data) < self.__cast(other)
def __le__(self, other):
return self._circuit._data <= self.__cast(other)
return list(self._circuit._data) <= self.__cast(other)
def __eq__(self, other):
return self._circuit._data == self.__cast(other)
def __gt__(self, other):
return self._circuit._data > self.__cast(other)
return list(self._circuit._data) > self.__cast(other)
def __ge__(self, other):
return self._circuit._data >= self.__cast(other)
return list(self._circuit._data) >= self.__cast(other)
def __add__(self, other):
return self._circuit._data + self.__cast(other)
return list(self._circuit._data) + self.__cast(other)
def __radd__(self, other):
return self.__cast(other) + self._circuit._data
return self.__cast(other) + list(self._circuit._data)
def __mul__(self, n):
return self._circuit._data * n
return list(self._circuit._data) * n
def __rmul__(self, n):
return n * self._circuit._data
return n * list(self._circuit._data)
def sort(self, *args, **kwargs):
"""In-place stable sort. Accepts arguments of list.sort."""
self._circuit._data.sort(*args, **kwargs)
data = list(self._circuit._data)
data.sort(*args, **kwargs)
self._circuit._data.clear()
self._circuit._data.reserve(len(data))
self._circuit._data.extend(data)
def copy(self):
"""Returns a shallow copy of instruction list."""
return self._circuit._data.copy()
return list(self._circuit._data)

View File

@ -100,32 +100,29 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None
qubit_map = {bit: q[idx] for idx, bit in enumerate(circuit.qubits)}
clbit_map = {bit: c[idx] for idx, bit in enumerate(circuit.clbits)}
definition = [
instruction.replace(
qc = QuantumCircuit(*regs, name=out_instruction.name)
qc._data.reserve(len(target.data))
for instruction in target._data:
rule = instruction.replace(
qubits=[qubit_map[y] for y in instruction.qubits],
clbits=[clbit_map[y] for y in instruction.clbits],
)
for instruction in target.data
]
# fix condition
for rule in definition:
# fix condition
condition = getattr(rule.operation, "condition", None)
if condition:
reg, val = condition
if isinstance(reg, Clbit):
rule.operation = rule.operation.c_if(clbit_map[reg], val)
rule = rule.replace(operation=rule.operation.c_if(clbit_map[reg], val))
elif reg.size == c.size:
rule.operation = rule.operation.c_if(c, val)
rule = rule.replace(operation=rule.operation.c_if(c, val))
else:
raise QiskitError(
"Cannot convert condition in circuit with "
"multiple classical registers to instruction"
)
qc._append(rule)
qc = QuantumCircuit(*regs, name=out_instruction.name)
for instruction in definition:
qc._append(instruction)
if circuit.global_phase:
qc.global_phase = circuit.global_phase

View File

@ -533,7 +533,8 @@ class LinComb(CircuitGradient):
qr_superpos_qubits = tuple(qr_superpos)
# copy the input circuit taking the gates by reference
out = QuantumCircuit(*circuit.qregs)
out._data = circuit._data.copy()
out._data.reserve(len(circuit._data))
out._data.extend(circuit._data)
out._parameter_table = ParameterTable(
{param: values.copy() for param, values in circuit._parameter_table.items()}
)

View File

@ -0,0 +1,17 @@
---
upgrade:
- |
To support a more compact in-memory representation, the
:class:`.QuantumCircuit` class is now limited to supporting
a maximum of ``2^32 (=4,294,967,296)`` qubits and clbits,
for each of these two bit types (the limit is not combined).
The number of unique sequences of indices used in
:attr:`.CircuitInstruction.qubits` and
:attr:`.CircuitInstruction.clbits` is also limited to ``2^32``
for instructions added to a single circuit.
other:
- |
The :class:`.QuantumCircuit` class now performs interning for the
``qubits`` and ``clbits`` of the :class:`.CircuitInstruction`
instances that it stores, resulting in a potentially significant
reduction in memory footprint, especially for large circuits.

View File

@ -11,14 +11,178 @@
# that they have been altered from the originals.
"""Test operations on circuit.data."""
import ddt
from qiskit._accelerate.quantum_circuit import CircuitData
from qiskit.circuit import QuantumCircuit, QuantumRegister, Parameter, CircuitInstruction, Operation
from qiskit.circuit import (
ClassicalRegister,
QuantumCircuit,
QuantumRegister,
Parameter,
CircuitInstruction,
Operation,
Qubit,
)
from qiskit.circuit.library import HGate, XGate, CXGate, RXGate
from qiskit.test import QiskitTestCase
from qiskit.circuit.exceptions import CircuitError
@ddt.ddt
class TestQuantumCircuitData(QiskitTestCase):
"""CircuitData (Rust) operation tests."""
@ddt.data(
slice(0, 5, 1), # Get everything.
slice(-1, -6, -1), # Get everything, reversed.
slice(0, 4, 1), # Get subslice.
slice(0, 5, 2), # Get every other.
slice(-1, -6, -2), # Get every other, reversed.
slice(2, 2, 1), # Get nothing.
slice(2, 3, 1), # Get at index 2.
slice(4, 10, 1), # Get index 4 to end, using excessive upper bound.
slice(5, 0, -2), # Get every other, reversed, excluding index 0.
slice(-10, -5, 1), # Get nothing.
slice(0, 10, 1), # Get everything.
)
def test_getitem_slice(self, sli):
"""Test that __getitem__ with slice is equivalent to that of list."""
qr = QuantumRegister(5)
data_list = [
CircuitInstruction(XGate(), [qr[0]], []),
CircuitInstruction(XGate(), [qr[1]], []),
CircuitInstruction(XGate(), [qr[2]], []),
CircuitInstruction(XGate(), [qr[3]], []),
CircuitInstruction(XGate(), [qr[4]], []),
]
data = CircuitData(qubits=list(qr), data=data_list)
self.assertEqual(data[sli], data_list[sli])
@ddt.data(
slice(0, 5, 1), # Delete everything.
slice(-1, -6, -1), # Delete everything, reversed.
slice(0, 4, 1), # Delete subslice.
slice(0, 5, 2), # Delete every other.
slice(-1, -6, -2), # Delete every other, reversed.
slice(2, 2, 1), # Delete nothing.
slice(2, 3, 1), # Delete at index 2.
slice(4, 10, 1), # Delete index 4 to end, excessive upper bound.
slice(5, 0, -2), # Delete every other, reversed, excluding index 0.
slice(-10, -5, 1), # Delete nothing.
slice(0, 10, 1), # Delete everything, excessive upper bound.
)
def test_delitem_slice(self, sli):
"""Test that __delitem__ with slice is equivalent to that of list."""
qr = QuantumRegister(5)
data_list = [
CircuitInstruction(XGate(), [qr[0]], []),
CircuitInstruction(XGate(), [qr[1]], []),
CircuitInstruction(XGate(), [qr[2]], []),
CircuitInstruction(XGate(), [qr[3]], []),
CircuitInstruction(XGate(), [qr[4]], []),
]
data = CircuitData(qubits=list(qr), data=data_list)
del data_list[sli]
del data[sli]
if data_list[sli] != data[sli]:
print(f"data_list: {data_list}")
print(f"data: {list(data)}")
self.assertEqual(data[sli], data_list[sli])
@ddt.data(
(slice(0, 5, 1), 5), # Replace entire slice.
(slice(-1, -6, -1), 5), # Replace entire slice, reversed.
(slice(0, 4, 1), 4), # Replace subslice.
(slice(0, 4, 1), 10), # Replace subslice with bigger sequence.
(slice(0, 5, 2), 3), # Replace every other.
(slice(-1, -6, -2), 3), # Replace every other, reversed.
(slice(2, 2, 1), 1), # Insert at index 2.
(slice(2, 3, 1), 1), # Replace at index 2.
(slice(2, 3, 1), 10), # Replace at index 2 with bigger sequence.
(slice(4, 10, 1), 2), # Replace index 4 with bigger sequence, excessive upper bound.
(slice(5, 10, 1), 10), # Append sequence.
(slice(4, 0, -1), 4), # Replace subslice at end, reversed.
)
@ddt.unpack
def test_setitem_slice(self, sli, value_length):
"""Test that __setitem__ with slice is equivalent to that of list."""
reg_size = 20
assert value_length <= reg_size
qr = QuantumRegister(reg_size)
default_bit = Qubit()
data_list = [
CircuitInstruction(XGate(), [default_bit], []),
CircuitInstruction(XGate(), [default_bit], []),
CircuitInstruction(XGate(), [default_bit], []),
CircuitInstruction(XGate(), [default_bit], []),
CircuitInstruction(XGate(), [default_bit], []),
]
data = CircuitData(qubits=list(qr) + [default_bit], data=data_list)
value = [CircuitInstruction(XGate(), [qr[i]]) for i in range(value_length)]
data_list[sli] = value
data[sli] = value
self.assertEqual(data, data_list)
@ddt.data(
(slice(0, 5, 2), 2), # Replace smaller, with gaps.
(slice(0, 5, 2), 4), # Replace larger, with gaps.
(slice(4, 0, -1), 10), # Replace larger, reversed.
(slice(-1, -6, -1), 6), # Replace larger, reversed, negative notation.
(slice(4, 3, -1), 10), # Replace at index 4 with bigger sequence, reversed.
)
@ddt.unpack
def test_setitem_slice_negative(self, sli, value_length):
"""Test that __setitem__ with slice is equivalent to that of list."""
reg_size = 20
assert value_length <= reg_size
qr = QuantumRegister(reg_size)
default_bit = Qubit()
data_list = [
CircuitInstruction(XGate(), [default_bit], []),
CircuitInstruction(XGate(), [default_bit], []),
CircuitInstruction(XGate(), [default_bit], []),
CircuitInstruction(XGate(), [default_bit], []),
CircuitInstruction(XGate(), [default_bit], []),
]
data = CircuitData(qubits=list(qr) + [default_bit], data=data_list)
value = [CircuitInstruction(XGate(), [qr[i]]) for i in range(value_length)]
with self.assertRaises(ValueError):
data_list[sli] = value
with self.assertRaises(ValueError):
data[sli] = value
self.assertEqual(data, data_list)
def test_unregistered_bit_error_new(self):
"""Test using foreign bits is not allowed."""
qr = QuantumRegister(1)
cr = ClassicalRegister(1)
with self.assertRaisesRegex(KeyError, "not been added to this circuit"):
CircuitData(qr, cr, [CircuitInstruction(XGate(), [Qubit()], [])])
def test_unregistered_bit_error_append(self):
"""Test using foreign bits is not allowed."""
qr = QuantumRegister(1)
cr = ClassicalRegister(1)
data = CircuitData(qr, cr)
with self.assertRaisesRegex(KeyError, "not been added to this circuit"):
qr_foreign = QuantumRegister(1)
data.append(CircuitInstruction(XGate(), [qr_foreign[0]], []))
def test_unregistered_bit_error_set(self):
"""Test using foreign bits is not allowed."""
qr = QuantumRegister(1)
cr = ClassicalRegister(1)
data = CircuitData(qr, cr, [CircuitInstruction(XGate(), [qr[0]], [])])
with self.assertRaisesRegex(KeyError, "not been added to this circuit"):
qr_foreign = QuantumRegister(1)
data[0] = CircuitInstruction(XGate(), [qr_foreign[0]], [])
class TestQuantumCircuitInstructionData(QiskitTestCase):
"""QuantumCircuit.data operation tests."""

View File

@ -1308,11 +1308,11 @@ class TestCircuitPrivateOperations(QiskitTestCase):
x, y = Parameter("x"), Parameter("y")
test = QuantumCircuit(1, 1)
test.rx(y, 0)
last_instructions = test.u(x, y, 0, 0)
last_instructions = list(test.u(x, y, 0, 0))
self.assertEqual({x, y}, set(test.parameters))
instruction = test._pop_previous_instruction_in_scope()
self.assertEqual(list(last_instructions), [instruction])
self.assertEqual(last_instructions, [instruction])
self.assertEqual({y}, set(test.parameters))
def test_decompose_gate_type(self):