diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 0169e3ed4cb4..a9c1839487a3 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -71,7 +71,7 @@ jobs: lcov --add-tracefile python.info --add-tracefile rust.info --output-file coveralls.info - name: Coveralls - uses: coverallsapp/github-action@v2.3.0 + uses: coverallsapp/github-action@v2.3.4 with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: coveralls.info diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 20c40dc38321..39439f7dd059 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: matrix: # Normally we test min and max version but we can't run python # 3.9 on arm64 until actions/setup-python#808 is resolved - python-version: ["3.10", "3.12"] + python-version: ["3.10", "3.13"] steps: - uses: actions/checkout@v4 - name: Install Rust toolchain @@ -44,7 +44,7 @@ jobs: python -m pip install -U -r requirements.txt -c constraints.txt python -m pip install -U -r requirements-dev.txt -c constraints.txt python -m pip install -c constraints.txt -e . - if: matrix.python-version == '3.12' + if: matrix.python-version == '3.13' - name: 'Install optionals' run: | python -m pip install -r requirements-optional.txt -c constraints.txt diff --git a/.github/workflows/wheels-build.yml b/.github/workflows/wheels-build.yml new file mode 100644 index 000000000000..a7453eb05568 --- /dev/null +++ b/.github/workflows/wheels-build.yml @@ -0,0 +1,247 @@ +name: Build release artifacts +on: + workflow_call: + inputs: + default-action: + description: >- + The default action for each artifact. + Choose from 'build' (default) or 'skip'. + type: string + default: "build" + required: false + + sdist: + description: >- + The action to take for the sdist. + Choose from 'default', 'build' or 'skip'. + type: string + default: "default" + required: false + + wheels-tier-1: + description: >- + The action to take for Tier 1 wheels. + Choose from 'default', 'build' or 'skip'. + This builds multiple artifacts, which all match 'wheels-tier-1-*'. + type: string + default: "default" + required: false + + wheels-32bit: + description: >- + The action to take for Tier 1 wheels. + Choose from 'default', 'build' or 'skip'. + This builds multiple artifacts, which all match 'wheels-32bit-*'. + type: string + default: "default" + required: false + + wheels-linux-s390x: + description: >- + The action to take for Linux s390x wheels. + Choose from 'default', 'build' or 'skip'. + type: string + default: "default" + required: false + + wheels-linux-ppc64le: + description: >- + The action to take for Linux ppc64le wheels. + Choose from 'default', 'build' or 'skip'. + type: string + default: "default" + required: false + + wheels-linux-aarch64: + description: >- + The action to take for Linux AArch64 wheels. + Choose from 'default', 'build' or 'skip'. + type: string + default: "default" + required: false + + artifact-prefix: + description: "A prefix to give all artifacts uploaded with 'actions/upload-artifact'." + type: string + default: "" + required: false + + python-version: + description: "The Python version to use to host the build runner." + type: string + default: "3.10" + required: false + + pgo: + description: "Whether to enable profile-guided optimizations for supported platforms." + type: boolean + default: true + required: false + + +jobs: + wheels-tier-1: + name: "Wheels / Tier 1" + if: (inputs.wheels-tier-1 == 'default' && inputs.default-action || inputs.wheels-tier-1) == 'build' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + # Used for the x86_64 builds. + - macos-12 + # Used for the ARM builds. + - macos-14 + - windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + architecture: ${{ matrix.os == 'macos-14' && 'arm64' || 'x64' }} + - uses: dtolnay/rust-toolchain@stable + with: + components: llvm-tools-preview + - name: Configure PGO + shell: bash + if: inputs.pgo + # The `$GITHUB_ENV` magic file uses some sort of custom parsing, so the variables shouldn't + # be quoted like they would be if bash were interpreting them. You still need to use quotes + # to avoid word splitting where appropriate in compound environment variables. + # + # Beware that the heredoc is configured to expand bash variables, but cibuildwheel has + # special handling for certain variables (`$PATH`, in particular), so you may need to escape + # some dollar signs to pass those through to cibuildwheel as variables, or use single quotes + # to prevent shell expansion. + run: | + set -e + mkdir -p "$PGO_WORK_DIR" + + cat >>"$GITHUB_ENV" <, + target: Option<&Target>, +) { + let mut remove_list: Vec = Vec::new(); + + let get_error_cutoff = |inst: &PackedInstruction| -> f64 { + match approx_degree { + Some(degree) => { + if degree == 1.0 { + f64::EPSILON + } else { + match target { + Some(target) => { + let qargs: Vec = dag + .get_qargs(inst.qubits) + .iter() + .map(|x| PhysicalQubit::new(x.0)) + .collect(); + let error_rate = target.get_error(inst.op.name(), qargs.as_slice()); + match error_rate { + Some(err) => err * degree, + None => f64::EPSILON.max(1. - degree), + } + } + None => f64::EPSILON.max(1. - degree), + } + } + } + None => match target { + Some(target) => { + let qargs: Vec = dag + .get_qargs(inst.qubits) + .iter() + .map(|x| PhysicalQubit::new(x.0)) + .collect(); + let error_rate = target.get_error(inst.op.name(), qargs.as_slice()); + match error_rate { + Some(err) => err, + None => f64::EPSILON, + } + } + None => f64::EPSILON, + }, + } + }; + + for op_node in dag.op_nodes(false) { + let inst = dag.dag()[op_node].unwrap_operation(); + match inst.op.view() { + OperationRef::Standard(gate) => { + let (dim, trace) = match gate { + StandardGate::RXGate | StandardGate::RYGate | StandardGate::RZGate => { + if let Param::Float(theta) = inst.params_view()[0] { + let trace = (theta / 2.).cos() * 2.; + (2., trace) + } else { + continue; + } + } + StandardGate::RXXGate + | StandardGate::RYYGate + | StandardGate::RZZGate + | StandardGate::RZXGate => { + if let Param::Float(theta) = inst.params_view()[0] { + let trace = (theta / 2.).cos() * 4.; + (4., trace) + } else { + continue; + } + } + _ => { + // Skip global phase gate + if gate.num_qubits() < 1 { + continue; + } + if let Some(matrix) = gate.matrix(inst.params_view()) { + let dim = matrix.shape()[0] as f64; + let trace = matrix.diag().iter().sum::().abs(); + (dim, trace) + } else { + continue; + } + } + }; + let error = get_error_cutoff(inst); + let f_pro = (trace / dim).powi(2); + let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); + if (1. - gate_fidelity).abs() < error { + remove_list.push(op_node) + } + } + OperationRef::Gate(gate) => { + // Skip global phase like gate + if gate.num_qubits() < 1 { + continue; + } + if let Some(matrix) = gate.matrix(inst.params_view()) { + let error = get_error_cutoff(inst); + let dim = matrix.shape()[0] as f64; + let trace: Complex64 = matrix.diag().iter().sum(); + let f_pro = (trace / dim).abs().powi(2); + let gate_fidelity = (dim * f_pro + 1.) / (dim + 1.); + if (1. - gate_fidelity).abs() < error { + remove_list.push(op_node) + } + } + } + _ => continue, + } + } + for node in remove_list { + dag.remove_op_node(node); + } +} + +pub fn remove_identity_equiv_mod(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(remove_identity_equiv))?; + Ok(()) +} diff --git a/crates/accelerate/src/sparse_observable.rs b/crates/accelerate/src/sparse_observable.rs new file mode 100644 index 000000000000..14e386f3a2cb --- /dev/null +++ b/crates/accelerate/src/sparse_observable.rs @@ -0,0 +1,2555 @@ +// 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 std::collections::btree_map; + +use num_complex::Complex64; +use num_traits::Zero; +use thiserror::Error; + +use numpy::{ + PyArray1, PyArrayDescr, PyArrayDescrMethods, PyArrayLike1, PyReadonlyArray1, PyReadonlyArray2, + PyUntypedArrayMethods, +}; +use pyo3::exceptions::{PyTypeError, PyValueError, PyZeroDivisionError}; +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::sync::GILOnceCell; +use pyo3::types::{IntoPyDict, PyList, PyType}; + +use qiskit_circuit::imports::{ImportOnceCell, NUMPY_COPY_ONLY_IF_NEEDED}; +use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; + +static PAULI_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Pauli"); +static SPARSE_PAULI_OP_TYPE: ImportOnceCell = + ImportOnceCell::new("qiskit.quantum_info", "SparsePauliOp"); + +/// Named handle to the alphabet of single-qubit terms. +/// +/// This is just the Rust-space representation. We make a separate Python-space `enum.IntEnum` to +/// represent the same information, since we enforce strongly typed interactions in Rust, including +/// not allowing the stored values to be outside the valid `BitTerm`s, but doing so in Python would +/// make it very difficult to use the class efficiently with Numpy array views. We attach this +/// sister class of `BitTerm` to `SparseObservable` as a scoped class. +/// +/// # Representation +/// +/// The `u8` representation and the exact numerical values of these are part of the public API. The +/// low two bits are the symplectic Pauli representation of the required measurement basis with Z in +/// the Lsb0 and X in the Lsb1 (e.g. X and its eigenstate projectors all have their two low bits as +/// `0b10`). The high two bits are `00` for the operator, `10` for the projector to the positive +/// eigenstate, and `01` for the projector to the negative eigenstate. +/// +/// The `0b00_00` representation thus ends up being the natural representation of the `I` operator, +/// but this is never stored, and is not named in the enumeration. +/// +/// This operator does not store phase terms of $-i$. `BitTerm::Y` has `(1, 1)` as its `(z, x)` +/// representation, and represents exactly the Pauli Y operator; any additional phase is stored only +/// in a corresponding coefficient. +/// +/// # Dev notes +/// +/// This type is required to be `u8`, but it's a subtype of `u8` because not all `u8` are valid +/// `BitTerm`s. For interop with Python space, we accept Numpy arrays of `u8` to represent this, +/// which we transmute into slices of `BitTerm`, after checking that all the values are correct (or +/// skipping the check if Python space promises that it upheld the checks). +/// +/// We deliberately _don't_ impl `numpy::Element` for `BitTerm` (which would let us accept and +/// return `PyArray1` at Python-space boundaries) so that it's clear when we're doing +/// the transmute, and we have to be explicit about the safety of that. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum BitTerm { + /// Pauli X operator. + X = 0b00_10, + /// Projector to the positive eigenstate of Pauli X. + Plus = 0b10_10, + /// Projector to the negative eigenstate of Pauli X. + Minus = 0b01_10, + /// Pauli Y operator. + Y = 0b00_11, + /// Projector to the positive eigenstate of Pauli Y. + Right = 0b10_11, + /// Projector to the negative eigenstate of Pauli Y. + Left = 0b01_11, + /// Pauli Z operator. + Z = 0b00_01, + /// Projector to the positive eigenstate of Pauli Z. + Zero = 0b10_01, + /// Projector to the negative eigenstate of Pauli Z. + One = 0b01_01, +} +impl From for u8 { + fn from(value: BitTerm) -> u8 { + value as u8 + } +} +unsafe impl ::bytemuck::CheckedBitPattern for BitTerm { + type Bits = u8; + + #[inline(always)] + fn is_valid_bit_pattern(bits: &Self::Bits) -> bool { + *bits <= 0b11_11 && (*bits & 0b11_00) < 0b11_00 && (*bits & 0b00_11) != 0 + } +} +unsafe impl ::bytemuck::NoUninit for BitTerm {} + +impl BitTerm { + /// Get the label of this `BitTerm` used in Python-space applications. This is a single-letter + /// string. + #[inline] + fn py_label(&self) -> &'static str { + match self { + Self::X => "X", + Self::Plus => "+", + Self::Minus => "-", + Self::Y => "Y", + Self::Right => "r", + Self::Left => "l", + Self::Z => "Z", + Self::Zero => "0", + Self::One => "1", + } + } + + /// Get the name of this `BitTerm`, which is how Python space refers to the integer constant. + #[inline] + fn py_name(&self) -> &'static str { + match self { + Self::X => "X", + Self::Plus => "PLUS", + Self::Minus => "MINUS", + Self::Y => "Y", + Self::Right => "RIGHT", + Self::Left => "LEFT", + Self::Z => "Z", + Self::Zero => "ZERO", + Self::One => "ONE", + } + } + + /// Attempt to convert a `u8` into `BitTerm`. + /// + /// Unlike the implementation of `TryFrom`, this allows `b'I'` as an alphabet letter, + /// returning `Ok(None)` for it. All other letters outside the alphabet return the complete + /// error condition. + #[inline] + fn try_from_u8(value: u8) -> Result, BitTermFromU8Error> { + match value { + b'+' => Ok(Some(BitTerm::Plus)), + b'-' => Ok(Some(BitTerm::Minus)), + b'0' => Ok(Some(BitTerm::Zero)), + b'1' => Ok(Some(BitTerm::One)), + b'I' => Ok(None), + b'X' => Ok(Some(BitTerm::X)), + b'Y' => Ok(Some(BitTerm::Y)), + b'Z' => Ok(Some(BitTerm::Z)), + b'l' => Ok(Some(BitTerm::Left)), + b'r' => Ok(Some(BitTerm::Right)), + _ => Err(BitTermFromU8Error(value)), + } + } +} + +static BIT_TERM_PY_ENUM: GILOnceCell> = GILOnceCell::new(); +static BIT_TERM_INTO_PY: GILOnceCell<[Option>; 16]> = GILOnceCell::new(); + +/// Construct the Python-space `IntEnum` that represents the same values as the Rust-spce `BitTerm`. +/// +/// We don't make `BitTerm` a direct `pyclass` because we want the behaviour of `IntEnum`, which +/// specifically also makes its variants subclasses of the Python `int` type; we use a type-safe +/// enum in Rust, but from Python space we expect people to (carefully) deal with the raw ints in +/// Numpy arrays for efficiency. +/// +/// The resulting class is attached to `SparseObservable` as a class attribute, and its +/// `__qualname__` is set to reflect this. +fn make_py_bit_term(py: Python) -> PyResult> { + let terms = [ + BitTerm::X, + BitTerm::Plus, + BitTerm::Minus, + BitTerm::Y, + BitTerm::Right, + BitTerm::Left, + BitTerm::Z, + BitTerm::Zero, + BitTerm::One, + ] + .into_iter() + .flat_map(|term| { + let mut out = vec![(term.py_name(), term as u8)]; + if term.py_name() != term.py_label() { + // Also ensure that the labels are created as aliases. These can't be (easily) accessed + // by attribute-getter (dot) syntax, but will work with the item-getter (square-bracket) + // syntax, or programmatically with `getattr`. + out.push((term.py_label(), term as u8)); + } + out + }) + .collect::>(); + let obj = py.import_bound("enum")?.getattr("IntEnum")?.call( + ("BitTerm", terms), + Some( + &[ + ("module", "qiskit.quantum_info"), + ("qualname", "SparseObservable.BitTerm"), + ] + .into_py_dict_bound(py), + ), + )?; + Ok(obj.downcast_into::()?.unbind()) +} + +// Return the relevant value from the Python-space sister enumeration. These are Python-space +// singletons and subclasses of Python `int`. We only use this for interaction with "high level" +// Python space; the efficient Numpy-like array paths use `u8` directly so Numpy can act on it +// efficiently. +impl IntoPy> for BitTerm { + fn into_py(self, py: Python) -> Py { + let terms = BIT_TERM_INTO_PY.get_or_init(py, || { + let py_enum = BIT_TERM_PY_ENUM + .get_or_try_init(py, || make_py_bit_term(py)) + .expect("creating a simple Python enum class should be infallible") + .bind(py); + ::std::array::from_fn(|val| { + ::bytemuck::checked::try_cast(val as u8) + .ok() + .map(|term: BitTerm| { + py_enum + .getattr(term.py_name()) + .expect("the created `BitTerm` enum should have matching attribute names to the terms") + .unbind() + }) + }) + }); + terms[self as usize] + .as_ref() + .expect("the lookup table initializer populated a 'Some' in all valid locations") + .clone_ref(py) + } +} +impl ToPyObject for BitTerm { + fn to_object(&self, py: Python) -> Py { + self.into_py(py) + } +} + +/// The error type for a failed conversion into `BitTerm`. +#[derive(Error, Debug)] +#[error("{0} is not a valid letter of the single-qubit alphabet")] +pub struct BitTermFromU8Error(u8); +impl From for PyErr { + fn from(value: BitTermFromU8Error) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +// `BitTerm` allows safe `as` casting into `u8`. This is the reverse, which is fallible, because +// `BitTerm` is a value-wise subtype of `u8`. +impl ::std::convert::TryFrom for BitTerm { + type Error = BitTermFromU8Error; + + fn try_from(value: u8) -> Result { + ::bytemuck::checked::try_cast(value).map_err(|_| BitTermFromU8Error(value)) + } +} + +/// Error cases stemming from data coherence at the point of entry into `SparseObservable` from raw +/// arrays. +/// +/// These are generally associated with the Python-space `ValueError` because all of the +/// `TypeError`-related ones are statically forbidden (within Rust) by the language, and conversion +/// failures on entry to Rust from Python space will automatically raise `TypeError`. +#[derive(Error, Debug)] +pub enum CoherenceError { + #[error("`boundaries` ({boundaries}) must be one element longer than `coeffs` ({coeffs})")] + MismatchedTermCount { coeffs: usize, boundaries: usize }, + #[error("`bit_terms` ({bit_terms}) and `indices` ({indices}) must be the same length")] + MismatchedItemCount { bit_terms: usize, indices: usize }, + #[error("the first item of `boundaries` ({0}) must be 0")] + BadInitialBoundary(usize), + #[error("the last item of `boundaries` ({last}) must match the length of `bit_terms` and `indices` ({items})")] + BadFinalBoundary { last: usize, items: usize }, + #[error("all qubit indices must be less than the number of qubits")] + BitIndexTooHigh, + #[error("the values in `boundaries` include backwards slices")] + DecreasingBoundaries, + #[error("the values in `indices` are not term-wise increasing")] + UnsortedIndices, +} +impl From for PyErr { + fn from(value: CoherenceError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +/// An error related to processing of a string label (both dense and sparse). +#[derive(Error, Debug)] +pub enum LabelError { + #[error("label with length {label} cannot be added to a {num_qubits}-qubit operator")] + WrongLengthDense { num_qubits: u32, label: usize }, + #[error("label with length {label} does not match indices of length {indices}")] + WrongLengthIndices { label: usize, indices: usize }, + #[error("index {index} is out of range for a {num_qubits}-qubit operator")] + BadIndex { index: u32, num_qubits: u32 }, + #[error("index {index} is duplicated in a single specifier")] + DuplicateIndex { index: u32 }, + #[error("labels must only contain letters from the alphabet 'IXYZ+-rl01'")] + OutsideAlphabet, +} +impl From for PyErr { + fn from(value: LabelError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +/// An observable over Pauli bases that stores its data in a qubit-sparse format. +/// +/// Mathematics +/// =========== +/// +/// This observable represents a sum over strings of the Pauli operators and Pauli-eigenstate +/// projectors, with each term weighted by some complex number. That is, the full observable is +/// +/// .. math:: +/// +/// \text{\texttt{SparseObservable}} = \sum_i c_i \bigotimes_n A^{(n)}_i +/// +/// for complex numbers :math:`c_i` and single-qubit operators acting on qubit :math:`n` from a +/// restricted alphabet :math:`A^{(n)}_i`. The sum over :math:`i` is the sum of the individual +/// terms, and the tensor product produces the operator strings. +/// +/// The alphabet of allowed single-qubit operators that the :math:`A^{(n)}_i` are drawn from is the +/// Pauli operators and the Pauli-eigenstate projection operators. Explicitly, these are: +/// +/// .. _sparse-observable-alphabet: +/// .. table:: Alphabet of single-qubit terms used in :class:`SparseObservable` +/// +/// ======= ======================================= =============== =========================== +/// Label Operator Numeric value :class:`.BitTerm` attribute +/// ======= ======================================= =============== =========================== +/// ``"I"`` :math:`I` (identity) Not stored. Not stored. +/// +/// ``"X"`` :math:`X` (Pauli X) ``0b0010`` (2) :attr:`~.BitTerm.X` +/// +/// ``"Y"`` :math:`Y` (Pauli Y) ``0b0011`` (3) :attr:`~.BitTerm.Y` +/// +/// ``"Z"`` :math:`Z` (Pauli Z) ``0b0001`` (1) :attr:`~.BitTerm.Z` +/// +/// ``"+"`` :math:`\lvert+\rangle\langle+\rvert` ``0b1010`` (10) :attr:`~.BitTerm.PLUS` +/// (projector to positive eigenstate of X) +/// +/// ``"-"`` :math:`\lvert-\rangle\langle-\rvert` ``0b0110`` (6) :attr:`~.BitTerm.MINUS` +/// (projector to negative eigenstate of X) +/// +/// ``"r"`` :math:`\lvert r\rangle\langle r\rvert` ``0b1011`` (11) :attr:`~.BitTerm.RIGHT` +/// (projector to positive eigenstate of Y) +/// +/// ``"l"`` :math:`\lvert l\rangle\langle l\rvert` ``0b0111`` (7) :attr:`~.BitTerm.LEFT` +/// (projector to negative eigenstate of Y) +/// +/// ``"0"`` :math:`\lvert0\rangle\langle0\rvert` ``0b1001`` (9) :attr:`~.BitTerm.ZERO` +/// (projector to positive eigenstate of Z) +/// +/// ``"1"`` :math:`\lvert1\rangle\langle1\rvert` ``0b0101`` (5) :attr:`~.BitTerm.ONE` +/// (projector to negative eigenstate of Z) +/// ======= ======================================= =============== =========================== +/// +/// The allowed alphabet forms an overcomplete basis of the operator space. This means that there +/// is not a unique summation to represent a given observable. By comparison, +/// :class:`.SparsePauliOp` uses a precise basis of the operator space, so (after combining terms of +/// the same Pauli string, removing zeros, and sorting the terms to :ref:`some canonical order +/// `) there is only one representation of any operator. +/// +/// :class:`SparseObservable` uses its particular overcomplete basis with the aim of making +/// "efficiency of measurement" equivalent to "efficiency of representation". For example, the +/// observable :math:`{\lvert0\rangle\langle0\rvert}^{\otimes n}` can be efficiently measured on +/// hardware with simple :math:`Z` measurements, but can only be represented by +/// :class:`.SparsePauliOp` as :math:`{(I + Z)}^{\otimes n}/2^n`, which requires :math:`2^n` stored +/// terms. :class:`SparseObservable` requires only a single term to store this. +/// +/// The downside to this is that it is impractical to take an arbitrary matrix or +/// :class:`.SparsePauliOp` and find the *best* :class:`SparseObservable` representation. You +/// typically will want to construct a :class:`SparseObservable` directly, rather than trying to +/// decompose into one. +/// +/// +/// Representation +/// ============== +/// +/// The internal representation of a :class:`SparseObservable` stores only the non-identity qubit +/// operators. This makes it significantly more efficient to represent observables such as +/// :math:`\sum_{n\in \text{qubits}} Z^{(n)}`; :class:`SparseObservable` requires an amount of +/// memory linear in the total number of qubits, while :class:`.SparsePauliOp` scales quadratically. +/// +/// The terms are stored compressed, similar in spirit to the compressed sparse row format of sparse +/// matrices. In this analogy, the terms of the sum are the "rows", and the qubit terms are the +/// "columns", where an absent entry represents the identity rather than a zero. More explicitly, +/// the representation is made up of four contiguous arrays: +/// +/// .. _sparse-observable-arrays: +/// .. table:: Data arrays used to represent :class:`.SparseObservable` +/// +/// ================== =========== ============================================================= +/// Attribute Length Description +/// ================== =========== ============================================================= +/// :attr:`coeffs` :math:`t` The complex scalar multiplier for each term. +/// +/// :attr:`bit_terms` :math:`s` Each of the non-identity single-qubit terms for all of the +/// operators, in order. These correspond to the non-identity +/// :math:`A^{(n)}_i` in the sum description, where the entries +/// are stored in order of increasing :math:`i` first, and in +/// order of increasing :math:`n` within each term. +/// +/// :attr:`indices` :math:`s` The corresponding qubit (:math:`n`) for each of the operators +/// in :attr:`bit_terms`. :class:`SparseObservable` requires +/// that this list is term-wise sorted, and algorithms can rely +/// on this invariant being upheld. +/// +/// :attr:`boundaries` :math:`t+1` The indices that partition :attr:`bit_terms` and +/// :attr:`indices` into complete terms. For term number +/// :math:`i`, its complex coefficient is ``coeffs[i]``, and its +/// non-identity single-qubit operators and their corresponding +/// qubits are the slice ``boundaries[i] : boundaries[i+1]`` into +/// :attr:`bit_terms` and :attr:`indices` respectively. +/// :attr:`boundaries` always has an explicit 0 as its first +/// element. +/// ================== =========== ============================================================= +/// +/// The length parameter :math:`t` is the number of terms in the sum, and the parameter :math:`s` is +/// the total number of non-identity single-qubit terms. +/// +/// As illustrative examples: +/// +/// * in the case of a zero operator, :attr:`boundaries` is length 1 (a single 0) and all other +/// vectors are empty. +/// * in the case of a fully simplified identity operator, :attr:`boundaries` is ``[0, 0]``, +/// :attr:`coeffs` has a single entry, and :attr:`bit_terms` and :attr:`indices` are empty. +/// * for the operator :math:`Z_2 Z_0 - X_3 Y_1`, :attr:`boundaries` is ``[0, 2, 4]``, +/// :attr:`coeffs` is ``[1.0, -1.0]``, :attr:`bit_terms` is ``[BitTerm.Z, BitTerm.Z, BitTerm.Y, +/// BitTerm.X]`` and :attr:`indices` is ``[0, 2, 1, 3]``. The operator might act on more than +/// four qubits, depending on the :attr:`num_qubits` parameter. The :attr:`bit_terms` are integer +/// values, whose magic numbers can be accessed via the :class:`BitTerm` attribute class. Note +/// that the single-bit terms and indices are sorted into termwise sorted order. This is a +/// requirement of the class. +/// +/// These cases are not special, they're fully consistent with the rules and should not need special +/// handling. +/// +/// The scalar item of the :attr:`bit_terms` array is stored as a numeric byte. The numeric values +/// are related to the symplectic Pauli representation that :class:`.SparsePauliOp` uses, and are +/// accessible with named access by an enumeration: +/// +/// .. +/// This is documented manually here because the Python-space `Enum` is generated +/// programmatically from Rust - it'd be _more_ confusing to try and write a docstring somewhere +/// else in this source file. The use of `autoattribute` is because it pulls in the numeric +/// value. +/// +/// .. py:class:: SparseObservable.BitTerm +/// +/// An :class:`~enum.IntEnum` that provides named access to the numerical values used to +/// represent each of the single-qubit alphabet terms enumerated in +/// :ref:`sparse-observable-alphabet`. +/// +/// This class is attached to :class:`.SparseObservable`. Access it as +/// :class:`.SparseObservable.BitTerm`. If this is too much typing, and you are solely dealing +/// with :class:¬SparseObservable` objects and the :class:`BitTerm` name is not ambiguous, you +/// might want to shorten it as:: +/// +/// >>> ops = SparseObservable.BitTerm +/// >>> assert ops.X is SparseObservable.BitTerm.X +/// +/// You can access all the values of the enumeration by either their full all-capitals name, or +/// by their single-letter label. The single-letter labels are not generally valid Python +/// identifiers, so you must use indexing notation to access them:: +/// +/// >>> assert SparseObservable.BitTerm.ZERO is SparseObservable.BitTerm["0"] +/// +/// The numeric structure of these is that they are all four-bit values of which the low two +/// bits are the (phase-less) symplectic representation of the Pauli operator related to the +/// object, where the low bit denotes a contribution by :math:`Z` and the second lowest a +/// contribution by :math:`X`, while the upper two bits are ``00`` for a Pauli operator, ``01`` +/// for the negative-eigenstate projector, and ``10`` for the positive-eigenstate projector. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.X +/// +/// The Pauli :math:`X` operator. Uses the single-letter label ``"X"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.PLUS +/// +/// The projector to the positive eigenstate of the :math:`X` operator: +/// :math:`\lvert+\rangle\langle+\rvert`. Uses the single-letter label ``"+"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.MINUS +/// +/// The projector to the negative eigenstate of the :math:`X` operator: +/// :math:`\lvert-\rangle\langle-\rvert`. Uses the single-letter label ``"-"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Y +/// +/// The Pauli :math:`Y` operator. Uses the single-letter label ``"Y"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.RIGHT +/// +/// The projector to the positive eigenstate of the :math:`Y` operator: +/// :math:`\lvert r\rangle\langle r\rvert`. Uses the single-letter label ``"r"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.LEFT +/// +/// The projector to the negative eigenstate of the :math:`Y` operator: +/// :math:`\lvert l\rangle\langle l\rvert`. Uses the single-letter label ``"l"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Z +/// +/// The Pauli :math:`Z` operator. Uses the single-letter label ``"Z"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ZERO +/// +/// The projector to the positive eigenstate of the :math:`Z` operator: +/// :math:`\lvert0\rangle\langle0\rvert`. Uses the single-letter label ``"0"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ONE +/// +/// The projector to the negative eigenstate of the :math:`Z` operator: +/// :math:`\lvert1\rangle\langle1\rvert`. Uses the single-letter label ``"1"``. +/// +/// Each of the array-like attributes behaves like a Python sequence. You can index and slice these +/// with standard :class:`list`-like semantics. Slicing an attribute returns a Numpy +/// :class:`~numpy.ndarray` containing a copy of the relevant data with the natural ``dtype`` of the +/// field; this lets you easily do mathematics on the results, like bitwise operations on +/// :attr:`bit_terms`. You can assign to indices or slices of each of the attributes, but beware +/// that you must uphold :ref:`the data coherence rules ` while doing +/// this. For example:: +/// +/// >>> obs = SparseObservable.from_list([("XZY", 1.5j), ("+1r", -0.5)]) +/// >>> assert isinstance(obs.coeffs[:], np.ndarray) +/// >>> # Reduce all single-qubit terms to the relevant Pauli operator, if they are a projector. +/// >>> obs.bit_terms[:] = obs.bit_terms[:] & 0b00_11 +/// >>> assert obs == SparseObservable.from_list([("XZY", 1.5j), ("XZY", -0.5)]) +/// +/// .. _sparse-observable-canonical-order: +/// +/// Canonical ordering +/// ------------------ +/// +/// For any given mathematical observable, there are several ways of representing it with +/// :class:`SparseObservable`. For example, the same set of single-bit terms and their +/// corresponding indices might appear multiple times in the observable. Mathematically, this is +/// equivalent to having only a single term with all the coefficients summed. Similarly, the terms +/// of the sum in a :class:`SparseObservable` can be in any order while representing the same +/// observable, since addition is commutative (although while floating-point addition is not +/// associative, :class:`SparseObservable` makes no guarantees about the summation order). +/// +/// These two categories of representation degeneracy can cause the ``==`` operator to claim that +/// two observables are not equal, despite representating the same object. In these cases, it can +/// be convenient to define some *canonical form*, which allows observables to be compared +/// structurally. +/// +/// You can put a :class:`SparseObservable` in canonical form by using the :meth:`simplify` method. +/// The precise ordering of terms in canonical ordering is not specified, and may change between +/// versions of Qiskit. Within the same version of Qiskit, however, you can compare two observables +/// structurally by comparing their simplified forms. +/// +/// .. note:: +/// +/// If you wish to account for floating-point tolerance in the comparison, it is safest to use +/// a recipe such as:: +/// +/// def equivalent(left, right, tol): +/// return (left - right).simplify(tol) == SparseObservable.zero(left.num_qubits) +/// +/// .. note:: +/// +/// The canonical form produced by :meth:`simplify` will still not universally detect all +/// observables that are equivalent due to the over-complete basis alphabet; it is not +/// computationally feasible to do this at scale. For example, on observable built from ``+`` +/// and ``-`` components will not canonicalize to a single ``X`` term. +/// +/// +/// Construction +/// ============ +/// +/// :class:`SparseObservable` defines several constructors. The default constructor will attempt to +/// delegate to one of the more specific constructors, based on the type of the input. You can +/// always use the specific constructors to have more control over the construction. +/// +/// .. _sparse-observable-convert-constructors: +/// .. table:: Construction from other objects +/// +/// ============================ ================================================================ +/// Method Summary +/// ============================ ================================================================ +/// :meth:`from_label` Convert a dense string label into a single-term +/// :class:`.SparseObservable`. +/// +/// :meth:`from_list` Sum a list of tuples of dense string labels and the associated +/// coefficients into an observable. +/// +/// :meth:`from_sparse_list` Sum a list of tuples of sparse string labels, the qubits they +/// apply to, and their coefficients into an observable. +/// +/// :meth:`from_pauli` Raise a single :class:`.Pauli` into a single-term +/// :class:`.SparseObservable`. +/// +/// :meth:`from_sparse_pauli_op` Raise a :class:`.SparsePauliOp` into a :class:`SparseObservable`. +/// +/// :meth:`from_raw_parts` Build the observable from :ref:`the raw data arrays +/// `. +/// ============================ ================================================================ +/// +/// .. py:function:: SparseObservable.__new__(data, /, num_qubits=None) +/// +/// The default constructor of :class:`SparseObservable`. +/// +/// This delegates to one of :ref:`the explicit conversion-constructor methods +/// `, based on the type of the ``data`` argument. If +/// ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not accept a +/// number, the given integer must match the input. +/// +/// :param data: The data type of the input. This can be another :class:`SparseObservable`, in +/// which case the input is copied, a :class:`.Pauli` or :class:`.SparsePauliOp`, in which +/// case :meth:`from_pauli` or :meth:`from_sparse_pauli_op` are called as appropriate, or it +/// can be a list in a valid format for either :meth:`from_list` or +/// :meth:`from_sparse_list`. +/// :param int|None num_qubits: Optional number of qubits for the operator. For most data +/// inputs, this can be inferred and need not be passed. It is only necessary for empty +/// lists or the sparse-list format. If given unnecessarily, it must match the data input. +/// +/// In addition to the conversion-based constructors, there are also helper methods that construct +/// special forms of observables. +/// +/// .. table:: Construction of special observables +/// +/// ============================ ================================================================ +/// Method Summary +/// ============================ ================================================================ +/// :meth:`zero` The zero operator on a given number of qubits. +/// +/// :meth:`identity` The identity operator on a given number of qubits. +/// ============================ ================================================================ +/// +/// +/// Mathematical manipulation +/// ========================= +/// +/// :class:`SparseObservable` supports the standard set of Python mathematical operators like other +/// :mod:`~qiskit.quantum_info` operators. +/// +/// In basic arithmetic, you can: +/// +/// * add two observables using ``+`` +/// * subtract two observables using ``-`` +/// * multiply or divide by an :class:`int`, :class:`float` or :class:`complex` using ``*`` and ``/`` +/// * negate all the coefficients in an observable with unary ``-`` +/// +/// Each of the basic binary arithmetic operators has a corresponding specialized in-place method, +/// which mutates the left-hand side in-place. Using these is typically more efficient than the +/// infix operators, especially for building an observable in a loop. +/// +/// The tensor product is calculated with :meth:`tensor` (for standard, juxtaposition ordering of +/// Pauli labels) or :meth:`expand` (for the reverse order). The ``^`` operator is overloaded to be +/// equivalent to :meth:`tensor`. +/// +/// .. note:: +/// +/// When using the binary operators ``^`` (:meth:`tensor`) and ``&`` (:meth:`compose`), beware +/// that `Python's operator-precedence rules +/// `__ may cause the +/// evaluation order to be different to your expectation. In particular, the operator ``+`` +/// binds more tightly than ``^`` or ``&``, just like ``*`` binds more tightly than ``+``. +/// +/// When using the operators in mixed expressions, it is safest to use parentheses to group the +/// operands of tensor products. +/// +/// A :class:`SparseObservable` has a well-defined :meth:`adjoint`. The notions of scalar complex +/// conjugation (:meth:`conjugate`) and real-value transposition (:meth:`transpose`) are defined +/// analogously to the matrix representation of other Pauli operators in Qiskit. +/// +/// +/// Efficiency notes +/// ---------------- +/// +/// Internally, :class:`SparseObservable` is in-place mutable, including using over-allocating +/// growable vectors for extending the number of terms. This means that the cost of appending to an +/// observable using ``+=`` is amortised linear in the total number of terms added, rather than +/// the quadratic complexity that the binary ``+`` would require. +/// +/// Additions and subtractions are implemented by a term-stacking operation; there is no automatic +/// "simplification" (summing of like terms), because the majority of additions to build up an +/// observable generate only a small number of duplications, and like-term detection has additional +/// costs. If this does not fit your use cases, you can either periodically call :meth:`simplify`, +/// or discuss further APIs with us for better building of observables. +#[pyclass(module = "qiskit.quantum_info")] +#[derive(Clone, Debug, PartialEq)] +pub struct SparseObservable { + /// The number of qubits the operator acts on. This is not inferable from any other shape or + /// values, since identities are not stored explicitly. + num_qubits: u32, + /// The coefficients of each abstract term in in the sum. This has as many elements as terms in + /// the sum. + coeffs: Vec, + /// A flat list of single-qubit terms. This is more naturally a list of lists, but is stored + /// flat for memory usage and locality reasons, with the sublists denoted by `boundaries.` + bit_terms: Vec, + /// A flat list of the qubit indices that the corresponding entries in `bit_terms` act on. This + /// list must always be term-wise sorted, where a term is a sublist as denoted by `boundaries`. + indices: Vec, + /// Indices that partition `bit_terms` and `indices` into sublists for each individual term in + /// the sum. `boundaries[0]..boundaries[1]` is the range of indices into `bit_terms` and + /// `indices` that correspond to the first term of the sum. All unspecified qubit indices are + /// implicitly the identity. This is one item longer than `coeffs`, since `boundaries[0]` is + /// always an explicit zero (for algorithmic ease). + boundaries: Vec, +} + +impl SparseObservable { + /// Create a new observable from the raw components that make it up. + /// + /// This checks the input values for data coherence on entry. If you are certain you have the + /// correct values, you can call `new_unchecked` instead. + pub fn new( + num_qubits: u32, + coeffs: Vec, + bit_terms: Vec, + indices: Vec, + boundaries: Vec, + ) -> Result { + if coeffs.len() + 1 != boundaries.len() { + return Err(CoherenceError::MismatchedTermCount { + coeffs: coeffs.len(), + boundaries: boundaries.len(), + }); + } + if bit_terms.len() != indices.len() { + return Err(CoherenceError::MismatchedItemCount { + bit_terms: bit_terms.len(), + indices: indices.len(), + }); + } + // We already checked that `boundaries` is at least length 1. + if *boundaries.first().unwrap() != 0 { + return Err(CoherenceError::BadInitialBoundary(boundaries[0])); + } + if *boundaries.last().unwrap() != indices.len() { + return Err(CoherenceError::BadFinalBoundary { + last: *boundaries.last().unwrap(), + items: indices.len(), + }); + } + for (&left, &right) in boundaries[..].iter().zip(&boundaries[1..]) { + if right < left { + return Err(CoherenceError::DecreasingBoundaries); + } + let indices = &indices[left..right]; + if !indices.is_empty() { + for (index_left, index_right) in indices[..].iter().zip(&indices[1..]) { + if index_left >= index_right { + return Err(CoherenceError::UnsortedIndices); + } + } + } + if indices.last().map(|&ix| ix >= num_qubits).unwrap_or(false) { + return Err(CoherenceError::BitIndexTooHigh); + } + } + // SAFETY: we've just done the coherence checks. + Ok(unsafe { Self::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries) }) + } + + /// Create a new observable from the raw components without checking data coherence. + /// + /// # Safety + /// + /// It is up to the caller to ensure that the data-coherence requirements, as enumerated in the + /// struct-level documentation, have been upheld. + #[inline(always)] + pub unsafe fn new_unchecked( + num_qubits: u32, + coeffs: Vec, + bit_terms: Vec, + indices: Vec, + boundaries: Vec, + ) -> Self { + Self { + num_qubits, + coeffs, + bit_terms, + indices, + boundaries, + } + } + + /// Create a zero operator with pre-allocated space for the given number of summands and + /// single-qubit bit terms. + #[inline] + pub fn with_capacity(num_qubits: u32, num_terms: usize, num_bit_terms: usize) -> Self { + Self { + num_qubits, + coeffs: Vec::with_capacity(num_terms), + bit_terms: Vec::with_capacity(num_bit_terms), + indices: Vec::with_capacity(num_bit_terms), + boundaries: { + let mut boundaries = Vec::with_capacity(num_terms + 1); + boundaries.push(0); + boundaries + }, + } + } + + /// Get an iterator over the individual terms of the operator. + pub fn iter(&'_ self) -> impl ExactSizeIterator> + '_ { + self.coeffs.iter().enumerate().map(|(i, coeff)| { + let start = self.boundaries[i]; + let end = self.boundaries[i + 1]; + SparseTermView { + coeff: *coeff, + bit_terms: &self.bit_terms[start..end], + indices: &self.indices[start..end], + } + }) + } + + /// Get an iterator over the individual terms of the operator that allows mutation of each term + /// in place without affecting its length or indices, both of which would allow breaking data + /// coherence. + pub fn iter_mut(&mut self) -> IterMut<'_> { + self.into() + } + + /// Reduce the observable to its canonical form. + /// + /// This sums like terms, removing them if the final complex coefficient's absolute value is + /// less than or equal to the tolerance. The terms are reordered to some canonical ordering. + /// + /// This function is idempotent. + pub fn canonicalize(&self, tol: f64) -> SparseObservable { + let mut terms = btree_map::BTreeMap::new(); + for term in self.iter() { + terms + .entry((term.indices, term.bit_terms)) + .and_modify(|c| *c += term.coeff) + .or_insert(term.coeff); + } + let mut out = SparseObservable::zero(self.num_qubits); + for ((indices, bit_terms), coeff) in terms { + if coeff.norm_sqr() <= tol * tol { + continue; + } + out.coeffs.push(coeff); + out.bit_terms.extend_from_slice(bit_terms); + out.indices.extend_from_slice(indices); + out.boundaries.push(out.indices.len()); + } + out + } + + /// Tensor product of `self` with `other`. + /// + /// The bit ordering is defined such that the qubit indices of `other` will remain the same, and + /// the indices of `self` will be offset by the number of qubits in `other`. This is the same + /// convention as used by the rest of Qiskit's `quantum_info` operators. + /// + /// Put another way, in the simplest case of two observables formed of dense labels: + /// + /// ``` + /// let mut left = SparseObservable::zero(5); + /// left.add_dense_label("IXY+Z", Complex64::new(1.0, 0.0)); + /// let mut right = SparseObservable::zero(6); + /// right.add_dense_label("IIrl01", Complex64::new(1.0, 0.0)); + /// + /// // The result is the concatenation of the two labels. + /// let mut out = SparseObservable::zero(11); + /// out.add_dense_label("IXY+ZIIrl01", Complex64::new(1.0, 0.0)); + /// + /// assert_eq!(left.tensor(right), out); + /// ``` + pub fn tensor(&self, other: &SparseObservable) -> SparseObservable { + let mut out = SparseObservable::with_capacity( + self.num_qubits + other.num_qubits, + self.coeffs.len() * other.coeffs.len(), + other.coeffs.len() * self.bit_terms.len() + self.coeffs.len() * other.bit_terms.len(), + ); + let mut self_indices = Vec::new(); + for self_term in self.iter() { + self_indices.clear(); + self_indices.extend(self_term.indices.iter().map(|i| i + other.num_qubits)); + for other_term in other.iter() { + out.coeffs.push(self_term.coeff * other_term.coeff); + out.indices.extend_from_slice(other_term.indices); + out.indices.extend_from_slice(&self_indices); + out.bit_terms.extend_from_slice(other_term.bit_terms); + out.bit_terms.extend_from_slice(self_term.bit_terms); + out.boundaries.push(out.bit_terms.len()); + } + } + out + } + + /// Add the term implied by a dense string label onto this observable. + pub fn add_dense_label>( + &mut self, + label: L, + coeff: Complex64, + ) -> Result<(), LabelError> { + let label = label.as_ref(); + if label.len() != self.num_qubits() as usize { + return Err(LabelError::WrongLengthDense { + num_qubits: self.num_qubits(), + label: label.len(), + }); + } + // The only valid characters in the alphabet are ASCII, so if we see something other than + // ASCII, we're already in the failure path. + for (i, letter) in label.iter().rev().enumerate() { + match BitTerm::try_from_u8(*letter) { + Ok(Some(term)) => { + self.bit_terms.push(term); + self.indices.push(i as u32); + } + Ok(None) => (), + Err(_) => { + // Undo any modifications to ourselves so we stay in a consistent state. + let num_single_terms = self.boundaries[self.boundaries.len() - 1]; + self.bit_terms.truncate(num_single_terms); + self.indices.truncate(num_single_terms); + return Err(LabelError::OutsideAlphabet); + } + } + } + self.coeffs.push(coeff); + self.boundaries.push(self.bit_terms.len()); + Ok(()) + } + + /// Return a suitable Python error if two observables do not have equal numbers of qubits. + fn check_equal_qubits(&self, other: &SparseObservable) -> PyResult<()> { + if self.num_qubits != other.num_qubits { + Err(PyValueError::new_err(format!( + "incompatible numbers of qubits: {} and {}", + self.num_qubits, other.num_qubits + ))) + } else { + Ok(()) + } + } +} + +#[pymethods] +impl SparseObservable { + #[pyo3(signature = (data, /, num_qubits=None))] + #[new] + fn py_new(data: &Bound, num_qubits: Option) -> PyResult { + let py = data.py(); + let check_num_qubits = |data: &Bound| -> PyResult<()> { + let Some(num_qubits) = num_qubits else { + return Ok(()); + }; + let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; + if num_qubits == other_qubits { + return Ok(()); + } + Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" + ))) + }; + + if data.is_instance(PAULI_TYPE.get_bound(py))? { + check_num_qubits(data)?; + return Self::py_from_pauli(data); + } + if data.is_instance(SPARSE_PAULI_OP_TYPE.get_bound(py))? { + check_num_qubits(data)?; + return Self::py_from_sparse_pauli_op(data); + } + if let Ok(label) = data.extract::() { + let num_qubits = num_qubits.unwrap_or(label.len() as u32); + if num_qubits as usize != label.len() { + return Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({}) does not match label ({})", + num_qubits, + label.len(), + ))); + } + return Self::py_from_label(&label).map_err(PyErr::from); + } + if let Ok(observable) = data.downcast::() { + check_num_qubits(data)?; + return Ok(observable.borrow().clone()); + } + // The type of `vec` is inferred from the subsequent calls to `Self::py_from_list` or + // `Self::py_from_sparse_list` to be either the two-tuple or the three-tuple form during the + // `extract`. The empty list will pass either, but it means the same to both functions. + if let Ok(vec) = data.extract() { + return Self::py_from_list(vec, num_qubits); + } + if let Ok(vec) = data.extract() { + let Some(num_qubits) = num_qubits else { + return Err(PyValueError::new_err( + "if using the sparse-list form, 'num_qubits' must be provided", + )); + }; + return Self::py_from_sparse_list(vec, num_qubits).map_err(PyErr::from); + } + Err(PyTypeError::new_err(format!( + "unknown input format for 'SparseObservable': {}", + data.get_type().repr()?, + ))) + } + + /// The number of qubits the operator acts on. + /// + /// This is not inferable from any other shape or values, since identities are not stored + /// explicitly. + #[getter] + #[inline] + pub fn num_qubits(&self) -> u32 { + self.num_qubits + } + + /// The number of terms in the sum this operator is tracking. + #[getter] + #[inline] + pub fn num_terms(&self) -> usize { + self.coeffs.len() + } + + /// The coefficients of each abstract term in in the sum. This has as many elements as terms in + /// the sum. + #[getter] + fn get_coeffs(slf_: Py) -> ArrayView { + ArrayView { + base: slf_, + slot: ArraySlot::Coeffs, + } + } + + /// A flat list of single-qubit terms. This is more naturally a list of lists, but is stored + /// flat for memory usage and locality reasons, with the sublists denoted by `boundaries.` + #[getter] + fn get_bit_terms(slf_: Py) -> ArrayView { + ArrayView { + base: slf_, + slot: ArraySlot::BitTerms, + } + } + + /// A flat list of the qubit indices that the corresponding entries in :attr:`bit_terms` act on. + /// This list must always be term-wise sorted, where a term is a sublist as denoted by + /// :attr:`boundaries`. + /// + /// .. warning:: + /// + /// If writing to this attribute from Python space, you *must* ensure that you only write in + /// indices that are term-wise sorted. + #[getter] + fn get_indices(slf_: Py) -> ArrayView { + ArrayView { + base: slf_, + slot: ArraySlot::Indices, + } + } + + /// Indices that partition :attr:`bit_terms` and :attr:`indices` into sublists for each + /// individual term in the sum. ``boundaries[0] : boundaries[1]`` is the range of indices into + /// :attr:`bit_terms` and :attr:`indices` that correspond to the first term of the sum. All + /// unspecified qubit indices are implicitly the identity. This is one item longer than + /// :attr:`coeffs`, since ``boundaries[0]`` is always an explicit zero (for algorithmic ease). + #[getter] + fn get_boundaries(slf_: Py) -> ArrayView { + ArrayView { + base: slf_, + slot: ArraySlot::Boundaries, + } + } + + // The documentation for this is inlined into the class-level documentation of + // `SparseObservable`. + #[allow(non_snake_case)] + #[classattr] + fn BitTerm(py: Python) -> PyResult> { + BIT_TERM_PY_ENUM + .get_or_try_init(py, || make_py_bit_term(py)) + .map(|obj| obj.clone_ref(py)) + } + + /// Get the zero operator over the given number of qubits. + /// + /// The zero operator is the operator whose expectation value is zero for all quantum states. + /// It has no terms. It is the identity element for addition of two :class:`SparseObservable` + /// instances; anything added to the zero operator is equal to itself. + /// + /// If you want the projector onto the all zeros state, use:: + /// + /// >>> num_qubits = 10 + /// >>> all_zeros = SparseObservable.from_label("0" * num_qubits) + /// + /// Examples: + /// + /// Get the zero operator for 100 qubits:: + /// + /// >>> SparseObservable.zero(100) + /// + #[pyo3(signature = (/, num_qubits))] + #[staticmethod] + pub fn zero(num_qubits: u32) -> Self { + Self::with_capacity(num_qubits, 0, 0) + } + + /// Get the identity operator over the given number of qubits. + /// + /// Examples: + /// + /// Get the identity operator for 100 qubits:: + /// + /// >>> SparseObservable.identity(100) + /// + #[pyo3(signature = (/, num_qubits))] + #[staticmethod] + pub fn identity(num_qubits: u32) -> Self { + Self { + num_qubits, + coeffs: vec![Complex64::new(1.0, 0.0)], + bit_terms: vec![], + indices: vec![], + boundaries: vec![0, 0], + } + } + + /// Clear all the terms from this operator, making it equal to the zero operator again. + /// + /// This does not change the capacity of the internal allocations, so subsequent addition or + /// substraction operations may not need to reallocate. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> obs = SparseObservable.from_list([("IX+-rl", 2.0), ("01YZII", -1j)]) + /// >>> obs.clear() + /// >>> assert obs == SparseObservable.zero(obs.num_qubits) + pub fn clear(&mut self) { + self.coeffs.clear(); + self.bit_terms.clear(); + self.indices.clear(); + self.boundaries.truncate(1); + } + + fn __repr__(&self) -> String { + let num_terms = format!( + "{} term{}", + self.num_terms(), + if self.num_terms() == 1 { "" } else { "s" } + ); + let qubits = format!( + "{} qubit{}", + self.num_qubits(), + if self.num_qubits() == 1 { "" } else { "s" } + ); + let terms = if self.num_terms() == 0 { + "0.0".to_owned() + } else { + self.iter() + .map(|term| { + let coeff = format!("{}", term.coeff).replace('i', "j"); + let paulis = term + .indices + .iter() + .zip(term.bit_terms) + .rev() + .map(|(i, op)| format!("{}_{}", op.py_label(), i)) + .collect::>() + .join(" "); + format!("({})({})", coeff, paulis) + }) + .collect::>() + .join(" + ") + }; + format!( + "", + num_terms, qubits, terms + ) + } + + fn __reduce__(&self, py: Python) -> PyResult> { + let bit_terms: &[u8] = ::bytemuck::cast_slice(&self.bit_terms); + Ok(( + py.get_type_bound::().getattr("from_raw_parts")?, + ( + self.num_qubits, + PyArray1::from_slice_bound(py, &self.coeffs), + PyArray1::from_slice_bound(py, bit_terms), + PyArray1::from_slice_bound(py, &self.indices), + PyArray1::from_slice_bound(py, &self.boundaries), + false, + ), + ) + .into_py(py)) + } + + fn __eq__(slf: Bound, other: Bound) -> bool { + if slf.is(&other) { + return true; + } + let Ok(other) = other.downcast_into::() else { + return false; + }; + slf.borrow().eq(&other.borrow()) + } + + fn __add__(slf_: &Bound, other: &Bound) -> PyResult> { + let py = slf_.py(); + if slf_.is(other) { + // This fast path is for consistency with the in-place `__iadd__`, which would otherwise + // struggle to do the addition to itself. + return Ok(<&SparseObservable as ::std::ops::Mul<_>>::mul( + &slf_.borrow(), + Complex64::new(2.0, 0.0), + ) + .into_py(py)); + } + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + let slf_ = slf_.borrow(); + let other = other.borrow(); + slf_.check_equal_qubits(&other)?; + Ok(<&SparseObservable as ::std::ops::Add>::add(&slf_, &other).into_py(py)) + } + fn __radd__(&self, other: &Bound) -> PyResult> { + // No need to handle the `self is other` case here, because `__add__` will get it. + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + let other = other.borrow(); + self.check_equal_qubits(&other)?; + Ok((<&SparseObservable as ::std::ops::Add>::add(&other, self)).into_py(py)) + } + fn __iadd__(slf_: Bound, other: &Bound) -> PyResult<()> { + if slf_.is(other) { + *slf_.borrow_mut() *= Complex64::new(2.0, 0.0); + return Ok(()); + } + let mut slf_ = slf_.borrow_mut(); + let Some(other) = coerce_to_observable(other)? else { + // This is not well behaved - we _should_ return `NotImplemented` to Python space + // without an exception, but limitations in PyO3 prevent this at the moment. See + // https://github.com/PyO3/pyo3/issues/4605. + return Err(PyTypeError::new_err(format!( + "invalid object for in-place addition of 'SparseObservable': {}", + other.repr()? + ))); + }; + let other = other.borrow(); + slf_.check_equal_qubits(&other)?; + *slf_ += &other; + Ok(()) + } + + fn __sub__(slf_: &Bound, other: &Bound) -> PyResult> { + let py = slf_.py(); + if slf_.is(other) { + return Ok(SparseObservable::zero(slf_.borrow().num_qubits).into_py(py)); + } + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + let slf_ = slf_.borrow(); + let other = other.borrow(); + slf_.check_equal_qubits(&other)?; + Ok(<&SparseObservable as ::std::ops::Sub>::sub(&slf_, &other).into_py(py)) + } + fn __rsub__(&self, other: &Bound) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + let other = other.borrow(); + self.check_equal_qubits(&other)?; + Ok((<&SparseObservable as ::std::ops::Sub>::sub(&other, self)).into_py(py)) + } + fn __isub__(slf_: Bound, other: &Bound) -> PyResult<()> { + if slf_.is(other) { + // This is not strictly the same thing as `a - a` if `a` contains non-finite + // floating-point values (`inf - inf` is `NaN`, for example); we don't really have a + // clear view on what floating-point guarantees we're going to make right now. + slf_.borrow_mut().clear(); + return Ok(()); + } + let mut slf_ = slf_.borrow_mut(); + let Some(other) = coerce_to_observable(other)? else { + // This is not well behaved - we _should_ return `NotImplemented` to Python space + // without an exception, but limitations in PyO3 prevent this at the moment. See + // https://github.com/PyO3/pyo3/issues/4605. + return Err(PyTypeError::new_err(format!( + "invalid object for in-place subtraction of 'SparseObservable': {}", + other.repr()? + ))); + }; + let other = other.borrow(); + slf_.check_equal_qubits(&other)?; + *slf_ -= &other; + Ok(()) + } + + fn __pos__(&self) -> SparseObservable { + self.clone() + } + fn __neg__(&self) -> SparseObservable { + -self + } + + fn __mul__(&self, other: Complex64) -> SparseObservable { + self * other + } + fn __rmul__(&self, other: Complex64) -> SparseObservable { + other * self + } + fn __imul__(&mut self, other: Complex64) { + *self *= other; + } + + fn __truediv__(&self, other: Complex64) -> PyResult { + if other.is_zero() { + return Err(PyZeroDivisionError::new_err("complex division by zero")); + } + Ok(self / other) + } + fn __itruediv__(&mut self, other: Complex64) -> PyResult<()> { + if other.is_zero() { + return Err(PyZeroDivisionError::new_err("complex division by zero")); + } + *self /= other; + Ok(()) + } + + fn __xor__(&self, other: &Bound) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + Ok(self.tensor(&other.borrow()).into_py(py)) + } + fn __rxor__(&self, other: &Bound) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + Ok(other.borrow().tensor(self).into_py(py)) + } + + // This doesn't actually have any interaction with Python space, but uses the `py_` prefix on + // its name to make it clear it's different to the Rust concept of `Copy`. + /// Get a copy of this observable. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> obs = SparseObservable.from_list([("IXZ+lr01", 2.5), ("ZXI-rl10", 0.5j)]) + /// >>> assert obs == obs.copy() + /// >>> assert obs is not obs.copy() + #[pyo3(name = "copy")] + fn py_copy(&self) -> Self { + self.clone() + } + + /// Construct a single-term observable from a dense string label. + /// + /// The resulting operator will have a coefficient of 1. The label must be a sequence of the + /// alphabet ``'IXYZ+-rl01'``. The label is interpreted analogously to a bitstring. In other + /// words, the right-most letter is associated with qubit 0, and so on. This is the same as the + /// labels for :class:`.Pauli` and :class:`.SparsePauliOp`. + /// + /// Args: + /// label (str): the dense label. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> SparseObservable.from_label("IIII+ZI") + /// + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli) + /// + /// See also: + /// :meth:`from_list` + /// A generalization of this method that constructs a sum operator from multiple labels + /// and their corresponding coefficients. + #[staticmethod] + #[pyo3(name = "from_label", signature = (label, /))] + fn py_from_label(label: &str) -> Result { + let mut out = Self::zero(label.len() as u32); + out.add_dense_label(label, Complex64::new(1.0, 0.0))?; + Ok(out) + } + + /// Construct an observable from a list of dense labels and coefficients. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_list`, except it uses + /// :ref:`the extended alphabet ` of :class:`.SparseObservable`. In + /// this dense form, you must supply all identities explicitly in each label. + /// + /// The label must be a sequence of the alphabet ``'IXYZ+-rl01'``. The label is interpreted + /// analogously to a bitstring. In other words, the right-most letter is associated with qubit + /// 0, and so on. This is the same as the labels for :class:`.Pauli` and + /// :class:`.SparsePauliOp`. + /// + /// Args: + /// iter (list[tuple[str, complex]]): Pairs of labels and their associated coefficients to + /// sum. The labels are interpreted the same way as in :meth:`from_label`. + /// num_qubits (int | None): It is not necessary to specify this if you are sure that + /// ``iter`` is not an empty sequence, since it can be inferred from the label lengths. + /// If ``iter`` may be empty, you must specify this argument to disambiguate how many + /// qubits the observable is for. If this is given and ``iter`` is not empty, the value + /// must match the label lengths. + /// + /// Examples: + /// + /// Construct an observable from a list of labels of the same length:: + /// + /// >>> SparseObservable.from_list([ + /// ... ("III++", 1.0), + /// ... ("II--I", 1.0j), + /// ... ("I++II", -0.5), + /// ... ("--III", -0.25j), + /// ... ]) + /// + /// + /// Use ``num_qubits`` to disambiguate potentially empty inputs:: + /// + /// >>> SparseObservable.from_list([], num_qubits=10) + /// + /// + /// This method is equivalent to calls to :meth:`from_sparse_list` with the explicit + /// qubit-arguments field set to decreasing integers:: + /// + /// >>> labels = ["XY+Z", "rl01", "-lXZ"] + /// >>> coeffs = [1.5j, 2.0, -0.5] + /// >>> from_list = SparseObservable.from_list(list(zip(labels, coeffs))) + /// >>> from_sparse_list = SparseObservable.from_sparse_list([ + /// ... (label, (3, 2, 1, 0), coeff) + /// ... for label, coeff in zip(labels, coeffs) + /// ... ]) + /// >>> assert from_list == from_sparse_list + /// + /// See also: + /// :meth:`from_label` + /// A similar constructor, but takes only a single label and always has its coefficient + /// set to ``1.0``. + /// + /// :meth:`from_sparse_list` + /// Construct the observable from a list of labels without explicit identities, but with + /// the qubits each single-qubit term applies to listed explicitly. + #[staticmethod] + #[pyo3(name = "from_list", signature = (iter, /, *, num_qubits=None))] + fn py_from_list(iter: Vec<(String, Complex64)>, num_qubits: Option) -> PyResult { + if iter.is_empty() && num_qubits.is_none() { + return Err(PyValueError::new_err( + "cannot construct an observable from an empty list without knowing `num_qubits`", + )); + } + let num_qubits = match num_qubits { + Some(num_qubits) => num_qubits, + None => iter[0].0.len() as u32, + }; + let mut out = Self::with_capacity(num_qubits, iter.len(), 0); + for (label, coeff) in iter { + out.add_dense_label(&label, coeff)?; + } + Ok(out) + } + + /// Construct an observable from a list of labels, the qubits each item applies to, and the + /// coefficient of the whole term. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`, except it uses + /// :ref:`the extended alphabet ` of :class:`.SparseObservable`. + /// + /// The "labels" and "indices" fields of the triples are associated by zipping them together. + /// For example, this means that a call to :meth:`from_list` can be converted to the form used + /// by this method by setting the "indices" field of each triple to ``(num_qubits-1, ..., 1, + /// 0)``. + /// + /// Args: + /// iter (list[tuple[str, Sequence[int], complex]]): triples of labels, the qubits + /// each single-qubit term applies to, and the coefficient of the entire term. + /// + /// num_qubits (int): the number of qubits in the operator. + /// + /// Examples: + /// + /// Construct a simple operator:: + /// + /// >>> SparseObservable.from_sparse_list( + /// ... [("ZX", (1, 4), 1.0), ("YY", (0, 3), 2j)], + /// ... num_qubits=5, + /// ... ) + /// + /// + /// Construct the identity observable (though really, just use :meth:`identity`):: + /// + /// >>> SparseObservable.from_sparse_list([("", (), 1.0)], num_qubits=100) + /// + /// + /// This method can replicate the behavior of :meth:`from_list`, if the qubit-arguments + /// field of the triple is set to decreasing integers:: + /// + /// >>> labels = ["XY+Z", "rl01", "-lXZ"] + /// >>> coeffs = [1.5j, 2.0, -0.5] + /// >>> from_list = SparseObservable.from_list(list(zip(labels, coeffs))) + /// >>> from_sparse_list = SparseObservable.from_sparse_list([ + /// ... (label, (3, 2, 1, 0), coeff) + /// ... for label, coeff in zip(labels, coeffs) + /// ... ]) + /// >>> assert from_list == from_sparse_list + #[staticmethod] + #[pyo3(name = "from_sparse_list", signature = (iter, /, num_qubits))] + fn py_from_sparse_list( + iter: Vec<(String, Vec, Complex64)>, + num_qubits: u32, + ) -> Result { + let coeffs = iter.iter().map(|(_, _, coeff)| *coeff).collect(); + let mut boundaries = Vec::with_capacity(iter.len() + 1); + boundaries.push(0); + let mut indices = Vec::new(); + let mut bit_terms = Vec::new(); + // Insertions to the `BTreeMap` keep it sorted by keys, so we use this to do the termwise + // sorting on-the-fly. + let mut sorted = btree_map::BTreeMap::new(); + for (label, qubits, _) in iter { + sorted.clear(); + let label: &[u8] = label.as_ref(); + if label.len() != qubits.len() { + return Err(LabelError::WrongLengthIndices { + label: label.len(), + indices: indices.len(), + }); + } + for (letter, index) in label.iter().zip(qubits) { + if index >= num_qubits { + return Err(LabelError::BadIndex { index, num_qubits }); + } + let btree_map::Entry::Vacant(entry) = sorted.entry(index) else { + return Err(LabelError::DuplicateIndex { index }); + }; + entry.insert( + BitTerm::try_from_u8(*letter).map_err(|_| LabelError::OutsideAlphabet)?, + ); + } + for (index, term) in sorted.iter() { + let Some(term) = term else { + continue; + }; + indices.push(*index); + bit_terms.push(*term); + } + boundaries.push(bit_terms.len()); + } + Ok(Self { + num_qubits, + coeffs, + indices, + bit_terms, + boundaries, + }) + } + + /// Construct a :class:`.SparseObservable` from a single :class:`.Pauli` instance. + /// + /// The output observable will have a single term, with a unitary coefficient dependent on the + /// phase. + /// + /// Args: + /// pauli (:class:`.Pauli`): the single Pauli to convert. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> SparseObservable.from_pauli(pauli) + /// + /// >>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli) + #[staticmethod] + #[pyo3(name = "from_pauli", signature = (pauli, /))] + fn py_from_pauli(pauli: &Bound) -> PyResult { + let py = pauli.py(); + let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; + let z = pauli + .getattr(intern!(py, "z"))? + .extract::>()?; + let x = pauli + .getattr(intern!(py, "x"))? + .extract::>()?; + let mut bit_terms = Vec::new(); + let mut indices = Vec::new(); + let mut num_ys = 0; + for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() { + // The only failure case possible here is the identity, because of how we're + // constructing the value to convert. + let Ok(term) = ::bytemuck::checked::try_cast((*x as u8) << 1 | (*z as u8)) else { + continue; + }; + num_ys += (term == BitTerm::Y) as isize; + indices.push(i as u32); + bit_terms.push(term); + } + let boundaries = vec![0, indices.len()]; + // The "empty" state of a `Pauli` represents the identity, which isn't our empty state + // (that's zero), so we're always going to have a coefficient. + let group_phase = pauli + // `Pauli`'s `_phase` is a Numpy array ... + .getattr(intern!(py, "_phase"))? + // ... that should have exactly 1 element ... + .call_method0(intern!(py, "item"))? + // ... which is some integral type. + .extract::()?; + let phase = match (group_phase - num_ys).rem_euclid(4) { + 0 => Complex64::new(1.0, 0.0), + 1 => Complex64::new(0.0, -1.0), + 2 => Complex64::new(-1.0, 0.0), + 3 => Complex64::new(0.0, 1.0), + _ => unreachable!("`x % 4` has only four values"), + }; + let coeffs = vec![phase]; + Ok(Self { + num_qubits, + coeffs, + bit_terms, + indices, + boundaries, + }) + } + + /// Construct a :class:`.SparseObservable` from a :class:`.SparsePauliOp` instance. + /// + /// This will be a largely direct translation of the :class:`.SparsePauliOp`; in particular, + /// there is no on-the-fly summing of like terms, nor any attempt to refactorize sums of Pauli + /// terms into equivalent projection operators. + /// + /// Args: + /// op (:class:`.SparsePauliOp`): the operator to convert. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> spo = SparsePauliOp.from_list([("III", 1.0), ("IIZ", 0.5), ("IZI", 0.5)]) + /// >>> SparseObservable.from_sparse_pauli_op(spo) + /// + #[staticmethod] + #[pyo3(name = "from_sparse_pauli_op", signature = (op, /))] + fn py_from_sparse_pauli_op(op: &Bound) -> PyResult { + let py = op.py(); + let pauli_list_ob = op.getattr(intern!(py, "paulis"))?; + let coeffs = op + .getattr(intern!(py, "coeffs"))? + .extract::>() + .map_err(|_| PyTypeError::new_err("only 'SparsePauliOp' with complex-typed coefficients can be converted to 'SparseObservable'"))? + .as_array() + .to_vec(); + let op_z = pauli_list_ob + .getattr(intern!(py, "z"))? + .extract::>()?; + let op_x = pauli_list_ob + .getattr(intern!(py, "x"))? + .extract::>()?; + // We don't extract the `phase`, because that's supposed to be 0 for all `SparsePauliOp` + // instances - they use the symplectic convention in the representation with any phase term + // absorbed into the coefficients (like us). + let [num_terms, num_qubits] = *op_z.shape() else { + unreachable!("shape is statically known to be 2D") + }; + if op_x.shape() != [num_terms, num_qubits] { + return Err(PyValueError::new_err(format!( + "'x' and 'z' have different shapes ({:?} and {:?})", + op_x.shape(), + op_z.shape() + ))); + } + if num_terms != coeffs.len() { + return Err(PyValueError::new_err(format!( + "'x' and 'z' have a different number of operators to 'coeffs' ({} and {})", + num_terms, + coeffs.len(), + ))); + } + + let mut bit_terms = Vec::new(); + let mut indices = Vec::new(); + let mut boundaries = Vec::with_capacity(num_terms + 1); + boundaries.push(0); + for (term_x, term_z) in op_x + .as_array() + .rows() + .into_iter() + .zip(op_z.as_array().rows()) + { + for (i, (x, z)) in term_x.iter().zip(term_z.iter()).enumerate() { + // The only failure case possible here is the identity, because of how we're + // constructing the value to convert. + let Ok(term) = ::bytemuck::checked::try_cast((*x as u8) << 1 | (*z as u8)) else { + continue; + }; + indices.push(i as u32); + bit_terms.push(term); + } + boundaries.push(indices.len()); + } + + Ok(Self { + num_qubits: num_qubits as u32, + coeffs, + bit_terms, + indices, + boundaries, + }) + } + + // SAFETY: this cannot invoke undefined behaviour if `check = true`, but if `check = false` then + // the `bit_terms` must all be valid `BitTerm` representations. + /// Construct a :class:`.SparseObservable` from raw Numpy arrays that match :ref:`the required + /// data representation described in the class-level documentation `. + /// + /// The data from each array is copied into fresh, growable Rust-space allocations. + /// + /// Args: + /// num_qubits: number of qubits in the observable. + /// coeffs: complex coefficients of each term of the observable. This should be a Numpy + /// array with dtype :attr:`~numpy.complex128`. + /// bit_terms: flattened list of the single-qubit terms comprising all complete terms. This + /// should be a Numpy array with dtype :attr:`~numpy.uint8` (which is compatible with + /// :class:`.BitTerm`). + /// indices: flattened term-wise sorted list of the qubits each single-qubit term corresponds + /// to. This should be a Numpy array with dtype :attr:`~numpy.uint32`. + /// boundaries: the indices that partition ``bit_terms`` and ``indices`` into terms. This + /// should be a Numpy array with dtype :attr:`~numpy.uintp`. + /// check: if ``True`` (the default), validate that the data satisfies all coherence + /// guarantees. If ``False``, no checks are done. + /// + /// .. warning:: + /// + /// If ``check=False``, the ``bit_terms`` absolutely *must* be all be valid values + /// of :class:`.SparseObservable.BitTerm`. If they are not, Rust-space undefined + /// behavior may occur, entirely invalidating the program execution. + /// + /// Examples: + /// + /// Construct a sum of :math:`Z` on each individual qubit:: + /// + /// >>> num_qubits = 100 + /// >>> terms = np.full((num_qubits,), SparseObservable.BitTerm.Z, dtype=np.uint8) + /// >>> indices = np.arange(num_qubits, dtype=np.uint32) + /// >>> coeffs = np.ones((num_qubits,), dtype=complex) + /// >>> boundaries = np.arange(num_qubits + 1, dtype=np.uintp) + /// >>> SparseObservable.from_raw_parts(num_qubits, coeffs, terms, indices, boundaries) + /// + #[deny(unsafe_op_in_unsafe_fn)] + #[staticmethod] + #[pyo3( + signature = (/, num_qubits, coeffs, bit_terms, indices, boundaries, check=true), + name = "from_raw_parts", + )] + unsafe fn py_from_raw_parts<'py>( + num_qubits: u32, + coeffs: PyArrayLike1<'py, Complex64>, + bit_terms: PyArrayLike1<'py, u8>, + indices: PyArrayLike1<'py, u32>, + boundaries: PyArrayLike1<'py, usize>, + check: bool, + ) -> PyResult { + let coeffs = coeffs.as_array().to_vec(); + let bit_terms = if check { + bit_terms + .as_array() + .into_iter() + .copied() + .map(BitTerm::try_from) + .collect::>()? + } else { + let bit_terms_as_u8 = bit_terms.as_array().to_vec(); + // SAFETY: the caller enforced that each `u8` is a valid `BitTerm`, and `BitTerm` is be + // represented by a `u8`. We can't use `bytemuck` because we're casting a `Vec`. + unsafe { ::std::mem::transmute::, Vec>(bit_terms_as_u8) } + }; + let indices = indices.as_array().to_vec(); + let boundaries = boundaries.as_array().to_vec(); + + if check { + Self::new(num_qubits, coeffs, bit_terms, indices, boundaries).map_err(PyErr::from) + } else { + // SAFETY: the caller promised they have upheld the coherence guarantees. + Ok(unsafe { Self::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries) }) + } + } + + /// Sum any like terms in this operator, removing them if the resulting complex coefficient has + /// an absolute value within tolerance of zero. + /// + /// As a side effect, this sorts the operator into :ref:`canonical order + /// `. + /// + /// .. note:: + /// + /// When using this for equality comparisons, note that floating-point rounding and the + /// non-associativity fo floating-point addition may cause non-zero coefficients of summed + /// terms to compare unequal. To compare two observables up to a tolerance, it is safest to + /// compare the canonicalized difference of the two observables to zero. + /// + /// Args: + /// tol (float): after summing like terms, any coefficients whose absolute value is less + /// than the given absolute tolerance will be suppressed from the output. + /// + /// Examples: + /// + /// Using :meth:`simplify` to compare two operators that represent the same observable, but + /// would compare unequal due to the structural tests by default:: + /// + /// >>> base = SparseObservable.from_sparse_list([ + /// ... ("XZ", (2, 1), 1e-10), # value too small + /// ... ("+-", (3, 1), 2j), + /// ... ("+-", (3, 1), 2j), # can be combined with the above + /// ... ("01", (3, 1), 0.5), # out of order compared to `expected` + /// ... ], num_qubits=5) + /// >>> expected = SparseObservable.from_list([("I0I1I", 0.5), ("I+I-I", 4j)]) + /// >>> assert base != expected # non-canonical comparison + /// >>> assert base.simplify() == expected.simplify() + /// + /// Note that in the above example, the coefficients are chosen such that all floating-point + /// calculations are exact, and there are no intermediate rounding or associativity + /// concerns. If this cannot be guaranteed to be the case, the safer form is:: + /// + /// >>> left = SparseObservable.from_list([("XYZ", 1.0/3.0)] * 3) # sums to 1.0 + /// >>> right = SparseObservable.from_list([("XYZ", 1.0/7.0)] * 7) # doesn't sum to 1.0 + /// >>> assert left.simplify() != right.simplify() + /// >>> assert (left - right).simplify() == SparseObservable.zero(left.num_qubits) + #[pyo3( + signature = (/, tol=1e-8), + name = "simplify", + )] + fn py_simplify(&self, tol: f64) -> SparseObservable { + self.canonicalize(tol) + } + + /// Tensor product of two observables. + /// + /// The bit ordering is defined such that the qubit indices of the argument will remain the + /// same, and the indices of ``self`` will be offset by the number of qubits in ``other``. This + /// is the same convention as used by the rest of Qiskit's :mod:`~qiskit.quantum_info` + /// operators. + /// + /// This function is used for the infix ``^`` operator. If using this operator, beware that + /// `Python's operator-precedence rules + /// `__ may cause the + /// evaluation order to be different to your expectation. In particular, the operator ``+`` + /// binds more tightly than ``^``, just like ``*`` binds more tightly than ``+``. Use + /// parentheses to fix the evaluation order, if needed. + /// + /// The argument will be cast to :class:`SparseObservable` using its default constructor, if it + /// is not already in the correct form. + /// + /// Args: + /// + /// other: the observable to put on the right-hand side of the tensor product. + /// + /// Examples: + /// + /// The bit ordering of this is such that the tensor product of two observables made from a + /// single label "looks like" an observable made by concatenating the two strings:: + /// + /// >>> left = SparseObservable.from_label("XYZ") + /// >>> right = SparseObservable.from_label("+-IIrl") + /// >>> assert left.tensor(right) == SparseObservable.from_label("XYZ+-IIrl") + /// + /// You can also use the infix ``^`` operator for tensor products, which will similarly cast + /// the right-hand side of the operation if it is not already a :class:`SparseObservable`:: + /// + /// >>> assert SparseObservable("rl") ^ Pauli("XYZ") == SparseObservable("rlXYZ") + /// + /// See also: + /// :meth:`expand` + /// + /// The same function, but with the order of arguments flipped. This can be useful if + /// you like using the casting behavior for the argument, but you want your existing + /// :class:`SparseObservable` to be on the right-hand side of the tensor ordering. + #[pyo3(signature = (other, /), name = "tensor")] + fn py_tensor(&self, other: &Bound) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Err(PyTypeError::new_err(format!( + "unknown type for tensor: {}", + other.get_type().repr()? + ))); + }; + Ok(self.tensor(&other.borrow()).into_py(py)) + } + + /// Reverse-order tensor product. + /// + /// This is equivalent to ``other.tensor(self)``, except that ``other`` will first be type-cast + /// to :class:`SparseObservable` if it isn't already one (by calling the default constructor). + /// + /// Args: + /// + /// other: the observable to put on the left-hand side of the tensor product. + /// + /// Examples: + /// + /// This is equivalent to :meth:`tensor` with the order of the arguments flipped:: + /// + /// >>> left = SparseObservable.from_label("XYZ") + /// >>> right = SparseObservable.from_label("+-IIrl") + /// >>> assert left.tensor(right) == right.expand(left) + /// + /// See also: + /// :meth:`tensor` + /// + /// The same function with the order of arguments flipped. :meth:`tensor` is the more + /// standard argument ordering, and matches Qiskit's other conventions. + #[pyo3(signature = (other, /), name = "expand")] + fn py_expand(&self, other: &Bound) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Err(PyTypeError::new_err(format!( + "unknown type for expand: {}", + other.get_type().repr()? + ))); + }; + Ok(other.borrow().tensor(self).into_py(py)) + } + + /// Calculate the adjoint of this observable. + /// + /// This is well defined in the abstract mathematical sense. All the terms of the single-qubit + /// alphabet are self-adjoint, so the result of this operation is the same observable, except + /// its coefficients are all their complex conjugates. + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> left = SparseObservable.from_list([("XY+-", 1j)]) + /// >>> right = SparseObservable.from_list([("XY+-", -1j)]) + /// >>> assert left.adjoint() == right + fn adjoint(&self) -> SparseObservable { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| c.conj()).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } + + /// Calculate the complex conjugation of this observable. + /// + /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the + /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related + /// alphabet terms are unaffected by the complex conjugation, but $Y$-related terms modify their + /// alphabet terms. Precisely: + /// + /// * :math:`Y` conjguates to :math:`-Y` + /// * :math:`\lvert r\rangle\langle r\rvert` conjugates to :math:`\lvert l\rangle\langle l\rvert` + /// * :math:`\lvert l\rangle\langle l\rvert` conjugates to :math:`\lvert r\rangle\langle r\rvert` + /// + /// Additionally, all coefficients are conjugated. + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) + /// >>> assert obs.conjugate() == SparseObservable([("III", -1j), ("Ylr", -0.5)]) + fn conjugate(&self) -> SparseObservable { + let mut out = self.transpose(); + for coeff in out.coeffs.iter_mut() { + *coeff = coeff.conj(); + } + out + } + + /// Calculate the matrix transposition of this observable. + /// + /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the + /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related + /// alphabet terms are unaffected by the transposition, but $Y$-related terms modify their + /// alphabet terms. Precisely: + /// + /// * :math:`Y` transposes to :math:`-Y` + /// * :math:`\lvert r\rangle\langle r\rvert` transposes to :math:`\lvert l\rangle\langle l\rvert` + /// * :math:`\lvert l\rangle\langle l\rvert` transposes to :math:`\lvert r\rangle\langle r\rvert` + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) + /// >>> assert obs.transpose() == SparseObservable([("III", 1j), ("Ylr", -0.5)]) + fn transpose(&self) -> SparseObservable { + let mut out = self.clone(); + for term in out.iter_mut() { + for bit_term in term.bit_terms { + match bit_term { + BitTerm::Y => { + *term.coeff = -*term.coeff; + } + BitTerm::Right => { + *bit_term = BitTerm::Left; + } + BitTerm::Left => { + *bit_term = BitTerm::Right; + } + _ => (), + } + } + } + out + } +} + +impl ::std::ops::Add<&SparseObservable> for SparseObservable { + type Output = SparseObservable; + + fn add(mut self, rhs: &SparseObservable) -> SparseObservable { + self += rhs; + self + } +} +impl ::std::ops::Add for &SparseObservable { + type Output = SparseObservable; + + fn add(self, rhs: &SparseObservable) -> SparseObservable { + let mut out = SparseObservable::with_capacity( + self.num_qubits, + self.coeffs.len() + rhs.coeffs.len(), + self.bit_terms.len() + rhs.bit_terms.len(), + ); + out += self; + out += rhs; + out + } +} +impl ::std::ops::AddAssign<&SparseObservable> for SparseObservable { + fn add_assign(&mut self, rhs: &SparseObservable) { + if self.num_qubits != rhs.num_qubits { + panic!("attempt to add two operators with incompatible qubit counts"); + } + self.coeffs.extend_from_slice(&rhs.coeffs); + self.bit_terms.extend_from_slice(&rhs.bit_terms); + self.indices.extend_from_slice(&rhs.indices); + // We only need to write out the new endpoints, not the initial zero. + let offset = self.boundaries[self.boundaries.len() - 1]; + self.boundaries + .extend(rhs.boundaries[1..].iter().map(|boundary| offset + boundary)); + } +} + +impl ::std::ops::Sub<&SparseObservable> for SparseObservable { + type Output = SparseObservable; + + fn sub(mut self, rhs: &SparseObservable) -> SparseObservable { + self -= rhs; + self + } +} +impl ::std::ops::Sub for &SparseObservable { + type Output = SparseObservable; + + fn sub(self, rhs: &SparseObservable) -> SparseObservable { + let mut out = SparseObservable::with_capacity( + self.num_qubits, + self.coeffs.len() + rhs.coeffs.len(), + self.bit_terms.len() + rhs.bit_terms.len(), + ); + out += self; + out -= rhs; + out + } +} +impl ::std::ops::SubAssign<&SparseObservable> for SparseObservable { + fn sub_assign(&mut self, rhs: &SparseObservable) { + if self.num_qubits != rhs.num_qubits { + panic!("attempt to subtract two operators with incompatible qubit counts"); + } + self.coeffs.extend(rhs.coeffs.iter().map(|coeff| -coeff)); + self.bit_terms.extend_from_slice(&rhs.bit_terms); + self.indices.extend_from_slice(&rhs.indices); + // We only need to write out the new endpoints, not the initial zero. + let offset = self.boundaries[self.boundaries.len() - 1]; + self.boundaries + .extend(rhs.boundaries[1..].iter().map(|boundary| offset + boundary)); + } +} + +impl ::std::ops::Mul for SparseObservable { + type Output = SparseObservable; + + fn mul(mut self, rhs: Complex64) -> SparseObservable { + self *= rhs; + self + } +} +impl ::std::ops::Mul for &SparseObservable { + type Output = SparseObservable; + + fn mul(self, rhs: Complex64) -> SparseObservable { + if rhs == Complex64::new(0.0, 0.0) { + SparseObservable::zero(self.num_qubits) + } else { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| c * rhs).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } + } +} +impl ::std::ops::Mul for Complex64 { + type Output = SparseObservable; + + fn mul(self, mut rhs: SparseObservable) -> SparseObservable { + rhs *= self; + rhs + } +} +impl ::std::ops::Mul<&SparseObservable> for Complex64 { + type Output = SparseObservable; + + fn mul(self, rhs: &SparseObservable) -> SparseObservable { + rhs * self + } +} +impl ::std::ops::MulAssign for SparseObservable { + fn mul_assign(&mut self, rhs: Complex64) { + if rhs == Complex64::new(0.0, 0.0) { + self.coeffs.clear(); + self.bit_terms.clear(); + self.indices.clear(); + self.boundaries.clear(); + self.boundaries.push(0); + } else { + self.coeffs.iter_mut().for_each(|c| *c *= rhs) + } + } +} + +impl ::std::ops::Div for SparseObservable { + type Output = SparseObservable; + + fn div(mut self, rhs: Complex64) -> SparseObservable { + self /= rhs; + self + } +} +impl ::std::ops::Div for &SparseObservable { + type Output = SparseObservable; + + fn div(self, rhs: Complex64) -> SparseObservable { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| c / rhs).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } +} +impl ::std::ops::DivAssign for SparseObservable { + fn div_assign(&mut self, rhs: Complex64) { + self.coeffs.iter_mut().for_each(|c| *c /= rhs) + } +} + +impl ::std::ops::Neg for &SparseObservable { + type Output = SparseObservable; + + fn neg(self) -> SparseObservable { + SparseObservable { + num_qubits: self.num_qubits, + coeffs: self.coeffs.iter().map(|c| -c).collect(), + bit_terms: self.bit_terms.clone(), + indices: self.indices.clone(), + boundaries: self.boundaries.clone(), + } + } +} +impl ::std::ops::Neg for SparseObservable { + type Output = SparseObservable; + + fn neg(mut self) -> SparseObservable { + self.coeffs.iter_mut().for_each(|c| *c = -*c); + self + } +} + +/// A view object onto a single term of a [SparseObservable]. +/// +/// The lengths of [bit_terms] and [indices] are guaranteed to be created equal, but might be zero +/// (in the case that the term is proportional to the identity). +#[derive(Clone, Copy, Debug)] +pub struct SparseTermView<'a> { + pub coeff: Complex64, + pub bit_terms: &'a [BitTerm], + pub indices: &'a [u32], +} + +/// A mutable view object onto a single term of a [SparseObservable]. +/// +/// The lengths of [bit_terms] and [indices] are guaranteed to be created equal, but might be zero +/// (in the case that the term is proportional to the identity). [indices] is not mutable because +/// this would allow data coherence to be broken. +#[derive(Debug)] +pub struct SparseTermViewMut<'a> { + pub coeff: &'a mut Complex64, + pub bit_terms: &'a mut [BitTerm], + pub indices: &'a [u32], +} + +/// Iterator type allowing in-place mutation of the [SparseObservable]. +/// +/// Created by [SparseObservable::iter_mut]. +#[derive(Debug)] +pub struct IterMut<'a> { + coeffs: &'a mut [Complex64], + bit_terms: &'a mut [BitTerm], + indices: &'a [u32], + boundaries: &'a [usize], + i: usize, +} +impl<'a> From<&'a mut SparseObservable> for IterMut<'a> { + fn from(value: &mut SparseObservable) -> IterMut { + IterMut { + coeffs: &mut value.coeffs, + bit_terms: &mut value.bit_terms, + indices: &value.indices, + boundaries: &value.boundaries, + i: 0, + } + } +} +impl<'a> Iterator for IterMut<'a> { + type Item = SparseTermViewMut<'a>; + + fn next(&mut self) -> Option { + // The trick here is that the lifetime of the 'self' borrow is shorter than the lifetime of + // the inner borrows. We can't give out mutable references to our inner borrow, because + // after the lifetime on 'self' expired, there'd be nothing preventing somebody using the + // 'self' borrow to see _another_ mutable borrow of the inner data, which would be an + // aliasing violation. Instead, we keep splitting the inner views we took out so the + // mutable references we return don't overlap with the ones we continue to hold. + let coeffs = ::std::mem::take(&mut self.coeffs); + let (coeff, other_coeffs) = coeffs.split_first_mut()?; + self.coeffs = other_coeffs; + + let len = self.boundaries[self.i + 1] - self.boundaries[self.i]; + self.i += 1; + + let all_bit_terms = ::std::mem::take(&mut self.bit_terms); + let all_indices = ::std::mem::take(&mut self.indices); + let (bit_terms, rest_bit_terms) = all_bit_terms.split_at_mut(len); + let (indices, rest_indices) = all_indices.split_at(len); + self.bit_terms = rest_bit_terms; + self.indices = rest_indices; + + Some(SparseTermViewMut { + coeff, + bit_terms, + indices, + }) + } + + fn size_hint(&self) -> (usize, Option) { + (self.coeffs.len(), Some(self.coeffs.len())) + } +} +impl<'a> ExactSizeIterator for IterMut<'a> {} +impl<'a> ::std::iter::FusedIterator for IterMut<'a> {} + +/// Helper class of `ArrayView` that denotes the slot of the `SparseObservable` we're looking at. +#[derive(Clone, Copy, PartialEq, Eq)] +enum ArraySlot { + Coeffs, + BitTerms, + Indices, + Boundaries, +} + +/// Custom wrapper sequence class to get safe views onto the Rust-space data. We can't directly +/// expose Python-managed wrapped pointers without introducing some form of runtime exclusion on the +/// ability of `SparseObservable` to re-allocate in place; we can't leave dangling pointers for +/// Python space. +#[pyclass(frozen, sequence)] +struct ArrayView { + base: Py, + slot: ArraySlot, +} +#[pymethods] +impl ArrayView { + fn __repr__(&self, py: Python) -> PyResult { + let obs = self.base.borrow(py); + let data = match self.slot { + // Simple integers look the same in Rust-space debug as Python. + ArraySlot::Indices => format!("{:?}", obs.indices), + ArraySlot::Boundaries => format!("{:?}", obs.boundaries), + // Complexes don't have a nice repr in Rust, so just delegate the whole load to Python + // and convert back. + ArraySlot::Coeffs => PyList::new_bound(py, &obs.coeffs).repr()?.to_string(), + ArraySlot::BitTerms => format!( + "[{}]", + obs.bit_terms + .iter() + .map(BitTerm::py_label) + .collect::>() + .join(", ") + ), + }; + Ok(format!( + "", + match self.slot { + ArraySlot::Coeffs => "coeffs", + ArraySlot::BitTerms => "bit_terms", + ArraySlot::Indices => "indices", + ArraySlot::Boundaries => "boundaries", + }, + data, + )) + } + + fn __getitem__(&self, py: Python, index: PySequenceIndex) -> PyResult> { + // The slightly verbose generic setup here is to allow the type of a scalar return to be + // different to the type that gets put into the Numpy array, since the `BitTerm` enum can be + // a direct scalar, but for Numpy, we need it to be a raw `u8`. + fn get_from_slice( + py: Python, + slice: &[T], + index: PySequenceIndex, + ) -> PyResult> + where + T: ToPyObject + Copy + Into, + S: ::numpy::Element, + { + match index.with_len(slice.len())? { + SequenceIndex::Int(index) => Ok(slice[index].to_object(py)), + indices => Ok(PyArray1::from_iter_bound( + py, + indices.iter().map(|index| slice[index].into()), + ) + .into_any() + .unbind()), + } + } + + let obs = self.base.borrow(py); + match self.slot { + ArraySlot::Coeffs => get_from_slice::<_, Complex64>(py, &obs.coeffs, index), + ArraySlot::BitTerms => get_from_slice::<_, u8>(py, &obs.bit_terms, index), + ArraySlot::Indices => get_from_slice::<_, u32>(py, &obs.indices, index), + ArraySlot::Boundaries => get_from_slice::<_, usize>(py, &obs.boundaries, index), + } + } + + fn __setitem__(&self, index: PySequenceIndex, values: &Bound) -> PyResult<()> { + /// Set values of a slice according to the indexer, using `extract` to retrieve the + /// Rust-space object from the collection of Python-space values. + /// + /// This indirects the Python extraction through an intermediate type to marginally improve + /// the error messages for things like `BitTerm`, where Python-space extraction might fail + /// because the user supplied an invalid alphabet letter. + /// + /// This allows broadcasting a single item into many locations in a slice (like Numpy), but + /// otherwise requires that the index and values are the same length (unlike Python's + /// `list`) because that would change the length. + fn set_in_slice<'py, T, S>( + slice: &mut [T], + index: PySequenceIndex<'py>, + values: &Bound<'py, PyAny>, + ) -> PyResult<()> + where + T: Copy + TryFrom, + S: FromPyObject<'py>, + PyErr: From<>::Error>, + { + match index.with_len(slice.len())? { + SequenceIndex::Int(index) => { + slice[index] = values.extract::()?.try_into()?; + Ok(()) + } + indices => { + if let Ok(value) = values.extract::() { + let value = value.try_into()?; + for index in indices { + slice[index] = value; + } + } else { + let values = values + .iter()? + .map(|value| value?.extract::()?.try_into().map_err(PyErr::from)) + .collect::>>()?; + if indices.len() != values.len() { + return Err(PyValueError::new_err(format!( + "tried to set a slice of length {} with a sequence of length {}", + indices.len(), + values.len(), + ))); + } + for (index, value) in indices.into_iter().zip(values) { + slice[index] = value; + } + } + Ok(()) + } + } + } + + let mut obs = self.base.borrow_mut(values.py()); + match self.slot { + ArraySlot::Coeffs => set_in_slice::<_, Complex64>(&mut obs.coeffs, index, values), + ArraySlot::BitTerms => set_in_slice::(&mut obs.bit_terms, index, values), + ArraySlot::Indices => set_in_slice::<_, u32>(&mut obs.indices, index, values), + ArraySlot::Boundaries => set_in_slice::<_, usize>(&mut obs.boundaries, index, values), + } + } + + fn __len__(&self, py: Python) -> usize { + let obs = self.base.borrow(py); + match self.slot { + ArraySlot::Coeffs => obs.coeffs.len(), + ArraySlot::BitTerms => obs.bit_terms.len(), + ArraySlot::Indices => obs.indices.len(), + ArraySlot::Boundaries => obs.boundaries.len(), + } + } + + #[pyo3(signature = (/, dtype=None, copy=None))] + fn __array__<'py>( + &self, + py: Python<'py>, + dtype: Option<&Bound<'py, PyAny>>, + copy: Option, + ) -> PyResult> { + // This method always copies, so we don't leave dangling pointers lying around in Numpy + // arrays; it's not enough just to set the `base` of the Numpy array to the + // `SparseObservable`, since the `Vec` we're referring to might re-allocate and invalidate + // the pointer the Numpy array is wrapping. + if !copy.unwrap_or(true) { + return Err(PyValueError::new_err( + "cannot produce a safe view onto movable memory", + )); + } + let obs = self.base.borrow(py); + match self.slot { + ArraySlot::Coeffs => { + cast_array_type(py, PyArray1::from_slice_bound(py, &obs.coeffs), dtype) + } + ArraySlot::Indices => { + cast_array_type(py, PyArray1::from_slice_bound(py, &obs.indices), dtype) + } + ArraySlot::Boundaries => { + cast_array_type(py, PyArray1::from_slice_bound(py, &obs.boundaries), dtype) + } + ArraySlot::BitTerms => { + let bit_terms: &[u8] = ::bytemuck::cast_slice(&obs.bit_terms); + cast_array_type(py, PyArray1::from_slice_bound(py, bit_terms), dtype) + } + } + } +} + +/// Use the Numpy Python API to convert a `PyArray` into a dynamically chosen `dtype`, copying only +/// if required. +fn cast_array_type<'py, T>( + py: Python<'py>, + array: Bound<'py, PyArray1>, + dtype: Option<&Bound<'py, PyAny>>, +) -> PyResult> { + let base_dtype = array.dtype(); + let dtype = dtype + .map(|dtype| PyArrayDescr::new_bound(py, dtype)) + .unwrap_or_else(|| Ok(base_dtype.clone()))?; + if dtype.is_equiv_to(&base_dtype) { + return Ok(array.into_any()); + } + PyModule::import_bound(py, intern!(py, "numpy"))? + .getattr(intern!(py, "array"))? + .call( + (array,), + Some( + &[ + (intern!(py, "copy"), NUMPY_COPY_ONLY_IF_NEEDED.get_bound(py)), + (intern!(py, "dtype"), dtype.as_any()), + ] + .into_py_dict_bound(py), + ), + ) + .map(|obj| obj.into_any()) +} + +/// Attempt to coerce an arbitrary Python object to a [SparseObservable]. +/// +/// This returns: +/// +/// * `Ok(Some(obs))` if the coercion was completely successful. +/// * `Ok(None)` if the input value was just completely the wrong type and no coercion could be +/// attempted. +/// * `Err` if the input was a valid type for coercion, but the coercion failed with a Python +/// exception. +/// +/// The purpose of this is for conversion the arithmetic operations, which should return +/// [PyNotImplemented] if the type is not valid for coercion. +fn coerce_to_observable<'py>( + value: &Bound<'py, PyAny>, +) -> PyResult>> { + let py = value.py(); + if let Ok(obs) = value.downcast_exact::() { + return Ok(Some(obs.clone())); + } + match SparseObservable::py_new(value, None) { + Ok(obs) => Ok(Some(Bound::new(py, obs)?)), + Err(e) => { + if e.is_instance_of::(py) { + Ok(None) + } else { + Err(e) + } + } + } +} + +pub fn sparse_observable(m: &Bound) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_error_safe_add_dense_label() { + let base = SparseObservable::identity(5); + let mut modified = base.clone(); + assert!(matches!( + modified.add_dense_label("IIZ$X", Complex64::new(1.0, 0.0)), + Err(LabelError::OutsideAlphabet) + )); + // `modified` should have been left in a valid state. + assert_eq!(base, modified); + } +} diff --git a/crates/accelerate/src/split_2q_unitaries.rs b/crates/accelerate/src/split_2q_unitaries.rs index f3a3d9454980..6950ea590212 100644 --- a/crates/accelerate/src/split_2q_unitaries.rs +++ b/crates/accelerate/src/split_2q_unitaries.rs @@ -33,15 +33,18 @@ pub fn split_2q_unitaries( for node in nodes { if let NodeType::Operation(inst) = &dag.dag()[node] { let qubits = dag.get_qargs(inst.qubits).to_vec(); - let matrix = inst.op.matrix(inst.params_view()); // We only attempt to split UnitaryGate objects, but this could be extended in future // -- however we need to ensure that we can compile the resulting single-qubit unitaries // to the supported basis gate set. if qubits.len() != 2 || inst.op.name() != "unitary" { continue; } + let matrix = inst + .op + .matrix(inst.params_view()) + .expect("'unitary' gates should always have a matrix form"); let decomp = TwoQubitWeylDecomposition::new_inner( - matrix.unwrap().view(), + matrix.view(), Some(requested_fidelity), None, )?; diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index 54ab34341a4c..9e6c220818a3 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -21,7 +21,7 @@ use std::ops::Index; use ahash::RandomState; use hashbrown::HashSet; -use indexmap::{IndexMap, IndexSet}; +use indexmap::IndexMap; use itertools::Itertools; use nullable_index_map::NullableIndexMap; use pyo3::{ @@ -57,7 +57,7 @@ type GateMapState = Vec<(String, Vec<(Option, Option u32 { + /// Gets the number of qubits of a [TargetOperation], will panic if the operation is [TargetOperation::Variadic]. + pub fn num_qubits(&self) -> u32 { match &self { - Self::Normal(normal) => normal.operation.view().num_qubits(), + Self::Normal(normal) => normal.operation.num_qubits(), Self::Variadic(_) => { - unreachable!("'num_qubits' property is reserved for normal operations only.") + panic!("'num_qubits' property doesn't exist for Variadic operations") } } } - fn params(&self) -> &[Param] { + /// Gets the parameters of a [TargetOperation], will panic if the operation is [TargetOperation::Variadic]. + pub fn params(&self) -> &[Param] { match &self { TargetOperation::Normal(normal) => normal.params.as_slice(), - TargetOperation::Variadic(_) => &[], + TargetOperation::Variadic(_) => { + panic!("'parameters' property doesn't exist for Variadic operations") + } } } } @@ -173,7 +177,6 @@ pub(crate) struct Target { #[pyo3(get)] _gate_name_map: IndexMap, global_operations: IndexMap, RandomState>, - variable_class_operations: IndexSet, qarg_gate_map: NullableIndexMap>>, non_global_strict_basis: Option>, non_global_basis: Option>, @@ -269,7 +272,6 @@ impl Target { concurrent_measurements, gate_map: GateMap::default(), _gate_name_map: IndexMap::default(), - variable_class_operations: IndexSet::default(), global_operations: IndexMap::default(), qarg_gate_map: NullableIndexMap::default(), non_global_basis: None, @@ -302,16 +304,15 @@ impl Target { ))); } let mut qargs_val: PropsMap; - match instruction { + match &instruction { TargetOperation::Variadic(_) => { qargs_val = PropsMap::with_capacity(1); qargs_val.extend([(None, None)]); - self.variable_class_operations.insert(name.to_string()); } - TargetOperation::Normal(_) => { + TargetOperation::Normal(normal) => { if let Some(mut properties) = properties { qargs_val = PropsMap::with_capacity(properties.len()); - let inst_num_qubits = instruction.num_qubits(); + let inst_num_qubits = normal.operation.view().num_qubits(); if properties.contains_key(None) { self.global_operations .entry(inst_num_qubits) @@ -619,7 +620,7 @@ impl Target { } else if let Some(operation_name) = operation_name { if let Some(parameters) = parameters { if let Some(obj) = self._gate_name_map.get(&operation_name) { - if self.variable_class_operations.contains(&operation_name) { + if matches!(obj, TargetOperation::Variadic(_)) { if let Some(_qargs) = qargs { let qarg_set: HashSet = _qargs.iter().cloned().collect(); return Ok(_qargs @@ -1053,8 +1054,8 @@ impl Target { if let Some(Some(qarg_gate_map_arg)) = self.qarg_gate_map.get(qargs).as_ref() { res.extend(qarg_gate_map_arg.iter().map(|key| key.as_str())); } - for name in self._gate_name_map.keys() { - if self.variable_class_operations.contains(name) { + for (name, obj) in self._gate_name_map.iter() { + if matches!(obj, TargetOperation::Variadic(_)) { res.insert(name); } } @@ -1160,34 +1161,40 @@ impl Target { } if gate_prop_name.contains_key(None) { let obj = &self._gate_name_map[operation_name]; - if self.variable_class_operations.contains(operation_name) { + match obj { + TargetOperation::Variadic(_) => { + return qargs.is_none() + || _qargs.iter().all(|qarg| { + qarg.index() <= self.num_qubits.unwrap_or_default() + }) && qarg_set.len() == _qargs.len(); + } + TargetOperation::Normal(obj) => { + let qubit_comparison = obj.operation.num_qubits(); + return qubit_comparison == _qargs.len() as u32 + && _qargs.iter().all(|qarg| { + qarg.index() < self.num_qubits.unwrap_or_default() + }); + } + } + } + } else { + // Duplicate case is if it contains none + let obj = &self._gate_name_map[operation_name]; + match obj { + TargetOperation::Variadic(_) => { return qargs.is_none() || _qargs.iter().all(|qarg| { qarg.index() <= self.num_qubits.unwrap_or_default() }) && qarg_set.len() == _qargs.len(); - } else { - let qubit_comparison = obj.num_qubits(); + } + TargetOperation::Normal(obj) => { + let qubit_comparison = obj.operation.num_qubits(); return qubit_comparison == _qargs.len() as u32 && _qargs.iter().all(|qarg| { qarg.index() < self.num_qubits.unwrap_or_default() }); } } - } else { - // Duplicate case is if it contains none - if self.variable_class_operations.contains(operation_name) { - return qargs.is_none() - || _qargs - .iter() - .all(|qarg| qarg.index() <= self.num_qubits.unwrap_or_default()) - && qarg_set.len() == _qargs.len(); - } else { - let qubit_comparison = self._gate_name_map[operation_name].num_qubits(); - return qubit_comparison == _qargs.len() as u32 - && _qargs - .iter() - .all(|qarg| qarg.index() < self.num_qubits.unwrap_or_default()); - } } } else { return true; diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 7f20caea104f..fb8c58baab9d 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -510,6 +510,15 @@ pub struct TwoQubitWeylDecomposition { } impl TwoQubitWeylDecomposition { + pub fn a(&self) -> f64 { + self.a + } + pub fn b(&self) -> f64 { + self.b + } + pub fn c(&self) -> f64 { + self.c + } fn weyl_gate( &self, simplify: bool, @@ -1231,6 +1240,7 @@ impl TwoQubitWeylDecomposition { type TwoQubitSequenceVec = Vec<(Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>)>; +#[derive(Clone, Debug)] #[pyclass(sequence)] pub struct TwoQubitGateSequence { gates: TwoQubitSequenceVec, @@ -1238,10 +1248,31 @@ pub struct TwoQubitGateSequence { global_phase: f64, } +impl TwoQubitGateSequence { + pub fn gates(&self) -> &TwoQubitSequenceVec { + &self.gates + } + + pub fn global_phase(&self) -> f64 { + self.global_phase + } + + pub fn set_state(&mut self, state: (TwoQubitSequenceVec, f64)) { + self.gates = state.0; + self.global_phase = state.1; + } +} + +impl Default for TwoQubitGateSequence { + fn default() -> Self { + Self::new() + } +} + #[pymethods] impl TwoQubitGateSequence { #[new] - fn new() -> Self { + pub fn new() -> Self { TwoQubitGateSequence { gates: Vec::new(), global_phase: 0., @@ -1273,6 +1304,8 @@ impl TwoQubitGateSequence { } } } + +#[derive(Clone, Debug)] #[allow(non_snake_case)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitBasisDecomposer { @@ -1660,7 +1693,7 @@ impl TwoQubitBasisDecomposer { Ok(res) } - fn new_inner( + pub fn new_inner( gate: String, gate_matrix: ArrayView2, basis_fidelity: f64, @@ -1798,7 +1831,7 @@ impl TwoQubitBasisDecomposer { }) } - fn call_inner( + pub fn call_inner( &self, unitary: ArrayView2, basis_fidelity: Option, diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs new file mode 100644 index 000000000000..b5ef66db4f1e --- /dev/null +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -0,0 +1,1081 @@ +// 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. +#![allow(clippy::too_many_arguments)] + +#[cfg(feature = "cache_pygates")] +use std::cell::OnceCell; +use std::f64::consts::PI; + +use approx::relative_eq; +use hashbrown::{HashMap, HashSet}; +use indexmap::IndexMap; +use itertools::Itertools; +use ndarray::prelude::*; +use num_complex::{Complex, Complex64}; +use numpy::IntoPyArray; +use qiskit_circuit::circuit_instruction::{ExtraInstructionAttributes, OperationFromPython}; +use smallvec::{smallvec, SmallVec}; + +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::types::{IntoPyDict, PyDict, PyString}; +use pyo3::wrap_pyfunction; +use pyo3::Python; + +use qiskit_circuit::converters::{circuit_to_dag, QuantumCircuitData}; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; +use qiskit_circuit::imports; +use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; +use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation}; +use qiskit_circuit::Qubit; + +use crate::euler_one_qubit_decomposer::{ + unitary_to_gate_sequence_inner, EulerBasis, EulerBasisSet, EULER_BASES, EULER_BASIS_NAMES, +}; +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::{NormalOperation, Target}; +use crate::two_qubit_decompose::{ + TwoQubitBasisDecomposer, TwoQubitGateSequence, TwoQubitWeylDecomposition, +}; +use crate::QiskitError; + +const PI2: f64 = PI / 2.; +const PI4: f64 = PI / 4.; + +#[derive(Clone, Debug)] +enum DecomposerType { + TwoQubitBasisDecomposer(Box), + XXDecomposer(PyObject), +} + +struct DecomposerElement { + decomposer: DecomposerType, + gate: NormalOperation, +} + +#[derive(Clone, Debug)] +struct TwoQubitUnitarySequence { + gate_sequence: TwoQubitGateSequence, + decomp_gate: NormalOperation, +} + +// Used in get_2q_decomposers. If the found 2q basis is a subset of GOODBYE_SET, +// then we know TwoQubitBasisDecomposer is an ideal decomposition and there is +// no need to bother trying the XXDecomposer. +static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"]; + +fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet { + let mut target_basis_set: EulerBasisSet = EulerBasisSet::new(); + let target_basis_list = target.operation_names_for_qargs(Some(&smallvec![qubit])); + match target_basis_list { + Ok(basis_list) => { + EULER_BASES + .iter() + .enumerate() + .filter_map(|(idx, gates)| { + if !gates.iter().all(|gate| basis_list.contains(gate)) { + return None; + } + let basis = EULER_BASIS_NAMES[idx]; + Some(basis) + }) + .for_each(|basis| target_basis_set.add_basis(basis)); + } + Err(_) => target_basis_set.support_all(), + } + if target_basis_set.basis_supported(EulerBasis::U3) + && target_basis_set.basis_supported(EulerBasis::U321) + { + target_basis_set.remove(EulerBasis::U3); + } + if target_basis_set.basis_supported(EulerBasis::ZSX) + && target_basis_set.basis_supported(EulerBasis::ZSXX) + { + target_basis_set.remove(EulerBasis::ZSX); + } + target_basis_set +} + +fn apply_synth_dag( + py: Python<'_>, + out_dag: &mut DAGCircuit, + out_qargs: &[Qubit], + synth_dag: &DAGCircuit, +) -> PyResult<()> { + for out_node in synth_dag.topological_op_nodes()? { + let mut out_packed_instr = synth_dag.dag()[out_node].unwrap_operation().clone(); + let synth_qargs = synth_dag.get_qargs(out_packed_instr.qubits); + let mapped_qargs: Vec = synth_qargs + .iter() + .map(|qarg| out_qargs[qarg.0 as usize]) + .collect(); + out_packed_instr.qubits = out_dag.qargs_interner.insert(&mapped_qargs); + out_dag.push_back(py, out_packed_instr)?; + } + out_dag.add_global_phase(py, &synth_dag.get_global_phase())?; + Ok(()) +} + +fn apply_synth_sequence( + py: Python<'_>, + out_dag: &mut DAGCircuit, + out_qargs: &[Qubit], + sequence: &TwoQubitUnitarySequence, +) -> PyResult<()> { + let mut instructions = Vec::with_capacity(sequence.gate_sequence.gates().len()); + for (gate, params, qubit_ids) in sequence.gate_sequence.gates() { + let gate_node = match gate { + None => sequence.decomp_gate.operation.standard_gate(), + Some(gate) => *gate, + }; + let mapped_qargs: Vec = qubit_ids.iter().map(|id| out_qargs[*id as usize]).collect(); + let new_params: Option>> = match gate { + Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), + None => Some(Box::new(sequence.decomp_gate.params.clone())), + }; + let instruction = PackedInstruction { + op: PackedOperation::from_standard(gate_node), + qubits: out_dag.qargs_interner.insert(&mapped_qargs), + clbits: out_dag.cargs_interner.get_default(), + params: new_params, + extra_attrs: ExtraInstructionAttributes::default(), + #[cfg(feature = "cache_pygates")] + py_op: OnceCell::new(), + }; + instructions.push(instruction); + } + out_dag.extend(py, instructions.into_iter())?; + out_dag.add_global_phase(py, &Param::Float(sequence.gate_sequence.global_phase()))?; + Ok(()) +} + +fn synth_error( + py: Python<'_>, + synth_circuit: impl Iterator< + Item = ( + String, + Option>, + SmallVec<[PhysicalQubit; 2]>, + ), + >, + target: &Target, +) -> f64 { + let (lower_bound, upper_bound) = synth_circuit.size_hint(); + let mut gate_fidelities = match upper_bound { + Some(bound) => Vec::with_capacity(bound), + None => Vec::with_capacity(lower_bound), + }; + let mut score_instruction = + |inst_name: &str, + inst_params: &Option>, + inst_qubits: &SmallVec<[PhysicalQubit; 2]>| { + if let Ok(names) = target.operation_names_for_qargs(Some(inst_qubits)) { + for name in names { + if let Ok(target_op) = target.operation_from_name(name) { + let are_params_close = if let Some(params) = inst_params { + params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { + p1.is_close(py, p2, 1e-10) + .expect("Unexpected parameter expression error.") + }) + } else { + false + }; + let is_parametrized = target_op + .params + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))); + if target_op.operation.name() == inst_name + && (is_parametrized || are_params_close) + { + match target[name].get(Some(inst_qubits)) { + Some(Some(props)) => { + gate_fidelities.push(1.0 - props.error.unwrap_or(0.0)) + } + _ => gate_fidelities.push(1.0), + } + break; + } + } + } + } + }; + + for (inst_name, inst_params, inst_qubits) in synth_circuit { + score_instruction(&inst_name, &inst_params, &inst_qubits); + } + 1.0 - gate_fidelities.into_iter().product::() +} + +// This is the outer-most run function. It is meant to be called from Python +// in `UnitarySynthesis.run()`. +#[pyfunction] +#[pyo3(name = "run_default_main_loop", signature=(dag, qubit_indices, min_qubits, target, coupling_edges, approximation_degree=None, natural_direction=None))] +fn py_run_main_loop( + py: Python, + dag: &mut DAGCircuit, + qubit_indices: Vec, + min_qubits: usize, + target: &Target, + coupling_edges: HashSet<[PhysicalQubit; 2]>, + approximation_degree: Option, + natural_direction: Option, +) -> PyResult { + // We need to use the python converter because the currently available Rust conversion + // is lossy. We need `QuantumCircuit` instances to be used in `replace_blocks`. + let dag_to_circuit = imports::DAG_TO_CIRCUIT.get_bound(py); + + let mut out_dag = dag.copy_empty_like(py, "alike")?; + + // Iterate over dag nodes and determine unitary synthesis approach + for node in dag.topological_op_nodes()? { + let mut packed_instr = dag.dag()[node].unwrap_operation().clone(); + + if packed_instr.op.control_flow() { + let OperationRef::Instruction(py_instr) = packed_instr.op.view() else { + unreachable!("Control flow op must be an instruction") + }; + let raw_blocks: Vec>> = py_instr + .instruction + .getattr(py, "blocks")? + .bind(py) + .iter()? + .collect(); + let mut new_blocks = Vec::with_capacity(raw_blocks.len()); + for raw_block in raw_blocks { + let new_ids = dag + .get_qargs(packed_instr.qubits) + .iter() + .map(|qarg| qubit_indices[qarg.0 as usize]) + .collect_vec(); + let res = py_run_main_loop( + py, + &mut circuit_to_dag( + py, + QuantumCircuitData::extract_bound(&raw_block?)?, + false, + None, + None, + )?, + new_ids, + min_qubits, + target, + coupling_edges.clone(), + approximation_degree, + natural_direction, + )?; + new_blocks.push(dag_to_circuit.call1((res,))?); + } + let new_node = py_instr + .instruction + .bind(py) + .call_method1("replace_blocks", (new_blocks,))?; + let new_node_op: OperationFromPython = new_node.extract()?; + packed_instr = PackedInstruction { + op: new_node_op.operation, + qubits: packed_instr.qubits, + clbits: packed_instr.clbits, + params: (!new_node_op.params.is_empty()).then(|| Box::new(new_node_op.params)), + extra_attrs: new_node_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: new_node.unbind().into(), + }; + } + if !(packed_instr.op.name() == "unitary" + && packed_instr.op.num_qubits() >= min_qubits as u32) + { + out_dag.push_back(py, packed_instr)?; + continue; + } + let unitary: Array, Dim<[usize; 2]>> = match packed_instr.op.matrix(&[]) { + Some(unitary) => unitary, + None => return Err(QiskitError::new_err("Unitary not found")), + }; + match unitary.shape() { + // Run 1q synthesis + [2, 2] => { + let qubit = dag.get_qargs(packed_instr.qubits)[0]; + let target_basis_set = get_target_basis_set(target, PhysicalQubit::new(qubit.0)); + let sequence = unitary_to_gate_sequence_inner( + unitary.view(), + &target_basis_set, + qubit.0 as usize, + None, + true, + None, + ); + match sequence { + Some(sequence) => { + for (gate, params) in sequence.gates { + let new_params: SmallVec<[Param; 3]> = + params.iter().map(|p| Param::Float(*p)).collect(); + out_dag.apply_operation_back( + py, + gate.into(), + &[qubit], + &[], + Some(new_params), + ExtraInstructionAttributes::default(), + #[cfg(feature = "cache_pygates")] + None, + )?; + } + out_dag.add_global_phase(py, &Param::Float(sequence.global_phase))?; + } + None => { + out_dag.push_back(py, packed_instr)?; + } + } + } + // Run 2q synthesis + [4, 4] => { + // "out_qargs" is used to append the synthesized instructions to the output dag + let out_qargs = dag.get_qargs(packed_instr.qubits); + // "ref_qubits" is used to access properties in the target. It accounts for control flow mapping. + let ref_qubits: &[PhysicalQubit; 2] = &[ + PhysicalQubit::new(qubit_indices[out_qargs[0].0 as usize] as u32), + PhysicalQubit::new(qubit_indices[out_qargs[1].0 as usize] as u32), + ]; + let apply_original_op = |out_dag: &mut DAGCircuit| -> PyResult<()> { + out_dag.push_back(py, packed_instr.clone())?; + Ok(()) + }; + run_2q_unitary_synthesis( + py, + unitary, + ref_qubits, + &coupling_edges, + target, + approximation_degree, + natural_direction, + &mut out_dag, + out_qargs, + apply_original_op, + )?; + } + // Run 3q+ synthesis + _ => { + let qs_decomposition: &Bound<'_, PyAny> = imports::QS_DECOMPOSITION.get_bound(py); + let synth_circ = qs_decomposition.call1((unitary.into_pyarray_bound(py),))?; + let synth_dag = circuit_to_dag( + py, + QuantumCircuitData::extract_bound(&synth_circ)?, + false, + None, + None, + )?; + out_dag = synth_dag; + } + } + } + Ok(out_dag) +} + +fn run_2q_unitary_synthesis( + py: Python, + unitary: Array2, + ref_qubits: &[PhysicalQubit; 2], + coupling_edges: &HashSet<[PhysicalQubit; 2]>, + target: &Target, + approximation_degree: Option, + natural_direction: Option, + out_dag: &mut DAGCircuit, + out_qargs: &[Qubit], + mut apply_original_op: impl FnMut(&mut DAGCircuit) -> PyResult<()>, +) -> PyResult<()> { + let decomposers = { + let decomposers_2q = + get_2q_decomposers_from_target(py, target, ref_qubits, approximation_degree)?; + decomposers_2q.unwrap_or_default() + }; + // If there's a single decomposer, avoid computing synthesis score + if decomposers.len() == 1 { + let decomposer_item = decomposers.first().unwrap(); + let preferred_dir = preferred_direction( + decomposer_item, + ref_qubits, + natural_direction, + coupling_edges, + target, + )?; + match decomposer_item.decomposer { + DecomposerType::TwoQubitBasisDecomposer(_) => { + let synth = synth_su4_sequence( + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_sequence(py, out_dag, out_qargs, &synth)?; + } + DecomposerType::XXDecomposer(_) => { + let synth = synth_su4_dag( + py, + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_dag(py, out_dag, out_qargs, &synth)?; + } + } + return Ok(()); + } + + let mut synth_errors_sequence = Vec::new(); + let mut synth_errors_dag = Vec::new(); + for decomposer in &decomposers { + let preferred_dir = preferred_direction( + decomposer, + ref_qubits, + natural_direction, + coupling_edges, + target, + )?; + match &decomposer.decomposer { + DecomposerType::TwoQubitBasisDecomposer(_) => { + let sequence = + synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; + let scoring_info = + sequence + .gate_sequence + .gates() + .iter() + .map(|(gate, params, qubit_ids)| { + let inst_qubits = + qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); + match gate { + Some(gate) => ( + gate.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + None => ( + sequence + .decomp_gate + .operation + .standard_gate() + .name() + .to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + } + }); + let synth_error_from_target = synth_error(py, scoring_info, target); + synth_errors_sequence.push((sequence, synth_error_from_target)); + } + DecomposerType::XXDecomposer(_) => { + let synth_dag = synth_su4_dag( + py, + &unitary, + decomposer, + preferred_dir, + approximation_degree, + )?; + let scoring_info = synth_dag + .topological_op_nodes() + .expect("Unexpected error in dag.topological_op_nodes()") + .map(|node| { + let NodeType::Operation(inst) = &synth_dag.dag()[node] else { + unreachable!("DAG node must be an instruction") + }; + let inst_qubits = synth_dag + .get_qargs(inst.qubits) + .iter() + .map(|q| ref_qubits[q.0 as usize]) + .collect(); + ( + inst.op.name().to_string(), + inst.params.clone().map(|boxed| *boxed), + inst_qubits, + ) + }); + let synth_error_from_target = synth_error(py, scoring_info, target); + synth_errors_dag.push((synth_dag, synth_error_from_target)); + } + } + } + + let synth_sequence = synth_errors_sequence + .iter() + .enumerate() + .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) + .map(|(index, _)| &synth_errors_sequence[index]); + + let synth_dag = synth_errors_dag + .iter() + .enumerate() + .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) + .map(|(index, _)| &synth_errors_dag[index]); + + match (synth_sequence, synth_dag) { + (None, None) => apply_original_op(out_dag)?, + (Some((sequence, _)), None) => apply_synth_sequence(py, out_dag, out_qargs, sequence)?, + (None, Some((dag, _))) => apply_synth_dag(py, out_dag, out_qargs, dag)?, + (Some((sequence, sequence_error)), Some((dag, dag_error))) => { + if sequence_error > dag_error { + apply_synth_dag(py, out_dag, out_qargs, dag)? + } else { + apply_synth_sequence(py, out_dag, out_qargs, sequence)? + } + } + }; + Ok(()) +} + +fn get_2q_decomposers_from_target( + py: Python, + target: &Target, + qubits: &[PhysicalQubit; 2], + approximation_degree: Option, +) -> PyResult>> { + let qubits: SmallVec<[PhysicalQubit; 2]> = SmallVec::from_buf(*qubits); + let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); + let mut available_2q_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); + let mut available_2q_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); + + let mut qubit_gate_map = IndexMap::new(); + match target.operation_names_for_qargs(Some(&qubits)) { + Ok(direct_keys) => { + qubit_gate_map.insert(&qubits, direct_keys); + if let Ok(reverse_keys) = target.operation_names_for_qargs(Some(&reverse_qubits)) { + qubit_gate_map.insert(&reverse_qubits, reverse_keys); + } + } + Err(_) => { + if let Ok(reverse_keys) = target.operation_names_for_qargs(Some(&reverse_qubits)) { + qubit_gate_map.insert(&reverse_qubits, reverse_keys); + } else { + return Err(QiskitError::new_err( + "Target has no gates available on qubits to synthesize over.", + )); + } + } + } + + #[inline] + fn replace_parametrized_gate(mut op: NormalOperation) -> NormalOperation { + if let Some(std_gate) = op.operation.try_standard_gate() { + match std_gate.name() { + "rxx" => { + if let Param::ParameterExpression(_) = op.params[0] { + op.params[0] = Param::Float(PI2) + } + } + "rzx" => { + if let Param::ParameterExpression(_) = op.params[0] { + op.params[0] = Param::Float(PI4) + } + } + "rzz" => { + if let Param::ParameterExpression(_) = op.params[0] { + op.params[0] = Param::Float(PI2) + } + } + _ => (), + } + } + op + } + + for (q_pair, gates) in qubit_gate_map { + for key in gates { + match target.operation_from_name(key) { + Ok(op) => { + match op.operation.view() { + OperationRef::Gate(_) => (), + OperationRef::Standard(_) => (), + _ => continue, + } + + available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); + + if target.contains_key(key) { + available_2q_props.insert( + key, + match &target[key].get(Some(q_pair)) { + Some(Some(props)) => (props.duration, props.error), + _ => (None, None), + }, + ); + } else { + continue; + } + } + _ => continue, + } + } + } + if available_2q_basis.is_empty() { + return Err(QiskitError::new_err( + "Target has no gates available on qubits to synthesize over.", + )); + } + + let target_basis_set = get_target_basis_set(target, qubits[0]); + let available_1q_basis: HashSet<&str> = + HashSet::from_iter(target_basis_set.get_bases().map(|basis| basis.as_str())); + let mut decomposers: Vec = Vec::new(); + + #[inline] + fn is_supercontrolled(op: &NormalOperation) -> bool { + match op.operation.matrix(&op.params) { + None => false, + Some(unitary_matrix) => { + let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) + .unwrap(); + relative_eq!(kak.a(), PI4) && relative_eq!(kak.c(), 0.0) + } + } + } + + #[inline] + fn is_controlled(op: &NormalOperation) -> bool { + match op.operation.matrix(&op.params) { + None => false, + Some(unitary_matrix) => { + let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) + .unwrap(); + relative_eq!(kak.b(), 0.0) && relative_eq!(kak.c(), 0.0) + } + } + } + + // Iterate over 1q and 2q supercontrolled basis, append TwoQubitBasisDecomposers + let supercontrolled_basis: IndexMap<&str, NormalOperation> = available_2q_basis + .iter() + .filter(|(_, v)| is_supercontrolled(v)) + .map(|(k, v)| (*k, v.clone())) + .collect(); + + for basis_1q in &available_1q_basis { + for (basis_2q, gate) in supercontrolled_basis.iter() { + let mut basis_2q_fidelity: f64 = match available_2q_props.get(basis_2q) { + Some(&(_, Some(e))) => 1.0 - e, + _ => 1.0, + }; + if let Some(approx_degree) = approximation_degree { + basis_2q_fidelity *= approx_degree; + } + let decomposer = TwoQubitBasisDecomposer::new_inner( + gate.operation.name().to_string(), + gate.operation.matrix(&gate.params).unwrap().view(), + basis_2q_fidelity, + basis_1q, + None, + )?; + + decomposers.push(DecomposerElement { + decomposer: DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), + gate: gate.clone(), + }); + } + } + + // If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer + // is an ideal decomposition and there is no need to bother calculating the XX embodiments + // or try the XX decomposer + let available_basis_set: HashSet<&str> = available_2q_basis.keys().copied().collect(); + + #[inline] + fn check_goodbye(basis_set: &HashSet<&str>) -> bool { + basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) + } + + if check_goodbye(&available_basis_set) { + return Ok(Some(decomposers)); + } + + // Let's now look for possible controlled decomposers (i.e. XXDecomposer) + let controlled_basis: IndexMap<&str, NormalOperation> = available_2q_basis + .iter() + .filter(|(_, v)| is_controlled(v)) + .map(|(k, v)| (*k, v.clone())) + .collect(); + let mut pi2_basis: Option<&str> = None; + let xx_embodiments: &Bound<'_, PyAny> = imports::XX_EMBODIMENTS.get_bound(py); + + // The xx decomposer args are the interaction strength (f64), basis_2q_fidelity (f64), + // and embodiments (Bound<'_, PyAny>). + let xx_decomposer_args = controlled_basis.iter().map( + |(name, op)| -> PyResult<(f64, f64, pyo3::Bound<'_, pyo3::PyAny>)> { + let strength = 2.0 + * TwoQubitWeylDecomposition::new_inner( + op.operation.matrix(&op.params).unwrap().view(), + None, + None, + ) + .unwrap() + .a(); + let mut fidelity_value = match available_2q_props.get(name) { + Some(&(_, error)) => 1.0 - error.unwrap_or_default(), // default is 0.0 + None => 1.0, + }; + if let Some(approx_degree) = approximation_degree { + fidelity_value *= approx_degree; + } + let mut embodiment = + xx_embodiments.get_item(op.to_object(py).getattr(py, "base_class")?)?; + + if embodiment.getattr("parameters")?.len()? == 1 { + embodiment = embodiment.call_method1("assign_parameters", (vec![strength],))?; + } + // basis equivalent to CX are well optimized so use for the pi/2 angle if available + if relative_eq!(strength, PI2) && supercontrolled_basis.contains_key(name) { + pi2_basis = Some(op.operation.name()); + } + Ok((strength, fidelity_value, embodiment)) + }, + ); + + let basis_2q_fidelity_dict = PyDict::new_bound(py); + let embodiments_dict = PyDict::new_bound(py); + for (strength, fidelity, embodiment) in xx_decomposer_args.flatten() { + basis_2q_fidelity_dict.set_item(strength, fidelity)?; + embodiments_dict.set_item(strength, embodiment.into_py(py))?; + } + + // Iterate over 2q fidelities and select decomposers + if basis_2q_fidelity_dict.len() > 0 { + let xx_decomposer: &Bound<'_, PyAny> = imports::XX_DECOMPOSER.get_bound(py); + for basis_1q in available_1q_basis { + let pi2_decomposer = if let Some(pi_2_basis) = pi2_basis { + if pi_2_basis == "cx" && basis_1q == "ZSX" { + let fidelity = match approximation_degree { + Some(approx_degree) => approx_degree, + None => match &target["cx"][Some(&qubits)] { + Some(props) => 1.0 - props.error.unwrap_or_default(), + None => 1.0, + }, + }; + Some(TwoQubitBasisDecomposer::new_inner( + pi_2_basis.to_string(), + StandardGate::CXGate.matrix(&[]).unwrap().view(), + fidelity, + basis_1q, + Some(true), + )?) + } else { + None + } + } else { + None + }; + + let decomposer = xx_decomposer.call1(( + &basis_2q_fidelity_dict, + PyString::new_bound(py, basis_1q), + &embodiments_dict, + pi2_decomposer, + ))?; + let decomposer_gate = decomposer + .getattr(intern!(py, "gate"))? + .extract::()?; + + decomposers.push(DecomposerElement { + decomposer: DecomposerType::XXDecomposer(decomposer.into()), + gate: decomposer_gate, + }); + } + } + Ok(Some(decomposers)) +} + +fn preferred_direction( + decomposer: &DecomposerElement, + ref_qubits: &[PhysicalQubit; 2], + natural_direction: Option, + coupling_edges: &HashSet<[PhysicalQubit; 2]>, + target: &Target, +) -> PyResult> { + // Returns: + // * true if gate qubits are in the hardware-native direction + // * false if gate qubits must be flipped to match hardware-native direction + let qubits: [PhysicalQubit; 2] = *ref_qubits; + let mut reverse_qubits: [PhysicalQubit; 2] = qubits; + reverse_qubits.reverse(); + + let compute_cost = + |lengths: bool, q_tuple: [PhysicalQubit; 2], in_cost: f64| -> PyResult { + let cost = match target.qargs_for_operation_name(decomposer.gate.operation.name()) { + Ok(_) => match target[decomposer.gate.operation.name()].get(Some( + &q_tuple + .into_iter() + .collect::>(), + )) { + Some(Some(_props)) => { + if lengths { + _props.duration.unwrap_or(in_cost) + } else { + _props.error.unwrap_or(in_cost) + } + } + _ => in_cost, + }, + Err(_) => in_cost, + }; + Ok(cost) + }; + + let preferred_direction = match natural_direction { + Some(false) => None, + _ => { + // None or Some(true) + let zero_one = coupling_edges.contains(&qubits); + let one_zero = coupling_edges.contains(&[qubits[1], qubits[0]]); + + match (zero_one, one_zero) { + (true, false) => Some(true), + (false, true) => Some(false), + _ => { + let mut cost_0_1: f64 = f64::INFINITY; + let mut cost_1_0: f64 = f64::INFINITY; + + // Try to find the cost in gate_lengths + cost_0_1 = compute_cost(true, qubits, cost_0_1)?; + cost_1_0 = compute_cost(true, reverse_qubits, cost_1_0)?; + + // If no valid cost was found in gate_lengths, check gate_errors + if !(cost_0_1 < f64::INFINITY || cost_1_0 < f64::INFINITY) { + cost_0_1 = compute_cost(false, qubits, cost_0_1)?; + cost_1_0 = compute_cost(false, reverse_qubits, cost_1_0)?; + } + + if cost_0_1 < cost_1_0 { + Some(true) + } else if cost_1_0 < cost_0_1 { + Some(false) + } else { + None + } + } + } + } + }; + + if natural_direction == Some(true) && preferred_direction.is_none() { + return Err(QiskitError::new_err(format!( + concat!( + "No preferred direction of gate on qubits {:?} ", + "could be determined from coupling map or gate lengths / gate errors." + ), + qubits + ))); + } + + Ok(preferred_direction) +} + +fn synth_su4_sequence( + su4_mat: &Array2, + decomposer_2q: &DecomposerElement, + preferred_direction: Option, + approximation_degree: Option, +) -> PyResult { + let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; + let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else { + unreachable!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") + }; + let sequence = TwoQubitUnitarySequence { + gate_sequence: synth, + decomp_gate: decomposer_2q.gate.clone(), + }; + + match preferred_direction { + None => Ok(sequence), + Some(preferred_dir) => { + let mut synth_direction: Option> = None; + // if the gates in synthesis are in the opposite direction of the preferred direction + // resynthesize a new operator which is the original conjugated by swaps. + // this new operator is doubly mirrored from the original and is locally equivalent. + for (gate, _, qubits) in sequence.gate_sequence.gates() { + if gate.is_none() || gate.unwrap().name() == "cx" { + synth_direction = Some(qubits.clone()); + } + } + + match synth_direction { + None => Ok(sequence), + Some(synth_direction) => { + let synth_dir = match synth_direction.as_slice() { + [0, 1] => true, + [1, 0] => false, + _ => unreachable!(), + }; + if synth_dir != preferred_dir { + reversed_synth_su4_sequence( + su4_mat.clone(), + decomposer_2q, + approximation_degree, + ) + } else { + Ok(sequence) + } + } + } + } + } +} + +fn reversed_synth_su4_sequence( + mut su4_mat: Array2, + decomposer_2q: &DecomposerElement, + approximation_degree: Option, +) -> PyResult { + let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; + // Swap rows 1 and 2 + let (mut row_1, mut row_2) = su4_mat.multi_slice_mut((s![1, ..], s![2, ..])); + azip!((x in &mut row_1, y in &mut row_2) (*x, *y) = (*y, *x)); + + // Swap columns 1 and 2 + let (mut col_1, mut col_2) = su4_mat.multi_slice_mut((s![.., 1], s![.., 2])); + azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); + + let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else { + unreachable!( + "reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer." + ) + }; + + let flip_bits: [u8; 2] = [1, 0]; + let mut reversed_gates = Vec::with_capacity(synth.gates().len()); + for (gate, params, qubit_ids) in synth.gates() { + let new_qubit_ids = qubit_ids + .into_iter() + .map(|x| flip_bits[*x as usize]) + .collect::>(); + reversed_gates.push((*gate, params.clone(), new_qubit_ids.clone())); + } + + let mut reversed_synth: TwoQubitGateSequence = TwoQubitGateSequence::new(); + reversed_synth.set_state((reversed_gates, synth.global_phase())); + let sequence = TwoQubitUnitarySequence { + gate_sequence: reversed_synth, + decomp_gate: decomposer_2q.gate.clone(), + }; + Ok(sequence) +} + +fn synth_su4_dag( + py: Python, + su4_mat: &Array2, + decomposer_2q: &DecomposerElement, + preferred_direction: Option, + approximation_degree: Option, +) -> PyResult { + let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; + let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { + let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] + .into_iter() + .collect(); + decomposer + .call_bound( + py, + (su4_mat.clone().into_pyarray_bound(py),), + Some(&kwargs.into_py_dict_bound(py)), + )? + .extract::(py)? + } else { + unreachable!("synth_su4_dag should only be called for XXDecomposer.") + }; + + match preferred_direction { + None => Ok(synth_dag), + Some(preferred_dir) => { + let mut synth_direction: Option> = None; + for node in synth_dag.topological_op_nodes()? { + let inst = &synth_dag.dag()[node].unwrap_operation(); + if inst.op.num_qubits() == 2 { + let qargs = synth_dag.get_qargs(inst.qubits); + synth_direction = Some(vec![qargs[0].0, qargs[1].0]); + } + } + match synth_direction { + None => Ok(synth_dag), + Some(synth_direction) => { + let synth_dir = match synth_direction.as_slice() { + [0, 1] => true, + [1, 0] => false, + _ => unreachable!("There are no more than 2 possible synth directions."), + }; + if synth_dir != preferred_dir { + reversed_synth_su4_dag( + py, + su4_mat.clone(), + decomposer_2q, + approximation_degree, + ) + } else { + Ok(synth_dag) + } + } + } + } + } +} + +fn reversed_synth_su4_dag( + py: Python<'_>, + mut su4_mat: Array2, + decomposer_2q: &DecomposerElement, + approximation_degree: Option, +) -> PyResult { + let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; + + // Swap rows 1 and 2 + let (mut row_1, mut row_2) = su4_mat.multi_slice_mut((s![1, ..], s![2, ..])); + azip!((x in &mut row_1, y in &mut row_2) (*x, *y) = (*y, *x)); + + // Swap columns 1 and 2 + let (mut col_1, mut col_2) = su4_mat.multi_slice_mut((s![.., 1], s![.., 2])); + azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); + + let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { + let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] + .into_iter() + .collect(); + decomposer + .call_bound( + py, + (su4_mat.clone().into_pyarray_bound(py),), + Some(&kwargs.into_py_dict_bound(py)), + )? + .extract::(py)? + } else { + unreachable!("reversed_synth_su4_dag should only be called for XXDecomposer") + }; + + let mut target_dag = synth_dag.copy_empty_like(py, "alike")?; + let flip_bits: [Qubit; 2] = [Qubit(1), Qubit(0)]; + for node in synth_dag.topological_op_nodes()? { + let mut inst = synth_dag.dag()[node].unwrap_operation().clone(); + let qubits: Vec = synth_dag + .qargs_interner() + .get(inst.qubits) + .iter() + .map(|x| flip_bits[x.0 as usize]) + .collect(); + inst.qubits = target_dag.qargs_interner.insert_owned(qubits); + target_dag.push_back(py, inst)?; + } + Ok(target_dag) +} + +#[pymodule] +pub fn unitary_synthesis(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(py_run_main_loop))?; + Ok(()) +} diff --git a/crates/circuit/src/converters.rs b/crates/circuit/src/converters.rs index 37ba83ae5173..dea366d02ff6 100644 --- a/crates/circuit/src/converters.rs +++ b/crates/circuit/src/converters.rs @@ -47,7 +47,10 @@ impl<'py> FromPyObject<'py> for QuantumCircuitData<'py> { Ok(QuantumCircuitData { data: data_borrowed, name: ob.getattr(intern!(py, "name")).ok(), - calibrations: ob.getattr(intern!(py, "calibrations"))?.extract().ok(), + calibrations: ob + .getattr(intern!(py, "_calibrations_prop"))? + .extract() + .ok(), metadata: ob.getattr(intern!(py, "metadata")).ok(), qregs: ob .getattr(intern!(py, "qregs")) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 14386b2f5e7e..73345f710083 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -250,9 +250,9 @@ pub struct DAGCircuit { cregs: Py, /// The cache used to intern instruction qargs. - qargs_interner: Interner<[Qubit]>, + pub qargs_interner: Interner<[Qubit]>, /// The cache used to intern instruction cargs. - cargs_interner: Interner<[Clbit]>, + pub cargs_interner: Interner<[Clbit]>, /// Qubits registered in the circuit. qubits: BitData, /// Clbits registered in the circuit. @@ -569,7 +569,7 @@ impl DAGCircuit { let out_dict = PyDict::new_bound(py); out_dict.set_item("name", self.name.as_ref().map(|x| x.clone_ref(py)))?; out_dict.set_item("metadata", self.metadata.as_ref().map(|x| x.clone_ref(py)))?; - out_dict.set_item("calibrations", self.calibrations.clone())?; + out_dict.set_item("_calibrations_prop", self.calibrations.clone())?; out_dict.set_item("qregs", self.qregs.clone_ref(py))?; out_dict.set_item("cregs", self.cregs.clone_ref(py))?; out_dict.set_item("global_phase", self.global_phase.clone())?; @@ -648,7 +648,10 @@ impl DAGCircuit { let dict_state = state.downcast_bound::(py)?; self.name = dict_state.get_item("name")?.unwrap().extract()?; self.metadata = dict_state.get_item("metadata")?.unwrap().extract()?; - self.calibrations = dict_state.get_item("calibrations")?.unwrap().extract()?; + self.calibrations = dict_state + .get_item("_calibrations_prop")? + .unwrap() + .extract()?; self.qregs = dict_state.get_item("qregs")?.unwrap().extract()?; self.cregs = dict_state.get_item("cregs")?.unwrap().extract()?; self.global_phase = dict_state.get_item("global_phase")?.unwrap().extract()?; @@ -864,8 +867,15 @@ impl DAGCircuit { /// /// The custom pulse definition of a given gate is of the form /// {'gate_name': {(qubits, params): schedule}} + /// + /// DEPRECATED since Qiskit 1.3.0 and will be removed in Qiskit 2.0.0 #[getter] - fn get_calibrations(&self) -> HashMap> { + fn get_calibrations(&self, py: Python) -> HashMap> { + emit_pulse_dependency_deprecation( + py, + "property ``qiskit.dagcircuit.dagcircuit.DAGCircuit.calibrations``", + ); + self.calibrations.clone() } @@ -874,8 +884,29 @@ impl DAGCircuit { /// Args: /// calibrations (dict): A dictionary of input in the format /// {'gate_name': {(qubits, gate_params): schedule}} + /// + /// DEPRECATED since Qiskit 1.3.0 and will be removed in Qiskit 2.0.0 #[setter] - fn set_calibrations(&mut self, calibrations: HashMap>) { + fn set_calibrations(&mut self, py: Python, calibrations: HashMap>) { + emit_pulse_dependency_deprecation( + py, + "property ``qiskit.dagcircuit.dagcircuit.DAGCircuit.calibrations``", + ); + + self.calibrations = calibrations; + } + + // This is an alternative and Python-private path to 'get_calibration' to avoid + // deprecation warnings + #[getter(_calibrations_prop)] + fn get_calibrations_prop(&self) -> HashMap> { + self.calibrations.clone() + } + + // This is an alternative and Python-private path to 'set_calibration' to avoid + // deprecation warnings + #[setter(_calibrations_prop)] + fn set_calibrations_prop(&mut self, calibrations: HashMap>) { self.calibrations = calibrations; } @@ -898,6 +929,11 @@ impl DAGCircuit { schedule: Py, mut params: Option>, ) -> PyResult<()> { + emit_pulse_dependency_deprecation( + py, + "method ``qiskit.dagcircuit.dagcircuit.DAGCircuit.add_calibration``", + ); + if gate.is_instance(imports::GATE.get_bound(py))? { params = Some(gate.getattr(intern!(py, "params"))?); gate = gate.getattr(intern!(py, "name"))?; @@ -955,7 +991,18 @@ def _format(operand): /// Return True if the dag has a calibration defined for the node operation. In this /// case, the operation does not need to be translated to the device basis. + /// + /// DEPRECATED since Qiskit 1.3.0 and will be removed in Qiskit 2.0.0 fn has_calibration_for(&self, py: Python, node: PyRef) -> PyResult { + emit_pulse_dependency_deprecation( + py, + "method ``qiskit.dagcircuit.dagcircuit.DAGCircuit.has_calibration_for``", + ); + + self._has_calibration_for(py, node) + } + + fn _has_calibration_for(&self, py: Python, node: PyRef) -> PyResult { if !self .calibrations .contains_key(node.instruction.operation.name()) @@ -3406,8 +3453,8 @@ def _format(operand): /// Raises: /// DAGCircuitError: If replacement operation was incompatible with /// location of target node. - #[pyo3(signature = (node, op, inplace=false, propagate_condition=true))] - fn substitute_node( + #[pyo3(name = "substitute_node", signature = (node, op, inplace=false, propagate_condition=true))] + pub fn py_substitute_node( &mut self, node: &Bound, op: &Bound, @@ -6960,3 +7007,20 @@ fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult } type SortKeyType<'a> = (&'a [Qubit], &'a [Clbit]); + +/// Emit a Python `DeprecationWarning` for pulse-related dependencies. +fn emit_pulse_dependency_deprecation(py: Python, msg: &str) { + let _ = imports::WARNINGS_WARN.get_bound(py).call1(( + PyString::new_bound( + py, + &format!( + "The {} is deprecated as of qiskit 1.3.0. It will be removed in Qiskit 2.0.0. \ + The entire Qiskit Pulse package is being deprecated \ + and this is a dependency on the package.", + msg + ), + ), + py.get_type_bound::(), + 1, + )); +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 6e87bcb4f101..67ae9f85897f 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -116,6 +116,14 @@ pub static UNITARY_GATE: ImportOnceCell = ImportOnceCell::new( "qiskit.circuit.library.generalized_gates.unitary", "UnitaryGate", ); +pub static QS_DECOMPOSITION: ImportOnceCell = + ImportOnceCell::new("qiskit.synthesis.unitary.qsd", "qs_decomposition"); +pub static XX_DECOMPOSER: ImportOnceCell = + ImportOnceCell::new("qiskit.synthesis.two_qubit.xx_decompose", "XXDecomposer"); +pub static XX_EMBODIMENTS: ImportOnceCell = + ImportOnceCell::new("qiskit.synthesis.two_qubit.xx_decompose", "XXEmbodiments"); +pub static NUMPY_COPY_ONLY_IF_NEEDED: ImportOnceCell = + ImportOnceCell::new("qiskit._numpy_compat", "COPY_ONLY_IF_NEEDED"); /// A mapping from the enum variant in crate::operations::StandardGate to the python /// module path and class name to import it. This is used to populate the conversion table diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index b59f3c8d8eda..49070e85db70 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -50,9 +50,11 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::optimize_1q_gates::optimize_1q_gates, "optimize_1q_gates")?; add_submodule(m, ::qiskit_accelerate::pauli_exp_val::pauli_expval, "pauli_expval")?; add_submodule(m, ::qiskit_accelerate::remove_diagonal_gates_before_measure::remove_diagonal_gates_before_measure, "remove_diagonal_gates_before_measure")?; + add_submodule(m, ::qiskit_accelerate::remove_identity_equiv::remove_identity_equiv_mod, "remove_identity_equiv")?; add_submodule(m, ::qiskit_accelerate::results::results, "results")?; add_submodule(m, ::qiskit_accelerate::sabre::sabre, "sabre")?; add_submodule(m, ::qiskit_accelerate::sampled_exp_val::sampled_exp_val, "sampled_exp_val")?; + add_submodule(m, ::qiskit_accelerate::sparse_observable::sparse_observable, "sparse_observable")?; add_submodule(m, ::qiskit_accelerate::sparse_pauli_op::sparse_pauli_op, "sparse_pauli_op")?; add_submodule(m, ::qiskit_accelerate::split_2q_unitaries::split_2q_unitaries_mod, "split_2q_unitaries")?; add_submodule(m, ::qiskit_accelerate::star_prerouting::star_prerouting, "star_prerouting")?; @@ -60,6 +62,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::synthesis::synthesis, "synthesis")?; add_submodule(m, ::qiskit_accelerate::target_transpiler::target, "target")?; add_submodule(m, ::qiskit_accelerate::two_qubit_decompose::two_qubit_decompose, "two_qubit_decompose")?; + add_submodule(m, ::qiskit_accelerate::unitary_synthesis::unitary_synthesis, "unitary_synthesis")?; add_submodule(m, ::qiskit_accelerate::uc_gate::uc_gate, "uc_gate")?; add_submodule(m, ::qiskit_accelerate::utils::utils, "utils")?; add_submodule(m, ::qiskit_accelerate::vf2_layout::vf2_layout, "vf2_layout")?; diff --git a/crates/qasm2/src/parse.rs b/crates/qasm2/src/parse.rs index 36e2a49f669c..ec8c61152bab 100644 --- a/crates/qasm2/src/parse.rs +++ b/crates/qasm2/src/parse.rs @@ -827,8 +827,16 @@ impl State { } bc.push(Some(InternalBytecode::EndDeclareGate {})); self.gate_symbols.clear(); - self.define_gate(Some(&gate_token), name, num_params, num_qubits)?; - Ok(statements + 2) + let num_bytecode = statements + 2; + if self.define_gate(Some(&gate_token), name, num_params, num_qubits)? { + Ok(num_bytecode) + } else { + // The gate was built-in, so we don't actually need to emit the bytecode. This is + // uncommon, so it doesn't matter too much that we throw away allocation work we did - + // it still helps that we verified that the gate body was valid OpenQASM 2. + bc.truncate(bc.len() - num_bytecode); + Ok(0) + } } /// Parse an `opaque` statement. This assumes that the `opaque` token is still in the token @@ -1634,6 +1642,9 @@ impl State { /// bytecode because not all gate definitions need something passing to Python. For example, /// the Python parser initializes its state including the built-in gates `U` and `CX`, and /// handles the `qelib1.inc` include specially as well. + /// + /// Returns whether the gate needs to be defined in Python space (`true`) or if it was some sort + /// of built-in that doesn't need the definition (`false`). fn define_gate( &mut self, owner: Option<&Token>, @@ -1685,12 +1696,14 @@ impl State { } match self.symbols.get(&name) { None => { + // The gate wasn't a built-in, so we need to move the symbol in, but we don't + // need to increment the number of gates because it's already got a gate ID + // assigned. self.symbols.insert(name, symbol.into()); - self.num_gates += 1; - Ok(true) + Ok(false) } Some(GlobalSymbol::Gate { .. }) => { - self.symbols.insert(name, symbol.into()); + // The gate was built-in and we can ignore the new definition (it's the same). Ok(false) } _ => already_defined(self, name), diff --git a/pyproject.toml b/pyproject.toml index 70a6e7c9d0cf..326d28f2a273 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering", ] # These are configured in the `tool.setuptools.dynamic` table. diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 196b10da3183..202ebc32be85 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -79,12 +79,14 @@ sys.modules["qiskit._accelerate.results"] = _accelerate.results sys.modules["qiskit._accelerate.sabre"] = _accelerate.sabre sys.modules["qiskit._accelerate.sampled_exp_val"] = _accelerate.sampled_exp_val +sys.modules["qiskit._accelerate.sparse_observable"] = _accelerate.sparse_observable sys.modules["qiskit._accelerate.sparse_pauli_op"] = _accelerate.sparse_pauli_op sys.modules["qiskit._accelerate.star_prerouting"] = _accelerate.star_prerouting sys.modules["qiskit._accelerate.stochastic_swap"] = _accelerate.stochastic_swap sys.modules["qiskit._accelerate.elide_permutations"] = _accelerate.elide_permutations sys.modules["qiskit._accelerate.target"] = _accelerate.target sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose +sys.modules["qiskit._accelerate.unitary_synthesis"] = _accelerate.unitary_synthesis sys.modules["qiskit._accelerate.vf2_layout"] = _accelerate.vf2_layout sys.modules["qiskit._accelerate.synthesis.permutation"] = _accelerate.synthesis.permutation sys.modules["qiskit._accelerate.synthesis.linear"] = _accelerate.synthesis.linear @@ -104,6 +106,7 @@ sys.modules["qiskit._accelerate.inverse_cancellation"] = _accelerate.inverse_cancellation sys.modules["qiskit._accelerate.check_map"] = _accelerate.check_map sys.modules["qiskit._accelerate.filter_op_nodes"] = _accelerate.filter_op_nodes +sys.modules["qiskit._accelerate.remove_identity_equiv"] = _accelerate.remove_identity_equiv from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/assembler/assemble_schedules.py b/qiskit/assembler/assemble_schedules.py index 2d5ebefa2fd1..697f3a46677a 100644 --- a/qiskit/assembler/assemble_schedules.py +++ b/qiskit/assembler/assemble_schedules.py @@ -22,8 +22,10 @@ from qiskit.pulse import instructions, transforms, library, schedule, channels from qiskit.qobj import utils as qobj_utils, converters from qiskit.qobj.converters.pulse_instruction import ParametricPulseShapes +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency +@deprecate_pulse_dependency def assemble_schedules( schedules: List[ Union[ diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 164dca16c00b..f36e6a304afb 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -47,6 +47,7 @@ from qiskit.circuit.parameter import Parameter from qiskit.circuit.exceptions import CircuitError from qiskit.utils import deprecate_func +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency from . import _classical_resource_map from .controlflow import ControlFlowOp, _builder_utils from .controlflow.builder import CircuitScopeInterface, ControlFlowBuilderBlock @@ -70,6 +71,7 @@ from .delay import Delay from .store import Store + if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import from qiskit.transpiler.layout import TranspileLayout # pylint: disable=cyclic-import @@ -1339,15 +1341,17 @@ def op_start_times(self) -> list[int]: return self._op_start_times @property + @deprecate_pulse_dependency(is_property=True) def calibrations(self) -> dict: """Return calibration dictionary. The custom pulse definition of a given gate is of the form ``{'gate_name': {(qubits, params): schedule}}`` """ - return dict(self._calibrations) + return self._calibrations_prop @calibrations.setter + @deprecate_pulse_dependency(is_property=True) def calibrations(self, calibrations: dict): """Set the circuit calibration data from a dictionary of calibration definition. @@ -1355,18 +1359,37 @@ def calibrations(self, calibrations: dict): calibrations (dict): A dictionary of input in the format ``{'gate_name': {(qubits, gate_params): schedule}}`` """ + self._calibrations_prop = calibrations + + @property + def _calibrations_prop(self) -> dict: + """An alternative private path to the `calibrations` property for + avoiding deprecation warnings.""" + return dict(self._calibrations) + + @_calibrations_prop.setter + def _calibrations_prop(self, calibrations: dict): + """An alternative private path to the `calibrations` property for + avoiding deprecation warnings.""" self._calibrations = defaultdict(dict, calibrations) + @deprecate_pulse_dependency def has_calibration_for(self, instruction: CircuitInstruction | tuple): """Return True if the circuit has a calibration defined for the instruction context. In this case, the operation does not need to be translated to the device basis. """ + + return self._has_calibration_for(instruction) + + def _has_calibration_for(self, instruction: CircuitInstruction | tuple): + """An alternative private path to the `has_calibration_for` method for + avoiding deprecation warnings.""" if isinstance(instruction, CircuitInstruction): operation = instruction.operation qubits = instruction.qubits else: operation, qubits, _ = instruction - if not self.calibrations or operation.name not in self.calibrations: + if not self._calibrations_prop or operation.name not in self._calibrations_prop: return False qubits = tuple(self.qubits.index(qubit) for qubit in qubits) params = [] @@ -1376,7 +1399,7 @@ def has_calibration_for(self, instruction: CircuitInstruction | tuple): else: params.append(p) params = tuple(params) - return (qubits, params) in self.calibrations[operation.name] + return (qubits, params) in self._calibrations_prop[operation.name] @property def metadata(self) -> dict: @@ -2017,7 +2040,7 @@ def replace_var(var: expr.Var, cache: Mapping[expr.Var, expr.Var]) -> expr.Var: ) edge_map.update(zip(other.clbits, dest._cbit_argument_conversion(clbits))) - for gate, cals in other.calibrations.items(): + for gate, cals in other._calibrations_prop.items(): dest._calibrations[gate].update(cals) dest.duration = None @@ -6477,6 +6500,7 @@ def continue_loop(self) -> InstructionSet: ContinueLoopOp(self.num_qubits, self.num_clbits), self.qubits, self.clbits, copy=False ) + @deprecate_pulse_dependency def add_calibration( self, gate: Union[Gate, str], diff --git a/qiskit/compiler/scheduler.py b/qiskit/compiler/scheduler.py index e75e9a388f04..9004dc24fd11 100644 --- a/qiskit/compiler/scheduler.py +++ b/qiskit/compiler/scheduler.py @@ -26,6 +26,7 @@ from qiskit.scheduler.config import ScheduleConfig from qiskit.scheduler.schedule_circuit import schedule_circuit from qiskit.utils.parallel import parallel_map +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency logger = logging.getLogger(__name__) @@ -35,6 +36,7 @@ def _log_schedule_time(start_time, end_time): logger.info(log_msg) +@deprecate_pulse_dependency(moving_to_dynamics=True) def schedule( circuits: Union[QuantumCircuit, List[QuantumCircuit]], backend: Optional[Backend] = None, diff --git a/qiskit/compiler/sequencer.py b/qiskit/compiler/sequencer.py index 541f78c9f762..5a381918417b 100644 --- a/qiskit/compiler/sequencer.py +++ b/qiskit/compiler/sequencer.py @@ -21,8 +21,10 @@ from qiskit.pulse import InstructionScheduleMap, Schedule from qiskit.scheduler import ScheduleConfig from qiskit.scheduler.sequence import sequence as _sequence +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency +@deprecate_pulse_dependency(moving_to_dynamics=True) def sequence( scheduled_circuits: Union[QuantumCircuit, List[QuantumCircuit]], backend: Optional[Backend] = None, diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 5e2dcf8a1560..2b17d8bbae12 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -32,12 +32,14 @@ from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.transpiler.target import Target +from qiskit.utils.deprecate_pulse import deprecate_pulse_arg logger = logging.getLogger(__name__) _CircuitT = TypeVar("_CircuitT", bound=Union[QuantumCircuit, List[QuantumCircuit]]) +@deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def transpile( # pylint: disable=too-many-return-statements circuits: _CircuitT, backend: Optional[Backend] = None, @@ -104,7 +106,7 @@ def transpile( # pylint: disable=too-many-return-statements will override the backend's. basis_gates: List of basis gate names to unroll to (e.g: ``['u1', 'u2', 'u3', 'cx']``). If ``None``, do not unroll. - inst_map: Mapping of unrolled gates to pulse schedules. If this is not provided, + inst_map: DEPRECATED. Mapping of unrolled gates to pulse schedules. If this is not provided, transpiler tries to get from the backend. If any user defined calibration is found in the map and this is used in a circuit, transpiler attaches the custom gate definition to the circuit. This enables one to flexibly diff --git a/qiskit/converters/circuit_to_dagdependency.py b/qiskit/converters/circuit_to_dagdependency.py index 7095d4773112..e617cf30c4fd 100644 --- a/qiskit/converters/circuit_to_dagdependency.py +++ b/qiskit/converters/circuit_to_dagdependency.py @@ -46,6 +46,6 @@ def circuit_to_dagdependency(circuit, create_preds_and_succs=True): dagdependency._add_predecessors() dagdependency._add_successors() - dagdependency.calibrations = circuit.calibrations + dagdependency._calibrations = circuit._calibrations_prop return dagdependency diff --git a/qiskit/converters/circuit_to_dagdependency_v2.py b/qiskit/converters/circuit_to_dagdependency_v2.py index 4852fdda56af..f55a0cf6716c 100644 --- a/qiskit/converters/circuit_to_dagdependency_v2.py +++ b/qiskit/converters/circuit_to_dagdependency_v2.py @@ -27,7 +27,7 @@ def _circuit_to_dagdependency_v2(circuit): dagdependency = _DAGDependencyV2() dagdependency.name = circuit.name dagdependency.metadata = circuit.metadata - dagdependency.calibrations = circuit.calibrations + dagdependency._calibrations = circuit._calibrations_prop dagdependency.global_phase = circuit.global_phase dagdependency.add_qubits(circuit.qubits) diff --git a/qiskit/converters/dag_to_circuit.py b/qiskit/converters/dag_to_circuit.py index 3350ebc5ab34..6ffa74527aa3 100644 --- a/qiskit/converters/dag_to_circuit.py +++ b/qiskit/converters/dag_to_circuit.py @@ -70,7 +70,7 @@ def dag_to_circuit(dag, copy_operations=True): for var in dag.iter_declared_vars(): circuit.add_uninitialized_var(var) circuit.metadata = dag.metadata - circuit.calibrations = dag.calibrations + circuit._calibrations_prop = dag._calibrations_prop circuit._data = circuit_data diff --git a/qiskit/converters/dag_to_dagdependency.py b/qiskit/converters/dag_to_dagdependency.py index dc0c3c289842..19fb70a31bd6 100644 --- a/qiskit/converters/dag_to_dagdependency.py +++ b/qiskit/converters/dag_to_dagdependency.py @@ -50,6 +50,6 @@ def dag_to_dagdependency(dag, create_preds_and_succs=True): # copy metadata dagdependency.global_phase = dag.global_phase - dagdependency.calibrations = dag.calibrations + dagdependency._calibrations_prop = dag._calibrations_prop return dagdependency diff --git a/qiskit/converters/dag_to_dagdependency_v2.py b/qiskit/converters/dag_to_dagdependency_v2.py index 5beb83b1177e..29eb12300f00 100644 --- a/qiskit/converters/dag_to_dagdependency_v2.py +++ b/qiskit/converters/dag_to_dagdependency_v2.py @@ -27,7 +27,7 @@ def _dag_to_dagdependency_v2(dag): dagdependency.name = dag.name dagdependency.metadata = dag.metadata dagdependency.global_phase = dag.global_phase - dagdependency.calibrations = dag.calibrations + dagdependency.calibrations = dag._calibrations_prop dagdependency.add_qubits(dag.qubits) dagdependency.add_clbits(dag.clbits) diff --git a/qiskit/converters/dagdependency_to_circuit.py b/qiskit/converters/dagdependency_to_circuit.py index 2c9a0d4389cb..541207d175d2 100644 --- a/qiskit/converters/dagdependency_to_circuit.py +++ b/qiskit/converters/dagdependency_to_circuit.py @@ -34,7 +34,11 @@ def dagdependency_to_circuit(dagdependency): ) circuit.metadata = dagdependency.metadata - circuit.calibrations = dagdependency.calibrations + if hasattr(dagdependency, "_calibrations_prop"): + circuit._calibrations_prop = dagdependency._calibrations_prop + else: + # This can be _DAGDependencyV2 + circuit._calibrations_prop = dagdependency.calibrations for node in dagdependency.topological_nodes(): circuit._append(CircuitInstruction(node.op.copy(), node.qargs, node.cargs)) diff --git a/qiskit/converters/dagdependency_to_dag.py b/qiskit/converters/dagdependency_to_dag.py index 440f5920eb0c..3b500d82c884 100644 --- a/qiskit/converters/dagdependency_to_dag.py +++ b/qiskit/converters/dagdependency_to_dag.py @@ -12,6 +12,7 @@ """Helper function for converting a dag dependency to a dag circuit""" from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagdependency import DAGDependency def dagdependency_to_dag(dagdependency): @@ -44,6 +45,10 @@ def dagdependency_to_dag(dagdependency): # copy metadata dagcircuit.global_phase = dagdependency.global_phase - dagcircuit.calibrations = dagdependency.calibrations + if isinstance(dagdependency, DAGDependency): + dagcircuit._calibrations_prop = dagdependency._calibrations_prop + else: + # This can be _DAGDependencyV2 + dagcircuit._calibrations_prop = dagdependency.calibrations return dagcircuit diff --git a/qiskit/dagcircuit/dagdependency.py b/qiskit/dagcircuit/dagdependency.py index 7f0b1a5801d4..2658cd731d69 100644 --- a/qiskit/dagcircuit/dagdependency.py +++ b/qiskit/dagcircuit/dagdependency.py @@ -29,6 +29,7 @@ from qiskit.dagcircuit.exceptions import DAGDependencyError from qiskit.dagcircuit.dagdepnode import DAGDepNode from qiskit.pulse import Schedule +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency if typing.TYPE_CHECKING: from qiskit.circuit.parameterexpression import ParameterExpression @@ -146,15 +147,17 @@ def global_phase(self, angle: float | ParameterExpression): self._global_phase = angle % (2 * math.pi) @property + @deprecate_pulse_dependency(is_property=True) def calibrations(self) -> dict[str, dict[tuple, Schedule]]: """Return calibration dictionary. The custom pulse definition of a given gate is of the form ``{'gate_name': {(qubits, params): schedule}}``. """ - return dict(self._calibrations) + return self._calibrations_prop @calibrations.setter + @deprecate_pulse_dependency(is_property=True) def calibrations(self, calibrations: dict[str, dict[tuple, Schedule]]): """Set the circuit calibration data from a dictionary of calibration definition. @@ -162,6 +165,16 @@ def calibrations(self, calibrations: dict[str, dict[tuple, Schedule]]): calibrations (dict): A dictionary of input in the format {'gate_name': {(qubits, gate_params): schedule}} """ + self._calibrations_prop = calibrations + + @property + def _calibrations_prop(self) -> dict[str, dict[tuple, Schedule]]: + """An alternative path to be used internally to avoid deprecation warnings""" + return dict(self._calibrations) + + @_calibrations_prop.setter + def _calibrations_prop(self, calibrations: dict[str, dict[tuple, Schedule]]): + """An alternative path to be used internally to avoid deprecation warnings""" self._calibrations = defaultdict(dict, calibrations) def to_retworkx(self): diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 6d9ed51043ec..4f46b6a51029 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -24,6 +24,7 @@ from qiskit.providers.models.backendstatus import BackendStatus from qiskit.circuit.gate import Instruction from qiskit.utils import deprecate_func +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency class Backend: @@ -485,10 +486,16 @@ def meas_map(self) -> List[List[int]]: raise NotImplementedError @property + @deprecate_pulse_dependency(is_property=True) def instruction_schedule_map(self): """Return the :class:`~qiskit.pulse.InstructionScheduleMap` for the instructions defined in this backend's target.""" - return self.target.instruction_schedule_map() + return self._instruction_schedule_map + + @property + def _instruction_schedule_map(self): + """An alternative private path to be used internally to avoid pulse deprecation warnings.""" + return self.target._get_instruction_schedule_map() def qubit_properties( self, qubit: Union[int, List[int]] @@ -524,6 +531,7 @@ def qubit_properties( return self.target.qubit_properties[qubit] return [self.target.qubit_properties[q] for q in qubit] + @deprecate_pulse_dependency def drive_channel(self, qubit: int): """Return the drive channel for the given qubit. @@ -539,6 +547,7 @@ def drive_channel(self, qubit: int): """ raise NotImplementedError + @deprecate_pulse_dependency def measure_channel(self, qubit: int): """Return the measure stimulus channel for the given qubit. @@ -554,6 +563,7 @@ def measure_channel(self, qubit: int): """ raise NotImplementedError + @deprecate_pulse_dependency def acquire_channel(self, qubit: int): """Return the acquisition channel for the given qubit. @@ -569,6 +579,7 @@ def acquire_channel(self, qubit: int): """ raise NotImplementedError + @deprecate_pulse_dependency def control_channel(self, qubits: Iterable[int]): """Return the secondary drive channel for the given qubit diff --git a/qiskit/providers/backend_compat.py b/qiskit/providers/backend_compat.py index f554897d34c4..6971f8b8328b 100644 --- a/qiskit/providers/backend_compat.py +++ b/qiskit/providers/backend_compat.py @@ -25,10 +25,13 @@ from qiskit.providers.models.pulsedefaults import PulseDefaults from qiskit.providers.options import Options from qiskit.providers.exceptions import BackendPropertyError +from qiskit.utils.deprecate_pulse import deprecate_pulse_arg, deprecate_pulse_dependency + logger = logging.getLogger(__name__) +@deprecate_pulse_arg("defaults") def convert_to_target( configuration: BackendConfiguration, properties: BackendProperties = None, @@ -46,7 +49,7 @@ def convert_to_target( Args: configuration: Backend configuration as ``BackendConfiguration`` properties: Backend property dictionary or ``BackendProperties`` - defaults: Backend pulse defaults dictionary or ``PulseDefaults`` + defaults: DEPRECATED. Backend pulse defaults dictionary or ``PulseDefaults`` custom_name_mapping: A name mapping must be supplied for the operation not included in Qiskit Standard Gate name mapping, otherwise the operation will be dropped in the resulting ``Target`` object. @@ -56,7 +59,20 @@ def convert_to_target( Returns: A ``Target`` instance. """ + return _convert_to_target( + configuration, properties, defaults, custom_name_mapping, add_delay, filter_faulty + ) + +def _convert_to_target( + configuration: BackendConfiguration, + properties: BackendProperties = None, + defaults: PulseDefaults = None, + custom_name_mapping: Optional[Dict[str, Any]] = None, + add_delay: bool = True, + filter_faulty: bool = True, +): + """An alternative private path to avoid pulse deprecations""" # importing packages where they are needed, to avoid cyclic-import. # pylint: disable=cyclic-import from qiskit.transpiler.target import ( @@ -265,7 +281,7 @@ def _get_value(prop_dict, prop_name): entry = inst_sched_map._get_calibration_entry(name, qubits) try: - prop_name_map[name][qubits].calibration = entry + prop_name_map[name][qubits]._calibration_prop = entry except AttributeError: # if instruction properties are "None", add entry prop_name_map[name].update({qubits: InstructionProperties(None, None, entry)}) @@ -410,7 +426,7 @@ def target(self): :rtype: Target """ if self._target is None: - self._target = convert_to_target( + self._target = _convert_to_target( configuration=self._config, properties=self._properties, defaults=self._defaults, @@ -436,15 +452,19 @@ def dtm(self) -> float: def meas_map(self) -> List[List[int]]: return self._config.meas_map + @deprecate_pulse_dependency def drive_channel(self, qubit: int): return self._config.drive(qubit) + @deprecate_pulse_dependency def measure_channel(self, qubit: int): return self._config.measure(qubit) + @deprecate_pulse_dependency def acquire_channel(self, qubit: int): return self._config.acquire(qubit) + @deprecate_pulse_dependency def control_channel(self, qubits: Iterable[int]): return self._config.control(qubits) diff --git a/qiskit/providers/fake_provider/fake_pulse_backend.py b/qiskit/providers/fake_provider/fake_pulse_backend.py index bf772d339bc7..65d5fe61df98 100644 --- a/qiskit/providers/fake_provider/fake_pulse_backend.py +++ b/qiskit/providers/fake_provider/fake_pulse_backend.py @@ -14,6 +14,8 @@ Fake backend abstract class for mock backends supporting OpenPulse. """ +import warnings + from qiskit.exceptions import QiskitError from qiskit.providers.models.backendconfiguration import PulseBackendConfiguration from qiskit.providers.models.pulsedefaults import PulseDefaults @@ -30,7 +32,10 @@ class FakePulseBackend(FakeQasmBackend): def defaults(self): """Returns a snapshot of device defaults""" if not self._defaults: - self._set_defaults_from_json() + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # Filter deprecation warnings emitted from Qiskit Pulse + self._set_defaults_from_json() return self._defaults def _set_defaults_from_json(self): diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index 770c8762152e..afb2c6b2bf82 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -42,6 +42,7 @@ from qiskit.pulse.calibration_entries import PulseQobjDef from qiskit.providers.models.pulsedefaults import MeasurementKernel, Discriminator from qiskit.qobj.pulse_qobj import QobjMeasurementOption +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency, deprecate_pulse_arg # Noise default values/ranges for duration and error of supported # instructions. There are two possible formats: @@ -518,6 +519,8 @@ class GenericBackendV2(BackendV2): transpilation. """ + @deprecate_pulse_arg("pulse_channels") + @deprecate_pulse_arg("calibrate_instructions") def __init__( self, num_qubits: int, @@ -560,7 +563,7 @@ def __init__( control_flow: Flag to enable control flow directives on the target (defaults to False). - calibrate_instructions: Instruction calibration settings, this argument + calibrate_instructions: DEPRECATED. Instruction calibration settings, this argument supports both boolean and :class:`.InstructionScheduleMap` as input types, and is ``None`` by default: @@ -578,7 +581,7 @@ def __init__( seed: Optional seed for generation of default values. - pulse_channels: If true, sets default pulse channel information on the backend. + pulse_channels: DEPRECATED. If true, sets default pulse channel information on the backend. noise_info: If true, associates gates and qubits with default noise information. """ @@ -648,14 +651,17 @@ def meas_map(self) -> list[list[int]]: return self._target.concurrent_measurements def _build_default_channels(self) -> None: - channels_map = { - "acquire": {(i,): [pulse.AcquireChannel(i)] for i in range(self.num_qubits)}, - "drive": {(i,): [pulse.DriveChannel(i)] for i in range(self.num_qubits)}, - "measure": {(i,): [pulse.MeasureChannel(i)] for i in range(self.num_qubits)}, - "control": { - (edge): [pulse.ControlChannel(i)] for i, edge in enumerate(self._coupling_map) - }, - } + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # Prevent pulse deprecation warnings from being emitted + channels_map = { + "acquire": {(i,): [pulse.AcquireChannel(i)] for i in range(self.num_qubits)}, + "drive": {(i,): [pulse.DriveChannel(i)] for i in range(self.num_qubits)}, + "measure": {(i,): [pulse.MeasureChannel(i)] for i in range(self.num_qubits)}, + "control": { + (edge): [pulse.ControlChannel(i)] for i, edge in enumerate(self._coupling_map) + }, + } setattr(self, "channels_map", channels_map) def _get_noise_defaults(self, name: str, num_qubits: int) -> tuple: @@ -867,27 +873,33 @@ def _add_noisy_instruction_to_target( duration, error = ( noise_params if len(noise_params) == 2 - else (self._rng.uniform(*noise_params[:2]), self._rng.uniform(*noise_params[2:])) - ) - if ( - calibration_inst_map is not None - and instruction.name not in ["reset", "delay"] - and qarg in calibration_inst_map.qubits_with_instruction(instruction.name) - ): - # Do NOT call .get method. This parses Qobj immediately. - # This operation is computationally expensive and should be bypassed. - calibration_entry = calibration_inst_map._get_calibration_entry( - instruction.name, qargs + else ( + self._rng.uniform(*noise_params[:2]), + self._rng.uniform(*noise_params[2:]), ) - else: - calibration_entry = None - if duration is not None and len(noise_params) > 2: - # Ensure exact conversion of duration from seconds to dt - dt = _QUBIT_PROPERTIES["dt"] - rounded_duration = round(duration / dt) * dt - # Clamp rounded duration to be between min and max values - duration = max(noise_params[0], min(rounded_duration, noise_params[1])) - props.update({qargs: InstructionProperties(duration, error, calibration_entry)}) + ) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # Prevent pulse deprecations from being emitted + if ( + calibration_inst_map is not None + and instruction.name not in ["reset", "delay"] + and qarg in calibration_inst_map.qubits_with_instruction(instruction.name) + ): + # Do NOT call .get method. This parses Qobj immediately. + # This operation is computationally expensive and should be bypassed. + calibration_entry = calibration_inst_map._get_calibration_entry( + instruction.name, qargs + ) + else: + calibration_entry = None + if duration is not None and len(noise_params) > 2: + # Ensure exact conversion of duration from seconds to dt + dt = _QUBIT_PROPERTIES["dt"] + rounded_duration = round(duration / dt) * dt + # Clamp rounded duration to be between min and max values + duration = max(noise_params[0], min(rounded_duration, noise_params[1])) + props.update({qargs: InstructionProperties(duration, error, calibration_entry)}) self._target.add_instruction(instruction, props) # The "measure" instruction calibrations need to be added qubit by qubit, once the @@ -990,6 +1002,7 @@ def _default_options(cls) -> Options: else: return BasicSimulator._default_options() + @deprecate_pulse_dependency def drive_channel(self, qubit: int): drive_channels_map = getattr(self, "channels_map", {}).get("drive", {}) qubits = (qubit,) @@ -997,6 +1010,7 @@ def drive_channel(self, qubit: int): return drive_channels_map[qubits][0] return None + @deprecate_pulse_dependency def measure_channel(self, qubit: int): measure_channels_map = getattr(self, "channels_map", {}).get("measure", {}) qubits = (qubit,) @@ -1004,6 +1018,7 @@ def measure_channel(self, qubit: int): return measure_channels_map[qubits][0] return None + @deprecate_pulse_dependency def acquire_channel(self, qubit: int): acquire_channels_map = getattr(self, "channels_map", {}).get("acquire", {}) qubits = (qubit,) @@ -1011,6 +1026,7 @@ def acquire_channel(self, qubit: int): return acquire_channels_map[qubits][0] return None + @deprecate_pulse_dependency def control_channel(self, qubits: Iterable[int]): control_channels_map = getattr(self, "channels_map", {}).get("control", {}) qubits = tuple(qubits) diff --git a/qiskit/providers/models/pulsedefaults.py b/qiskit/providers/models/pulsedefaults.py index 1302994d40e6..8f4101ffc510 100644 --- a/qiskit/providers/models/pulsedefaults.py +++ b/qiskit/providers/models/pulsedefaults.py @@ -18,6 +18,7 @@ from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap, PulseQobjDef from qiskit.qobj import PulseLibraryItem, PulseQobjInstruction from qiskit.qobj.converters import QobjToInstructionConverter +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency class MeasurementKernel: @@ -169,6 +170,7 @@ class PulseDefaults: _data = {} + @deprecate_pulse_dependency def __init__( self, qubit_freq_est: List[float], diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index 09d06cb5242d..743418168063 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -436,6 +436,7 @@ from qiskit.pulse.instructions import directives from qiskit.pulse.schedule import Schedule, ScheduleBlock from qiskit.pulse.transforms.alignments import AlignmentKind +from qiskit.utils.deprecate_pulse import deprecate_pulse_func if sys.version_info >= (3, 12): @@ -773,6 +774,7 @@ def get_dt(self): return self.backend.configuration().dt +@deprecate_pulse_func def build( backend=None, schedule: ScheduleBlock | None = None, @@ -846,6 +848,7 @@ def _active_builder() -> _PulseBuilder: ) from ex +@deprecate_pulse_func def active_backend(): """Get the backend of the currently active builder context. @@ -896,6 +899,7 @@ def append_instruction(instruction: instructions.Instruction): _active_builder().append_instruction(instruction) +@deprecate_pulse_func def num_qubits() -> int: """Return number of qubits in the currently active backend. @@ -922,6 +926,7 @@ def num_qubits() -> int: return active_backend().configuration().n_qubits +@deprecate_pulse_func def seconds_to_samples(seconds: float | np.ndarray) -> int | np.ndarray: """Obtain the number of samples that will elapse in ``seconds`` on the active backend. @@ -940,6 +945,7 @@ def seconds_to_samples(seconds: float | np.ndarray) -> int | np.ndarray: return int(seconds / dt) +@deprecate_pulse_func def samples_to_seconds(samples: int | np.ndarray) -> float | np.ndarray: """Obtain the time in seconds that will elapse for the input number of samples on the active backend. @@ -953,6 +959,7 @@ def samples_to_seconds(samples: int | np.ndarray) -> float | np.ndarray: return samples * _active_builder().get_dt() +@deprecate_pulse_func def qubit_channels(qubit: int) -> set[chans.Channel]: """Returns the set of channels associated with a qubit. @@ -1025,6 +1032,7 @@ def _qubits_to_channels(*channels_or_qubits: int | chans.Channel) -> set[chans.C @contextmanager +@deprecate_pulse_func def align_left() -> Generator[None, None, None]: """Left alignment pulse scheduling context. @@ -1063,6 +1071,7 @@ def align_left() -> Generator[None, None, None]: @contextmanager +@deprecate_pulse_func def align_right() -> Generator[None, None, None]: """Right alignment pulse scheduling context. @@ -1101,6 +1110,7 @@ def align_right() -> Generator[None, None, None]: @contextmanager +@deprecate_pulse_func def align_sequential() -> Generator[None, None, None]: """Sequential alignment pulse scheduling context. @@ -1139,6 +1149,7 @@ def align_sequential() -> Generator[None, None, None]: @contextmanager +@deprecate_pulse_func def align_equispaced(duration: int | ParameterExpression) -> Generator[None, None, None]: """Equispaced alignment pulse scheduling context. @@ -1191,6 +1202,7 @@ def align_equispaced(duration: int | ParameterExpression) -> Generator[None, Non @contextmanager +@deprecate_pulse_func def align_func( duration: int | ParameterExpression, func: Callable[[int], float] ) -> Generator[None, None, None]: @@ -1279,6 +1291,7 @@ def general_transforms(alignment_context: AlignmentKind) -> Generator[None, None @contextmanager +@deprecate_pulse_func def phase_offset(phase: float, *channels: chans.PulseChannel) -> Generator[None, None, None]: """Shift the phase of input channels on entry into context and undo on exit. @@ -1315,6 +1328,7 @@ def phase_offset(phase: float, *channels: chans.PulseChannel) -> Generator[None, @contextmanager +@deprecate_pulse_func def frequency_offset( frequency: float, *channels: chans.PulseChannel, compensate_phase: bool = False ) -> Generator[None, None, None]: @@ -1380,6 +1394,7 @@ def frequency_offset( # Channels +@deprecate_pulse_func def drive_channel(qubit: int) -> chans.DriveChannel: """Return ``DriveChannel`` for ``qubit`` on the active builder backend. @@ -1403,6 +1418,7 @@ def drive_channel(qubit: int) -> chans.DriveChannel: return active_backend().configuration().drive(qubit) +@deprecate_pulse_func def measure_channel(qubit: int) -> chans.MeasureChannel: """Return ``MeasureChannel`` for ``qubit`` on the active builder backend. @@ -1426,6 +1442,7 @@ def measure_channel(qubit: int) -> chans.MeasureChannel: return active_backend().configuration().measure(qubit) +@deprecate_pulse_func def acquire_channel(qubit: int) -> chans.AcquireChannel: """Return ``AcquireChannel`` for ``qubit`` on the active builder backend. @@ -1449,6 +1466,7 @@ def acquire_channel(qubit: int) -> chans.AcquireChannel: return active_backend().configuration().acquire(qubit) +@deprecate_pulse_func def control_channels(*qubits: Iterable[int]) -> list[chans.ControlChannel]: """Return ``ControlChannel`` for ``qubit`` on the active builder backend. @@ -1483,6 +1501,7 @@ def control_channels(*qubits: Iterable[int]) -> list[chans.ControlChannel]: # Base Instructions +@deprecate_pulse_func def delay(duration: int, channel: chans.Channel, name: str | None = None): """Delay on a ``channel`` for a ``duration``. @@ -1505,6 +1524,7 @@ def delay(duration: int, channel: chans.Channel, name: str | None = None): append_instruction(instructions.Delay(duration, channel, name=name)) +@deprecate_pulse_func def play(pulse: library.Pulse | np.ndarray, channel: chans.PulseChannel, name: str | None = None): """Play a ``pulse`` on a ``channel``. @@ -1538,6 +1558,7 @@ class _MetaDataType(TypedDict, total=False): name: str +@deprecate_pulse_func def acquire( duration: int, qubit_or_channel: int | chans.AcquireChannel, @@ -1591,6 +1612,7 @@ def acquire( raise exceptions.PulseError(f'Register of type: "{type(register)}" is not supported') +@deprecate_pulse_func def set_frequency(frequency: float, channel: chans.PulseChannel, name: str | None = None): """Set the ``frequency`` of a pulse ``channel``. @@ -1613,6 +1635,7 @@ def set_frequency(frequency: float, channel: chans.PulseChannel, name: str | Non append_instruction(instructions.SetFrequency(frequency, channel, name=name)) +@deprecate_pulse_func def shift_frequency(frequency: float, channel: chans.PulseChannel, name: str | None = None): """Shift the ``frequency`` of a pulse ``channel``. @@ -1636,6 +1659,7 @@ def shift_frequency(frequency: float, channel: chans.PulseChannel, name: str | N append_instruction(instructions.ShiftFrequency(frequency, channel, name=name)) +@deprecate_pulse_func def set_phase(phase: float, channel: chans.PulseChannel, name: str | None = None): """Set the ``phase`` of a pulse ``channel``. @@ -1661,6 +1685,7 @@ def set_phase(phase: float, channel: chans.PulseChannel, name: str | None = None append_instruction(instructions.SetPhase(phase, channel, name=name)) +@deprecate_pulse_func def shift_phase(phase: float, channel: chans.PulseChannel, name: str | None = None): """Shift the ``phase`` of a pulse ``channel``. @@ -1685,6 +1710,7 @@ def shift_phase(phase: float, channel: chans.PulseChannel, name: str | None = No append_instruction(instructions.ShiftPhase(phase, channel, name)) +@deprecate_pulse_func def snapshot(label: str, snapshot_type: str = "statevector"): """Simulator snapshot. @@ -1704,6 +1730,7 @@ def snapshot(label: str, snapshot_type: str = "statevector"): append_instruction(instructions.Snapshot(label, snapshot_type=snapshot_type)) +@deprecate_pulse_func def call( target: Schedule | ScheduleBlock | None, name: str | None = None, @@ -1898,6 +1925,7 @@ def call( _active_builder().call_subroutine(target, name, value_dict, **kw_params) +@deprecate_pulse_func def reference(name: str, *extra_keys: str): """Refer to undefined subroutine by string keys. @@ -1924,6 +1952,7 @@ def reference(name: str, *extra_keys: str): # Directives +@deprecate_pulse_func def barrier(*channels_or_qubits: chans.Channel | int, name: str | None = None): """Barrier directive for a set of channels and qubits. @@ -2061,6 +2090,7 @@ def wrapper(*args, **kwargs): return wrapper +@deprecate_pulse_func def measure( qubits: list[int] | int, registers: list[StorageLocation] | StorageLocation = None, @@ -2149,6 +2179,7 @@ def measure( return registers +@deprecate_pulse_func def measure_all() -> list[chans.MemorySlot]: r"""Measure all qubits within the currently active builder context. @@ -2192,6 +2223,7 @@ def measure_all() -> list[chans.MemorySlot]: return registers +@deprecate_pulse_func def delay_qubits(duration: int, *qubits: int): r"""Insert delays on all the :class:`channels.Channel`\s that correspond to the input ``qubits`` at the same time. diff --git a/qiskit/pulse/calibration_entries.py b/qiskit/pulse/calibration_entries.py index 8a5ba1b6e3d6..f0c0e4497fa1 100644 --- a/qiskit/pulse/calibration_entries.py +++ b/qiskit/pulse/calibration_entries.py @@ -321,7 +321,10 @@ def __init__( def _build_schedule(self): """Build pulse schedule from cmd-def sequence.""" - schedule = Schedule(name=self._name) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # `Schedule` is being deprecated in Qiskit 1.3 + schedule = Schedule(name=self._name) try: for qobj_inst in self._source: for qiskit_inst in self._converter._get_sequences(qobj_inst): diff --git a/qiskit/pulse/channels.py b/qiskit/pulse/channels.py index c70aac38ba19..8b196345c179 100644 --- a/qiskit/pulse/channels.py +++ b/qiskit/pulse/channels.py @@ -58,6 +58,7 @@ from qiskit.circuit import Parameter from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.pulse.exceptions import PulseError +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class Channel(metaclass=ABCMeta): @@ -90,6 +91,7 @@ def __new__(cls, *args, **kwargs): return super().__new__(cls) + @deprecate_pulse_func def __init__(self, index: int): """Channel class. diff --git a/qiskit/pulse/exceptions.py b/qiskit/pulse/exceptions.py index 21bda97ee1b9..29d5288bc121 100644 --- a/qiskit/pulse/exceptions.py +++ b/qiskit/pulse/exceptions.py @@ -12,11 +12,13 @@ """Exception for errors raised by the pulse module.""" from qiskit.exceptions import QiskitError +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class PulseError(QiskitError): """Errors raised by the pulse module.""" + @deprecate_pulse_func def __init__(self, *message): """Set the error message.""" super().__init__(*message) diff --git a/qiskit/pulse/instruction_schedule_map.py b/qiskit/pulse/instruction_schedule_map.py index afa71b6825a7..2815a9897db0 100644 --- a/qiskit/pulse/instruction_schedule_map.py +++ b/qiskit/pulse/instruction_schedule_map.py @@ -46,6 +46,7 @@ ) from qiskit.pulse.exceptions import PulseError from qiskit.pulse.schedule import Schedule, ScheduleBlock +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class InstructionScheduleMap: @@ -62,6 +63,7 @@ class InstructionScheduleMap: These can usually be seen as gate calibrations. """ + @deprecate_pulse_func def __init__(self): """Initialize a circuit instruction to schedule mapper instance.""" # The processed and reformatted circuit instruction definitions @@ -354,7 +356,10 @@ def get_parameters( instruction = _get_instruction_string(instruction) self.assert_has(instruction, qubits) - signature = self._map[instruction][_to_tuple(qubits)].get_signature() + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # Prevent `get_signature` from emitting pulse package deprecation warnings + signature = self._map[instruction][_to_tuple(qubits)].get_signature() return tuple(signature.parameters.keys()) def __str__(self): diff --git a/qiskit/pulse/instructions/acquire.py b/qiskit/pulse/instructions/acquire.py index 98fbf460c1b3..3b5964d4ee3e 100644 --- a/qiskit/pulse/instructions/acquire.py +++ b/qiskit/pulse/instructions/acquire.py @@ -19,6 +19,7 @@ from qiskit.pulse.configuration import Kernel, Discriminator from qiskit.pulse.exceptions import PulseError from qiskit.pulse.instructions.instruction import Instruction +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class Acquire(Instruction): @@ -38,6 +39,7 @@ class Acquire(Instruction): * the discriminator to classify kerneled IQ points. """ + @deprecate_pulse_func def __init__( self, duration: int | ParameterExpression, diff --git a/qiskit/pulse/instructions/delay.py b/qiskit/pulse/instructions/delay.py index 13f36d3b7d9b..6dd028c94fcd 100644 --- a/qiskit/pulse/instructions/delay.py +++ b/qiskit/pulse/instructions/delay.py @@ -16,6 +16,7 @@ from qiskit.circuit import ParameterExpression from qiskit.pulse.channels import Channel from qiskit.pulse.instructions.instruction import Instruction +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class Delay(Instruction): @@ -34,6 +35,7 @@ class Delay(Instruction): The ``channel`` will output no signal from time=0 up until time=10. """ + @deprecate_pulse_func def __init__( self, duration: int | ParameterExpression, diff --git a/qiskit/pulse/instructions/directives.py b/qiskit/pulse/instructions/directives.py index 3b7ec4c5e04c..7d44d14dd4ea 100644 --- a/qiskit/pulse/instructions/directives.py +++ b/qiskit/pulse/instructions/directives.py @@ -18,6 +18,7 @@ from qiskit.pulse import channels as chans from qiskit.pulse.instructions import instruction from qiskit.pulse.exceptions import PulseError +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class Directive(instruction.Instruction, ABC): @@ -35,6 +36,7 @@ def duration(self) -> int: class RelativeBarrier(Directive): """Pulse ``RelativeBarrier`` directive.""" + @deprecate_pulse_func def __init__(self, *channels: chans.Channel, name: str | None = None): """Create a relative barrier directive. @@ -107,6 +109,7 @@ class TimeBlockade(Directive): user can insert another instruction without timing overlap. """ + @deprecate_pulse_func def __init__( self, duration: int, diff --git a/qiskit/pulse/instructions/frequency.py b/qiskit/pulse/instructions/frequency.py index a14e60ee1a32..545d26c92639 100644 --- a/qiskit/pulse/instructions/frequency.py +++ b/qiskit/pulse/instructions/frequency.py @@ -19,6 +19,7 @@ from qiskit.pulse.channels import PulseChannel from qiskit.pulse.instructions.instruction import Instruction from qiskit.pulse.exceptions import PulseError +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class SetFrequency(Instruction): @@ -35,6 +36,7 @@ class SetFrequency(Instruction): The duration of SetFrequency is 0. """ + @deprecate_pulse_func def __init__( self, frequency: Union[float, ParameterExpression], @@ -85,6 +87,7 @@ def duration(self) -> int: class ShiftFrequency(Instruction): """Shift the channel frequency away from the current frequency.""" + @deprecate_pulse_func def __init__( self, frequency: Union[float, ParameterExpression], diff --git a/qiskit/pulse/instructions/instruction.py b/qiskit/pulse/instructions/instruction.py index 61ebe67777f8..fda854d2b7eb 100644 --- a/qiskit/pulse/instructions/instruction.py +++ b/qiskit/pulse/instructions/instruction.py @@ -28,6 +28,7 @@ from qiskit.circuit import Parameter, ParameterExpression from qiskit.pulse.channels import Channel from qiskit.pulse.exceptions import PulseError +from qiskit.utils.deprecate_pulse import deprecate_pulse_func # pylint: disable=bad-docstring-quotes @@ -38,6 +39,7 @@ class Instruction(ABC): channels. """ + @deprecate_pulse_func def __init__( self, operands: tuple, diff --git a/qiskit/pulse/instructions/phase.py b/qiskit/pulse/instructions/phase.py index 21f46dfb5f72..1d08918cb7e9 100644 --- a/qiskit/pulse/instructions/phase.py +++ b/qiskit/pulse/instructions/phase.py @@ -21,6 +21,7 @@ from qiskit.pulse.channels import PulseChannel from qiskit.pulse.instructions.instruction import Instruction from qiskit.pulse.exceptions import PulseError +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class ShiftPhase(Instruction): @@ -40,6 +41,7 @@ class ShiftPhase(Instruction): by using a ShiftPhase to update the frame tracking the qubit state. """ + @deprecate_pulse_func def __init__( self, phase: Union[complex, ParameterExpression], @@ -101,6 +103,7 @@ class SetPhase(Instruction): The ``SetPhase`` instruction sets :math:`\phi` to the instruction's ``phase`` operand. """ + @deprecate_pulse_func def __init__( self, phase: Union[complex, ParameterExpression], diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py index 1f42b8e3a08d..8c86555bbc8c 100644 --- a/qiskit/pulse/instructions/play.py +++ b/qiskit/pulse/instructions/play.py @@ -21,6 +21,7 @@ from qiskit.pulse.exceptions import PulseError from qiskit.pulse.instructions.instruction import Instruction from qiskit.pulse.library.pulse import Pulse +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class Play(Instruction): @@ -32,6 +33,7 @@ class Play(Instruction): cycle time, dt, of the backend. """ + @deprecate_pulse_func def __init__(self, pulse: Pulse, channel: PulseChannel, name: str | None = None): """Create a new pulse instruction. diff --git a/qiskit/pulse/instructions/reference.py b/qiskit/pulse/instructions/reference.py index cf3e01f8ea6e..1f17327877a7 100644 --- a/qiskit/pulse/instructions/reference.py +++ b/qiskit/pulse/instructions/reference.py @@ -17,6 +17,7 @@ from qiskit.pulse.channels import Channel from qiskit.pulse.exceptions import PulseError, UnassignedReferenceError from qiskit.pulse.instructions import instruction +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class Reference(instruction.Instruction): @@ -40,6 +41,7 @@ class Reference(instruction.Instruction): # Delimiter for tuple keys. key_delimiter = "," + @deprecate_pulse_func def __init__(self, name: str, *extra_keys: str): """Create new reference. diff --git a/qiskit/pulse/instructions/snapshot.py b/qiskit/pulse/instructions/snapshot.py index b78858b5d2c0..1692d13abc8f 100644 --- a/qiskit/pulse/instructions/snapshot.py +++ b/qiskit/pulse/instructions/snapshot.py @@ -18,11 +18,13 @@ from qiskit.pulse.channels import SnapshotChannel from qiskit.pulse.exceptions import PulseError from qiskit.pulse.instructions.instruction import Instruction +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class Snapshot(Instruction): """An instruction targeted for simulators, to capture a moment in the simulation.""" + @deprecate_pulse_func def __init__(self, label: str, snapshot_type: str = "statevector", name: Optional[str] = None): """Create new snapshot. diff --git a/qiskit/pulse/library/pulse.py b/qiskit/pulse/library/pulse.py index ebcc689e1e48..89d67a381ec5 100644 --- a/qiskit/pulse/library/pulse.py +++ b/qiskit/pulse/library/pulse.py @@ -18,6 +18,7 @@ import typing from abc import ABC, abstractmethod from typing import Any +from qiskit.utils.deprecate_pulse import deprecate_pulse_func from qiskit.circuit.parameterexpression import ParameterExpression @@ -36,6 +37,7 @@ class Pulse(ABC): limit_amplitude = True @abstractmethod + @deprecate_pulse_func def __init__( self, duration: int | ParameterExpression, diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index b7ba658da1be..0f3104dfa656 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -31,6 +31,7 @@ from qiskit.pulse.exceptions import PulseError from qiskit.pulse.library.pulse import Pulse from qiskit.pulse.library.waveform import Waveform +from qiskit.utils.deprecate_pulse import deprecate_pulse_func def _lifted_gaussian( @@ -403,6 +404,7 @@ def Sawtooth(duration, amp, freq, name): _constraints_lam = LambdifiedExpression("_constraints") _valid_amp_conditions_lam = LambdifiedExpression("_valid_amp_conditions") + @deprecate_pulse_func def __init__( self, pulse_type: str, @@ -780,6 +782,10 @@ def __new__( valid_amp_conditions=valid_amp_conditions_expr, ) + @deprecate_pulse_func + def __init__(self): + pass + class GaussianSquare(metaclass=_PulseType): """A square pulse with a Gaussian shaped risefall on both sides lifted such that @@ -908,7 +914,12 @@ def __new__( valid_amp_conditions=valid_amp_conditions_expr, ) + @deprecate_pulse_func + def __init__(self): + pass + +@deprecate_pulse_func def GaussianSquareDrag( duration: int | ParameterExpression, amp: float | ParameterExpression, @@ -1061,6 +1072,7 @@ def GaussianSquareDrag( ) +@deprecate_pulse_func def gaussian_square_echo( duration: int | ParameterValueType, amp: float | ParameterExpression, @@ -1264,6 +1276,7 @@ def gaussian_square_echo( ) +@deprecate_pulse_func def GaussianDeriv( duration: int | ParameterValueType, amp: float | ParameterExpression, @@ -1424,6 +1437,10 @@ def __new__( valid_amp_conditions=valid_amp_conditions_expr, ) + @deprecate_pulse_func + def __init__(self): + pass + class Constant(metaclass=_PulseType): """A simple constant pulse, with an amplitude value and a duration: @@ -1485,7 +1502,12 @@ def __new__( valid_amp_conditions=valid_amp_conditions_expr, ) + @deprecate_pulse_func + def __init__(self): + pass + +@deprecate_pulse_func def Sin( duration: int | ParameterExpression, amp: float | ParameterExpression, @@ -1550,6 +1572,7 @@ def Sin( ) +@deprecate_pulse_func def Cos( duration: int | ParameterExpression, amp: float | ParameterExpression, @@ -1614,6 +1637,7 @@ def Cos( ) +@deprecate_pulse_func def Sawtooth( duration: int | ParameterExpression, amp: float | ParameterExpression, @@ -1682,6 +1706,7 @@ def Sawtooth( ) +@deprecate_pulse_func def Triangle( duration: int | ParameterExpression, amp: float | ParameterExpression, @@ -1750,6 +1775,7 @@ def Triangle( ) +@deprecate_pulse_func def Square( duration: int | ParameterValueType, amp: float | ParameterExpression, @@ -1821,6 +1847,7 @@ def Square( ) +@deprecate_pulse_func def Sech( duration: int | ParameterValueType, amp: float | ParameterExpression, @@ -1897,6 +1924,7 @@ def Sech( ) +@deprecate_pulse_func def SechDeriv( duration: int | ParameterValueType, amp: float | ParameterExpression, diff --git a/qiskit/pulse/library/waveform.py b/qiskit/pulse/library/waveform.py index ad852f226ac2..0a31ac11a8d8 100644 --- a/qiskit/pulse/library/waveform.py +++ b/qiskit/pulse/library/waveform.py @@ -18,6 +18,7 @@ from qiskit.pulse.exceptions import PulseError from qiskit.pulse.library.pulse import Pulse +from qiskit.utils.deprecate_pulse import deprecate_pulse_func class Waveform(Pulse): @@ -25,6 +26,7 @@ class Waveform(Pulse): duration of the backend cycle-time, dt. """ + @deprecate_pulse_func def __init__( self, samples: np.ndarray | list[complex], diff --git a/qiskit/pulse/macros.py b/qiskit/pulse/macros.py index 3a39932e5b10..a01441dfc2f2 100644 --- a/qiskit/pulse/macros.py +++ b/qiskit/pulse/macros.py @@ -194,7 +194,7 @@ def _measure_v2( for measure_qubit in meas_group: try: if measure_qubit in qubits: - default_sched = target.get_calibration(measure_name, (measure_qubit,)).filter( + default_sched = target._get_calibration(measure_name, (measure_qubit,)).filter( channels=[ channels.MeasureChannel(measure_qubit), channels.AcquireChannel(measure_qubit), diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py index c8d5a3510fb1..d4753847b5d2 100644 --- a/qiskit/pulse/schedule.py +++ b/qiskit/pulse/schedule.py @@ -54,6 +54,7 @@ from qiskit.pulse.reference_manager import ReferenceManager from qiskit.utils.multiprocessing import is_main_process from qiskit.utils import deprecate_arg +from qiskit.utils.deprecate_pulse import deprecate_pulse_func Interval = Tuple[int, int] @@ -121,6 +122,7 @@ class Schedule: # Counter to count instance number. instances_counter = itertools.count() + @deprecate_pulse_func def __init__( self, *schedules: "ScheduleComponent" | tuple[int, "ScheduleComponent"], @@ -982,6 +984,7 @@ class ScheduleBlock: # Counter to count instance number. instances_counter = itertools.count() + @deprecate_pulse_func def __init__( self, name: str | None = None, metadata: dict | None = None, alignment_context=None ): diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index e17036210d57..3a0947f34f09 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -31,6 +31,7 @@ from qiskit.qobj import QobjMeasurementOption, PulseLibraryItem, PulseQobjInstruction from qiskit.qobj.utils import MeasLevel from qiskit.utils import deprecate_func +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency class ParametricPulseShapes(Enum): @@ -87,9 +88,9 @@ class InstructionToQobjConverter: This converter converts the Qiskit Pulse in-memory representation into the transfer layer format to submit the data from client to the server. - The transfer layer format must be the text representation that coforms to + The transfer layer format must be the text representation that conforms to the `OpenPulse specification`__. - Extention to the OpenPulse can be achieved by subclassing this this with + Extension to the OpenPulse can be achieved by subclassing this this with extra methods corresponding to each augmented instruction. For example, .. code-block:: python @@ -509,9 +510,9 @@ class QobjToInstructionConverter: This converter converts data from transfer layer into the in-memory representation of the front-end of Qiskit Pulse. - The transfer layer format must be the text representation that coforms to + The transfer layer format must be the text representation that conforms to the `OpenPulse specification`__. - Extention to the OpenPulse can be achieved by subclassing this this with + Extension to the OpenPulse can be achieved by subclassing this this with extra methods corresponding to each augmented instruction. For example, .. code-block:: python @@ -532,6 +533,7 @@ def _convert_new_inst(self, instruction): __chan_regex__ = re.compile(r"([a-zA-Z]+)(\d+)") + @deprecate_pulse_dependency def __init__( self, pulse_library: Optional[List[PulseLibraryItem]] = None, diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 142639a4e164..3fe1834db6ef 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -1310,7 +1310,7 @@ def write_circuit( instruction_buffer.close() # Write calibrations - _write_calibrations(file_obj, circuit.calibrations, metadata_serializer, version=version) + _write_calibrations(file_obj, circuit._calibrations_prop, metadata_serializer, version=version) _write_layout(file_obj, circuit) @@ -1440,7 +1440,9 @@ def read_circuit(file_obj, version, metadata_deserializer=None, use_symengine=Fa # Read calibrations if version >= 5: - circ.calibrations = _read_calibrations(file_obj, version, vectors, metadata_deserializer) + circ._calibrations_prop = _read_calibrations( + file_obj, version, vectors, metadata_deserializer + ) for vec_name, (vector, initialized_params) in vectors.items(): if len(initialized_params) != len(vector): diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 1bf86d254186..4afddb29992a 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -28,6 +28,7 @@ from qiskit.qpy.binary_io import value from qiskit.qpy.exceptions import QpyError from qiskit.pulse.configuration import Kernel, Discriminator +from qiskit.utils.deprecate_pulse import ignore_pulse_deprecation_warnings def _read_channel(file_obj, version): @@ -510,6 +511,7 @@ def _dumps_reference_item(schedule, metadata_serializer, version): return type_key, data_bytes +@ignore_pulse_deprecation_warnings def read_schedule_block(file_obj, version, metadata_deserializer=None, use_symengine=False): """Read a single ScheduleBlock from the file like object. diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index 2410e4b0a79d..688064625fb7 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -25,8 +25,9 @@ from qiskit.pulse import ScheduleBlock from qiskit.exceptions import QiskitError from qiskit.qpy import formats, common, binary_io, type_keys -from qiskit.qpy.exceptions import QpyError +from qiskit.qpy.exceptions import QPYLoadingDeprecatedFeatureWarning, QpyError from qiskit.version import __version__ +from qiskit.utils.deprecate_pulse import deprecate_pulse_arg # pylint: disable=invalid-name @@ -73,6 +74,11 @@ VERSION_PATTERN_REGEX = re.compile(VERSION_PATTERN, re.VERBOSE | re.IGNORECASE) +@deprecate_pulse_arg( + "programs", + deprecation_description="Passing `ScheduleBlock` to `programs`", + predicate=lambda p: isinstance(p, ScheduleBlock), +) def dump( programs: Union[List[QPY_SUPPORTED_TYPES], QPY_SUPPORTED_TYPES], file_obj: BinaryIO, @@ -120,6 +126,7 @@ def dump( programs: QPY supported object(s) to store in the specified file like object. QPY supports :class:`.QuantumCircuit` and :class:`.ScheduleBlock`. Different data types must be separately serialized. + Support for :class:`.ScheduleBlock` is deprecated since Qiskit 1.3.0. file_obj: The file like object to write the QPY data too metadata_serializer: An optional JSONEncoder class that will be passed the ``.metadata`` attribute for each program in ``programs`` and will be @@ -208,7 +215,10 @@ def dump( file_obj.write(header) common.write_type_key(file_obj, type_key) + pulse_gates = False for program in programs: + if type_key == type_keys.Program.CIRCUIT and program._calibrations_prop: + pulse_gates = True writer( file_obj, program, @@ -217,6 +227,13 @@ def dump( version=version, ) + if pulse_gates: + warnings.warn( + category=DeprecationWarning, + message="Pulse gates serialization is deprecated as of Qiskit 1.3. " + "It will be removed in Qiskit 2.0.", + ) + def load( file_obj: BinaryIO, @@ -331,6 +348,14 @@ def load( loader = binary_io.read_circuit elif type_key == type_keys.Program.SCHEDULE_BLOCK: loader = binary_io.read_schedule_block + warnings.warn( + category=QPYLoadingDeprecatedFeatureWarning, + message="Pulse gates deserialization is deprecated as of Qiskit 1.3 and " + "will be removed in Qiskit 2.0. This is part of the deprecation plan for " + "the entire Qiskit Pulse package. Once Pulse is removed, `ScheduleBlock` " + "sections will be ignored when loading QPY files with pulse data.", + ) + else: raise TypeError(f"Invalid payload format data kind '{type_key}'.") diff --git a/qiskit/quantum_info/__init__.py b/qiskit/quantum_info/__init__.py index ba3299c9e100..2f5d707c8802 100644 --- a/qiskit/quantum_info/__init__.py +++ b/qiskit/quantum_info/__init__.py @@ -28,6 +28,7 @@ Pauli Clifford ScalarOp + SparseObservable SparsePauliOp CNOTDihedral PauliList @@ -113,6 +114,9 @@ """ from __future__ import annotations + +from qiskit._accelerate.sparse_observable import SparseObservable + from .analysis import hellinger_distance, hellinger_fidelity, Z2Symmetries from .operators import ( Clifford, diff --git a/qiskit/quantum_info/operators/channel/transformations.py b/qiskit/quantum_info/operators/channel/transformations.py index 8f429cad8cea..657ee62703ec 100644 --- a/qiskit/quantum_info/operators/channel/transformations.py +++ b/qiskit/quantum_info/operators/channel/transformations.py @@ -220,32 +220,39 @@ def _kraus_to_choi(data): def _choi_to_kraus(data, input_dim, output_dim, atol=ATOL_DEFAULT): """Transform Choi representation to Kraus representation.""" - from scipy import linalg as la + import scipy.linalg # Check if hermitian matrix if is_hermitian_matrix(data, atol=atol): - # Get eigen-decomposition of Choi-matrix - # This should be a call to la.eigh, but there is an OpenBlas - # threading issue that is causing segfaults. - # Need schur here since la.eig does not - # guarantee orthogonality in degenerate subspaces - w, v = la.schur(data, output="complex") - w = w.diagonal().real - # Check eigenvalues are non-negative - if len(w[w < -atol]) == 0: - # CP-map Kraus representation - kraus = [] - for val, vec in zip(w, v.T): - if abs(val) > atol: - k = np.sqrt(val) * vec.reshape((output_dim, input_dim), order="F") - kraus.append(k) - # If we are converting a zero matrix, we need to return a Kraus set - # with a single zero-element Kraus matrix + # Ideally we'd use `eigh`, but `scipy.linalg.eigh` has stability problems on macOS (at a + # minimum from SciPy 1.1 to 1.13 with the bundled OpenBLAS, or ~0.3.6 before they started + # bundling one in). The Schur form of a Hermitian matrix is guaranteed diagonal: + # + # H = U T U+ for upper-triangular T. + # => H+ = U T+ U+ + # => T = T+ because H = H+, and thus T cannot have super-diagonal elements. + # + # So the eigenvalues are on the diagonal, therefore the basis-transformation matrix must be + # a spanning set of the eigenspace. + triangular, vecs = scipy.linalg.schur(data) + values = triangular.diagonal().real + # If we're not a CP map, fall-through back to the generalization handling. Since we needed + # to get the eigenvalues anyway, we can do the CP check manually rather than deferring to a + # separate re-calculation. + if all(values >= -atol): + kraus = [ + math.sqrt(value) * vec.reshape((output_dim, input_dim), order="F") + for value, vec in zip(values, vecs.T) + if abs(value) > atol + ] + # If we are converting a zero matrix, we need to return a Kraus set with a single + # zero-element Kraus matrix if not kraus: - kraus.append(np.zeros((output_dim, input_dim), dtype=complex)) + kraus = [np.zeros((output_dim, input_dim), dtype=complex)] return kraus, None - # Non-CP-map generalized Kraus representation - mat_u, svals, mat_vh = la.svd(data) + # Fall through. + # Non-CP-map generalized Kraus representation. + mat_u, svals, mat_vh = scipy.linalg.svd(data) kraus_l = [] kraus_r = [] for val, vec_l, vec_r in zip(svals, mat_u.T, mat_vh.conj()): diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index 42593626f2cc..fc3dd9ee187a 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -16,6 +16,7 @@ from __future__ import annotations +import cmath import copy as _copy import re from numbers import Number @@ -540,11 +541,37 @@ def compose(self, other: Operator, qargs: list | None = None, front: bool = Fals ret._op_shape = new_shape return ret - def power(self, n: float) -> Operator: + def power(self, n: float, branch_cut_rotation=cmath.pi * 1e-12) -> Operator: """Return the matrix power of the operator. + Non-integer powers of operators with an eigenvalue whose complex phase is :math:`\\pi` have + a branch cut in the complex plane, which makes the calculation of the principal root around + this cut subject to precision / differences in BLAS implementation. For example, the square + root of Pauli Y can return the :math:`\\pi/2` or :math:`\\-pi/2` Y rotation depending on + whether the -1 eigenvalue is found as ``complex(-1, tiny)`` or ``complex(-1, -tiny)``. Such + eigenvalues are really common in quantum information, so this function first phase-rotates + the input matrix to shift the branch cut to a far less common point. The underlying + numerical precision issues around the branch-cut point remain, if an operator has an + eigenvalue close to this phase. The magnitude of this rotation can be controlled with the + ``branch_cut_rotation`` parameter. + + The choice of ``branch_cut_rotation`` affects the principal root that is found. For + example, the square root of :class:`.ZGate` will be calculated as either :class:`.SGate` or + :class:`.SdgGate` depending on which way the rotation is done:: + + from qiskit.circuit import library + from qiskit.quantum_info import Operator + + z_op = Operator(library.ZGate()) + assert z_op.power(0.5, branch_cut_rotation=1e-3) == Operator(library.SGate()) + assert z_op.power(0.5, branch_cut_rotation=-1e-3) == Operator(library.SdgGate()) + Args: n (float): the power to raise the matrix to. + branch_cut_rotation (float): The rotation angle to apply to the branch cut in the + complex plane. This shifts the branch cut away from the common point of :math:`-1`, + but can cause a different root to be selected as the principal root. The rotation + is anticlockwise, following the standard convention for complex phase. Returns: Operator: the resulting operator ``O ** n``. @@ -561,13 +588,11 @@ def power(self, n: float) -> Operator: else: import scipy.linalg - # Experimentally, for fractional powers this seems to be 3x faster than - # calling scipy.linalg.fractional_matrix_power(self.data, n) - decomposition, unitary = scipy.linalg.schur(self.data, output="complex") - decomposition_diagonal = decomposition.diagonal() - decomposition_power = [pow(element, n) for element in decomposition_diagonal] - unitary_power = unitary @ np.diag(decomposition_power) @ unitary.conj().T - ret._data = unitary_power + ret._data = cmath.rect( + 1, branch_cut_rotation * n + ) * scipy.linalg.fractional_matrix_power( + cmath.rect(1, -branch_cut_rotation) * self.data, n + ) return ret def tensor(self, other: Operator) -> Operator: diff --git a/qiskit/scheduler/config.py b/qiskit/scheduler/config.py index b8e2d5ac2425..a1c5ba9a2c59 100644 --- a/qiskit/scheduler/config.py +++ b/qiskit/scheduler/config.py @@ -16,11 +16,13 @@ from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.pulse.utils import format_meas_map +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency class ScheduleConfig: """Configuration for pulse scheduling.""" + @deprecate_pulse_dependency(moving_to_dynamics=True) def __init__(self, inst_map: InstructionScheduleMap, meas_map: List[List[int]], dt: float): """ Container for information needed to schedule a QuantumCircuit into a pulse Schedule. diff --git a/qiskit/scheduler/methods/basic.py b/qiskit/scheduler/methods/basic.py index 60b2f20056b3..b08f0f866ab0 100644 --- a/qiskit/scheduler/methods/basic.py +++ b/qiskit/scheduler/methods/basic.py @@ -23,8 +23,10 @@ from qiskit.scheduler.config import ScheduleConfig from qiskit.scheduler.lowering import lower_gates from qiskit.providers import BackendV1, BackendV2 +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency +@deprecate_pulse_dependency(moving_to_dynamics=True) def as_soon_as_possible( circuit: QuantumCircuit, schedule_config: ScheduleConfig, @@ -78,6 +80,7 @@ def update_times(inst_qubits: List[int], time: int = 0) -> None: return schedule +@deprecate_pulse_dependency(moving_to_dynamics=True) def as_late_as_possible( circuit: QuantumCircuit, schedule_config: ScheduleConfig, diff --git a/qiskit/scheduler/schedule_circuit.py b/qiskit/scheduler/schedule_circuit.py index 52fd8bf72b76..2cc32a8a7b3c 100644 --- a/qiskit/scheduler/schedule_circuit.py +++ b/qiskit/scheduler/schedule_circuit.py @@ -20,8 +20,10 @@ from qiskit.scheduler.config import ScheduleConfig from qiskit.scheduler.methods import as_soon_as_possible, as_late_as_possible from qiskit.providers import BackendV1, BackendV2 +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency +@deprecate_pulse_dependency(moving_to_dynamics=True) def schedule_circuit( circuit: QuantumCircuit, schedule_config: ScheduleConfig, diff --git a/qiskit/scheduler/sequence.py b/qiskit/scheduler/sequence.py index d73a5cdf93df..7f69f5f65a6a 100644 --- a/qiskit/scheduler/sequence.py +++ b/qiskit/scheduler/sequence.py @@ -25,8 +25,10 @@ from qiskit.scheduler.config import ScheduleConfig from qiskit.scheduler.lowering import lower_gates from qiskit.providers import BackendV1, BackendV2 +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency +@deprecate_pulse_dependency(moving_to_dynamics=True) def sequence( scheduled_circuit: QuantumCircuit, schedule_config: ScheduleConfig, diff --git a/qiskit/transpiler/basepasses.py b/qiskit/transpiler/basepasses.py index 396f5cf49344..b4993915c1cc 100644 --- a/qiskit/transpiler/basepasses.py +++ b/qiskit/transpiler/basepasses.py @@ -201,7 +201,7 @@ def execute( if state.workflow_status.previous_run == RunState.SUCCESS: if isinstance(new_dag, DAGCircuit): # Copy calibration data from the original program - new_dag.calibrations = passmanager_ir.calibrations + new_dag._calibrations_prop = passmanager_ir._calibrations_prop else: raise TranspilerError( "Transformation passes should return a transformed dag." diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index ca4e3545a98b..5bc1ae555a5e 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -93,6 +93,7 @@ NormalizeRXAngle OptimizeAnnotated Split2QUnitaries + RemoveIdentityEquivalent Calibration ============= @@ -247,6 +248,7 @@ from .optimization import ElidePermutations from .optimization import NormalizeRXAngle from .optimization import OptimizeAnnotated +from .optimization import RemoveIdentityEquivalent from .optimization import Split2QUnitaries # circuit analysis diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index a1d3e7f0d39c..c75bff1a6ebc 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -276,7 +276,7 @@ def apply_translation(dag, wire_map): out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) continue - if dag.has_calibration_for(node): + if dag._has_calibration_for(node): out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) continue if qubit_set in extra_instr_map: @@ -383,7 +383,7 @@ def _extract_basis(self, circuit): @_extract_basis.register def _(self, dag: DAGCircuit): for node in dag.op_nodes(): - if not dag.has_calibration_for(node) and len(node.qargs) >= self._min_qubits: + if not dag._has_calibration_for(node) and len(node.qargs) >= self._min_qubits: yield (node.name, node.num_qubits) if node.name in CONTROL_FLOW_OP_NAMES: for block in node.op.blocks: @@ -394,7 +394,7 @@ def _(self, circ: QuantumCircuit): for instruction in circ.data: operation = instruction.operation if ( - not circ.has_calibration_for(instruction) + not circ._has_calibration_for(instruction) and len(instruction.qubits) >= self._min_qubits ): yield (operation.name, operation.num_qubits) @@ -411,7 +411,7 @@ def _extract_basis_target( qargs_local_source_basis = defaultdict(set) for node in dag.op_nodes(): qargs = tuple(qarg_indices[bit] for bit in node.qargs) - if dag.has_calibration_for(node) or len(node.qargs) < self._min_qubits: + if dag._has_calibration_for(node) or len(node.qargs) < self._min_qubits: continue # Treat the instruction as on an incomplete basis if the qargs are in the # qargs_with_non_global_operation dictionary or if any of the qubits in qargs diff --git a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py index 0c6d780f052a..885009a23e03 100644 --- a/qiskit/transpiler/passes/basis/unroll_3q_or_more.py +++ b/qiskit/transpiler/passes/basis/unroll_3q_or_more.py @@ -54,7 +54,7 @@ def run(self, dag): QiskitError: if a 3q+ gate is not decomposable """ for node in dag.multi_qubit_ops(): - if dag.has_calibration_for(node): + if dag._has_calibration_for(node): continue if isinstance(node.op, ControlFlowOp): diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py index 51e116033bb6..11ba6afcd1b3 100644 --- a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -74,7 +74,7 @@ def run(self, dag): if getattr(node.op, "_directive", False): continue - if dag.has_calibration_for(node) or len(node.qargs) < self._min_qubits: + if dag._has_calibration_for(node) or len(node.qargs) < self._min_qubits: continue controlled_gate_open_ctrl = isinstance(node.op, ControlledGate) and node.op._open_ctrl diff --git a/qiskit/transpiler/passes/calibration/pulse_gate.py b/qiskit/transpiler/passes/calibration/pulse_gate.py index eacabbe89057..f5b56ad0f359 100644 --- a/qiskit/transpiler/passes/calibration/pulse_gate.py +++ b/qiskit/transpiler/passes/calibration/pulse_gate.py @@ -19,6 +19,7 @@ from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.transpiler.target import Target from qiskit.transpiler.exceptions import TranspilerError +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency from .base_builder import CalibrationBuilder @@ -47,6 +48,7 @@ class PulseGates(CalibrationBuilder): https://arxiv.org/abs/2104.14722 """ + @deprecate_pulse_dependency def __init__( self, inst_map: InstructionScheduleMap = None, @@ -80,7 +82,7 @@ def supported(self, node_op: CircuitInst, qubits: List) -> bool: Returns: Return ``True`` is calibration can be provided. """ - return self.target.has_calibration(node_op.name, tuple(qubits)) + return self.target._has_calibration(node_op.name, tuple(qubits)) def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: """Gets the calibrated schedule for the given instruction and qubits. @@ -95,4 +97,4 @@ def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, Raises: TranspilerError: When node is parameterized and calibration is raw schedule object. """ - return self.target.get_calibration(node_op.name, tuple(qubits), *node_op.params) + return self.target._get_calibration(node_op.name, tuple(qubits), *node_op.params) diff --git a/qiskit/transpiler/passes/calibration/rx_builder.py b/qiskit/transpiler/passes/calibration/rx_builder.py index 8d9955ed48dd..8543badc1289 100644 --- a/qiskit/transpiler/passes/calibration/rx_builder.py +++ b/qiskit/transpiler/passes/calibration/rx_builder.py @@ -24,6 +24,7 @@ from qiskit.pulse.library.symbolic_pulses import Drag from qiskit.transpiler.passes.calibration.base_builder import CalibrationBuilder from qiskit.transpiler.target import Target +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency class RXCalibrationBuilder(CalibrationBuilder): @@ -75,6 +76,7 @@ class RXCalibrationBuilder(CalibrationBuilder): `arXiv:2004.11205 ` """ + @deprecate_pulse_dependency def __init__( self, target: Target = None, @@ -99,13 +101,15 @@ def supported(self, node_op: Instruction, qubits: list) -> bool: """ return ( isinstance(node_op, RXGate) - and self.target.has_calibration("sx", tuple(qubits)) - and (len(self.target.get_calibration("sx", tuple(qubits)).instructions) == 1) + and self.target._has_calibration("sx", tuple(qubits)) + and (len(self.target._get_calibration("sx", tuple(qubits)).instructions) == 1) and isinstance( - self.target.get_calibration("sx", tuple(qubits)).instructions[0][1].pulse, + self.target._get_calibration("sx", tuple(qubits)).instructions[0][1].pulse, ScalableSymbolicPulse, ) - and self.target.get_calibration("sx", tuple(qubits)).instructions[0][1].pulse.pulse_type + and self.target._get_calibration("sx", tuple(qubits)) + .instructions[0][1] + .pulse.pulse_type == "Drag" ) @@ -122,13 +126,13 @@ def get_calibration(self, node_op: Instruction, qubits: list) -> Union[Schedule, raise QiskitError("Target rotation angle is not assigned.") from ex params = ( - self.target.get_calibration("sx", tuple(qubits)) + self.target._get_calibration("sx", tuple(qubits)) .instructions[0][1] .pulse.parameters.copy() ) new_rx_sched = _create_rx_sched( rx_angle=angle, - channel=self.target.get_calibration("sx", tuple(qubits)).channels[0], + channel=self.target._get_calibration("sx", tuple(qubits)).channels[0], duration=params["duration"], amp=params["amp"], sigma=params["sigma"], diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py index c153c3eeef33..72cf347db9bb 100644 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ b/qiskit/transpiler/passes/calibration/rzx_builder.py @@ -36,6 +36,7 @@ from qiskit.pulse.filters import filter_instructions from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.transpiler.target import Target +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency from .base_builder import CalibrationBuilder from .exceptions import CalibrationNotAvailable @@ -65,6 +66,7 @@ class RZXCalibrationBuilder(CalibrationBuilder): angle. Additional details can be found in https://arxiv.org/abs/2012.11660. """ + @deprecate_pulse_dependency def __init__( self, instruction_schedule_map: InstructionScheduleMap = None, @@ -89,7 +91,7 @@ def __init__( self._inst_map = instruction_schedule_map self._verbose = verbose if target: - self._inst_map = target.instruction_schedule_map() + self._inst_map = target._instruction_schedule_map() if self._inst_map is None: raise QiskitError("Calibrations can only be added to Pulse-enabled backends") @@ -202,15 +204,20 @@ def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | Sche # The CR instruction is in the forward (native) direction if cal_type in [CRCalType.ECR_CX_FORWARD, CRCalType.ECR_FORWARD]: - xgate = self._inst_map.get("x", qubits[0]) - with builder.build( - default_alignment="sequential", name=f"rzx({theta:.3f})" - ) as rzx_theta_native: - for cr_tone, comp_tone in zip(cr_tones, comp_tones): - with builder.align_left(): - self.rescale_cr_inst(cr_tone, theta) - self.rescale_cr_inst(comp_tone, theta) - builder.call(xgate) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # `InstructionScheduleMap.get` and the pulse builder emit deprecation warnings + # as they use classes and methods which are deprecated in Qiskit 1.3 as part of the + # Qiskit Pulse deprecation + xgate = self._inst_map.get("x", qubits[0]) + with builder.build( + default_alignment="sequential", name=f"rzx({theta:.3f})" + ) as rzx_theta_native: + for cr_tone, comp_tone in zip(cr_tones, comp_tones): + with builder.align_left(): + self.rescale_cr_inst(cr_tone, theta) + self.rescale_cr_inst(comp_tone, theta) + builder.call(xgate) return rzx_theta_native # The direction is not native. Add Hadamard gates to flip the direction. @@ -297,11 +304,15 @@ def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | Sche # RZXCalibrationNoEcho only good for forward CR direction if cal_type in [CRCalType.ECR_CX_FORWARD, CRCalType.ECR_FORWARD]: - with builder.build(default_alignment="left", name=f"rzx({theta:.3f})") as rzx_theta: - stretched_dur = self.rescale_cr_inst(cr_tones[0], 2 * theta) - self.rescale_cr_inst(comp_tones[0], 2 * theta) - # Placeholder to make pulse gate work - builder.delay(stretched_dur, DriveChannel(qubits[0])) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # Pulse builder emits deprecation warnings as part of the + # Qiskit Pulse deprecation + with builder.build(default_alignment="left", name=f"rzx({theta:.3f})") as rzx_theta: + stretched_dur = self.rescale_cr_inst(cr_tones[0], 2 * theta) + self.rescale_cr_inst(comp_tones[0], 2 * theta) + # Placeholder to make pulse gate work + builder.delay(stretched_dur, DriveChannel(qubits[0])) return rzx_theta raise QiskitError("RZXCalibrationBuilderNoEcho only supports hardware-native RZX gates.") @@ -347,22 +358,27 @@ def _check_calibration_type( QiskitError: Unknown calibration type is detected. """ cal_type = None - if inst_sched_map.has("cx", qubits): - cr_sched = inst_sched_map.get("cx", qubits=qubits) - elif inst_sched_map.has("ecr", qubits): - cr_sched = inst_sched_map.get("ecr", qubits=qubits) - cal_type = CRCalType.ECR_FORWARD - elif inst_sched_map.has("ecr", tuple(reversed(qubits))): - cr_sched = inst_sched_map.get("ecr", tuple(reversed(qubits))) - cal_type = CRCalType.ECR_REVERSE - else: - raise QiskitError( - f"Native direction cannot be determined: operation on qubits {qubits} " - f"for the following instruction schedule map:\n{inst_sched_map}" - ) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # `InstructionScheduleMap.get` and `filter_instructions` emit deprecation warnings + # as they use classes and methods which are deprecated in Qiskit 1.3 as part of the + # Qiskit Pulse deprecation + if inst_sched_map.has("cx", qubits): + cr_sched = inst_sched_map.get("cx", qubits=qubits) + elif inst_sched_map.has("ecr", qubits): + cr_sched = inst_sched_map.get("ecr", qubits=qubits) + cal_type = CRCalType.ECR_FORWARD + elif inst_sched_map.has("ecr", tuple(reversed(qubits))): + cr_sched = inst_sched_map.get("ecr", tuple(reversed(qubits))) + cal_type = CRCalType.ECR_REVERSE + else: + raise QiskitError( + f"Native direction cannot be determined: operation on qubits {qubits} " + f"for the following instruction schedule map:\n{inst_sched_map}" + ) - cr_tones = [t[1] for t in filter_instructions(cr_sched, [_filter_cr_tone]).instructions] - comp_tones = [t[1] for t in filter_instructions(cr_sched, [_filter_comp_tone]).instructions] + cr_tones = [t[1] for t in filter_instructions(cr_sched, [_filter_cr_tone]).instructions] + comp_tones = [t[1] for t in filter_instructions(cr_sched, [_filter_comp_tone]).instructions] if cal_type is None: if len(comp_tones) == 0: diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 8e2883b27781..c0e455b2065b 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -38,5 +38,6 @@ from .elide_permutations import ElidePermutations from .normalize_rx_angle import NormalizeRXAngle from .optimize_annotated import OptimizeAnnotated +from .remove_identity_equiv import RemoveIdentityEquivalent from .split_2q_unitaries import Split2QUnitaries from .collect_and_collapse import CollectAndCollapse diff --git a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py index 4b96c9c86dfb..c926e15800ae 100644 --- a/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py +++ b/qiskit/transpiler/passes/optimization/echo_rzx_weyl_decomposition.py @@ -21,6 +21,7 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout from qiskit.transpiler.passes.calibration.rzx_builder import _check_calibration_type, CRCalType +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency from qiskit.dagcircuit import DAGCircuit from qiskit.converters import circuit_to_dag @@ -34,6 +35,7 @@ class EchoRZXWeylDecomposition(TransformationPass): Each pair of RZXGates forms an echoed RZXGate. """ + @deprecate_pulse_dependency def __init__(self, instruction_schedule_map=None, target=None): """EchoRZXWeylDecomposition pass. diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index eadff1bfe867..55232ac1be20 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -170,13 +170,13 @@ def _substitution_checks( return False # do we even have calibrations? - has_cals_p = dag.calibrations is not None and len(dag.calibrations) > 0 + has_cals_p = dag._calibrations_prop is not None and len(dag._calibrations_prop) > 0 # does this run have uncalibrated gates? - uncalibrated_p = not has_cals_p or any(not dag.has_calibration_for(g) for g in old_run) + uncalibrated_p = not has_cals_p or any(not dag._has_calibration_for(g) for g in old_run) # does this run have gates not in the image of ._decomposers _and_ uncalibrated? if basis is not None: uncalibrated_and_not_basis_p = any( - g.name not in basis and (not has_cals_p or not dag.has_calibration_for(g)) + g.name not in basis and (not has_cals_p or not dag._has_calibration_for(g)) for g in old_run ) else: diff --git a/qiskit/transpiler/passes/optimization/remove_identity_equiv.py b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py new file mode 100644 index 000000000000..4f8551f12442 --- /dev/null +++ b/qiskit/transpiler/passes/optimization/remove_identity_equiv.py @@ -0,0 +1,69 @@ +# 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. + +"""Transpiler pass to drop gates with negligible effects.""" + +from __future__ import annotations + +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.target import Target +from qiskit.transpiler.basepasses import TransformationPass +from qiskit._accelerate.remove_identity_equiv import remove_identity_equiv + + +class RemoveIdentityEquivalent(TransformationPass): + r"""Remove gates with negligible effects. + + Removes gates whose effect is close to an identity operation, up to the specified + tolerance. Zero qubit gates such as :class:`.GlobalPhaseGate` are not considered + by this pass. + + For a cutoff fidelity :math:`f`, this pass removes gates whose average + gate fidelity with respect to the identity is below :math:`f`. Concretely, + a gate :math:`G` is removed if :math:`\bar F < f` where + + .. math:: + + \bar{F} = \frac{1 + F_{\text{process}}{1 + d} + + F_{\text{process}} = \frac{|\mathrm{Tr}(G)|^2}{d^2} + + where :math:`d = 2^n` is the dimension of the gate for :math:`n` qubits. + """ + + def __init__( + self, *, approximation_degree: float | None = 1.0, target: None | Target = None + ) -> None: + """Initialize the transpiler pass. + + Args: + approximation_degree: The degree to approximate for the equivalence check. This can be a + floating point value between 0 and 1, or ``None``. If the value is 1 this does not + approximate above floating point precision. For a value < 1 this is used as a scaling + factor for the cutoff fidelity. If the value is ``None`` this approximates up to the + fidelity for the gate specified in ``target``. + + target: If ``approximation_degree`` is set to ``None`` and a :class:`.Target` is provided + for this field the tolerance for determining whether an operation is equivalent to + identity will be set to the reported error rate in the target. If + ``approximation_degree`` (the default) this has no effect, if + ``approximation_degree=None`` it uses the error rate specified in the ``Target`` for + the gate being evaluated, and a numeric value other than 1 with ``target`` set is + used as a scaling factor of the target's error rate. + """ + super().__init__() + self._approximation_degree = approximation_degree + self._target = target + + def run(self, dag: DAGCircuit) -> DAGCircuit: + remove_identity_equiv(dag, self._approximation_degree, self._target) + return dag diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index 10ae623c4659..cdbdd4654f3c 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -144,7 +144,7 @@ def run(self, dag): new_dag.name = dag.name new_dag.metadata = dag.metadata - new_dag.calibrations = dag.calibrations + new_dag._calibrations_prop = dag._calibrations_prop # set circuit duration and unit to indicate it is scheduled new_dag.duration = circuit_duration diff --git a/qiskit/transpiler/passes/scheduling/alignments/check_durations.py b/qiskit/transpiler/passes/scheduling/alignments/check_durations.py index 46fdb1f5160b..3edfdf7ec741 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/check_durations.py +++ b/qiskit/transpiler/passes/scheduling/alignments/check_durations.py @@ -70,7 +70,7 @@ def run(self, dag: DAGCircuit): return # Check custom gate durations - for inst_defs in dag.calibrations.values(): + for inst_defs in dag._calibrations_prop.values(): for caldef in inst_defs.values(): dur = caldef.duration if not (dur % self.acquire_align == 0 and dur % self.pulse_align == 0): diff --git a/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py b/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py index a39b81092fbf..885730aac48a 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py +++ b/qiskit/transpiler/passes/scheduling/alignments/pulse_gate_validation.py @@ -17,6 +17,7 @@ from qiskit.transpiler.basepasses import AnalysisPass from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.target import Target +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency class ValidatePulseGates(AnalysisPass): @@ -40,6 +41,7 @@ class ValidatePulseGates(AnalysisPass): the backend control electronics. """ + @deprecate_pulse_dependency def __init__( self, granularity: int = 1, diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index b1e559718143..13ff58b1b0f0 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -167,7 +167,7 @@ def run(self, dag): new_dag.name = dag.name new_dag.metadata = dag.metadata - new_dag.calibrations = dag.calibrations + new_dag._calibrations_prop = dag._calibrations_prop # set circuit duration and unit to indicate it is scheduled new_dag.duration = circuit_duration diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index 74256b33d351..c380c9f8c199 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -266,10 +266,10 @@ def _get_node_duration( """A helper method to get duration from node or calibration.""" indices = [dag.find_bit(qarg).index for qarg in node.qargs] - if dag.has_calibration_for(node): + if dag._has_calibration_for(node): # If node has calibration, this value should be the highest priority cal_key = tuple(indices), tuple(float(p) for p in node.op.params) - duration = dag.calibrations[node.op.name][cal_key].duration + duration = dag._calibrations_prop[node.op.name][cal_key].duration else: duration = node.op.duration diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index a4695c24439c..7ae27ddf03d0 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -13,6 +13,7 @@ """Dynamical Decoupling insertion pass.""" import itertools +import warnings import numpy as np from qiskit.circuit import Gate, Delay, Reset @@ -288,11 +289,15 @@ def _update_inst_durations(self, dag): """ circ_durations = InstructionDurations() - if dag.calibrations: + if dag._calibrations_prop: cal_durations = [] - for gate, gate_cals in dag.calibrations.items(): - for (qubits, parameters), schedule in gate_cals.items(): - cal_durations.append((gate, qubits, parameters, schedule.duration)) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # `schedule.duration` emits pulse deprecation warnings which we don't want + # to see here + for gate, gate_cals in dag._calibrations_prop.items(): + for (qubits, parameters), schedule in gate_cals.items(): + cal_durations.append((gate, qubits, parameters, schedule.duration)) circ_durations.update(cal_durations, circ_durations.dt) if self._durations is not None: diff --git a/qiskit/transpiler/passes/scheduling/padding/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py index 4ce17e7bc261..e8876920aa03 100644 --- a/qiskit/transpiler/passes/scheduling/padding/base_padding.py +++ b/qiskit/transpiler/passes/scheduling/padding/base_padding.py @@ -99,7 +99,7 @@ def run(self, dag: DAGCircuit): new_dag.name = dag.name new_dag.metadata = dag.metadata new_dag.unit = self.property_set["time_unit"] - new_dag.calibrations = dag.calibrations + new_dag._calibrations_prop = dag._calibrations_prop new_dag.global_phase = dag.global_phase idle_after = {bit: 0 for bit in dag.qubits} diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 2217d32f847c..0a692a85621b 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -14,6 +14,7 @@ from __future__ import annotations import logging +import warnings import numpy as np from qiskit.circuit import Gate, ParameterExpression, Qubit @@ -187,11 +188,15 @@ def _update_inst_durations(self, dag): """ circ_durations = InstructionDurations() - if dag.calibrations: + if dag._calibrations_prop: cal_durations = [] - for gate, gate_cals in dag.calibrations.items(): - for (qubits, parameters), schedule in gate_cals.items(): - cal_durations.append((gate, qubits, parameters, schedule.duration)) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # `schedule.duration` emits pulse deprecation warnings which we don't want + # to see here + for gate, gate_cals in dag._calibrations_prop.items(): + for (qubits, parameters), schedule in gate_cals.items(): + cal_durations.append((gate, qubits, parameters, schedule.duration)) circ_durations.update(cal_durations, circ_durations.dt) if self._durations is not None: @@ -252,7 +257,13 @@ def _pre_runhook(self, dag: DAGCircuit): try: # Check calibration. params = self._resolve_params(gate) - gate_length = dag.calibrations[gate.name][((physical_index,), params)].duration + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # `schedule.duration` emits pulse deprecation warnings which we don't want + # to see here + gate_length = dag._calibrations_prop[gate.name][ + ((physical_index,), params) + ].duration if gate_length % self._alignment != 0: # This is necessary to implement lightweight scheduling logic for this pass. # Usually the pulse alignment constraint and pulse data chunk size take diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index d9f3d77f2915..4c21a210c1c0 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -64,10 +64,14 @@ def _get_node_duration( """A helper method to get duration from node or calibration.""" indices = [dag.find_bit(qarg).index for qarg in node.qargs] - if dag.has_calibration_for(node): + if dag._has_calibration_for(node): # If node has calibration, this value should be the highest priority cal_key = tuple(indices), tuple(float(p) for p in node.op.params) - duration = dag.calibrations[node.op.name][cal_key].duration + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # `schedule.duration` emits pulse deprecation warnings which we don't want + # to see here + duration = dag._calibrations_prop[node.op.name][cal_key].duration # Note that node duration is updated (but this is analysis pass) op = node.op.to_mutable() diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index f4f70210b785..8bb743ce6b3e 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -12,6 +12,7 @@ """Unify time unit in circuit for scheduling and following passes.""" from typing import Set +import warnings from qiskit.circuit import Delay from qiskit.dagcircuit import DAGCircuit @@ -121,11 +122,15 @@ def _update_inst_durations(self, dag): """ circ_durations = InstructionDurations() - if dag.calibrations: + if dag._calibrations_prop: cal_durations = [] - for gate, gate_cals in dag.calibrations.items(): - for (qubits, parameters), schedule in gate_cals.items(): - cal_durations.append((gate, qubits, parameters, schedule.duration)) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # `schedule.duration` emits pulse deprecation warnings which we don't want + # to see here + for gate, gate_cals in dag._calibrations_prop.items(): + for (qubits, parameters), schedule in gate_cals.items(): + cal_durations.append((gate, qubits, parameters, schedule.duration)) circ_durations.update(cal_durations, circ_durations.dt) if self._durations_provided: diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 733d5465fd0a..8bdd0a761edf 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -646,7 +646,7 @@ def _definitely_skip_node( node (which is _most_ nodes).""" if ( - dag.has_calibration_for(node) + dag._has_calibration_for(node) or len(node.qargs) < self._min_qubits or node.is_directive() ): diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index e31f6918f452..a2bd044c7341 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -73,6 +73,7 @@ from qiskit.transpiler.passes.synthesis import plugin from qiskit.transpiler.target import Target +from qiskit._accelerate.unitary_synthesis import run_default_main_loop GATE_NAME_MAP = { "cx": CXGate._standard_gate, @@ -456,37 +457,6 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: default_kwargs = {} method_list = [(plugin_method, plugin_kwargs), (default_method, default_kwargs)] - for method, kwargs in method_list: - if method.supports_basis_gates: - kwargs["basis_gates"] = self._basis_gates - if method.supports_natural_direction: - kwargs["natural_direction"] = self._natural_direction - if method.supports_pulse_optimize: - kwargs["pulse_optimize"] = self._pulse_optimize - if method.supports_gate_lengths: - _gate_lengths = _gate_lengths or _build_gate_lengths( - self._backend_props, self._target - ) - kwargs["gate_lengths"] = _gate_lengths - if method.supports_gate_errors: - _gate_errors = _gate_errors or _build_gate_errors(self._backend_props, self._target) - kwargs["gate_errors"] = _gate_errors - if method.supports_gate_lengths_by_qubit: - _gate_lengths_by_qubit = _gate_lengths_by_qubit or _build_gate_lengths_by_qubit( - self._backend_props, self._target - ) - kwargs["gate_lengths_by_qubit"] = _gate_lengths_by_qubit - if method.supports_gate_errors_by_qubit: - _gate_errors_by_qubit = _gate_errors_by_qubit or _build_gate_errors_by_qubit( - self._backend_props, self._target - ) - kwargs["gate_errors_by_qubit"] = _gate_errors_by_qubit - supported_bases = method.supported_bases - if supported_bases is not None: - kwargs["matched_basis"] = _choose_bases(self._basis_gates, supported_bases) - if method.supports_target: - kwargs["target"] = self._target - # Handle approximation degree as a special case for backwards compatibility, it's # not part of the plugin interface and only something needed for the default # pass. @@ -501,9 +471,60 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if plugin_method.supports_coupling_map or default_method.supports_coupling_map else {} ) - return self._run_main_loop( - dag, qubit_indices, plugin_method, plugin_kwargs, default_method, default_kwargs - ) + + if self.method == "default" and self._target is not None: + _coupling_edges = ( + set(self._coupling_map.get_edges()) if self._coupling_map is not None else set() + ) + + out = run_default_main_loop( + dag, + list(qubit_indices.values()), + self._min_qubits, + self._target, + _coupling_edges, + self._approximation_degree, + self._natural_direction, + ) + return out + else: + for method, kwargs in method_list: + if method.supports_basis_gates: + kwargs["basis_gates"] = self._basis_gates + if method.supports_natural_direction: + kwargs["natural_direction"] = self._natural_direction + if method.supports_pulse_optimize: + kwargs["pulse_optimize"] = self._pulse_optimize + if method.supports_gate_lengths: + _gate_lengths = _gate_lengths or _build_gate_lengths( + self._backend_props, self._target + ) + kwargs["gate_lengths"] = _gate_lengths + if method.supports_gate_errors: + _gate_errors = _gate_errors or _build_gate_errors( + self._backend_props, self._target + ) + kwargs["gate_errors"] = _gate_errors + if method.supports_gate_lengths_by_qubit: + _gate_lengths_by_qubit = _gate_lengths_by_qubit or _build_gate_lengths_by_qubit( + self._backend_props, self._target + ) + kwargs["gate_lengths_by_qubit"] = _gate_lengths_by_qubit + if method.supports_gate_errors_by_qubit: + _gate_errors_by_qubit = _gate_errors_by_qubit or _build_gate_errors_by_qubit( + self._backend_props, self._target + ) + kwargs["gate_errors_by_qubit"] = _gate_errors_by_qubit + supported_bases = method.supported_bases + if supported_bases is not None: + kwargs["matched_basis"] = _choose_bases(self._basis_gates, supported_bases) + if method.supports_target: + kwargs["target"] = self._target + + out = self._run_main_loop( + dag, qubit_indices, plugin_method, plugin_kwargs, default_method, default_kwargs + ) + return out def _run_main_loop( self, dag, qubit_indices, plugin_method, plugin_kwargs, default_method, default_kwargs diff --git a/qiskit/transpiler/passes/utils/gate_direction.py b/qiskit/transpiler/passes/utils/gate_direction.py index 8ea77f7ccd46..198eb34c501f 100644 --- a/qiskit/transpiler/passes/utils/gate_direction.py +++ b/qiskit/transpiler/passes/utils/gate_direction.py @@ -192,7 +192,7 @@ def _run_coupling_map(self, dag, wire_map, edges=None): continue if len(node.qargs) != 2: continue - if dag.has_calibration_for(node): + if dag._has_calibration_for(node): continue qargs = (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) if qargs not in edges and (qargs[1], qargs[0]) not in edges: @@ -239,7 +239,7 @@ def _run_target(self, dag, wire_map): continue if len(node.qargs) != 2: continue - if dag.has_calibration_for(node): + if dag._has_calibration_for(node): continue qargs = (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) swapped = (qargs[1], qargs[0]) @@ -311,7 +311,7 @@ def _run_target(self, dag, wire_map): ) elif self.target.instruction_supported(node.name, qargs): continue - elif self.target.instruction_supported(node.name, swapped) or dag.has_calibration_for( + elif self.target.instruction_supported(node.name, swapped) or dag._has_calibration_for( _swap_node_qargs(node) ): raise TranspilerError( diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 0ebbff430301..baf4482a3b05 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -160,7 +160,7 @@ def from_backend(cls, backend, _skip_target=False, **pass_manager_options): if defaults is not None: res.inst_map = defaults.instruction_schedule_map else: - res.inst_map = backend.instruction_schedule_map + res.inst_map = backend._instruction_schedule_map if res.coupling_map is None: if backend_version < 2: cmap_edge_list = getattr(config, "coupling_map", None) diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 68e266a09e70..9301588c0744 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -13,6 +13,7 @@ """Built-in transpiler stage plugins for preset pass managers.""" import os +import warnings from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries from qiskit.transpiler.passmanager import PassManager @@ -676,9 +677,13 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana inst_map = pass_manager_config.inst_map target = pass_manager_config.target - return common.generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map, target - ) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # Passing `inst_map` to `generate_scheduling` is deprecated in Qiskit 1.3 + # so filtering these warning when building pass managers + return common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map, target + ) class AsapSchedulingPassManager(PassManagerStagePlugin): @@ -693,9 +698,13 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana inst_map = pass_manager_config.inst_map target = pass_manager_config.target - return common.generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map, target - ) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # Passing `inst_map` to `generate_scheduling` is deprecated in Qiskit 1.3 + # so filtering these warning when building pass managers + return common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map, target + ) class DefaultSchedulingPassManager(PassManagerStagePlugin): @@ -710,9 +719,13 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana inst_map = pass_manager_config.inst_map target = pass_manager_config.target - return common.generate_scheduling( - instruction_durations, scheduling_method, timing_constraints, inst_map, target - ) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # Passing `inst_map` to `generate_scheduling` is deprecated in Qiskit 1.3 + # so filtering these warning when building pass managers + return common.generate_scheduling( + instruction_durations, scheduling_method, timing_constraints, inst_map, target + ) class DefaultLayoutPassManager(PassManagerStagePlugin): diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index f01639ed115e..25d21880bd23 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -52,6 +52,7 @@ from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout +from qiskit.utils.deprecate_pulse import deprecate_pulse_arg _ControlFlowState = collections.namedtuple("_ControlFlowState", ("working", "not_working")) @@ -529,6 +530,7 @@ def generate_translation_passmanager( return PassManager(unroll) +@deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def generate_scheduling( instruction_durations, scheduling_method, timing_constraints, inst_map, target=None ): @@ -540,7 +542,7 @@ def generate_scheduling( ``'asap'``/``'as_soon_as_possible'`` or ``'alap'``/``'as_late_as_possible'`` timing_constraints (TimingConstraints): Hardware time alignment restrictions. - inst_map (InstructionScheduleMap): Mapping object that maps gate to schedule. + inst_map (InstructionScheduleMap): DEPRECATED. Mapping object that maps gate to schedule. target (Target): The :class:`~.Target` object representing the backend Returns: diff --git a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py index 830618845352..779a3512faab 100644 --- a/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py +++ b/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py @@ -29,6 +29,7 @@ from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.target import Target, target_to_backend_properties from qiskit.transpiler.timing_constraints import TimingConstraints +from qiskit.utils.deprecate_pulse import deprecate_pulse_arg from .level0 import level_0_pass_manager from .level1 import level_1_pass_manager @@ -36,6 +37,7 @@ from .level3 import level_3_pass_manager +@deprecate_pulse_arg("inst_map", predicate=lambda inst_map: inst_map is not None) def generate_preset_pass_manager( optimization_level=2, backend=None, @@ -122,7 +124,7 @@ def generate_preset_pass_manager( and ``backend_properties``. basis_gates (list): List of basis gate names to unroll to (e.g: ``['u1', 'u2', 'u3', 'cx']``). - inst_map (InstructionScheduleMap): Mapping object that maps gates to schedules. + inst_map (InstructionScheduleMap): DEPRECATED. Mapping object that maps gates to schedules. If any user defined calibration is found in the map and this is used in a circuit, transpiler attaches the custom gate definition to the circuit. This enables one to flexibly override the low-level instruction @@ -339,7 +341,7 @@ def generate_preset_pass_manager( if instruction_durations is None: instruction_durations = target.durations() if inst_map is None: - inst_map = target.instruction_schedule_map() + inst_map = target._get_instruction_schedule_map() if timing_constraints is None: timing_constraints = target.timing_constraints() if backend_properties is None: @@ -452,7 +454,7 @@ def _parse_basis_gates(basis_gates, backend, inst_map, skip_target): def _parse_inst_map(inst_map, backend): # try getting inst_map from user, else backend if inst_map is None and backend is not None: - inst_map = backend.target.instruction_schedule_map() + inst_map = backend.target._get_instruction_schedule_map() return inst_map diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index da4a44a8ee02..1edc89013572 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -57,6 +57,7 @@ from qiskit.providers.backend import QubitProperties # pylint: disable=unused-import from qiskit.providers.models.backendproperties import BackendProperties from qiskit.utils import deprecate_func +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency, deprecate_pulse_arg logger = logging.getLogger(__name__) @@ -86,6 +87,7 @@ def __new__( # pylint: disable=keyword-arg-before-vararg cls, duration, error ) + @deprecate_pulse_arg("calibration", predicate=lambda cals: cals is not None) def __init__( self, duration: float | None = None, # pylint: disable=unused-argument @@ -99,13 +101,14 @@ def __init__( specified set of qubits error: The average error rate for the instruction on the specified set of qubits. - calibration: The pulse representation of the instruction. + calibration: DEPRECATED. The pulse representation of the instruction. """ super().__init__() self._calibration: CalibrationEntry | None = None - self.calibration = calibration + self._calibration_prop = calibration @property + @deprecate_pulse_dependency(is_property=True) def calibration(self): """The pulse representation of the instruction. @@ -133,12 +136,24 @@ def calibration(self): use own definition to compile the circuit down to the execution format. """ - if self._calibration is None: - return None - return self._calibration.get_schedule() + return self._calibration_prop @calibration.setter + @deprecate_pulse_dependency(is_property=True) def calibration(self, calibration: Schedule | ScheduleBlock | CalibrationEntry): + self._calibration_prop = calibration + + @property + def _calibration_prop(self): + if self._calibration is None: + return None + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # Clean this alternative path from deprecation warning emitted by `get_schedule` + return self._calibration.get_schedule() + + @_calibration_prop.setter + def _calibration_prop(self, calibration: Schedule | ScheduleBlock | CalibrationEntry): if isinstance(calibration, (Schedule, ScheduleBlock)): new_entry = ScheduleDef() new_entry.define(calibration, user_provided=True) @@ -153,11 +168,11 @@ def __repr__(self): ) def __getstate__(self) -> tuple: - return (super().__getstate__(), self.calibration, self._calibration) + return (super().__getstate__(), self._calibration_prop, self._calibration) def __setstate__(self, state: tuple): super().__setstate__(state[0]) - self.calibration = state[1] + self._calibration_prop = state[1] self._calibration = state[2] @@ -447,6 +462,7 @@ def update_instruction_properties(self, instruction, qargs, properties): self._instruction_durations = None self._instruction_schedule_map = None + @deprecate_pulse_dependency def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, error_dict=None): """Update the target from an instruction schedule map. @@ -604,6 +620,7 @@ def timing_constraints(self): self.granularity, self.min_length, self.pulse_alignment, self.acquire_alignment ) + @deprecate_pulse_dependency def instruction_schedule_map(self): """Return an :class:`~qiskit.pulse.InstructionScheduleMap` for the instructions in the target with a pulse schedule defined. @@ -612,9 +629,17 @@ def instruction_schedule_map(self): InstructionScheduleMap: The instruction schedule map for the instructions in this target with a pulse schedule defined. """ + return self._get_instruction_schedule_map() + + def _get_instruction_schedule_map(self): if self._instruction_schedule_map is not None: return self._instruction_schedule_map - out_inst_schedule_map = InstructionScheduleMap() + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + # `InstructionScheduleMap` is deprecated in Qiskit 1.3 but we want this alternative + # path to be clean of deprecation warnings + out_inst_schedule_map = InstructionScheduleMap() + for instruction, qargs in self._gate_map.items(): for qarg, properties in qargs.items(): # Directly getting CalibrationEntry not to invoke .get_schedule(). @@ -626,6 +651,7 @@ def instruction_schedule_map(self): self._instruction_schedule_map = out_inst_schedule_map return out_inst_schedule_map + @deprecate_pulse_dependency def has_calibration( self, operation_name: str, @@ -640,6 +666,13 @@ def has_calibration( Returns: Returns ``True`` if the calibration is supported and ``False`` if it isn't. """ + return self._has_calibration(operation_name, qargs) + + def _has_calibration( + self, + operation_name: str, + qargs: tuple[int, ...], + ) -> bool: qargs = tuple(qargs) if operation_name not in self._gate_map: return False @@ -647,6 +680,7 @@ def has_calibration( return False return getattr(self._gate_map[operation_name][qargs], "_calibration", None) is not None + @deprecate_pulse_dependency def get_calibration( self, operation_name: str, @@ -668,7 +702,16 @@ def get_calibration( Returns: Calibrated pulse schedule of corresponding instruction. """ - if not self.has_calibration(operation_name, qargs): + return self._get_calibration(operation_name, qargs, *args, *kwargs) + + def _get_calibration( + self, + operation_name: str, + qargs: tuple[int, ...], + *args: ParameterValueType, + **kwargs: ParameterValueType, + ) -> Schedule | ScheduleBlock: + if not self._has_calibration(operation_name, qargs): raise KeyError( f"Calibration of instruction {operation_name} for qubit {qargs} is not defined." ) @@ -927,6 +970,7 @@ def __setstate__(self, state: tuple): super().__setstate__(state["base"]) @classmethod + @deprecate_pulse_arg("inst_map") def from_configuration( cls, basis_gates: list[str], @@ -965,7 +1009,7 @@ def from_configuration( coupling_map: The coupling map representing connectivity constraints on the backend. If specified all gates from ``basis_gates`` will be supported on all qubits (or pairs of qubits). - inst_map: The instruction schedule map representing the pulse + inst_map: DEPRECATED. The instruction schedule map representing the pulse :class:`~.Schedule` definitions for each instruction. If this is specified ``coupling_map`` must be specified. The ``coupling_map`` is used as the source of truth for connectivity diff --git a/qiskit/utils/deprecate_pulse.py b/qiskit/utils/deprecate_pulse.py new file mode 100644 index 000000000000..376e3e06c8f1 --- /dev/null +++ b/qiskit/utils/deprecate_pulse.py @@ -0,0 +1,119 @@ +# 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. + +""" +Deprecation functions for Qiskit Pulse. To be removed in Qiskit 2.0. +""" + +import warnings +import functools + +from qiskit.utils.deprecation import deprecate_func, deprecate_arg + + +def deprecate_pulse_func(func): + """Deprecation message for functions and classes""" + return deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The entire Qiskit Pulse package is being deprecated " + "and will be moved to the Qiskit Dynamics repository: " + "https://github.com/qiskit-community/qiskit-dynamics", + )(func) + + +def deprecate_pulse_dependency(*args, moving_to_dynamics: bool = False, **kwargs): + # pylint: disable=missing-param-doc + """Deprecation message for functions and classes which use or depend on Pulse + + Args: + moving_to_dynamics: set to True if the dependency is moving to Qiskit Dynamics. This affects + the deprecation message being printed, namely saying explicitly whether the dependency will + be moved to Qiskit Dynamics or whether it will just be removed without an alternative. + """ + + def msg_handler(func): + fully_qual_name = format(f"{func.__module__}.{func.__qualname__}") + if ".__init__" in fully_qual_name: # Deprecating a class' vis it __init__ method + fully_qual_name = fully_qual_name[:-9] + elif "is_property" not in kwargs: # Deprecating either a function or a method + fully_qual_name += "()" + + message = ( + "The entire Qiskit Pulse package is being deprecated and will be moved to the Qiskit " + "Dynamics repository: https://github.com/qiskit-community/qiskit-dynamics." + + ( + format(f" Note that ``{fully_qual_name}`` will be moved as well.") + if moving_to_dynamics + else format( + f" Note that once removed, ``{fully_qual_name}`` will have no alternative in Qiskit." + ) + ) + ) + + decorator = deprecate_func( + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg=message, + **kwargs, + )(func) + + # Taken when `deprecate_pulse_dependency` is used with no arguments and with empty parentheses, + # in which case the decorated function is passed + + return decorator + + if args: + return msg_handler(args[0]) + return msg_handler + + +def deprecate_pulse_arg(arg_name: str, **kwargs): + """Deprecation message for arguments related to Pulse""" + return deprecate_arg( + name=arg_name, + since="1.3", + package_name="Qiskit", + removal_timeline="in Qiskit 2.0", + additional_msg="The entire Qiskit Pulse package is being deprecated " + "and this argument uses a dependency on the package.", + **kwargs, + ) + + +def ignore_pulse_deprecation_warnings(func): + """Ignore deprecation warnings emitted from the pulse package""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=DeprecationWarning, message="The (.*) ``qiskit.pulse" + ) + return func(*args, **kwargs) + + return wrapper + + +def decorate_test_methods(decorator): + """Put a given decorator on all the decorated class methods whose name starts with `test_`""" + + def cls_wrapper(cls): + for attr in dir(cls): + if attr.startswith("test_") and callable(object.__getattribute__(cls, attr)): + setattr(cls, attr, decorator(object.__getattribute__(cls, attr))) + + return cls + + return cls_wrapper diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index 9c4fa25309fc..62e1e0e7b156 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -135,7 +135,7 @@ def __init__( self._initial_state = initial_state self._global_phase = self._circuit.global_phase - self._calibrations = self._circuit.calibrations + self._calibrations = self._circuit._calibrations_prop self._expr_len = expr_len self._cregbundle = cregbundle diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index a6a111fe75e9..8f8b8fc89097 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -81,7 +81,7 @@ def dag_drawer(dag, scale=0.7, filename=None, style="color"): ``rustworkx`` package to draw the DAG. Args: - dag (DAGCircuit): The dag to draw. + dag (DAGCircuit or DAGDependency): The dag to draw. scale (float): scaling factor filename (str): file path to save image to (format inferred from name) style (str): 'plain': B&W graph diff --git a/qiskit/visualization/pulse_v2/interface.py b/qiskit/visualization/pulse_v2/interface.py index 75370905d8d9..6feb4bc83339 100644 --- a/qiskit/visualization/pulse_v2/interface.py +++ b/qiskit/visualization/pulse_v2/interface.py @@ -28,9 +28,11 @@ from qiskit.visualization.pulse_v2 import core, device_info, stylesheet, types from qiskit.exceptions import MissingOptionalLibraryError from qiskit.utils import deprecate_arg +from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency -@deprecate_arg("show_barriers", new_alias="plot_barriers", since="1.1.0", pending=True) +@deprecate_pulse_dependency(moving_to_dynamics=True) +@deprecate_arg("show_barrier", new_alias="plot_barrier", since="1.1.0", pending=True) def draw( program: Union[Waveform, SymbolicPulse, Schedule, ScheduleBlock], style: Optional[Dict[str, Any]] = None, diff --git a/releasenotes/notes/deprecate-pulse-package-07a621be1db7fa30.yaml b/releasenotes/notes/deprecate-pulse-package-07a621be1db7fa30.yaml new file mode 100644 index 000000000000..5b4941f9d847 --- /dev/null +++ b/releasenotes/notes/deprecate-pulse-package-07a621be1db7fa30.yaml @@ -0,0 +1,80 @@ +--- +deprecations: + - | + The Qiskit Pulse package is being deprecated and will be removed in Qiskit 2.0.0. Pulse-level + access is currently only supported on a subset of Eagle devices and not supported on the Heron architecture. Furthermore, newer IBM Quantum architectures will not support pulse-level access. + As a consequence, supporting Pulse as a first-class citizen frontend in the Qiskit SDK itself makes little + sense going forward. The deprecation includes all pulse code in :mod:`qiskit.pulse` as well as functionality + dependant or related to pulse such as pulse visualization, serialization and custom calibration support. For + more details see the deprecation sections. + + The Pulse package as a whole, along with directly related components in Qiskit, will be moved to the + `Qiskit Dynamics `__ repository to further enable + pulse and low-level control simulation. +deprecations_circuits: + - | + As part of the Qiskit Pulse package deprecation, the following dependencies are deprecated as well: + * :attr:`qiskit.circuit.QuantumCircuit.calibrations` + * :meth:`qiskit.circuit.QuantumCircuit.has_calibration_for` + * :meth:`qiskit.circuit.QuantumCircuit.add_calibration` + * :attr:`qiskit.dagcircuit.DAGCircuit.calibrations` + * :meth:`qiskit.dagcircuit.DAGCircuit.has_calibration_for` + * :meth:`qiskit.dagcircuit.DAGCircuit.add_calibration` + * :attr:`qiskit.dagcircuit.DAGDependency.calibrations` +deprecations_qpy: + - | + As part of the Qiskit Pulse package deprecation, serializing a :class:`qiskit.pulse.ScheduleBlock`-based payloads + is being deprecated. In particular, passing :class:`qiskit.pulse.ScheduleBlock` objects to the `programs` argument in + the :func:`qiskit.qpy.dump` function is being deprecated. +deprecations_transpiler: + - | + As part of the Qiskit Pulse package deprecation, pulse-related aspects in the :class:`qiskit.transpiler.Target` class are being deprecated. These include: + * :attr:`~qiskit.transpiler.Target.calibration` + * :meth:`~qiskit.transpiler.Target.update_from_instruction_schedule_map` + * :meth:`~qiskit.transpiler.Target.has_calibration` + * :meth:`~qiskit.transpiler.Target.get_calibration` + * :meth:`~qiskit.transpiler.Target.instruction_schedule_map` + + In addition the following transpiler passer are also being deprecated: + * :class:`~qiskit.transpiler.passes.PulseGates` + * :class:`~qiskit.transpiler.passes.ValidatePulseGates` + * :class:`~qiskit.transpiler.passes.RXCalibrationBuilder` + * :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` + * :class:`~qiskit.transpiler.passes.EchoRZXWeylDecomposition` + - | + The `inst_map` argument in :func:`~qiskit.transpiler.generate_preset_pass_manager`, + :meth:`~transpiler.target.Target.from_configuration` and :func:`~qiskit.transpiler.preset_passmanagers.common.generate_scheduling` + is being deprecated. + - | + The `calibration` argument in :func:`~qiskit.transpiler.target.InstructionProperties` initializer methods is being + deprecated. +deprecations_visualization: + - | + As part of the Qiskit Pulse package deprecation, pulse drawing via :meth:`qiskit.visualization.pulse_drawer` + is being deprecated. +deprecations_providers: + - | + As part of the Qiskit Pulse package deprecation, all pulse-related functionality in :class:`qiskit.providers.BackendV2` class is being deprecated. This includes the following methods: + * :meth:`~qiskit.providers.BackendV2.instruction_schedule_map` + * :meth:`~qiskit.providers.BackendV2.drive_channel` + * :meth:`~qiskit.providers.BackendV2.measure_channel` + * :meth:`~qiskit.providers.BackendV2.acquire_channel` + * :meth:`~qiskit.providers.BackendV2.control_channel` + + Consequently, the corresponding channel methods in the :class:`qiskit.providers.BackendV2Converter` and + :class:`qiskit.providers.fake_provider.GenericBackendV2` classes are being deprecated as well. + + In addition, the `pulse_channels` and `calibrate_instructions` arguments in the :class:`~qiskit.providers.BackendV2` + initializer method are being deprecated. + - | + The `defaults` argument is being deprecated from the :func:`qiskit.providers.backend_compat.convert_to_target` function. +deprecations_misc: + - | + As part of the Qiskit Pulse package deprecation, the following functions and class are being deprecated as well: + * :meth:`qiskit.compiler.schedule` + * :meth:`qiskit.compiler.sequence` + * :meth:`qiskit.assembler.assemble_schedules` + * :meth:`qiskit.scheduler.methods.as_soon_as_possible` + * :meth:`qiskit.scheduler.methods.as_late_as_possible` + * :meth:`qiskit.scheduler.schedule_circuit.schedule_circuit` + * :class:`qiskit.scheduler.ScheduleConfig` diff --git a/releasenotes/notes/qasm2-builtin-gate-d80c2868cdf5f958.yaml b/releasenotes/notes/qasm2-builtin-gate-d80c2868cdf5f958.yaml new file mode 100644 index 000000000000..bfcf867f2a3b --- /dev/null +++ b/releasenotes/notes/qasm2-builtin-gate-d80c2868cdf5f958.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + The OpenQASM 2 importer previously would output incorrect :class:`.Gate` instances for gate + calls referring to a ``gate`` definition that followed a prior ``gate`` definition that was + being treated as a built-in operation by a :class:`~.qasm2.CustomInstruction`. See + `#13339 `__ for more detail. diff --git a/releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml b/releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml new file mode 100644 index 000000000000..90d016803d62 --- /dev/null +++ b/releasenotes/notes/remove_identity_equiv-9c627c8c35b2298a.yaml @@ -0,0 +1,29 @@ +--- +features_transpiler: + - | + Added a new transpiler pass, :class:`.RemoveIdentityEquivalent` that is used + to remove gates that are equivalent to an identity up to some tolerance. + For example if you had a circuit like: + + .. plot:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(2) + qc.cp(1e-20, 0, 1) + qc.draw("mpl") + + running the pass would eliminate the :class:`.CPhaseGate`: + + .. plot:: + :include-source: + + from qiskit.circuit import QuantumCircuit + from qiskit.transpiler.passes import RemoveIdentityEquivalent + + qc = QuantumCircuit(2) + qc.cp(1e-20, 0, 1) + + removal_pass = RemoveIdentityEquivalent() + result = removal_pass(qc) + result.draw("mpl") diff --git a/releasenotes/notes/scipy-1.14-951d1c245473aee9.yaml b/releasenotes/notes/scipy-1.14-951d1c245473aee9.yaml new file mode 100644 index 000000000000..41f4b8790286 --- /dev/null +++ b/releasenotes/notes/scipy-1.14-951d1c245473aee9.yaml @@ -0,0 +1,25 @@ +--- +fixes: + - | + Fixed :meth:`.Operator.power` when called with non-integer powers on a matrix whose Schur form + is not diagonal (for example, most non-unitary matrices). + - | + :meth:`.Operator.power` will now more reliably return the expected principal value from a + fractional matrix power of a unitary matrix with a :math:`-1` eigenvalue. This is tricky in + general, because floating-point rounding effects can cause a matrix to _truly_ have an eigenvalue + on the negative side of the branch cut (even if its exact mathematical relation would not), and + imprecision in various BLAS calls can falsely find the wrong side of the branch cut. + + :meth:`.Operator.power` now shifts the branch-cut location for matrix powers to be a small + complex rotation away from :math:`-1`. This does not solve the problem, it just shifts it to a + place where it is far less likely to be noticeable for the types of operators that usually + appear. Use the new ``branch_cut_rotation`` parameter to have more control over this. + + See `#13305 `__. +features_quantum_info: + - | + The method :meth:`.Operator.power` has a new parameter ``branch_cut_rotation``. This can be + used to shift the branch-cut point of the root around, which can affect which matrix is chosen + as the principal root. By default, it is set to a small positive rotation to make roots of + operators with a real-negative eigenvalue (like Pauli operators) more stable against numerical + precision differences. diff --git a/releasenotes/notes/sparse-observable-7de70dcdf6962a64.yaml b/releasenotes/notes/sparse-observable-7de70dcdf6962a64.yaml new file mode 100644 index 000000000000..48617e6d1968 --- /dev/null +++ b/releasenotes/notes/sparse-observable-7de70dcdf6962a64.yaml @@ -0,0 +1,32 @@ +--- +features_quantum_info: + - | + A new observable class has been added. :class:`.SparseObservable` represents observables as a + sum of terms, similar to :class:`.SparsePauliOp`, but with two core differences: + + 1. Each complete term is stored as (effectively) a series of ``(qubit, bit_term)`` pairs, + without storing qubits that undergo the identity for that term. This significantly improves + the memory usage of observables such as the weighted sum of Paulis :math:`\sum_i c_i Z_i`. + + 2. The single-qubit term alphabet is overcomplete for the operator space; it can represent Pauli + operators (like :class:`.SparsePauliOp`), but also projectors onto the eigenstates of the + Pauli operators, like :math:`\lvert 0\rangle\langle 0\rangle`. Such projectors can be + measured on hardware equally as efficiently as their corresponding Pauli operator, but + :class:`.SparsePauliOp` would require an exponential number of terms to represent + :math:`{\lvert0\rangle\langle0\rvert}^{\otimes n}` over :math:`n` qubits, while + :class:`.SparseObservable` needs only a single term. + + You can construct and manipulate :class:`.SparseObservable` using an interface familiar to users + of :class:`.SparsePauliOp`:: + + from qiskit.quantum_info import SparseObservable + + obs = SparseObservable.from_sparse_list([ + ("XZY", (2, 1, 0), 1.5j), + ("+-", (100, 99), 0.5j), + ("01", (50, 49), 0.5), + ]) + + :class:`.SparseObservable` is not currently supported as an input format to the primitives + (:mod:`qiskit.primitives`), but we expect to expand these interfaces to include them in the + future. diff --git a/test/python/circuit/test_calibrations.py b/test/python/circuit/test_calibrations.py index cd0d4cb0b6cb..6df7302ee50e 100644 --- a/test/python/circuit/test_calibrations.py +++ b/test/python/circuit/test_calibrations.py @@ -27,29 +27,34 @@ def test_iadd(self): """Test that __iadd__ keeps the calibrations.""" qc_cal = QuantumCircuit(2) qc_cal.rzx(0.5, 0, 1) - qc_cal.add_calibration(RZXGate, (0, 1), params=[0.5], schedule=Schedule()) + with self.assertWarns(DeprecationWarning): + qc_cal.add_calibration(RZXGate, (0, 1), params=[0.5], schedule=Schedule()) qc = QuantumCircuit(2) qc &= qc_cal - self.assertEqual(qc.calibrations[RZXGate], {((0, 1), (0.5,)): Schedule(name="test")}) - self.assertEqual(qc_cal.calibrations, qc.calibrations) + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.calibrations[RZXGate], {((0, 1), (0.5,)): Schedule(name="test")}) + self.assertEqual(qc_cal.calibrations, qc.calibrations) def test_add(self): """Test that __add__ keeps the calibrations.""" qc_cal = QuantumCircuit(2) qc_cal.rzx(0.5, 0, 1) - qc_cal.add_calibration(RZXGate, (0, 1), params=[0.5], schedule=Schedule()) + with self.assertWarns(DeprecationWarning): + qc_cal.add_calibration(RZXGate, (0, 1), params=[0.5], schedule=Schedule()) qc = QuantumCircuit(2) & qc_cal - self.assertEqual(qc.calibrations[RZXGate], {((0, 1), (0.5,)): Schedule(name="test")}) - self.assertEqual(qc_cal.calibrations, qc.calibrations) + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.calibrations[RZXGate], {((0, 1), (0.5,)): Schedule(name="test")}) + self.assertEqual(qc_cal.calibrations, qc.calibrations) qc = qc_cal & QuantumCircuit(2) - self.assertEqual(qc.calibrations[RZXGate], {((0, 1), (0.5,)): Schedule(name="test")}) - self.assertEqual(qc_cal.calibrations, qc.calibrations) + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.calibrations[RZXGate], {((0, 1), (0.5,)): Schedule(name="test")}) + self.assertEqual(qc_cal.calibrations, qc.calibrations) if __name__ == "__main__": diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index db8663ace558..5e45d0671021 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -338,18 +338,22 @@ def test_bound_calibration_parameter(self): """ amp = Parameter("amp") - with pulse.builder.build() as sched: - pulse.builder.play(pulse.Constant(100, amp), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.builder.build() as sched: + pulse.builder.play(pulse.Constant(100, amp), pulse.DriveChannel(0)) gate = Gate("custom", 1, [amp]) qc = QuantumCircuit(1) qc.append(gate, (0,)) - qc.add_calibration(gate, (0,), sched) + with self.assertWarns(DeprecationWarning): + qc.add_calibration(gate, (0,), sched) qc.assign_parameters({amp: 1 / 3}, inplace=True) qpy_file = io.BytesIO() - dump(qc, qpy_file) + with self.assertWarns(DeprecationWarning): + # qpy.dump warns for deprecations of pulse gate serialization + dump(qc, qpy_file) qpy_file.seek(0) new_circ = load(qpy_file)[0] self.assertEqual(qc, new_circ) @@ -360,7 +364,8 @@ def test_bound_calibration_parameter(self): ) # Make sure that looking for a calibration based on the instruction's # parameters succeeds - self.assertIn(cal_key, new_circ.calibrations[gate.name]) + with self.assertWarns(DeprecationWarning): + self.assertIn(cal_key, new_circ.calibrations[gate.name]) def test_parameter_expression(self): """Test a circuit with a parameter expression.""" diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index f374f82504a6..2d1e136d4580 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -400,8 +400,10 @@ def test_copy_empty_like_circuit(self): qc.h(qr[0]) qc.measure(qr[0], cr[0]) qc.measure(qr[1], cr[1]) - sched = Schedule(Play(Gaussian(160, 0.1, 40), DriveChannel(0))) - qc.add_calibration("h", [0, 1], sched) + + with self.assertWarns(DeprecationWarning): + sched = Schedule(Play(Gaussian(160, 0.1, 40), DriveChannel(0))) + qc.add_calibration("h", [0, 1], sched) copied = qc.copy_empty_like() qc.clear() @@ -409,7 +411,8 @@ def test_copy_empty_like_circuit(self): self.assertEqual(qc.global_phase, copied.global_phase) self.assertEqual(qc.name, copied.name) self.assertEqual(qc.metadata, copied.metadata) - self.assertEqual(qc.calibrations, copied.calibrations) + with self.assertWarns(DeprecationWarning): + self.assertEqual(qc.calibrations, copied.calibrations) copied = qc.copy_empty_like("copy") self.assertEqual(copied.name, "copy") diff --git a/test/python/circuit/test_circuit_properties.py b/test/python/circuit/test_circuit_properties.py index d51dd0c75616..d87487639672 100644 --- a/test/python/circuit/test_circuit_properties.py +++ b/test/python/circuit/test_circuit_properties.py @@ -1179,88 +1179,98 @@ def test_calibrations_basis_gates(self): """Check if the calibrations for basis gates provided are added correctly.""" circ = QuantumCircuit(2) - with pulse.build() as q0_x180: - pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) - with pulse.build() as q1_y90: - pulse.play(pulse.library.Gaussian(20, -1.0, 3.0), pulse.DriveChannel(1)) - - # Add calibration - circ.add_calibration(RXGate(3.14), [0], q0_x180) - circ.add_calibration(RYGate(1.57), [1], q1_y90) - - self.assertEqual(set(circ.calibrations.keys()), {"rx", "ry"}) - self.assertEqual(set(circ.calibrations["rx"].keys()), {((0,), (3.14,))}) - self.assertEqual(set(circ.calibrations["ry"].keys()), {((1,), (1.57,))}) - self.assertEqual( - circ.calibrations["rx"][((0,), (3.14,))].instructions, q0_x180.instructions - ) - self.assertEqual(circ.calibrations["ry"][((1,), (1.57,))].instructions, q1_y90.instructions) + with self.assertWarns(DeprecationWarning): + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + with pulse.build() as q1_y90: + pulse.play(pulse.library.Gaussian(20, -1.0, 3.0), pulse.DriveChannel(1)) + + # Add calibration + circ.add_calibration(RXGate(3.14), [0], q0_x180) + circ.add_calibration(RYGate(1.57), [1], q1_y90) + + self.assertEqual(set(circ.calibrations.keys()), {"rx", "ry"}) + self.assertEqual(set(circ.calibrations["rx"].keys()), {((0,), (3.14,))}) + self.assertEqual(set(circ.calibrations["ry"].keys()), {((1,), (1.57,))}) + self.assertEqual( + circ.calibrations["rx"][((0,), (3.14,))].instructions, q0_x180.instructions + ) + self.assertEqual( + circ.calibrations["ry"][((1,), (1.57,))].instructions, q1_y90.instructions + ) def test_calibrations_custom_gates(self): """Check if the calibrations for custom gates with params provided are added correctly.""" circ = QuantumCircuit(3) - with pulse.build() as q0_x180: - pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) - # Add calibrations with a custom gate 'rxt' - circ.add_calibration("rxt", [0], q0_x180, params=[1.57, 3.14, 4.71]) + # Add calibrations with a custom gate 'rxt' + circ.add_calibration("rxt", [0], q0_x180, params=[1.57, 3.14, 4.71]) - self.assertEqual(set(circ.calibrations.keys()), {"rxt"}) - self.assertEqual(set(circ.calibrations["rxt"].keys()), {((0,), (1.57, 3.14, 4.71))}) - self.assertEqual( - circ.calibrations["rxt"][((0,), (1.57, 3.14, 4.71))].instructions, q0_x180.instructions - ) + self.assertEqual(set(circ.calibrations.keys()), {"rxt"}) + self.assertEqual(set(circ.calibrations["rxt"].keys()), {((0,), (1.57, 3.14, 4.71))}) + self.assertEqual( + circ.calibrations["rxt"][((0,), (1.57, 3.14, 4.71))].instructions, + q0_x180.instructions, + ) def test_calibrations_no_params(self): """Check calibrations if the no params is provided with just gate name.""" circ = QuantumCircuit(3) - with pulse.build() as q0_x180: - pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) - circ.add_calibration("h", [0], q0_x180) + circ.add_calibration("h", [0], q0_x180) - self.assertEqual(set(circ.calibrations.keys()), {"h"}) - self.assertEqual(set(circ.calibrations["h"].keys()), {((0,), ())}) - self.assertEqual(circ.calibrations["h"][((0,), ())].instructions, q0_x180.instructions) + self.assertEqual(set(circ.calibrations.keys()), {"h"}) + self.assertEqual(set(circ.calibrations["h"].keys()), {((0,), ())}) + self.assertEqual(circ.calibrations["h"][((0,), ())].instructions, q0_x180.instructions) def test_has_calibration_for(self): """Test that `has_calibration_for` returns a correct answer.""" qc = QuantumCircuit(3) - with pulse.build() as q0_x180: - pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) - qc.add_calibration("h", [0], q0_x180) + with self.assertWarns(DeprecationWarning): + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + qc.add_calibration("h", [0], q0_x180) qc.h(0) qc.h(1) - self.assertTrue(qc.has_calibration_for(qc.data[0])) - self.assertFalse(qc.has_calibration_for(qc.data[1])) + with self.assertWarns(DeprecationWarning): + self.assertTrue(qc.has_calibration_for(qc.data[0])) + self.assertFalse(qc.has_calibration_for(qc.data[1])) def test_has_calibration_for_legacy(self): """Test that `has_calibration_for` returns a correct answer when presented with a legacy 3 tuple.""" qc = QuantumCircuit(3) - with pulse.build() as q0_x180: - pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) - qc.add_calibration("h", [0], q0_x180) + with self.assertWarns(DeprecationWarning): + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + qc.add_calibration("h", [0], q0_x180) qc.h(0) qc.h(1) - self.assertTrue( - qc.has_calibration_for( - (qc.data[0].operation, list(qc.data[0].qubits), list(qc.data[0].clbits)) + with self.assertWarns(DeprecationWarning): + self.assertTrue( + qc.has_calibration_for( + (qc.data[0].operation, list(qc.data[0].qubits), list(qc.data[0].clbits)) + ) ) - ) - self.assertFalse( - qc.has_calibration_for( - (qc.data[1].operation, list(qc.data[1].qubits), list(qc.data[1].clbits)) + self.assertFalse( + qc.has_calibration_for( + (qc.data[1].operation, list(qc.data[1].qubits), list(qc.data[1].clbits)) + ) ) - ) def test_metadata_copy_does_not_share_state(self): """Verify mutating the metadata of a circuit copy does not impact original.""" diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index 582bee082d99..0981b5f3ed79 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -576,21 +576,25 @@ def test_compose_gate(self): def test_compose_calibrations(self): """Test that composing two circuits updates calibrations.""" circ_left = QuantumCircuit(1) - circ_left.add_calibration("h", [0], None) circ_right = QuantumCircuit(1) - circ_right.add_calibration("rx", [0], None) + with self.assertWarns(DeprecationWarning): + circ_left.add_calibration("h", [0], None) + circ_right.add_calibration("rx", [0], None) circ = circ_left.compose(circ_right) - self.assertEqual(len(circ.calibrations), 2) - self.assertEqual(len(circ_left.calibrations), 1) + with self.assertWarns(DeprecationWarning): + self.assertEqual(len(circ.calibrations), 2) + self.assertEqual(len(circ_left.calibrations), 1) circ_left = QuantumCircuit(1) - circ_left.add_calibration("h", [0], None) circ_right = QuantumCircuit(1) - circ_right.add_calibration("h", [1], None) + with self.assertWarns(DeprecationWarning): + circ_left.add_calibration("h", [0], None) + circ_right.add_calibration("h", [1], None) circ = circ_left.compose(circ_right) - self.assertEqual(len(circ.calibrations), 1) - self.assertEqual(len(circ.calibrations["h"]), 2) - self.assertEqual(len(circ_left.calibrations), 1) + with self.assertWarns(DeprecationWarning): + self.assertEqual(len(circ.calibrations), 1) + self.assertEqual(len(circ.calibrations["h"]), 2) + self.assertEqual(len(circ_left.calibrations), 1) # Ensure that transpiled _calibration is defaultdict qc = QuantumCircuit(2, 2) @@ -598,7 +602,8 @@ def test_compose_calibrations(self): qc.cx(0, 1) qc.measure(0, 0) qc = transpile(qc, None, basis_gates=["h", "cx"], coupling_map=[[0, 1], [1, 0]]) - qc.add_calibration("cx", [0, 1], Schedule()) + with self.assertWarns(DeprecationWarning): + qc.add_calibration("cx", [0, 1], Schedule()) def test_compose_one_liner(self): """Test building a circuit in one line, for fun.""" diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index c10e274be791..e291eb415813 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -717,14 +717,15 @@ def test_calibration_assignment(self): circ.append(Gate("rxt", 1, [theta]), [0]) circ.measure(0, 0) - rxt_q0 = pulse.Schedule( - pulse.Play( - pulse.library.Gaussian(duration=128, sigma=16, amp=0.2 * theta / 3.14), - pulse.DriveChannel(0), + with self.assertWarns(DeprecationWarning): + rxt_q0 = pulse.Schedule( + pulse.Play( + pulse.library.Gaussian(duration=128, sigma=16, amp=0.2 * theta / 3.14), + pulse.DriveChannel(0), + ) ) - ) - circ.add_calibration("rxt", [0], rxt_q0, [theta]) + circ.add_calibration("rxt", [0], rxt_q0, [theta]) circ = circ.assign_parameters({theta: 3.14}) instruction = circ.data[0] @@ -733,9 +734,11 @@ def test_calibration_assignment(self): tuple(instruction.operation.params), ) self.assertEqual(cal_key, ((0,), (3.14,))) - # Make sure that key from instruction data matches the calibrations dictionary - self.assertIn(cal_key, circ.calibrations["rxt"]) - sched = circ.calibrations["rxt"][cal_key] + + with self.assertWarns(DeprecationWarning): + # Make sure that key from instruction data matches the calibrations dictionary + self.assertIn(cal_key, circ.calibrations["rxt"]) + sched = circ.calibrations["rxt"][cal_key] self.assertEqual(sched.instructions[0][1].pulse.amp, 0.2) def test_calibration_assignment_doesnt_mutate(self): @@ -745,19 +748,21 @@ def test_calibration_assignment_doesnt_mutate(self): circ.append(Gate("rxt", 1, [theta]), [0]) circ.measure(0, 0) - rxt_q0 = pulse.Schedule( - pulse.Play( - pulse.library.Gaussian(duration=128, sigma=16, amp=0.2 * theta / 3.14), - pulse.DriveChannel(0), + with self.assertWarns(DeprecationWarning): + rxt_q0 = pulse.Schedule( + pulse.Play( + pulse.library.Gaussian(duration=128, sigma=16, amp=0.2 * theta / 3.14), + pulse.DriveChannel(0), + ) ) - ) - circ.add_calibration("rxt", [0], rxt_q0, [theta]) + circ.add_calibration("rxt", [0], rxt_q0, [theta]) circ_copy = copy.deepcopy(circ) assigned_circ = circ.assign_parameters({theta: 3.14}) - self.assertEqual(circ.calibrations, circ_copy.calibrations) - self.assertNotEqual(assigned_circ.calibrations, circ.calibrations) + with self.assertWarns(DeprecationWarning): + self.assertEqual(circ.calibrations, circ_copy.calibrations) + self.assertNotEqual(assigned_circ.calibrations, circ.calibrations) def test_calibration_assignment_w_expressions(self): """That calibrations with multiple parameters are assigned correctly""" @@ -767,14 +772,15 @@ def test_calibration_assignment_w_expressions(self): circ.append(Gate("rxt", 1, [theta / 2, sigma]), [0]) circ.measure(0, 0) - rxt_q0 = pulse.Schedule( - pulse.Play( - pulse.library.Gaussian(duration=128, sigma=4 * sigma, amp=0.2 * theta / 3.14), - pulse.DriveChannel(0), + with self.assertWarns(DeprecationWarning): + rxt_q0 = pulse.Schedule( + pulse.Play( + pulse.library.Gaussian(duration=128, sigma=4 * sigma, amp=0.2 * theta / 3.14), + pulse.DriveChannel(0), + ) ) - ) - circ.add_calibration("rxt", [0], rxt_q0, [theta / 2, sigma]) + circ.add_calibration("rxt", [0], rxt_q0, [theta / 2, sigma]) circ = circ.assign_parameters({theta: 3.14, sigma: 4}) instruction = circ.data[0] @@ -783,9 +789,10 @@ def test_calibration_assignment_w_expressions(self): tuple(instruction.operation.params), ) self.assertEqual(cal_key, ((0,), (3.14 / 2, 4))) - # Make sure that key from instruction data matches the calibrations dictionary - self.assertIn(cal_key, circ.calibrations["rxt"]) - sched = circ.calibrations["rxt"][cal_key] + with self.assertWarns(DeprecationWarning): + # Make sure that key from instruction data matches the calibrations dictionary + self.assertIn(cal_key, circ.calibrations["rxt"]) + sched = circ.calibrations["rxt"][cal_key] self.assertEqual(sched.instructions[0][1].pulse.amp, 0.2) self.assertEqual(sched.instructions[0][1].pulse.sigma, 16) @@ -793,16 +800,19 @@ def test_substitution(self): """Test Parameter substitution (vs bind).""" alpha = Parameter("⍺") beta = Parameter("beta") - schedule = pulse.Schedule(pulse.ShiftPhase(alpha, pulse.DriveChannel(0))) + with self.assertWarns(DeprecationWarning): + schedule = pulse.Schedule(pulse.ShiftPhase(alpha, pulse.DriveChannel(0))) circ = QuantumCircuit(3, 3) circ.append(Gate("my_rz", 1, [alpha]), [0]) - circ.add_calibration("my_rz", [0], schedule, [alpha]) + with self.assertWarns(DeprecationWarning): + circ.add_calibration("my_rz", [0], schedule, [alpha]) circ = circ.assign_parameters({alpha: 2 * beta}) circ = circ.assign_parameters({beta: 1.57}) - cal_sched = circ.calibrations["my_rz"][((0,), (3.14,))] + with self.assertWarns(DeprecationWarning): + cal_sched = circ.calibrations["my_rz"][((0,), (3.14,))] self.assertEqual(float(cal_sched.instructions[0][1].phase), 3.14) def test_partial_assignment(self): @@ -812,14 +822,16 @@ def test_partial_assignment(self): gamma = Parameter("γ") phi = Parameter("ϕ") - with pulse.build() as my_cal: - pulse.set_frequency(alpha + beta, pulse.DriveChannel(0)) - pulse.shift_frequency(gamma + beta, pulse.DriveChannel(0)) - pulse.set_phase(phi, pulse.DriveChannel(1)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as my_cal: + pulse.set_frequency(alpha + beta, pulse.DriveChannel(0)) + pulse.shift_frequency(gamma + beta, pulse.DriveChannel(0)) + pulse.set_phase(phi, pulse.DriveChannel(1)) circ = QuantumCircuit(2, 2) circ.append(Gate("custom", 2, [alpha, beta, gamma, phi]), [0, 1]) - circ.add_calibration("custom", [0, 1], my_cal, [alpha, beta, gamma, phi]) + with self.assertWarns(DeprecationWarning): + circ.add_calibration("custom", [0, 1], my_cal, [alpha, beta, gamma, phi]) # Partial bind delta = 1e9 @@ -828,22 +840,34 @@ def test_partial_assignment(self): phase = 3.14 / 4 circ = circ.assign_parameters({alpha: freq - delta}) - cal_sched = list(circ.calibrations["custom"].values())[0] - self.assertEqual(cal_sched.instructions[0][1].frequency, freq - delta + beta) + with self.assertWarns(DeprecationWarning): + cal_sched = list(circ.calibrations["custom"].values())[0] + with self.assertWarns(DeprecationWarning): + # instructions triggers conversion to Schedule + self.assertEqual(cal_sched.instructions[0][1].frequency, freq - delta + beta) circ = circ.assign_parameters({beta: delta}) - cal_sched = list(circ.calibrations["custom"].values())[0] - self.assertEqual(float(cal_sched.instructions[0][1].frequency), freq) - self.assertEqual(cal_sched.instructions[1][1].frequency, gamma + delta) + with self.assertWarns(DeprecationWarning): + cal_sched = list(circ.calibrations["custom"].values())[0] + with self.assertWarns(DeprecationWarning): + # instructions triggers conversion to Schedule + self.assertEqual(float(cal_sched.instructions[0][1].frequency), freq) + self.assertEqual(cal_sched.instructions[1][1].frequency, gamma + delta) circ = circ.assign_parameters({gamma: shift - delta}) - cal_sched = list(circ.calibrations["custom"].values())[0] - self.assertEqual(float(cal_sched.instructions[1][1].frequency), shift) + with self.assertWarns(DeprecationWarning): + cal_sched = list(circ.calibrations["custom"].values())[0] + with self.assertWarns(DeprecationWarning): + # instructions triggers conversion to Schedule + self.assertEqual(float(cal_sched.instructions[1][1].frequency), shift) + self.assertEqual(cal_sched.instructions[2][1].phase, phi) - self.assertEqual(cal_sched.instructions[2][1].phase, phi) circ = circ.assign_parameters({phi: phase}) - cal_sched = list(circ.calibrations["custom"].values())[0] - self.assertEqual(float(cal_sched.instructions[2][1].phase), phase) + with self.assertWarns(DeprecationWarning): + cal_sched = list(circ.calibrations["custom"].values())[0] + with self.assertWarns(DeprecationWarning): + # instructions triggers conversion to Schedule + self.assertEqual(float(cal_sched.instructions[2][1].phase), phase) def test_circuit_generation(self): """Test creating a series of circuits parametrically""" diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py index ce5cd8213105..6348d31487bd 100644 --- a/test/python/circuit/test_scheduled_circuit.py +++ b/test/python/circuit/test_scheduled_circuit.py @@ -56,7 +56,8 @@ def test_schedule_circuit_when_backend_tells_dt(self): qc.h(0) # 195[dt] qc.h(1) # 210[dt] - backend = GenericBackendV2(2, calibrate_instructions=True, seed=42) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2(2, calibrate_instructions=True, seed=42) sc = transpile(qc, backend, scheduling_method="alap", layout_method="trivial") self.assertEqual(sc.duration, 451095) @@ -355,12 +356,13 @@ def test_convert_duration_to_dt(self): """Test that circuit duration unit conversion is applied only when necessary. Tests fix for bug reported in PR #11782.""" - backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True, seed=42) - schedule_config = ScheduleConfig( - inst_map=backend.target.instruction_schedule_map(), - meas_map=backend.meas_map, - dt=backend.dt, - ) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True, seed=42) + schedule_config = ScheduleConfig( + inst_map=backend.target.instruction_schedule_map(), + meas_map=backend.meas_map, + dt=backend.dt, + ) circ = QuantumCircuit(2) circ.cx(0, 1) diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py index c333ce9ace22..b384d8b9267f 100644 --- a/test/python/compiler/test_assembler.py +++ b/test/python/compiler/test_assembler.py @@ -71,12 +71,13 @@ def setUp(self): # lo test values self.default_qubit_lo_freq = [5e9 for _ in range(self.num_qubits)] self.default_meas_lo_freq = [6.7e9 for _ in range(self.num_qubits)] - self.user_lo_config_dict = { - pulse.DriveChannel(0): 5.55e9, - pulse.MeasureChannel(0): 6.64e9, - pulse.DriveChannel(3): 4.91e9, - pulse.MeasureChannel(4): 6.1e9, - } + with self.assertWarns(DeprecationWarning): + self.user_lo_config_dict = { + pulse.DriveChannel(0): 5.55e9, + pulse.MeasureChannel(0): 6.64e9, + pulse.DriveChannel(3): 4.91e9, + pulse.MeasureChannel(4): 6.1e9, + } self.user_lo_config = pulse.LoConfig(self.user_lo_config_dict) def test_assemble_single_circuit(self): @@ -541,17 +542,17 @@ def test_pulse_gates_single_circ(self): circ.append(RxGate(theta), [1]) circ = circ.assign_parameters({theta: 3.14}) - with pulse.build() as custom_h_schedule: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as custom_h_schedule: + pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) - with pulse.build() as x180: - pulse.play(pulse.library.Gaussian(50, 0.2, 5), pulse.DriveChannel(1)) + with pulse.build() as x180: + pulse.play(pulse.library.Gaussian(50, 0.2, 5), pulse.DriveChannel(1)) - circ.add_calibration("h", [0], custom_h_schedule) - circ.add_calibration(RxGate(3.14), [0], x180) - circ.add_calibration(RxGate(3.14), [1], x180) + circ.add_calibration("h", [0], custom_h_schedule) + circ.add_calibration(RxGate(3.14), [0], x180) + circ.add_calibration(RxGate(3.14), [1], x180) - with self.assertWarns(DeprecationWarning): qobj = assemble(circ, FakeOpenPulse2Q()) # Only one circuit, so everything is stored at the job level cals = qobj.config.calibrations @@ -568,31 +569,33 @@ def test_custom_pulse_gates_single_circ(self): circ = QuantumCircuit(2) circ.h(0) - with pulse.build() as custom_h_schedule: - pulse.play(pulse.library.Triangle(50, 0.1, 0.2), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as custom_h_schedule: + pulse.play(pulse.library.Triangle(50, 0.1, 0.2), pulse.DriveChannel(0)) - circ.add_calibration("h", [0], custom_h_schedule) + circ.add_calibration("h", [0], custom_h_schedule) - with self.assertWarns(DeprecationWarning): qobj = assemble(circ, FakeOpenPulse2Q()) lib = qobj.config.pulse_library self.assertEqual(len(lib), 1) - np.testing.assert_almost_equal( - lib[0].samples, pulse.library.Triangle(50, 0.1, 0.2).get_waveform().samples - ) + with self.assertWarns(DeprecationWarning): + np.testing.assert_almost_equal( + lib[0].samples, pulse.library.Triangle(50, 0.1, 0.2).get_waveform().samples + ) def test_pulse_gates_with_parameteric_pulses(self): """Test that pulse gates are assembled efficiently for backends that enable parametric pulses. """ - with pulse.build() as custom_h_schedule: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as custom_h_schedule: + pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) circ = QuantumCircuit(2) circ.h(0) - circ.add_calibration("h", [0], custom_h_schedule) with self.assertWarns(DeprecationWarning): + circ.add_calibration("h", [0], custom_h_schedule) backend = FakeOpenPulse2Q() backend.configuration().parametric_pulses = ["drag"] with self.assertWarns(DeprecationWarning): @@ -602,14 +605,16 @@ def test_pulse_gates_with_parameteric_pulses(self): def test_pulse_gates_multiple_circuits(self): """Test one circuit with cals and another without.""" - with pulse.build() as dummy_sched: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as dummy_sched: + pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) circ = QuantumCircuit(2) circ.h(0) circ.append(RxGate(3.14), [1]) - circ.add_calibration("h", [0], dummy_sched) - circ.add_calibration(RxGate(3.14), [1], dummy_sched) + with self.assertWarns(DeprecationWarning): + circ.add_calibration("h", [0], dummy_sched) + circ.add_calibration(RxGate(3.14), [1], dummy_sched) circ2 = QuantumCircuit(2) circ2.h(0) @@ -623,18 +628,21 @@ def test_pulse_gates_multiple_circuits(self): def test_pulse_gates_common_cals(self): """Test that common calibrations are added at the top level.""" - with pulse.build() as dummy_sched: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as dummy_sched: + pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) circ = QuantumCircuit(2) circ.h(0) circ.append(RxGate(3.14), [1]) - circ.add_calibration("h", [0], dummy_sched) - circ.add_calibration(RxGate(3.14), [1], dummy_sched) + with self.assertWarns(DeprecationWarning): + circ.add_calibration("h", [0], dummy_sched) + circ.add_calibration(RxGate(3.14), [1], dummy_sched) circ2 = QuantumCircuit(2) circ2.h(0) - circ2.add_calibration(RxGate(3.14), [1], dummy_sched) + with self.assertWarns(DeprecationWarning): + circ2.add_calibration(RxGate(3.14), [1], dummy_sched) with self.assertWarns(DeprecationWarning): qobj = assemble([circ, circ2], FakeOpenPulse2Q()) @@ -661,9 +669,9 @@ def test_pulse_gates_delay_only(self): """Test that a single delay gate is translated to an instruction.""" circ = QuantumCircuit(2) circ.append(Gate("test", 1, []), [0]) - test_sched = pulse.Delay(64, DriveChannel(0)) + pulse.Delay(160, DriveChannel(0)) - circ.add_calibration("test", [0], test_sched) with self.assertWarns(DeprecationWarning): + test_sched = pulse.Delay(64, DriveChannel(0)) + pulse.Delay(160, DriveChannel(0)) + circ.add_calibration("test", [0], test_sched) qobj = assemble(circ, FakeOpenPulse2Q()) self.assertEqual(len(qobj.config.calibrations.gates[0].instructions), 2) self.assertEqual( @@ -812,12 +820,13 @@ def test_assemble_single_circ_multi_lo_config(self): """Test assembling a single circuit, with multiple experiment level lo configs (frequency sweep). """ - user_lo_config_dict2 = { - pulse.DriveChannel(1): 5.55e9, - pulse.MeasureChannel(1): 6.64e9, - pulse.DriveChannel(4): 4.91e9, - pulse.MeasureChannel(3): 6.1e9, - } + with self.assertWarns(DeprecationWarning): + user_lo_config_dict2 = { + pulse.DriveChannel(1): 5.55e9, + pulse.MeasureChannel(1): 6.64e9, + pulse.DriveChannel(4): 4.91e9, + pulse.MeasureChannel(3): 6.1e9, + } user_lo_config2 = pulse.LoConfig(user_lo_config_dict2) with self.assertWarns(DeprecationWarning): qobj = assemble( @@ -844,12 +853,13 @@ def test_assemble_single_circ_multi_lo_config(self): def test_assemble_multi_circ_multi_lo_config(self): """Test assembling circuits, with the same number of experiment level lo configs (n:n setup).""" - user_lo_config_dict2 = { - pulse.DriveChannel(1): 5.55e9, - pulse.MeasureChannel(1): 6.64e9, - pulse.DriveChannel(4): 4.91e9, - pulse.MeasureChannel(3): 6.1e9, - } + with self.assertWarns(DeprecationWarning): + user_lo_config_dict2 = { + pulse.DriveChannel(1): 5.55e9, + pulse.MeasureChannel(1): 6.64e9, + pulse.DriveChannel(4): 4.91e9, + pulse.MeasureChannel(3): 6.1e9, + } user_lo_config2 = pulse.LoConfig(user_lo_config_dict2) with self.assertWarns(DeprecationWarning): qobj = assemble( @@ -908,18 +918,19 @@ def test_assemble_circ_lo_config_errors(self): some are missing or if default values are not provided. Also check that experiment level lo range is validated.""" # no defaults, but have drive/meas experiment level los for each qubit (no error) - full_lo_config_dict = { - pulse.DriveChannel(0): 4.85e9, - pulse.DriveChannel(1): 4.9e9, - pulse.DriveChannel(2): 4.95e9, - pulse.DriveChannel(3): 5e9, - pulse.DriveChannel(4): 5.05e9, - pulse.MeasureChannel(0): 6.8e9, - pulse.MeasureChannel(1): 6.85e9, - pulse.MeasureChannel(2): 6.9e9, - pulse.MeasureChannel(3): 6.95e9, - pulse.MeasureChannel(4): 7e9, - } + with self.assertWarns(DeprecationWarning): + full_lo_config_dict = { + pulse.DriveChannel(0): 4.85e9, + pulse.DriveChannel(1): 4.9e9, + pulse.DriveChannel(2): 4.95e9, + pulse.DriveChannel(3): 5e9, + pulse.DriveChannel(4): 5.05e9, + pulse.MeasureChannel(0): 6.8e9, + pulse.MeasureChannel(1): 6.85e9, + pulse.MeasureChannel(2): 6.9e9, + pulse.MeasureChannel(3): 6.95e9, + pulse.MeasureChannel(4): 7e9, + } with self.assertWarns(DeprecationWarning): qobj = assemble(self.circ, self.backend, schedule_los=full_lo_config_dict) @@ -930,15 +941,15 @@ def test_assemble_circ_lo_config_errors(self): # no defaults and missing experiment level drive lo raises missing_drive_lo_config_dict = copy.deepcopy(full_lo_config_dict) - missing_drive_lo_config_dict.pop(pulse.DriveChannel(0)) with self.assertWarns(DeprecationWarning): + missing_drive_lo_config_dict.pop(pulse.DriveChannel(0)) with self.assertRaises(QiskitError): qobj = assemble(self.circ, self.backend, schedule_los=missing_drive_lo_config_dict) # no defaults and missing experiment level meas lo raises missing_meas_lo_config_dict = copy.deepcopy(full_lo_config_dict) - missing_meas_lo_config_dict.pop(pulse.MeasureChannel(0)) with self.assertWarns(DeprecationWarning): + missing_meas_lo_config_dict.pop(pulse.MeasureChannel(0)) with self.assertRaises(QiskitError): qobj = assemble(self.circ, self.backend, schedule_los=missing_meas_lo_config_dict) @@ -948,8 +959,8 @@ def test_assemble_circ_lo_config_errors(self): meas_lo_range = [[freq - 5e6, freq + 5e6] for freq in lo_values[5:]] # out of range drive lo - full_lo_config_dict[pulse.DriveChannel(0)] -= 5.5e6 with self.assertWarns(DeprecationWarning): + full_lo_config_dict[pulse.DriveChannel(0)] -= 5.5e6 with self.assertRaises(QiskitError): qobj = assemble( self.circ, @@ -957,11 +968,11 @@ def test_assemble_circ_lo_config_errors(self): qubit_lo_range=qubit_lo_range, schedule_los=full_lo_config_dict, ) - full_lo_config_dict[pulse.DriveChannel(0)] += 5.5e6 # reset drive value + full_lo_config_dict[pulse.DriveChannel(0)] += 5.5e6 # reset drive value - # out of range meas lo - full_lo_config_dict[pulse.MeasureChannel(0)] += 5.5e6 with self.assertWarns(DeprecationWarning): + # out of range meas lo + full_lo_config_dict[pulse.MeasureChannel(0)] += 5.5e6 with self.assertRaises(QiskitError): qobj = assemble( self.circ, @@ -980,19 +991,20 @@ def setUp(self): self.backend = FakeOpenPulse2Q() self.backend_config = self.backend.configuration() - test_pulse = pulse.Waveform( - samples=np.array([0.02739068, 0.05, 0.05, 0.05, 0.02739068], dtype=np.complex128), - name="pulse0", - ) - - self.schedule = pulse.Schedule(name="fake_experiment") - self.schedule = self.schedule.insert(0, Play(test_pulse, self.backend_config.drive(0))) - for i in range(self.backend_config.n_qubits): - self.schedule = self.schedule.insert( - 5, Acquire(5, self.backend_config.acquire(i), MemorySlot(i)) + with self.assertWarns(DeprecationWarning): + test_pulse = pulse.Waveform( + samples=np.array([0.02739068, 0.05, 0.05, 0.05, 0.02739068], dtype=np.complex128), + name="pulse0", ) - self.user_lo_config_dict = {self.backend_config.drive(0): 4.91e9} + self.schedule = pulse.Schedule(name="fake_experiment") + self.schedule = self.schedule.insert(0, Play(test_pulse, self.backend_config.drive(0))) + for i in range(self.backend_config.n_qubits): + self.schedule = self.schedule.insert( + 5, Acquire(5, self.backend_config.acquire(i), MemorySlot(i)) + ) + + self.user_lo_config_dict = {self.backend_config.drive(0): 4.91e9} self.user_lo_config = pulse.LoConfig(self.user_lo_config_dict) self.default_qubit_lo_freq = [4.9e9, 5.0e9] @@ -1020,16 +1032,18 @@ def test_assemble_adds_schedule_metadata_to_experiment_header(self): def test_assemble_sample_pulse(self): """Test that the pulse lib and qobj instruction can be paired up.""" - schedule = pulse.Schedule() - schedule += pulse.Play( - pulse.Waveform([0.1] * 16, name="test0"), pulse.DriveChannel(0), name="test1" - ) - schedule += pulse.Play( - pulse.Waveform([0.1] * 16, name="test1"), pulse.DriveChannel(0), name="test2" - ) - schedule += pulse.Play( - pulse.Waveform([0.5] * 16, name="test0"), pulse.DriveChannel(0), name="test1" - ) + with self.assertWarns(DeprecationWarning): + schedule = pulse.Schedule() + schedule += pulse.Play( + pulse.Waveform([0.1] * 16, name="test0"), pulse.DriveChannel(0), name="test1" + ) + schedule += pulse.Play( + pulse.Waveform([0.1] * 16, name="test1"), pulse.DriveChannel(0), name="test2" + ) + schedule += pulse.Play( + pulse.Waveform([0.5] * 16, name="test0"), pulse.DriveChannel(0), name="test1" + ) + with self.assertWarns(DeprecationWarning): qobj = assemble( schedule, @@ -1172,9 +1186,10 @@ def test_assemble_multi_schedules_with_wrong_number_of_multi_lo_configs(self): def test_assemble_meas_map(self): """Test assembling a single schedule, no lo config.""" - schedule = Schedule(name="fake_experiment") - schedule = schedule.insert(5, Acquire(5, AcquireChannel(0), MemorySlot(0))) - schedule = schedule.insert(5, Acquire(5, AcquireChannel(1), MemorySlot(1))) + with self.assertWarns(DeprecationWarning): + schedule = Schedule(name="fake_experiment") + schedule = schedule.insert(5, Acquire(5, AcquireChannel(0), MemorySlot(0))) + schedule = schedule.insert(5, Acquire(5, AcquireChannel(1), MemorySlot(1))) with self.assertWarns(DeprecationWarning): qobj = assemble( @@ -1199,9 +1214,10 @@ def test_assemble_memory_slots(self): n_memoryslots = 10 # single acquisition - schedule = Acquire( - 5, self.backend_config.acquire(0), mem_slot=pulse.MemorySlot(n_memoryslots - 1) - ) + with self.assertWarns(DeprecationWarning): + schedule = Acquire( + 5, self.backend_config.acquire(0), mem_slot=pulse.MemorySlot(n_memoryslots - 1) + ) with self.assertWarns(DeprecationWarning): qobj = assemble( @@ -1216,15 +1232,17 @@ def test_assemble_memory_slots(self): self.assertEqual(qobj.experiments[0].header.memory_slots, n_memoryslots) # multiple acquisition - schedule = Acquire( - 5, self.backend_config.acquire(0), mem_slot=pulse.MemorySlot(n_memoryslots - 1) - ) - schedule = schedule.insert( - 10, - Acquire( + with self.assertWarns(DeprecationWarning): + schedule = Acquire( 5, self.backend_config.acquire(0), mem_slot=pulse.MemorySlot(n_memoryslots - 1) - ), - ) + ) + schedule = schedule.insert( + 10, + Acquire( + 5, self.backend_config.acquire(0), mem_slot=pulse.MemorySlot(n_memoryslots - 1) + ), + ) + with self.assertWarns(DeprecationWarning): qobj = assemble( schedule, @@ -1242,11 +1260,12 @@ def test_assemble_memory_slots_for_schedules(self): n_memoryslots = [10, 5, 7] schedules = [] - for n_memoryslot in n_memoryslots: - schedule = Acquire( - 5, self.backend_config.acquire(0), mem_slot=pulse.MemorySlot(n_memoryslot - 1) - ) - schedules.append(schedule) + with self.assertWarns(DeprecationWarning): + for n_memoryslot in n_memoryslots: + schedule = Acquire( + 5, self.backend_config.acquire(0), mem_slot=pulse.MemorySlot(n_memoryslot - 1) + ) + schedules.append(schedule) with self.assertWarns(DeprecationWarning): qobj = assemble( @@ -1263,13 +1282,14 @@ def test_assemble_memory_slots_for_schedules(self): def test_pulse_name_conflicts(self): """Test that pulse name conflicts can be resolved.""" - name_conflict_pulse = pulse.Waveform( - samples=np.array([0.02, 0.05, 0.05, 0.05, 0.02], dtype=np.complex128), name="pulse0" - ) + with self.assertWarns(DeprecationWarning): + name_conflict_pulse = pulse.Waveform( + samples=np.array([0.02, 0.05, 0.05, 0.05, 0.02], dtype=np.complex128), name="pulse0" + ) - self.schedule = self.schedule.insert( - 1, Play(name_conflict_pulse, self.backend_config.drive(1)) - ) + self.schedule = self.schedule.insert( + 1, Play(name_conflict_pulse, self.backend_config.drive(1)) + ) with self.assertWarns(DeprecationWarning): qobj = assemble( @@ -1290,12 +1310,15 @@ def test_pulse_name_conflicts_in_other_schedule(self): defaults = backend.defaults() schedules = [] - ch_d0 = pulse.DriveChannel(0) - for amp in (0.1, 0.2): - sched = Schedule() - sched += Play(pulse.Gaussian(duration=100, amp=amp, sigma=30, name="my_pulse"), ch_d0) - sched += measure(qubits=[0], backend=backend) << 100 - schedules.append(sched) + with self.assertWarns(DeprecationWarning): + ch_d0 = pulse.DriveChannel(0) + for amp in (0.1, 0.2): + sched = Schedule() + sched += Play( + pulse.Gaussian(duration=100, amp=amp, sigma=30, name="my_pulse"), ch_d0 + ) + sched += measure(qubits=[0], backend=backend) << 100 + schedules.append(sched) with self.assertWarns(DeprecationWarning): qobj = assemble( @@ -1309,8 +1332,9 @@ def test_pulse_name_conflicts_in_other_schedule(self): def test_assemble_with_delay(self): """Test that delay instruction is not ignored in assembly.""" - delay_schedule = pulse.Delay(10, self.backend_config.drive(0)) - delay_schedule += self.schedule + with self.assertWarns(DeprecationWarning): + delay_schedule = pulse.Delay(10, self.backend_config.drive(0)) + delay_schedule += self.schedule with self.assertWarns(DeprecationWarning): delay_qobj = assemble(delay_schedule, self.backend) @@ -1323,20 +1347,23 @@ def test_delay_removed_on_acq_ch(self): """Test that delay instructions on acquire channels are skipped on assembly with times shifted properly. """ - delay0 = pulse.Delay(5, self.backend_config.acquire(0)) - delay1 = pulse.Delay(7, self.backend_config.acquire(1)) + with self.assertWarns(DeprecationWarning): + delay0 = pulse.Delay(5, self.backend_config.acquire(0)) + delay1 = pulse.Delay(7, self.backend_config.acquire(1)) sched0 = delay0 - sched0 += self.schedule # includes ``Acquire`` instr - sched0 += delay1 + with self.assertWarns(DeprecationWarning): + sched0 += self.schedule # includes ``Acquire`` instr + sched0 += delay1 - sched1 = self.schedule # includes ``Acquire`` instr - sched1 += delay0 - sched1 += delay1 + with self.assertWarns(DeprecationWarning): + sched1 = self.schedule # includes ``Acquire`` instr + sched1 += delay0 + sched1 += delay1 - sched2 = delay0 - sched2 += delay1 - sched2 += self.schedule # includes ``Acquire`` instr + sched2 = delay0 + sched2 += delay1 + sched2 += self.schedule # includes ``Acquire`` instr with self.assertWarns(DeprecationWarning): delay_qobj = assemble([sched0, sched1, sched2], self.backend) @@ -1379,21 +1406,26 @@ def test_assemble_parametric(self): """Test that parametric pulses can be assembled properly into a PulseQobj.""" amp = [0.5, 0.6, 1, 0.2] angle = [np.pi / 2, 0.6, 0, 0] - sched = pulse.Schedule(name="test_parametric") - sched += Play( - pulse.Gaussian(duration=25, sigma=4, amp=amp[0], angle=angle[0]), DriveChannel(0) - ) - sched += Play( - pulse.Drag(duration=25, amp=amp[1], angle=angle[1], sigma=7.8, beta=4), DriveChannel(1) - ) - sched += Play(pulse.Constant(duration=25, amp=amp[2], angle=angle[2]), DriveChannel(2)) - sched += ( - Play( - pulse.GaussianSquare(duration=150, amp=amp[3], angle=angle[3], sigma=8, width=140), - MeasureChannel(0), + with self.assertWarns(DeprecationWarning): + sched = pulse.Schedule(name="test_parametric") + sched += Play( + pulse.Gaussian(duration=25, sigma=4, amp=amp[0], angle=angle[0]), DriveChannel(0) ) - << sched.duration - ) + sched += Play( + pulse.Drag(duration=25, amp=amp[1], angle=angle[1], sigma=7.8, beta=4), + DriveChannel(1), + ) + sched += Play(pulse.Constant(duration=25, amp=amp[2], angle=angle[2]), DriveChannel(2)) + sched += ( + Play( + pulse.GaussianSquare( + duration=150, amp=amp[3], angle=angle[3], sigma=8, width=140 + ), + MeasureChannel(0), + ) + << sched.duration + ) + with self.assertWarns(DeprecationWarning): backend = FakeOpenPulse3Q() backend.configuration().parametric_pulses = [ @@ -1436,11 +1468,12 @@ def test_assemble_parametric_unsupported(self): """Test that parametric pulses are translated to Waveform if they're not supported by the backend during assemble time. """ - sched = pulse.Schedule(name="test_parametric_to_sample_pulse") - sched += Play( - pulse.Drag(duration=25, amp=0.5, angle=-0.3, sigma=7.8, beta=4), DriveChannel(1) - ) - sched += Play(pulse.Constant(duration=25, amp=1), DriveChannel(2)) + with self.assertWarns(DeprecationWarning): + sched = pulse.Schedule(name="test_parametric_to_sample_pulse") + sched += Play( + pulse.Drag(duration=25, amp=0.5, angle=-0.3, sigma=7.8, beta=4), DriveChannel(1) + ) + sched += Play(pulse.Constant(duration=25, amp=1), DriveChannel(2)) with self.assertWarns(DeprecationWarning): backend = FakeOpenPulse3Q() @@ -1461,12 +1494,12 @@ def test_assemble_parametric_pulse_kwarg_with_backend_setting(self): qc = QuantumCircuit(1, 1) qc.x(0) qc.measure(0, 0) - with pulse.build(backend, name="x") as x_q0: - pulse.play(pulse.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build(backend, name="x") as x_q0: + pulse.play(pulse.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(0)) - qc.add_calibration("x", (0,), x_q0) + qc.add_calibration("x", (0,), x_q0) - with self.assertWarns(DeprecationWarning): qobj = assemble(qc, backend, parametric_pulses=["gaussian"]) self.assertEqual(qobj.config.parametric_pulses, ["gaussian"]) @@ -1478,12 +1511,12 @@ def test_assemble_parametric_pulse_kwarg_empty_list_with_backend_setting(self): qc = QuantumCircuit(1, 1) qc.x(0) qc.measure(0, 0) - with pulse.build(backend, name="x") as x_q0: - pulse.play(pulse.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(0)) - - qc.add_calibration("x", (0,), x_q0) + with self.assertWarns(DeprecationWarning): + with pulse.build(backend, name="x") as x_q0: + pulse.play(pulse.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(0)) with self.assertWarns(DeprecationWarning): + qc.add_calibration("x", (0,), x_q0) qobj = assemble(qc, backend, parametric_pulses=[]) self.assertEqual(qobj.config.parametric_pulses, []) @@ -1582,13 +1615,14 @@ def test_assemble_with_individual_discriminators(self): disc_one = Discriminator("disc_one", test_params=True) disc_two = Discriminator("disc_two", test_params=False) - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0), discriminator=disc_one), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1), discriminator=disc_two), - ) + with self.assertWarns(DeprecationWarning): + schedule = Schedule() + schedule = schedule.append( + Acquire(5, AcquireChannel(0), MemorySlot(0), discriminator=disc_one), + ) + schedule = schedule.append( + Acquire(5, AcquireChannel(1), MemorySlot(1), discriminator=disc_two), + ) with self.assertWarns(DeprecationWarning): qobj = assemble( @@ -1609,13 +1643,14 @@ def test_assemble_with_single_discriminators(self): """Test that assembly works with both a single discriminator.""" disc_one = Discriminator("disc_one", test_params=True) - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0), discriminator=disc_one), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)), - ) + with self.assertWarns(DeprecationWarning): + schedule = Schedule() + schedule = schedule.append( + Acquire(5, AcquireChannel(0), MemorySlot(0), discriminator=disc_one), + ) + schedule = schedule.append( + Acquire(5, AcquireChannel(1), MemorySlot(1)), + ) with self.assertWarns(DeprecationWarning): qobj = assemble( @@ -1636,10 +1671,11 @@ def test_assemble_with_unequal_discriminators(self): disc_one = Discriminator("disc_one", test_params=True) disc_two = Discriminator("disc_two", test_params=False) - schedule = Schedule() - schedule += Acquire(5, AcquireChannel(0), MemorySlot(0), discriminator=disc_one) - schedule += Acquire(5, AcquireChannel(1), MemorySlot(1), discriminator=disc_two) - schedule += Acquire(5, AcquireChannel(2), MemorySlot(2)) + with self.assertWarns(DeprecationWarning): + schedule = Schedule() + schedule += Acquire(5, AcquireChannel(0), MemorySlot(0), discriminator=disc_one) + schedule += Acquire(5, AcquireChannel(1), MemorySlot(1), discriminator=disc_two) + schedule += Acquire(5, AcquireChannel(2), MemorySlot(2)) with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): assemble( @@ -1654,13 +1690,14 @@ def test_assemble_with_individual_kernels(self): disc_one = Kernel("disc_one", test_params=True) disc_two = Kernel("disc_two", test_params=False) - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0), kernel=disc_one), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1), kernel=disc_two), - ) + with self.assertWarns(DeprecationWarning): + schedule = Schedule() + schedule = schedule.append( + Acquire(5, AcquireChannel(0), MemorySlot(0), kernel=disc_one), + ) + schedule = schedule.append( + Acquire(5, AcquireChannel(1), MemorySlot(1), kernel=disc_two), + ) with self.assertWarns(DeprecationWarning): qobj = assemble( @@ -1681,13 +1718,14 @@ def test_assemble_with_single_kernels(self): """Test that assembly works with both a single kernel.""" disc_one = Kernel("disc_one", test_params=True) - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0), kernel=disc_one), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)), - ) + with self.assertWarns(DeprecationWarning): + schedule = Schedule() + schedule = schedule.append( + Acquire(5, AcquireChannel(0), MemorySlot(0), kernel=disc_one), + ) + schedule = schedule.append( + Acquire(5, AcquireChannel(1), MemorySlot(1)), + ) with self.assertWarns(DeprecationWarning): qobj = assemble( @@ -1708,10 +1746,11 @@ def test_assemble_with_unequal_kernels(self): disc_one = Kernel("disc_one", test_params=True) disc_two = Kernel("disc_two", test_params=False) - schedule = Schedule() - schedule += Acquire(5, AcquireChannel(0), MemorySlot(0), kernel=disc_one) - schedule += Acquire(5, AcquireChannel(1), MemorySlot(1), kernel=disc_two) - schedule += Acquire(5, AcquireChannel(2), MemorySlot(2)) + with self.assertWarns(DeprecationWarning): + schedule = Schedule() + schedule += Acquire(5, AcquireChannel(0), MemorySlot(0), kernel=disc_one) + schedule += Acquire(5, AcquireChannel(1), MemorySlot(1), kernel=disc_two) + schedule += Acquire(5, AcquireChannel(2), MemorySlot(2)) with self.assertRaises(QiskitError), self.assertWarns(DeprecationWarning): assemble( @@ -1723,19 +1762,20 @@ def test_assemble_with_unequal_kernels(self): def test_assemble_single_instruction(self): """Test assembling schedules, no lo config.""" - inst = pulse.Play(pulse.Constant(100, 1.0), pulse.DriveChannel(0)) with self.assertWarns(DeprecationWarning): + inst = pulse.Play(pulse.Constant(100, 1.0), pulse.DriveChannel(0)) self.assertIsInstance(assemble(inst, self.backend), PulseQobj) def test_assemble_overlapping_time(self): """Test that assembly errors when qubits are measured in overlapping time.""" - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0)), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)) << 1, - ) + with self.assertWarns(DeprecationWarning): + schedule = Schedule() + schedule = schedule.append( + Acquire(5, AcquireChannel(0), MemorySlot(0)), + ) + schedule = schedule.append( + Acquire(5, AcquireChannel(1), MemorySlot(1)) << 1, + ) with self.assertRaises(QiskitError): with self.assertWarns(DeprecationWarning): assemble( @@ -1748,11 +1788,12 @@ def test_assemble_overlapping_time(self): def test_assemble_meas_map_vs_insts(self): """Test that assembly errors when the qubits are measured in overlapping time and qubits are not in the first meas_map list.""" - schedule = Schedule() - schedule += Acquire(5, AcquireChannel(0), MemorySlot(0)) - schedule += Acquire(5, AcquireChannel(1), MemorySlot(1)) - schedule += Acquire(5, AcquireChannel(2), MemorySlot(2)) << 2 - schedule += Acquire(5, AcquireChannel(3), MemorySlot(3)) << 2 + with self.assertWarns(DeprecationWarning): + schedule = Schedule() + schedule += Acquire(5, AcquireChannel(0), MemorySlot(0)) + schedule += Acquire(5, AcquireChannel(1), MemorySlot(1)) + schedule += Acquire(5, AcquireChannel(2), MemorySlot(2)) << 2 + schedule += Acquire(5, AcquireChannel(3), MemorySlot(3)) << 2 with self.assertRaises(QiskitError): with self.assertWarns(DeprecationWarning): @@ -1766,13 +1807,14 @@ def test_assemble_meas_map_vs_insts(self): def test_assemble_non_overlapping_time_single_meas_map(self): """Test that assembly works when qubits are measured in non-overlapping time within the same measurement map list.""" - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0)), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)) << 5, - ) + with self.assertWarns(DeprecationWarning): + schedule = Schedule() + schedule = schedule.append( + Acquire(5, AcquireChannel(0), MemorySlot(0)), + ) + schedule = schedule.append( + Acquire(5, AcquireChannel(1), MemorySlot(1)) << 5, + ) with self.assertWarns(DeprecationWarning): qobj = assemble( schedule, @@ -1784,13 +1826,14 @@ def test_assemble_non_overlapping_time_single_meas_map(self): def test_assemble_disjoint_time(self): """Test that assembly works when qubits are in disjoint meas map sets.""" - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(0), MemorySlot(0)), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)) << 1, - ) + with self.assertWarns(DeprecationWarning): + schedule = Schedule() + schedule = schedule.append( + Acquire(5, AcquireChannel(0), MemorySlot(0)), + ) + schedule = schedule.append( + Acquire(5, AcquireChannel(1), MemorySlot(1)) << 1, + ) with self.assertWarns(DeprecationWarning): qobj = assemble( schedule, @@ -1803,16 +1846,17 @@ def test_assemble_disjoint_time(self): def test_assemble_valid_qubits(self): """Test that assembly works when qubits that are in the measurement map is measured.""" - schedule = Schedule() - schedule = schedule.append( - Acquire(5, AcquireChannel(1), MemorySlot(1)), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(2), MemorySlot(2)), - ) - schedule = schedule.append( - Acquire(5, AcquireChannel(3), MemorySlot(3)), - ) + with self.assertWarns(DeprecationWarning): + schedule = Schedule() + schedule = schedule.append( + Acquire(5, AcquireChannel(1), MemorySlot(1)), + ) + schedule = schedule.append( + Acquire(5, AcquireChannel(2), MemorySlot(2)), + ) + schedule = schedule.append( + Acquire(5, AcquireChannel(3), MemorySlot(3)), + ) with self.assertWarns(DeprecationWarning): qobj = assemble( schedule, @@ -1828,22 +1872,23 @@ class TestPulseAssemblerMissingKwargs(QiskitTestCase): def setUp(self): super().setUp() - self.schedule = pulse.Schedule(name="fake_experiment") - with self.assertWarns(DeprecationWarning): + self.schedule = pulse.Schedule(name="fake_experiment") self.backend = FakeOpenPulse2Q() + self.config = self.backend.configuration() self.defaults = self.backend.defaults() self.qubit_lo_freq = list(self.defaults.qubit_freq_est) self.meas_lo_freq = list(self.defaults.meas_freq_est) self.qubit_lo_range = self.config.qubit_lo_range self.meas_lo_range = self.config.meas_lo_range - self.schedule_los = { - pulse.DriveChannel(0): self.qubit_lo_freq[0], - pulse.DriveChannel(1): self.qubit_lo_freq[1], - pulse.MeasureChannel(0): self.meas_lo_freq[0], - pulse.MeasureChannel(1): self.meas_lo_freq[1], - } + with self.assertWarns(DeprecationWarning): + self.schedule_los = { + pulse.DriveChannel(0): self.qubit_lo_freq[0], + pulse.DriveChannel(1): self.qubit_lo_freq[1], + pulse.MeasureChannel(0): self.meas_lo_freq[0], + pulse.MeasureChannel(1): self.meas_lo_freq[1], + } self.meas_map = self.config.meas_map self.memory_slots = self.config.n_qubits @@ -1995,14 +2040,15 @@ def test_single_and_deprecated_acquire_styles(self): """Test that acquires are identically combined with Acquires that take a single channel.""" with self.assertWarns(DeprecationWarning): backend = FakeOpenPulse2Q() - new_style_schedule = Schedule() + new_style_schedule = Schedule() acq_dur = 1200 - for i in range(2): - new_style_schedule += Acquire(acq_dur, AcquireChannel(i), MemorySlot(i)) + with self.assertWarns(DeprecationWarning): + for i in range(2): + new_style_schedule += Acquire(acq_dur, AcquireChannel(i), MemorySlot(i)) - deprecated_style_schedule = Schedule() - for i in range(2): - deprecated_style_schedule += Acquire(1200, AcquireChannel(i), MemorySlot(i)) + deprecated_style_schedule = Schedule() + for i in range(2): + deprecated_style_schedule += Acquire(1200, AcquireChannel(i), MemorySlot(i)) # The Qobj IDs will be different with self.assertWarns(DeprecationWarning): diff --git a/test/python/compiler/test_disassembler.py b/test/python/compiler/test_disassembler.py index 0525b54e2107..805fac1178a1 100644 --- a/test/python/compiler/test_disassembler.py +++ b/test/python/compiler/test_disassembler.py @@ -319,8 +319,9 @@ def assertCircuitCalibrationsEqual(self, in_circuits, out_circuits): """Verify circuit calibrations are equivalent pre-assembly and post-disassembly""" self.assertEqual(len(in_circuits), len(out_circuits)) for in_qc, out_qc in zip(in_circuits, out_circuits): - in_cals = in_qc.calibrations - out_cals = out_qc.calibrations + with self.assertWarns(DeprecationWarning): + in_cals = in_qc.calibrations + out_cals = out_qc.calibrations self.assertEqual(in_cals.keys(), out_cals.keys()) for gate_name in in_cals: self.assertEqual(in_cals[gate_name].keys(), out_cals[gate_name].keys()) @@ -337,31 +338,33 @@ def test_single_circuit_calibrations(self): qc.rx(theta, 1) qc = qc.assign_parameters({theta: np.pi}) - with pulse.build() as h_sched: - pulse.play(pulse.library.Drag(1, 0.15, 4, 2), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as h_sched: + pulse.play(pulse.library.Drag(1, 0.15, 4, 2), pulse.DriveChannel(0)) - with pulse.build() as x180: - pulse.play(pulse.library.Gaussian(1, 0.2, 5), pulse.DriveChannel(0)) + with pulse.build() as x180: + pulse.play(pulse.library.Gaussian(1, 0.2, 5), pulse.DriveChannel(0)) - qc.add_calibration("h", [0], h_sched) - qc.add_calibration(RXGate(np.pi), [0], x180) + qc.add_calibration("h", [0], h_sched) + qc.add_calibration(RXGate(np.pi), [0], x180) with self.assertWarns(DeprecationWarning): qobj = assemble(qc, FakeOpenPulse2Q()) output_circuits, _, _ = disassemble(qobj) - self.assertCircuitCalibrationsEqual([qc], output_circuits) + self.assertCircuitCalibrationsEqual([qc], output_circuits) def test_parametric_pulse_circuit_calibrations(self): """Test that disassembler parses parametric pulses back to pulse gates.""" - with pulse.build() as h_sched: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as h_sched: + pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) qc = QuantumCircuit(2) qc.h(0) - qc.add_calibration("h", [0], h_sched) with self.assertWarns(DeprecationWarning): + qc.add_calibration("h", [0], h_sched) backend = FakeOpenPulse2Q() backend.configuration().parametric_pulses = ["drag"] @@ -369,27 +372,30 @@ def test_parametric_pulse_circuit_calibrations(self): output_circuits, _, _ = disassemble(qobj) out_qc = output_circuits[0] - self.assertCircuitCalibrationsEqual([qc], output_circuits) - self.assertTrue( - all( - qc_sched.instructions == out_qc_sched.instructions - for (_, qc_gate), (_, out_qc_gate) in zip( - qc.calibrations.items(), out_qc.calibrations.items() - ) - for qc_sched, out_qc_sched in zip(qc_gate.values(), out_qc_gate.values()) - ), - ) + with self.assertWarns(DeprecationWarning): + self.assertCircuitCalibrationsEqual([qc], output_circuits) + self.assertTrue( + all( + qc_sched.instructions == out_qc_sched.instructions + for (_, qc_gate), (_, out_qc_gate) in zip( + qc.calibrations.items(), out_qc.calibrations.items() + ) + for qc_sched, out_qc_sched in zip(qc_gate.values(), out_qc_gate.values()) + ), + ) def test_multi_circuit_uncommon_calibrations(self): """Test that disassembler parses uncommon calibrations (stored at QOBJ experiment-level).""" - with pulse.build() as sched: - pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as sched: + pulse.play(pulse.library.Drag(50, 0.15, 4, 2), pulse.DriveChannel(0)) qc_0 = QuantumCircuit(2) qc_0.h(0) qc_0.append(RXGate(np.pi), [1]) - qc_0.add_calibration("h", [0], sched) - qc_0.add_calibration(RXGate(np.pi), [1], sched) + with self.assertWarns(DeprecationWarning): + qc_0.add_calibration("h", [0], sched) + qc_0.add_calibration(RXGate(np.pi), [1], sched) qc_1 = QuantumCircuit(2) qc_1.h(0) @@ -399,57 +405,61 @@ def test_multi_circuit_uncommon_calibrations(self): qobj = assemble(circuits, FakeOpenPulse2Q()) output_circuits, _, _ = disassemble(qobj) - self.assertCircuitCalibrationsEqual(circuits, output_circuits) + self.assertCircuitCalibrationsEqual(circuits, output_circuits) def test_multi_circuit_common_calibrations(self): """Test that disassembler parses common calibrations (stored at QOBJ-level).""" - with pulse.build() as sched: - pulse.play(pulse.library.Drag(1, 0.15, 4, 2), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as sched: + pulse.play(pulse.library.Drag(1, 0.15, 4, 2), pulse.DriveChannel(0)) qc_0 = QuantumCircuit(2) qc_0.h(0) qc_0.append(RXGate(np.pi), [1]) - qc_0.add_calibration("h", [0], sched) - qc_0.add_calibration(RXGate(np.pi), [1], sched) + with self.assertWarns(DeprecationWarning): + qc_0.add_calibration("h", [0], sched) + qc_0.add_calibration(RXGate(np.pi), [1], sched) qc_1 = QuantumCircuit(2) qc_1.h(0) - qc_1.add_calibration(RXGate(np.pi), [1], sched) + with self.assertWarns(DeprecationWarning): + qc_1.add_calibration(RXGate(np.pi), [1], sched) circuits = [qc_0, qc_1] with self.assertWarns(DeprecationWarning): qobj = assemble(circuits, FakeOpenPulse2Q()) output_circuits, _, _ = disassemble(qobj) - self.assertCircuitCalibrationsEqual(circuits, output_circuits) + self.assertCircuitCalibrationsEqual(circuits, output_circuits) def test_single_circuit_delay_calibrations(self): """Test that disassembler parses delay instruction back to delay gate.""" qc = QuantumCircuit(2) qc.append(Gate("test", 1, []), [0]) - test_sched = pulse.Delay(64, pulse.DriveChannel(0)) + pulse.Delay( - 160, pulse.DriveChannel(0) - ) + with self.assertWarns(DeprecationWarning): + test_sched = pulse.Delay(64, pulse.DriveChannel(0)) + pulse.Delay( + 160, pulse.DriveChannel(0) + ) - qc.add_calibration("test", [0], test_sched) + qc.add_calibration("test", [0], test_sched) - with self.assertWarns(DeprecationWarning): qobj = assemble(qc, FakeOpenPulse2Q()) output_circuits, _, _ = disassemble(qobj) - self.assertEqual(len(qc.calibrations), len(output_circuits[0].calibrations)) - self.assertEqual(qc.calibrations.keys(), output_circuits[0].calibrations.keys()) - self.assertTrue( - all( - qc_cal.keys() == out_qc_cal.keys() - for qc_cal, out_qc_cal in zip( - qc.calibrations.values(), output_circuits[0].calibrations.values() + self.assertEqual(len(qc.calibrations), len(output_circuits[0].calibrations)) + self.assertEqual(qc.calibrations.keys(), output_circuits[0].calibrations.keys()) + self.assertTrue( + all( + qc_cal.keys() == out_qc_cal.keys() + for qc_cal, out_qc_cal in zip( + qc.calibrations.values(), output_circuits[0].calibrations.values() + ) ) ) - ) - self.assertEqual( - qc.calibrations["test"][((0,), ())], output_circuits[0].calibrations["test"][((0,), ())] - ) + self.assertEqual( + qc.calibrations["test"][((0,), ())], + output_circuits[0].calibrations["test"][((0,), ())], + ) class TestPulseScheduleDisassembler(QiskitTestCase): @@ -464,19 +474,20 @@ def setUp(self): def test_disassemble_single_schedule(self): """Test disassembling a single schedule.""" - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - with pulse.build(self.backend) as sched: - with pulse.align_right(): - pulse.play(pulse.library.Constant(10, 1.0), d0) - pulse.set_phase(1.0, d0) - pulse.shift_phase(3.11, d0) - pulse.set_frequency(1e9, d0) - pulse.shift_frequency(1e7, d0) - pulse.delay(20, d0) - pulse.delay(10, d1) - pulse.play(pulse.library.Constant(8, 0.1), d1) - pulse.measure_all() + with self.assertWarns(DeprecationWarning): + d0 = pulse.DriveChannel(0) + d1 = pulse.DriveChannel(1) + with pulse.build(self.backend) as sched: + with pulse.align_right(): + pulse.play(pulse.library.Constant(10, 1.0), d0) + pulse.set_phase(1.0, d0) + pulse.shift_phase(3.11, d0) + pulse.set_frequency(1e9, d0) + pulse.shift_frequency(1e7, d0) + pulse.delay(20, d0) + pulse.delay(10, d1) + pulse.play(pulse.library.Constant(8, 0.1), d1) + pulse.measure_all() with self.assertWarns(DeprecationWarning): qobj = assemble(sched, backend=self.backend, shots=2000) @@ -490,36 +501,38 @@ def test_disassemble_single_schedule(self): self.assertEqual(run_config_out.qubit_lo_freq, self.backend.defaults().qubit_freq_est) self.assertEqual(run_config_out.rep_time, 99) self.assertEqual(len(scheds), 1) - self.assertEqual(scheds[0], target_qobj_transform(sched)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(scheds[0], target_qobj_transform(sched)) def test_disassemble_multiple_schedules(self): """Test disassembling multiple schedules, all should have the same config.""" - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - with pulse.build(self.backend) as sched0: - with pulse.align_right(): - pulse.play(pulse.library.Constant(10, 1.0), d0) - pulse.set_phase(1.0, d0) - pulse.shift_phase(3.11, d0) - pulse.set_frequency(1e9, d0) - pulse.shift_frequency(1e7, d0) - pulse.delay(20, d0) - pulse.delay(10, d1) - pulse.play(pulse.library.Constant(8, 0.1), d1) - pulse.measure_all() - - with pulse.build(self.backend) as sched1: - with pulse.align_right(): - pulse.play(pulse.library.Constant(8, 0.1), d0) - pulse.play(pulse.library.Waveform([0.0, 1.0]), d1) - pulse.set_phase(1.1, d0) - pulse.shift_phase(3.5, d0) - pulse.set_frequency(2e9, d0) - pulse.shift_frequency(3e7, d1) - pulse.delay(20, d1) - pulse.delay(10, d0) - pulse.play(pulse.library.Constant(8, 0.4), d1) - pulse.measure_all() + with self.assertWarns(DeprecationWarning): + d0 = pulse.DriveChannel(0) + d1 = pulse.DriveChannel(1) + with pulse.build(self.backend) as sched0: + with pulse.align_right(): + pulse.play(pulse.library.Constant(10, 1.0), d0) + pulse.set_phase(1.0, d0) + pulse.shift_phase(3.11, d0) + pulse.set_frequency(1e9, d0) + pulse.shift_frequency(1e7, d0) + pulse.delay(20, d0) + pulse.delay(10, d1) + pulse.play(pulse.library.Constant(8, 0.1), d1) + pulse.measure_all() + + with pulse.build(self.backend) as sched1: + with pulse.align_right(): + pulse.play(pulse.library.Constant(8, 0.1), d0) + pulse.play(pulse.library.Waveform([0.0, 1.0]), d1) + pulse.set_phase(1.1, d0) + pulse.shift_phase(3.5, d0) + pulse.set_frequency(2e9, d0) + pulse.shift_frequency(3e7, d1) + pulse.delay(20, d1) + pulse.delay(10, d0) + pulse.play(pulse.library.Constant(8, 0.4), d1) + pulse.measure_all() with self.assertWarns(DeprecationWarning): qobj = assemble([sched0, sched1], backend=self.backend, shots=2000) @@ -529,33 +542,36 @@ def test_disassemble_multiple_schedules(self): self.assertEqual(run_config_out.shots, 2000) self.assertEqual(run_config_out.memory, False) self.assertEqual(len(scheds), 2) - self.assertEqual(scheds[0], target_qobj_transform(sched0)) - self.assertEqual(scheds[1], target_qobj_transform(sched1)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(scheds[0], target_qobj_transform(sched0)) + self.assertEqual(scheds[1], target_qobj_transform(sched1)) def test_disassemble_parametric_pulses(self): """Test disassembling multiple schedules all should have the same config.""" - d0 = pulse.DriveChannel(0) - with pulse.build(self.backend) as sched: - with pulse.align_right(): - pulse.play(pulse.library.Constant(10, 1.0), d0) - pulse.play(pulse.library.Gaussian(10, 1.0, 2.0), d0) - pulse.play(pulse.library.GaussianSquare(10, 1.0, 2.0, 3), d0) - pulse.play(pulse.library.Drag(10, 1.0, 2.0, 0.1), d0) + with self.assertWarns(DeprecationWarning): + d0 = pulse.DriveChannel(0) + with pulse.build(self.backend) as sched: + with pulse.align_right(): + pulse.play(pulse.library.Constant(10, 1.0), d0) + pulse.play(pulse.library.Gaussian(10, 1.0, 2.0), d0) + pulse.play(pulse.library.GaussianSquare(10, 1.0, 2.0, 3), d0) + pulse.play(pulse.library.Drag(10, 1.0, 2.0, 0.1), d0) with self.assertWarns(DeprecationWarning): qobj = assemble(sched, backend=self.backend, shots=2000) scheds, _, _ = disassemble(qobj) - self.assertEqual(scheds[0], target_qobj_transform(sched)) + self.assertEqual(scheds[0], target_qobj_transform(sched)) def test_disassemble_schedule_los(self): """Test disassembling schedule los.""" - d0 = pulse.DriveChannel(0) - m0 = pulse.MeasureChannel(0) - d1 = pulse.DriveChannel(1) - m1 = pulse.MeasureChannel(1) + with self.assertWarns(DeprecationWarning): + d0 = pulse.DriveChannel(0) + m0 = pulse.MeasureChannel(0) + d1 = pulse.DriveChannel(1) + m1 = pulse.MeasureChannel(1) - sched0 = pulse.Schedule() - sched1 = pulse.Schedule() + sched0 = pulse.Schedule() + sched1 = pulse.Schedule() schedule_los = [ {d0: 4.5e9, d1: 5e9, m0: 6e9, m1: 7e9}, diff --git a/test/python/compiler/test_scheduler.py b/test/python/compiler/test_scheduler.py index ad9b14b24c4a..c349bf054c3b 100644 --- a/test/python/compiler/test_scheduler.py +++ b/test/python/compiler/test_scheduler.py @@ -37,9 +37,10 @@ def setUp(self): self.circ2.cx(qr2[0], qr2[1]) self.circ2.measure(qr2, cr2) - self.backend = GenericBackendV2( - 3, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) + with self.assertWarns(DeprecationWarning): + self.backend = GenericBackendV2( + 3, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) def test_instruction_map_and_backend_not_supplied(self): """Test instruction map and backend not supplied.""" @@ -47,7 +48,8 @@ def test_instruction_map_and_backend_not_supplied(self): QiskitError, r"Must supply either a backend or InstructionScheduleMap for scheduling passes.", ): - schedule(self.circ) + with self.assertWarns(DeprecationWarning): + schedule(self.circ) def test_instruction_map_and_backend_defaults_unavailable(self): """Test backend defaults unavailable when backend is provided, but instruction map is not.""" @@ -57,7 +59,8 @@ def test_instruction_map_and_backend_defaults_unavailable(self): with self.assertRaisesRegex( QiskitError, r"The backend defaults are unavailable. The backend may not support pulse." ): - schedule(self.circ, self.backend) + with self.assertWarns(DeprecationWarning): + schedule(self.circ, self.backend) def test_measurement_map_and_backend_not_supplied(self): """Test measurement map and backend not supplied.""" @@ -65,11 +68,13 @@ def test_measurement_map_and_backend_not_supplied(self): QiskitError, r"Must supply either a backend or a meas_map for scheduling passes.", ): - schedule(self.circ, inst_map=InstructionScheduleMap()) + with self.assertWarns(DeprecationWarning): + schedule(self.circ, inst_map=InstructionScheduleMap()) def test_schedules_single_circuit(self): """Test scheduling of a single circuit.""" - circuit_schedule = schedule(self.circ, self.backend) + with self.assertWarns(DeprecationWarning): + circuit_schedule = schedule(self.circ, self.backend) self.assertIsInstance(circuit_schedule, Schedule) self.assertEqual(circuit_schedule.name, "circ") @@ -79,18 +84,20 @@ def test_schedules_multiple_circuits(self): self.enable_parallel_processing() circuits = [self.circ, self.circ2] - circuit_schedules = schedule(circuits, self.backend, method="asap") + with self.assertWarns(DeprecationWarning): + circuit_schedules = schedule(circuits, self.backend, method="asap") self.assertEqual(len(circuit_schedules), len(circuits)) circuit_one_schedule = circuit_schedules[0] circuit_two_schedule = circuit_schedules[1] - self.assertEqual( - circuit_one_schedule, - schedule(self.circ, self.backend, method="asap"), - ) - - self.assertEqual( - circuit_two_schedule, - schedule(self.circ2, self.backend, method="asap"), - ) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + circuit_one_schedule, + schedule(self.circ, self.backend, method="asap"), + ) + + self.assertEqual( + circuit_two_schedule, + schedule(self.circ2, self.backend, method="asap"), + ) diff --git a/test/python/compiler/test_sequencer.py b/test/python/compiler/test_sequencer.py index ae75348a5cdf..3fcfc16674a6 100644 --- a/test/python/compiler/test_sequencer.py +++ b/test/python/compiler/test_sequencer.py @@ -34,7 +34,8 @@ def setUp(self): self.backend.configuration().timing_constraints = {} def test_sequence_empty(self): - self.assertEqual(sequence([], self.backend), []) + with self.assertWarns(DeprecationWarning): + self.assertEqual(sequence([], self.backend), []) def test_transpile_and_sequence_agree_with_schedule(self): qc = QuantumCircuit(2, name="bell") @@ -47,14 +48,17 @@ def test_transpile_and_sequence_agree_with_schedule(self): "stop supporting inputs of type `BackendV1`", ): sc = transpile(qc, self.backend, scheduling_method="alap") - actual = sequence(sc, self.backend) + with self.assertWarns(DeprecationWarning): + actual = sequence(sc, self.backend) with self.assertWarnsRegex( DeprecationWarning, expected_regex="The `transpile` function will " "stop supporting inputs of type `BackendV1`", ): expected = schedule(transpile(qc, self.backend), self.backend) - self.assertEqual(actual, pad(expected)) + with self.assertWarns(DeprecationWarning): + # pad adds Delay which is deprecated + self.assertEqual(actual, pad(expected)) def test_transpile_and_sequence_agree_with_schedule_for_circuit_with_delay(self): qc = QuantumCircuit(1, 1, name="t2") @@ -68,17 +72,19 @@ def test_transpile_and_sequence_agree_with_schedule_for_circuit_with_delay(self) "stop supporting inputs of type `BackendV1`", ): sc = transpile(qc, self.backend, scheduling_method="alap") - actual = sequence(sc, self.backend) + with self.assertWarns(DeprecationWarning): + actual = sequence(sc, self.backend) with self.assertWarnsRegex( DeprecationWarning, expected_regex="The `transpile` function will " "stop supporting inputs of type `BackendV1`", ): expected = schedule(transpile(qc, self.backend), self.backend) - self.assertEqual( - actual.exclude(instruction_types=[pulse.Delay]), - expected.exclude(instruction_types=[pulse.Delay]), - ) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + actual.exclude(instruction_types=[pulse.Delay]), + expected.exclude(instruction_types=[pulse.Delay]), + ) @unittest.skip("not yet determined if delays on ancilla should be removed or not") def test_transpile_and_sequence_agree_with_schedule_for_circuits_without_measures(self): @@ -91,11 +97,13 @@ def test_transpile_and_sequence_agree_with_schedule_for_circuits_without_measure "stop supporting inputs of type `BackendV1`", ): sc = transpile(qc, self.backend, scheduling_method="alap") - actual = sequence(sc, self.backend) + with self.assertWarns(DeprecationWarning): + actual = sequence(sc, self.backend) with self.assertWarnsRegex( DeprecationWarning, expected_regex="The `transpile` function will " "stop supporting inputs of type `BackendV1`", ): - expected = schedule(transpile(qc, self.backend), self.backend) + with self.assertWarns(DeprecationWarning): + expected = schedule(transpile(qc, self.backend), self.backend) self.assertEqual(actual, pad(expected)) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index a90c2bfee7d2..38991b63a63e 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -19,6 +19,7 @@ import sys from logging import StreamHandler, getLogger from unittest.mock import patch +import warnings import numpy as np import rustworkx as rx from ddt import data, ddt, unpack @@ -1305,14 +1306,15 @@ def test_transpiled_custom_gates_calibration(self): circ.append(custom_180, [0]) circ.append(custom_90, [1]) - with pulse.build() as q0_x180: - pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) - with pulse.build() as q1_y90: - pulse.play(pulse.library.Gaussian(20, -1.0, 3.0), pulse.DriveChannel(1)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + with pulse.build() as q1_y90: + pulse.play(pulse.library.Gaussian(20, -1.0, 3.0), pulse.DriveChannel(1)) - # Add calibration - circ.add_calibration(custom_180, [0], q0_x180) - circ.add_calibration(custom_90, [1], q1_y90) + # Add calibration + circ.add_calibration(custom_180, [0], q0_x180) + circ.add_calibration(custom_90, [1], q1_y90) transpiled_circuit = transpile( circ, @@ -1320,7 +1322,8 @@ def test_transpiled_custom_gates_calibration(self): layout_method="trivial", seed_transpiler=42, ) - self.assertEqual(transpiled_circuit.calibrations, circ.calibrations) + with self.assertWarns(DeprecationWarning): + self.assertEqual(transpiled_circuit.calibrations, circ.calibrations) self.assertEqual(list(transpiled_circuit.count_ops().keys()), ["mycustom"]) self.assertEqual(list(transpiled_circuit.count_ops().values()), [2]) @@ -1329,16 +1332,18 @@ def test_transpiled_basis_gates_calibrations(self): circ = QuantumCircuit(2) circ.h(0) - with pulse.build() as q0_x180: - pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) - # Add calibration - circ.add_calibration("h", [0], q0_x180) + # Add calibration + circ.add_calibration("h", [0], q0_x180) transpiled_circuit = transpile( circ, backend=GenericBackendV2(num_qubits=4, seed=42), seed_transpiler=42 ) - self.assertEqual(transpiled_circuit.calibrations, circ.calibrations) + with self.assertWarns(DeprecationWarning): + self.assertEqual(transpiled_circuit.calibrations, circ.calibrations) def test_transpile_calibrated_custom_gate_on_diff_qubit(self): """Test if the custom, non calibrated gate raises QiskitError.""" @@ -1347,11 +1352,12 @@ def test_transpile_calibrated_custom_gate_on_diff_qubit(self): circ = QuantumCircuit(2) circ.append(custom_180, [0]) - with pulse.build() as q0_x180: - pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) - # Add calibration - circ.add_calibration(custom_180, [1], q0_x180) + # Add calibration + circ.add_calibration(custom_180, [1], q0_x180) with self.assertRaises(QiskitError): transpile( @@ -1369,16 +1375,18 @@ def test_transpile_calibrated_nonbasis_gate_on_diff_qubit(self): circ.h(0) circ.h(1) - with pulse.build() as q0_x180: - pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) - # Add calibration - circ.add_calibration("h", [1], q0_x180) + # Add calibration + circ.add_calibration("h", [1], q0_x180) transpiled_circuit = transpile( circ, backend=GenericBackendV2(num_qubits=4), seed_transpiler=42, optimization_level=1 ) - self.assertEqual(transpiled_circuit.calibrations, circ.calibrations) + with self.assertWarns(DeprecationWarning): + self.assertEqual(transpiled_circuit.calibrations, circ.calibrations) self.assertEqual(set(transpiled_circuit.count_ops().keys()), {"rz", "sx", "h"}) def test_transpile_subset_of_calibrated_gates(self): @@ -1391,11 +1399,12 @@ def test_transpile_subset_of_calibrated_gates(self): circ.append(x_180, [0]) circ.h(1) - with pulse.build() as q0_x180: - pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as q0_x180: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) - circ.add_calibration(x_180, [0], q0_x180) - circ.add_calibration("h", [1], q0_x180) # 'h' is calibrated on qubit 1 + circ.add_calibration(x_180, [0], q0_x180) + circ.add_calibration("h", [1], q0_x180) # 'h' is calibrated on qubit 1 transpiled_circ = transpile( circ, @@ -1413,11 +1422,13 @@ def test_parameterized_calibrations_transpile(self): circ.append(Gate("rxt", 1, [2 * 3.14 * tau]), [0]) def q0_rxt(tau): - with pulse.build() as q0_rxt: - pulse.play(pulse.library.Gaussian(20, 0.4 * tau, 3.0), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as q0_rxt: + pulse.play(pulse.library.Gaussian(20, 0.4 * tau, 3.0), pulse.DriveChannel(0)) return q0_rxt - circ.add_calibration("rxt", [0], q0_rxt(tau), [2 * 3.14 * tau]) + with self.assertWarns(DeprecationWarning): + circ.add_calibration("rxt", [0], q0_rxt(tau), [2 * 3.14 * tau]) transpiled_circ = transpile( circ, @@ -1442,12 +1453,14 @@ def test_inst_durations_from_calibrations(self): qc = QuantumCircuit(2) qc.append(Gate("custom", 1, []), [0]) - with pulse.build() as cal: - pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) - qc.add_calibration("custom", [0], cal) + with self.assertWarns(DeprecationWarning): + with pulse.build() as cal: + pulse.play(pulse.library.Gaussian(20, 1.0, 3.0), pulse.DriveChannel(0)) + qc.add_calibration("custom", [0], cal) out = transpile(qc, scheduling_method="alap", seed_transpiler=42) - self.assertEqual(out.duration, cal.duration) + with self.assertWarns(DeprecationWarning): + self.assertEqual(out.duration, cal.duration) @data(0, 1, 2, 3) def test_multiqubit_gates_calibrations(self, opt_level): @@ -1461,35 +1474,36 @@ def test_multiqubit_gates_calibrations(self, opt_level): circ.measure_all() backend = GenericBackendV2(num_qubits=6) - with pulse.build(backend=backend, name="custom") as my_schedule: - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(0) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(1) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(2) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(3) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(4) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(1) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(2) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(3) - ) - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(4) - ) - circ.add_calibration("my_custom_gate", [0, 1, 2, 3, 4], my_schedule, []) + with self.assertWarns(DeprecationWarning): + with pulse.build(backend=backend, name="custom") as my_schedule: + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(0) + ) + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(1) + ) + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(2) + ) + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(3) + ) + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.drive_channel(4) + ) + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(1) + ) + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(2) + ) + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(3) + ) + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.ControlChannel(4) + ) + circ.add_calibration("my_custom_gate", [0, 1, 2, 3, 4], my_schedule, []) trans_circ = transpile( circ, backend=backend, @@ -1555,13 +1569,13 @@ def test_scheduling_timing_constraints(self): with self.assertWarns(DeprecationWarning): backend_v1 = Fake27QPulseV1() - backend_v2 = GenericBackendV2( - num_qubits=27, - calibrate_instructions=True, - control_flow=True, - coupling_map=MUMBAI_CMAP, - seed=42, - ) + backend_v2 = GenericBackendV2( + num_qubits=27, + calibrate_instructions=True, + control_flow=True, + coupling_map=MUMBAI_CMAP, + seed=42, + ) # the original timing constraints are granularity = min_length = 16 timing_constraints = TimingConstraints(granularity=32, min_length=64) error_msgs = { @@ -1575,15 +1589,19 @@ def test_scheduling_timing_constraints(self): qc.h(0) qc.cx(0, 1) qc.measure_all() - qc.add_calibration( - "h", [0], Schedule(Play(Gaussian(duration, 0.2, 4), DriveChannel(0))), [0, 0] - ) - qc.add_calibration( - "cx", - [0, 1], - Schedule(Play(Gaussian(duration, 0.2, 4), DriveChannel(1))), - [0, 0], - ) + with self.assertWarns(DeprecationWarning): + qc.add_calibration( + "h", + [0], + Schedule(Play(Gaussian(duration, 0.2, 4), DriveChannel(0))), + [0, 0], + ) + qc.add_calibration( + "cx", + [0, 1], + Schedule(Play(Gaussian(duration, 0.2, 4), DriveChannel(1))), + [0, 0], + ) with self.assertRaisesRegex(TranspilerError, error_msgs[duration]): with self.assertWarns(DeprecationWarning): _ = transpile( @@ -2784,18 +2802,22 @@ def __init__(self, target): def run(self, dag): """Run test pass that adds calibration of SX gate of qubit 0.""" - dag.add_calibration( - "sx", - qubits=(0,), - schedule=self.target["sx"][(0,)].calibration, # PulseQobj is parsed here - ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + # DAGCircuit.add_calibration() is deprecated but we can't use self.assertWarns() here + dag.add_calibration( + "sx", + qubits=(0,), + schedule=self.target["sx"][(0,)].calibration, # PulseQobj is parsed here + ) return dag # Create backend with empty calibrations (PulseQobjEntries) - backend = GenericBackendV2( - num_qubits=4, - calibrate_instructions=False, - ) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2( + num_qubits=4, + calibrate_instructions=False, + ) # This target has PulseQobj entries that provide a serialized schedule data pass_ = TestAddCalibration(backend.target) @@ -2807,10 +2829,11 @@ def run(self, dag): qc_copied = [qc for _ in range(10)] qcs_cal_added = pm.run(qc_copied) - ref_cal = backend.target["sx"][(0,)].calibration - for qc_test in qcs_cal_added: - added_cal = qc_test.calibrations["sx"][((0,), ())] - self.assertEqual(added_cal, ref_cal) + with self.assertWarns(DeprecationWarning): + ref_cal = backend.target["sx"][(0,)].calibration + for qc_test in qcs_cal_added: + added_cal = qc_test.calibrations["sx"][((0,), ())] + self.assertEqual(added_cal, ref_cal) @data(0, 1, 2, 3) def test_parallel_singleton_conditional_gate(self, opt_level): @@ -2910,20 +2933,22 @@ def test_backend_and_custom_gate(self, opt_level): coupling_map=[[0, 1], [1, 0], [1, 2], [1, 3], [2, 1], [3, 1], [3, 4], [4, 3]], seed=42, ) - inst_map = InstructionScheduleMap() - inst_map.add("newgate", [0, 1], pulse.ScheduleBlock()) + with self.assertWarns(DeprecationWarning): + inst_map = InstructionScheduleMap() + inst_map.add("newgate", [0, 1], pulse.ScheduleBlock()) newgate = Gate("newgate", 2, []) circ = QuantumCircuit(2) circ.append(newgate, [0, 1]) - tqc = transpile( - circ, - backend, - inst_map=inst_map, - basis_gates=["newgate"], - optimization_level=opt_level, - seed_transpiler=42, - ) + with self.assertWarns(DeprecationWarning): + tqc = transpile( + circ, + backend, + inst_map=inst_map, + basis_gates=["newgate"], + optimization_level=opt_level, + seed_transpiler=42, + ) self.assertEqual(len(tqc.data), 1) self.assertEqual(tqc.data[0].operation, newgate) for x in tqc.data[0].qubits: diff --git a/test/python/converters/test_circuit_to_dag.py b/test/python/converters/test_circuit_to_dag.py index 0bded9c0f4a2..852cb324aa79 100644 --- a/test/python/converters/test_circuit_to_dag.py +++ b/test/python/converters/test_circuit_to_dag.py @@ -45,14 +45,17 @@ def test_circuit_and_dag(self): def test_calibrations(self): """Test that calibrations are properly copied over.""" circuit_in = QuantumCircuit(1) - circuit_in.add_calibration("h", [0], None) - self.assertEqual(len(circuit_in.calibrations), 1) + with self.assertWarns(DeprecationWarning): + circuit_in.add_calibration("h", [0], None) + self.assertEqual(len(circuit_in.calibrations), 1) dag = circuit_to_dag(circuit_in) - self.assertEqual(len(dag.calibrations), 1) + with self.assertWarns(DeprecationWarning): + self.assertEqual(len(dag.calibrations), 1) circuit_out = dag_to_circuit(dag) - self.assertEqual(len(circuit_out.calibrations), 1) + with self.assertWarns(DeprecationWarning): + self.assertEqual(len(circuit_out.calibrations), 1) def test_wires_from_expr_nodes_condition(self): """Test that the classical wires implied by an `Expr` node in a control-flow op's diff --git a/test/python/converters/test_circuit_to_dagdependency.py b/test/python/converters/test_circuit_to_dagdependency.py index 9d33192d50a1..65221e9cf2c1 100644 --- a/test/python/converters/test_circuit_to_dagdependency.py +++ b/test/python/converters/test_circuit_to_dagdependency.py @@ -62,14 +62,17 @@ def test_circuit_and_dag_canonical2(self): def test_calibrations(self): """Test that calibrations are properly copied over.""" circuit_in = QuantumCircuit(1) - circuit_in.add_calibration("h", [0], None) - self.assertEqual(len(circuit_in.calibrations), 1) + with self.assertWarns(DeprecationWarning): + circuit_in.add_calibration("h", [0], None) + self.assertEqual(len(circuit_in.calibrations), 1) dag_dependency = circuit_to_dagdependency(circuit_in) - self.assertEqual(len(dag_dependency.calibrations), 1) + with self.assertWarns(DeprecationWarning): + self.assertEqual(len(dag_dependency.calibrations), 1) circuit_out = dagdependency_to_circuit(dag_dependency) - self.assertEqual(len(circuit_out.calibrations), 1) + with self.assertWarns(DeprecationWarning): + self.assertEqual(len(circuit_out.calibrations), 1) def test_metadata(self): """Test circuit metadata is preservered through conversion.""" diff --git a/test/python/converters/test_circuit_to_dagdependency_v2.py b/test/python/converters/test_circuit_to_dagdependency_v2.py index edeea3a59fea..3323ca0e6768 100644 --- a/test/python/converters/test_circuit_to_dagdependency_v2.py +++ b/test/python/converters/test_circuit_to_dagdependency_v2.py @@ -44,14 +44,16 @@ def test_circuit_and_dag_canonical(self): def test_calibrations(self): """Test that calibrations are properly copied over.""" circuit_in = QuantumCircuit(1) - circuit_in.add_calibration("h", [0], None) - self.assertEqual(len(circuit_in.calibrations), 1) + with self.assertWarns(DeprecationWarning): + circuit_in.add_calibration("h", [0], None) + self.assertEqual(len(circuit_in.calibrations), 1) dag_dependency = _circuit_to_dagdependency_v2(circuit_in) self.assertEqual(len(dag_dependency.calibrations), 1) circuit_out = dagdependency_to_circuit(dag_dependency) - self.assertEqual(len(circuit_out.calibrations), 1) + with self.assertWarns(DeprecationWarning): + self.assertEqual(len(circuit_out.calibrations), 1) def test_metadata(self): """Test circuit metadata is preservered through conversion.""" diff --git a/test/python/dagcircuit/test_compose.py b/test/python/dagcircuit/test_compose.py index ff5014eacef7..27404cec05c2 100644 --- a/test/python/dagcircuit/test_compose.py +++ b/test/python/dagcircuit/test_compose.py @@ -630,15 +630,18 @@ def test_compose_calibrations(self): """Test that compose carries over the calibrations.""" dag_cal = QuantumCircuit(1) dag_cal.append(Gate("", 1, []), qargs=[0]) - dag_cal.add_calibration(Gate("", 1, []), [0], Schedule()) + with self.assertWarns(DeprecationWarning): + dag_cal.add_calibration(Gate("", 1, []), [0], Schedule()) empty_dag = circuit_to_dag(QuantumCircuit(1)) calibrated_dag = circuit_to_dag(dag_cal) composed_dag = empty_dag.compose(calibrated_dag, inplace=False) - cal = {"": {((0,), ()): Schedule(name="sched0")}} - self.assertEqual(composed_dag.calibrations, cal) - self.assertEqual(calibrated_dag.calibrations, cal) + with self.assertWarns(DeprecationWarning): + cal = {"": {((0,), ()): Schedule(name="sched0")}} + with self.assertWarns(DeprecationWarning): + self.assertEqual(composed_dag.calibrations, cal) + self.assertEqual(calibrated_dag.calibrations, cal) if __name__ == "__main__": diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py index 27e438d30874..eb3b79f1b911 100644 --- a/test/python/primitives/test_backend_sampler.py +++ b/test/python/primitives/test_backend_sampler.py @@ -368,9 +368,10 @@ def max_circuits(self): def test_primitive_job_size_limit_backend_v1(self): """Test primitive respects backend's job size limit.""" - backend = GenericBackendV2( - 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2( + 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) qc = QuantumCircuit(1) qc.measure_all() qc2 = QuantumCircuit(1) @@ -378,8 +379,8 @@ def test_primitive_job_size_limit_backend_v1(self): qc2.measure_all() with self.assertWarns(DeprecationWarning): sampler = BackendSampler(backend=backend) - result = sampler.run([qc, qc2]).result() - self.assertIsInstance(result, SamplerResult) + result = sampler.run([qc, qc2]).result() + self.assertIsInstance(result, SamplerResult) self.assertEqual(len(result.quasi_dists), 2) self.assertDictAlmostEqual(result.quasi_dists[0], {0: 1}, 0.1) @@ -408,9 +409,10 @@ def test_circuit_with_dynamic_circuit(self): def test_sequential_run(self): """Test sequential run.""" - backend = GenericBackendV2( - 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2( + 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) qc = QuantumCircuit(1) qc.measure_all() qc2 = QuantumCircuit(1) @@ -458,10 +460,10 @@ def callback(msg): bound_counter = CallbackPass("bound_pass_manager", callback) bound_pass = PassManager(bound_counter) - backend = GenericBackendV2( - 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2( + 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) sampler = BackendSampler(backend=backend, bound_pass_manager=bound_pass) _ = sampler.run([self._circuit[0]]).result() expected = [ @@ -482,9 +484,13 @@ def callback(msg): # pylint: disable=function-redefined bound_counter = CallbackPass("bound_pass_manager", callback) bound_pass = PassManager(bound_counter) - backend = GenericBackendV2( - 7, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2( + 7, + calibrate_instructions=True, + basis_gates=["cx", "u1", "u2", "u3"], + seed=42, + ) with self.assertWarns(DeprecationWarning): sampler = BackendSampler(backend=backend, bound_pass_manager=bound_pass) _ = sampler.run([self._circuit[0], self._circuit[0]]).result() diff --git a/test/python/primitives/test_backend_sampler_v2.py b/test/python/primitives/test_backend_sampler_v2.py index 372ae3a6715c..e66b594cc738 100644 --- a/test/python/primitives/test_backend_sampler_v2.py +++ b/test/python/primitives/test_backend_sampler_v2.py @@ -1371,9 +1371,10 @@ def max_circuits(self): def test_job_size_limit_backend_v1(self): """Test BackendSamplerV2 respects backend's job size limit.""" - backend = GenericBackendV2( - 2, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2( + 2, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) qc = QuantumCircuit(1) qc.measure_all() qc2 = QuantumCircuit(1) diff --git a/test/python/primitives/test_primitive.py b/test/python/primitives/test_primitive.py index fb96081fa001..fc0118564f3d 100644 --- a/test/python/primitives/test_primitive.py +++ b/test/python/primitives/test_primitive.py @@ -135,13 +135,17 @@ def test_func(n): with self.subTest("pulse circuit"): def test_with_scheduling(n): - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(160 * n, 0.1), pulse.DriveChannel(0)), inplace=True - ) - qc = QuantumCircuit(1) + with self.assertWarns(DeprecationWarning): + custom_gate = pulse.Schedule(name="custom_x_gate") + custom_gate.insert( + 0, + pulse.Play(pulse.Constant(160 * n, 0.1), pulse.DriveChannel(0)), + inplace=True, + ) + qc = QuantumCircuit(1) qc.x(0) - qc.add_calibration("x", qubits=(0,), schedule=custom_gate) + with self.assertWarns(DeprecationWarning): + qc.add_calibration("x", qubits=(0,), schedule=custom_gate) backend = GenericBackendV2( num_qubits=2, basis_gates=["id", "u1", "u2", "u3", "cx"], seed=42 diff --git a/test/python/providers/fake_provider/test_generic_backend_v2.py b/test/python/providers/fake_provider/test_generic_backend_v2.py index 42f46b7d7851..d42a6dbf7e07 100644 --- a/test/python/providers/fake_provider/test_generic_backend_v2.py +++ b/test/python/providers/fake_provider/test_generic_backend_v2.py @@ -50,13 +50,14 @@ def test_ccx_2Q(self): def test_calibration_no_noise_info(self): """Test failing with a backend with calibration and no noise info""" with self.assertRaises(QiskitError): - GenericBackendV2( - num_qubits=2, - basis_gates=["ccx", "id"], - calibrate_instructions=True, - noise_info=False, - seed=42, - ) + with self.assertWarns(DeprecationWarning): + GenericBackendV2( + num_qubits=2, + basis_gates=["ccx", "id"], + calibrate_instructions=True, + noise_info=False, + seed=42, + ) def test_no_noise(self): """Test no noise info when parameter is false""" @@ -90,13 +91,14 @@ def test_no_noise_fully_connected(self): def test_no_info(self): """Test no noise info when parameter is false""" - backend = GenericBackendV2( - num_qubits=5, - coupling_map=CouplingMap.from_line(5), - noise_info=False, - pulse_channels=False, - seed=42, - ) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2( + num_qubits=5, + coupling_map=CouplingMap.from_line(5), + noise_info=False, + pulse_channels=False, + seed=42, + ) qc = QuantumCircuit(5) qc.h(0) qc.cx(0, 1) @@ -110,9 +112,10 @@ def test_no_info(self): def test_no_pulse_channels(self): """Test no/empty pulse channels when parameter is false""" - backend = GenericBackendV2( - num_qubits=5, coupling_map=CouplingMap.from_line(5), pulse_channels=False, seed=42 - ) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2( + num_qubits=5, coupling_map=CouplingMap.from_line(5), pulse_channels=False, seed=42 + ) qc = QuantumCircuit(5) qc.h(0) qc.cx(0, 1) diff --git a/test/python/providers/test_backend_v2.py b/test/python/providers/test_backend_v2.py index df5f7b9abdea..b3c5d3531ff1 100644 --- a/test/python/providers/test_backend_v2.py +++ b/test/python/providers/test_backend_v2.py @@ -208,24 +208,27 @@ def test_transpile_mumbai_target(self): def test_drive_channel(self, qubit): """Test getting drive channel with qubit index.""" backend = GenericBackendV2(num_qubits=5, seed=42) - chan = backend.drive_channel(qubit) - ref = channels.DriveChannel(qubit) + with self.assertWarns(DeprecationWarning): + chan = backend.drive_channel(qubit) + ref = channels.DriveChannel(qubit) self.assertEqual(chan, ref) @data(0, 1, 2, 3, 4) def test_measure_channel(self, qubit): """Test getting measure channel with qubit index.""" backend = GenericBackendV2(num_qubits=5, seed=42) - chan = backend.measure_channel(qubit) - ref = channels.MeasureChannel(qubit) + with self.assertWarns(DeprecationWarning): + chan = backend.measure_channel(qubit) + ref = channels.MeasureChannel(qubit) self.assertEqual(chan, ref) @data(0, 1, 2, 3, 4) def test_acquire_channel(self, qubit): """Test getting acquire channel with qubit index.""" backend = GenericBackendV2(num_qubits=5, seed=42) - chan = backend.acquire_channel(qubit) - ref = channels.AcquireChannel(qubit) + with self.assertWarns(DeprecationWarning): + chan = backend.acquire_channel(qubit) + ref = channels.AcquireChannel(qubit) self.assertEqual(chan, ref) @data((4, 3), (3, 4), (3, 2), (2, 3), (1, 2), (2, 1), (1, 0), (0, 1)) @@ -242,6 +245,7 @@ def test_control_channel(self, qubits): (0, 1): 0, } backend = GenericBackendV2(num_qubits=5, coupling_map=BOGOTA_CMAP, seed=42) - chan = backend.control_channel(qubits)[0] - ref = channels.ControlChannel(bogota_cr_channels_map[qubits]) + with self.assertWarns(DeprecationWarning): + chan = backend.control_channel(qubits)[0] + ref = channels.ControlChannel(bogota_cr_channels_map[qubits]) self.assertEqual(chan, ref) diff --git a/test/python/providers/test_backendconfiguration.py b/test/python/providers/test_backendconfiguration.py index 82bbd1c6847f..f3309fda4b40 100644 --- a/test/python/providers/test_backendconfiguration.py +++ b/test/python/providers/test_backendconfiguration.py @@ -70,64 +70,76 @@ def test_hamiltonian(self): def test_get_channels(self): """Test requesting channels from the system.""" - self.assertEqual(self.config.drive(0), DriveChannel(0)) - self.assertEqual(self.config.measure(1), MeasureChannel(1)) - self.assertEqual(self.config.acquire(0), AcquireChannel(0)) + + with self.assertWarns(DeprecationWarning): + self.assertEqual(self.config.drive(0), DriveChannel(0)) + self.assertEqual(self.config.measure(1), MeasureChannel(1)) + self.assertEqual(self.config.acquire(0), AcquireChannel(0)) with self.assertRaises(BackendConfigurationError): # Check that an error is raised if the system doesn't have that many qubits self.assertEqual(self.config.acquire(10), AcquireChannel(10)) - self.assertEqual(self.config.control(qubits=[0, 1]), [ControlChannel(0)]) + with self.assertWarns(DeprecationWarning): + self.assertEqual(self.config.control(qubits=[0, 1]), [ControlChannel(0)]) with self.assertRaises(BackendConfigurationError): # Check that an error is raised if key not found in self._qubit_channel_map self.config.control(qubits=(10, 1)) def test_get_channel_qubits(self): """Test to get all qubits operated on a given channel.""" - self.assertEqual(self.config.get_channel_qubits(channel=DriveChannel(0)), [0]) - self.assertEqual(self.config.get_channel_qubits(channel=ControlChannel(0)), [0, 1]) + with self.assertWarns(DeprecationWarning): + self.assertEqual(self.config.get_channel_qubits(channel=DriveChannel(0)), [0]) + self.assertEqual(self.config.get_channel_qubits(channel=ControlChannel(0)), [0, 1]) with self.assertWarns(DeprecationWarning): backend_3q = FakeOpenPulse3Q() - self.assertEqual(backend_3q.configuration().get_channel_qubits(ControlChannel(2)), [2, 1]) - self.assertEqual(backend_3q.configuration().get_channel_qubits(ControlChannel(1)), [1, 0]) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + backend_3q.configuration().get_channel_qubits(ControlChannel(2)), [2, 1] + ) + self.assertEqual( + backend_3q.configuration().get_channel_qubits(ControlChannel(1)), [1, 0] + ) with self.assertRaises(BackendConfigurationError): - # Check that an error is raised if key not found in self._channel_qubit_map - self.config.get_channel_qubits(MeasureChannel(10)) + with self.assertWarns(DeprecationWarning): + # Check that an error is raised if key not found in self._channel_qubit_map + self.config.get_channel_qubits(MeasureChannel(10)) def test_get_qubit_channels(self): """Test to get all channels operated on a given qubit.""" - self.assertTrue( - self._test_lists_equal( - actual=self.config.get_qubit_channels(qubit=(1,)), - expected=[DriveChannel(1), MeasureChannel(1), AcquireChannel(1)], + with self.assertWarns(DeprecationWarning): + self.assertTrue( + self._test_lists_equal( + actual=self.config.get_qubit_channels(qubit=(1,)), + expected=[DriveChannel(1), MeasureChannel(1), AcquireChannel(1)], + ) ) - ) - self.assertTrue( - self._test_lists_equal( - actual=self.config.get_qubit_channels(qubit=1), - expected=[ - ControlChannel(0), - ControlChannel(1), - AcquireChannel(1), - DriveChannel(1), - MeasureChannel(1), - ], + with self.assertWarns(DeprecationWarning): + self.assertTrue( + self._test_lists_equal( + actual=self.config.get_qubit_channels(qubit=1), + expected=[ + ControlChannel(0), + ControlChannel(1), + AcquireChannel(1), + DriveChannel(1), + MeasureChannel(1), + ], + ) ) - ) with self.assertWarns(DeprecationWarning): backend_3q = FakeOpenPulse3Q() - self.assertTrue( - self._test_lists_equal( - actual=backend_3q.configuration().get_qubit_channels(1), - expected=[ - MeasureChannel(1), - ControlChannel(0), - ControlChannel(2), - AcquireChannel(1), - DriveChannel(1), - ControlChannel(1), - ], + self.assertTrue( + self._test_lists_equal( + actual=backend_3q.configuration().get_qubit_channels(1), + expected=[ + MeasureChannel(1), + ControlChannel(0), + ControlChannel(2), + AcquireChannel(1), + DriveChannel(1), + ControlChannel(1), + ], + ) ) - ) with self.assertRaises(BackendConfigurationError): # Check that an error is raised if key not found in self._channel_qubit_map self.config.get_qubit_channels(10) diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index 3df3e7d5893f..c03c31e55c65 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -764,7 +764,8 @@ def test_convert_to_target_control_flow(self): "switch_case", ] defaults = backend.defaults() - target = convert_to_target(configuration, properties, defaults) + with self.assertWarns(DeprecationWarning): + target = convert_to_target(configuration, properties, defaults) self.assertTrue(target.instruction_supported("if_else", ())) self.assertFalse(target.instruction_supported("while_loop", ())) self.assertTrue(target.instruction_supported("for_loop", ())) @@ -796,7 +797,8 @@ def test_convert_unrelated_supported_instructions(self): "switch_case", ] defaults = backend.defaults() - target = convert_to_target(configuration, properties, defaults) + with self.assertWarns(DeprecationWarning): + target = convert_to_target(configuration, properties, defaults) self.assertTrue(target.instruction_supported("if_else", ())) self.assertFalse(target.instruction_supported("while_loop", ())) self.assertTrue(target.instruction_supported("for_loop", ())) diff --git a/test/python/providers/test_pulse_defaults.py b/test/python/providers/test_pulse_defaults.py index 18f849255917..2d8fe5b7bf11 100644 --- a/test/python/providers/test_pulse_defaults.py +++ b/test/python/providers/test_pulse_defaults.py @@ -29,10 +29,10 @@ def setUp(self): with self.assertWarns(DeprecationWarning): # BackendV2 does not have defaults self.defs = FakeOpenPulse2Q().defaults() - backend = GenericBackendV2( - 2, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 - ) - self.inst_map = backend.instruction_schedule_map + backend = GenericBackendV2( + 2, calibrate_instructions=True, basis_gates=["cx", "u1", "u2", "u3"], seed=42 + ) + self.inst_map = backend.instruction_schedule_map def test_buffer(self): """Test getting the buffer value.""" diff --git a/test/python/pulse/test_block.py b/test/python/pulse/test_block.py index c6a2c2384f64..652eba3ccf18 100644 --- a/test/python/pulse/test_block.py +++ b/test/python/pulse/test_block.py @@ -20,11 +20,14 @@ from qiskit.pulse.exceptions import PulseError from qiskit.providers.fake_provider import FakeOpenPulse2Q from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class BaseTestBlock(QiskitTestCase): """ScheduleBlock tests.""" + @ignore_pulse_deprecation_warnings def setUp(self): super().setUp() @@ -52,6 +55,7 @@ def assertScheduleEqual(self, target, reference): self.assertEqual(transforms.target_qobj_transform(target), reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestTransformation(BaseTestBlock): """Test conversion of ScheduleBlock to Schedule.""" @@ -140,6 +144,7 @@ def test_nested_alignment(self): self.assertScheduleEqual(block_main, ref_sched) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestBlockOperation(BaseTestBlock): """Test fundamental operation on schedule block. @@ -150,6 +155,7 @@ class TestBlockOperation(BaseTestBlock): This operation should be tested in `test.python.pulse.test_block.TestTransformation`. """ + @ignore_pulse_deprecation_warnings def setUp(self): super().setUp() @@ -372,6 +378,7 @@ def test_inherit_from(self): self.assertDictEqual(new_sched.metadata, ref_metadata) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestBlockEquality(BaseTestBlock): """Test equality of blocks. @@ -611,9 +618,11 @@ def test_instruction_out_of_order_complex_not_equal(self): self.assertNotEqual(block2_a, block2_b) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestParametrizedBlockOperation(BaseTestBlock): """Test fundamental operation with parametrization.""" + @ignore_pulse_deprecation_warnings def setUp(self): super().setUp() @@ -719,6 +728,7 @@ def test_parametrized_context(self): self.assertScheduleEqual(block, ref_sched) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestBlockFilter(BaseTestBlock): """Test ScheduleBlock filtering methods.""" diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py index 72a6de11ae08..9501d176c9e0 100644 --- a/test/python/pulse/test_builder.py +++ b/test/python/pulse/test_builder.py @@ -23,11 +23,14 @@ from qiskit.pulse import library, instructions from qiskit.pulse.exceptions import PulseError from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestBuilder(QiskitTestCase): """Test the pulse builder context.""" + @ignore_pulse_deprecation_warnings def setUp(self): super().setUp() with self.assertWarns(DeprecationWarning): @@ -44,6 +47,7 @@ def assertScheduleEqual(self, program, target): self.assertEqual(target_qobj_transform(program), target_qobj_transform(target)) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestBuilderBase(TestBuilder): """Test builder base.""" @@ -132,6 +136,7 @@ def test_unknown_string_identifier(self): pass +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestContexts(TestBuilder): """Test builder contexts.""" @@ -255,6 +260,7 @@ def test_phase_compensated_frequency_offset(self): self.assertScheduleEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestChannels(TestBuilder): """Test builder channels.""" @@ -279,6 +285,7 @@ def test_control_channel(self): self.assertEqual(pulse.control_channels(0, 1)[0], pulse.ControlChannel(0)) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestInstructions(TestBuilder): """Test builder instructions.""" @@ -457,6 +464,7 @@ def test_snapshot(self): self.assertScheduleEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestDirectives(TestBuilder): """Test builder directives.""" @@ -538,6 +546,7 @@ def test_trivial_barrier(self): self.assertEqual(schedule, pulse.ScheduleBlock()) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestUtilities(TestBuilder): """Test builder utilities.""" @@ -627,6 +636,7 @@ def test_seconds_to_samples_array(self): np.testing.assert_allclose(pulse.seconds_to_samples(times), np.array([100, 200, 300])) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestMacros(TestBuilder): """Test builder macros.""" @@ -694,7 +704,8 @@ def test_measure_all(self): backend = Fake127QPulseV1() num_qubits = backend.configuration().num_qubits with pulse.build(backend) as schedule: - regs = pulse.measure_all() + with self.assertWarns(DeprecationWarning): + regs = pulse.measure_all() reference = backend.defaults().instruction_schedule_map.get( "measure", list(range(num_qubits)) @@ -749,6 +760,7 @@ def test_delay_qubits(self): self.assertScheduleEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestBuilderComposition(TestBuilder): """Test more sophisticated composite builder examples.""" @@ -772,7 +784,8 @@ def get_sched(qubit_idx: [int], backend): "stop supporting inputs of type `BackendV1`", ): transpiled = compiler.transpile(qc, backend=backend, optimization_level=1) - return compiler.schedule(transpiled, backend) + with self.assertWarns(DeprecationWarning): + return compiler.schedule(transpiled, backend) with pulse.build(self.backend) as schedule: with pulse.align_sequential(): @@ -798,7 +811,8 @@ def get_sched(qubit_idx: [int], backend): "stop supporting inputs of type `BackendV1`", ): single_u2_qc = compiler.transpile(single_u2_qc, self.backend, optimization_level=1) - single_u2_sched = compiler.schedule(single_u2_qc, self.backend) + with self.assertWarns(DeprecationWarning): + single_u2_sched = compiler.schedule(single_u2_qc, self.backend) # sequential context sequential_reference = pulse.Schedule() @@ -828,7 +842,8 @@ def get_sched(qubit_idx: [int], backend): "stop supporting inputs of type `BackendV1`", ): triple_u2_qc = compiler.transpile(triple_u2_qc, self.backend, optimization_level=1) - align_left_reference = compiler.schedule(triple_u2_qc, self.backend, method="alap") + with self.assertWarns(DeprecationWarning): + align_left_reference = compiler.schedule(triple_u2_qc, self.backend, method="alap") # measurement measure_reference = macros.measure( @@ -846,6 +861,7 @@ def get_sched(qubit_idx: [int], backend): self.assertScheduleEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSubroutineCall(TestBuilder): """Test for calling subroutine.""" diff --git a/test/python/pulse/test_builder_v2.py b/test/python/pulse/test_builder_v2.py index 79d3ac5b6020..29248f6179ca 100644 --- a/test/python/pulse/test_builder_v2.py +++ b/test/python/pulse/test_builder_v2.py @@ -22,18 +22,21 @@ from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.pulse import instructions from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings from ..legacy_cmaps import MUMBAI_CMAP +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestBuilderV2(QiskitTestCase): """Test the pulse builder context with backendV2.""" def setUp(self): super().setUp() - self.backend = GenericBackendV2( - num_qubits=27, coupling_map=MUMBAI_CMAP, calibrate_instructions=True, seed=42 - ) + with self.assertWarns(DeprecationWarning): + self.backend = GenericBackendV2( + num_qubits=27, coupling_map=MUMBAI_CMAP, calibrate_instructions=True, seed=42 + ) def assertScheduleEqual(self, program, target): """Assert an error when two pulse programs are not equal. @@ -43,6 +46,7 @@ def assertScheduleEqual(self, program, target): self.assertEqual(target_qobj_transform(program), target_qobj_transform(target)) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestContextsV2(TestBuilderV2): """Test builder contexts.""" @@ -64,30 +68,36 @@ def test_phase_compensated_frequency_offset(self): self.assertScheduleEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestChannelsV2(TestBuilderV2): """Test builder channels.""" def test_drive_channel(self): """Text context builder drive channel.""" with pulse.build(self.backend): - self.assertEqual(pulse.drive_channel(0), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pulse.drive_channel(0), pulse.DriveChannel(0)) def test_measure_channel(self): """Text context builder measure channel.""" with pulse.build(self.backend): - self.assertEqual(pulse.measure_channel(0), pulse.MeasureChannel(0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pulse.measure_channel(0), pulse.MeasureChannel(0)) def test_acquire_channel(self): """Text context builder acquire channel.""" - with pulse.build(self.backend): - self.assertEqual(pulse.acquire_channel(0), pulse.AcquireChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build(self.backend): + self.assertEqual(pulse.acquire_channel(0), pulse.AcquireChannel(0)) def test_control_channel(self): """Text context builder control channel.""" with pulse.build(self.backend): - self.assertEqual(pulse.control_channels(0, 1)[0], pulse.ControlChannel(0)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pulse.control_channels(0, 1)[0], pulse.ControlChannel(0)) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestDirectivesV2(TestBuilderV2): """Test builder directives.""" @@ -100,7 +110,8 @@ def test_barrier_on_qubits(self): 2 """ with pulse.build(self.backend) as schedule: - pulse.barrier(0, 1) + with self.assertWarns(DeprecationWarning): + pulse.barrier(0, 1) reference = pulse.ScheduleBlock() reference += directives.RelativeBarrier( pulse.DriveChannel(0), @@ -119,6 +130,7 @@ def test_barrier_on_qubits(self): self.assertEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestUtilitiesV2(TestBuilderV2): """Test builder utilities.""" @@ -130,7 +142,8 @@ def test_active_backend(self): def test_qubit_channels(self): """Test getting the qubit channels of the active builder's backend.""" with pulse.build(self.backend): - qubit_channels = pulse.qubit_channels(0) + with self.assertWarns(DeprecationWarning): + qubit_channels = pulse.qubit_channels(0) self.assertEqual( qubit_channels, @@ -187,6 +200,7 @@ def test_seconds_to_samples_array(self): np.testing.assert_allclose(pulse.seconds_to_samples(times), np.array([100, 200, 300])) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestMacrosV2(TestBuilderV2): """Test builder macros with backendV2.""" @@ -195,12 +209,14 @@ def test_macro(self): @pulse.macro def nested(a): - pulse.play(pulse.Gaussian(100, a, 20), pulse.drive_channel(0)) + with self.assertWarns(DeprecationWarning): + pulse.play(pulse.Gaussian(100, a, 20), pulse.drive_channel(0)) return a * 2 @pulse.macro def test(): - pulse.play(pulse.Constant(100, 1.0), pulse.drive_channel(0)) + with self.assertWarns(DeprecationWarning): + pulse.play(pulse.Constant(100, 1.0), pulse.drive_channel(0)) output = nested(0.5) return output @@ -217,7 +233,8 @@ def test(): def test_measure(self): """Test utility function - measure with backendV2.""" with pulse.build(self.backend) as schedule: - reg = pulse.measure(0) + with self.assertWarns(DeprecationWarning): + reg = pulse.measure(0) self.assertEqual(reg, pulse.MemorySlot(0)) @@ -228,7 +245,8 @@ def test_measure(self): def test_measure_multi_qubits(self): """Test utility function - measure with multi qubits with backendV2.""" with pulse.build(self.backend) as schedule: - regs = pulse.measure([0, 1]) + with self.assertWarns(DeprecationWarning): + regs = pulse.measure([0, 1]) self.assertListEqual(regs, [pulse.MemorySlot(0), pulse.MemorySlot(1)]) @@ -241,7 +259,8 @@ def test_measure_multi_qubits(self): def test_measure_all(self): """Test utility function - measure with backendV2..""" with pulse.build(self.backend) as schedule: - regs = pulse.measure_all() + with self.assertWarns(DeprecationWarning): + regs = pulse.measure_all() self.assertEqual(regs, [pulse.MemorySlot(i) for i in range(self.backend.num_qubits)]) reference = macros.measure_all(self.backend) @@ -251,7 +270,8 @@ def test_measure_all(self): def test_delay_qubit(self): """Test delaying on a qubit macro.""" with pulse.build(self.backend) as schedule: - pulse.delay_qubits(10, 0) + with self.assertWarns(DeprecationWarning): + pulse.delay_qubits(10, 0) d0 = pulse.DriveChannel(0) m0 = pulse.MeasureChannel(0) @@ -271,7 +291,8 @@ def test_delay_qubit(self): def test_delay_qubits(self): """Test delaying on multiple qubits with backendV2 to make sure we don't insert delays twice.""" with pulse.build(self.backend) as schedule: - pulse.delay_qubits(10, 0, 1) + with self.assertWarns(DeprecationWarning): + pulse.delay_qubits(10, 0, 1) d0 = pulse.DriveChannel(0) d1 = pulse.DriveChannel(1) diff --git a/test/python/pulse/test_calibration_entries.py b/test/python/pulse/test_calibration_entries.py index cc31789ef683..6a112ab854d5 100644 --- a/test/python/pulse/test_calibration_entries.py +++ b/test/python/pulse/test_calibration_entries.py @@ -33,8 +33,10 @@ from qiskit.qobj.converters.pulse_instruction import QobjToInstructionConverter from qiskit.qobj.pulse_qobj import PulseLibraryItem, PulseQobjInstruction from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSchedule(QiskitTestCase): """Test case for the ScheduleDef.""" @@ -181,6 +183,7 @@ def test_equality(self): self.assertNotEqual(entry1, entry2) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestCallable(QiskitTestCase): """Test case for the CallableDef.""" @@ -276,6 +279,7 @@ def factory2(): self.assertNotEqual(entry1, entry2) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestPulseQobj(QiskitTestCase): """Test case for the PulseQobjDef.""" @@ -421,13 +425,16 @@ def test_equality(self): ) ] - entry1 = PulseQobjDef(name="my_gate1") + with self.assertWarns(DeprecationWarning): + entry1 = PulseQobjDef(name="my_gate1") entry1.define(serialized_program1) - entry2 = PulseQobjDef(name="my_gate2") + with self.assertWarns(DeprecationWarning): + entry2 = PulseQobjDef(name="my_gate2") entry2.define(serialized_program2) - entry3 = PulseQobjDef(name="my_gate3") + with self.assertWarns(DeprecationWarning): + entry3 = PulseQobjDef(name="my_gate3") entry3.define(serialized_program1) self.assertEqual(entry1, entry3) @@ -450,7 +457,7 @@ def test_equality_with_schedule(self): parameters={"amp": 0.1, "duration": 10}, ) ] - entry1 = PulseQobjDef(name="qobj_entry") + entry1 = PulseQobjDef(name="qobj_entry") entry1.define(serialized_program) program = Schedule() @@ -483,7 +490,8 @@ def test_calibration_missing_waveform(self): ch="d0", ) ] - entry = PulseQobjDef(name="qobj_entry") + with self.assertWarns(DeprecationWarning): + entry = PulseQobjDef(name="qobj_entry") entry.define(serialized_program) # This is pulse qobj before parsing it diff --git a/test/python/pulse/test_channels.py b/test/python/pulse/test_channels.py index 59c4d15bf67b..ed104bd6b3a1 100644 --- a/test/python/pulse/test_channels.py +++ b/test/python/pulse/test_channels.py @@ -28,6 +28,7 @@ PulseError, ) from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings class TestChannel(QiskitTestCase): @@ -48,6 +49,7 @@ def test_cannot_be_instantiated(self): PulseChannel(0) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestAcquireChannel(QiskitTestCase): """AcquireChannel tests.""" @@ -69,6 +71,7 @@ def test_channel_hash(self): self.assertEqual(hash_1, hash_2) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestClassicalIOChannel(QiskitTestCase): """Test base classical IO channel.""" @@ -78,6 +81,7 @@ def test_cannot_be_instantiated(self): ClassicalIOChannel(0) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestMemorySlot(QiskitTestCase): """MemorySlot tests.""" @@ -97,6 +101,7 @@ def test_validation(self): MemorySlot(-1) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestRegisterSlot(QiskitTestCase): """RegisterSlot tests.""" @@ -116,6 +121,7 @@ def test_validation(self): RegisterSlot(-1) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSnapshotChannel(QiskitTestCase): """SnapshotChannel tests.""" @@ -128,6 +134,7 @@ def test_default(self): self.assertTrue(isinstance(snapshot_channel, ClassicalIOChannel)) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestDriveChannel(QiskitTestCase): """DriveChannel tests.""" @@ -146,6 +153,7 @@ def test_validation(self): DriveChannel(-1) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestControlChannel(QiskitTestCase): """ControlChannel tests.""" @@ -164,6 +172,7 @@ def test_validation(self): ControlChannel(-1) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestMeasureChannel(QiskitTestCase): """MeasureChannel tests.""" diff --git a/test/python/pulse/test_experiment_configurations.py b/test/python/pulse/test_experiment_configurations.py index 8d6a93b39eac..8702ba2d1cf5 100644 --- a/test/python/pulse/test_experiment_configurations.py +++ b/test/python/pulse/test_experiment_configurations.py @@ -18,6 +18,7 @@ from qiskit.pulse.exceptions import PulseError from qiskit.pulse import LoConfig, LoRange, Kernel, Discriminator from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings class TestLoRange(QiskitTestCase): @@ -40,6 +41,7 @@ def test_properties_includes_and_eq(self): self.assertFalse(lo_range_1 == lo_range_3) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestLoConfig(QiskitTestCase): """LoConfig tests.""" diff --git a/test/python/pulse/test_instruction_schedule_map.py b/test/python/pulse/test_instruction_schedule_map.py index d1610d7ebcc0..1b3d9e31cb6c 100644 --- a/test/python/pulse/test_instruction_schedule_map.py +++ b/test/python/pulse/test_instruction_schedule_map.py @@ -35,8 +35,10 @@ from qiskit.qobj.converters import QobjToInstructionConverter from qiskit.providers.fake_provider import FakeOpenPulse2Q, Fake7QPulseV1 from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestInstructionScheduleMap(QiskitTestCase): """Test the InstructionScheduleMap.""" @@ -537,7 +539,7 @@ def test_two_instmaps_equal(self): """Test eq method when two instmaps are identical.""" with self.assertWarns(DeprecationWarning): backend = Fake7QPulseV1() - instmap1 = backend.defaults().instruction_schedule_map + instmap1 = backend.defaults().instruction_schedule_map instmap2 = copy.deepcopy(instmap1) self.assertEqual(instmap1, instmap2) @@ -546,7 +548,7 @@ def test_two_instmaps_different(self): """Test eq method when two instmaps are not identical.""" with self.assertWarns(DeprecationWarning): backend = Fake7QPulseV1() - instmap1 = backend.defaults().instruction_schedule_map + instmap1 = backend.defaults().instruction_schedule_map instmap2 = copy.deepcopy(instmap1) # override one of instruction @@ -558,7 +560,7 @@ def test_instmap_picklable(self): """Test if instmap can be pickled.""" with self.assertWarns(DeprecationWarning): backend = Fake7QPulseV1() - instmap = backend.defaults().instruction_schedule_map + instmap = backend.defaults().instruction_schedule_map ser_obj = pickle.dumps(instmap) deser_instmap = pickle.loads(ser_obj) @@ -574,7 +576,7 @@ def test_instmap_picklable_with_arguments(self): """ with self.assertWarns(DeprecationWarning): backend = Fake7QPulseV1() - instmap = backend.defaults().instruction_schedule_map + instmap = backend.defaults().instruction_schedule_map param1 = Parameter("P1") param2 = Parameter("P2") @@ -596,7 +598,7 @@ def test_check_backend_provider_cals(self): """Test if schedules provided by backend provider is distinguishable.""" with self.assertWarns(DeprecationWarning): backend = FakeOpenPulse2Q() - instmap = backend.defaults().instruction_schedule_map + instmap = backend.defaults().instruction_schedule_map publisher = instmap.get("u1", (0,), P0=0).metadata["publisher"] self.assertEqual(publisher, CalibrationPublisher.BACKEND_PROVIDER) @@ -605,7 +607,7 @@ def test_check_user_cals(self): """Test if schedules provided by user is distinguishable.""" with self.assertWarns(DeprecationWarning): backend = FakeOpenPulse2Q() - instmap = backend.defaults().instruction_schedule_map + instmap = backend.defaults().instruction_schedule_map test_u1 = Schedule() test_u1 += ShiftPhase(Parameter("P0"), DriveChannel(0)) @@ -619,7 +621,7 @@ def test_has_custom_gate(self): """Test method to check custom gate.""" with self.assertWarns(DeprecationWarning): backend = FakeOpenPulse2Q() - instmap = backend.defaults().instruction_schedule_map + instmap = backend.defaults().instruction_schedule_map self.assertFalse(instmap.has_custom_gate()) diff --git a/test/python/pulse/test_instructions.py b/test/python/pulse/test_instructions.py index f0bdd165db08..1eaf0f928499 100644 --- a/test/python/pulse/test_instructions.py +++ b/test/python/pulse/test_instructions.py @@ -17,8 +17,10 @@ from qiskit import circuit from qiskit.pulse import channels, configuration, instructions, library, exceptions from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestAcquire(QiskitTestCase): """Acquisition tests.""" @@ -85,6 +87,7 @@ def test_instructions_hash(self): self.assertEqual(hash_1, hash_2) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestDelay(QiskitTestCase): """Delay tests.""" @@ -121,6 +124,7 @@ def test_operator_delay(self): self.assertEqual(op_delay, op_identity) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSetFrequency(QiskitTestCase): """Set frequency tests.""" @@ -155,6 +159,7 @@ def test_parameter_expression(self): self.assertSetEqual(instr.parameters, {p1, p2}) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestShiftFrequency(QiskitTestCase): """Shift frequency tests.""" @@ -191,6 +196,7 @@ def test_parameter_expression(self): self.assertSetEqual(instr.parameters, {p1, p2}) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSetPhase(QiskitTestCase): """Test the instruction construction.""" @@ -226,6 +232,7 @@ def test_parameter_expression(self): self.assertSetEqual(instr.parameters, {p1, p2}) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestShiftPhase(QiskitTestCase): """Test the instruction construction.""" @@ -261,6 +268,7 @@ def test_parameter_expression(self): self.assertSetEqual(instr.parameters, {p1, p2}) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSnapshot(QiskitTestCase): """Snapshot tests.""" @@ -276,9 +284,11 @@ def test_default(self): self.assertEqual(repr(snapshot), "Snapshot(test_name, state, name='test_name')") +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestPlay(QiskitTestCase): """Play tests.""" + @ignore_pulse_deprecation_warnings def setUp(self): """Setup play tests.""" super().setUp() @@ -304,6 +314,7 @@ def test_play_non_pulse_ch_raises(self): instructions.Play(self.pulse_op, channels.AcquireChannel(0)) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestDirectives(QiskitTestCase): """Test pulse directives.""" diff --git a/test/python/pulse/test_macros.py b/test/python/pulse/test_macros.py index 6d87320cf480..c1f0b93339ab 100644 --- a/test/python/pulse/test_macros.py +++ b/test/python/pulse/test_macros.py @@ -26,11 +26,14 @@ from qiskit.pulse.exceptions import PulseError from qiskit.providers.fake_provider import FakeOpenPulse2Q, Fake27QPulseV1, GenericBackendV2 from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestMeasure(QiskitTestCase): """Pulse measure macro.""" + @ignore_pulse_deprecation_warnings def setUp(self): super().setUp() with self.assertWarns(DeprecationWarning): @@ -38,11 +41,12 @@ def setUp(self): self.backend_v1 = Fake27QPulseV1() self.inst_map = self.backend.defaults().instruction_schedule_map - self.backend_v2 = GenericBackendV2( - num_qubits=27, - calibrate_instructions=self.backend_v1.defaults().instruction_schedule_map, - seed=42, - ) + with self.assertWarns(DeprecationWarning): + self.backend_v2 = GenericBackendV2( + num_qubits=27, + calibrate_instructions=self.backend_v1.defaults().instruction_schedule_map, + seed=42, + ) def test_measure(self): """Test macro - measure.""" @@ -101,19 +105,21 @@ def test_fail_measure(self): def test_measure_v2(self): """Test macro - measure with backendV2.""" sched = macros.measure(qubits=[0], backend=self.backend_v2) - expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( - channels=[MeasureChannel(0), AcquireChannel(0)] - ) + with self.assertWarns(DeprecationWarning): + expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( + channels=[MeasureChannel(0), AcquireChannel(0)] + ) self.assertEqual(sched.instructions, expected.instructions) def test_measure_v2_sched_with_qubit_mem_slots(self): """Test measure with backendV2 and custom qubit_mem_slots.""" sched = macros.measure(qubits=[0], backend=self.backend_v2, qubit_mem_slots={0: 2}) - expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( - channels=[ - MeasureChannel(0), - ] - ) + with self.assertWarns(DeprecationWarning): + expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( + channels=[ + MeasureChannel(0), + ] + ) measure_duration = expected.filter(instruction_types=[Play]).duration expected += Acquire(measure_duration, AcquireChannel(0), MemorySlot(2)) self.assertEqual(sched.instructions, expected.instructions) @@ -126,11 +132,12 @@ def test_measure_v2_sched_with_meas_map(self): sched_with_meas_map_dict = macros.measure( qubits=[0], backend=self.backend_v2, meas_map={0: [0, 1], 1: [0, 1]} ) - expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( - channels=[ - MeasureChannel(0), - ] - ) + with self.assertWarns(DeprecationWarning): + expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( + channels=[ + MeasureChannel(0), + ] + ) measure_duration = expected.filter(instruction_types=[Play]).duration expected += Acquire(measure_duration, AcquireChannel(0), MemorySlot(0)) self.assertEqual(sched_with_meas_map_list.instructions, expected.instructions) @@ -139,16 +146,17 @@ def test_measure_v2_sched_with_meas_map(self): def test_multiple_measure_v2(self): """Test macro - multiple qubit measure with backendV2.""" sched = macros.measure(qubits=[0, 1], backend=self.backend_v2) - expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( - channels=[ - MeasureChannel(0), - ] - ) - expected += self.backend_v2.target.get_calibration("measure", (1,)).filter( - channels=[ - MeasureChannel(1), - ] - ) + with self.assertWarns(DeprecationWarning): + expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( + channels=[ + MeasureChannel(0), + ] + ) + expected += self.backend_v2.target.get_calibration("measure", (1,)).filter( + channels=[ + MeasureChannel(1), + ] + ) measure_duration = expected.filter(instruction_types=[Play]).duration expected += Acquire(measure_duration, AcquireChannel(0), MemorySlot(0)) expected += Acquire(measure_duration, AcquireChannel(1), MemorySlot(1)) @@ -210,19 +218,22 @@ def test_output_with_multiple_measure_v1_and_measure_v2(self): self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestMeasureAll(QiskitTestCase): """Pulse measure all macro.""" + @ignore_pulse_deprecation_warnings def setUp(self): super().setUp() with self.assertWarns(DeprecationWarning): self.backend_v1 = FakeOpenPulse2Q() self.inst_map = self.backend_v1.defaults().instruction_schedule_map - self.backend_v2 = GenericBackendV2( - num_qubits=2, - calibrate_instructions=self.backend_v1.defaults().instruction_schedule_map, - seed=42, - ) + with self.assertWarns(DeprecationWarning): + self.backend_v2 = GenericBackendV2( + num_qubits=2, + calibrate_instructions=self.backend_v1.defaults().instruction_schedule_map, + seed=42, + ) def test_measure_all(self): """Test measure_all function.""" diff --git a/test/python/pulse/test_parameter_manager.py b/test/python/pulse/test_parameter_manager.py index 0b91aaeaab4a..32c0e5a9d907 100644 --- a/test/python/pulse/test_parameter_manager.py +++ b/test/python/pulse/test_parameter_manager.py @@ -27,11 +27,14 @@ from qiskit.pulse.transforms import AlignEquispaced, AlignLeft, inline_subroutines from qiskit.pulse.utils import format_parameter_value from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class ParameterTestBase(QiskitTestCase): """A base class for parameter manager unittest, providing test schedule.""" + @ignore_pulse_deprecation_warnings def setUp(self): """Just some useful, reusable Parameters, constants, schedules.""" super().setUp() @@ -100,6 +103,7 @@ def setUp(self): self.test_sched = long_schedule +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestParameterGetter(ParameterTestBase): """Test getting parameters.""" @@ -183,6 +187,7 @@ def test_get_parameter_from_complex_schedule(self): self.assertEqual(len(visitor.parameters), 17) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestParameterSetter(ParameterTestBase): """Test setting parameters.""" @@ -451,6 +456,7 @@ def test_set_parameter_to_complex_schedule(self): self.assertEqual(assigned, ref_obj) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestAssignFromProgram(QiskitTestCase): """Test managing parameters from programs. Parameter manager is implicitly called.""" @@ -554,6 +560,7 @@ def test_pulse_assignment_with_parameter_names(self): self.assertEqual(sched1.instructions[3][1].phase, 1.57) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestScheduleTimeslots(QiskitTestCase): """Test for edge cases of timing overlap on parametrized channels. @@ -632,6 +639,7 @@ def test_cannot_build_schedule_with_unassigned_duration(self): @ddt.ddt +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestFormatParameter(QiskitTestCase): """Test format_parameter_value function.""" diff --git a/test/python/pulse/test_parser.py b/test/python/pulse/test_parser.py index 8fb491b9da74..d9a3f7dd9a7a 100644 --- a/test/python/pulse/test_parser.py +++ b/test/python/pulse/test_parser.py @@ -15,8 +15,10 @@ from qiskit.pulse.parser import parse_string_expr from qiskit.pulse.exceptions import PulseError from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestInstructionToQobjConverter(QiskitTestCase): """Expression parser test.""" diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index 9c57579d0bf1..fa60c2adb3e5 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -39,8 +39,10 @@ ) from qiskit.pulse import functional_pulse, PulseError from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestWaveform(QiskitTestCase): """Waveform tests.""" @@ -122,6 +124,7 @@ def test_pulse_limits(self): self.fail("Waveform incorrectly failed to approximately unit norm samples.") +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSymbolicPulses(QiskitTestCase): """Tests for all subclasses of SymbolicPulse.""" @@ -789,46 +792,43 @@ def test_gaussian_deprecated_type_check(self): gaussian_pulse = Gaussian(160, 0.1, 40) self.assertTrue(isinstance(gaussian_pulse, SymbolicPulse)) - with self.assertWarns(PendingDeprecationWarning): - self.assertTrue(isinstance(gaussian_pulse, Gaussian)) - self.assertFalse(isinstance(gaussian_pulse, GaussianSquare)) - self.assertFalse(isinstance(gaussian_pulse, Drag)) - self.assertFalse(isinstance(gaussian_pulse, Constant)) + self.assertTrue(isinstance(gaussian_pulse, Gaussian)) + self.assertFalse(isinstance(gaussian_pulse, GaussianSquare)) + self.assertFalse(isinstance(gaussian_pulse, Drag)) + self.assertFalse(isinstance(gaussian_pulse, Constant)) def test_gaussian_square_deprecated_type_check(self): """Test isinstance check works with deprecation.""" gaussian_square_pulse = GaussianSquare(800, 0.1, 64, 544) self.assertTrue(isinstance(gaussian_square_pulse, SymbolicPulse)) - with self.assertWarns(PendingDeprecationWarning): - self.assertFalse(isinstance(gaussian_square_pulse, Gaussian)) - self.assertTrue(isinstance(gaussian_square_pulse, GaussianSquare)) - self.assertFalse(isinstance(gaussian_square_pulse, Drag)) - self.assertFalse(isinstance(gaussian_square_pulse, Constant)) + self.assertFalse(isinstance(gaussian_square_pulse, Gaussian)) + self.assertTrue(isinstance(gaussian_square_pulse, GaussianSquare)) + self.assertFalse(isinstance(gaussian_square_pulse, Drag)) + self.assertFalse(isinstance(gaussian_square_pulse, Constant)) def test_drag_deprecated_type_check(self): """Test isinstance check works with deprecation.""" drag_pulse = Drag(160, 0.1, 40, 1.5) self.assertTrue(isinstance(drag_pulse, SymbolicPulse)) - with self.assertWarns(PendingDeprecationWarning): - self.assertFalse(isinstance(drag_pulse, Gaussian)) - self.assertFalse(isinstance(drag_pulse, GaussianSquare)) - self.assertTrue(isinstance(drag_pulse, Drag)) - self.assertFalse(isinstance(drag_pulse, Constant)) + self.assertFalse(isinstance(drag_pulse, Gaussian)) + self.assertFalse(isinstance(drag_pulse, GaussianSquare)) + self.assertTrue(isinstance(drag_pulse, Drag)) + self.assertFalse(isinstance(drag_pulse, Constant)) def test_constant_deprecated_type_check(self): """Test isinstance check works with deprecation.""" constant_pulse = Constant(160, 0.1, 40, 1.5) self.assertTrue(isinstance(constant_pulse, SymbolicPulse)) - with self.assertWarns(PendingDeprecationWarning): - self.assertFalse(isinstance(constant_pulse, Gaussian)) - self.assertFalse(isinstance(constant_pulse, GaussianSquare)) - self.assertFalse(isinstance(constant_pulse, Drag)) - self.assertTrue(isinstance(constant_pulse, Constant)) + self.assertFalse(isinstance(constant_pulse, Gaussian)) + self.assertFalse(isinstance(constant_pulse, GaussianSquare)) + self.assertFalse(isinstance(constant_pulse, Drag)) + self.assertTrue(isinstance(constant_pulse, Constant)) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestFunctionalPulse(QiskitTestCase): """Waveform tests.""" @@ -868,6 +868,7 @@ def local_gaussian(duration, amp, t0, sig): self.assertEqual(len(pulse_wf_inst.samples), _duration) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestScalableSymbolicPulse(QiskitTestCase): """ScalableSymbolicPulse tests""" diff --git a/test/python/pulse/test_reference.py b/test/python/pulse/test_reference.py index 3d7603461756..94e4215d1b58 100644 --- a/test/python/pulse/test_reference.py +++ b/test/python/pulse/test_reference.py @@ -18,8 +18,10 @@ from qiskit.pulse import ScheduleBlock, builder from qiskit.pulse.transforms import inline_subroutines from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestReference(QiskitTestCase): """Test for basic behavior of reference mechanism.""" @@ -430,9 +432,11 @@ def test_assign_existing_reference(self): sched_z1.assign_references({("conflict_name",): sched_y1}) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSubroutineWithCXGate(QiskitTestCase): """Test called program scope with practical example of building fully parametrized CX gate.""" + @ignore_pulse_deprecation_warnings def setUp(self): super().setUp() diff --git a/test/python/pulse/test_samplers.py b/test/python/pulse/test_samplers.py index 4e6ab46737d6..59a8805c0c06 100644 --- a/test/python/pulse/test_samplers.py +++ b/test/python/pulse/test_samplers.py @@ -18,6 +18,7 @@ from qiskit.pulse import library from qiskit.pulse.library import samplers from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings def linear(times: np.ndarray, m: float, b: float = 0.1) -> np.ndarray: @@ -32,6 +33,7 @@ def linear(times: np.ndarray, m: float, b: float = 0.1) -> np.ndarray: return m * times + b +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSampler(QiskitTestCase): """Test continuous pulse function samplers.""" diff --git a/test/python/pulse/test_schedule.py b/test/python/pulse/test_schedule.py index 5e5676e7c2d9..0d96977aab5e 100644 --- a/test/python/pulse/test_schedule.py +++ b/test/python/pulse/test_schedule.py @@ -47,11 +47,14 @@ from qiskit.pulse.schedule import Schedule, _overlaps, _find_insertion_index from qiskit.providers.fake_provider import FakeOpenPulse2Q from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class BaseTestSchedule(QiskitTestCase): """Schedule tests.""" + @ignore_pulse_deprecation_warnings def setUp(self): super().setUp() @@ -65,6 +68,7 @@ def linear(duration, slope, intercept): self.config = FakeOpenPulse2Q().configuration() +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestScheduleBuilding(BaseTestSchedule): """Test construction of schedules.""" @@ -469,6 +473,7 @@ def test_inherit_from(self): self.assertDictEqual(new_sched.metadata, ref_metadata) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestReplace(BaseTestSchedule): """Test schedule replacement.""" @@ -527,6 +532,7 @@ def test_replace_fails_on_overlap(self): sched.replace(old, new) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestDelay(BaseTestSchedule): """Test Delay Instruction""" @@ -601,6 +607,7 @@ def test_delay_snapshot_channel(self): self.assertIsInstance(sched, Schedule) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestScheduleFilter(BaseTestSchedule): """Test Schedule filtering methods""" @@ -882,6 +889,7 @@ def _filter_and_test_consistency(self, schedule: Schedule, *args, **kwargs): return filtered, excluded +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestScheduleEquality(BaseTestSchedule): """Test equality of schedules.""" @@ -945,6 +953,7 @@ def test_different_name_equal(self): ) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestTimingUtils(QiskitTestCase): """Test the Schedule helper functions.""" diff --git a/test/python/pulse/test_transforms.py b/test/python/pulse/test_transforms.py index c16405cff4d1..fd7fd726d2ff 100644 --- a/test/python/pulse/test_transforms.py +++ b/test/python/pulse/test_transforms.py @@ -39,11 +39,14 @@ from qiskit.pulse.instructions import directives from qiskit.providers.fake_provider import FakeOpenPulse2Q from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestAlignMeasures(QiskitTestCase): """Test the helper function which aligns acquires.""" + @ignore_pulse_deprecation_warnings def setUp(self): super().setUp() with self.assertWarns(DeprecationWarning): @@ -198,9 +201,11 @@ def test_measurement_at_zero(self): self.assertEqual(time, 0) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestAddImplicitAcquires(QiskitTestCase): """Test the helper function which makes implicit acquires explicit.""" + @ignore_pulse_deprecation_warnings def setUp(self): super().setUp() with self.assertWarns(DeprecationWarning): @@ -252,6 +257,7 @@ def test_multiple_acquires(self): self.assertEqual(sched.instructions, ((0, acq_q0), (2400, acq_q0))) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestPad(QiskitTestCase): """Test padding of schedule with delays.""" @@ -388,6 +394,7 @@ def get_pulse_ids(schedules: List[Schedule]) -> Set[int]: return ids +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestCompressTransform(QiskitTestCase): """Compress function test.""" @@ -514,6 +521,7 @@ def test_multiple_schedules(self): self.assertEqual(len(compressed_pulse_ids), 2) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestAlignSequential(QiskitTestCase): """Test sequential alignment transform.""" @@ -562,6 +570,7 @@ def test_align_sequential_with_barrier(self): self.assertEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestAlignLeft(QiskitTestCase): """Test left alignment transform.""" @@ -625,6 +634,7 @@ def test_align_left_with_barrier(self): self.assertEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestAlignRight(QiskitTestCase): """Test right alignment transform.""" @@ -689,6 +699,7 @@ def test_align_right_with_barrier(self): self.assertEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestAlignEquispaced(QiskitTestCase): """Test equispaced alignment transform.""" @@ -767,6 +778,7 @@ def test_equispaced_with_multiple_channels_longer_duration(self): self.assertEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestAlignFunc(QiskitTestCase): """Test callback alignment transform.""" @@ -812,6 +824,7 @@ def test_numerical_with_longer_duration(self): self.assertEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestFlatten(QiskitTestCase): """Test flattening transform.""" @@ -861,6 +874,7 @@ def channels(self): return self.operands +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestRemoveDirectives(QiskitTestCase): """Test removing of directives.""" @@ -881,6 +895,7 @@ def test_remove_directives(self): self.assertEqual(schedule, reference) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestRemoveTrivialBarriers(QiskitTestCase): """Test scheduling transforms.""" diff --git a/test/python/qasm2/test_structure.py b/test/python/qasm2/test_structure.py index d963eea7e255..a5e5bbd77329 100644 --- a/test/python/qasm2/test_structure.py +++ b/test/python/qasm2/test_structure.py @@ -1648,6 +1648,47 @@ def __init__(self, a): qc.append(MyGate(0.5), [0]) self.assertEqual(parsed, qc) + def test_compatible_definition_of_builtin_is_ignored(self): + program = """ + qreg q[1]; + gate my_gate a { U(0, 0, 0) a; } + my_gate q[0]; + """ + + class MyGate(Gate): + def __init__(self): + super().__init__("my_gate", 1, []) + + def _define(self): + self._definition = QuantumCircuit(1) + self._definition.z(0) + + parsed = qiskit.qasm2.loads( + program, custom_instructions=[qiskit.qasm2.CustomInstruction("my_gate", 0, 1, MyGate)] + ) + self.assertEqual(parsed.data[0].operation.definition, MyGate().definition) + + def test_gates_defined_after_a_builtin_align(self): + """It's easy to get out of sync between the Rust-space and Python-space components when + ``builtin=True``. See https://github.com/Qiskit/qiskit/issues/13339.""" + program = """ + OPENQASM 2.0; + gate first a { U(0, 0, 0) a; } + gate second a { U(pi, pi, pi) a; } + + qreg q[1]; + first q[0]; + second q[0]; + """ + custom = qiskit.qasm2.CustomInstruction("first", 0, 1, lib.XGate, builtin=True) + parsed = qiskit.qasm2.loads(program, custom_instructions=[custom]) + # Provided definitions for built-in gates are ignored, so it should be an XGate directly. + self.assertEqual(parsed.data[0].operation, lib.XGate()) + self.assertEqual(parsed.data[1].operation.name, "second") + defn = parsed.data[1].operation.definition.copy_empty_like() + defn.u(math.pi, math.pi, math.pi, 0) + self.assertEqual(parsed.data[1].operation.definition, defn) + class TestCustomClassical(QiskitTestCase): def test_qiskit_extensions(self): diff --git a/test/python/qobj/test_pulse_converter.py b/test/python/qobj/test_pulse_converter.py index 528d87244fe8..8f920eb96d80 100644 --- a/test/python/qobj/test_pulse_converter.py +++ b/test/python/qobj/test_pulse_converter.py @@ -68,7 +68,9 @@ def test_gaussian_pulse_instruction(self): angle = -0.7 with self.assertWarns(DeprecationWarning): converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) - instruction = Play(Gaussian(duration=25, sigma=15, amp=amp, angle=angle), DriveChannel(0)) + instruction = Play( + Gaussian(duration=25, sigma=15, amp=amp, angle=angle), DriveChannel(0) + ) with self.assertWarns(DeprecationWarning): valid_qobj = PulseQobjInstruction( name="parametric_pulse", @@ -85,10 +87,11 @@ def test_gaussian_square_pulse_instruction(self): converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) amp = 0.7 angle = -0.6 - instruction = Play( - GaussianSquare(duration=1500, sigma=15, amp=amp, width=1300, angle=angle), - MeasureChannel(1), - ) + with self.assertWarns(DeprecationWarning): + instruction = Play( + GaussianSquare(duration=1500, sigma=15, amp=amp, width=1300, angle=angle), + MeasureChannel(1), + ) with self.assertWarns(DeprecationWarning): valid_qobj = PulseQobjInstruction( @@ -109,7 +112,7 @@ def test_constant_pulse_instruction(self): """Test that parametric pulses are correctly converted to PulseQobjInstructions.""" with self.assertWarns(DeprecationWarning): converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) - instruction = Play(Constant(duration=25, amp=1, angle=np.pi), ControlChannel(2)) + instruction = Play(Constant(duration=25, amp=1, angle=np.pi), ControlChannel(2)) with self.assertWarns(DeprecationWarning): valid_qobj = PulseQobjInstruction( @@ -127,9 +130,9 @@ def test_drag_pulse_instruction(self): angle = -0.6 with self.assertWarns(DeprecationWarning): converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) - instruction = Play( - Drag(duration=25, sigma=15, amp=amp, angle=angle, beta=0.5), DriveChannel(0) - ) + instruction = Play( + Drag(duration=25, sigma=15, amp=amp, angle=angle, beta=0.5), DriveChannel(0) + ) with self.assertWarns(DeprecationWarning): valid_qobj = PulseQobjInstruction( @@ -188,16 +191,15 @@ def test_acquire(self): """Test converted qobj from AcquireInstruction.""" with self.assertWarns(DeprecationWarning): converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) - instruction = Acquire(10, AcquireChannel(0), MemorySlot(0), RegisterSlot(0)) - with self.assertWarns(DeprecationWarning): + instruction = Acquire(10, AcquireChannel(0), MemorySlot(0), RegisterSlot(0)) + valid_qobj = PulseQobjInstruction( name="acquire", t0=0, duration=10, qubits=[0], memory_slot=[0], register_slot=[0] ) self.assertEqual(converter(0, instruction), valid_qobj) - # without register - instruction = Acquire(10, AcquireChannel(0), MemorySlot(0)) - with self.assertWarns(DeprecationWarning): + # without register + instruction = Acquire(10, AcquireChannel(0), MemorySlot(0)) valid_qobj = PulseQobjInstruction( name="acquire", t0=0, duration=10, qubits=[0], memory_slot=[0] ) @@ -207,7 +209,7 @@ def test_snapshot(self): """Test converted qobj from Snapshot.""" with self.assertWarns(DeprecationWarning): converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) - instruction = Snapshot(label="label", snapshot_type="type") + instruction = Snapshot(label="label", snapshot_type="type") with self.assertWarns(DeprecationWarning): valid_qobj = PulseQobjInstruction(name="snapshot", t0=0, label="label", type="type") @@ -220,30 +222,31 @@ class TestQobjToInstructionConverter(QiskitTestCase): def setUp(self): super().setUp() - self.linear = Waveform(np.arange(0, 0.01), name="linear") with self.assertWarns(DeprecationWarning): + self.linear = Waveform(np.arange(0, 0.01), name="linear") self.pulse_library = [ PulseLibraryItem(name=self.linear.name, samples=self.linear.samples.tolist()) ] - self.converter = QobjToInstructionConverter(self.pulse_library, buffer=0) + with self.assertWarns(DeprecationWarning): + self.converter = QobjToInstructionConverter(self.pulse_library, buffer=0) self.num_qubits = 2 def test_drive_instruction(self): """Test converted qobj from PulseInstruction.""" - instruction = Play(self.linear, DriveChannel(0)) with self.assertWarns(DeprecationWarning): + instruction = Play(self.linear, DriveChannel(0)) qobj = PulseQobjInstruction(name="linear", ch="d0", t0=10) - converted_instruction = self.converter(qobj) - self.assertEqual(converted_instruction.instructions[0][-1], instruction) + converted_instruction = self.converter(qobj) + self.assertEqual(converted_instruction.instructions[0][-1], instruction) def test_parametric_pulses(self): """Test converted qobj from ParametricInstruction.""" - instruction = Play( - Gaussian(duration=25, sigma=15, amp=0.5, angle=np.pi / 2, name="pulse1"), - DriveChannel(0), - ) with self.assertWarns(DeprecationWarning): + instruction = Play( + Gaussian(duration=25, sigma=15, amp=0.5, angle=np.pi / 2, name="pulse1"), + DriveChannel(0), + ) qobj = PulseQobjInstruction( name="parametric_pulse", label="pulse1", @@ -252,7 +255,7 @@ def test_parametric_pulses(self): t0=0, parameters={"duration": 25, "sigma": 15, "amp": 0.5j}, ) - converted_instruction = self.converter(qobj) + converted_instruction = self.converter(qobj) self.assertEqual(converted_instruction.start_time, 0) self.assertEqual(converted_instruction.duration, 25) self.assertAlmostEqual(converted_instruction.instructions[0][-1], instruction) @@ -272,28 +275,28 @@ def test_parametric_pulses_no_label(self): t0=0, parameters={"duration": 25, "sigma": 15, "amp": -0.5 + 0.2j}, ) - converted_instruction = self.converter(qobj) + converted_instruction = self.converter(qobj) self.assertEqual(converted_instruction.instructions[0][-1].pulse.name, pulse_name) def test_frame_change(self): """Test converted qobj from ShiftPhase.""" with self.assertWarns(DeprecationWarning): qobj = PulseQobjInstruction(name="fc", ch="m0", t0=0, phase=0.1) - converted_instruction = self.converter(qobj) + converted_instruction = self.converter(qobj) - instruction = ShiftPhase(0.1, MeasureChannel(0)) + instruction = ShiftPhase(0.1, MeasureChannel(0)) self.assertEqual(converted_instruction.start_time, 0) self.assertEqual(converted_instruction.duration, 0) self.assertEqual(converted_instruction.instructions[0][-1], instruction) def test_parameterized_frame_change(self): """Test converted qobj from ShiftPhase.""" - instruction = ShiftPhase(4.0, MeasureChannel(0)) - shifted = instruction << 10 - with self.assertWarns(DeprecationWarning): + instruction = ShiftPhase(4.0, MeasureChannel(0)) + shifted = instruction << 10 + qobj = PulseQobjInstruction(name="fc", ch="m0", t0=10, phase="P1*2") - converted_instruction = self.converter(qobj) + converted_instruction = self.converter(qobj) self.assertIsInstance(converted_instruction, Schedule) @@ -308,9 +311,9 @@ def test_set_phase(self): """Test converted qobj from SetPhase.""" with self.assertWarns(DeprecationWarning): qobj = PulseQobjInstruction(name="setp", ch="m0", t0=0, phase=3.14) - converted_instruction = self.converter(qobj) + converted_instruction = self.converter(qobj) - instruction = SetPhase(3.14, MeasureChannel(0)) + instruction = SetPhase(3.14, MeasureChannel(0)) self.assertEqual(converted_instruction.start_time, 0) self.assertEqual(converted_instruction.duration, 0) self.assertEqual(converted_instruction.instructions[0][-1], instruction) @@ -319,24 +322,25 @@ def test_parameterized_set_phase(self): """Test converted qobj from SetPhase, with parameterized phase.""" with self.assertWarns(DeprecationWarning): qobj = PulseQobjInstruction(name="setp", ch="m0", t0=0, phase="p/2") - converted_instruction = self.converter(qobj) + converted_instruction = self.converter(qobj) self.assertIsInstance(converted_instruction, Schedule) bind_dict = {converted_instruction.get_parameters("p")[0]: 3.14} evaluated_instruction = converted_instruction.assign_parameters(bind_dict) - instruction = SetPhase(3.14 / 2, MeasureChannel(0)) + with self.assertWarns(DeprecationWarning): + instruction = SetPhase(3.14 / 2, MeasureChannel(0)) self.assertEqual(evaluated_instruction.start_time, 0) self.assertEqual(evaluated_instruction.duration, 0) self.assertEqual(evaluated_instruction.instructions[0][-1], instruction) def test_set_frequency(self): """Test converted qobj from SetFrequency.""" - instruction = SetFrequency(8.0e9, DriveChannel(0)) - with self.assertWarns(DeprecationWarning): + instruction = SetFrequency(8.0e9, DriveChannel(0)) + qobj = PulseQobjInstruction(name="setf", ch="d0", t0=0, frequency=8.0) - converted_instruction = self.converter(qobj) + converted_instruction = self.converter(qobj) self.assertEqual(converted_instruction.start_time, 0) self.assertEqual(converted_instruction.duration, 0) @@ -349,13 +353,15 @@ def test_parameterized_set_frequency(self): qobj = PulseQobjInstruction(name="setf", ch="d0", t0=2, frequency="f") self.assertTrue("frequency" in qobj.to_dict()) - converted_instruction = self.converter(qobj) + with self.assertWarns(DeprecationWarning): + converted_instruction = self.converter(qobj) self.assertIsInstance(converted_instruction, Schedule) bind_dict = {converted_instruction.get_parameters("f")[0]: 2.0} evaluated_instruction = converted_instruction.assign_parameters(bind_dict) - instruction = SetFrequency(2.0e9, DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + instruction = SetFrequency(2.0e9, DriveChannel(0)) self.assertEqual(evaluated_instruction.start_time, 2) self.assertEqual(evaluated_instruction.duration, 2) @@ -363,11 +369,11 @@ def test_parameterized_set_frequency(self): def test_shift_frequency(self): """Test converted qobj from ShiftFrequency.""" - instruction = ShiftFrequency(8.0e9, DriveChannel(0)) - with self.assertWarns(DeprecationWarning): + instruction = ShiftFrequency(8.0e9, DriveChannel(0)) + qobj = PulseQobjInstruction(name="shiftf", ch="d0", t0=0, frequency=8.0) - converted_instruction = self.converter(qobj) + converted_instruction = self.converter(qobj) self.assertEqual(converted_instruction.start_time, 0) self.assertEqual(converted_instruction.duration, 0) @@ -380,13 +386,15 @@ def test_parameterized_shift_frequency(self): qobj = PulseQobjInstruction(name="shiftf", ch="d0", t0=1, frequency="f / 1000") self.assertTrue("frequency" in qobj.to_dict()) - converted_instruction = self.converter(qobj) + with self.assertWarns(DeprecationWarning): + converted_instruction = self.converter(qobj) self.assertIsInstance(converted_instruction, Schedule) bind_dict = {converted_instruction.get_parameters("f")[0]: 3.14} evaluated_instruction = converted_instruction.assign_parameters(bind_dict) - instruction = ShiftFrequency(3.14e6, DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + instruction = ShiftFrequency(3.14e6, DriveChannel(0)) self.assertEqual(evaluated_instruction.start_time, 1) self.assertEqual(evaluated_instruction.duration, 1) @@ -394,11 +402,11 @@ def test_parameterized_shift_frequency(self): def test_delay(self): """Test converted qobj from Delay.""" - instruction = Delay(10, DriveChannel(0)) - with self.assertWarns(DeprecationWarning): + instruction = Delay(10, DriveChannel(0)) + qobj = PulseQobjInstruction(name="delay", ch="d0", t0=0, duration=10) - converted_instruction = self.converter(qobj) + converted_instruction = self.converter(qobj) self.assertTrue("delay" in qobj.to_dict().values()) self.assertEqual(converted_instruction.duration, instruction.duration) @@ -406,17 +414,17 @@ def test_delay(self): def test_acquire(self): """Test converted qobj from Acquire.""" - schedule = Schedule() - for i in range(self.num_qubits): - schedule |= Acquire( - 10, - AcquireChannel(i), - MemorySlot(i), - RegisterSlot(i), - kernel=Kernel(name="test_kern", test_params="test"), - discriminator=Discriminator(name="test_disc", test_params=1.0), - ) with self.assertWarns(DeprecationWarning): + schedule = Schedule() + for i in range(self.num_qubits): + schedule |= Acquire( + 10, + AcquireChannel(i), + MemorySlot(i), + RegisterSlot(i), + kernel=Kernel(name="test_kern", test_params="test"), + discriminator=Discriminator(name="test_disc", test_params=1.0), + ) qobj = PulseQobjInstruction( name="acquire", t0=0, @@ -429,7 +437,8 @@ def test_acquire(self): QobjMeasurementOption(name="test_disc", params={"test_params": 1.0}) ], ) - converted_instruction = self.converter(qobj) + with self.assertWarns(DeprecationWarning): + converted_instruction = self.converter(qobj) self.assertEqual(converted_instruction.start_time, 0) self.assertEqual(converted_instruction.duration, 10) @@ -437,16 +446,17 @@ def test_acquire(self): self.assertEqual( converted_instruction.instructions[0][-1].kernel.params, {"test_params": "test"} ) - self.assertEqual(converted_instruction.instructions[1][-1].channel, AcquireChannel(1)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(converted_instruction.instructions[1][-1].channel, AcquireChannel(1)) def test_snapshot(self): """Test converted qobj from SnapShot.""" - instruction = Snapshot(label="label", snapshot_type="type") - shifted = instruction << 10 - with self.assertWarns(DeprecationWarning): + instruction = Snapshot(label="label", snapshot_type="type") + shifted = instruction << 10 + qobj = PulseQobjInstruction(name="snapshot", t0=10, label="label", type="type") - converted_instruction = self.converter(qobj) + converted_instruction = self.converter(qobj) self.assertEqual(converted_instruction.start_time, shifted.start_time) self.assertEqual(converted_instruction.duration, shifted.duration) @@ -459,27 +469,30 @@ def test_instruction_name_collision(self): PulseLibraryItem(name="pulse123", samples=[0.1, 0.1, 0.1]), PulseLibraryItem(name="pulse456", samples=[0.3, 0.3, 0.3]), ] - converter_of_backend_x = QobjToInstructionConverter(pulse_library_from_backend_x, buffer=0) + converter_of_backend_x = QobjToInstructionConverter( + pulse_library_from_backend_x, buffer=0 + ) - with self.assertWarns(DeprecationWarning): pulse_library_from_backend_y = [ PulseLibraryItem(name="pulse123", samples=[0.2, 0.2, 0.2]) ] - converter_of_backend_y = QobjToInstructionConverter(pulse_library_from_backend_y, buffer=0) + converter_of_backend_y = QobjToInstructionConverter( + pulse_library_from_backend_y, buffer=0 + ) - with self.assertWarns(DeprecationWarning): qobj1 = PulseQobjInstruction(name="pulse123", qubits=[0], t0=0, ch="d0") qobj2 = PulseQobjInstruction(name="pulse456", qubits=[0], t0=0, ch="d0") - sched_out_x = converter_of_backend_x(qobj1) - sched_out_y = converter_of_backend_y(qobj1) + sched_out_x = converter_of_backend_x(qobj1) + sched_out_y = converter_of_backend_y(qobj1) # pulse123 have different definition on backend-x and backend-y self.assertNotEqual(sched_out_x, sched_out_y) with self.assertRaises(QiskitError): - # This should not exist in backend-y command namespace. - converter_of_backend_y(qobj2) + with self.assertWarns(DeprecationWarning): + # This should not exist in backend-y command namespace. + converter_of_backend_y(qobj2) class TestLoConverter(QiskitTestCase): @@ -487,8 +500,8 @@ class TestLoConverter(QiskitTestCase): def test_qubit_los(self): """Test qubit channel configuration.""" - user_lo_config = LoConfig({DriveChannel(0): 1.3e9}) with self.assertWarns(DeprecationWarning): + user_lo_config = LoConfig({DriveChannel(0): 1.3e9}) converter = LoConfigConverter( PulseQobjExperimentConfig, [1.2e9], [3.4e9], [(0.0, 5e9)], [(0.0, 5e9)] ) @@ -499,8 +512,8 @@ def test_qubit_los(self): def test_meas_los(self): """Test measurement channel configuration.""" - user_lo_config = LoConfig({MeasureChannel(0): 3.5e9}) with self.assertWarns(DeprecationWarning): + user_lo_config = LoConfig({MeasureChannel(0): 3.5e9}) converter = LoConfigConverter( PulseQobjExperimentConfig, [1.2e9], [3.4e9], [(0.0, 5e9)], [(0.0, 5e9)] ) diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index 10c1b8eda1ee..6085618e8362 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -14,6 +14,7 @@ import io import unittest +import warnings from ddt import ddt, data, unpack import numpy as np import symengine as sym @@ -38,6 +39,7 @@ from qiskit.pulse.instructions import Play, TimeBlockade from qiskit.circuit import Parameter, QuantumCircuit, Gate from qiskit.qpy import dump, load +from qiskit.qpy.exceptions import QPYLoadingDeprecatedFeatureWarning from qiskit.utils import optionals as _optional from qiskit.pulse.configuration import Kernel, Discriminator from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -49,7 +51,8 @@ class QpyScheduleTestCase(QiskitTestCase): def assert_roundtrip_equal(self, block, use_symengine=False): """QPY roundtrip equal test.""" qpy_file = io.BytesIO() - dump(block, qpy_file, use_symengine=use_symengine) + with self.assertWarns(DeprecationWarning): + dump(block, qpy_file, use_symengine=use_symengine) qpy_file.seek(0) new_block = load(qpy_file)[0] @@ -71,12 +74,14 @@ class TestLoadFromQPY(QpyScheduleTestCase): @unpack def test_library_pulse_play(self, envelope, channel, *params): """Test playing standard pulses.""" - with builder.build() as test_sched: - builder.play( - envelope(*params), - channel(0), - ) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.play( + envelope(*params), + channel(0), + ) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_playing_custom_symbolic_pulse(self): """Test playing a custom user pulse.""" @@ -84,82 +89,102 @@ def test_playing_custom_symbolic_pulse(self): t, amp, freq = sym.symbols("t, amp, freq") sym_envelope = 2 * amp * (freq * t - sym.floor(1 / 2 + freq * t)) - my_pulse = SymbolicPulse( - pulse_type="Sawtooth", - duration=100, - parameters={"amp": 0.1, "freq": 0.05}, - envelope=sym_envelope, - name="pulse1", - ) - with builder.build() as test_sched: - builder.play(my_pulse, DriveChannel(0)) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + my_pulse = SymbolicPulse( + pulse_type="Sawtooth", + duration=100, + parameters={"amp": 0.1, "freq": 0.05}, + envelope=sym_envelope, + name="pulse1", + ) + with builder.build() as test_sched: + builder.play(my_pulse, DriveChannel(0)) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_symbolic_amplitude_limit(self): """Test applying amplitude limit to symbolic pulse.""" - with builder.build() as test_sched: - builder.play( - Gaussian(160, 20, 40, limit_amplitude=False), - DriveChannel(0), - ) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.play( + Gaussian(160, 20, 40, limit_amplitude=False), + DriveChannel(0), + ) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_waveform_amplitude_limit(self): """Test applying amplitude limit to waveform.""" - with builder.build() as test_sched: - builder.play( - Waveform([1, 2, 3, 4, 5], limit_amplitude=False), - DriveChannel(0), - ) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.play( + Waveform([1, 2, 3, 4, 5], limit_amplitude=False), + DriveChannel(0), + ) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_playing_waveform(self): """Test playing waveform.""" # pylint: disable=invalid-name t = np.linspace(0, 1, 100) waveform = 0.1 * np.sin(2 * np.pi * t) - with builder.build() as test_sched: - builder.play(waveform, DriveChannel(0)) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.play(waveform, DriveChannel(0)) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_phases(self): """Test phase.""" - with builder.build() as test_sched: - builder.shift_phase(0.1, DriveChannel(0)) - builder.set_phase(0.4, DriveChannel(1)) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.shift_phase(0.1, DriveChannel(0)) + builder.set_phase(0.4, DriveChannel(1)) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_frequencies(self): """Test frequency.""" - with builder.build() as test_sched: - builder.shift_frequency(10e6, DriveChannel(0)) - builder.set_frequency(5e9, DriveChannel(1)) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.shift_frequency(10e6, DriveChannel(0)) + builder.set_frequency(5e9, DriveChannel(1)) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_delay(self): """Test delay.""" - with builder.build() as test_sched: - builder.delay(100, DriveChannel(0)) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.delay(100, DriveChannel(0)) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_barrier(self): """Test barrier.""" - with builder.build() as test_sched: - builder.barrier(DriveChannel(0), DriveChannel(1), ControlChannel(2)) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.barrier(DriveChannel(0), DriveChannel(1), ControlChannel(2)) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_time_blockade(self): """Test time blockade.""" - with builder.build() as test_sched: - builder.append_instruction(TimeBlockade(10, DriveChannel(0))) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.append_instruction(TimeBlockade(10, DriveChannel(0))) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_measure(self): """Test measurement.""" - with builder.build() as test_sched: - builder.acquire(100, AcquireChannel(0), MemorySlot(0)) - builder.acquire(100, AcquireChannel(1), RegisterSlot(1)) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.acquire(100, AcquireChannel(0), MemorySlot(0)) + builder.acquire(100, AcquireChannel(1), RegisterSlot(1)) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) @data( (0, Parameter("dur"), 0.1, 40), @@ -170,24 +195,28 @@ def test_measure(self): @unpack def test_parameterized(self, channel, *params): """Test playing parameterized pulse.""" - with builder.build() as test_sched: - builder.play(Gaussian(*params), DriveChannel(channel)) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.play(Gaussian(*params), DriveChannel(channel)) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_nested_blocks(self): """Test nested blocks with different alignment contexts.""" - with builder.build() as test_sched: - with builder.align_equispaced(duration=1200): - with builder.align_left(): - builder.delay(100, DriveChannel(0)) - builder.delay(200, DriveChannel(1)) - with builder.align_right(): - builder.delay(100, DriveChannel(0)) - builder.delay(200, DriveChannel(1)) - with builder.align_sequential(): - builder.delay(100, DriveChannel(0)) - builder.delay(200, DriveChannel(1)) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + with builder.align_equispaced(duration=1200): + with builder.align_left(): + builder.delay(100, DriveChannel(0)) + builder.delay(200, DriveChannel(1)) + with builder.align_right(): + builder.delay(100, DriveChannel(0)) + builder.delay(200, DriveChannel(1)) + with builder.align_sequential(): + builder.delay(100, DriveChannel(0)) + builder.delay(200, DriveChannel(1)) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_called_schedule(self): """Test referenced pulse Schedule object. @@ -195,126 +224,146 @@ def test_called_schedule(self): Referenced object is naively converted into ScheduleBlock with TimeBlockade instructions. Thus referenced Schedule is still QPY compatible. """ - refsched = Schedule() - refsched.insert(20, Play(Constant(100, 0.1), DriveChannel(0))) - refsched.insert(50, Play(Constant(100, 0.1), DriveChannel(1))) + with self.assertWarns(DeprecationWarning): + refsched = Schedule() + refsched.insert(20, Play(Constant(100, 0.1), DriveChannel(0))) + refsched.insert(50, Play(Constant(100, 0.1), DriveChannel(1))) - with builder.build() as test_sched: - builder.call(refsched, name="test_ref") - self.assert_roundtrip_equal(test_sched) + with builder.build() as test_sched: + builder.call(refsched, name="test_ref") + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_unassigned_reference(self): """Test schedule with unassigned reference.""" - with builder.build() as test_sched: - builder.reference("custom1", "q0") - builder.reference("custom1", "q1") + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.reference("custom1", "q0") + builder.reference("custom1", "q1") - self.assert_roundtrip_equal(test_sched) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_partly_assigned_reference(self): """Test schedule with partly assigned reference.""" - with builder.build() as test_sched: - builder.reference("custom1", "q0") - builder.reference("custom1", "q1") + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.reference("custom1", "q0") + builder.reference("custom1", "q1") - with builder.build() as sub_q0: - builder.delay(Parameter("duration"), DriveChannel(0)) + with builder.build() as sub_q0: + builder.delay(Parameter("duration"), DriveChannel(0)) test_sched.assign_references( {("custom1", "q0"): sub_q0}, inplace=True, ) - self.assert_roundtrip_equal(test_sched) + with warnings.catch_warnings(): + warnings.simplefilter(action="ignore", category=DeprecationWarning) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_nested_assigned_reference(self): """Test schedule with assigned reference for nested schedule.""" - with builder.build() as test_sched: - with builder.align_left(): - builder.reference("custom1", "q0") - builder.reference("custom1", "q1") + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + with builder.align_left(): + builder.reference("custom1", "q0") + builder.reference("custom1", "q1") - with builder.build() as sub_q0: - builder.delay(Parameter("duration"), DriveChannel(0)) + with builder.build() as sub_q0: + builder.delay(Parameter("duration"), DriveChannel(0)) - with builder.build() as sub_q1: - builder.delay(Parameter("duration"), DriveChannel(1)) + with builder.build() as sub_q1: + builder.delay(Parameter("duration"), DriveChannel(1)) test_sched.assign_references( {("custom1", "q0"): sub_q0, ("custom1", "q1"): sub_q1}, inplace=True, ) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_bell_schedule(self): """Test complex schedule to create a Bell state.""" - with builder.build() as test_sched: - with builder.align_sequential(): - # H - builder.shift_phase(-1.57, DriveChannel(0)) - builder.play(Drag(160, 0.05, 40, 1.3), DriveChannel(0)) - builder.shift_phase(-1.57, DriveChannel(0)) - # ECR - with builder.align_left(): - builder.play(GaussianSquare(800, 0.05, 64, 544), DriveChannel(1)) - builder.play(GaussianSquare(800, 0.22, 64, 544, 2), ControlChannel(0)) - builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) - with builder.align_left(): - builder.play(GaussianSquare(800, -0.05, 64, 544), DriveChannel(1)) - builder.play(GaussianSquare(800, -0.22, 64, 544, 2), ControlChannel(0)) - builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) - # Measure - with builder.align_left(): - builder.play(GaussianSquare(8000, 0.2, 64, 7744), MeasureChannel(0)) - builder.acquire(8000, AcquireChannel(0), MemorySlot(0)) - - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + with builder.align_sequential(): + # H + builder.shift_phase(-1.57, DriveChannel(0)) + builder.play(Drag(160, 0.05, 40, 1.3), DriveChannel(0)) + builder.shift_phase(-1.57, DriveChannel(0)) + # ECR + with builder.align_left(): + builder.play(GaussianSquare(800, 0.05, 64, 544), DriveChannel(1)) + builder.play(GaussianSquare(800, 0.22, 64, 544, 2), ControlChannel(0)) + builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) + with builder.align_left(): + builder.play(GaussianSquare(800, -0.05, 64, 544), DriveChannel(1)) + builder.play(GaussianSquare(800, -0.22, 64, 544, 2), ControlChannel(0)) + builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) + # Measure + with builder.align_left(): + builder.play(GaussianSquare(8000, 0.2, 64, 7744), MeasureChannel(0)) + builder.acquire(8000, AcquireChannel(0), MemorySlot(0)) + + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) @unittest.skipUnless(_optional.HAS_SYMENGINE, "Symengine required for this test") def test_bell_schedule_use_symengine(self): """Test complex schedule to create a Bell state.""" - with builder.build() as test_sched: - with builder.align_sequential(): - # H - builder.shift_phase(-1.57, DriveChannel(0)) - builder.play(Drag(160, 0.05, 40, 1.3), DriveChannel(0)) - builder.shift_phase(-1.57, DriveChannel(0)) - # ECR - with builder.align_left(): - builder.play(GaussianSquare(800, 0.05, 64, 544), DriveChannel(1)) - builder.play(GaussianSquare(800, 0.22, 64, 544, 2), ControlChannel(0)) - builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) - with builder.align_left(): - builder.play(GaussianSquare(800, -0.05, 64, 544), DriveChannel(1)) - builder.play(GaussianSquare(800, -0.22, 64, 544, 2), ControlChannel(0)) - builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) - # Measure - with builder.align_left(): - builder.play(GaussianSquare(8000, 0.2, 64, 7744), MeasureChannel(0)) - builder.acquire(8000, AcquireChannel(0), MemorySlot(0)) - - self.assert_roundtrip_equal(test_sched, True) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + with builder.align_sequential(): + # H + builder.shift_phase(-1.57, DriveChannel(0)) + builder.play(Drag(160, 0.05, 40, 1.3), DriveChannel(0)) + builder.shift_phase(-1.57, DriveChannel(0)) + # ECR + with builder.align_left(): + builder.play(GaussianSquare(800, 0.05, 64, 544), DriveChannel(1)) + builder.play(GaussianSquare(800, 0.22, 64, 544, 2), ControlChannel(0)) + builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) + with builder.align_left(): + builder.play(GaussianSquare(800, -0.05, 64, 544), DriveChannel(1)) + builder.play(GaussianSquare(800, -0.22, 64, 544, 2), ControlChannel(0)) + builder.play(Drag(160, 0.1, 40, 1.5), DriveChannel(0)) + # Measure + with builder.align_left(): + builder.play(GaussianSquare(8000, 0.2, 64, 7744), MeasureChannel(0)) + builder.acquire(8000, AcquireChannel(0), MemorySlot(0)) + + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched, True) def test_with_acquire_instruction_with_kernel(self): """Test a schedblk with acquire instruction with kernel.""" kernel = Kernel( name="my_kernel", kernel={"real": np.ones(10), "imag": np.zeros(10)}, bias=[0, 0] ) - with builder.build() as test_sched: - builder.acquire(100, AcquireChannel(0), MemorySlot(0), kernel=kernel) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.acquire(100, AcquireChannel(0), MemorySlot(0), kernel=kernel) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) def test_with_acquire_instruction_with_discriminator(self): """Test a schedblk with acquire instruction with a discriminator.""" discriminator = Discriminator( name="my_discriminator", discriminator_type="linear", params=[1, 0] ) - with builder.build() as test_sched: - builder.acquire(100, AcquireChannel(0), MemorySlot(0), discriminator=discriminator) + with self.assertWarns(DeprecationWarning): + with builder.build() as test_sched: + builder.acquire(100, AcquireChannel(0), MemorySlot(0), discriminator=discriminator) - self.assert_roundtrip_equal(test_sched) + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + self.assert_roundtrip_equal(test_sched) class TestPulseGate(QpyScheduleTestCase): @@ -324,12 +373,14 @@ def test_1q_gate(self): """Test for single qubit pulse gate.""" mygate = Gate("mygate", 1, []) - with builder.build() as caldef: - builder.play(Constant(100, 0.1), DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with builder.build() as caldef: + builder.play(Constant(100, 0.1), DriveChannel(0)) qc = QuantumCircuit(2) qc.append(mygate, [0]) - qc.add_calibration(mygate, (0,), caldef) + with self.assertWarns(DeprecationWarning): + qc.add_calibration(mygate, (0,), caldef) self.assert_roundtrip_equal(qc) @@ -337,12 +388,14 @@ def test_2q_gate(self): """Test for two qubit pulse gate.""" mygate = Gate("mygate", 2, []) - with builder.build() as caldef: - builder.play(Constant(100, 0.1), ControlChannel(0)) + with self.assertWarns(DeprecationWarning): + with builder.build() as caldef: + builder.play(Constant(100, 0.1), ControlChannel(0)) qc = QuantumCircuit(2) qc.append(mygate, [0, 1]) - qc.add_calibration(mygate, (0, 1), caldef) + with self.assertWarns(DeprecationWarning): + qc.add_calibration(mygate, (0, 1), caldef) self.assert_roundtrip_equal(qc) @@ -352,12 +405,14 @@ def test_parameterized_gate(self): angle = Parameter("angle") mygate = Gate("mygate", 2, [amp, angle]) - with builder.build() as caldef: - builder.play(Constant(100, amp * np.exp(1j * angle)), ControlChannel(0)) + with self.assertWarns(DeprecationWarning): + with builder.build() as caldef: + builder.play(Constant(100, amp * np.exp(1j * angle)), ControlChannel(0)) qc = QuantumCircuit(2) qc.append(mygate, [0, 1]) - qc.add_calibration(mygate, (0, 1), caldef) + with self.assertWarns(DeprecationWarning): + qc.add_calibration(mygate, (0, 1), caldef) self.assert_roundtrip_equal(qc) @@ -365,12 +420,14 @@ def test_override(self): """Test for overriding standard gate with pulse gate.""" amp = Parameter("amp") - with builder.build() as caldef: - builder.play(Constant(100, amp), ControlChannel(0)) + with self.assertWarns(DeprecationWarning): + with builder.build() as caldef: + builder.play(Constant(100, amp), ControlChannel(0)) qc = QuantumCircuit(2) qc.rx(amp, 0) - qc.add_calibration("rx", (0,), caldef, [amp]) + with self.assertWarns(DeprecationWarning): + qc.add_calibration("rx", (0,), caldef, [amp]) self.assert_roundtrip_equal(qc) @@ -380,17 +437,19 @@ def test_multiple_calibrations(self): amp2 = Parameter("amp2") mygate = Gate("mygate", 1, [amp2]) - with builder.build() as caldef1: - builder.play(Constant(100, amp1), DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with builder.build() as caldef1: + builder.play(Constant(100, amp1), DriveChannel(0)) - with builder.build() as caldef2: - builder.play(Constant(100, amp2), DriveChannel(1)) + with builder.build() as caldef2: + builder.play(Constant(100, amp2), DriveChannel(1)) qc = QuantumCircuit(2) qc.rx(amp1, 0) qc.append(mygate, [1]) - qc.add_calibration("rx", (0,), caldef1, [amp1]) - qc.add_calibration(mygate, (1,), caldef2) + with self.assertWarns(DeprecationWarning): + qc.add_calibration("rx", (0,), caldef1, [amp1]) + qc.add_calibration(mygate, (1,), caldef2) self.assert_roundtrip_equal(qc) @@ -400,12 +459,14 @@ def test_with_acquire_instruction_with_kernel(self): name="my_kernel", kernel={"real": np.zeros(10), "imag": np.zeros(10)}, bias=[0, 0] ) - with builder.build() as sched: - builder.acquire(10, AcquireChannel(0), MemorySlot(0), kernel=kernel) + with self.assertWarns(DeprecationWarning): + with builder.build() as sched: + builder.acquire(10, AcquireChannel(0), MemorySlot(0), kernel=kernel) qc = QuantumCircuit(1, 1) qc.measure(0, 0) - qc.add_calibration("measure", (0,), sched) + with self.assertWarns(DeprecationWarning): + qc.add_calibration("measure", (0,), sched) self.assert_roundtrip_equal(qc) @@ -413,12 +474,14 @@ def test_with_acquire_instruction_with_discriminator(self): """Test a pulse gate with acquire instruction with discriminator.""" discriminator = Discriminator("my_discriminator") - with builder.build() as sched: - builder.acquire(10, AcquireChannel(0), MemorySlot(0), discriminator=discriminator) + with self.assertWarns(DeprecationWarning): + with builder.build() as sched: + builder.acquire(10, AcquireChannel(0), MemorySlot(0), discriminator=discriminator) qc = QuantumCircuit(1, 1) qc.measure(0, 0) - qc.add_calibration("measure", (0,), sched) + with self.assertWarns(DeprecationWarning): + qc.add_calibration("measure", (0,), sched) self.assert_roundtrip_equal(qc) @@ -433,15 +496,16 @@ def setUp(self): t, amp, freq = sym.symbols("t, amp, freq") sym_envelope = 2 * amp * (freq * t - sym.floor(1 / 2 + freq * t)) - my_pulse = SymbolicPulse( - pulse_type="Sawtooth", - duration=100, - parameters={"amp": 0.1, "freq": 0.05}, - envelope=sym_envelope, - name="pulse1", - ) - with builder.build() as test_sched: - builder.play(my_pulse, DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + my_pulse = SymbolicPulse( + pulse_type="Sawtooth", + duration=100, + parameters={"amp": 0.1, "freq": 0.05}, + envelope=sym_envelope, + name="pulse1", + ) + with builder.build() as test_sched: + builder.play(my_pulse, DriveChannel(0)) self.test_sched = test_sched @@ -449,7 +513,9 @@ def setUp(self): def test_symengine_full_path(self): """Test use_symengine option for circuit with parameter expressions.""" qpy_file = io.BytesIO() - dump(self.test_sched, qpy_file, use_symengine=True) + with self.assertWarns(DeprecationWarning): + dump(self.test_sched, qpy_file, use_symengine=True) qpy_file.seek(0) - new_sched = load(qpy_file)[0] + with self.assertWarns(QPYLoadingDeprecatedFeatureWarning): + new_sched = load(qpy_file)[0] self.assertEqual(self.test_sched, new_sched) diff --git a/test/python/qpy/test_circuit_load_from_qpy.py b/test/python/qpy/test_circuit_load_from_qpy.py index 8f0cffb36a5f..e909f7ced455 100644 --- a/test/python/qpy/test_circuit_load_from_qpy.py +++ b/test/python/qpy/test_circuit_load_from_qpy.py @@ -69,24 +69,26 @@ def setUp(self): @data(0.1, 0.7, 1.5) def test_rzx_calibration(self, angle): """RZX builder calibration pass with echo.""" - pass_ = passes.RZXCalibrationBuilder(self.inst_map) + with self.assertWarns(DeprecationWarning): + pass_ = passes.RZXCalibrationBuilder(self.inst_map) pass_manager = PassManager(pass_) test_qc = QuantumCircuit(2) test_qc.rzx(angle, 0, 1) rzx_qc = pass_manager.run(test_qc) - - self.assert_roundtrip_equal(rzx_qc) + with self.assertWarns(DeprecationWarning): + self.assert_roundtrip_equal(rzx_qc) @data(0.1, 0.7, 1.5) def test_rzx_calibration_echo(self, angle): """RZX builder calibration pass without echo.""" - pass_ = passes.RZXCalibrationBuilderNoEcho(self.inst_map) + with self.assertWarns(DeprecationWarning): + pass_ = passes.RZXCalibrationBuilderNoEcho(self.inst_map) pass_manager = PassManager(pass_) test_qc = QuantumCircuit(2) test_qc.rzx(angle, 0, 1) rzx_qc = pass_manager.run(test_qc) - - self.assert_roundtrip_equal(rzx_qc) + with self.assertWarns(DeprecationWarning): + self.assert_roundtrip_equal(rzx_qc) class TestVersions(QpyCircuitTestCase): diff --git a/test/python/quantum_info/operators/channel/test_kraus.py b/test/python/quantum_info/operators/channel/test_kraus.py index 5d50ee9b4759..3b75b2dd614b 100644 --- a/test/python/quantum_info/operators/channel/test_kraus.py +++ b/test/python/quantum_info/operators/channel/test_kraus.py @@ -19,7 +19,7 @@ from qiskit import QiskitError from qiskit.quantum_info.states import DensityMatrix -from qiskit.quantum_info import Kraus +from qiskit.quantum_info import Kraus, Operator from .channel_test_case import ChannelTestCase @@ -68,7 +68,14 @@ def test_circuit_init(self): circuit, target = self.simple_circuit_no_measure() op = Kraus(circuit) target = Kraus(target) - self.assertEqual(op, target) + # The given separable circuit should only have a single Kraus operator. + self.assertEqual(len(op.data), 1) + self.assertEqual(len(target.data), 1) + kraus_op = Operator(op.data[0]) + kraus_target = Operator(target.data[0]) + # THe Kraus representation is not unique, but for a single operator, the only gauge freedom + # is the global phase. + self.assertTrue(kraus_op.equiv(kraus_target)) def test_circuit_init_except(self): """Test initialization from circuit with measure raises exception.""" diff --git a/test/python/quantum_info/operators/channel/test_stinespring.py b/test/python/quantum_info/operators/channel/test_stinespring.py index 9bcc886a026c..693e85d7c1cc 100644 --- a/test/python/quantum_info/operators/channel/test_stinespring.py +++ b/test/python/quantum_info/operators/channel/test_stinespring.py @@ -19,7 +19,7 @@ from qiskit import QiskitError from qiskit.quantum_info.states import DensityMatrix -from qiskit.quantum_info import Stinespring +from qiskit.quantum_info import Stinespring, Operator from .channel_test_case import ChannelTestCase @@ -61,7 +61,10 @@ def test_circuit_init(self): circuit, target = self.simple_circuit_no_measure() op = Stinespring(circuit) target = Stinespring(target) - self.assertEqual(op, target) + # If the Stinespring is CPTP (and it should be), it's defined in terms of a single + # rectangular operator, which has global-phase gauge freedom. + self.assertTrue(op.is_cptp()) + self.assertTrue(Operator(op.data).equiv(Operator(target.data))) def test_circuit_init_except(self): """Test initialization from circuit with measure raises exception.""" diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index dedd84279a8d..65f19eb8e44c 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -271,7 +271,7 @@ def test_to_matrix_zero(self): zero_sparse = zero.to_matrix(sparse=True) self.assertIsInstance(zero_sparse, scipy.sparse.csr_matrix) - np.testing.assert_array_equal(zero_sparse.A, zero_numpy) + np.testing.assert_array_equal(zero_sparse.todense(), zero_numpy) def test_to_matrix_parallel_vs_serial(self): """Parallel execution should produce the same results as serial execution up to diff --git a/test/python/quantum_info/operators/test_operator.py b/test/python/quantum_info/operators/test_operator.py index 725c46576a9d..d9423d0ec141 100644 --- a/test/python/quantum_info/operators/test_operator.py +++ b/test/python/quantum_info/operators/test_operator.py @@ -20,12 +20,14 @@ from test import combine import numpy as np -from ddt import ddt +import ddt from numpy.testing import assert_allclose -import scipy.linalg as la +import scipy.stats +import scipy.linalg from qiskit import QiskitError from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit +from qiskit.circuit import library from qiskit.circuit.library import HGate, CHGate, CXGate, QFT from qiskit.transpiler import CouplingMap from qiskit.transpiler.layout import Layout, TranspileLayout @@ -97,7 +99,7 @@ def simple_circuit_with_measure(self): return circ -@ddt +@ddt.ddt class TestOperator(OperatorTestCase): """Tests for Operator linear operator class.""" @@ -290,7 +292,7 @@ def test_copy(self): def test_is_unitary(self): """Test is_unitary method.""" # X-90 rotation - X90 = la.expm(-1j * 0.5 * np.pi * np.array([[0, 1], [1, 0]]) / 2) + X90 = scipy.linalg.expm(-1j * 0.5 * np.pi * np.array([[0, 1], [1, 0]]) / 2) self.assertTrue(Operator(X90).is_unitary()) # Non-unitary should return false self.assertFalse(Operator([[1, 0], [0, 0]]).is_unitary()) @@ -495,7 +497,7 @@ def test_compose_front_subsystem(self): def test_power(self): """Test power method.""" - X90 = la.expm(-1j * 0.5 * np.pi * np.array([[0, 1], [1, 0]]) / 2) + X90 = scipy.linalg.expm(-1j * 0.5 * np.pi * np.array([[0, 1], [1, 0]]) / 2) op = Operator(X90) self.assertEqual(op.power(2), Operator([[0, -1j], [-1j, 0]])) self.assertEqual(op.power(4), Operator(-1 * np.eye(2))) @@ -513,6 +515,43 @@ def test_floating_point_power(self): self.assertEqual(op.power(0.25), expected_op) + def test_power_of_nonunitary(self): + """Test power method for a non-unitary matrix.""" + data = [[1, 1], [0, -1]] + powered = Operator(data).power(0.5) + expected = Operator([[1.0 + 0.0j, 0.5 - 0.5j], [0.0 + 0.0j, 0.0 + 1.0j]]) + assert_allclose(powered.data, expected.data) + + @ddt.data(0.5, 1.0 / 3.0, 0.25) + def test_root_stability(self, root): + """Test that the root of operators that have eigenvalues that are -1 up to floating-point + imprecision stably choose the positive side of the principal-root branch cut.""" + rng = np.random.default_rng(2024_10_22) + + eigenvalues = np.array([1.0, -1.0], dtype=complex) + # We have the eigenvalues exactly, so this will safely find the principal root. + root_eigenvalues = eigenvalues**root + + # Now, we can construct a bunch of Haar-random unitaries with our chosen eigenvalues. Since + # we already know their eigenvalue decompositions exactly (up to floating-point precision in + # the matrix multiplications), we can also compute the principal values of the roots of the + # complete matrices. + bases = scipy.stats.unitary_group.rvs(2, size=16, random_state=rng) + matrices = [basis.conj().T @ np.diag(eigenvalues) @ basis for basis in bases] + expected = [basis.conj().T @ np.diag(root_eigenvalues) @ basis for basis in bases] + self.assertEqual( + [Operator(matrix).power(root) for matrix in matrices], + [Operator(single) for single in expected], + ) + + def test_root_branch_cut(self): + """Test that we can choose where the branch cut appears in the root.""" + z_op = Operator(library.ZGate()) + # Depending on the direction we move the branch cut, we should be able to select the root to + # be either of the two valid options. + self.assertEqual(z_op.power(0.5, branch_cut_rotation=1e-3), Operator(library.SGate())) + self.assertEqual(z_op.power(0.5, branch_cut_rotation=-1e-3), Operator(library.SdgGate())) + def test_expand(self): """Test expand method.""" mat1 = self.UX diff --git a/test/python/quantum_info/test_sparse_observable.py b/test/python/quantum_info/test_sparse_observable.py new file mode 100644 index 000000000000..551ea414998e --- /dev/null +++ b/test/python/quantum_info/test_sparse_observable.py @@ -0,0 +1,1535 @@ +# 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. + +# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring + +import copy +import pickle +import unittest + +import ddt +import numpy as np + +from qiskit.circuit import Parameter +from qiskit.exceptions import QiskitError +from qiskit.quantum_info import SparseObservable, SparsePauliOp, Pauli + +from test import QiskitTestCase, combine # pylint: disable=wrong-import-order + + +def single_cases(): + return [ + SparseObservable.zero(0), + SparseObservable.zero(10), + SparseObservable.identity(0), + SparseObservable.identity(1_000), + SparseObservable.from_label("IIXIZI"), + SparseObservable.from_list([("YIXZII", -0.25), ("01rl+-", 0.25 + 0.5j)]), + # Includes a duplicate entry. + SparseObservable.from_list([("IXZ", -0.25), ("01I", 0.25 + 0.5j), ("IXZ", 0.75)]), + ] + + +class AllowRightArithmetic: + """Some type that implements only the right-hand-sided arithmatic operations, and allows + `SparseObservable` to pass through them. + + The purpose of this is to detect that `SparseObservable` is correctly delegating binary + operators to the other type if given an object it cannot coerce because of its type.""" + + SENTINEL = object() + + __radd__ = __rsub__ = __rmul__ = __rtruediv__ = __rxor__ = lambda self, other: self.SENTINEL + + +@ddt.ddt +class TestSparseObservable(QiskitTestCase): + def test_default_constructor_pauli(self): + data = Pauli("IXYIZ") + self.assertEqual(SparseObservable(data), SparseObservable.from_pauli(data)) + self.assertEqual( + SparseObservable(data, num_qubits=data.num_qubits), SparseObservable.from_pauli(data) + ) + with self.assertRaisesRegex(ValueError, "explicitly given 'num_qubits'"): + SparseObservable(data, num_qubits=data.num_qubits + 1) + + with_phase = Pauli("-jIYYXY") + self.assertEqual(SparseObservable(with_phase), SparseObservable.from_pauli(with_phase)) + self.assertEqual( + SparseObservable(with_phase, num_qubits=data.num_qubits), + SparseObservable.from_pauli(with_phase), + ) + + self.assertEqual(SparseObservable(Pauli("")), SparseObservable.from_pauli(Pauli(""))) + + def test_default_constructor_sparse_pauli_op(self): + data = SparsePauliOp.from_list([("IIXIY", 1.0), ("XYYZI", -0.25), ("XYIYY", -0.25 + 0.75j)]) + self.assertEqual(SparseObservable(data), SparseObservable.from_sparse_pauli_op(data)) + self.assertEqual( + SparseObservable(data, num_qubits=data.num_qubits), + SparseObservable.from_sparse_pauli_op(data), + ) + with self.assertRaisesRegex(ValueError, "explicitly given 'num_qubits'"): + SparseObservable(data, num_qubits=data.num_qubits + 1) + with self.assertRaisesRegex(TypeError, "complex-typed coefficients"): + SparseObservable(SparsePauliOp(["XX"], [Parameter("x")])) + + def test_default_constructor_label(self): + data = "IIXI+-I01rlIYZ" + self.assertEqual(SparseObservable(data), SparseObservable.from_label(data)) + self.assertEqual( + SparseObservable(data, num_qubits=len(data)), SparseObservable.from_label(data) + ) + with self.assertRaisesRegex(ValueError, "explicitly given 'num_qubits'"): + SparseObservable(data, num_qubits=len(data) + 1) + + def test_default_constructor_list(self): + data = [("IXIIZ", 0.5), ("+I-II", 1.0 - 0.25j), ("IIrlI", -0.75)] + self.assertEqual(SparseObservable(data), SparseObservable.from_list(data)) + self.assertEqual(SparseObservable(data, num_qubits=5), SparseObservable.from_list(data)) + with self.assertRaisesRegex(ValueError, "label with length 5 cannot be added"): + SparseObservable(data, num_qubits=4) + with self.assertRaisesRegex(ValueError, "label with length 5 cannot be added"): + SparseObservable(data, num_qubits=6) + self.assertEqual( + SparseObservable([], num_qubits=5), SparseObservable.from_list([], num_qubits=5) + ) + + def test_default_constructor_sparse_list(self): + data = [("ZX", (0, 3), 0.5), ("-+", (2, 4), 1.0 - 0.25j), ("rl", (2, 1), -0.75)] + self.assertEqual( + SparseObservable(data, num_qubits=5), + SparseObservable.from_sparse_list(data, num_qubits=5), + ) + self.assertEqual( + SparseObservable(data, num_qubits=10), + SparseObservable.from_sparse_list(data, num_qubits=10), + ) + with self.assertRaisesRegex(ValueError, "'num_qubits' must be provided"): + SparseObservable(data) + self.assertEqual( + SparseObservable([], num_qubits=5), SparseObservable.from_sparse_list([], num_qubits=5) + ) + + def test_default_constructor_copy(self): + base = SparseObservable.from_list([("IXIZIY", 1.0), ("+-rl01", -1.0j)]) + copied = SparseObservable(base) + self.assertEqual(base, copied) + self.assertIsNot(base, copied) + + # Modifications to `copied` don't propagate back. + copied.coeffs[1] = -0.5j + self.assertNotEqual(base, copied) + + with self.assertRaisesRegex(ValueError, "explicitly given 'num_qubits'"): + SparseObservable(base, num_qubits=base.num_qubits + 1) + + def test_default_constructor_failed_inference(self): + with self.assertRaises(TypeError): + # Mixed dense/sparse list. + SparseObservable([("IIXIZ", 1.0), ("+-", (2, 3), -1.0)], num_qubits=5) + + def test_num_qubits(self): + self.assertEqual(SparseObservable.zero(0).num_qubits, 0) + self.assertEqual(SparseObservable.zero(10).num_qubits, 10) + + self.assertEqual(SparseObservable.identity(0).num_qubits, 0) + self.assertEqual(SparseObservable.identity(1_000_000).num_qubits, 1_000_000) + + def test_num_terms(self): + self.assertEqual(SparseObservable.zero(0).num_terms, 0) + self.assertEqual(SparseObservable.zero(10).num_terms, 0) + self.assertEqual(SparseObservable.identity(0).num_terms, 1) + self.assertEqual(SparseObservable.identity(1_000_000).num_terms, 1) + self.assertEqual( + SparseObservable.from_list([("IIIXIZ", 1.0), ("YY+-II", 0.5j)]).num_terms, 2 + ) + + def test_bit_term_enum(self): + # These are very explicit tests that effectively just duplicate magic numbers, but the point + # is that those magic numbers are required to be constant as their values are part of the + # public interface. + + self.assertEqual( + set(SparseObservable.BitTerm), + { + SparseObservable.BitTerm.X, + SparseObservable.BitTerm.Y, + SparseObservable.BitTerm.Z, + SparseObservable.BitTerm.PLUS, + SparseObservable.BitTerm.MINUS, + SparseObservable.BitTerm.RIGHT, + SparseObservable.BitTerm.LEFT, + SparseObservable.BitTerm.ZERO, + SparseObservable.BitTerm.ONE, + }, + ) + # All the enumeration items should also be integers. + self.assertIsInstance(SparseObservable.BitTerm.X, int) + values = { + "X": 0b00_10, + "Y": 0b00_11, + "Z": 0b00_01, + "PLUS": 0b10_10, + "MINUS": 0b01_10, + "RIGHT": 0b10_11, + "LEFT": 0b01_11, + "ZERO": 0b10_01, + "ONE": 0b01_01, + } + self.assertEqual({name: getattr(SparseObservable.BitTerm, name) for name in values}, values) + + # The single-character label aliases can be accessed with index notation. + labels = { + "X": SparseObservable.BitTerm.X, + "Y": SparseObservable.BitTerm.Y, + "Z": SparseObservable.BitTerm.Z, + "+": SparseObservable.BitTerm.PLUS, + "-": SparseObservable.BitTerm.MINUS, + "r": SparseObservable.BitTerm.RIGHT, + "l": SparseObservable.BitTerm.LEFT, + "0": SparseObservable.BitTerm.ZERO, + "1": SparseObservable.BitTerm.ONE, + } + self.assertEqual({label: SparseObservable.BitTerm[label] for label in labels}, labels) + + @ddt.idata(single_cases()) + def test_pickle(self, observable): + self.assertEqual(observable, copy.copy(observable)) + self.assertIsNot(observable, copy.copy(observable)) + self.assertEqual(observable, copy.deepcopy(observable)) + self.assertEqual(observable, pickle.loads(pickle.dumps(observable))) + + @ddt.data( + # This is every combination of (0, 1, many) for (terms, qubits, non-identites per term). + SparseObservable.zero(0), + SparseObservable.zero(1), + SparseObservable.zero(10), + SparseObservable.identity(0), + SparseObservable.identity(1), + SparseObservable.identity(1_000), + SparseObservable.from_label("IIXIZI"), + SparseObservable.from_label("X"), + SparseObservable.from_list([("YIXZII", -0.25), ("01rl+-", 0.25 + 0.5j)]), + ) + def test_repr(self, data): + # The purpose of this is just to test that the `repr` doesn't crash, rather than asserting + # that it has any particular form. + self.assertIsInstance(repr(data), str) + self.assertIn("SparseObservable", repr(data)) + + def test_from_raw_parts(self): + # Happiest path: exactly typed inputs. + num_qubits = 100 + terms = np.full((num_qubits,), SparseObservable.BitTerm.Z, dtype=np.uint8) + indices = np.arange(num_qubits, dtype=np.uint32) + coeffs = np.ones((num_qubits,), dtype=complex) + boundaries = np.arange(num_qubits + 1, dtype=np.uintp) + observable = SparseObservable.from_raw_parts(num_qubits, coeffs, terms, indices, boundaries) + self.assertEqual(observable.num_qubits, num_qubits) + np.testing.assert_equal(observable.bit_terms, terms) + np.testing.assert_equal(observable.indices, indices) + np.testing.assert_equal(observable.coeffs, coeffs) + np.testing.assert_equal(observable.boundaries, boundaries) + + self.assertEqual( + observable, + SparseObservable.from_raw_parts( + num_qubits, coeffs, terms, indices, boundaries, check=False + ), + ) + + # At least the initial implementation of `SparseObservable` requires `from_raw_parts` to be + # a copy constructor in order to allow it to be resized by Rust space. This is checking for + # that, but if the implementation changes, it could potentially be relaxed. + self.assertFalse(np.may_share_memory(observable.coeffs, coeffs)) + + # Conversion from array-likes, including mis-typed but compatible arrays. + observable = SparseObservable.from_raw_parts( + num_qubits, list(coeffs), tuple(terms), observable.indices, boundaries.astype(np.int16) + ) + self.assertEqual(observable.num_qubits, num_qubits) + np.testing.assert_equal(observable.bit_terms, terms) + np.testing.assert_equal(observable.indices, indices) + np.testing.assert_equal(observable.coeffs, coeffs) + np.testing.assert_equal(observable.boundaries, boundaries) + + # Construction of zero operator. + self.assertEqual( + SparseObservable.from_raw_parts(10, [], [], [], [0]), SparseObservable.zero(10) + ) + + # Construction of an operator with an intermediate identity term. For the initial + # constructor tests, it's hard to check anything more than the construction succeeded. + self.assertEqual( + SparseObservable.from_raw_parts( + 10, [1.0j, 0.5, 2.0], [1, 3, 2], [0, 1, 2], [0, 1, 1, 3] + ).num_terms, + # The three are [(1.0j)*(Z_1), 0.5, 2.0*(X_2 Y_1)] + 3, + ) + + def test_from_raw_parts_checks_coherence(self): + with self.assertRaisesRegex(ValueError, "not a valid letter"): + SparseObservable.from_raw_parts(2, [1.0j], [ord("$")], [0], [0, 1]) + with self.assertRaisesRegex(ValueError, r"boundaries.*must be one element longer"): + SparseObservable.from_raw_parts(2, [1.0j], [SparseObservable.BitTerm.Z], [0], [0]) + with self.assertRaisesRegex(ValueError, r"`bit_terms` \(1\) and `indices` \(0\)"): + SparseObservable.from_raw_parts(2, [1.0j], [SparseObservable.BitTerm.Z], [], [0, 1]) + with self.assertRaisesRegex(ValueError, r"`bit_terms` \(0\) and `indices` \(1\)"): + SparseObservable.from_raw_parts(2, [1.0j], [], [1], [0, 1]) + with self.assertRaisesRegex(ValueError, r"the first item of `boundaries` \(1\) must be 0"): + SparseObservable.from_raw_parts(2, [1.0j], [SparseObservable.BitTerm.Z], [0], [1, 1]) + with self.assertRaisesRegex(ValueError, r"the last item of `boundaries` \(2\)"): + SparseObservable.from_raw_parts(2, [1.0j], [1], [0], [0, 2]) + with self.assertRaisesRegex(ValueError, r"the last item of `boundaries` \(1\)"): + SparseObservable.from_raw_parts(2, [1.0j], [1, 2], [0, 1], [0, 1]) + with self.assertRaisesRegex(ValueError, r"all qubit indices must be less than the number"): + SparseObservable.from_raw_parts(4, [1.0j], [1, 2], [0, 4], [0, 2]) + with self.assertRaisesRegex(ValueError, r"all qubit indices must be less than the number"): + SparseObservable.from_raw_parts(4, [1.0j, -0.5j], [1, 2], [0, 4], [0, 1, 2]) + with self.assertRaisesRegex(ValueError, "the values in `boundaries` include backwards"): + SparseObservable.from_raw_parts( + 5, [1.0j, -0.5j, 2.0], [1, 2, 3, 2], [0, 1, 2, 3], [0, 2, 1, 4] + ) + with self.assertRaisesRegex( + ValueError, "the values in `indices` are not term-wise increasing" + ): + SparseObservable.from_raw_parts(4, [1.0j], [1, 2], [1, 0], [0, 2]) + + # There's no test of attempting to pass incoherent data and `check=False` because that + # permits undefined behaviour in Rust (it's unsafe), so all bets would be off. + + def test_from_label(self): + # The label is interpreted like a bitstring, with the right-most item associated with qubit + # 0, and increasing as we move to the left (like `Pauli`, and other bitstring conventions). + self.assertEqual( + # Ruler for counting terms: dcba9876543210 + SparseObservable.from_label("I+-II01IrlIXYZ"), + SparseObservable.from_raw_parts( + 14, + [1.0], + [ + SparseObservable.BitTerm.Z, + SparseObservable.BitTerm.Y, + SparseObservable.BitTerm.X, + SparseObservable.BitTerm.LEFT, + SparseObservable.BitTerm.RIGHT, + SparseObservable.BitTerm.ONE, + SparseObservable.BitTerm.ZERO, + SparseObservable.BitTerm.MINUS, + SparseObservable.BitTerm.PLUS, + ], + [0, 1, 2, 4, 5, 7, 8, 11, 12], + [0, 9], + ), + ) + + self.assertEqual(SparseObservable.from_label("I" * 10), SparseObservable.identity(10)) + + # The empty label case is a 0-qubit identity, since `from_label` always sets a coefficient + # of 1. + self.assertEqual(SparseObservable.from_label(""), SparseObservable.identity(0)) + + def test_from_label_failures(self): + with self.assertRaisesRegex(ValueError, "labels must only contain letters from"): + # Bad letters that are still ASCII. + SparseObservable.from_label("I+-$%I") + with self.assertRaisesRegex(ValueError, "labels must only contain letters from"): + # Unicode shenangigans. + SparseObservable.from_label("🐍") + + def test_from_list(self): + label = "IXYI+-0lr1ZZY" + self.assertEqual( + SparseObservable.from_list([(label, 1.0)]), SparseObservable.from_label(label) + ) + self.assertEqual( + SparseObservable.from_list([(label, 1.0)], num_qubits=len(label)), + SparseObservable.from_label(label), + ) + + self.assertEqual( + SparseObservable.from_list([("IIIXZI", 1.0j), ("+-IIII", -0.5)]), + SparseObservable.from_raw_parts( + 6, + [1.0j, -0.5], + [ + SparseObservable.BitTerm.Z, + SparseObservable.BitTerm.X, + SparseObservable.BitTerm.MINUS, + SparseObservable.BitTerm.PLUS, + ], + [1, 2, 4, 5], + [0, 2, 4], + ), + ) + + self.assertEqual(SparseObservable.from_list([], num_qubits=5), SparseObservable.zero(5)) + self.assertEqual(SparseObservable.from_list([], num_qubits=0), SparseObservable.zero(0)) + + def test_from_list_failures(self): + with self.assertRaisesRegex(ValueError, "labels must only contain letters from"): + # Bad letters that are still ASCII. + SparseObservable.from_list([("XZIIZY", 0.5), ("I+-$%I", 1.0j)]) + with self.assertRaisesRegex(ValueError, "labels must only contain letters from"): + # Unicode shenangigans. + SparseObservable.from_list([("🐍", 0.5)]) + with self.assertRaisesRegex(ValueError, "label with length 4 cannot be added"): + SparseObservable.from_list([("IIZ", 0.5), ("IIXI", 1.0j)]) + with self.assertRaisesRegex(ValueError, "label with length 2 cannot be added"): + SparseObservable.from_list([("IIZ", 0.5), ("II", 1.0j)]) + with self.assertRaisesRegex(ValueError, "label with length 3 cannot be added"): + SparseObservable.from_list([("IIZ", 0.5), ("IXI", 1.0j)], num_qubits=2) + with self.assertRaisesRegex(ValueError, "label with length 3 cannot be added"): + SparseObservable.from_list([("IIZ", 0.5), ("IXI", 1.0j)], num_qubits=4) + with self.assertRaisesRegex(ValueError, "cannot construct.*without knowing `num_qubits`"): + SparseObservable.from_list([]) + + def test_from_sparse_list(self): + self.assertEqual( + SparseObservable.from_sparse_list( + [ + ("XY", (0, 1), 0.5), + ("+-", (1, 3), -0.25j), + ("rl0", (0, 2, 4), 1.0j), + ], + num_qubits=5, + ), + SparseObservable.from_list([("IIIYX", 0.5), ("I-I+I", -0.25j), ("0IlIr", 1.0j)]), + ) + + # The indices should be allowed to be given in unsorted order, but they should be term-wise + # sorted in the output. + from_unsorted = SparseObservable.from_sparse_list( + [ + ("XYZ", (2, 1, 0), 1.5j), + ("+rl", (2, 0, 1), -0.5), + ], + num_qubits=3, + ) + self.assertEqual(from_unsorted, SparseObservable.from_list([("XYZ", 1.5j), ("+lr", -0.5)])) + np.testing.assert_equal( + from_unsorted.indices, np.array([0, 1, 2, 0, 1, 2], dtype=np.uint32) + ) + + # Explicit identities should still work, just be skipped over. + explicit_identity = SparseObservable.from_sparse_list( + [ + ("ZXI", (0, 1, 2), 1.0j), + ("XYIII", (0, 1, 2, 3, 8), -0.5j), + ], + num_qubits=10, + ) + self.assertEqual( + explicit_identity, + SparseObservable.from_sparse_list( + [("XZ", (1, 0), 1.0j), ("YX", (1, 0), -0.5j)], num_qubits=10 + ), + ) + np.testing.assert_equal(explicit_identity.indices, np.array([0, 1, 0, 1], dtype=np.uint32)) + + self.assertEqual( + SparseObservable.from_sparse_list([("", (), 1.0)], num_qubits=5), + SparseObservable.identity(5), + ) + self.assertEqual( + SparseObservable.from_sparse_list([("", (), 1.0)], num_qubits=0), + SparseObservable.identity(0), + ) + + self.assertEqual( + SparseObservable.from_sparse_list([], num_qubits=1_000_000), + SparseObservable.zero(1_000_000), + ) + self.assertEqual( + SparseObservable.from_sparse_list([], num_qubits=0), + SparseObservable.zero(0), + ) + + def test_from_sparse_list_failures(self): + with self.assertRaisesRegex(ValueError, "labels must only contain letters from"): + # Bad letters that are still ASCII. + SparseObservable.from_sparse_list( + [("XZZY", (5, 3, 1, 0), 0.5), ("+$", (2, 1), 1.0j)], num_qubits=8 + ) + # Unicode shenangigans. These two should fail with a `ValueError`, but the exact message + # isn't important. "\xff" is "ÿ", which is two bytes in UTF-8 (so has a length of 2 in + # Rust), but has a length of 1 in Python, so try with both a length-1 and length-2 index + # sequence, and both should still raise `ValueError`. + with self.assertRaises(ValueError): + SparseObservable.from_sparse_list([("\xff", (1,), 0.5)], num_qubits=5) + with self.assertRaises(ValueError): + SparseObservable.from_sparse_list([("\xff", (1, 2), 0.5)], num_qubits=5) + + with self.assertRaisesRegex(ValueError, "label with length 2 does not match indices"): + SparseObservable.from_sparse_list([("XZ", (0,), 1.0)], num_qubits=5) + with self.assertRaisesRegex(ValueError, "label with length 2 does not match indices"): + SparseObservable.from_sparse_list([("XZ", (0, 1, 2), 1.0)], num_qubits=5) + + with self.assertRaisesRegex(ValueError, "index 3 is out of range for a 3-qubit operator"): + SparseObservable.from_sparse_list([("XZY", (0, 1, 3), 1.0)], num_qubits=3) + with self.assertRaisesRegex(ValueError, "index 4 is out of range for a 3-qubit operator"): + SparseObservable.from_sparse_list([("XZY", (0, 1, 4), 1.0)], num_qubits=3) + with self.assertRaisesRegex(ValueError, "index 3 is out of range for a 3-qubit operator"): + # ... even if it's for an explicit identity. + SparseObservable.from_sparse_list([("+-I", (0, 1, 3), 1.0)], num_qubits=3) + + with self.assertRaisesRegex(ValueError, "index 3 is duplicated"): + SparseObservable.from_sparse_list([("XZ", (3, 3), 1.0)], num_qubits=5) + with self.assertRaisesRegex(ValueError, "index 3 is duplicated"): + SparseObservable.from_sparse_list([("XYZXZ", (3, 0, 1, 2, 3), 1.0)], num_qubits=5) + + def test_from_pauli(self): + # This function should be infallible provided `Pauli` doesn't change its interface and the + # user doesn't violate the typing. + + # Simple check that the labels are interpreted in the same order. + self.assertEqual( + SparseObservable.from_pauli(Pauli("IIXZI")), SparseObservable.from_label("IIXZI") + ) + + # `Pauli` accepts a phase in its label, which we can't (because of clashes with the other + # alphabet letters), and we should get that right. + self.assertEqual( + SparseObservable.from_pauli(Pauli("iIXZIX")), + SparseObservable.from_list([("IXZIX", 1.0j)]), + ) + self.assertEqual( + SparseObservable.from_pauli(Pauli("-iIXZIX")), + SparseObservable.from_list([("IXZIX", -1.0j)]), + ) + self.assertEqual( + SparseObservable.from_pauli(Pauli("-IXZIX")), + SparseObservable.from_list([("IXZIX", -1.0)]), + ) + + # `Pauli` has its internal phase convention for how it stores `Y`; we should get this right + # regardless of how many Ys are in the label, or if there's a phase. + paulis = {"IXYZ" * n: Pauli("IXYZ" * n) for n in range(1, 5)} + from_paulis, from_labels = zip( + *( + (SparseObservable.from_pauli(pauli), SparseObservable.from_label(label)) + for label, pauli in paulis.items() + ) + ) + self.assertEqual(from_paulis, from_labels) + + phased_paulis = {"IXYZ" * n: Pauli("j" + "IXYZ" * n) for n in range(1, 5)} + from_paulis, from_lists = zip( + *( + (SparseObservable.from_pauli(pauli), SparseObservable.from_list([(label, 1.0j)])) + for label, pauli in phased_paulis.items() + ) + ) + self.assertEqual(from_paulis, from_lists) + + self.assertEqual(SparseObservable.from_pauli(Pauli("III")), SparseObservable.identity(3)) + self.assertEqual(SparseObservable.from_pauli(Pauli("")), SparseObservable.identity(0)) + + def test_from_sparse_pauli_op(self): + self.assertEqual( + SparseObservable.from_sparse_pauli_op(SparsePauliOp.from_list([("IIIII", 1.0)])), + SparseObservable.identity(5), + ) + + data = [("ZXZXZ", 0.25), ("IYXZI", 1.0j), ("IYYZX", 0.5), ("YYYXI", -0.5), ("IYYYY", 2j)] + self.assertEqual( + SparseObservable.from_sparse_pauli_op(SparsePauliOp.from_list(data)), + SparseObservable.from_list(data), + ) + + # These two _should_ produce the same structure as `SparseObservable.zero(num_qubits)`, but + # because `SparsePauliOp` doesn't represent the zero operator "natively" - with an empty sum + # - they actually come out looking like `0.0` times the identity, which is less efficient + # but acceptable. + self.assertEqual( + SparseObservable.from_sparse_pauli_op(SparsePauliOp.from_list([], num_qubits=1)), + SparseObservable.from_list([("I", 0.0)]), + ) + self.assertEqual( + SparseObservable.from_sparse_pauli_op(SparsePauliOp.from_list([], num_qubits=0)), + SparseObservable.from_list([("", 0.0)]), + ) + + def test_from_sparse_pauli_op_failures(self): + parametric = SparsePauliOp.from_list([("IIXZ", Parameter("x"))], dtype=object) + with self.assertRaisesRegex(TypeError, "complex-typed coefficients"): + SparseObservable.from_sparse_pauli_op(parametric) + + def test_zero(self): + zero_5 = SparseObservable.zero(5) + self.assertEqual(zero_5.num_qubits, 5) + np.testing.assert_equal(zero_5.coeffs, np.array([], dtype=complex)) + np.testing.assert_equal(zero_5.bit_terms, np.array([], dtype=np.uint8)) + np.testing.assert_equal(zero_5.indices, np.array([], dtype=np.uint32)) + np.testing.assert_equal(zero_5.boundaries, np.array([0], dtype=np.uintp)) + + zero_0 = SparseObservable.zero(0) + self.assertEqual(zero_0.num_qubits, 0) + np.testing.assert_equal(zero_0.coeffs, np.array([], dtype=complex)) + np.testing.assert_equal(zero_0.bit_terms, np.array([], dtype=np.uint8)) + np.testing.assert_equal(zero_0.indices, np.array([], dtype=np.uint32)) + np.testing.assert_equal(zero_0.boundaries, np.array([0], dtype=np.uintp)) + + def test_identity(self): + id_5 = SparseObservable.identity(5) + self.assertEqual(id_5.num_qubits, 5) + np.testing.assert_equal(id_5.coeffs, np.array([1], dtype=complex)) + np.testing.assert_equal(id_5.bit_terms, np.array([], dtype=np.uint8)) + np.testing.assert_equal(id_5.indices, np.array([], dtype=np.uint32)) + np.testing.assert_equal(id_5.boundaries, np.array([0, 0], dtype=np.uintp)) + + id_0 = SparseObservable.identity(0) + self.assertEqual(id_0.num_qubits, 0) + np.testing.assert_equal(id_0.coeffs, np.array([1], dtype=complex)) + np.testing.assert_equal(id_0.bit_terms, np.array([], dtype=np.uint8)) + np.testing.assert_equal(id_0.indices, np.array([], dtype=np.uint32)) + np.testing.assert_equal(id_0.boundaries, np.array([0, 0], dtype=np.uintp)) + + @ddt.idata(single_cases()) + def test_copy(self, obs): + self.assertEqual(obs, obs.copy()) + self.assertIsNot(obs, obs.copy()) + + def test_equality(self): + sparse_data = [("XZ", (1, 0), 0.5j), ("+lr", (3, 1, 0), -0.25)] + op = SparseObservable.from_sparse_list(sparse_data, num_qubits=5) + self.assertEqual(op, op.copy()) + # Take care that Rust space allows multiple views onto the same object. + self.assertEqual(op, op) + + # Comparison to some other object shouldn't fail. + self.assertNotEqual(op, None) + + # No costly automatic simplification (mathematically, these operators _are_ the same). + self.assertNotEqual( + SparseObservable.from_list([("+", 1.0), ("-", 1.0)]), SparseObservable.from_label("X") + ) + + # Difference in qubit count. + self.assertNotEqual( + op, SparseObservable.from_sparse_list(sparse_data, num_qubits=op.num_qubits + 1) + ) + self.assertNotEqual(SparseObservable.zero(2), SparseObservable.zero(3)) + self.assertNotEqual(SparseObservable.identity(2), SparseObservable.identity(3)) + + # Difference in coeffs. + self.assertNotEqual( + SparseObservable.from_list([("IIXZI", 1.0), ("+-rl0", -0.5j)]), + SparseObservable.from_list([("IIXZI", 1.0), ("+-rl0", 0.5j)]), + ) + self.assertNotEqual( + SparseObservable.from_list([("IIXZI", 1.0), ("+-rl0", -0.5j)]), + SparseObservable.from_list([("IIXZI", 1.0j), ("+-rl0", -0.5j)]), + ) + + # Difference in bit terms. + self.assertNotEqual( + SparseObservable.from_list([("IIXZI", 1.0), ("+-rl0", -0.5j)]), + SparseObservable.from_list([("IIYZI", 1.0), ("+-rl0", -0.5j)]), + ) + self.assertNotEqual( + SparseObservable.from_list([("IIXZI", 1.0), ("+-rl0", -0.5j)]), + SparseObservable.from_list([("IIXZI", 1.0), ("+-rl1", -0.5j)]), + ) + + # Difference in indices. + self.assertNotEqual( + SparseObservable.from_list([("IIXZI", 1.0), ("+Irl0", -0.5j)]), + SparseObservable.from_list([("IXIZI", 1.0), ("+Irl0", -0.5j)]), + ) + self.assertNotEqual( + SparseObservable.from_list([("IIXZI", 1.0), ("+Irl0", -0.5j)]), + SparseObservable.from_list([("IIXZI", 1.0), ("I+rl0", -0.5j)]), + ) + + # Difference in boundaries. + self.assertNotEqual( + SparseObservable.from_sparse_list( + [("XZ", (0, 1), 1.5), ("+-", (2, 3), -0.5j)], num_qubits=5 + ), + SparseObservable.from_sparse_list( + [("XZ+", (0, 1, 2), 1.5), ("-", (3,), -0.5j)], num_qubits=5 + ), + ) + + def test_write_into_attributes_scalar(self): + coeffs = SparseObservable.from_sparse_list( + [("XZ", (1, 0), 1.5j), ("+-", (3, 2), -1.5j)], num_qubits=8 + ) + coeffs.coeffs[0] = -2.0 + self.assertEqual( + coeffs, + SparseObservable.from_sparse_list( + [("XZ", (1, 0), -2.0), ("+-", (3, 2), -1.5j)], num_qubits=8 + ), + ) + coeffs.coeffs[1] = 1.5 + 0.25j + self.assertEqual( + coeffs, + SparseObservable.from_sparse_list( + [("XZ", (1, 0), -2.0), ("+-", (3, 2), 1.5 + 0.25j)], num_qubits=8 + ), + ) + + bit_terms = SparseObservable.from_sparse_list( + [("XZ", (0, 1), 1.5j), ("+-", (2, 3), -1.5j)], num_qubits=8 + ) + bit_terms.bit_terms[0] = SparseObservable.BitTerm.Y + bit_terms.bit_terms[3] = SparseObservable.BitTerm.LEFT + self.assertEqual( + bit_terms, + SparseObservable.from_sparse_list( + [("YZ", (0, 1), 1.5j), ("+l", (2, 3), -1.5j)], num_qubits=8 + ), + ) + + indices = SparseObservable.from_sparse_list( + [("XZ", (0, 1), 1.5j), ("+-", (2, 3), -1.5j)], num_qubits=8 + ) + # These two sets keep the observable in term-wise increasing order. We don't test what + # happens if somebody violates the Rust-space requirement to be term-wise increasing. + indices.indices[1] = 4 + indices.indices[3] = 7 + self.assertEqual( + indices, + SparseObservable.from_sparse_list( + [("XZ", (0, 4), 1.5j), ("+-", (2, 7), -1.5j)], num_qubits=8 + ), + ) + + boundaries = SparseObservable.from_sparse_list( + [("XZ", (0, 1), 1.5j), ("+-", (2, 3), -1.5j)], num_qubits=8 + ) + # Move a single-qubit term from the second summand into the first (the particular indices + # ensure we remain term-wise sorted). + boundaries.boundaries[1] += 1 + self.assertEqual( + boundaries, + SparseObservable.from_sparse_list( + [("XZ+", (0, 1, 2), 1.5j), ("-", (3,), -1.5j)], num_qubits=8 + ), + ) + + def test_write_into_attributes_broadcast(self): + coeffs = SparseObservable.from_list([("XIIZI", 1.5j), ("IIIl0", -0.25), ("1IIIY", 0.5)]) + coeffs.coeffs[:] = 1.5j + np.testing.assert_array_equal(coeffs.coeffs, [1.5j, 1.5j, 1.5j]) + coeffs.coeffs[1:] = 1.0j + np.testing.assert_array_equal(coeffs.coeffs, [1.5j, 1.0j, 1.0j]) + coeffs.coeffs[:2] = -0.5 + np.testing.assert_array_equal(coeffs.coeffs, [-0.5, -0.5, 1.0j]) + coeffs.coeffs[::2] = 1.5j + np.testing.assert_array_equal(coeffs.coeffs, [1.5j, -0.5, 1.5j]) + coeffs.coeffs[::-1] = -0.5j + np.testing.assert_array_equal(coeffs.coeffs, [-0.5j, -0.5j, -0.5j]) + + # It's hard to broadcast into `indices` without breaking data coherence; the broadcasting is + # more meant for fast modifications to `coeffs` and `bit_terms`. + indices = SparseObservable.from_list([("XIIZI", 1.5j), ("IIlI0", -0.25), ("1IIIY", 0.5)]) + indices.indices[::2] = 1 + self.assertEqual( + indices, SparseObservable.from_list([("XIIZI", 1.5j), ("IIl0I", -0.25), ("1IIYI", 0.5)]) + ) + + bit_terms = SparseObservable.from_list([("XIIZI", 1.5j), ("IIlI0", -0.25), ("1IIIY", 0.5)]) + bit_terms.bit_terms[::2] = SparseObservable.BitTerm.Z + self.assertEqual( + bit_terms, + SparseObservable.from_list([("XIIZI", 1.5j), ("IIlIZ", -0.25), ("1IIIZ", 0.5)]), + ) + bit_terms.bit_terms[3:1:-1] = SparseObservable.BitTerm.PLUS + self.assertEqual( + bit_terms, + SparseObservable.from_list([("XIIZI", 1.5j), ("II+I+", -0.25), ("1IIIZ", 0.5)]), + ) + bit_terms.bit_terms[bit_terms.boundaries[2] : bit_terms.boundaries[3]] = ( + SparseObservable.BitTerm.MINUS + ) + self.assertEqual( + bit_terms, + SparseObservable.from_list([("XIIZI", 1.5j), ("II+I+", -0.25), ("-III-", 0.5)]), + ) + + boundaries = SparseObservable.from_list([("IIIIZX", 1j), ("II+-II", -0.5), ("rlIIII", 0.5)]) + boundaries.boundaries[1:3] = 1 + self.assertEqual( + boundaries, + SparseObservable.from_list([("IIIIIX", 1j), ("IIIIII", -0.5), ("rl+-ZI", 0.5)]), + ) + + def test_write_into_attributes_slice(self): + coeffs = SparseObservable.from_list([("XIIZI", 1.5j), ("IIIl0", -0.25), ("1IIIY", 0.5)]) + coeffs.coeffs[:] = [2.0, 0.5, -0.25] + self.assertEqual( + coeffs, SparseObservable.from_list([("XIIZI", 2.0), ("IIIl0", 0.5), ("1IIIY", -0.25)]) + ) + # This should assign the coefficients in reverse order - we more usually spell it + # `coeffs[:] = coeffs{::-1]`, but the idea is to check the set-item slicing order. + coeffs.coeffs[::-1] = coeffs.coeffs[:] + self.assertEqual( + coeffs, SparseObservable.from_list([("XIIZI", -0.25), ("IIIl0", 0.5), ("1IIIY", 2.0)]) + ) + + indices = SparseObservable.from_list([("IIIIZX", 0.25), ("II+-II", 1j), ("rlIIII", 0.5)]) + indices.indices[:4] = [4, 5, 1, 2] + self.assertEqual( + indices, SparseObservable.from_list([("ZXIIII", 0.25), ("III+-I", 1j), ("rlIIII", 0.5)]) + ) + + bit_terms = SparseObservable.from_list([("IIIIZX", 0.25), ("II+-II", 1j), ("rlIIII", 0.5)]) + bit_terms.bit_terms[::2] = [ + SparseObservable.BitTerm.Y, + SparseObservable.BitTerm.RIGHT, + SparseObservable.BitTerm.ZERO, + ] + self.assertEqual( + bit_terms, + SparseObservable.from_list([("IIIIZY", 0.25), ("II+rII", 1j), ("r0IIII", 0.5)]), + ) + + operators = SparseObservable.from_list([("XZY", 1.5j), ("+1r", -0.5)]) + # Reduce all single-qubit terms to the relevant Pauli operator, if they are a projector. + operators.bit_terms[:] = operators.bit_terms[:] & 0b00_11 + self.assertEqual(operators, SparseObservable.from_list([("XZY", 1.5j), ("XZY", -0.5)])) + + boundaries = SparseObservable.from_list([("IIIIZX", 0.25), ("II+-II", 1j), ("rlIIII", 0.5)]) + boundaries.boundaries[1:-1] = [1, 5] + self.assertEqual( + boundaries, + SparseObservable.from_list([("IIIIIX", 0.25), ("Il+-ZI", 1j), ("rIIIII", 0.5)]), + ) + + def test_attributes_reject_bad_writes(self): + obs = SparseObservable.from_list([("XZY", 1.5j), ("+-r", -0.5)]) + with self.assertRaises(TypeError): + obs.coeffs[0] = [0.25j, 0.5j] + with self.assertRaises(TypeError): + obs.bit_terms[0] = [SparseObservable.BitTerm.PLUS] * 4 + with self.assertRaises(TypeError): + obs.indices[0] = [0, 1] + with self.assertRaises(TypeError): + obs.boundaries[0] = (0, 1) + with self.assertRaisesRegex(ValueError, "not a valid letter"): + obs.bit_terms[0] = 0 + with self.assertRaisesRegex(ValueError, "not a valid letter"): + obs.bit_terms[:] = 0 + with self.assertRaisesRegex( + ValueError, "tried to set a slice of length 2 with a sequence of length 1" + ): + obs.coeffs[:] = [1.0j] + with self.assertRaisesRegex( + ValueError, "tried to set a slice of length 6 with a sequence of length 8" + ): + obs.bit_terms[:] = [SparseObservable.BitTerm.Z] * 8 + + def test_attributes_sequence(self): + """Test attributes of the `Sequence` protocol.""" + # Length + obs = SparseObservable.from_list([("XZY", 1.5j), ("+-r", -0.5)]) + self.assertEqual(len(obs.coeffs), 2) + self.assertEqual(len(obs.indices), 6) + self.assertEqual(len(obs.bit_terms), 6) + self.assertEqual(len(obs.boundaries), 3) + + # Iteration + self.assertEqual(list(obs.coeffs), [1.5j, -0.5]) + self.assertEqual(tuple(obs.indices), (0, 1, 2, 0, 1, 2)) + self.assertEqual(next(iter(obs.boundaries)), 0) + # multiple iteration through same object + bit_terms = obs.bit_terms + self.assertEqual(set(bit_terms), {SparseObservable.BitTerm[x] for x in "XYZ+-r"}) + self.assertEqual(set(bit_terms), {SparseObservable.BitTerm[x] for x in "XYZ+-r"}) + + # Implicit iteration methods. + self.assertIn(SparseObservable.BitTerm.PLUS, obs.bit_terms) + self.assertNotIn(4, obs.indices) + self.assertEqual(list(reversed(obs.coeffs)), [-0.5, 1.5j]) + + # Index by scalar + self.assertEqual(obs.coeffs[1], -0.5) + self.assertEqual(obs.indices[-1], 2) + self.assertEqual(obs.bit_terms[0], SparseObservable.BitTerm.Y) + # Make sure that Rust-space actually returns the enum value, not just an `int` (which could + # have compared equal). + self.assertIsInstance(obs.bit_terms[0], SparseObservable.BitTerm) + self.assertEqual(obs.boundaries[-2], 3) + with self.assertRaises(IndexError): + _ = obs.coeffs[10] + with self.assertRaises(IndexError): + _ = obs.boundaries[-4] + + # Index by slice. This is API guaranteed to be a Numpy array to make it easier to + # manipulate subslices with mathematic operations. + self.assertIsInstance(obs.coeffs[:], np.ndarray) + np.testing.assert_array_equal( + obs.coeffs[:], np.array([1.5j, -0.5], dtype=np.complex128), strict=True + ) + self.assertIsInstance(obs.indices[::-1], np.ndarray) + np.testing.assert_array_equal( + obs.indices[::-1], np.array([2, 1, 0, 2, 1, 0], dtype=np.uint32), strict=True + ) + self.assertIsInstance(obs.bit_terms[2:4], np.ndarray) + np.testing.assert_array_equal( + obs.bit_terms[2:4], + np.array([SparseObservable.BitTerm.X, SparseObservable.BitTerm.RIGHT], dtype=np.uint8), + strict=True, + ) + self.assertIsInstance(obs.boundaries[-2:-3:-1], np.ndarray) + np.testing.assert_array_equal( + obs.boundaries[-2:-3:-1], np.array([3], dtype=np.uintp), strict=True + ) + + def test_attributes_to_array(self): + obs = SparseObservable.from_list([("XZY", 1.5j), ("+-r", -0.5)]) + + # Natural dtypes. + np.testing.assert_array_equal( + obs.coeffs, np.array([1.5j, -0.5], dtype=np.complex128), strict=True + ) + np.testing.assert_array_equal( + obs.indices, np.array([0, 1, 2, 0, 1, 2], dtype=np.uint32), strict=True + ) + np.testing.assert_array_equal( + obs.bit_terms, + np.array([SparseObservable.BitTerm[x] for x in "YZXr-+"], dtype=np.uint8), + strict=True, + ) + np.testing.assert_array_equal( + obs.boundaries, np.array([0, 3, 6], dtype=np.uintp), strict=True + ) + + # Cast dtypes. + np.testing.assert_array_equal( + np.array(obs.indices, dtype=np.uint8), + np.array([0, 1, 2, 0, 1, 2], dtype=np.uint8), + strict=True, + ) + np.testing.assert_array_equal( + np.array(obs.boundaries, dtype=np.int64), + np.array([0, 3, 6], dtype=np.int64), + strict=True, + ) + + @unittest.skipIf( + int(np.__version__.split(".", maxsplit=1)[0]) < 2, + "Numpy 1.x did not have a 'copy' keyword parameter to 'numpy.asarray'", + ) + def test_attributes_reject_no_copy_array(self): + obs = SparseObservable.from_list([("XZY", 1.5j), ("+-r", -0.5)]) + with self.assertRaisesRegex(ValueError, "cannot produce a safe view"): + np.asarray(obs.coeffs, copy=False) + with self.assertRaisesRegex(ValueError, "cannot produce a safe view"): + np.asarray(obs.indices, copy=False) + with self.assertRaisesRegex(ValueError, "cannot produce a safe view"): + np.asarray(obs.bit_terms, copy=False) + with self.assertRaisesRegex(ValueError, "cannot produce a safe view"): + np.asarray(obs.boundaries, copy=False) + + def test_attributes_repr(self): + # We're not testing much about the outputs here, just that they don't crash. + obs = SparseObservable.from_list([("XZY", 1.5j), ("+-r", -0.5)]) + self.assertIn("coeffs", repr(obs.coeffs)) + self.assertIn("bit_terms", repr(obs.bit_terms)) + self.assertIn("indices", repr(obs.indices)) + self.assertIn("boundaries", repr(obs.boundaries)) + + @combine( + obs=single_cases(), + # This includes some elements that aren't native `complex`, but still should be cast. + coeff=[0.5, 3j, 2, 0.25 - 0.75j], + ) + def test_multiply(self, obs, coeff): + obs = obs.copy() + initial = obs.copy() + expected = obs.copy() + expected.coeffs[:] = np.asarray(expected.coeffs) * complex(coeff) + self.assertEqual(obs * coeff, expected) + self.assertEqual(coeff * obs, expected) + # Check that nothing applied in-place. + self.assertEqual(obs, initial) + obs *= coeff + self.assertEqual(obs, expected) + self.assertIs(obs * AllowRightArithmetic(), AllowRightArithmetic.SENTINEL) + + @ddt.idata(single_cases()) + def test_multiply_zero(self, obs): + initial = obs.copy() + self.assertEqual(obs * 0.0, SparseObservable.zero(initial.num_qubits)) + self.assertEqual(0.0 * obs, SparseObservable.zero(initial.num_qubits)) + self.assertEqual(obs, initial) + + obs *= 0.0 + self.assertEqual(obs, SparseObservable.zero(initial.num_qubits)) + + @combine( + obs=single_cases(), + # This includes some elements that aren't native `complex`, but still should be cast. Be + # careful that the floating-point operation should not involve rounding. + coeff=[0.5, 4j, 2, -0.25], + ) + def test_divide(self, obs, coeff): + obs = obs.copy() + initial = obs.copy() + expected = obs.copy() + expected.coeffs[:] = np.asarray(expected.coeffs) / complex(coeff) + self.assertEqual(obs / coeff, expected) + # Check that nothing applied in-place. + self.assertEqual(obs, initial) + obs /= coeff + self.assertEqual(obs, expected) + self.assertIs(obs / AllowRightArithmetic(), AllowRightArithmetic.SENTINEL) + + @ddt.idata(single_cases()) + def test_divide_zero_raises(self, obs): + with self.assertRaises(ZeroDivisionError): + _ = obs / 0.0j + with self.assertRaises(ZeroDivisionError): + obs /= 0.0j + + def test_add_simple(self): + num_qubits = 12 + terms = [ + ("ZXY", (5, 2, 1), 1.5j), + ("+r", (8, 0), -0.25), + ("-0l1", (10, 9, 4, 3), 0.5 + 1j), + ("XZ", (7, 5), 0.75j), + ("rl01", (5, 3, 1, 0), 0.25j), + ] + expected = SparseObservable.from_sparse_list(terms, num_qubits=num_qubits) + for pivot in range(1, len(terms) - 1): + left = SparseObservable.from_sparse_list(terms[:pivot], num_qubits=num_qubits) + left_initial = left.copy() + right = SparseObservable.from_sparse_list(terms[pivot:], num_qubits=num_qubits) + right_initial = right.copy() + # Addition is documented to be term-stacking, so structural equality without `simplify` + # should hold. + self.assertEqual(left + right, expected) + # This is a different order, so check the simplification and canonicalisation works. + self.assertEqual((right + left).simplify(), expected.simplify()) + # Neither was modified in place. + self.assertEqual(left, left_initial) + self.assertEqual(right, right_initial) + + left += right + self.assertEqual(left, expected) + self.assertEqual(right, right_initial) + + @ddt.idata(single_cases()) + def test_add_self(self, obs): + """Test that addition to `self` works fine, including in-place mutation. This is a case + where we might fall afoul of Rust's borrowing rules.""" + initial = obs.copy() + expected = (2.0 * obs).simplify() + self.assertEqual((obs + obs).simplify(), expected) + self.assertEqual(obs, initial) + + obs += obs + self.assertEqual(obs.simplify(), expected) + + @ddt.idata(single_cases()) + def test_add_zero(self, obs): + expected = obs.copy() + zero = SparseObservable.zero(obs.num_qubits) + self.assertEqual(obs + zero, expected) + self.assertEqual(zero + obs, expected) + + obs += zero + self.assertEqual(obs, expected) + zero += obs + self.assertEqual(zero, expected) + + def test_add_coercion(self): + """Other quantum-info operators coerce with the ``+`` operator, so we do too.""" + base = SparseObservable.zero(9) + + pauli_label = "IIIXYZIII" + expected = SparseObservable.from_label(pauli_label) + self.assertEqual(base + pauli_label, expected) + self.assertEqual(pauli_label + base, expected) + + pauli = Pauli(pauli_label) + self.assertEqual(base + pauli, expected) + self.assertEqual(pauli + base, expected) + + spo = SparsePauliOp(pauli_label) + self.assertEqual(base + spo, expected) + with self.assertRaisesRegex(QiskitError, "Invalid input data for Pauli"): + # This doesn't work because `SparsePauliOp` is badly behaved in its coercion (it gets + # first dibs at `__add__`, not our `__radd__`), and will not return `NotImplemented` for + # bad types. This _shouldn't_ raise, and this test here is to remind us to flip it to a + # proper assertion of correctness if `Pauli` starts playing nicely. + _ = spo + base + + obs_label = "10+-rlXYZ" + expected = SparseObservable.from_label(obs_label) + self.assertEqual(base + obs_label, expected) + self.assertEqual(obs_label + base, expected) + + with self.assertRaises(TypeError): + _ = base + {} + with self.assertRaises(TypeError): + _ = {} + base + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = base + "$$$" + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = "$$$" + base + + self.assertIs(base + AllowRightArithmetic(), AllowRightArithmetic.SENTINEL) + with self.assertRaisesRegex(TypeError, "invalid object for in-place addition"): + # This actually _shouldn't_ be a `TypeError` - `__iadd_` should defer to + # `AllowRightArithmetic.__radd__` in the same way that `__add__` does, but a limitation + # in PyO3 (see PyO3/pyo3#4605) prevents this. + base += AllowRightArithmetic() + + def test_add_failures(self): + with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + _ = SparseObservable.zero(4) + SparseObservable.zero(6) + with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + _ = SparseObservable.zero(6) + SparseObservable.zero(4) + + def test_sub_simple(self): + num_qubits = 12 + terms = [ + ("ZXY", (5, 2, 1), 1.5j), + ("+r", (8, 0), -0.25), + ("-0l1", (10, 9, 4, 3), 0.5 + 1j), + ("XZ", (7, 5), 0.75j), + ("rl01", (5, 3, 1, 0), 0.25j), + ] + for pivot in range(1, len(terms) - 1): + expected = SparseObservable.from_sparse_list( + [ + (label, indices, coeff if i < pivot else -coeff) + for i, (label, indices, coeff) in enumerate(terms) + ], + num_qubits=num_qubits, + ) + left = SparseObservable.from_sparse_list(terms[:pivot], num_qubits=num_qubits) + left_initial = left.copy() + right = SparseObservable.from_sparse_list(terms[pivot:], num_qubits=num_qubits) + right_initial = right.copy() + # Addition is documented to be term-stacking, so structural equality without `simplify` + # should hold. + self.assertEqual(left - right, expected) + # This is a different order, so check the simplification and canonicalisation works. + self.assertEqual((right - left).simplify(), -expected.simplify()) + # Neither was modified in place. + self.assertEqual(left, left_initial) + self.assertEqual(right, right_initial) + + left -= right + self.assertEqual(left, expected) + self.assertEqual(right, right_initial) + + @ddt.idata(single_cases()) + def test_sub_self(self, obs): + """Test that subtraction of `self` works fine, including in-place mutation. This is a case + where we might fall afoul of Rust's borrowing rules.""" + initial = obs.copy() + expected = SparseObservable.zero(obs.num_qubits) + self.assertEqual((obs - obs).simplify(), expected) + self.assertEqual(obs, initial) + + obs -= obs + self.assertEqual(obs.simplify(), expected) + + @ddt.idata(single_cases()) + def test_sub_zero(self, obs): + expected = obs.copy() + zero = SparseObservable.zero(obs.num_qubits) + self.assertEqual(obs - zero, expected) + self.assertEqual(zero - obs, -expected) + + obs -= zero + self.assertEqual(obs, expected) + zero -= obs + self.assertEqual(zero, -expected) + + def test_sub_coercion(self): + """Other quantum-info operators coerce with the ``-`` operator, so we do too.""" + base = SparseObservable.zero(9) + + pauli_label = "IIIXYZIII" + expected = SparseObservable.from_label(pauli_label) + self.assertEqual(base - pauli_label, -expected) + self.assertEqual(pauli_label - base, expected) + + pauli = Pauli(pauli_label) + self.assertEqual(base - pauli, -expected) + self.assertEqual(pauli - base, expected) + + spo = SparsePauliOp(pauli_label) + self.assertEqual(base - spo, -expected) + with self.assertRaisesRegex(QiskitError, "Invalid input data for Pauli"): + # This doesn't work because `SparsePauliOp` is badly behaved in its coercion (it gets + # first dibs at `__add__`, not our `__radd__`), and will not return `NotImplemented` for + # bad types. This _shouldn't_ raise, and this test here is to remind us to flip it to a + # proper assertion of correctness if `Pauli` starts playing nicely. + _ = spo + base + + obs_label = "10+-rlXYZ" + expected = SparseObservable.from_label(obs_label) + self.assertEqual(base - obs_label, -expected) + self.assertEqual(obs_label - base, expected) + + with self.assertRaises(TypeError): + _ = base - {} + with self.assertRaises(TypeError): + _ = {} - base + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = base - "$$$" + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = "$$$" - base + + self.assertIs(base + AllowRightArithmetic(), AllowRightArithmetic.SENTINEL) + with self.assertRaisesRegex(TypeError, "invalid object for in-place subtraction"): + # This actually _shouldn't_ be a `TypeError` - `__isub_` should defer to + # `AllowRightArithmetic.__rsub__` in the same way that `__sub__` does, but a limitation + # in PyO3 (see PyO3/pyo3#4605) prevents this. + base -= AllowRightArithmetic() + + def test_sub_failures(self): + with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + _ = SparseObservable.zero(4) - SparseObservable.zero(6) + with self.assertRaisesRegex(ValueError, "incompatible numbers of qubits"): + _ = SparseObservable.zero(6) - SparseObservable.zero(4) + + @ddt.idata(single_cases()) + def test_neg(self, obs): + initial = obs.copy() + expected = obs.copy() + expected.coeffs[:] = -np.asarray(expected.coeffs) + self.assertEqual(-obs, expected) + # Test that there's no in-place modification. + self.assertEqual(obs, initial) + + @ddt.idata(single_cases()) + def test_pos(self, obs): + initial = obs.copy() + self.assertEqual(+obs, initial) + self.assertIsNot(+obs, obs) + + @combine(left=single_cases(), right=single_cases()) + def test_tensor(self, left, right): + + def expected(left, right): + coeffs = [] + bit_terms = [] + indices = [] + boundaries = [0] + for left_ptr in range(left.num_terms): + left_start, left_end = left.boundaries[left_ptr], left.boundaries[left_ptr + 1] + for right_ptr in range(right.num_terms): + right_start = right.boundaries[right_ptr] + right_end = right.boundaries[right_ptr + 1] + coeffs.append(left.coeffs[left_ptr] * right.coeffs[right_ptr]) + bit_terms.extend(right.bit_terms[right_start:right_end]) + bit_terms.extend(left.bit_terms[left_start:left_end]) + indices.extend(right.indices[right_start:right_end]) + indices.extend(i + right.num_qubits for i in left.indices[left_start:left_end]) + boundaries.append(len(indices)) + return SparseObservable.from_raw_parts( + left.num_qubits + right.num_qubits, coeffs, bit_terms, indices, boundaries + ) + + # We deliberately have the arguments flipped when appropriate, here. + # pylint: disable=arguments-out-of-order + + left_initial = left.copy() + right_initial = right.copy() + self.assertEqual(left.tensor(right), expected(left, right)) + self.assertEqual(left, left_initial) + self.assertEqual(right, right_initial) + self.assertEqual(right.tensor(left), expected(right, left)) + + self.assertEqual(left.expand(right), expected(right, left)) + self.assertEqual(left, left_initial) + self.assertEqual(right, right_initial) + self.assertEqual(right.expand(left), expected(left, right)) + + self.assertEqual(left.tensor(right), right.expand(left)) + self.assertEqual(left.expand(right), right.tensor(left)) + + @combine( + obs=single_cases(), identity=[SparseObservable.identity(0), SparseObservable.identity(5)] + ) + def test_tensor_identity(self, obs, identity): + initial = obs.copy() + expected_left = SparseObservable.from_raw_parts( + obs.num_qubits + identity.num_qubits, + obs.coeffs, + obs.bit_terms, + [x + identity.num_qubits for x in obs.indices], + obs.boundaries, + ) + expected_right = SparseObservable.from_raw_parts( + obs.num_qubits + identity.num_qubits, + obs.coeffs, + obs.bit_terms, + obs.indices, + obs.boundaries, + ) + self.assertEqual(obs.tensor(identity), expected_left) + self.assertEqual(identity.tensor(obs), expected_right) + self.assertEqual(obs.expand(identity), expected_right) + self.assertEqual(identity.expand(obs), expected_left) + self.assertEqual(obs ^ identity, expected_left) + self.assertEqual(identity ^ obs, expected_right) + self.assertEqual(obs, initial) + obs ^= identity + self.assertEqual(obs, expected_left) + + @combine(obs=single_cases(), zero=[SparseObservable.zero(0), SparseObservable.zero(5)]) + def test_tensor_zero(self, obs, zero): + initial = obs.copy() + expected = SparseObservable.zero(obs.num_qubits + zero.num_qubits) + self.assertEqual(obs.tensor(zero), expected) + self.assertEqual(zero.tensor(obs), expected) + self.assertEqual(obs.expand(zero), expected) + self.assertEqual(zero.expand(obs), expected) + self.assertEqual(obs ^ zero, expected) + self.assertEqual(zero ^ obs, expected) + self.assertEqual(obs, initial) + obs ^= zero + self.assertEqual(obs, expected) + + def test_tensor_coercion(self): + """Other quantum-info operators coerce with the ``tensor`` method and operator, so we do + too.""" + base = SparseObservable.identity(0) + + pauli_label = "IIXYZII" + expected = SparseObservable.from_label(pauli_label) + self.assertEqual(base.tensor(pauli_label), expected) + self.assertEqual(base.expand(pauli_label), expected) + self.assertEqual(base ^ pauli_label, expected) + self.assertEqual(pauli_label ^ base, expected) + + pauli = Pauli(pauli_label) + self.assertEqual(base.tensor(pauli), expected) + self.assertEqual(base.expand(pauli), expected) + self.assertEqual(base ^ pauli, expected) + with self.assertRaisesRegex(QiskitError, "Invalid input data for Pauli"): + # This doesn't work because `Pauli` is badly behaved in its coercion (it gets first dibs + # at `__xor__`, not our `__rxor__`), and will not return `NotImplemented` for bad types. + # This _shouldn't_ raise, and this test here is to remind us to flip it to a proper + # assertion of correctness if `Pauli` starts playing nicely. + _ = pauli ^ base + + spo = SparsePauliOp(pauli_label) + self.assertEqual(base.tensor(spo), expected) + self.assertEqual(base.expand(spo), expected) + self.assertEqual(base ^ spo, expected) + with self.assertRaisesRegex(QiskitError, "Invalid input data for Pauli"): + # This doesn't work because `SparsePauliOp` is badly behaved in its coercion (it gets + # first dibs at `__xor__`, not our `__rxor__`), and will not return `NotImplemented` for + # bad types. This _shouldn't_ raise, and this test here is to remind us to flip it to a + # proper assertion of correctness if `Pauli` starts playing nicely. + _ = spo ^ base + + obs_label = "10+-rlXYZ" + expected = SparseObservable.from_label(obs_label) + self.assertEqual(base.tensor(obs_label), expected) + self.assertEqual(base.expand(obs_label), expected) + self.assertEqual(base ^ obs_label, expected) + self.assertEqual(obs_label ^ base, expected) + + with self.assertRaises(TypeError): + _ = base ^ {} + with self.assertRaises(TypeError): + _ = {} ^ base + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = base ^ "$$$" + with self.assertRaisesRegex(ValueError, "only contain letters from the alphabet"): + _ = "$$$" ^ base + + self.assertIs(base ^ AllowRightArithmetic(), AllowRightArithmetic.SENTINEL) + + @ddt.idata(single_cases()) + def test_adjoint(self, obs): + initial = obs.copy() + expected = obs.copy() + expected.coeffs[:] = np.conjugate(expected.coeffs) + self.assertEqual(obs.adjoint(), expected) + self.assertEqual(obs, initial) + self.assertEqual(obs.adjoint().adjoint(), initial) + self.assertEqual(obs.adjoint(), obs.conjugate().transpose()) + self.assertEqual(obs.adjoint(), obs.transpose().conjugate()) + + @ddt.idata(single_cases()) + def test_conjugate(self, obs): + initial = obs.copy() + + term_map = {term: (term, 1.0) for term in SparseObservable.BitTerm} + term_map[SparseObservable.BitTerm.Y] = (SparseObservable.BitTerm.Y, -1.0) + term_map[SparseObservable.BitTerm.RIGHT] = (SparseObservable.BitTerm.LEFT, 1.0) + term_map[SparseObservable.BitTerm.LEFT] = (SparseObservable.BitTerm.RIGHT, 1.0) + + expected = obs.copy() + for i in range(expected.num_terms): + start, end = expected.boundaries[i], expected.boundaries[i + 1] + coeff = expected.coeffs[i] + for offset, bit_term in enumerate(expected.bit_terms[start:end]): + new_term, multiplier = term_map[bit_term] + coeff *= multiplier + expected.bit_terms[start + offset] = new_term + expected.coeffs[i] = coeff.conjugate() + + self.assertEqual(obs.conjugate(), expected) + self.assertEqual(obs, initial) + self.assertEqual(obs.conjugate().conjugate(), initial) + self.assertEqual(obs.conjugate(), obs.transpose().adjoint()) + self.assertEqual(obs.conjugate(), obs.adjoint().transpose()) + + def test_conjugate_explicit(self): + # The description of conjugation on the operator is not 100% trivial to see is correct, so + # here's an explicit case to verify. + obs = SparseObservable.from_sparse_list( + [ + ("Y", (1,), 2.0), + ("X+-", (5, 4, 3), 1.5), + ("Z01", (5, 4, 3), 1.5j), + ("YY", (2, 0), 0.25), + ("YY", (3, 1), 0.25j), + ("YYY", (3, 2, 1), 0.75), + ("rlrl", (4, 3, 2, 1), 1.0), + ("lrlr", (4, 3, 2, 1), 1.0j), + ("", (), 1.5j), + ], + num_qubits=6, + ) + expected = SparseObservable.from_sparse_list( + [ + ("Y", (1,), -2.0), + ("X+-", (5, 4, 3), 1.5), + ("Z01", (5, 4, 3), -1.5j), + ("YY", (2, 0), 0.25), + ("YY", (3, 1), -0.25j), + ("YYY", (3, 2, 1), -0.75), + ("lrlr", (4, 3, 2, 1), 1.0), + ("rlrl", (4, 3, 2, 1), -1.0j), + ("", (), -1.5j), + ], + num_qubits=6, + ) + self.assertEqual(obs.conjugate(), expected) + self.assertEqual(obs.conjugate().conjugate(), obs) + + @ddt.idata(single_cases()) + def test_transpose(self, obs): + initial = obs.copy() + + term_map = {term: (term, 1.0) for term in SparseObservable.BitTerm} + term_map[SparseObservable.BitTerm.Y] = (SparseObservable.BitTerm.Y, -1.0) + term_map[SparseObservable.BitTerm.RIGHT] = (SparseObservable.BitTerm.LEFT, 1.0) + term_map[SparseObservable.BitTerm.LEFT] = (SparseObservable.BitTerm.RIGHT, 1.0) + + expected = obs.copy() + for i in range(expected.num_terms): + start, end = expected.boundaries[i], expected.boundaries[i + 1] + coeff = expected.coeffs[i] + for offset, bit_term in enumerate(expected.bit_terms[start:end]): + new_term, multiplier = term_map[bit_term] + coeff *= multiplier + expected.bit_terms[start + offset] = new_term + expected.coeffs[i] = coeff + + self.assertEqual(obs.transpose(), expected) + self.assertEqual(obs, initial) + self.assertEqual(obs.transpose().transpose(), initial) + self.assertEqual(obs.transpose(), obs.conjugate().adjoint()) + self.assertEqual(obs.transpose(), obs.adjoint().conjugate()) + + def test_transpose_explicit(self): + # The description of transposition on the operator is not 100% trivial to see is correct, so + # here's a few explicit cases to verify. + obs = SparseObservable.from_sparse_list( + [ + ("Y", (1,), 2.0), + ("X+-", (5, 4, 3), 1.5), + ("Z01", (5, 4, 3), 1.5j), + ("YY", (2, 0), 0.25), + ("YY", (3, 1), 0.25j), + ("YYY", (3, 2, 1), 0.75), + ("rlrl", (4, 3, 2, 1), 1.0), + ("lrlr", (4, 3, 2, 1), 1.0j), + ("", (), 1.5j), + ], + num_qubits=6, + ) + expected = SparseObservable.from_sparse_list( + [ + ("Y", (1,), -2.0), + ("X+-", (5, 4, 3), 1.5), + ("Z01", (5, 4, 3), 1.5j), + ("YY", (2, 0), 0.25), + ("YY", (3, 1), 0.25j), + ("YYY", (3, 2, 1), -0.75), + ("lrlr", (4, 3, 2, 1), 1.0), + ("rlrl", (4, 3, 2, 1), 1.0j), + ("", (), 1.5j), + ], + num_qubits=6, + ) + self.assertEqual(obs.transpose(), expected) + self.assertEqual(obs.transpose().transpose(), obs) + + def test_simplify(self): + self.assertEqual((1e-10 * SparseObservable("XX")).simplify(1e-8), SparseObservable.zero(2)) + self.assertEqual((1e-10j * SparseObservable("XX")).simplify(1e-8), SparseObservable.zero(2)) + self.assertEqual( + (1e-7 * SparseObservable("XX")).simplify(1e-8), 1e-7 * SparseObservable("XX") + ) + + exact_coeff = 2.0**-10 + self.assertEqual( + (exact_coeff * SparseObservable("XX")).simplify(exact_coeff), SparseObservable.zero(2) + ) + self.assertEqual( + (exact_coeff * 1j * SparseObservable("XX")).simplify(exact_coeff), + SparseObservable.zero(2), + ) + coeff = 3e-5 + 4e-5j + self.assertEqual( + (coeff * SparseObservable("ZZ")).simplify(abs(coeff)), SparseObservable.zero(2) + ) + + sum_alike = SparseObservable.from_list( + [ + ("XX", 1.0), + ("YY", 1j), + ("XX", -1.0), + ] + ) + self.assertEqual(sum_alike.simplify(), 1j * SparseObservable("YY")) + + terms = [ + ("XYIZI", 1.5), + ("+-IYI", 2.0), + ("XYIZI", 2j), + ("+-IYI", -2.0), + ("rlIZI", -2.0), + ] + canonical_forwards = SparseObservable.from_list(terms) + canonical_backwards = SparseObservable.from_list(list(reversed(terms))) + self.assertNotEqual(canonical_forwards.simplify(), canonical_forwards) + self.assertNotEqual(canonical_forwards, canonical_backwards) + self.assertEqual(canonical_forwards.simplify(), canonical_backwards.simplify()) + self.assertEqual(canonical_forwards.simplify(), canonical_forwards.simplify().simplify()) + + @ddt.idata(single_cases()) + def test_clear(self, obs): + num_qubits = obs.num_qubits + obs.clear() + self.assertEqual(obs, SparseObservable.zero(num_qubits)) diff --git a/test/python/scheduler/test_basic_scheduler.py b/test/python/scheduler/test_basic_scheduler.py index 947a255ddf21..5adbb3fdc1ff 100644 --- a/test/python/scheduler/test_basic_scheduler.py +++ b/test/python/scheduler/test_basic_scheduler.py @@ -57,7 +57,8 @@ def test_unavailable_defaults(self): with self.assertWarns(DeprecationWarning): backend = FakeBackend(None) backend.defaults = backend.configuration - self.assertRaises(QiskitError, lambda: schedule(qc, backend)) + with self.assertWarns(DeprecationWarning): + self.assertRaises(QiskitError, lambda: schedule(qc, backend)) def test_alap_pass(self): """Test ALAP scheduling.""" @@ -79,15 +80,17 @@ def test_alap_pass(self): qc.barrier(q[0], [q[1]]) qc.cx(q[0], q[1]) qc.measure(q, c) - sched = schedule(qc, self.backend) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend) # X pulse on q0 should end at the start of the CNOT - expected = Schedule( - (2, self.inst_map.get("u2", [0], 3.14, 1.57)), - self.inst_map.get("u2", [1], 0.5, 0.25), - (2, self.inst_map.get("u2", [1], 0.5, 0.25)), - (4, self.inst_map.get("cx", [0, 1])), - (26, self.inst_map.get("measure", [0, 1])), - ) + with self.assertWarns(DeprecationWarning): + expected = Schedule( + (2, self.inst_map.get("u2", [0], 3.14, 1.57)), + self.inst_map.get("u2", [1], 0.5, 0.25), + (2, self.inst_map.get("u2", [1], 0.5, 0.25)), + (4, self.inst_map.get("cx", [0, 1])), + (26, self.inst_map.get("measure", [0, 1])), + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -97,8 +100,9 @@ def test_single_circuit_list_schedule(self): q = QuantumRegister(2) c = ClassicalRegister(2) qc = QuantumCircuit(q, c) - sched = schedule([qc], self.backend, method="alap") - expected = Schedule() + with self.assertWarns(DeprecationWarning): + sched = schedule([qc], self.backend, method="alap") + expected = Schedule() self.assertIsInstance(sched, list) self.assertEqual(sched[0].instructions, expected.instructions) @@ -110,10 +114,11 @@ def test_alap_with_barriers(self): qc.append(U2Gate(0, 0), [q[0]]) qc.barrier(q[0], q[1]) qc.append(U2Gate(0, 0), [q[1]]) - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - self.inst_map.get("u2", [0], 0, 0), (2, self.inst_map.get("u2", [1], 0, 0)) - ) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") + expected = Schedule( + self.inst_map.get("u2", [0], 0, 0), (2, self.inst_map.get("u2", [1], 0, 0)) + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -123,8 +128,9 @@ def test_empty_circuit_schedule(self): q = QuantumRegister(2) c = ClassicalRegister(2) qc = QuantumCircuit(q, c) - sched = schedule(qc, self.backend, method="alap") - expected = Schedule() + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") + expected = Schedule() self.assertEqual(sched.instructions, expected.instructions) def test_alap_aligns_end(self): @@ -134,16 +140,18 @@ def test_alap_aligns_end(self): qc = QuantumCircuit(q, c) qc.append(U3Gate(0, 0, 0), [q[0]]) qc.append(U2Gate(0, 0), [q[1]]) - sched = schedule(qc, self.backend, method="alap") - expected_sched = Schedule( - (2, self.inst_map.get("u2", [1], 0, 0)), self.inst_map.get("u3", [0], 0, 0, 0) - ) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") + expected_sched = Schedule( + (2, self.inst_map.get("u2", [1], 0, 0)), self.inst_map.get("u3", [0], 0, 0, 0) + ) for actual, expected in zip(sched.instructions, expected_sched.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) - self.assertEqual( - sched.ch_duration(DriveChannel(0)), expected_sched.ch_duration(DriveChannel(1)) - ) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + sched.ch_duration(DriveChannel(0)), expected_sched.ch_duration(DriveChannel(1)) + ) def test_asap_pass(self): """Test ASAP scheduling.""" @@ -165,15 +173,16 @@ def test_asap_pass(self): qc.barrier(q[0], q[1]) qc.cx(q[0], q[1]) qc.measure(q, c) - sched = schedule(qc, self.backend, method="as_soon_as_possible") - # X pulse on q0 should start at t=0 - expected = Schedule( - self.inst_map.get("u2", [0], 3.14, 1.57), - self.inst_map.get("u2", [1], 0.5, 0.25), - (2, self.inst_map.get("u2", [1], 0.5, 0.25)), - (4, self.inst_map.get("cx", [0, 1])), - (26, self.inst_map.get("measure", [0, 1])), - ) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="as_soon_as_possible") + # X pulse on q0 should start at t=0 + expected = Schedule( + self.inst_map.get("u2", [0], 3.14, 1.57), + self.inst_map.get("u2", [1], 0.5, 0.25), + (2, self.inst_map.get("u2", [1], 0.5, 0.25)), + (4, self.inst_map.get("cx", [0, 1])), + (26, self.inst_map.get("measure", [0, 1])), + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -187,7 +196,8 @@ def test_alap_resource_respecting(self): qc = QuantumCircuit(q, c) qc.cx(q[0], q[1]) qc.append(U2Gate(0.5, 0.25), [q[1]]) - sched = schedule(qc, self.backend, method="as_late_as_possible") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="as_late_as_possible") insts = sched.instructions self.assertEqual(insts[0][0], 0) self.assertEqual(insts[6][0], 22) @@ -196,7 +206,8 @@ def test_alap_resource_respecting(self): qc.cx(q[0], q[1]) qc.append(U2Gate(0.5, 0.25), [q[1]]) qc.measure(q, c) - sched = schedule(qc, self.backend, method="as_late_as_possible") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="as_late_as_possible") self.assertEqual(sched.instructions[-1][0], 24) def test_inst_map_schedules_unaltered(self): @@ -205,8 +216,9 @@ def test_inst_map_schedules_unaltered(self): c = ClassicalRegister(2) qc = QuantumCircuit(q, c) qc.cx(q[0], q[1]) - sched1 = schedule(qc, self.backend, method="as_soon_as_possible") - sched2 = schedule(qc, self.backend, method="as_late_as_possible") + with self.assertWarns(DeprecationWarning): + sched1 = schedule(qc, self.backend, method="as_soon_as_possible") + sched2 = schedule(qc, self.backend, method="as_late_as_possible") for asap, alap in zip(sched1.instructions, sched2.instructions): self.assertEqual(asap[0], alap[0]) self.assertEqual(asap[1], alap[1]) @@ -233,14 +245,15 @@ def test_measure_combined(self): qc.measure(q[0], c[0]) qc.measure(q[1], c[1]) qc.measure(q[1], c[1]) - sched = schedule(qc, self.backend, method="as_soon_as_possible") - expected = Schedule( - self.inst_map.get("u2", [0], 3.14, 1.57), - (2, self.inst_map.get("cx", [0, 1])), - (24, self.inst_map.get("measure", [0, 1])), - (34, self.inst_map.get("measure", [0, 1]).filter(channels=[MeasureChannel(1)])), - (34, Acquire(10, AcquireChannel(1), MemorySlot(1))), - ) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="as_soon_as_possible") + expected = Schedule( + self.inst_map.get("u2", [0], 3.14, 1.57), + (2, self.inst_map.get("cx", [0, 1])), + (24, self.inst_map.get("measure", [0, 1])), + (34, self.inst_map.get("measure", [0, 1]).filter(channels=[MeasureChannel(1)])), + (34, Acquire(10, AcquireChannel(1), MemorySlot(1))), + ) self.assertEqual(sched.instructions, expected.instructions) # @@ -266,15 +279,16 @@ def test_3q_schedule(self): qc.append(U2Gate(3.14, 1.57), [q[1]]) qc.cx(q[1], q[2]) qc.append(U2Gate(0.778, 0.122), [q[2]]) - sched = schedule(qc, backend) - expected = Schedule( - inst_map.get("cx", [0, 1]), - (22, inst_map.get("u2", [1], 3.14, 1.57)), - (22, inst_map.get("u2", [2], 0.778, 0.122)), - (24, inst_map.get("cx", [1, 2])), - (44, inst_map.get("u3", [0], 3.14, 1.57, 0)), - (46, inst_map.get("u2", [2], 0.778, 0.122)), - ) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, backend) + expected = Schedule( + inst_map.get("cx", [0, 1]), + (22, inst_map.get("u2", [1], 3.14, 1.57)), + (22, inst_map.get("u2", [2], 0.778, 0.122)), + (24, inst_map.get("cx", [1, 2])), + (44, inst_map.get("u3", [0], 3.14, 1.57, 0)), + (46, inst_map.get("u2", [2], 0.778, 0.122)), + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -287,8 +301,9 @@ def test_schedule_multi(self): qc0.cx(q[0], q[1]) qc1 = QuantumCircuit(q, c) qc1.cx(q[0], q[1]) - schedules = schedule([qc0, qc1], self.backend) - expected_insts = schedule(qc0, self.backend).instructions + with self.assertWarns(DeprecationWarning): + schedules = schedule([qc0, qc1], self.backend) + expected_insts = schedule(qc0, self.backend).instructions for actual, expected in zip(schedules[0].instructions, expected_insts): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -299,9 +314,11 @@ def test_circuit_name_kept(self): c = ClassicalRegister(2) qc = QuantumCircuit(q, c, name="CIRCNAME") qc.cx(q[0], q[1]) - sched = schedule(qc, self.backend, method="asap") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="asap") self.assertEqual(sched.name, qc.name) - sched = schedule(qc, self.backend, method="alap") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") self.assertEqual(sched.name, qc.name) def test_can_add_gates_into_free_space(self): @@ -321,15 +338,16 @@ def test_can_add_gates_into_free_space(self): qc.append(U2Gate(0, 0), [qr[i]]) qc.append(U1Gate(3.14), [qr[i]]) qc.append(U2Gate(0, 0), [qr[i]]) - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - self.inst_map.get("u2", [0], 0, 0), - self.inst_map.get("u2", [1], 0, 0), - (2, self.inst_map.get("u1", [0], 3.14)), - (2, self.inst_map.get("u1", [1], 3.14)), - (2, self.inst_map.get("u2", [0], 0, 0)), - (2, self.inst_map.get("u2", [1], 0, 0)), - ) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") + expected = Schedule( + self.inst_map.get("u2", [0], 0, 0), + self.inst_map.get("u2", [1], 0, 0), + (2, self.inst_map.get("u1", [0], 3.14)), + (2, self.inst_map.get("u1", [1], 3.14)), + (2, self.inst_map.get("u2", [0], 0, 0)), + (2, self.inst_map.get("u2", [1], 0, 0)), + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -346,15 +364,16 @@ def test_barriers_in_middle(self): qc.append(U1Gate(3.14), [qr[i]]) qc.barrier(qr[i]) qc.append(U2Gate(0, 0), [qr[i]]) - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - self.inst_map.get("u2", [0], 0, 0), - self.inst_map.get("u2", [1], 0, 0), - (2, self.inst_map.get("u1", [0], 3.14)), - (2, self.inst_map.get("u1", [1], 3.14)), - (2, self.inst_map.get("u2", [0], 0, 0)), - (2, self.inst_map.get("u2", [1], 0, 0)), - ) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") + expected = Schedule( + self.inst_map.get("u2", [0], 0, 0), + self.inst_map.get("u2", [1], 0, 0), + (2, self.inst_map.get("u1", [0], 3.14)), + (2, self.inst_map.get("u1", [1], 3.14)), + (2, self.inst_map.get("u2", [0], 0, 0)), + (2, self.inst_map.get("u2", [1], 0, 0)), + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -364,11 +383,13 @@ def test_parametric_input(self): qr = QuantumRegister(1) qc = QuantumCircuit(qr) qc.append(Gate("gauss", 1, []), qargs=[qr[0]]) - custom_gauss = Schedule( - Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=pi / 2), DriveChannel(0)) - ) + with self.assertWarns(DeprecationWarning): + custom_gauss = Schedule( + Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=pi / 2), DriveChannel(0)) + ) self.inst_map.add("gauss", [0], custom_gauss) - sched = schedule(qc, self.backend, inst_map=self.inst_map) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, inst_map=self.inst_map) self.assertEqual(sched.instructions[0], custom_gauss.instructions[0]) def test_pulse_gates(self): @@ -378,14 +399,20 @@ def test_pulse_gates(self): qc.append(U2Gate(0, 0), [q[0]]) qc.barrier(q[0], q[1]) qc.append(U2Gate(0, 0), [q[1]]) - qc.add_calibration("u2", [0], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(0))), [0, 0]) - qc.add_calibration("u2", [1], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1))), [0, 0]) - - sched = schedule(qc, self.backend) - expected = Schedule( - Play(Gaussian(28, 0.2, 4), DriveChannel(0)), - (28, Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1)))), - ) + with self.assertWarns(DeprecationWarning): + qc.add_calibration( + "u2", [0], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(0))), [0, 0] + ) + qc.add_calibration( + "u2", [1], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1))), [0, 0] + ) + + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend) + expected = Schedule( + Play(Gaussian(28, 0.2, 4), DriveChannel(0)), + (28, Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1)))), + ) self.assertEqual(sched.instructions, expected.instructions) def test_calibrated_measurements(self): @@ -396,12 +423,13 @@ def test_calibrated_measurements(self): qc.append(U2Gate(0, 0), [q[0]]) qc.measure(q[0], c[0]) - meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) - meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) + with self.assertWarns(DeprecationWarning): + meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) + meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) + qc.add_calibration("measure", [0], meas_sched) - sched = schedule(qc, self.backend) - expected = Schedule(self.inst_map.get("u2", [0], 0, 0), (2, meas_sched)) + sched = schedule(qc, self.backend) + expected = Schedule(self.inst_map.get("u2", [0], 0, 0), (2, meas_sched)) self.assertEqual(sched.instructions, expected.instructions) def test_subset_calibrated_measurements(self): @@ -413,20 +441,23 @@ def test_subset_calibrated_measurements(self): qc.measure(2, 2) meas_scheds = [] for qubit in [0, 2]: - meas = Play(Gaussian(1200, 0.2, 4), MeasureChannel(qubit)) + Acquire( - 1200, AcquireChannel(qubit), MemorySlot(qubit) - ) + with self.assertWarns(DeprecationWarning): + meas = Play(Gaussian(1200, 0.2, 4), MeasureChannel(qubit)) + Acquire( + 1200, AcquireChannel(qubit), MemorySlot(qubit) + ) meas_scheds.append(meas) - qc.add_calibration("measure", [qubit], meas) + with self.assertWarns(DeprecationWarning): + qc.add_calibration("measure", [qubit], meas) with self.assertWarns(DeprecationWarning): backend = FakeOpenPulse3Q() - meas = macros.measure([1], backend) - meas = meas.exclude(channels=[AcquireChannel(0), AcquireChannel(2)]) + meas = macros.measure([1], backend) + meas = meas.exclude(channels=[AcquireChannel(0), AcquireChannel(2)]) with self.assertWarns(DeprecationWarning): backend = FakeOpenPulse3Q() - sched = schedule(qc, backend) - expected = Schedule(meas_scheds[0], meas_scheds[1], meas) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, backend) + expected = Schedule(meas_scheds[0], meas_scheds[1], meas) self.assertEqual(sched.instructions, expected.instructions) def test_clbits_of_calibrated_measurements(self): @@ -436,13 +467,16 @@ def test_clbits_of_calibrated_measurements(self): qc = QuantumCircuit(q, c) qc.measure(q[0], c[1]) - meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) - meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) + with self.assertWarns(DeprecationWarning): + meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) + meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) + qc.add_calibration("measure", [0], meas_sched) - sched = schedule(qc, self.backend) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend) # Doesn't use the calibrated schedule because the classical memory slots do not match - expected = Schedule(macros.measure([0], self.backend, qubit_mem_slots={0: 1})) + with self.assertWarns(DeprecationWarning): + expected = Schedule(macros.measure([0], self.backend, qubit_mem_slots={0: 1})) self.assertEqual(sched.instructions, expected.instructions) def test_metadata_is_preserved_alap(self): @@ -453,7 +487,8 @@ def test_metadata_is_preserved_alap(self): qc.barrier(q[0], q[1]) qc.append(U2Gate(0, 0), [q[1]]) qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - sched = schedule(qc, self.backend, method="alap") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) def test_metadata_is_preserved_asap(self): @@ -464,7 +499,8 @@ def test_metadata_is_preserved_asap(self): qc.barrier(q[0], q[1]) qc.append(U2Gate(0, 0), [q[1]]) qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - sched = schedule(qc, self.backend, method="asap") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="asap") self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) def test_scheduler_with_params_bound(self): @@ -472,10 +508,14 @@ def test_scheduler_with_params_bound(self): x = Parameter("x") qc = QuantumCircuit(2) qc.append(Gate("pulse_gate", 1, [x]), [0]) - expected_schedule = Schedule() - qc.add_calibration(gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x]) + with self.assertWarns(DeprecationWarning): + expected_schedule = Schedule() + qc.add_calibration( + gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] + ) qc = qc.assign_parameters({x: 1}) - sched = schedule(qc, self.backend) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend) self.assertEqual(sched, expected_schedule) def test_scheduler_with_params_not_bound(self): @@ -483,30 +523,36 @@ def test_scheduler_with_params_not_bound(self): x = Parameter("amp") qc = QuantumCircuit(2) qc.append(Gate("pulse_gate", 1, [x]), [0]) - with build() as expected_schedule: - play(Gaussian(duration=160, amp=x, sigma=40), DriveChannel(0)) - qc.add_calibration(gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x]) - sched = schedule(qc, self.backend) - self.assertEqual(sched, transforms.target_qobj_transform(expected_schedule)) + with self.assertWarns(DeprecationWarning): + with build() as expected_schedule: + play(Gaussian(duration=160, amp=x, sigma=40), DriveChannel(0)) + qc.add_calibration( + gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] + ) + sched = schedule(qc, self.backend) + self.assertEqual(sched, transforms.target_qobj_transform(expected_schedule)) def test_schedule_block_in_instmap(self): """Test schedule block in instmap can be scheduled.""" duration = Parameter("duration") - with build() as pulse_prog: - play(Gaussian(duration, 0.1, 10), DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with build() as pulse_prog: + play(Gaussian(duration, 0.1, 10), DriveChannel(0)) - instmap = InstructionScheduleMap() + instmap = InstructionScheduleMap() instmap.add("block_gate", (0,), pulse_prog, ["duration"]) qc = QuantumCircuit(1) qc.append(Gate("block_gate", 1, [duration]), [0]) qc.assign_parameters({duration: 100}, inplace=True) - sched = schedule(qc, self.backend, inst_map=instmap) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, inst_map=instmap) - ref_sched = Schedule() - ref_sched += Play(Gaussian(100, 0.1, 10), DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + ref_sched = Schedule() + ref_sched += Play(Gaussian(100, 0.1, 10), DriveChannel(0)) self.assertEqual(sched, ref_sched) @@ -516,8 +562,9 @@ class TestBasicScheduleV2(QiskitTestCase): def setUp(self): super().setUp() - self.backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True, seed=42) - self.inst_map = self.backend.instruction_schedule_map + with self.assertWarns(DeprecationWarning): + self.backend = GenericBackendV2(num_qubits=3, calibrate_instructions=True, seed=42) + self.inst_map = self.backend.instruction_schedule_map # self.pulse_2_samples is the pulse sequence used to calibrate "measure" in # GenericBackendV2. See class construction for more details. self.pulse_2_samples = np.linspace(0, 1.0, 32, dtype=np.complex128) @@ -547,7 +594,8 @@ def test_alap_pass(self): qc.cx(q[0], q[1]) qc.measure(q, c) - sched = schedule(circuits=qc, backend=self.backend, method="alap") + with self.assertWarns(DeprecationWarning): + sched = schedule(circuits=qc, backend=self.backend, method="alap") # Since, the method of scheduling chosen here is 'as_late_as_possible' # so all the π/2 pulse here should be right shifted. @@ -560,30 +608,31 @@ def test_alap_pass(self): # cx pulse( pulse on drive channel, control channel) should start with a delay # of 16dt+16dt. # measure pulse should start with a delay of 16dt+16dt+64dt(64dt for cx gate). - expected = Schedule( - (0, self.inst_map.get("sx", [1])), - (0 + 16, self.inst_map.get("sx", [0])), # Right shifted because of alap. - (0 + 16, self.inst_map.get("sx", [1])), - (0 + 16 + 16, self.inst_map.get("cx", [0, 1])), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(0), - name="pulse_2", + with self.assertWarns(DeprecationWarning): + expected = Schedule( + (0, self.inst_map.get("sx", [1])), + (0 + 16, self.inst_map.get("sx", [0])), # Right shifted because of alap. + (0 + 16, self.inst_map.get("sx", [1])), + (0 + 16 + 16, self.inst_map.get("cx", [0, 1])), + ( + 0 + 16 + 16 + 64, + Play( + Waveform(samples=self.pulse_2_samples, name="pulse_2"), + MeasureChannel(0), + name="pulse_2", + ), ), - ), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(1), - name="pulse_2", + ( + 0 + 16 + 16 + 64, + Play( + Waveform(samples=self.pulse_2_samples, name="pulse_2"), + MeasureChannel(1), + name="pulse_2", + ), ), - ), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(0), MemorySlot(0))), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(1), MemorySlot(1))), - ) + (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(0), MemorySlot(0))), + (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(1), MemorySlot(1))), + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -593,8 +642,9 @@ def test_single_circuit_list_schedule(self): q = QuantumRegister(2) c = ClassicalRegister(2) qc = QuantumCircuit(q, c) - sched = schedule([qc], self.backend, method="alap") - expected = Schedule() + with self.assertWarns(DeprecationWarning): + sched = schedule([qc], self.backend, method="alap") + expected = Schedule() self.assertIsInstance(sched, list) self.assertEqual(sched[0].instructions, expected.instructions) @@ -615,10 +665,13 @@ def test_alap_with_barriers(self): qc.append(SXGate(), [q[0]]) qc.barrier(q[0], q[1]) qc.append(SXGate(), [q[1]]) - sched = schedule(qc, self.backend, method="alap") - # If there wasn't a barrier the π/2 pulse on q1 would have started from 0dt, but since, - # there is a barrier so the π/2 pulse on q1 should start with a delay of 160dt. - expected = Schedule((0, self.inst_map.get("sx", [0])), (16, self.inst_map.get("sx", [1]))) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") + # If there wasn't a barrier the π/2 pulse on q1 would have started from 0dt, but since, + # there is a barrier so the π/2 pulse on q1 should start with a delay of 160dt. + expected = Schedule( + (0, self.inst_map.get("sx", [0])), (16, self.inst_map.get("sx", [1])) + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual, expected) @@ -627,8 +680,9 @@ def test_empty_circuit_schedule(self): q = QuantumRegister(2) c = ClassicalRegister(2) qc = QuantumCircuit(q, c) - sched = schedule(qc, self.backend, method="alap") - expected = Schedule() + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") + expected = Schedule() self.assertEqual(sched.instructions, expected.instructions) def test_alap_aligns_end(self): @@ -645,16 +699,18 @@ def test_alap_aligns_end(self): qc = QuantumCircuit(q, c) qc.sx(q[0]) qc.sx(q[1]) - sched = schedule(qc, self.backend, method="alap") - expected_sched = Schedule( - (0, self.inst_map.get("sx", [1])), (0, self.inst_map.get("sx", [0])) - ) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") + expected_sched = Schedule( + (0, self.inst_map.get("sx", [1])), (0, self.inst_map.get("sx", [0])) + ) for actual, expected in zip(sched.instructions, expected_sched.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) - self.assertEqual( - sched.ch_duration(DriveChannel(0)), expected_sched.ch_duration(DriveChannel(1)) - ) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + sched.ch_duration(DriveChannel(0)), expected_sched.ch_duration(DriveChannel(1)) + ) def test_asap_pass(self): """Test ASAP scheduling.""" @@ -681,7 +737,8 @@ def test_asap_pass(self): qc.cx(q[0], q[1]) qc.measure(q, c) - sched = schedule(circuits=qc, backend=self.backend, method="asap") + with self.assertWarns(DeprecationWarning): + sched = schedule(circuits=qc, backend=self.backend, method="asap") # Since, the method of scheduling chosen here is 'as_soon_as_possible' # so all the π/2 pulse here should be left shifted. # @@ -693,30 +750,31 @@ def test_asap_pass(self): # cx pulse( pulse on drive channel, control channel) should start with a delay # of 16dt+16dt. # measure pulse should start with a delay of 16dt+16dt+64dt(64dt for cx gate). - expected = Schedule( - (0, self.inst_map.get("sx", [1])), - (0, self.inst_map.get("sx", [0])), # Left shifted because of asap. - (0 + 16, self.inst_map.get("sx", [1])), - (0 + 16 + 16, self.inst_map.get("cx", [0, 1])), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(0), - name="pulse_2", + with self.assertWarns(DeprecationWarning): + expected = Schedule( + (0, self.inst_map.get("sx", [1])), + (0, self.inst_map.get("sx", [0])), # Left shifted because of asap. + (0 + 16, self.inst_map.get("sx", [1])), + (0 + 16 + 16, self.inst_map.get("cx", [0, 1])), + ( + 0 + 16 + 16 + 64, + Play( + Waveform(samples=self.pulse_2_samples, name="pulse_2"), + MeasureChannel(0), + name="pulse_2", + ), ), - ), - ( - 0 + 16 + 16 + 64, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(1), - name="pulse_2", + ( + 0 + 16 + 16 + 64, + Play( + Waveform(samples=self.pulse_2_samples, name="pulse_2"), + MeasureChannel(1), + name="pulse_2", + ), ), - ), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(0), MemorySlot(0))), - (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(1), MemorySlot(1))), - ) + (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(0), MemorySlot(0))), + (0 + 16 + 16 + 64, Acquire(1792, AcquireChannel(1), MemorySlot(1))), + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -730,7 +788,8 @@ def test_alap_resource_respecting(self): qc = QuantumCircuit(q, c) qc.cx(q[0], q[1]) qc.sx(q[1]) - sched = schedule(qc, self.backend, method="as_late_as_possible") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="as_late_as_possible") insts = sched.instructions # This is ShiftPhase for the cx operation. @@ -745,7 +804,8 @@ def test_alap_resource_respecting(self): qc.cx(q[0], q[1]) qc.sx(q[1]) qc.measure(q, c) - sched = schedule(qc, self.backend, method="as_late_as_possible") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="as_late_as_possible") # 64dt for cx operation + 16dt for sx operation # So, the pulses in MeasureChannel0 and 1 starts from 80dt. @@ -757,8 +817,9 @@ def test_inst_map_schedules_unaltered(self): c = ClassicalRegister(2) qc = QuantumCircuit(q, c) qc.cx(q[0], q[1]) - sched1 = schedule(qc, self.backend, method="as_soon_as_possible") - sched2 = schedule(qc, self.backend, method="as_late_as_possible") + with self.assertWarns(DeprecationWarning): + sched1 = schedule(qc, self.backend, method="as_soon_as_possible") + sched2 = schedule(qc, self.backend, method="as_late_as_possible") for asap, alap in zip(sched1.instructions, sched2.instructions): self.assertEqual(asap[0], alap[0]) self.assertEqual(asap[1], alap[1]) @@ -784,36 +845,37 @@ def test_measure_combined(self): qc.measure(q[1], c[1]) qc.measure(q[1], c[1]) - sched = schedule(qc, self.backend, method="as_soon_as_possible") - - expected_sched = Schedule( - # This is the schedule to implement sx gate. - (0, self.inst_map.get("sx", [0])), - # This is the schedule to implement cx gate - (0 + 16, self.inst_map.get("cx", [0, 1])), - # This is the schedule for the measurements on qubits 0 and 1 (combined) - ( - 0 + 16 + 64, - self.inst_map.get("measure", [0]).filter( - channels=[MeasureChannel(0), MeasureChannel(1)] + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="as_soon_as_possible") + + expected_sched = Schedule( + # This is the schedule to implement sx gate. + (0, self.inst_map.get("sx", [0])), + # This is the schedule to implement cx gate + (0 + 16, self.inst_map.get("cx", [0, 1])), + # This is the schedule for the measurements on qubits 0 and 1 (combined) + ( + 0 + 16 + 64, + self.inst_map.get("measure", [0]).filter( + channels=[MeasureChannel(0), MeasureChannel(1)] + ), + ), + ( + 0 + 16 + 64, + self.inst_map.get("measure", [0]).filter( + channels=[AcquireChannel(0), AcquireChannel(1)] + ), ), - ), - ( - 0 + 16 + 64, - self.inst_map.get("measure", [0]).filter( - channels=[AcquireChannel(0), AcquireChannel(1)] + # This is the schedule for the second measurement on qubit 1 + ( + 0 + 16 + 64 + 1792, + self.inst_map.get("measure", [1]).filter(channels=[MeasureChannel(1)]), ), - ), - # This is the schedule for the second measurement on qubit 1 - ( - 0 + 16 + 64 + 1792, - self.inst_map.get("measure", [1]).filter(channels=[MeasureChannel(1)]), - ), - ( - 0 + 16 + 64 + 1792, - self.inst_map.get("measure", [1]).filter(channels=[AcquireChannel(1)]), - ), - ) + ( + 0 + 16 + 64 + 1792, + self.inst_map.get("measure", [1]).filter(channels=[AcquireChannel(1)]), + ), + ) self.assertEqual(sched.instructions, expected_sched.instructions) def test_3q_schedule(self): @@ -838,15 +900,16 @@ def test_3q_schedule(self): qc.cx(q[1], q[2]) qc.sx(q[2]) - sched = schedule(qc, self.backend, method="asap") - expected = Schedule( - (0, self.inst_map.get("cx", [0, 1])), - (0, self.inst_map.get("sx", [2])), - (0 + 64, self.inst_map.get("sx", [0])), - (0 + 64, self.inst_map.get("x", [1])), - (0 + 64 + 16, self.inst_map.get("cx", [1, 2])), - (0 + 64 + 16 + 64, self.inst_map.get("sx", [2])), - ) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="asap") + expected = Schedule( + (0, self.inst_map.get("cx", [0, 1])), + (0, self.inst_map.get("sx", [2])), + (0 + 64, self.inst_map.get("sx", [0])), + (0 + 64, self.inst_map.get("x", [1])), + (0 + 64 + 16, self.inst_map.get("cx", [1, 2])), + (0 + 64 + 16 + 64, self.inst_map.get("sx", [2])), + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -859,8 +922,9 @@ def test_schedule_multi(self): qc0.cx(q[0], q[1]) qc1 = QuantumCircuit(q, c) qc1.cx(q[0], q[1]) - schedules = schedule([qc0, qc1], self.backend) - expected_insts = schedule(qc0, self.backend).instructions + with self.assertWarns(DeprecationWarning): + schedules = schedule([qc0, qc1], self.backend) + expected_insts = schedule(qc0, self.backend).instructions for actual, expected in zip(schedules[0].instructions, expected_insts): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -871,9 +935,11 @@ def test_circuit_name_kept(self): c = ClassicalRegister(2) qc = QuantumCircuit(q, c, name="CIRCNAME") qc.cx(q[0], q[1]) - sched = schedule(qc, self.backend, method="asap") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="asap") self.assertEqual(sched.name, qc.name) - sched = schedule(qc, self.backend, method="alap") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") self.assertEqual(sched.name, qc.name) def test_can_add_gates_into_free_space(self): @@ -893,15 +959,16 @@ def test_can_add_gates_into_free_space(self): qc.sx(qr[i]) qc.x(qr[i]) qc.sx(qr[i]) - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - (0, self.inst_map.get("sx", [0])), - (0, self.inst_map.get("sx", [1])), - (0 + 16, self.inst_map.get("x", [0])), - (0 + 16, self.inst_map.get("x", [1])), - (0 + 16 + 16, self.inst_map.get("sx", [0])), - (0 + 16 + 16, self.inst_map.get("sx", [1])), - ) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") + expected = Schedule( + (0, self.inst_map.get("sx", [0])), + (0, self.inst_map.get("sx", [1])), + (0 + 16, self.inst_map.get("x", [0])), + (0 + 16, self.inst_map.get("x", [1])), + (0 + 16 + 16, self.inst_map.get("sx", [0])), + (0 + 16 + 16, self.inst_map.get("sx", [1])), + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -918,15 +985,16 @@ def test_barriers_in_middle(self): qc.x(qr[i]) qc.barrier(qr[i]) qc.sx(qr[i]) - sched = schedule(qc, self.backend, method="alap") - expected = Schedule( - (0, self.inst_map.get("sx", [0])), - (0, self.inst_map.get("sx", [1])), - (0 + 16, self.inst_map.get("x", [0])), - (0 + 16, self.inst_map.get("x", [1])), - (0 + 16 + 16, self.inst_map.get("sx", [0])), - (0 + 16 + 16, self.inst_map.get("sx", [1])), - ) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") + expected = Schedule( + (0, self.inst_map.get("sx", [0])), + (0, self.inst_map.get("sx", [1])), + (0 + 16, self.inst_map.get("x", [0])), + (0 + 16, self.inst_map.get("x", [1])), + (0 + 16 + 16, self.inst_map.get("sx", [0])), + (0 + 16 + 16, self.inst_map.get("sx", [1])), + ) for actual, expected in zip(sched.instructions, expected.instructions): self.assertEqual(actual[0], expected[0]) self.assertEqual(actual[1], expected[1]) @@ -936,11 +1004,13 @@ def test_parametric_input(self): qr = QuantumRegister(1) qc = QuantumCircuit(qr) qc.append(Gate("gauss", 1, []), qargs=[qr[0]]) - custom_gauss = Schedule( - Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=pi / 2), DriveChannel(0)) - ) + with self.assertWarns(DeprecationWarning): + custom_gauss = Schedule( + Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=pi / 2), DriveChannel(0)) + ) self.inst_map.add("gauss", [0], custom_gauss) - sched = schedule(qc, self.backend, inst_map=self.inst_map) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, inst_map=self.inst_map) self.assertEqual(sched.instructions[0], custom_gauss.instructions[0]) def test_pulse_gates(self): @@ -950,14 +1020,20 @@ def test_pulse_gates(self): qc.append(U2Gate(0, 0), [q[0]]) qc.barrier(q[0], q[1]) qc.append(U2Gate(0, 0), [q[1]]) - qc.add_calibration("u2", [0], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(0))), [0, 0]) - qc.add_calibration("u2", [1], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1))), [0, 0]) - - sched = schedule(qc, self.backend) - expected = Schedule( - Play(Gaussian(28, 0.2, 4), DriveChannel(0)), - (28, Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1)))), - ) + with self.assertWarns(DeprecationWarning): + qc.add_calibration( + "u2", [0], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(0))), [0, 0] + ) + qc.add_calibration( + "u2", [1], Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1))), [0, 0] + ) + + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend) + expected = Schedule( + Play(Gaussian(28, 0.2, 4), DriveChannel(0)), + (28, Schedule(Play(Gaussian(28, 0.2, 4), DriveChannel(1)))), + ) self.assertEqual(sched.instructions, expected.instructions) def test_calibrated_measurements(self): @@ -968,22 +1044,23 @@ def test_calibrated_measurements(self): qc.sx(0) qc.measure(q[0], c[0]) - meas_sched = Play( - GaussianSquare( - duration=1472, - sigma=64, - width=1216, - amp=0.2400000000002, - angle=-0.247301694, - name="my_custom_calibration", - ), - MeasureChannel(0), - ) - meas_sched |= Acquire(1472, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) - - sched = schedule(qc, self.backend) - expected = Schedule(self.inst_map.get("sx", [0]), (16, meas_sched)) + with self.assertWarns(DeprecationWarning): + meas_sched = Play( + GaussianSquare( + duration=1472, + sigma=64, + width=1216, + amp=0.2400000000002, + angle=-0.247301694, + name="my_custom_calibration", + ), + MeasureChannel(0), + ) + meas_sched |= Acquire(1472, AcquireChannel(0), MemorySlot(0)) + qc.add_calibration("measure", [0], meas_sched) + + sched = schedule(qc, self.backend) + expected = Schedule(self.inst_map.get("sx", [0]), (16, meas_sched)) self.assertEqual(sched.instructions, expected.instructions) def test_subset_calibrated_measurements(self): @@ -994,17 +1071,19 @@ def test_subset_calibrated_measurements(self): qc.measure(1, 1) qc.measure(2, 2) meas_scheds = [] - for qubit in [0, 2]: - meas = Play(Gaussian(1200, 0.2, 4), MeasureChannel(qubit)) + Acquire( - 1200, AcquireChannel(qubit), MemorySlot(qubit) - ) - meas_scheds.append(meas) - qc.add_calibration("measure", [qubit], meas) - - meas = macros.measure(qubits=[1], backend=self.backend, qubit_mem_slots={0: 0, 1: 1}) - meas = meas.exclude(channels=[AcquireChannel(0), AcquireChannel(2)]) - sched = schedule(qc, self.backend) - expected = Schedule(meas_scheds[0], meas_scheds[1], meas) + with self.assertWarns(DeprecationWarning): + for qubit in [0, 2]: + meas = Play(Gaussian(1200, 0.2, 4), MeasureChannel(qubit)) + Acquire( + 1200, AcquireChannel(qubit), MemorySlot(qubit) + ) + meas_scheds.append(meas) + with self.assertWarns(DeprecationWarning): + qc.add_calibration("measure", [qubit], meas) + + meas = macros.measure(qubits=[1], backend=self.backend, qubit_mem_slots={0: 0, 1: 1}) + meas = meas.exclude(channels=[AcquireChannel(0), AcquireChannel(2)]) + sched = schedule(qc, self.backend) + expected = Schedule(meas_scheds[0], meas_scheds[1], meas) self.assertEqual(sched.instructions, expected.instructions) def test_clbits_of_calibrated_measurements(self): @@ -1014,13 +1093,14 @@ def test_clbits_of_calibrated_measurements(self): qc = QuantumCircuit(q, c) qc.measure(q[0], c[1]) - meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) - meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) - qc.add_calibration("measure", [0], meas_sched) + with self.assertWarns(DeprecationWarning): + meas_sched = Play(Gaussian(1200, 0.2, 4), MeasureChannel(0)) + meas_sched |= Acquire(1200, AcquireChannel(0), MemorySlot(0)) + qc.add_calibration("measure", [0], meas_sched) - sched = schedule(qc, self.backend) - # Doesn't use the calibrated schedule because the classical memory slots do not match - expected = Schedule(macros.measure([0], self.backend, qubit_mem_slots={0: 1})) + sched = schedule(qc, self.backend) + # Doesn't use the calibrated schedule because the classical memory slots do not match + expected = Schedule(macros.measure([0], self.backend, qubit_mem_slots={0: 1})) self.assertEqual(sched.instructions, expected.instructions) def test_metadata_is_preserved_alap(self): @@ -1031,7 +1111,8 @@ def test_metadata_is_preserved_alap(self): qc.barrier(q[0], q[1]) qc.sx(q[1]) qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - sched = schedule(qc, self.backend, method="alap") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="alap") self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) def test_metadata_is_preserved_asap(self): @@ -1042,7 +1123,8 @@ def test_metadata_is_preserved_asap(self): qc.barrier(q[0], q[1]) qc.sx(q[1]) qc.metadata = {"experiment_type": "gst", "execution_number": "1234"} - sched = schedule(qc, self.backend, method="asap") + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, method="asap") self.assertEqual({"experiment_type": "gst", "execution_number": "1234"}, sched.metadata) def test_scheduler_with_params_bound(self): @@ -1050,10 +1132,14 @@ def test_scheduler_with_params_bound(self): x = Parameter("x") qc = QuantumCircuit(2) qc.append(Gate("pulse_gate", 1, [x]), [0]) - expected_schedule = Schedule() - qc.add_calibration(gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x]) + with self.assertWarns(DeprecationWarning): + expected_schedule = Schedule() + qc.add_calibration( + gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] + ) qc = qc.assign_parameters({x: 1}) - sched = schedule(qc, self.backend) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend) self.assertEqual(sched, expected_schedule) def test_scheduler_with_params_not_bound(self): @@ -1061,65 +1147,72 @@ def test_scheduler_with_params_not_bound(self): x = Parameter("amp") qc = QuantumCircuit(2) qc.append(Gate("pulse_gate", 1, [x]), [0]) - with build() as expected_schedule: - play(Gaussian(duration=160, amp=x, sigma=40), DriveChannel(0)) - qc.add_calibration(gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x]) - sched = schedule(qc, self.backend) - self.assertEqual(sched, transforms.target_qobj_transform(expected_schedule)) + with self.assertWarns(DeprecationWarning): + with build() as expected_schedule: + play(Gaussian(duration=160, amp=x, sigma=40), DriveChannel(0)) + qc.add_calibration( + gate="pulse_gate", qubits=[0], schedule=expected_schedule, params=[x] + ) + sched = schedule(qc, self.backend) + with self.assertWarns(DeprecationWarning): + self.assertEqual(sched, transforms.target_qobj_transform(expected_schedule)) def test_schedule_block_in_instmap(self): """Test schedule block in instmap can be scheduled.""" duration = Parameter("duration") - with build() as pulse_prog: - play(Gaussian(duration, 0.1, 10), DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with build() as pulse_prog: + play(Gaussian(duration, 0.1, 10), DriveChannel(0)) - instmap = InstructionScheduleMap() + instmap = InstructionScheduleMap() instmap.add("block_gate", (0,), pulse_prog, ["duration"]) qc = QuantumCircuit(1) qc.append(Gate("block_gate", 1, [duration]), [0]) qc.assign_parameters({duration: 100}, inplace=True) - sched = schedule(qc, self.backend, inst_map=instmap) + with self.assertWarns(DeprecationWarning): + sched = schedule(qc, self.backend, inst_map=instmap) - ref_sched = Schedule() - ref_sched += Play(Gaussian(100, 0.1, 10), DriveChannel(0)) + ref_sched = Schedule() + ref_sched += Play(Gaussian(100, 0.1, 10), DriveChannel(0)) self.assertEqual(sched, ref_sched) def test_inst_sched_map_get_measure_0(self): """Test that Schedule returned by backend.instruction_schedule_map.get('measure', [0]) is actually Schedule for just qubit_0""" - sched_from_backend = self.backend.instruction_schedule_map.get("measure", [0]) - expected_sched = Schedule( - ( - 0, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(0), - name="pulse_2", + with self.assertWarns(DeprecationWarning): + sched_from_backend = self.backend.instruction_schedule_map.get("measure", [0]) + expected_sched = Schedule( + ( + 0, + Play( + Waveform(samples=self.pulse_2_samples, name="pulse_2"), + MeasureChannel(0), + name="pulse_2", + ), ), - ), - ( - 0, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(1), - name="pulse_2", + ( + 0, + Play( + Waveform(samples=self.pulse_2_samples, name="pulse_2"), + MeasureChannel(1), + name="pulse_2", + ), ), - ), - ( - 0, - Play( - Waveform(samples=self.pulse_2_samples, name="pulse_2"), - MeasureChannel(2), - name="pulse_2", + ( + 0, + Play( + Waveform(samples=self.pulse_2_samples, name="pulse_2"), + MeasureChannel(2), + name="pulse_2", + ), ), - ), - (0, Acquire(1792, AcquireChannel(0), MemorySlot(0))), - (0, Acquire(1792, AcquireChannel(1), MemorySlot(1))), - (0, Acquire(1792, AcquireChannel(2), MemorySlot(2))), - name="measure", - ) + (0, Acquire(1792, AcquireChannel(0), MemorySlot(0))), + (0, Acquire(1792, AcquireChannel(1), MemorySlot(1))), + (0, Acquire(1792, AcquireChannel(2), MemorySlot(2))), + name="measure", + ) self.assertEqual(sched_from_backend, expected_sched) diff --git a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py index c95d65422d4b..3f093d18c517 100644 --- a/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py +++ b/test/python/transpiler/legacy_scheduling/test_instruction_alignments.py @@ -332,21 +332,25 @@ class TestPulseGateValidation(QiskitTestCase): def setUp(self): super().setUp() - self.pulse_gate_validation_pass = ValidatePulseGates(granularity=16, min_length=64) + with self.assertWarns(DeprecationWarning): + self.pulse_gate_validation_pass = ValidatePulseGates(granularity=16, min_length=64) def test_invalid_pulse_duration(self): """Kill pass manager if invalid pulse gate is found.""" # this is invalid duration pulse # this will cause backend error since this doesn't fit with waveform memory chunk. - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)), inplace=True - ) + + with self.assertWarns(DeprecationWarning): + custom_gate = pulse.Schedule(name="custom_x_gate") + custom_gate.insert( + 0, pulse.Play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)), inplace=True + ) circuit = QuantumCircuit(1) circuit.x(0) - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + with self.assertWarns(DeprecationWarning): + circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) with self.assertRaises(TranspilerError): self.pulse_gate_validation_pass(circuit) @@ -356,14 +360,16 @@ def test_short_pulse_duration(self): # this is invalid duration pulse # this will cause backend error since this doesn't fit with waveform memory chunk. - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) + with self.assertWarns(DeprecationWarning): + custom_gate = pulse.Schedule(name="custom_x_gate") + custom_gate.insert( + 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True + ) circuit = QuantumCircuit(1) circuit.x(0) - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + with self.assertWarns(DeprecationWarning): + circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) with self.assertRaises(TranspilerError): self.pulse_gate_validation_pass(circuit) @@ -374,17 +380,19 @@ def test_short_pulse_duration_multiple_pulse(self): # this is invalid duration pulse # however total gate schedule length is 64, which accidentally satisfies the constraints # this should fail in the validation - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) - custom_gate.insert( - 32, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True - ) + with self.assertWarns(DeprecationWarning): + custom_gate = pulse.Schedule(name="custom_x_gate") + custom_gate.insert( + 0, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True + ) + custom_gate.insert( + 32, pulse.Play(pulse.Constant(32, 0.1), pulse.DriveChannel(0)), inplace=True + ) circuit = QuantumCircuit(1) circuit.x(0) - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + with self.assertWarns(DeprecationWarning): + circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) with self.assertRaises(TranspilerError): self.pulse_gate_validation_pass(circuit) @@ -393,14 +401,16 @@ def test_valid_pulse_duration(self): """No error raises if valid calibration is provided.""" # this is valid duration pulse - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(160, 0.1), pulse.DriveChannel(0)), inplace=True - ) + with self.assertWarns(DeprecationWarning): + custom_gate = pulse.Schedule(name="custom_x_gate") + custom_gate.insert( + 0, pulse.Play(pulse.Constant(160, 0.1), pulse.DriveChannel(0)), inplace=True + ) circuit = QuantumCircuit(1) circuit.x(0) - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + with self.assertWarns(DeprecationWarning): + circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) # just not raise an error self.pulse_gate_validation_pass(circuit) diff --git a/test/python/transpiler/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py index 90676a38a586..04bc8aef00da 100644 --- a/test/python/transpiler/test_calibrationbuilder.py +++ b/test/python/transpiler/test_calibrationbuilder.py @@ -45,8 +45,10 @@ RXCalibrationBuilder, ) from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestCalibrationBuilder(QiskitTestCase): """Test the Calibration Builder.""" @@ -103,6 +105,7 @@ def d1m_play(self, cr_schedule): @ddt +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestRZXCalibrationBuilder(TestCalibrationBuilder): """Test RZXCalibrationBuilder.""" @@ -185,8 +188,9 @@ def build_reverse( rz_qc_q1 = QuantumCircuit(2) rz_qc_q1.rz(pi / 2, 1) - rz_sched_q0 = schedule(rz_qc_q0, backend) - rz_sched_q1 = schedule(rz_qc_q1, backend) + with self.assertWarns(DeprecationWarning): + rz_sched_q0 = schedule(rz_qc_q0, backend) + rz_sched_q1 = schedule(rz_qc_q1, backend) with builder.build( backend, @@ -270,7 +274,7 @@ def test_rzx_calibration_cr_pulse_stretch(self, theta: float): # TODO this tests does not work with BackendV2/GenericBackendV2 # https://github.com/Qiskit/qiskit/issues/12834 backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map + inst_map = backend.defaults().instruction_schedule_map cr_schedule = inst_map.get("cx", (0, 1)) with builder.build() as test_sched: RZXCalibrationBuilder.rescale_cr_inst(self.u0p_play(cr_schedule), theta) @@ -284,7 +288,7 @@ def test_rzx_calibration_rotary_pulse_stretch(self, theta: float): """Test that rotary pulse durations are computed correctly.""" with self.assertWarns(DeprecationWarning): backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map + inst_map = backend.defaults().instruction_schedule_map cr_schedule = inst_map.get("cx", (0, 1)) with builder.build() as test_sched: RZXCalibrationBuilder.rescale_cr_inst(self.d1p_play(cr_schedule), theta) @@ -302,15 +306,16 @@ def test_raise(self): dag = circuit_to_dag(qc) with self.assertWarns(DeprecationWarning): backend = Fake7QPulseV1() - # The error is raised when calibrations in multi-qubit - # gates are not detected. - # We force this by removing the 'cx' entries from the - # instruction schedule map. - inst_map = backend.defaults().instruction_schedule_map + # The error is raised when calibrations in multi-qubit + # gates are not detected. + # We force this by removing the 'cx' entries from the + # instruction schedule map. + inst_map = backend.defaults().instruction_schedule_map for qubits in inst_map.qubits_with_instruction("cx"): inst_map.remove("cx", qubits) inst_map = backend.defaults().instruction_schedule_map - _pass = RZXCalibrationBuilder(inst_map) + with self.assertWarns(DeprecationWarning): + _pass = RZXCalibrationBuilder(inst_map) qubit_map = {qubit: i for i, qubit in enumerate(dag.qubits)} with self.assertRaises(QiskitError): @@ -328,8 +333,8 @@ def test_ecr_cx_forward(self): with self.assertWarns(DeprecationWarning): backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - _pass = RZXCalibrationBuilder(inst_map) + inst_map = backend.defaults().instruction_schedule_map + _pass = RZXCalibrationBuilder(inst_map) test_qc = PassManager(_pass).run(qc) cr_schedule = inst_map.get("cx", (0, 1)) @@ -342,7 +347,8 @@ def test_ecr_cx_forward(self): self.d1m_play(cr_schedule), ) - self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) def test_ecr_cx_reverse(self): """Test that correct pulse sequence is generated for non-native CR pair.""" @@ -354,8 +360,8 @@ def test_ecr_cx_reverse(self): with self.assertWarns(DeprecationWarning): backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map - _pass = RZXCalibrationBuilder(inst_map) + inst_map = backend.defaults().instruction_schedule_map + _pass = RZXCalibrationBuilder(inst_map) test_qc = PassManager(_pass).run(qc) cr_schedule = inst_map.get("cx", (0, 1)) @@ -368,7 +374,8 @@ def test_ecr_cx_reverse(self): self.d1m_play(cr_schedule), ) - self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) def test_pass_alive_with_dcx_ish(self): """Test if the pass is not terminated by error with direct CX input.""" @@ -381,20 +388,23 @@ def test_pass_alive_with_dcx_ish(self): compensation_tone = Waveform(0.1 * np.ones(800, dtype=complex)) cx_sched.insert(0, Play(compensation_tone, DriveChannel(0)), inplace=True) - inst_map = InstructionScheduleMap() + with self.assertWarns(DeprecationWarning): + inst_map = InstructionScheduleMap() inst_map.add("cx", (1, 0), schedule=cx_sched) theta = pi / 3 rzx_qc = circuit.QuantumCircuit(2) rzx_qc.rzx(theta, 1, 0) - pass_ = RZXCalibrationBuilder(instruction_schedule_map=inst_map) + with self.assertWarns(DeprecationWarning): + pass_ = RZXCalibrationBuilder(instruction_schedule_map=inst_map) with self.assertWarns(UserWarning): # User warning that says q0 q1 is invalid cal_qc = PassManager(pass_).run(rzx_qc) self.assertEqual(cal_qc, rzx_qc) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestRZXCalibrationBuilderNoEcho(TestCalibrationBuilder): """Test RZXCalibrationBuilderNoEcho.""" @@ -442,9 +452,9 @@ def test_ecr_cx_forward(self): with self.assertWarns(DeprecationWarning): backend = Fake27QPulseV1() - inst_map = backend.defaults().instruction_schedule_map + inst_map = backend.defaults().instruction_schedule_map - _pass = RZXCalibrationBuilderNoEcho(inst_map) + _pass = RZXCalibrationBuilderNoEcho(inst_map) test_qc = PassManager(_pass).run(qc) cr_schedule = inst_map.get("cx", (0, 1)) @@ -454,7 +464,8 @@ def test_ecr_cx_forward(self): self.d1p_play(cr_schedule), ) - self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(schedule(test_qc, backend), target_qobj_transform(ref_sched)) # # TODO - write test for forward ECR native pulse # def test_ecr_forward(self): @@ -470,14 +481,16 @@ def test_pass_alive_with_dcx_ish(self): compensation_tone = Waveform(0.1 * np.ones(800, dtype=complex)) cx_sched.insert(0, Play(compensation_tone, DriveChannel(0)), inplace=True) - inst_map = InstructionScheduleMap() + with self.assertWarns(DeprecationWarning): + inst_map = InstructionScheduleMap() inst_map.add("cx", (1, 0), schedule=cx_sched) theta = pi / 3 rzx_qc = circuit.QuantumCircuit(2) rzx_qc.rzx(theta, 1, 0) - pass_ = RZXCalibrationBuilderNoEcho(instruction_schedule_map=inst_map) + with self.assertWarns(DeprecationWarning): + pass_ = RZXCalibrationBuilderNoEcho(instruction_schedule_map=inst_map) with self.assertWarns(UserWarning): # User warning that says q0 q1 is invalid cal_qc = PassManager(pass_).run(rzx_qc) @@ -485,6 +498,7 @@ def test_pass_alive_with_dcx_ish(self): @ddt +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestRXCalibrationBuilder(QiskitTestCase): """Test RXCalibrationBuilder.""" @@ -495,7 +509,8 @@ def compute_correct_rx_amplitude(self, rx_theta: float, sx_amp: float): def test_not_supported_if_no_sx_schedule(self): """Test that supported() returns False when the target does not have SX calibration.""" empty_target = Target() - tp = RXCalibrationBuilder(empty_target) + with self.assertWarns(DeprecationWarning): + tp = RXCalibrationBuilder(empty_target) qubits = (0,) node_op = DAGOpNode(RXGate(0.5), qubits, []) self.assertFalse(tp.supported(node_op, qubits)) @@ -505,8 +520,11 @@ def test_not_supported_if_sx_not_drag(self): target = Target() with builder.build() as square_sx_cal: builder.play(Square(amp=0.1, duration=160, phase=0), DriveChannel(0)) - target.add_instruction(SXGate(), {(0,): InstructionProperties(calibration=square_sx_cal)}) - tp = RXCalibrationBuilder(target) + with self.assertWarns(DeprecationWarning): + target.add_instruction( + SXGate(), {(0,): InstructionProperties(calibration=square_sx_cal)} + ) + tp = RXCalibrationBuilder(target) qubits = (0,) node_op = DAGOpNode(RXGate(0.5), qubits, []) self.assertFalse(tp.supported(node_op, qubits)) @@ -517,7 +535,8 @@ def test_raises_error_when_rotation_angle_not_assigned(self): The QiskitError occurs while trying to typecast the Parameter into a float. """ backend = GenericBackendV2(num_qubits=5, seed=42) - tp = RXCalibrationBuilder(backend.target) + with self.assertWarns(DeprecationWarning): + tp = RXCalibrationBuilder(backend.target) qubits = (0,) rx = RXGate(Parameter("theta")) with self.assertRaises(QiskitError): @@ -538,12 +557,13 @@ def test_pulse_schedule(self, theta: float): ), DriveChannel(0), ) - dummy_target.add_instruction( - SXGate(), {(0,): InstructionProperties(calibration=dummy_sx_cal)} - ) - tp = RXCalibrationBuilder(dummy_target) - test = tp.get_calibration(RXGate(theta), qubits=(0,)) + with self.assertWarns(DeprecationWarning): + dummy_target.add_instruction( + SXGate(), {(0,): InstructionProperties(calibration=dummy_sx_cal)} + ) + tp = RXCalibrationBuilder(dummy_target) + test = tp.get_calibration(RXGate(theta), qubits=(0,)) with builder.build(backend=backend) as correct_rx_schedule: builder.play( @@ -577,12 +597,15 @@ def test_with_normalizerxangles(self): ), inplace=True, ) - ism = InstructionScheduleMap() + with self.assertWarns(DeprecationWarning): + ism = InstructionScheduleMap() ism.add("sx", (0,), sched) - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=ism, seed=42) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2(num_qubits=5, calibrate_instructions=ism, seed=42) # NormalizeRXAngle pass should also be included because it's a required pass. - pm = PassManager(RXCalibrationBuilder(backend.target)) + with self.assertWarns(DeprecationWarning): + pm = PassManager(RXCalibrationBuilder(backend.target)) qc = QuantumCircuit(1) qc.rx(np.pi / 3, 0) @@ -592,4 +615,5 @@ def test_with_normalizerxangles(self): # Only RX(pi/3) should get a rx calibration. # The others should be converted to SX and X tc = pm.run(qc) - self.assertEqual(len(tc.calibrations["rx"]), 1) + with self.assertWarns(DeprecationWarning): + self.assertEqual(len(tc.calibrations["rx"]), 1) diff --git a/test/python/transpiler/test_dynamical_decoupling.py b/test/python/transpiler/test_dynamical_decoupling.py index 38c5c3c2d4ab..d56595cd639a 100644 --- a/test/python/transpiler/test_dynamical_decoupling.py +++ b/test/python/transpiler/test_dynamical_decoupling.py @@ -374,11 +374,13 @@ def test_insert_dd_with_pulse_gate_calibrations(self): # Change duration to 100 from the 50 in self.durations to make sure # gate duration is used correctly. - with pulse.builder.build() as x_sched: - pulse.builder.delay(100, pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.builder.build() as x_sched: + pulse.builder.delay(100, pulse.DriveChannel(0)) circ_in = self.ghz4.measure_all(inplace=False) - circ_in.add_calibration(XGate(), (0,), x_sched) + with self.assertWarns(DeprecationWarning): + circ_in.add_calibration(XGate(), (0,), x_sched) ghz4_dd = pm.run(circ_in) @@ -397,7 +399,8 @@ def test_insert_dd_with_pulse_gate_calibrations(self): expected = expected.compose(Delay(300), [1]) expected.measure_all() - expected.add_calibration(XGate(), (0,), x_sched) + with self.assertWarns(DeprecationWarning): + expected.add_calibration(XGate(), (0,), x_sched) self.assertEqual(ghz4_dd, expected) @@ -430,11 +433,12 @@ def test_insert_dd_with_pulse_gate_calibrations_with_parmas(self): # Change duration to 100 from the 50 in self.durations to make sure # gate duration is used correctly. amp = Parameter("amp") - with pulse.builder.build() as sched: - pulse.builder.play( - pulse.Gaussian(100, amp=amp, sigma=10.0), - pulse.DriveChannel(0), - ) + with self.assertWarns(DeprecationWarning): + with pulse.builder.build() as sched: + pulse.builder.play( + pulse.Gaussian(100, amp=amp, sigma=10.0), + pulse.DriveChannel(0), + ) class Echo(Gate): """Dummy Gate subclass for testing @@ -457,7 +461,8 @@ def __array__(self, dtype=None, copy=None): echo = Echo("echo", 1, [amp, 10.0]) circ_in = self.ghz4.measure_all(inplace=False) - circ_in.add_calibration(echo, (0,), sched) + with self.assertWarns(DeprecationWarning): + circ_in.add_calibration(echo, (0,), sched) dd_sequence = [echo, echo] pm = PassManager( @@ -484,7 +489,8 @@ def __array__(self, dtype=None, copy=None): expected = expected.compose(Delay(300), [1]) expected.measure_all() - expected.add_calibration(echo, (0,), sched) + with self.assertWarns(DeprecationWarning): + expected.add_calibration(echo, (0,), sched) self.assertEqual(ghz4_dd, expected) @@ -857,10 +863,14 @@ def test_dd_with_calibrations_with_parameters(self, param_value): rx_duration = int(param_value * 1000) - with pulse.build() as rx: - pulse.play(pulse.Gaussian(rx_duration, 0.1, rx_duration // 4), pulse.DriveChannel(1)) + with self.assertWarns(DeprecationWarning): + with pulse.build() as rx: + pulse.play( + pulse.Gaussian(rx_duration, 0.1, rx_duration // 4), pulse.DriveChannel(1) + ) - circ.add_calibration("rx", (1,), rx, params=[param_value]) + with self.assertWarns(DeprecationWarning): + circ.add_calibration("rx", (1,), rx, params=[param_value]) durations = InstructionDurations([("x", None, 100), ("cx", None, 300)]) diff --git a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py index 8f876dd261d7..0f279e4bb8c5 100644 --- a/test/python/transpiler/test_echo_rzx_weyl_decomposition.py +++ b/test/python/transpiler/test_echo_rzx_weyl_decomposition.py @@ -74,8 +74,11 @@ def test_rzx_number_native_weyl_decomposition(self): circuit.cx(qr[0], qr[1]) unitary_circuit = qi.Operator(circuit).data - - after = EchoRZXWeylDecomposition(self.inst_map)(circuit) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + after = EchoRZXWeylDecomposition(self.inst_map)(circuit) unitary_after = qi.Operator(after).data @@ -97,11 +100,19 @@ def test_h_number_non_native_weyl_decomposition_1(self): circuit_non_native.rzz(theta, qr[1], qr[0]) dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after = dag_to_circuit(pass_.run(dag)) dag_non_native = circuit_to_dag(circuit_non_native) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after_non_native = dag_to_circuit(pass_.run(dag_non_native)) circuit_rzx_number = self.count_gate_number("rzx", after) @@ -127,11 +138,19 @@ def test_h_number_non_native_weyl_decomposition_2(self): circuit_non_native.swap(qr[1], qr[0]) dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after = dag_to_circuit(pass_.run(dag)) dag_non_native = circuit_to_dag(circuit_non_native) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after_non_native = dag_to_circuit(pass_.run(dag_non_native)) circuit_rzx_number = self.count_gate_number("rzx", after) @@ -166,7 +185,11 @@ def test_weyl_decomposition_gate_angles(self): unitary_circuit = qi.Operator(circuit).data dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after = dag_to_circuit(pass_.run(dag)) dag_after = circuit_to_dag(after) @@ -221,7 +244,11 @@ def test_weyl_unitaries_random_circuit(self): unitary_circuit = qi.Operator(circuit).data dag = circuit_to_dag(circuit) - pass_ = EchoRZXWeylDecomposition(self.inst_map) + with self.assertWarnsRegex( + DeprecationWarning, + expected_regex="The entire Qiskit Pulse package", + ): + pass_ = EchoRZXWeylDecomposition(self.inst_map) after = dag_to_circuit(pass_.run(dag)) unitary_after = qi.Operator(after).data diff --git a/test/python/transpiler/test_gate_direction.py b/test/python/transpiler/test_gate_direction.py index 569a210f8a9d..c22ad4d58b41 100644 --- a/test/python/transpiler/test_gate_direction.py +++ b/test/python/transpiler/test_gate_direction.py @@ -496,7 +496,8 @@ def test_target_cannot_flip_message_calibrated(self): gate = Gate("my_2q_gate", 2, []) circuit = QuantumCircuit(2) circuit.append(gate, (1, 0)) - circuit.add_calibration(gate, (0, 1), pulse.ScheduleBlock()) + with self.assertWarns(DeprecationWarning): + circuit.add_calibration(gate, (0, 1), pulse.ScheduleBlock()) pass_ = GateDirection(None, target) with self.assertRaisesRegex(TranspilerError, "'my_2q_gate' would be supported.*"): @@ -524,7 +525,8 @@ def test_allows_calibrated_gates_coupling_map(self): gate = Gate("my_2q_gate", 2, []) circuit = QuantumCircuit(2) circuit.append(gate, (0, 1)) - circuit.add_calibration(gate, (0, 1), pulse.ScheduleBlock()) + with self.assertWarns(DeprecationWarning): + circuit.add_calibration(gate, (0, 1), pulse.ScheduleBlock()) pass_ = GateDirection(cm) self.assertEqual(pass_(circuit), circuit) @@ -538,7 +540,8 @@ def test_allows_calibrated_gates_target(self): gate = Gate("my_2q_gate", 2, []) circuit = QuantumCircuit(2) circuit.append(gate, (0, 1)) - circuit.add_calibration(gate, (0, 1), pulse.ScheduleBlock()) + with self.assertWarns(DeprecationWarning): + circuit.add_calibration(gate, (0, 1), pulse.ScheduleBlock()) pass_ = GateDirection(None, target) self.assertEqual(pass_(circuit), circuit) diff --git a/test/python/transpiler/test_instruction_alignments.py b/test/python/transpiler/test_instruction_alignments.py index 1431449779b3..74d5cc5e825e 100644 --- a/test/python/transpiler/test_instruction_alignments.py +++ b/test/python/transpiler/test_instruction_alignments.py @@ -17,8 +17,10 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import ValidatePulseGates from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestPulseGateValidation(QiskitTestCase): """A test for pulse gate validation pass.""" @@ -27,16 +29,19 @@ def test_invalid_pulse_duration(self): # this is invalid duration pulse # this will cause backend error since this doesn't fit with waveform memory chunk. - custom_gate = pulse.Schedule(name="custom_x_gate") - custom_gate.insert( - 0, pulse.Play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)), inplace=True - ) + with self.assertWarns(DeprecationWarning): + custom_gate = pulse.Schedule(name="custom_x_gate") + custom_gate.insert( + 0, pulse.Play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)), inplace=True + ) circuit = QuantumCircuit(1) circuit.x(0) - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + with self.assertWarns(DeprecationWarning): + circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) + with self.assertWarns(DeprecationWarning): + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) with self.assertRaises(TranspilerError): pm.run(circuit) @@ -52,9 +57,11 @@ def test_short_pulse_duration(self): circuit = QuantumCircuit(1) circuit.x(0) - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + with self.assertWarns(DeprecationWarning): + circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) + with self.assertWarns(DeprecationWarning): + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) with self.assertRaises(TranspilerError): pm.run(circuit) @@ -74,9 +81,11 @@ def test_short_pulse_duration_multiple_pulse(self): circuit = QuantumCircuit(1) circuit.x(0) - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + with self.assertWarns(DeprecationWarning): + circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) + with self.assertWarns(DeprecationWarning): + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) with self.assertRaises(TranspilerError): pm.run(circuit) @@ -91,10 +100,12 @@ def test_valid_pulse_duration(self): circuit = QuantumCircuit(1) circuit.x(0) - circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + with self.assertWarns(DeprecationWarning): + circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) # just not raise an error - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) + with self.assertWarns(DeprecationWarning): + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) pm.run(circuit) def test_no_calibration(self): @@ -104,5 +115,6 @@ def test_no_calibration(self): circuit.x(0) # just not raise an error - pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) + with self.assertWarns(DeprecationWarning): + pm = PassManager(ValidatePulseGates(granularity=16, min_length=64)) pm.run(circuit) diff --git a/test/python/transpiler/test_instruction_durations.py b/test/python/transpiler/test_instruction_durations.py index c59d0ff2a6cc..d9a3ef2b1773 100644 --- a/test/python/transpiler/test_instruction_durations.py +++ b/test/python/transpiler/test_instruction_durations.py @@ -96,7 +96,8 @@ def test_fail_if_get_unbounded_duration_with_unit_conversion_when_dt_is_not_prov def test_from_backend_with_backendv2(self): """Test if `from_backend()` method allows using BackendV2""" - backend = GenericBackendV2(num_qubits=4, calibrate_instructions=True, seed=42) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2(num_qubits=4, calibrate_instructions=True, seed=42) inst_durations = InstructionDurations.from_backend(backend) self.assertEqual(inst_durations, backend.target.durations()) self.assertIsInstance(inst_durations, InstructionDurations) diff --git a/test/python/transpiler/test_passmanager.py b/test/python/transpiler/test_passmanager.py index 60375a820655..6764745facd3 100644 --- a/test/python/transpiler/test_passmanager.py +++ b/test/python/transpiler/test_passmanager.py @@ -106,7 +106,8 @@ def callback(**kwargs): calls.append(out_dict) passmanager = PassManager() - passmanager.append(RXCalibrationBuilder()) + with self.assertWarns(DeprecationWarning): + passmanager.append(RXCalibrationBuilder()) passmanager.run(circuit, callback=callback) self.assertEqual(len(calls), 2) self.assertEqual(len(calls[0]), 5) diff --git a/test/python/transpiler/test_passmanager_config.py b/test/python/transpiler/test_passmanager_config.py index 569f67738a16..85cbb7909aef 100644 --- a/test/python/transpiler/test_passmanager_config.py +++ b/test/python/transpiler/test_passmanager_config.py @@ -45,7 +45,8 @@ def test_config_from_backend_v2(self): backend = GenericBackendV2(num_qubits=27, seed=42) config = PassManagerConfig.from_backend(backend) self.assertEqual(config.basis_gates, backend.operation_names) - self.assertEqual(config.inst_map, backend.instruction_schedule_map) + with self.assertWarns(DeprecationWarning): + self.assertEqual(config.inst_map, backend.instruction_schedule_map) self.assertEqual(config.coupling_map.get_edges(), backend.coupling_map.get_edges()) def test_invalid_backend(self): @@ -86,13 +87,14 @@ def test_from_backend_and_user(self): qr = QuantumRegister(4, "qr") initial_layout = [None, qr[0], qr[1], qr[2], None, qr[3]] - backend = GenericBackendV2( - num_qubits=20, - coupling_map=ALMADEN_CMAP, - basis_gates=["id", "u1", "u2", "u3", "cx"], - calibrate_instructions=None, - seed=42, - ) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2( + num_qubits=20, + coupling_map=ALMADEN_CMAP, + basis_gates=["id", "u1", "u2", "u3", "cx"], + calibrate_instructions=None, + seed=42, + ) config = PassManagerConfig.from_backend( backend, basis_gates=["user_gate"], initial_layout=initial_layout ) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index b762d84032bb..00fd8944061c 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -1316,15 +1316,16 @@ def test_default_optimization_level_target_first_pos_arg(self): def test_with_no_backend(self, optimization_level): """Test a passmanager is constructed with no backend and optimization level.""" target = GenericBackendV2(num_qubits=7, coupling_map=LAGOS_CMAP, seed=42) - pm = generate_preset_pass_manager( - optimization_level, - coupling_map=target.coupling_map, - basis_gates=target.operation_names, - inst_map=target.instruction_schedule_map, - instruction_durations=target.instruction_durations, - timing_constraints=target.target.timing_constraints(), - target=target.target, - ) + with self.assertWarns(DeprecationWarning): + pm = generate_preset_pass_manager( + optimization_level, + coupling_map=target.coupling_map, + basis_gates=target.operation_names, + inst_map=target.instruction_schedule_map, + instruction_durations=target.instruction_durations, + timing_constraints=target.target.timing_constraints(), + target=target.target, + ) self.assertIsInstance(pm, PassManager) @data(0, 1, 2, 3) diff --git a/test/python/transpiler/test_pulse_gate_pass.py b/test/python/transpiler/test_pulse_gate_pass.py index 1dd42d662860..e617f64d8859 100644 --- a/test/python/transpiler/test_pulse_gate_pass.py +++ b/test/python/transpiler/test_pulse_gate_pass.py @@ -19,14 +19,16 @@ from qiskit.providers.models.backendconfiguration import GateConfig from qiskit.quantum_info.random import random_unitary from test import QiskitTestCase # pylint: disable=wrong-import-order - +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings from ..legacy_cmaps import BOGOTA_CMAP @ddt.ddt +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestPulseGate(QiskitTestCase): """Integration test of pulse gate pass with custom backend.""" + @ignore_pulse_deprecation_warnings def setUp(self): super().setUp() @@ -78,14 +80,16 @@ def test_transpile_with_bare_backend(self): transpiled_qc = transpile(qc, backend, initial_layout=[0, 1]) ref_calibration = {} - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) def test_transpile_with_backend_target(self): """Test transpile without custom calibrations from target.""" - target = GenericBackendV2( - num_qubits=5, coupling_map=BOGOTA_CMAP, calibrate_instructions=True, seed=42 - ).target + with self.assertWarns(DeprecationWarning): + target = GenericBackendV2( + num_qubits=5, coupling_map=BOGOTA_CMAP, calibrate_instructions=True, seed=42 + ).target qc = circuit.QuantumCircuit(2) qc.sx(0) @@ -97,15 +101,16 @@ def test_transpile_with_backend_target(self): transpiled_qc = transpile(qc, initial_layout=[0, 1], target=target) ref_calibration = {} - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) def test_transpile_with_custom_basis_gate(self): """Test transpile with custom calibrations.""" with self.assertWarns(DeprecationWarning): # TODO Move this test to backendV2 backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) - backend.defaults().instruction_schedule_map.add("sx", (1,), self.custom_sx_q1) + backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) + backend.defaults().instruction_schedule_map.add("sx", (1,), self.custom_sx_q1) # Remove timing constraints to avoid triggering # scheduling passes. backend.configuration().timing_constraints = {} @@ -130,21 +135,22 @@ def test_transpile_with_custom_basis_gate(self): ((1,), ()): self.custom_sx_q1, } } - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) def test_transpile_with_custom_basis_gate_in_target(self): """Test transpile with custom calibrations.""" with self.assertWarns(DeprecationWarning): backend_pulse = Fake27QPulseV1() - target = GenericBackendV2( - num_qubits=5, - coupling_map=BOGOTA_CMAP, - calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, - seed=42, - ).target + target = GenericBackendV2( + num_qubits=5, + coupling_map=BOGOTA_CMAP, + calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, + seed=42, + ).target - target["sx"][(0,)].calibration = self.custom_sx_q0 - target["sx"][(1,)].calibration = self.custom_sx_q1 + target["sx"][(0,)].calibration = self.custom_sx_q0 + target["sx"][(1,)].calibration = self.custom_sx_q1 qc = circuit.QuantumCircuit(2) qc.sx(0) @@ -161,14 +167,15 @@ def test_transpile_with_custom_basis_gate_in_target(self): ((1,), ()): self.custom_sx_q1, } } - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) def test_transpile_with_instmap(self): """Test providing instruction schedule map.""" with self.assertWarns(DeprecationWarning): # TODO Move this test to backendV2 backend = Fake27QPulseV1() - instmap = backend.defaults().instruction_schedule_map + instmap = backend.defaults().instruction_schedule_map instmap.add("sx", (0,), self.custom_sx_q0) instmap.add("sx", (1,), self.custom_sx_q1) @@ -200,19 +207,20 @@ def test_transpile_with_instmap(self): ((1,), ()): self.custom_sx_q1, } } - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) def test_transpile_with_custom_gate(self): """Test providing non-basis gate.""" with self.assertWarns(DeprecationWarning): # TODO Move this test to backendV2 backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) - backend.defaults().instruction_schedule_map.add( - "my_gate", (1,), self.my_gate_q1, arguments=["P0"] - ) + backend.defaults().instruction_schedule_map.add( + "my_gate", (0,), self.my_gate_q0, arguments=["P0"] + ) + backend.defaults().instruction_schedule_map.add( + "my_gate", (1,), self.my_gate_q1, arguments=["P0"] + ) # Add gate to backend configuration backend.configuration().basis_gates.append("my_gate") with self.assertWarns(DeprecationWarning): @@ -244,16 +252,17 @@ def test_transpile_with_custom_gate(self): ((1,), (2.0,)): my_gate_q1_2_0, } } - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) def test_transpile_with_parameterized_custom_gate(self): """Test providing non-basis gate, which is kept parameterized throughout transpile.""" with self.assertWarns(DeprecationWarning): # TODO convert this to BackendV2/Target backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) + backend.defaults().instruction_schedule_map.add( + "my_gate", (0,), self.my_gate_q0, arguments=["P0"] + ) # Add gate to backend configuration backend.configuration().basis_gates.append("my_gate") with self.assertWarns(DeprecationWarning): @@ -283,16 +292,17 @@ def test_transpile_with_parameterized_custom_gate(self): ((0,), (param,)): my_gate_q0_p, } } - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) def test_transpile_with_multiple_circuits(self): """Test transpile with multiple circuits with custom gate.""" with self.assertWarns(DeprecationWarning): # TODO move this test to backendV2 backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) + backend.defaults().instruction_schedule_map.add( + "my_gate", (0,), self.my_gate_q0, arguments=["P0"] + ) # Add gate to backend configuration backend.configuration().basis_gates.append("my_gate") with self.assertWarns(DeprecationWarning): @@ -323,16 +333,17 @@ def test_transpile_with_multiple_circuits(self): {self.sched_param: param}, inplace=False ) ref_calibration = {"my_gate": {((0,), (param,)): my_gate_q0_x}} - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) def test_multiple_instructions_with_different_parameters(self): """Test adding many instruction with different parameter binding.""" with self.assertWarns(DeprecationWarning): # TODO Move this test to backendV2 backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add( - "my_gate", (0,), self.my_gate_q0, arguments=["P0"] - ) + backend.defaults().instruction_schedule_map.add( + "my_gate", (0,), self.my_gate_q0, arguments=["P0"] + ) # Add gate to backend configuration backend.configuration().basis_gates.append("my_gate") with self.assertWarns(DeprecationWarning): @@ -367,14 +378,15 @@ def test_multiple_instructions_with_different_parameters(self): ((0,), (3.0,)): my_gate_q0_3_0, } } - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) def test_transpile_with_different_qubit(self): """Test transpile with qubit without custom gate.""" with self.assertWarns(DeprecationWarning): # TODO Move this test to backendV2 backend = Fake27QPulseV1() - backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) + backend.defaults().instruction_schedule_map.add("sx", (0,), self.custom_sx_q0) # Remove timing constraints to avoid triggering # scheduling passes. backend.configuration().timing_constraints = {} @@ -390,7 +402,8 @@ def test_transpile_with_different_qubit(self): ): transpiled_qc = transpile(qc, backend, initial_layout=[3]) - self.assertDictEqual(transpiled_qc.calibrations, {}) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, {}) @ddt.data(0, 1, 2, 3) def test_transpile_with_both_instmap_and_empty_target(self, opt_level): @@ -401,33 +414,34 @@ def test_transpile_with_both_instmap_and_empty_target(self, opt_level): """ with self.assertWarns(DeprecationWarning): backend = Fake27QPulseV1() - instmap = backend.defaults().instruction_schedule_map + instmap = backend.defaults().instruction_schedule_map instmap.add("sx", (0,), self.custom_sx_q0) instmap.add("sx", (1,), self.custom_sx_q1) instmap.add("cx", (0, 1), self.custom_cx_q01) with self.assertWarns(DeprecationWarning): backend_pulse = Fake27QPulseV1() - # This doesn't have custom schedule definition - target = GenericBackendV2( - num_qubits=5, - coupling_map=BOGOTA_CMAP, - calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, - seed=42, - ).target + # This doesn't have custom schedule definition + target = GenericBackendV2( + num_qubits=5, + coupling_map=BOGOTA_CMAP, + calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, + seed=42, + ).target qc = circuit.QuantumCircuit(2) qc.append(random_unitary(4, seed=123), [0, 1]) qc.measure_all() - transpiled_qc = transpile( - qc, - optimization_level=opt_level, - basis_gates=["sx", "rz", "x", "cx"], - inst_map=instmap, - target=target, - initial_layout=[0, 1], - ) + with self.assertWarns(DeprecationWarning): + transpiled_qc = transpile( + qc, + optimization_level=opt_level, + basis_gates=["sx", "rz", "x", "cx"], + inst_map=instmap, + target=target, + initial_layout=[0, 1], + ) ref_calibration = { "sx": { ((0,), ()): self.custom_sx_q0, @@ -437,7 +451,8 @@ def test_transpile_with_both_instmap_and_empty_target(self, opt_level): ((0, 1), ()): self.custom_cx_q01, }, } - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) @ddt.data(0, 1, 2, 3) def test_transpile_with_instmap_with_v2backend(self, opt_level): @@ -448,7 +463,7 @@ def test_transpile_with_instmap_with_v2backend(self, opt_level): with self.assertWarns(DeprecationWarning): backend = Fake27QPulseV1() - instmap = backend.defaults().instruction_schedule_map + instmap = backend.defaults().instruction_schedule_map instmap.add("sx", (0,), self.custom_sx_q0) instmap.add("sx", (1,), self.custom_sx_q1) instmap.add("cx", (0, 1), self.custom_cx_q01) @@ -460,19 +475,20 @@ def test_transpile_with_instmap_with_v2backend(self, opt_level): with self.assertWarns(DeprecationWarning): backend_pulse = Fake27QPulseV1() - backend = GenericBackendV2( - num_qubits=5, - calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, - seed=42, - ) - - transpiled_qc = transpile( - qc, - backend, - optimization_level=opt_level, - inst_map=instmap, - initial_layout=[0, 1], - ) + backend = GenericBackendV2( + num_qubits=5, + calibrate_instructions=backend_pulse.defaults().instruction_schedule_map, + seed=42, + ) + + with self.assertWarns(DeprecationWarning): + transpiled_qc = transpile( + qc, + backend, + optimization_level=opt_level, + inst_map=instmap, + initial_layout=[0, 1], + ) ref_calibration = { "sx": { ((0,), ()): self.custom_sx_q0, @@ -482,7 +498,8 @@ def test_transpile_with_instmap_with_v2backend(self, opt_level): ((0, 1), ()): self.custom_cx_q01, }, } - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) @ddt.data(0, 1, 2, 3) def test_transpile_with_instmap_with_v2backend_with_custom_gate(self, opt_level): @@ -498,7 +515,7 @@ def test_transpile_with_instmap_with_v2backend_with_custom_gate(self, opt_level) pulse.play(pulse.Constant(100, 0.4), pulse.DriveChannel(0)) with self.assertWarns(DeprecationWarning): backend = Fake27QPulseV1() - instmap = backend.defaults().instruction_schedule_map + instmap = backend.defaults().instruction_schedule_map instmap.add("rabi12", (0,), rabi12) gate = circuit.Gate("rabi12", 1, []) @@ -506,20 +523,22 @@ def test_transpile_with_instmap_with_v2backend_with_custom_gate(self, opt_level) qc.append(gate, [0]) qc.measure_all() - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True, seed=42) - transpiled_qc = transpile( - qc, - backend, - optimization_level=opt_level, - inst_map=instmap, - initial_layout=[0], - ) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True, seed=42) + transpiled_qc = transpile( + qc, + backend, + optimization_level=opt_level, + inst_map=instmap, + initial_layout=[0], + ) ref_calibration = { "rabi12": { ((0,), ()): rabi12, } } - self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) + with self.assertWarns(DeprecationWarning): + self.assertDictEqual(transpiled_qc.calibrations, ref_calibration) def test_transpile_with_instmap_not_mutate_backend(self): """Do not override default backend target when transpile with inst map. @@ -529,28 +548,30 @@ def test_transpile_with_instmap_not_mutate_backend(self): This should not override the source object since the same backend may be used for future transpile without intention of instruction overriding. """ - backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True, seed=42) - original_sx0 = backend.target["sx"][(0,)].calibration + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2(num_qubits=5, calibrate_instructions=True, seed=42) + original_sx0 = backend.target["sx"][(0,)].calibration with self.assertWarns(DeprecationWarning): backend_pulse = Fake27QPulseV1() - instmap = backend_pulse.defaults().instruction_schedule_map + instmap = backend_pulse.defaults().instruction_schedule_map instmap.add("sx", (0,), self.custom_sx_q0) qc = circuit.QuantumCircuit(1) qc.sx(0) qc.measure_all() - transpiled_qc = transpile( - qc, - backend, - inst_map=instmap, - initial_layout=[0], - ) - self.assertTrue(transpiled_qc.has_calibration_for(transpiled_qc.data[0])) - - self.assertEqual( - backend.target["sx"][(0,)].calibration, - original_sx0, - ) + with self.assertWarns(DeprecationWarning): + transpiled_qc = transpile( + qc, + backend, + inst_map=instmap, + initial_layout=[0], + ) + self.assertTrue(transpiled_qc.has_calibration_for(transpiled_qc.data[0])) + + self.assertEqual( + backend.target["sx"][(0,)].calibration, + original_sx0, + ) diff --git a/test/python/transpiler/test_remove_identity_equivalent.py b/test/python/transpiler/test_remove_identity_equivalent.py new file mode 100644 index 000000000000..1db392d3654b --- /dev/null +++ b/test/python/transpiler/test_remove_identity_equivalent.py @@ -0,0 +1,185 @@ +# 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. + +"""Tests for the DropNegligible transpiler pass.""" + +import numpy as np + +from qiskit.circuit import Parameter, QuantumCircuit, QuantumRegister, Gate +from qiskit.circuit.library import ( + CPhaseGate, + RXGate, + RXXGate, + RYGate, + RYYGate, + RZGate, + RZZGate, + XXMinusYYGate, + XXPlusYYGate, + GlobalPhaseGate, +) +from qiskit.quantum_info import Operator +from qiskit.transpiler.passes import RemoveIdentityEquivalent +from qiskit.transpiler.target import Target, InstructionProperties + +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +class TestDropNegligible(QiskitTestCase): + """Test the DropNegligible pass.""" + + def test_drops_negligible_gates(self): + """Test that negligible gates are dropped.""" + qubits = QuantumRegister(2) + circuit = QuantumCircuit(qubits) + a, b = qubits + circuit.append(CPhaseGate(1e-5), [a, b]) + circuit.append(CPhaseGate(1e-8), [a, b]) + circuit.append(RXGate(1e-5), [a]) + circuit.append(RXGate(1e-8), [a]) + circuit.append(RYGate(1e-5), [a]) + circuit.append(RYGate(1e-8), [a]) + circuit.append(RZGate(1e-5), [a]) + circuit.append(RZGate(1e-8), [a]) + circuit.append(RXXGate(1e-5), [a, b]) + circuit.append(RXXGate(1e-8), [a, b]) + circuit.append(RYYGate(1e-5), [a, b]) + circuit.append(RYYGate(1e-8), [a, b]) + circuit.append(RZZGate(1e-5), [a, b]) + circuit.append(RZZGate(1e-8), [a, b]) + circuit.append(XXPlusYYGate(1e-5, 1e-8), [a, b]) + circuit.append(XXPlusYYGate(1e-8, 1e-8), [a, b]) + circuit.append(XXMinusYYGate(1e-5, 1e-8), [a, b]) + circuit.append(XXMinusYYGate(1e-8, 1e-8), [a, b]) + transpiled = RemoveIdentityEquivalent()(circuit) + self.assertEqual(circuit.count_ops()["cp"], 2) + self.assertEqual(transpiled.count_ops()["cp"], 1) + self.assertEqual(circuit.count_ops()["rx"], 2) + self.assertEqual(transpiled.count_ops()["rx"], 1) + self.assertEqual(circuit.count_ops()["ry"], 2) + self.assertEqual(transpiled.count_ops()["ry"], 1) + self.assertEqual(circuit.count_ops()["rz"], 2) + self.assertEqual(transpiled.count_ops()["rz"], 1) + self.assertEqual(circuit.count_ops()["rxx"], 2) + self.assertEqual(transpiled.count_ops()["rxx"], 1) + self.assertEqual(circuit.count_ops()["ryy"], 2) + self.assertEqual(transpiled.count_ops()["ryy"], 1) + self.assertEqual(circuit.count_ops()["rzz"], 2) + self.assertEqual(transpiled.count_ops()["rzz"], 1) + self.assertEqual(circuit.count_ops()["xx_plus_yy"], 2) + self.assertEqual(transpiled.count_ops()["xx_plus_yy"], 1) + self.assertEqual(circuit.count_ops()["xx_minus_yy"], 2) + self.assertEqual(transpiled.count_ops()["xx_minus_yy"], 1) + np.testing.assert_allclose( + np.array(Operator(circuit)), np.array(Operator(transpiled)), atol=1e-7 + ) + + def test_handles_parameters(self): + """Test that gates with parameters are ignored gracefully.""" + qubits = QuantumRegister(2) + circuit = QuantumCircuit(qubits) + a, b = qubits + theta = Parameter("theta") + circuit.append(CPhaseGate(theta), [a, b]) + circuit.append(CPhaseGate(1e-5), [a, b]) + circuit.append(CPhaseGate(1e-8), [a, b]) + transpiled = RemoveIdentityEquivalent()(circuit) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 2) + + def test_approximation_degree(self): + """Test that approximation degree handled correctly.""" + qubits = QuantumRegister(2) + circuit = QuantumCircuit(qubits) + a, b = qubits + circuit.append(CPhaseGate(1e-4), [a, b]) + # fidelity 0.9999850001249996 which is above the threshold and not excluded + # so 1e-2 is the only gate remaining + circuit.append(CPhaseGate(1e-2), [a, b]) + circuit.append(CPhaseGate(1e-20), [a, b]) + transpiled = RemoveIdentityEquivalent(approximation_degree=0.9999999)(circuit) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 1) + self.assertEqual(transpiled.data[0].operation.params[0], 1e-2) + + def test_target_approx_none(self): + """Test error rate with target.""" + + target = Target() + props = {(0, 1): InstructionProperties(error=1e-10)} + target.add_instruction(CPhaseGate(Parameter("theta")), props) + circuit = QuantumCircuit(2) + circuit.append(CPhaseGate(1e-4), [0, 1]) + circuit.append(CPhaseGate(1e-2), [0, 1]) + circuit.append(CPhaseGate(1e-20), [0, 1]) + transpiled = RemoveIdentityEquivalent(approximation_degree=None, target=target)(circuit) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 2) + + def test_target_approx_approx_degree(self): + """Test error rate with target.""" + + target = Target() + props = {(0, 1): InstructionProperties(error=1e-10)} + target.add_instruction(CPhaseGate(Parameter("theta")), props) + circuit = QuantumCircuit(2) + circuit.append(CPhaseGate(1e-4), [0, 1]) + circuit.append(CPhaseGate(1e-2), [0, 1]) + circuit.append(CPhaseGate(1e-20), [0, 1]) + transpiled = RemoveIdentityEquivalent(approximation_degree=0.9999999, target=target)( + circuit + ) + self.assertEqual(circuit.count_ops()["cp"], 3) + self.assertEqual(transpiled.count_ops()["cp"], 2) + + def test_custom_gate_no_matrix(self): + """Test that opaque gates are ignored.""" + + class CustomOpaqueGate(Gate): + """Custom opaque gate.""" + + def __init__(self): + super().__init__("opaque", 2, []) + + qc = QuantumCircuit(3) + qc.append(CustomOpaqueGate(), [0, 1]) + transpiled = RemoveIdentityEquivalent()(qc) + self.assertEqual(qc, transpiled) + + def test_custom_gate_identity_matrix(self): + """Test that custom gates with matrix are evaluated.""" + + class CustomGate(Gate): + """Custom gate.""" + + def __init__(self): + super().__init__("custom", 3, []) + + def to_matrix(self): + return np.eye(8, dtype=complex) + + qc = QuantumCircuit(3) + qc.append(CustomGate(), [0, 1, 2]) + transpiled = RemoveIdentityEquivalent()(qc) + expected = QuantumCircuit(3) + self.assertEqual(expected, transpiled) + + def test_global_phase_ignored(self): + """Test that global phase gate isn't considered.""" + + qc = QuantumCircuit(1) + qc.id(0) + qc.append(GlobalPhaseGate(0)) + transpiled = RemoveIdentityEquivalent()(qc) + expected = QuantumCircuit(1) + expected.append(GlobalPhaseGate(0)) + self.assertEqual(transpiled, expected) diff --git a/test/python/transpiler/test_sabre_swap.py b/test/python/transpiler/test_sabre_swap.py index c342def0ba14..d196e4982ea9 100644 --- a/test/python/transpiler/test_sabre_swap.py +++ b/test/python/transpiler/test_sabre_swap.py @@ -13,7 +13,7 @@ """Test the Sabre Swap pass""" import unittest - +import warnings import itertools import ddt @@ -1327,13 +1327,17 @@ class TestSabreSwapRandomCircuitValidOutput(QiskitTestCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.backend = GenericBackendV2( - num_qubits=27, - calibrate_instructions=True, - control_flow=True, - coupling_map=MUMBAI_CMAP, - seed=42, - ) + with warnings.catch_warnings(): + # Catch warnings since self.assertWarns cannot be used here. + # The `calibrate_instructions` argument is deprecated in Qiksit 1.3 + warnings.simplefilter("ignore", category=DeprecationWarning) + cls.backend = GenericBackendV2( + num_qubits=27, + calibrate_instructions=True, + control_flow=True, + coupling_map=MUMBAI_CMAP, + seed=42, + ) cls.coupling_edge_set = {tuple(x) for x in cls.backend.coupling_map} cls.basis_gates = set(cls.backend.operation_names) diff --git a/test/python/transpiler/test_scheduling_padding_pass.py b/test/python/transpiler/test_scheduling_padding_pass.py index f2f4f6a24bc6..a1ae04d5e68e 100644 --- a/test/python/transpiler/test_scheduling_padding_pass.py +++ b/test/python/transpiler/test_scheduling_padding_pass.py @@ -794,8 +794,9 @@ def test_scheduling_with_calibration(self): qc.x(1) qc.cx(0, 1) - xsched = Schedule(Play(Constant(300, 0.1), DriveChannel(0))) - qc.add_calibration("x", (0,), xsched) + with self.assertWarns(DeprecationWarning): + xsched = Schedule(Play(Constant(300, 0.1), DriveChannel(0))) + qc.add_calibration("x", (0,), xsched) durations = InstructionDurations([("x", None, 160), ("cx", None, 600)]) pm = PassManager([ASAPScheduleAnalysis(durations), PadDelay()]) @@ -808,7 +809,8 @@ def test_scheduling_with_calibration(self): expected.x(1) expected.delay(160, 0) expected.cx(0, 1) - expected.add_calibration("x", (0,), xsched) + with self.assertWarns(DeprecationWarning): + expected.add_calibration("x", (0,), xsched) self.assertEqual(expected, scheduled) diff --git a/test/python/transpiler/test_stochastic_swap.py b/test/python/transpiler/test_stochastic_swap.py index ee5d8a1dad31..7e4dfe63de3c 100644 --- a/test/python/transpiler/test_stochastic_swap.py +++ b/test/python/transpiler/test_stochastic_swap.py @@ -13,6 +13,7 @@ """Test the Stochastic Swap pass""" import unittest +import warnings import numpy.random @@ -1527,9 +1528,13 @@ class TestStochasticSwapRandomCircuitValidOutput(QiskitTestCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.backend = GenericBackendV2( - num_qubits=27, calibrate_instructions=True, control_flow=True, seed=42 - ) + with warnings.catch_warnings(): + # Catch warnings since self.assertWarns cannot be used here. + # The `calibrate_instructions` argument is deprecated in Qiksit 1.3 + warnings.simplefilter("ignore", category=DeprecationWarning) + cls.backend = GenericBackendV2( + num_qubits=27, calibrate_instructions=True, control_flow=True, seed=42 + ) cls.coupling_edge_set = {tuple(x) for x in cls.backend.coupling_map} cls.basis_gates = set(cls.backend.operation_names) diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 928cd09c8dd1..980924224d15 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -11,6 +11,7 @@ # that they have been altered from the originals. # pylint: disable=missing-docstring +from pickle import loads, dumps import math import numpy as np @@ -30,6 +31,7 @@ CCXGate, RZXGate, CZGate, + UnitaryGate, ) from qiskit.circuit import IfElseOp, ForLoopOp, WhileLoopOp, SwitchCaseOp from qiskit.circuit.measure import Measure @@ -1166,6 +1168,23 @@ def test_instruction_supported_no_args(self): def test_instruction_supported_no_operation(self): self.assertFalse(self.ibm_target.instruction_supported(qargs=(0,), parameters=[math.pi])) + def test_target_serialization_preserve_variadic(self): + """Checks that variadics are still seen as variadic after serialization""" + + target = Target("test", 2) + # Add variadic example gate with no properties. + target.add_instruction(UnitaryGate, None, "u_var") + + # Check that this this instruction is compatible with qargs (0,). Should be + # true since variadic operation can be used with any valid qargs. + self.assertTrue(target.instruction_supported("u_var", (0, 1))) + + # Rebuild the target using serialization + deserialized_target = loads(dumps(target)) + + # Perform check again, should not throw exception + self.assertTrue(deserialized_target.instruction_supported("u_var", (0, 1))) + class TestPulseTarget(QiskitTestCase): def setUp(self): @@ -1173,22 +1192,24 @@ def setUp(self): self.pulse_target = Target( dt=3e-7, granularity=2, min_length=4, pulse_alignment=8, acquire_alignment=8 ) - with pulse.build(name="sx_q0") as self.custom_sx_q0: - pulse.play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)) - with pulse.build(name="sx_q1") as self.custom_sx_q1: - pulse.play(pulse.Constant(100, 0.2), pulse.DriveChannel(1)) - sx_props = { - (0,): InstructionProperties( - duration=35.5e-9, error=0.000413, calibration=self.custom_sx_q0 - ), - (1,): InstructionProperties( - duration=35.5e-9, error=0.000502, calibration=self.custom_sx_q1 - ), - } + with self.assertWarns(DeprecationWarning): + with pulse.build(name="sx_q0") as self.custom_sx_q0: + pulse.play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)) + with pulse.build(name="sx_q1") as self.custom_sx_q1: + pulse.play(pulse.Constant(100, 0.2), pulse.DriveChannel(1)) + sx_props = { + (0,): InstructionProperties( + duration=35.5e-9, error=0.000413, calibration=self.custom_sx_q0 + ), + (1,): InstructionProperties( + duration=35.5e-9, error=0.000502, calibration=self.custom_sx_q1 + ), + } self.pulse_target.add_instruction(SXGate(), sx_props) def test_instruction_schedule_map(self): - inst_map = self.pulse_target.instruction_schedule_map() + with self.assertWarns(DeprecationWarning): + inst_map = self.pulse_target.instruction_schedule_map() self.assertIn("sx", inst_map.instructions) self.assertEqual(inst_map.qubits_with_instruction("sx"), [0, 1]) self.assertTrue("sx" in inst_map.qubit_instructions(0)) @@ -1209,8 +1230,9 @@ def test_instruction_schedule_map_ideal_sim_backend(self): Measure(), ]: ideal_sim_target.add_instruction(inst, {None: None}) - inst_map = ideal_sim_target.instruction_schedule_map() - self.assertEqual(InstructionScheduleMap(), inst_map) + with self.assertWarns(DeprecationWarning): + inst_map = ideal_sim_target.instruction_schedule_map() + self.assertEqual(InstructionScheduleMap(), inst_map) def test_str(self): expected = """Target @@ -1230,36 +1252,43 @@ def test_str(self): def test_update_from_instruction_schedule_map_add_instruction(self): target = Target() - inst_map = InstructionScheduleMap() + with self.assertWarns(DeprecationWarning): + inst_map = InstructionScheduleMap() inst_map.add("sx", 0, self.custom_sx_q0) inst_map.add("sx", 1, self.custom_sx_q1) - target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) - self.assertEqual(inst_map, target.instruction_schedule_map()) + with self.assertWarns(DeprecationWarning): + target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) + self.assertEqual(inst_map, target.instruction_schedule_map()) def test_update_from_instruction_schedule_map_with_schedule_parameter(self): self.pulse_target.dt = None - inst_map = InstructionScheduleMap() + with self.assertWarns(DeprecationWarning): + inst_map = InstructionScheduleMap() duration = Parameter("duration") - with pulse.build(name="sx_q0") as custom_sx: - pulse.play(pulse.Constant(duration, 0.2), pulse.DriveChannel(0)) + with self.assertWarns(DeprecationWarning): + with pulse.build(name="sx_q0") as custom_sx: + pulse.play(pulse.Constant(duration, 0.2), pulse.DriveChannel(0)) inst_map.add("sx", 0, custom_sx, ["duration"]) target = Target(dt=3e-7) - target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) - self.assertEqual(inst_map, target.instruction_schedule_map()) + with self.assertWarns(DeprecationWarning): + target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) + self.assertEqual(inst_map, target.instruction_schedule_map()) def test_update_from_instruction_schedule_map_update_schedule(self): self.pulse_target.dt = None - inst_map = InstructionScheduleMap() - with pulse.build(name="sx_q1") as custom_sx: - pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) + with self.assertWarns(DeprecationWarning): + inst_map = InstructionScheduleMap() + with pulse.build(name="sx_q1") as custom_sx: + pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) inst_map.add("sx", 0, self.custom_sx_q0) inst_map.add("sx", 1, custom_sx) - self.pulse_target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) - self.assertEqual(inst_map, self.pulse_target.instruction_schedule_map()) + with self.assertWarns(DeprecationWarning): + self.pulse_target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) + self.assertEqual(inst_map, self.pulse_target.instruction_schedule_map()) # Calibration doesn't change for q0 self.assertEqual(self.pulse_target["sx"][(0,)].duration, 35.5e-9) self.assertEqual(self.pulse_target["sx"][(0,)].error, 0.000413) @@ -1269,31 +1298,37 @@ def test_update_from_instruction_schedule_map_update_schedule(self): def test_update_from_instruction_schedule_map_new_instruction_no_name_map(self): target = Target() - inst_map = InstructionScheduleMap() + with self.assertWarns(DeprecationWarning): + inst_map = InstructionScheduleMap() inst_map.add("sx", 0, self.custom_sx_q0) inst_map.add("sx", 1, self.custom_sx_q1) - target.update_from_instruction_schedule_map(inst_map) - self.assertEqual(target["sx"][(0,)].calibration, self.custom_sx_q0) - self.assertEqual(target["sx"][(1,)].calibration, self.custom_sx_q1) + with self.assertWarns(DeprecationWarning): + target.update_from_instruction_schedule_map(inst_map) + self.assertEqual(target["sx"][(0,)].calibration, self.custom_sx_q0) + self.assertEqual(target["sx"][(1,)].calibration, self.custom_sx_q1) def test_update_from_instruction_schedule_map_new_qarg_raises(self): - inst_map = InstructionScheduleMap() + with self.assertWarns(DeprecationWarning): + inst_map = InstructionScheduleMap() inst_map.add("sx", 0, self.custom_sx_q0) inst_map.add("sx", 1, self.custom_sx_q1) inst_map.add("sx", 2, self.custom_sx_q1) - self.pulse_target.update_from_instruction_schedule_map(inst_map) + with self.assertWarns(DeprecationWarning): + self.pulse_target.update_from_instruction_schedule_map(inst_map) self.assertFalse(self.pulse_target.instruction_supported("sx", (2,))) def test_update_from_instruction_schedule_map_with_dt_set(self): - inst_map = InstructionScheduleMap() - with pulse.build(name="sx_q1") as custom_sx: - pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) + with self.assertWarns(DeprecationWarning): + inst_map = InstructionScheduleMap() + with pulse.build(name="sx_q1") as custom_sx: + pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) inst_map.add("sx", 0, self.custom_sx_q0) inst_map.add("sx", 1, custom_sx) self.pulse_target.dt = 1.0 - self.pulse_target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) - self.assertEqual(inst_map, self.pulse_target.instruction_schedule_map()) + with self.assertWarns(DeprecationWarning): + self.pulse_target.update_from_instruction_schedule_map(inst_map, {"sx": SXGate()}) + self.assertEqual(inst_map, self.pulse_target.instruction_schedule_map()) self.assertEqual(self.pulse_target["sx"][(1,)].duration, 1000.0) self.assertIsNone(self.pulse_target["sx"][(1,)].error) # This is an edge case. @@ -1303,18 +1338,20 @@ def test_update_from_instruction_schedule_map_with_dt_set(self): self.assertEqual(self.pulse_target["sx"][(0,)].error, 0.000413) def test_update_from_instruction_schedule_map_with_error_dict(self): - inst_map = InstructionScheduleMap() - with pulse.build(name="sx_q1") as custom_sx: - pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) + with self.assertWarns(DeprecationWarning): + inst_map = InstructionScheduleMap() + with pulse.build(name="sx_q1") as custom_sx: + pulse.play(pulse.Constant(1000, 0.2), pulse.DriveChannel(1)) inst_map.add("sx", 0, self.custom_sx_q0) inst_map.add("sx", 1, custom_sx) self.pulse_target.dt = 1.0 error_dict = {"sx": {(1,): 1.0}} - self.pulse_target.update_from_instruction_schedule_map( - inst_map, {"sx": SXGate()}, error_dict=error_dict - ) + with self.assertWarns(DeprecationWarning): + self.pulse_target.update_from_instruction_schedule_map( + inst_map, {"sx": SXGate()}, error_dict=error_dict + ) self.assertEqual(self.pulse_target["sx"][(1,)].error, 1.0) self.assertEqual(self.pulse_target["sx"][(0,)].error, 0.000413) @@ -1330,33 +1367,39 @@ def test_timing_constraints(self): ) def test_default_instmap_has_no_custom_gate(self): - backend = GenericBackendV2(num_qubits=27, calibrate_instructions=True) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2(num_qubits=27, calibrate_instructions=True) target = backend.target # This copies .calibration of InstructionProperties of each instruction # This must not convert PulseQobj to Schedule during this. # See qiskit-terra/#9595 - inst_map = target.instruction_schedule_map() + with self.assertWarns(DeprecationWarning): + inst_map = target.instruction_schedule_map() self.assertFalse(inst_map.has_custom_gate()) # Get pulse schedule. This generates Schedule provided by backend. - sched = inst_map.get("sx", (0,)) + with self.assertWarns(DeprecationWarning): + sched = inst_map.get("sx", (0,)) self.assertEqual(sched.metadata["publisher"], CalibrationPublisher.BACKEND_PROVIDER) self.assertFalse(inst_map.has_custom_gate()) # Update target with custom instruction. This is user provided schedule. - new_prop = InstructionProperties( - duration=self.custom_sx_q0.duration, - error=None, - calibration=self.custom_sx_q0, - ) + with self.assertWarns(DeprecationWarning): + new_prop = InstructionProperties( + duration=self.custom_sx_q0.duration, + error=None, + calibration=self.custom_sx_q0, + ) target.update_instruction_properties(instruction="sx", qargs=(0,), properties=new_prop) - inst_map = target.instruction_schedule_map() + with self.assertWarns(DeprecationWarning): + inst_map = target.instruction_schedule_map() self.assertTrue(inst_map.has_custom_gate()) empty = InstructionProperties() target.update_instruction_properties(instruction="sx", qargs=(0,), properties=empty) - inst_map = target.instruction_schedule_map() + with self.assertWarns(DeprecationWarning): + inst_map = target.instruction_schedule_map() self.assertFalse(inst_map.has_custom_gate()) def test_get_empty_target_calibration(self): @@ -1364,7 +1407,8 @@ def test_get_empty_target_calibration(self): properties = {(0,): InstructionProperties(duration=100, error=0.1)} target.add_instruction(XGate(), properties) - self.assertIsNone(target["x"][(0,)].calibration) + with self.assertWarns(DeprecationWarning): + self.assertIsNone(target["x"][(0,)].calibration) def test_has_calibration(self): target = Target() @@ -1374,22 +1418,25 @@ def test_has_calibration(self): } target.add_instruction(XGate(), properties) - # Test false for properties with no calibration - self.assertFalse(target.has_calibration("x", (0,))) - # Test false for no properties - self.assertFalse(target.has_calibration("x", (1,))) + with self.assertWarns(DeprecationWarning): + # Test false for properties with no calibration + self.assertFalse(target.has_calibration("x", (0,))) + # Test false for no properties + self.assertFalse(target.has_calibration("x", (1,))) - properties = { - (0,): InstructionProperties( - duration=self.custom_sx_q0.duration, - error=None, - calibration=self.custom_sx_q0, - ) - } + with self.assertWarns(DeprecationWarning): + properties = { + (0,): InstructionProperties( + duration=self.custom_sx_q0.duration, + error=None, + calibration=self.custom_sx_q0, + ) + } target.add_instruction(SXGate(), properties) # Test true for properties with calibration - self.assertTrue(target.has_calibration("sx", (0,))) + with self.assertWarns(DeprecationWarning): + self.assertTrue(target.has_calibration("sx", (0,))) def test_loading_legacy_ugate_instmap(self): # This is typical IBM backend situation. @@ -1400,9 +1447,10 @@ def test_loading_legacy_ugate_instmap(self): # Target is implicitly updated with inst map when it is set in transpile. # If u gates are not excluded, they may appear in the transpiled circuit. # These gates are no longer supported by hardware. - entry = ScheduleDef() - entry.define(pulse.Schedule(name="fake_u3"), user_provided=False) # backend provided - instmap = InstructionScheduleMap() + with self.assertWarns(DeprecationWarning): + entry = ScheduleDef() + entry.define(pulse.Schedule(name="fake_u3"), user_provided=False) # backend provided + instmap = InstructionScheduleMap() instmap._add("u3", (0,), entry) # Today's standard IBM backend target with sx, rz basis @@ -1412,7 +1460,8 @@ def test_loading_legacy_ugate_instmap(self): target.add_instruction(Measure(), {(0,): InstructionProperties()}) names_before = set(target.operation_names) - target.update_from_instruction_schedule_map(instmap) + with self.assertWarns(DeprecationWarning): + target.update_from_instruction_schedule_map(instmap) names_after = set(target.operation_names) # Otherwise u3 and sx-rz basis conflict in 1q decomposition. @@ -1973,16 +2022,17 @@ def test_inst_map(self): properties = fake_backend.properties() defaults = fake_backend.defaults() constraints = TimingConstraints(**config.timing_constraints) - target = Target.from_configuration( - basis_gates=config.basis_gates, - num_qubits=config.num_qubits, - coupling_map=CouplingMap(config.coupling_map), - backend_properties=properties, - dt=config.dt, - inst_map=defaults.instruction_schedule_map, - timing_constraints=constraints, - ) - self.assertIsNotNone(target["sx"][(0,)].calibration) + with self.assertWarns(DeprecationWarning): + target = Target.from_configuration( + basis_gates=config.basis_gates, + num_qubits=config.num_qubits, + coupling_map=CouplingMap(config.coupling_map), + backend_properties=properties, + dt=config.dt, + inst_map=defaults.instruction_schedule_map, + timing_constraints=constraints, + ) + self.assertIsNotNone(target["sx"][(0,)].calibration) self.assertEqual(target.granularity, constraints.granularity) self.assertEqual(target.min_length, constraints.min_length) self.assertEqual(target.pulse_alignment, constraints.pulse_alignment) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 4abf6511d8d2..e763a6206d78 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -18,6 +18,7 @@ import unittest import numpy as np +import scipy from ddt import ddt, data from qiskit import transpile @@ -60,11 +61,14 @@ from qiskit.circuit import Measure from qiskit.circuit.controlflow import IfElseOp from qiskit.circuit import Parameter, Gate +from qiskit.synthesis.unitary.qsd import qs_decomposition + from test import combine # pylint: disable=wrong-import-order from test import QiskitTestCase # pylint: disable=wrong-import-order from test.python.providers.fake_mumbai_v2 import ( # pylint: disable=wrong-import-order FakeMumbaiFractionalCX, ) + from ..legacy_cmaps import YORKTOWN_CMAP @@ -675,14 +679,15 @@ def test_coupling_map_unequal_durations(self, opt): qr = QuantumRegister(2) circ = QuantumCircuit(qr) circ.append(random_unitary(4, seed=1), [1, 0]) - backend = GenericBackendV2( - num_qubits=5, - coupling_map=YORKTOWN_CMAP, - basis_gates=["id", "rz", "sx", "x", "cx", "reset"], - calibrate_instructions=True, - pulse_channels=True, - seed=42, - ) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2( + num_qubits=5, + coupling_map=YORKTOWN_CMAP, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + calibrate_instructions=True, + pulse_channels=True, + seed=42, + ) tqc = transpile( circ, backend=backend, @@ -1033,6 +1038,18 @@ def test_parameterized_basis_gate_in_target(self): self.assertTrue(set(opcount).issubset({"rz", "rx", "rxx"})) self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + @data(1, 2, 3) + def test_qsd(self, opt): + """Test that the unitary synthesis pass runs qsd successfully with a target.""" + num_qubits = 3 + target = Target(num_qubits=num_qubits) + target.add_instruction(UGate(Parameter("theta"), Parameter("phi"), Parameter("lam"))) + target.add_instruction(CXGate()) + mat = scipy.stats.ortho_group.rvs(2**num_qubits) + qc = qs_decomposition(mat, opt_a1=True, opt_a2=False) + qc_transpiled = transpile(qc, target=target, optimization_level=opt) + self.assertTrue(np.allclose(mat, Operator(qc_transpiled).data)) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index 4e4844f5f701..1f7bb1aacc98 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -53,8 +53,9 @@ def assertLayout(self, dag, coupling_map, property_set, strict_direction=False): def run(dag, wire_map): for gate in dag.two_qubit_ops(): - if dag.has_calibration_for(gate) or isinstance(gate.op, ControlFlowOp): - continue + with self.assertWarns(DeprecationWarning): + if dag.has_calibration_for(gate) or isinstance(gate.op, ControlFlowOp): + continue physical_q0 = wire_map[gate.qargs[0]] physical_q1 = wire_map[gate.qargs[1]] @@ -711,7 +712,8 @@ def test_reasonable_limits_for_simple_layouts_v1(self): def test_reasonable_limits_for_simple_layouts(self): """Test that the default trials is set to a reasonable number.""" - backend = GenericBackendV2(27, calibrate_instructions=True, seed=42) + with self.assertWarns(DeprecationWarning): + backend = GenericBackendV2(27, calibrate_instructions=True, seed=42) qc = QuantumCircuit(5) qc.cx(2, 3) qc.cx(0, 1) diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py index 5acdd3ba6ebc..592987e62dbf 100644 --- a/test/python/transpiler/test_vf2_post_layout.py +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -46,8 +46,9 @@ def assertLayout(self, dag, coupling_map, property_set): def run(dag, wire_map): for gate in dag.two_qubit_ops(): - if dag.has_calibration_for(gate) or isinstance(gate.op, ControlFlowOp): - continue + with self.assertWarns(DeprecationWarning): + if dag.has_calibration_for(gate) or isinstance(gate.op, ControlFlowOp): + continue physical_q0 = wire_map[gate.qargs[0]] physical_q1 = wire_map[gate.qargs[1]] self.assertTrue((physical_q0, physical_q1) in edges) @@ -71,8 +72,9 @@ def assertLayoutV2(self, dag, target, property_set): def run(dag, wire_map): for gate in dag.two_qubit_ops(): - if dag.has_calibration_for(gate) or isinstance(gate.op, ControlFlowOp): - continue + with self.assertWarns(DeprecationWarning): + if dag.has_calibration_for(gate) or isinstance(gate.op, ControlFlowOp): + continue physical_q0 = wire_map[gate.qargs[0]] physical_q1 = wire_map[gate.qargs[1]] qargs = (physical_q0, physical_q1) @@ -552,8 +554,9 @@ def assertLayout(self, dag, coupling_map, property_set): layout = property_set["post_layout"] for gate in dag.two_qubit_ops(): - if dag.has_calibration_for(gate): - continue + with self.assertWarns(DeprecationWarning): + if dag.has_calibration_for(gate): + continue physical_q0 = layout[gate.qargs[0]] physical_q1 = layout[gate.qargs[1]] self.assertTrue(coupling_map.graph.has_edge(physical_q0, physical_q1)) @@ -567,8 +570,9 @@ def assertLayoutV2(self, dag, target, property_set): layout = property_set["post_layout"] for gate in dag.two_qubit_ops(): - if dag.has_calibration_for(gate): - continue + with self.assertWarns(DeprecationWarning): + if dag.has_calibration_for(gate): + continue physical_q0 = layout[gate.qargs[0]] physical_q1 = layout[gate.qargs[1]] qargs = (physical_q0, physical_q1) diff --git a/test/python/utils/test_deprecation.py b/test/python/utils/test_deprecation.py index 5d3b82e75aa2..08872b9e7a41 100644 --- a/test/python/utils/test_deprecation.py +++ b/test/python/utils/test_deprecation.py @@ -14,6 +14,7 @@ from __future__ import annotations +import sys from textwrap import dedent from qiskit.utils.deprecation import ( @@ -409,17 +410,21 @@ def func3(): to a new line.""" add_deprecation_to_docstring(func3, msg="Deprecated!", since="9.99", pending=False) - self.assertEqual( - func3.__doc__, - ( - f"""Docstring extending + expected_doc = f"""Docstring extending to a new line. {indent} .. deprecated:: 9.99 Deprecated! {indent}""" - ), - ) + if sys.version_info >= (3, 13, 0): + expected_doc = """Docstring extending +to a new line. + +.. deprecated:: 9.99 + Deprecated! +""" + + self.assertEqual(func3.__doc__, expected_doc) def func4(): """ @@ -427,10 +432,7 @@ def func4(): """ add_deprecation_to_docstring(func4, msg="Deprecated!", since="9.99", pending=False) - self.assertEqual( - func4.__doc__, - ( - f"""\ + expected_doc = f"""\ Docstring starting on a new line. {indent} @@ -438,7 +440,18 @@ def func4(): .. deprecated:: 9.99 Deprecated! {indent}""" - ), + if sys.version_info >= (3, 13, 0): + expected_doc = """\ + +Docstring starting on a new line. + +.. deprecated:: 9.99 + Deprecated! +""" + + self.assertEqual( + func4.__doc__, + expected_doc, ) def func5(): @@ -450,11 +463,7 @@ def func5(): """ - add_deprecation_to_docstring(func5, msg="Deprecated!", since="9.99", pending=False) - self.assertEqual( - func5.__doc__, - ( - f"""\ + expected_doc = f"""\ Paragraph 1, line 1. Line 2. @@ -466,7 +475,24 @@ def func5(): .. deprecated:: 9.99 Deprecated! {indent}""" - ), + + if sys.version_info >= (3, 13, 0): + expected_doc = """\ + +Paragraph 1, line 1. +Line 2. + +Paragraph 2. + + +.. deprecated:: 9.99 + Deprecated! +""" + + add_deprecation_to_docstring(func5, msg="Deprecated!", since="9.99", pending=False) + self.assertEqual( + func5.__doc__, + expected_doc, ) def func6(): @@ -478,11 +504,7 @@ def func6(): continued """ - add_deprecation_to_docstring(func6, msg="Deprecated!", since="9.99", pending=False) - self.assertEqual( - func6.__doc__, - ( - f"""Blah. + expected_doc = f"""Blah. A list. * element 1 @@ -493,7 +515,23 @@ def func6(): .. deprecated:: 9.99 Deprecated! {indent}""" - ), + + if sys.version_info >= (3, 13, 0): + expected_doc = """Blah. + +A list. + * element 1 + * element 2 + continued + +.. deprecated:: 9.99 + Deprecated! +""" + + add_deprecation_to_docstring(func6, msg="Deprecated!", since="9.99", pending=False) + self.assertEqual( + func6.__doc__, + expected_doc, ) def test_add_deprecation_docstring_meta_lines(self) -> None: @@ -511,10 +549,7 @@ def func1(): """ add_deprecation_to_docstring(func1, msg="Deprecated!", since="9.99", pending=False) - self.assertEqual( - func1.__doc__, - ( - f"""\ + expected_doc = f"""\ {indent} .. deprecated:: 9.99 Deprecated! @@ -526,7 +561,22 @@ def func1(): Raises: SomeError {indent}""" - ), + if sys.version_info >= (3, 13, 0): + expected_doc = """\ + +.. deprecated:: 9.99 + Deprecated! + + +Returns: + Content. + +Raises: + SomeError""" + + self.assertEqual( + func1.__doc__, + expected_doc, ) def func2(): @@ -537,10 +587,7 @@ def func2(): """ add_deprecation_to_docstring(func2, msg="Deprecated!", since="9.99", pending=False) - self.assertEqual( - func2.__doc__, - ( - f"""Docstring. + expected_doc = f"""Docstring. {indent} .. deprecated:: 9.99 Deprecated! @@ -549,8 +596,17 @@ def func2(): Returns: Content. {indent}""" - ), - ) + if sys.version_info >= (3, 13, 0): + expected_doc = """Docstring. + +.. deprecated:: 9.99 + Deprecated! + + +Returns: + Content.""" + + self.assertEqual(func2.__doc__, expected_doc) def func3(): """ @@ -562,11 +618,7 @@ def func3(): Content. """ - add_deprecation_to_docstring(func3, msg="Deprecated!", since="9.99", pending=False) - self.assertEqual( - func3.__doc__, - ( - f"""\ + expected_doc = f"""\ Docstring starting on a new line. @@ -579,7 +631,24 @@ def func3(): Examples: Content. {indent}""" - ), + if sys.version_info >= (3, 13, 0): + expected_doc = """\ + +Docstring starting on a new line. + +Paragraph 2. + +.. deprecated:: 9.99 + Deprecated! + + +Examples: + Content.""" + + add_deprecation_to_docstring(func3, msg="Deprecated!", since="9.99", pending=False) + self.assertEqual( + func3.__doc__, + expected_doc, ) def test_add_deprecation_docstring_multiple_entries(self) -> None: @@ -613,10 +682,7 @@ def func2(): add_deprecation_to_docstring(func2, msg="Deprecated #1!", since="9.99", pending=False) add_deprecation_to_docstring(func2, msg="Deprecated #2!", since="9.99", pending=False) - self.assertEqual( - func2.__doc__, - ( - f"""\ + expected_doc = f"""\ Docstring starting on a new line. {indent} @@ -628,7 +694,21 @@ def func2(): .. deprecated:: 9.99 Deprecated #2! {indent}""" - ), + if sys.version_info >= (3, 13, 0): + expected_doc = """\ + +Docstring starting on a new line. + +.. deprecated:: 9.99 + Deprecated #1! + +.. deprecated:: 9.99 + Deprecated #2! +""" + + self.assertEqual( + func2.__doc__, + expected_doc, ) def func3(): @@ -638,12 +718,7 @@ def func3(): Content. """ - add_deprecation_to_docstring(func3, msg="Deprecated #1!", since="9.99", pending=False) - add_deprecation_to_docstring(func3, msg="Deprecated #2!", since="9.99", pending=False) - self.assertEqual( - func3.__doc__, - ( - f"""Docstring. + expected_doc = f"""Docstring. {indent} .. deprecated:: 9.99 Deprecated #1! @@ -656,7 +731,26 @@ def func3(): Yields: Content. {indent}""" - ), + + if sys.version_info >= (3, 13, 0): + expected_doc = """Docstring. + +.. deprecated:: 9.99 + Deprecated #1! + + +.. deprecated:: 9.99 + Deprecated #2! + + +Yields: + Content.""" + + add_deprecation_to_docstring(func3, msg="Deprecated #1!", since="9.99", pending=False) + add_deprecation_to_docstring(func3, msg="Deprecated #2!", since="9.99", pending=False) + self.assertEqual( + func3.__doc__, + expected_doc, ) def test_add_deprecation_docstring_pending(self) -> None: diff --git a/test/python/utils/test_parallel.py b/test/python/utils/test_parallel.py index 13a4e9b11c2a..f6de443b7b03 100644 --- a/test/python/utils/test_parallel.py +++ b/test/python/utils/test_parallel.py @@ -13,6 +13,7 @@ """Tests for qiskit/tools/parallel""" import os import time +import warnings from unittest.mock import patch @@ -36,7 +37,10 @@ def _build_simple_circuit(_): def _build_simple_schedule(_): - return Schedule() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + # `Schedule` is deprecated in Qiskit 1.3 + return Schedule() class TestGetPlatformParallelDefault(QiskitTestCase): diff --git a/test/python/visualization/pulse_v2/test_core.py b/test/python/visualization/pulse_v2/test_core.py index 5855bbafae4a..8c677e554dd9 100644 --- a/test/python/visualization/pulse_v2/test_core.py +++ b/test/python/visualization/pulse_v2/test_core.py @@ -19,11 +19,14 @@ from qiskit.visualization.exceptions import VisualizationError from qiskit.visualization.pulse_v2 import core, stylesheet, device_info, drawings, types, layouts from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestChart(QiskitTestCase): """Tests for chart.""" + @ignore_pulse_deprecation_warnings def setUp(self) -> None: super().setUp() @@ -228,9 +231,11 @@ def test_update(self): self.assertEqual(chart.scale, 2.0) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestDrawCanvas(QiskitTestCase): """Tests for draw canvas.""" + @ignore_pulse_deprecation_warnings def setUp(self) -> None: super().setUp() self.style = stylesheet.QiskitPulseStyle() diff --git a/test/python/visualization/pulse_v2/test_drawings.py b/test/python/visualization/pulse_v2/test_drawings.py index 0e07decdd4b3..d0766fc4f9bb 100644 --- a/test/python/visualization/pulse_v2/test_drawings.py +++ b/test/python/visualization/pulse_v2/test_drawings.py @@ -15,11 +15,14 @@ from qiskit import pulse from qiskit.visualization.pulse_v2 import drawings, types from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestDrawingObjects(QiskitTestCase): """Tests for DrawingObjects.""" + @ignore_pulse_deprecation_warnings def setUp(self) -> None: """Setup.""" super().setUp() diff --git a/test/python/visualization/pulse_v2/test_events.py b/test/python/visualization/pulse_v2/test_events.py index fbef9d3de807..74fbf00f325c 100644 --- a/test/python/visualization/pulse_v2/test_events.py +++ b/test/python/visualization/pulse_v2/test_events.py @@ -15,8 +15,10 @@ from qiskit import pulse, circuit from qiskit.visualization.pulse_v2 import events from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestChannelEvents(QiskitTestCase): """Tests for ChannelEvents.""" diff --git a/test/python/visualization/pulse_v2/test_generators.py b/test/python/visualization/pulse_v2/test_generators.py index c5f5e8da98ac..749ec4ab3488 100644 --- a/test/python/visualization/pulse_v2/test_generators.py +++ b/test/python/visualization/pulse_v2/test_generators.py @@ -20,6 +20,7 @@ from qiskit.visualization.pulse_v2 import drawings, types, stylesheet, device_info from qiskit.visualization.pulse_v2.generators import barrier, chart, frame, snapshot, waveform from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings def create_instruction(inst, phase, freq, t0, dt, is_opaque=False): @@ -28,9 +29,11 @@ def create_instruction(inst, phase, freq, t0, dt, is_opaque=False): return types.PulseInstruction(t0=t0, dt=dt, frame=frame_info, inst=inst, is_opaque=is_opaque) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestWaveformGenerators(QiskitTestCase): """Tests for waveform generators.""" + @ignore_pulse_deprecation_warnings def setUp(self) -> None: super().setUp() style = stylesheet.QiskitPulseStyle() @@ -400,9 +403,11 @@ def test_gen_filled_waveform_stepwise_opaque(self): self.assertEqual(objs[1].text, "Gaussian(amp)") +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestChartGenerators(QiskitTestCase): """Tests for chart info generators.""" + @ignore_pulse_deprecation_warnings def setUp(self) -> None: super().setUp() style = stylesheet.QiskitPulseStyle() @@ -532,9 +537,11 @@ def test_gen_frequency_info(self): self.assertDictEqual(obj.styles, ref_style) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestFrameGenerators(QiskitTestCase): """Tests for frame info generators.""" + @ignore_pulse_deprecation_warnings def setUp(self) -> None: super().setUp() style = stylesheet.QiskitPulseStyle() @@ -736,9 +743,11 @@ def gen_frame_symbol_with_parameters(self): self.assertDictEqual(obj.meta, ref_meta) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestSnapshotGenerators(QiskitTestCase): """Tests for snapshot generators.""" + @ignore_pulse_deprecation_warnings def setUp(self) -> None: super().setUp() style = stylesheet.QiskitPulseStyle() @@ -830,9 +839,11 @@ def gen_snapshot_symbol(self): self.assertDictEqual(obj.styles, ref_style) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestBarrierGenerators(QiskitTestCase): """Tests for barrier generators.""" + @ignore_pulse_deprecation_warnings def setUp(self) -> None: super().setUp() style = stylesheet.QiskitPulseStyle() diff --git a/test/python/visualization/pulse_v2/test_layouts.py b/test/python/visualization/pulse_v2/test_layouts.py index d6a868956fa3..3985f1dd9318 100644 --- a/test/python/visualization/pulse_v2/test_layouts.py +++ b/test/python/visualization/pulse_v2/test_layouts.py @@ -15,11 +15,14 @@ from qiskit import pulse from qiskit.visualization.pulse_v2 import layouts, device_info from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestChannelArrangement(QiskitTestCase): """Tests for channel mapping functions.""" + @ignore_pulse_deprecation_warnings def setUp(self) -> None: super().setUp() self.channels = [ @@ -180,6 +183,7 @@ def test_channel_qubit_index_sort(self): self.assertListEqual(list(out_layout), ref) +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestHorizontalAxis(QiskitTestCase): """Tests for horizontal axis mapping functions.""" @@ -226,9 +230,11 @@ def test_time_map_in_without_dt(self): self.assertEqual(haxis.label, "System cycle time (dt)") +@decorate_test_methods(ignore_pulse_deprecation_warnings) class TestFigureTitle(QiskitTestCase): """Tests for figure title generation.""" + @ignore_pulse_deprecation_warnings def setUp(self) -> None: super().setUp() self.device = device_info.OpenPulseBackendInfo(name="test_backend", dt=1e-9) diff --git a/test/python/visualization/test_gate_map.py b/test/python/visualization/test_gate_map.py index fcac1e71c400..d2d589574103 100644 --- a/test/python/visualization/test_gate_map.py +++ b/test/python/visualization/test_gate_map.py @@ -109,7 +109,6 @@ def test_plot_error_map_backend_v1(self): """Test plotting error map with fake backend v1.""" backend = GenericBackendV2( num_qubits=27, - pulse_channels=True, coupling_map=MUMBAI_CMAP, ) img_ref = path_to_diagram_reference("fake_27_q_error.png") @@ -128,7 +127,6 @@ def test_plot_error_map_backend_v2(self): coupling_map = MUMBAI_CMAP backend = GenericBackendV2( num_qubits=27, - pulse_channels=True, coupling_map=coupling_map, ) img_ref = path_to_diagram_reference("fake_27_q_v2_error.png") @@ -145,9 +143,7 @@ def test_plot_error_map_backend_v2(self): def test_plot_error_map_over_100_qubit(self): """Test plotting error map with large fake backend.""" coupling_map = KYOTO_CMAP - backend = GenericBackendV2( - num_qubits=127, coupling_map=coupling_map, pulse_channels=True, seed=42 - ) + backend = GenericBackendV2(num_qubits=127, coupling_map=coupling_map, seed=42) img_ref = path_to_diagram_reference("fake_127_q_error.png") fig = plot_error_map(backend) with BytesIO() as img_buffer: @@ -447,9 +443,7 @@ def test_plot_error_map_over_100_qubit_backend_v2(self): [126, 112], [126, 125], ] - backend = GenericBackendV2( - num_qubits=127, coupling_map=coupling_map, pulse_channels=True, seed=42 - ) + backend = GenericBackendV2(num_qubits=127, coupling_map=coupling_map, seed=42) img_ref = path_to_diagram_reference("fake_127_q_v2_error.png") fig = plot_error_map(backend) with BytesIO() as img_buffer: diff --git a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py index 9e3dd5cc48eb..4da725c4b5d6 100644 --- a/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py +++ b/test/visual/mpl/circuit/test_circuit_matplotlib_drawer.py @@ -124,12 +124,13 @@ def test_calibrations(self): from qiskit import pulse - with pulse.build(name="hadamard") as h_q0: - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(0) - ) + with self.assertWarns(DeprecationWarning): + with pulse.build(name="hadamard") as h_q0: + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(0) + ) - circuit.add_calibration("h", [0], h_q0) + circuit.add_calibration("h", [0], h_q0) fname = "calibrations.png" self.circuit_drawer(circuit, output="mpl", filename=fname) @@ -154,19 +155,20 @@ def test_calibrations_with_control_gates(self): from qiskit import pulse - with pulse.build(name="cnot") as cx_q01: - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) - ) + with self.assertWarns(DeprecationWarning): + with pulse.build(name="cnot") as cx_q01: + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) + ) - circuit.add_calibration("cx", [0, 1], cx_q01) + circuit.add_calibration("cx", [0, 1], cx_q01) - with pulse.build(name="ch") as ch_q01: - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) - ) + with pulse.build(name="ch") as ch_q01: + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) + ) - circuit.add_calibration("ch", [0, 1], ch_q01) + circuit.add_calibration("ch", [0, 1], ch_q01) fname = "calibrations_with_control_gates.png" self.circuit_drawer(circuit, output="mpl", filename=fname) @@ -191,19 +193,20 @@ def test_calibrations_with_swap_and_reset(self): from qiskit import pulse - with pulse.build(name="swap") as swap_q01: - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) - ) + with self.assertWarns(DeprecationWarning): + with pulse.build(name="swap") as swap_q01: + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) + ) - circuit.add_calibration("swap", [0, 1], swap_q01) + circuit.add_calibration("swap", [0, 1], swap_q01) - with pulse.build(name="reset") as reset_q0: - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) - ) + with pulse.build(name="reset") as reset_q0: + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) + ) - circuit.add_calibration("reset", [0], reset_q0) + circuit.add_calibration("reset", [0], reset_q0) fname = "calibrations_with_swap_and_reset.png" self.circuit_drawer(circuit, output="mpl", filename=fname) @@ -227,19 +230,20 @@ def test_calibrations_with_rzz_and_rxx(self): from qiskit import pulse - with pulse.build(name="rzz") as rzz_q01: - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) - ) + with self.assertWarns(DeprecationWarning): + with pulse.build(name="rzz") as rzz_q01: + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) + ) - circuit.add_calibration("rzz", [0, 1], rzz_q01) + circuit.add_calibration("rzz", [0, 1], rzz_q01) - with pulse.build(name="rxx") as rxx_q01: - pulse.play( - pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) - ) + with pulse.build(name="rxx") as rxx_q01: + pulse.play( + pulse.library.Gaussian(duration=128, amp=0.1, sigma=16), pulse.DriveChannel(1) + ) - circuit.add_calibration("rxx", [0, 1], rxx_q01) + circuit.add_calibration("rxx", [0, 1], rxx_q01) fname = "calibrations_with_rzz_and_rxx.png" self.circuit_drawer(circuit, output="mpl", filename=fname) diff --git a/tools/build_pgo.sh b/tools/build_pgo.sh index 8553691bdfe4..ae0b58440946 100755 --- a/tools/build_pgo.sh +++ b/tools/build_pgo.sh @@ -2,16 +2,15 @@ set -x -merged_path=$1 - -python -c 'import sys;assert sys.platform == "win32"' -is_win=$? +work_dir="$1" +out_path="$2" set -e + # Create venv for instrumented build and test python -m venv build_pgo -if [[ $is_win -eq 0 ]]; then +if python -c 'import sys; assert sys.platform == "win32"'; then source build_pgo/Scripts/activate else source build_pgo/bin/activate @@ -25,8 +24,8 @@ fi # Build with instrumentation pip install -U -c constraints.txt setuptools-rust wheel setuptools -RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" pip install --prefer-binary -c constraints.txt -r requirements-dev.txt -e . -RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" python setup.py build_rust --release --inplace +RUSTFLAGS="-Cprofile-generate=$work_dir" pip install --prefer-binary -c constraints.txt -r requirements-dev.txt -e . +RUSTFLAGS="-Cprofile-generate=$work_dir" python setup.py build_rust --release --inplace # Run profile data generation QISKIT_PARALLEL=FALSE stestr run --abbreviate @@ -35,4 +34,4 @@ python tools/pgo_scripts/test_utility_scale.py deactivate -${HOME}/.rustup/toolchains/*$arch*/lib/rustlib/$arch*/bin/llvm-profdata merge -o $merged_path /tmp/pgo-data +${HOME}/.rustup/toolchains/*$arch*/lib/rustlib/$arch*/bin/llvm-profdata merge -o "$out_path" "$work_dir" diff --git a/tox.ini b/tox.ini index 5456e7731920..d4d822766675 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 4.0 -envlist = py39, py310, py311, py312, lint-incr +envlist = py39, py310, py311, py312, py313, lint-incr isolated_build = true [testenv]