From d9e31ed551f2be4c6d5079717acc47af76010378 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Sat, 6 Jul 2024 01:19:28 +0100 Subject: [PATCH 1/6] Rebalance `CircuitInstruction` and `PackedInstruction` This is a large overhaul of how circuit instructions are both stored in Rust (`PackedInstruction`) and how they are presented to Python (`CircuitInstruction`). In summary: * The old `OperationType` enum is now collapsed into a manually managed `PackedOperation`. This is logically equivalent, but stores a `PyGate`/`PyInstruction`/`PyOperation` indirectly through a boxed pointer, and stores a `StandardGate` inline. As we expect the vast majority of gates to be standard, this hugely reduces the memory usage. The enumeration is manually compressed to a single pointer, hiding the discriminant in the low, alignment-required bytes of the pointer. * `PackedOperation::view()` unpacks the operation into a proper reference-like enumeration `OperationRef<'a>`, which implements `Operation` (though there is also a `try_standard_gate` method to get the gate without unpacking the whole enumeration). * Both `PackedInstruction` and `CircuitInstruction` use this `PackedOperation` as the operation storage. * `PackedInstruction` is now completely the Rust-space format for data, and `CircuitInstruction` is purely for communication with Python. On my machine, this commit brings the utility-scale benchmarks to within 10% of the runtime of 1.1.0 (and some to parity), despite all the additional overhead. Changes to accepting and building Python objects ------------------------------------------------ * A `PackedInstruction` is created by copy constructor from a `CircuitInstruction` by `CircuitData::pack`. There is no `pack_owned` (really, there never was - the previous method didn't take ownership) because there's never owned `CircuitInstruction`s coming in; they're Python-space interop, so we never own them (unless we clone them) other than when we're unpacking them. * `PackedInstruction` is currently just created manually when not coming from a `CircuitInstruction`. It's not hard, and makes it easier to re-use known intern indices than to waste time re-interning them. There is no need to go via `CircuitInstruction`. * `CircuitInstruction` now has two separated Python-space constructors: the old one, which is the default and takes `(operation, qubits, clbits)` (and extracts the information), and a new fast-path `from_standard` which asks only for the standard gate, qubits and params, avoiding operator construction. * To accept a Python-space operation, extract a Python object to `OperationFromPython`. This extracts the components that are separate in Rust space, but joined in Python space (the operation, params and extra attributes). This replaces `OperationInput` and `OperationTypeConstruct`, being more efficient at the extraction, including providing the data in the formats needed for `PackedInstruction` or `CircuitInstruction`. * To retrieve the Python-space operation, use `CircuitInstruction::get_operation` or `PackedInstruction::unpack_py_op` as appropriate. Both will cache and reuse the op, if `cache_pygates` is active. (Though note that if the op is created by `CircuitInstruction`, it will not propagate back to a `PackedInstruction`.) Avoiding operation creation --------------------------- The `_raw_op` field of `CircuitInstruction` is gone, because `PyGate`, `PyInstruction` and `PyOperation` are no longer pyclasses and no longer exposed to Python. Instead, we avoid operation creation by: * having an internal `DAGNode::_to_circuit_instruction`, which returns a copy of the internal `CircuitInstruction`, which can then be used with `CircuitInstruction.replace`, etc. * having `CircuitInstruction::is_standard_gate` to query from Python space if we should bother to create the operator. * changing `CircuitData::map_ops` to `map_nonstandard_ops`, and having it only call the Python callback function if the operation is not an unconditional standard gate. Memory usage ------------ Given the very simple example construction script: ```python from qiskit.circuit import QuantumCircuit qc = QuantumCircuit(1_000) for _ in range(3_000): for q in qc.qubits: qc.rz(0.0, q) for q in qc.qubits: qc.rx(0.0, q) for q in qc.qubits: qc.rz(0.0, q) for a, b in zip(qc.qubits[:-1], qc.qubits[1:]): qc.cx(a, b) ``` This uses 1.5GB in max resident set size on my Macbook (note that it's about 12 million gates) on both 1.1.0 and with this commit, so we've undone our memory losses. The parent of this commit uses 2GB. However, we're in a strong position to beat 1.1.0 in the future now; there are two obvious large remaining costs: * There are 16 bytes per `PackedInstruction` for the Python-operation caching (worth about 180MB in this benchmark, since no Python operations are actually created). * There is also significant memory wastage in the current `SmallVec<[Param; 3]>` storage of the parameters; for all standard gates, we know statically how many parameters are / should be stored, and we never need to increase the capacity. Further, the `Param` enum is 16 bytes wide per parameter, of which nearly 8 bytes is padding, but for all our current use cases, we only care if _all_ the parameters or floats (for everything else, we're going to have to defer to Python). We could move the discriminant out to the level of the parameters structure, and save a large amount of padding. Further work ------------ There's still performance left on the table here: * We still copy-in and copy-out of `CircuitInstruction` too much right now; we might want to make all the `CircuitInstruction` fields nullable and have `CircuitData::append` take them by _move_ rather than by copy. * The qubits/clbits interner requires owned arrays going in, but most interning should return an existing entry. We probably want to switch to have the interner take references/iterators by default, and clone when necessary. We could have a small circuit optimisation where the intern contexts reserve the first n entries to use for an all-to-all connectivity interning for up to (say) 8 qubits, since the transpiler will want to create a lot of ephemeral small circuits. * The `Param` vectors are too heavy at the moment; `SmallVec<[Param; 3]>` is 56 bytes wide, despite the vast majority of gates we care about having at most one single float (8 bytes). Dead padding is a large chunk of the memory use currently. --- Cargo.lock | 5 +- Cargo.toml | 1 + .../src/euler_one_qubit_decomposer.rs | 33 +- crates/circuit/Cargo.toml | 1 + crates/circuit/src/circuit_data.rs | 277 ++--- crates/circuit/src/circuit_instruction.rs | 1029 +++++------------ crates/circuit/src/dag_node.rs | 141 +-- crates/circuit/src/lib.rs | 4 +- crates/circuit/src/operations.rs | 358 +++--- crates/circuit/src/packed_instruction.rs | 497 ++++++++ qiskit/circuit/commutation_checker.py | 4 +- qiskit/circuit/instructionset.py | 4 +- qiskit/circuit/quantumcircuit.py | 140 +-- qiskit/converters/circuit_to_dag.py | 21 +- qiskit/converters/circuit_to_instruction.py | 2 +- qiskit/converters/dag_to_circuit.py | 19 +- qiskit/dagcircuit/dagcircuit.py | 77 +- .../optimization/optimize_1q_decomposition.py | 4 +- .../passes/synthesis/unitary_synthesis.py | 23 +- test/python/circuit/test_circuit_data.py | 4 +- test/python/circuit/test_rust_equivalence.py | 2 +- 21 files changed, 1297 insertions(+), 1349 deletions(-) create mode 100644 crates/circuit/src/packed_instruction.rs diff --git a/Cargo.lock b/Cargo.lock index 125ce6abe876..010e192691de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,9 +102,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" dependencies = [ "bytemuck_derive", ] @@ -1187,6 +1187,7 @@ dependencies = [ name = "qiskit-circuit" version = "1.2.0" dependencies = [ + "bytemuck", "hashbrown 0.14.5", "ndarray", "num-complex", diff --git a/Cargo.toml b/Cargo.toml index a6ccf60f7f4b..aa6d3d82570a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ license = "Apache-2.0" # # Each crate can add on specific features freely as it inherits. [workspace.dependencies] +bytemuck = "1.16" indexmap.version = "2.2.6" hashbrown.version = "0.14.0" num-complex = "0.4" diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 24c4f6e87c2a..f42cb7f705ee 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -743,7 +743,7 @@ pub fn compute_error_list( .iter() .map(|node| { ( - node.instruction.operation.name().to_string(), + node.instruction.op().name().to_string(), smallvec![], // Params not needed in this path ) }) @@ -988,11 +988,10 @@ pub fn optimize_1q_gates_decomposition( .iter() .map(|node| { if let Some(err_map) = error_map { - error *= - compute_error_term(node.instruction.operation.name(), err_map, qubit) + error *= compute_error_term(node.instruction.op().name(), err_map, qubit) } node.instruction - .operation + .op() .matrix(&node.instruction.params) .expect("No matrix defined for operation") }) @@ -1046,24 +1045,16 @@ fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { #[pyfunction] pub fn collect_1q_runs_filter(node: &Bound) -> bool { - let op_node = node.downcast::(); - match op_node { - Ok(bound_node) => { - let node = bound_node.borrow(); - node.instruction.operation.num_qubits() == 1 - && node.instruction.operation.num_clbits() == 0 - && node - .instruction - .operation - .matrix(&node.instruction.params) - .is_some() - && match &node.instruction.extra_attrs { - None => true, - Some(attrs) => attrs.condition.is_none(), - } + let Ok(node) = node.downcast::() else { return false }; + let node = node.borrow(); + let op = node.instruction.op(); + op.num_qubits() == 1 + && op.num_clbits() == 0 + && op.matrix(&node.instruction.params).is_some() + && match &node.instruction.extra_attrs { + None => true, + Some(attrs) => attrs.condition.is_none(), } - Err(_) => false, - } } #[pymodule] diff --git a/crates/circuit/Cargo.toml b/crates/circuit/Cargo.toml index 50160c7bac17..3eb430515fcf 100644 --- a/crates/circuit/Cargo.toml +++ b/crates/circuit/Cargo.toml @@ -10,6 +10,7 @@ name = "qiskit_circuit" doctest = false [dependencies] +bytemuck.workspace = true hashbrown.workspace = true num-complex.workspace = true ndarray.workspace = true diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 501645415874..66d35cb9377c 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -14,20 +14,18 @@ use std::cell::RefCell; use crate::bit_data::BitData; -use crate::circuit_instruction::{ - convert_py_to_operation_type, CircuitInstruction, ExtraInstructionAttributes, OperationInput, - PackedInstruction, -}; -use crate::imports::{BUILTIN_LIST, DEEPCOPY, QUBIT}; +use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; +use crate::imports::{BUILTIN_LIST, QUBIT}; use crate::interner::{IndexedInterner, Interner, InternerKey}; -use crate::operations::{Operation, OperationType, Param, StandardGate}; +use crate::operations::{Operation, Param, StandardGate}; +use crate::packed_instruction::PackedInstruction; use crate::parameter_table::{ParamEntry, ParamTable, GLOBAL_PHASE_INDEX}; use crate::slice::{PySequenceIndex, SequenceIndex}; use crate::{Clbit, Qubit}; use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; -use pyo3::types::{PyList, PySet, PyTuple, PyType}; +use pyo3::types::{PyDict, PyList, PySet, PyTuple, PyType}; use pyo3::{intern, PyTraverseError, PyVisit}; use hashbrown::{HashMap, HashSet}; @@ -145,22 +143,23 @@ impl CircuitData { res.add_qubit(py, &bit, true)?; } } + let no_clbit_index = (&mut res.cargs_interner) + .intern(InternerKey::Value(Vec::new()))? + .index; for (operation, params, qargs) in instruction_iter { - let qubits = PyTuple::new_bound(py, res.qubits.map_indices(&qargs)).unbind(); - let clbits = PyTuple::empty_bound(py).unbind(); - let inst = res.pack_owned( - py, - &CircuitInstruction { - operation: OperationType::Standard(operation), - qubits, - clbits, - params, - extra_attrs: None, - #[cfg(feature = "cache_pygates")] - py_op: None, - }, - )?; - res.data.push(inst); + let qubits = (&mut res.qargs_interner) + .intern(InternerKey::Value(qargs.to_vec()))? + .index; + let params = (!params.is_empty()).then(|| Box::new(params)); + res.data.push(PackedInstruction { + op: operation.into(), + qubits, + clbits: no_clbit_index, + params, + extra_attrs: None, + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new(None), + }); } Ok(res) } @@ -213,7 +212,7 @@ impl CircuitData { } // Update the parameter table let mut new_param = false; - let inst_params = &self.data[inst_index].params; + let inst_params = self.data[inst_index].params_view(); if !inst_params.is_empty() { let params: Vec<(usize, PyObject)> = inst_params .iter() @@ -275,9 +274,9 @@ impl CircuitData { .discard_references(uuid, inst_index, param_index, name); } } - } else if !self.data[inst_index].params.is_empty() { + } else if !self.data[inst_index].params_view().is_empty() { let params: Vec<(usize, PyObject)> = self.data[inst_index] - .params + .params_view() .iter() .enumerate() .filter_map(|(idx, x)| match x { @@ -321,8 +320,8 @@ impl CircuitData { Ok(()) } - pub fn append_inner(&mut self, py: Python, value: PyRef) -> PyResult { - let packed = self.pack(value)?; + pub fn append_inner(&mut self, py: Python, value: &CircuitInstruction) -> 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) @@ -479,52 +478,53 @@ impl CircuitData { Some(self.qubits.cached().bind(py)), Some(self.clbits.cached().bind(py)), None, - 0, + self.data.len(), self.global_phase.clone(), )?; res.qargs_interner = self.qargs_interner.clone(); res.cargs_interner = self.cargs_interner.clone(); - res.data.clone_from(&self.data); res.param_table.clone_from(&self.param_table); if deepcopy { - for inst in &mut res.data { - match &mut inst.op { - OperationType::Standard(_) => {} - OperationType::Gate(ref mut op) => { - op.gate = DEEPCOPY.get_bound(py).call1((&op.gate,))?.unbind(); - } - OperationType::Instruction(ref mut op) => { - op.instruction = DEEPCOPY.get_bound(py).call1((&op.instruction,))?.unbind(); - } - OperationType::Operation(ref mut op) => { - op.operation = DEEPCOPY.get_bound(py).call1((&op.operation,))?.unbind(); - } - }; - #[cfg(feature = "cache_pygates")] - { - *inst.py_op.borrow_mut() = None; - } + let memo = PyDict::new_bound(py); + for inst in &self.data { + res.data.push(PackedInstruction { + op: inst.op.py_deepcopy(py, Some(&memo))?, + qubits: inst.qubits, + clbits: inst.clbits, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new(None), + }); } } else if copy_instructions { - for inst in &mut res.data { - match &mut inst.op { - OperationType::Standard(_) => {} - OperationType::Gate(ref mut op) => { - op.gate = op.gate.call_method0(py, intern!(py, "copy"))?; - } - OperationType::Instruction(ref mut op) => { - op.instruction = op.instruction.call_method0(py, intern!(py, "copy"))?; - } - OperationType::Operation(ref mut op) => { - op.operation = op.operation.call_method0(py, intern!(py, "copy"))?; - } - }; + for inst in &self.data { + res.data.push(PackedInstruction { + op: inst.op.py_copy(py)?, + qubits: inst.qubits, + clbits: inst.clbits, + params: inst.params.clone(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new(None), + }); + } + } else { + res.data.extend(self.data.iter().map(|inst| { + // Clippy 1.70 doesn't like feature-gating only the middle statement, because if + // `cache_pygates` is off, it looks like a let followed immediately by a return. #[cfg(feature = "cache_pygates")] { - *inst.py_op.borrow_mut() = None; + let out = inst.clone(); + *out.py_op.borrow_mut() = None; + out } - } + #[cfg(not(feature = "cache_pygates"))] + { + inst.clone() + } + })) } Ok(res) } @@ -548,10 +548,10 @@ impl CircuitData { let qubits = PySet::empty_bound(py)?; let clbits = PySet::empty_bound(py)?; for inst in self.data.iter() { - for b in self.qargs_interner.intern(inst.qubits_id).value.iter() { + for b in self.qargs_interner.intern(inst.qubits).value.iter() { qubits.add(self.qubits.get(*b).unwrap().clone_ref(py))?; } - for b in self.cargs_interner.intern(inst.clbits_id).value.iter() { + for b in self.cargs_interner.intern(inst.clbits).value.iter() { clbits.add(self.clbits.get(*b).unwrap().clone_ref(py))?; } } @@ -586,70 +586,37 @@ impl CircuitData { Ok(()) } - /// Invokes callable ``func`` with each instruction's operation, - /// replacing the operation with the result. + /// Invokes callable ``func`` with each instruction's operation, replacing the operation with + /// the result, if the operation is not a standard gate without a condition. /// - /// .. note:: + /// .. warning:: /// - /// 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. + /// This is a shim for while there are still important components of the circuit still + /// implemented in Python space. This method **skips** any instruction that contains an + /// non-conditional standard gate (which is likely to be most instructions). /// /// Args: /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): - /// A callable used to map original operation to their - /// replacements. + /// A callable used to map original operations to their replacements. #[pyo3(signature = (func))] - pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { + pub fn map_nonstandard_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter_mut() { - let py_op = { - if let OperationType::Standard(op) = inst.op { - match inst.extra_attrs.as_deref() { - None - | Some(ExtraInstructionAttributes { - condition: None, .. - }) => op.into_py(py), - _ => inst.unpack_py_op(py)?, - } - } else { - inst.unpack_py_op(py)? - } - }; - let result: OperationInput = func.call1((py_op,))?.extract()?; - match result { - OperationInput::Standard(op) => { - inst.op = OperationType::Standard(op); - } - OperationInput::Gate(op) => { - inst.op = OperationType::Gate(op); - } - OperationInput::Instruction(op) => { - inst.op = OperationType::Instruction(op); - } - OperationInput::Operation(op) => { - inst.op = OperationType::Operation(op); - } - OperationInput::Object(new_op) => { - let new_inst_details = convert_py_to_operation_type(py, new_op.clone_ref(py))?; - inst.op = new_inst_details.operation; - inst.params = new_inst_details.params; - if new_inst_details.label.is_some() - || new_inst_details.duration.is_some() - || new_inst_details.unit.is_some() - || new_inst_details.condition.is_some() - { - inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { - label: new_inst_details.label, - duration: new_inst_details.duration, - unit: new_inst_details.unit, - condition: new_inst_details.condition, - })) - } - #[cfg(feature = "cache_pygates")] - { - *inst.py_op.borrow_mut() = Some(new_op); - } - } + if inst.op.try_standard_gate().is_some() + && !inst + .extra_attrs + .as_ref() + .is_some_and(|attrs| attrs.condition.is_some()) + { + continue; + } + let py_op = func.call1((inst.unpack_py_op(py)?,))?; + let result = py_op.extract::()?; + inst.op = result.operation; + inst.params = (!result.params.is_empty()).then(|| Box::new(result.params)); + inst.extra_attrs = result.extra_attrs; + #[cfg(feature = "cache_pygates")] + { + *inst.py_op.borrow_mut() = Some(py_op.unbind()); } } Ok(()) @@ -683,7 +650,7 @@ impl CircuitData { /// To modify bits referenced by an operation, use /// :meth:`~.CircuitData.foreach_op` or /// :meth:`~.CircuitData.foreach_op_indexed` or - /// :meth:`~.CircuitData.map_ops` to adjust the operations manually + /// :meth:`~.CircuitData.map_nonstandard_ops` to adjust the operations manually /// after calling this method. /// /// Examples: @@ -746,14 +713,14 @@ impl CircuitData { // Get a single item, assuming the index is validated as in bounds. let get_single = |index: usize| { let inst = &self.data[index]; - let qubits = self.qargs_interner.intern(inst.qubits_id); - let clbits = self.cargs_interner.intern(inst.clbits_id); + let qubits = self.qargs_interner.intern(inst.qubits); + let clbits = self.cargs_interner.intern(inst.clbits); CircuitInstruction::new( py, inst.op.clone(), self.qubits.map_indices(qubits.value), self.clbits.map_indices(clbits.value), - inst.params.clone(), + inst.params_view().iter().cloned().collect(), inst.extra_attrs.clone(), ) .into_py(py) @@ -770,20 +737,19 @@ impl CircuitData { pub fn setitem_no_param_table_update( &mut self, + py: Python, index: usize, - value: PyRef, + value: &CircuitInstruction, ) -> PyResult<()> { - let mut packed = self.pack(value)?; - std::mem::swap(&mut packed, &mut self.data[index]); + self.data[index] = self.pack(py, value)?; Ok(()) } pub fn __setitem__(&mut self, index: PySequenceIndex, value: &Bound) -> PyResult<()> { fn set_single(slf: &mut CircuitData, index: usize, value: &Bound) -> PyResult<()> { let py = value.py(); - let mut packed = slf.pack(value.downcast::()?.borrow())?; + slf.data[index] = slf.pack(py, &value.downcast::()?.borrow())?; slf.remove_from_parameter_table(py, index)?; - std::mem::swap(&mut packed, &mut slf.data[index]); slf.update_param_table(py, index, None)?; Ok(()) } @@ -851,7 +817,7 @@ impl CircuitData { } }; let py = value.py(); - let packed = self.pack(value)?; + let packed = self.pack(py, &value)?; self.data.insert(index, packed); if index == self.data.len() - 1 { self.update_param_table(py, index, None)?; @@ -875,21 +841,21 @@ impl CircuitData { value: &Bound, params: Option)>>, ) -> PyResult { - let packed = self.pack(value.try_borrow()?)?; let new_index = self.data.len(); + let packed = self.pack(py, &value.borrow())?; self.data.push(packed); self.update_param_table(py, new_index, params) } pub fn extend(&mut self, py: Python<'_>, itr: &Bound) -> PyResult<()> { - if let Ok(other) = itr.extract::>() { - // Fast path to avoid unnecessary construction of - // CircuitInstruction instances. + if let Ok(other) = itr.downcast::() { + let other = other.borrow(); + // Fast path to avoid unnecessary construction of CircuitInstruction instances. self.data.reserve(other.data.len()); for inst in other.data.iter() { let qubits = other .qargs_interner - .intern(inst.qubits_id) + .intern(inst.qubits) .value .iter() .map(|b| { @@ -901,7 +867,7 @@ impl CircuitData { .collect::>>()?; let clbits = other .cargs_interner - .intern(inst.clbits_id) + .intern(inst.clbits) .value .iter() .map(|b| { @@ -918,8 +884,8 @@ impl CircuitData { Interner::intern(&mut self.cargs_interner, InternerKey::Value(clbits))?; self.data.push(PackedInstruction { op: inst.op.clone(), - qubits_id: qubits_id.index, - clbits_id: clbits_id.index, + qubits: qubits_id.index, + clbits: clbits_id.index, params: inst.params.clone(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] @@ -930,7 +896,7 @@ impl CircuitData { return Ok(()); } for v in itr.iter()? { - self.append_inner(py, v?.extract()?)?; + self.append_inner(py, &v?.downcast()?.borrow())?; } Ok(()) } @@ -1107,7 +1073,7 @@ impl CircuitData { pub fn num_nonlocal_gates(&self) -> usize { self.data .iter() - .filter(|inst| inst.op.num_qubits() > 1 && !inst.op.directive()) + .filter(|inst| inst.op().num_qubits() > 1 && !inst.op().directive()) .count() } } @@ -1127,28 +1093,7 @@ impl CircuitData { Ok(()) } - fn pack(&mut self, inst: PyRef) -> PyResult { - let py = inst.py(); - let qubits = Interner::intern( - &mut self.qargs_interner, - InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), - )?; - let clbits = Interner::intern( - &mut self.cargs_interner, - InternerKey::Value(self.clbits.map_bits(inst.clbits.bind(py))?.collect()), - )?; - Ok(PackedInstruction { - op: inst.operation.clone(), - qubits_id: qubits.index, - clbits_id: clbits.index, - params: inst.params.clone(), - extra_attrs: inst.extra_attrs.clone(), - #[cfg(feature = "cache_pygates")] - py_op: RefCell::new(inst.py_op.clone()), - }) - } - - fn pack_owned(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult { + fn pack(&mut self, py: Python, inst: &CircuitInstruction) -> PyResult { let qubits = Interner::intern( &mut self.qargs_interner, InternerKey::Value(self.qubits.map_bits(inst.qubits.bind(py))?.collect()), @@ -1159,12 +1104,12 @@ impl CircuitData { )?; Ok(PackedInstruction { op: inst.operation.clone(), - qubits_id: qubits.index, - clbits_id: clbits.index, - params: inst.params.clone(), + qubits: qubits.index, + clbits: clbits.index, + params: (!inst.params.is_empty()).then(|| Box::new(inst.params.clone())), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] - py_op: RefCell::new(inst.py_op.clone()), + py_op: RefCell::new(inst.py_op.borrow().as_ref().map(|obj| obj.clone_ref(py))), }) } } diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index ed1c358cbc5b..c436bdd297a2 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -15,20 +15,20 @@ use std::cell::RefCell; use numpy::IntoPyArray; use pyo3::basic::CompareOp; -use pyo3::exceptions::{PyDeprecationWarning, PyValueError}; +use pyo3::exceptions::{PyDeprecationWarning, PyTypeError}; use pyo3::prelude::*; -use pyo3::types::{IntoPyDict, PyList, PyTuple, PyType}; +use pyo3::types::{PyList, PyTuple, PyType}; use pyo3::{intern, IntoPy, PyObject, PyResult}; -use smallvec::{smallvec, SmallVec}; + +use smallvec::SmallVec; use crate::imports::{ - get_std_gate_class, populate_std_gate_map, GATE, INSTRUCTION, OPERATION, - SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, WARNINGS_WARN, + GATE, INSTRUCTION, OPERATION, SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, WARNINGS_WARN, }; -use crate::interner::Index; use crate::operations::{ - Operation, OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate, + Operation, OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate, }; +use crate::packed_instruction::PackedOperation; /// 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 @@ -42,71 +42,26 @@ pub struct ExtraInstructionAttributes { pub condition: Option, } -/// Private type used to store instructions with interned arg lists. -#[derive(Clone, Debug)] -pub(crate) struct PackedInstruction { - /// The Python-side operation instance. - pub op: OperationType, - /// The index under which the interner has stored `qubits`. - pub qubits_id: Index, - /// The index under which the interner has stored `clbits`. - pub clbits_id: Index, - pub params: SmallVec<[Param; 3]>, - pub extra_attrs: Option>, - - #[cfg(feature = "cache_pygates")] - /// This is hidden in a `RefCell` because, while that has additional memory-usage implications - /// while we're still building with the feature enabled, we intend to remove the feature in the - /// future, and hiding the cache within a `RefCell` lets us keep the cache transparently in our - /// interfaces, without needing various functions to unnecessarily take `&mut` references. - pub py_op: RefCell>, -} - -impl PackedInstruction { - /// Build a reference to the Python-space operation object (the `Gate`, etc) packed into this - /// instruction. This may construct the reference if the `PackedInstruction` is a standard - /// gate with no already stored operation. - /// - /// A standard-gate operation object returned by this function is disconnected from the - /// containing circuit; updates to its label, duration, unit and condition will not be - /// propagated back. - pub fn unpack_py_op(&self, py: Python) -> PyResult> { - #[cfg(feature = "cache_pygates")] - { - if let Some(cached_op) = self.py_op.borrow().as_ref() { - return Ok(cached_op.clone_ref(py)); - } - } - let (label, duration, unit, condition) = match self.extra_attrs.as_deref() { - Some(ExtraInstructionAttributes { +impl ExtraInstructionAttributes { + /// Construct a new set of the extra attributes if any of the elements are not `None`, or return + /// `None` if there is no need for an object. + #[inline] + pub fn new( + label: Option, + duration: Option>, + unit: Option, + condition: Option>, + ) -> Option { + if label.is_some() || duration.is_some() || unit.is_some() || condition.is_some() { + Some(Self { label, duration, unit, condition, - }) => ( - label.as_deref(), - duration.as_ref(), - unit.as_deref(), - condition.as_ref(), - ), - None => (None, None, None, None), - }; - let out = operation_type_and_data_to_py( - py, - &self.op, - &self.params, - label, - duration, - unit, - condition, - )?; - #[cfg(feature = "cache_pygates")] - { - if let Ok(mut cell) = self.py_op.try_borrow_mut() { - cell.get_or_insert_with(|| out.clone_ref(py)); - } + }) + } else { + None } - Ok(out) } } @@ -145,7 +100,7 @@ impl PackedInstruction { #[pyclass(freelist = 20, sequence, module = "qiskit._accelerate.circuit")] #[derive(Clone, Debug)] pub struct CircuitInstruction { - pub operation: OperationType, + pub operation: PackedOperation, /// A sequence of the qubits that the operation is applied to. #[pyo3(get)] pub qubits: Py, @@ -155,28 +110,13 @@ pub struct CircuitInstruction { pub params: SmallVec<[Param; 3]>, pub extra_attrs: Option>, #[cfg(feature = "cache_pygates")] - pub py_op: Option, -} - -/// This enum is for backwards compatibility if a user was doing something from -/// Python like CircuitInstruction(SXGate(), [qr[0]], []) by passing a python -/// gate object directly to a CircuitInstruction. In this case we need to -/// create a rust side object from the pyobject in CircuitInstruction.new() -/// With the `Object` variant which will convert the python object to a rust -/// `OperationType` -#[derive(FromPyObject, Debug)] -pub enum OperationInput { - Standard(StandardGate), - Gate(PyGate), - Instruction(PyInstruction), - Operation(PyOperation), - Object(PyObject), + pub py_op: RefCell>, } impl CircuitInstruction { pub fn new( py: Python, - operation: OperationType, + operation: PackedOperation, qubits: impl IntoIterator, clbits: impl IntoIterator, params: SmallVec<[Param; 3]>, @@ -195,191 +135,64 @@ impl CircuitInstruction { params, extra_attrs, #[cfg(feature = "cache_pygates")] - py_op: None, + py_op: RefCell::new(None), } } -} -impl From for OperationInput { - fn from(value: OperationType) -> Self { - match value { - OperationType::Standard(op) => Self::Standard(op), - OperationType::Gate(gate) => Self::Gate(gate), - OperationType::Instruction(inst) => Self::Instruction(inst), - OperationType::Operation(op) => Self::Operation(op), - } + /// View the operation in this `CircuitInstruction`. + pub fn op(&self) -> OperationRef { + self.operation.view() } } #[pymethods] impl CircuitInstruction { - #[allow(clippy::too_many_arguments)] #[new] - #[pyo3(signature = (operation, qubits=None, clbits=None, params=smallvec![], label=None, duration=None, unit=None, condition=None))] + #[pyo3(signature = (operation, qubits=None, clbits=None))] pub fn py_new( - py: Python<'_>, - operation: OperationInput, - qubits: Option<&Bound>, - clbits: Option<&Bound>, + operation: &Bound, + qubits: Option>, + clbits: Option>, + ) -> PyResult { + let py = operation.py(); + let op_parts = operation.extract::()?; + + Ok(Self { + operation: op_parts.operation, + qubits: as_tuple(py, qubits)?.unbind(), + clbits: as_tuple(py, clbits)?.unbind(), + params: op_parts.params, + extra_attrs: op_parts.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new(Some(operation.into_py(py))), + }) + } + + #[pyo3(signature = (standard, qubits, params, label=None))] + #[staticmethod] + pub fn from_standard( + py: Python, + standard: StandardGate, + qubits: Option>, params: SmallVec<[Param; 3]>, label: Option, - duration: Option, - unit: Option, - condition: Option, ) -> PyResult { - fn as_tuple(py: Python<'_>, seq: Option<&Bound>) -> PyResult> { - match seq { - None => Ok(PyTuple::empty_bound(py).unbind()), - Some(seq) => { - if seq.is_instance_of::() { - Ok(seq.downcast_exact::()?.into_py(py)) - } else if seq.is_instance_of::() { - let seq = seq.downcast_exact::()?; - Ok(seq.to_tuple().unbind()) - } else { - // New tuple from iterable. - Ok(PyTuple::new_bound( - py, - seq.iter()? - .map(|o| Ok(o?.unbind())) - .collect::>>()?, - ) - .unbind()) - } - } - } - } - - let extra_attrs = - if label.is_some() || duration.is_some() || unit.is_some() || condition.is_some() { - Some(Box::new(ExtraInstructionAttributes { - label, - duration, - unit, - condition, - })) - } else { - None - }; - - match operation { - OperationInput::Standard(operation) => { - let operation = OperationType::Standard(operation); - Ok(CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - params, - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: None, - }) - } - OperationInput::Gate(operation) => { - let operation = OperationType::Gate(operation); - Ok(CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - params, - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: None, - }) - } - OperationInput::Instruction(operation) => { - let operation = OperationType::Instruction(operation); - Ok(CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - params, - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: None, - }) - } - OperationInput::Operation(operation) => { - let operation = OperationType::Operation(operation); - Ok(CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - params, - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: None, + Ok(Self { + operation: standard.into(), + qubits: as_tuple(py, qubits)?.unbind(), + clbits: PyTuple::empty_bound(py).unbind(), + params, + extra_attrs: label.map(|label| { + Box::new(ExtraInstructionAttributes { + label: Some(label), + duration: None, + unit: None, + condition: None, }) - } - OperationInput::Object(old_op) => { - let op = convert_py_to_operation_type(py, old_op.clone_ref(py))?; - let extra_attrs = if op.label.is_some() - || op.duration.is_some() - || op.unit.is_some() - || op.condition.is_some() - { - Some(Box::new(ExtraInstructionAttributes { - label: op.label, - duration: op.duration, - unit: op.unit, - condition: op.condition, - })) - } else { - None - }; - - match op.operation { - OperationType::Standard(operation) => { - let operation = OperationType::Standard(operation); - Ok(CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - params: op.params, - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: Some(old_op.clone_ref(py)), - }) - } - OperationType::Gate(operation) => { - let operation = OperationType::Gate(operation); - Ok(CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - params: op.params, - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: Some(old_op.clone_ref(py)), - }) - } - OperationType::Instruction(operation) => { - let operation = OperationType::Instruction(operation); - Ok(CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - params: op.params, - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: Some(old_op.clone_ref(py)), - }) - } - OperationType::Operation(operation) => { - let operation = OperationType::Operation(operation); - Ok(CircuitInstruction { - operation, - qubits: as_tuple(py, qubits)?, - clbits: as_tuple(py, clbits)?, - params: op.params, - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: Some(old_op.clone_ref(py)), - }) - } - } - } - } + }), + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new(None), + }) } /// Returns a shallow copy. @@ -391,34 +204,38 @@ 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) - } + pub fn get_operation(&self, py: Python) -> PyResult { + #[cfg(feature = "cache_pygates")] + { + if let Ok(Some(cached_op)) = self.py_op.try_borrow().as_deref() { + return Ok(cached_op.clone_ref(py)); + } + } - #[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 + let out = match self.operation.view() { + OperationRef::Standard(standard) => standard + .create_py_op(py, Some(&self.params), self.extra_attrs.as_deref())? + .into_any(), + OperationRef::Gate(gate) => gate.gate.clone_ref(py), + OperationRef::Instruction(instruction) => instruction.instruction.clone_ref(py), + OperationRef::Operation(operation) => operation.operation.clone_ref(py), + }; + + #[cfg(feature = "cache_pygates")] + { + if let Ok(mut cell) = self.py_op.try_borrow_mut() { + cell.get_or_insert_with(|| out.clone_ref(py)); } - }) - } + } - #[getter] - fn _raw_op(&self, py: Python) -> PyObject { - self.operation.clone().into_py(py) + Ok(out) } /// Returns the Instruction name corresponding to the op for this node #[getter] fn get_name(&self, py: Python) -> PyObject { - self.operation.name().to_object(py) + self.op().name().to_object(py) } #[getter] @@ -428,7 +245,7 @@ impl CircuitInstruction { #[getter] fn matrix(&self, py: Python) -> Option { - let matrix = self.operation.matrix(&self.params); + let matrix = self.operation.view().matrix(&self.params); matrix.map(|mat| mat.into_pyarray_bound(py).into()) } @@ -460,6 +277,11 @@ impl CircuitInstruction { .and_then(|attrs| attrs.unit.as_deref()) } + #[getter] + pub fn is_standard_gate(&self) -> bool { + self.operation.try_standard_gate().is_some() + } + pub fn is_parameterized(&self) -> bool { self.params .iter() @@ -470,103 +292,58 @@ impl CircuitInstruction { /// /// Returns: /// CircuitInstruction: A new instance with the given fields replaced. - #[allow(clippy::too_many_arguments)] pub fn replace( &self, - py: Python<'_>, - operation: Option, - qubits: Option<&Bound>, - clbits: Option<&Bound>, - params: Option>, - label: Option, - duration: Option, - unit: Option, - condition: Option, + py: Python, + operation: Option<&Bound>, + qubits: Option>, + clbits: Option>, + params: Option>, ) -> PyResult { - let operation = operation.unwrap_or_else(|| self.operation.clone().into()); - - let params = match params { - Some(params) => params, - None => self.params.clone(), + let qubits = match qubits { + None => self.qubits.clone_ref(py), + Some(qubits) => as_tuple(py, Some(qubits))?.unbind(), }; - - let label = match label { - Some(label) => Some(label), - None => match &self.extra_attrs { - Some(extra_attrs) => extra_attrs.label.clone(), - None => None, - }, - }; - let duration = match duration { - Some(duration) => Some(duration), - None => match &self.extra_attrs { - Some(extra_attrs) => extra_attrs.duration.clone(), - None => None, - }, + let clbits = match clbits { + None => self.clbits.clone_ref(py), + Some(clbits) => as_tuple(py, Some(clbits))?.unbind(), }; - - let unit: Option = match unit { - Some(unit) => Some(unit), - None => match &self.extra_attrs { - Some(extra_attrs) => extra_attrs.unit.clone(), - None => None, - }, - }; - - let condition: Option = match condition { - Some(condition) => Some(condition), - None => match &self.extra_attrs { - Some(extra_attrs) => extra_attrs.condition.clone(), - None => None, - }, - }; - - CircuitInstruction::py_new( - py, - operation, - 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()?; - 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, - })); + let params = params + .map(|params| params.extract::>()) + .transpose()?; + + if let Some(operation) = operation { + let op_parts = operation.extract::()?; + Ok(Self { + operation: op_parts.operation, + qubits, + clbits, + params: params.unwrap_or(op_parts.params), + extra_attrs: op_parts.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new(Some(operation.into_py(py))), + }) + } else { + Ok(Self { + operation: self.operation.clone(), + qubits, + clbits, + params: params.unwrap_or_else(|| self.params.clone()), + extra_attrs: self.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new( + self.py_op + .try_borrow() + .ok() + .and_then(|opt| opt.as_ref().map(|op| op.clone_ref(py))), + ), + }) } - Ok(()) } pub fn __getnewargs__(&self, py: Python<'_>) -> PyResult { Ok(( - operation_type_to_py(py, self)?, + self.get_operation(py)?, self.qubits.bind(py), self.clbits.bind(py), ) @@ -577,13 +354,9 @@ impl CircuitInstruction { let type_name = self_.get_type().qualname()?; let r = self_.try_borrow()?; Ok(format!( - "{}(\ - operation={}\ - , qubits={}\ - , clbits={}\ - )", + "{}(operation={}, qubits={}, clbits={})", type_name, - operation_type_to_py(py, &r)?, + r.get_operation(py)?.bind(py).repr()?, r.qubits.bind(py).repr()?, r.clbits.bind(py).repr()? )) @@ -595,64 +368,27 @@ 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.get_operation(py)?, self.qubits.bind(py).to_list().into(), self.clbits.bind(py).to_list().into(), ], )) } - #[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.bind(py).to_list().into(), - self.clbits.bind(py).to_list().into(), - ], - )) - } - - #[cfg(not(feature = "cache_pygates"))] pub fn __getitem__(&self, py: Python<'_>, key: &Bound) -> PyResult { warn_on_legacy_circuit_instruction_iteration(py)?; Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) } - #[cfg(feature = "cache_pygates")] - pub fn __getitem__(&mut self, py: Python<'_>, key: &Bound) -> PyResult { - warn_on_legacy_circuit_instruction_iteration(py)?; - 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 { warn_on_legacy_circuit_instruction_iteration(py)?; Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) } - #[cfg(feature = "cache_pygates")] - pub fn __iter__(&mut self, py: Python<'_>) -> PyResult { - warn_on_legacy_circuit_instruction_iteration(py)?; - Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) - } - pub fn __len__(&self, py: Python) -> PyResult { warn_on_legacy_circuit_instruction_iteration(py)?; Ok(3) @@ -664,6 +400,32 @@ impl CircuitInstruction { op: CompareOp, py: Python<'_>, ) -> PyResult { + fn params_eq(py: Python, left: &[Param], right: &[Param]) -> PyResult { + if left.len() != right.len() { + return Ok(false); + } + for (left, right) in left.iter().zip(right) { + let eq = match left { + Param::Float(left) => match right { + Param::Float(right) => left == right, + Param::ParameterExpression(right) | Param::Obj(right) => { + right.bind(py).eq(left)? + } + }, + Param::ParameterExpression(left) | Param::Obj(left) => match right { + Param::Float(right) => left.bind(py).eq(right)?, + Param::ParameterExpression(right) | Param::Obj(right) => { + left.bind(py).eq(right)? + } + }, + }; + if !eq { + return Ok(false); + } + } + Ok(true) + } + fn eq( py: Python<'_>, self_: &Bound, @@ -674,328 +436,177 @@ impl CircuitInstruction { } let self_ = self_.try_borrow()?; - if other.is_instance_of::() { - let other: PyResult> = other.extract(); - return other.map_or(Ok(Some(false)), |v| { - let v = v.try_borrow()?; - let op_eq = match &self_.operation { - OperationType::Standard(op) => { - if let OperationType::Standard(other) = &v.operation { - if op != other { - false - } else { - let other_params = &v.params; - let mut out = true; - for (param_a, param_b) in self_.params.iter().zip(other_params) - { - match param_a { - Param::Float(val_a) => { - if let Param::Float(val_b) = param_b { - if val_a != val_b { - out = false; - break; - } - } else { - out = false; - break; - } - } - Param::ParameterExpression(val_a) => { - if let Param::ParameterExpression(val_b) = param_b { - if !val_a.bind(py).eq(val_b.bind(py))? { - out = false; - break; - } - } else { - out = false; - break; - } - } - Param::Obj(val_a) => { - if let Param::Obj(val_b) = param_b { - if !val_a.bind(py).eq(val_b.bind(py))? { - out = false; - break; - } - } else { - out = false; - break; - } - } - } - } - out - } - } else { - false - } - } - OperationType::Gate(op) => { - if let OperationType::Gate(other) = &v.operation { - op.gate.bind(py).eq(other.gate.bind(py))? - } else { - false - } - } - OperationType::Instruction(op) => { - if let OperationType::Instruction(other) = &v.operation { - op.instruction.bind(py).eq(other.instruction.bind(py))? - } else { - false - } - } - OperationType::Operation(op) => { - if let OperationType::Operation(other) = &v.operation { - op.operation.bind(py).eq(other.operation.bind(py))? - } else { - false - } - } - }; - - Ok(Some( - self_.clbits.bind(py).eq(v.clbits.bind(py))? - && self_.qubits.bind(py).eq(v.qubits.bind(py))? - && op_eq, - )) - }); - } 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)?)); + return Ok(Some(self_._legacy_format(py)?.eq(other)?)); } - - Ok(None) + let Ok(other) = other.downcast::() else { return Ok(None) }; + let other = other.try_borrow()?; + + Ok(Some( + self_.qubits.bind(py).eq(other.qubits.bind(py))? + && self_.clbits.bind(py).eq(other.clbits.bind(py))? + && self_.operation.py_eq(py, &other.operation)? + && (self_.operation.try_standard_gate().is_none() + || params_eq(py, &self_.params, &other.params)?), + )) } match op { - CompareOp::Eq => eq(py, self_, other).map(|r| { - r.map(|b| b.into_py(py)) - .unwrap_or_else(|| py.NotImplemented()) - }), - CompareOp::Ne => eq(py, self_, other).map(|r| { - r.map(|b| (!b).into_py(py)) - .unwrap_or_else(|| py.NotImplemented()) - }), + CompareOp::Eq => Ok(eq(py, self_, other)? + .map(|b| b.into_py(py)) + .unwrap_or_else(|| py.NotImplemented())), + CompareOp::Ne => Ok(eq(py, self_, other)? + .map(|b| (!b).into_py(py)) + .unwrap_or_else(|| py.NotImplemented())), _ => Ok(py.NotImplemented()), } } } -/// Take a reference to a `CircuitInstruction` and convert the operation -/// inside that to a python side object. -pub fn operation_type_to_py(py: Python, circuit_inst: &CircuitInstruction) -> PyResult { - let (label, duration, unit, condition) = match &circuit_inst.extra_attrs { - None => (None, None, None, None), - Some(extra_attrs) => ( - extra_attrs.label.as_deref(), - extra_attrs.duration.as_ref(), - extra_attrs.unit.as_deref(), - extra_attrs.condition.as_ref(), - ), - }; - operation_type_and_data_to_py( - py, - &circuit_inst.operation, - &circuit_inst.params, - label, - duration, - unit, - condition, - ) +/// A container struct that contains the conversion from some `Operation` subclass input, on its way +/// to becoming a `PackedInstruction`. +/// +/// This is the primary way of converting an incoming `Gate` / `Instruction` / `Operation` from +/// Python space into Rust-space data. A typical access pattern is: +/// +/// ```rust +/// #[pyfunction] +/// fn accepts_op_from_python(ob: &Bound) -> PyResult<()> { +/// let py_op = ob.extract::()?; +/// // ... use `py_op.operation`, `py_op.params`, etc. +/// Ok(()) +/// } +/// ``` +/// +/// though you can also accept `ob: OperationFromPython` directly, if you don't also need a handle +/// to the Python object that it came from. The handle is useful for the Python-operation caching. +#[derive(Debug)] +pub(crate) struct OperationFromPython { + pub operation: PackedOperation, + pub params: SmallVec<[Param; 3]>, + pub extra_attrs: Option>, } -/// 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 fn operation_type_and_data_to_py( - py: Python, - operation: &OperationType, - params: &[Param], - label: Option<&str>, - duration: Option<&PyObject>, - unit: Option<&str>, - condition: Option<&PyObject>, -) -> PyResult { - match &operation { - OperationType::Standard(op) => { - let gate_class: &PyObject = &get_std_gate_class(py, *op)?; +impl<'py> FromPyObject<'py> for OperationFromPython { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let py = ob.py(); + let ob_type = ob + .getattr(intern!(py, "base_class")) + .ok() + .map(|base| base.downcast_into::()) + .transpose()? + .unwrap_or_else(|| ob.get_type()); + + let extract_params = || { + ob.getattr(intern!(py, "params")) + .ok() + .map(|params| params.extract()) + .transpose() + .map(|params| params.unwrap_or_default()) + }; + let extract_extra = || -> PyResult<_> { + Ok(ExtraInstructionAttributes::new( + ob.getattr(intern!(py, "label"))?.extract()?, + ob.getattr(intern!(py, "duration"))?.extract()?, + ob.getattr(intern!(py, "unit"))?.extract()?, + ob.getattr(intern!(py, "condition"))?.extract()?, + ) + .map(Box::from)) + }; - let args = if params.is_empty() { - PyTuple::empty_bound(py) - } else { - PyTuple::new_bound(py, params) - }; - let kwargs = [ - ("label", label.to_object(py)), - ("unit", unit.to_object(py)), - ("duration", duration.to_object(py)), - ] - .into_py_dict_bound(py); - let mut out = gate_class.call_bound(py, args, Some(&kwargs))?; - if condition.is_some() { - out = out.call_method0(py, "to_mutable")?; - out.setattr(py, "condition", condition.to_object(py))?; + 'standard: { + let Some(standard) = ob_type + .getattr(intern!(py, "_standard_gate")) + .and_then(|standard| standard.extract::()) + .ok() else { break 'standard }; + + // 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 future 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 ob.getattr(intern!(py, "mutable"))?.is_truthy()? + && (ob.is_instance(SINGLETON_GATE.get_bound(py))? + || ob.is_instance(SINGLETON_CONTROLLED_GATE.get_bound(py))?) + { + break 'standard; } - Ok(out) + return Ok(OperationFromPython { + operation: PackedOperation::from_standard(standard), + params: extract_params()?, + extra_attrs: extract_extra()?, + }); } - 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, Clone)] -pub struct OperationTypeConstruct { - pub operation: OperationType, - pub params: SmallVec<[Param; 3]>, - pub label: Option, - pub duration: Option, - pub unit: Option, - pub condition: Option, -} - -/// Convert an inbound Python object for a Qiskit operation and build a rust -/// representation of that operation. This will map it to appropriate variant -/// of operation type based on class -pub fn convert_py_to_operation_type( - py: Python, - py_op: PyObject, -) -> PyResult { - let attr = intern!(py, "_standard_gate"); - let py_op_bound = py_op.clone_ref(py).into_bound(py); - // Get PyType from either base_class if it exists, or if not use the - // class/type info from the pyobject - let binding = py_op_bound.getattr(intern!(py, "base_class")).ok(); - let op_obj = py_op_bound.get_type(); - let raw_op_type: Py = match binding { - Some(base_class) => base_class.downcast()?.clone().unbind(), - None => op_obj.unbind(), - }; - let op_type: Bound = raw_op_type.into_bound(py); - let mut standard: Option = match op_type.getattr(attr) { - Ok(stdgate) => stdgate.extract().ok().unwrap_or_default(), - Err(_) => None, - }; - // If the input instruction is a standard gate and a singleton instance - // we should check for mutable state. A mutable instance should be treated - // as a custom gate not a standard gate because it has custom properties. - // - // In the futuer we can revisit this when we've dropped `duration`, `unit`, - // and `condition` from the api as we should own the label in the - // `CircuitInstruction`. The other piece here is for controlled gates there - // is the control state, so for `SingletonControlledGates` we'll still need - // this check. - if standard.is_some() { - let mutable: bool = py_op.getattr(py, intern!(py, "mutable"))?.extract(py)?; - if mutable - && (py_op_bound.is_instance(SINGLETON_GATE.get_bound(py))? - || py_op_bound.is_instance(SINGLETON_CONTROLLED_GATE.get_bound(py))?) - { - standard = None; + if ob_type.is_subclass(GATE.get_bound(py))? { + let params = extract_params()?; + let gate = Box::new(PyGate { + qubits: ob.getattr(intern!(py, "num_qubits"))?.extract()?, + clbits: 0, + params: params.len() as u32, + op_name: ob.getattr(intern!(py, "name"))?.extract()?, + gate: ob.into_py(py), + }); + return Ok(OperationFromPython { + operation: PackedOperation::from_gate(gate), + params, + extra_attrs: extract_extra()?, + }); } + if ob_type.is_subclass(INSTRUCTION.get_bound(py))? { + let params = extract_params()?; + let instruction = Box::new(PyInstruction { + qubits: ob.getattr(intern!(py, "num_qubits"))?.extract()?, + clbits: ob.getattr(intern!(py, "num_clbits"))?.extract()?, + params: params.len() as u32, + op_name: ob.getattr(intern!(py, "name"))?.extract()?, + instruction: ob.into_py(py), + }); + return Ok(OperationFromPython { + operation: PackedOperation::from_instruction(instruction), + params, + extra_attrs: extract_extra()?, + }); + } + if ob_type.is_subclass(OPERATION.get_bound(py))? { + let params = extract_params()?; + let operation = Box::new(PyOperation { + qubits: ob.getattr(intern!(py, "num_qubits"))?.extract()?, + clbits: ob.getattr(intern!(py, "num_clbits"))?.extract()?, + params: params.len() as u32, + op_name: ob.getattr(intern!(py, "name"))?.extract()?, + operation: ob.into_py(py), + }); + return Ok(OperationFromPython { + operation: PackedOperation::from_operation(operation), + params, + extra_attrs: None, + }); + } + Err(PyTypeError::new_err(format!("invalid input: {}", ob))) } - if let Some(op) = standard { - let base_class = op_type.to_object(py); - populate_std_gate_map(py, op, base_class); - return Ok(OperationTypeConstruct { - operation: OperationType::Standard(op), - params: py_op.getattr(py, intern!(py, "params"))?.extract(py)?, - label: py_op.getattr(py, intern!(py, "label"))?.extract(py)?, - duration: py_op.getattr(py, intern!(py, "duration"))?.extract(py)?, - unit: py_op.getattr(py, intern!(py, "unit"))?.extract(py)?, - condition: py_op.getattr(py, intern!(py, "condition"))?.extract(py)?, - }); - } - if op_type.is_subclass(GATE.get_bound(py))? { - let params = py_op.getattr(py, intern!(py, "params"))?.extract(py)?; - let label = py_op.getattr(py, intern!(py, "label"))?.extract(py)?; - let duration = py_op.getattr(py, intern!(py, "duration"))?.extract(py)?; - let unit = py_op.getattr(py, intern!(py, "unit"))?.extract(py)?; - let condition = py_op.getattr(py, intern!(py, "condition"))?.extract(py)?; - - let out_op = PyGate { - qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, - clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, - params: py_op - .getattr(py, intern!(py, "params"))? - .downcast_bound::(py)? - .len() as u32, - op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, - gate: py_op, - }; - return Ok(OperationTypeConstruct { - operation: OperationType::Gate(out_op), - params, - label, - duration, - unit, - condition, - }); - } - if op_type.is_subclass(INSTRUCTION.get_bound(py))? { - let params = py_op.getattr(py, intern!(py, "params"))?.extract(py)?; - let label = py_op.getattr(py, intern!(py, "label"))?.extract(py)?; - let duration = py_op.getattr(py, intern!(py, "duration"))?.extract(py)?; - let unit = py_op.getattr(py, intern!(py, "unit"))?.extract(py)?; - let condition = py_op.getattr(py, intern!(py, "condition"))?.extract(py)?; - - let out_op = PyInstruction { - qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, - clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, - params: py_op - .getattr(py, intern!(py, "params"))? - .downcast_bound::(py)? - .len() as u32, - op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, - instruction: py_op, - }; - return Ok(OperationTypeConstruct { - operation: OperationType::Instruction(out_op), - params, - label, - duration, - unit, - condition, - }); - } +} - if op_type.is_subclass(OPERATION.get_bound(py))? { - let params = match py_op.getattr(py, intern!(py, "params")) { - Ok(value) => value.extract(py)?, - Err(_) => smallvec![], - }; - let label = None; - let duration = None; - let unit = None; - let condition = None; - let out_op = PyOperation { - qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, - clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, - params: match py_op.getattr(py, intern!(py, "params")) { - Ok(value) => value.downcast_bound::(py)?.len() as u32, - Err(_) => 0, - }, - op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, - operation: py_op, - }; - return Ok(OperationTypeConstruct { - operation: OperationType::Operation(out_op), - params, - label, - duration, - unit, - condition, - }); +/// Convert a sequence-like Python object to a tuple. +fn as_tuple<'py>(py: Python<'py>, seq: Option>) -> PyResult> { + let Some(seq) = seq else { return Ok(PyTuple::empty_bound(py)) }; + if seq.is_instance_of::() { + Ok(seq.downcast_into_exact::()?) + } else if seq.is_instance_of::() { + Ok(seq.downcast_exact::()?.to_tuple()) + } else { + // New tuple from iterable. + Ok(PyTuple::new_bound( + py, + seq.iter()? + .map(|o| Ok(o?.unbind())) + .collect::>>()?, + )) } - Err(PyValueError::new_err(format!("Invalid input: {}", py_op))) } /// Issue a Python `DeprecationWarning` about using the legacy tuple-like interface to diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 55a40c83dc39..51ef09eea543 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -10,17 +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::{ - convert_py_to_operation_type, operation_type_to_py, CircuitInstruction, - ExtraInstructionAttributes, -}; +#[cfg(feature = "cache_pygates")] +use std::cell::RefCell; + +use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; use crate::imports::QUANTUM_CIRCUIT; use crate::operations::Operation; + use numpy::IntoPyArray; + use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; use pyo3::{intern, IntoPy, PyObject, PyResult, ToPyObject}; -use smallvec::smallvec; /// Parent class for DAGOpNode, DAGInNode, and DAGOutNode. #[pyclass(module = "qiskit._accelerate.circuit", subclass)] @@ -73,19 +74,13 @@ pub struct DAGOpNode { #[pymethods] impl DAGOpNode { - #[allow(clippy::too_many_arguments)] #[new] - #[pyo3(signature = (op, qargs=None, cargs=None, params=smallvec![], label=None, duration=None, unit=None, condition=None, dag=None))] + #[pyo3(signature = (op, qargs=None, cargs=None, *, dag=None))] fn new( py: Python, - op: crate::circuit_instruction::OperationInput, + op: &Bound, qargs: Option<&Bound>, cargs: Option<&Bound>, - params: smallvec::SmallVec<[crate::operations::Param; 3]>, - label: Option, - duration: Option, - unit: Option, - condition: Option, dag: Option<&Bound>, ) -> PyResult<(Self, DAGNode)> { let qargs = @@ -120,30 +115,29 @@ impl DAGOpNode { } None => qargs.str()?.into_any(), }; - - let mut instruction = CircuitInstruction::py_new( - py, op, None, None, params, label, duration, unit, condition, - )?; - instruction.qubits = qargs.into(); - instruction.clbits = cargs.into(); - Ok(( DAGOpNode { - instruction, + instruction: CircuitInstruction::py_new( + op, + Some(qargs.into_any()), + Some(cargs.into_any()), + )?, sort_key: sort_key.unbind(), }, DAGNode { _node_id: -1 }, )) } + #[pyo3(signature = (instruction, /, *, dag=None, deepcopy=false))] #[staticmethod] fn from_instruction( py: Python, - instruction: CircuitInstruction, + mut instruction: CircuitInstruction, dag: Option<&Bound>, + deepcopy: bool, ) -> PyResult { - let qargs = instruction.qubits.clone_ref(py).into_bound(py); - let cargs = instruction.clbits.clone_ref(py).into_bound(py); + let qargs = instruction.qubits.bind(py); + let cargs = instruction.clbits.bind(py); let sort_key = match dag { Some(dag) => { @@ -172,6 +166,13 @@ impl DAGOpNode { } None => qargs.str()?.into_any(), }; + if deepcopy { + instruction.operation = instruction.operation.py_deepcopy(py, None)?; + #[cfg(feature = "cache_pygates")] + { + *instruction.py_op.borrow_mut() = None; + } + } let base = PyClassInitializer::from(DAGNode { _node_id: -1 }); let sub = base.add_subclass(DAGOpNode { instruction, @@ -180,12 +181,13 @@ impl DAGOpNode { Ok(Py::new(py, sub)?.to_object(py)) } - fn __reduce__(slf: PyRef, py: Python) -> PyResult { + fn __reduce__(slf: PyRef) -> PyResult { + let py = slf.py(); let state = (slf.as_ref()._node_id, &slf.sort_key); Ok(( py.get_type_bound::(), ( - operation_type_to_py(py, &slf.instruction)?, + slf.instruction.get_operation(py)?, &slf.instruction.qubits, &slf.instruction.clbits, ), @@ -201,42 +203,56 @@ impl DAGOpNode { Ok(()) } + /// Get a `CircuitInstruction` that represents the same information as this `DAGOpNode`. If + /// `deepcopy`, any internal Python objects are deep-copied. + /// + /// Note: this ought to be a temporary method, while the DAG/QuantumCircuit converters still go + /// via Python space; this still involves copy-out and copy-in of the data, whereas doing it all + /// within Rust space could directly re-pack the instruction from a `DAGOpNode` to a + /// `PackedInstruction` with no intermediate copy. + #[pyo3(signature = (/, *, deepcopy=false))] + fn _to_circuit_instruction(&self, py: Python, deepcopy: bool) -> PyResult { + Ok(CircuitInstruction { + operation: if deepcopy { + self.instruction.operation.py_deepcopy(py, None)? + } else { + self.instruction.operation.clone() + }, + qubits: self.instruction.qubits.clone_ref(py), + clbits: self.instruction.clbits.clone_ref(py), + params: self.instruction.params.clone(), + extra_attrs: self.instruction.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new(None), + }) + } + #[getter] fn get_op(&self, py: Python) -> PyResult { - operation_type_to_py(py, &self.instruction) + self.instruction.get_operation(py) } #[setter] - fn set_op(&mut self, py: Python, op: PyObject) -> PyResult<()> { - let res = convert_py_to_operation_type(py, op)?; + fn set_op(&mut self, op: &Bound) -> PyResult<()> { + let res = op.extract::()?; self.instruction.operation = res.operation; self.instruction.params = res.params; - let extra_attrs = if res.label.is_some() - || res.duration.is_some() - || res.unit.is_some() - || res.condition.is_some() + self.instruction.extra_attrs = res.extra_attrs; + #[cfg(feature = "cache_pygates")] { - Some(Box::new(ExtraInstructionAttributes { - label: res.label, - duration: res.duration, - unit: res.unit, - condition: res.condition, - })) - } else { - None - }; - self.instruction.extra_attrs = extra_attrs; + *self.instruction.py_op.borrow_mut() = None; + } Ok(()) } #[getter] fn num_qubits(&self) -> u32 { - self.instruction.operation.num_qubits() + self.instruction.op().num_qubits() } #[getter] fn num_clbits(&self) -> u32 { - self.instruction.operation.num_clbits() + self.instruction.op().num_clbits() } #[getter] @@ -261,8 +277,8 @@ impl DAGOpNode { /// Returns the Instruction name corresponding to the op for this node #[getter] - fn get_name(&self) -> &str { - self.instruction.operation.name() + fn get_name(&self, py: Python) -> Py { + self.instruction.op().name().into_py(py) } #[getter] @@ -276,7 +292,7 @@ impl DAGOpNode { #[getter] fn matrix(&self, py: Python) -> Option { - let matrix = self.instruction.operation.matrix(&self.instruction.params); + let matrix = self.instruction.op().matrix(&self.instruction.params); matrix.map(|mat| mat.into_pyarray_bound(py).into()) } @@ -312,6 +328,11 @@ impl DAGOpNode { .and_then(|attrs| attrs.unit.as_deref()) } + #[getter] + pub fn is_standard_gate(&self) -> bool { + self.instruction.is_standard_gate() + } + #[setter] fn set_label(&mut self, val: Option) { match self.instruction.extra_attrs.as_mut() { @@ -342,11 +363,9 @@ impl DAGOpNode { #[getter] fn definition<'py>(&self, py: Python<'py>) -> PyResult>> { - let definition = self - .instruction - .operation - .definition(&self.instruction.params); - definition + self.instruction + .op() + .definition(&self.instruction.params) .map(|data| { QUANTUM_CIRCUIT .get_bound(py) @@ -358,25 +377,17 @@ impl DAGOpNode { /// Sets the Instruction name corresponding to the op for this node #[setter] 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; + let op = self.instruction.get_operation(py)?.into_bound(py); + op.setattr(intern!(py, "name"), new_name)?; + self.instruction.operation = op.extract::()?.operation; Ok(()) } - #[getter] - fn _raw_op(&self, py: Python) -> PyObject { - self.instruction.operation.clone().into_py(py) - } - /// Returns a representation of the DAGOpNode fn __repr__(&self, py: Python) -> PyResult { Ok(format!( "DAGOpNode(op={}, qargs={}, cargs={})", - operation_type_to_py(py, &self.instruction)? - .bind(py) - .repr()?, + self.instruction.get_operation(py)?.bind(py).repr()?, self.instruction.qubits.bind(py).repr()?, self.instruction.clbits.bind(py).repr()? )) diff --git a/crates/circuit/src/lib.rs b/crates/circuit/src/lib.rs index 9f0a8017bf21..8204da4b753a 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -16,6 +16,7 @@ pub mod dag_node; pub mod gate_matrix; pub mod imports; pub mod operations; +pub mod packed_instruction; pub mod parameter_table; pub mod slice; pub mod util; @@ -64,8 +65,5 @@ pub fn circuit(m: Bound) -> PyResult<()> { 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 index 3bfef81d29ce..5ab96d977aa4 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -13,178 +13,175 @@ use std::f64::consts::PI; use crate::circuit_data::CircuitData; -use crate::imports::{DEEPCOPY, PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; +use crate::circuit_instruction::ExtraInstructionAttributes; +use crate::imports::get_std_gate_class; +use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; use crate::{gate_matrix, Qubit}; use ndarray::{aview2, Array2}; use num_complex::Complex64; +use smallvec::smallvec; + use numpy::IntoPyArray; use numpy::PyReadonlyArray2; use pyo3::prelude::*; +use pyo3::types::{IntoPyDict, PyTuple}; use pyo3::{intern, IntoPy, Python}; -use smallvec::smallvec; -/// Valid types for an operation field in a CircuitInstruction -/// -/// These are basically the types allowed in a QuantumCircuit -#[derive(FromPyObject, Clone, Debug)] -pub enum OperationType { - Standard(StandardGate), - Instruction(PyInstruction), - Gate(PyGate), - Operation(PyOperation), +#[derive(Clone, Debug)] +pub enum Param { + ParameterExpression(PyObject), + Float(f64), + Obj(PyObject), +} + +impl<'py> FromPyObject<'py> for Param { + fn extract_bound(b: &Bound<'py, PyAny>) -> Result { + Ok( + if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))? + || b.is_instance(QUANTUM_CIRCUIT.get_bound(b.py()))? + { + Param::ParameterExpression(b.clone().unbind()) + } else if let Ok(val) = b.extract::() { + Param::Float(val) + } else { + Param::Obj(b.clone().unbind()) + }, + ) + } } -impl IntoPy for OperationType { +impl IntoPy for Param { fn into_py(self, py: Python) -> PyObject { + match &self { + Self::Float(val) => val.to_object(py), + Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), + } + } +} + +impl ToPyObject for Param { + fn to_object(&self, py: Python) -> PyObject { match self { - Self::Standard(gate) => gate.into_py(py), - Self::Instruction(inst) => inst.into_py(py), - Self::Gate(gate) => gate.into_py(py), - Self::Operation(op) => op.into_py(py), + Self::Float(val) => val.to_object(py), + Self::ParameterExpression(val) => val.clone_ref(py), + Self::Obj(val) => val.clone_ref(py), } } } -impl Operation for OperationType { +/// Trait for generic circuit operations these define the common attributes +/// needed for something to be addable to the circuit struct +pub trait Operation { + fn name(&self) -> &str; + fn num_qubits(&self) -> u32; + fn num_clbits(&self) -> u32; + fn num_params(&self) -> u32; + fn control_flow(&self) -> bool; + fn matrix(&self, params: &[Param]) -> Option>; + fn definition(&self, params: &[Param]) -> Option; + fn standard_gate(&self) -> Option; + fn directive(&self) -> bool; +} + +/// Unpacked view object onto a `PackedOperation`. This is the return value of +/// `PackedInstruction::op`, and in turn is a view object onto a `PackedOperation`. +/// +/// This is the main way that we interact immutably with general circuit operations from Rust space. +pub enum OperationRef<'a> { + Standard(StandardGate), + Gate(&'a PyGate), + Instruction(&'a PyInstruction), + Operation(&'a PyOperation), +} + +impl<'a> Operation for OperationRef<'a> { + #[inline] 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(), + Self::Standard(standard) => standard.name(), + Self::Gate(gate) => gate.name(), + Self::Instruction(instruction) => instruction.name(), + Self::Operation(operation) => operation.name(), } } - + #[inline] 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(), + Self::Standard(standard) => standard.num_qubits(), + Self::Gate(gate) => gate.num_qubits(), + Self::Instruction(instruction) => instruction.num_qubits(), + Self::Operation(operation) => operation.num_qubits(), } } + #[inline] 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(), + Self::Standard(standard) => standard.num_clbits(), + Self::Gate(gate) => gate.num_clbits(), + Self::Instruction(instruction) => instruction.num_clbits(), + Self::Operation(operation) => operation.num_clbits(), } } - + #[inline] 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(), + Self::Standard(standard) => standard.num_params(), + Self::Gate(gate) => gate.num_params(), + Self::Instruction(instruction) => instruction.num_params(), + Self::Operation(operation) => operation.num_params(), } } - fn matrix(&self, params: &[Param]) -> Option> { + #[inline] + fn control_flow(&self) -> bool { 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), + Self::Standard(standard) => standard.control_flow(), + Self::Gate(gate) => gate.control_flow(), + Self::Instruction(instruction) => instruction.control_flow(), + Self::Operation(operation) => operation.control_flow(), } } - - fn control_flow(&self) -> bool { + #[inline] + fn matrix(&self, params: &[Param]) -> Option> { 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(), + Self::Standard(standard) => standard.matrix(params), + Self::Gate(gate) => gate.matrix(params), + Self::Instruction(instruction) => instruction.matrix(params), + Self::Operation(operation) => operation.matrix(params), } } - + #[inline] fn definition(&self, params: &[Param]) -> Option { match self { - Self::Standard(op) => op.definition(params), - Self::Gate(op) => op.definition(params), - Self::Instruction(op) => op.definition(params), - Self::Operation(op) => op.definition(params), + Self::Standard(standard) => standard.definition(params), + Self::Gate(gate) => gate.definition(params), + Self::Instruction(instruction) => instruction.definition(params), + Self::Operation(operation) => operation.definition(params), } } - + #[inline] 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(), + Self::Standard(standard) => standard.standard_gate(), + Self::Gate(gate) => gate.standard_gate(), + Self::Instruction(instruction) => instruction.standard_gate(), + Self::Operation(operation) => operation.standard_gate(), } } - + #[inline] fn directive(&self) -> bool { match self { - Self::Standard(op) => op.directive(), - Self::Gate(op) => op.directive(), - Self::Instruction(op) => op.directive(), - Self::Operation(op) => op.directive(), - } - } -} - -/// Trait for generic circuit operations these define the common attributes -/// needed for something to be addable to the circuit struct -pub trait Operation { - fn name(&self) -> &str; - fn num_qubits(&self) -> u32; - fn num_clbits(&self) -> u32; - fn num_params(&self) -> u32; - fn control_flow(&self) -> bool; - fn matrix(&self, params: &[Param]) -> Option>; - fn definition(&self, params: &[Param]) -> Option; - fn standard_gate(&self) -> Option; - fn directive(&self) -> bool; -} - -#[derive(Clone, Debug)] -pub enum Param { - ParameterExpression(PyObject), - Float(f64), - Obj(PyObject), -} - -impl<'py> FromPyObject<'py> for Param { - fn extract_bound(b: &Bound<'py, PyAny>) -> Result { - Ok( - if b.is_instance(PARAMETER_EXPRESSION.get_bound(b.py()))? - || b.is_instance(QUANTUM_CIRCUIT.get_bound(b.py()))? - { - Param::ParameterExpression(b.clone().unbind()) - } else if let Ok(val) = b.extract::() { - Param::Float(val) - } else { - Param::Obj(b.clone().unbind()) - }, - ) - } -} - -impl IntoPy for Param { - fn into_py(self, py: Python) -> PyObject { - match &self { - Self::Float(val) => val.to_object(py), - Self::ParameterExpression(val) => val.clone_ref(py), - Self::Obj(val) => val.clone_ref(py), - } - } -} - -impl ToPyObject for Param { - fn to_object(&self, py: Python) -> PyObject { - match self { - Self::Float(val) => val.to_object(py), - Self::ParameterExpression(val) => val.clone_ref(py), - Self::Obj(val) => val.clone_ref(py), + Self::Standard(standard) => standard.directive(), + Self::Gate(gate) => gate.directive(), + Self::Instruction(instruction) => instruction.directive(), + Self::Operation(operation) => operation.directive(), } } } #[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] +#[repr(u8)] #[pyclass(module = "qiskit._accelerate.circuit")] pub enum StandardGate { ZGate = 0, @@ -242,9 +239,18 @@ pub enum StandardGate { RZXGate = 52, } +unsafe impl ::bytemuck::CheckedBitPattern for StandardGate { + type Bits = u8; + + fn is_valid_bit_pattern(bits: &Self::Bits) -> bool { + *bits < 53 + } +} +unsafe impl ::bytemuck::NoUninit for StandardGate {} + impl ToPyObject for StandardGate { - fn to_object(&self, py: Python) -> PyObject { - self.into_py(py) + fn to_object(&self, py: Python) -> Py { + (*self).into_py(py) } } @@ -324,6 +330,37 @@ static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ "rzx", // 52 ]; +impl StandardGate { + pub fn create_py_op( + &self, + py: Python, + params: Option<&[Param]>, + extra_attrs: Option<&ExtraInstructionAttributes>, + ) -> PyResult> { + let gate_class = get_std_gate_class(py, *self)?; + let args = match params.unwrap_or(&[]) { + &[] => PyTuple::empty_bound(py), + params => PyTuple::new_bound(py, params), + }; + if let Some(extra) = extra_attrs { + let kwargs = [ + ("label", extra.label.to_object(py)), + ("unit", extra.unit.to_object(py)), + ("duration", extra.duration.to_object(py)), + ] + .into_py_dict_bound(py); + let mut out = gate_class.call_bound(py, args, Some(&kwargs))?; + if let Some(ref condition) = extra.condition { + out = out.call_method0(py, "to_mutable")?; + out.setattr(py, "condition", condition)?; + } + Ok(out) + } else { + gate_class.call_bound(py, args, None) + } + } +} + #[pymethods] impl StandardGate { pub fn copy(&self) -> Self { @@ -1444,7 +1481,8 @@ fn add_param(param: &Param, summand: f64, py: Python) -> Param { /// This class is used to wrap a Python side Instruction that is not in the standard library #[derive(Clone, Debug)] -#[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] +// We bit-pack pointers to this, so having a known alignment even on 32-bit systems is good. +#[repr(align(8))] pub struct PyInstruction { pub qubits: u32, pub clbits: u32, @@ -1453,30 +1491,6 @@ 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, - } - } - - fn __deepcopy__(&self, py: Python, _memo: PyObject) -> PyResult { - Ok(PyInstruction { - qubits: self.qubits, - clbits: self.clbits, - params: self.params, - op_name: self.op_name.clone(), - instruction: DEEPCOPY.get_bound(py).call1((&self.instruction,))?.unbind(), - }) - } -} - impl Operation for PyInstruction { fn name(&self) -> &str { self.op_name.as_str() @@ -1533,7 +1547,8 @@ 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(freelist = 20, module = "qiskit._accelerate.circuit")] +// We bit-pack pointers to this, so having a known alignment even on 32-bit systems is good. +#[repr(align(8))] pub struct PyGate { pub qubits: u32, pub clbits: u32, @@ -1542,30 +1557,6 @@ pub struct PyGate { 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, - } - } - - fn __deepcopy__(&self, py: Python, _memo: PyObject) -> PyResult { - Ok(PyGate { - qubits: self.qubits, - clbits: self.clbits, - params: self.params, - op_name: self.op_name.clone(), - gate: DEEPCOPY.get_bound(py).call1((&self.gate,))?.unbind(), - }) - } -} - impl Operation for PyGate { fn name(&self) -> &str { self.op_name.as_str() @@ -1635,7 +1626,8 @@ 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(freelist = 20, module = "qiskit._accelerate.circuit")] +// We bit-pack pointers to this, so having a known alignment even on 32-bit systems is good. +#[repr(align(8))] pub struct PyOperation { pub qubits: u32, pub clbits: u32, @@ -1644,30 +1636,6 @@ 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, - } - } - - fn __deepcopy__(&self, py: Python, _memo: PyObject) -> PyResult { - Ok(PyOperation { - qubits: self.qubits, - clbits: self.clbits, - params: self.params, - op_name: self.op_name.clone(), - operation: DEEPCOPY.get_bound(py).call1((&self.operation,))?.unbind(), - }) - } -} - impl Operation for PyOperation { fn name(&self) -> &str { self.op_name.as_str() diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs new file mode 100644 index 000000000000..f2fdeda29e50 --- /dev/null +++ b/crates/circuit/src/packed_instruction.rs @@ -0,0 +1,497 @@ +// 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. + +#[cfg(feature = "cache_pygates")] +use std::cell::RefCell; +use std::ptr::NonNull; + +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::types::PyDict; + +use smallvec::SmallVec; + +use crate::circuit_instruction::ExtraInstructionAttributes; +use crate::imports::DEEPCOPY; +use crate::operations::{OperationRef, Param, PyGate, PyInstruction, PyOperation, StandardGate}; + +/// The logical discriminant of `PackedOperation`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +enum PackedOperationType { + // It's important that the `StandardGate` item is 0, so that zeroing out a `PackedOperation` + // will make it appear as a standard gate, which will never allow accidental dangling-pointer + // dereferencing. + StandardGate = 0, + Gate = 1, + Instruction = 2, + Operation = 3, +} +unsafe impl ::bytemuck::CheckedBitPattern for PackedOperationType { + type Bits = u8; + + fn is_valid_bit_pattern(bits: &Self::Bits) -> bool { + *bits < 4 + } +} +unsafe impl ::bytemuck::NoUninit for PackedOperationType {} + +/// A bit-packed `OperationType` enumeration. +/// +/// This is logically equivalent to: +/// +/// ```rust +/// enum Operation { +/// Standard(StandardGate), +/// Gate(Box), +/// Instruction(Box), +/// Operation(Box), +/// } +/// ``` +/// +/// including all ownership semantics, except it bit-packs the enumeration into a single pointer. +/// This works because `PyGate` (and friends) have an alignment of 8, so pointers to them always +/// have the low three bits set to 0, and `StandardGate` has a width much smaller than a pointer. +/// This lets us store the enum discriminant in the low data bits, and then type-pun a suitable +/// bitmask on the contained value back into proper data. +/// +/// Explicity, this is logical memory layout of `PackedOperation` on a 64-bit system, written out as +/// a binary integer. `x` marks padding bits with undefined values, `S` is the bits that make up a +/// `StandardGate`, and `P` is bits that make up part of a pointer. +/// +/// ```text +/// Standard gate: +/// 0b_xxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxSS_SSSSSS00 +/// |-------||| +/// | | +/// Standard gate, stored inline as a u8. --+ +-- Discriminant. +/// +/// Python object: +/// 0b_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPPPPP_PPPPP10 +/// |------------------------------------------------------------------||| +/// | | +/// The high 62 bits of the pointer. Because of alignment, the low 3 | Discriminant of the +/// bits of the full 64 bits are guaranteed to be zero (so one marked +-- enumeration. This +/// `P` is always zero here), so we can retrieve the "full" pointer by is 0b10, which means +/// taking the whole `usize` and zeroing the low 3 bits, letting us that this points to +/// store the discriminant in there at other times. a `PyInstruction`. +/// ``` +/// +/// There is currently one spare bit that could be used for additional metadata, if required. +/// +/// # Construction +/// +/// From Rust space, build this type using one of the `from_*` methods, depending on which +/// implementer of `Operation` you have. `StandardGate` has an implementation of `Into` for this. +/// +/// From Python space, use the supplied `FromPyObject`. +/// +/// # Safety +/// +/// `PackedOperation` asserts ownership over its contained pointer (if not a `StandardGate`). This +/// has the following requirements: +/// +/// * The pointer must be managed by a `Box` using the global allocator. +/// * The pointed-to data must match the type of the discriminant used to store it. +/// * `PackedOperation` must take care to forward implementations of `Clone` and `Drop` to the +/// contained pointer. +#[derive(Debug)] +#[repr(transparent)] +pub struct PackedOperation(usize); + +impl PackedOperation { + /// The bits representing the `PackedOperationType` discriminant. This can be used to mask out + /// the discriminant, and defines the rest of the bit shifting. + const DISCRIMINANT_MASK: usize = 0b11; + /// The number of bits used to store the discriminant metadata. + const DISCRIMINANT_BITS: u32 = Self::DISCRIMINANT_MASK.count_ones(); + /// A bitmask that masks out only the standard gate information. This should always have the + /// same effect as `POINTER_MASK` because the high bits should be 0 for a `StandardGate`, but + /// this is defensive against us adding further metadata on `StandardGate` later. After + /// masking, the resulting integer still needs shifting downwards to retrieve the standard gate. + const STANDARD_GATE_MASK: usize = (u8::MAX as usize) << Self::DISCRIMINANT_BITS; + /// A bitmask that retrieves the stored pointer directly. The discriminant is stored in the + /// low pointer bits that are guaranteed to be 0 by alignment, so no shifting is required. + const POINTER_MASK: usize = usize::MAX ^ Self::DISCRIMINANT_MASK; + + /// Extract the discriminant of the operation. + #[inline] + fn discriminant(&self) -> PackedOperationType { + ::bytemuck::checked::cast((self.0 & Self::DISCRIMINANT_MASK) as u8) + } + + /// Get the contained pointer to the `PyGate`/`PyInstruction`/`PyOperation` that this object + /// contains. + /// + /// **Panics** if the object represents a standard gate; see `try_pointer`. + #[inline] + fn pointer(&self) -> NonNull<()> { + self.try_pointer().unwrap() + } + + /// Get the contained pointer to the `PyGate`/`PyInstruction`/`PyOperation` that this object + /// contains. + /// + /// Returns `None` if the object represents a standard gate. + #[inline] + pub fn try_pointer(&self) -> Option> { + match self.discriminant() { + PackedOperationType::StandardGate => None, + PackedOperationType::Gate + | PackedOperationType::Instruction + | PackedOperationType::Operation => { + let ptr = (self.0 & Self::POINTER_MASK) as *mut (); + // SAFETY: `PackedOperation` can only be constructed from a pointer via `Box`, which + // is always non-null (except in the case that we're partway through a `Drop`). + Some(unsafe { NonNull::new_unchecked(ptr) }) + } + } + } + + /// Get the contained `StandardGate`. + /// + /// **Panics** if this `PackedOperation` doesn't contain a `StandardGate`; see + /// `try_standard_gate`. + #[inline] + pub fn standard_gate(&self) -> StandardGate { + self.try_standard_gate().unwrap() + } + + /// Get the contained `StandardGate`, if any. + #[inline] + pub fn try_standard_gate(&self) -> Option { + match self.discriminant() { + PackedOperationType::StandardGate => ::bytemuck::checked::try_cast( + ((self.0 & Self::STANDARD_GATE_MASK) >> Self::DISCRIMINANT_BITS) as u8, + ) + .ok(), + _ => None, + } + } + + /// Get a safe view onto the packed data within, without assuming ownership. + #[inline] + pub fn view(&self) -> OperationRef { + match self.discriminant() { + PackedOperationType::StandardGate => OperationRef::Standard(self.standard_gate()), + PackedOperationType::Gate => { + let ptr = self.pointer().cast::(); + OperationRef::Gate(unsafe { ptr.as_ref() }) + } + PackedOperationType::Instruction => { + let ptr = self.pointer().cast::(); + OperationRef::Instruction(unsafe { ptr.as_ref() }) + } + PackedOperationType::Operation => { + let ptr = self.pointer().cast::(); + OperationRef::Operation(unsafe { ptr.as_ref() }) + } + } + } + + /// Create a `PackedOperation` from a `StandardGate`. + #[inline] + pub fn from_standard(standard: StandardGate) -> Self { + Self((standard as usize) << Self::DISCRIMINANT_BITS) + } + + /// Create a `PackedOperation` given a raw pointer to the inner type. + /// + /// **Panics** if the given `discriminant` does not correspond to a pointer type. + /// + /// SAFETY: the inner pointer must have come from an owning `Box` in the global allocator, whose + /// type matches that indicated by the discriminant. The returned `PackedOperation` takes + /// ownership of the pointed-to data. + #[inline] + unsafe fn from_py_wrapper(discriminant: PackedOperationType, value: NonNull<()>) -> Self { + if discriminant == PackedOperationType::StandardGate { + panic!("given standard-gate discriminant during pointer-type construction") + } + let addr = value.as_ptr() as usize; + assert_eq!(addr & Self::DISCRIMINANT_MASK, 0); + Self(addr | (discriminant as usize)) + } + + /// Construct a new `PackedOperation` from an owned heap-allocated `PyGate`. + pub fn from_gate(gate: Box) -> Self { + let ptr = NonNull::from(Box::leak(gate)).cast::<()>(); + // SAFETY: the `ptr` comes directly from a owning `Box` of the correct type. + unsafe { Self::from_py_wrapper(PackedOperationType::Gate, ptr) } + } + + /// Construct a new `PackedOperation` from an owned heap-allocated `PyInstruction`. + pub fn from_instruction(instruction: Box) -> Self { + let ptr = NonNull::from(Box::leak(instruction)).cast::<()>(); + // SAFETY: the `ptr` comes directly from a owning `Box` of the correct type. + unsafe { Self::from_py_wrapper(PackedOperationType::Instruction, ptr) } + } + + /// Construct a new `PackedOperation` from an owned heap-allocated `PyOperation`. + pub fn from_operation(operation: Box) -> Self { + let ptr = NonNull::from(Box::leak(operation)).cast::<()>(); + // SAFETY: the `ptr` comes directly from a owning `Box` of the correct type. + unsafe { Self::from_py_wrapper(PackedOperationType::Operation, ptr) } + } + + /// Check equality of the operation, including Python-space checks, if appropriate. + pub fn py_eq(&self, py: Python, other: &PackedOperation) -> PyResult { + match (self.view(), other.view()) { + (OperationRef::Standard(left), OperationRef::Standard(right)) => Ok(left == right), + (OperationRef::Gate(left), OperationRef::Gate(right)) => { + left.gate.bind(py).eq(&right.gate) + } + (OperationRef::Instruction(left), OperationRef::Instruction(right)) => { + left.instruction.bind(py).eq(&right.instruction) + } + (OperationRef::Operation(left), OperationRef::Operation(right)) => { + left.operation.bind(py).eq(&right.operation) + } + _ => Ok(false), + } + } + + /// Copy this operation, including a Python-space deep copy, if required. + pub fn py_deepcopy<'py>( + &self, + py: Python<'py>, + memo: Option<&Bound<'py, PyDict>>, + ) -> PyResult { + let deepcopy = DEEPCOPY.get_bound(py); + match self.view() { + OperationRef::Standard(standard) => Ok(standard.into()), + OperationRef::Gate(gate) => Ok(PyGate { + gate: deepcopy.call1((&gate.gate, memo))?.unbind(), + qubits: gate.qubits, + clbits: gate.clbits, + params: gate.params, + op_name: gate.op_name.clone(), + } + .into()), + OperationRef::Instruction(instruction) => Ok(PyInstruction { + instruction: deepcopy.call1((&instruction.instruction, memo))?.unbind(), + qubits: instruction.qubits, + clbits: instruction.clbits, + params: instruction.params, + op_name: instruction.op_name.clone(), + } + .into()), + OperationRef::Operation(operation) => Ok(PyOperation { + operation: deepcopy.call1((&operation.operation, memo))?.unbind(), + qubits: operation.qubits, + clbits: operation.clbits, + params: operation.params, + op_name: operation.op_name.clone(), + } + .into()), + } + } + + /// Copy this operation, including a Python-space call to `copy` on the `Operation` subclass, if + /// any. + pub fn py_copy(&self, py: Python) -> PyResult { + let copy_attr = intern!(py, "copy"); + match self.view() { + OperationRef::Standard(standard) => Ok(standard.into()), + OperationRef::Gate(gate) => Ok(Box::new(PyGate { + gate: gate.gate.call_method0(py, copy_attr)?, + qubits: gate.qubits, + clbits: gate.clbits, + params: gate.params, + op_name: gate.op_name.clone(), + }) + .into()), + OperationRef::Instruction(instruction) => Ok(Box::new(PyInstruction { + instruction: instruction.instruction.call_method0(py, copy_attr)?, + qubits: instruction.qubits, + clbits: instruction.clbits, + params: instruction.params, + op_name: instruction.op_name.clone(), + }) + .into()), + OperationRef::Operation(operation) => Ok(Box::new(PyOperation { + operation: operation.operation.call_method0(py, copy_attr)?, + qubits: operation.qubits, + clbits: operation.clbits, + params: operation.params, + op_name: operation.op_name.clone(), + }) + .into()), + } + } +} + +impl From for PackedOperation { + #[inline] + fn from(value: StandardGate) -> Self { + Self::from_standard(value) + } +} + +macro_rules! impl_packed_operation_from_py { + ($type:ty, $constructor:path) => { + impl From<$type> for PackedOperation { + #[inline] + fn from(value: $type) -> Self { + $constructor(Box::new(value)) + } + } + + impl From> for PackedOperation { + #[inline] + fn from(value: Box<$type>) -> Self { + $constructor(value) + } + } + }; +} +impl_packed_operation_from_py!(PyGate, PackedOperation::from_gate); +impl_packed_operation_from_py!(PyInstruction, PackedOperation::from_instruction); +impl_packed_operation_from_py!(PyOperation, PackedOperation::from_operation); + +impl Clone for PackedOperation { + fn clone(&self) -> Self { + match self.view() { + OperationRef::Standard(standard) => Self::from_standard(standard), + OperationRef::Gate(gate) => Self::from_gate(Box::new(gate.to_owned())), + OperationRef::Instruction(instruction) => { + Self::from_instruction(Box::new(instruction.to_owned())) + } + OperationRef::Operation(operation) => { + Self::from_operation(Box::new(operation.to_owned())) + } + } + } +} +impl Drop for PackedOperation { + fn drop(&mut self) { + fn drop_pointer_as(slf: &mut PackedOperation) { + // This should only ever be called when the pointer is valid, but this is defensive just + // to 100% ensure that our `Drop` implementation doesn't panic. + let Some(pointer) = slf.try_pointer() else { return }; + // SAFETY: `PackedOperation` asserts ownership over its contents, and the contained + // pointer can only be null if we were already dropped. We set our discriminant to mark + // ourselves as plain old data immediately just as a defensive measure. + let boxed = unsafe { Box::from_raw(pointer.cast::().as_ptr()) }; + slf.0 = PackedOperationType::StandardGate as usize; + ::std::mem::drop(boxed); + } + + match self.discriminant() { + PackedOperationType::StandardGate => (), + PackedOperationType::Gate => drop_pointer_as::(self), + PackedOperationType::Instruction => drop_pointer_as::(self), + PackedOperationType::Operation => drop_pointer_as::(self), + } + } +} + +/// The data-at-rest compressed storage format for a circuit instruction. +/// +/// Much of the actual data of a `PackedInstruction` is stored in the `CircuitData` (or +/// DAG-equivalent) context objects, and the `PackedInstruction` itself just contains handles to +/// that data. Components of the `PackedInstruction` can be unpacked individually by passing the +/// `CircuitData` object to the relevant getter method. Many `PackedInstruction`s may contain +/// handles to the same data within a `CircuitData` objects; we are re-using what we can. +/// +/// A `PackedInstruction` in general cannot be safely mutated outside the context of its +/// `CircuitData`, because the majority of the data is not actually stored here. +#[derive(Clone, Debug)] +pub struct PackedInstruction { + pub op: PackedOperation, + /// The index under which the interner has stored `qubits`. + pub qubits: crate::interner::Index, + /// The index under which the interner has stored `clbits`. + pub clbits: crate::interner::Index, + pub params: Option>>, + pub extra_attrs: Option>, + + #[cfg(feature = "cache_pygates")] + /// This is hidden in a `RefCell` because, while that has additional memory-usage implications + /// while we're still building with the feature enabled, we intend to remove the feature in the + /// future, and hiding the cache within a `RefCell` lets us keep the cache transparently in our + /// interfaces, without needing various functions to unnecessarily take `&mut` references. + pub py_op: RefCell>>, +} + +impl PackedInstruction { + /// Immutably view the contained operation. + /// + /// If you only care whether the contained operation is a `StandardGate` or not, you can use + /// `PackedInstruction::standard_gate`, which is a bit cheaper than this function. + #[inline] + pub fn op(&self) -> OperationRef { + self.op.view() + } + + /// Access the standard gate in this `PackedInstruction`, if it is one. If the instruction + /// refers to a Python-space object, `None` is returned. + #[inline] + pub fn standard_gate(&self) -> Option { + self.op.try_standard_gate() + } + + /// Get a slice view onto the contained parameters. + #[inline] + pub fn params_view(&self) -> &[Param] { + self.params + .as_deref() + .map(SmallVec::as_slice) + .unwrap_or(&[]) + } + + /// Get a mutable slice view onto the contained parameters. + #[inline] + pub fn params_mut(&mut self) -> &mut [Param] { + self.params + .as_deref_mut() + .map(SmallVec::as_mut_slice) + .unwrap_or(&mut []) + } + + /// Build a reference to the Python-space operation object (the `Gate`, etc) packed into this + /// instruction. This may construct the reference if the `PackedInstruction` is a standard + /// gate with no already stored operation. + /// + /// A standard-gate operation object returned by this function is disconnected from the + /// containing circuit; updates to its parameters, label, duration, unit and condition will not + /// be propagated back. + pub fn unpack_py_op(&self, py: Python) -> PyResult> { + #[cfg(feature = "cache_pygates")] + { + if let Ok(Some(cached_op)) = self.py_op.try_borrow().as_deref() { + return Ok(cached_op.clone_ref(py)); + } + } + + let out = match self.op.view() { + OperationRef::Standard(standard) => standard + .create_py_op( + py, + self.params.as_deref().map(SmallVec::as_slice), + self.extra_attrs.as_deref(), + )? + .into_any(), + OperationRef::Gate(gate) => gate.gate.clone_ref(py), + OperationRef::Instruction(instruction) => instruction.instruction.clone_ref(py), + OperationRef::Operation(operation) => operation.operation.clone_ref(py), + }; + + #[cfg(feature = "cache_pygates")] + { + if let Ok(mut cell) = self.py_op.try_borrow_mut() { + cell.get_or_insert_with(|| out.clone_ref(py)); + } + } + + Ok(out) + } +} diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py index 79f04a65714d..572142440372 100644 --- a/qiskit/circuit/commutation_checker.py +++ b/qiskit/circuit/commutation_checker.py @@ -67,11 +67,11 @@ def commute_nodes( """Checks if two DAGOpNodes commute.""" qargs1 = op1.qargs cargs1 = op2.cargs - if not isinstance(op1._raw_op, StandardGate): + if not op1.is_standard_gate: op1 = op1.op qargs2 = op2.qargs cargs2 = op2.cargs - if not isinstance(op2._raw_op, StandardGate): + if not op2.is_standard_gate: op2 = op2.op return self.commute(op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits) diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index 576d5dee8267..cc8a050fd2b0 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -143,9 +143,7 @@ def c_if(self, classical: Clbit | ClassicalRegister | int, val: int) -> "Instruc for idx, instruction in enumerate(self._instructions): if isinstance(instruction, CircuitInstruction): updated = instruction.operation.c_if(classical, val) - self._instructions[idx] = instruction.replace( - operation=updated, condition=updated.condition - ) + self._instructions[idx] = instruction.replace(operation=updated) else: data, idx = instruction instruction = data[idx] diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 6d41e6fdcd25..4692e0f4468e 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, PyGate, PyInstruction, PyOperation +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 @@ -2031,7 +2031,7 @@ def map_vars(op): instructions = source._data.copy(copy_instructions=copy) instructions.replace_bits(qubits=new_qubits, clbits=new_clbits) - instructions.map_ops(map_vars) + instructions.map_nonstandard_ops(map_vars) dest._current_scope().extend(instructions) append_existing = None @@ -2304,9 +2304,8 @@ def cbit_argument_conversion(self, clbit_representation: ClbitSpecifier) -> list def _append_standard_gate( self, op: StandardGate, - params: Sequence[ParameterValueType] | None = None, - qargs: Sequence[QubitSpecifier] | None = None, - cargs: Sequence[ClbitSpecifier] | None = None, + qargs: Sequence[QubitSpecifier] = (), + params: Sequence[ParameterValueType] = (), label: str | None = None, ) -> InstructionSet: """An internal method to bypass some checking when directly appending a standard gate.""" @@ -2316,16 +2315,14 @@ def _append_standard_gate( params = [] expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []] - expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []] - if params is not None: - for param in params: - Gate.validate_parameter(op, param) + 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) + broadcast_iter = Gate.broadcast_arguments(op, expanded_qargs, []) for qarg, carg in broadcast_iter: self._check_dups(qarg) - instruction = CircuitInstruction(op, qarg, carg, params=params, label=label) + instruction = CircuitInstruction.from_standard(op, qarg, params, label=label) circuit_scope.append(instruction, _standard_gate=True) instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) return instructions @@ -2427,38 +2424,10 @@ 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, - ) - + base_instruction = CircuitInstruction(operation, (), ()) for qarg, carg in broadcast_iter: self._check_dups(qarg) - instruction = CircuitInstruction(operation, qarg, carg, params=params) + instruction = base_instruction.replace(qubits=qarg, clbits=carg) circuit_scope.append(instruction) instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) return instructions @@ -4492,7 +4461,7 @@ def h(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.HGate, [], qargs=[qubit]) + return self._append_standard_gate(StandardGate.HGate, [qubit], ()) def ch( self, @@ -4519,7 +4488,7 @@ def ch( # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( - StandardGate.CHGate, [], qargs=[control_qubit, target_qubit], label=label + StandardGate.CHGate, [control_qubit, target_qubit], (), label=label ) from .library.standard_gates.h import CHGate @@ -4542,7 +4511,7 @@ def id(self, qubit: QubitSpecifier) -> InstructionSet: # pylint: disable=invali Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.IGate, None, qargs=[qubit]) + return self._append_standard_gate(StandardGate.IGate, [qubit], ()) def ms(self, theta: ParameterValueType, qubits: Sequence[QubitSpecifier]) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.MSGate`. @@ -4573,7 +4542,7 @@ def p(self, theta: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.PhaseGate, [theta], qargs=[qubit]) + return self._append_standard_gate(StandardGate.PhaseGate, [qubit], (theta,)) def cp( self, @@ -4602,7 +4571,7 @@ def cp( # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( - StandardGate.CPhaseGate, [theta], qargs=[control_qubit, target_qubit], label=label + StandardGate.CPhaseGate, [control_qubit, target_qubit], (theta,), label=label ) from .library.standard_gates.p import CPhaseGate @@ -4661,7 +4630,7 @@ def r( Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.RGate, [theta, phi], qargs=[qubit]) + return self._append_standard_gate(StandardGate.RGate, [qubit], [theta, phi]) def rv( self, @@ -4758,7 +4727,7 @@ def rx( Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.RXGate, [theta], [qubit], None, label=label) + return self._append_standard_gate(StandardGate.RXGate, [qubit], [theta], label=label) def crx( self, @@ -4787,7 +4756,7 @@ def crx( # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( - StandardGate.CRXGate, [theta], [control_qubit, target_qubit], None, label=label + StandardGate.CRXGate, [control_qubit, target_qubit], [theta], label=label ) from .library.standard_gates.rx import CRXGate @@ -4814,7 +4783,7 @@ def rxx( Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.RXXGate, [theta], [qubit1, qubit2]) + return self._append_standard_gate(StandardGate.RXXGate, [qubit1, qubit2], [theta]) def ry( self, theta: ParameterValueType, qubit: QubitSpecifier, label: str | None = None @@ -4831,7 +4800,7 @@ def ry( Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.RYGate, [theta], [qubit], None, label=label) + return self._append_standard_gate(StandardGate.RYGate, [qubit], [theta], label=label) def cry( self, @@ -4860,7 +4829,7 @@ def cry( # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( - StandardGate.CRYGate, [theta], [control_qubit, target_qubit], None, label=label + StandardGate.CRYGate, [control_qubit, target_qubit], [theta], label=label ) from .library.standard_gates.ry import CRYGate @@ -4887,7 +4856,7 @@ def ryy( Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.RYYGate, [theta], [qubit1, qubit2]) + return self._append_standard_gate(StandardGate.RYYGate, [qubit1, qubit2], [theta]) def rz(self, phi: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.RZGate`. @@ -4901,7 +4870,7 @@ def rz(self, phi: ParameterValueType, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.RZGate, [phi], [qubit], None) + return self._append_standard_gate(StandardGate.RZGate, [qubit], [phi]) def crz( self, @@ -4930,7 +4899,7 @@ def crz( # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( - StandardGate.CRZGate, [theta], [control_qubit, target_qubit], None, label=label + StandardGate.CRZGate, [control_qubit, target_qubit], [theta], label=label ) from .library.standard_gates.rz import CRZGate @@ -4957,7 +4926,7 @@ def rzx( Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.RZXGate, [theta], [qubit1, qubit2]) + return self._append_standard_gate(StandardGate.RZXGate, [qubit1, qubit2], [theta]) def rzz( self, theta: ParameterValueType, qubit1: QubitSpecifier, qubit2: QubitSpecifier @@ -4974,7 +4943,7 @@ def rzz( Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.RZZGate, [theta], [qubit1, qubit2]) + return self._append_standard_gate(StandardGate.RZZGate, [qubit1, qubit2], [theta]) def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.ECRGate`. @@ -4987,7 +4956,7 @@ def ecr(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.ECRGate, [], qargs=[qubit1, qubit2]) + return self._append_standard_gate(StandardGate.ECRGate, [qubit1, qubit2], ()) def s(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SGate`. @@ -5000,7 +4969,7 @@ def s(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SGate, [], qargs=[qubit]) + return self._append_standard_gate(StandardGate.SGate, [qubit], ()) def sdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SdgGate`. @@ -5013,7 +4982,7 @@ def sdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SdgGate, [], qargs=[qubit]) + return self._append_standard_gate(StandardGate.SdgGate, [qubit], ()) def cs( self, @@ -5040,7 +5009,7 @@ def cs( # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( - StandardGate.CSGate, [], qargs=[control_qubit, target_qubit], label=label + StandardGate.CSGate, [control_qubit, target_qubit], (), label=label ) from .library.standard_gates.s import CSGate @@ -5077,7 +5046,7 @@ def csdg( # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( - StandardGate.CSdgGate, [], qargs=[control_qubit, target_qubit], label=label + StandardGate.CSdgGate, [control_qubit, target_qubit], (), label=label ) from .library.standard_gates.s import CSdgGate @@ -5102,8 +5071,8 @@ def swap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet """ return self._append_standard_gate( StandardGate.SwapGate, - [], - qargs=[qubit1, qubit2], + [qubit1, qubit2], + (), ) def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: @@ -5117,7 +5086,7 @@ def iswap(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSe Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.ISwapGate, [], qargs=[qubit1, qubit2]) + return self._append_standard_gate(StandardGate.ISwapGate, [qubit1, qubit2], ()) def cswap( self, @@ -5147,8 +5116,8 @@ def cswap( if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( StandardGate.CSwapGate, - [], - qargs=[control_qubit, target_qubit1, target_qubit2], + [control_qubit, target_qubit1, target_qubit2], + (), label=label, ) @@ -5172,7 +5141,7 @@ def sx(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SXGate, [], qargs=[qubit]) + return self._append_standard_gate(StandardGate.SXGate, [qubit], ()) def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.SXdgGate`. @@ -5185,7 +5154,7 @@ def sxdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.SXdgGate, [], qargs=[qubit]) + return self._append_standard_gate(StandardGate.SXdgGate, [qubit], ()) def csx( self, @@ -5212,7 +5181,7 @@ def csx( # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( - StandardGate.CSXGate, [], qargs=[control_qubit, target_qubit], label=label + StandardGate.CSXGate, [control_qubit, target_qubit], (), label=label ) from .library.standard_gates.sx import CSXGate @@ -5235,7 +5204,7 @@ def t(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.TGate, [], qargs=[qubit]) + return self._append_standard_gate(StandardGate.TGate, [qubit], ()) def tdg(self, qubit: QubitSpecifier) -> InstructionSet: """Apply :class:`~qiskit.circuit.library.TdgGate`. @@ -5248,7 +5217,7 @@ def tdg(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.TdgGate, [], qargs=[qubit]) + return self._append_standard_gate(StandardGate.TdgGate, [qubit], ()) def u( self, @@ -5270,7 +5239,7 @@ def u( Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.UGate, [theta, phi, lam], qargs=[qubit]) + return self._append_standard_gate(StandardGate.UGate, [qubit], [theta, phi, lam]) def cu( self, @@ -5323,7 +5292,7 @@ def x(self, qubit: QubitSpecifier, label: str | None = None) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.XGate, None, qargs=[qubit], label=label) + return self._append_standard_gate(StandardGate.XGate, [qubit], (), label=label) def cx( self, @@ -5351,9 +5320,8 @@ def cx( if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( StandardGate.CXGate, - [], - qargs=[control_qubit, target_qubit], - cargs=None, + [control_qubit, target_qubit], + (), label=label, ) @@ -5378,7 +5346,7 @@ def dcx(self, qubit1: QubitSpecifier, qubit2: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(op=StandardGate.DCXGate, qargs=[qubit1, qubit2]) + return self._append_standard_gate(StandardGate.DCXGate, [qubit1, qubit2], ()) def ccx( self, @@ -5406,9 +5374,8 @@ def ccx( if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( StandardGate.CCXGate, - [], - qargs=[control_qubit1, control_qubit2, target_qubit], - cargs=None, + [control_qubit1, control_qubit2, target_qubit], + (), ) from .library.standard_gates.x import CCXGate @@ -5515,7 +5482,7 @@ def y(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.YGate, None, qargs=[qubit]) + return self._append_standard_gate(StandardGate.YGate, [qubit], ()) def cy( self, @@ -5543,9 +5510,8 @@ def cy( if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( StandardGate.CYGate, - [], - qargs=[control_qubit, target_qubit], - cargs=None, + [control_qubit, target_qubit], + (), label=label, ) @@ -5569,7 +5535,7 @@ def z(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.ZGate, None, qargs=[qubit]) + return self._append_standard_gate(StandardGate.ZGate, [qubit], ()) def cz( self, @@ -5596,7 +5562,7 @@ def cz( # if the control state is |1> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( - StandardGate.CZGate, [], qargs=[control_qubit, target_qubit], label=label + StandardGate.CZGate, [control_qubit, target_qubit], (), label=label ) from .library.standard_gates.z import CZGate diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index 88d9c72f1d61..6ec615d1a8d2 100644 --- a/qiskit/converters/circuit_to_dag.py +++ b/qiskit/converters/circuit_to_dag.py @@ -94,24 +94,9 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord dagcircuit.add_creg(register) for instruction in circuit.data: - if not isinstance(instruction._raw_op, StandardGate): - op = instruction.operation - if copy_operations: - op = copy.deepcopy(op) - dagcircuit.apply_operation_back(op, instruction.qubits, instruction.clbits, check=False) - else: - node = DAGOpNode( - instruction._raw_op, - qargs=instruction.qubits, - cargs=instruction.clbits, - params=instruction.params, - label=instruction.label, - duration=instruction.duration, - unit=instruction.unit, - condition=instruction.condition, - dag=dagcircuit, - ) - dagcircuit._apply_op_node_back(node) + dagcircuit._apply_op_node_back( + DAGOpNode.from_instruction(instruction, dag=dagcircuit, deepcopy=copy_operations) + ) dagcircuit.duration = circuit.duration dagcircuit.unit = circuit.unit diff --git a/qiskit/converters/circuit_to_instruction.py b/qiskit/converters/circuit_to_instruction.py index 4d0570542b03..4487a65e08fd 100644 --- a/qiskit/converters/circuit_to_instruction.py +++ b/qiskit/converters/circuit_to_instruction.py @@ -142,7 +142,7 @@ def fix_condition(op): data = target._data.copy() data.replace_bits(qubits=qreg, clbits=creg) - data.map_ops(fix_condition) + data.map_nonstandard_ops(fix_condition) qc = QuantumCircuit(*regs, name=out_instruction.name) qc._data = data diff --git a/qiskit/converters/dag_to_circuit.py b/qiskit/converters/dag_to_circuit.py index 3667c2183eae..59c3fbedbed4 100644 --- a/qiskit/converters/dag_to_circuit.py +++ b/qiskit/converters/dag_to_circuit.py @@ -72,24 +72,7 @@ def dag_to_circuit(dag, copy_operations=True): circuit.calibrations = dag.calibrations for node in dag.topological_op_nodes(): - if not isinstance(node._raw_op, StandardGate): - op = node.op - if copy_operations: - op = copy.deepcopy(op) - circuit._append(CircuitInstruction(op, node.qargs, node.cargs)) - else: - circuit._append( - CircuitInstruction( - node._raw_op, - node.qargs, - node.cargs, - params=node.params, - label=node.label, - duration=node.duration, - unit=node.unit, - condition=node.condition, - ) - ) + circuit._append(node._to_circuit_instruction(deepcopy=copy_operations)) circuit.duration = dag.duration circuit.unit = dag.unit diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index b93a90e47f7b..1f647ff0e6eb 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -718,11 +718,17 @@ def copy_empty_like(self, *, vars_mode: _VarsMode = "alike"): return target_dag - def _apply_op_node_back(self, node: DAGOpNode): + def _apply_op_node_back(self, node: DAGOpNode, *, check: bool = False): additional = () if _may_have_additional_wires(node): # This is the slow path; most of the time, this won't happen. - additional = set(_additional_wires(node)).difference(node.cargs) + additional = set(_additional_wires(node.op)).difference(node.cargs) + + if check: + self._check_condition(node.name, node.condition) + self._check_wires(node.qargs, self.output_map) + self._check_wires(node.cargs, self.output_map) + self._check_wires(additional, self.output_map) node._node_id = self._multi_graph.add_node(node) self._increment_op(node.name) @@ -738,6 +744,7 @@ def _apply_op_node_back(self, node: DAGOpNode): for bit in bits ], ) + return node def apply_operation_back( self, @@ -765,32 +772,9 @@ def apply_operation_back( DAGCircuitError: if a leaf node is connected to multiple outputs """ - qargs = tuple(qargs) - cargs = tuple(cargs) - additional = () - - if _may_have_additional_wires(op): - # This is the slow path; most of the time, this won't happen. - additional = set(_additional_wires(op)).difference(cargs) - - if check: - self._check_condition(op.name, getattr(op, "condition", None)) - self._check_wires(qargs, self.output_map) - self._check_wires(cargs, self.output_map) - self._check_wires(additional, self.output_map) - - node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) - node._node_id = self._multi_graph.add_node(node) - self._increment_op(op.name) - - # Add new in-edges from predecessors of the output nodes to the - # operation node while deleting the old in-edges of the output nodes - # and adding new edges from the operation node to each output node - self._multi_graph.insert_node_on_in_edges_multiple( - node._node_id, - [self.output_map[bit]._node_id for bits in (qargs, cargs, additional) for bit in bits], + return self._apply_op_node_back( + DAGOpNode(op=op, qargs=tuple(qargs), cargs=tuple(cargs), dag=self), check=check ) - return node def apply_operation_front( self, @@ -821,26 +805,30 @@ def apply_operation_front( cargs = tuple(cargs) additional = () - if _may_have_additional_wires(op): + node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) + if _may_have_additional_wires(node): # This is the slow path; most of the time, this won't happen. - additional = set(_additional_wires(op)).difference(cargs) + additional = set(_additional_wires(node.op)).difference(cargs) if check: - self._check_condition(op.name, getattr(op, "condition", None)) - self._check_wires(qargs, self.output_map) - self._check_wires(cargs, self.output_map) + self._check_condition(node.name, node.condition) + self._check_wires(node.qargs, self.output_map) + self._check_wires(node.cargs, self.output_map) self._check_wires(additional, self.output_map) - node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) node._node_id = self._multi_graph.add_node(node) - self._increment_op(op.name) + self._increment_op(node.name) # Add new out-edges to successors of the input nodes from the # operation node while deleting the old out-edges of the input nodes # and adding new edges to the operation node from each input node self._multi_graph.insert_node_on_out_edges_multiple( node._node_id, - [self.input_map[bit]._node_id for bits in (qargs, cargs, additional) for bit in bits], + [ + self.input_map[bit]._node_id + for bits in (node.qargs, node.cargs, additional) + for bit in bits + ], ) return node @@ -1432,7 +1420,7 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit node_wire_order = list(node.qargs) + list(node.cargs) # If we're not propagating it, the number of wires in the input DAG should include the # condition as well. - if not propagate_condition and _may_have_additional_wires(node.op): + if not propagate_condition and _may_have_additional_wires(node): node_wire_order += [ wire for wire in _additional_wires(node.op) if wire not in node_cargs ] @@ -1454,7 +1442,7 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit raise DAGCircuitError( f"bit mapping invalid: {input_dag_wire} and {our_wire} are different bit types" ) - if _may_have_additional_wires(node.op): + if _may_have_additional_wires(node): node_vars = {var for var in _additional_wires(node.op) if isinstance(var, expr.Var)} else: node_vars = set() @@ -2376,24 +2364,25 @@ def __init__(self, var: expr.Var, type_: _DAGVarType, in_node: DAGInNode, out_no self.out_node = out_node -def _may_have_additional_wires(operation) -> bool: - """Return whether a given :class:`.Operation` may contain references to additional wires - locations within itself. If this is ``False``, it doesn't necessarily mean that the operation - _will_ access memory inherently, but a ``True`` return guarantees that it won't. +def _may_have_additional_wires(node) -> bool: + """Return whether a given :class:`.DAGOpNode` may contain references to additional wires + locations within its :class:`.Operation`. If this is ``True``, it doesn't necessarily mean + that the operation _will_ access memory inherently, but a ``False`` return guarantees that it + won't. The memory might be classical bits or classical variables, such as a control-flow operation or a store. Args: - operation (qiskit.circuit.Operation): the operation to check. + operation (qiskit.dagcircuit.DAGOpNode): the operation to check. """ # This is separate to `_additional_wires` because most of the time there won't be any extra # wires beyond the explicit `qargs` and `cargs` so we want a fast path to be able to skip # creating and testing a generator for emptiness. # # If updating this, you most likely also need to update `_additional_wires`. - return getattr(operation, "condition", None) is not None or isinstance( - operation, (ControlFlowOp, Store) + return node.condition is not None or ( + not node.is_standard_gate and isinstance(node.op, (ControlFlowOp, Store)) ) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 04d95312aa6d..e7c502c9ef9f 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -154,7 +154,7 @@ def _gate_sequence_to_dag(self, best_synth_circuit): out_dag.global_phase = best_synth_circuit.global_phase for gate_name, angles in best_synth_circuit: - op = CircuitInstruction(gate_name, qubits=qubits, params=angles) + op = CircuitInstruction.from_standard(gate_name, qubits, angles) out_dag.apply_operation_back(op.operation, qubits, check=False) return out_dag @@ -241,7 +241,7 @@ def run(self, dag): first_node_id = run[0]._node_id qubit = run[0].qargs for gate, angles in best_circuit_sequence: - op = CircuitInstruction(gate, qubits=qubit, params=angles) + op = CircuitInstruction.from_standard(gate, qubit, angles) node = DAGOpNode.from_instruction(op, dag=dag) node._node_id = dag._multi_graph.add_node(node) dag._increment_op(gate.name) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index ab7c5e04649f..08b6a15fd03d 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -42,7 +42,7 @@ ) from qiskit.quantum_info import Operator from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit.circuit import Gate, Parameter +from qiskit.circuit import Gate, Parameter, CircuitInstruction from qiskit.circuit.library.standard_gates import ( iSwapGate, CXGate, @@ -566,17 +566,18 @@ def _run_main_loop( qargs, ) in node_list: if op_name == "USER_GATE": - node = DAGOpNode( - user_gate_node._raw_op, - params=user_gate_node.params, - qargs=tuple(qubits[x] for x in qargs), + node = DAGOpNode.from_instruction( + user_gate_node._to_circuit_instruction().replace( + params=user_gate_node.params, + qubits=tuple(qubits[x] for x in qargs), + ), dag=out_dag, ) else: - node = DAGOpNode( - GATE_NAME_MAP[op_name], - params=params, - qargs=tuple(qubits[x] for x in qargs), + node = DAGOpNode.from_instruction( + CircuitInstruction.from_standard( + GATE_NAME_MAP[op_name], tuple(qubits[x] for x in qargs), params + ), dag=out_dag, ) out_dag._apply_op_node_back(node) @@ -1043,6 +1044,8 @@ def _reversed_synth_su4(self, su4_mat, decomposer2q, approximation_degree): flip_bits = out_dag.qubits[::-1] for node in synth_circ.topological_op_nodes(): qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) - node = DAGOpNode(node._raw_op, qargs=qubits, params=node.params) + node = DAGOpNode.from_instruction( + node._to_circuit_instruction().replace(qubits=qubits, params=node.params) + ) out_dag._apply_op_node_back(node) return out_dag diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 55028c8e883e..e75d67ed5dc1 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -184,7 +184,7 @@ def test_foreach_op_indexed(self): self.assertEqual(len(visited_ops), len(data_list)) self.assertTrue(all(op is inst.operation for op, inst in zip(visited_ops, data_list))) - def test_map_ops(self): + def test_map_nonstandard_ops(self): """Test all operations are replaced.""" qr = QuantumRegister(5) @@ -203,7 +203,7 @@ class CustomXGate(XGate): CircuitInstruction(CustomXGate(), [qr[4]], []), ] data = CircuitData(qubits=list(qr), data=data_list) - data.map_ops(lambda op: op.to_mutable()) + data.map_nonstandard_ops(lambda op: op.to_mutable()) self.assertTrue(all(inst.operation.mutable for inst in data)) def test_replace_bits(self): diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index 6c0cc977e58a..74d6362ceb29 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -49,7 +49,7 @@ def test_gate_cross_domain_conversion(self): with self.subTest(name=name): qc = QuantumCircuit(standard_gate.num_qubits) qc._append( - CircuitInstruction(standard_gate, qubits=qc.qubits, params=gate_class.params) + CircuitInstruction.from_standard(standard_gate, qc.qubits, gate_class.params) ) self.assertEqual(qc.data[0].operation.base_class, gate_class.base_class) self.assertEqual(qc.data[0].operation, gate_class) From 16c0b4ff9b825d84da304eddad9cffc2ca9714c7 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Sun, 7 Jul 2024 23:44:16 +0100 Subject: [PATCH 2/6] Fix clippy in no-gate-cache mode --- crates/circuit/src/circuit_data.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 66d35cb9377c..bf290b72d113 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -511,20 +511,20 @@ impl CircuitData { }); } } else { - res.data.extend(self.data.iter().map(|inst| { - // Clippy 1.70 doesn't like feature-gating only the middle statement, because if - // `cache_pygates` is off, it looks like a let followed immediately by a return. - #[cfg(feature = "cache_pygates")] - { + // Clippy complains in some versions if you attempt to just feature-gate out the + // ref-cell setting line from the middle. + #[cfg(feature = "cache_pygates")] + { + res.data.extend(self.data.iter().map(|inst| { let out = inst.clone(); *out.py_op.borrow_mut() = None; out - } - #[cfg(not(feature = "cache_pygates"))] - { - inst.clone() - } - })) + })); + } + #[cfg(not(feature = "cache_pygates"))] + { + res.data.extend(self.data.iter().cloned()); + } } Ok(res) } From a6cd6bb21f50432819b0c26201d5eaf08c36b438 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 8 Jul 2024 00:29:56 +0100 Subject: [PATCH 3/6] Fix pylint unused-import complaints --- qiskit/circuit/commutation_checker.py | 1 - qiskit/circuit/quantumcircuit.py | 3 +-- qiskit/converters/circuit_to_dag.py | 2 -- qiskit/converters/dag_to_circuit.py | 4 +--- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py index 572142440372..5c1fb5586cb7 100644 --- a/qiskit/circuit/commutation_checker.py +++ b/qiskit/circuit/commutation_checker.py @@ -21,7 +21,6 @@ from qiskit.circuit.operation import Operation from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.quantum_info.operators import Operator -from qiskit._accelerate.circuit import StandardGate _skipped_op_names = {"measure", "reset", "delay", "initialize"} _no_cache_op_names = {"annotated"} diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 4692e0f4468e..e2dcf8f86629 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2319,8 +2319,7 @@ def _append_standard_gate( Gate.validate_parameter(op, param) instructions = InstructionSet(resource_requester=circuit_scope.resolve_classical_resource) - broadcast_iter = Gate.broadcast_arguments(op, expanded_qargs, []) - for qarg, carg in broadcast_iter: + for qarg, _ in Gate.broadcast_arguments(op, expanded_qargs, []): self._check_dups(qarg) instruction = CircuitInstruction.from_standard(op, qarg, params, label=label) circuit_scope.append(instruction, _standard_gate=True) diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index 6ec615d1a8d2..10a48df99778 100644 --- a/qiskit/converters/circuit_to_dag.py +++ b/qiskit/converters/circuit_to_dag.py @@ -11,10 +11,8 @@ # that they have been altered from the originals. """Helper function for converting a circuit to a dag""" -import copy from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode -from qiskit._accelerate.circuit import StandardGate def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_order=None): diff --git a/qiskit/converters/dag_to_circuit.py b/qiskit/converters/dag_to_circuit.py index 59c3fbedbed4..47adee456380 100644 --- a/qiskit/converters/dag_to_circuit.py +++ b/qiskit/converters/dag_to_circuit.py @@ -11,10 +11,8 @@ # that they have been altered from the originals. """Helper function for converting a dag to a circuit.""" -import copy -from qiskit.circuit import QuantumCircuit, CircuitInstruction -from qiskit._accelerate.circuit import StandardGate +from qiskit.circuit import QuantumCircuit def dag_to_circuit(dag, copy_operations=True): From 59d1fc046fbe49ff63cc63a0ab7781b8dd7d71b4 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 9 Jul 2024 17:47:43 +0100 Subject: [PATCH 4/6] Fix broken assumptions around the gate model The `compose` test had a now-broken assumption, because the Python-space `is` check is no longer expected to return an identical object when a standard gate is moved from one circuit to another and has its components remapped as part of the `compose` operation. This doesn't constitute the unpleasant deep-copy that that test is preventing. A custom gate still satisfies that, however, so we can just change the test. `DAGNode::set_name` could cause problems if it was called for the first time on a `CircuitInstruction` that was for a standard gate; these would be created as immutable instances. Given the changes in operator extraction to Rust space, it can now be the case that a standard gate that comes in as mutable is unpacked into Rust space, the cache is some time later invalidated, and then the operation is recreated immutably. --- crates/circuit/src/circuit_instruction.rs | 16 ++++++++++++++++ crates/circuit/src/dag_node.rs | 2 +- test/python/circuit/test_compose.py | 8 +++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 698fb734f2f0..7fc35269d1f0 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -116,6 +116,22 @@ impl CircuitInstruction { pub fn op(&self) -> OperationRef { self.operation.view() } + + /// Get the Python-space operation, ensuring that it is mutable from Python space (singleton + /// gates might not necessarily satisfy this otherwise). + /// + /// This returns the cached instruction if valid, and replaces the cached instruction if not. + pub fn get_operation_mut(&self, py: Python) -> PyResult> { + let mut out = self.get_operation(py)?.into_bound(py); + if !out.getattr(intern!(py, "mutable"))?.extract::()? { + out = out.call_method0(intern!(py, "to_mutable"))?; + } + #[cfg(feature = "cache_pygates")] + { + *self.py_op.borrow_mut() = Some(out.to_object(py)); + } + Ok(out.unbind()) + } } #[pymethods] diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 7b517bdab16a..8d76a4f25973 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -377,7 +377,7 @@ impl DAGOpNode { /// Sets the Instruction name corresponding to the op for this node #[setter] fn set_name(&mut self, py: Python, new_name: PyObject) -> PyResult<()> { - let op = self.instruction.get_operation(py)?.into_bound(py); + let op = self.instruction.get_operation_mut(py)?.into_bound(py); op.setattr(intern!(py, "name"), new_name)?; self.instruction.operation = op.extract::()?.operation; Ok(()) diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index 7bb36a1401f8..582bee082d99 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -360,8 +360,14 @@ def test_compose_copy(self): # 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) + class Custom(Gate): + """Custom gate that cannot be decomposed into Rust space.""" + + def __init__(self): + super().__init__("mygate", 1, []) + conditional = QuantumCircuit(1, 1) - conditional.x(0).c_if(conditional.clbits[0], True) + conditional.append(Custom(), [0], []).c_if(conditional.clbits[0], True) test = base.compose(conditional, qubits=[0], clbits=[0], copy=False) self.assertIs(test.data[-1].operation, conditional.data[-1].operation) self.assertEqual(test.data[-1].operation.condition, (test.clbits[0], True)) From 87b446ef1539abbadf3736736909bebdfefd7163 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 22 Jul 2024 18:33:18 +0100 Subject: [PATCH 5/6] Fix lint --- qiskit/transpiler/passes/basis/basis_translator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 2f615fb20719..30b25b271755 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -36,7 +36,6 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES -from qiskit._accelerate.circuit import StandardGate logger = logging.getLogger(__name__) From f991157574cfd9c462418366486b307e712f9059 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 22 Jul 2024 18:42:17 +0100 Subject: [PATCH 6/6] Fix minor documentation --- crates/circuit/src/packed_instruction.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index f2fdeda29e50..9f7cf9c0135d 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -64,9 +64,9 @@ unsafe impl ::bytemuck::NoUninit for PackedOperationType {} /// This lets us store the enum discriminant in the low data bits, and then type-pun a suitable /// bitmask on the contained value back into proper data. /// -/// Explicity, this is logical memory layout of `PackedOperation` on a 64-bit system, written out as -/// a binary integer. `x` marks padding bits with undefined values, `S` is the bits that make up a -/// `StandardGate`, and `P` is bits that make up part of a pointer. +/// Explicitly, this is logical memory layout of `PackedOperation` on a 64-bit system, written out +/// as a binary integer. `x` marks padding bits with undefined values, `S` is the bits that make up +/// a `StandardGate`, and `P` is bits that make up part of a pointer. /// /// ```text /// Standard gate: @@ -135,7 +135,8 @@ impl PackedOperation { /// **Panics** if the object represents a standard gate; see `try_pointer`. #[inline] fn pointer(&self) -> NonNull<()> { - self.try_pointer().unwrap() + self.try_pointer() + .expect("the caller is responsible for knowing the correct type") } /// Get the contained pointer to the `PyGate`/`PyInstruction`/`PyOperation` that this object @@ -163,7 +164,8 @@ impl PackedOperation { /// `try_standard_gate`. #[inline] pub fn standard_gate(&self) -> StandardGate { - self.try_standard_gate().unwrap() + self.try_standard_gate() + .expect("the caller is responsible for knowing the correct type") } /// Get the contained `StandardGate`, if any.