From fb7081404cb71023b617d0a8524721bcfd6cba63 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 3 May 2024 17:21:27 -0400 Subject: [PATCH 01/61] Add infrastructure for gates, instruction, and operations in Rust This commit adds a native representation of Gates, Instruction, and Operations to rust's circuit module. At a high level this works by either wrapping the Python object in a rust wrapper struct that tracks metadata about the operations (name, num_qubits, etc) and then for other details it calls back to Python to get dynamic details like the definition, matrix, etc. For standard library gates like Swap, CX, H, etc this replaces the on-circuit representation with a new rust enum StandardGate. The enum representation is much more efficient and has a minimal memory footprint (just the enum variant and then any parameters or other mutable state stored in the circuit instruction). All the gate properties such as the matrix, definiton, name, etc are statically defined in rust code based on the enum variant (which represents the gate). The use of an enum to represent standard gates does mean a change in what we store on a CircuitInstruction. To represent a standard gate fully we need to store the mutable properties of the existing Gate class on the circuit instruction as the gate by itself doesn't contain this detail. That means, the parameters, label, unit, duration, and condition are added to the rust side of circuit instrucion. However no Python side access methods are added for these as they're internal only to the Rust code. In Qiskit 2.0 to simplify this storage we'll be able to drop, unit, duration, and condition from the api leaving only label and parameters. But for right now we're tracking all of the fields. To facilitate working with circuits and gates full from rust the setting the `operation` attribute of a `CircuitInstruction` object now transltates the python object to an internal rust representation. For standard gates this translates it to the enum form described earlier, and for other circuit operations 3 new Rust structs: PyGate, PyInstruction, and PyOperation are used to wrap the underlying Python object in a Rust api. These structs cache some commonly accessed static properties of the operation, such as the name, number of qubits, etc. However for dynamic pieces, such as the definition or matrix, callback to python to get a rust representation for those. Similarly whenever the `operation` attribute is accessed from Python it converts it back to the normal Python object representation. For standard gates this involves creating a new instance of a Python object based on it's internal rust representation. For the wrapper structs a reference to the wrapped PyObject is returned. To manage the 4 variants of operation (`StandardGate`, `PyGate`, `PyInstruction`, and `PyOperation`) a new Rust trait `Operation` is created that defines a standard interface for getting the properties of a given circuit operation. This common interface is implemented for the 4 variants as well as the `OperationType` enum which wraps all 4 (and is used as the type for `CircuitInstruction.operation` in the rust code. As everything in the `QuantumCircuit` data model is quite coupled moving the source of truth for the operations to exist in Rust means that more of the underlying `QuantumCircuit`'s responsibility has to move to Rust as well. Primarily this involves the `ParameterTable` which was an internal class for tracking which instructions in the circuit have a `ParameterExpression` parameter so that when we go to bind parameters we can lookup which operations need to be updated with the bind value. Since the representation of those instructions now lives in Rust and Python only recieves a ephemeral copy of the instructions the ParameterTable had to be reimplemented in Rust to track the instructions. This new parameter table maps the Parameter's uuid (as a u128) as a unique identifier for each parameter and maps this to a positional index in the circuit data to the underlying instruction using that parameter. This is a bit different from the Python parameter table which was mapping a parameter object to the id of the operation object using that parmaeter. This also leads to a difference in the binding mechanics as the parameter assignment was done by reference in the old model, but now we need to update the entire instruction more explicitly in rust. Additionally, because the global phase of a circuit can be parameterized the ownership of global phase is moved from Python into Rust in this commit as well. After this commit the only properties of a circuit that are not defined in Rust for the source of truth are the bits (and vars) of the circuit, and when creating circuits from rust this is what causes a Python interaction to still be required. This commit does not translate the full standard library of gates as that would make the pull request huge, instead this adds the basic infrastructure for having a more efficient standard gate representation on circuits. There will be follow up pull requests to add the missing gates and round out support in rust. The goal of this pull request is primarily to add the infrastructure for representing the full circuit model (and dag model in the future) in rust. By itself this is not expected to improve runtime performance (if anything it will probably hurt performance because of extra type conversions) but it is intended to enable writing native circuit manipulations in Rust, including transpiler passes without needing involvement from Python. Longer term this should greatly improve the runtime performance and reduce the memory overhead of Qiskit. But, this is just an early step towards that goal, and is more about unlocking the future capability. The next steps after this commit are to finish migrating the standard gate library and also update the `QuantumCircuit` methods to better leverage the more complete rust representation (which should help offset the performance penalty introduced by this). Fixes: #12205 --- Cargo.lock | 11 + Cargo.toml | 5 + crates/accelerate/Cargo.toml | 8 +- crates/circuit/Cargo.toml | 13 +- crates/circuit/README.md | 63 ++ crates/circuit/src/circuit_data.rs | 561 +++++++++++++- crates/circuit/src/circuit_instruction.rs | 687 ++++++++++++++++- crates/circuit/src/dag_node.rs | 60 +- crates/circuit/src/gate_matrix.rs | 330 ++++++++ crates/circuit/src/lib.rs | 7 + crates/circuit/src/operations.rs | 726 ++++++++++++++++++ crates/circuit/src/parameter_table.rs | 189 +++++ qiskit/circuit/instruction.py | 1 + qiskit/circuit/instructionset.py | 9 +- qiskit/circuit/library/blueprintcircuit.py | 4 +- qiskit/circuit/library/standard_gates/ecr.py | 3 + .../library/standard_gates/global_phase.py | 3 + qiskit/circuit/library/standard_gates/h.py | 3 + qiskit/circuit/library/standard_gates/i.py | 3 + qiskit/circuit/library/standard_gates/p.py | 3 + qiskit/circuit/library/standard_gates/rx.py | 3 + qiskit/circuit/library/standard_gates/ry.py | 3 + qiskit/circuit/library/standard_gates/rz.py | 3 + qiskit/circuit/library/standard_gates/swap.py | 3 + qiskit/circuit/library/standard_gates/sx.py | 3 + qiskit/circuit/library/standard_gates/u.py | 3 + qiskit/circuit/library/standard_gates/x.py | 7 + qiskit/circuit/library/standard_gates/y.py | 5 + qiskit/circuit/library/standard_gates/z.py | 5 + qiskit/circuit/parametertable.py | 188 ----- qiskit/circuit/quantumcircuit.py | 190 ++--- qiskit/circuit/quantumcircuitdata.py | 6 +- qiskit/converters/circuit_to_instruction.py | 12 +- qiskit/qasm3/exporter.py | 3 +- .../operators/dihedral/dihedral.py | 3 +- .../padding/dynamical_decoupling.py | 16 +- .../passes/scheduling/time_unit_conversion.py | 7 +- .../circuit-gates-rust-5c6ab6c58f7fd2c9.yaml | 70 ++ .../circuit/library/test_blueprintcircuit.py | 6 +- test/python/circuit/library/test_nlocal.py | 2 +- test/python/circuit/test_circuit_data.py | 9 +- .../python/circuit/test_circuit_operations.py | 2 +- test/python/circuit/test_compose.py | 3 +- test/python/circuit/test_instructions.py | 12 +- test/python/circuit/test_isometry.py | 1 - test/python/circuit/test_parameters.py | 190 +---- test/python/circuit/test_rust_equivalence.py | 110 +++ 47 files changed, 2952 insertions(+), 602 deletions(-) create mode 100644 crates/circuit/src/gate_matrix.rs create mode 100644 crates/circuit/src/operations.rs create mode 100644 crates/circuit/src/parameter_table.rs create mode 100644 releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml create mode 100644 test/python/circuit/test_rust_equivalence.py diff --git a/Cargo.lock b/Cargo.lock index d812f8fc1c58..d0a2fdb5da9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -618,6 +618,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.154" @@ -1124,7 +1130,12 @@ name = "qiskit-circuit" version = "1.2.0" dependencies = [ "hashbrown 0.14.5", + "lazy_static", + "ndarray", + "num-complex", + "numpy", "pyo3", + "smallvec", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2827b2206f4d..13f43cfabcdc 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 4f2c80ebff2b..e76100907a3b 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.18.2" @@ -25,7 +25,7 @@ itertools = "0.12.1" 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/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 6ec38392cc38..5350c67058d5 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -11,4 +11,15 @@ doctest = false [dependencies] hashbrown.workspace = true -pyo3.workspace = true +num-complex.workspace = true +ndarray.workspace = true +numpy.workspace = true +lazy_static = "1.4" + +[dependencies.pyo3] +workspace = true +features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"] + +[dependencies.smallvec] +workspace = true +features = ["union"] diff --git a/crates/circuit/README.md b/crates/circuit/README.md index b9375c9f99da..f84bbdc9c5eb 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 `ParmaeterExpression` 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 enuerates 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 defined 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. + +## ParameterTable + +The `ParameterTable` 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 `ParameterEntry`, the `name` to the uuid, and the uuid to the PyObject for the actual `Parameter`. + +The `ParameterEntry` 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 for the `CircuitInstruction.params` field of +a give instruction where the given `Parameter` is used in the circuit. If the instruction index is +`usize::MAX` that points to the global phase property of the circuit instead of a `CircuitInstruction`. diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 944565cf36d8..961c25b94432 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -10,12 +10,18 @@ // 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_and_data_to_py, CircuitInstruction, +}; use crate::intern_context::{BitType, IndexType, InternContext}; +use crate::operations::{OperationType, Param}; +use crate::parameter_table::{ParamEntry, ParamTable}; use crate::SliceOrInt; +use smallvec::SmallVec; -use hashbrown::HashMap; +use hashbrown::{HashMap, HashSet}; use pyo3::exceptions::{PyIndexError, PyKeyError, PyRuntimeError, PyValueError}; +use pyo3::intern; use pyo3::prelude::*; use pyo3::types::{PyList, PySet, PySlice, PyTuple, PyType}; use pyo3::{PyObject, PyResult, PyTraverseError, PyVisit}; @@ -25,11 +31,16 @@ use std::hash::{Hash, Hasher}; #[derive(Clone, Debug)] struct PackedInstruction { /// The Python-side operation instance. - op: PyObject, + op: OperationType, /// The index under which the interner has stored `qubits`. qubits_id: IndexType, /// The index under which the interner has stored `clbits`. clbits_id: IndexType, + params: Option>, + label: Option, + duration: Option, + unit: Option, + condition: Option, } /// Private wrapper for Python-side Bit instances that implements @@ -152,18 +163,281 @@ pub struct CircuitData { qubits: Py, /// The clbits registered, cached as a ``list[Clbit]``. clbits: Py, + param_table: ParamTable, + #[pyo3(get)] + global_phase: Param, +} + +type InstructionEntryType<'a> = (OperationType, Option<&'a [Param]>, &'a [u32]); + +impl CircuitData { + /// A helper method to build a new CircuitData from an owned definition + /// as a slice of OperationType, parameters, and qubits. + pub fn build_new_from( + py: Python, + num_qubits: usize, + num_clbits: usize, + instructions: &[InstructionEntryType], + global_phase: Param, + ) -> PyResult { + let mut res = CircuitData { + data: Vec::with_capacity(instructions.len()), + intern_context: InternContext::new(), + qubits_native: Vec::with_capacity(num_qubits), + clbits_native: Vec::with_capacity(num_clbits), + qubit_indices_native: HashMap::with_capacity(num_qubits), + clbit_indices_native: HashMap::with_capacity(num_clbits), + qubits: PyList::empty_bound(py).unbind(), + clbits: PyList::empty_bound(py).unbind(), + param_table: ParamTable::new(), + global_phase, + }; + if num_qubits > 0 { + let qubit_mod = py.import_bound("qiskit.circuit.quantumregister")?; + let qubit_cls = qubit_mod.getattr("Qubit")?; + for _i in 0..num_qubits { + let bit = qubit_cls.call0()?; + res.add_qubit(py, &bit, true)?; + } + } + if num_clbits > 0 { + let clbit_mod = py.import_bound(intern!(py, "qiskit.circuit.classicalregister"))?; + let clbit_cls = clbit_mod.getattr(intern!(py, "Clbit"))?; + for _i in 0..num_clbits { + let bit = clbit_cls.call0()?; + res.add_clbit(py, &bit, true)?; + } + } + for (operation, params, qargs) in instructions { + let qubits = PyTuple::new_bound( + py, + qargs + .iter() + .map(|x| res.qubits_native[*x as usize].clone_ref(py)) + .collect::>(), + ) + .unbind(); + let empty: [u8; 0] = []; + let clbits = PyTuple::new_bound(py, empty); + let params: Option> = + params.as_ref().map(|p| p.iter().cloned().collect()); + let inst = res.pack_owned( + py, + &CircuitInstruction { + operation: operation.clone(), + qubits, + clbits: clbits.into(), + params, + label: None, + duration: None, + unit: None, + condition: None, + }, + )?; + res.data.push(inst); + } + Ok(res) + } + + /// 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 { + let mut new_param = false; + for (param_index, raw_param_objs) in ¶ms { + let atomic_parameters: HashMap = raw_param_objs + .iter() + .map(|x| { + ( + 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), + ) + }) + .collect(); + for (param_uuid, param_obj) in atomic_parameters.into_iter() { + match self.param_table.table.get_mut(¶m_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, new_entry)?; + } + }; + } + } + return Ok(new_param); + } + // Update the parameter table + let mut new_param = false; + let inst_params = &self.data[inst_index].params; + if let Some(raw_params) = inst_params { + let param_mod = + PyModule::import_bound(py, intern!(py, "qiskit.circuit.parameterexpression"))?; + let param_class = param_mod.getattr(intern!(py, "ParameterExpression"))?; + let circuit_mod = + PyModule::import_bound(py, intern!(py, "qiskit.circuit.quantumcircuit"))?; + let circuit_class = circuit_mod.getattr(intern!(py, "QuantumCircuit"))?; + let params: Vec<(usize, PyObject)> = raw_params + .iter() + .enumerate() + .filter_map(|(idx, x)| match x { + Param::ParameterExpression(param_obj) => { + if param_obj + .clone_ref(py) + .into_bound(py) + .is_instance(¶m_class) + .unwrap() + || param_obj + .clone_ref(py) + .into_bound(py) + .is_instance(&circuit_class) + .unwrap() + { + Some((idx, param_obj.clone_ref(py))) + } else { + None + } + } + _ => None, + }) + .collect(); + if !params.is_empty() { + let builtins = PyModule::import_bound(py, "builtins")?; + let list_builtin = builtins.getattr("list")?; + + 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 atomic_parameters: HashMap = raw_param_objs + .into_iter() + .map(|x| { + ( + x.getattr(py, intern!(py, "_uuid")) + .expect("Not a parameter") + .getattr(py, intern!(py, "int")) + .expect("Not a uuid") + .extract(py) + .unwrap(), + x, + ) + }) + .collect(); + for (param_uuid, param_obj) in atomic_parameters.into_iter() { + match self.param_table.table.get_mut(¶m_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, new_entry)?; + } + }; + } + } + } + } + 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 builtins = PyModule::import_bound(py, "builtins")?; + let list_builtin = builtins.getattr(intern!(py, "list"))?; + if inst_index == usize::MAX { + 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 let Some(raw_params) = &self.data[inst_index].params { + let params: Vec<(usize, PyObject)> = raw_params + .iter() + .enumerate() + .filter_map(|(idx, x)| match x { + Param::ParameterExpression(param_obj) => { + let param_mod = + PyModule::import_bound(py, "qiskit.circuit.parameterexpression") + .ok()?; + let param_class = + param_mod.getattr(intern!(py, "ParameterExpression")).ok()?; + if param_obj + .clone_ref(py) + .into_bound(py) + .is_instance(¶m_class) + .unwrap() + { + Some((idx, param_obj.clone_ref(py))) + } else { + None + } + } + _ => 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(()) + } } #[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(), @@ -174,7 +448,10 @@ impl CircuitData { clbit_indices_native: HashMap::new(), qubits: PyList::empty_bound(py).unbind(), clbits: PyList::empty_bound(py).unbind(), + 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)?; @@ -322,9 +599,11 @@ impl CircuitData { Some(self.clbits.bind(py)), None, 0, + self.global_phase.clone(), )?; res.intern_context = self.intern_context.clone(); res.data.clone_from(&self.data); + res.param_table.clone_from(&self.param_table); Ok(res) } @@ -366,7 +645,16 @@ impl CircuitData { #[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 op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + &inst.label, + &inst.duration, + &inst.unit, + &inst.condition, + )?; + func.call1((op,))?; } Ok(()) } @@ -380,7 +668,16 @@ impl CircuitData { #[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 op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + &inst.label, + &inst.duration, + &inst.unit, + &inst.condition, + )?; + func.call1((index, op))?; } Ok(()) } @@ -395,7 +692,23 @@ impl CircuitData { #[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 = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + &inst.label, + &inst.duration, + &inst.unit, + &inst.condition, + )?; + let new_op = func.call1((old_op,))?; + let new_inst_details = convert_py_to_operation_type(py, new_op.into())?; + inst.op = new_inst_details.operation; + inst.params = new_inst_details.params; + inst.label = new_inst_details.label; + inst.duration = new_inst_details.duration; + inst.unit = new_inst_details.unit; + inst.condition = new_inst_details.condition; } Ok(()) } @@ -458,7 +771,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.qubits_native.len() < self.qubits_native.len() { return Err(PyValueError::new_err(format!( @@ -528,7 +841,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 = { @@ -541,14 +854,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!( @@ -560,6 +883,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.extract()?; + let mut packed = self.pack(py, value)?; + std::mem::swap(&mut packed, &mut self.data[index]); + Ok(()) + } + pub fn __setitem__( &mut self, py: Python<'_>, @@ -593,7 +929,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() { @@ -608,7 +944,9 @@ impl CircuitData { let index = self.convert_py_index(index)?; let value: PyRef = value.extract()?; let mut packed = self.pack(py, value)?; + self.remove_from_parameter_table(py, index)?; std::mem::swap(&mut packed, &mut self.data[index]); + self.update_param_table(py, index, None)?; Ok(()) } } @@ -621,8 +959,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(()) } @@ -630,14 +974,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<()> { + pub fn append( + &mut self, + py: Python<'_>, + value: PyRef, + _params: Option)>>, + ) -> PyResult { let packed = self.pack(py, value)?; + 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<()> { @@ -664,24 +1015,30 @@ impl CircuitData { [&BitAsKey::new(other.clbits_native[*b as usize].bind(py))?]) }) .collect::>>()?; - + let new_index = self.data.len(); self.data.push(PackedInstruction { - op: inst.op.clone_ref(py), + op: inst.op.clone(), qubits_id: self.intern_context.intern(qubits)?, clbits_id: self.intern_context.intern(clbits)?, + params: inst.params.clone(), + label: inst.label.clone(), + duration: inst.duration.clone(), + unit: inst.unit.clone(), + condition: inst.condition.clone(), }); + self.update_param_table(py, new_index, None)?; } return Ok(()); } - for v in itr.iter()? { - self.append(py, v?.extract()?)?; + self.append(py, v?.extract()?, None)?; } Ok(()) } pub fn clear(&mut self, _py: Python<'_>) -> PyResult<()> { std::mem::take(&mut self.data); + self.param_table.clear(); Ok(()) } @@ -720,7 +1077,7 @@ impl CircuitData { fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { for packed in self.data.iter() { - visit.call(&packed.op)?; + visit.call(&packed.duration)?; } for bit in self.qubits_native.iter().chain(self.clbits_native.iter()) { visit.call(bit)?; @@ -743,6 +1100,127 @@ impl CircuitData { self.qubit_indices_native.clear(); self.clbit_indices_native.clear(); } + + #[setter] + pub fn global_phase(&mut self, py: Python, angle: Param) -> PyResult<()> { + let builtins = PyModule::import_bound(py, "builtins")?; + let list_builtin = builtins.getattr(intern!(py, "list"))?; + self.remove_from_parameter_table(py, usize::MAX)?; + match angle { + Param::Float(angle) => { + self.global_phase = Param::Float(angle.rem_euclid(2. * std::f64::consts::PI)); + } + Param::ParameterExpression(angle) => { + // usize::MAX is the global phase sentinel value for the inst index + let inst_index = usize::MAX; + 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(inst_index, param_index), + None => { + let new_entry = ParamEntry::new(inst_index, param_index); + self.param_table.insert(py, param_obj, new_entry)?; + } + }; + } + self.global_phase = Param::ParameterExpression(angle); + } + }; + Ok(()) + } + + /// Get the global_phase sentinel value + #[staticmethod] + pub fn global_phase_param_index() -> usize { + usize::MAX + } + + // 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) -> Vec { + self.param_table + .uuid_map + .values() + .map(|x| x.clone_ref(py)) + .collect() + } + + 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()) + } } impl CircuitData { @@ -820,9 +1298,43 @@ impl CircuitData { self.intern_context.intern(args) }; Ok(PackedInstruction { - op: inst.operation.clone_ref(py), + op: inst.operation.clone(), + qubits_id: interned_bits(&self.qubit_indices_native, inst.qubits.bind(py))?, + clbits_id: interned_bits(&self.clbit_indices_native, inst.clbits.bind(py))?, + params: inst.params.clone(), + label: inst.label.clone(), + duration: inst.duration.clone(), + unit: inst.unit.clone(), + condition: inst.condition.clone(), + }) + } + + fn pack_owned(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult { + let mut interned_bits = + |indices: &HashMap, bits: &Bound| -> PyResult { + let args = bits + .into_iter() + .map(|b| { + let key = BitAsKey::new(&b)?; + indices.get(&key).copied().ok_or_else(|| { + PyKeyError::new_err(format!( + "Bit {:?} has not been added to this circuit.", + b + )) + }) + }) + .collect::>>()?; + self.intern_context.intern(args) + }; + Ok(PackedInstruction { + op: inst.operation.clone(), qubits_id: interned_bits(&self.qubit_indices_native, inst.qubits.bind(py))?, clbits_id: interned_bits(&self.clbit_indices_native, inst.clbits.bind(py))?, + params: inst.params.clone(), + label: inst.label.clone(), + duration: inst.duration.clone(), + unit: inst.unit.clone(), + condition: inst.condition.clone(), }) } @@ -830,7 +1342,7 @@ impl CircuitData { Py::new( py, CircuitInstruction { - operation: inst.op.clone_ref(py), + operation: inst.op.clone(), qubits: PyTuple::new_bound( py, self.intern_context @@ -849,6 +1361,11 @@ impl CircuitData { .collect::>(), ) .unbind(), + params: inst.params.clone(), + label: inst.label.clone(), + duration: inst.duration.clone(), + unit: inst.unit.clone(), + condition: inst.condition.clone(), }, ) } diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 86bd2e69c111..11d80e967bec 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -10,10 +10,26 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use lazy_static::lazy_static; + +use hashbrown::HashMap; 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; +use std::sync::Mutex; + +use crate::operations::{ + OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate, STANDARD_GATE_SIZE, +}; + +// TODO Come up with a better cacheing mechanism for this +lazy_static! { + static ref STANDARD_GATE_MAP: Mutex> = + Mutex::new(HashMap::with_capacity(STANDARD_GATE_SIZE)); +} /// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and /// various operands. @@ -47,30 +63,52 @@ 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: Option>, + pub label: Option, + pub duration: Option, + pub unit: Option, + pub condition: 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), } #[pymethods] impl CircuitInstruction { + #[allow(clippy::too_many_arguments)] #[new] pub fn new( py: Python<'_>, - operation: PyObject, + operation: OperationInput, qubits: Option<&Bound>, clbits: Option<&Bound>, + params: Option>, + label: Option, + duration: Option, + unit: Option, + condition: Option, ) -> PyResult { fn as_tuple(py: Python<'_>, seq: Option<&Bound>) -> PyResult> { match seq { @@ -95,11 +133,117 @@ impl CircuitInstruction { } } - Ok(CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - }) + match operation { + OperationInput::Standard(operation) => { + let operation = OperationType::Standard(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + label, + duration, + unit, + condition, + }) + } + OperationInput::Gate(operation) => { + let operation = OperationType::Gate(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + label, + duration, + unit, + condition, + }) + } + OperationInput::Instruction(operation) => { + let operation = OperationType::Instruction(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + label, + duration, + unit, + condition, + }) + } + OperationInput::Operation(operation) => { + let operation = OperationType::Operation(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params, + label, + duration, + unit, + condition, + }) + } + OperationInput::Object(op) => { + let op = convert_py_to_operation_type(py, op)?; + 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, + label: op.label, + duration: op.duration, + unit: op.unit, + condition: op.condition, + }) + } + OperationType::Gate(operation) => { + let operation = OperationType::Gate(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + label: op.label, + duration: op.duration, + unit: op.unit, + condition: op.condition, + }) + } + OperationType::Instruction(operation) => { + let operation = OperationType::Instruction(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + label: op.label, + duration: op.duration, + unit: op.unit, + condition: op.condition, + }) + } + OperationType::Operation(operation) => { + let operation = OperationType::Operation(operation); + Ok(CircuitInstruction { + operation, + qubits: as_tuple(py, qubits)?, + clbits: as_tuple(py, clbits)?, + params: op.params, + label: op.label, + duration: op.duration, + unit: op.unit, + condition: op.condition, + }) + } + } + } + } } /// Returns a shallow copy. @@ -110,28 +254,101 @@ impl CircuitInstruction { self.clone() } + /// The logical operation that this instruction represents an execution of. + #[getter] + pub fn operation(&self, py: Python) -> PyResult { + operation_type_to_py(py, self) + } + /// 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>, + params: Option>, + label: Option, + duration: Option, + unit: Option, + condition: Option, ) -> PyResult { + let operation = match operation { + Some(operation) => operation, + None => match &self.operation { + OperationType::Standard(op) => OperationInput::Standard(*op), + OperationType::Gate(gate) => OperationInput::Gate(gate.clone()), + OperationType::Instruction(inst) => OperationInput::Instruction(inst.clone()), + OperationType::Operation(op) => OperationInput::Operation(op.clone()), + }, + }; + + let params = match params { + Some(params) => Some(params), + None => self.params.clone(), + }; + + let label = match label { + Some(label) => Some(label), + None => self.label.clone(), + }; + let duration = match duration { + Some(duration) => Some(duration), + None => self.duration.clone(), + }; + + let unit: Option = match unit { + Some(unit) => Some(unit), + None => self.unit.clone(), + }; + + let condition: Option = match condition { + Some(condition) => Some(condition), + None => self.condition.clone(), + }; + CircuitInstruction::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 __getstate__(&self, py: Python<'_>) -> PyResult { + Ok(( + 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()?; + self.label = op.label; + self.duration = op.duration; + self.unit = op.unit; + self.condition = op.condition; + Ok(()) } - fn __getnewargs__(&self, py: Python<'_>) -> PyResult { + pub fn __getnewargs__(&self, py: Python<'_>) -> PyResult { Ok(( - self.operation.bind(py), + operation_type_to_py(py, self)?, self.qubits.bind(py), self.clbits.bind(py), ) @@ -148,7 +365,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()? )) @@ -160,23 +377,20 @@ 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( + 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)], + )) } 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)) } 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)) } pub fn __len__(&self) -> usize { @@ -203,16 +417,85 @@ 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 if let Some(self_params) = &self_.params { + if v.params.is_none() { + return Ok(Some(false)); + } + let other_params = v.params.as_ref().unwrap(); + 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; + } + } + } + } + out + } else { + v.params.is_none() + } + } 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)?)); + let legacy_format = self_._legacy_format(py)?; + return Ok(Some(legacy_format.eq(other)?)); } Ok(None) @@ -231,3 +514,341 @@ 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 { + operation_type_and_data_to_py( + py, + &circuit_inst.operation, + &circuit_inst.params, + &circuit_inst.label, + &circuit_inst.duration, + &circuit_inst.unit, + &circuit_inst.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: &Option>, + label: &Option, + duration: &Option, + unit: &Option, + condition: &Option, +) -> PyResult { + match &operation { + OperationType::Standard(op) => { + let gate_class: &PyObject = &STANDARD_GATE_MAP.lock().unwrap()[op]; + + let args = if let Some(params) = ¶ms { + if params.is_empty() { + PyTuple::empty_bound(py) + } else { + PyTuple::new_bound(py, params) + } + } 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: Option>, + 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() { + Some(stdgate) => match stdgate.extract().ok() { + Some(gate) => gate, + None => None, + }, + None => 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 { + let singleton_mod = py.import_bound("qiskit.circuit.singleton")?; + let singleton_class = singleton_mod.getattr(intern!(py, "SingletonGate"))?; + let singleton_control = + singleton_mod.getattr(intern!(py, "SingletonControlledGate"))?; + if py_op_bound.is_instance(&singleton_class)? + || py_op_bound.is_instance(&singleton_control)? + { + standard = None; + } + } + } + if let Some(op) = standard { + let base_class = op_type.to_object(py); + STANDARD_GATE_MAP + .lock() + .unwrap() + .entry(op) + .or_insert(base_class); + return Ok(OperationTypeConstruct { + operation: OperationType::Standard(op), + params: py_op + .getattr(py, intern!(py, "params")) + .ok() + .unwrap() + .extract(py)?, + label: py_op + .getattr(py, intern!(py, "label")) + .ok() + .unwrap() + .extract(py)?, + duration: py_op + .getattr(py, intern!(py, "duration")) + .ok() + .unwrap() + .extract(py)?, + unit: py_op + .getattr(py, intern!(py, "unit")) + .ok() + .unwrap() + .extract(py)?, + condition: py_op + .getattr(py, intern!(py, "condition")) + .ok() + .unwrap() + .extract(py)?, + }); + } + let gate_class = py + .import_bound("qiskit.circuit.gate")? + .getattr(intern!(py, "Gate")) + .ok() + .unwrap(); + if op_type.is_subclass(&gate_class)? { + let params = py_op + .getattr(py, intern!(py, "params")) + .ok() + .unwrap() + .extract(py)?; + let label = py_op + .getattr(py, intern!(py, "label")) + .ok() + .unwrap() + .extract(py)?; + let duration = py_op + .getattr(py, intern!(py, "duration")) + .ok() + .unwrap() + .extract(py)?; + let unit = py_op + .getattr(py, intern!(py, "unit")) + .ok() + .unwrap() + .extract(py)?; + let condition = py_op + .getattr(py, intern!(py, "condition")) + .ok() + .unwrap() + .extract(py)?; + + let out_op = PyGate { + qubits: py_op + .getattr(py, intern!(py, "num_qubits")) + .ok() + .map(|x| x.extract(py).unwrap()) + .unwrap_or(0), + clbits: py_op + .getattr(py, intern!(py, "num_clbits")) + .ok() + .map(|x| x.extract(py).unwrap()) + .unwrap_or(0), + params: py_op + .getattr(py, intern!(py, "params")) + .ok() + .unwrap() + .downcast_bound::(py)? + .len() as u32, + op_name: py_op + .getattr(py, intern!(py, "name")) + .ok() + .unwrap() + .extract(py)?, + gate: py_op, + }; + return Ok(OperationTypeConstruct { + operation: OperationType::Gate(out_op), + params, + label, + duration, + unit, + condition, + }); + } + let instruction_class = py + .import_bound("qiskit.circuit.instruction")? + .getattr(intern!(py, "Instruction")) + .ok() + .unwrap(); + if op_type.is_subclass(&instruction_class)? { + let params = py_op + .getattr(py, intern!(py, "params")) + .ok() + .unwrap() + .extract(py)?; + let label = py_op + .getattr(py, intern!(py, "label")) + .ok() + .unwrap() + .extract(py)?; + let duration = py_op + .getattr(py, intern!(py, "duration")) + .ok() + .unwrap() + .extract(py)?; + let unit = py_op + .getattr(py, intern!(py, "unit")) + .ok() + .unwrap() + .extract(py)?; + let condition = py_op + .getattr(py, intern!(py, "condition")) + .ok() + .unwrap() + .extract(py)?; + + let out_op = PyInstruction { + qubits: py_op + .getattr(py, intern!(py, "num_qubits")) + .ok() + .map(|x| x.extract(py).unwrap()) + .unwrap_or(0), + clbits: py_op + .getattr(py, intern!(py, "num_clbits")) + .ok() + .map(|x| x.extract(py).unwrap()) + .unwrap_or(0), + params: py_op + .getattr(py, intern!(py, "params")) + .ok() + .unwrap() + .downcast_bound::(py)? + .len() as u32, + op_name: py_op + .getattr(py, intern!(py, "name")) + .ok() + .unwrap() + .extract(py)?, + instruction: py_op, + }; + return Ok(OperationTypeConstruct { + operation: OperationType::Instruction(out_op), + params, + label, + duration, + unit, + condition, + }); + } + + let operation_class = py + .import_bound("qiskit.circuit.operation")? + .getattr(intern!(py, "Operation")) + .ok() + .unwrap(); + if op_type.is_subclass(&operation_class)? { + let params = match py_op.getattr(py, intern!(py, "params")).ok() { + Some(value) => value.extract(py)?, + None => None, + }; + 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")) + .ok() + .map(|x| x.extract(py).unwrap()) + .unwrap_or(0), + clbits: py_op + .getattr(py, intern!(py, "num_clbits")) + .ok() + .map(|x| x.extract(py).unwrap()) + .unwrap_or(0), + params: match py_op.getattr(py, intern!(py, "params")).ok() { + Some(value) => value.downcast_bound::(py)?.len() as u32, + None => 0, + }, + op_name: py_op + .getattr(py, intern!(py, "name")) + .ok() + .unwrap() + .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 c766461bb510..31ca70b605d3 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -10,7 +10,10 @@ // 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, +}; +use crate::operations::Operation; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; use pyo3::{intern, PyObject, PyResult}; @@ -106,13 +109,19 @@ impl DAGOpNode { } None => qargs.str()?.into_any(), }; + let res = convert_py_to_operation_type(py, op)?; Ok(( DAGOpNode { instruction: CircuitInstruction { - operation: op, + operation: res.operation, qubits: qargs.unbind(), clbits: cargs.unbind(), + params: res.params, + label: res.label, + duration: res.duration, + unit: res.unit, + condition: res.condition, }, sort_key: sort_key.unbind(), }, @@ -120,18 +129,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 +151,20 @@ 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; + self.instruction.label = res.label; + self.instruction.duration = res.duration; + self.instruction.unit = res.unit; + self.instruction.condition = res.condition; + Ok(()) } #[getter] @@ -173,29 +189,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 000000000000..4f0750729dcd --- /dev/null +++ b/crates/circuit/src/gate_matrix.rs @@ -0,0 +1,330 @@ +// 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. + +// In numpy matrices real and imaginary components are adjacent: +// np.array([1,2,3], dtype='complex').view('float64') +// array([1., 0., 2., 0., 3., 0.]) +// The matrix faer::Mat has this layout. +// faer::Mat> instead stores a matrix +// of real components and one of imaginary components. +// In order to avoid copying we want to use `MatRef` or `MatMut`. + +use num_complex::Complex64; +use std::f64::consts::FRAC_1_SQRT_2; + +pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [ + [Complex64::new(1., 0.), Complex64::new(0., 0.)], + [Complex64::new(0., 0.), Complex64::new(1., 0.)], +]; + +#[inline] +pub fn rx_gate(theta: f64) -> [[Complex64; 2]; 2] { + let half_theta = theta / 2.; + let cos = Complex64::new(half_theta.cos(), 0.); + let isin = Complex64::new(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 = Complex64::new(half_theta.cos(), 0.); + let sin = Complex64::new(half_theta.sin(), 0.); + [[cos, -sin], [sin, cos]] +} + +#[inline] +pub fn rz_gate(theta: f64) -> [[Complex64; 2]; 2] { + let ilam2 = Complex64::new(0., 0.5 * theta); + [ + [(-ilam2).exp(), Complex64::new(0., 0.)], + [Complex64::new(0., 0.), ilam2.exp()], + ] +} + +pub 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.), + ], +]; + +pub 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.), + ], +]; + +pub 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)], +]; + +pub static XGATE: [[Complex64; 2]; 2] = [ + [Complex64::new(0., 0.), Complex64::new(1., 0.)], + [Complex64::new(1., 0.), Complex64::new(0., 0.)], +]; + +pub static ZGATE: [[Complex64; 2]; 2] = [ + [Complex64::new(1., 0.), Complex64::new(0., 0.)], + [Complex64::new(0., 0.), Complex64::new(-1., 0.)], +]; + +pub static YGATE: [[Complex64; 2]; 2] = [ + [Complex64::new(0., 0.), Complex64::new(0., -1.)], + [Complex64::new(0., 1.), Complex64::new(0., 0.)], +]; + +pub static CZGATE: [[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(1., 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(0., 0.), + Complex64::new(0., 0.), + Complex64::new(-1., 0.), + ], +]; + +pub static CYGATE: [[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(0., -1.), + ], + [ + Complex64::new(0., 0.), + Complex64::new(0., 0.), + Complex64::new(1., 0.), + Complex64::new(0., 0.), + ], + [ + Complex64::new(0., 0.), + Complex64::new(0., 1.), + Complex64::new(0., 0.), + Complex64::new(0., 0.), + ], +]; + +pub static CCXGATE: [[Complex64; 8]; 8] = [ + [ + 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(0., 0.), + ], + [ + Complex64::new(0., 0.), + 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(0., 0.), + Complex64::new(0., 0.), + 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(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(0., 0.), + Complex64::new(0., 0.), + 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(0., 0.), + Complex64::new(0., 0.), + 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(0., 0.), + Complex64::new(0., 0.), + Complex64::new(1., 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(0., 0.), + Complex64::new(0., 0.), + ], +]; + +pub static ECRGATE: [[Complex64; 4]; 4] = [ + [ + Complex64::new(0., 0.), + Complex64::new(FRAC_1_SQRT_2, 0.), + Complex64::new(0., 0.), + Complex64::new(0., FRAC_1_SQRT_2), + ], + [ + Complex64::new(FRAC_1_SQRT_2, 0.), + Complex64::new(0., 0.), + Complex64::new(0., -FRAC_1_SQRT_2), + Complex64::new(0., 0.), + ], + [ + Complex64::new(0., 0.), + Complex64::new(0., FRAC_1_SQRT_2), + Complex64::new(0., 0.), + Complex64::new(FRAC_1_SQRT_2, 0.), + ], + [ + Complex64::new(0., -FRAC_1_SQRT_2), + Complex64::new(0., 0.), + Complex64::new(FRAC_1_SQRT_2, 0.), + Complex64::new(0., 0.), + ], +]; + +pub static SWAPGATE: [[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(1., 0.), + Complex64::new(0., 0.), + ], + [ + Complex64::new(0., 0.), + 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(1., 0.), + ], +]; + +#[inline] +pub fn global_phase_gate(theta: f64) -> [[Complex64; 1]; 1] { + [[Complex64::new(0., theta).exp()]] +} + +#[inline] +pub fn phase_gate(lam: f64) -> [[Complex64; 2]; 2] { + [ + [Complex64::new(1., 0.), Complex64::new(0., 0.)], + [Complex64::new(0., 0.), Complex64::new(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(); + [ + [ + Complex64::new(cos, 0.), + (-Complex64::new(0., lam).exp()) * sin, + ], + [ + Complex64::new(0., phi).exp() * sin, + Complex64::new(0., phi + lam).exp() * cos, + ], + ] +} diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index c186c4243e93..a6364c43cacd 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -13,7 +13,10 @@ pub mod circuit_data; pub mod circuit_instruction; pub mod dag_node; +pub mod gate_matrix; pub mod intern_context; +pub mod operations; +pub mod parameter_table; use pyo3::prelude::*; use pyo3::types::PySlice; @@ -36,5 +39,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 000000000000..6a2817e65c27 --- /dev/null +++ b/crates/circuit/src/operations.rs @@ -0,0 +1,726 @@ +// 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::gate_matrix; +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 OperationType +#[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: Option>) -> 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: Option>) -> 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(), + } + } +} + +/// 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: Option>) -> Option>; + fn definition(&self, params: Option>) -> Option; + fn standard_gate(&self) -> Option; +} + +#[derive(FromPyObject, Clone, Debug)] +pub enum Param { + Float(f64), + ParameterExpression(PyObject), +} + +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), + } + } +} + +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), + } + } +} + +#[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] +#[pyclass] +pub enum StandardGate { + // Pauli Gates + ZGate, + YGate, + XGate, + // Controlled Pauli Gates + CZGate, + CYGate, + CXGate, + CCXGate, + RXGate, + RYGate, + RZGate, + ECRGate, + SwapGate, + SXGate, + GlobalPhaseGate, + IGate, + HGate, + PhaseGate, + UGate, +} + +#[pymethods] +impl StandardGate { + pub fn copy(&self) -> Self { + *self + } + + // These pymethods are for testing: + pub fn _to_matrix(&self, py: Python, params: Option>) -> Option { + self.matrix(params).map(|x| x.into_pyarray_bound(py).into()) + } + + pub fn _num_params(&self) -> u32 { + self.num_params() + } + + pub fn _get_definition(&self, params: Option>) -> Option { + self.definition(params) + } +} + +// 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 = 17; + +impl Operation for StandardGate { + fn name(&self) -> &str { + match self { + Self::ZGate => "z", + Self::YGate => "y", + Self::XGate => "x", + Self::CZGate => "cz", + Self::CYGate => "cy", + Self::CXGate => "cx", + Self::CCXGate => "ccx", + Self::RXGate => "rx", + Self::RYGate => "ry", + Self::RZGate => "rz", + Self::ECRGate => "ecr", + Self::SwapGate => "swap", + Self::SXGate => "sx", + Self::GlobalPhaseGate => "global_phase", + Self::IGate => "id", + Self::HGate => "h", + Self::PhaseGate => "p", + Self::UGate => "u", + } + } + + fn num_qubits(&self) -> u32 { + match self { + Self::ZGate => 1, + Self::YGate => 1, + Self::XGate => 1, + Self::CZGate => 2, + Self::CYGate => 2, + Self::CXGate => 2, + Self::CCXGate => 3, + Self::RXGate => 1, + Self::RYGate => 1, + Self::RZGate => 1, + Self::ECRGate => 2, + Self::SwapGate => 2, + Self::SXGate => 1, + Self::GlobalPhaseGate => 0, + Self::IGate => 1, + Self::HGate => 1, + Self::PhaseGate => 1, + Self::UGate => 1, + } + } + + fn num_params(&self) -> u32 { + match self { + Self::ZGate => 0, + Self::YGate => 0, + Self::XGate => 0, + Self::CZGate => 0, + Self::CYGate => 0, + Self::CXGate => 0, + Self::CCXGate => 0, + Self::RXGate => 1, + Self::RYGate => 1, + Self::RZGate => 1, + Self::ECRGate => 0, + Self::SwapGate => 0, + Self::SXGate => 0, + Self::GlobalPhaseGate => 1, + Self::IGate => 0, + Self::HGate => 0, + Self::PhaseGate => 1, + Self::UGate => 3, + } + } + + fn num_clbits(&self) -> u32 { + 0 + } + + fn control_flow(&self) -> bool { + false + } + + fn matrix(&self, params: Option>) -> Option> { + match self { + Self::ZGate => Some(aview2(&gate_matrix::ZGATE).to_owned()), + Self::YGate => Some(aview2(&gate_matrix::YGATE).to_owned()), + Self::XGate => Some(aview2(&gate_matrix::XGATE).to_owned()), + Self::CZGate => Some(aview2(&gate_matrix::CZGATE).to_owned()), + Self::CYGate => Some(aview2(&gate_matrix::CYGATE).to_owned()), + Self::CXGate => Some(aview2(&gate_matrix::CXGATE).to_owned()), + Self::CCXGate => Some(aview2(&gate_matrix::CCXGATE).to_owned()), + Self::RXGate => { + let theta = ¶ms.unwrap()[0]; + match theta { + Param::Float(theta) => Some(aview2(&gate_matrix::rx_gate(*theta)).to_owned()), + _ => None, + } + } + Self::RYGate => { + let theta = ¶ms.unwrap()[0]; + match theta { + Param::Float(theta) => Some(aview2(&gate_matrix::ry_gate(*theta)).to_owned()), + _ => None, + } + } + Self::RZGate => { + let theta = ¶ms.unwrap()[0]; + match theta { + Param::Float(theta) => Some(aview2(&gate_matrix::rz_gate(*theta)).to_owned()), + _ => None, + } + } + Self::ECRGate => Some(aview2(&gate_matrix::ECRGATE).to_owned()), + Self::SwapGate => Some(aview2(&gate_matrix::SWAPGATE).to_owned()), + Self::SXGate => Some(aview2(&gate_matrix::SXGATE).to_owned()), + Self::GlobalPhaseGate => { + let theta = ¶ms.unwrap()[0]; + match theta { + Param::Float(theta) => { + Some(aview2(&gate_matrix::global_phase_gate(*theta)).to_owned()) + } + _ => None, + } + } + Self::IGate => Some(aview2(&gate_matrix::ONE_QUBIT_IDENTITY).to_owned()), + Self::HGate => Some(aview2(&gate_matrix::HGATE).to_owned()), + Self::PhaseGate => { + let theta = ¶ms.unwrap()[0]; + match theta { + Param::Float(theta) => { + Some(aview2(&gate_matrix::phase_gate(*theta)).to_owned()) + } + _ => None, + } + } + Self::UGate => { + let params = params.unwrap(); + let theta: Option = match params[0] { + Param::Float(val) => Some(val), + Param::ParameterExpression(_) => None, + }; + let phi: Option = match params[1] { + Param::Float(val) => Some(val), + Param::ParameterExpression(_) => None, + }; + let lam: Option = match params[2] { + Param::Float(val) => Some(val), + Param::ParameterExpression(_) => None, + }; + // If let chains as needed here are unstable ignore clippy to + // workaround. Upstream rust tracking issue: + // https://github.com/rust-lang/rust/issues/53667 + #[allow(clippy::unnecessary_unwrap)] + if theta.is_none() || phi.is_none() || lam.is_none() { + None + } else { + Some( + aview2(&gate_matrix::u_gate( + theta.unwrap(), + phi.unwrap(), + lam.unwrap(), + )) + .to_owned(), + ) + } + } + } + } + + fn definition(&self, params: Option>) -> Option { + // TODO: Add definition for completeness. This shouldn't be necessary in practice + // though because nothing will rely on this in practice. + match self { + Self::ZGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::build_new_from( + py, + 1, + 0, + &[( + OperationType::Standard(Self::PhaseGate), + Some(&[Param::Float(PI)]), + &[0], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::YGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::build_new_from( + py, + 1, + 0, + &[( + OperationType::Standard(Self::UGate), + Some(&[ + Param::Float(PI), + Param::Float(PI / 2.), + Param::Float(PI / 2.), + ]), + &[0], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::XGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::build_new_from( + py, + 1, + 0, + &[( + OperationType::Standard(Self::UGate), + Some(&[Param::Float(PI), Param::Float(0.), Param::Float(PI)]), + &[0], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::CZGate => Python::with_gil(|py| -> Option { + let q1: Vec = vec![1]; + let q0_1: Vec = vec![0, 1]; + Some( + CircuitData::build_new_from( + py, + 2, + 0, + &[ + (OperationType::Standard(Self::HGate), None, &q1), + (OperationType::Standard(Self::CXGate), None, &q0_1), + (OperationType::Standard(Self::HGate), None, &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 { + let params = params.unwrap(); + match ¶ms[0] { + Param::Float(theta) => Some( + CircuitData::build_new_from( + py, + 1, + 0, + &[( + OperationType::Standard(Self::PhaseGate), + Some(&[Param::Float(*theta)]), + &[0], + )], + Param::Float(-0.5 * theta), + ) + .expect("Unexpected Qiskit python bug"), + ), + Param::ParameterExpression(theta) => Some( + CircuitData::build_new_from( + py, + 1, + 0, + &[( + OperationType::Standard(Self::PhaseGate), + Some(&[Param::ParameterExpression(theta.clone_ref(py))]), + &[0], + )], + Param::ParameterExpression( + theta + .call_method1(py, intern!(py, "__rmul__"), (-0.5,)) + .expect("Parameter expression for global phase failed"), + ), + ) + .expect("Unexpected Qiskit python bug"), + ), + } + }), + Self::ECRGate => todo!("Add when we have RZX"), + Self::SwapGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::build_new_from( + py, + 2, + 0, + &[ + (OperationType::Standard(Self::CXGate), None, &[0, 1]), + (OperationType::Standard(Self::CXGate), None, &[1, 0]), + (OperationType::Standard(Self::CXGate), None, &[0, 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::build_new_from(py, 0, 0, &[], params.unwrap()[0].clone()) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::IGate => None, + Self::HGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::build_new_from( + py, + 1, + 0, + &[( + OperationType::Standard(Self::UGate), + Some(&[Param::Float(PI / 2.), Param::Float(0.), Param::Float(PI)]), + &[0], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::PhaseGate => Python::with_gil(|py| -> Option { + Some( + CircuitData::build_new_from( + py, + 1, + 0, + &[( + OperationType::Standard(Self::UGate), + Some(&[ + Param::Float(0.), + Param::Float(0.), + params.unwrap()[0].clone(), + ]), + &[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] +pub struct PyInstruction { + pub qubits: u32, + pub clbits: u32, + pub params: u32, + pub op_name: String, + pub instruction: PyObject, +} + +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: Option>) -> Option> { + None + } + fn definition(&self, _params: Option>) -> 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 + } +} + +/// This class is used to wrap a Python side Gate that is not in the standard library +#[derive(Clone, Debug)] +#[pyclass] +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: Option>) -> 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: Option>) -> 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, + } + }) + } +} + +/// This class is used to wrap a Python side Operation that is not in the standard library +#[derive(Clone, Debug)] +#[pyclass] +pub struct PyOperation { + pub qubits: u32, + pub clbits: u32, + pub params: u32, + pub op_name: String, + pub operation: PyObject, +} + +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: Option>) -> Option> { + None + } + fn definition(&self, _params: Option>) -> Option { + None + } + fn standard_gate(&self) -> Option { + None + } +} diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs new file mode 100644 index 000000000000..21b6db93d242 --- /dev/null +++ b/crates/circuit/src/parameter_table.rs @@ -0,0 +1,189 @@ +// 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::exceptions::PyIndexError; +use pyo3::prelude::*; +use pyo3::{import_exception, intern, PyObject}; + +import_exception!(qiskit.circuit.exceptions, CircuitError); + +use hashbrown::{HashMap, HashSet}; + +#[pyclass] +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] +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(mapping)] +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(), + } + } + + fn __len__(&self) -> usize { + self.table.len() + } + + pub fn clear(&mut self) { + self.table.clear(); + self.names.clear(); + self.uuid_map.clear(); + } + + fn __contains__(&self, key: u128) -> bool { + self.table.contains_key(&key) + } + + fn __getitem__(&self, key: u128) -> PyResult { + match self.table.get(&key) { + Some(res) => Ok(res.clone()), + None => Err(PyIndexError::new_err(format!( + "No param uuid entry {:?}", + key + ))), + } + } + + 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/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index e339cb8d94bb..44155783d409 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 ac3d9fabd64b..576d5dee8267 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 2bbd5ca5650a..eb6e546c2a87 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 diff --git a/qiskit/circuit/library/standard_gates/ecr.py b/qiskit/circuit/library/standard_gates/ecr.py index 73bb1bb03898..f00c02df538d 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 ccd758e47241..59d6b56373da 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 cc06a071a3f6..2d273eed74d5 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 93523215d6f0..13a98ce0df8a 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 6de0307dc798..1a792649feab 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 eaa73cf87c91..5579f9d3707d 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 633a518bca77..e27398cc2960 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 3040f9568346..e8ee0f976036 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 0e49783308c4..243a84701ef5 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 0c003748a660..93ca85da0198 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 3d631898850a..3495bc180f08 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 7195df90dc98..6e959b3e62cb 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 e69e1e2b794b..d62586aa2b9b 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 2b69595936d8..19e4382cd846 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 6803126ec107..51ed3aee129b 100644 --- a/qiskit/circuit/parametertable.py +++ b/qiskit/circuit/parametertable.py @@ -17,194 +17,6 @@ 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)})" - - class ParameterView(MappingView): """Temporary class to transition from a set return-type to list. diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index a157f04375a3..413de04b3cc2 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -57,7 +57,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 +1124,11 @@ 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._data.global_phase: ParameterValueType = 0.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: @@ -2457,38 +2462,36 @@ def _append(self, instruction, qargs=(), cargs=()): old_style = not isinstance(instruction, CircuitInstruction) if old_style: instruction = CircuitInstruction(instruction, qargs, cargs) - self._data.append(instruction) + params = None + # If there is a reference to the outer circuit in an + # instruction param we need to handle the params + # before calling the inner rust append method. This is to avoid trying + # to reference the circuit twice at the same time from rust. 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. + if hasattr(instruction.operation, "params") and any( + x is self for x in instruction.operation.params + ): + 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) + else: + new_param = self._data.append(instruction) + if new_param: + # clear cache if new parameter is added + self._parameters = None + self._track_operation(instruction.operation) return instruction.operation if old_style else instruction 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) 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 - @typing.overload def get_parameter(self, name: str, default: T) -> Union[Parameter, T]: ... @@ -2538,7 +2541,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 @@ -3550,28 +3553,10 @@ 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 + return op.copy() 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( @@ -3650,12 +3635,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) @@ -3675,7 +3657,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 @@ -3959,10 +3940,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 @@ -4035,7 +4015,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): @@ -4046,23 +4026,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(previous := 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: @@ -4132,7 +4107,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. @@ -4145,7 +4120,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 set(self._data.get_params_unsorted()) @overload def assign_parameters( @@ -4294,7 +4269,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 = {} @@ -4305,20 +4280,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): @@ -4330,12 +4313,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. @@ -4382,6 +4371,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( @@ -5921,36 +5911,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, @@ -6602,6 +6565,7 @@ def append(self, instruction): def extend(self, data: CircuitData): self.circuit._data.extend(data) + self.circuit._parameters = None data.foreach_op(self.circuit._track_operation) def resolve_classical_resource(self, specifier): diff --git a/qiskit/circuit/quantumcircuitdata.py b/qiskit/circuit/quantumcircuitdata.py index 3e29f36c6bee..82e5b79486fd 100644 --- a/qiskit/circuit/quantumcircuitdata.py +++ b/qiskit/circuit/quantumcircuitdata.py @@ -20,6 +20,8 @@ from .exceptions import CircuitError from .instruction import Instruction from .operation import Operation +from .instruction import Instruction +from .gate import Gate CircuitInstruction = qiskit._accelerate.circuit.CircuitInstruction @@ -45,8 +47,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 +76,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 2bdcbfef3583..1a5907c3ec84 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 16bc2529ca74..a25d25805220 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -1102,7 +1102,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 4f49879063ec..75b455410f49 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 7cb309dd9aa1..806e001f2bda 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/time_unit_conversion.py b/qiskit/transpiler/passes/scheduling/time_unit_conversion.py index 25672c137f34..08ac932d8aeb 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 000000000000..521d1588a956 --- /dev/null +++ b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml @@ -0,0 +1,70 @@ +--- +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. +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].operations.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/test/python/circuit/library/test_blueprintcircuit.py b/test/python/circuit/library/test_blueprintcircuit.py index 2a5070e8ac74..5f0a2814872f 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/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py index 2e308af48ff4..8a1bd0cd925d 100644 --- a/test/python/circuit/library/test_nlocal.py +++ b/test/python/circuit/library/test_nlocal.py @@ -235,7 +235,7 @@ def test_parameters_setter(self, params): initial_params = ParameterVector("p", length=6) circuit = QuantumCircuit(1) for i, initial_param in enumerate(initial_params): - circuit.ry(i * initial_param, 0) + circuit.ry((i + 1) * initial_param, 0) # create an NLocal from the circuit and set the new parameters nlocal = NLocal(1, entanglement_blocks=circuit, reps=1) diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 73398e4316bb..7ebe38dc763a 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -828,6 +828,11 @@ 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) + param_entry_0 = qc0._data._get_param(b.uuid.int) + param_entry_1 = qc1._data._get_param(a.uuid.int) + 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 6caf194d37d9..e9a7416f78c4 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 db6280b88230..7bb36a1401f8 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 edd01c5cc1ce..4ac69278fd44 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 a09ff331e02a..35ff639cedd5 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 7bcc2cd35f3a..cfa6bfb386ed 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 @@ -614,7 +619,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) @@ -2163,155 +2168,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 000000000000..eb75bba98d36 --- /dev/null +++ b/test/python/circuit/test_rust_equivalence.py @@ -0,0 +1,110 @@ +# 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 + +import numpy as np + +from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping + +from test import QiskitTestCase + + +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 + 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): + print(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) From 37c0780ef1836c0926b9fadd3e0f5854b9514d9b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 25 May 2024 15:04:26 -0400 Subject: [PATCH 02/61] Fix Python->Rust Param conversion This commit adds a custom implementation of the FromPyObject trait for the Param enum. Previously, the Param trait derived it's impl of the trait, but this logic wasn't perfect. In cases whern a ParameterExpression was effectively a constant (such as `0 * x`) the trait's attempt to coerce to a float first would result in those ParameterExpressions being dropped from the circuit at insertion time. This was a change in behavior from before having gates in Rust as the parameters would disappear from the circuit at insertion time instead of at bind time. This commit fixes this by having a custom impl for FromPyObject that first tries to figure out if the parameter is a ParameterExpression (or a QuantumCircuit) by using a Python isinstance() check, then tries to extract it as a float, and finally stores a non-parameter object; which is a new variant in the Param enum. This new variant also lets us simplify the logic around adding gates to the parameter table as we're able to know ahead of time which gate parameters are `ParameterExpression`s and which are other objects (and don't need to be tracked in the parameter table. Additionally this commit tweaks two tests, the first is test.python.circuit.library.test_nlocal.TestNLocal.test_parameters_setter which was adjusted in the previous commit to workaround the bug fixed by this commit. The second is test.python.circuit.test_parameters which was testing that a bound ParameterExpression with a value of 0 defaults to an int which was a side effect of passing an int input to symengine for the bind value and not part of the api and didn't need to be checked. This assertion was removed from the test because the rust representation is only storing f64 values for the numeric parameters and it is never an int after binding from the Python perspective it isn't any different to have float(0) and int(0) unless you explicit isinstance check like the test previously was. --- crates/circuit/src/circuit_data.rs | 43 ++-------------------- crates/circuit/src/circuit_instruction.rs | 11 ++++++ crates/circuit/src/operations.rs | 33 ++++++++++++++++- test/python/circuit/library/test_nlocal.py | 2 +- test/python/circuit/test_parameters.py | 1 - 5 files changed, 46 insertions(+), 44 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 961c25b94432..1f0a84aaa122 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -280,33 +280,11 @@ impl CircuitData { let mut new_param = false; let inst_params = &self.data[inst_index].params; if let Some(raw_params) = inst_params { - let param_mod = - PyModule::import_bound(py, intern!(py, "qiskit.circuit.parameterexpression"))?; - let param_class = param_mod.getattr(intern!(py, "ParameterExpression"))?; - let circuit_mod = - PyModule::import_bound(py, intern!(py, "qiskit.circuit.quantumcircuit"))?; - let circuit_class = circuit_mod.getattr(intern!(py, "QuantumCircuit"))?; let params: Vec<(usize, PyObject)> = raw_params .iter() .enumerate() .filter_map(|(idx, x)| match x { - Param::ParameterExpression(param_obj) => { - if param_obj - .clone_ref(py) - .into_bound(py) - .is_instance(¶m_class) - .unwrap() - || param_obj - .clone_ref(py) - .into_bound(py) - .is_instance(&circuit_class) - .unwrap() - { - Some((idx, param_obj.clone_ref(py))) - } else { - None - } - } + Param::ParameterExpression(param_obj) => Some((idx, param_obj.clone_ref(py))), _ => None, }) .collect(); @@ -370,23 +348,7 @@ impl CircuitData { .iter() .enumerate() .filter_map(|(idx, x)| match x { - Param::ParameterExpression(param_obj) => { - let param_mod = - PyModule::import_bound(py, "qiskit.circuit.parameterexpression") - .ok()?; - let param_class = - param_mod.getattr(intern!(py, "ParameterExpression")).ok()?; - if param_obj - .clone_ref(py) - .into_bound(py) - .is_instance(¶m_class) - .unwrap() - { - Some((idx, param_obj.clone_ref(py))) - } else { - None - } - } + Param::ParameterExpression(param_obj) => Some((idx, param_obj.clone_ref(py))), _ => None, }) .collect(); @@ -1131,6 +1093,7 @@ impl CircuitData { } self.global_phase = Param::ParameterExpression(angle); } + Param::Obj(_) => return Err(PyValueError::new_err("Invalid type for global phase")), }; Ok(()) } diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 11d80e967bec..0844a94dfa48 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -452,6 +452,17 @@ impl CircuitInstruction { 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 diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 6a2817e65c27..6a57c14e1cfc 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -116,10 +116,33 @@ pub trait Operation { fn standard_gate(&self) -> Option; } -#[derive(FromPyObject, Clone, Debug)] +#[derive(Clone, Debug)] pub enum Param { - Float(f64), ParameterExpression(PyObject), + Float(f64), + Obj(PyObject), +} + +impl<'py> FromPyObject<'py> for Param { + fn extract_bound(b: &Bound<'py, PyAny>) -> Result { + let param_mod = PyModule::import_bound( + b.py(), + intern!(b.py(), "qiskit.circuit.parameterexpression"), + )?; + let param_class = param_mod.getattr(intern!(b.py(), "ParameterExpression"))?; + let circuit_mod = + PyModule::import_bound(b.py(), intern!(b.py(), "qiskit.circuit.quantumcircuit"))?; + let circuit_class = circuit_mod.getattr(intern!(b.py(), "QuantumCircuit"))?; + Ok( + if b.is_instance(¶m_class)? || b.is_instance(&circuit_class)? { + 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 { @@ -127,6 +150,7 @@ impl IntoPy for Param { match &self { Self::Float(val) => val.to_object(py), Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), } } } @@ -136,6 +160,7 @@ impl ToPyObject for Param { match self { Self::Float(val) => val.to_object(py), Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), } } } @@ -328,14 +353,17 @@ impl Operation for StandardGate { let theta: Option = match params[0] { Param::Float(val) => Some(val), Param::ParameterExpression(_) => None, + Param::Obj(_) => None, }; let phi: Option = match params[1] { Param::Float(val) => Some(val), Param::ParameterExpression(_) => None, + Param::Obj(_) => None, }; let lam: Option = match params[2] { Param::Float(val) => Some(val), Param::ParameterExpression(_) => None, + Param::Obj(_) => None, }; // If let chains as needed here are unstable ignore clippy to // workaround. Upstream rust tracking issue: @@ -471,6 +499,7 @@ impl Operation for StandardGate { ) .expect("Unexpected Qiskit python bug"), ), + Param::Obj(_) => unreachable!(), } }), Self::ECRGate => todo!("Add when we have RZX"), diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py index 8a1bd0cd925d..2e308af48ff4 100644 --- a/test/python/circuit/library/test_nlocal.py +++ b/test/python/circuit/library/test_nlocal.py @@ -235,7 +235,7 @@ def test_parameters_setter(self, params): initial_params = ParameterVector("p", length=6) circuit = QuantumCircuit(1) for i, initial_param in enumerate(initial_params): - circuit.ry((i + 1) * initial_param, 0) + circuit.ry(i * initial_param, 0) # create an NLocal from the circuit and set the new parameters nlocal = NLocal(1, entanglement_blocks=circuit, reps=1) diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index cfa6bfb386ed..466bf5a40aff 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -585,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): From a6e69ba4c99cdd2fa50ed10399cada322bc88903 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 25 May 2024 16:17:21 -0400 Subject: [PATCH 03/61] Fix qasm3 exporter for std gates without stdgates.inc This commit fixes the handling of standard gates in Qiskit when the user specifies excluding the use of the stdgates.inc file from the exported qasm. Previously the object id of the standard gates were used to maintain a lookup table of the global definitions for all the standard gates explicitly in the file. However, the rust refactor means that every time the exporter accesses `circuit.data[x].operation` a new instance is returned. This means that on subsequent lookups for the definition the gate definitions are never found. To correct this issue this commit adds to the lookup table a fallback of the gate name + parameters to do the lookup for. This should be unique for any standard gate and not interfere with the previous logic that's still in place and functional for other custom gate definitions. While this fixes the logic in the exporter the test is still failing because the test is asserting the object ids are the same in the qasm3 file, which isn't the case anymore. The test will be updated in a subsequent commit to validate the qasm3 file is correct without using a hardcoded object id. --- qiskit/qasm3/exporter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index a25d25805220..03602ca69857 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -252,6 +252,7 @@ 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 + self._data[f"{instruction.name}_{instruction.params}"] = name_str def __getitem__(self, key): if isinstance(key, Instruction): @@ -262,6 +263,11 @@ def __getitem__(self, key): pass # Built-in gates. if key.name not in self._data: + try: + # Registerd qiskit standard gate without stgates.inc + return self._data[f"{key.name}_{key.params}"] + except KeyError: + pass raise KeyError(key) return key.name return self._data[key] From 4e34642a58f4ce80decb4bd997935e1f0da57d61 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 25 May 2024 18:36:55 -0400 Subject: [PATCH 04/61] Fix base scheduler analysis pass duration setting When ALAPScheduleAnalysis and ASAPScheduleAnalysis were setting the duration of a gate they were doing `node.op.duration = duration` this wasn't always working because if `node.op` was a standard gate it returned a new Python object created from the underlying rust representation. This commit fixes the passes so that they modify the duration and then explicit set the operation to update it's rust representation. --- .../passes/scheduling/scheduling/base_scheduler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/scheduling/base_scheduler.py index 3792a149fd71..69bea32acca7 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 From 0edcfb0a643cb35df7a676c569bf3168d86abd0b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 25 May 2024 18:49:24 -0400 Subject: [PATCH 05/61] Fix python lint --- qiskit/circuit/parametertable.py | 5 ++--- qiskit/circuit/quantumcircuit.py | 12 +++++------- qiskit/circuit/quantumcircuitdata.py | 2 -- test/python/circuit/test_circuit_data.py | 2 -- test/python/circuit/test_rust_equivalence.py | 5 ++--- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/qiskit/circuit/parametertable.py b/qiskit/circuit/parametertable.py index 51ed3aee129b..e5a41b1971c2 100644 --- a/qiskit/circuit/parametertable.py +++ b/qiskit/circuit/parametertable.py @@ -12,9 +12,8 @@ """ Look-up table for variable parameters in QuantumCircuit. """ -import operator -import typing -from collections.abc import MappingView, MutableMapping, MutableSet + +from collections.abc import MappingView class ParameterView(MappingView): diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 413de04b3cc2..6b40fc994f8b 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2484,13 +2484,10 @@ def _append(self, instruction, qargs=(), cargs=()): # clear cache if new parameter is added self._parameters = None - self._track_operation(instruction.operation) - return instruction.operation if old_style else instruction - - def _track_operation(self, operation: Operation): - """Sync all non-data-list internal data structures for a newly tracked operation.""" + # Invalidate whole circuit duration if an instruction is added self.duration = None self.unit = "dt" + return instruction.operation if old_style else instruction @typing.overload def get_parameter(self, name: str, default: T) -> Union[Parameter, T]: ... @@ -4026,7 +4023,7 @@ 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. - if isinstance(previous := getattr(self._data, "global_phase", None), ParameterExpression): + if isinstance(getattr(self._data, "global_phase", None), ParameterExpression): self._parameters = None if isinstance(angle, ParameterExpression): if angle.parameters: @@ -6566,7 +6563,8 @@ def append(self, instruction): def extend(self, data: CircuitData): self.circuit._data.extend(data) self.circuit._parameters = None - data.foreach_op(self.circuit._track_operation) + 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 82e5b79486fd..9ecc8e6a6cac 100644 --- a/qiskit/circuit/quantumcircuitdata.py +++ b/qiskit/circuit/quantumcircuitdata.py @@ -20,8 +20,6 @@ from .exceptions import CircuitError from .instruction import Instruction from .operation import Operation -from .instruction import Instruction -from .gate import Gate CircuitInstruction = qiskit._accelerate.circuit.CircuitInstruction diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 7ebe38dc763a..de9b63894300 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -831,8 +831,6 @@ def test_param_gate_instance(self): # 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) - param_entry_0 = qc0._data._get_param(b.uuid.int) - param_entry_1 = qc1._data._get_param(a.uuid.int) 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_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index eb75bba98d36..b657ed327fa3 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -14,14 +14,13 @@ 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 -from test import QiskitTestCase - - SKIP_LIST = {"cy", "ccx", "rx", "ry", "ecr", "sx"} CUSTOM_MAPPING = {"x", "rz"} From f8965126c872f4f1a151fcf51a8b340dcfbca012 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 25 May 2024 19:08:14 -0400 Subject: [PATCH 06/61] Fix last failing qasm3 test for std gates without stdgates.inc While the logic for the qasm3 exporter was fixed in commit a6e69ba4c99cdd2fa50ed10399cada322bc88903 to handle the edge case of a user specifying that the qasm exporter does not use the stdgates.inc include file in the output, but also has qiskit's standard gates in their circuit being exported. The one unit test to provide coverage for that scenario was not passing because when an id was used for the gate definitions in the qasm3 file it was being referenced against a temporary created by accessing a standard gate from the circuit and the ids weren't the same so the reference string didn't match what the exporter generated. This commit fixes this by changing the test to not do an exact string comparison, but instead a line by line comparison that either does exact equality check or a regex search for the expected line and the ids are checked as being any 15 character integer. --- test/python/qasm3/test_export.py | 98 +++++++++++++++++--------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 598405beaae1..70c106c94d19 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -2130,52 +2130,58 @@ def test_no_include(self): 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(u3_1))) + 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 From 046737f938d6a0ada3408be6e4f10bd83ea417d0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 25 May 2024 19:51:59 -0400 Subject: [PATCH 07/61] Remove superfluous comment --- crates/circuit/src/gate_matrix.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 4f0750729dcd..d53c0269b9ff 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -10,14 +10,6 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -// In numpy matrices real and imaginary components are adjacent: -// np.array([1,2,3], dtype='complex').view('float64') -// array([1., 0., 2., 0., 3., 0.]) -// The matrix faer::Mat has this layout. -// faer::Mat> instead stores a matrix -// of real components and one of imaginary components. -// In order to avoid copying we want to use `MatRef` or `MatMut`. - use num_complex::Complex64; use std::f64::consts::FRAC_1_SQRT_2; From 5c5b90f3774417729f8a6e1fab9874b717fb4d16 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 25 May 2024 20:47:29 -0400 Subject: [PATCH 08/61] Cache imported classes with GILOnceCell --- crates/circuit/src/circuit_data.rs | 58 +++++++++++++++--- crates/circuit/src/circuit_instruction.rs | 74 +++++++++++++++-------- crates/circuit/src/lib.rs | 1 + crates/circuit/src/operations.rs | 29 ++++++--- 4 files changed, 119 insertions(+), 43 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 1f0a84aaa122..863f79c121ff 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -13,6 +13,7 @@ use crate::circuit_instruction::{ convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction, }; +use crate::imports::{BUILTIN_LIST, CLBIT, QUBIT}; use crate::intern_context::{BitType, IndexType, InternContext}; use crate::operations::{OperationType, Param}; use crate::parameter_table::{ParamEntry, ParamTable}; @@ -193,16 +194,30 @@ impl CircuitData { global_phase, }; if num_qubits > 0 { - let qubit_mod = py.import_bound("qiskit.circuit.quantumregister")?; - let qubit_cls = qubit_mod.getattr("Qubit")?; + let qubit_cls = QUBIT + .get_or_init(py, || { + py.import_bound("qiskit.circuit.quantumregister") + .unwrap() + .getattr("Qubit") + .unwrap() + .unbind() + }) + .bind(py); for _i in 0..num_qubits { let bit = qubit_cls.call0()?; res.add_qubit(py, &bit, true)?; } } if num_clbits > 0 { - let clbit_mod = py.import_bound(intern!(py, "qiskit.circuit.classicalregister"))?; - let clbit_cls = clbit_mod.getattr(intern!(py, "Clbit"))?; + let clbit_cls = CLBIT + .get_or_init(py, || { + py.import_bound("qiskit.circuit.classicalregister") + .unwrap() + .getattr("Clbit") + .unwrap() + .unbind() + }) + .bind(py); for _i in 0..num_clbits { let bit = clbit_cls.call0()?; res.add_clbit(py, &bit, true)?; @@ -289,8 +304,15 @@ impl CircuitData { }) .collect(); if !params.is_empty() { - let builtins = PyModule::import_bound(py, "builtins")?; - let list_builtin = builtins.getattr("list")?; + let list_builtin = BUILTIN_LIST + .get_or_init(py, || { + PyModule::import_bound(py, "builtins") + .unwrap() + .getattr("list") + .unwrap() + .unbind() + }) + .bind(py); for (param_index, param) in ¶ms { let temp: PyObject = param.getattr(py, intern!(py, "parameters"))?; @@ -327,8 +349,16 @@ impl CircuitData { /// Remove an index's entries from the parameter table. fn remove_from_parameter_table(&mut self, py: Python, inst_index: usize) -> PyResult<()> { - let builtins = PyModule::import_bound(py, "builtins")?; - let list_builtin = builtins.getattr(intern!(py, "list"))?; + let list_builtin = BUILTIN_LIST + .get_or_init(py, || { + PyModule::import_bound(py, "builtins") + .unwrap() + .getattr("list") + .unwrap() + .unbind() + }) + .bind(py); + if inst_index == usize::MAX { if let Param::ParameterExpression(global_phase) = &self.global_phase { let temp: PyObject = global_phase.getattr(py, intern!(py, "parameters"))?; @@ -1065,8 +1095,16 @@ impl CircuitData { #[setter] pub fn global_phase(&mut self, py: Python, angle: Param) -> PyResult<()> { - let builtins = PyModule::import_bound(py, "builtins")?; - let list_builtin = builtins.getattr(intern!(py, "list"))?; + let list_builtin = BUILTIN_LIST + .get_or_init(py, || { + PyModule::import_bound(py, "builtins") + .unwrap() + .getattr("list") + .unwrap() + .unbind() + }) + .bind(py); + self.remove_from_parameter_table(py, usize::MAX)?; match angle { Param::Float(angle) => { diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 0844a94dfa48..2c8c91bd95d2 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -21,6 +21,7 @@ use pyo3::{intern, IntoPy, PyObject, PyResult}; use smallvec::SmallVec; use std::sync::Mutex; +use crate::imports::{GATE, INSTRUCTION, OPERATION, SINGLETON_CONTROLLED_GATE, SINGLETON_GATE}; use crate::operations::{ OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate, STANDARD_GATE_SIZE, }; @@ -638,12 +639,24 @@ pub(crate) fn convert_py_to_operation_type( if standard.is_some() { let mutable: bool = py_op.getattr(py, intern!(py, "mutable"))?.extract(py)?; if mutable { - let singleton_mod = py.import_bound("qiskit.circuit.singleton")?; - let singleton_class = singleton_mod.getattr(intern!(py, "SingletonGate"))?; - let singleton_control = - singleton_mod.getattr(intern!(py, "SingletonControlledGate"))?; - if py_op_bound.is_instance(&singleton_class)? - || py_op_bound.is_instance(&singleton_control)? + let singleton_class = SINGLETON_GATE + .get_or_init(py, || { + let singleton_mod = py.import_bound("qiskit.circuit.singleton").unwrap(); + singleton_mod.getattr("SingletonGate").unwrap().unbind() + }) + .bind(py); + let singleton_control = SINGLETON_CONTROLLED_GATE + .get_or_init(py, || { + let singleton_mod = py.import_bound("qiskit.circuit.singleton").unwrap(); + singleton_mod + .getattr("SingletonControlledGate") + .unwrap() + .unbind() + }) + .bind(py); + + if py_op_bound.is_instance(singleton_class)? + || py_op_bound.is_instance(singleton_control)? { standard = None; } @@ -685,12 +698,17 @@ pub(crate) fn convert_py_to_operation_type( .extract(py)?, }); } - let gate_class = py - .import_bound("qiskit.circuit.gate")? - .getattr(intern!(py, "Gate")) - .ok() - .unwrap(); - if op_type.is_subclass(&gate_class)? { + let gate_class = GATE + .get_or_init(py, || { + py.import_bound("qiskit.circuit.gate") + .unwrap() + .getattr("Gate") + .unwrap() + .unbind() + }) + .bind(py); + + if op_type.is_subclass(gate_class)? { let params = py_op .getattr(py, intern!(py, "params")) .ok() @@ -750,12 +768,16 @@ pub(crate) fn convert_py_to_operation_type( condition, }); } - let instruction_class = py - .import_bound("qiskit.circuit.instruction")? - .getattr(intern!(py, "Instruction")) - .ok() - .unwrap(); - if op_type.is_subclass(&instruction_class)? { + let instruction_class = INSTRUCTION + .get_or_init(py, || { + py.import_bound("qiskit.circuit.instruction") + .unwrap() + .getattr("Instruction") + .unwrap() + .unbind() + }) + .bind(py); + if op_type.is_subclass(instruction_class)? { let params = py_op .getattr(py, intern!(py, "params")) .ok() @@ -816,12 +838,16 @@ pub(crate) fn convert_py_to_operation_type( }); } - let operation_class = py - .import_bound("qiskit.circuit.operation")? - .getattr(intern!(py, "Operation")) - .ok() - .unwrap(); - if op_type.is_subclass(&operation_class)? { + let operation_class = OPERATION + .get_or_init(py, || { + py.import_bound("qiskit.circuit.operation") + .unwrap() + .getattr("Operation") + .unwrap() + .unbind() + }) + .bind(py); + if op_type.is_subclass(operation_class)? { let params = match py_op.getattr(py, intern!(py, "params")).ok() { Some(value) => value.extract(py)?, None => None, diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index a6364c43cacd..ff5defbfa5e8 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -14,6 +14,7 @@ pub mod circuit_data; pub mod circuit_instruction; pub mod dag_node; pub mod gate_matrix; +pub mod imports; pub mod intern_context; pub mod operations; pub mod parameter_table; diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 6a57c14e1cfc..716b0f2a617a 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -14,6 +14,7 @@ use std::f64::consts::PI; use crate::circuit_data::CircuitData; use crate::gate_matrix; +use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; use ndarray::{aview2, Array2}; use num_complex::Complex64; use numpy::IntoPyArray; @@ -125,16 +126,26 @@ pub enum Param { impl<'py> FromPyObject<'py> for Param { fn extract_bound(b: &Bound<'py, PyAny>) -> Result { - let param_mod = PyModule::import_bound( - b.py(), - intern!(b.py(), "qiskit.circuit.parameterexpression"), - )?; - let param_class = param_mod.getattr(intern!(b.py(), "ParameterExpression"))?; - let circuit_mod = - PyModule::import_bound(b.py(), intern!(b.py(), "qiskit.circuit.quantumcircuit"))?; - let circuit_class = circuit_mod.getattr(intern!(b.py(), "QuantumCircuit"))?; + let param_class = PARAMETER_EXPRESSION + .get_or_init(b.py(), || { + PyModule::import_bound(b.py(), "qiskit.circuit.parameterexpression") + .unwrap() + .getattr("ParameterExpression") + .unwrap() + .unbind() + }) + .bind(b.py()); + let circuit_class = QUANTUM_CIRCUIT + .get_or_init(b.py(), || { + PyModule::import_bound(b.py(), "qiskit.circuit.quantumcircuit") + .unwrap() + .getattr("QuantumCircuit") + .unwrap() + .unbind() + }) + .bind(b.py()); Ok( - if b.is_instance(¶m_class)? || b.is_instance(&circuit_class)? { + if b.is_instance(param_class)? || b.is_instance(circuit_class)? { Param::ParameterExpression(b.clone().unbind()) } else if let Ok(val) = b.extract::() { Param::Float(val) From 7329399b1be1950256edf143b35321c3f3299948 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 25 May 2024 20:49:35 -0400 Subject: [PATCH 09/61] Remove unused python variables --- test/python/qasm3/test_export.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 70c106c94d19..7454ee3488e7 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -2120,17 +2120,7 @@ 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 - id_len = len(str(id(u3_1))) + 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), From ae64fd7ae5c83527dc94e7c0cb06fb17bdc72706 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 25 May 2024 20:52:57 -0400 Subject: [PATCH 10/61] Add missing file --- crates/circuit/src/imports.rs | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 crates/circuit/src/imports.rs diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs new file mode 100644 index 000000000000..18cd44162cb7 --- /dev/null +++ b/crates/circuit/src/imports.rs @@ -0,0 +1,39 @@ +// 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; + +// builtin list: +pub static BUILTIN_LIST: GILOnceCell = GILOnceCell::new(); +// qiskit.circuit.operation.Operation +pub static OPERATION: GILOnceCell = GILOnceCell::new(); +// qiskit.circuit.instruction.Instruction +pub static INSTRUCTION: GILOnceCell = GILOnceCell::new(); +// qiskit.circuit.gate.Gate +pub static GATE: GILOnceCell = GILOnceCell::new(); +// qiskit.circuit.quantumregister.Qubit +pub static QUBIT: GILOnceCell = GILOnceCell::new(); +// qiskit.circuit.classicalregister.Clbit +pub static CLBIT: GILOnceCell = GILOnceCell::new(); +// qiskit.circuit.parameterexpression.ParameterExpression +pub static PARAMETER_EXPRESSION: GILOnceCell = GILOnceCell::new(); +// qiskit.circuit.quantumcircuit.QuantumCircuit +pub static QUANTUM_CIRCUIT: GILOnceCell = GILOnceCell::new(); +// qiskit.circuit.singleton.SingletonGate +pub static SINGLETON_GATE: GILOnceCell = GILOnceCell::new(); +// qiskit.circuit.singleton.SingletonControlledGate +pub static SINGLETON_CONTROLLED_GATE: GILOnceCell = GILOnceCell::new(); From 76599b2bf510bc0656210985e365d077c78502ea Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 26 May 2024 11:31:08 -0400 Subject: [PATCH 11/61] Update QuantumCircuit gate methods to bypass Python object This commit updates the QuantumCircuit gate methods which add a given gate to the circuit to bypass the python gate object creation and directly insert a rust representation of the gate. This avoids a conversion in the rust side of the code. While in practice this is just the Python side object creation and a getattr for the rust code to determine it's a standard gate that we're skipping. This may add up over time if there are a lot of gates being created by the method. To accomplish this the rust code handling the mapping of rust StandardGate variants to the Python classes that represent those gates needed to be updated as well. By bypassing the python object creation we need a fallback to populate the gate class for when a user access the operation object from Python. Previously this mapping was only being populated at insertion time and if we never insert the python object (for a circuit created only via the methods) then we need a way to find what the gate class is. A static lookup table of import paths and class names are added to `qiskit_circuit::imports` module to faciliate this and helper functions are added to facilitate interacting with the class objects that represent each gate. --- Cargo.lock | 7 - crates/circuit/Cargo.toml | 1 - crates/circuit/src/circuit_instruction.rs | 25 +--- crates/circuit/src/imports.rs | 133 +++++++++++++++++-- crates/circuit/src/operations.rs | 50 ++++--- qiskit/circuit/quantumcircuit.py | 151 +++++++++++++--------- test/python/qasm3/test_export.py | 36 +++--- 7 files changed, 264 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62b969495dbd..7e0326f7ed04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -618,12 +618,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.154" @@ -1130,7 +1124,6 @@ name = "qiskit-circuit" version = "1.2.0" dependencies = [ "hashbrown 0.14.5", - "lazy_static", "ndarray", "num-complex", "numpy", diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 5350c67058d5..97285b4f90d0 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -14,7 +14,6 @@ hashbrown.workspace = true num-complex.workspace = true ndarray.workspace = true numpy.workspace = true -lazy_static = "1.4" [dependencies.pyo3] workspace = true diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 2c8c91bd95d2..abbea346f8b2 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -10,27 +10,18 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use lazy_static::lazy_static; - -use hashbrown::HashMap; use pyo3::basic::CompareOp; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyList, PyTuple, PyType}; use pyo3::{intern, IntoPy, PyObject, PyResult}; use smallvec::SmallVec; -use std::sync::Mutex; -use crate::imports::{GATE, INSTRUCTION, OPERATION, SINGLETON_CONTROLLED_GATE, SINGLETON_GATE}; -use crate::operations::{ - OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate, STANDARD_GATE_SIZE, +use crate::imports::{ + get_std_gate_class, populate_std_gate_map, GATE, INSTRUCTION, OPERATION, + SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, }; - -// TODO Come up with a better cacheing mechanism for this -lazy_static! { - static ref STANDARD_GATE_MAP: Mutex> = - Mutex::new(HashMap::with_capacity(STANDARD_GATE_SIZE)); -} +use crate::operations::{OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate}; /// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and /// various operands. @@ -560,7 +551,7 @@ pub(crate) fn operation_type_and_data_to_py( ) -> PyResult { match &operation { OperationType::Standard(op) => { - let gate_class: &PyObject = &STANDARD_GATE_MAP.lock().unwrap()[op]; + let gate_class: &PyObject = &get_std_gate_class(py, *op)?; let args = if let Some(params) = ¶ms { if params.is_empty() { @@ -664,11 +655,7 @@ pub(crate) fn convert_py_to_operation_type( } if let Some(op) = standard { let base_class = op_type.to_object(py); - STANDARD_GATE_MAP - .lock() - .unwrap() - .entry(op) - .or_insert(base_class); + populate_std_gate_map(py, op, base_class); return Ok(OperationTypeConstruct { operation: OperationType::Standard(op), params: py_op diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 18cd44162cb7..e7aa6c2972fb 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -17,23 +17,136 @@ use pyo3::prelude::*; use pyo3::sync::GILOnceCell; -// builtin list: +use crate::operations::{StandardGate, STANDARD_GATE_SIZE}; + +/// builtin list pub static BUILTIN_LIST: GILOnceCell = GILOnceCell::new(); -// qiskit.circuit.operation.Operation +/// qiskit.circuit.operation.Operation pub static OPERATION: GILOnceCell = GILOnceCell::new(); -// qiskit.circuit.instruction.Instruction +/// qiskit.circuit.instruction.Instruction pub static INSTRUCTION: GILOnceCell = GILOnceCell::new(); -// qiskit.circuit.gate.Gate +/// qiskit.circuit.gate.Gate pub static GATE: GILOnceCell = GILOnceCell::new(); -// qiskit.circuit.quantumregister.Qubit +/// qiskit.circuit.quantumregister.Qubit pub static QUBIT: GILOnceCell = GILOnceCell::new(); -// qiskit.circuit.classicalregister.Clbit +/// qiskit.circuit.classicalregister.Clbit pub static CLBIT: GILOnceCell = GILOnceCell::new(); -// qiskit.circuit.parameterexpression.ParameterExpression +/// qiskit.circuit.parameterexpression.ParameterExpression pub static PARAMETER_EXPRESSION: GILOnceCell = GILOnceCell::new(); -// qiskit.circuit.quantumcircuit.QuantumCircuit +/// qiskit.circuit.quantumcircuit.QuantumCircuit pub static QUANTUM_CIRCUIT: GILOnceCell = GILOnceCell::new(); -// qiskit.circuit.singleton.SingletonGate +/// qiskit.circuit.singleton.SingletonGate pub static SINGLETON_GATE: GILOnceCell = GILOnceCell::new(); -// qiskit.circuit.singleton.SingletonControlledGate +/// qiskit.circuit.singleton.SingletonControlledGate pub static SINGLETON_CONTROLLED_GATE: GILOnceCell = GILOnceCell::new(); + +/// A mapping from the enum varian 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 => { + // A fixed size array is initialized like this because using the `[T; 5]` syntax + // requires T to be `Copy`. But `PyObject` isn't Copy so therefore Option + // as T isn't Copy. To avoid that we just list out None STANDARD_GATE_SIZE times + let array: [Option; STANDARD_GATE_SIZE] = [ + None, None, None, None, None, None, None, None, None, None, None, None, None, + None, None, None, None, 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, || { + // A fixed size array is initialized like this because using the `[T; 5]` syntax + // requires T to be `Copy`. But `PyObject` isn't Copy so therefore Option + // as T isn't Copy. To avoid that we just list out None STANDARD_GATE_SIZE times + let array: [Option; STANDARD_GATE_SIZE] = [ + None, None, None, None, None, None, None, None, None, None, None, None, None, None, + None, None, None, None, + ]; + array + }) + }; + 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/operations.rs b/crates/circuit/src/operations.rs index 716b0f2a617a..bbb1871eee75 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -179,26 +179,24 @@ impl ToPyObject for Param { #[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] #[pyclass] pub enum StandardGate { - // Pauli Gates - ZGate, - YGate, - XGate, - // Controlled Pauli Gates - CZGate, - CYGate, - CXGate, - CCXGate, - RXGate, - RYGate, - RZGate, - ECRGate, - SwapGate, - SXGate, - GlobalPhaseGate, - IGate, - HGate, - PhaseGate, - UGate, + 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, } #[pymethods] @@ -219,6 +217,16 @@ impl StandardGate { pub fn _get_definition(&self, params: Option>) -> Option { self.definition(params) } + + #[getter] + pub fn get_num_qubits(&self) -> u32 { + self.num_qubits() + } + + #[getter] + pub fn get_num_clbits(&self) -> u32 { + self.num_clbits() + } } // This must be kept up-to-date with `StandardGate` when adding or removing @@ -226,7 +234,7 @@ impl StandardGate { // // Remove this when std::mem::variant_count() is stabilized (see // https://github.com/rust-lang/rust/issues/73662 ) -pub const STANDARD_GATE_SIZE: usize = 17; +pub const STANDARD_GATE_SIZE: usize = 18; impl Operation for StandardGate { fn name(&self) -> &str { diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 6b40fc994f8b..0b08126fe8fe 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 from qiskit.exceptions import QiskitError from qiskit.utils.multiprocessing import is_main_process from qiskit.circuit.instruction import Instruction @@ -2301,6 +2302,29 @@ 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() + + expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []] + expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []] + + 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) + instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) + return instructions + def append( self, instruction: Operation | CircuitInstruction, @@ -4451,9 +4475,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, @@ -4497,9 +4519,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`. @@ -4530,9 +4550,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, @@ -4713,9 +4731,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, @@ -4784,9 +4800,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, @@ -4852,9 +4866,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, @@ -4938,9 +4950,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`. @@ -5045,9 +5057,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`. @@ -5108,9 +5123,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`. @@ -5208,9 +5221,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, @@ -5263,9 +5274,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, @@ -5289,14 +5298,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: @@ -5337,13 +5349,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( @@ -5441,9 +5460,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, @@ -5467,13 +5484,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: @@ -5487,9 +5509,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, @@ -5513,13 +5533,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( diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 7454ee3488e7..d31ccd27f487 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1957,27 +1957,23 @@ 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( - [ + id_len = len(str(id(first_x))) + expected_qasm = [ "OPENQASM 3.0;", - f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_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;", "}", - 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;", + 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 {", - f" u2_{id(u2)}(0, pi) _gate_q_0;", + re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), "}", - f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", + 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 {", - f" u3_{id(u3_2)}(pi, 0, pi) _gate_q_0;", + re.compile(r" u3_\d{%s}\(pi, 0, pi\) _gate_q_0;" % id_len), "}", "bit[2] c;", "qubit[3] q;", @@ -1997,12 +1993,16 @@ def test_basis_gates(self): " z q[2];", "}", "", - ] - ) - self.assertEqual( - Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(qc), - expected_qasm, - ) + ] + 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""" From 14b7133775d26fc7af8ddb32cbcffe947e95f7b0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 26 May 2024 13:12:48 -0400 Subject: [PATCH 12/61] Deduplicate gate matrix definitions --- crates/accelerate/src/isometry.rs | 2 +- crates/accelerate/src/two_qubit_decompose.rs | 54 +------------------- 2 files changed, 2 insertions(+), 54 deletions(-) diff --git a/crates/accelerate/src/isometry.rs b/crates/accelerate/src/isometry.rs index 8d0761666bb6..55afe35b87e5 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 5e833bd86fda..45fc8b90c637 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::{CXGATE, HGATE, ONE_QUBIT_IDENTITY, SXGATE, XGATE}; 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(); From 0863830506ae45e7fbdbca2e06c36bd2ae26b0c9 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 26 May 2024 13:17:30 -0400 Subject: [PATCH 13/61] Fix lint --- test/python/qasm3/test_export.py | 69 ++++++++++++++++---------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index d31ccd27f487..a4bbff5ad94a 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1959,40 +1959,40 @@ def test_basis_gates(self): 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];", - "}", - "", + "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): @@ -2003,7 +2003,6 @@ def test_basis_gates(self): expected.search(result), f"Line {result} doesn't match regex: {expected}" ) - def test_teleportation(self): """Teleportation with physical qubits""" qc = QuantumCircuit(3, 2) From c4cda8d7f19def0d8646dc6bdfe0901c2f617362 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 26 May 2024 18:24:54 -0400 Subject: [PATCH 14/61] Attempt to fix qasm3 test failure --- qiskit/qasm3/exporter.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 03602ca69857..7f737af76eb4 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -252,7 +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 - self._data[f"{instruction.name}_{instruction.params}"] = 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): @@ -263,12 +265,9 @@ def __getitem__(self, key): pass # Built-in gates. if key.name not in self._data: - try: - # Registerd qiskit standard gate without stgates.inc - return self._data[f"{key.name}_{key.params}"] - except KeyError: - pass - 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] From e9bb053c346db68bfdc517c21c39d989e087da6a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 28 May 2024 16:50:37 -0400 Subject: [PATCH 15/61] Add compile time option to cache py gate returns for rust std gates This commit adds a new rust crate feature flag for the qiskit-circuits and qiskit-pyext that enables caching the output from CircuitInstruction.operation to python space. Previously, for memory efficiency we were reconstructing the python object on demand for every access. This was to avoid carrying around an extra pointer and keeping the ephemeral python object around longer term if it's only needed once. But right now nothing is directly using the rust representation yet and everything is accessing via the python interface, so recreating gate objects on the fly has a huge performance penalty. To avoid that this adds caching by default as a temporary solution to avoid this until we have more usage of the rust representation of gates. There is an inherent tension between an optimal rust representation and something that is performant for Python access and there isn't a clear cut answer on which one is better to optimize for. A build time feature lets the user pick, if what we settle on for the default doesn't agree with their priorities or use case. Personally I'd like to see us disable the caching longer term (hopefully before releasing this functionality), but that's dependent on a sufficent level of usage from rust superseding the current Python space usage in the core of Qiskit. --- .github/workflows/tests.yml | 9 ++ CONTRIBUTING.md | 12 ++ crates/circuit/Cargo.toml | 3 + crates/circuit/src/circuit_data.rs | 116 ++++++++++++++++++ crates/circuit/src/circuit_instruction.rs | 68 +++++++++- crates/circuit/src/dag_node.rs | 4 +- crates/pyext/Cargo.toml | 1 + .../circuit-gates-rust-5c6ab6c58f7fd2c9.yaml | 9 ++ setup.py | 12 ++ 9 files changed, 231 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 08530adfd4f1..20e40dec9824 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 c75b51750017..d3a01004c72c 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 build. 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/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 97285b4f90d0..dd7e878537d9 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -22,3 +22,6 @@ features = ["hashbrown", "indexmap", "num-complex", "num-bigint", "smallvec"] [dependencies.smallvec] workspace = true features = ["union"] + +[features] +cache_pygates = [] diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 863f79c121ff..0fcaed03b3d0 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -42,6 +42,8 @@ struct PackedInstruction { duration: Option, unit: Option, condition: Option, + #[cfg(feature = "cache_pygates")] + py_op: Option, } /// Private wrapper for Python-side Bit instances that implements @@ -247,6 +249,8 @@ impl CircuitData { duration: None, unit: None, condition: None, + #[cfg(feature = "cache_pygates")] + py_op: None, }, )?; res.data.push(inst); @@ -634,6 +638,7 @@ 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() { @@ -651,12 +656,43 @@ impl CircuitData { 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 new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + &inst.label, + &inst.duration, + &inst.unit, + &inst.condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } + }; + func.call1((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(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() { @@ -674,6 +710,37 @@ impl CircuitData { 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 new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + &inst.label, + &inst.duration, + &inst.unit, + &inst.condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } + }; + func.call1((index, op))?; + } + Ok(()) + } + /// Invokes callable ``func`` with each instruction's operation, /// replacing the operation with the result. /// @@ -681,6 +748,7 @@ impl CircuitData { /// 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() { @@ -705,6 +773,46 @@ impl CircuitData { Ok(()) } + /// Invokes callable ``func`` with each instruction's operation, + /// replacing the operation with the result. + /// + /// 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 => { + let new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + &inst.label, + &inst.duration, + &inst.unit, + &inst.condition, + )?; + inst.py_op = Some(new_op.clone_ref(py)); + new_op + } + }; + let new_op = func.call1((old_op,))?; + let new_inst_details = convert_py_to_operation_type(py, new_op.clone().into())?; + inst.op = new_inst_details.operation; + inst.params = new_inst_details.params; + inst.label = new_inst_details.label; + inst.duration = new_inst_details.duration; + inst.unit = new_inst_details.unit; + inst.condition = new_inst_details.condition; + inst.py_op = Some(new_op.unbind()); + } + Ok(()) + } + /// Replaces the bits of this container with the given ``qubits`` /// and/or ``clbits``. /// @@ -1017,6 +1125,8 @@ impl CircuitData { duration: inst.duration.clone(), unit: inst.unit.clone(), condition: inst.condition.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), }); self.update_param_table(py, new_index, None)?; } @@ -1307,6 +1417,8 @@ impl CircuitData { duration: inst.duration.clone(), unit: inst.unit.clone(), condition: inst.condition.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), }) } @@ -1336,6 +1448,8 @@ impl CircuitData { duration: inst.duration.clone(), unit: inst.unit.clone(), condition: inst.condition.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), }) } @@ -1367,6 +1481,8 @@ impl CircuitData { duration: inst.duration.clone(), unit: inst.unit.clone(), condition: inst.condition.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 abbea346f8b2..a51f67abddd1 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -70,6 +70,8 @@ pub struct CircuitInstruction { pub duration: Option, pub unit: Option, pub condition: Option, + #[cfg(feature = "cache_pygates")] + pub py_op: Option, } /// This enum is for backwards compatibility if a user was doing something from @@ -137,6 +139,8 @@ impl CircuitInstruction { duration, unit, condition, + #[cfg(feature = "cache_pygates")] + py_op: None, }) } OperationInput::Gate(operation) => { @@ -150,6 +154,8 @@ impl CircuitInstruction { duration, unit, condition, + #[cfg(feature = "cache_pygates")] + py_op: None, }) } OperationInput::Instruction(operation) => { @@ -163,6 +169,8 @@ impl CircuitInstruction { duration, unit, condition, + #[cfg(feature = "cache_pygates")] + py_op: None, }) } OperationInput::Operation(operation) => { @@ -176,10 +184,12 @@ impl CircuitInstruction { duration, unit, condition, + #[cfg(feature = "cache_pygates")] + py_op: None, }) } - OperationInput::Object(op) => { - let op = convert_py_to_operation_type(py, op)?; + OperationInput::Object(old_op) => { + let op = convert_py_to_operation_type(py, old_op.clone_ref(py))?; match op.operation { OperationType::Standard(operation) => { let operation = OperationType::Standard(operation); @@ -192,6 +202,8 @@ impl CircuitInstruction { duration: op.duration, unit: op.unit, condition: op.condition, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), }) } OperationType::Gate(operation) => { @@ -205,6 +217,8 @@ impl CircuitInstruction { duration: op.duration, unit: op.unit, condition: op.condition, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), }) } OperationType::Instruction(operation) => { @@ -218,6 +232,8 @@ impl CircuitInstruction { duration: op.duration, unit: op.unit, condition: op.condition, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), }) } OperationType::Operation(operation) => { @@ -231,6 +247,8 @@ impl CircuitInstruction { duration: op.duration, unit: op.unit, condition: op.condition, + #[cfg(feature = "cache_pygates")] + py_op: Some(old_op.clone_ref(py)), }) } } @@ -247,11 +265,25 @@ impl CircuitInstruction { } /// 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: @@ -369,22 +401,52 @@ 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. + #[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, + [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)) } + #[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)) } + #[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 { 3 } @@ -497,6 +559,8 @@ impl CircuitInstruction { } if other.is_instance_of::() { + #[cfg(feature = "cache_pygates")] + let mut self_ = self_.clone(); let legacy_format = self_._legacy_format(py)?; return Ok(Some(legacy_format.eq(other)?)); } diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 31ca70b605d3..9ac717b0c3ad 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -109,7 +109,7 @@ impl DAGOpNode { } None => qargs.str()?.into_any(), }; - let res = convert_py_to_operation_type(py, op)?; + let res = convert_py_to_operation_type(py, op.clone_ref(py))?; Ok(( DAGOpNode { @@ -122,6 +122,8 @@ impl DAGOpNode { duration: res.duration, unit: res.unit, condition: res.condition, + #[cfg(feature = "cache_pygates")] + py_op: Some(op), }, sort_key: sort_key.unbind(), }, diff --git a/crates/pyext/Cargo.toml b/crates/pyext/Cargo.toml index daaf19e1f6a4..413165e84b1f 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/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml index 521d1588a956..933631a030e3 100644 --- a/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml +++ b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml @@ -6,6 +6,15 @@ features_circuits: 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` diff --git a/setup.py b/setup.py index 9bb5b04ae6ec..e82174d4d245 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"}}, From 0980d8d8df00818b583ad558ecce8446a9ade343 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 29 May 2024 16:41:29 -0400 Subject: [PATCH 16/61] Add num_nonlocal_gates implementation in rust This commit adds a native rust implementation to rust for the num_nonlocal_gates method on QuantumCircuit. Now that we have a rust representation of gates it is potentially faster to do the count because the iteration and filtering is done rust side. --- crates/circuit/src/circuit_data.rs | 8 ++++++ crates/circuit/src/operations.rs | 41 ++++++++++++++++++++++++++++++ qiskit/circuit/quantumcircuit.py | 8 +----- setup.py | 2 +- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 0fcaed03b3d0..3d1fdf5d8fd7 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -20,6 +20,7 @@ use crate::parameter_table::{ParamEntry, ParamTable}; use crate::SliceOrInt; use smallvec::SmallVec; +use crate::operations::Operation; use hashbrown::{HashMap, HashSet}; use pyo3::exceptions::{PyIndexError, PyKeyError, PyRuntimeError, PyValueError}; use pyo3::intern; @@ -1332,6 +1333,13 @@ impl CircuitData { .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 { diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index bbb1871eee75..1772dfa37f69 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -102,6 +102,15 @@ impl Operation for OperationType { 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 @@ -115,6 +124,7 @@ pub trait Operation { fn matrix(&self, params: Option>) -> Option>; fn definition(&self, params: Option>) -> Option; fn standard_gate(&self) -> Option; + fn directive(&self) -> bool; } #[derive(Clone, Debug)] @@ -314,6 +324,10 @@ impl Operation for StandardGate { false } + fn directive(&self) -> bool { + false + } + fn matrix(&self, params: Option>) -> Option> { match self { Self::ZGate => Some(aview2(&gate_matrix::ZGATE).to_owned()), @@ -644,6 +658,18 @@ impl Operation for PyInstruction { 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 @@ -733,6 +759,9 @@ impl Operation for PyGate { } }) } + fn directive(&self) -> bool { + false + } } /// This class is used to wrap a Python side Operation that is not in the standard library @@ -771,4 +800,16 @@ impl Operation for PyOperation { 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/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 0b08126fe8fe..337781744453 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -3453,13 +3453,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. diff --git a/setup.py b/setup.py index e82174d4d245..38af5286e817 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ "crates/pyext/Cargo.toml", binding=Binding.PyO3, debug=rust_debug, - features=features + features=features, ) ], options={"bdist_wheel": {"py_limited_api": "cp38"}}, From b35bdbd0ef6dfa0e9569309fcb68ccfd039ed29f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 30 May 2024 17:00:22 -0400 Subject: [PATCH 17/61] Performance tuning circuit construction This commit fixes some performance issues with the addition of standard gates to a circuit. To workaround potential reference cycles in Python when calling rust we need to check the parameters of the operation. This was causing our fast path for standard gates to access the `operation` attribute to get the parameters. This causes the gate to be eagerly constructed on the getter. However, the reference cycle case can only happen in situations without a standard gate, and the fast path for adding standard gates directly won't need to run this so a skip is added if we're adding a standard gate. --- crates/circuit/src/operations.rs | 8 +++---- crates/circuit/src/parameter_table.rs | 25 +++------------------- qiskit/circuit/controlflow/builder.py | 8 +++++-- qiskit/circuit/library/blueprintcircuit.py | 4 ++-- qiskit/circuit/quantumcircuit.py | 20 ++++++++++++----- 5 files changed, 30 insertions(+), 35 deletions(-) diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 1772dfa37f69..fc5eef795a1b 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -187,7 +187,7 @@ impl ToPyObject for Param { } #[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] -#[pyclass] +#[pyclass(module = "qiskit._accelerate.circuit")] pub enum StandardGate { ZGate = 0, YGate = 1, @@ -609,7 +609,7 @@ 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] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] pub struct PyInstruction { pub qubits: u32, pub clbits: u32, @@ -674,7 +674,7 @@ impl Operation for PyInstruction { /// This class is used to wrap a Python side Gate that is not in the standard library #[derive(Clone, Debug)] -#[pyclass] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] pub struct PyGate { pub qubits: u32, pub clbits: u32, @@ -766,7 +766,7 @@ impl Operation for PyGate { /// This class is used to wrap a Python side Operation that is not in the standard library #[derive(Clone, Debug)] -#[pyclass] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] pub struct PyOperation { pub qubits: u32, pub clbits: u32, diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs index 21b6db93d242..1e8ae88e0475 100644 --- a/crates/circuit/src/parameter_table.rs +++ b/crates/circuit/src/parameter_table.rs @@ -10,7 +10,6 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -use pyo3::exceptions::PyIndexError; use pyo3::prelude::*; use pyo3::{import_exception, intern, PyObject}; @@ -18,7 +17,7 @@ import_exception!(qiskit.circuit.exceptions, CircuitError); use hashbrown::{HashMap, HashSet}; -#[pyclass] +#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] pub(crate) struct ParamEntryKeys { keys: Vec<(usize, usize)>, iter_pos: usize, @@ -42,7 +41,7 @@ impl ParamEntryKeys { } #[derive(Clone, Debug)] -#[pyclass] +#[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 @@ -85,7 +84,7 @@ impl ParamEntry { } #[derive(Clone, Debug)] -#[pyclass(mapping)] +#[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, @@ -145,30 +144,12 @@ impl ParamTable { } } - fn __len__(&self) -> usize { - self.table.len() - } - pub fn clear(&mut self) { self.table.clear(); self.names.clear(); self.uuid_map.clear(); } - fn __contains__(&self, key: u128) -> bool { - self.table.contains_key(&key) - } - - fn __getitem__(&self, key: u128) -> PyResult { - match self.table.get(&key) { - Some(res) => Ok(res.clone()), - None => Err(PyIndexError::new_err(format!( - "No param uuid entry {:?}", - key - ))), - } - } - pub fn pop(&mut self, key: u128, name: String) -> Option { self.names.remove(&name); self.uuid_map.remove(&key); diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index c6c95d27f924..bb0a30ea6af6 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/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py index eb6e546c2a87..16cc0e3dbafa 100644 --- a/qiskit/circuit/library/blueprintcircuit.py +++ b/qiskit/circuit/library/blueprintcircuit.py @@ -120,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/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 337781744453..11875d34dd03 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2321,7 +2321,7 @@ def _append_standard_gate( for qarg, carg in broadcast_iter: self._check_dups(qarg) instruction = CircuitInstruction(op, qarg, carg, params=params, label=label) - circuit_scope.append(instruction) + circuit_scope.append(instruction, _standard_gate=True) instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) return instructions @@ -2431,7 +2431,9 @@ def append( # 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 @@ -2442,7 +2444,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:: @@ -2483,6 +2485,14 @@ 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) @@ -6575,9 +6585,9 @@ 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) From 3ea95de30d91590e870ea0c26276c01bfe64a0a8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 31 May 2024 06:44:07 -0400 Subject: [PATCH 18/61] Add back validation of parameters on gate methods In the previous commit a side effect of the accidental eager operation creation was that the parameter input for gates were being validated by that. By fixing that in the previous commit the validation of input parameters on the circuit methods was broken. This commit fixes that oversight and adds back the validation. --- crates/circuit/src/operations.rs | 5 +++++ qiskit/circuit/quantumcircuit.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index fc5eef795a1b..a74da2783945 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -237,6 +237,11 @@ impl StandardGate { pub fn get_num_clbits(&self) -> u32 { self.num_clbits() } + + #[getter] + pub fn get_name(&self) -> &str { + self.name() + } } // This must be kept up-to-date with `StandardGate` when adding or removing diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 11875d34dd03..e6fa525f72e8 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2315,6 +2315,9 @@ def _append_standard_gate( 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) From 2f81bde8bf32b06b4165048896eabdb36470814d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 31 May 2024 15:36:27 -0400 Subject: [PATCH 19/61] Skip validation on gate creation from rust --- crates/circuit/src/circuit_instruction.rs | 14 +++++++++++--- qiskit/circuit/gate.py | 7 ++++++- .../library/standard_gates/global_phase.py | 18 ++++++++++++++++-- qiskit/circuit/library/standard_gates/p.py | 18 ++++++++++++++++-- qiskit/circuit/library/standard_gates/rx.py | 18 ++++++++++++++++-- qiskit/circuit/library/standard_gates/ry.py | 18 ++++++++++++++++-- qiskit/circuit/library/standard_gates/rz.py | 18 ++++++++++++++++-- qiskit/circuit/library/standard_gates/u.py | 11 ++++++++++- qiskit/circuit/quantumcircuit.py | 2 +- 9 files changed, 108 insertions(+), 16 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index a51f67abddd1..7927d5248e4d 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -626,12 +626,20 @@ pub(crate) fn operation_type_and_data_to_py( } else { PyTuple::new_bound(py, params) }; - let kwargs = [ + let mut kwargs_list = vec![ ("label", label.to_object(py)), ("unit", unit.to_object(py)), ("duration", duration.to_object(py)), - ] - .into_py_dict_bound(py); + ]; + if let Some(params) = params { + if !params.is_empty() { + kwargs_list.push( + ("_skip_validation", true.to_object(py)) + ); + } + } + + let kwargs = kwargs_list.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")?; diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index 132526775860..5e644a2679ef 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -18,6 +18,7 @@ from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.exceptions import CircuitError +from qiskit._accelerate.circuit import StandardGate from .annotated_operation import AnnotatedOperation, ControlModifier, PowerModifier from .instruction import Instruction @@ -33,6 +34,7 @@ def __init__( label: str | None = None, duration=None, unit="dt", + _skip_validation=False, ) -> None: """Create a new gate. @@ -42,6 +44,7 @@ def __init__( params: A list of parameters. label: An optional label for the gate. """ + self._skip_validation = _skip_validation self.definition = None super().__init__(name, num_qubits, 0, params, label=label, duration=duration, unit=unit) @@ -238,7 +241,9 @@ def broadcast_arguments(self, qargs: list, cargs: list) -> Iterable[tuple[list, else: raise CircuitError("This gate cannot handle %i arguments" % len(qargs)) - def validate_parameter(self, parameter): + def validate_parameter(self, parameter, _force_validation=False): + if (isinstance(self, StandardGate) or self._skip_validation) and not _force_validation: + return parameter """Gate parameters should be int, float, or ParameterExpression""" if isinstance(parameter, ParameterExpression): if len(parameter.parameters) > 0: diff --git a/qiskit/circuit/library/standard_gates/global_phase.py b/qiskit/circuit/library/standard_gates/global_phase.py index 59d6b56373da..253970ad5f9a 100644 --- a/qiskit/circuit/library/standard_gates/global_phase.py +++ b/qiskit/circuit/library/standard_gates/global_phase.py @@ -40,14 +40,28 @@ class GlobalPhaseGate(Gate): _standard_gate = StandardGate.GlobalPhaseGate def __init__( - self, phase: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" + self, + phase: ParameterValueType, + label: Optional[str] = None, + *, + duration=None, + unit="dt", + _skip_validation=False, ): """ Args: phase: The value of phase it takes. label: An optional label for the gate. """ - super().__init__("global_phase", 0, [phase], label=label, duration=duration, unit=unit) + super().__init__( + "global_phase", + 0, + [phase], + label=label, + duration=duration, + unit=unit, + _skip_validation=_skip_validation, + ) def _define(self): q = QuantumRegister(0, "q") diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 1a792649feab..a1bb0214503b 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -79,10 +79,24 @@ class PhaseGate(Gate): _standard_gate = StandardGate.PhaseGate def __init__( - self, theta: ParameterValueType, label: str | None = None, *, duration=None, unit="dt" + self, + theta: ParameterValueType, + label: str | None = None, + *, + duration=None, + unit="dt", + _skip_validation=False, ): """Create new Phase gate.""" - super().__init__("p", 1, [theta], label=label, duration=duration, unit=unit) + super().__init__( + "p", + 1, + [theta], + label=label, + duration=duration, + unit=unit, + _skip_validation=_skip_validation, + ) def _define(self): # pylint: disable=cyclic-import diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index 5579f9d3707d..1a825485808d 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -54,10 +54,24 @@ class RXGate(Gate): _standard_gate = StandardGate.RXGate def __init__( - self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" + self, + theta: ParameterValueType, + label: Optional[str] = None, + *, + duration=None, + unit="dt", + _skip_validation=False, ): """Create new RX gate.""" - super().__init__("rx", 1, [theta], label=label, duration=duration, unit=unit) + super().__init__( + "rx", + 1, + [theta], + label=label, + duration=duration, + unit=unit, + _skip_validation=_skip_validation, + ) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index e27398cc2960..9308329e0f6b 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -53,10 +53,24 @@ class RYGate(Gate): _standard_gate = StandardGate.RYGate def __init__( - self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" + self, + theta: ParameterValueType, + label: Optional[str] = None, + *, + duration=None, + unit="dt", + _skip_validation=False, ): """Create new RY gate.""" - super().__init__("ry", 1, [theta], label=label, duration=duration, unit=unit) + super().__init__( + "ry", + 1, + [theta], + label=label, + duration=duration, + unit=unit, + _skip_validation=_skip_validation, + ) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index e8ee0f976036..2087d878b72d 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -63,10 +63,24 @@ class RZGate(Gate): _standard_gate = StandardGate.RZGate def __init__( - self, phi: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" + self, + phi: ParameterValueType, + label: Optional[str] = None, + *, + duration=None, + unit="dt", + _skip_validation=False, ): """Create new RZ gate.""" - super().__init__("rz", 1, [phi], label=label, duration=duration, unit=unit) + super().__init__( + "rz", + 1, + [phi], + label=label, + duration=duration, + unit=unit, + _skip_validation=_skip_validation, + ) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/u.py b/qiskit/circuit/library/standard_gates/u.py index 3495bc180f08..3af63a21c994 100644 --- a/qiskit/circuit/library/standard_gates/u.py +++ b/qiskit/circuit/library/standard_gates/u.py @@ -80,9 +80,18 @@ def __init__( *, duration=None, unit="dt", + _skip_validation=False, ): """Create new U gate.""" - super().__init__("u", 1, [theta, phi, lam], label=label, duration=duration, unit=unit) + super().__init__( + "u", + 1, + [theta, phi, lam], + label=label, + duration=duration, + unit=unit, + _skip_validation=_skip_validation, + ) def inverse(self, annotated: bool = False): r"""Return inverted U gate. diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index e6fa525f72e8..42070e704e0a 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2317,7 +2317,7 @@ def _append_standard_gate( 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) + Gate.validate_parameter(op, param, _force_validation=True) instructions = InstructionSet(resource_requester=circuit_scope.resolve_classical_resource) broadcast_iter = Gate.broadcast_arguments(op, expanded_qargs, expanded_cargs) From 725f22656fbf673a6cb68451206d9f2ad59334d6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 3 Jun 2024 14:03:22 -0400 Subject: [PATCH 20/61] Offload operation copying to rust This commit fixes a performance regression in the `QuantumCircuit.copy()` method which was previously using Python to copy the operations which had extra overhead to go from rust to python and vice versa. This moves that logic to exist in rust and improve the copy performance. --- crates/circuit/src/circuit_data.rs | 32 +++++++++++++++++++++++ crates/circuit/src/circuit_instruction.rs | 4 +-- qiskit/circuit/quantumcircuit.py | 5 ---- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 3d1fdf5d8fd7..92c173cd6bc8 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -601,6 +601,38 @@ impl CircuitData { res.intern_context = self.intern_context.clone(); res.data.clone_from(&self.data); res.param_table.clone_from(&self.param_table); + + 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) } diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 7927d5248e4d..d3d68291bcd5 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -633,9 +633,7 @@ pub(crate) fn operation_type_and_data_to_py( ]; if let Some(params) = params { if !params.is_empty() { - kwargs_list.push( - ("_skip_validation", true.to_object(py)) - ); + kwargs_list.push(("_skip_validation", true.to_object(py))); } } diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 42070e704e0a..8e58c9e0f7ac 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -3580,11 +3580,6 @@ def copy(self, name: str | None = None) -> typing.Self: """ cpy = self.copy_empty_like(name) cpy._data = self._data.copy() - - def memo_copy(op): - return op.copy() - - cpy._data.map_ops(memo_copy) return cpy def copy_empty_like( From ed422761a6cfb91013b8921b4f97cc183e759140 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 3 Jun 2024 15:57:01 -0400 Subject: [PATCH 21/61] Fix lint --- crates/circuit/src/circuit_data.rs | 57 ++++++++++++++++-------------- qiskit/circuit/quantumcircuit.py | 6 ++-- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 92c173cd6bc8..5bd7e6da4399 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -589,7 +589,8 @@ impl CircuitData { /// /// Returns: /// CircuitData: The shallow copy. - pub fn copy(&self, py: Python<'_>) -> PyResult { + #[pyo3(signature = (copy_instructions=true))] + pub fn copy(&self, py: Python<'_>, copy_instructions: bool) -> PyResult { let mut res = CircuitData::new( py, Some(self.qubits.bind(py)), @@ -602,36 +603,38 @@ impl CircuitData { res.data.clone_from(&self.data); res.param_table.clone_from(&self.param_table); - for inst in &mut res.data { - match &mut inst.op { - OperationType::Standard(_) => { - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; + 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::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::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; + 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) } diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 8e58c9e0f7ac..3e0303575340 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1902,7 +1902,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: @@ -2030,14 +2030,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, From 8017ca6f0c11aa65b3333799669e4bfe7da32b33 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 3 Jun 2024 16:48:11 -0400 Subject: [PATCH 22/61] Perform deepcopy in rust This commit moves the deepcopy handling to occur solely in Rust. Previously each instruction would be directly deepcopied by iterating over the circuit data. However, we can do this rust side now and doing this is more efficient because while we need to rely on Python to run a deepcopy we can skip it for the Rust standard gates and rely on Rust to copy those gates. --- crates/circuit/src/circuit_data.rs | 39 +++++++++++++++++++++++++++--- qiskit/circuit/quantumcircuit.py | 3 +-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 5bd7e6da4399..34c7d80e801e 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -589,8 +589,8 @@ impl CircuitData { /// /// Returns: /// CircuitData: The shallow copy. - #[pyo3(signature = (copy_instructions=true))] - pub fn copy(&self, py: Python<'_>, copy_instructions: bool) -> 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.bind(py)), @@ -603,7 +603,40 @@ impl CircuitData { res.data.clone_from(&self.data); res.param_table.clone_from(&self.param_table); - if copy_instructions { + 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(_) => { diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 3e0303575340..e15c080164cb 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1388,12 +1388,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 From 9e2111625fd5078c92dcaa658255849d0a60b949 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 3 Jun 2024 17:39:00 -0400 Subject: [PATCH 23/61] Fix QuantumCircuit.compose() performance regression This commit fixes a performance regression in the compose() method. This was caused by the checking for classical conditions in the method requiring eagerly converting all standard gates to a Python object. This changes the logic to do this only if we know we have a condition (which we can determine Python side now). --- crates/circuit/src/circuit_data.rs | 149 +++++++++++++++++++++-------- 1 file changed, 108 insertions(+), 41 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 34c7d80e801e..a8c65d256691 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -11,7 +11,7 @@ // that they have been altered from the originals. use crate::circuit_instruction::{ - convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction, + convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction, OperationInput, }; use crate::imports::{BUILTIN_LIST, CLBIT, QUBIT}; use crate::intern_context::{BitType, IndexType, InternContext}; @@ -604,7 +604,9 @@ impl CircuitData { res.param_table.clone_from(&self.param_table); if deepcopy { - let deepcopy = py.import_bound(intern!(py, "copy"))?.getattr(intern!(py, "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(_) => { @@ -813,6 +815,12 @@ 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 @@ -821,23 +829,50 @@ impl CircuitData { #[pyo3(signature = (func))] pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter_mut() { - let old_op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - &inst.label, - &inst.duration, - &inst.unit, - &inst.condition, - )?; - let new_op = func.call1((old_op,))?; - let new_inst_details = convert_py_to_operation_type(py, new_op.into())?; - inst.op = new_inst_details.operation; - inst.params = new_inst_details.params; - inst.label = new_inst_details.label; - inst.duration = new_inst_details.duration; - inst.unit = new_inst_details.unit; - inst.condition = new_inst_details.condition; + let old_op = match &inst.op { + OperationType::Standard(op) => { + if inst.condition.is_some() { + operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + &inst.label, + &inst.duration, + &inst.unit, + &inst.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; + inst.label = new_inst_details.label; + inst.duration = new_inst_details.duration; + inst.unit = new_inst_details.unit; + inst.condition = new_inst_details.condition; + } + } } Ok(()) } @@ -845,6 +880,12 @@ 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 @@ -855,29 +896,55 @@ impl CircuitData { for inst in self.data.iter_mut() { let old_op = match &inst.py_op { Some(op) => op.clone_ref(py), - None => { - let new_op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - &inst.label, - &inst.duration, - &inst.unit, - &inst.condition, - )?; - inst.py_op = Some(new_op.clone_ref(py)); - new_op - } + None => match &inst.op { + OperationType::Standard(op) => { + if inst.condition.is_some() { + let new_op = operation_type_and_data_to_py( + py, + &inst.op, + &inst.params, + &inst.label, + &inst.duration, + &inst.unit, + &inst.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 new_op = func.call1((old_op,))?; - let new_inst_details = convert_py_to_operation_type(py, new_op.clone().into())?; - inst.op = new_inst_details.operation; - inst.params = new_inst_details.params; - inst.label = new_inst_details.label; - inst.duration = new_inst_details.duration; - inst.unit = new_inst_details.unit; - inst.condition = new_inst_details.condition; - inst.py_op = Some(new_op.unbind()); + 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; + inst.label = new_inst_details.label; + inst.duration = new_inst_details.duration; + inst.unit = new_inst_details.unit; + inst.condition = new_inst_details.condition; + inst.py_op = Some(new_op); + } + } } Ok(()) } From 29f278f04cfec8a9cbabd635879b2e10a89d6aed Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 3 Jun 2024 18:30:00 -0400 Subject: [PATCH 24/61] Fix map_ops test case with no caching case --- crates/circuit/src/circuit_data.rs | 3 +-- test/python/circuit/test_circuit_data.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index a8c65d256691..f1ff98944515 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -934,8 +934,7 @@ impl CircuitData { inst.op = OperationType::Operation(op); } OperationInput::Object(new_op) => { - let new_inst_details = - convert_py_to_operation_type(py, new_op.clone_ref(py))?; + 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; inst.label = new_inst_details.label; diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index de9b63894300..91967107f9c9 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -187,12 +187,18 @@ 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): + _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()) From a7061d5a62a188412b0b40a48f5fbdf815cb579b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 4 Jun 2024 08:34:50 -0400 Subject: [PATCH 25/61] Fix typos in docs This commit fixes several docs typos that were caught during code review. Co-authored-by: Eli Arbel <46826214+eliarbel@users.noreply.github.com> --- crates/circuit/README.md | 4 ++-- crates/circuit/src/imports.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/circuit/README.md b/crates/circuit/README.md index f84bbdc9c5eb..b29660ee0033 100644 --- a/crates/circuit/README.md +++ b/crates/circuit/README.md @@ -30,7 +30,7 @@ enum. The `OperationType` enum has four variants which are used to define the di 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 enuerates all the gates in the library and statically defines all the gate properties + 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, @@ -53,7 +53,7 @@ operation objects that can be on a circuit: 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 defined methods to access the standard data +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. ## ParameterTable diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index e7aa6c2972fb..05cd7b59b91f 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -40,7 +40,7 @@ pub static SINGLETON_GATE: GILOnceCell = GILOnceCell::new(); /// qiskit.circuit.singleton.SingletonControlledGate pub static SINGLETON_CONTROLLED_GATE: GILOnceCell = GILOnceCell::new(); -/// A mapping from the enum varian in crate::operations::StandardGate to the python +/// 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. From 42d5a48770254881f124a27620619a0128db9b88 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 4 Jun 2024 13:46:27 -0400 Subject: [PATCH 26/61] Shrink memory usage for extra mutable instruction state This commit changes how we store the extra mutable instruction state (condition, duration, unit, and label) for each `CircuitInstruction` and `PackedInstruction` in the circuit. Previously it was all stored as separate `Option` fields on the struct, which required at least a pointer's width for each field which was wasted space the majority of the time as using these fields are not common. To optimize the memory layout of the struct this moves these attributes to a new struct which is put in an `Option>` which reduces it from 4 pointer widths down to 1 per object. This comes from extra runtime cost from the extra layer of pointer indirection but as this is the uncommon path this tradeoff is fine. --- crates/circuit/src/circuit_data.rs | 230 ++++++++++++++++------ crates/circuit/src/circuit_instruction.rs | 146 +++++++++----- crates/circuit/src/dag_node.rs | 40 +++- 3 files changed, 298 insertions(+), 118 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index f1ff98944515..0bf1021cbbe5 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -11,7 +11,8 @@ // that they have been altered from the originals. use crate::circuit_instruction::{ - convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction, OperationInput, + convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction, + ExtraInstructionAttributes, OperationInput, }; use crate::imports::{BUILTIN_LIST, CLBIT, QUBIT}; use crate::intern_context::{BitType, IndexType, InternContext}; @@ -39,10 +40,7 @@ struct PackedInstruction { /// The index under which the interner has stored `clbits`. clbits_id: IndexType, params: Option>, - label: Option, - duration: Option, - unit: Option, - condition: Option, + extra_attrs: Option>, #[cfg(feature = "cache_pygates")] py_op: Option, } @@ -246,10 +244,7 @@ impl CircuitData { qubits, clbits: clbits.into(), params, - label: None, - duration: None, - unit: None, - condition: None, + extra_attrs: None, #[cfg(feature = "cache_pygates")] py_op: None, }, @@ -713,14 +708,33 @@ impl CircuitData { #[pyo3(signature = (func))] pub fn foreach_op(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter() { + 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, - &inst.label, - &inst.duration, - &inst.unit, - &inst.condition, + label, + duration, + unit, + condition, )?; func.call1((op,))?; } @@ -739,14 +753,32 @@ impl CircuitData { 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, - &inst.label, - &inst.duration, - &inst.unit, - &inst.condition, + label, + duration, + unit, + condition, )?; inst.py_op = Some(new_op.clone_ref(py)); new_op @@ -767,14 +799,33 @@ impl CircuitData { #[pyo3(signature = (func))] pub fn foreach_op_indexed(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for (index, inst) in self.data.iter().enumerate() { + 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, - &inst.label, - &inst.duration, - &inst.unit, - &inst.condition, + label, + duration, + unit, + condition, )?; func.call1((index, op))?; } @@ -794,14 +845,32 @@ impl CircuitData { 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, - &inst.label, - &inst.duration, - &inst.unit, - &inst.condition, + label, + duration, + unit, + condition, )?; inst.py_op = Some(new_op.clone_ref(py)); new_op @@ -831,15 +900,33 @@ impl CircuitData { for inst in self.data.iter_mut() { let old_op = match &inst.op { OperationType::Standard(op) => { - if inst.condition.is_some() { + 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, - &inst.label, - &inst.duration, - &inst.unit, - &inst.condition, + label, + duration, + unit, + condition, )? } else { op.into_py(py) @@ -867,10 +954,18 @@ impl CircuitData { let new_inst_details = convert_py_to_operation_type(py, new_op)?; inst.op = new_inst_details.operation; inst.params = new_inst_details.params; - inst.label = new_inst_details.label; - inst.duration = new_inst_details.duration; - inst.unit = new_inst_details.unit; - inst.condition = new_inst_details.condition; + 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, + })) + } } } } @@ -898,15 +993,33 @@ impl CircuitData { Some(op) => op.clone_ref(py), None => match &inst.op { OperationType::Standard(op) => { - if inst.condition.is_some() { + 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, - &inst.label, - &inst.duration, - &inst.unit, - &inst.condition, + label, + duration, + unit, + condition, )?; inst.py_op = Some(new_op.clone_ref(py)); new_op @@ -937,10 +1050,18 @@ impl CircuitData { 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; - inst.label = new_inst_details.label; - inst.duration = new_inst_details.duration; - inst.unit = new_inst_details.unit; - inst.condition = new_inst_details.condition; + 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); } } @@ -1256,10 +1377,7 @@ impl CircuitData { qubits_id: self.intern_context.intern(qubits)?, clbits_id: self.intern_context.intern(clbits)?, params: inst.params.clone(), - label: inst.label.clone(), - duration: inst.duration.clone(), - unit: inst.unit.clone(), - condition: inst.condition.clone(), + extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] py_op: inst.py_op.clone(), }); @@ -1313,9 +1431,6 @@ impl CircuitData { } fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - for packed in self.data.iter() { - visit.call(&packed.duration)?; - } for bit in self.qubits_native.iter().chain(self.clbits_native.iter()) { visit.call(bit)?; } @@ -1555,10 +1670,7 @@ impl CircuitData { qubits_id: interned_bits(&self.qubit_indices_native, inst.qubits.bind(py))?, clbits_id: interned_bits(&self.clbit_indices_native, inst.clbits.bind(py))?, params: inst.params.clone(), - label: inst.label.clone(), - duration: inst.duration.clone(), - unit: inst.unit.clone(), - condition: inst.condition.clone(), + extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] py_op: inst.py_op.clone(), }) @@ -1586,10 +1698,7 @@ impl CircuitData { qubits_id: interned_bits(&self.qubit_indices_native, inst.qubits.bind(py))?, clbits_id: interned_bits(&self.clbit_indices_native, inst.clbits.bind(py))?, params: inst.params.clone(), - label: inst.label.clone(), - duration: inst.duration.clone(), - unit: inst.unit.clone(), - condition: inst.condition.clone(), + extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] py_op: inst.py_op.clone(), }) @@ -1619,10 +1728,7 @@ impl CircuitData { ) .unbind(), params: inst.params.clone(), - label: inst.label.clone(), - duration: inst.duration.clone(), - unit: inst.unit.clone(), - condition: inst.condition.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 d3d68291bcd5..86b744dfdbf5 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -23,6 +23,18 @@ use crate::imports::{ }; 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, +} + /// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and /// various operands. /// @@ -66,10 +78,7 @@ pub struct CircuitInstruction { #[pyo3(get)] pub clbits: Py, pub params: Option>, - pub label: Option, - pub duration: Option, - pub unit: Option, - pub condition: Option, + pub extra_attrs: Option>, #[cfg(feature = "cache_pygates")] pub py_op: Option, } @@ -127,6 +136,18 @@ impl CircuitInstruction { } } + 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); @@ -135,10 +156,7 @@ impl CircuitInstruction { qubits: as_tuple(py, qubits)?, clbits: as_tuple(py, clbits)?, params, - label, - duration, - unit, - condition, + extra_attrs, #[cfg(feature = "cache_pygates")] py_op: None, }) @@ -150,10 +168,7 @@ impl CircuitInstruction { qubits: as_tuple(py, qubits)?, clbits: as_tuple(py, clbits)?, params, - label, - duration, - unit, - condition, + extra_attrs, #[cfg(feature = "cache_pygates")] py_op: None, }) @@ -165,10 +180,7 @@ impl CircuitInstruction { qubits: as_tuple(py, qubits)?, clbits: as_tuple(py, clbits)?, params, - label, - duration, - unit, - condition, + extra_attrs, #[cfg(feature = "cache_pygates")] py_op: None, }) @@ -180,16 +192,28 @@ impl CircuitInstruction { qubits: as_tuple(py, qubits)?, clbits: as_tuple(py, clbits)?, params, - label, - duration, - unit, - condition, + 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); @@ -198,10 +222,7 @@ impl CircuitInstruction { qubits: as_tuple(py, qubits)?, clbits: as_tuple(py, clbits)?, params: op.params, - label: op.label, - duration: op.duration, - unit: op.unit, - condition: op.condition, + extra_attrs, #[cfg(feature = "cache_pygates")] py_op: Some(old_op.clone_ref(py)), }) @@ -213,10 +234,7 @@ impl CircuitInstruction { qubits: as_tuple(py, qubits)?, clbits: as_tuple(py, clbits)?, params: op.params, - label: op.label, - duration: op.duration, - unit: op.unit, - condition: op.condition, + extra_attrs, #[cfg(feature = "cache_pygates")] py_op: Some(old_op.clone_ref(py)), }) @@ -228,10 +246,7 @@ impl CircuitInstruction { qubits: as_tuple(py, qubits)?, clbits: as_tuple(py, clbits)?, params: op.params, - label: op.label, - duration: op.duration, - unit: op.unit, - condition: op.condition, + extra_attrs, #[cfg(feature = "cache_pygates")] py_op: Some(old_op.clone_ref(py)), }) @@ -243,10 +258,7 @@ impl CircuitInstruction { qubits: as_tuple(py, qubits)?, clbits: as_tuple(py, clbits)?, params: op.params, - label: op.label, - duration: op.duration, - unit: op.unit, - condition: op.condition, + extra_attrs, #[cfg(feature = "cache_pygates")] py_op: Some(old_op.clone_ref(py)), }) @@ -318,21 +330,33 @@ impl CircuitInstruction { let label = match label { Some(label) => Some(label), - None => self.label.clone(), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.label.clone(), + None => None, + }, }; let duration = match duration { Some(duration) => Some(duration), - None => self.duration.clone(), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.duration.clone(), + None => None, + }, }; let unit: Option = match unit { Some(unit) => Some(unit), - None => self.unit.clone(), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.unit.clone(), + None => None, + }, }; let condition: Option = match condition { Some(condition) => Some(condition), - None => self.condition.clone(), + None => match &self.extra_attrs { + Some(extra_attrs) => extra_attrs.condition.clone(), + None => None, + }, }; CircuitInstruction::new( @@ -363,10 +387,18 @@ impl CircuitInstruction { self.params = op.params; self.qubits = state.get_item(1)?.extract()?; self.clbits = state.get_item(2)?.extract()?; - self.label = op.label; - self.duration = op.duration; - self.unit = op.unit; - self.condition = op.condition; + 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(()) } @@ -588,14 +620,32 @@ pub(crate) fn operation_type_to_py( py: Python, circuit_inst: &CircuitInstruction, ) -> PyResult { + let label; + let duration; + let unit; + let condition; + match &circuit_inst.extra_attrs { + None => { + label = None; + duration = None; + unit = None; + condition = None; + } + Some(extra_attrs) => { + label = extra_attrs.label.clone(); + duration = extra_attrs.duration.clone(); + unit = extra_attrs.unit.clone(); + condition = extra_attrs.condition.clone(); + } + } operation_type_and_data_to_py( py, &circuit_inst.operation, &circuit_inst.params, - &circuit_inst.label, - &circuit_inst.duration, - &circuit_inst.unit, - &circuit_inst.condition, + &label, + &duration, + &unit, + &condition, ) } diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 9ac717b0c3ad..c8b6a4c8b082 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -12,6 +12,7 @@ use crate::circuit_instruction::{ convert_py_to_operation_type, operation_type_to_py, CircuitInstruction, + ExtraInstructionAttributes, }; use crate::operations::Operation; use pyo3::prelude::*; @@ -111,6 +112,21 @@ impl DAGOpNode { }; 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 { @@ -118,10 +134,7 @@ impl DAGOpNode { qubits: qargs.unbind(), clbits: cargs.unbind(), params: res.params, - label: res.label, - duration: res.duration, - unit: res.unit, - condition: res.condition, + extra_attrs, #[cfg(feature = "cache_pygates")] py_op: Some(op), }, @@ -162,10 +175,21 @@ impl DAGOpNode { let res = convert_py_to_operation_type(py, op)?; self.instruction.operation = res.operation; self.instruction.params = res.params; - self.instruction.label = res.label; - self.instruction.duration = res.duration; - self.instruction.unit = res.unit; - self.instruction.condition = res.condition; + 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(()) } From 1ac5d4a643d72e0a44f0c0ebab244f1375dfac9f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 4 Jun 2024 15:22:56 -0400 Subject: [PATCH 27/61] Remove Option<> from params field in CircuitInstruction This commit removes the Option<> from the params field in CircuitInstruction. There is no real distinction between an empty vec and None in this case, so the option just added another layer in the API that we didn't need to deal with. Also depending on the memory alignment using an Option might have ended up in a little extra memory usage too, so removing it removes that potential source of overhead. --- crates/circuit/src/circuit_data.rs | 16 ++++----- crates/circuit/src/circuit_instruction.rs | 41 +++++++++-------------- crates/circuit/src/operations.rs | 30 ++++++++--------- qiskit/circuit/quantumcircuit.py | 3 ++ 4 files changed, 42 insertions(+), 48 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 0bf1021cbbe5..abc9cb8b07d5 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -39,7 +39,7 @@ struct PackedInstruction { qubits_id: IndexType, /// The index under which the interner has stored `clbits`. clbits_id: IndexType, - params: Option>, + params: SmallVec<[Param; 3]>, extra_attrs: Option>, #[cfg(feature = "cache_pygates")] py_op: Option, @@ -170,7 +170,7 @@ pub struct CircuitData { global_phase: Param, } -type InstructionEntryType<'a> = (OperationType, Option<&'a [Param]>, &'a [u32]); +type InstructionEntryType<'a> = (OperationType, &'a [Param], &'a [u32]); impl CircuitData { /// A helper method to build a new CircuitData from an owned definition @@ -235,8 +235,7 @@ impl CircuitData { .unbind(); let empty: [u8; 0] = []; let clbits = PyTuple::new_bound(py, empty); - let params: Option> = - params.as_ref().map(|p| p.iter().cloned().collect()); + let params: SmallVec<[Param; 3]> = params.iter().cloned().collect(); let inst = res.pack_owned( py, &CircuitInstruction { @@ -294,8 +293,8 @@ impl CircuitData { // Update the parameter table let mut new_param = false; let inst_params = &self.data[inst_index].params; - if let Some(raw_params) = inst_params { - let params: Vec<(usize, PyObject)> = raw_params + if !inst_params.is_empty() { + let params: Vec<(usize, PyObject)> = inst_params .iter() .enumerate() .filter_map(|(idx, x)| match x { @@ -373,8 +372,9 @@ impl CircuitData { .discard_references(uuid, inst_index, param_index, name); } } - } else if let Some(raw_params) = &self.data[inst_index].params { - let params: Vec<(usize, PyObject)> = raw_params + } 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 { diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 86b744dfdbf5..efff3331dab2 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -15,7 +15,7 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyList, PyTuple, PyType}; use pyo3::{intern, IntoPy, PyObject, PyResult}; -use smallvec::SmallVec; +use smallvec::{smallvec, SmallVec}; use crate::imports::{ get_std_gate_class, populate_std_gate_map, GATE, INSTRUCTION, OPERATION, @@ -77,7 +77,7 @@ pub struct CircuitInstruction { /// A sequence of the classical bits that this operation reads from or writes to. #[pyo3(get)] pub clbits: Py, - pub params: Option>, + pub params: SmallVec<[Param; 3]>, pub extra_attrs: Option>, #[cfg(feature = "cache_pygates")] pub py_op: Option, @@ -102,12 +102,13 @@ pub enum OperationInput { 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 new( py: Python<'_>, operation: OperationInput, qubits: Option<&Bound>, clbits: Option<&Bound>, - params: Option>, + params: SmallVec<[Param; 3]>, label: Option, duration: Option, unit: Option, @@ -324,7 +325,7 @@ impl CircuitInstruction { }; let params = match params { - Some(params) => Some(params), + Some(params) => params, None => self.params.clone(), }; @@ -508,13 +509,11 @@ impl CircuitInstruction { if let OperationType::Standard(other) = &v.operation { if op != other { false - } else if let Some(self_params) = &self_.params { - if v.params.is_none() { - return Ok(Some(false)); - } - let other_params = v.params.as_ref().unwrap(); + } else { + let other_params = &v.params; let mut out = true; - for (param_a, param_b) in self_params.iter().zip(other_params) { + 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 { @@ -552,8 +551,6 @@ impl CircuitInstruction { } } out - } else { - v.params.is_none() } } else { false @@ -657,7 +654,7 @@ pub(crate) fn operation_type_to_py( pub(crate) fn operation_type_and_data_to_py( py: Python, operation: &OperationType, - params: &Option>, + params: &SmallVec<[Param; 3]>, label: &Option, duration: &Option, unit: &Option, @@ -667,12 +664,8 @@ pub(crate) fn operation_type_and_data_to_py( OperationType::Standard(op) => { let gate_class: &PyObject = &get_std_gate_class(py, *op)?; - let args = if let Some(params) = ¶ms { - if params.is_empty() { - PyTuple::empty_bound(py) - } else { - PyTuple::new_bound(py, params) - } + let args = if params.is_empty() { + PyTuple::empty_bound(py) } else { PyTuple::new_bound(py, params) }; @@ -681,10 +674,8 @@ pub(crate) fn operation_type_and_data_to_py( ("unit", unit.to_object(py)), ("duration", duration.to_object(py)), ]; - if let Some(params) = params { - if !params.is_empty() { - kwargs_list.push(("_skip_validation", true.to_object(py))); - } + if !params.is_empty() { + kwargs_list.push(("_skip_validation", true.to_object(py))); } let kwargs = kwargs_list.into_py_dict_bound(py); @@ -706,7 +697,7 @@ pub(crate) fn operation_type_and_data_to_py( #[derive(Debug)] pub(crate) struct OperationTypeConstruct { pub operation: OperationType, - pub params: Option>, + pub params: SmallVec<[Param; 3]>, pub label: Option, pub duration: Option, pub unit: Option, @@ -957,7 +948,7 @@ pub(crate) fn convert_py_to_operation_type( if op_type.is_subclass(operation_class)? { let params = match py_op.getattr(py, intern!(py, "params")).ok() { Some(value) => value.extract(py)?, - None => None, + None => smallvec![], }; let label = None; let duration = None; diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index a74da2783945..47b2694e7e0f 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -435,7 +435,7 @@ impl Operation for StandardGate { 0, &[( OperationType::Standard(Self::PhaseGate), - Some(&[Param::Float(PI)]), + &[Param::Float(PI)], &[0], )], FLOAT_ZERO, @@ -451,11 +451,11 @@ impl Operation for StandardGate { 0, &[( OperationType::Standard(Self::UGate), - Some(&[ + &[ Param::Float(PI), Param::Float(PI / 2.), Param::Float(PI / 2.), - ]), + ], &[0], )], FLOAT_ZERO, @@ -471,7 +471,7 @@ impl Operation for StandardGate { 0, &[( OperationType::Standard(Self::UGate), - Some(&[Param::Float(PI), Param::Float(0.), Param::Float(PI)]), + &[Param::Float(PI), Param::Float(0.), Param::Float(PI)], &[0], )], FLOAT_ZERO, @@ -488,9 +488,9 @@ impl Operation for StandardGate { 2, 0, &[ - (OperationType::Standard(Self::HGate), None, &q1), - (OperationType::Standard(Self::CXGate), None, &q0_1), - (OperationType::Standard(Self::HGate), None, &q1), + (OperationType::Standard(Self::HGate), &[], &q1), + (OperationType::Standard(Self::CXGate), &[], &q0_1), + (OperationType::Standard(Self::HGate), &[], &q1), ], FLOAT_ZERO, ) @@ -512,7 +512,7 @@ impl Operation for StandardGate { 0, &[( OperationType::Standard(Self::PhaseGate), - Some(&[Param::Float(*theta)]), + &[Param::Float(*theta)], &[0], )], Param::Float(-0.5 * theta), @@ -526,7 +526,7 @@ impl Operation for StandardGate { 0, &[( OperationType::Standard(Self::PhaseGate), - Some(&[Param::ParameterExpression(theta.clone_ref(py))]), + &[Param::ParameterExpression(theta.clone_ref(py))], &[0], )], Param::ParameterExpression( @@ -548,9 +548,9 @@ impl Operation for StandardGate { 2, 0, &[ - (OperationType::Standard(Self::CXGate), None, &[0, 1]), - (OperationType::Standard(Self::CXGate), None, &[1, 0]), - (OperationType::Standard(Self::CXGate), None, &[0, 1]), + (OperationType::Standard(Self::CXGate), &[], &[0, 1]), + (OperationType::Standard(Self::CXGate), &[], &[1, 0]), + (OperationType::Standard(Self::CXGate), &[], &[0, 1]), ], FLOAT_ZERO, ) @@ -573,7 +573,7 @@ impl Operation for StandardGate { 0, &[( OperationType::Standard(Self::UGate), - Some(&[Param::Float(PI / 2.), Param::Float(0.), Param::Float(PI)]), + &[Param::Float(PI / 2.), Param::Float(0.), Param::Float(PI)], &[0], )], FLOAT_ZERO, @@ -589,11 +589,11 @@ impl Operation for StandardGate { 0, &[( OperationType::Standard(Self::UGate), - Some(&[ + &[ Param::Float(0.), Param::Float(0.), params.unwrap()[0].clone(), - ]), + ], &[0], )], FLOAT_ZERO, diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index e15c080164cb..7af7fa214c8e 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2312,6 +2312,9 @@ def _append_standard_gate( """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: From 0398e6a21df247b62df7434b9981c7896f2f70ae Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 4 Jun 2024 16:57:34 -0400 Subject: [PATCH 28/61] Eagerly construct rust python wrappers in .append() This commit updates the Python code in QuantumCircuit.append() method to eagerly construct the rust wrapper objects for python defined circuit operations. --- crates/circuit/src/operations.rs | 28 +++++++++++++++++++++++++++ qiskit/circuit/quantumcircuit.py | 33 ++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 47b2694e7e0f..7f8cdd34120c 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -623,6 +623,20 @@ pub struct PyInstruction { 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() @@ -780,6 +794,20 @@ pub struct PyOperation { 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() diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index bfa52048676e..da1cb2476086 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -37,7 +37,7 @@ ) import numpy as np from qiskit._accelerate.circuit import CircuitData -from qiskit._accelerate.circuit import StandardGate +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 @@ -2427,9 +2427,38 @@ 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 From 80651846d771cade8f64238eaee81baec5e773e9 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 4 Jun 2024 17:53:43 -0400 Subject: [PATCH 29/61] Simplify code around handling python errors in rust --- crates/circuit/src/circuit_instruction.rs | 170 +++++----------------- 1 file changed, 35 insertions(+), 135 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index efff3331dab2..0adb46efbcc1 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -722,12 +722,12 @@ pub(crate) fn convert_py_to_operation_type( None => op_obj.unbind(), }; let op_type: Bound = raw_op_type.into_bound(py); - let mut standard: Option = match op_type.getattr(attr).ok() { - Some(stdgate) => match stdgate.extract().ok() { + let mut standard: Option = match op_type.getattr(attr) { + Ok(stdgate) => match stdgate.extract().ok() { Some(gate) => gate, None => None, }, - 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 @@ -769,31 +769,11 @@ pub(crate) fn convert_py_to_operation_type( populate_std_gate_map(py, op, base_class); return Ok(OperationTypeConstruct { operation: OperationType::Standard(op), - params: py_op - .getattr(py, intern!(py, "params")) - .ok() - .unwrap() - .extract(py)?, - label: py_op - .getattr(py, intern!(py, "label")) - .ok() - .unwrap() - .extract(py)?, - duration: py_op - .getattr(py, intern!(py, "duration")) - .ok() - .unwrap() - .extract(py)?, - unit: py_op - .getattr(py, intern!(py, "unit")) - .ok() - .unwrap() - .extract(py)?, - condition: py_op - .getattr(py, intern!(py, "condition")) - .ok() - .unwrap() - .extract(py)?, + 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)?, }); } let gate_class = GATE @@ -807,54 +787,20 @@ pub(crate) fn convert_py_to_operation_type( .bind(py); if op_type.is_subclass(gate_class)? { - let params = py_op - .getattr(py, intern!(py, "params")) - .ok() - .unwrap() - .extract(py)?; - let label = py_op - .getattr(py, intern!(py, "label")) - .ok() - .unwrap() - .extract(py)?; - let duration = py_op - .getattr(py, intern!(py, "duration")) - .ok() - .unwrap() - .extract(py)?; - let unit = py_op - .getattr(py, intern!(py, "unit")) - .ok() - .unwrap() - .extract(py)?; - let condition = py_op - .getattr(py, intern!(py, "condition")) - .ok() - .unwrap() - .extract(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")) - .ok() - .map(|x| x.extract(py).unwrap()) - .unwrap_or(0), - clbits: py_op - .getattr(py, intern!(py, "num_clbits")) - .ok() - .map(|x| x.extract(py).unwrap()) - .unwrap_or(0), + 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")) - .ok() - .unwrap() + .getattr(py, intern!(py, "params"))? .downcast_bound::(py)? .len() as u32, - op_name: py_op - .getattr(py, intern!(py, "name")) - .ok() - .unwrap() - .extract(py)?, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, gate: py_op, }; return Ok(OperationTypeConstruct { @@ -876,54 +822,20 @@ pub(crate) fn convert_py_to_operation_type( }) .bind(py); if op_type.is_subclass(instruction_class)? { - let params = py_op - .getattr(py, intern!(py, "params")) - .ok() - .unwrap() - .extract(py)?; - let label = py_op - .getattr(py, intern!(py, "label")) - .ok() - .unwrap() - .extract(py)?; - let duration = py_op - .getattr(py, intern!(py, "duration")) - .ok() - .unwrap() - .extract(py)?; - let unit = py_op - .getattr(py, intern!(py, "unit")) - .ok() - .unwrap() - .extract(py)?; - let condition = py_op - .getattr(py, intern!(py, "condition")) - .ok() - .unwrap() - .extract(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")) - .ok() - .map(|x| x.extract(py).unwrap()) - .unwrap_or(0), - clbits: py_op - .getattr(py, intern!(py, "num_clbits")) - .ok() - .map(|x| x.extract(py).unwrap()) - .unwrap_or(0), + 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")) - .ok() - .unwrap() + .getattr(py, intern!(py, "params"))? .downcast_bound::(py)? .len() as u32, - op_name: py_op - .getattr(py, intern!(py, "name")) - .ok() - .unwrap() - .extract(py)?, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, instruction: py_op, }; return Ok(OperationTypeConstruct { @@ -946,34 +858,22 @@ pub(crate) fn convert_py_to_operation_type( }) .bind(py); if op_type.is_subclass(operation_class)? { - let params = match py_op.getattr(py, intern!(py, "params")).ok() { - Some(value) => value.extract(py)?, - None => smallvec![], + 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")) - .ok() - .map(|x| x.extract(py).unwrap()) - .unwrap_or(0), - clbits: py_op - .getattr(py, intern!(py, "num_clbits")) - .ok() - .map(|x| x.extract(py).unwrap()) - .unwrap_or(0), - params: match py_op.getattr(py, intern!(py, "params")).ok() { - Some(value) => value.downcast_bound::(py)?.len() as u32, - None => 0, + 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")) - .ok() - .unwrap() - .extract(py)?, + op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, operation: py_op, }; return Ok(OperationTypeConstruct { From 39be17b4d5ad2e40a89935d8d960f6d90cbeadd3 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 5 Jun 2024 09:31:35 -0400 Subject: [PATCH 30/61] Revert "Skip validation on gate creation from rust" This reverts commit 2f81bde8bf32b06b4165048896eabdb36470814d. The validation skipping was unsound in some cases and could lead to invalid circuit being generated. If we end up needing this as an optimization we can remove this in the future in a follow-up PR that explores this in isolation. --- crates/circuit/src/circuit_instruction.rs | 10 +++------- qiskit/circuit/gate.py | 7 +------ .../library/standard_gates/global_phase.py | 18 ++---------------- qiskit/circuit/library/standard_gates/p.py | 18 ++---------------- qiskit/circuit/library/standard_gates/rx.py | 18 ++---------------- qiskit/circuit/library/standard_gates/ry.py | 18 ++---------------- qiskit/circuit/library/standard_gates/rz.py | 18 ++---------------- qiskit/circuit/library/standard_gates/u.py | 11 +---------- qiskit/circuit/quantumcircuit.py | 2 +- 9 files changed, 16 insertions(+), 104 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 0adb46efbcc1..30f2d7164791 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -669,16 +669,12 @@ pub(crate) fn operation_type_and_data_to_py( } else { PyTuple::new_bound(py, params) }; - let mut kwargs_list = vec![ + let kwargs = [ ("label", label.to_object(py)), ("unit", unit.to_object(py)), ("duration", duration.to_object(py)), - ]; - if !params.is_empty() { - kwargs_list.push(("_skip_validation", true.to_object(py))); - } - - let kwargs = kwargs_list.into_py_dict_bound(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")?; diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index 5e644a2679ef..132526775860 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -18,7 +18,6 @@ from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.exceptions import CircuitError -from qiskit._accelerate.circuit import StandardGate from .annotated_operation import AnnotatedOperation, ControlModifier, PowerModifier from .instruction import Instruction @@ -34,7 +33,6 @@ def __init__( label: str | None = None, duration=None, unit="dt", - _skip_validation=False, ) -> None: """Create a new gate. @@ -44,7 +42,6 @@ def __init__( params: A list of parameters. label: An optional label for the gate. """ - self._skip_validation = _skip_validation self.definition = None super().__init__(name, num_qubits, 0, params, label=label, duration=duration, unit=unit) @@ -241,9 +238,7 @@ def broadcast_arguments(self, qargs: list, cargs: list) -> Iterable[tuple[list, else: raise CircuitError("This gate cannot handle %i arguments" % len(qargs)) - def validate_parameter(self, parameter, _force_validation=False): - if (isinstance(self, StandardGate) or self._skip_validation) and not _force_validation: - return parameter + def validate_parameter(self, parameter): """Gate parameters should be int, float, or ParameterExpression""" if isinstance(parameter, ParameterExpression): if len(parameter.parameters) > 0: diff --git a/qiskit/circuit/library/standard_gates/global_phase.py b/qiskit/circuit/library/standard_gates/global_phase.py index 253970ad5f9a..59d6b56373da 100644 --- a/qiskit/circuit/library/standard_gates/global_phase.py +++ b/qiskit/circuit/library/standard_gates/global_phase.py @@ -40,28 +40,14 @@ class GlobalPhaseGate(Gate): _standard_gate = StandardGate.GlobalPhaseGate def __init__( - self, - phase: ParameterValueType, - label: Optional[str] = None, - *, - duration=None, - unit="dt", - _skip_validation=False, + self, phase: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): """ Args: phase: The value of phase it takes. label: An optional label for the gate. """ - super().__init__( - "global_phase", - 0, - [phase], - label=label, - duration=duration, - unit=unit, - _skip_validation=_skip_validation, - ) + super().__init__("global_phase", 0, [phase], label=label, duration=duration, unit=unit) def _define(self): q = QuantumRegister(0, "q") diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index a1bb0214503b..1a792649feab 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -79,24 +79,10 @@ class PhaseGate(Gate): _standard_gate = StandardGate.PhaseGate def __init__( - self, - theta: ParameterValueType, - label: str | None = None, - *, - duration=None, - unit="dt", - _skip_validation=False, + self, theta: ParameterValueType, label: str | None = None, *, duration=None, unit="dt" ): """Create new Phase gate.""" - super().__init__( - "p", - 1, - [theta], - label=label, - duration=duration, - unit=unit, - _skip_validation=_skip_validation, - ) + super().__init__("p", 1, [theta], label=label, duration=duration, unit=unit) def _define(self): # pylint: disable=cyclic-import diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index 1a825485808d..5579f9d3707d 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -54,24 +54,10 @@ class RXGate(Gate): _standard_gate = StandardGate.RXGate def __init__( - self, - theta: ParameterValueType, - label: Optional[str] = None, - *, - duration=None, - unit="dt", - _skip_validation=False, + self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): """Create new RX gate.""" - super().__init__( - "rx", - 1, - [theta], - label=label, - duration=duration, - unit=unit, - _skip_validation=_skip_validation, - ) + super().__init__("rx", 1, [theta], label=label, duration=duration, unit=unit) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index 9308329e0f6b..e27398cc2960 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -53,24 +53,10 @@ class RYGate(Gate): _standard_gate = StandardGate.RYGate def __init__( - self, - theta: ParameterValueType, - label: Optional[str] = None, - *, - duration=None, - unit="dt", - _skip_validation=False, + self, theta: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): """Create new RY gate.""" - super().__init__( - "ry", - 1, - [theta], - label=label, - duration=duration, - unit=unit, - _skip_validation=_skip_validation, - ) + super().__init__("ry", 1, [theta], label=label, duration=duration, unit=unit) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index 2087d878b72d..e8ee0f976036 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -63,24 +63,10 @@ class RZGate(Gate): _standard_gate = StandardGate.RZGate def __init__( - self, - phi: ParameterValueType, - label: Optional[str] = None, - *, - duration=None, - unit="dt", - _skip_validation=False, + self, phi: ParameterValueType, label: Optional[str] = None, *, duration=None, unit="dt" ): """Create new RZ gate.""" - super().__init__( - "rz", - 1, - [phi], - label=label, - duration=duration, - unit=unit, - _skip_validation=_skip_validation, - ) + super().__init__("rz", 1, [phi], label=label, duration=duration, unit=unit) def _define(self): """ diff --git a/qiskit/circuit/library/standard_gates/u.py b/qiskit/circuit/library/standard_gates/u.py index 3af63a21c994..3495bc180f08 100644 --- a/qiskit/circuit/library/standard_gates/u.py +++ b/qiskit/circuit/library/standard_gates/u.py @@ -80,18 +80,9 @@ def __init__( *, duration=None, unit="dt", - _skip_validation=False, ): """Create new U gate.""" - super().__init__( - "u", - 1, - [theta, phi, lam], - label=label, - duration=duration, - unit=unit, - _skip_validation=_skip_validation, - ) + super().__init__("u", 1, [theta, phi, lam], label=label, duration=duration, unit=unit) def inverse(self, annotated: bool = False): r"""Return inverted U gate. diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index da1cb2476086..19d0e249ea48 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2319,7 +2319,7 @@ def _append_standard_gate( 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, _force_validation=True) + Gate.validate_parameter(op, param) instructions = InstructionSet(resource_requester=circuit_scope.resolve_classical_resource) broadcast_iter = Gate.broadcast_arguments(op, expanded_qargs, expanded_cargs) From 142d71b7a0941761b668875daa6ab1f68743438e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 5 Jun 2024 10:04:36 -0400 Subject: [PATCH 31/61] Temporarily use git for qasm3 import In Qiskit/qiskit-qasm3-import#34 the issue we're hitting caused by qiskit-qasm3-import using the private circuit attributes removed in this PR was fixed. This commit temporarily moves to installing it from git so we can fully run CI. When qiskit-qasm3-import is released we should revert this commit. --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 36985cdd7cd9..83b2d3ca085c 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 @ git+https://github.com/Qiskit/qiskit-qasm3-import python-constraint>=1.4 cvxpy scikit-learn>=0.20.0 From 39f1358932249ece0bdbb84085e3ce55acfa5583 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 5 Jun 2024 11:08:57 -0400 Subject: [PATCH 32/61] Fix lint --- test/python/circuit/test_circuit_data.py | 2 ++ test/python/qasm3/test_export.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 91967107f9c9..6fc6e8e72bd7 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -191,6 +191,8 @@ def test_map_ops(self): # 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 = [ diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index a4bbff5ad94a..f16f7e93d911 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)[0].operation qc.cx(1, 2) qc.barrier() qc.cx(0, 1) From 5139411c1b670d1774196be22eba80469ec4a74e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 5 Jun 2024 11:42:21 -0400 Subject: [PATCH 33/61] Fix lint for real (we really need to use a py312 compatible version of pylint) --- test/python/qasm3/test_export.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index f16f7e93d911..b4429213006d 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1946,7 +1946,6 @@ class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCa def test_basis_gates(self): """Teleportation with physical qubits""" qc = QuantumCircuit(3, 2) - qc.h(1)[0].operation qc.cx(1, 2) qc.barrier() qc.cx(0, 1) From 0d59bd4d92861bc401b03ae1017c8e2381881938 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 5 Jun 2024 12:03:45 -0400 Subject: [PATCH 34/61] Fix test failure caused by incorrect lint fix --- test/python/qasm3/test_export.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index b4429213006d..135e874be488 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1946,6 +1946,7 @@ class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCa def test_basis_gates(self): """Teleportation with physical qubits""" qc = QuantumCircuit(3, 2) + qc.h(1) qc.cx(1, 2) qc.barrier() qc.cx(0, 1) From cfcc3d64bb7d61dab9009e1e94ecd26fd8a69dd7 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 6 Jun 2024 14:50:41 +0100 Subject: [PATCH 35/61] Relax trait-method typing requirements --- crates/circuit/src/operations.rs | 200 ++++++++++++++----------------- 1 file changed, 93 insertions(+), 107 deletions(-) diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 7f8cdd34120c..8232634dc1bc 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -21,7 +21,6 @@ use numpy::IntoPyArray; use numpy::PyReadonlyArray2; use pyo3::prelude::*; use pyo3::{intern, IntoPy, Python}; -use smallvec::SmallVec; /// Valid types for OperationType #[derive(FromPyObject, Clone, Debug)] @@ -67,7 +66,7 @@ impl Operation for OperationType { Self::Operation(op) => op.num_params(), } } - fn matrix(&self, params: Option>) -> Option> { + fn matrix(&self, params: &[Param]) -> Option> { match self { Self::Standard(op) => op.matrix(params), Self::Gate(op) => op.matrix(params), @@ -85,7 +84,7 @@ impl Operation for OperationType { } } - fn definition(&self, params: Option>) -> Option { + fn definition(&self, params: &[Param]) -> Option { match self { Self::Standard(op) => op.definition(params), Self::Gate(op) => op.definition(params), @@ -121,8 +120,8 @@ pub trait Operation { fn num_clbits(&self) -> u32; fn num_params(&self) -> u32; fn control_flow(&self) -> bool; - fn matrix(&self, params: Option>) -> Option>; - fn definition(&self, params: Option>) -> Option; + fn matrix(&self, params: &[Param]) -> Option>; + fn definition(&self, params: &[Param]) -> Option; fn standard_gate(&self) -> Option; fn directive(&self) -> bool; } @@ -216,16 +215,17 @@ impl StandardGate { } // These pymethods are for testing: - pub fn _to_matrix(&self, py: Python, params: Option>) -> Option { - self.matrix(params).map(|x| x.into_pyarray_bound(py).into()) + 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: Option>) -> Option { - self.definition(params) + pub fn _get_definition(&self, params: Vec) -> Option { + self.definition(¶ms) } #[getter] @@ -333,97 +333,88 @@ impl Operation for StandardGate { false } - fn matrix(&self, params: Option>) -> Option> { + fn matrix(&self, params: &[Param]) -> Option> { match self { - Self::ZGate => Some(aview2(&gate_matrix::ZGATE).to_owned()), - Self::YGate => Some(aview2(&gate_matrix::YGATE).to_owned()), - Self::XGate => Some(aview2(&gate_matrix::XGATE).to_owned()), - Self::CZGate => Some(aview2(&gate_matrix::CZGATE).to_owned()), - Self::CYGate => Some(aview2(&gate_matrix::CYGATE).to_owned()), - Self::CXGate => Some(aview2(&gate_matrix::CXGATE).to_owned()), - Self::CCXGate => Some(aview2(&gate_matrix::CCXGATE).to_owned()), - Self::RXGate => { - let theta = ¶ms.unwrap()[0]; - match theta { - Param::Float(theta) => Some(aview2(&gate_matrix::rx_gate(*theta)).to_owned()), - _ => None, - } - } - Self::RYGate => { - let theta = ¶ms.unwrap()[0]; - match theta { - Param::Float(theta) => Some(aview2(&gate_matrix::ry_gate(*theta)).to_owned()), - _ => None, - } - } - Self::RZGate => { - let theta = ¶ms.unwrap()[0]; - match theta { - Param::Float(theta) => Some(aview2(&gate_matrix::rz_gate(*theta)).to_owned()), - _ => None, - } - } - Self::ECRGate => Some(aview2(&gate_matrix::ECRGATE).to_owned()), - Self::SwapGate => Some(aview2(&gate_matrix::SWAPGATE).to_owned()), - Self::SXGate => Some(aview2(&gate_matrix::SXGATE).to_owned()), - Self::GlobalPhaseGate => { - let theta = ¶ms.unwrap()[0]; - match theta { - Param::Float(theta) => { - Some(aview2(&gate_matrix::global_phase_gate(*theta)).to_owned()) - } - _ => None, - } - } - Self::IGate => Some(aview2(&gate_matrix::ONE_QUBIT_IDENTITY).to_owned()), - Self::HGate => Some(aview2(&gate_matrix::HGATE).to_owned()), - Self::PhaseGate => { - let theta = ¶ms.unwrap()[0]; - match theta { - Param::Float(theta) => { - Some(aview2(&gate_matrix::phase_gate(*theta)).to_owned()) - } - _ => None, + Self::ZGate => match params { + [] => Some(aview2(&gate_matrix::ZGATE).to_owned()), + _ => None, + }, + Self::YGate => match params { + [] => Some(aview2(&gate_matrix::YGATE).to_owned()), + _ => None, + }, + Self::XGate => match params { + [] => Some(aview2(&gate_matrix::XGATE).to_owned()), + _ => None, + }, + Self::CZGate => match params { + [] => Some(aview2(&gate_matrix::CZGATE).to_owned()), + _ => None, + }, + Self::CYGate => match params { + [] => Some(aview2(&gate_matrix::CYGATE).to_owned()), + _ => None, + }, + Self::CXGate => match params { + [] => Some(aview2(&gate_matrix::CXGATE).to_owned()), + _ => None, + }, + Self::CCXGate => match params { + [] => Some(aview2(&gate_matrix::CCXGATE).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::ECRGATE).to_owned()), + _ => None, + }, + Self::SwapGate => match params { + [] => Some(aview2(&gate_matrix::SWAPGATE).to_owned()), + _ => None, + }, + Self::SXGate => match params { + [] => Some(aview2(&gate_matrix::SXGATE).to_owned()), + _ => None, + }, + Self::GlobalPhaseGate => match params { + [Param::Float(theta)] => { + Some(aview2(&gate_matrix::global_phase_gate(*theta)).to_owned()) } - } - Self::UGate => { - let params = params.unwrap(); - let theta: Option = match params[0] { - Param::Float(val) => Some(val), - Param::ParameterExpression(_) => None, - Param::Obj(_) => None, - }; - let phi: Option = match params[1] { - Param::Float(val) => Some(val), - Param::ParameterExpression(_) => None, - Param::Obj(_) => None, - }; - let lam: Option = match params[2] { - Param::Float(val) => Some(val), - Param::ParameterExpression(_) => None, - Param::Obj(_) => None, - }; - // If let chains as needed here are unstable ignore clippy to - // workaround. Upstream rust tracking issue: - // https://github.com/rust-lang/rust/issues/53667 - #[allow(clippy::unnecessary_unwrap)] - if theta.is_none() || phi.is_none() || lam.is_none() { - None - } else { - Some( - aview2(&gate_matrix::u_gate( - theta.unwrap(), - phi.unwrap(), - lam.unwrap(), - )) - .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::HGATE).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: Option>) -> Option { + fn definition(&self, params: &[Param]) -> Option { // TODO: Add definition for completeness. This shouldn't be necessary in practice // though because nothing will rely on this in practice. match self { @@ -503,7 +494,6 @@ impl Operation for StandardGate { Self::RXGate => todo!("Add when we have R"), Self::RYGate => todo!("Add when we have R"), Self::RZGate => Python::with_gil(|py| -> Option { - let params = params.unwrap(); match ¶ms[0] { Param::Float(theta) => Some( CircuitData::build_new_from( @@ -560,7 +550,7 @@ impl Operation for StandardGate { Self::SXGate => todo!("Add when we have S dagger"), Self::GlobalPhaseGate => Python::with_gil(|py| -> Option { Some( - CircuitData::build_new_from(py, 0, 0, &[], params.unwrap()[0].clone()) + CircuitData::build_new_from(py, 0, 0, &[], params[0].clone()) .expect("Unexpected Qiskit python bug"), ) }), @@ -589,11 +579,7 @@ impl Operation for StandardGate { 0, &[( OperationType::Standard(Self::UGate), - &[ - Param::Float(0.), - Param::Float(0.), - params.unwrap()[0].clone(), - ], + &[Param::Float(0.), Param::Float(0.), params[0].clone()], &[0], )], FLOAT_ZERO, @@ -653,10 +639,10 @@ impl Operation for PyInstruction { fn control_flow(&self) -> bool { false } - fn matrix(&self, _params: Option>) -> Option> { + fn matrix(&self, _params: &[Param]) -> Option> { None } - fn definition(&self, _params: Option>) -> Option { + fn definition(&self, _params: &[Param]) -> Option { Python::with_gil(|py| -> Option { match self.instruction.getattr(py, intern!(py, "definition")) { Ok(definition) => { @@ -732,7 +718,7 @@ impl Operation for PyGate { fn control_flow(&self) -> bool { false } - fn matrix(&self, _params: Option>) -> Option> { + fn matrix(&self, _params: &[Param]) -> Option> { Python::with_gil(|py| -> Option> { match self.gate.getattr(py, intern!(py, "to_matrix")) { Ok(to_matrix) => { @@ -749,7 +735,7 @@ impl Operation for PyGate { } }) } - fn definition(&self, _params: Option>) -> Option { + fn definition(&self, _params: &[Param]) -> Option { Python::with_gil(|py| -> Option { match self.gate.getattr(py, intern!(py, "definition")) { Ok(definition) => { @@ -824,10 +810,10 @@ impl Operation for PyOperation { fn control_flow(&self) -> bool { false } - fn matrix(&self, _params: Option>) -> Option> { + fn matrix(&self, _params: &[Param]) -> Option> { None } - fn definition(&self, _params: Option>) -> Option { + fn definition(&self, _params: &[Param]) -> Option { None } fn standard_gate(&self) -> Option { From 8cfa4d0b00e214c153c9a39c120475fe91f1776e Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 6 Jun 2024 15:46:13 +0100 Subject: [PATCH 36/61] Encapsulate `GILOnceCell` initialisers to local logic --- crates/circuit/src/circuit_data.rs | 53 ++--------------- crates/circuit/src/circuit_instruction.rs | 61 +++---------------- crates/circuit/src/imports.rs | 72 ++++++++++++++++------- crates/circuit/src/operations.rs | 22 +------ 4 files changed, 68 insertions(+), 140 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index c0663b12d5c4..34d1314f698d 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -195,30 +195,14 @@ impl CircuitData { global_phase, }; if num_qubits > 0 { - let qubit_cls = QUBIT - .get_or_init(py, || { - py.import_bound("qiskit.circuit.quantumregister") - .unwrap() - .getattr("Qubit") - .unwrap() - .unbind() - }) - .bind(py); + let qubit_cls = QUBIT.get_bound(py); for _i in 0..num_qubits { let bit = qubit_cls.call0()?; res.add_qubit(py, &bit, true)?; } } if num_clbits > 0 { - let clbit_cls = CLBIT - .get_or_init(py, || { - py.import_bound("qiskit.circuit.classicalregister") - .unwrap() - .getattr("Clbit") - .unwrap() - .unbind() - }) - .bind(py); + let clbit_cls = CLBIT.get_bound(py); for _i in 0..num_clbits { let bit = clbit_cls.call0()?; res.add_clbit(py, &bit, true)?; @@ -303,16 +287,7 @@ impl CircuitData { }) .collect(); if !params.is_empty() { - let list_builtin = BUILTIN_LIST - .get_or_init(py, || { - PyModule::import_bound(py, "builtins") - .unwrap() - .getattr("list") - .unwrap() - .unbind() - }) - .bind(py); - + let list_builtin = BUILTIN_LIST.get_bound(py); 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()?; @@ -348,16 +323,7 @@ impl CircuitData { /// 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_or_init(py, || { - PyModule::import_bound(py, "builtins") - .unwrap() - .getattr("list") - .unwrap() - .unbind() - }) - .bind(py); - + let list_builtin = BUILTIN_LIST.get_bound(py); if inst_index == usize::MAX { if let Param::ParameterExpression(global_phase) = &self.global_phase { let temp: PyObject = global_phase.getattr(py, intern!(py, "parameters"))?; @@ -1484,16 +1450,7 @@ impl CircuitData { #[setter] pub fn global_phase(&mut self, py: Python, angle: Param) -> PyResult<()> { - let list_builtin = BUILTIN_LIST - .get_or_init(py, || { - PyModule::import_bound(py, "builtins") - .unwrap() - .getattr("list") - .unwrap() - .unbind() - }) - .bind(py); - + let list_builtin = BUILTIN_LIST.get_bound(py); self.remove_from_parameter_table(py, usize::MAX)?; match angle { Param::Float(angle) => { diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 30f2d7164791..4c4b3c58b0bb 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -736,28 +736,11 @@ pub(crate) fn convert_py_to_operation_type( // this check. if standard.is_some() { let mutable: bool = py_op.getattr(py, intern!(py, "mutable"))?.extract(py)?; - if mutable { - let singleton_class = SINGLETON_GATE - .get_or_init(py, || { - let singleton_mod = py.import_bound("qiskit.circuit.singleton").unwrap(); - singleton_mod.getattr("SingletonGate").unwrap().unbind() - }) - .bind(py); - let singleton_control = SINGLETON_CONTROLLED_GATE - .get_or_init(py, || { - let singleton_mod = py.import_bound("qiskit.circuit.singleton").unwrap(); - singleton_mod - .getattr("SingletonControlledGate") - .unwrap() - .unbind() - }) - .bind(py); - - if py_op_bound.is_instance(singleton_class)? - || py_op_bound.is_instance(singleton_control)? - { - standard = None; - } + 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 { @@ -772,17 +755,7 @@ pub(crate) fn convert_py_to_operation_type( condition: py_op.getattr(py, intern!(py, "condition"))?.extract(py)?, }); } - let gate_class = GATE - .get_or_init(py, || { - py.import_bound("qiskit.circuit.gate") - .unwrap() - .getattr("Gate") - .unwrap() - .unbind() - }) - .bind(py); - - if op_type.is_subclass(gate_class)? { + 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)?; @@ -808,16 +781,7 @@ pub(crate) fn convert_py_to_operation_type( condition, }); } - let instruction_class = INSTRUCTION - .get_or_init(py, || { - py.import_bound("qiskit.circuit.instruction") - .unwrap() - .getattr("Instruction") - .unwrap() - .unbind() - }) - .bind(py); - if op_type.is_subclass(instruction_class)? { + 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)?; @@ -844,16 +808,7 @@ pub(crate) fn convert_py_to_operation_type( }); } - let operation_class = OPERATION - .get_or_init(py, || { - py.import_bound("qiskit.circuit.operation") - .unwrap() - .getattr("Operation") - .unwrap() - .unbind() - }) - .bind(py); - if op_type.is_subclass(operation_class)? { + 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![], diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 05cd7b59b91f..fccd230c53c4 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -19,26 +19,58 @@ use pyo3::sync::GILOnceCell; use crate::operations::{StandardGate, STANDARD_GATE_SIZE}; -/// builtin list -pub static BUILTIN_LIST: GILOnceCell = GILOnceCell::new(); -/// qiskit.circuit.operation.Operation -pub static OPERATION: GILOnceCell = GILOnceCell::new(); -/// qiskit.circuit.instruction.Instruction -pub static INSTRUCTION: GILOnceCell = GILOnceCell::new(); -/// qiskit.circuit.gate.Gate -pub static GATE: GILOnceCell = GILOnceCell::new(); -/// qiskit.circuit.quantumregister.Qubit -pub static QUBIT: GILOnceCell = GILOnceCell::new(); -/// qiskit.circuit.classicalregister.Clbit -pub static CLBIT: GILOnceCell = GILOnceCell::new(); -/// qiskit.circuit.parameterexpression.ParameterExpression -pub static PARAMETER_EXPRESSION: GILOnceCell = GILOnceCell::new(); -/// qiskit.circuit.quantumcircuit.QuantumCircuit -pub static QUANTUM_CIRCUIT: GILOnceCell = GILOnceCell::new(); -/// qiskit.circuit.singleton.SingletonGate -pub static SINGLETON_GATE: GILOnceCell = GILOnceCell::new(); -/// qiskit.circuit.singleton.SingletonControlledGate -pub static SINGLETON_CONTROLLED_GATE: GILOnceCell = GILOnceCell::new(); +/// 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 diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 8232634dc1bc..a3ad2f9a3db7 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -135,26 +135,10 @@ pub enum Param { impl<'py> FromPyObject<'py> for Param { fn extract_bound(b: &Bound<'py, PyAny>) -> Result { - let param_class = PARAMETER_EXPRESSION - .get_or_init(b.py(), || { - PyModule::import_bound(b.py(), "qiskit.circuit.parameterexpression") - .unwrap() - .getattr("ParameterExpression") - .unwrap() - .unbind() - }) - .bind(b.py()); - let circuit_class = QUANTUM_CIRCUIT - .get_or_init(b.py(), || { - PyModule::import_bound(b.py(), "qiskit.circuit.quantumcircuit") - .unwrap() - .getattr("QuantumCircuit") - .unwrap() - .unbind() - }) - .bind(b.py()); Ok( - if b.is_instance(param_class)? || b.is_instance(circuit_class)? { + 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) From 1e3c06423718ef8e8bf1cd9e25b5383cc6b26a37 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 6 Jun 2024 14:00:23 -0400 Subject: [PATCH 37/61] Simplify Interface for building circuit of standard gates in rust --- crates/circuit/src/circuit_data.rs | 60 +++++++++------- crates/circuit/src/operations.rs | 110 +++++++++++++---------------- 2 files changed, 83 insertions(+), 87 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 34d1314f698d..c44ad96840fb 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -14,9 +14,9 @@ use crate::circuit_instruction::{ convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction, ExtraInstructionAttributes, OperationInput, }; -use crate::imports::{BUILTIN_LIST, CLBIT, QUBIT}; +use crate::imports::{BUILTIN_LIST, QUBIT}; use crate::intern_context::{BitType, IndexType, InternContext}; -use crate::operations::{OperationType, Param}; +use crate::operations::{OperationType, Param, StandardGate}; use crate::parameter_table::{ParamEntry, ParamTable}; use crate::SliceOrInt; use smallvec::SmallVec; @@ -170,25 +170,41 @@ pub struct CircuitData { global_phase: Param, } -type InstructionEntryType<'a> = (OperationType, &'a [Param], &'a [u32]); - impl CircuitData { - /// A helper method to build a new CircuitData from an owned definition - /// as a slice of OperationType, parameters, and qubits. - pub fn build_new_from( + /// 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: usize, - num_clbits: usize, - instructions: &[InstructionEntryType], + num_qubits: u32, + instructions: I, global_phase: Param, - ) -> PyResult { + ) -> PyResult + where + I: IntoIterator, SmallVec<[u32; 2]>)>, + { + let instruction_iter = instructions.into_iter(); let mut res = CircuitData { - data: Vec::with_capacity(instructions.len()), + data: Vec::with_capacity(instruction_iter.size_hint().0), intern_context: InternContext::new(), - qubits_native: Vec::with_capacity(num_qubits), - clbits_native: Vec::with_capacity(num_clbits), - qubit_indices_native: HashMap::with_capacity(num_qubits), - clbit_indices_native: HashMap::with_capacity(num_clbits), + qubits_native: Vec::with_capacity(num_qubits as usize), + clbits_native: Vec::new(), + qubit_indices_native: HashMap::with_capacity(num_qubits as usize), + clbit_indices_native: HashMap::new(), qubits: PyList::empty_bound(py).unbind(), clbits: PyList::empty_bound(py).unbind(), param_table: ParamTable::new(), @@ -201,14 +217,7 @@ impl CircuitData { res.add_qubit(py, &bit, true)?; } } - if num_clbits > 0 { - let clbit_cls = CLBIT.get_bound(py); - for _i in 0..num_clbits { - let bit = clbit_cls.call0()?; - res.add_clbit(py, &bit, true)?; - } - } - for (operation, params, qargs) in instructions { + for (operation, params, qargs) in instruction_iter { let qubits = PyTuple::new_bound( py, qargs @@ -219,11 +228,10 @@ impl CircuitData { .unbind(); let empty: [u8; 0] = []; let clbits = PyTuple::new_bound(py, empty); - let params: SmallVec<[Param; 3]> = params.iter().cloned().collect(); let inst = res.pack_owned( py, &CircuitInstruction { - operation: operation.clone(), + operation: OperationType::Standard(operation), qubits, clbits: clbits.into(), params, diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index a3ad2f9a3db7..b0dbb4ad5649 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -21,8 +21,11 @@ use numpy::IntoPyArray; use numpy::PyReadonlyArray2; use pyo3::prelude::*; use pyo3::{intern, IntoPy, Python}; +use smallvec::{smallvec, SmallVec}; -/// Valid types for OperationType +/// 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), @@ -399,20 +402,13 @@ impl Operation for StandardGate { } fn definition(&self, params: &[Param]) -> Option { - // TODO: Add definition for completeness. This shouldn't be necessary in practice - // though because nothing will rely on this in practice. match self { Self::ZGate => Python::with_gil(|py| -> Option { Some( - CircuitData::build_new_from( + CircuitData::from_standard_gates( py, 1, - 0, - &[( - OperationType::Standard(Self::PhaseGate), - &[Param::Float(PI)], - &[0], - )], + [(Self::PhaseGate, smallvec![Param::Float(PI)], smallvec![0])], FLOAT_ZERO, ) .expect("Unexpected Qiskit python bug"), @@ -420,18 +416,17 @@ impl Operation for StandardGate { }), Self::YGate => Python::with_gil(|py| -> Option { Some( - CircuitData::build_new_from( + CircuitData::from_standard_gates( py, 1, - 0, - &[( - OperationType::Standard(Self::UGate), - &[ + [( + Self::UGate, + smallvec![ Param::Float(PI), Param::Float(PI / 2.), Param::Float(PI / 2.), ], - &[0], + smallvec![0], )], FLOAT_ZERO, ) @@ -440,14 +435,13 @@ impl Operation for StandardGate { }), Self::XGate => Python::with_gil(|py| -> Option { Some( - CircuitData::build_new_from( + CircuitData::from_standard_gates( py, 1, - 0, - &[( - OperationType::Standard(Self::UGate), - &[Param::Float(PI), Param::Float(0.), Param::Float(PI)], - &[0], + [( + Self::UGate, + smallvec![Param::Float(PI), Param::Float(0.), Param::Float(PI)], + smallvec![0], )], FLOAT_ZERO, ) @@ -455,17 +449,16 @@ impl Operation for StandardGate { ) }), Self::CZGate => Python::with_gil(|py| -> Option { - let q1: Vec = vec![1]; - let q0_1: Vec = vec![0, 1]; + let q1: SmallVec<[u32; 2]> = smallvec![1]; + let q0_1: SmallVec<[u32; 2]> = smallvec![0, 1]; Some( - CircuitData::build_new_from( + CircuitData::from_standard_gates( py, 2, - 0, - &[ - (OperationType::Standard(Self::HGate), &[], &q1), - (OperationType::Standard(Self::CXGate), &[], &q0_1), - (OperationType::Standard(Self::HGate), &[], &q1), + [ + (Self::HGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1), + (Self::HGate, smallvec![], q1), ], FLOAT_ZERO, ) @@ -480,28 +473,26 @@ impl Operation for StandardGate { Self::RZGate => Python::with_gil(|py| -> Option { match ¶ms[0] { Param::Float(theta) => Some( - CircuitData::build_new_from( + CircuitData::from_standard_gates( py, 1, - 0, - &[( - OperationType::Standard(Self::PhaseGate), - &[Param::Float(*theta)], - &[0], + [( + Self::PhaseGate, + smallvec![Param::Float(*theta)], + smallvec![0], )], Param::Float(-0.5 * theta), ) .expect("Unexpected Qiskit python bug"), ), Param::ParameterExpression(theta) => Some( - CircuitData::build_new_from( + CircuitData::from_standard_gates( py, 1, - 0, - &[( - OperationType::Standard(Self::PhaseGate), - &[Param::ParameterExpression(theta.clone_ref(py))], - &[0], + [( + Self::PhaseGate, + smallvec![Param::ParameterExpression(theta.clone_ref(py))], + smallvec![0], )], Param::ParameterExpression( theta @@ -517,14 +508,13 @@ impl Operation for StandardGate { Self::ECRGate => todo!("Add when we have RZX"), Self::SwapGate => Python::with_gil(|py| -> Option { Some( - CircuitData::build_new_from( + CircuitData::from_standard_gates( py, 2, - 0, - &[ - (OperationType::Standard(Self::CXGate), &[], &[0, 1]), - (OperationType::Standard(Self::CXGate), &[], &[1, 0]), - (OperationType::Standard(Self::CXGate), &[], &[0, 1]), + [ + (Self::CXGate, smallvec![], smallvec![0, 1]), + (Self::CXGate, smallvec![], smallvec![1, 0]), + (Self::CXGate, smallvec![], smallvec![0, 1]), ], FLOAT_ZERO, ) @@ -534,21 +524,20 @@ impl Operation for StandardGate { Self::SXGate => todo!("Add when we have S dagger"), Self::GlobalPhaseGate => Python::with_gil(|py| -> Option { Some( - CircuitData::build_new_from(py, 0, 0, &[], params[0].clone()) + 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::build_new_from( + CircuitData::from_standard_gates( py, 1, - 0, - &[( - OperationType::Standard(Self::UGate), - &[Param::Float(PI / 2.), Param::Float(0.), Param::Float(PI)], - &[0], + [( + Self::UGate, + smallvec![Param::Float(PI / 2.), Param::Float(0.), Param::Float(PI)], + smallvec![0], )], FLOAT_ZERO, ) @@ -557,14 +546,13 @@ impl Operation for StandardGate { }), Self::PhaseGate => Python::with_gil(|py| -> Option { Some( - CircuitData::build_new_from( + CircuitData::from_standard_gates( py, 1, - 0, - &[( - OperationType::Standard(Self::UGate), - &[Param::Float(0.), Param::Float(0.), params[0].clone()], - &[0], + [( + Self::UGate, + smallvec![Param::Float(0.), Param::Float(0.), params[0].clone()], + smallvec![0], )], FLOAT_ZERO, ) From faac6552778621a56285a20d422baaf94c3e819b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 6 Jun 2024 16:03:44 -0400 Subject: [PATCH 38/61] Simplify complex64 creation in gate_matrix.rs This just switches Complex64::new(re, im) to be c64(re, im) to reduce the amount of typing. c64 needs to be defined inplace so it can be a const fn. --- crates/circuit/src/gate_matrix.rs | 374 +++++++++++------------------- 1 file changed, 138 insertions(+), 236 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index d53c0269b9ff..181746a5dc0c 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -13,295 +13,203 @@ use num_complex::Complex64; use std::f64::consts::FRAC_1_SQRT_2; -pub static ONE_QUBIT_IDENTITY: [[Complex64; 2]; 2] = [ - [Complex64::new(1., 0.), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(1., 0.)], -]; +// 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 = Complex64::new(half_theta.cos(), 0.); - let isin = Complex64::new(0., -half_theta.sin()); + 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 = Complex64::new(half_theta.cos(), 0.); - let sin = Complex64::new(half_theta.sin(), 0.); + 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 = Complex64::new(0., 0.5 * theta); - [ - [(-ilam2).exp(), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), ilam2.exp()], - ] + let ilam2 = c64(0., 0.5 * theta); + [[(-ilam2).exp(), c64(0., 0.)], [c64(0., 0.), ilam2.exp()]] } pub 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.), - ], + [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 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.), - ], + [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 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)], + [c64(0.5, 0.5), c64(0.5, -0.5)], + [c64(0.5, -0.5), c64(0.5, 0.5)], ]; -pub static XGATE: [[Complex64; 2]; 2] = [ - [Complex64::new(0., 0.), Complex64::new(1., 0.)], - [Complex64::new(1., 0.), Complex64::new(0., 0.)], -]; +pub static XGATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(1., 0.)], [c64(1., 0.), c64(0., 0.)]]; -pub static ZGATE: [[Complex64; 2]; 2] = [ - [Complex64::new(1., 0.), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(-1., 0.)], -]; +pub static ZGATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(-1., 0.)]]; -pub static YGATE: [[Complex64; 2]; 2] = [ - [Complex64::new(0., 0.), Complex64::new(0., -1.)], - [Complex64::new(0., 1.), Complex64::new(0., 0.)], -]; +pub static YGATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(0., -1.)], [c64(0., 1.), c64(0., 0.)]]; pub static CZGATE: [[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(1., 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(0., 0.), - Complex64::new(0., 0.), - Complex64::new(-1., 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.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(-1., 0.)], ]; pub static CYGATE: [[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(0., -1.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 0.), - Complex64::new(1., 0.), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - Complex64::new(0., 1.), - Complex64::new(0., 0.), - Complex64::new(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., -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 CCXGATE: [[Complex64; 8]; 8] = [ [ - 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(0., 0.), - ], - [ - Complex64::new(0., 0.), - 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(0., 0.), - Complex64::new(0., 0.), - 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(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(0., 0.), - Complex64::new(0., 0.), - 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(0., 0.), - Complex64::new(0., 0.), - 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(0., 0.), - Complex64::new(0., 0.), - Complex64::new(1., 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(0., 0.), - Complex64::new(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(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 ECRGATE: [[Complex64; 4]; 4] = [ [ - Complex64::new(0., 0.), - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(0., 0.), - Complex64::new(0., FRAC_1_SQRT_2), + c64(0., 0.), + c64(FRAC_1_SQRT_2, 0.), + c64(0., 0.), + c64(0., FRAC_1_SQRT_2), ], [ - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(0., 0.), - Complex64::new(0., -FRAC_1_SQRT_2), - Complex64::new(0., 0.), + c64(FRAC_1_SQRT_2, 0.), + c64(0., 0.), + c64(0., -FRAC_1_SQRT_2), + c64(0., 0.), ], [ - Complex64::new(0., 0.), - Complex64::new(0., FRAC_1_SQRT_2), - Complex64::new(0., 0.), - Complex64::new(FRAC_1_SQRT_2, 0.), + c64(0., 0.), + c64(0., FRAC_1_SQRT_2), + c64(0., 0.), + c64(FRAC_1_SQRT_2, 0.), ], [ - Complex64::new(0., -FRAC_1_SQRT_2), - Complex64::new(0., 0.), - Complex64::new(FRAC_1_SQRT_2, 0.), - Complex64::new(0., 0.), + c64(0., -FRAC_1_SQRT_2), + c64(0., 0.), + c64(FRAC_1_SQRT_2, 0.), + c64(0., 0.), ], ]; pub static SWAPGATE: [[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(1., 0.), - Complex64::new(0., 0.), - ], - [ - Complex64::new(0., 0.), - 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(1., 0.), - ], + [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] { - [[Complex64::new(0., theta).exp()]] + [[c64(0., theta).exp()]] } #[inline] pub fn phase_gate(lam: f64) -> [[Complex64; 2]; 2] { [ - [Complex64::new(1., 0.), Complex64::new(0., 0.)], - [Complex64::new(0., 0.), Complex64::new(0., lam).exp()], + [c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., lam).exp()], ] } @@ -310,13 +218,7 @@ pub fn u_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] { let cos = (theta / 2.).cos(); let sin = (theta / 2.).sin(); [ - [ - Complex64::new(cos, 0.), - (-Complex64::new(0., lam).exp()) * sin, - ], - [ - Complex64::new(0., phi).exp() * sin, - Complex64::new(0., phi + lam).exp() * cos, - ], + [c64(cos, 0.), (-c64(0., lam).exp()) * sin], + [c64(0., phi).exp() * sin, c64(0., phi + lam).exp() * cos], ] } From c9ac618e6afa508c78d6b42cd2559d88d633b9e2 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Thu, 6 Jun 2024 17:28:30 -0400 Subject: [PATCH 39/61] Simplify initialization of array of elements that are not Copy (#28) * Simplify initialization of array of elements that are not Copy * Only generate array when necessary --- crates/circuit/src/imports.rs | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index fccd230c53c4..9120d3a8675a 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -136,13 +136,7 @@ pub fn populate_std_gate_map(py: Python, rs_gate: StandardGate, py_gate: PyObjec match STDGATE_PYTHON_GATES.get_mut() { Some(gate_map) => gate_map, None => { - // A fixed size array is initialized like this because using the `[T; 5]` syntax - // requires T to be `Copy`. But `PyObject` isn't Copy so therefore Option - // as T isn't Copy. To avoid that we just list out None STANDARD_GATE_SIZE times - let array: [Option; STANDARD_GATE_SIZE] = [ - None, None, None, None, None, None, None, None, None, None, None, None, None, - None, None, None, None, None, - ]; + let array: [Option; STANDARD_GATE_SIZE] = core::array::from_fn(|_| None); STDGATE_PYTHON_GATES.set(py, array).unwrap(); STDGATE_PYTHON_GATES.get_mut().unwrap() } @@ -156,18 +150,8 @@ pub fn populate_std_gate_map(py: Python, rs_gate: StandardGate, py_gate: PyObjec #[inline] pub fn get_std_gate_class(py: Python, rs_gate: StandardGate) -> PyResult { - let gate_map = unsafe { - STDGATE_PYTHON_GATES.get_or_init(py, || { - // A fixed size array is initialized like this because using the `[T; 5]` syntax - // requires T to be `Copy`. But `PyObject` isn't Copy so therefore Option - // as T isn't Copy. To avoid that we just list out None STANDARD_GATE_SIZE times - let array: [Option; STANDARD_GATE_SIZE] = [ - None, None, None, None, None, None, None, None, None, None, None, None, None, None, - None, None, None, None, - ]; - array - }) - }; + let gate_map = + unsafe { STDGATE_PYTHON_GATES.get_or_init(py, || core::array::from_fn(|_| None)) }; let gate = &gate_map[rs_gate as usize]; let populate = gate.is_none(); let out_gate = match gate { From 7715744989c51478d8778091764679f2b873f384 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 7 Jun 2024 14:45:37 -0400 Subject: [PATCH 40/61] Fix doc typos Co-authored-by: Kevin Hartman --- CONTRIBUTING.md | 2 +- crates/circuit/README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3a01004c72c..4641c7878fc1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,7 +138,7 @@ dependency packages installed in your environment, which are listed in the ### Compile time options When building qiskit from source there are options available to control how -Qiskit is build. Right now the only option is if you set the environment +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 diff --git a/crates/circuit/README.md b/crates/circuit/README.md index b29660ee0033..ef0fd933ced0 100644 --- a/crates/circuit/README.md +++ b/crates/circuit/README.md @@ -20,7 +20,7 @@ in the instructions to reduce the memory overhead of `CircuitData`. The `PackedI 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 `ParmaeterExpression` objects for any parameters and also +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 @@ -45,7 +45,7 @@ operation objects that can be on a circuit: 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 + 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` @@ -64,6 +64,6 @@ symbolic expression that defines operations using `Parameter` objects. Each `Par 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 `ParameterEntry`, the `name` to the uuid, and the uuid to the PyObject for the actual `Parameter`. -The `ParameterEntry` 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 for the `CircuitInstruction.params` field of +The `ParameterEntry` 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 `usize::MAX` that points to the global phase property of the circuit instead of a `CircuitInstruction`. From 4a93c83b232d554ea471f74b3c8cb115278ea54a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 7 Jun 2024 15:18:29 -0400 Subject: [PATCH 41/61] Add conversion trait for OperationType -> OperationInput and simplify CircuitInstruction::replace() --- crates/circuit/src/circuit_instruction.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 914659e3a2ae..d379e0a269b2 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -141,6 +141,17 @@ impl CircuitInstruction { } } +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)] @@ -357,15 +368,7 @@ impl CircuitInstruction { unit: Option, condition: Option, ) -> PyResult { - let operation = match operation { - Some(operation) => operation, - None => match &self.operation { - OperationType::Standard(op) => OperationInput::Standard(*op), - OperationType::Gate(gate) => OperationInput::Gate(gate.clone()), - OperationType::Instruction(inst) => OperationInput::Instruction(inst.clone()), - OperationType::Operation(op) => OperationInput::Operation(op.clone()), - }, - }; + let operation = operation.unwrap_or_else(|| self.operation.clone().into()); let params = match params { Some(params) => params, From 1809a22b9c36594fe8c0cc1c72c3f12fa54a21ac Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 7 Jun 2024 15:24:34 -0400 Subject: [PATCH 42/61] Use destructuring for operation_type_to_py extra attr handling --- crates/circuit/src/circuit_instruction.rs | 27 ++++++++--------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index d379e0a269b2..ace871b68aea 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -663,24 +663,15 @@ pub(crate) fn operation_type_to_py( py: Python, circuit_inst: &CircuitInstruction, ) -> PyResult { - let label; - let duration; - let unit; - let condition; - match &circuit_inst.extra_attrs { - None => { - label = None; - duration = None; - unit = None; - condition = None; - } - Some(extra_attrs) => { - label = extra_attrs.label.clone(); - duration = extra_attrs.duration.clone(); - unit = extra_attrs.unit.clone(); - condition = extra_attrs.condition.clone(); - } - } + 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, From f8581584c82a61451010487a0971fe34d5619a16 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 7 Jun 2024 15:26:06 -0400 Subject: [PATCH 43/61] Simplify trait bounds for map_indices() The map_indices() method previously specified both Iterator and ExactSizeIterator for it's trait bounds, but Iterator is a supertrait of ExactSizeIterator and we don't need to explicitly list both. This commit removes the duplicate trait bound. --- crates/circuit/src/bit_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/circuit/src/bit_data.rs b/crates/circuit/src/bit_data.rs index f7921733c198..40540f9df5a4 100644 --- a/crates/circuit/src/bit_data.rs +++ b/crates/circuit/src/bit_data.rs @@ -151,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() } From 26bc1aeb98a89d6dda5c3f61d008950a965cced0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 7 Jun 2024 15:56:48 -0400 Subject: [PATCH 44/61] Make Qubit and Clbit newtype member public As we start to use Qubit and Clbit for creating circuits from accelerate and other crates in the Qiskit workspace we need to be able to create instances of them. However, the newtype member BitType was not public which prevented creating new Qubits. This commit fixes this by making it public. --- crates/circuit/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index c7c4ceeef001..d7f285911750 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -36,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 { From 649509fa30f519af8ec05188a7a0ad28959d026f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 8 Jun 2024 12:29:53 -0400 Subject: [PATCH 45/61] Use snakecase for gate matrix names --- crates/accelerate/src/two_qubit_decompose.rs | 16 +++++++------- crates/circuit/src/gate_matrix.rs | 22 ++++++++++---------- crates/circuit/src/operations.rs | 22 ++++++++++---------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 45fc8b90c637..f93eb2a8d99c 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -51,7 +51,7 @@ use rand::prelude::*; use rand_distr::StandardNormal; use rand_pcg::Pcg64Mcg; -use qiskit_circuit::gate_matrix::{CXGATE, HGATE, ONE_QUBIT_IDENTITY, SXGATE, XGATE}; +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; @@ -350,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) @@ -1429,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]); @@ -1437,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])); @@ -1498,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/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 181746a5dc0c..72e1087637c0 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -46,44 +46,44 @@ pub fn rz_gate(theta: f64) -> [[Complex64; 2]; 2] { [[(-ilam2).exp(), c64(0., 0.)], [c64(0., 0.), ilam2.exp()]] } -pub static HGATE: [[Complex64; 2]; 2] = [ +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 CXGATE: [[Complex64; 4]; 4] = [ +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 SXGATE: [[Complex64; 2]; 2] = [ +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 XGATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(1., 0.)], [c64(1., 0.), c64(0., 0.)]]; +pub static X_GATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(1., 0.)], [c64(1., 0.), c64(0., 0.)]]; -pub static ZGATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(-1., 0.)]]; +pub static Z_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(-1., 0.)]]; -pub static YGATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(0., -1.)], [c64(0., 1.), c64(0., 0.)]]; +pub static Y_GATE: [[Complex64; 2]; 2] = [[c64(0., 0.), c64(0., -1.)], [c64(0., 1.), c64(0., 0.)]]; -pub static CZGATE: [[Complex64; 4]; 4] = [ +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 CYGATE: [[Complex64; 4]; 4] = [ +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 CCXGATE: [[Complex64; 8]; 8] = [ +pub static CCX_GATE: [[Complex64; 8]; 8] = [ [ c64(1., 0.), c64(0., 0.), @@ -166,7 +166,7 @@ pub static CCXGATE: [[Complex64; 8]; 8] = [ ], ]; -pub static ECRGATE: [[Complex64; 4]; 4] = [ +pub static ECR_GATE: [[Complex64; 4]; 4] = [ [ c64(0., 0.), c64(FRAC_1_SQRT_2, 0.), @@ -193,7 +193,7 @@ pub static ECRGATE: [[Complex64; 4]; 4] = [ ], ]; -pub static SWAPGATE: [[Complex64; 4]; 4] = [ +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.)], diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 43b5744651c4..978fb307f1f3 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -324,31 +324,31 @@ impl Operation for StandardGate { fn matrix(&self, params: &[Param]) -> Option> { match self { Self::ZGate => match params { - [] => Some(aview2(&gate_matrix::ZGATE).to_owned()), + [] => Some(aview2(&gate_matrix::Z_GATE).to_owned()), _ => None, }, Self::YGate => match params { - [] => Some(aview2(&gate_matrix::YGATE).to_owned()), + [] => Some(aview2(&gate_matrix::Y_GATE).to_owned()), _ => None, }, Self::XGate => match params { - [] => Some(aview2(&gate_matrix::XGATE).to_owned()), + [] => Some(aview2(&gate_matrix::X_GATE).to_owned()), _ => None, }, Self::CZGate => match params { - [] => Some(aview2(&gate_matrix::CZGATE).to_owned()), + [] => Some(aview2(&gate_matrix::CZ_GATE).to_owned()), _ => None, }, Self::CYGate => match params { - [] => Some(aview2(&gate_matrix::CYGATE).to_owned()), + [] => Some(aview2(&gate_matrix::CY_GATE).to_owned()), _ => None, }, Self::CXGate => match params { - [] => Some(aview2(&gate_matrix::CXGATE).to_owned()), + [] => Some(aview2(&gate_matrix::CX_GATE).to_owned()), _ => None, }, Self::CCXGate => match params { - [] => Some(aview2(&gate_matrix::CCXGATE).to_owned()), + [] => Some(aview2(&gate_matrix::CCX_GATE).to_owned()), _ => None, }, Self::RXGate => match params { @@ -364,15 +364,15 @@ impl Operation for StandardGate { _ => None, }, Self::ECRGate => match params { - [] => Some(aview2(&gate_matrix::ECRGATE).to_owned()), + [] => Some(aview2(&gate_matrix::ECR_GATE).to_owned()), _ => None, }, Self::SwapGate => match params { - [] => Some(aview2(&gate_matrix::SWAPGATE).to_owned()), + [] => Some(aview2(&gate_matrix::SWAP_GATE).to_owned()), _ => None, }, Self::SXGate => match params { - [] => Some(aview2(&gate_matrix::SXGATE).to_owned()), + [] => Some(aview2(&gate_matrix::SX_GATE).to_owned()), _ => None, }, Self::GlobalPhaseGate => match params { @@ -386,7 +386,7 @@ impl Operation for StandardGate { _ => None, }, Self::HGate => match params { - [] => Some(aview2(&gate_matrix::HGATE).to_owned()), + [] => Some(aview2(&gate_matrix::H_GATE).to_owned()), _ => None, }, Self::PhaseGate => match params { From 067c5b40ed37c8403031fa1f9a884ca52695e508 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 8 Jun 2024 12:31:23 -0400 Subject: [PATCH 46/61] Remove pointless underscore prefix --- crates/circuit/src/circuit_data.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 3535a1428a03..89d8dc876f44 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -167,9 +167,9 @@ impl CircuitData { &mut self, py: Python, inst_index: usize, - _params: Option)>>, + params: Option)>>, ) -> PyResult { - if let Some(params) = _params { + if let Some(params) = params { let mut new_param = false; for (param_index, raw_param_objs) in ¶ms { let atomic_parameters: HashMap = raw_param_objs @@ -1211,12 +1211,12 @@ impl CircuitData { &mut self, py: Python<'_>, value: PyRef, - _params: Option)>>, + params: Option)>>, ) -> PyResult { let packed = self.pack(py, value)?; let new_index = self.data.len(); self.data.push(packed); - self.update_param_table(py, new_index, _params) + self.update_param_table(py, new_index, params) } pub fn extend(&mut self, py: Python<'_>, itr: &Bound) -> PyResult<()> { From 1231cb339e821e3a8afa2b7dbba0db00b579f90e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 9 Jun 2024 03:46:47 -0400 Subject: [PATCH 47/61] Use downcast instead of bound --- crates/circuit/src/circuit_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 89d8dc876f44..a0830bf36f99 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -1118,7 +1118,7 @@ impl CircuitData { value: &Bound, ) -> PyResult<()> { let index = self.convert_py_index(index)?; - let value: PyRef = value.extract()?; + let value: PyRef = value.downcast()?.borrow(); let mut packed = self.pack(py, value)?; std::mem::swap(&mut packed, &mut self.data[index]); Ok(()) From 0281b6c9d7f4b1dd0e0ec87e3157ee3095e33d1c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 9 Jun 2024 04:14:50 -0400 Subject: [PATCH 48/61] Rwork _append reference cycle handling This commit reworks the multiple borrow handling in the _append() method to leveraging `Bound.try_borrow()` to return a consistent error message if we're unable to borrow a CircuitInstruction in the rust code meaning there is a cyclical reference in the code. Previously we tried to detect this cycle up-front which added significant overhead for a corner case. --- crates/circuit/src/circuit_data.rs | 13 ++++++++++--- qiskit/circuit/quantumcircuit.py | 20 ++++++++------------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index a0830bf36f99..9ecb0f029d0f 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -308,6 +308,13 @@ impl CircuitData { 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] @@ -1210,10 +1217,10 @@ impl CircuitData { pub fn append( &mut self, py: Python<'_>, - value: PyRef, + value: &Bound, params: Option)>>, ) -> PyResult { - let packed = self.pack(py, value)?; + let packed = self.pack(py, value.try_borrow()?)?; let new_index = self.data.len(); self.data.push(packed); self.update_param_table(py, new_index, params) @@ -1268,7 +1275,7 @@ impl CircuitData { return Ok(()); } for v in itr.iter()? { - self.append(py, v?.extract()?, None)?; + self.append_inner(py, v?.extract()?)?; } Ok(()) } diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 5aed1b66a708..d953ba18773b 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2530,24 +2530,20 @@ def _append(self, instruction, qargs=(), cargs=(), *, _standard_gate: bool = Fal old_style = not isinstance(instruction, CircuitInstruction) if old_style: instruction = CircuitInstruction(instruction, qargs, cargs) - params = None # If there is a reference to the outer circuit in an - # instruction param we need to handle the params - # before calling the inner rust append method. This is to avoid trying - # to reference the circuit twice at the same time from rust. 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. - if hasattr(instruction.operation, "params") and any( - x is self for x in instruction.operation.params - ): + # 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) - else: - new_param = self._data.append(instruction) if new_param: # clear cache if new parameter is added self._parameters = None From c78c44ae26681565f556bb58b3461db7ef227b53 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 9 Jun 2024 04:27:48 -0400 Subject: [PATCH 49/61] Make CircuitData.global_phase_param_index a class attr --- crates/circuit/src/circuit_data.rs | 4 ++-- qiskit/circuit/quantumcircuit.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 9ecb0f029d0f..c5eabfdf9fe7 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -1375,8 +1375,8 @@ impl CircuitData { } /// Get the global_phase sentinel value - #[staticmethod] - pub fn global_phase_param_index() -> usize { + #[classattr] + pub const fn global_phase_param_index() -> usize { usize::MAX } diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index d953ba18773b..d1bff1d3fa38 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -4315,7 +4315,7 @@ def assign_parameters( # pylint: disable=missing-raises-doc else () ) for inst_index, index in references: - if inst_index == self._data.global_phase_param_index(): + if inst_index == self._data.global_phase_param_index: operation = None seen_operations[inst_index] = None assignee = target.global_phase @@ -4347,7 +4347,7 @@ 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 inst_index == self._data.global_phase_param_index(): + 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._data.global_phase = new_parameter From c61d1ad2cd153046f592d5974b5bbe0c5e92609c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 9 Jun 2024 04:31:42 -0400 Subject: [PATCH 50/61] Use &[Param] instead of &SmallVec<..> for operation_type_and_data_to_py --- crates/circuit/src/circuit_instruction.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index ace871b68aea..2bb90367082d 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -691,7 +691,7 @@ pub(crate) fn operation_type_to_py( pub(crate) fn operation_type_and_data_to_py( py: Python, operation: &OperationType, - params: &SmallVec<[Param; 3]>, + params: &[Param], label: &Option, duration: &Option, unit: &Option, From 252a089e9549fefa707dc85a22b91bce655ad188 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 9 Jun 2024 04:49:54 -0400 Subject: [PATCH 51/61] Have get_params_unsorted return a set --- crates/circuit/src/circuit_data.rs | 8 ++------ qiskit/circuit/quantumcircuit.py | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index c5eabfdf9fe7..8fa3e8fe749c 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -1391,12 +1391,8 @@ impl CircuitData { self.param_table.get_param_from_name(py, name) } - pub fn get_params_unsorted(&self, py: Python) -> Vec { - self.param_table - .uuid_map - .values() - .map(|x| x.clone_ref(py)) - .collect() + 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( diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index d1bff1d3fa38..eb35e3b75ebb 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -4154,7 +4154,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 set(self._data.get_params_unsorted()) + return self._data.get_params_unsorted() @overload def assign_parameters( From 402bc293467850a140c03d406ce48aa8bb3965bb Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 9 Jun 2024 06:17:43 -0400 Subject: [PATCH 52/61] Use lookup table for static property methods of StandardGate --- crates/circuit/src/imports.rs | 4 +- crates/circuit/src/operations.rs | 95 ++++++++------------ test/python/circuit/test_rust_equivalence.py | 34 ++++++- 3 files changed, 69 insertions(+), 64 deletions(-) diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 9120d3a8675a..050f7f2e053c 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -136,7 +136,7 @@ pub fn populate_std_gate_map(py: Python, rs_gate: StandardGate, py_gate: PyObjec match STDGATE_PYTHON_GATES.get_mut() { Some(gate_map) => gate_map, None => { - let array: [Option; STANDARD_GATE_SIZE] = core::array::from_fn(|_| 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() } @@ -151,7 +151,7 @@ pub fn populate_std_gate_map(py: Python, rs_gate: StandardGate, py_gate: PyObjec #[inline] pub fn get_std_gate_class(py: Python, rs_gate: StandardGate) -> PyResult { let gate_map = - unsafe { STDGATE_PYTHON_GATES.get_or_init(py, || core::array::from_fn(|_| None)) }; + 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 { diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 978fb307f1f3..ead1b8ee1ebb 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -196,6 +196,33 @@ pub enum StandardGate { 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 { @@ -226,6 +253,11 @@ impl StandardGate { self.num_clbits() } + #[getter] + pub fn get_num_params(&self) -> u32 { + self.num_params() + } + #[getter] pub fn get_name(&self) -> &str { self.name() @@ -241,72 +273,15 @@ pub const STANDARD_GATE_SIZE: usize = 18; impl Operation for StandardGate { fn name(&self) -> &str { - match self { - Self::ZGate => "z", - Self::YGate => "y", - Self::XGate => "x", - Self::CZGate => "cz", - Self::CYGate => "cy", - Self::CXGate => "cx", - Self::CCXGate => "ccx", - Self::RXGate => "rx", - Self::RYGate => "ry", - Self::RZGate => "rz", - Self::ECRGate => "ecr", - Self::SwapGate => "swap", - Self::SXGate => "sx", - Self::GlobalPhaseGate => "global_phase", - Self::IGate => "id", - Self::HGate => "h", - Self::PhaseGate => "p", - Self::UGate => "u", - } + STANDARD_GATE_NAME[*self as usize] } fn num_qubits(&self) -> u32 { - match self { - Self::ZGate => 1, - Self::YGate => 1, - Self::XGate => 1, - Self::CZGate => 2, - Self::CYGate => 2, - Self::CXGate => 2, - Self::CCXGate => 3, - Self::RXGate => 1, - Self::RYGate => 1, - Self::RZGate => 1, - Self::ECRGate => 2, - Self::SwapGate => 2, - Self::SXGate => 1, - Self::GlobalPhaseGate => 0, - Self::IGate => 1, - Self::HGate => 1, - Self::PhaseGate => 1, - Self::UGate => 1, - } + STANDARD_GATE_NUM_QUBITS[*self as usize] } fn num_params(&self) -> u32 { - match self { - Self::ZGate => 0, - Self::YGate => 0, - Self::XGate => 0, - Self::CZGate => 0, - Self::CYGate => 0, - Self::CXGate => 0, - Self::CCXGate => 0, - Self::RXGate => 1, - Self::RYGate => 1, - Self::RZGate => 1, - Self::ECRGate => 0, - Self::SwapGate => 0, - Self::SXGate => 0, - Self::GlobalPhaseGate => 1, - Self::IGate => 0, - Self::HGate => 0, - Self::PhaseGate => 1, - Self::UGate => 3, - } + STANDARD_GATE_NUM_PARAMS[*self as usize] } fn num_clbits(&self) -> u32 { diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index b657ed327fa3..3a92a44c228a 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -31,7 +31,7 @@ class TestRustGateEquivalence(QiskitTestCase): 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 + # 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): @@ -51,7 +51,6 @@ def test_definitions(self): continue with self.subTest(name=name): - print(name) params = [pi] * standard_gate._num_params() py_def = gate_class.base_class(*params).definition rs_def = standard_gate._get_definition(params) @@ -107,3 +106,34 @@ def test_matrix(self): 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): + 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): + 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): + 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") From 978d90fbed4e7f87cf17f0eebcf5a29af953bab2 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 9 Jun 2024 06:35:01 -0400 Subject: [PATCH 53/61] Use PyTuple::empty_bound() --- crates/circuit/src/circuit_data.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 8fa3e8fe749c..b9c169000856 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -143,14 +143,13 @@ impl CircuitData { } for (operation, params, qargs) in instruction_iter { let qubits = PyTuple::new_bound(py, res.qubits.map_indices(&qargs)).unbind(); - let empty: [u8; 0] = []; - let clbits = PyTuple::new_bound(py, empty); + let clbits = PyTuple::empty_bound(py).unbind(); let inst = res.pack_owned( py, &CircuitInstruction { operation: OperationType::Standard(operation), qubits, - clbits: clbits.into(), + clbits, params, extra_attrs: None, #[cfg(feature = "cache_pygates")] From a116719a643b7c220d91196b08a5aff6262dcb1a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 9 Jun 2024 07:21:17 -0400 Subject: [PATCH 54/61] Fix lint --- test/python/circuit/test_rust_equivalence.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index 3a92a44c228a..f04564f203be 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -117,7 +117,6 @@ def test_name(self): with self.subTest(name=name): self.assertEqual(gate_class.name, standard_gate.name) - def test_num_qubits(self): for name, gate_class in self.standard_gates.items(): standard_gate = getattr(gate_class, "_standard_gate", None) @@ -136,4 +135,6 @@ def test_num_params(self): continue with self.subTest(name=name): - self.assertEqual(len(gate_class.params), standard_gate.num_params, msg=f"{name} not equal") + self.assertEqual( + len(gate_class.params), standard_gate.num_params, msg=f"{name} not equal" + ) From de4b91e4ce12f8c8a188fe352a4370390df66308 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 9 Jun 2024 15:11:44 -0400 Subject: [PATCH 55/61] Add missing test method docstring --- test/python/circuit/test_rust_equivalence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index f04564f203be..06d4ed86a60a 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -108,6 +108,7 @@ def test_matrix(self): 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: @@ -118,6 +119,7 @@ def test_name(self): 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: @@ -128,6 +130,7 @@ def test_num_qubits(self): 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: From ef31751649811adf6d16d897757ef19dae99e9d0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sun, 9 Jun 2024 15:23:21 -0400 Subject: [PATCH 56/61] Reuse allocations in parameter table update --- crates/circuit/src/circuit_data.rs | 68 +++++++++++++++--------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index b9c169000856..b3553ecba063 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -170,31 +170,31 @@ impl CircuitData { ) -> PyResult { if let Some(params) = params { let mut new_param = false; + let mut atomic_parameters: HashMap = HashMap::new(); for (param_index, raw_param_objs) in ¶ms { - let atomic_parameters: HashMap = raw_param_objs - .iter() - .map(|x| { - ( - 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), - ) - }) - .collect(); - for (param_uuid, param_obj) in atomic_parameters.into_iter() { - match self.param_table.table.get_mut(¶m_uuid) { + 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, new_entry)?; + self.param_table + .insert(py, param_obj.clone_ref(py), new_entry)?; } }; } + atomic_parameters.clear() } return Ok(new_param); } @@ -212,33 +212,33 @@ impl CircuitData { .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()?; - let atomic_parameters: HashMap = raw_param_objs - .into_iter() - .map(|x| { - ( - x.getattr(py, intern!(py, "_uuid")) - .expect("Not a parameter") - .getattr(py, intern!(py, "int")) - .expect("Not a uuid") - .extract(py) - .unwrap(), - x, - ) - }) - .collect(); - for (param_uuid, param_obj) in atomic_parameters.into_iter() { - match self.param_table.table.get_mut(¶m_uuid) { + 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, new_entry)?; + self.param_table + .insert(py, param_obj.clone_ref(py), new_entry)?; } }; } + atomic_parameters.clear(); } } } From 6c68e601b0c63c38ec4feea0dccf434bbb76d266 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 10 Jun 2024 04:40:21 -0400 Subject: [PATCH 57/61] Remove unnecessary global phase zeroing --- qiskit/circuit/quantumcircuit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index eb35e3b75ebb..3fbf6902cdb4 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1129,7 +1129,6 @@ def __init__( self._parameters = None self._layout = None - self._data.global_phase: ParameterValueType = 0.0 self.global_phase = global_phase # Add classical variables. Resolve inputs and captures first because they can't depend on From 1723267be34ebad1ed62ce4dcb4e9cad5562eab1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 10 Jun 2024 04:50:24 -0400 Subject: [PATCH 58/61] Move manually set params to a separate function --- crates/circuit/src/circuit_data.rs | 65 +++++++++++++++++------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index b3553ecba063..1fc265bec7c0 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -161,6 +161,42 @@ impl CircuitData { Ok(res) } + 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, @@ -169,34 +205,7 @@ impl CircuitData { params: Option)>>, ) -> PyResult { if let Some(params) = params { - let mut new_param = false; - let mut atomic_parameters: HashMap = HashMap::new(); - for (param_index, raw_param_objs) in ¶ms { - 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() - } - return Ok(new_param); + return self.handle_manual_params(py, inst_index, ¶ms); } // Update the parameter table let mut new_param = false; From b4387489a16bccc83f550f168e4bade75f7ca163 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Jun 2024 08:21:25 -0400 Subject: [PATCH 59/61] Fix release note typo --- releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml index 933631a030e3..d826bc15e488 100644 --- a/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml +++ b/releasenotes/notes/circuit-gates-rust-5c6ab6c58f7fd2c9.yaml @@ -73,7 +73,7 @@ upgrade_circuits: qc = QuantumCircuit(1) qc.p(0) - qc.data[0].operations.params[0] = 3.14 + 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. From 56969cb036b237abcc879dda8fbf77c14ba1579a Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 13 Jun 2024 10:45:17 +0200 Subject: [PATCH 60/61] Use constant for global-phase index --- crates/circuit/README.md | 10 +++++----- crates/circuit/src/circuit_data.rs | 14 ++++++-------- crates/circuit/src/parameter_table.rs | 3 +++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/circuit/README.md b/crates/circuit/README.md index ef0fd933ced0..bbb4e54651ad 100644 --- a/crates/circuit/README.md +++ b/crates/circuit/README.md @@ -56,14 +56,14 @@ There is also an `Operation` trait defined which defines the common access patte 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. -## ParameterTable +## ParamTable -The `ParameterTable` struct is used to track which circuit instructions are using `ParameterExpression` +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 `ParameterEntry`, the `name` to the uuid, and the uuid to the PyObject for the actual `Parameter`. +`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 `ParameterEntry` 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 +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 -`usize::MAX` that points to the global phase property of the circuit instead of a `CircuitInstruction`. +`GLOBAL_PHASE_MAX`, that points to the global phase property of the circuit instead of a `CircuitInstruction`. diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 1fc265bec7c0..da35787e3207 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -18,7 +18,7 @@ use crate::circuit_instruction::{ 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}; +use crate::parameter_table::{ParamEntry, ParamTable, GLOBAL_PHASE_INDEX}; use crate::{Clbit, Qubit, SliceOrInt}; use pyo3::exceptions::{PyIndexError, PyValueError}; @@ -257,7 +257,7 @@ impl CircuitData { /// 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 == usize::MAX { + 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()?; @@ -1351,14 +1351,12 @@ impl CircuitData { #[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, usize::MAX)?; + 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) => { - // usize::MAX is the global phase sentinel value for the inst index - let inst_index = usize::MAX; let temp: PyObject = angle.getattr(py, intern!(py, "parameters"))?; let raw_param_objs: Vec = list_builtin.call1((temp,))?.extract()?; @@ -1368,9 +1366,9 @@ impl CircuitData { .getattr(py, intern!(py, "int"))? .extract(py)?; match self.param_table.table.get_mut(¶m_uuid) { - Some(entry) => entry.add(inst_index, param_index), + Some(entry) => entry.add(GLOBAL_PHASE_INDEX, param_index), None => { - let new_entry = ParamEntry::new(inst_index, param_index); + let new_entry = ParamEntry::new(GLOBAL_PHASE_INDEX, param_index); self.param_table.insert(py, param_obj, new_entry)?; } }; @@ -1385,7 +1383,7 @@ impl CircuitData { /// Get the global_phase sentinel value #[classattr] pub const fn global_phase_param_index() -> usize { - usize::MAX + GLOBAL_PHASE_INDEX } // Below are functions to interact with the parameter table. These methods diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs index 1e8ae88e0475..48c779eed3a3 100644 --- a/crates/circuit/src/parameter_table.rs +++ b/crates/circuit/src/parameter_table.rs @@ -17,6 +17,9 @@ 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)>, From adbe9e739f88c2dc4b96a8a2c86e2acb936e3332 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 13 Jun 2024 11:13:46 +0200 Subject: [PATCH 61/61] Switch requirement to release version --- requirements-optional.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-optional.txt b/requirements-optional.txt index 83b2d3ca085c..3dfc2031d026 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 @ git+https://github.com/Qiskit/qiskit-qasm3-import +qiskit-qasm3-import>=0.5.0 python-constraint>=1.4 cvxpy scikit-learn>=0.20.0