diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 08530adfd4f..20e40dec982 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,6 +36,15 @@ 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.10' + env: + QISKIT_NO_CACHE_GATES: 1 + - name: 'Install dependencies' + run: | + 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' - name: 'Install optionals' run: | python -m pip install -r requirements-optional.txt -c constraints.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c75b5175001..4641c7878fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,6 +135,18 @@ Note that in order to run `python setup.py ...` commands you need have build dependency packages installed in your environment, which are listed in the `pyproject.toml` file under the `[build-system]` section. +### Compile time options + +When building qiskit from source there are options available to control how +Qiskit is built. Right now the only option is if you set the environment +variable `QISKIT_NO_CACHE_GATES=1` this will disable runtime caching of +Python gate objects when accessing them from a `QuantumCircuit` or `DAGCircuit`. +This makes a tradeoff between runtime performance for Python access and memory +overhead. Caching gates will result in better runtime for users of Python at +the cost of increased memory consumption. If you're working with any custom +transpiler passes written in python or are otherwise using a workflow that +repeatedly accesses the `operation` attribute of a `CircuitInstruction` or `op` +attribute of `DAGOpNode` enabling caching is recommended. ## Issues and pull requests diff --git a/Cargo.lock b/Cargo.lock index cf8e4f365df..aefa3c932a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1196,7 +1196,11 @@ name = "qiskit-circuit" version = "1.2.0" dependencies = [ "hashbrown 0.14.5", + "ndarray", + "num-complex", + "numpy", "pyo3", + "smallvec", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2827b2206f4..13f43cfabcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,11 @@ license = "Apache-2.0" [workspace.dependencies] indexmap.version = "2.2.6" hashbrown.version = "0.14.0" +num-complex = "0.4" +ndarray = "^0.15.6" +numpy = "0.21.0" +smallvec = "1.13" + # Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an # actual C extension (the feature disables linking in `libpython`, which is forbidden in Python # distributions). We only activate that feature when building the C extension module; we still need diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 63be9ad90b4..d9865d54543 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -11,13 +11,13 @@ doctest = false [dependencies] rayon = "1.10" -numpy = "0.21.0" +numpy.workspace = true rand = "0.8" rand_pcg = "0.3" rand_distr = "0.4.3" ahash = "0.8.11" num-traits = "0.2" -num-complex = "0.4" +num-complex.workspace = true num-bigint = "0.4" rustworkx-core = "0.14" faer = "0.19.0" @@ -25,7 +25,7 @@ itertools = "0.13.0" qiskit-circuit.workspace = true [dependencies.smallvec] -version = "1.13" +workspace = true features = ["union"] [dependencies.pyo3] @@ -33,7 +33,7 @@ workspace = true features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"] [dependencies.ndarray] -version = "^0.15.6" +workspace = true features = ["rayon", "approx-0_5"] [dependencies.approx] diff --git a/crates/accelerate/src/isometry.rs b/crates/accelerate/src/isometry.rs index a4e83358a7d..a3a8be38dae 100644 --- a/crates/accelerate/src/isometry.rs +++ b/crates/accelerate/src/isometry.rs @@ -23,7 +23,7 @@ use itertools::Itertools; use ndarray::prelude::*; use numpy::{IntoPyArray, PyReadonlyArray1, PyReadonlyArray2}; -use crate::two_qubit_decompose::ONE_QUBIT_IDENTITY; +use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; /// Find special unitary matrix that maps [c0,c1] to [r,0] or [0,r] if basis_state=0 or /// basis_state=1 respectively diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 5e833bd86fd..f93eb2a8d99 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -51,6 +51,7 @@ use rand::prelude::*; use rand_distr::StandardNormal; use rand_pcg::Pcg64Mcg; +use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; use qiskit_circuit::SliceOrInt; const PI2: f64 = PI / 2.0; @@ -60,11 +61,6 @@ const TWO_PI: f64 = 2.0 * PI; const C1: c64 = c64 { re: 1.0, im: 0.0 }; -pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [ - [Complex64::new(1., 0.), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(1., 0.)], -]; - static B_NON_NORMALIZED: [[Complex64; 4]; 4] = [ [ Complex64::new(1.0, 0.), @@ -342,54 +338,6 @@ fn rz_matrix(theta: f64) -> Array2 { ] } -static HGATE: [[Complex64; 2]; 2] = [ - [ - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(FRAC_1_SQRT_2, 0.), - ], - [ - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(-FRAC_1_SQRT_2, 0.), - ], -]; - -static CXGATE: [[Complex64; 4]; 4] = [ - [ - Complex64::new(1., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(1., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(1., 0.), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(1., 0.), - Complex64::new(0., 0.), - Complex64::new(0., 0.), - ], -]; - -static SXGATE: [[Complex64; 2]; 2] = [ - [Complex64::new(0.5, 0.5), Complex64::new(0.5, -0.5)], - [Complex64::new(0.5, -0.5), Complex64::new(0.5, 0.5)], -]; - -static XGATE: [[Complex64; 2]; 2] = [ - [Complex64::new(0., 0.), Complex64::new(1., 0.)], - [Complex64::new(1., 0.), Complex64::new(0., 0.)], -]; - fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2 { let identity = aview2(&ONE_QUBIT_IDENTITY); let phase = Complex64::new(0., global_phase).exp(); @@ -402,10 +350,10 @@ fn compute_unitary(sequence: &TwoQubitSequenceVec, global_phase: f64) -> Array2< // sequence. If we get a different gate this is getting called // by something else and is invalid. let gate_matrix = match inst.0.as_ref() { - "sx" => aview2(&SXGATE).to_owned(), + "sx" => aview2(&SX_GATE).to_owned(), "rz" => rz_matrix(inst.1[0]), - "cx" => aview2(&CXGATE).to_owned(), - "x" => aview2(&XGATE).to_owned(), + "cx" => aview2(&CX_GATE).to_owned(), + "x" => aview2(&X_GATE).to_owned(), _ => unreachable!("Undefined gate"), }; (gate_matrix, &inst.2) @@ -1481,7 +1429,7 @@ impl TwoQubitBasisDecomposer { } else { euler_matrix_q0 = rz_matrix(euler_q0[0][2] + euler_q0[1][0]).dot(&euler_matrix_q0); } - euler_matrix_q0 = aview2(&HGATE).dot(&euler_matrix_q0); + euler_matrix_q0 = aview2(&H_GATE).dot(&euler_matrix_q0); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix_q0.view(), 0); let rx_0 = rx_matrix(euler_q1[0][0]); @@ -1489,7 +1437,7 @@ impl TwoQubitBasisDecomposer { let rx_1 = rx_matrix(euler_q1[0][2] + euler_q1[1][0]); let mut euler_matrix_q1 = rz.dot(&rx_0); euler_matrix_q1 = rx_1.dot(&euler_matrix_q1); - euler_matrix_q1 = aview2(&HGATE).dot(&euler_matrix_q1); + euler_matrix_q1 = aview2(&H_GATE).dot(&euler_matrix_q1); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix_q1.view(), 1); gates.push(("cx".to_string(), smallvec![], smallvec![1, 0])); @@ -1550,12 +1498,12 @@ impl TwoQubitBasisDecomposer { return None; } gates.push(("cx".to_string(), smallvec![], smallvec![1, 0])); - let mut euler_matrix = rz_matrix(euler_q0[2][2] + euler_q0[3][0]).dot(&aview2(&HGATE)); + let mut euler_matrix = rz_matrix(euler_q0[2][2] + euler_q0[3][0]).dot(&aview2(&H_GATE)); euler_matrix = rx_matrix(euler_q0[3][1]).dot(&euler_matrix); euler_matrix = rz_matrix(euler_q0[3][2]).dot(&euler_matrix); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix.view(), 0); - let mut euler_matrix = rx_matrix(euler_q1[2][2] + euler_q1[3][0]).dot(&aview2(&HGATE)); + let mut euler_matrix = rx_matrix(euler_q1[2][2] + euler_q1[3][0]).dot(&aview2(&H_GATE)); euler_matrix = rz_matrix(euler_q1[3][1]).dot(&euler_matrix); euler_matrix = rx_matrix(euler_q1[3][2]).dot(&euler_matrix); self.append_1q_sequence(&mut gates, &mut global_phase, euler_matrix.view(), 1); diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 6ec38392cc3..dd7e878537d 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -11,4 +11,17 @@ doctest = false [dependencies] hashbrown.workspace = true -pyo3.workspace = true +num-complex.workspace = true +ndarray.workspace = true +numpy.workspace = true + +[dependencies.pyo3] +workspace = true +features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"] + +[dependencies.smallvec] +workspace = true +features = ["union"] + +[features] +cache_pygates = [] diff --git a/crates/circuit/README.md b/crates/circuit/README.md index b9375c9f99d..bbb4e54651a 100644 --- a/crates/circuit/README.md +++ b/crates/circuit/README.md @@ -4,3 +4,66 @@ The Rust-based data structures for circuits. This currently defines the core data collections for `QuantumCircuit`, but may expand in the future to back `DAGCircuit` as well. This crate is a very low part of the Rust stack, if not the very lowest. + +The data model exposed by this crate is as follows. + +## CircuitData + +The core representation of a quantum circuit in Rust is the `CircuitData` struct. This containts the list +of instructions that are comprising the circuit. Each element in this list is modeled by a +`CircuitInstruction` struct. The `CircuitInstruction` contains the operation object and it's operands. +This includes the parameters and bits. It also contains the potential mutable state of the Operation representation from the legacy Python data model; namely `duration`, `unit`, `condition`, and `label`. +In the future we'll be able to remove all of that except for label. + +At rest a `CircuitInstruction` is compacted into a `PackedInstruction` which caches reused qargs +in the instructions to reduce the memory overhead of `CircuitData`. The `PackedInstruction` objects +get unpacked back to `CircuitInstruction` when accessed for a more convienent working form. + +Additionally the `CircuitData` contains a `param_table` field which is used to track parameterized +instructions that are using python defined `ParameterExpression` objects for any parameters and also +a global phase field which is used to track the global phase of the circuit. + +## Operation Model + +In the circuit crate all the operations used in a `CircuitInstruction` are part of the `OperationType` +enum. The `OperationType` enum has four variants which are used to define the different types of +operation objects that can be on a circuit: + + - `StandardGate`: a rust native representation of a member of the Qiskit standard gate library. This is + an `enum` that enumerates all the gates in the library and statically defines all the gate properties + except for gates that take parameters, + - `PyGate`: A struct that wraps a gate outside the standard library defined in Python. This struct wraps + a `Gate` instance (or subclass) as a `PyObject`. The static properties of this object (such as name, + number of qubits, etc) are stored in Rust for performance but the dynamic properties such as + the matrix or definition are accessed by calling back into Python to get them from the stored + `PyObject` + - `PyInstruction`: A struct that wraps an instruction defined in Python. This struct wraps an + `Instruction` instance (or subclass) as a `PyObject`. The static properties of this object (such as + name, number of qubits, etc) are stored in Rust for performance but the dynamic properties such as + the definition are accessed by calling back into Python to get them from the stored `PyObject`. As + the primary difference between `Gate` and `Instruction` in the python data model are that `Gate` is a + specialized `Instruction` subclass that represents unitary operations the primary difference between + this and `PyGate` are that `PyInstruction` will always return `None` when it's matrix is accessed. + - `PyOperation`: A struct that wraps an operation defined in Python. This struct wraps an `Operation` + instance (or subclass) as a `PyObject`. The static properties of this object (such as name, number + of qubits, etc) are stored in Rust for performance. As `Operation` is the base abstract interface + definition of what can be put on a circuit this is mostly just a container for custom Python objects. + Anything that's operating on a bare operation will likely need to access it via the `PyObject` + manually because the interface doesn't define many standard properties outside of what's cached in + the struct. + +There is also an `Operation` trait defined which defines the common access pattern interface to these +4 types along with the `OperationType` parent. This trait defines methods to access the standard data +model attributes of operations in Qiskit. This includes things like the name, number of qubits, the matrix, the definition, etc. + +## ParamTable + +The `ParamTable` struct is used to track which circuit instructions are using `ParameterExpression` +objects for any of their parameters. The Python space `ParameterExpression` is comprised of a symengine +symbolic expression that defines operations using `Parameter` objects. Each `Parameter` is modeled by +a uuid and a name to uniquely identify it. The parameter table maps the `Parameter` objects to the +`CircuitInstruction` in the `CircuitData` that are using them. The `Parameter` comprised of 3 `HashMaps` internally that map the uuid (as `u128`, which is accesible in Python by using `uuid.int`) to the `ParamEntry`, the `name` to the uuid, and the uuid to the PyObject for the actual `Parameter`. + +The `ParamEntry` is just a `HashSet` of 2-tuples with usize elements. The two usizes represent the instruction index in the `CircuitData` and the index of the `CircuitInstruction.params` field of +a give instruction where the given `Parameter` is used in the circuit. If the instruction index is +`GLOBAL_PHASE_MAX`, that points to the global phase property of the circuit instead of a `CircuitInstruction`. diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index 7964ec186e0..40540f9df5a 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -12,7 +12,7 @@ use crate::BitType; use hashbrown::HashMap; -use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; use pyo3::prelude::*; use pyo3::types::PyList; use std::fmt::Debug; @@ -83,6 +83,15 @@ pub(crate) struct BitData { pub(crate) struct BitNotFoundError<'py>(pub(crate) Bound<'py, PyAny>); +impl<'py> From> for PyErr { + fn from(error: BitNotFoundError) -> Self { + PyKeyError::new_err(format!( + "Bit {:?} has not been added to this circuit.", + error.0 + )) + } +} + impl BitData where T: From + Copy, @@ -142,7 +151,7 @@ where /// Map the provided native indices to the corresponding Python /// bit instances. /// Panics if any of the indices are out of range. - pub fn map_indices(&self, bits: &[T]) -> impl Iterator> + ExactSizeIterator { + pub fn map_indices(&self, bits: &[T]) -> impl ExactSizeIterator> { let v: Vec<_> = bits.iter().map(|i| self.get(*i).unwrap()).collect(); v.into_iter() } diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index fbb7c06fc89..da35787e320 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -10,17 +10,24 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use crate::bit_data::{BitData, BitNotFoundError}; -use crate::circuit_instruction::CircuitInstruction; -use crate::interner::{CacheFullError, IndexedInterner, Interner, InternerKey}; -use crate::packed_instruction::PackedInstruction; +use crate::bit_data::BitData; +use crate::circuit_instruction::{ + convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction, + ExtraInstructionAttributes, OperationInput, PackedInstruction, +}; +use crate::imports::{BUILTIN_LIST, QUBIT}; +use crate::interner::{IndexedInterner, Interner, InternerKey}; +use crate::operations::{Operation, OperationType, Param, StandardGate}; +use crate::parameter_table::{ParamEntry, ParamTable, GLOBAL_PHASE_INDEX}; use crate::{Clbit, Qubit, SliceOrInt}; -use pyo3::exceptions::{PyIndexError, PyKeyError, PyRuntimeError, PyValueError}; +use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; use pyo3::types::{PyList, PySet, PySlice, PyTuple, PyType}; -use pyo3::{PyObject, PyResult, PyTraverseError, PyVisit}; -use std::mem; +use pyo3::{intern, PyTraverseError, PyVisit}; + +use hashbrown::{HashMap, HashSet}; +use smallvec::SmallVec; /// A container for :class:`.QuantumCircuit` instruction listings that stores /// :class:`.CircuitInstruction` instances in a packed form by interning @@ -85,33 +92,250 @@ pub struct CircuitData { qubits: BitData, /// Clbits registered in the circuit. clbits: BitData, + param_table: ParamTable, + #[pyo3(get)] + global_phase: Param, } -impl<'py> From> for PyErr { - fn from(error: BitNotFoundError) -> Self { - PyKeyError::new_err(format!( - "Bit {:?} has not been added to this circuit.", - error.0 - )) +impl CircuitData { + /// An alternate constructor to build a new `CircuitData` from an iterator + /// of standard gates. This can be used to build a circuit from a sequence + /// of standard gates, such as for a `StandardGate` definition or circuit + /// synthesis without needing to involve Python. + /// + /// This can be connected with the Python space + /// QuantumCircuit.from_circuit_data() constructor to build a full + /// QuantumCircuit from Rust. + /// + /// # Arguments + /// + /// * py: A GIL handle this is needed to instantiate Qubits in Python space + /// * num_qubits: The number of qubits in the circuit. These will be created + /// in Python as loose bits without a register. + /// * instructions: An iterator of the standard gate params and qubits to + /// add to the circuit + /// * global_phase: The global phase to use for the circuit + pub fn from_standard_gates( + py: Python, + num_qubits: u32, + instructions: I, + global_phase: Param, + ) -> PyResult + where + I: IntoIterator, SmallVec<[Qubit; 2]>)>, + { + let instruction_iter = instructions.into_iter(); + let mut res = CircuitData { + data: Vec::with_capacity(instruction_iter.size_hint().0), + qargs_interner: IndexedInterner::new(), + cargs_interner: IndexedInterner::new(), + qubits: BitData::new(py, "qubits".to_string()), + clbits: BitData::new(py, "clbits".to_string()), + param_table: ParamTable::new(), + global_phase, + }; + if num_qubits > 0 { + let qubit_cls = QUBIT.get_bound(py); + for _i in 0..num_qubits { + let bit = qubit_cls.call0()?; + res.add_qubit(py, &bit, true)?; + } + } + for (operation, params, qargs) in instruction_iter { + let qubits = PyTuple::new_bound(py, res.qubits.map_indices(&qargs)).unbind(); + let clbits = PyTuple::empty_bound(py).unbind(); + let inst = res.pack_owned( + py, + &CircuitInstruction { + operation: OperationType::Standard(operation), + qubits, + clbits, + params, + extra_attrs: None, + #[cfg(feature = "cache_pygates")] + py_op: None, + }, + )?; + res.data.push(inst); + } + Ok(res) } -} -impl From for PyErr { - fn from(_: CacheFullError) -> Self { - PyRuntimeError::new_err("The bit operands cache is full!") + fn handle_manual_params( + &mut self, + py: Python, + inst_index: usize, + params: &[(usize, Vec)], + ) -> PyResult { + let mut new_param = false; + let mut atomic_parameters: HashMap = HashMap::new(); + for (param_index, raw_param_objs) in params { + raw_param_objs.iter().for_each(|x| { + atomic_parameters.insert( + x.getattr(py, intern!(py, "_uuid")) + .expect("Not a parameter") + .getattr(py, intern!(py, "int")) + .expect("Not a uuid") + .extract::(py) + .unwrap(), + x.clone_ref(py), + ); + }); + for (param_uuid, param_obj) in atomic_parameters.iter() { + match self.param_table.table.get_mut(param_uuid) { + Some(entry) => entry.add(inst_index, *param_index), + None => { + new_param = true; + let new_entry = ParamEntry::new(inst_index, *param_index); + self.param_table + .insert(py, param_obj.clone_ref(py), new_entry)?; + } + }; + } + atomic_parameters.clear() + } + Ok(new_param) + } + + /// Add an instruction's entries to the parameter table + fn update_param_table( + &mut self, + py: Python, + inst_index: usize, + params: Option)>>, + ) -> PyResult { + if let Some(params) = params { + return self.handle_manual_params(py, inst_index, ¶ms); + } + // Update the parameter table + let mut new_param = false; + let inst_params = &self.data[inst_index].params; + if !inst_params.is_empty() { + let params: Vec<(usize, PyObject)> = inst_params + .iter() + .enumerate() + .filter_map(|(idx, x)| match x { + Param::ParameterExpression(param_obj) => Some((idx, param_obj.clone_ref(py))), + _ => None, + }) + .collect(); + if !params.is_empty() { + let list_builtin = BUILTIN_LIST.get_bound(py); + let mut atomic_parameters: HashMap = HashMap::new(); + for (param_index, param) in ¶ms { + let temp: PyObject = param.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; + raw_param_objs.iter().for_each(|x| { + atomic_parameters.insert( + x.getattr(py, intern!(py, "_uuid")) + .expect("Not a parameter") + .getattr(py, intern!(py, "int")) + .expect("Not a uuid") + .extract(py) + .unwrap(), + x.clone_ref(py), + ); + }); + for (param_uuid, param_obj) in &atomic_parameters { + match self.param_table.table.get_mut(param_uuid) { + Some(entry) => entry.add(inst_index, *param_index), + None => { + new_param = true; + let new_entry = ParamEntry::new(inst_index, *param_index); + self.param_table + .insert(py, param_obj.clone_ref(py), new_entry)?; + } + }; + } + atomic_parameters.clear(); + } + } + } + Ok(new_param) + } + + /// Remove an index's entries from the parameter table. + fn remove_from_parameter_table(&mut self, py: Python, inst_index: usize) -> PyResult<()> { + let list_builtin = BUILTIN_LIST.get_bound(py); + if inst_index == GLOBAL_PHASE_INDEX { + if let Param::ParameterExpression(global_phase) = &self.global_phase { + let temp: PyObject = global_phase.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; + for (param_index, param_obj) in raw_param_objs.iter().enumerate() { + let uuid: u128 = param_obj + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + let name: String = param_obj.getattr(py, intern!(py, "name"))?.extract(py)?; + self.param_table + .discard_references(uuid, inst_index, param_index, name); + } + } + } else if !self.data[inst_index].params.is_empty() { + let params: Vec<(usize, PyObject)> = self.data[inst_index] + .params + .iter() + .enumerate() + .filter_map(|(idx, x)| match x { + Param::ParameterExpression(param_obj) => Some((idx, param_obj.clone_ref(py))), + _ => None, + }) + .collect(); + if !params.is_empty() { + for (param_index, param) in ¶ms { + let temp: PyObject = param.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; + let mut atomic_parameters: HashSet<(u128, String)> = + HashSet::with_capacity(params.len()); + for x in raw_param_objs { + let uuid = x + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + let name = x.getattr(py, intern!(py, "name"))?.extract(py)?; + atomic_parameters.insert((uuid, name)); + } + for (uuid, name) in atomic_parameters { + self.param_table + .discard_references(uuid, inst_index, *param_index, name); + } + } + } + } + Ok(()) + } + + fn reindex_parameter_table(&mut self, py: Python) -> PyResult<()> { + self.param_table.clear(); + + for inst_index in 0..self.data.len() { + self.update_param_table(py, inst_index, None)?; + } + // Technically we could keep the global phase entry directly if it exists, but we're + // the incremental cost is minimal after reindexing everything. + self.global_phase(py, self.global_phase.clone())?; + Ok(()) + } + + pub fn append_inner(&mut self, py: Python, value: PyRef) -> PyResult { + let packed = self.pack(py, value)?; + let new_index = self.data.len(); + self.data.push(packed); + self.update_param_table(py, new_index, None) } } #[pymethods] impl CircuitData { #[new] - #[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0))] + #[pyo3(signature = (qubits=None, clbits=None, data=None, reserve=0, global_phase=Param::Float(0.0)))] pub fn new( py: Python<'_>, qubits: Option<&Bound>, clbits: Option<&Bound>, data: Option<&Bound>, reserve: usize, + global_phase: Param, ) -> PyResult { let mut self_ = CircuitData { data: Vec::new(), @@ -119,7 +343,10 @@ impl CircuitData { cargs_interner: IndexedInterner::new(), qubits: BitData::new(py, "qubits".to_string()), clbits: BitData::new(py, "clbits".to_string()), + param_table: ParamTable::new(), + global_phase: Param::Float(0.), }; + self_.global_phase(py, global_phase)?; if let Some(qubits) = qubits { for bit in qubits.iter()? { self_.add_qubit(py, &bit?, true)?; @@ -241,17 +468,89 @@ impl CircuitData { /// /// Returns: /// CircuitData: The shallow copy. - pub fn copy(&self, py: Python<'_>) -> PyResult { + #[pyo3(signature = (copy_instructions=true, deepcopy=false))] + pub fn copy(&self, py: Python<'_>, copy_instructions: bool, deepcopy: bool) -> PyResult { let mut res = CircuitData::new( py, Some(self.qubits.cached().bind(py)), Some(self.clbits.cached().bind(py)), None, 0, + self.global_phase.clone(), )?; res.qargs_interner = self.qargs_interner.clone(); res.cargs_interner = self.cargs_interner.clone(); res.data.clone_from(&self.data); + res.param_table.clone_from(&self.param_table); + + if deepcopy { + let deepcopy = py + .import_bound(intern!(py, "copy"))? + .getattr(intern!(py, "deepcopy"))?; + for inst in &mut res.data { + match &mut inst.op { + OperationType::Standard(_) => { + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Gate(ref mut op) => { + op.gate = deepcopy.call1((&op.gate,))?.unbind(); + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Instruction(ref mut op) => { + op.instruction = deepcopy.call1((&op.instruction,))?.unbind(); + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Operation(ref mut op) => { + op.operation = deepcopy.call1((&op.operation,))?.unbind(); + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + }; + } + } else if copy_instructions { + for inst in &mut res.data { + match &mut inst.op { + OperationType::Standard(_) => { + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Gate(ref mut op) => { + op.gate = op.gate.call_method0(py, intern!(py, "copy"))?; + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Instruction(ref mut op) => { + op.instruction = op.instruction.call_method0(py, intern!(py, "copy"))?; + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + OperationType::Operation(ref mut op) => { + op.operation = op.operation.call_method0(py, intern!(py, "copy"))?; + #[cfg(feature = "cache_pygates")] + { + inst.py_op = None; + } + } + }; + } + } Ok(res) } @@ -290,10 +589,87 @@ impl CircuitData { /// Args: /// func (Callable[[:class:`~.Operation`], None]): /// The callable to invoke. + #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn foreach_op(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter() { - func.call1((inst.op.bind(py),))?; + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + + let op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + func.call1((op,))?; + } + Ok(()) + } + + /// Invokes callable ``func`` with each instruction's operation. + /// + /// Args: + /// func (Callable[[:class:`~.Operation`], None]): + /// The callable to invoke. + #[cfg(feature = "cache_pygates")] + #[pyo3(signature = (func))] + pub fn foreach_op(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { + for inst in self.data.iter_mut() { + let op = match &inst.py_op { + Some(op) => op.clone_ref(py), + None => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + let new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } + }; + func.call1((op,))?; } Ok(()) } @@ -304,10 +680,88 @@ impl CircuitData { /// Args: /// func (Callable[[int, :class:`~.Operation`], None]): /// The callable to invoke. + #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn foreach_op_indexed(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for (index, inst) in self.data.iter().enumerate() { - func.call1((index, inst.op.bind(py)))?; + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + + let op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + func.call1((index, op))?; + } + Ok(()) + } + + /// Invokes callable ``func`` with the positional index and operation + /// of each instruction. + /// + /// Args: + /// func (Callable[[int, :class:`~.Operation`], None]): + /// The callable to invoke. + #[cfg(feature = "cache_pygates")] + #[pyo3(signature = (func))] + pub fn foreach_op_indexed(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { + for (index, inst) in self.data.iter_mut().enumerate() { + let op = match &inst.py_op { + Some(op) => op.clone_ref(py), + None => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + let new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } + }; + func.call1((index, op))?; } Ok(()) } @@ -315,14 +769,187 @@ impl CircuitData { /// Invokes callable ``func`` with each instruction's operation, /// replacing the operation with the result. /// + /// .. note:: + /// + /// This is only to be used by map_vars() in quantumcircuit.py it + /// assumes that a full Python instruction will only be returned from + /// standard gates iff a condition is set. + /// /// Args: /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): /// A callable used to map original operation to their /// replacements. + #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter_mut() { - inst.op = func.call1((inst.op.bind(py),))?.into_py(py); + let old_op = match &inst.op { + OperationType::Standard(op) => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + if condition.is_some() { + operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )? + } else { + op.into_py(py) + } + } + OperationType::Gate(op) => op.gate.clone_ref(py), + OperationType::Instruction(op) => op.instruction.clone_ref(py), + OperationType::Operation(op) => op.operation.clone_ref(py), + }; + let result: OperationInput = func.call1((old_op,))?.extract()?; + match result { + OperationInput::Standard(op) => { + inst.op = OperationType::Standard(op); + } + OperationInput::Gate(op) => { + inst.op = OperationType::Gate(op); + } + OperationInput::Instruction(op) => { + inst.op = OperationType::Instruction(op); + } + OperationInput::Operation(op) => { + inst.op = OperationType::Operation(op); + } + OperationInput::Object(new_op) => { + let new_inst_details = convert_py_to_operation_type(py, new_op)?; + inst.op = new_inst_details.operation; + inst.params = new_inst_details.params; + if new_inst_details.label.is_some() + || new_inst_details.duration.is_some() + || new_inst_details.unit.is_some() + || new_inst_details.condition.is_some() + { + inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + label: new_inst_details.label, + duration: new_inst_details.duration, + unit: new_inst_details.unit, + condition: new_inst_details.condition, + })) + } + } + } + } + Ok(()) + } + + /// Invokes callable ``func`` with each instruction's operation, + /// replacing the operation with the result. + /// + /// .. note:: + /// + /// This is only to be used by map_vars() in quantumcircuit.py it + /// assumes that a full Python instruction will only be returned from + /// standard gates iff a condition is set. + /// + /// Args: + /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): + /// A callable used to map original operation to their + /// replacements. + #[cfg(feature = "cache_pygates")] + #[pyo3(signature = (func))] + pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { + for inst in self.data.iter_mut() { + let old_op = match &inst.py_op { + Some(op) => op.clone_ref(py), + None => match &inst.op { + OperationType::Standard(op) => { + let label; + let duration; + let unit; + let condition; + match &inst.extra_attrs { + Some(extra_attrs) => { + label = &extra_attrs.label; + duration = &extra_attrs.duration; + unit = &extra_attrs.unit; + condition = &extra_attrs.condition; + } + None => { + label = &None; + duration = &None; + unit = &None; + condition = &None; + } + } + if condition.is_some() { + let new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + label, + duration, + unit, + condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } else { + op.into_py(py) + } + } + OperationType::Gate(op) => op.gate.clone_ref(py), + OperationType::Instruction(op) => op.instruction.clone_ref(py), + OperationType::Operation(op) => op.operation.clone_ref(py), + }, + }; + let result: OperationInput = func.call1((old_op,))?.extract()?; + match result { + OperationInput::Standard(op) => { + inst.op = OperationType::Standard(op); + } + OperationInput::Gate(op) => { + inst.op = OperationType::Gate(op); + } + OperationInput::Instruction(op) => { + inst.op = OperationType::Instruction(op); + } + OperationInput::Operation(op) => { + inst.op = OperationType::Operation(op); + } + OperationInput::Object(new_op) => { + let new_inst_details = convert_py_to_operation_type(py, new_op.clone_ref(py))?; + inst.op = new_inst_details.operation; + inst.params = new_inst_details.params; + if new_inst_details.label.is_some() + || new_inst_details.duration.is_some() + || new_inst_details.unit.is_some() + || new_inst_details.condition.is_some() + { + inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + label: new_inst_details.label, + duration: new_inst_details.duration, + unit: new_inst_details.unit, + condition: new_inst_details.condition, + })) + } + inst.py_op = Some(new_op); + } + } } Ok(()) } @@ -385,7 +1012,7 @@ impl CircuitData { qubits: Option<&Bound>, clbits: Option<&Bound>, ) -> PyResult<()> { - let mut temp = CircuitData::new(py, qubits, clbits, None, 0)?; + let mut temp = CircuitData::new(py, qubits, clbits, None, 0, self.global_phase.clone())?; if qubits.is_some() { if temp.num_qubits() < self.num_qubits() { return Err(PyValueError::new_err(format!( @@ -394,7 +1021,7 @@ impl CircuitData { self.num_qubits(), ))); } - mem::swap(&mut temp.qubits, &mut self.qubits); + std::mem::swap(&mut temp.qubits, &mut self.qubits); } if clbits.is_some() { if temp.num_clbits() < self.num_clbits() { @@ -404,7 +1031,7 @@ impl CircuitData { self.num_clbits(), ))); } - mem::swap(&mut temp.clbits, &mut self.clbits); + std::mem::swap(&mut temp.clbits, &mut self.clbits); } Ok(()) } @@ -430,9 +1057,11 @@ impl CircuitData { py, CircuitInstruction::new( py, - inst.op.clone_ref(py), + inst.op.clone(), self_.qubits.map_indices(qubits.value), self_.clbits.map_indices(clbits.value), + inst.params.clone(), + inst.extra_attrs.clone(), ), ) } else { @@ -455,7 +1084,7 @@ impl CircuitData { } } - pub fn __delitem__(&mut self, index: SliceOrInt) -> PyResult<()> { + pub fn __delitem__(&mut self, py: Python, index: SliceOrInt) -> PyResult<()> { match index { SliceOrInt::Slice(slice) => { let slice = { @@ -468,14 +1097,24 @@ impl CircuitData { s }; for i in slice.into_iter() { - self.__delitem__(SliceOrInt::Int(i))?; + self.__delitem__(py, SliceOrInt::Int(i))?; } + self.reindex_parameter_table(py)?; Ok(()) } SliceOrInt::Int(index) => { let index = self.convert_py_index(index)?; if self.data.get(index).is_some() { - self.data.remove(index); + if index == self.data.len() { + // For individual removal from param table before + // deletion + self.remove_from_parameter_table(py, index)?; + self.data.remove(index); + } else { + // For delete in the middle delete before reindexing + self.data.remove(index); + self.reindex_parameter_table(py)?; + } Ok(()) } else { Err(PyIndexError::new_err(format!( @@ -487,6 +1126,19 @@ impl CircuitData { } } + pub fn setitem_no_param_table_update( + &mut self, + py: Python<'_>, + index: isize, + value: &Bound, + ) -> PyResult<()> { + let index = self.convert_py_index(index)?; + let value: PyRef = value.downcast()?.borrow(); + let mut packed = self.pack(py, value)?; + std::mem::swap(&mut packed, &mut self.data[index]); + Ok(()) + } + pub fn __setitem__( &mut self, py: Python<'_>, @@ -520,7 +1172,7 @@ impl CircuitData { indices.stop, 1isize, ); - self.__delitem__(SliceOrInt::Slice(slice))?; + self.__delitem__(py, SliceOrInt::Slice(slice))?; } else { // Insert any extra values. for v in values.iter().skip(slice.len()).rev() { @@ -535,7 +1187,9 @@ impl CircuitData { let index = self.convert_py_index(index)?; let value: PyRef = value.extract()?; let mut packed = self.pack(py, value)?; - mem::swap(&mut packed, &mut self.data[index]); + self.remove_from_parameter_table(py, index)?; + std::mem::swap(&mut packed, &mut self.data[index]); + self.update_param_table(py, index, None)?; Ok(()) } } @@ -548,8 +1202,14 @@ impl CircuitData { value: PyRef, ) -> PyResult<()> { let index = self.convert_py_index_clamped(index); + let old_len = self.data.len(); let packed = self.pack(py, value)?; self.data.insert(index, packed); + if index == old_len { + self.update_param_table(py, old_len, None)?; + } else { + self.reindex_parameter_table(py)?; + } Ok(()) } @@ -557,14 +1217,21 @@ impl CircuitData { let index = index.unwrap_or_else(|| std::cmp::max(0, self.data.len() as isize - 1).into_py(py)); let item = self.__getitem__(py, index.bind(py))?; - self.__delitem__(index.bind(py).extract()?)?; + + self.__delitem__(py, index.bind(py).extract()?)?; Ok(item) } - pub fn append(&mut self, py: Python<'_>, value: PyRef) -> PyResult<()> { - let packed = self.pack(py, value)?; + pub fn append( + &mut self, + py: Python<'_>, + value: &Bound, + params: Option)>>, + ) -> PyResult { + let packed = self.pack(py, value.try_borrow()?)?; + let new_index = self.data.len(); self.data.push(packed); - Ok(()) + self.update_param_table(py, new_index, params) } pub fn extend(&mut self, py: Python<'_>, itr: &Bound) -> PyResult<()> { @@ -597,28 +1264,33 @@ impl CircuitData { .unwrap()) }) .collect::>>()?; - + let new_index = self.data.len(); let qubits_id = Interner::intern(&mut self.qargs_interner, InternerKey::Value(qubits))?; let clbits_id = Interner::intern(&mut self.cargs_interner, InternerKey::Value(clbits))?; self.data.push(PackedInstruction { - op: inst.op.clone_ref(py), + op: inst.op.clone(), qubits_id: qubits_id.index, clbits_id: clbits_id.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), }); + self.update_param_table(py, new_index, None)?; } return Ok(()); } - for v in itr.iter()? { - self.append(py, v?.extract()?)?; + self.append_inner(py, v?.extract()?)?; } Ok(()) } pub fn clear(&mut self, _py: Python<'_>) -> PyResult<()> { std::mem::take(&mut self.data); + self.param_table.clear(); Ok(()) } @@ -656,9 +1328,6 @@ impl CircuitData { } fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - for packed in self.data.iter() { - visit.call(&packed.op)?; - } for bit in self.qubits.bits().iter().chain(self.clbits.bits().iter()) { visit.call(bit)?; } @@ -678,6 +1347,128 @@ impl CircuitData { self.qubits.dispose(); self.clbits.dispose(); } + + #[setter] + pub fn global_phase(&mut self, py: Python, angle: Param) -> PyResult<()> { + let list_builtin = BUILTIN_LIST.get_bound(py); + self.remove_from_parameter_table(py, GLOBAL_PHASE_INDEX)?; + match angle { + Param::Float(angle) => { + self.global_phase = Param::Float(angle.rem_euclid(2. * std::f64::consts::PI)); + } + Param::ParameterExpression(angle) => { + let temp: PyObject = angle.getattr(py, intern!(py, "parameters"))?; + let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; + + for (param_index, param_obj) in raw_param_objs.into_iter().enumerate() { + let param_uuid: u128 = param_obj + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + match self.param_table.table.get_mut(¶m_uuid) { + Some(entry) => entry.add(GLOBAL_PHASE_INDEX, param_index), + None => { + let new_entry = ParamEntry::new(GLOBAL_PHASE_INDEX, param_index); + self.param_table.insert(py, param_obj, new_entry)?; + } + }; + } + self.global_phase = Param::ParameterExpression(angle); + } + Param::Obj(_) => return Err(PyValueError::new_err("Invalid type for global phase")), + }; + Ok(()) + } + + /// Get the global_phase sentinel value + #[classattr] + pub const fn global_phase_param_index() -> usize { + GLOBAL_PHASE_INDEX + } + + // Below are functions to interact with the parameter table. These methods + // are done to avoid needing to deal with shared references and provide + // an entry point via python through an owned CircuitData object. + pub fn num_params(&self) -> usize { + self.param_table.table.len() + } + + pub fn get_param_from_name(&self, py: Python, name: String) -> Option { + self.param_table.get_param_from_name(py, name) + } + + pub fn get_params_unsorted(&self, py: Python) -> PyResult> { + Ok(PySet::new_bound(py, self.param_table.uuid_map.values())?.unbind()) + } + + pub fn pop_param( + &mut self, + py: Python, + uuid: u128, + name: String, + default: PyObject, + ) -> PyObject { + match self.param_table.pop(uuid, name) { + Some(res) => res.into_py(py), + None => default.clone_ref(py), + } + } + + pub fn _get_param(&self, py: Python, uuid: u128) -> PyObject { + self.param_table.table[&uuid].clone().into_py(py) + } + + pub fn contains_param(&self, uuid: u128) -> bool { + self.param_table.table.contains_key(&uuid) + } + + pub fn add_new_parameter( + &mut self, + py: Python, + param: PyObject, + inst_index: usize, + param_index: usize, + ) -> PyResult<()> { + self.param_table.insert( + py, + param.clone_ref(py), + ParamEntry::new(inst_index, param_index), + )?; + Ok(()) + } + + pub fn update_parameter_entry( + &mut self, + uuid: u128, + inst_index: usize, + param_index: usize, + ) -> PyResult<()> { + match self.param_table.table.get_mut(&uuid) { + Some(entry) => { + entry.add(inst_index, param_index); + Ok(()) + } + None => Err(PyIndexError::new_err(format!( + "Invalid parameter uuid: {:?}", + uuid + ))), + } + } + + pub fn _get_entry_count(&self, py: Python, param_obj: PyObject) -> PyResult { + let uuid: u128 = param_obj + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + Ok(self.param_table.table[&uuid].index_ids.len()) + } + + pub fn num_nonlocal_gates(&self) -> usize { + self.data + .iter() + .filter(|inst| inst.op.num_qubits() > 1 && !inst.op.directive()) + .count() + } } impl CircuitData { @@ -730,23 +1521,43 @@ impl CircuitData { Ok(index as usize) } - fn pack( - &mut self, - py: Python, - value: PyRef, - ) -> PyResult { + fn pack(&mut self, py: Python, inst: PyRef) -> PyResult { + let qubits = Interner::intern( + &mut self.qargs_interner, + InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), + )?; + let clbits = Interner::intern( + &mut self.cargs_interner, + InternerKey::Value(self.clbits.map_bits(inst.clbits.bind(py))?.collect()), + )?; + Ok(PackedInstruction { + op: inst.operation.clone(), + qubits_id: qubits.index, + clbits_id: clbits.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), + }) + } + + fn pack_owned(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult { let qubits = Interner::intern( &mut self.qargs_interner, - InternerKey::Value(self.qubits.map_bits(value.qubits.bind(py))?.collect()), + InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), )?; let clbits = Interner::intern( &mut self.cargs_interner, - InternerKey::Value(self.clbits.map_bits(value.clbits.bind(py))?.collect()), + InternerKey::Value(self.clbits.map_bits(inst.clbits.bind(py))?.collect()), )?; Ok(PackedInstruction { - op: value.operation.clone_ref(py), + op: inst.operation.clone(), qubits_id: qubits.index, clbits_id: clbits.index, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), }) } } diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index ac61ae81a61..2bb90367082 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -11,9 +11,45 @@ // that they have been altered from the originals. use pyo3::basic::CompareOp; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::{PyList, PyTuple}; -use pyo3::{PyObject, PyResult}; +use pyo3::types::{IntoPyDict, PyList, PyTuple, PyType}; +use pyo3::{intern, IntoPy, PyObject, PyResult}; +use smallvec::{smallvec, SmallVec}; + +use crate::imports::{ + get_std_gate_class, populate_std_gate_map, GATE, INSTRUCTION, OPERATION, + SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, +}; +use crate::interner::Index; +use crate::operations::{OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate}; + +/// These are extra mutable attributes for a circuit instruction's state. In general we don't +/// typically deal with this in rust space and the majority of the time they're not used in Python +/// space either. To save memory these are put in a separate struct and are stored inside a +/// `Box` on `CircuitInstruction` and `PackedInstruction`. +#[derive(Debug, Clone)] +pub struct ExtraInstructionAttributes { + pub label: Option, + pub duration: Option, + pub unit: Option, + pub condition: Option, +} + +/// Private type used to store instructions with interned arg lists. +#[derive(Clone, Debug)] +pub(crate) struct PackedInstruction { + /// The Python-side operation instance. + pub op: OperationType, + /// The index under which the interner has stored `qubits`. + pub qubits_id: Index, + /// The index under which the interner has stored `clbits`. + pub clbits_id: Index, + pub params: SmallVec<[Param; 3]>, + pub extra_attrs: Option>, + #[cfg(feature = "cache_pygates")] + pub py_op: Option, +} /// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and /// various operands. @@ -47,28 +83,45 @@ use pyo3::{PyObject, PyResult}; /// mutations of the object do not invalidate the types, nor the restrictions placed on it by /// its context. Typically this will mean, for example, that :attr:`qubits` must be a sequence /// of distinct items, with no duplicates. -#[pyclass( - freelist = 20, - sequence, - get_all, - module = "qiskit._accelerate.circuit" -)] +#[pyclass(freelist = 20, sequence, module = "qiskit._accelerate.circuit")] #[derive(Clone, Debug)] pub struct CircuitInstruction { - /// The logical operation that this instruction represents an execution of. - pub operation: PyObject, + pub operation: OperationType, /// A sequence of the qubits that the operation is applied to. + #[pyo3(get)] pub qubits: Py, /// A sequence of the classical bits that this operation reads from or writes to. + #[pyo3(get)] pub clbits: Py, + pub params: SmallVec<[Param; 3]>, + pub extra_attrs: Option>, + #[cfg(feature = "cache_pygates")] + pub py_op: Option, +} + +/// This enum is for backwards compatibility if a user was doing something from +/// Python like CircuitInstruction(SXGate(), [qr[0]], []) by passing a python +/// gate object directly to a CircuitInstruction. In this case we need to +/// create a rust side object from the pyobject in CircuitInstruction.new() +/// With the `Object` variant which will convert the python object to a rust +/// `OperationType` +#[derive(FromPyObject, Debug)] +pub enum OperationInput { + Standard(StandardGate), + Gate(PyGate), + Instruction(PyInstruction), + Operation(PyOperation), + Object(PyObject), } impl CircuitInstruction { pub fn new( py: Python, - operation: PyObject, + operation: OperationType, qubits: impl IntoIterator, clbits: impl IntoIterator, + params: SmallVec<[Param; 3]>, + extra_attrs: Option>, ) -> Self where T1: ToPyObject, @@ -80,19 +133,41 @@ impl CircuitInstruction { operation, qubits: PyTuple::new_bound(py, qubits).unbind(), clbits: PyTuple::new_bound(py, clbits).unbind(), + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + } + } +} + +impl From for OperationInput { + fn from(value: OperationType) -> Self { + match value { + OperationType::Standard(op) => Self::Standard(op), + OperationType::Gate(gate) => Self::Gate(gate), + OperationType::Instruction(inst) => Self::Instruction(inst), + OperationType::Operation(op) => Self::Operation(op), } } } #[pymethods] impl CircuitInstruction { + #[allow(clippy::too_many_arguments)] #[new] + #[pyo3(signature = (operation, qubits=None, clbits=None, params=smallvec![], label=None, duration=None, unit=None, condition=None))] pub fn py_new( py: Python<'_>, - operation: PyObject, + operation: OperationInput, qubits: Option<&Bound>, clbits: Option<&Bound>, - ) -> PyResult> { + params: SmallVec<[Param; 3]>, + label: Option, + duration: Option, + unit: Option, + condition: Option, + ) -> PyResult { fn as_tuple(py: Python<'_>, seq: Option<&Bound>) -> PyResult> { match seq { None => Ok(PyTuple::empty_bound(py).unbind()), @@ -116,14 +191,136 @@ impl CircuitInstruction { } } - Py::new( - py, - CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - }, - ) + let extra_attrs = + if label.is_some() || duration.is_some() || unit.is_some() || condition.is_some() { + Some(Box::new(ExtraInstructionAttributes { + label, + duration, + unit, + condition, + })) + } else { + None + }; + + match operation { + OperationInput::Standard(operation) => { + let operation = OperationType::Standard(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Gate(operation) => { + let operation = OperationType::Gate(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Instruction(operation) => { + let operation = OperationType::Instruction(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Operation(operation) => { + let operation = OperationType::Operation(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: None, + }) + } + OperationInput::Object(old_op) => { + let op = convert_py_to_operation_type(py, old_op.clone_ref(py))?; + let extra_attrs = if op.label.is_some() + || op.duration.is_some() + || op.unit.is_some() + || op.condition.is_some() + { + Some(Box::new(ExtraInstructionAttributes { + label: op.label, + duration: op.duration, + unit: op.unit, + condition: op.condition, + })) + } else { + None + }; + + match op.operation { + OperationType::Standard(operation) => { + let operation = OperationType::Standard(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + OperationType::Gate(operation) => { + let operation = OperationType::Gate(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + OperationType::Instruction(operation) => { + let operation = OperationType::Instruction(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + OperationType::Operation(operation) => { + let operation = OperationType::Operation(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), + }) + } + } + } + } } /// Returns a shallow copy. @@ -134,28 +331,127 @@ impl CircuitInstruction { self.clone() } + /// The logical operation that this instruction represents an execution of. + #[cfg(not(feature = "cache_pygates"))] + #[getter] + pub fn operation(&self, py: Python) -> PyResult { + operation_type_to_py(py, self) + } + + #[cfg(feature = "cache_pygates")] + #[getter] + pub fn operation(&mut self, py: Python) -> PyResult { + Ok(match &self.py_op { + Some(op) => op.clone_ref(py), + None => { + let op = operation_type_to_py(py, self)?; + self.py_op = Some(op.clone_ref(py)); + op + } + }) + } + /// Creates a shallow copy with the given fields replaced. /// /// Returns: /// CircuitInstruction: A new instance with the given fields replaced. + #[allow(clippy::too_many_arguments)] pub fn replace( &self, py: Python<'_>, - operation: Option, + operation: Option, qubits: Option<&Bound>, clbits: Option<&Bound>, - ) -> PyResult> { + params: Option>, + label: Option, + duration: Option, + unit: Option, + condition: Option, + ) -> PyResult { + let operation = operation.unwrap_or_else(|| self.operation.clone().into()); + + let params = match params { + Some(params) => params, + None => self.params.clone(), + }; + + let label = match label { + Some(label) => Some(label), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.label.clone(), + None => None, + }, + }; + let duration = match duration { + Some(duration) => Some(duration), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.duration.clone(), + None => None, + }, + }; + + let unit: Option = match unit { + Some(unit) => Some(unit), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.unit.clone(), + None => None, + }, + }; + + let condition: Option = match condition { + Some(condition) => Some(condition), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.condition.clone(), + None => None, + }, + }; + CircuitInstruction::py_new( py, - operation.unwrap_or_else(|| self.operation.clone_ref(py)), + operation, Some(qubits.unwrap_or_else(|| self.qubits.bind(py))), Some(clbits.unwrap_or_else(|| self.clbits.bind(py))), + params, + label, + duration, + unit, + condition, ) } - fn __getnewargs__(&self, py: Python<'_>) -> PyResult { + fn __getstate__(&self, py: Python<'_>) -> PyResult { Ok(( - self.operation.bind(py), + operation_type_to_py(py, self)?, + self.qubits.bind(py), + self.clbits.bind(py), + ) + .into_py(py)) + } + + fn __setstate__(&mut self, py: Python<'_>, state: &Bound) -> PyResult<()> { + let op = convert_py_to_operation_type(py, state.get_item(0)?.into())?; + self.operation = op.operation; + self.params = op.params; + self.qubits = state.get_item(1)?.extract()?; + self.clbits = state.get_item(2)?.extract()?; + if op.label.is_some() + || op.duration.is_some() + || op.unit.is_some() + || op.condition.is_some() + { + self.extra_attrs = Some(Box::new(ExtraInstructionAttributes { + label: op.label, + duration: op.duration, + unit: op.unit, + condition: op.condition, + })); + } + Ok(()) + } + + pub fn __getnewargs__(&self, py: Python<'_>) -> PyResult { + Ok(( + operation_type_to_py(py, self)?, self.qubits.bind(py), self.clbits.bind(py), ) @@ -172,7 +468,7 @@ impl CircuitInstruction { , clbits={}\ )", type_name, - r.operation.bind(py).repr()?, + operation_type_to_py(py, &r)?, r.qubits.bind(py).repr()?, r.clbits.bind(py).repr()? )) @@ -184,23 +480,50 @@ impl CircuitInstruction { // the interface to behave exactly like the old 3-tuple `(inst, qargs, cargs)` if it's treated // like that via unpacking or similar. That means that the `parameters` field is completely // absent, and the qubits and clbits must be converted to lists. - pub fn _legacy_format<'py>(&self, py: Python<'py>) -> Bound<'py, PyTuple> { - PyTuple::new_bound( + #[cfg(not(feature = "cache_pygates"))] + pub fn _legacy_format<'py>(&self, py: Python<'py>) -> PyResult> { + let op = operation_type_to_py(py, self)?; + + Ok(PyTuple::new_bound( py, - [ - self.operation.bind(py), - &self.qubits.bind(py).to_list(), - &self.clbits.bind(py).to_list(), - ], - ) + [op, self.qubits.to_object(py), self.clbits.to_object(py)], + )) } + #[cfg(feature = "cache_pygates")] + pub fn _legacy_format<'py>(&mut self, py: Python<'py>) -> PyResult> { + let op = match &self.py_op { + Some(op) => op.clone_ref(py), + None => { + let op = operation_type_to_py(py, self)?; + self.py_op = Some(op.clone_ref(py)); + op + } + }; + Ok(PyTuple::new_bound( + py, + [op, self.qubits.to_object(py), self.clbits.to_object(py)], + )) + } + + #[cfg(not(feature = "cache_pygates"))] pub fn __getitem__(&self, py: Python<'_>, key: &Bound) -> PyResult { - Ok(self._legacy_format(py).as_any().get_item(key)?.into_py(py)) + Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) } + #[cfg(feature = "cache_pygates")] + pub fn __getitem__(&mut self, py: Python<'_>, key: &Bound) -> PyResult { + Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) + } + + #[cfg(not(feature = "cache_pygates"))] pub fn __iter__(&self, py: Python<'_>) -> PyResult { - Ok(self._legacy_format(py).as_any().iter()?.into_py(py)) + Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) + } + + #[cfg(feature = "cache_pygates")] + pub fn __iter__(&mut self, py: Python<'_>) -> PyResult { + Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) } pub fn __len__(&self) -> usize { @@ -227,16 +550,94 @@ impl CircuitInstruction { let other: PyResult> = other.extract(); return other.map_or(Ok(Some(false)), |v| { let v = v.try_borrow()?; + let op_eq = match &self_.operation { + OperationType::Standard(op) => { + if let OperationType::Standard(other) = &v.operation { + if op != other { + false + } else { + let other_params = &v.params; + let mut out = true; + for (param_a, param_b) in self_.params.iter().zip(other_params) + { + match param_a { + Param::Float(val_a) => { + if let Param::Float(val_b) = param_b { + if val_a != val_b { + out = false; + break; + } + } else { + out = false; + break; + } + } + Param::ParameterExpression(val_a) => { + if let Param::ParameterExpression(val_b) = param_b { + if !val_a.bind(py).eq(val_b.bind(py))? { + out = false; + break; + } + } else { + out = false; + break; + } + } + Param::Obj(val_a) => { + if let Param::Obj(val_b) = param_b { + if !val_a.bind(py).eq(val_b.bind(py))? { + out = false; + break; + } + } else { + out = false; + break; + } + } + } + } + out + } + } else { + false + } + } + OperationType::Gate(op) => { + if let OperationType::Gate(other) = &v.operation { + op.gate.bind(py).eq(other.gate.bind(py))? + } else { + false + } + } + OperationType::Instruction(op) => { + if let OperationType::Instruction(other) = &v.operation { + op.instruction.bind(py).eq(other.instruction.bind(py))? + } else { + false + } + } + OperationType::Operation(op) => { + if let OperationType::Operation(other) = &v.operation { + op.operation.bind(py).eq(other.operation.bind(py))? + } else { + false + } + } + }; + Ok(Some( self_.clbits.bind(py).eq(v.clbits.bind(py))? && self_.qubits.bind(py).eq(v.qubits.bind(py))? - && self_.operation.bind(py).eq(v.operation.bind(py))?, + && op_eq, )) }); } if other.is_instance_of::() { - return Ok(Some(self_._legacy_format(py).eq(other)?)); + #[cfg(feature = "cache_pygates")] + let mut self_ = self_.clone(); + let legacy_format = self_._legacy_format(py)?; + return Ok(Some(legacy_format.eq(other)?)); } Ok(None) @@ -255,3 +656,222 @@ impl CircuitInstruction { } } } + +/// Take a reference to a `CircuitInstruction` and convert the operation +/// inside that to a python side object. +pub(crate) fn operation_type_to_py( + py: Python, + circuit_inst: &CircuitInstruction, +) -> PyResult { + let (label, duration, unit, condition) = match &circuit_inst.extra_attrs { + None => (None, None, None, None), + Some(extra_attrs) => ( + extra_attrs.label.clone(), + extra_attrs.duration.clone(), + extra_attrs.unit.clone(), + extra_attrs.condition.clone(), + ), + }; + operation_type_and_data_to_py( + py, + &circuit_inst.operation, + &circuit_inst.params, + &label, + &duration, + &unit, + &condition, + ) +} + +/// Take an OperationType and the other mutable state fields from a +/// rust instruction representation and return a PyObject representing +/// a Python side full-fat Qiskit operation as a PyObject. This is typically +/// used by accessor functions that need to return an operation to Qiskit, such +/// as accesing `CircuitInstruction.operation`. +pub(crate) fn operation_type_and_data_to_py( + py: Python, + operation: &OperationType, + params: &[Param], + label: &Option, + duration: &Option, + unit: &Option, + condition: &Option, +) -> PyResult { + match &operation { + OperationType::Standard(op) => { + let gate_class: &PyObject = &get_std_gate_class(py, *op)?; + + let args = if params.is_empty() { + PyTuple::empty_bound(py) + } else { + PyTuple::new_bound(py, params) + }; + let kwargs = [ + ("label", label.to_object(py)), + ("unit", unit.to_object(py)), + ("duration", duration.to_object(py)), + ] + .into_py_dict_bound(py); + let mut out = gate_class.call_bound(py, args, Some(&kwargs))?; + if condition.is_some() { + out = out.call_method0(py, "to_mutable")?; + out.setattr(py, "condition", condition.to_object(py))?; + } + Ok(out) + } + OperationType::Gate(gate) => Ok(gate.gate.clone_ref(py)), + OperationType::Instruction(inst) => Ok(inst.instruction.clone_ref(py)), + OperationType::Operation(op) => Ok(op.operation.clone_ref(py)), + } +} + +/// A container struct that contains the output from the Python object to +/// conversion to construct a CircuitInstruction object +#[derive(Debug)] +pub(crate) struct OperationTypeConstruct { + pub operation: OperationType, + pub params: SmallVec<[Param; 3]>, + pub label: Option, + pub duration: Option, + pub unit: Option, + pub condition: Option, +} + +/// Convert an inbound Python object for a Qiskit operation and build a rust +/// representation of that operation. This will map it to appropriate variant +/// of operation type based on class +pub(crate) fn convert_py_to_operation_type( + py: Python, + py_op: PyObject, +) -> PyResult { + let attr = intern!(py, "_standard_gate"); + let py_op_bound = py_op.clone_ref(py).into_bound(py); + // Get PyType from either base_class if it exists, or if not use the + // class/type info from the pyobject + let binding = py_op_bound.getattr(intern!(py, "base_class")).ok(); + let op_obj = py_op_bound.get_type(); + let raw_op_type: Py = match binding { + Some(base_class) => base_class.downcast()?.clone().unbind(), + None => op_obj.unbind(), + }; + let op_type: Bound = raw_op_type.into_bound(py); + let mut standard: Option = match op_type.getattr(attr) { + Ok(stdgate) => match stdgate.extract().ok() { + Some(gate) => gate, + None => None, + }, + Err(_) => None, + }; + // If the input instruction is a standard gate and a singleton instance + // we should check for mutable state. A mutable instance should be treated + // as a custom gate not a standard gate because it has custom properties. + // + // In the futuer we can revisit this when we've dropped `duration`, `unit`, + // and `condition` from the api as we should own the label in the + // `CircuitInstruction`. The other piece here is for controlled gates there + // is the control state, so for `SingletonControlledGates` we'll still need + // this check. + if standard.is_some() { + let mutable: bool = py_op.getattr(py, intern!(py, "mutable"))?.extract(py)?; + if mutable + && (py_op_bound.is_instance(SINGLETON_GATE.get_bound(py))? + || py_op_bound.is_instance(SINGLETON_CONTROLLED_GATE.get_bound(py))?) + { + standard = None; + } + } + if let Some(op) = standard { + let base_class = op_type.to_object(py); + populate_std_gate_map(py, op, base_class); + return Ok(OperationTypeConstruct { + operation: OperationType::Standard(op), + params: py_op.getattr(py, intern!(py, "params"))?.extract(py)?, + label: py_op.getattr(py, intern!(py, "label"))?.extract(py)?, + duration: py_op.getattr(py, intern!(py, "duration"))?.extract(py)?, + unit: py_op.getattr(py, intern!(py, "unit"))?.extract(py)?, + condition: py_op.getattr(py, intern!(py, "condition"))?.extract(py)?, + }); + } + if op_type.is_subclass(GATE.get_bound(py))? { + let params = py_op.getattr(py, intern!(py, "params"))?.extract(py)?; + let label = py_op.getattr(py, intern!(py, "label"))?.extract(py)?; + let duration = py_op.getattr(py, intern!(py, "duration"))?.extract(py)?; + let unit = py_op.getattr(py, intern!(py, "unit"))?.extract(py)?; + let condition = py_op.getattr(py, intern!(py, "condition"))?.extract(py)?; + + let out_op = PyGate { + qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, + clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, + params: py_op + .getattr(py, intern!(py, "params"))? + .downcast_bound::(py)? + .len() as u32, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, + gate: py_op, + }; + return Ok(OperationTypeConstruct { + operation: OperationType::Gate(out_op), + params, + label, + duration, + unit, + condition, + }); + } + if op_type.is_subclass(INSTRUCTION.get_bound(py))? { + let params = py_op.getattr(py, intern!(py, "params"))?.extract(py)?; + let label = py_op.getattr(py, intern!(py, "label"))?.extract(py)?; + let duration = py_op.getattr(py, intern!(py, "duration"))?.extract(py)?; + let unit = py_op.getattr(py, intern!(py, "unit"))?.extract(py)?; + let condition = py_op.getattr(py, intern!(py, "condition"))?.extract(py)?; + + let out_op = PyInstruction { + qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, + clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, + params: py_op + .getattr(py, intern!(py, "params"))? + .downcast_bound::(py)? + .len() as u32, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, + instruction: py_op, + }; + return Ok(OperationTypeConstruct { + operation: OperationType::Instruction(out_op), + params, + label, + duration, + unit, + condition, + }); + } + + if op_type.is_subclass(OPERATION.get_bound(py))? { + let params = match py_op.getattr(py, intern!(py, "params")) { + Ok(value) => value.extract(py)?, + Err(_) => smallvec![], + }; + let label = None; + let duration = None; + let unit = None; + let condition = None; + let out_op = PyOperation { + qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, + clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, + params: match py_op.getattr(py, intern!(py, "params")) { + Ok(value) => value.downcast_bound::(py)?.len() as u32, + Err(_) => 0, + }, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, + operation: py_op, + }; + return Ok(OperationTypeConstruct { + operation: OperationType::Operation(out_op), + params, + label, + duration, + unit, + condition, + }); + } + Err(PyValueError::new_err(format!("Invalid input: {}", py_op))) +} diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index c766461bb51..c8b6a4c8b08 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -10,7 +10,11 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use crate::circuit_instruction::CircuitInstruction; +use crate::circuit_instruction::{ + convert_py_to_operation_type, operation_type_to_py, CircuitInstruction, + ExtraInstructionAttributes, +}; +use crate::operations::Operation; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; use pyo3::{intern, PyObject, PyResult}; @@ -106,13 +110,33 @@ impl DAGOpNode { } None => qargs.str()?.into_any(), }; + let res = convert_py_to_operation_type(py, op.clone_ref(py))?; + + let extra_attrs = if res.label.is_some() + || res.duration.is_some() + || res.unit.is_some() + || res.condition.is_some() + { + Some(Box::new(ExtraInstructionAttributes { + label: res.label, + duration: res.duration, + unit: res.unit, + condition: res.condition, + })) + } else { + None + }; Ok(( DAGOpNode { instruction: CircuitInstruction { - operation: op, + operation: res.operation, qubits: qargs.unbind(), clbits: cargs.unbind(), + params: res.params, + extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: Some(op), }, sort_key: sort_key.unbind(), }, @@ -120,18 +144,18 @@ impl DAGOpNode { )) } - fn __reduce__(slf: PyRef, py: Python) -> PyObject { + fn __reduce__(slf: PyRef, py: Python) -> PyResult { let state = (slf.as_ref()._node_id, &slf.sort_key); - ( + Ok(( py.get_type_bound::(), ( - &slf.instruction.operation, + operation_type_to_py(py, &slf.instruction)?, &slf.instruction.qubits, &slf.instruction.clbits, ), state, ) - .into_py(py) + .into_py(py)) } fn __setstate__(mut slf: PyRefMut, state: &Bound) -> PyResult<()> { @@ -142,13 +166,31 @@ impl DAGOpNode { } #[getter] - fn get_op(&self, py: Python) -> PyObject { - self.instruction.operation.clone_ref(py) + fn get_op(&self, py: Python) -> PyResult { + operation_type_to_py(py, &self.instruction) } #[setter] - fn set_op(&mut self, op: PyObject) { - self.instruction.operation = op; + fn set_op(&mut self, py: Python, op: PyObject) -> PyResult<()> { + let res = convert_py_to_operation_type(py, op)?; + self.instruction.operation = res.operation; + self.instruction.params = res.params; + let extra_attrs = if res.label.is_some() + || res.duration.is_some() + || res.unit.is_some() + || res.condition.is_some() + { + Some(Box::new(ExtraInstructionAttributes { + label: res.label, + duration: res.duration, + unit: res.unit, + condition: res.condition, + })) + } else { + None + }; + self.instruction.extra_attrs = extra_attrs; + Ok(()) } #[getter] @@ -173,29 +215,27 @@ impl DAGOpNode { /// Returns the Instruction name corresponding to the op for this node #[getter] - fn get_name(&self, py: Python) -> PyResult { - Ok(self - .instruction - .operation - .bind(py) - .getattr(intern!(py, "name"))? - .unbind()) + fn get_name(&self, py: Python) -> PyObject { + self.instruction.operation.name().to_object(py) } /// Sets the Instruction name corresponding to the op for this node #[setter] - fn set_name(&self, py: Python, new_name: PyObject) -> PyResult<()> { - self.instruction - .operation - .bind(py) - .setattr(intern!(py, "name"), new_name) + fn set_name(&mut self, py: Python, new_name: PyObject) -> PyResult<()> { + let op = operation_type_to_py(py, &self.instruction)?; + op.bind(py).setattr(intern!(py, "name"), new_name)?; + let res = convert_py_to_operation_type(py, op)?; + self.instruction.operation = res.operation; + Ok(()) } /// Returns a representation of the DAGOpNode fn __repr__(&self, py: Python) -> PyResult { Ok(format!( "DAGOpNode(op={}, qargs={}, cargs={})", - self.instruction.operation.bind(py).repr()?, + operation_type_to_py(py, &self.instruction)? + .bind(py) + .repr()?, self.instruction.qubits.bind(py).repr()?, self.instruction.clbits.bind(py).repr()? )) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs new file mode 100644 index 00000000000..72e1087637c --- /dev/null +++ b/crates/circuit/src/gate_matrix.rs @@ -0,0 +1,224 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2023 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use num_complex::Complex64; +use std::f64::consts::FRAC_1_SQRT_2; + +// num-complex exposes an equivalent function but it's not a const function +// so it's not compatible with static definitions. This is a const func and +// just reduces the amount of typing we need. +#[inline(always)] +const fn c64(re: f64, im: f64) -> Complex64 { + Complex64::new(re, im) +} + +pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = + [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(1., 0.)]]; + +#[inline] +pub fn rx_gate(theta: f64) -> [[Complex64; 2]; 2] { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let isin = c64(0., -half_theta.sin()); + [[cos, isin], [isin, cos]] +} + +#[inline] +pub fn ry_gate(theta: f64) -> [[Complex64; 2]; 2] { + let half_theta = theta / 2.; + let cos = c64(half_theta.cos(), 0.); + let sin = c64(half_theta.sin(), 0.); + [[cos, -sin], [sin, cos]] +} + +#[inline] +pub fn rz_gate(theta: f64) -> [[Complex64; 2]; 2] { + let ilam2 = c64(0., 0.5 * theta); + [[(-ilam2).exp(), c64(0., 0.)], [c64(0., 0.), ilam2.exp()]] +} + +pub static H_GATE: [[Complex64; 2]; 2] = [ + [c64(FRAC_1_SQRT_2, 0.), c64(FRAC_1_SQRT_2, 0.)], + [c64(FRAC_1_SQRT_2, 0.), c64(-FRAC_1_SQRT_2, 0.)], +]; + +pub static CX_GATE: [[Complex64; 4]; 4] = [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], + [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], +]; + +pub static SX_GATE: [[Complex64; 2]; 2] = [ + [c64(0.5, 0.5), c64(0.5, -0.5)], + [c64(0.5, -0.5), c64(0.5, 0.5)], +]; + +pub static X_GATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(1., 0.)], [c64(1., 0.), c64(0., 0.)]]; + +pub static Z_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(-1., 0.)]]; + +pub static Y_GATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(0., -1.)], [c64(0., 1.), c64(0., 0.)]]; + +pub static CZ_GATE: [[Complex64; 4]; 4] = [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(-1., 0.)], +]; + +pub static CY_GATE: [[Complex64; 4]; 4] = [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(0., -1.)], + [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 1.), c64(0., 0.), c64(0., 0.)], +]; + +pub static CCX_GATE: [[Complex64; 8]; 8] = [ + [ + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(1., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., 0.), + ], +]; + +pub static ECR_GATE: [[Complex64; 4]; 4] = [ + [ + c64(0., 0.), + c64(FRAC_1_SQRT_2, 0.), + c64(0., 0.), + c64(0., FRAC_1_SQRT_2), + ], + [ + c64(FRAC_1_SQRT_2, 0.), + c64(0., 0.), + c64(0., -FRAC_1_SQRT_2), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., FRAC_1_SQRT_2), + c64(0., 0.), + c64(FRAC_1_SQRT_2, 0.), + ], + [ + c64(0., -FRAC_1_SQRT_2), + c64(0., 0.), + c64(FRAC_1_SQRT_2, 0.), + c64(0., 0.), + ], +]; + +pub static SWAP_GATE: [[Complex64; 4]; 4] = [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], +]; + +#[inline] +pub fn global_phase_gate(theta: f64) -> [[Complex64; 1]; 1] { + [[c64(0., theta).exp()]] +} + +#[inline] +pub fn phase_gate(lam: f64) -> [[Complex64; 2]; 2] { + [ + [c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., lam).exp()], + ] +} + +#[inline] +pub fn u_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [c64(cos, 0.), (-c64(0., lam).exp()) * sin], + [c64(0., phi).exp() * sin, c64(0., phi + lam).exp() * cos], + ] +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs new file mode 100644 index 00000000000..050f7f2e053 --- /dev/null +++ b/crates/circuit/src/imports.rs @@ -0,0 +1,168 @@ +// 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. + +// This module contains objects imported from Python that are reused. These are +// typically data model classes that are used to identify an object, or for +// python side casting + +use pyo3::prelude::*; +use pyo3::sync::GILOnceCell; + +use crate::operations::{StandardGate, STANDARD_GATE_SIZE}; + +/// Helper wrapper around `GILOnceCell` instances that are just intended to store a Python object +/// that is lazily imported. +pub struct ImportOnceCell { + module: &'static str, + object: &'static str, + cell: GILOnceCell>, +} + +impl ImportOnceCell { + const fn new(module: &'static str, object: &'static str) -> Self { + Self { + module, + object, + cell: GILOnceCell::new(), + } + } + + /// Get the underlying GIL-independent reference to the contained object, importing if + /// required. + #[inline] + pub fn get(&self, py: Python) -> &Py { + self.cell.get_or_init(py, || { + py.import_bound(self.module) + .unwrap() + .getattr(self.object) + .unwrap() + .unbind() + }) + } + + /// Get a GIL-bound reference to the contained object, importing if required. + #[inline] + pub fn get_bound<'py>(&self, py: Python<'py>) -> &Bound<'py, PyAny> { + self.get(py).bind(py) + } +} + +pub static BUILTIN_LIST: ImportOnceCell = ImportOnceCell::new("builtins", "list"); +pub static OPERATION: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.operation", "Operation"); +pub static INSTRUCTION: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.instruction", "Instruction"); +pub static GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.gate", "Gate"); +pub static QUBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.quantumregister", "Qubit"); +pub static CLBIT: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.classicalregister", "Clbit"); +pub static PARAMETER_EXPRESSION: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.parameterexpression", "ParameterExpression"); +pub static QUANTUM_CIRCUIT: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.quantumcircuit", "QuantumCircuit"); +pub static SINGLETON_GATE: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.singleton", "SingletonGate"); +pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = + ImportOnceCell::new("qiskit.circuit.singleton", "SingletonControlledGate"); + +/// 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 +/// when a gate is added directly via the StandardGate path and there isn't a Python object +/// to poll the _standard_gate attribute for. +/// +/// NOTE: the order here is significant it must match the StandardGate variant's number must match +/// index of it's entry in this table. This is all done statically for performance +static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ + // ZGate = 0 + ["qiskit.circuit.library.standard_gates.z", "ZGate"], + // YGate = 1 + ["qiskit.circuit.library.standard_gates.y", "YGate"], + // XGate = 2 + ["qiskit.circuit.library.standard_gates.x", "XGate"], + // CZGate = 3 + ["qiskit.circuit.library.standard_gates.z", "CZGate"], + // CYGate = 4 + ["qiskit.circuit.library.standard_gates.y", "CYGate"], + // CXGate = 5 + ["qiskit.circuit.library.standard_gates.x", "CXGate"], + // CCXGate = 6 + ["qiskit.circuit.library.standard_gates.x", "CCXGate"], + // RXGate = 7 + ["qiskit.circuit.library.standard_gates.rx", "RXGate"], + // RYGate = 8 + ["qiskit.circuit.library.standard_gates.ry", "RYGate"], + // RZGate = 9 + ["qiskit.circuit.library.standard_gates.rz", "RZGate"], + // ECRGate = 10 + ["qiskit.circuit.library.standard_gates.ecr", "ECRGate"], + // SwapGate = 11 + ["qiskit.circuit.library.standard_gates.swap", "SwapGate"], + // SXGate = 12 + ["qiskit.circuit.library.standard_gates.sx", "SXGate"], + // GlobalPhaseGate = 13 + [ + "qiskit.circuit.library.standard_gates.global_phase", + "GlobalPhaseGate", + ], + // IGate = 14 + ["qiskit.circuit.library.standard_gates.i", "IGate"], + // HGate = 15 + ["qiskit.circuit.library.standard_gates.h", "HGate"], + // PhaseGate = 16 + ["qiskit.circuit.library.standard_gates.p", "PhaseGate"], + // UGate = 17 + ["qiskit.circuit.library.standard_gates.u", "UGate"], +]; + +/// A mapping from the enum variant in crate::operations::StandardGate to the python object for the +/// class that matches it. This is typically used when we need to convert from the internal rust +/// representation to a Python object for a python user to interact with. +/// +/// NOTE: the order here is significant it must match the StandardGate variant's number must match +/// index of it's entry in this table. This is all done statically for performance +static mut STDGATE_PYTHON_GATES: GILOnceCell<[Option; STANDARD_GATE_SIZE]> = + GILOnceCell::new(); + +#[inline] +pub fn populate_std_gate_map(py: Python, rs_gate: StandardGate, py_gate: PyObject) { + let gate_map = unsafe { + match STDGATE_PYTHON_GATES.get_mut() { + Some(gate_map) => gate_map, + None => { + let array: [Option; STANDARD_GATE_SIZE] = std::array::from_fn(|_| None); + STDGATE_PYTHON_GATES.set(py, array).unwrap(); + STDGATE_PYTHON_GATES.get_mut().unwrap() + } + } + }; + let gate_cls = &gate_map[rs_gate as usize]; + if gate_cls.is_none() { + gate_map[rs_gate as usize] = Some(py_gate.clone_ref(py)); + } +} + +#[inline] +pub fn get_std_gate_class(py: Python, rs_gate: StandardGate) -> PyResult { + let gate_map = + unsafe { STDGATE_PYTHON_GATES.get_or_init(py, || std::array::from_fn(|_| None)) }; + let gate = &gate_map[rs_gate as usize]; + let populate = gate.is_none(); + let out_gate = match gate { + Some(gate) => gate.clone_ref(py), + None => { + let [py_mod, py_class] = STDGATE_IMPORT_PATHS[rs_gate as usize]; + py.import_bound(py_mod)?.getattr(py_class)?.unbind() + } + }; + if populate { + populate_std_gate_map(py, rs_gate, out_gate.clone_ref(py)); + } + Ok(out_gate) +} diff --git a/crates/circuit/src/interner.rs b/crates/circuit/src/interner.rs index 42667570205..f22bb80ae05 100644 --- a/crates/circuit/src/interner.rs +++ b/crates/circuit/src/interner.rs @@ -10,11 +10,13 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use hashbrown::HashMap; -use pyo3::{IntoPy, PyObject, Python}; use std::hash::Hash; use std::sync::Arc; +use hashbrown::HashMap; +use pyo3::exceptions::PyRuntimeError; +use pyo3::prelude::*; + #[derive(Clone, Copy, Debug)] pub struct Index(u32); @@ -42,6 +44,12 @@ impl IntoPy for Index { pub struct CacheFullError; +impl From for PyErr { + fn from(_: CacheFullError) -> Self { + PyRuntimeError::new_err("The bit operands cache is full!") + } +} + /// An append-only data structure for interning generic /// Rust types. #[derive(Clone, Debug)] diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 90f2b7c7f07..d7f28591175 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -13,10 +13,13 @@ pub mod circuit_data; pub mod circuit_instruction; pub mod dag_node; +pub mod gate_matrix; +pub mod imports; +pub mod operations; +pub mod parameter_table; mod bit_data; mod interner; -mod packed_instruction; use pyo3::prelude::*; use pyo3::types::PySlice; @@ -33,9 +36,9 @@ pub enum SliceOrInt<'a> { pub type BitType = u32; #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] -pub struct Qubit(BitType); +pub struct Qubit(pub BitType); #[derive(Copy, Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] -pub struct Clbit(BitType); +pub struct Clbit(pub BitType); impl From for Qubit { fn from(value: BitType) -> Self { @@ -69,5 +72,9 @@ pub fn circuit(m: Bound) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs new file mode 100644 index 00000000000..ead1b8ee1eb --- /dev/null +++ b/crates/circuit/src/operations.rs @@ -0,0 +1,786 @@ +// 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::f64::consts::PI; + +use crate::circuit_data::CircuitData; +use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; +use crate::{gate_matrix, Qubit}; + +use ndarray::{aview2, Array2}; +use num_complex::Complex64; +use numpy::IntoPyArray; +use numpy::PyReadonlyArray2; +use pyo3::prelude::*; +use pyo3::{intern, IntoPy, Python}; +use smallvec::smallvec; + +/// Valid types for an operation field in a CircuitInstruction +/// +/// These are basically the types allowed in a QuantumCircuit +#[derive(FromPyObject, Clone, Debug)] +pub enum OperationType { + Standard(StandardGate), + Instruction(PyInstruction), + Gate(PyGate), + Operation(PyOperation), +} + +impl Operation for OperationType { + fn name(&self) -> &str { + match self { + Self::Standard(op) => op.name(), + Self::Gate(op) => op.name(), + Self::Instruction(op) => op.name(), + Self::Operation(op) => op.name(), + } + } + + fn num_qubits(&self) -> u32 { + match self { + Self::Standard(op) => op.num_qubits(), + Self::Gate(op) => op.num_qubits(), + Self::Instruction(op) => op.num_qubits(), + Self::Operation(op) => op.num_qubits(), + } + } + fn num_clbits(&self) -> u32 { + match self { + Self::Standard(op) => op.num_clbits(), + Self::Gate(op) => op.num_clbits(), + Self::Instruction(op) => op.num_clbits(), + Self::Operation(op) => op.num_clbits(), + } + } + + fn num_params(&self) -> u32 { + match self { + Self::Standard(op) => op.num_params(), + Self::Gate(op) => op.num_params(), + Self::Instruction(op) => op.num_params(), + Self::Operation(op) => op.num_params(), + } + } + fn matrix(&self, params: &[Param]) -> Option> { + match self { + Self::Standard(op) => op.matrix(params), + Self::Gate(op) => op.matrix(params), + Self::Instruction(op) => op.matrix(params), + Self::Operation(op) => op.matrix(params), + } + } + + fn control_flow(&self) -> bool { + match self { + Self::Standard(op) => op.control_flow(), + Self::Gate(op) => op.control_flow(), + Self::Instruction(op) => op.control_flow(), + Self::Operation(op) => op.control_flow(), + } + } + + fn definition(&self, params: &[Param]) -> Option { + match self { + Self::Standard(op) => op.definition(params), + Self::Gate(op) => op.definition(params), + Self::Instruction(op) => op.definition(params), + Self::Operation(op) => op.definition(params), + } + } + + fn standard_gate(&self) -> Option { + match self { + Self::Standard(op) => op.standard_gate(), + Self::Gate(op) => op.standard_gate(), + Self::Instruction(op) => op.standard_gate(), + Self::Operation(op) => op.standard_gate(), + } + } + + fn directive(&self) -> bool { + match self { + Self::Standard(op) => op.directive(), + Self::Gate(op) => op.directive(), + Self::Instruction(op) => op.directive(), + Self::Operation(op) => op.directive(), + } + } +} + +/// Trait for generic circuit operations these define the common attributes +/// needed for something to be addable to the circuit struct +pub trait Operation { + fn name(&self) -> &str; + fn num_qubits(&self) -> u32; + fn num_clbits(&self) -> u32; + fn num_params(&self) -> u32; + fn control_flow(&self) -> bool; + fn matrix(&self, params: &[Param]) -> Option>; + fn definition(&self, params: &[Param]) -> Option; + fn standard_gate(&self) -> Option; + fn directive(&self) -> bool; +} + +#[derive(Clone, Debug)] +pub enum Param { + ParameterExpression(PyObject), + Float(f64), + Obj(PyObject), +} + +impl<'py> FromPyObject<'py> for Param { + fn extract_bound(b: &Bound<'py, PyAny>) -> Result { + Ok( + if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))? + || b.is_instance(QUANTUM_CIRCUIT.get_bound(b.py()))? + { + Param::ParameterExpression(b.clone().unbind()) + } else if let Ok(val) = b.extract::() { + Param::Float(val) + } else { + Param::Obj(b.clone().unbind()) + }, + ) + } +} + +impl IntoPy for Param { + fn into_py(self, py: Python) -> PyObject { + match &self { + Self::Float(val) => val.to_object(py), + Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), + } + } +} + +impl ToPyObject for Param { + fn to_object(&self, py: Python) -> PyObject { + match self { + Self::Float(val) => val.to_object(py), + Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), + } + } +} + +#[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] +#[pyclass(module = "qiskit._accelerate.circuit")] +pub enum StandardGate { + ZGate = 0, + YGate = 1, + XGate = 2, + CZGate = 3, + CYGate = 4, + CXGate = 5, + CCXGate = 6, + RXGate = 7, + RYGate = 8, + RZGate = 9, + ECRGate = 10, + SwapGate = 11, + SXGate = 12, + GlobalPhaseGate = 13, + IGate = 14, + HGate = 15, + PhaseGate = 16, + UGate = 17, +} + +static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = + [1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1]; + +static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3]; + +static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ + "z", + "y", + "x", + "cz", + "cy", + "cx", + "ccx", + "rx", + "ry", + "rz", + "ecr", + "swap", + "sx", + "global_phase", + "id", + "h", + "p", + "u", +]; + +#[pymethods] +impl StandardGate { + pub fn copy(&self) -> Self { + *self + } + + // These pymethods are for testing: + pub fn _to_matrix(&self, py: Python, params: Vec) -> Option { + self.matrix(¶ms) + .map(|x| x.into_pyarray_bound(py).into()) + } + + pub fn _num_params(&self) -> u32 { + self.num_params() + } + + pub fn _get_definition(&self, params: Vec) -> Option { + self.definition(¶ms) + } + + #[getter] + pub fn get_num_qubits(&self) -> u32 { + self.num_qubits() + } + + #[getter] + pub fn get_num_clbits(&self) -> u32 { + self.num_clbits() + } + + #[getter] + pub fn get_num_params(&self) -> u32 { + self.num_params() + } + + #[getter] + pub fn get_name(&self) -> &str { + self.name() + } +} + +// This must be kept up-to-date with `StandardGate` when adding or removing +// gates from the enum +// +// Remove this when std::mem::variant_count() is stabilized (see +// https://github.com/rust-lang/rust/issues/73662 ) +pub const STANDARD_GATE_SIZE: usize = 18; + +impl Operation for StandardGate { + fn name(&self) -> &str { + STANDARD_GATE_NAME[*self as usize] + } + + fn num_qubits(&self) -> u32 { + STANDARD_GATE_NUM_QUBITS[*self as usize] + } + + fn num_params(&self) -> u32 { + STANDARD_GATE_NUM_PARAMS[*self as usize] + } + + fn num_clbits(&self) -> u32 { + 0 + } + + fn control_flow(&self) -> bool { + false + } + + fn directive(&self) -> bool { + false + } + + fn matrix(&self, params: &[Param]) -> Option> { + match self { + Self::ZGate => match params { + [] => Some(aview2(&gate_matrix::Z_GATE).to_owned()), + _ => None, + }, + Self::YGate => match params { + [] => Some(aview2(&gate_matrix::Y_GATE).to_owned()), + _ => None, + }, + Self::XGate => match params { + [] => Some(aview2(&gate_matrix::X_GATE).to_owned()), + _ => None, + }, + Self::CZGate => match params { + [] => Some(aview2(&gate_matrix::CZ_GATE).to_owned()), + _ => None, + }, + Self::CYGate => match params { + [] => Some(aview2(&gate_matrix::CY_GATE).to_owned()), + _ => None, + }, + Self::CXGate => match params { + [] => Some(aview2(&gate_matrix::CX_GATE).to_owned()), + _ => None, + }, + Self::CCXGate => match params { + [] => Some(aview2(&gate_matrix::CCX_GATE).to_owned()), + _ => None, + }, + Self::RXGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::rx_gate(*theta)).to_owned()), + _ => None, + }, + Self::RYGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::ry_gate(*theta)).to_owned()), + _ => None, + }, + Self::RZGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::rz_gate(*theta)).to_owned()), + _ => None, + }, + Self::ECRGate => match params { + [] => Some(aview2(&gate_matrix::ECR_GATE).to_owned()), + _ => None, + }, + Self::SwapGate => match params { + [] => Some(aview2(&gate_matrix::SWAP_GATE).to_owned()), + _ => None, + }, + Self::SXGate => match params { + [] => Some(aview2(&gate_matrix::SX_GATE).to_owned()), + _ => None, + }, + Self::GlobalPhaseGate => match params { + [Param::Float(theta)] => { + Some(aview2(&gate_matrix::global_phase_gate(*theta)).to_owned()) + } + _ => None, + }, + Self::IGate => match params { + [] => Some(aview2(&gate_matrix::ONE_QUBIT_IDENTITY).to_owned()), + _ => None, + }, + Self::HGate => match params { + [] => Some(aview2(&gate_matrix::H_GATE).to_owned()), + _ => None, + }, + Self::PhaseGate => match params { + [Param::Float(theta)] => Some(aview2(&gate_matrix::phase_gate(*theta)).to_owned()), + _ => None, + }, + Self::UGate => match params { + [Param::Float(theta), Param::Float(phi), Param::Float(lam)] => { + Some(aview2(&gate_matrix::u_gate(*theta, *phi, *lam)).to_owned()) + } + _ => None, + }, + } + } + + fn definition(&self, params: &[Param]) -> Option { + match self { + Self::ZGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(PI)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::YGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![ + Param::Float(PI), + Param::Float(PI / 2.), + Param::Float(PI / 2.), + ], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::XGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(PI), Param::Float(0.), Param::Float(PI)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CZGate => Python::with_gil(|py| -> Option { + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::HGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1), + (Self::HGate, smallvec![], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CYGate => todo!("Add when we have S and S dagger"), + Self::CXGate => None, + Self::CCXGate => todo!("Add when we have T and TDagger"), + Self::RXGate => todo!("Add when we have R"), + Self::RYGate => todo!("Add when we have R"), + Self::RZGate => Python::with_gil(|py| -> Option { + match ¶ms[0] { + Param::Float(theta) => Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::Float(*theta)], + smallvec![Qubit(0)], + )], + Param::Float(-0.5 * theta), + ) + .expect("Unexpected Qiskit python bug"), + ), + Param::ParameterExpression(theta) => Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![Param::ParameterExpression(theta.clone_ref(py))], + smallvec![Qubit(0)], + )], + Param::ParameterExpression( + theta + .call_method1(py, intern!(py, "__rmul__"), (-0.5,)) + .expect("Parameter expression for global phase failed"), + ), + ) + .expect("Unexpected Qiskit python bug"), + ), + Param::Obj(_) => unreachable!(), + } + }), + Self::ECRGate => todo!("Add when we have RZX"), + Self::SwapGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + (Self::CXGate, smallvec![], smallvec![Qubit(1), Qubit(0)]), + (Self::CXGate, smallvec![], smallvec![Qubit(0), Qubit(1)]), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::SXGate => todo!("Add when we have S dagger"), + Self::GlobalPhaseGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates(py, 0, [], params[0].clone()) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::IGate => None, + Self::HGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(PI / 2.), Param::Float(0.), Param::Float(PI)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::PhaseGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(0.), Param::Float(0.), params[0].clone()], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::UGate => None, + } + } + + fn standard_gate(&self) -> Option { + Some(*self) + } +} + +const FLOAT_ZERO: Param = Param::Float(0.0); + +/// This class is used to wrap a Python side Instruction that is not in the standard library +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub struct PyInstruction { + pub qubits: u32, + pub clbits: u32, + pub params: u32, + pub op_name: String, + pub instruction: PyObject, +} + +#[pymethods] +impl PyInstruction { + #[new] + fn new(op_name: String, qubits: u32, clbits: u32, params: u32, instruction: PyObject) -> Self { + PyInstruction { + qubits, + clbits, + params, + op_name, + instruction, + } + } +} + +impl Operation for PyInstruction { + fn name(&self) -> &str { + self.op_name.as_str() + } + fn num_qubits(&self) -> u32 { + self.qubits + } + fn num_clbits(&self) -> u32 { + self.clbits + } + fn num_params(&self) -> u32 { + self.params + } + fn control_flow(&self) -> bool { + false + } + fn matrix(&self, _params: &[Param]) -> Option> { + None + } + fn definition(&self, _params: &[Param]) -> Option { + Python::with_gil(|py| -> Option { + match self.instruction.getattr(py, intern!(py, "definition")) { + Ok(definition) => { + let res: Option = definition.call0(py).ok()?.extract(py).ok(); + match res { + Some(x) => { + let out: CircuitData = + x.getattr(py, intern!(py, "data")).ok()?.extract(py).ok()?; + Some(out) + } + None => None, + } + } + Err(_) => None, + } + }) + } + fn standard_gate(&self) -> Option { + None + } + + fn directive(&self) -> bool { + Python::with_gil(|py| -> bool { + match self.instruction.getattr(py, intern!(py, "_directive")) { + Ok(directive) => { + let res: bool = directive.extract(py).unwrap(); + res + } + Err(_) => false, + } + }) + } +} + +/// This class is used to wrap a Python side Gate that is not in the standard library +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub struct PyGate { + pub qubits: u32, + pub clbits: u32, + pub params: u32, + pub op_name: String, + pub gate: PyObject, +} + +#[pymethods] +impl PyGate { + #[new] + fn new(op_name: String, qubits: u32, clbits: u32, params: u32, gate: PyObject) -> Self { + PyGate { + qubits, + clbits, + params, + op_name, + gate, + } + } +} + +impl Operation for PyGate { + fn name(&self) -> &str { + self.op_name.as_str() + } + fn num_qubits(&self) -> u32 { + self.qubits + } + fn num_clbits(&self) -> u32 { + self.clbits + } + fn num_params(&self) -> u32 { + self.params + } + fn control_flow(&self) -> bool { + false + } + fn matrix(&self, _params: &[Param]) -> Option> { + Python::with_gil(|py| -> Option> { + match self.gate.getattr(py, intern!(py, "to_matrix")) { + Ok(to_matrix) => { + let res: Option = to_matrix.call0(py).ok()?.extract(py).ok(); + match res { + Some(x) => { + let array: PyReadonlyArray2 = x.extract(py).ok()?; + Some(array.as_array().to_owned()) + } + None => None, + } + } + Err(_) => None, + } + }) + } + fn definition(&self, _params: &[Param]) -> Option { + Python::with_gil(|py| -> Option { + match self.gate.getattr(py, intern!(py, "definition")) { + Ok(definition) => { + let res: Option = definition.call0(py).ok()?.extract(py).ok(); + match res { + Some(x) => { + let out: CircuitData = + x.getattr(py, intern!(py, "data")).ok()?.extract(py).ok()?; + Some(out) + } + None => None, + } + } + Err(_) => None, + } + }) + } + fn standard_gate(&self) -> Option { + Python::with_gil(|py| -> Option { + match self.gate.getattr(py, intern!(py, "_standard_gate")) { + Ok(stdgate) => match stdgate.extract(py) { + Ok(out_gate) => out_gate, + Err(_) => None, + }, + Err(_) => None, + } + }) + } + fn directive(&self) -> bool { + false + } +} + +/// This class is used to wrap a Python side Operation that is not in the standard library +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub struct PyOperation { + pub qubits: u32, + pub clbits: u32, + pub params: u32, + pub op_name: String, + pub operation: PyObject, +} + +#[pymethods] +impl PyOperation { + #[new] + fn new(op_name: String, qubits: u32, clbits: u32, params: u32, operation: PyObject) -> Self { + PyOperation { + qubits, + clbits, + params, + op_name, + operation, + } + } +} + +impl Operation for PyOperation { + fn name(&self) -> &str { + self.op_name.as_str() + } + fn num_qubits(&self) -> u32 { + self.qubits + } + fn num_clbits(&self) -> u32 { + self.clbits + } + fn num_params(&self) -> u32 { + self.params + } + fn control_flow(&self) -> bool { + false + } + fn matrix(&self, _params: &[Param]) -> Option> { + None + } + fn definition(&self, _params: &[Param]) -> Option { + None + } + fn standard_gate(&self) -> Option { + None + } + + fn directive(&self) -> bool { + Python::with_gil(|py| -> bool { + match self.operation.getattr(py, intern!(py, "_directive")) { + Ok(directive) => { + let res: bool = directive.extract(py).unwrap(); + res + } + Err(_) => false, + } + }) + } +} diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs deleted file mode 100644 index 0c793f2b640..00000000000 --- a/crates/circuit/src/packed_instruction.rs +++ /dev/null @@ -1,25 +0,0 @@ -// 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 crate::interner::Index; -use pyo3::prelude::*; - -/// Private type used to store instructions with interned arg lists. -#[derive(Clone, Debug)] -pub(crate) struct PackedInstruction { - /// The Python-side operation instance. - pub op: PyObject, - /// The index under which the interner has stored `qubits`. - pub qubits_id: Index, - /// The index under which the interner has stored `clbits`. - pub clbits_id: Index, -} diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs new file mode 100644 index 00000000000..48c779eed3a --- /dev/null +++ b/crates/circuit/src/parameter_table.rs @@ -0,0 +1,173 @@ +// 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 pyo3::prelude::*; +use pyo3::{import_exception, intern, PyObject}; + +import_exception!(qiskit.circuit.exceptions, CircuitError); + +use hashbrown::{HashMap, HashSet}; + +/// The index value in a `ParamEntry` that indicates the global phase. +pub const GLOBAL_PHASE_INDEX: usize = usize::MAX; + +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub(crate) struct ParamEntryKeys { + keys: Vec<(usize, usize)>, + iter_pos: usize, +} + +#[pymethods] +impl ParamEntryKeys { + fn __iter__(slf: PyRef) -> Py { + slf.into() + } + + fn __next__(mut slf: PyRefMut) -> Option<(usize, usize)> { + if slf.iter_pos < slf.keys.len() { + let res = Some(slf.keys[slf.iter_pos]); + slf.iter_pos += 1; + res + } else { + None + } + } +} + +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub(crate) struct ParamEntry { + /// Mapping of tuple of instruction index (in CircuitData) and parameter index to the actual + /// parameter object + pub index_ids: HashSet<(usize, usize)>, +} + +impl ParamEntry { + pub fn add(&mut self, inst_index: usize, param_index: usize) { + self.index_ids.insert((inst_index, param_index)); + } + + pub fn discard(&mut self, inst_index: usize, param_index: usize) { + self.index_ids.remove(&(inst_index, param_index)); + } +} + +#[pymethods] +impl ParamEntry { + #[new] + pub fn new(inst_index: usize, param_index: usize) -> Self { + ParamEntry { + index_ids: HashSet::from([(inst_index, param_index)]), + } + } + + pub fn __len__(&self) -> usize { + self.index_ids.len() + } + + pub fn __contains__(&self, key: (usize, usize)) -> bool { + self.index_ids.contains(&key) + } + + pub fn __iter__(&self) -> ParamEntryKeys { + ParamEntryKeys { + keys: self.index_ids.iter().copied().collect(), + iter_pos: 0, + } + } +} + +#[derive(Clone, Debug)] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +pub(crate) struct ParamTable { + /// Mapping of parameter uuid (as an int) to the Parameter Entry + pub table: HashMap, + /// Mapping of parameter name to uuid as an int + pub names: HashMap, + /// Mapping of uuid to a parameter object + pub uuid_map: HashMap, +} + +impl ParamTable { + pub fn insert(&mut self, py: Python, parameter: PyObject, entry: ParamEntry) -> PyResult<()> { + let uuid: u128 = parameter + .getattr(py, intern!(py, "_uuid"))? + .getattr(py, intern!(py, "int"))? + .extract(py)?; + let name: String = parameter.getattr(py, intern!(py, "name"))?.extract(py)?; + + if self.names.contains_key(&name) && !self.table.contains_key(&uuid) { + return Err(CircuitError::new_err(format!( + "Name conflict on adding parameter: {}", + name + ))); + } + self.table.insert(uuid, entry); + self.names.insert(name, uuid); + self.uuid_map.insert(uuid, parameter); + Ok(()) + } + + pub fn discard_references( + &mut self, + uuid: u128, + inst_index: usize, + param_index: usize, + name: String, + ) { + if let Some(refs) = self.table.get_mut(&uuid) { + if refs.__len__() == 1 { + self.table.remove(&uuid); + self.names.remove(&name); + self.uuid_map.remove(&uuid); + } else { + refs.discard(inst_index, param_index); + } + } + } +} + +#[pymethods] +impl ParamTable { + #[new] + pub fn new() -> Self { + ParamTable { + table: HashMap::new(), + names: HashMap::new(), + uuid_map: HashMap::new(), + } + } + + pub fn clear(&mut self) { + self.table.clear(); + self.names.clear(); + self.uuid_map.clear(); + } + + pub fn pop(&mut self, key: u128, name: String) -> Option { + self.names.remove(&name); + self.uuid_map.remove(&key); + self.table.remove(&key) + } + + fn set(&mut self, uuid: u128, name: String, param: PyObject, refs: ParamEntry) { + self.names.insert(name, uuid); + self.table.insert(uuid, refs); + self.uuid_map.insert(uuid, param); + } + + pub fn get_param_from_name(&self, py: Python, name: String) -> Option { + self.names + .get(&name) + .map(|x| self.uuid_map.get(x).map(|y| y.clone_ref(py)))? + } +} diff --git a/crates/pyext/Cargo.toml b/crates/pyext/Cargo.toml index daaf19e1f6a..413165e84b1 100644 --- a/crates/pyext/Cargo.toml +++ b/crates/pyext/Cargo.toml @@ -17,6 +17,7 @@ crate-type = ["cdylib"] # crates as standalone binaries, executables, we need `libpython` to be linked in, so we make the # feature a default, and run `cargo test --no-default-features` to turn it off. default = ["pyo3/extension-module"] +cache_pygates = ["pyo3/extension-module", "qiskit-circuit/cache_pygates"] [dependencies] pyo3.workspace = true diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index c6c95d27f92..bb0a30ea6af 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -57,7 +57,9 @@ def instructions(self) -> Sequence[CircuitInstruction]: """Indexable view onto the :class:`.CircuitInstruction`s backing this scope.""" @abc.abstractmethod - def append(self, instruction: CircuitInstruction) -> CircuitInstruction: + def append( + self, instruction: CircuitInstruction, *, _standard_gate=False + ) -> CircuitInstruction: """Low-level 'append' primitive; this may assume that the qubits, clbits and operation are all valid for the circuit. @@ -420,7 +422,9 @@ def _raise_on_jump(operation): " because it is not in a loop." ) - def append(self, instruction: CircuitInstruction) -> CircuitInstruction: + def append( + self, instruction: CircuitInstruction, *, _standard_gate: bool = False + ) -> CircuitInstruction: if self._forbidden_message is not None: raise CircuitError(self._forbidden_message) if not self._allow_jumps: diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index e339cb8d94b..44155783d40 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -58,6 +58,7 @@ class Instruction(Operation): # Class attribute to treat like barrier for transpiler, unroller, drawer # NOTE: Using this attribute may change in the future (See issue # 5811) _directive = False + _standard_gate = None def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt", label=None): """Create a new instruction. diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index ac3d9fabd64..576d5dee826 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -140,13 +140,12 @@ def c_if(self, classical: Clbit | ClassicalRegister | int, val: int) -> "Instruc ) if self._requester is not None: classical = self._requester(classical) - for instruction in self._instructions: + for idx, instruction in enumerate(self._instructions): if isinstance(instruction, CircuitInstruction): updated = instruction.operation.c_if(classical, val) - if updated is not instruction.operation: - raise CircuitError( - "SingletonGate instances can only be added to InstructionSet via _add_ref" - ) + self._instructions[idx] = instruction.replace( + operation=updated, condition=updated.condition + ) else: data, idx = instruction instruction = data[idx] diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py index 2bbd5ca5650..16cc0e3dbaf 100644 --- a/qiskit/circuit/library/blueprintcircuit.py +++ b/qiskit/circuit/library/blueprintcircuit.py @@ -17,7 +17,7 @@ from qiskit._accelerate.circuit import CircuitData from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister -from qiskit.circuit.parametertable import ParameterTable, ParameterView +from qiskit.circuit.parametertable import ParameterView class BlueprintCircuit(QuantumCircuit, ABC): @@ -68,7 +68,6 @@ def _build(self) -> None: def _invalidate(self) -> None: """Invalidate the current circuit build.""" self._data = CircuitData(self._data.qubits, self._data.clbits) - self._parameter_table = ParameterTable() self.global_phase = 0 self._is_built = False @@ -88,7 +87,6 @@ def qregs(self, qregs): self._ancillas = [] self._qubit_indices = {} self._data = CircuitData(clbits=self._data.clbits) - self._parameter_table = ParameterTable() self.global_phase = 0 self._is_built = False @@ -122,10 +120,10 @@ def parameters(self) -> ParameterView: self._build() return super().parameters - def _append(self, instruction, _qargs=None, _cargs=None): + def _append(self, instruction, _qargs=None, _cargs=None, *, _standard_gate=False): if not self._is_built: self._build() - return super()._append(instruction, _qargs, _cargs) + return super()._append(instruction, _qargs, _cargs, _standard_gate=_standard_gate) def compose( self, diff --git a/qiskit/circuit/library/standard_gates/ecr.py b/qiskit/circuit/library/standard_gates/ecr.py index 73bb1bb0389..f00c02df538 100644 --- a/qiskit/circuit/library/standard_gates/ecr.py +++ b/qiskit/circuit/library/standard_gates/ecr.py @@ -17,6 +17,7 @@ from qiskit.circuit._utils import with_gate_array from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key +from qiskit._accelerate.circuit import StandardGate from .rzx import RZXGate from .x import XGate @@ -84,6 +85,8 @@ class ECRGate(SingletonGate): \end{pmatrix} """ + _standard_gate = StandardGate.ECRGate + def __init__(self, label=None, *, duration=None, unit="dt"): """Create new ECR gate.""" super().__init__("ecr", 2, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/global_phase.py b/qiskit/circuit/library/standard_gates/global_phase.py index ccd758e4724..59d6b56373d 100644 --- a/qiskit/circuit/library/standard_gates/global_phase.py +++ b/qiskit/circuit/library/standard_gates/global_phase.py @@ -20,6 +20,7 @@ from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class GlobalPhaseGate(Gate): @@ -36,6 +37,8 @@ class GlobalPhaseGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.GlobalPhaseGate + def __init__( self, phase: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index cc06a071a3f..2d273eed74d 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -17,6 +17,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _H_ARRAY = 1 / sqrt(2) * numpy.array([[1, 1], [1, -1]], dtype=numpy.complex128) @@ -51,6 +52,8 @@ class HGate(SingletonGate): \end{pmatrix} """ + _standard_gate = StandardGate.HGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new H gate.""" super().__init__("h", 1, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/i.py b/qiskit/circuit/library/standard_gates/i.py index 93523215d6f..13a98ce0df8 100644 --- a/qiskit/circuit/library/standard_gates/i.py +++ b/qiskit/circuit/library/standard_gates/i.py @@ -15,6 +15,7 @@ from typing import Optional from qiskit.circuit.singleton import SingletonGate, stdlib_singleton_key from qiskit.circuit._utils import with_gate_array +from qiskit._accelerate.circuit import StandardGate @with_gate_array([[1, 0], [0, 1]]) @@ -45,6 +46,8 @@ class IGate(SingletonGate): └───┘ """ + _standard_gate = StandardGate.IGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Identity gate.""" super().__init__("id", 1, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 6de0307dc79..1a792649fea 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -19,6 +19,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class PhaseGate(Gate): @@ -75,6 +76,8 @@ class PhaseGate(Gate): `1612.00858 `_ """ + _standard_gate = StandardGate.PhaseGate + def __init__( self, theta: ParameterValueType, label: str | None = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index eaa73cf87c9..5579f9d3707 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -21,6 +21,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RXGate(Gate): @@ -50,6 +51,8 @@ class RXGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RXGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index 633a518bca7..e27398cc296 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -20,6 +20,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RYGate(Gate): @@ -49,6 +50,8 @@ class RYGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RYGate + def __init__( self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index 3040f956834..e8ee0f97603 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -17,6 +17,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RZGate(Gate): @@ -59,6 +60,8 @@ class RZGate(Gate): `1612.00858 `_ """ + _standard_gate = StandardGate.RZGate + def __init__( self, phi: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/swap.py b/qiskit/circuit/library/standard_gates/swap.py index 0e49783308c..243a84701ef 100644 --- a/qiskit/circuit/library/standard_gates/swap.py +++ b/qiskit/circuit/library/standard_gates/swap.py @@ -17,6 +17,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _SWAP_ARRAY = numpy.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) @@ -58,6 +59,8 @@ class SwapGate(SingletonGate): |a, b\rangle \rightarrow |b, a\rangle """ + _standard_gate = StandardGate.SwapGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SWAP gate.""" super().__init__("swap", 2, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 0c003748a66..93ca85da019 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -17,6 +17,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _SX_ARRAY = [[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]] @@ -62,6 +63,8 @@ class SXGate(SingletonGate): """ + _standard_gate = StandardGate.SXGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new SX gate.""" super().__init__("sx", 1, [], label=label, duration=duration, unit=unit) diff --git a/qiskit/circuit/library/standard_gates/u.py b/qiskit/circuit/library/standard_gates/u.py index 3d631898850..3495bc180f0 100644 --- a/qiskit/circuit/library/standard_gates/u.py +++ b/qiskit/circuit/library/standard_gates/u.py @@ -21,6 +21,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class UGate(Gate): @@ -68,6 +69,8 @@ class UGate(Gate): U(\theta, 0, 0) = RY(\theta) """ + _standard_gate = StandardGate.UGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index 7195df90dc9..6e959b3e62c 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -19,6 +19,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _ctrl_state_to_int, with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _X_ARRAY = [[0, 1], [1, 0]] @@ -70,6 +71,8 @@ class XGate(SingletonGate): |1\rangle \rightarrow |0\rangle """ + _standard_gate = StandardGate.XGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new X gate.""" super().__init__("x", 1, [], label=label, duration=duration, unit=unit) @@ -212,6 +215,8 @@ class CXGate(SingletonControlledGate): `|a, b\rangle \rightarrow |a, a \oplus b\rangle` """ + _standard_gate = StandardGate.CXGate + def __init__( self, label: Optional[str] = None, @@ -362,6 +367,8 @@ class CCXGate(SingletonControlledGate): """ + _standard_gate = StandardGate.CCXGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index e69e1e2b794..d62586aa2b9 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -19,6 +19,7 @@ from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array +from qiskit._accelerate.circuit import StandardGate _Y_ARRAY = [[0, -1j], [1j, 0]] @@ -70,6 +71,8 @@ class YGate(SingletonGate): |1\rangle \rightarrow -i|0\rangle """ + _standard_gate = StandardGate.YGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Y gate.""" super().__init__("y", 1, [], label=label, duration=duration, unit=unit) @@ -197,6 +200,8 @@ class CYGate(SingletonControlledGate): """ + _standard_gate = StandardGate.CYGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index 2b69595936d..19e4382cd84 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -20,6 +20,7 @@ from qiskit.circuit._utils import with_gate_array, with_controlled_gate_array from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate from .p import PhaseGate @@ -73,6 +74,8 @@ class ZGate(SingletonGate): |1\rangle \rightarrow -|1\rangle """ + _standard_gate = StandardGate.ZGate + def __init__(self, label: Optional[str] = None, *, duration=None, unit="dt"): """Create new Z gate.""" super().__init__("z", 1, [], label=label, duration=duration, unit=unit) @@ -181,6 +184,8 @@ class CZGate(SingletonControlledGate): the target qubit if the control qubit is in the :math:`|1\rangle` state. """ + _standard_gate = StandardGate.CZGate + def __init__( self, label: Optional[str] = None, diff --git a/qiskit/circuit/parametertable.py b/qiskit/circuit/parametertable.py index 6803126ec10..e5a41b1971c 100644 --- a/qiskit/circuit/parametertable.py +++ b/qiskit/circuit/parametertable.py @@ -12,197 +12,8 @@ """ Look-up table for variable parameters in QuantumCircuit. """ -import operator -import typing -from collections.abc import MappingView, MutableMapping, MutableSet - -class ParameterReferences(MutableSet): - """A set of instruction parameter slot references. - Items are expected in the form ``(instruction, param_index)``. Membership - testing is overridden such that items that are otherwise value-wise equal - are still considered distinct if their ``instruction``\\ s are referentially - distinct. - - In the case of the special value :attr:`.ParameterTable.GLOBAL_PHASE` for ``instruction``, the - ``param_index`` should be ``None``. - """ - - def _instance_key(self, ref): - return (id(ref[0]), ref[1]) - - def __init__(self, refs): - self._instance_ids = {} - - for ref in refs: - if not isinstance(ref, tuple) or len(ref) != 2: - raise ValueError("refs must be in form (instruction, param_index)") - k = self._instance_key(ref) - self._instance_ids[k] = ref[0] - - def __getstate__(self): - # Leave behind the reference IDs (keys of _instance_ids) since they'll - # be incorrect after unpickling on the other side. - return list(self) - - def __setstate__(self, refs): - # Recompute reference IDs for the newly unpickled instructions. - self._instance_ids = {self._instance_key(ref): ref[0] for ref in refs} - - def __len__(self): - return len(self._instance_ids) - - def __iter__(self): - for (_, idx), instruction in self._instance_ids.items(): - yield (instruction, idx) - - def __contains__(self, x) -> bool: - return self._instance_key(x) in self._instance_ids - - def __repr__(self) -> str: - return f"ParameterReferences({repr(list(self))})" - - def add(self, value): - """Adds a reference to the listing if it's not already present.""" - k = self._instance_key(value) - self._instance_ids[k] = value[0] - - def discard(self, value): - k = self._instance_key(value) - self._instance_ids.pop(k, None) - - def copy(self): - """Create a shallow copy.""" - return ParameterReferences(self) - - -class ParameterTable(MutableMapping): - """Class for tracking references to circuit parameters by specific - instruction instances. - - Keys are parameters. Values are of type :class:`~ParameterReferences`, - which overrides membership testing to be referential for instructions, - and is set-like. Elements of :class:`~ParameterReferences` - are tuples of ``(instruction, param_index)``. - """ - - __slots__ = ["_table", "_keys", "_names"] - - class _GlobalPhaseSentinel: - __slots__ = () - - def __copy__(self): - return self - - def __deepcopy__(self, memo=None): - return self - - def __reduce__(self): - return (operator.attrgetter("GLOBAL_PHASE"), (ParameterTable,)) - - def __repr__(self): - return "" - - GLOBAL_PHASE = _GlobalPhaseSentinel() - """Tracking object to indicate that a reference refers to the global phase of a circuit.""" - - def __init__(self, mapping=None): - """Create a new instance, initialized with ``mapping`` if provided. - - Args: - mapping (Mapping[Parameter, ParameterReferences]): - Mapping of parameter to the set of parameter slots that reference - it. - - Raises: - ValueError: A value in ``mapping`` is not a :class:`~ParameterReferences`. - """ - if mapping is not None: - if any(not isinstance(refs, ParameterReferences) for refs in mapping.values()): - raise ValueError("Values must be of type ParameterReferences") - self._table = mapping.copy() - else: - self._table = {} - - self._keys = set(self._table) - self._names = {x.name: x for x in self._table} - - def __getitem__(self, key): - return self._table[key] - - def __setitem__(self, parameter, refs): - """Associate a parameter with the set of parameter slots ``(instruction, param_index)`` - that reference it. - - .. note:: - - Items in ``refs`` are considered unique if their ``instruction`` is referentially - unique. See :class:`~ParameterReferences` for details. - - Args: - parameter (Parameter): the parameter - refs (Union[ParameterReferences, Iterable[(Instruction, int)]]): the parameter slots. - If this is an iterable, a new :class:`~ParameterReferences` is created from its - contents. - """ - if not isinstance(refs, ParameterReferences): - refs = ParameterReferences(refs) - - self._table[parameter] = refs - self._keys.add(parameter) - self._names[parameter.name] = parameter - - def get_keys(self): - """Return a set of all keys in the parameter table - - Returns: - set: A set of all the keys in the parameter table - """ - return self._keys - - def get_names(self): - """Return a set of all parameter names in the parameter table - - Returns: - set: A set of all the names in the parameter table - """ - return self._names.keys() - - def parameter_from_name(self, name: str, default: typing.Any = None): - """Get a :class:`.Parameter` with references in this table by its string name. - - If the parameter is not present, return the ``default`` value. - - Args: - name: The name of the :class:`.Parameter` - default: The object that should be returned if the parameter is missing. - """ - return self._names.get(name, default) - - def discard_references(self, expression, key): - """Remove all references to parameters contained within ``expression`` at the given table - ``key``. This also discards parameter entries from the table if they have no further - references. No action is taken if the object is not tracked.""" - for parameter in expression.parameters: - if (refs := self._table.get(parameter)) is not None: - if len(refs) == 1: - del self[parameter] - else: - refs.discard(key) - - def __delitem__(self, key): - del self._table[key] - self._keys.discard(key) - del self._names[key.name] - - def __iter__(self): - return iter(self._table) - - def __len__(self): - return len(self._table) - - def __repr__(self): - return f"ParameterTable({repr(self._table)})" +from collections.abc import MappingView class ParameterView(MappingView): diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index ea4361fd825..3fbf6902cdb 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -37,6 +37,7 @@ ) import numpy as np from qiskit._accelerate.circuit import CircuitData +from qiskit._accelerate.circuit import StandardGate, PyGate, PyInstruction, PyOperation from qiskit.exceptions import QiskitError from qiskit.utils.multiprocessing import is_main_process from qiskit.circuit.instruction import Instruction @@ -57,7 +58,7 @@ from .parameterexpression import ParameterExpression, ParameterValueType from .quantumregister import QuantumRegister, Qubit, AncillaRegister, AncillaQubit from .classicalregister import ClassicalRegister, Clbit -from .parametertable import ParameterReferences, ParameterTable, ParameterView +from .parametertable import ParameterView from .parametervector import ParameterVector from .instructionset import InstructionSet from .operation import Operation @@ -1124,14 +1125,10 @@ def __init__( self._calibrations: DefaultDict[str, dict[tuple, Any]] = defaultdict(dict) self.add_register(*regs) - # Parameter table tracks instructions with variable parameters. - self._parameter_table = ParameterTable() - # Cache to avoid re-sorting parameters self._parameters = None self._layout = None - self._global_phase: ParameterValueType = 0 self.global_phase = global_phase # Add classical variables. Resolve inputs and captures first because they can't depend on @@ -1159,6 +1156,15 @@ def __init__( Qiskit will not examine the content of this mapping, but it will pass it through the transpiler and reattach it to the output, so you can track your own metadata.""" + @classmethod + def _from_circuit_data(cls, data: CircuitData) -> typing.Self: + """A private constructor from rust space circuit data.""" + out = QuantumCircuit() + out.add_bits(data.qubits) + out.add_bits(data.clbits) + out._data = data + return out + @staticmethod def from_instructions( instructions: Iterable[ @@ -1259,7 +1265,6 @@ def data(self, data_input: Iterable): data_input = list(data_input) self._data.clear() self._parameters = None - self._parameter_table = ParameterTable() # Repopulate the parameter table with any global-phase entries. self.global_phase = self.global_phase if not data_input: @@ -1382,12 +1387,11 @@ def __deepcopy__(self, memo=None): # Avoids pulling self._data into a Python list # like we would when pickling. - result._data = self._data.copy() + result._data = self._data.copy(deepcopy=True) result._data.replace_bits( qubits=_copy.deepcopy(self._data.qubits, memo), clbits=_copy.deepcopy(self._data.clbits, memo), ) - result._data.map_ops(lambda op: _copy.deepcopy(op, memo)) return result @classmethod @@ -1896,7 +1900,7 @@ def replace_var(var: expr.Var, cache: Mapping[expr.Var, expr.Var]) -> expr.Var: clbits = self.clbits[: other.num_clbits] if front: # Need to keep a reference to the data for use after we've emptied it. - old_data = dest._data.copy() + old_data = dest._data.copy(copy_instructions=copy) dest.clear() dest.append(other, qubits, clbits, copy=copy) for instruction in old_data: @@ -2024,14 +2028,14 @@ def map_vars(op): ) return n_op.copy() if n_op is op and copy else n_op - instructions = source._data.copy() + instructions = source._data.copy(copy_instructions=copy) instructions.replace_bits(qubits=new_qubits, clbits=new_clbits) instructions.map_ops(map_vars) dest._current_scope().extend(instructions) append_existing = None if front: - append_existing = dest._data.copy() + append_existing = dest._data.copy(copy_instructions=copy) dest.clear() copy_with_remapping( other, @@ -2296,6 +2300,35 @@ def cbit_argument_conversion(self, clbit_representation: ClbitSpecifier) -> list clbit_representation, self.clbits, self._clbit_indices, Clbit ) + def _append_standard_gate( + self, + op: StandardGate, + params: Sequence[ParameterValueType] | None = None, + qargs: Sequence[QubitSpecifier] | None = None, + cargs: Sequence[ClbitSpecifier] | None = None, + label: str | None = None, + ) -> InstructionSet: + """An internal method to bypass some checking when directly appending a standard gate.""" + circuit_scope = self._current_scope() + + if params is None: + params = [] + + expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []] + expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []] + if params is not None: + for param in params: + Gate.validate_parameter(op, param) + + instructions = InstructionSet(resource_requester=circuit_scope.resolve_classical_resource) + broadcast_iter = Gate.broadcast_arguments(op, expanded_qargs, expanded_cargs) + for qarg, carg in broadcast_iter: + self._check_dups(qarg) + instruction = CircuitInstruction(op, qarg, carg, params=params, label=label) + circuit_scope.append(instruction, _standard_gate=True) + instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) + return instructions + def append( self, instruction: Operation | CircuitInstruction, @@ -2393,16 +2426,47 @@ def append( if isinstance(operation, Instruction) else Instruction.broadcast_arguments(operation, expanded_qargs, expanded_cargs) ) + params = None + if isinstance(operation, Gate): + params = operation.params + operation = PyGate( + operation.name, + operation.num_qubits, + operation.num_clbits, + len(params), + operation, + ) + elif isinstance(operation, Instruction): + params = operation.params + operation = PyInstruction( + operation.name, + operation.num_qubits, + operation.num_clbits, + len(params), + operation, + ) + elif isinstance(operation, Operation): + params = getattr(operation, "params", ()) + operation = PyOperation( + operation.name, + operation.num_qubits, + operation.num_clbits, + len(params), + operation, + ) + for qarg, carg in broadcast_iter: self._check_dups(qarg) - instruction = CircuitInstruction(operation, qarg, carg) + instruction = CircuitInstruction(operation, qarg, carg, params=params) circuit_scope.append(instruction) instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) return instructions # Preferred new style. @typing.overload - def _append(self, instruction: CircuitInstruction) -> CircuitInstruction: ... + def _append( + self, instruction: CircuitInstruction, *, _standard_gate: bool + ) -> CircuitInstruction: ... # To-be-deprecated old style. @typing.overload @@ -2413,7 +2477,7 @@ def _append( cargs: Sequence[Clbit], ) -> Operation: ... - def _append(self, instruction, qargs=(), cargs=()): + def _append(self, instruction, qargs=(), cargs=(), *, _standard_gate: bool = False): """Append an instruction to the end of the circuit, modifying the circuit in place. .. warning:: @@ -2454,40 +2518,39 @@ def _append(self, instruction, qargs=(), cargs=()): :meta public: """ + if _standard_gate: + new_param = self._data.append(instruction) + if new_param: + self._parameters = None + self.duration = None + self.unit = "dt" + return instruction + old_style = not isinstance(instruction, CircuitInstruction) if old_style: instruction = CircuitInstruction(instruction, qargs, cargs) - self._data.append(instruction) - self._track_operation(instruction.operation) - return instruction.operation if old_style else instruction + # If there is a reference to the outer circuit in an + # instruction param the inner rust append method will raise a runtime error. + # When this happens we need to handle the parameters separately. + # This shouldn't happen in practice but 2 tests were doing this and it's not + # explicitly prohibted by the API so this and the `params` optional argument + # path guard against it. + try: + new_param = self._data.append(instruction) + except RuntimeError: + params = [] + for idx, param in enumerate(instruction.operation.params): + if isinstance(param, (ParameterExpression, QuantumCircuit)): + params.append((idx, list(set(param.parameters)))) + new_param = self._data.append(instruction, params) + if new_param: + # clear cache if new parameter is added + self._parameters = None - def _track_operation(self, operation: Operation): - """Sync all non-data-list internal data structures for a newly tracked operation.""" - if isinstance(operation, Instruction): - self._update_parameter_table(operation) + # Invalidate whole circuit duration if an instruction is added self.duration = None self.unit = "dt" - - def _update_parameter_table(self, instruction: Instruction): - for param_index, param in enumerate(instruction.params): - if isinstance(param, (ParameterExpression, QuantumCircuit)): - # Scoped constructs like the control-flow ops use QuantumCircuit as a parameter. - atomic_parameters = set(param.parameters) - else: - atomic_parameters = set() - - for parameter in atomic_parameters: - if parameter in self._parameter_table: - self._parameter_table[parameter].add((instruction, param_index)) - else: - if parameter.name in self._parameter_table.get_names(): - raise CircuitError(f"Name conflict on adding parameter: {parameter.name}") - self._parameter_table[parameter] = ParameterReferences( - ((instruction, param_index),) - ) - - # clear cache if new parameter is added - self._parameters = None + return instruction.operation if old_style else instruction @typing.overload def get_parameter(self, name: str, default: T) -> Union[Parameter, T]: ... @@ -2538,7 +2601,7 @@ def get_parameter(self, name: str, default: typing.Any = ...) -> Parameter: A similar method, but for :class:`.expr.Var` run-time variables instead of :class:`.Parameter` compile-time parameters. """ - if (parameter := self._parameter_table.parameter_from_name(name, None)) is None: + if (parameter := self._data.get_param_from_name(name)) is None: if default is Ellipsis: raise KeyError(f"no parameter named '{name}' is present") return default @@ -3413,13 +3476,7 @@ def num_nonlocal_gates(self) -> int: Conditional nonlocal gates are also included. """ - multi_qubit_gates = 0 - for instruction in self._data: - if instruction.operation.num_qubits > 1 and not getattr( - instruction.operation, "_directive", False - ): - multi_qubit_gates += 1 - return multi_qubit_gates + return self._data.num_nonlocal_gates() def get_instructions(self, name: str) -> list[CircuitInstruction]: """Get instructions matching name. @@ -3533,29 +3590,6 @@ def copy(self, name: str | None = None) -> typing.Self: """ cpy = self.copy_empty_like(name) cpy._data = self._data.copy() - - # The special global-phase sentinel doesn't need copying, but it's - # added here to ensure it's recognised. The global phase itself was - # already copied over in `copy_empty_like`. - operation_copies = {id(ParameterTable.GLOBAL_PHASE): ParameterTable.GLOBAL_PHASE} - - def memo_copy(op): - if (out := operation_copies.get(id(op))) is not None: - return out - copied = op.copy() - operation_copies[id(op)] = copied - return copied - - cpy._data.map_ops(memo_copy) - cpy._parameter_table = ParameterTable( - { - param: ParameterReferences( - (operation_copies[id(operation)], param_index) - for operation, param_index in self._parameter_table[param] - ) - for param in self._parameter_table - } - ) return cpy def copy_empty_like( @@ -3634,12 +3668,9 @@ def copy_empty_like( else: # pragma: no cover raise ValueError(f"unknown vars_mode: '{vars_mode}'") - cpy._parameter_table = ParameterTable() - for parameter in getattr(cpy.global_phase, "parameters", ()): - cpy._parameter_table[parameter] = ParameterReferences( - [(ParameterTable.GLOBAL_PHASE, None)] - ) - cpy._data = CircuitData(self._data.qubits, self._data.clbits) + cpy._data = CircuitData( + self._data.qubits, self._data.clbits, global_phase=self._data.global_phase + ) cpy._calibrations = _copy.deepcopy(self._calibrations) cpy._metadata = _copy.deepcopy(self._metadata) @@ -3659,7 +3690,6 @@ def clear(self) -> None: quantum and classical typed data, but without mutating the original circuit. """ self._data.clear() - self._parameter_table.clear() # Repopulate the parameter table with any phase symbols. self.global_phase = self.global_phase @@ -3943,10 +3973,9 @@ def remove_final_measurements(self, inplace: bool = True) -> Optional["QuantumCi circ._clbit_indices = {} # Clear instruction info - circ._data = CircuitData(qubits=circ._data.qubits, reserve=len(circ._data)) - circ._parameter_table.clear() - # Repopulate the parameter table with any global-phase entries. - circ.global_phase = circ.global_phase + circ._data = CircuitData( + qubits=circ._data.qubits, reserve=len(circ._data), global_phase=circ.global_phase + ) # We must add the clbits first to preserve the original circuit # order. This way, add_register never adds clbits and just @@ -4019,7 +4048,7 @@ def global_phase(self) -> ParameterValueType: """The global phase of the current circuit scope in radians.""" if self._control_flow_scopes: return self._control_flow_scopes[-1].global_phase - return self._global_phase + return self._data.global_phase @global_phase.setter def global_phase(self, angle: ParameterValueType): @@ -4030,23 +4059,18 @@ def global_phase(self, angle: ParameterValueType): """ # If we're currently parametric, we need to throw away the references. This setter is # called by some subclasses before the inner `_global_phase` is initialised. - global_phase_reference = (ParameterTable.GLOBAL_PHASE, None) - if isinstance(previous := getattr(self, "_global_phase", None), ParameterExpression): + if isinstance(getattr(self._data, "global_phase", None), ParameterExpression): self._parameters = None - self._parameter_table.discard_references(previous, global_phase_reference) - - if isinstance(angle, ParameterExpression) and angle.parameters: - for parameter in angle.parameters: - if parameter not in self._parameter_table: - self._parameters = None - self._parameter_table[parameter] = ParameterReferences(()) - self._parameter_table[parameter].add(global_phase_reference) + if isinstance(angle, ParameterExpression): + if angle.parameters: + self._parameters = None else: angle = _normalize_global_phase(angle) + if self._control_flow_scopes: self._control_flow_scopes[-1].global_phase = angle else: - self._global_phase = angle + self._data.global_phase = angle @property def parameters(self) -> ParameterView: @@ -4116,7 +4140,7 @@ def parameters(self) -> ParameterView: @property def num_parameters(self) -> int: """The number of parameter objects in the circuit.""" - return len(self._parameter_table) + return self._data.num_params() def _unsorted_parameters(self) -> set[Parameter]: """Efficiently get all parameters in the circuit, without any sorting overhead. @@ -4129,7 +4153,7 @@ def _unsorted_parameters(self) -> set[Parameter]: """ # This should be free, by accessing the actual backing data structure of the table, but that # means that we need to copy it if adding keys from the global phase. - return self._parameter_table.get_keys() + return self._data.get_params_unsorted() @overload def assign_parameters( @@ -4278,7 +4302,7 @@ def assign_parameters( # pylint: disable=missing-raises-doc target._parameters = None # This is deliberately eager, because we want the side effect of clearing the table. all_references = [ - (parameter, value, target._parameter_table.pop(parameter, ())) + (parameter, value, target._data.pop_param(parameter.uuid.int, parameter.name, ())) for parameter, value in parameter_binds.items() ] seen_operations = {} @@ -4289,20 +4313,28 @@ def assign_parameters( # pylint: disable=missing-raises-doc if isinstance(bound_value, ParameterExpression) else () ) - for operation, index in references: - seen_operations[id(operation)] = operation - if operation is ParameterTable.GLOBAL_PHASE: + for inst_index, index in references: + if inst_index == self._data.global_phase_param_index: + operation = None + seen_operations[inst_index] = None assignee = target.global_phase validate = _normalize_global_phase else: + operation = target._data[inst_index].operation + seen_operations[inst_index] = operation assignee = operation.params[index] validate = operation.validate_parameter if isinstance(assignee, ParameterExpression): new_parameter = assignee.assign(to_bind, bound_value) for parameter in update_parameters: - if parameter not in target._parameter_table: - target._parameter_table[parameter] = ParameterReferences(()) - target._parameter_table[parameter].add((operation, index)) + if not target._data.contains_param(parameter.uuid.int): + target._data.add_new_parameter(parameter, inst_index, index) + else: + target._data.update_parameter_entry( + parameter.uuid.int, + inst_index, + index, + ) if not new_parameter.parameters: new_parameter = validate(new_parameter.numeric()) elif isinstance(assignee, QuantumCircuit): @@ -4314,12 +4346,18 @@ def assign_parameters( # pylint: disable=missing-raises-doc f"Saw an unknown type during symbolic binding: {assignee}." " This may indicate an internal logic error in symbol tracking." ) - if operation is ParameterTable.GLOBAL_PHASE: + if inst_index == self._data.global_phase_param_index: # We've already handled parameter table updates in bulk, so we need to skip the # public setter trying to do it again. - target._global_phase = new_parameter + target._data.global_phase = new_parameter else: - operation.params[index] = new_parameter + temp_params = operation.params + temp_params[index] = new_parameter + operation.params = temp_params + target._data.setitem_no_param_table_update( + inst_index, + target._data[inst_index].replace(operation=operation, params=temp_params), + ) # After we've been through everything at the top level, make a single visit to each # operation we've seen, rebinding its definition if necessary. @@ -4366,6 +4404,7 @@ def map_calibration(qubits, parameters, schedule): for gate, calibrations in target._calibrations.items() ), ) + target._parameters = None return None if inplace else target def _unroll_param_dict( @@ -4448,9 +4487,7 @@ def h(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.h import HGate - - return self.append(HGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.HGate, [], qargs=[qubit]) def ch( self, @@ -4494,9 +4531,7 @@ def id(self, qubit: QubitSpecifier) -> InstructionSet: # pylint: disable=invali Returns: A handle to the instructions created. """ - from .library.standard_gates.i import IGate - - return self.append(IGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.IGate, None, qargs=[qubit]) def ms(self, theta: ParameterValueType, qubits: Sequence[QubitSpecifier]) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.MSGate`. @@ -4527,9 +4562,7 @@ def p(self, theta: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.p import PhaseGate - - return self.append(PhaseGate(theta), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.PhaseGate, [theta], qargs=[qubit]) def cp( self, @@ -4710,9 +4743,7 @@ def rx( Returns: A handle to the instructions created. """ - from .library.standard_gates.rx import RXGate - - return self.append(RXGate(theta, label=label), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.RXGate, [theta], [qubit], None, label=label) def crx( self, @@ -4781,9 +4812,7 @@ def ry( Returns: A handle to the instructions created. """ - from .library.standard_gates.ry import RYGate - - return self.append(RYGate(theta, label=label), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.RYGate, [theta], [qubit], None, label=label) def cry( self, @@ -4849,9 +4878,7 @@ def rz(self, phi: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.rz import RZGate - - return self.append(RZGate(phi), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.RZGate, [phi], [qubit], None) def crz( self, @@ -4935,9 +4962,9 @@ def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.ecr import ECRGate - - return self.append(ECRGate(), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate( + StandardGate.ECRGate, [], qargs=[qubit1, qubit2], cargs=None + ) def s(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SGate`. @@ -5042,9 +5069,12 @@ def swap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet Returns: A handle to the instructions created. """ - from .library.standard_gates.swap import SwapGate - - return self.append(SwapGate(), [qubit1, qubit2], [], copy=False) + return self._append_standard_gate( + StandardGate.SwapGate, + [], + qargs=[qubit1, qubit2], + cargs=None, + ) def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.iSwapGate`. @@ -5105,9 +5135,7 @@ def sx(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.sx import SXGate - - return self.append(SXGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.SXGate, None, qargs=[qubit]) def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SXdgGate`. @@ -5205,9 +5233,7 @@ def u( Returns: A handle to the instructions created. """ - from .library.standard_gates.u import UGate - - return self.append(UGate(theta, phi, lam), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.UGate, [theta, phi, lam], qargs=[qubit]) def cu( self, @@ -5260,9 +5286,7 @@ def x(self, qubit: QubitSpecifier, label: str | None = None) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.x import XGate - - return self.append(XGate(label=label), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.XGate, None, qargs=[qubit], label=label) def cx( self, @@ -5286,14 +5310,17 @@ def cx( Returns: A handle to the instructions created. """ + if ctrl_state is not None: + from .library.standard_gates.x import CXGate - from .library.standard_gates.x import CXGate - - return self.append( - CXGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], - [], - copy=False, + return self.append( + CXGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, + ) + return self._append_standard_gate( + StandardGate.CXGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label ) def dcx(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: @@ -5334,13 +5361,20 @@ def ccx( Returns: A handle to the instructions created. """ - from .library.standard_gates.x import CCXGate + if ctrl_state is not None: + from .library.standard_gates.x import CCXGate - return self.append( - CCXGate(ctrl_state=ctrl_state), - [control_qubit1, control_qubit2, target_qubit], + return self.append( + CCXGate(ctrl_state=ctrl_state), + [control_qubit1, control_qubit2, target_qubit], + [], + copy=False, + ) + return self._append_standard_gate( + StandardGate.CCXGate, [], - copy=False, + qargs=[control_qubit1, control_qubit2, target_qubit], + cargs=None, ) def mcx( @@ -5438,9 +5472,7 @@ def y(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.y import YGate - - return self.append(YGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.YGate, None, qargs=[qubit]) def cy( self, @@ -5464,13 +5496,18 @@ def cy( Returns: A handle to the instructions created. """ - from .library.standard_gates.y import CYGate + if ctrl_state is not None: + from .library.standard_gates.y import CYGate - return self.append( - CYGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], - [], - copy=False, + return self.append( + CYGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, + ) + + return self._append_standard_gate( + StandardGate.CYGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label ) def z(self, qubit: QubitSpecifier) -> InstructionSet: @@ -5484,9 +5521,7 @@ def z(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - from .library.standard_gates.z import ZGate - - return self.append(ZGate(), [qubit], [], copy=False) + return self._append_standard_gate(StandardGate.ZGate, None, qargs=[qubit]) def cz( self, @@ -5510,13 +5545,18 @@ def cz( Returns: A handle to the instructions created. """ - from .library.standard_gates.z import CZGate + if ctrl_state is not None: + from .library.standard_gates.z import CZGate - return self.append( - CZGate(label=label, ctrl_state=ctrl_state), - [control_qubit, target_qubit], - [], - copy=False, + return self.append( + CZGate(label=label, ctrl_state=ctrl_state), + [control_qubit, target_qubit], + [], + copy=False, + ) + + return self._append_standard_gate( + StandardGate.CZGate, [], qargs=[control_qubit, target_qubit], cargs=None, label=label ) def ccz( @@ -5905,36 +5945,9 @@ def _pop_previous_instruction_in_scope(self) -> CircuitInstruction: if not self._data: raise CircuitError("This circuit contains no instructions.") instruction = self._data.pop() - if isinstance(instruction.operation, Instruction): - self._update_parameter_table_on_instruction_removal(instruction) + self._parameters = None return instruction - def _update_parameter_table_on_instruction_removal(self, instruction: CircuitInstruction): - """Update the :obj:`.ParameterTable` of this circuit given that an instance of the given - ``instruction`` has just been removed from the circuit. - - .. note:: - - This does not account for the possibility for the same instruction instance being added - more than once to the circuit. At the time of writing (2021-11-17, main commit 271a82f) - there is a defensive ``deepcopy`` of parameterised instructions inside - :meth:`.QuantumCircuit.append`, so this should be safe. Trying to account for it would - involve adding a potentially quadratic-scaling loop to check each entry in ``data``. - """ - atomic_parameters: list[tuple[Parameter, int]] = [] - for index, parameter in enumerate(instruction.operation.params): - if isinstance(parameter, (ParameterExpression, QuantumCircuit)): - atomic_parameters.extend((p, index) for p in parameter.parameters) - for atomic_parameter, index in atomic_parameters: - new_entries = self._parameter_table[atomic_parameter].copy() - new_entries.discard((instruction.operation, index)) - if not new_entries: - del self._parameter_table[atomic_parameter] - # Invalidate cache. - self._parameters = None - else: - self._parameter_table[atomic_parameter] = new_entries - @typing.overload def while_loop( self, @@ -6580,13 +6593,15 @@ def __init__(self, circuit: QuantumCircuit): def instructions(self): return self.circuit._data - def append(self, instruction): + def append(self, instruction, *, _standard_gate: bool = False): # QuantumCircuit._append is semi-public, so we just call back to it. - return self.circuit._append(instruction) + return self.circuit._append(instruction, _standard_gate=_standard_gate) def extend(self, data: CircuitData): self.circuit._data.extend(data) - data.foreach_op(self.circuit._track_operation) + self.circuit._parameters = None + self.circuit.duration = None + self.circuit.unit = "dt" def resolve_classical_resource(self, specifier): # This is slightly different to cbit_argument_conversion, because it should not diff --git a/qiskit/circuit/quantumcircuitdata.py b/qiskit/circuit/quantumcircuitdata.py index 3e29f36c6be..9ecc8e6a6ca 100644 --- a/qiskit/circuit/quantumcircuitdata.py +++ b/qiskit/circuit/quantumcircuitdata.py @@ -45,8 +45,6 @@ def __setitem__(self, key, value): operation, qargs, cargs = value value = self._resolve_legacy_value(operation, qargs, cargs) self._circuit._data[key] = value - if isinstance(value.operation, Instruction): - self._circuit._update_parameter_table(value.operation) def _resolve_legacy_value(self, operation, qargs, cargs) -> CircuitInstruction: """Resolve the old-style 3-tuple into the new :class:`CircuitInstruction` type.""" @@ -76,7 +74,7 @@ def _resolve_legacy_value(self, operation, qargs, cargs) -> CircuitInstruction: return CircuitInstruction(operation, tuple(qargs), tuple(cargs)) def insert(self, index, value): - self._circuit._data.insert(index, CircuitInstruction(None, (), ())) + self._circuit._data.insert(index, value.replace(qubits=(), clbits=())) try: self[index] = value except CircuitError: diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index 2bdcbfef358..1a5907c3ec8 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -11,7 +11,6 @@ # that they have been altered from the originals. """Helper function for converting a circuit to an instruction.""" -from qiskit.circuit.parametertable import ParameterTable, ParameterReferences from qiskit.exceptions import QiskitError from qiskit.circuit.instruction import Instruction from qiskit.circuit.quantumregister import QuantumRegister @@ -121,7 +120,7 @@ def circuit_to_instruction(circuit, parameter_map=None, equivalence_library=None regs.append(creg) clbit_map = {bit: creg[idx] for idx, bit in enumerate(circuit.clbits)} - operation_map = {id(ParameterTable.GLOBAL_PHASE): ParameterTable.GLOBAL_PHASE} + operation_map = {} def fix_condition(op): original_id = id(op) @@ -149,15 +148,6 @@ def fix_condition(op): qc = QuantumCircuit(*regs, name=out_instruction.name) qc._data = data - qc._parameter_table = ParameterTable( - { - param: ParameterReferences( - (operation_map[id(operation)], param_index) - for operation, param_index in target._parameter_table[param] - ) - for param in target._parameter_table - } - ) if circuit.global_phase: qc.global_phase = circuit.global_phase diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 16bc2529ca7..7f737af76eb 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -252,6 +252,9 @@ def __init__(self, includelist, basis_gates=()): def __setitem__(self, name_str, instruction): self._data[name_str] = instruction.base_class self._data[id(instruction)] = name_str + ctrl_state = str(getattr(instruction, "ctrl_state", "")) + + self._data[f"{instruction.name}_{ctrl_state}_{instruction.params}"] = name_str def __getitem__(self, key): if isinstance(key, Instruction): @@ -262,7 +265,9 @@ def __getitem__(self, key): pass # Built-in gates. if key.name not in self._data: - raise KeyError(key) + # Registerd qiskit standard gate without stgates.inc + ctrl_state = str(getattr(key, "ctrl_state", "")) + return self._data[f"{key.name}_{ctrl_state}_{key.params}"] return key.name return self._data[key] @@ -1102,7 +1107,8 @@ def is_loop_variable(circuit, parameter): # _should_ be an intrinsic part of the parameter, or somewhere publicly accessible, but # Terra doesn't have those concepts yet. We can only try and guess at the type by looking # at all the places it's used in the circuit. - for instruction, index in circuit._parameter_table[parameter]: + for instr_index, index in circuit._data._get_param(parameter.uuid.int): + instruction = circuit.data[instr_index].operation if isinstance(instruction, ForLoopOp): # The parameters of ForLoopOp are (indexset, loop_parameter, body). if index == 1: diff --git a/qiskit/quantum_info/operators/dihedral/dihedral.py b/qiskit/quantum_info/operators/dihedral/dihedral.py index 4f49879063e..75b455410f4 100644 --- a/qiskit/quantum_info/operators/dihedral/dihedral.py +++ b/qiskit/quantum_info/operators/dihedral/dihedral.py @@ -452,8 +452,7 @@ def conjugate(self): new_qubits = [bit_indices[tup] for tup in instruction.qubits] if instruction.operation.name == "p": params = 2 * np.pi - instruction.operation.params[0] - instruction.operation.params[0] = params - new_circ.append(instruction.operation, new_qubits) + new_circ.p(params, new_qubits) elif instruction.operation.name == "t": instruction.operation.name = "tdg" new_circ.append(instruction.operation, new_qubits) diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index 7cb309dd9aa..806e001f2bd 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -361,17 +361,17 @@ def _pad( theta, phi, lam, phase = OneQubitEulerDecomposer().angles_and_phase(u_inv) if isinstance(next_node, DAGOpNode) and isinstance(next_node.op, (UGate, U3Gate)): # Absorb the inverse into the successor (from left in circuit) - theta_r, phi_r, lam_r = next_node.op.params - next_node.op.params = Optimize1qGates.compose_u3( - theta_r, phi_r, lam_r, theta, phi, lam - ) + op = next_node.op + theta_r, phi_r, lam_r = op.params + op.params = Optimize1qGates.compose_u3(theta_r, phi_r, lam_r, theta, phi, lam) + next_node.op = op sequence_gphase += phase elif isinstance(prev_node, DAGOpNode) and isinstance(prev_node.op, (UGate, U3Gate)): # Absorb the inverse into the predecessor (from right in circuit) - theta_l, phi_l, lam_l = prev_node.op.params - prev_node.op.params = Optimize1qGates.compose_u3( - theta, phi, lam, theta_l, phi_l, lam_l - ) + op = prev_node.op + theta_l, phi_l, lam_l = op.params + op.params = Optimize1qGates.compose_u3(theta, phi, lam, theta_l, phi_l, lam_l) + prev_node.op = op sequence_gphase += phase else: # Don't do anything if there's no single-qubit gate to absorb the inverse diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index 3792a149fd7..69bea32acca 100644 --- a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py @@ -70,8 +70,9 @@ def _get_node_duration( duration = dag.calibrations[node.op.name][cal_key].duration # Note that node duration is updated (but this is analysis pass) - node.op = node.op.to_mutable() - node.op.duration = duration + op = node.op.to_mutable() + op.duration = duration + node.op = op else: duration = node.op.duration diff --git a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index 25672c137f3..08ac932d8ae 100644 --- a/qiskit/transpiler/passes/scheduling/time_unit_conversion.py +++ b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py @@ -105,9 +105,10 @@ def run(self, dag: DAGCircuit): ) except TranspilerError: continue - node.op = node.op.to_mutable() - node.op.duration = duration - node.op.unit = time_unit + op = node.op.to_mutable() + op.duration = duration + op.unit = time_unit + node.op = op self.property_set["time_unit"] = time_unit return dag diff --git a/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml new file mode 100644 index 00000000000..d826bc15e48 --- /dev/null +++ b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml @@ -0,0 +1,79 @@ +--- +features_circuits: + - | + A native rust representation of Qiskit's standard gate library has been added. When a standard gate + is added to a :class:`~.QuantumCircuit` or :class:`~.DAGCircuit` it is now represented in a more + efficient manner directly in Rust seamlessly. Accessing that gate object from a circuit or dag will + return a new Python object representing the standard gate. This leads to faster and more efficient + transpilation and manipulation of circuits for functionality written in Rust. +features_misc: + - | + Added a new build-time environment variable ``QISKIT_NO_CACHE_GATES`` which + when set to a value of ``1`` (i.e. ``QISKIT_NO_CACHE_GATES=1``) which + decreases the memory overhead of a :class:`.CircuitInstruction` and + :class:`.DAGOpNode` object at the cost of decreased runtime on multiple + accesses to :attr:`.CircuitInstruction.operation` and :attr:`.DAGOpNode.op`. + If this environment variable is set when building the Qiskit python package + from source the caching of the return of these attributes will be disabled. +upgrade_circuits: + - | + The :class:`.Operation` instances of :attr:`.DAGOpNode.op` + being returned will not necessarily share a common reference to the + underlying object anymore. This was never guaranteed to be the case and + mutating the :attr:`~.DAGOpNode.op` directly by reference + was unsound and always likely to corrupt the dag's internal state tracking + Due to the internal refactor of the :class:`.QuantumCircuit` and + :class:`.DAGCircuit` to store standard gates in rust the output object from + :attr:`.DAGOpNode.op` will now likely be a copy instead of a shared instance. If you + need to mutate an element should ensure that you either do:: + + op = dag_node.op + op.params[0] = 3.14159 + dag_node.op = op + + or:: + + op = dag_node.op + op.params[0] = 3.14159 + dag.substitute_node(dag_node, op) + + instead of doing something like:: + + dag_node.op.params[0] = 3.14159 + + which will not work for any standard gates in this release. It would have + likely worked by chance in a previous release but was never an API guarantee. + - | + The :class:`.Operation` instances of :attr:`.CircuitInstruction.operation` + being returned will not necessarily share a common reference to the + underlying object anymore. This was never guaranteed to be the case and + mutating the :attr:`~.CircuitInstruction.operation` directly by reference + was unsound and always likely to corrupt the circuit, especially when + parameters were in use. Due to the internal refactor of the QuantumCircuit + to store standard gates in rust the output object from + :attr:`.CircuitInstruction.operation` will now likely be a copy instead + of a shared instance. If you need to mutate an element in the circuit (which + is strongly **not** recommended as it's inefficient and error prone) you + should ensure that you do:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(1) + qc.p(0) + + op = qc.data[0].operation + op.params[0] = 3.14 + + qc.data[0] = qc.data[0].replace(operation=op) + + instead of doing something like:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(1) + qc.p(0) + + qc.data[0].operation.params[0] = 3.14 + + which will not work for any standard gates in this release. It would have + likely worked by chance in a previous release but was never an API guarantee. diff --git a/requirements-optional.txt b/requirements-optional.txt index 36985cdd7cd..3dfc2031d02 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -19,7 +19,7 @@ seaborn>=0.9.0 # Functionality and accelerators. qiskit-aer -qiskit-qasm3-import +qiskit-qasm3-import>=0.5.0 python-constraint>=1.4 cvxpy scikit-learn>=0.20.0 diff --git a/setup.py b/setup.py index 9bb5b04ae6e..38af5286e81 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,17 @@ # it's an editable installation. rust_debug = True if os.getenv("RUST_DEBUG") == "1" else None +# If QISKIT_NO_CACHE_GATES is set then don't enable any features while building +# +# TODO: before final release we should reverse this by default once the default transpiler pass +# is all in rust (default to no caching and make caching an opt-in feature). This is opt-out +# right now to avoid the runtime overhead until we are leveraging the rust gates infrastructure. +if os.getenv("QISKIT_NO_CACHE_GATES") == "1": + features = [] +else: + features = ["cache_pygates"] + + setup( rust_extensions=[ RustExtension( @@ -37,6 +48,7 @@ "crates/pyext/Cargo.toml", binding=Binding.PyO3, debug=rust_debug, + features=features, ) ], options={"bdist_wheel": {"py_limited_api": "cp38"}}, diff --git a/test/python/circuit/library/test_blueprintcircuit.py b/test/python/circuit/library/test_blueprintcircuit.py index 2a5070e8ac7..5f0a2814872 100644 --- a/test/python/circuit/library/test_blueprintcircuit.py +++ b/test/python/circuit/library/test_blueprintcircuit.py @@ -77,17 +77,17 @@ def test_invalidate_rebuild(self): with self.subTest(msg="after building"): self.assertGreater(len(mock._data), 0) - self.assertEqual(len(mock._parameter_table), 1) + self.assertEqual(mock._data.num_params(), 1) mock._invalidate() with self.subTest(msg="after invalidating"): self.assertFalse(mock._is_built) - self.assertEqual(len(mock._parameter_table), 0) + self.assertEqual(mock._data.num_params(), 0) mock._build() with self.subTest(msg="after re-building"): self.assertGreater(len(mock._data), 0) - self.assertEqual(len(mock._parameter_table), 1) + self.assertEqual(mock._data.num_params(), 1) def test_calling_attributes_works(self): """Test that the circuit is constructed when attributes are called.""" diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 73398e4316b..6fc6e8e72bd 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -187,12 +187,20 @@ def test_foreach_op_indexed(self): def test_map_ops(self): """Test all operations are replaced.""" qr = QuantumRegister(5) + + # Use a custom gate to ensure we get a gate class returned and not + # a standard gate. + class CustomXGate(XGate): + """A custom X gate that doesn't have rust native representation.""" + + _standard_gate = None + data_list = [ - CircuitInstruction(XGate(), [qr[0]], []), - CircuitInstruction(XGate(), [qr[1]], []), - CircuitInstruction(XGate(), [qr[2]], []), - CircuitInstruction(XGate(), [qr[3]], []), - CircuitInstruction(XGate(), [qr[4]], []), + CircuitInstruction(CustomXGate(), [qr[0]], []), + CircuitInstruction(CustomXGate(), [qr[1]], []), + CircuitInstruction(CustomXGate(), [qr[2]], []), + CircuitInstruction(CustomXGate(), [qr[3]], []), + CircuitInstruction(CustomXGate(), [qr[4]], []), ] data = CircuitData(qubits=list(qr), data=data_list) data.map_ops(lambda op: op.to_mutable()) @@ -828,6 +836,9 @@ def test_param_gate_instance(self): qc0.append(rx, [0]) qc1.append(rx, [0]) qc0.assign_parameters({a: b}, inplace=True) - qc0_instance = next(iter(qc0._parameter_table[b]))[0] - qc1_instance = next(iter(qc1._parameter_table[a]))[0] + # A fancy way of doing qc0_instance = qc0.data[0] and qc1_instance = qc1.data[0] + # but this at least verifies the parameter table is point from the parameter to + # the correct instruction (which is the only one) + qc0_instance = qc0._data[next(iter(qc0._data._get_param(b.uuid.int)))[0]] + qc1_instance = qc1._data[next(iter(qc1._data._get_param(a.uuid.int)))[0]] self.assertNotEqual(qc0_instance, qc1_instance) diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 6caf194d37d..e9a7416f78c 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -593,7 +593,7 @@ def test_clear_circuit(self): qc.clear() self.assertEqual(len(qc.data), 0) - self.assertEqual(len(qc._parameter_table), 0) + self.assertEqual(qc._data.num_params(), 0) def test_barrier(self): """Test multiple argument forms of barrier.""" diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index db6280b8823..7bb36a1401f 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -357,7 +357,8 @@ def test_compose_copy(self): self.assertIsNot(should_copy.data[-1].operation, parametric.data[-1].operation) self.assertEqual(should_copy.data[-1].operation, parametric.data[-1].operation) forbid_copy = base.compose(parametric, qubits=[0], copy=False) - self.assertIs(forbid_copy.data[-1].operation, parametric.data[-1].operation) + # For standard gates a fresh copy is returned from the data list each time + self.assertEqual(forbid_copy.data[-1].operation, parametric.data[-1].operation) conditional = QuantumCircuit(1, 1) conditional.x(0).c_if(conditional.clbits[0], True) diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index edd01c5cc1c..4ac69278fd4 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -577,14 +577,14 @@ def test_instructionset_c_if_with_no_requester(self): instructions.add(instruction, [Qubit()], []) register = ClassicalRegister(2) instructions.c_if(register, 0) - self.assertIs(instruction.condition[0], register) + self.assertIs(instructions[0].operation.condition[0], register) with self.subTest("accepts arbitrary bit"): instruction = RZGate(0) instructions = InstructionSet() instructions.add(instruction, [Qubit()], []) bit = Clbit() instructions.c_if(bit, 0) - self.assertIs(instruction.condition[0], bit) + self.assertIs(instructions[0].operation.condition[0], bit) with self.subTest("rejects index"): instruction = RZGate(0) instructions = InstructionSet() @@ -617,7 +617,7 @@ def dummy_requester(specifier): bit = Clbit() instructions.c_if(bit, 0) dummy_requester.assert_called_once_with(bit) - self.assertIs(instruction.condition[0], sentinel_bit) + self.assertIs(instructions[0].operation.condition[0], sentinel_bit) with self.subTest("calls requester with index"): dummy_requester.reset_mock() instruction = RZGate(0) @@ -626,7 +626,7 @@ def dummy_requester(specifier): index = 0 instructions.c_if(index, 0) dummy_requester.assert_called_once_with(index) - self.assertIs(instruction.condition[0], sentinel_bit) + self.assertIs(instructions[0].operation.condition[0], sentinel_bit) with self.subTest("calls requester with register"): dummy_requester.reset_mock() instruction = RZGate(0) @@ -635,7 +635,7 @@ def dummy_requester(specifier): register = ClassicalRegister(2) instructions.c_if(register, 0) dummy_requester.assert_called_once_with(register) - self.assertIs(instruction.condition[0], sentinel_register) + self.assertIs(instructions[0].operation.condition[0], sentinel_register) with self.subTest("calls requester only once when broadcast"): dummy_requester.reset_mock() instruction_list = [RZGate(0), RZGate(0), RZGate(0)] @@ -646,7 +646,7 @@ def dummy_requester(specifier): instructions.c_if(register, 0) dummy_requester.assert_called_once_with(register) for instruction in instruction_list: - self.assertIs(instruction.condition[0], sentinel_register) + self.assertIs(instructions[0].operation.condition[0], sentinel_register) def test_label_type_enforcement(self): """Test instruction label type enforcement.""" diff --git a/test/python/circuit/test_isometry.py b/test/python/circuit/test_isometry.py index a09ff331e02..35ff639cedd 100644 --- a/test/python/circuit/test_isometry.py +++ b/test/python/circuit/test_isometry.py @@ -102,7 +102,6 @@ def test_isometry_tolerance(self, iso): # Simulate the decomposed gate unitary = Operator(qc).data iso_from_circuit = unitary[::, 0 : 2**num_q_input] - self.assertTrue(np.allclose(iso_from_circuit, iso)) @data( diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index ed82f33eac9..2c8e2ee83d4 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -26,7 +26,7 @@ from qiskit.circuit.library.standard_gates.rz import RZGate from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.circuit import Gate, Instruction, Parameter, ParameterExpression, ParameterVector -from qiskit.circuit.parametertable import ParameterReferences, ParameterTable, ParameterView +from qiskit.circuit.parametertable import ParameterView from qiskit.circuit.exceptions import CircuitError from qiskit.compiler import assemble, transpile from qiskit import pulse @@ -45,8 +45,6 @@ def raise_if_parameter_table_invalid(circuit): CircuitError: if QuantumCircuit and ParameterTable are inconsistent. """ - table = circuit._parameter_table - # Assert parameters present in circuit match those in table. circuit_parameters = { parameter @@ -55,7 +53,7 @@ def raise_if_parameter_table_invalid(circuit): for parameter in param.parameters if isinstance(param, ParameterExpression) } - table_parameters = set(table._table.keys()) + table_parameters = set(circuit._data.get_params_unsorted()) if circuit_parameters != table_parameters: raise CircuitError( @@ -67,8 +65,10 @@ def raise_if_parameter_table_invalid(circuit): # Assert parameter locations in table are present in circuit. circuit_instructions = [instr.operation for instr in circuit._data] - for parameter, instr_list in table.items(): - for instr, param_index in instr_list: + for parameter in table_parameters: + instr_list = circuit._data._get_param(parameter.uuid.int) + for instr_index, param_index in instr_list: + instr = circuit.data[instr_index].operation if instr not in circuit_instructions: raise CircuitError(f"ParameterTable instruction not present in circuit: {instr}.") @@ -88,13 +88,15 @@ def raise_if_parameter_table_invalid(circuit): ) # Assert circuit has no other parameter locations other than those in table. - for instruction in circuit._data: + for instr_index, instruction in enumerate(circuit._data): for param_index, param in enumerate(instruction.operation.params): if isinstance(param, ParameterExpression): parameters = param.parameters for parameter in parameters: - if (instruction.operation, param_index) not in table[parameter]: + if (instr_index, param_index) not in circuit._data._get_param( + parameter.uuid.int + ): raise CircuitError( "Found parameterized instruction not " "present in table. Instruction: {} " @@ -158,15 +160,19 @@ def test_append_copies_parametric(self): self.assertIsNot(qc.data[-1].operation, gate_param) self.assertEqual(qc.data[-1].operation, gate_param) + # Standard gates are not stored as Python objects so a fresh object + # is always instantiated on accessing `CircuitInstruction.operation` qc.append(gate_param, [0], copy=False) - self.assertIs(qc.data[-1].operation, gate_param) + self.assertEqual(qc.data[-1].operation, gate_param) qc.append(gate_expr, [0], copy=True) self.assertIsNot(qc.data[-1].operation, gate_expr) self.assertEqual(qc.data[-1].operation, gate_expr) + # Standard gates are not stored as Python objects so a fresh object + # is always instantiated on accessing `CircuitInstruction.operation` qc.append(gate_expr, [0], copy=False) - self.assertIs(qc.data[-1].operation, gate_expr) + self.assertEqual(qc.data[-1].operation, gate_expr) def test_parameters_property(self): """Test instantiating gate with variable parameters""" @@ -177,10 +183,9 @@ def test_parameters_property(self): qc = QuantumCircuit(qr) rxg = RXGate(theta) qc.append(rxg, [qr[0]], []) - vparams = qc._parameter_table - self.assertEqual(len(vparams), 1) - self.assertIs(theta, next(iter(vparams))) - self.assertEqual(rxg, next(iter(vparams[theta]))[0]) + self.assertEqual(qc._data.num_params(), 1) + self.assertIs(theta, next(iter(qc._data.get_params_unsorted()))) + self.assertEqual(rxg, qc.data[next(iter(qc._data._get_param(theta.uuid.int)))[0]].operation) def test_parameters_property_by_index(self): """Test getting parameters by index""" @@ -553,12 +558,12 @@ def test_two_parameter_expression_binding(self): qc.rx(theta, 0) qc.ry(phi, 0) - self.assertEqual(len(qc._parameter_table[theta]), 1) - self.assertEqual(len(qc._parameter_table[phi]), 1) + self.assertEqual(qc._data._get_entry_count(theta), 1) + self.assertEqual(qc._data._get_entry_count(phi), 1) qc.assign_parameters({theta: -phi}, inplace=True) - self.assertEqual(len(qc._parameter_table[phi]), 2) + self.assertEqual(qc._data._get_entry_count(phi), 2) def test_expression_partial_binding_zero(self): """Verify that binding remains possible even if a previous partial bind @@ -580,7 +585,6 @@ def test_expression_partial_binding_zero(self): fbqc = pqc.assign_parameters({phi: 1}) self.assertEqual(fbqc.parameters, set()) - self.assertIsInstance(fbqc.data[0].operation.params[0], int) self.assertEqual(float(fbqc.data[0].operation.params[0]), 0) def test_raise_if_assigning_params_not_in_circuit(self): @@ -614,7 +618,7 @@ def test_gate_multiplicity_binding(self): qc.append(gate, [0], []) qc.append(gate, [0], []) qc2 = qc.assign_parameters({theta: 1.0}) - self.assertEqual(len(qc2._parameter_table), 0) + self.assertEqual(qc2._data.num_params(), 0) for instruction in qc2.data: self.assertEqual(float(instruction.operation.params[0]), 1.0) @@ -2170,155 +2174,6 @@ def test_parameter_symbol_equal_after_ufunc(self): self.assertEqual(phi._parameter_symbols, cos_phi._parameter_symbols) -class TestParameterReferences(QiskitTestCase): - """Test the ParameterReferences class.""" - - def test_equal_inst_diff_instance(self): - """Different value equal instructions are treated as distinct.""" - - theta = Parameter("theta") - gate1 = RZGate(theta) - gate2 = RZGate(theta) - - self.assertIsNot(gate1, gate2) - self.assertEqual(gate1, gate2) - - refs = ParameterReferences(((gate1, 0), (gate2, 0))) - - # test __contains__ - self.assertIn((gate1, 0), refs) - self.assertIn((gate2, 0), refs) - - gate_ids = {id(gate1), id(gate2)} - self.assertEqual(gate_ids, {id(gate) for gate, _ in refs}) - self.assertTrue(all(idx == 0 for _, idx in refs)) - - def test_pickle_unpickle(self): - """Membership testing after pickle/unpickle.""" - - theta = Parameter("theta") - gate1 = RZGate(theta) - gate2 = RZGate(theta) - - self.assertIsNot(gate1, gate2) - self.assertEqual(gate1, gate2) - - refs = ParameterReferences(((gate1, 0), (gate2, 0))) - - to_pickle = (gate1, refs) - pickled = pickle.dumps(to_pickle) - (gate1_new, refs_new) = pickle.loads(pickled) - - self.assertEqual(len(refs_new), len(refs)) - self.assertNotIn((gate1, 0), refs_new) - self.assertIn((gate1_new, 0), refs_new) - - def test_equal_inst_same_instance(self): - """Referentially equal instructions are treated as same.""" - - theta = Parameter("theta") - gate = RZGate(theta) - - refs = ParameterReferences(((gate, 0), (gate, 0))) - - self.assertIn((gate, 0), refs) - self.assertEqual(len(refs), 1) - self.assertIs(next(iter(refs))[0], gate) - self.assertEqual(next(iter(refs))[1], 0) - - def test_extend_refs(self): - """Extending references handles duplicates.""" - - theta = Parameter("theta") - ref0 = (RZGate(theta), 0) - ref1 = (RZGate(theta), 0) - ref2 = (RZGate(theta), 0) - - refs = ParameterReferences((ref0,)) - refs |= ParameterReferences((ref0, ref1, ref2, ref1, ref0)) - - self.assertEqual(refs, ParameterReferences((ref0, ref1, ref2))) - - def test_copy_param_refs(self): - """Copy of parameter references is a shallow copy.""" - - theta = Parameter("theta") - ref0 = (RZGate(theta), 0) - ref1 = (RZGate(theta), 0) - ref2 = (RZGate(theta), 0) - ref3 = (RZGate(theta), 0) - - refs = ParameterReferences((ref0, ref1)) - refs_copy = refs.copy() - - # Check same gate instances in copy - gate_ids = {id(ref0[0]), id(ref1[0])} - self.assertEqual({id(gate) for gate, _ in refs_copy}, gate_ids) - - # add new ref to original and check copy not modified - refs.add(ref2) - self.assertNotIn(ref2, refs_copy) - self.assertEqual(refs_copy, ParameterReferences((ref0, ref1))) - - # add new ref to copy and check original not modified - refs_copy.add(ref3) - self.assertNotIn(ref3, refs) - self.assertEqual(refs, ParameterReferences((ref0, ref1, ref2))) - - -class TestParameterTable(QiskitTestCase): - """Test the ParameterTable class.""" - - def test_init_param_table(self): - """Parameter table init from mapping.""" - - p1 = Parameter("theta") - p2 = Parameter("theta") - - ref0 = (RZGate(p1), 0) - ref1 = (RZGate(p1), 0) - ref2 = (RZGate(p2), 0) - - mapping = {p1: ParameterReferences((ref0, ref1)), p2: ParameterReferences((ref2,))} - - table = ParameterTable(mapping) - - # make sure editing mapping doesn't change `table` - del mapping[p1] - - self.assertEqual(table[p1], ParameterReferences((ref0, ref1))) - self.assertEqual(table[p2], ParameterReferences((ref2,))) - - def test_set_references(self): - """References replacement by parameter key.""" - - p1 = Parameter("theta") - - ref0 = (RZGate(p1), 0) - ref1 = (RZGate(p1), 0) - - table = ParameterTable() - table[p1] = ParameterReferences((ref0, ref1)) - self.assertEqual(table[p1], ParameterReferences((ref0, ref1))) - - table[p1] = ParameterReferences((ref1,)) - self.assertEqual(table[p1], ParameterReferences((ref1,))) - - def test_set_references_from_iterable(self): - """Parameter table init from iterable.""" - - p1 = Parameter("theta") - - ref0 = (RZGate(p1), 0) - ref1 = (RZGate(p1), 0) - ref2 = (RZGate(p1), 0) - - table = ParameterTable({p1: ParameterReferences((ref0, ref1))}) - table[p1] = (ref2, ref1, ref0) - - self.assertEqual(table[p1], ParameterReferences((ref2, ref1, ref0))) - - class TestParameterView(QiskitTestCase): """Test the ParameterView object.""" diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py new file mode 100644 index 00000000000..06d4ed86a60 --- /dev/null +++ b/test/python/circuit/test_rust_equivalence.py @@ -0,0 +1,143 @@ +# 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. + +"""Rust gate definition tests""" + +from math import pi + +from test import QiskitTestCase + +import numpy as np + +from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping + +SKIP_LIST = {"cy", "ccx", "rx", "ry", "ecr", "sx"} +CUSTOM_MAPPING = {"x", "rz"} + + +class TestRustGateEquivalence(QiskitTestCase): + """Tests that compile time rust gate definitions is correct.""" + + def setUp(self): + super().setUp() + self.standard_gates = get_standard_gate_name_mapping() + # Pre-warm gate mapping cache, this is needed so rust -> py conversion is done + qc = QuantumCircuit(3) + for gate in self.standard_gates.values(): + if getattr(gate, "_standard_gate", None): + if gate.params: + gate = gate.base_class(*[pi] * len(gate.params)) + qc.append(gate, list(range(gate.num_qubits))) + + def test_definitions(self): + """Test definitions are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if name in SKIP_LIST: + # gate does not have a rust definition yet + continue + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + params = [pi] * standard_gate._num_params() + py_def = gate_class.base_class(*params).definition + rs_def = standard_gate._get_definition(params) + if py_def is None: + self.assertIsNone(rs_def) + else: + rs_def = QuantumCircuit._from_circuit_data(rs_def) + for rs_inst, py_inst in zip(rs_def._data, py_def._data): + # Rust uses U but python still uses U3 and u2 + if rs_inst.operation.name == "u": + if py_inst.operation.name == "u3": + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + elif py_inst.operation.name == "u2": + self.assertEqual( + rs_inst.operation.params, + [ + pi / 2, + py_inst.operation.params[0], + py_inst.operation.params[1], + ], + ) + + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + # Rust uses P but python still uses u1 + elif rs_inst.operation.name == "p": + self.assertEqual(py_inst.operation.name, "u1") + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + else: + self.assertEqual(py_inst.operation.name, rs_inst.operation.name) + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + + def test_matrix(self): + """Test matrices are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + params = [pi] * standard_gate._num_params() + py_def = gate_class.base_class(*params).to_matrix() + rs_def = standard_gate._to_matrix(params) + np.testing.assert_allclose(rs_def, py_def) + + def test_name(self): + """Test that the gate name properties match in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + self.assertEqual(gate_class.name, standard_gate.name) + + def test_num_qubits(self): + """Test the number of qubits are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + self.assertEqual(gate_class.num_qubits, standard_gate.num_qubits) + + def test_num_params(self): + """Test the number of parameters are the same in rust space.""" + for name, gate_class in self.standard_gates.items(): + standard_gate = getattr(gate_class, "_standard_gate", None) + if standard_gate is None: + # gate is not in rust yet + continue + + with self.subTest(name=name): + self.assertEqual( + len(gate_class.params), standard_gate.num_params, msg=f"{name} not equal" + ) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 598405beaae..135e874be48 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1946,7 +1946,7 @@ class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCa def test_basis_gates(self): """Teleportation with physical qubits""" qc = QuantumCircuit(3, 2) - first_h = qc.h(1)[0].operation + qc.h(1) qc.cx(1, 2) qc.barrier() qc.cx(0, 1) @@ -1957,52 +1957,51 @@ def test_basis_gates(self): first_x = qc.x(2).c_if(qc.clbits[1], 1)[0].operation qc.z(2).c_if(qc.clbits[0], 1) - u2 = first_h.definition.data[0].operation - u3_1 = u2.definition.data[0].operation - u3_2 = first_x.definition.data[0].operation - - expected_qasm = "\n".join( - [ - "OPENQASM 3.0;", - f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi/2, 0, pi) _gate_q_0;", - "}", - f"gate u2_{id(u2)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", - f" u3_{id(u3_1)}(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - f" u2_{id(u2)}(0, pi) _gate_q_0;", - "}", - f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi, 0, pi) _gate_q_0;", - "}", - "gate x _gate_q_0 {", - f" u3_{id(u3_2)}(pi, 0, pi) _gate_q_0;", - "}", - "bit[2] c;", - "qubit[3] q;", - "h q[1];", - "cx q[1], q[2];", - "barrier q[0], q[1], q[2];", - "cx q[0], q[1];", - "h q[0];", - "barrier q[0], q[1], q[2];", - "c[0] = measure q[0];", - "c[1] = measure q[1];", - "barrier q[0], q[1], q[2];", - "if (c[1]) {", - " x q[2];", - "}", - "if (c[0]) {", - " z q[2];", - "}", - "", - ] - ) - self.assertEqual( - Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(qc), - expected_qasm, - ) + id_len = len(str(id(first_x))) + expected_qasm = [ + "OPENQASM 3.0;", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi/2, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate h _gate_q_0 {", + re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi, 0, pi) _gate_q_0;", + "}", + "gate x _gate_q_0 {", + re.compile(r" u3_\d{%s}\(pi, 0, pi\) _gate_q_0;" % id_len), + "}", + "bit[2] c;", + "qubit[3] q;", + "h q[1];", + "cx q[1], q[2];", + "barrier q[0], q[1], q[2];", + "cx q[0], q[1];", + "h q[0];", + "barrier q[0], q[1], q[2];", + "c[0] = measure q[0];", + "c[1] = measure q[1];", + "barrier q[0], q[1], q[2];", + "if (c[1]) {", + " x q[2];", + "}", + "if (c[0]) {", + " z q[2];", + "}", + "", + ] + res = Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(qc).splitlines() + for result, expected in zip(res, expected_qasm): + if isinstance(expected, str): + self.assertEqual(result, expected) + else: + self.assertTrue( + expected.search(result), f"Line {result} doesn't match regex: {expected}" + ) def test_teleportation(self): """Teleportation with physical qubits""" @@ -2120,62 +2119,58 @@ def test_no_include(self): circuit.sx(0) circuit.cx(0, 1) - rz = circuit.data[0].operation - u1_1 = rz.definition.data[0].operation - u3_1 = u1_1.definition.data[0].operation - sx = circuit.data[1].operation - sdg = sx.definition.data[0].operation - u1_2 = sdg.definition.data[0].operation - u3_2 = u1_2.definition.data[0].operation - h_ = sx.definition.data[1].operation - u2_1 = h_.definition.data[0].operation - u3_3 = u2_1.definition.data[0].operation - expected_qasm = "\n".join( - [ - "OPENQASM 3.0;", - f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(0, 0, pi/2) _gate_q_0;", - "}", - f"gate u1_{id(u1_1)}(_gate_p_0) _gate_q_0 {{", - f" u3_{id(u3_1)}(0, 0, pi/2) _gate_q_0;", - "}", - f"gate rz_{id(rz)}(_gate_p_0) _gate_q_0 {{", - f" u1_{id(u1_1)}(pi/2) _gate_q_0;", - "}", - f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(0, 0, -pi/2) _gate_q_0;", - "}", - f"gate u1_{id(u1_2)}(_gate_p_0) _gate_q_0 {{", - f" u3_{id(u3_2)}(0, 0, -pi/2) _gate_q_0;", - "}", - "gate sdg _gate_q_0 {", - f" u1_{id(u1_2)}(-pi/2) _gate_q_0;", - "}", - f"gate u3_{id(u3_3)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi/2, 0, pi) _gate_q_0;", - "}", - f"gate u2_{id(u2_1)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", - f" u3_{id(u3_3)}(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - f" u2_{id(u2_1)}(0, pi) _gate_q_0;", - "}", - "gate sx _gate_q_0 {", - " sdg _gate_q_0;", - " h _gate_q_0;", - " sdg _gate_q_0;", - "}", - "gate cx c, t {", - " ctrl @ U(pi, 0, pi) c, t;", - "}", - "qubit[2] q;", - f"rz_{id(rz)}(pi/2) q[0];", - "sx q[0];", - "cx q[0], q[1];", - "", - ] - ) - self.assertEqual(Exporter(includes=[]).dumps(circuit), expected_qasm) + id_len = len(str(id(circuit.data[0].operation))) + expected_qasm = [ + "OPENQASM 3.0;", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(0, 0, pi/2) _gate_q_0;", + "}", + re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(0, 0, pi/2\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate rz_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u1_\d{%s}\(pi/2\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(0, 0, -pi/2) _gate_q_0;", + "}", + re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(0, 0, -pi/2\) _gate_q_0;" % id_len), + "}", + "gate sdg _gate_q_0 {", + re.compile(r" u1_\d{%s}\(-pi/2\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi/2, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate h _gate_q_0 {", + re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), + "}", + "gate sx _gate_q_0 {", + " sdg _gate_q_0;", + " h _gate_q_0;", + " sdg _gate_q_0;", + "}", + "gate cx c, t {", + " ctrl @ U(pi, 0, pi) c, t;", + "}", + "qubit[2] q;", + re.compile(r"rz_\d{%s}\(pi/2\) q\[0\];" % id_len), + "sx q[0];", + "cx q[0], q[1];", + "", + ] + res = Exporter(includes=[]).dumps(circuit).splitlines() + for result, expected in zip(res, expected_qasm): + if isinstance(expected, str): + self.assertEqual(result, expected) + else: + self.assertTrue( + expected.search(result), f"Line {result} doesn't match regex: {expected}" + ) def test_unusual_conditions(self): """Test that special QASM constructs such as ``measure`` are correctly handled when the