mirror of https://github.com/Qiskit/qiskit.git
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:
parent
45efe668eb
commit
f3857f18d0
|
@ -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))?;
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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())) }
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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}."
|
||||
|
|
|
@ -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()]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()}
|
||||
)
|
||||
|
|
|
@ -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.
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue