diff --git a/Cargo.lock b/Cargo.lock index 68a3d3214060..6ce26b3baea5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,16 +548,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.2.6" @@ -906,7 +896,7 @@ checksum = "a8c3d637a7db9ddb3811719db8a466bd4960ea668df4b2d14043a1b0038465b0" dependencies = [ "cov-mark", "either", - "indexmap 2.2.6", + "indexmap", "itertools 0.10.5", "once_cell", "oq3_lexer", @@ -996,12 +986,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap", ] [[package]] @@ -1018,12 +1008,13 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "priority-queue" -version = "1.4.0" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bda9164fe05bc9225752d54aae413343c36f684380005398a6a8fde95fe785" +checksum = "70c501afe3a2e25c9bd219aa56ec1e04cdb3fcdd763055be268778c13fa82c1f" dependencies = [ "autocfg", - "indexmap 1.9.3", + "equivalent", + "indexmap", ] [[package]] @@ -1104,7 +1095,7 @@ checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" dependencies = [ "cfg-if", "hashbrown 0.14.5", - "indexmap 2.2.6", + "indexmap", "indoc", "libc", "memoffset", @@ -1173,7 +1164,7 @@ dependencies = [ "faer", "faer-ext", "hashbrown 0.14.5", - "indexmap 2.2.6", + "indexmap", "itertools 0.13.0", "ndarray", "num-bigint", @@ -1189,6 +1180,7 @@ dependencies = [ "rayon", "rustworkx-core", "smallvec", + "thiserror", ] [[package]] @@ -1201,6 +1193,7 @@ dependencies = [ "numpy", "pyo3", "smallvec", + "thiserror", ] [[package]] @@ -1228,7 +1221,7 @@ name = "qiskit-qasm3" version = "1.2.0" dependencies = [ "hashbrown 0.14.5", - "indexmap 2.2.6", + "indexmap", "oq3_semantics", "pyo3", ] @@ -1399,14 +1392,15 @@ checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "rustworkx-core" -version = "0.14.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529027dfaa8125aa61bb7736ae9484f41e8544f448af96918c8da6b1def7f57b" +checksum = "c2b9aa5926b35dd3029530aef27eac0926b544c78f8e8f1aad4d37854b132fe9" dependencies = [ "ahash 0.8.11", "fixedbitset", "hashbrown 0.14.5", - "indexmap 2.2.6", + "indexmap", + "ndarray", "num-traits", "petgraph", "priority-queue", diff --git a/Cargo.toml b/Cargo.toml index 13f43cfabcdc..a6ccf60f7f4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ num-complex = "0.4" ndarray = "^0.15.6" numpy = "0.21.0" smallvec = "1.13" +thiserror = "1.0" # Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an # actual C extension (the feature disables linking in `libpython`, which is forbidden in Python diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index b377a9b38a6d..9d6024783996 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -19,10 +19,11 @@ ahash = "0.8.11" num-traits = "0.2" num-complex.workspace = true num-bigint = "0.4" -rustworkx-core = "0.14" +rustworkx-core = "0.15" faer = "0.19.1" itertools = "0.13.0" qiskit-circuit.workspace = true +thiserror.workspace = true [dependencies.smallvec] workspace = true diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 9f10f76de467..01725269bb84 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -21,9 +21,9 @@ use std::f64::consts::PI; use std::ops::Deref; use std::str::FromStr; -use pyo3::exceptions::{PyIndexError, PyValueError}; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::PyString; +use pyo3::types::{PyList, PyString}; use pyo3::wrap_pyfunction; use pyo3::Python; @@ -31,8 +31,8 @@ use ndarray::prelude::*; use numpy::PyReadonlyArray2; use pyo3::pybacked::PyBackedStr; +use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::c64; -use qiskit_circuit::SliceOrInt; pub const ANGLE_ZERO_EPSILON: f64 = 1e-12; @@ -97,46 +97,15 @@ impl OneQubitGateSequence { Ok(self.gates.len()) } - fn __getitem__(&self, py: Python, idx: SliceOrInt) -> PyResult { - match idx { - SliceOrInt::Slice(slc) => { - let len = self.gates.len().try_into().unwrap(); - let indices = slc.indices(len)?; - let mut out_vec: Vec<(String, SmallVec<[f64; 3]>)> = Vec::new(); - // Start and stop will always be positive the slice api converts - // negatives to the index for example: - // list(range(5))[-1:-3:-1] - // will return start=4, stop=2, and step=-1 - let mut pos: isize = indices.start; - let mut cond = if indices.step < 0 { - pos > indices.stop - } else { - pos < indices.stop - }; - while cond { - if pos < len as isize { - out_vec.push(self.gates[pos as usize].clone()); - } - pos += indices.step; - if indices.step < 0 { - cond = pos > indices.stop; - } else { - cond = pos < indices.stop; - } - } - Ok(out_vec.into_py(py)) - } - SliceOrInt::Int(idx) => { - let len = self.gates.len() as isize; - if idx >= len || idx < -len { - Err(PyIndexError::new_err(format!("Invalid index, {idx}"))) - } else if idx < 0 { - let len = self.gates.len(); - Ok(self.gates[len - idx.unsigned_abs()].to_object(py)) - } else { - Ok(self.gates[idx as usize].to_object(py)) - } - } + fn __getitem__(&self, py: Python, idx: PySequenceIndex) -> PyResult { + match idx.with_len(self.gates.len())? { + SequenceIndex::Int(idx) => Ok(self.gates[idx].to_object(py)), + indices => Ok(PyList::new_bound( + py, + indices.iter().map(|pos| self.gates[pos].to_object(py)), + ) + .into_any() + .unbind()), } } } diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 8637cb03c735..37061d5159f4 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -21,10 +21,6 @@ use approx::{abs_diff_eq, relative_eq}; use num_complex::{Complex, Complex64, ComplexFloat}; use num_traits::Zero; -use pyo3::exceptions::{PyIndexError, PyValueError}; -use pyo3::prelude::*; -use pyo3::wrap_pyfunction; -use pyo3::Python; use smallvec::{smallvec, SmallVec}; use std::f64::consts::{FRAC_1_SQRT_2, PI}; use std::ops::Deref; @@ -37,7 +33,11 @@ use ndarray::prelude::*; use ndarray::Zip; use numpy::PyReadonlyArray2; use numpy::{IntoPyArray, ToPyArray}; + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; +use pyo3::types::PyList; use crate::convert_2q_block_matrix::change_basis; use crate::euler_one_qubit_decomposer::{ @@ -52,8 +52,8 @@ use rand_distr::StandardNormal; use rand_pcg::Pcg64Mcg; use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; +use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::{c64, GateArray1Q, GateArray2Q, C_M_ONE, C_ONE, C_ZERO, IM, M_IM}; -use qiskit_circuit::SliceOrInt; const PI2: f64 = PI / 2.; const PI4: f64 = PI / 4.; @@ -1131,46 +1131,15 @@ impl TwoQubitGateSequence { Ok(self.gates.len()) } - fn __getitem__(&self, py: Python, idx: SliceOrInt) -> PyResult { - match idx { - SliceOrInt::Slice(slc) => { - let len = self.gates.len().try_into().unwrap(); - let indices = slc.indices(len)?; - let mut out_vec: TwoQubitSequenceVec = Vec::new(); - // Start and stop will always be positive the slice api converts - // negatives to the index for example: - // list(range(5))[-1:-3:-1] - // will return start=4, stop=2, and step=- - let mut pos: isize = indices.start; - let mut cond = if indices.step < 0 { - pos > indices.stop - } else { - pos < indices.stop - }; - while cond { - if pos < len as isize { - out_vec.push(self.gates[pos as usize].clone()); - } - pos += indices.step; - if indices.step < 0 { - cond = pos > indices.stop; - } else { - cond = pos < indices.stop; - } - } - Ok(out_vec.into_py(py)) - } - SliceOrInt::Int(idx) => { - let len = self.gates.len() as isize; - if idx >= len || idx < -len { - Err(PyIndexError::new_err(format!("Invalid index, {idx}"))) - } else if idx < 0 { - let len = self.gates.len(); - Ok(self.gates[len - idx.unsigned_abs()].to_object(py)) - } else { - Ok(self.gates[idx as usize].to_object(py)) - } - } + fn __getitem__(&self, py: Python, idx: PySequenceIndex) -> PyResult { + match idx.with_len(self.gates.len())? { + SequenceIndex::Int(idx) => Ok(self.gates[idx].to_object(py)), + indices => Ok(PyList::new_bound( + py, + indices.iter().map(|pos| self.gates[pos].to_object(py)), + ) + .into_any() + .unbind()), } } } diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index dd7e878537d9..50160c7bac17 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -14,6 +14,7 @@ hashbrown.workspace = true num-complex.workspace = true ndarray.workspace = true numpy.workspace = true +thiserror.workspace = true [dependencies.pyo3] workspace = true diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 07f4579a4cd3..10e0691021a1 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -22,11 +22,12 @@ use crate::imports::{BUILTIN_LIST, QUBIT}; use crate::interner::{IndexedInterner, Interner, InternerKey}; use crate::operations::{Operation, OperationType, Param, StandardGate}; use crate::parameter_table::{ParamEntry, ParamTable, GLOBAL_PHASE_INDEX}; -use crate::{Clbit, Qubit, SliceOrInt}; +use crate::slice::{PySequenceIndex, SequenceIndex}; +use crate::{Clbit, Qubit}; use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; -use pyo3::types::{PyList, PySet, PySlice, PyTuple, PyType}; +use pyo3::types::{PyList, PySet, PyTuple, PyType}; use pyo3::{intern, PyTraverseError, PyVisit}; use hashbrown::{HashMap, HashSet}; @@ -321,7 +322,7 @@ impl CircuitData { } pub fn append_inner(&mut self, py: Python, value: PyRef) -> PyResult { - let packed = self.pack(py, value)?; + let packed = self.pack(value)?; let new_index = self.data.len(); self.data.push(packed); self.update_param_table(py, new_index, None) @@ -744,184 +745,130 @@ impl CircuitData { } // Note: we also rely on this to make us iterable! - pub fn __getitem__(&self, py: Python, index: &Bound) -> PyResult { - // Internal helper function to get a specific - // instruction by index. - fn get_at( - self_: &CircuitData, - py: Python<'_>, - index: isize, - ) -> PyResult> { - let index = self_.convert_py_index(index)?; - if let Some(inst) = self_.data.get(index) { - let qubits = self_.qargs_interner.intern(inst.qubits_id); - let clbits = self_.cargs_interner.intern(inst.clbits_id); - Py::new( - py, - CircuitInstruction::new( - py, - inst.op.clone(), - self_.qubits.map_indices(qubits.value), - self_.clbits.map_indices(clbits.value), - inst.params.clone(), - inst.extra_attrs.clone(), - ), - ) - } else { - Err(PyIndexError::new_err(format!( - "No element at index {:?} in circuit data", - index - ))) - } - } - - if index.is_exact_instance_of::() { - let slice = self.convert_py_slice(index.downcast_exact::()?)?; - let result = slice - .into_iter() - .map(|i| get_at(self, py, i)) - .collect::>>()?; - Ok(result.into_py(py)) - } else { - Ok(get_at(self, py, index.extract()?)?.into_py(py)) + pub fn __getitem__(&self, py: Python, index: PySequenceIndex) -> PyResult { + // Get a single item, assuming the index is validated as in bounds. + let get_single = |index: usize| { + let inst = &self.data[index]; + let qubits = self.qargs_interner.intern(inst.qubits_id); + let clbits = self.cargs_interner.intern(inst.clbits_id); + CircuitInstruction::new( + py, + inst.op.clone(), + self.qubits.map_indices(qubits.value), + self.clbits.map_indices(clbits.value), + inst.params.clone(), + inst.extra_attrs.clone(), + ) + .into_py(py) + }; + match index.with_len(self.data.len())? { + SequenceIndex::Int(index) => Ok(get_single(index)), + indices => Ok(PyList::new_bound(py, indices.iter().map(get_single)).into_py(py)), } } - 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))?; - } - self.reindex_parameter_table(py)?; - Ok(()) - } - SliceOrInt::Int(index) => { - let index = self.convert_py_index(index)?; - if self.data.get(index).is_some() { - if index == self.data.len() { - // For individual removal from param table before - // deletion - self.remove_from_parameter_table(py, index)?; - self.data.remove(index); - } else { - // For delete in the middle delete before reindexing - self.data.remove(index); - self.reindex_parameter_table(py)?; - } - Ok(()) - } else { - Err(PyIndexError::new_err(format!( - "No element at index {:?} in circuit data", - index - ))) - } - } - } + pub fn __delitem__(&mut self, py: Python, index: PySequenceIndex) -> PyResult<()> { + self.delitem(py, index.with_len(self.data.len())?) } pub fn setitem_no_param_table_update( &mut self, - py: Python<'_>, - index: isize, - value: &Bound, + index: usize, + value: PyRef, ) -> PyResult<()> { - let index = self.convert_py_index(index)?; - let value: PyRef = value.downcast()?.borrow(); - let mut packed = self.pack(py, value)?; + let mut packed = self.pack(value)?; std::mem::swap(&mut packed, &mut self.data[index]); Ok(()) } - pub fn __setitem__( - &mut self, - py: Python<'_>, - index: SliceOrInt, - value: &Bound, - ) -> PyResult<()> { - match index { - SliceOrInt::Slice(slice) => { - let indices = slice.indices(self.data.len().try_into().unwrap())?; - let slice = self.convert_py_slice(&slice)?; - let values = value.iter()?.collect::>>>()?; - if indices.step != 1 && slice.len() != values.len() { - // A replacement of a different length when step isn't exactly '1' - // would result in holes. - return Err(PyValueError::new_err(format!( - "attempt to assign sequence of size {:?} to extended slice of size {:?}", - values.len(), - slice.len(), - ))); - } + pub fn __setitem__(&mut self, index: PySequenceIndex, value: &Bound) -> PyResult<()> { + fn set_single(slf: &mut CircuitData, index: usize, value: &Bound) -> PyResult<()> { + let py = value.py(); + let mut packed = slf.pack(value.downcast::()?.borrow())?; + slf.remove_from_parameter_table(py, index)?; + std::mem::swap(&mut packed, &mut slf.data[index]); + slf.update_param_table(py, index, None)?; + Ok(()) + } - for (i, v) in slice.iter().zip(values.iter()) { - self.__setitem__(py, SliceOrInt::Int(*i), v)?; + let py = value.py(); + match index.with_len(self.data.len())? { + SequenceIndex::Int(index) => set_single(self, index, value), + indices @ SequenceIndex::PosRange { + start, + stop, + step: 1, + } => { + // `list` allows setting a slice with step +1 to an arbitrary length. + let values = value.iter()?.collect::>>()?; + for (index, value) in indices.iter().zip(values.iter()) { + set_single(self, index, value)?; } - - if slice.len() > values.len() { - // Delete any extras. - let slice = PySlice::new_bound( + if indices.len() > values.len() { + self.delitem( py, - indices.start + values.len() as isize, - indices.stop, - 1isize, - ); - self.__delitem__(py, SliceOrInt::Slice(slice))?; + SequenceIndex::PosRange { + start: start + values.len(), + stop, + step: 1, + }, + )? } else { - // Insert any extra values. - for v in values.iter().skip(slice.len()).rev() { - let v: PyRef = v.extract()?; - self.insert(py, indices.stop, v)?; + for value in values[indices.len()..].iter().rev() { + self.insert(stop as isize, value.downcast()?.borrow())?; } } - Ok(()) } - SliceOrInt::Int(index) => { - let index = self.convert_py_index(index)?; - let value: PyRef = value.extract()?; - let mut packed = self.pack(py, value)?; - self.remove_from_parameter_table(py, index)?; - std::mem::swap(&mut packed, &mut self.data[index]); - self.update_param_table(py, index, None)?; - Ok(()) + indices => { + let values = value.iter()?.collect::>>()?; + if indices.len() == values.len() { + for (index, value) in indices.iter().zip(values.iter()) { + set_single(self, index, value)?; + } + Ok(()) + } else { + Err(PyValueError::new_err(format!( + "attempt to assign sequence of size {:?} to extended slice of size {:?}", + values.len(), + indices.len(), + ))) + } } } } - pub fn insert( - &mut self, - py: Python<'_>, - index: isize, - value: PyRef, - ) -> PyResult<()> { - let index = self.convert_py_index_clamped(index); - let old_len = self.data.len(); - let packed = self.pack(py, value)?; + pub fn insert(&mut self, mut index: isize, value: PyRef) -> PyResult<()> { + // `list.insert` has special-case extra clamping logic for its index argument. + let index = { + if index < 0 { + // This can't exceed `isize::MAX` because `self.data[0]` is larger than a byte. + index += self.data.len() as isize; + } + if index < 0 { + 0 + } else if index as usize > self.data.len() { + self.data.len() + } else { + index as usize + } + }; + let py = value.py(); + let packed = self.pack(value)?; self.data.insert(index, packed); - if index == old_len { - self.update_param_table(py, old_len, None)?; + if index == self.data.len() - 1 { + self.update_param_table(py, index, None)?; } else { self.reindex_parameter_table(py)?; } Ok(()) } - pub fn pop(&mut self, py: Python<'_>, index: Option) -> PyResult { - let index = - index.unwrap_or_else(|| std::cmp::max(0, self.data.len() as isize - 1).into_py(py)); - let item = self.__getitem__(py, index.bind(py))?; - - self.__delitem__(py, index.bind(py).extract()?)?; + pub fn pop(&mut self, py: Python<'_>, index: Option) -> PyResult { + let index = index.unwrap_or(PySequenceIndex::Int(-1)); + let native_index = index.with_len(self.data.len())?; + let item = self.__getitem__(py, index)?; + self.delitem(py, native_index)?; Ok(item) } @@ -931,7 +878,7 @@ impl CircuitData { value: &Bound, params: Option)>>, ) -> PyResult { - let packed = self.pack(py, value.try_borrow()?)?; + let packed = self.pack(value.try_borrow()?)?; let new_index = self.data.len(); self.data.push(packed); self.update_param_table(py, new_index, params) @@ -1175,56 +1122,22 @@ impl CircuitData { } impl CircuitData { - /// Converts a Python slice to a `Vec` of indices into - /// the instruction listing, [CircuitData.data]. - fn convert_py_slice(&self, slice: &Bound) -> PyResult> { - let indices = slice.indices(self.data.len().try_into().unwrap())?; - if indices.step > 0 { - Ok((indices.start..indices.stop) - .step_by(indices.step as usize) - .collect()) - } else { - let mut out = Vec::with_capacity(indices.slicelength as usize); - let mut x = indices.start; - while x > indices.stop { - out.push(x); - x += indices.step; - } - Ok(out) + /// Native internal driver of `__delitem__` that uses a Rust-space version of the + /// `SequenceIndex`. This assumes that the `SequenceIndex` contains only in-bounds indices, and + /// panics if not. + fn delitem(&mut self, py: Python, indices: SequenceIndex) -> PyResult<()> { + // We need to delete in reverse order so we don't invalidate higher indices with a deletion. + for index in indices.descending() { + self.data.remove(index); } - } - - /// Converts a Python index to an index into the instruction listing, - /// or one past its end. - /// If the resulting index would be < 0, clamps to 0. - /// If the resulting index would be > len(data), clamps to len(data). - fn convert_py_index_clamped(&self, index: isize) -> usize { - let index = if index < 0 { - index + self.data.len() as isize - } else { - index - }; - std::cmp::min(std::cmp::max(0, index), self.data.len() as isize) as usize - } - - /// Converts a Python index to an index into the instruction listing. - fn convert_py_index(&self, index: isize) -> PyResult { - let index = if index < 0 { - index + self.data.len() as isize - } else { - index - }; - - if index < 0 || index >= self.data.len() as isize { - return Err(PyIndexError::new_err(format!( - "Index {:?} is out of bounds.", - index, - ))); + if !indices.is_empty() { + self.reindex_parameter_table(py)?; } - Ok(index as usize) + Ok(()) } - fn pack(&mut self, py: Python, inst: PyRef) -> PyResult { + fn pack(&mut self, inst: PyRef) -> PyResult { + let py = inst.py(); let qubits = Interner::intern( &mut self.qargs_interner, InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 74302b526d51..ffa6bb0c652c 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -728,10 +728,7 @@ impl CircuitInstruction { /// Take a reference to a `CircuitInstruction` and convert the operation /// inside that to a python side object. -pub(crate) fn operation_type_to_py( - py: Python, - circuit_inst: &CircuitInstruction, -) -> PyResult { +pub fn operation_type_to_py(py: Python, circuit_inst: &CircuitInstruction) -> PyResult { let (label, duration, unit, condition) = match &circuit_inst.extra_attrs { None => (None, None, None, None), Some(extra_attrs) => ( @@ -757,7 +754,7 @@ pub(crate) fn operation_type_to_py( /// a Python side full-fat Qiskit operation as a PyObject. This is typically /// used by accessor functions that need to return an operation to Qiskit, such /// as accesing `CircuitInstruction.operation`. -pub(crate) fn operation_type_and_data_to_py( +pub fn operation_type_and_data_to_py( py: Python, operation: &OperationType, params: &[Param], @@ -796,8 +793,8 @@ pub(crate) fn operation_type_and_data_to_py( /// A container struct that contains the output from the Python object to /// conversion to construct a CircuitInstruction object -#[derive(Debug)] -pub(crate) struct OperationTypeConstruct { +#[derive(Debug, Clone)] +pub struct OperationTypeConstruct { pub operation: OperationType, pub params: SmallVec<[Param; 3]>, pub label: Option, @@ -809,7 +806,7 @@ pub(crate) struct OperationTypeConstruct { /// Convert an inbound Python object for a Qiskit operation and build a rust /// representation of that operation. This will map it to appropriate variant /// of operation type based on class -pub(crate) fn convert_py_to_operation_type( +pub fn convert_py_to_operation_type( py: Python, py_op: PyObject, ) -> PyResult { diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 074b1c2ac682..46585ff6da6e 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -53,43 +53,6 @@ pub fn rz_gate(theta: f64) -> GateArray1Q { [[(-ilam2).exp(), C_ZERO], [C_ZERO, ilam2.exp()]] } -#[inline] -pub fn crx_gate(theta: f64) -> GateArray2Q { - let half_theta = theta / 2.; - let cos = c64(half_theta.cos(), 0.); - let isin = c64(0., half_theta.sin()); - [ - [C_ONE, C_ZERO, C_ZERO, C_ZERO], - [C_ZERO, cos, C_ZERO, -isin], - [C_ZERO, C_ZERO, C_ONE, C_ZERO], - [C_ZERO, -isin, C_ZERO, cos], - ] -} - -#[inline] -pub fn cry_gate(theta: f64) -> GateArray2Q { - let half_theta = theta / 2.; - let cos = c64(half_theta.cos(), 0.); - let sin = c64(half_theta.sin(), 0.); - [ - [C_ONE, C_ZERO, C_ZERO, C_ZERO], - [C_ZERO, cos, C_ZERO, -sin], - [C_ZERO, C_ZERO, C_ONE, C_ZERO], - [C_ZERO, sin, C_ZERO, cos], - ] -} - -#[inline] -pub fn crz_gate(theta: f64) -> GateArray2Q { - let i_half_theta = c64(0., theta / 2.); - [ - [C_ONE, C_ZERO, C_ZERO, C_ZERO], - [C_ZERO, (-i_half_theta).exp(), C_ZERO, C_ZERO], - [C_ZERO, C_ZERO, C_ONE, C_ZERO], - [C_ZERO, C_ZERO, C_ZERO, i_half_theta.exp()], - ] -} - pub static H_GATE: GateArray1Q = [ [c64(FRAC_1_SQRT_2, 0.), c64(FRAC_1_SQRT_2, 0.)], [c64(FRAC_1_SQRT_2, 0.), c64(-FRAC_1_SQRT_2, 0.)], @@ -210,6 +173,71 @@ pub static TDG_GATE: GateArray1Q = [ [C_ZERO, c64(FRAC_1_SQRT_2, -FRAC_1_SQRT_2)], ]; +pub static CH_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [ + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + ], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [ + C_ZERO, + c64(FRAC_1_SQRT_2, 0.), + C_ZERO, + c64(-FRAC_1_SQRT_2, 0.), + ], +]; + +pub static CS_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, IM], +]; + +pub static CSDG_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, M_IM], +]; + +pub static CSX_GATE: GateArray2Q = [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, c64(0.5, 0.5), C_ZERO, c64(0.5, -0.5)], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, c64(0.5, -0.5), C_ZERO, c64(0.5, 0.5)], +]; + +pub static CSWAP_GATE: GateArray3Q = [ + [ + C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, C_ZERO, C_ZERO, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, C_ZERO, + ], + [ + C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ZERO, C_ONE, + ], +]; + pub static DCX_GATE: GateArray2Q = [ [C_ONE, C_ZERO, C_ZERO, C_ZERO], [C_ZERO, C_ZERO, C_ZERO, C_ONE], @@ -217,6 +245,43 @@ pub static DCX_GATE: GateArray2Q = [ [C_ZERO, C_ZERO, C_ONE, C_ZERO], ]; +#[inline] +pub fn crx_gate(theta: f64) -> GateArray2Q { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let isin = c64(0., half_theta.sin()); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, cos, C_ZERO, -isin], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, -isin, C_ZERO, cos], + ] +} + +#[inline] +pub fn cry_gate(theta: f64) -> GateArray2Q { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let sin = c64(half_theta.sin(), 0.); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, cos, C_ZERO, -sin], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, sin, C_ZERO, cos], + ] +} + +#[inline] +pub fn crz_gate(theta: f64) -> GateArray2Q { + let i_half_theta = c64(0., theta / 2.); + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, (-i_half_theta).exp(), C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, i_half_theta.exp()], + ] +} + #[inline] pub fn global_phase_gate(theta: f64) -> GateArray0Q { [[c64(0., theta).exp()]] @@ -309,3 +374,69 @@ pub fn xx_plus_yy_gate(theta: f64, beta: f64) -> GateArray2Q { [C_ZERO, C_ZERO, C_ZERO, C_ONE], ] } + +#[inline] +pub fn cp_gate(lam: f64) -> GateArray2Q { + [ + [C_ONE, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, C_ONE, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, C_ONE, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, c64(0., lam).exp()], + ] +} + +#[inline] +pub fn rxx_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let ccos = c64(cost, 0.); + let csinm = c64(0., -sint); + + [ + [ccos, C_ZERO, C_ZERO, csinm], + [C_ZERO, ccos, csinm, C_ZERO], + [C_ZERO, csinm, ccos, C_ZERO], + [csinm, C_ZERO, C_ZERO, ccos], + ] +} + +#[inline] +pub fn ryy_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let ccos = c64(cost, 0.); + let csin = c64(0., sint); + + [ + [ccos, C_ZERO, C_ZERO, csin], + [C_ZERO, ccos, -csin, C_ZERO], + [C_ZERO, -csin, ccos, C_ZERO], + [csin, C_ZERO, C_ZERO, ccos], + ] +} + +#[inline] +pub fn rzz_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let exp_it2 = c64(cost, sint); + let exp_mit2 = c64(cost, -sint); + + [ + [exp_mit2, C_ZERO, C_ZERO, C_ZERO], + [C_ZERO, exp_it2, C_ZERO, C_ZERO], + [C_ZERO, C_ZERO, exp_it2, C_ZERO], + [C_ZERO, C_ZERO, C_ZERO, exp_mit2], + ] +} + +#[inline] +pub fn rzx_gate(theta: f64) -> GateArray2Q { + let (sint, cost) = (theta / 2.0).sin_cos(); + let ccos = c64(cost, 0.); + let csin = c64(0., sint); + + [ + [ccos, C_ZERO, -csin, C_ZERO], + [C_ZERO, ccos, C_ZERO, csin], + [-csin, C_ZERO, ccos, C_ZERO], + [C_ZERO, csin, C_ZERO, ccos], + ] +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 530e635c94f1..53fee34f486e 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -191,13 +191,13 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ // RC3XGate = 48 ["placeholder", "placeholder"], // RXXGate = 49 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.rxx", "RXXGate"], // RYYGate = 50 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.ryy", "RYYGate"], // RZZGate = 51 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.rzz", "RZZGate"], // RZXGate = 52 - ["placeholder", "placeholder"], + ["qiskit.circuit.library.standard_gates.rzx", "RZXGate"], ]; /// A mapping from the enum variant in crate::operations::StandardGate to the python object for the diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 9fcaa36480cf..9f0a8017bf21 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -17,23 +17,13 @@ pub mod gate_matrix; pub mod imports; pub mod operations; pub mod parameter_table; +pub mod slice; pub mod util; mod bit_data; mod interner; use pyo3::prelude::*; -use pyo3::types::PySlice; - -/// A private enumeration type used to extract arguments to pymethod -/// that may be either an index or a slice -#[derive(FromPyObject)] -pub enum SliceOrInt<'a> { - // The order here defines the order the variants are tried in the FromPyObject` derivation. - // `Int` is _much_ more common, so that should be first. - Int(isize), - Slice(Bound<'a, PySlice>), -} pub type BitType = u32; #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 85192b63dbd7..df15b4abb415 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -237,8 +237,8 @@ static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, // 10-19 1, 1, 1, 2, 2, 2, 1, 1, 1, 2, // 20-29 2, 2, 1, 2, 2, 2, 2, 2, 3, 2, // 30-39 - 2, 2, 34, 34, 34, 2, 34, 34, 34, 34, // 40-49 - 34, 34, 34, // 50-52 + 2, 2, 34, 34, 34, 2, 34, 34, 34, 2, // 40-49 + 2, 2, 2, // 50-52 ]; // TODO: replace all 34s (placeholders) with actual number @@ -247,8 +247,8 @@ static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, // 10-19 0, 0, 0, 0, 2, 2, 1, 2, 3, 1, // 20-29 1, 1, 2, 0, 1, 0, 0, 0, 0, 3, // 30-39 - 1, 3, 34, 34, 34, 0, 34, 34, 34, 34, // 40-49 - 34, 34, 34, // 50-52 + 1, 3, 34, 34, 34, 0, 34, 34, 34, 1, // 40-49 + 1, 1, 1, // 50-52 ]; static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ @@ -522,28 +522,60 @@ impl Operation for StandardGate { } _ => None, }, + Self::CHGate => match params { + [] => Some(aview2(&gate_matrix::CH_GATE).to_owned()), + _ => None, + }, + Self::CPhaseGate => match params { + [Param::Float(lam)] => Some(aview2(&gate_matrix::cp_gate(*lam)).to_owned()), + _ => None, + }, + Self::CSGate => match params { + [] => Some(aview2(&gate_matrix::CS_GATE).to_owned()), + _ => None, + }, + Self::CSdgGate => match params { + [] => Some(aview2(&gate_matrix::CSDG_GATE).to_owned()), + _ => None, + }, + Self::CSXGate => match params { + [] => Some(aview2(&gate_matrix::CSX_GATE).to_owned()), + _ => None, + }, + Self::CSwapGate => match params { + [] => Some(aview2(&gate_matrix::CSWAP_GATE).to_owned()), + _ => None, + }, + Self::CUGate | Self::CU1Gate | Self::CU3Gate => todo!(), + Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), Self::RGate => match params { [Param::Float(theta), Param::Float(phi)] => { Some(aview2(&gate_matrix::r_gate(*theta, *phi)).to_owned()) } _ => None, }, - Self::CHGate => todo!(), - Self::CPhaseGate => todo!(), - Self::CSGate => todo!(), - Self::CSdgGate => todo!(), - Self::CSXGate => todo!(), - Self::CSwapGate => todo!(), - Self::CUGate | Self::CU1Gate | Self::CU3Gate => todo!(), - Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), Self::DCXGate => match params { [] => Some(aview2(&gate_matrix::DCX_GATE).to_owned()), _ => None, }, Self::CCZGate => todo!(), Self::RCCXGate | Self::RC3XGate => todo!(), - Self::RXXGate | Self::RYYGate | Self::RZZGate => todo!(), - Self::RZXGate => todo!(), + Self::RXXGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::rxx_gate(theta)).to_owned()), + _ => None, + }, + Self::RYYGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::ryy_gate(theta)).to_owned()), + _ => None, + }, + Self::RZZGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::rzz_gate(theta)).to_owned()), + _ => None, + }, + Self::RZXGate => match params[0] { + Param::Float(theta) => Some(aview2(&gate_matrix::rzx_gate(theta)).to_owned()), + _ => None, + }, } } @@ -856,14 +888,14 @@ impl Operation for StandardGate { ) }), Self::UGate => None, - Self::SGate => Python::with_gil(|py| -> Option { + Self::U1Gate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( Self::PhaseGate, - smallvec![Param::Float(PI / 2.)], + params.iter().cloned().collect(), smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -871,14 +903,14 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::U1Gate => Python::with_gil(|py| -> Option { + Self::U2Gate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( - Self::PhaseGate, - params.iter().cloned().collect(), + Self::UGate, + smallvec![Param::Float(PI / 2.), params[0].clone(), params[1].clone()], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -886,14 +918,14 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::SdgGate => Python::with_gil(|py| -> Option { + Self::U3Gate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( - Self::PhaseGate, - smallvec![Param::Float(-PI / 2.)], + Self::UGate, + params.iter().cloned().collect(), smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -901,14 +933,14 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::U2Gate => Python::with_gil(|py| -> Option { + Self::SGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( - Self::UGate, - smallvec![Param::Float(PI / 2.), params[0].clone(), params[1].clone()], + Self::PhaseGate, + smallvec![Param::Float(PI / 2.)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -916,14 +948,14 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::TGate => Python::with_gil(|py| -> Option { + Self::SdgGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( Self::PhaseGate, - smallvec![Param::Float(PI / 4.)], + smallvec![Param::Float(-PI / 2.)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -931,14 +963,14 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::U3Gate => Python::with_gil(|py| -> Option { + Self::TGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( py, 1, [( - Self::UGate, - params.iter().cloned().collect(), + Self::PhaseGate, + smallvec![Param::Float(PI / 4.)], smallvec![Qubit(0)], )], FLOAT_ZERO, @@ -1061,6 +1093,143 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::CHGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::SGate, smallvec![], q1.clone()), + (Self::HGate, smallvec![], q1.clone()), + (Self::TGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1), + (Self::TdgGate, smallvec![], q1.clone()), + (Self::HGate, smallvec![], q1.clone()), + (Self::SdgGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CPhaseGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::PhaseGate, + smallvec![multiply_param(¶ms[0], 0.5, py)], + q0, + ), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::PhaseGate, + smallvec![multiply_param(¶ms[0], -0.5, py)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + ( + Self::PhaseGate, + smallvec![multiply_param(¶ms[0], 0.5, py)], + q1, + ), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::PhaseGate, smallvec![Param::Float(PI / 4.)], q0), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::PhaseGate, + smallvec![Param::Float(-PI / 4.)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + (Self::PhaseGate, smallvec![Param::Float(PI / 4.)], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSdgGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::PhaseGate, smallvec![Param::Float(-PI / 4.)], q0), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::PhaseGate, + smallvec![Param::Float(PI / 4.)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + (Self::PhaseGate, smallvec![Param::Float(-PI / 4.)], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSXGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q1.clone()), + (Self::CPhaseGate, smallvec![Param::Float(PI / 2.)], q0_1), + (Self::HGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CSwapGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 3, + [ + (Self::CXGate, smallvec![], smallvec![Qubit(2), Qubit(1)]), + ( + Self::CCXGate, + smallvec![], + smallvec![Qubit(0), Qubit(1), Qubit(2)], + ), + (Self::CXGate, smallvec![], smallvec![Qubit(2), Qubit(1)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::RGate => Python::with_gil(|py| -> Option { let theta_expr = clone_param(¶ms[0], py); let phi_expr1 = add_param(¶ms[1], -PI / 2., py); @@ -1076,12 +1245,6 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::CHGate => todo!(), - Self::CPhaseGate => todo!(), - Self::CSGate => todo!(), - Self::CSdgGate => todo!(), - Self::CSXGate => todo!(), - Self::CSwapGate => todo!(), Self::CUGate => todo!(), Self::CU1Gate => todo!(), Self::CU3Gate => todo!(), @@ -1100,11 +1263,92 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::CCZGate => todo!(), Self::RCCXGate | Self::RC3XGate => todo!(), - Self::RXXGate | Self::RYYGate | Self::RZZGate => todo!(), - Self::RZXGate => todo!(), + Self::RXXGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q0.clone()), + (Self::HGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1.clone()), + (Self::CXGate, smallvec![], q0_q1), + (Self::HGate, smallvec![], q1), + (Self::HGate, smallvec![], q0), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RYYGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::RXGate, smallvec![Param::Float(PI / 2.)], q0.clone()), + (Self::RXGate, smallvec![Param::Float(PI / 2.)], q1.clone()), + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1.clone()), + (Self::CXGate, smallvec![], q0_q1), + (Self::RXGate, smallvec![Param::Float(-PI / 2.)], q0), + (Self::RXGate, smallvec![Param::Float(-PI / 2.)], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RZZGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1), + (Self::CXGate, smallvec![], q0_q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RZXGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_q1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_q1.clone()), + (Self::RZGate, smallvec![theta.clone()], q1.clone()), + (Self::CXGate, smallvec![], q0_q1), + (Self::HGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), } } @@ -1115,7 +1359,7 @@ impl Operation for StandardGate { const FLOAT_ZERO: Param = Param::Float(0.0); -// Return explictly requested copy of `param`, handling +// Return explicitly requested copy of `param`, handling // each variant separately. fn clone_param(param: &Param, py: Python) -> Param { match param { diff --git a/crates/circuit/src/slice.rs b/crates/circuit/src/slice.rs new file mode 100644 index 000000000000..056adff0a282 --- /dev/null +++ b/crates/circuit/src/slice.rs @@ -0,0 +1,375 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// 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 thiserror::Error; + +use pyo3::exceptions::PyIndexError; +use pyo3::prelude::*; +use pyo3::types::PySlice; + +use self::sealed::{Descending, SequenceIndexIter}; + +/// A Python-space indexer for the standard `PySequence` type; a single integer or a slice. +/// +/// These come in as `isize`s from Python space, since Python typically allows negative indices. +/// Use `with_len` to specialize the index to a valid Rust-space indexer into a collection of the +/// given length. +pub enum PySequenceIndex<'py> { + Int(isize), + Slice(Bound<'py, PySlice>), +} + +impl<'py> FromPyObject<'py> for PySequenceIndex<'py> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + // `slice` can't be subclassed in Python, so it's safe (and faster) to check for it exactly. + // The `downcast_exact` check is just a pointer comparison, so while `slice` is the less + // common input, doing that first has little-to-no impact on the speed of the `isize` path, + // while the reverse makes `slice` inputs significantly slower. + if let Ok(slice) = ob.downcast_exact::() { + return Ok(Self::Slice(slice.clone())); + } + Ok(Self::Int(ob.extract()?)) + } +} + +impl<'py> PySequenceIndex<'py> { + /// Specialize this index to a collection of the given `len`, returning a Rust-native type. + pub fn with_len(&self, len: usize) -> Result { + match self { + PySequenceIndex::Int(index) => { + let index = if *index >= 0 { + let index = *index as usize; + if index >= len { + return Err(PySequenceIndexError::OutOfRange); + } + index + } else { + len.checked_sub(index.unsigned_abs()) + .ok_or(PySequenceIndexError::OutOfRange)? + }; + Ok(SequenceIndex::Int(index)) + } + PySequenceIndex::Slice(slice) => { + let indices = slice + .indices(len as ::std::os::raw::c_long) + .map_err(PySequenceIndexError::from)?; + if indices.step > 0 { + Ok(SequenceIndex::PosRange { + start: indices.start as usize, + stop: indices.stop as usize, + step: indices.step as usize, + }) + } else { + Ok(SequenceIndex::NegRange { + // `indices.start` can be negative if the collection length is 0. + start: (indices.start >= 0).then_some(indices.start as usize), + // `indices.stop` can be negative if the 0 index should be output. + stop: (indices.stop >= 0).then_some(indices.stop as usize), + step: indices.step.unsigned_abs(), + }) + } + } + } + } +} + +/// Error type for problems encountered when calling methods on `PySequenceIndex`. +#[derive(Error, Debug)] +pub enum PySequenceIndexError { + #[error("index out of range")] + OutOfRange, + #[error(transparent)] + InnerPy(#[from] PyErr), +} +impl From for PyErr { + fn from(value: PySequenceIndexError) -> PyErr { + match value { + PySequenceIndexError::OutOfRange => PyIndexError::new_err("index out of range"), + PySequenceIndexError::InnerPy(inner) => inner, + } + } +} + +/// Rust-native version of a Python sequence-like indexer. +/// +/// Typically this is constructed by a call to `PySequenceIndex::with_len`, which guarantees that +/// all the indices will be in bounds for a collection of the given length. +/// +/// This splits the positive- and negative-step versions of the slice in two so it can be translated +/// more easily into static dispatch. This type can be converted into several types of iterator. +#[derive(Clone, Copy, Debug)] +pub enum SequenceIndex { + Int(usize), + PosRange { + start: usize, + stop: usize, + step: usize, + }, + NegRange { + start: Option, + stop: Option, + step: usize, + }, +} + +impl SequenceIndex { + /// The number of indices this refers to. + pub fn len(&self) -> usize { + match self { + Self::Int(_) => 1, + Self::PosRange { start, stop, step } => { + let gap = stop.saturating_sub(*start); + gap / *step + (gap % *step != 0) as usize + } + Self::NegRange { start, stop, step } => 'arm: { + let Some(start) = start else { break 'arm 0 }; + let gap = stop + .map(|stop| start.saturating_sub(stop)) + .unwrap_or(*start + 1); + gap / step + (gap % step != 0) as usize + } + } + } + + pub fn is_empty(&self) -> bool { + // This is just to keep clippy happy; the length is already fairly inexpensive to calculate. + self.len() == 0 + } + + /// Get an iterator over the indices. This will be a single-item iterator for the case of + /// `Self::Int`, but you probably wanted to destructure off that case beforehand anyway. + pub fn iter(&self) -> SequenceIndexIter { + match self { + Self::Int(value) => SequenceIndexIter::Int(Some(*value)), + Self::PosRange { start, step, .. } => SequenceIndexIter::PosRange { + lowest: *start, + step: *step, + indices: 0..self.len(), + }, + Self::NegRange { start, step, .. } => SequenceIndexIter::NegRange { + // We can unwrap `highest` to an arbitrary value if `None`, because in that case the + // `len` is 0 and the iterator will not yield any objects. + highest: start.unwrap_or_default(), + step: *step, + indices: 0..self.len(), + }, + } + } + + // Get an iterator over the contained indices that is guaranteed to iterate from the highest + // index to the lowest. + pub fn descending(&self) -> Descending { + Descending(self.iter()) + } +} + +impl IntoIterator for SequenceIndex { + type Item = usize; + type IntoIter = SequenceIndexIter; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +// Private module to make it impossible to construct or inspect the internals of the iterator types +// from outside this file, while still allowing them to be used. +mod sealed { + /// Custom iterator for indices for Python sequence-likes. + /// + /// In the range types, the `indices ` are `Range` objects that run from 0 to the length of the + /// iterator. In theory, we could generate the iterators ourselves, but that ends up with a lot of + /// boilerplate. + #[derive(Clone, Debug)] + pub enum SequenceIndexIter { + Int(Option), + PosRange { + lowest: usize, + step: usize, + indices: ::std::ops::Range, + }, + NegRange { + highest: usize, + // The step of the iterator, but note that this is a negative range, so the forwards method + // steps downwards from `upper` towards `lower`. + step: usize, + indices: ::std::ops::Range, + }, + } + impl Iterator for SequenceIndexIter { + type Item = usize; + + #[inline] + fn next(&mut self) -> Option { + match self { + Self::Int(value) => value.take(), + Self::PosRange { + lowest, + step, + indices, + } => indices.next().map(|idx| *lowest + idx * *step), + Self::NegRange { + highest, + step, + indices, + } => indices.next().map(|idx| *highest - idx * *step), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self { + Self::Int(None) => (0, Some(0)), + Self::Int(Some(_)) => (1, Some(1)), + Self::PosRange { indices, .. } | Self::NegRange { indices, .. } => { + indices.size_hint() + } + } + } + } + impl DoubleEndedIterator for SequenceIndexIter { + #[inline] + fn next_back(&mut self) -> Option { + match self { + Self::Int(value) => value.take(), + Self::PosRange { + lowest, + step, + indices, + } => indices.next_back().map(|idx| *lowest + idx * *step), + Self::NegRange { + highest, + step, + indices, + } => indices.next_back().map(|idx| *highest - idx * *step), + } + } + } + impl ExactSizeIterator for SequenceIndexIter {} + + pub struct Descending(pub SequenceIndexIter); + impl Iterator for Descending { + type Item = usize; + + #[inline] + fn next(&mut self) -> Option { + match self.0 { + SequenceIndexIter::Int(_) | SequenceIndexIter::NegRange { .. } => self.0.next(), + SequenceIndexIter::PosRange { .. } => self.0.next_back(), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } + } + impl DoubleEndedIterator for Descending { + #[inline] + fn next_back(&mut self) -> Option { + match self.0 { + SequenceIndexIter::Int(_) | SequenceIndexIter::NegRange { .. } => { + self.0.next_back() + } + SequenceIndexIter::PosRange { .. } => self.0.next(), + } + } + } + impl ExactSizeIterator for Descending {} +} + +#[cfg(test)] +mod test { + use super::*; + + /// Get a set of test parametrisations for iterator methods. The second argument is the + /// expected values from a normal forward iteration. + fn index_iterator_cases() -> impl Iterator)> { + let pos = |start, stop, step| SequenceIndex::PosRange { start, stop, step }; + let neg = |start, stop, step| SequenceIndex::NegRange { start, stop, step }; + + [ + (SequenceIndex::Int(3), vec![3]), + (pos(0, 5, 2), vec![0, 2, 4]), + (pos(2, 10, 1), vec![2, 3, 4, 5, 6, 7, 8, 9]), + (pos(1, 15, 3), vec![1, 4, 7, 10, 13]), + (neg(Some(3), None, 1), vec![3, 2, 1, 0]), + (neg(Some(3), None, 2), vec![3, 1]), + (neg(Some(2), Some(0), 1), vec![2, 1]), + (neg(Some(2), Some(0), 2), vec![2]), + (neg(Some(2), Some(0), 3), vec![2]), + (neg(Some(10), Some(2), 3), vec![10, 7, 4]), + (neg(None, None, 1), vec![]), + (neg(None, None, 3), vec![]), + ] + .into_iter() + } + + /// Test that the index iterator's implementation of `ExactSizeIterator` is correct. + #[test] + fn index_iterator() { + for (index, forwards) in index_iterator_cases() { + // We're testing that all the values are the same, and the `size_hint` is correct at + // every single point. + let mut actual = Vec::new(); + let mut sizes = Vec::new(); + let mut iter = index.iter(); + loop { + sizes.push(iter.size_hint().0); + if let Some(next) = iter.next() { + actual.push(next); + } else { + break; + } + } + assert_eq!( + actual, forwards, + "values for {:?}\nActual : {:?}\nExpected: {:?}", + index, actual, forwards, + ); + let expected_sizes = (0..=forwards.len()).rev().collect::>(); + assert_eq!( + sizes, expected_sizes, + "sizes for {:?}\nActual : {:?}\nExpected: {:?}", + index, sizes, expected_sizes, + ); + } + } + + /// Test that the index iterator's implementation of `DoubleEndedIterator` is correct. + #[test] + fn reversed_index_iterator() { + for (index, forwards) in index_iterator_cases() { + let actual = index.iter().rev().collect::>(); + let expected = forwards.into_iter().rev().collect::>(); + assert_eq!( + actual, expected, + "reversed {:?}\nActual : {:?}\nExpected: {:?}", + index, actual, expected, + ); + } + } + + /// Test that `descending` produces its values in reverse-sorted order. + #[test] + fn descending() { + for (index, mut expected) in index_iterator_cases() { + let actual = index.descending().collect::>(); + expected.sort_by(|left, right| right.cmp(left)); + assert_eq!( + actual, expected, + "descending {:?}\nActual : {:?}\nExpected: {:?}", + index, actual, expected, + ); + } + } +} diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index 2d273eed74d5..c07895ebbeaa 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -185,6 +185,8 @@ class CHGate(SingletonControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CHGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 1a792649feab..8c83aa464027 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -200,6 +200,8 @@ class CPhaseGate(ControlledGate): phase difference. """ + _standard_gate = StandardGate.CPhaseGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/rxx.py b/qiskit/circuit/library/standard_gates/rxx.py index c4e35e53d55e..1c06ae05a85b 100644 --- a/qiskit/circuit/library/standard_gates/rxx.py +++ b/qiskit/circuit/library/standard_gates/rxx.py @@ -17,6 +17,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RXXGate(Gate): @@ -72,6 +73,8 @@ class RXXGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RXXGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/ryy.py b/qiskit/circuit/library/standard_gates/ryy.py index 98847b7b2182..91d7d8096cf9 100644 --- a/qiskit/circuit/library/standard_gates/ryy.py +++ b/qiskit/circuit/library/standard_gates/ryy.py @@ -17,6 +17,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RYYGate(Gate): @@ -72,6 +73,8 @@ class RYYGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RYYGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/rzx.py b/qiskit/circuit/library/standard_gates/rzx.py index 1f930ab422df..90e7b71c0a33 100644 --- a/qiskit/circuit/library/standard_gates/rzx.py +++ b/qiskit/circuit/library/standard_gates/rzx.py @@ -16,6 +16,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RZXGate(Gate): @@ -117,6 +118,8 @@ class RZXGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RZXGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/rzz.py b/qiskit/circuit/library/standard_gates/rzz.py index 5ca974764d32..119dd370e20c 100644 --- a/qiskit/circuit/library/standard_gates/rzz.py +++ b/qiskit/circuit/library/standard_gates/rzz.py @@ -16,6 +16,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RZZGate(Gate): @@ -84,6 +85,8 @@ class RZZGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RZZGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index f62d16a10d40..975d1cb3be8c 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -215,6 +215,8 @@ class CSGate(SingletonControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CSGate + def __init__( self, label: Optional[str] = None, @@ -301,6 +303,8 @@ class CSdgGate(SingletonControlledGate): \end{pmatrix} """ + _standard_gate = StandardGate.CSdgGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 243a84701ef5..5d33bc74b8d0 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -216,6 +216,8 @@ class CSwapGate(SingletonControlledGate): |1, b, c\rangle \rightarrow |1, c, b\rangle """ + _standard_gate = StandardGate.CSwapGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 72e4a8f9b5bf..ec3c87653148 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -266,6 +266,8 @@ class CSXGate(SingletonControlledGate): """ + _standard_gate = StandardGate.CSXGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 7b8fe6e031f1..6d41e6fdcd25 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -3257,11 +3257,11 @@ def draw( alternative value set. For example, ``circuit_reverse_bits = True``. plot_barriers: Enable/disable drawing barriers in the output circuit. Defaults to ``True``. - justify: Options are ``left``, ``right`` or ``none``. If - anything else is supplied, it defaults to left justified. It refers - to where gates should be placed in the output circuit if there is - an option. ``none`` results in each gate being placed in its own - column. + justify: Options are ``"left"``, ``"right"`` or ``"none"`` (str). + If anything else is supplied, left justified will be used instead. + It refers to where gates should be placed in the output circuit if + there is an option. ``none`` results in each gate being placed in + its own column. Defaults to ``left``. vertical_compression: ``high``, ``medium`` or ``low``. It merges the lines generated by the `text` output so the drawing will take less vertical room. Default is ``medium``. Only used by @@ -4516,6 +4516,12 @@ def ch( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CHGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.h import CHGate return self.append( @@ -4593,6 +4599,12 @@ def cp( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CPhaseGate, [theta], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.p import CPhaseGate return self.append( @@ -4772,14 +4784,14 @@ def crx( Returns: A handle to the instructions created. """ - from .library.standard_gates.rx import CRXGate - # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( StandardGate.CRXGate, [theta], [control_qubit, target_qubit], None, label=label ) + from .library.standard_gates.rx import CRXGate + return self.append( CRXGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], @@ -4802,9 +4814,7 @@ def rxx( Returns: A handle to the instructions created. """ - from .library.standard_gates.rxx import RXXGate - - return self.append(RXXGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RXXGate, [theta], [qubit1, qubit2]) def ry( self, theta: ParameterValueType, qubit: QubitSpecifier, label: str | None = None @@ -4847,14 +4857,14 @@ def cry( Returns: A handle to the instructions created. """ - from .library.standard_gates.ry import CRYGate - # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( StandardGate.CRYGate, [theta], [control_qubit, target_qubit], None, label=label ) + from .library.standard_gates.ry import CRYGate + return self.append( CRYGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], @@ -4877,9 +4887,7 @@ def ryy( Returns: A handle to the instructions created. """ - from .library.standard_gates.ryy import RYYGate - - return self.append(RYYGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RYYGate, [theta], [qubit1, qubit2]) def rz(self, phi: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.RZGate`. @@ -4919,14 +4927,14 @@ def crz( Returns: A handle to the instructions created. """ - from .library.standard_gates.rz import CRZGate - # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( StandardGate.CRZGate, [theta], [control_qubit, target_qubit], None, label=label ) + from .library.standard_gates.rz import CRZGate + return self.append( CRZGate(theta, label=label, ctrl_state=ctrl_state), [control_qubit, target_qubit], @@ -4949,9 +4957,7 @@ def rzx( Returns: A handle to the instructions created. """ - from .library.standard_gates.rzx import RZXGate - - return self.append(RZXGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RZXGate, [theta], [qubit1, qubit2]) def rzz( self, theta: ParameterValueType, qubit1: QubitSpecifier, qubit2: QubitSpecifier @@ -4968,9 +4974,7 @@ def rzz( Returns: A handle to the instructions created. """ - from .library.standard_gates.rzz import RZZGate - - return self.append(RZZGate(theta), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate(StandardGate.RZZGate, [theta], [qubit1, qubit2]) def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.ECRGate`. @@ -4983,9 +4987,7 @@ def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate( - StandardGate.ECRGate, [], qargs=[qubit1, qubit2], cargs=None - ) + return self._append_standard_gate(StandardGate.ECRGate, [], qargs=[qubit1, qubit2]) def s(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SGate`. @@ -4998,7 +5000,7 @@ def s(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SGate, [], [qubit], cargs=None) + return self._append_standard_gate(StandardGate.SGate, [], qargs=[qubit]) def sdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SdgGate`. @@ -5011,7 +5013,7 @@ def sdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SdgGate, [], [qubit], cargs=None) + return self._append_standard_gate(StandardGate.SdgGate, [], qargs=[qubit]) def cs( self, @@ -5035,6 +5037,12 @@ def cs( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.s import CSGate return self.append( @@ -5066,6 +5074,12 @@ def csdg( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSdgGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.s import CSdgGate return self.append( @@ -5090,7 +5104,6 @@ def swap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet StandardGate.SwapGate, [], qargs=[qubit1, qubit2], - cargs=None, ) def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: @@ -5104,7 +5117,7 @@ def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSe Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.ISwapGate, [], [qubit1, qubit2], cargs=None) + return self._append_standard_gate(StandardGate.ISwapGate, [], qargs=[qubit1, qubit2]) def cswap( self, @@ -5130,6 +5143,15 @@ def cswap( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSwapGate, + [], + qargs=[control_qubit, target_qubit1, target_qubit2], + label=label, + ) + from .library.standard_gates.swap import CSwapGate return self.append( @@ -5150,7 +5172,7 @@ def sx(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SXGate, None, qargs=[qubit]) + return self._append_standard_gate(StandardGate.SXGate, [], qargs=[qubit]) def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SXdgGate`. @@ -5163,7 +5185,7 @@ def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SXdgGate, None, qargs=[qubit]) + return self._append_standard_gate(StandardGate.SXdgGate, [], qargs=[qubit]) def csx( self, @@ -5187,6 +5209,12 @@ def csx( Returns: A handle to the instructions created. """ + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CSXGate, [], qargs=[control_qubit, target_qubit], label=label + ) + from .library.standard_gates.sx import CSXGate return self.append( @@ -5207,7 +5235,7 @@ def t(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.TGate, [], [qubit], cargs=None) + return self._append_standard_gate(StandardGate.TGate, [], qargs=[qubit]) def tdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.TdgGate`. @@ -5220,7 +5248,7 @@ def tdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.TdgGate, [], [qubit], cargs=None) + return self._append_standard_gate(StandardGate.TdgGate, [], qargs=[qubit]) def u( self, @@ -5319,17 +5347,23 @@ def cx( Returns: A handle to the instructions created. """ - if ctrl_state is not None: - from .library.standard_gates.x import CXGate - - return self.append( - CXGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CXGate, [], - copy=False, + qargs=[control_qubit, target_qubit], + cargs=None, + label=label, ) - return self._append_standard_gate( - StandardGate.CXGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label + + from .library.standard_gates.x import CXGate + + return self.append( + CXGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def dcx(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: @@ -5368,20 +5402,22 @@ def ccx( Returns: A handle to the instructions created. """ - if ctrl_state is not None: - from .library.standard_gates.x import CCXGate - - return self.append( - CCXGate(ctrl_state=ctrl_state), - [control_qubit1, control_qubit2, target_qubit], + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CCXGate, [], - copy=False, + qargs=[control_qubit1, control_qubit2, target_qubit], + cargs=None, ) - return self._append_standard_gate( - StandardGate.CCXGate, + + from .library.standard_gates.x import CCXGate + + return self.append( + CCXGate(ctrl_state=ctrl_state), + [control_qubit1, control_qubit2, target_qubit], [], - qargs=[control_qubit1, control_qubit2, target_qubit], - cargs=None, + copy=False, ) def mcx( @@ -5503,18 +5539,23 @@ def cy( Returns: A handle to the instructions created. """ - if ctrl_state is not None: - from .library.standard_gates.y import CYGate - - return self.append( - CYGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CYGate, [], - copy=False, + qargs=[control_qubit, target_qubit], + cargs=None, + label=label, ) - return self._append_standard_gate( - StandardGate.CYGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label + from .library.standard_gates.y import CYGate + + return self.append( + CYGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def z(self, qubit: QubitSpecifier) -> InstructionSet: @@ -5552,18 +5593,19 @@ def cz( Returns: A handle to the instructions created. """ - if ctrl_state is not None: - from .library.standard_gates.z import CZGate - - return self.append( - CZGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], - [], - copy=False, + # if the control state is |1> use the fast Rust version of the gate + if ctrl_state is None or ctrl_state in ["1", 1]: + return self._append_standard_gate( + StandardGate.CZGate, [], qargs=[control_qubit, target_qubit], label=label ) - return self._append_standard_gate( - StandardGate.CZGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label + from .library.standard_gates.z import CZGate + + return self.append( + CZGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, ) def ccz( diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index d14340a8cb9b..944e2df625b0 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -1342,6 +1342,13 @@ def replace_block_with_op( block_cargs.sort(key=wire_pos_map.get) new_node = DAGOpNode(op, block_qargs, block_cargs, dag=self) + # check the op to insert matches the number of qubits we put it on + if op.num_qubits != len(block_qargs): + raise DAGCircuitError( + f"Number of qubits in the replacement operation ({op.num_qubits}) is not equal to " + f"the number of qubits in the block ({len(block_qargs)})!" + ) + try: new_node._node_id = self._multi_graph.contract_nodes( block_ids, new_node, check_cycle=cycle_check @@ -2264,36 +2271,44 @@ def quantum_causal_cone(self, qubit): output_node = self.output_map.get(qubit, None) if not output_node: raise DAGCircuitError(f"Qubit {qubit} is not part of this circuit.") - # Add the qubit to the causal cone. - qubits_to_check = {qubit} - # Add predecessors of output node to the queue. - queue = deque(self.predecessors(output_node)) - # While queue isn't empty + qubits_in_cone = {qubit} + queue = deque(self.quantum_predecessors(output_node)) + + # The processed_non_directive_nodes stores the set of processed non-directive nodes. + # This is an optimization to avoid considering the same non-directive node multiple + # times when reached from different paths. + # The directive nodes (such as barriers or measures) are trickier since when processing + # them we only add their predecessors that intersect qubits_in_cone. Hence, directive + # nodes have to be considered multiple times. + processed_non_directive_nodes = set() + while queue: - # Pop first element. node_to_check = queue.popleft() - # Check whether element is input or output node. + if isinstance(node_to_check, DAGOpNode): - # Keep all the qubits in the operation inside a set. - qubit_set = set(node_to_check.qargs) - # Check if there are any qubits in common and that the operation is not a barrier. - if ( - len(qubit_set.intersection(qubits_to_check)) > 0 - and node_to_check.op.name != "barrier" - and not getattr(node_to_check.op, "_directive") - ): - # If so, add all the qubits to the causal cone. - qubits_to_check = qubits_to_check.union(qubit_set) - # For each predecessor of the current node, filter input/output nodes, - # also make sure it has at least one qubit in common. Then append. - for node in self.quantum_predecessors(node_to_check): - if ( - isinstance(node, DAGOpNode) - and len(qubits_to_check.intersection(set(node.qargs))) > 0 - ): - queue.append(node) - return qubits_to_check + # If the operation is not a directive (in particular not a barrier nor a measure), + # we do not do anything if it was already processed. Otherwise, we add its qubits + # to qubits_in_cone, and append its predecessors to queue. + if not getattr(node_to_check.op, "_directive"): + if node_to_check in processed_non_directive_nodes: + continue + qubits_in_cone = qubits_in_cone.union(set(node_to_check.qargs)) + processed_non_directive_nodes.add(node_to_check) + for pred in self.quantum_predecessors(node_to_check): + if isinstance(pred, DAGOpNode): + queue.append(pred) + else: + # Directives (such as barriers and measures) may be defined over all the qubits, + # yet not all of these qubits should be considered in the causal cone. So we + # only add those predecessors that have qubits in common with qubits_in_cone. + for pred in self.quantum_predecessors(node_to_check): + if isinstance(pred, DAGOpNode) and not qubits_in_cone.isdisjoint( + set(pred.qargs) + ): + queue.append(pred) + + return qubits_in_cone def properties(self): """Return a dictionary of circuit properties.""" diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py index 7ccd5053e6e0..c89fe3b4e306 100644 --- a/qiskit/pulse/schedule.py +++ b/qiskit/pulse/schedule.py @@ -252,7 +252,7 @@ def children(self) -> tuple[tuple[int, "ScheduleComponent"], ...]: Notes: Nested schedules are returned as-is. If you want to collect only instructions, - use py:meth:`~Schedule.instructions` instead. + use :py:meth:`~Schedule.instructions` instead. Returns: A tuple, where each element is a two-tuple containing the initial @@ -490,7 +490,7 @@ def exclude( ) -> "Schedule": """Return a ``Schedule`` with only the instructions from this Schedule *failing* at least one of the provided filters. - This method is the complement of py:meth:`~self.filter`, so that:: + This method is the complement of :py:meth:`~Schedule.filter`, so that:: self.filter(args) | self.exclude(args) == self @@ -1300,7 +1300,7 @@ def exclude( ): """Return a new ``ScheduleBlock`` with only the instructions from this ``ScheduleBlock`` *failing* at least one of the provided filters. - This method is the complement of py:meth:`~self.filter`, so that:: + This method is the complement of :py:meth:`~ScheduleBlock.filter`, so that:: self.filter(args) + self.exclude(args) == self in terms of instructions included. diff --git a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/pauli_2q_evolution_commutation.py b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/pauli_2q_evolution_commutation.py index 641b40c9f3e1..beadc884465b 100644 --- a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/pauli_2q_evolution_commutation.py +++ b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/pauli_2q_evolution_commutation.py @@ -51,7 +51,11 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: sub_dag = self._decompose_to_2q(dag, node.op) block_op = Commuting2qBlock(set(sub_dag.op_nodes())) - wire_order = {wire: idx for idx, wire in enumerate(dag.qubits)} + wire_order = { + wire: idx + for idx, wire in enumerate(sub_dag.qubits) + if wire not in sub_dag.idle_wires() + } dag.replace_block_with_op([node], block_op, wire_order) return dag diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index ca29794b9626..2077a3891542 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -14,21 +14,25 @@ import re from collections import OrderedDict +from warnings import warn import numpy as np from qiskit.circuit import ( + ClassicalRegister, Clbit, + ControlFlowOp, ControlledGate, Delay, Gate, Instruction, Measure, + QuantumCircuit, + Qubit, ) +from qiskit.circuit.annotated_operation import AnnotatedOperation, InverseModifier, PowerModifier from qiskit.circuit.controlflow import condition_resources from qiskit.circuit.library import PauliEvolutionGate -from qiskit.circuit import ClassicalRegister, QuantumCircuit, Qubit, ControlFlowOp -from qiskit.circuit.annotated_operation import AnnotatedOperation, InverseModifier, PowerModifier from qiskit.circuit.tools import pi_check from qiskit.converters import circuit_to_dag from qiskit.utils import optionals as _optionals @@ -370,6 +374,29 @@ def generate_latex_label(label): return final_str.replace(" ", "\\,") # Put in proper spaces +def _get_valid_justify_arg(justify): + """Returns a valid `justify` argument, warning if necessary.""" + if isinstance(justify, str): + justify = justify.lower() + + if justify is None: + justify = "left" + + if justify not in ("left", "right", "none"): + # This code should be changed to an error raise, once the deprecation is complete. + warn( + f"Setting QuantumCircuit.draw()’s or circuit_drawer()'s justify argument: {justify}, to a " + "value other than 'left', 'right', 'none' or None (='left'). Default 'left' will be used. " + "Support for invalid justify arguments is deprecated as of qiskit 1.2.0. Starting no " + "earlier than 3 months after the release date, invalid arguments will error.", + DeprecationWarning, + 2, + ) + justify = "left" + + return justify + + def _get_layered_instructions( circuit, reverse_bits=False, justify=None, idle_wires=True, wire_order=None, wire_map=None ): @@ -384,9 +411,10 @@ def _get_layered_instructions( reverse_bits (bool): If true the order of the bits in the registers is reversed. justify (str) : `left`, `right` or `none`. Defaults to `left`. Says how - the circuit should be justified. + the circuit should be justified. If an invalid value is provided, + default `left` will be used. idle_wires (bool): Include idle wires. Default is True. - wire_order (list): A list of ints that modifies the order of the bits + wire_order (list): A list of ints that modifies the order of the bits. Returns: Tuple(list,list,list): To be consumed by the visualizer directly. @@ -394,11 +422,7 @@ def _get_layered_instructions( Raises: VisualizationError: if both reverse_bits and wire_order are entered. """ - if justify: - justify = justify.lower() - - # default to left - justify = justify if justify in ("right", "none") else "left" + justify = _get_valid_justify_arg(justify) if wire_map is not None: qubits = [bit for bit in wire_map if isinstance(bit, Qubit)] diff --git a/qiskit/visualization/circuit/circuit_visualization.py b/qiskit/visualization/circuit/circuit_visualization.py index 146de9d32dee..33f6cadb46d3 100644 --- a/qiskit/visualization/circuit/circuit_visualization.py +++ b/qiskit/visualization/circuit/circuit_visualization.py @@ -28,21 +28,22 @@ import logging import os +import shutil import subprocess import tempfile -import shutil import typing from warnings import warn from qiskit import user_config -from qiskit.utils import optionals as _optionals from qiskit.circuit import ControlFlowOp, Measure +from qiskit.utils import optionals as _optionals + +from ..exceptions import VisualizationError +from ..utils import _trim as trim_image +from . import _utils from . import latex as _latex -from . import text as _text from . import matplotlib as _matplotlib -from . import _utils -from ..utils import _trim as trim_image -from ..exceptions import VisualizationError +from . import text as _text if typing.TYPE_CHECKING: from typing import Any @@ -131,11 +132,11 @@ def circuit_drawer( alternative value set. For example, ``circuit_reverse_bits = True``. plot_barriers: Enable/disable drawing barriers in the output circuit. Defaults to ``True``. - justify: Options are ``left``, ``right`` or ``none``. If - anything else is supplied, it defaults to left justified. It refers - to where gates should be placed in the output circuit if there is - an option. ``none`` results in each gate being placed in its own - column. + justify: Options are ``"left"``, ``"right"`` or ``"none"`` (str). + If anything else is supplied, left justified will be used instead. + It refers to where gates should be placed in the output circuit if + there is an option. ``none`` results in each gate being placed in + its own column. Defaults to ``left``. vertical_compression: ``high``, ``medium`` or ``low``. It merges the lines generated by the `text` output so the drawing will take less vertical room. Default is ``medium``. Only used by diff --git a/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml b/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml index 551ea9e918c6..d29089ef9491 100644 --- a/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml +++ b/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml @@ -1,7 +1,7 @@ --- features_pulse: - | - It is now possible to assign parameters to pulse :class:`.Schedule`and :class:`.ScheduleBlock` objects by specifying + It is now possible to assign parameters to pulse :class:`.Schedule` and :class:`.ScheduleBlock` objects by specifying the parameter name as a string. The parameter name can be used to assign values to all parameters within the `Schedule` or `ScheduleBlock` that have the same name. Moreover, the parameter name of a `ParameterVector` can be used to assign all values of the vector simultaneously (the list of values should therefore match the diff --git a/releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml b/releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml index f3f69ce5cb19..d1be6c450ee3 100644 --- a/releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml +++ b/releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml @@ -3,29 +3,29 @@ features_quantum_info: - | Added a new :meth:`~.Pauli.apply_layout` method that is equivalent to :meth:`~.SparsePauliOp.apply_layout`. This method is used to apply - a :class:`~.TranspileLayout` layout from the transpiler to a :class:~.Pauli` + a :class:`~.TranspileLayout` layout from the transpiler to a :class:`~.Pauli` observable that was built for an input circuit. This enables working with :class:`~.BaseEstimator` / :class:`~.BaseEstimatorV2` implementations and local transpilation when the input is of type :class:`~.Pauli`. For example:: - from qiskit.circuit.library import RealAmplitudes - from qiskit.primitives import BackendEstimatorV2 - from qiskit.providers.fake_provider import GenericBackendV2 - from qiskit.quantum_info import Pauli - from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + from qiskit.circuit.library import RealAmplitudes + from qiskit.primitives import BackendEstimatorV2 + from qiskit.providers.fake_provider import GenericBackendV2 + from qiskit.quantum_info import Pauli + from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + + psi = RealAmplitudes(num_qubits=2, reps=2) + H1 = Pauli("XI") + backend = GenericBackendV2(num_qubits=7) + estimator = BackendEstimatorV2(backend=backend) + thetas = [0, 1, 1, 2, 3, 5] + pm = generate_preset_pass_manager(optimization_level=3, backend=backend) + transpiled_psi = pm.run(psi) + permuted_op = H1.apply_layout(transpiled_psi.layout) + res = estimator.run([(transpiled_psi, permuted_op, thetas)]).result() - psi = RealAmplitudes(num_qubits=2, reps=2) - H1 = Pauli("XI") - backend = GenericBackendV2(num_qubits=7) - estimator = BackendEstimatorV2(backend=backend) - thetas = [0, 1, 1, 2, 3, 5] - pm = generate_preset_pass_manager(optimization_level=3, backend=backend) - transpiled_psi = pm.run(psi) - permuted_op = H1.apply_layout(transpiled_psi.layout) - res = estimator.run([(transpiled_psi, permuted_op, thetas)]).result() - - where an input circuit is transpiled locally before it's passed to - :class:`~.BaseEstimator.run`. Transpilation expands the original - circuit from 2 to 7 qubits (the size of ``backend``) and permutes its layout, - which is then applied to ``H1`` using :meth:`~.Pauli.apply_layout` - to reflect the transformations performed by ``pm.run()``. \ No newline at end of file + where an input circuit is transpiled locally before it's passed to + :class:`~.BaseEstimator.run`. Transpilation expands the original + circuit from 2 to 7 qubits (the size of ``backend``) and permutes its layout, + which is then applied to ``H1`` using :meth:`~.Pauli.apply_layout` + to reflect the transformations performed by ``pm.run()``. \ No newline at end of file diff --git a/releasenotes/notes/circuit-draw-warn-justify-03434d30cccda452.yaml b/releasenotes/notes/circuit-draw-warn-justify-03434d30cccda452.yaml new file mode 100644 index 000000000000..9ae15212fd56 --- /dev/null +++ b/releasenotes/notes/circuit-draw-warn-justify-03434d30cccda452.yaml @@ -0,0 +1,13 @@ +--- +deprecations_visualization: + - | + The ``justify`` argument of :func:`circuit_drawer` or :meth:`QuantumCircuit.draw`, will + no longer support invalid values (previously changing them to the default), and in a future + release they will error. Valid justify values are ``"left"``, ``"right"`` or ``"none"``. +fixes: + - | + Fixed an issue where :func:`circuit_drawer` or the :meth:`QuantumCircuit.draw` method would + not raise a warning when an invalid value was passed to the ``justify`` argument, before + changing it to the default. Now, it will raise a warning if an invalid value is passed. + Valid justify values are ``"left"``, ``"right"`` or ``"none"``. Refer to + `#12089 ` for more details. diff --git a/releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml b/releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml new file mode 100644 index 000000000000..5a072f481abc --- /dev/null +++ b/releasenotes/notes/improve-quantum-causal-cone-f63eaaa9ab658811.yaml @@ -0,0 +1,5 @@ +--- +features_circuits: + - | + Improved performance of the method :meth:`.DAGCircuit.quantum_causal_cone` by not examining + the same non-directive node multiple times when reached from different paths. diff --git a/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml index 6fa548d9245c..d0466b8f75d8 100644 --- a/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml +++ b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml @@ -1,10 +1,10 @@ --- features: - | - The :class:'.StabilizerState' class now has a new method - :meth:'~.StabilizerState.probabilities_dict_from_bitstring' allowing the + The :class:`.StabilizerState` class now has a new method + :meth:`~.StabilizerState.probabilities_dict_from_bitstring` allowing the user to pass single bitstring to measure an outcome for. Previouslly the - :meth:'~.StabilizerState.probabilities_dict' would be utilized and would + :meth:`~.StabilizerState.probabilities_dict` would be utilized and would at worst case calculate (2^n) number of probability calculations (depending on the state), even if a user wanted a single result. With this new method the user can calculate just the single outcome bitstring value a user passes diff --git a/releasenotes/notes/raise-on-illegal-replace-block-50cef8da757a580a.yaml b/releasenotes/notes/raise-on-illegal-replace-block-50cef8da757a580a.yaml new file mode 100644 index 000000000000..f4971fe520a0 --- /dev/null +++ b/releasenotes/notes/raise-on-illegal-replace-block-50cef8da757a580a.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Previously, :meth:`.DAGCircuit.replace_block_with_op` allowed to place an + ``n``-qubit operation onto a block of ``m`` qubits, leaving the DAG in an + invalid state. This behavior has been fixed, and the attempt will raise + a :class:`.DAGCircuitError`. diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index b20db4c79f94..6c0cc977e58a 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -73,6 +73,7 @@ def test_definitions(self): self.assertIsNone(rs_def) else: rs_def = QuantumCircuit._from_circuit_data(rs_def) + for rs_inst, py_inst in zip(rs_def._data, py_def._data): # Rust uses U but python still uses U3 and u2 if rs_inst.operation.name == "u": @@ -92,8 +93,8 @@ def test_definitions(self): [py_def.find_bit(x).index for x in py_inst.qubits], [rs_def.find_bit(x).index for x in rs_inst.qubits], ) - # Rust uses P but python still uses u1 - elif rs_inst.operation.name == "p": + # Rust uses p but python still uses u1/u3 in some cases + elif rs_inst.operation.name == "p" and not name in ["cp", "cs", "csdg"]: if py_inst.operation.name == "u1": self.assertEqual(py_inst.operation.name, "u1") self.assertEqual(rs_inst.operation.params, py_inst.operation.params) @@ -110,7 +111,14 @@ def test_definitions(self): [py_def.find_bit(x).index for x in py_inst.qubits], [rs_def.find_bit(x).index for x in rs_inst.qubits], ) - + # Rust uses cp but python still uses cu1 in some cases + elif rs_inst.operation.name == "cp": + self.assertEqual(py_inst.operation.name, "cu1") + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) else: self.assertEqual(py_inst.operation.name, rs_inst.operation.name) self.assertEqual(rs_inst.operation.params, py_inst.operation.params) diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 4ab4e392cbb1..26f7e4788083 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -2903,6 +2903,22 @@ def test_single_node_block(self): self.assertEqual(expected_dag.count_ops(), dag.count_ops()) self.assertIsInstance(new_node.op, XGate) + def test_invalid_replacement_size(self): + """Test inserting an operation on a wrong number of qubits raises.""" + + # two X gates, normal circuit + qc = QuantumCircuit(2) + qc.x(range(2)) + + # mutilate the DAG + dag = circuit_to_dag(qc) + to_replace = list(dag.op_nodes()) + new_node = XGate() + idx_map = {node.qargs[0]: i for i, node in enumerate(to_replace)} + + with self.assertRaises(DAGCircuitError): + dag.replace_block_with_op(to_replace, new_node, idx_map) + def test_replace_control_flow_block(self): """Test that we can replace a block of control-flow nodes with a single one.""" body = QuantumCircuit(1) @@ -3475,6 +3491,103 @@ def test_causal_cone_barriers(self): self.assertEqual(result, expected) + def test_causal_cone_more_barriers(self): + """Test causal cone for circuit with barriers. This example shows + why barriers may need to be examined multiple times.""" + + # q0_0: ──■────────░──────────────────────── + # ┌─┴─┐ ░ + # q0_1: ┤ X ├──────░───■──────────────────── + # ├───┤ ░ ┌─┴─┐┌───┐┌───┐┌───┐ + # q0_2: ┤ H ├──────░─┤ X ├┤ H ├┤ H ├┤ H ├─X─ + # ├───┤┌───┐ ░ └───┘└───┘└───┘└───┘ │ + # q0_3: ┤ H ├┤ X ├─░──────────────────────X─ + # ├───┤└─┬─┘ ░ + # q0_4: ┤ X ├──■───░──────────────────────── + # └─┬─┘ ░ + # q0_5: ──■────────░──────────────────────── + + qreg = QuantumRegister(6) + qc = QuantumCircuit(qreg) + qc.cx(0, 1) + qc.h(2) + qc.cx(5, 4) + qc.h(3) + qc.cx(4, 3) + qc.barrier() + qc.cx(1, 2) + + qc.h(2) + qc.h(2) + qc.h(2) + qc.swap(2, 3) + + dag = circuit_to_dag(qc) + + result = dag.quantum_causal_cone(qreg[2]) + expected = {qreg[0], qreg[1], qreg[2], qreg[3], qreg[4], qreg[5]} + + self.assertEqual(result, expected) + + def test_causal_cone_measure(self): + """Test causal cone with measures.""" + + # ┌───┐ ░ ┌─┐ + # q_0: ┤ H ├─░─┤M├──────────── + # ├───┤ ░ └╥┘┌─┐ + # q_1: ┤ H ├─░──╫─┤M├───────── + # ├───┤ ░ ║ └╥┘┌─┐ + # q_2: ┤ H ├─░──╫──╫─┤M├────── + # ├───┤ ░ ║ ║ └╥┘┌─┐ + # q_3: ┤ H ├─░──╫──╫──╫─┤M├─── + # ├───┤ ░ ║ ║ ║ └╥┘┌─┐ + # q_4: ┤ H ├─░──╫──╫──╫──╫─┤M├ + # └───┘ ░ ║ ║ ║ ║ └╥┘ + # c: 5/═════════╬══╬══╬══╬══╬═ + # ║ ║ ║ ║ ║ + # meas: 5/═════════╩══╩══╩══╩══╩═ + # 0 1 2 3 4 + + qreg = QuantumRegister(5) + creg = ClassicalRegister(5) + circuit = QuantumCircuit(qreg, creg) + for i in range(5): + circuit.h(i) + circuit.measure_all() + + dag = circuit_to_dag(circuit) + + result = dag.quantum_causal_cone(dag.qubits[1]) + expected = {qreg[1]} + self.assertEqual(result, expected) + + def test_reconvergent_paths(self): + """Test circuit with reconvergent paths.""" + + # q0_0: ──■─────────■─────────■─────────■─────────■─────────■─────── + # ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ + # q0_1: ┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■── + # └───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐ + # q0_2: ──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├──■──┤ X ├ + # ┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘┌─┴─┐└───┘ + # q0_3: ┤ X ├─────┤ X ├─────┤ X ├─────┤ X ├─────┤ X ├─────┤ X ├───── + # └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ + # q0_4: ──────────────────────────────────────────────────────────── + + qreg = QuantumRegister(5) + circuit = QuantumCircuit(qreg) + + for _ in range(6): + circuit.cx(0, 1) + circuit.cx(2, 3) + circuit.cx(1, 2) + + dag = circuit_to_dag(circuit) + + result = dag.quantum_causal_cone(dag.qubits[1]) + expected = {qreg[0], qreg[1], qreg[2], qreg[3]} + self.assertEqual(result, expected) + if __name__ == "__main__": unittest.main() diff --git a/test/python/visualization/test_circuit_drawer.py b/test/python/visualization/test_circuit_drawer.py index e6b430c4ee82..d459bd945046 100644 --- a/test/python/visualization/test_circuit_drawer.py +++ b/test/python/visualization/test_circuit_drawer.py @@ -14,16 +14,16 @@ import os import pathlib +import re import shutil import tempfile import unittest import warnings from unittest.mock import patch -from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, visualization from qiskit.utils import optionals -from qiskit import visualization -from qiskit.visualization.circuit import text, styles +from qiskit.visualization.circuit import styles, text from qiskit.visualization.exceptions import VisualizationError from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -241,6 +241,24 @@ def test_reverse_bits(self): result = visualization.circuit_drawer(circuit, output="text", reverse_bits=True) self.assertEqual(result.__str__(), expected) + def test_warning_for_bad_justify_argument(self): + """Test that the correct DeprecationWarning is raised when the justify parameter is badly input, + for both of the public interfaces.""" + circuit = QuantumCircuit() + bad_arg = "bad" + error_message = re.escape( + f"Setting QuantumCircuit.draw()’s or circuit_drawer()'s justify argument: {bad_arg}, to a " + "value other than 'left', 'right', 'none' or None (='left'). Default 'left' will be used. " + "Support for invalid justify arguments is deprecated as of qiskit 1.2.0. Starting no " + "earlier than 3 months after the release date, invalid arguments will error.", + ) + + with self.assertWarnsRegex(DeprecationWarning, error_message): + visualization.circuit_drawer(circuit, justify=bad_arg) + + with self.assertWarnsRegex(DeprecationWarning, error_message): + circuit.draw(justify=bad_arg) + @unittest.skipUnless(optionals.HAS_PYLATEX, "needs pylatexenc for LaTeX conversion") def test_no_explict_cregbundle(self): """Test no explicit cregbundle should not raise warnings about being disabled