diff --git a/Cargo.lock b/Cargo.lock index 7880496cf797..c397b7999557 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/convert_2q_block_matrix.rs b/crates/accelerate/src/convert_2q_block_matrix.rs index 7a9165777dc9..dd61137c54f4 100644 --- a/crates/accelerate/src/convert_2q_block_matrix.rs +++ b/crates/accelerate/src/convert_2q_block_matrix.rs @@ -23,11 +23,11 @@ use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2}; use smallvec::SmallVec; use qiskit_circuit::bit_data::BitData; -use qiskit_circuit::circuit_instruction::{operation_type_to_py, CircuitInstruction}; +use qiskit_circuit::circuit_instruction::CircuitInstruction; use qiskit_circuit::dag_node::DAGOpNode; use qiskit_circuit::gate_matrix::ONE_QUBIT_IDENTITY; use qiskit_circuit::imports::QI_OPERATOR; -use qiskit_circuit::operations::{Operation, OperationType}; +use qiskit_circuit::operations::{Operation, OperationRef}; use crate::QiskitError; @@ -35,21 +35,20 @@ fn get_matrix_from_inst<'py>( py: Python<'py>, inst: &'py CircuitInstruction, ) -> PyResult> { - match inst.operation.matrix(&inst.params) { - Some(mat) => Ok(mat), - None => match inst.operation { - OperationType::Standard(_) => Err(QiskitError::new_err( - "Parameterized gates can't be consolidated", - )), - OperationType::Gate(_) => Ok(QI_OPERATOR - .get_bound(py) - .call1((operation_type_to_py(py, inst)?,))? - .getattr(intern!(py, "data"))? - .extract::>()? - .as_array() - .to_owned()), - _ => unreachable!("Only called for unitary ops"), - }, + if let Some(mat) = inst.op().matrix(&inst.params) { + Ok(mat) + } else if inst.operation.try_standard_gate().is_some() { + Err(QiskitError::new_err( + "Parameterized gates can't be consolidated", + )) + } else { + Ok(QI_OPERATOR + .get_bound(py) + .call1((inst.get_operation(py)?,))? + .getattr(intern!(py, "data"))? + .extract::>()? + .as_array() + .to_owned()) } } @@ -127,34 +126,20 @@ pub fn change_basis(matrix: ArrayView2) -> Array2 { #[pyfunction] pub fn collect_2q_blocks_filter(node: &Bound) -> Option { - match node.downcast::() { - Ok(bound_node) => { - let node = bound_node.borrow(); - match &node.instruction.operation { - OperationType::Standard(gate) => Some( - gate.num_qubits() <= 2 - && node - .instruction - .extra_attrs - .as_ref() - .and_then(|attrs| attrs.condition.as_ref()) - .is_none() - && !node.is_parameterized(), - ), - OperationType::Gate(gate) => Some( - gate.num_qubits() <= 2 - && node - .instruction - .extra_attrs - .as_ref() - .and_then(|attrs| attrs.condition.as_ref()) - .is_none() - && !node.is_parameterized(), - ), - _ => Some(false), - } - } - Err(_) => None, + let Ok(node) = node.downcast::() else { return None }; + let node = node.borrow(); + match node.instruction.op() { + gate @ (OperationRef::Standard(_) | OperationRef::Gate(_)) => Some( + gate.num_qubits() <= 2 + && node + .instruction + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref()) + .is_none() + && !node.is_parameterized(), + ), + _ => Some(false), } } 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 0b4d60b6c91a..a325ca4e1d5c 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,40 @@ 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"))?; - } - }; - #[cfg(feature = "cache_pygates")] - { - *inst.py_op.borrow_mut() = None; - } + 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().cloned()); } Ok(res) } @@ -548,10 +535,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 +573,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 +637,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,16 +700,17 @@ 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); - CircuitInstruction::new( - py, - inst.op.clone(), - self.qubits.map_indices(qubits.value), - self.clbits.map_indices(clbits.value), - inst.params.clone(), - inst.extra_attrs.clone(), - ) + let qubits = self.qargs_interner.intern(inst.qubits); + let clbits = self.cargs_interner.intern(inst.clbits); + CircuitInstruction { + operation: inst.op.clone(), + qubits: PyTuple::new_bound(py, self.qubits.map_indices(qubits.value)).unbind(), + clbits: PyTuple::new_bound(py, self.clbits.map_indices(clbits.value)).unbind(), + params: inst.params_view().iter().cloned().collect(), + extra_attrs: inst.extra_attrs.clone(), + #[cfg(feature = "cache_pygates")] + py_op: inst.py_op.clone(), + } .into_py(py) }; match index.with_len(self.data.len())? { @@ -770,20 +725,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 +805,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 +829,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 +855,7 @@ impl CircuitData { .collect::>>()?; let clbits = other .cargs_interner - .intern(inst.clbits_id) + .intern(inst.clbits) .value .iter() .map(|b| { @@ -918,8 +872,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 +884,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 +1061,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 +1081,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 +1092,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 ecb7a1623a20..7fc35269d1f0 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -15,20 +15,18 @@ 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 crate::imports::{ - get_std_gate_class, populate_std_gate_map, CONTROLLED_GATE, GATE, INSTRUCTION, OPERATION, - SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, WARNINGS_WARN, -}; -use crate::interner::Index; +use smallvec::SmallVec; + +use crate::imports::{GATE, INSTRUCTION, OPERATION, WARNINGS_WARN}; 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 +40,26 @@ pub struct ExtraInstructionAttributes { pub condition: Option, } -/// Private type used to store instructions with interned arg lists. -#[derive(Clone, Debug)] -pub 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 +98,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,231 +108,80 @@ 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, - qubits: impl IntoIterator, - clbits: impl IntoIterator, - params: SmallVec<[Param; 3]>, - extra_attrs: Option>, - ) -> Self - where - T1: ToPyObject, - T2: ToPyObject, - U1: ExactSizeIterator, - U2: ExactSizeIterator, - { - CircuitInstruction { - operation, - qubits: PyTuple::new_bound(py, qubits).unbind(), - clbits: PyTuple::new_bound(py, clbits).unbind(), - params, - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: None, - } + /// View the operation in this `CircuitInstruction`. + pub fn op(&self) -> OperationRef { + self.operation.view() } -} -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), + /// 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] 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 +193,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 +234,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 +266,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 +281,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 label = match label { - Some(label) => Some(label), - None => match &self.extra_attrs { - Some(extra_attrs) => extra_attrs.label.clone(), - None => None, - }, - }; - let duration = match duration { - Some(duration) => Some(duration), - None => match &self.extra_attrs { - Some(extra_attrs) => extra_attrs.duration.clone(), - None => None, - }, - }; - - let unit: Option = match unit { - Some(unit) => Some(unit), - None => match &self.extra_attrs { - Some(extra_attrs) => extra_attrs.unit.clone(), - None => None, - }, + let qubits = match qubits { + None => self.qubits.clone_ref(py), + Some(qubits) => as_tuple(py, Some(qubits))?.unbind(), }; - - let condition: Option = match condition { - Some(condition) => Some(condition), - None => match &self.extra_attrs { - Some(extra_attrs) => extra_attrs.condition.clone(), - None => None, - }, + let clbits = match clbits { + None => self.clbits.clone_ref(py), + Some(clbits) => as_tuple(py, Some(clbits))?.unbind(), }; - - 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 +343,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 +357,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 +389,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,353 +425,176 @@ 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, - ) -} - -/// 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)?; - - let args = if params.is_empty() { - PyTuple::empty_bound(py) - } else { - PyTuple::new_bound(py, params) - }; - let kwargs = [ - ("label", label.to_object(py)), - ("unit", unit.to_object(py)), - ("duration", duration.to_object(py)), - ] - .into_py_dict_bound(py); - let mut out = gate_class.call_bound(py, args, Some(&kwargs))?; - if condition.is_some() { - out = out.call_method0(py, "to_mutable")?; - out.setattr(py, "condition", condition.to_object(py))?; - } - Ok(out) - } - OperationType::Gate(gate) => Ok(gate.gate.clone_ref(py)), - OperationType::Instruction(inst) => Ok(inst.instruction.clone_ref(py)), - OperationType::Operation(op) => Ok(op.operation.clone_ref(py)), - } -} - -/// A container struct that contains the output from the Python object to -/// conversion to construct a CircuitInstruction object -#[derive(Debug, Clone)] -pub struct OperationTypeConstruct { - pub operation: OperationType, +/// 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 label: Option, - pub duration: Option, - pub unit: Option, - pub condition: Option, + pub extra_attrs: 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. - // Controlled gates with non-default control states are also considered - // custom gates even if a standard representation exists for the default - // control state. - - // 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 standard.is_some() { - let mutable: bool = py_op.getattr(py, intern!(py, "mutable"))?.extract(py)?; - // The default ctrl_states are the all 1 state and None. - // These are the only cases where controlled gates can be standard. - let is_default_ctrl_state = || -> PyResult { - match py_op.getattr(py, intern!(py, "ctrl_state")) { - Ok(c_state) => match c_state.extract::>(py) { - Ok(c_state_int) => match c_state_int { - Some(c_int) => { - let qubits: u32 = - py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?; - Ok(c_int == (2_i32.pow(qubits - 1) - 1)) - } - None => Ok(true), - }, - Err(_) => Ok(false), - }, - Err(_) => Ok(false), - } +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()) }; - - if (mutable - && (py_op_bound.is_instance(SINGLETON_GATE.get_bound(py))? - || py_op_bound.is_instance(SINGLETON_CONTROLLED_GATE.get_bound(py))?)) - || (py_op_bound.is_instance(CONTROLLED_GATE.get_bound(py))? - && !is_default_ctrl_state()?) - { - standard = None; - } - } - - if let Some(op) = standard { - let base_class = op_type.to_object(py); - populate_std_gate_map(py, op, base_class); - return Ok(OperationTypeConstruct { - operation: OperationType::Standard(op), - params: py_op.getattr(py, intern!(py, "params"))?.extract(py)?, - label: py_op.getattr(py, intern!(py, "label"))?.extract(py)?, - duration: py_op.getattr(py, intern!(py, "duration"))?.extract(py)?, - unit: py_op.getattr(py, intern!(py, "unit"))?.extract(py)?, - condition: py_op.getattr(py, intern!(py, "condition"))?.extract(py)?, - }); - } - if op_type.is_subclass(GATE.get_bound(py))? { - let params = py_op.getattr(py, intern!(py, "params"))?.extract(py)?; - let label = py_op.getattr(py, intern!(py, "label"))?.extract(py)?; - let duration = py_op.getattr(py, intern!(py, "duration"))?.extract(py)?; - let unit = py_op.getattr(py, intern!(py, "unit"))?.extract(py)?; - let condition = py_op.getattr(py, intern!(py, "condition"))?.extract(py)?; - - let out_op = PyGate { - qubits: py_op.getattr(py, intern!(py, "num_qubits"))?.extract(py)?, - clbits: py_op.getattr(py, intern!(py, "num_clbits"))?.extract(py)?, - params: py_op - .getattr(py, intern!(py, "params"))? - .downcast_bound::(py)? - .len() as u32, - op_name: py_op.getattr(py, intern!(py, "name"))?.extract(py)?, - gate: py_op, + 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)) }; - 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, - }); + 'standard: { + let Some(standard) = ob_type + .getattr(intern!(py, "_standard_gate")) + .and_then(|standard| standard.extract::()) + .ok() else { break 'standard }; + + // If the instruction is a controlled gate with a not-all-ones control state, it doesn't + // fit our definition of standard. We abuse the fact that we know our standard-gate + // mapping to avoid an `isinstance` check on `ControlledGate` - a standard gate has + // nonzero `num_ctrl_qubits` iff it is a `ControlledGate`. + // + // `ControlledGate` also has a `base_gate` attribute, and we don't track enough in Rust + // space to handle the case that that was mutated away from a standard gate. + if standard.num_ctrl_qubits() != 0 + && ((ob.getattr(intern!(py, "ctrl_state"))?.extract::()? + != (1 << standard.num_ctrl_qubits()) - 1) + || ob.getattr(intern!(py, "mutable"))?.extract()?) + { + break 'standard; + } + return Ok(OperationFromPython { + operation: PackedOperation::from_standard(standard), + params: extract_params()?, + extra_attrs: extract_extra()?, + }); + } + 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 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 f347ec72c811..db9f6f650174 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() = Some(op.into_py(op.py())); + } 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] @@ -281,7 +297,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()) } @@ -317,6 +333,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() { @@ -347,11 +368,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) @@ -363,25 +382,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_mut(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 c7469434c668..8a13aab33cea 100644 --- a/crates/circuit/src/lib.rs +++ b/crates/circuit/src/lib.rs @@ -17,6 +17,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 4eb50d7e5014..7a20001ffdf3 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 IntoPy for OperationType { +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::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 { GlobalPhaseGate = 0, @@ -241,9 +238,18 @@ pub enum StandardGate { RC3XGate = 51, } +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) } } @@ -265,6 +271,15 @@ static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ 0, 0, // 50-51 ]; +static STANDARD_GATE_NUM_CTRL_QUBITS: [u32; STANDARD_GATE_SIZE] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-9 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10-19 + 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, // 20-29 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 30-39 + 0, 0, 0, 0, 0, 2, 2, 1, 0, 3, // 40-49 + 3, 0, // 50-51 +]; + static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ "global_phase", // 0 "h", // 1 @@ -320,6 +335,41 @@ static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ "rcccx", // 51 ("rc3x") ]; +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) + } + } + + pub fn num_ctrl_qubits(&self) -> u32 { + STANDARD_GATE_NUM_CTRL_QUBITS[*self as usize] + } +} + #[pymethods] impl StandardGate { pub fn copy(&self) -> Self { @@ -1957,7 +2007,8 @@ fn radd_param(param1: Param, param2: Param, 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, @@ -1966,30 +2017,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() @@ -2046,7 +2073,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, @@ -2055,30 +2083,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() @@ -2148,7 +2152,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, @@ -2157,30 +2162,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..9f7cf9c0135d --- /dev/null +++ b/crates/circuit/src/packed_instruction.rs @@ -0,0 +1,499 @@ +// 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. +/// +/// 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: +/// 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() + .expect("the caller is responsible for knowing the correct type") + } + + /// 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() + .expect("the caller is responsible for knowing the correct type") + } + + /// 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..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"} @@ -67,11 +66,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 9d7cdfa2fb50..515ab393e6e0 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 @@ -2034,7 +2034,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 @@ -2307,9 +2307,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.""" @@ -2319,16 +2318,13 @@ 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) - for qarg, carg in broadcast_iter: + for qarg, _ in Gate.broadcast_arguments(op, expanded_qargs, []): 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 @@ -2430,38 +2426,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 @@ -4495,7 +4463,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, @@ -4522,7 +4490,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 @@ -4545,7 +4513,7 @@ def id(self, qubit: QubitSpecifier) -> InstructionSet: # pylint: disable=invali Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.IGate, [], 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`. @@ -4576,7 +4544,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, @@ -4605,7 +4573,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 @@ -4664,7 +4632,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, @@ -4712,7 +4680,7 @@ def rccx( A handle to the instructions created. """ return self._append_standard_gate( - StandardGate.RCCXGate, [], qargs=[control_qubit1, control_qubit2, target_qubit] + StandardGate.RCCXGate, [control_qubit1, control_qubit2, target_qubit], () ) def rcccx( @@ -4737,8 +4705,8 @@ def rcccx( """ return self._append_standard_gate( StandardGate.RC3XGate, - [], - qargs=[control_qubit1, control_qubit2, control_qubit3, target_qubit], + [control_qubit1, control_qubit2, control_qubit3, target_qubit], + (), ) def rx( @@ -4756,7 +4724,7 @@ def rx( Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.RXGate, [theta], [qubit], label=label) + return self._append_standard_gate(StandardGate.RXGate, [qubit], [theta], label=label) def crx( self, @@ -4785,7 +4753,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], label=label + StandardGate.CRXGate, [control_qubit, target_qubit], [theta], label=label ) from .library.standard_gates.rx import CRXGate @@ -4812,7 +4780,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 @@ -4829,7 +4797,7 @@ def ry( Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.RYGate, [theta], [qubit], label=label) + return self._append_standard_gate(StandardGate.RYGate, [qubit], [theta], label=label) def cry( self, @@ -4858,7 +4826,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], label=label + StandardGate.CRYGate, [control_qubit, target_qubit], [theta], label=label ) from .library.standard_gates.ry import CRYGate @@ -4885,7 +4853,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`. @@ -4899,7 +4867,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]) + return self._append_standard_gate(StandardGate.RZGate, [qubit], [phi]) def crz( self, @@ -4928,7 +4896,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], label=label + StandardGate.CRZGate, [control_qubit, target_qubit], [theta], label=label ) from .library.standard_gates.rz import CRZGate @@ -4955,7 +4923,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 @@ -4972,7 +4940,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`. @@ -4985,7 +4953,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`. @@ -4998,7 +4966,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`. @@ -5011,7 +4979,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, @@ -5038,7 +5006,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 @@ -5075,7 +5043,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 @@ -5100,8 +5068,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: @@ -5115,7 +5083,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, @@ -5145,8 +5113,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, ) @@ -5170,7 +5138,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`. @@ -5183,7 +5151,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, @@ -5210,7 +5178,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 @@ -5233,7 +5201,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`. @@ -5246,7 +5214,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, @@ -5268,7 +5236,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, @@ -5304,8 +5272,8 @@ def cu( if ctrl_state is None or ctrl_state in ["1", 1]: return self._append_standard_gate( StandardGate.CUGate, + [control_qubit, target_qubit], [theta, phi, lam, gamma], - qargs=[control_qubit, target_qubit], label=label, ) @@ -5330,7 +5298,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, @@ -5358,9 +5326,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, ) @@ -5385,7 +5352,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, @@ -5412,7 +5379,9 @@ def ccx( # if the control state is |11> use the fast Rust version of the gate if ctrl_state is None or ctrl_state in ["11", 3]: return self._append_standard_gate( - StandardGate.CCXGate, [], qargs=[control_qubit1, control_qubit2, target_qubit] + StandardGate.CCXGate, + [control_qubit1, control_qubit2, target_qubit], + (), ) from .library.standard_gates.x import CCXGate @@ -5519,7 +5488,7 @@ def y(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.YGate, [], qargs=[qubit]) + return self._append_standard_gate(StandardGate.YGate, [qubit], ()) def cy( self, @@ -5547,8 +5516,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], + [control_qubit, target_qubit], + (), label=label, ) @@ -5572,7 +5541,7 @@ def z(self, qubit: QubitSpecifier) -> InstructionSet: Returns: A handle to the instructions created. """ - return self._append_standard_gate(StandardGate.ZGate, [], qargs=[qubit]) + return self._append_standard_gate(StandardGate.ZGate, [qubit], ()) def cz( self, @@ -5599,7 +5568,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 @@ -5639,8 +5608,8 @@ def ccz( if ctrl_state is None or ctrl_state in ["11", 3]: return self._append_standard_gate( StandardGate.CCZGate, - [], - qargs=[control_qubit1, control_qubit2, target_qubit], + [control_qubit1, control_qubit2, target_qubit], + (), label=label, ) diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index 88d9c72f1d61..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): @@ -94,24 +92,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..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): @@ -72,24 +70,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 796e0bc2b700..28a1c16002fa 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -719,11 +719,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) @@ -739,6 +745,7 @@ def _apply_op_node_back(self, node: DAGOpNode): for bit in bits ], ) + return node def apply_operation_back( self, @@ -766,32 +773,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, @@ -822,26 +806,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 @@ -1433,7 +1421,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 ] @@ -1455,7 +1443,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() @@ -2360,24 +2348,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/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index e69887a3b940..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__) @@ -310,16 +309,12 @@ def _replace_node(self, dag, node, instr_map): parameter_map = dict(zip(target_params, node.params)) bound_target_dag = target_dag.copy_empty_like() for inner_node in target_dag.topological_op_nodes(): - new_op = inner_node._raw_op - if not isinstance(inner_node._raw_op, StandardGate): - new_op = inner_node.op.copy() - new_node = DAGOpNode( - new_op, - qargs=inner_node.qargs, - cargs=inner_node.cargs, - params=inner_node.params, + new_node = DAGOpNode.from_instruction( + inner_node._to_circuit_instruction(), dag=bound_target_dag, ) + if not new_node.is_standard_gate: + new_node.op = new_node.op.copy() if any(isinstance(x, ParameterExpression) for x in inner_node.params): new_params = [] for param in new_node.params: @@ -337,8 +332,8 @@ def _replace_node(self, dag, node, instr_map): new_value = new_value.numeric() new_params.append(new_value) new_node.params = new_params - if not isinstance(new_op, StandardGate): - new_op.params = new_params + if not new_node.is_standard_gate: + new_node.op.params = new_params bound_target_dag._apply_op_node_back(new_node) if isinstance(target_dag.global_phase, ParameterExpression): old_phase = target_dag.global_phase 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_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)) diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 25fa55b62203..6d7b237915fa 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -774,7 +774,7 @@ def test_mcxgraycode_gates_yield_explicit_gates(self, num_ctrl_qubits): qc = QuantumCircuit(num_ctrl_qubits + 1) qc.append(MCXGrayCode(num_ctrl_qubits), list(range(qc.num_qubits)), []) explicit = {1: CXGate, 2: CCXGate, 3: C3XGate, 4: C4XGate} - self.assertEqual(type(qc[0].operation), explicit[num_ctrl_qubits]) + self.assertEqual(qc[0].operation.base_class, explicit[num_ctrl_qubits]) @data(3, 4, 5, 8) def test_mcx_gates(self, num_ctrl_qubits): diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index c226ae004896..b6de438d04dd 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -52,7 +52,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)