diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index dcfbdc9f1878..314fa5ff7c58 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -29,6 +29,7 @@ pub mod sampled_exp_val; pub mod sparse_pauli_op; pub mod stochastic_swap; pub mod synthesis; +pub mod target_transpiler; pub mod two_qubit_decompose; pub mod uc_gate; pub mod utils; diff --git a/crates/accelerate/src/target_transpiler/errors.rs b/crates/accelerate/src/target_transpiler/errors.rs new file mode 100644 index 000000000000..0201e4c1fabe --- /dev/null +++ b/crates/accelerate/src/target_transpiler/errors.rs @@ -0,0 +1,34 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use std::{error::Error, fmt::Display}; + +/// Error thrown when operation key is not present in the Target +#[derive(Debug)] +pub struct TargetKeyError { + pub message: String, +} + +impl TargetKeyError { + /// Initializes the new error + pub fn new_err(message: String) -> Self { + Self { message } + } +} + +impl Display for TargetKeyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl Error for TargetKeyError {} diff --git a/crates/accelerate/src/target_transpiler/instruction_properties.rs b/crates/accelerate/src/target_transpiler/instruction_properties.rs new file mode 100644 index 000000000000..a7a31c87924c --- /dev/null +++ b/crates/accelerate/src/target_transpiler/instruction_properties.rs @@ -0,0 +1,72 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use pyo3::{prelude::*, pyclass}; + +/** + A representation of an ``InstructionProperties`` object. +*/ +#[pyclass( + subclass, + name = "BaseInstructionProperties", + module = "qiskit._accelerate.target" +)] +#[derive(Clone, Debug)] +pub struct InstructionProperties { + #[pyo3(get, set)] + pub duration: Option, + #[pyo3(get, set)] + pub error: Option, +} + +#[pymethods] +impl InstructionProperties { + /// Create a new ``BaseInstructionProperties`` object + /// + /// Args: + /// duration (Option): The duration, in seconds, of the instruction on the + /// specified set of qubits + /// error (Option): The average error rate for the instruction on the specified + /// set of qubits. + /// calibration (Option): The pulse representation of the instruction. + #[new] + #[pyo3(signature = (duration=None, error=None))] + pub fn new(_py: Python<'_>, duration: Option, error: Option) -> Self { + Self { error, duration } + } + + fn __getstate__(&self) -> PyResult<(Option, Option)> { + Ok((self.duration, self.error)) + } + + fn __setstate__(&mut self, _py: Python<'_>, state: (Option, Option)) -> PyResult<()> { + self.duration = state.0; + self.error = state.1; + Ok(()) + } + + fn __repr__(&self, _py: Python<'_>) -> String { + format!( + "InstructionProperties(duration={}, error={})", + if let Some(duration) = self.duration { + duration.to_string() + } else { + "None".to_string() + }, + if let Some(error) = self.error { + error.to_string() + } else { + "None".to_string() + } + ) + } +} diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs new file mode 100644 index 000000000000..b5c56dc6d091 --- /dev/null +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -0,0 +1,1264 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#![allow(clippy::too_many_arguments)] + +mod errors; +mod instruction_properties; +mod nullable_index_map; + +use std::ops::Index; + +use ahash::RandomState; + +use ahash::HashSet; +use indexmap::{IndexMap, IndexSet}; +use itertools::Itertools; +use nullable_index_map::NullableIndexMap; +use pyo3::{ + exceptions::{PyAttributeError, PyIndexError, PyKeyError, PyValueError}, + prelude::*, + pyclass, + types::{PyDict, PyList, PySet, PyTuple}, +}; + +use qiskit_circuit::circuit_instruction::OperationFromPython; +use qiskit_circuit::operations::{Operation, Param}; +use qiskit_circuit::packed_instruction::PackedOperation; +use smallvec::SmallVec; + +use crate::nlayout::PhysicalQubit; + +use errors::TargetKeyError; +use instruction_properties::InstructionProperties; + +use self::exceptions::TranspilerError; + +mod exceptions { + use pyo3::import_exception_bound; + import_exception_bound! {qiskit.exceptions, QiskitError} + import_exception_bound! {qiskit.transpiler.exceptions, TranspilerError} +} + +// Custom types +type Qargs = SmallVec<[PhysicalQubit; 2]>; +type GateMap = IndexMap; +type PropsMap = NullableIndexMap>; +type GateMapState = Vec<(String, Vec<(Option, Option)>)>; + +/// Represents a Qiskit `Gate` object or a Variadic instruction. +/// Keeps a reference to its Python instance for caching purposes. +#[derive(Debug, Clone, FromPyObject)] +pub(crate) enum TargetOperation { + Normal(NormalOperation), + Variadic(PyObject), +} + +impl IntoPy for TargetOperation { + fn into_py(self, py: Python<'_>) -> PyObject { + match self { + Self::Normal(normal) => normal.into_py(py), + Self::Variadic(variable) => variable, + } + } +} + +impl ToPyObject for TargetOperation { + fn to_object(&self, py: Python<'_>) -> PyObject { + match self { + Self::Normal(normal) => normal.to_object(py), + Self::Variadic(variable) => variable.clone_ref(py), + } + } +} + +impl TargetOperation { + fn num_qubits(&self) -> u32 { + match &self { + Self::Normal(normal) => normal.operation.view().num_qubits(), + Self::Variadic(_) => { + unreachable!("'num_qubits' property is reserved for normal operations only.") + } + } + } + + fn params(&self) -> &[Param] { + match &self { + TargetOperation::Normal(normal) => normal.params.as_slice(), + TargetOperation::Variadic(_) => &[], + } + } +} + +/// Represents a Qiskit `Gate` object, keeps a reference to its Python +/// instance for caching purposes. +#[derive(Debug, Clone)] +pub(crate) struct NormalOperation { + pub operation: PackedOperation, + pub params: SmallVec<[Param; 3]>, + op_object: PyObject, +} + +impl<'py> FromPyObject<'py> for NormalOperation { + fn extract(ob: &'py PyAny) -> PyResult { + let operation: OperationFromPython = ob.extract()?; + Ok(Self { + operation: operation.operation, + params: operation.params, + op_object: ob.into(), + }) + } +} + +impl IntoPy for NormalOperation { + fn into_py(self, py: Python<'_>) -> PyObject { + self.to_object(py) + } +} + +impl ToPyObject for NormalOperation { + fn to_object(&self, py: Python<'_>) -> PyObject { + self.op_object.clone_ref(py) + } +} + +/** +The base class for a Python ``Target`` object. Contains data representing the +constraints of a particular backend. + +The intent of this struct is to contain data that can be representable and +accessible through both Rust and Python, so it can be used for rust-based +transpiler processes. + +This structure contains duplicates of every element in the Python counterpart of +`gate_map`. Which improves access for Python while sacrificing a small amount of +memory. + */ +#[pyclass( + mapping, + subclass, + name = "BaseTarget", + module = "qiskit._accelerate.target" +)] +#[derive(Clone, Debug)] +pub(crate) struct Target { + #[pyo3(get, set)] + pub description: Option, + #[pyo3(get)] + pub num_qubits: Option, + #[pyo3(get, set)] + pub dt: Option, + #[pyo3(get, set)] + pub granularity: u32, + #[pyo3(get, set)] + pub min_length: usize, + #[pyo3(get, set)] + pub pulse_alignment: u32, + #[pyo3(get, set)] + pub acquire_alignment: u32, + #[pyo3(get, set)] + pub qubit_properties: Option>, + #[pyo3(get, set)] + pub concurrent_measurements: Option>>, + gate_map: GateMap, + #[pyo3(get)] + _gate_name_map: IndexMap, + global_operations: IndexMap, RandomState>, + variable_class_operations: IndexSet, + qarg_gate_map: NullableIndexMap>>, + non_global_strict_basis: Option>, + non_global_basis: Option>, +} + +#[pymethods] +impl Target { + /// Create a new ``Target`` object + /// + ///Args: + /// description (str): An optional string to describe the Target. + /// num_qubits (int): An optional int to specify the number of qubits + /// the backend target has. If not set it will be implicitly set + /// based on the qargs when :meth:`~qiskit.Target.add_instruction` + /// is called. Note this must be set if the backend target is for a + /// noiseless simulator that doesn't have constraints on the + /// instructions so the transpiler knows how many qubits are + /// available. + /// dt (float): The system time resolution of input signals in seconds + /// granularity (int): An integer value representing minimum pulse gate + /// resolution in units of ``dt``. A user-defined pulse gate should + /// have duration of a multiple of this granularity value. + /// min_length (int): An integer value representing minimum pulse gate + /// length in units of ``dt``. A user-defined pulse gate should be + /// longer than this length. + /// pulse_alignment (int): An integer value representing a time + /// resolution of gate instruction starting time. Gate instruction + /// should start at time which is a multiple of the alignment + /// value. + /// acquire_alignment (int): An integer value representing a time + /// resolution of measure instruction starting time. Measure + /// instruction should start at time which is a multiple of the + /// alignment value. + /// qubit_properties (list): A list of :class:`~.QubitProperties` + /// objects defining the characteristics of each qubit on the + /// target device. If specified the length of this list must match + /// the number of qubits in the target, where the index in the list + /// matches the qubit number the properties are defined for. If some + /// qubits don't have properties available you can set that entry to + /// ``None`` + /// concurrent_measurements(list): A list of sets of qubits that must be + /// measured together. This must be provided + /// as a nested list like ``[[0, 1], [2, 3, 4]]``. + ///Raises: + /// ValueError: If both ``num_qubits`` and ``qubit_properties`` are both + /// defined and the value of ``num_qubits`` differs from the length of + /// ``qubit_properties``. + #[new] + #[pyo3(signature = ( + description = None, + num_qubits = None, + dt = None, + granularity = None, + min_length = None, + pulse_alignment = None, + acquire_alignment = None, + qubit_properties = None, + concurrent_measurements = None, + ))] + fn new( + description: Option, + num_qubits: Option, + dt: Option, + granularity: Option, + min_length: Option, + pulse_alignment: Option, + acquire_alignment: Option, + qubit_properties: Option>, + concurrent_measurements: Option>>, + ) -> PyResult { + let mut num_qubits = num_qubits; + if let Some(qubit_properties) = qubit_properties.as_ref() { + if let Some(num_qubits) = num_qubits { + if num_qubits != qubit_properties.len() { + return Err(PyValueError::new_err( + "The value of num_qubits specified does not match the \ + length of the input qubit_properties list", + )); + } + } else { + num_qubits = Some(qubit_properties.len()) + } + } + Ok(Target { + description, + num_qubits, + dt, + granularity: granularity.unwrap_or(1), + min_length: min_length.unwrap_or(1), + pulse_alignment: pulse_alignment.unwrap_or(1), + acquire_alignment: acquire_alignment.unwrap_or(0), + qubit_properties, + concurrent_measurements, + gate_map: GateMap::default(), + _gate_name_map: IndexMap::default(), + variable_class_operations: IndexSet::default(), + global_operations: IndexMap::default(), + qarg_gate_map: NullableIndexMap::default(), + non_global_basis: None, + non_global_strict_basis: None, + }) + } + + /// Add a new instruction to the `Target` after it has been processed in python. + /// + /// Args: + /// instruction: An instance of `Instruction` or the class representing said instructionm + /// if representing a variadic. + /// properties: A mapping of qargs and ``InstructionProperties``. + /// name: A name assigned to the provided gate. + /// Raises: + /// AttributeError: If gate is already in map + /// TranspilerError: If an operation class is passed in for ``instruction`` and no name + /// is specified or ``properties`` is set. + #[pyo3(signature = (instruction, name, properties=None))] + fn add_instruction( + &mut self, + instruction: TargetOperation, + name: &str, + properties: Option, + ) -> PyResult<()> { + if self.gate_map.contains_key(name) { + return Err(PyAttributeError::new_err(format!( + "Instruction {:?} is already in the target", + name + ))); + } + let mut qargs_val: PropsMap; + match instruction { + TargetOperation::Variadic(_) => { + qargs_val = PropsMap::with_capacity(1); + qargs_val.extend([(None, None)].into_iter()); + self.variable_class_operations.insert(name.to_string()); + } + TargetOperation::Normal(_) => { + if let Some(mut properties) = properties { + qargs_val = PropsMap::with_capacity(properties.len()); + let inst_num_qubits = instruction.num_qubits(); + if properties.contains_key(None) { + self.global_operations + .entry(inst_num_qubits) + .and_modify(|e| { + e.insert(name.to_string()); + }) + .or_insert(HashSet::from_iter([name.to_string()])); + } + let property_keys: Vec> = + properties.keys().map(|qargs| qargs.cloned()).collect(); + for qarg in property_keys { + if let Some(qarg) = qarg.as_ref() { + if qarg.len() != inst_num_qubits as usize { + return Err(TranspilerError::new_err(format!( + "The number of qubits for {name} does not match\ + the number of qubits in the properties dictionary: {:?}", + qarg + ))); + } + self.num_qubits = + Some(self.num_qubits.unwrap_or_default().max( + qarg.iter().fold(0, |acc, x| { + if acc > x.index() { + acc + } else { + x.index() + } + }) + 1, + )); + } + let inst_properties = properties.swap_remove(qarg.as_ref()).unwrap(); + qargs_val.insert(qarg.clone(), inst_properties); + if let Some(Some(value)) = self.qarg_gate_map.get_mut(qarg.as_ref()) { + value.insert(name.to_string()); + } else { + self.qarg_gate_map + .insert(qarg, Some(HashSet::from_iter([name.to_string()]))); + } + } + } else { + qargs_val = PropsMap::with_capacity(0); + } + } + } + self._gate_name_map.insert(name.to_string(), instruction); + self.gate_map.insert(name.to_string(), qargs_val); + self.non_global_basis = None; + self.non_global_strict_basis = None; + Ok(()) + } + + /// Update the property object for an instruction qarg pair already in the `Target` + /// + /// Args: + /// instruction (str): The instruction name to update + /// qargs (tuple): The qargs to update the properties of + /// properties (InstructionProperties): The properties to set for this instruction + /// Raises: + /// KeyError: If ``instruction`` or ``qarg`` are not in the target + #[pyo3(text_signature = "(instruction, qargs, properties, /,)")] + fn update_instruction_properties( + &mut self, + instruction: String, + qargs: Option, + properties: Option, + ) -> PyResult<()> { + if !self.contains_key(&instruction) { + return Err(PyKeyError::new_err(format!( + "Provided instruction: '{:?}' not in this Target.", + &instruction + ))); + }; + let mut prop_map = self[&instruction].clone(); + if !(prop_map.contains_key(qargs.as_ref())) { + return Err(PyKeyError::new_err(format!( + "Provided qarg {:?} not in this Target for {:?}.", + &qargs.unwrap_or_default(), + &instruction + ))); + } + if let Some(e) = prop_map.get_mut(qargs.as_ref()) { + *e = properties; + } + self.gate_map + .entry(instruction) + .and_modify(|e| *e = prop_map); + Ok(()) + } + + /// Get the qargs for a given operation name + /// + /// Args: + /// operation (str): The operation name to get qargs for + /// Returns: + /// set: The set of qargs the gate instance applies to. + #[pyo3(name = "qargs_for_operation_name")] + pub fn py_qargs_for_operation_name( + &self, + py: Python, + operation: &str, + ) -> PyResult>> { + match self.qargs_for_operation_name(operation) { + Ok(option_set) => { + Ok(option_set.map(|qargs| qargs.map(|qargs| qargs.to_object(py)).collect())) + } + Err(e) => Err(PyKeyError::new_err(e.message)), + } + } + + /// Get the operation class object for a given name + /// + /// Args: + /// instruction (str): The instruction name to get the + /// :class:`~qiskit.circuit.Instruction` instance for + /// Returns: + /// qiskit.circuit.Instruction: The Instruction instance corresponding to the + /// name. This also can also be the class for globally defined variable with + /// operations. + #[pyo3(name = "operation_from_name")] + pub fn py_operation_from_name(&self, py: Python, instruction: &str) -> PyResult { + match self._operation_from_name(instruction) { + Ok(instruction) => Ok(instruction.to_object(py)), + Err(e) => Err(PyKeyError::new_err(e.message)), + } + } + + /// Get the operation class object for a specified qargs tuple + /// + /// Args: + /// qargs (tuple): A qargs tuple of the qubits to get the gates that apply + /// to it. For example, ``(0,)`` will return the set of all + /// instructions that apply to qubit 0. If set to ``None`` this will + /// return any globally defined operations in the target. + /// Returns: + /// list: The list of :class:`~qiskit.circuit.Instruction` instances + /// that apply to the specified qarg. This may also be a class if + /// a variable width operation is globally defined. + /// + /// Raises: + /// KeyError: If qargs is not in target + #[pyo3(name = "operations_for_qargs", signature=(qargs=None, /))] + pub fn py_operations_for_qargs( + &self, + py: Python, + qargs: Option, + ) -> PyResult> { + // Move to rust native once Gates are in rust + Ok(self + .py_operation_names_for_qargs(qargs)? + .into_iter() + .map(|x| self._gate_name_map[x].to_object(py)) + .collect()) + } + + /// Get the operation names for a specified qargs tuple + /// + /// Args: + /// qargs (tuple): A ``qargs`` tuple of the qubits to get the gates that apply + /// to it. For example, ``(0,)`` will return the set of all + /// instructions that apply to qubit 0. If set to ``None`` this will + /// return the names for any globally defined operations in the target. + /// Returns: + /// set: The set of operation names that apply to the specified ``qargs``. + /// + /// Raises: + /// KeyError: If ``qargs`` is not in target + #[pyo3(name = "operation_names_for_qargs", signature=(qargs=None, /))] + pub fn py_operation_names_for_qargs(&self, qargs: Option) -> PyResult> { + match self.operation_names_for_qargs(qargs.as_ref()) { + Ok(set) => Ok(set), + Err(e) => Err(PyKeyError::new_err(e.message)), + } + } + + /// Return whether the instruction (operation + qubits) is supported by the target + /// + /// Args: + /// operation_name (str): The name of the operation for the instruction. Either + /// this or ``operation_class`` must be specified, if both are specified + /// ``operation_class`` will take priority and this argument will be ignored. + /// qargs (tuple): The tuple of qubit indices for the instruction. If this is + /// not specified then this method will return ``True`` if the specified + /// operation is supported on any qubits. The typical application will + /// always have this set (otherwise it's the same as just checking if the + /// target contains the operation). Normally you would not set this argument + /// if you wanted to check more generally that the target supports an operation + /// with the ``parameters`` on any qubits. + /// operation_class (Type[qiskit.circuit.Instruction]): The operation class to check whether + /// the target supports a particular operation by class rather + /// than by name. This lookup is more expensive as it needs to + /// iterate over all operations in the target instead of just a + /// single lookup. If this is specified it will supersede the + /// ``operation_name`` argument. The typical use case for this + /// operation is to check whether a specific variant of an operation + /// is supported on the backend. For example, if you wanted to + /// check whether a :class:`~.RXGate` was supported on a specific + /// qubit with a fixed angle. That fixed angle variant will + /// typically have a name different from the object's + /// :attr:`~.Instruction.name` attribute (``"rx"``) in the target. + /// This can be used to check if any instances of the class are + /// available in such a case. + /// parameters (list): A list of parameters to check if the target + /// supports them on the specified qubits. If the instruction + /// supports the parameter values specified in the list on the + /// operation and qargs specified this will return ``True`` but + /// if the parameters are not supported on the specified + /// instruction it will return ``False``. If this argument is not + /// specified this method will return ``True`` if the instruction + /// is supported independent of the instruction parameters. If + /// specified with any :class:`~.Parameter` objects in the list, + /// that entry will be treated as supporting any value, however parameter names + /// will not be checked (for example if an operation in the target + /// is listed as parameterized with ``"theta"`` and ``"phi"`` is + /// passed into this function that will return ``True``). For + /// example, if called with:: + /// + /// parameters = [Parameter("theta")] + /// target.instruction_supported("rx", (0,), parameters=parameters) + /// + /// will return ``True`` if an :class:`~.RXGate` is supported on qubit 0 + /// that will accept any parameter. If you need to check for a fixed numeric + /// value parameter this argument is typically paired with the ``operation_class`` + /// argument. For example:: + /// + /// target.instruction_supported("rx", (0,), RXGate, parameters=[pi / 4]) + /// + /// will return ``True`` if an RXGate(pi/4) exists on qubit 0. + /// + /// Returns: + /// bool: Returns ``True`` if the instruction is supported and ``False`` if it isn't. + #[pyo3( + name = "instruction_supported", + signature = (operation_name=None, qargs=None, operation_class=None, parameters=None) + )] + pub fn py_instruction_supported( + &self, + py: Python, + operation_name: Option, + qargs: Option, + operation_class: Option<&Bound>, + parameters: Option>, + ) -> PyResult { + let mut qargs = qargs; + if self.num_qubits.is_none() { + qargs = None; + } + if let Some(_operation_class) = operation_class { + for (op_name, obj) in self._gate_name_map.iter() { + match obj { + TargetOperation::Variadic(variable) => { + if !_operation_class.eq(variable)? { + continue; + } + // If no qargs operation class is supported + if let Some(_qargs) = &qargs { + let qarg_set: HashSet = _qargs.iter().cloned().collect(); + // If qargs set then validate no duplicates and all indices are valid on device + return Ok(_qargs + .iter() + .all(|qarg| qarg.index() <= self.num_qubits.unwrap_or_default()) + && qarg_set.len() == _qargs.len()); + } else { + return Ok(true); + } + } + TargetOperation::Normal(normal) => { + if python_is_instance(py, normal, _operation_class)? { + if let Some(parameters) = ¶meters { + if parameters.len() != normal.params.len() { + continue; + } + if !check_obj_params(parameters, normal) { + continue; + } + } + if let Some(_qargs) = &qargs { + if self.gate_map.contains_key(op_name) { + let gate_map_name = &self.gate_map[op_name]; + if gate_map_name.contains_key(qargs.as_ref()) { + return Ok(true); + } + if gate_map_name.contains_key(None) { + let qubit_comparison = + self._gate_name_map[op_name].num_qubits(); + return Ok(qubit_comparison == _qargs.len() as u32 + && _qargs.iter().all(|x| { + x.index() < self.num_qubits.unwrap_or_default() + })); + } + } else { + let qubit_comparison = obj.num_qubits(); + return Ok(qubit_comparison == _qargs.len() as u32 + && _qargs.iter().all(|x| { + x.index() < self.num_qubits.unwrap_or_default() + })); + } + } else { + return Ok(true); + } + } + } + } + } + Ok(false) + } else if let Some(operation_name) = operation_name { + if let Some(parameters) = parameters { + if let Some(obj) = self._gate_name_map.get(&operation_name) { + if self.variable_class_operations.contains(&operation_name) { + if let Some(_qargs) = qargs { + let qarg_set: HashSet = _qargs.iter().cloned().collect(); + return Ok(_qargs + .iter() + .all(|qarg| qarg.index() <= self.num_qubits.unwrap_or_default()) + && qarg_set.len() == _qargs.len()); + } else { + return Ok(true); + } + } + + let obj_params = obj.params(); + if parameters.len() != obj_params.len() { + return Ok(false); + } + for (index, params) in parameters.iter().enumerate() { + let mut matching_params = false; + let obj_at_index = &obj_params[index]; + if matches!(obj_at_index, Param::ParameterExpression(_)) + || python_compare(py, ¶ms, &obj_params[index])? + { + matching_params = true; + } + if !matching_params { + return Ok(false); + } + } + return Ok(true); + } + } + Ok(self.instruction_supported(&operation_name, qargs.as_ref())) + } else { + Ok(false) + } + } + + /// Get the instruction properties for a specific instruction tuple + /// + /// This method is to be used in conjunction with the + /// :attr:`~qiskit.transpiler.Target.instructions` attribute of a + /// :class:`~qiskit.transpiler.Target` object. You can use this method to quickly + /// get the instruction properties for an element of + /// :attr:`~qiskit.transpiler.Target.instructions` by using the index in that list. + /// However, if you're not working with :attr:`~qiskit.transpiler.Target.instructions` + /// directly it is likely more efficient to access the target directly via the name + /// and qubits to get the instruction properties. For example, if + /// :attr:`~qiskit.transpiler.Target.instructions` returned:: + /// + /// [(XGate(), (0,)), (XGate(), (1,))] + /// + /// you could get the properties of the ``XGate`` on qubit 1 with:: + /// + /// props = target.instruction_properties(1) + /// + /// but just accessing it directly via the name would be more efficient:: + /// + /// props = target['x'][(1,)] + /// + /// (assuming the ``XGate``'s canonical name in the target is ``'x'``) + /// This is especially true for larger targets as this will scale worse with the number + /// of instruction tuples in a target. + /// + /// Args: + /// index (int): The index of the instruction tuple from the + /// :attr:`~qiskit.transpiler.Target.instructions` attribute. For, example + /// if you want the properties from the third element in + /// :attr:`~qiskit.transpiler.Target.instructions` you would set this to be ``2``. + /// Returns: + /// InstructionProperties: The instruction properties for the specified instruction tuple + pub fn instruction_properties(&self, index: usize) -> PyResult> { + let mut index_counter = 0; + for (_operation, props_map) in self.gate_map.iter() { + let gate_map_oper = props_map.values(); + for inst_props in gate_map_oper { + if index_counter == index { + return Ok(inst_props.clone()); + } + index_counter += 1; + } + } + Err(PyIndexError::new_err(format!( + "Index: {:?} is out of range.", + index + ))) + } + + /// Return the non-global operation names for the target + /// + /// The non-global operations are those in the target which don't apply + /// on all qubits (for single qubit operations) or all multi-qubit qargs + /// (for multi-qubit operations). + /// + /// Args: + /// strict_direction (bool): If set to ``True`` the multi-qubit + /// operations considered as non-global respect the strict + /// direction (or order of qubits in the qargs is significant). For + /// example, if ``cx`` is defined on ``(0, 1)`` and ``ecr`` is + /// defined over ``(1, 0)`` by default neither would be considered + /// non-global, but if ``strict_direction`` is set ``True`` both + /// ``cx`` and ``ecr`` would be returned. + /// + /// Returns: + /// List[str]: A list of operation names for operations that aren't global in this target + #[pyo3(name = "get_non_global_operation_names", signature = (/, strict_direction=false,))] + fn py_get_non_global_operation_names( + &mut self, + py: Python<'_>, + strict_direction: bool, + ) -> PyObject { + self.get_non_global_operation_names(strict_direction) + .to_object(py) + } + + // Instance attributes + + /// The set of qargs in the target. + #[getter] + #[pyo3(name = "qargs")] + fn py_qargs(&self, py: Python) -> PyResult { + if let Some(qargs) = self.qargs() { + let qargs = qargs.map(|qargs| qargs.map(|q| PyTuple::new_bound(py, q))); + let set = PySet::empty_bound(py)?; + for qargs in qargs { + set.add(qargs)?; + } + Ok(set.into_any().unbind()) + } else { + Ok(py.None()) + } + } + + /// Get the list of tuples ``(:class:`~qiskit.circuit.Instruction`, (qargs))`` + /// for the target + /// + /// For globally defined variable width operations the tuple will be of the form + /// ``(class, None)`` where class is the actual operation class that + /// is globally defined. + #[getter] + #[pyo3(name = "instructions")] + pub fn py_instructions(&self, py: Python<'_>) -> PyResult> { + let list = PyList::empty_bound(py); + for (inst, qargs) in self._instructions() { + let qargs = qargs.map(|q| PyTuple::new_bound(py, q).unbind()); + list.append((inst, qargs))?; + } + Ok(list.unbind()) + } + /// Get the operation names in the target. + #[getter] + #[pyo3(name = "operation_names")] + fn py_operation_names(&self, py: Python<'_>) -> Py { + PyList::new_bound(py, self.operation_names()).unbind() + } + + /// Get the operation objects in the target. + #[getter] + #[pyo3(name = "operations")] + fn py_operations(&self, py: Python<'_>) -> Py { + PyList::new_bound(py, self._gate_name_map.values()).unbind() + } + + /// Returns a sorted list of physical qubits. + #[getter] + #[pyo3(name = "physical_qubits")] + fn py_physical_qubits(&self, py: Python<'_>) -> Py { + PyList::new_bound(py, self.physical_qubits()).unbind() + } + + // Magic methods: + + fn __len__(&self) -> PyResult { + Ok(self.gate_map.len()) + } + + fn __getstate__(&self, py: Python<'_>) -> PyResult> { + let result_list = PyDict::new_bound(py); + result_list.set_item("description", self.description.clone())?; + result_list.set_item("num_qubits", self.num_qubits)?; + result_list.set_item("dt", self.dt)?; + result_list.set_item("granularity", self.granularity)?; + result_list.set_item("min_length", self.min_length)?; + result_list.set_item("pulse_alignment", self.pulse_alignment)?; + result_list.set_item("acquire_alignment", self.acquire_alignment)?; + result_list.set_item("qubit_properties", self.qubit_properties.clone())?; + result_list.set_item( + "concurrent_measurements", + self.concurrent_measurements.clone(), + )?; + result_list.set_item( + "gate_map", + self.gate_map + .clone() + .into_iter() + .map(|(key, value)| { + ( + key, + value + .into_iter() + .collect::, Option)>>(), + ) + }) + .collect::() + .into_py(py), + )?; + result_list.set_item("gate_name_map", self._gate_name_map.to_object(py))?; + result_list.set_item("global_operations", self.global_operations.clone())?; + result_list.set_item( + "qarg_gate_map", + self.qarg_gate_map.clone().into_iter().collect_vec(), + )?; + result_list.set_item("non_global_basis", self.non_global_basis.clone())?; + result_list.set_item( + "non_global_strict_basis", + self.non_global_strict_basis.clone(), + )?; + Ok(result_list.unbind()) + } + + fn __setstate__(&mut self, state: Bound) -> PyResult<()> { + self.description = state + .get_item("description")? + .unwrap() + .extract::>()?; + self.num_qubits = state + .get_item("num_qubits")? + .unwrap() + .extract::>()?; + self.dt = state.get_item("dt")?.unwrap().extract::>()?; + self.granularity = state.get_item("granularity")?.unwrap().extract::()?; + self.min_length = state.get_item("min_length")?.unwrap().extract::()?; + self.pulse_alignment = state + .get_item("pulse_alignment")? + .unwrap() + .extract::()?; + self.acquire_alignment = state + .get_item("acquire_alignment")? + .unwrap() + .extract::()?; + self.qubit_properties = state + .get_item("qubit_properties")? + .unwrap() + .extract::>>()?; + self.concurrent_measurements = state + .get_item("concurrent_measurements")? + .unwrap() + .extract::>>>()?; + self.gate_map = IndexMap::from_iter( + state + .get_item("gate_map")? + .unwrap() + .extract::()? + .into_iter() + .map(|(name, prop_map)| (name, PropsMap::from_iter(prop_map.into_iter()))), + ); + self._gate_name_map = state + .get_item("gate_name_map")? + .unwrap() + .extract::>()?; + self.global_operations = state + .get_item("global_operations")? + .unwrap() + .extract::, RandomState>>()?; + self.qarg_gate_map = NullableIndexMap::from_iter( + state + .get_item("qarg_gate_map")? + .unwrap() + .extract::, Option>)>>()?, + ); + self.non_global_basis = state + .get_item("non_global_basis")? + .unwrap() + .extract::>>()?; + self.non_global_strict_basis = state + .get_item("non_global_strict_basis")? + .unwrap() + .extract::>>()?; + Ok(()) + } +} + +// Rust native methods +impl Target { + /// Returns an iterator over all the instructions present in the `Target` + /// as pair of `&OperationType`, `&SmallVec<[Param; 3]>` and `Option<&Qargs>`. + // TODO: Remove once `Target` is being consumed. + #[allow(dead_code)] + pub fn instructions(&self) -> impl Iterator)> { + self._instructions() + .filter_map(|(operation, qargs)| match &operation { + TargetOperation::Normal(oper) => Some((oper, qargs)), + _ => None, + }) + } + + /// Returns an iterator over all the instructions present in the `Target` + /// as pair of `&TargetOperation` and `Option<&Qargs>`. + fn _instructions(&self) -> impl Iterator)> { + self.gate_map.iter().flat_map(move |(op, props_map)| { + props_map + .keys() + .map(move |qargs| (&self._gate_name_map[op], qargs)) + }) + } + /// Returns an iterator over the operation names in the target. + // TODO: Remove once `Target` is being consumed. + #[allow(dead_code)] + pub fn operation_names(&self) -> impl ExactSizeIterator { + self.gate_map.keys().map(|x| x.as_str()) + } + + /// Get the `OperationType` objects present in the target. + // TODO: Remove once `Target` is being consumed. + #[allow(dead_code)] + pub fn operations(&self) -> impl Iterator { + return self._gate_name_map.values().filter_map(|oper| match oper { + TargetOperation::Normal(oper) => Some(oper), + _ => None, + }); + } + + /// Get an iterator over the indices of all physical qubits of the target + pub fn physical_qubits(&self) -> impl ExactSizeIterator { + 0..self.num_qubits.unwrap_or_default() + } + + /// Generate non global operations if missing + fn generate_non_global_op_names(&mut self, strict_direction: bool) -> &[String] { + let mut search_set: HashSet = HashSet::default(); + if strict_direction { + // Build search set + search_set = self.qarg_gate_map.keys().flatten().cloned().collect(); + } else { + for qarg_key in self.qarg_gate_map.keys().flatten() { + if qarg_key.len() != 1 { + let mut vec = qarg_key.clone(); + vec.sort_unstable(); + search_set.insert(vec); + } + } + } + let mut incomplete_basis_gates: Vec = vec![]; + let mut size_dict: IndexMap = IndexMap::default(); + *size_dict + .entry(1) + .or_insert(self.num_qubits.unwrap_or_default()) = self.num_qubits.unwrap_or_default(); + for qarg in &search_set { + if qarg.len() == 1 { + continue; + } + *size_dict.entry(qarg.len()).or_insert(0) += 1; + } + for (inst, qargs_props) in self.gate_map.iter() { + let mut qarg_len = qargs_props.len(); + let mut qargs_keys = qargs_props.keys().peekable(); + let qarg_sample = qargs_keys.peek().cloned(); + if let Some(qarg_sample) = qarg_sample { + if qarg_sample.is_none() { + continue; + } + if !strict_direction { + let mut deduplicated_qargs: HashSet> = + HashSet::default(); + for qarg in qargs_keys.flatten() { + let mut ordered_qargs = qarg.clone(); + ordered_qargs.sort_unstable(); + deduplicated_qargs.insert(ordered_qargs); + } + qarg_len = deduplicated_qargs.len(); + } + if let Some(qarg_sample) = qarg_sample { + if qarg_len != *size_dict.entry(qarg_sample.len()).or_insert(0) { + incomplete_basis_gates.push(inst.clone()); + } + } + } + } + if strict_direction { + self.non_global_strict_basis = Some(incomplete_basis_gates); + self.non_global_strict_basis.as_ref().unwrap() + } else { + self.non_global_basis = Some(incomplete_basis_gates.clone()); + self.non_global_basis.as_ref().unwrap() + } + } + + /// Get all non_global operation names. + pub fn get_non_global_operation_names(&mut self, strict_direction: bool) -> Option<&[String]> { + if strict_direction { + if self.non_global_strict_basis.is_some() { + return self.non_global_strict_basis.as_deref(); + } + } else if self.non_global_basis.is_some() { + return self.non_global_basis.as_deref(); + } + return Some(self.generate_non_global_op_names(strict_direction)); + } + + /// Gets all the operation names that use these qargs. Rust native equivalent of ``BaseTarget.operation_names_for_qargs()`` + pub fn operation_names_for_qargs( + &self, + qargs: Option<&Qargs>, + ) -> Result, TargetKeyError> { + // When num_qubits == 0 we return globally defined operators + let mut res: HashSet<&str> = HashSet::default(); + let mut qargs = qargs; + if self.num_qubits.unwrap_or_default() == 0 || self.num_qubits.is_none() { + qargs = None; + } + if let Some(qargs) = qargs.as_ref() { + if qargs + .iter() + .any(|x| !(0..self.num_qubits.unwrap_or_default()).contains(&x.index())) + { + return Err(TargetKeyError::new_err(format!( + "{:?} not in Target", + qargs + ))); + } + } + if let Some(Some(qarg_gate_map_arg)) = self.qarg_gate_map.get(qargs).as_ref() { + res.extend(qarg_gate_map_arg.iter().map(|key| key.as_str())); + } + for name in self._gate_name_map.keys() { + if self.variable_class_operations.contains(name) { + res.insert(name); + } + } + if let Some(qargs) = qargs.as_ref() { + if let Some(global_gates) = self.global_operations.get(&(qargs.len() as u32)) { + res.extend(global_gates.iter().map(|key| key.as_str())) + } + } + if res.is_empty() { + return Err(TargetKeyError::new_err(format!( + "{:?} not in target", + qargs + ))); + } + Ok(res) + } + + /// Returns an iterator of `OperationType` instances and parameters present in the Target that affect the provided qargs. + // TODO: Remove once `Target` is being consumed. + #[allow(dead_code)] + pub fn operations_for_qargs( + &self, + qargs: Option<&Qargs>, + ) -> Result, TargetKeyError> { + self.operation_names_for_qargs(qargs).map(|operations| { + operations + .into_iter() + .filter_map(|oper| match &self._gate_name_map[oper] { + TargetOperation::Normal(normal) => Some(normal), + _ => None, + }) + }) + } + + /// Gets an iterator with all the qargs used by the specified operation name. + /// + /// Rust native equivalent of ``BaseTarget.qargs_for_operation_name()`` + pub fn qargs_for_operation_name( + &self, + operation: &str, + ) -> Result>, TargetKeyError> { + if let Some(gate_map_oper) = self.gate_map.get(operation) { + if gate_map_oper.contains_key(None) { + return Ok(None); + } + let qargs = gate_map_oper.keys().flatten(); + Ok(Some(qargs)) + } else { + Err(TargetKeyError::new_err(format!( + "Operation: {operation} not in Target." + ))) + } + } + + /// Gets a tuple of Operation object and Parameters based on the operation name if present in the Target. + // TODO: Remove once `Target` is being consumed. + #[allow(dead_code)] + pub fn operation_from_name( + &self, + instruction: &str, + ) -> Result<&NormalOperation, TargetKeyError> { + match self._operation_from_name(instruction) { + Ok(TargetOperation::Normal(operation)) => Ok(operation), + Ok(TargetOperation::Variadic(_)) => Err(TargetKeyError::new_err(format!( + "Instruction {:?} was found in the target, but the instruction is Varidic.", + instruction + ))), + Err(e) => Err(e), + } + } + + /// Gets the instruction object based on the operation name + fn _operation_from_name(&self, instruction: &str) -> Result<&TargetOperation, TargetKeyError> { + if let Some(gate_obj) = self._gate_name_map.get(instruction) { + Ok(gate_obj) + } else { + Err(TargetKeyError::new_err(format!( + "Instruction {:?} not in target", + instruction + ))) + } + } + + /// Returns an iterator over all the qargs of a specific Target object + pub fn qargs(&self) -> Option>> { + let mut qargs = self.qarg_gate_map.keys().peekable(); + let next_entry = qargs.peek(); + let is_none = next_entry.is_none() || next_entry.unwrap().is_none(); + if qargs.len() == 1 && is_none { + return None; + } + Some(qargs) + } + + /// Checks whether an instruction is supported by the Target based on instruction name and qargs. + pub fn instruction_supported(&self, operation_name: &str, qargs: Option<&Qargs>) -> bool { + if self.gate_map.contains_key(operation_name) { + if let Some(_qargs) = qargs { + let qarg_set: HashSet<&PhysicalQubit> = _qargs.iter().collect(); + if let Some(gate_prop_name) = self.gate_map.get(operation_name) { + if gate_prop_name.contains_key(qargs) { + return true; + } + if gate_prop_name.contains_key(None) { + let obj = &self._gate_name_map[operation_name]; + if self.variable_class_operations.contains(operation_name) { + return qargs.is_none() + || _qargs.iter().all(|qarg| { + qarg.index() <= self.num_qubits.unwrap_or_default() + }) && qarg_set.len() == _qargs.len(); + } else { + let qubit_comparison = obj.num_qubits(); + return qubit_comparison == _qargs.len() as u32 + && _qargs.iter().all(|qarg| { + qarg.index() < self.num_qubits.unwrap_or_default() + }); + } + } + } else { + // Duplicate case is if it contains none + if self.variable_class_operations.contains(operation_name) { + return qargs.is_none() + || _qargs + .iter() + .all(|qarg| qarg.index() <= self.num_qubits.unwrap_or_default()) + && qarg_set.len() == _qargs.len(); + } else { + let qubit_comparison = self._gate_name_map[operation_name].num_qubits(); + return qubit_comparison == _qargs.len() as u32 + && _qargs + .iter() + .all(|qarg| qarg.index() < self.num_qubits.unwrap_or_default()); + } + } + } else { + return true; + } + } + false + } + + // IndexMap methods + + /// Retreive all the gate names in the Target + // TODO: Remove once `Target` is being consumed. + #[allow(dead_code)] + pub fn keys(&self) -> impl Iterator { + self.gate_map.keys().map(|x| x.as_str()) + } + + /// Retrieves an iterator over the property maps stored within the Target + // TODO: Remove once `Target` is being consumed. + #[allow(dead_code)] + pub fn values(&self) -> impl Iterator { + self.gate_map.values() + } + + /// Checks if a key exists in the Target + pub fn contains_key(&self, key: &str) -> bool { + self.gate_map.contains_key(key) + } +} + +// To access the Target's gate map by gate name. +impl Index<&str> for Target { + type Output = PropsMap; + fn index(&self, index: &str) -> &Self::Output { + self.gate_map.index(index) + } +} + +// For instruction_supported +fn check_obj_params(parameters: &[Param], obj: &NormalOperation) -> bool { + for (index, param) in parameters.iter().enumerate() { + let param_at_index = &obj.params[index]; + match (param, param_at_index) { + (Param::Float(p1), Param::Float(p2)) => { + if p1 != p2 { + return false; + } + } + (&Param::ParameterExpression(_), Param::Float(_)) => return false, + (&Param::ParameterExpression(_), Param::Obj(_)) => return false, + _ => continue, + } + } + true +} + +pub fn python_compare(py: Python, obj: &T, other: &U) -> PyResult +where + T: ToPyObject, + U: ToPyObject, +{ + let obj = obj.to_object(py); + let obj_bound = obj.bind(py); + obj_bound.eq(other) +} + +pub fn python_is_instance(py: Python, obj: &T, other: &U) -> PyResult +where + T: ToPyObject, + U: ToPyObject, +{ + let obj = obj.to_object(py); + let other_obj = other.to_object(py); + let obj_bound = obj.bind(py); + obj_bound.is_instance(other_obj.bind(py)) +} + +#[pymodule] +pub fn target(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) +} + +// TODO: Add rust-based unit testing. diff --git a/crates/accelerate/src/target_transpiler/nullable_index_map.rs b/crates/accelerate/src/target_transpiler/nullable_index_map.rs new file mode 100644 index 000000000000..d3056c9edd8a --- /dev/null +++ b/crates/accelerate/src/target_transpiler/nullable_index_map.rs @@ -0,0 +1,457 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use ahash::RandomState; +use indexmap::{ + map::{IntoIter as BaseIntoIter, Iter as BaseIter, Keys as BaseKeys, Values as BaseValues}, + IndexMap, +}; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use pyo3::IntoPy; +use rustworkx_core::dictmap::InitWithHasher; +use std::ops::Index; +use std::{hash::Hash, mem::swap}; + +type BaseMap = IndexMap; + +/// +/// An `IndexMap`-like structure thet can be used when one of the keys can have a `None` value. +/// +/// This structure is essentially a wrapper around the `IndexMap` struct that allows the +/// storage of `Option` key values as `K`` and keep an extra slot reserved only for the +/// `None` instance. There are some upsides to this including: +/// +/// The ability to index using Option<&K> to index a specific key. +/// Store keys as non option wrapped to obtain references to K instead of reference to Option. +/// +/// **Warning:** This is an experimental feature and should be used with care as it does not +/// fully implement all the methods present in `IndexMap` due to API limitations. +#[derive(Debug, Clone)] +pub(crate) struct NullableIndexMap +where + K: Eq + Hash + Clone, + V: Clone, +{ + map: BaseMap, + null_val: Option, +} + +impl NullableIndexMap +where + K: Eq + Hash + Clone, + V: Clone, +{ + /// Returns a reference to the value stored at `key`, if it does not exist + /// `None` is returned instead. + pub fn get(&self, key: Option<&K>) -> Option<&V> { + match key { + Some(key) => self.map.get(key), + None => self.null_val.as_ref(), + } + } + + /// Returns a mutable reference to the value stored at `key`, if it does not + /// exist `None` is returned instead. + pub fn get_mut(&mut self, key: Option<&K>) -> Option<&mut V> { + match key { + Some(key) => self.map.get_mut(key), + None => self.null_val.as_mut(), + } + } + + /// Inserts a `value` in the slot alotted to `key`. + /// + /// If a previous value existed there previously it will be returned, otherwise + /// `None` will be returned. + pub fn insert(&mut self, key: Option, value: V) -> Option { + match key { + Some(key) => self.map.insert(key, value), + None => { + let mut old_val = Some(value); + swap(&mut old_val, &mut self.null_val); + old_val + } + } + } + + /// Creates an instance of `NullableIndexMap` with capacity to hold `n`+1 key-value + /// pairs. + /// + /// Notice that an extra space needs to be alotted to store the instance of `None` a + /// key. + pub fn with_capacity(n: usize) -> Self { + Self { + map: BaseMap::with_capacity(n), + null_val: None, + } + } + + /// Creates an instance of `NullableIndexMap` from an iterator over instances of + /// `(Option, V)`. + pub fn from_iter<'a, I>(iter: I) -> Self + where + I: IntoIterator, V)> + 'a, + { + let mut null_val = None; + let filtered = iter.into_iter().filter_map(|item| match item { + (Some(key), value) => Some((key, value)), + (None, value) => { + null_val = Some(value); + None + } + }); + Self { + map: IndexMap::from_iter(filtered), + null_val, + } + } + + /// Returns `true` if the map contains a slot indexed by `key`, otherwise `false`. + pub fn contains_key(&self, key: Option<&K>) -> bool { + match key { + Some(key) => self.map.contains_key(key), + None => self.null_val.is_some(), + } + } + + /// Extends the key-value pairs in the map with the contents of an iterator over + /// `(Option, V)`. + /// + /// If an already existent key is provided, it will be replaced by the entry provided + /// in the iterator. + pub fn extend<'a, I>(&mut self, iter: I) + where + I: IntoIterator, V)> + 'a, + { + let filtered = iter.into_iter().filter_map(|item| match item { + (Some(key), value) => Some((key, value)), + (None, value) => { + self.null_val = Some(value); + None + } + }); + self.map.extend(filtered) + } + + /// Removes the entry allotted to `key` from the map and returns it. The index of + /// this entry is then replaced by the entry located at the last index. + /// + /// `None` will be returned if the `key` is not present in the map. + pub fn swap_remove(&mut self, key: Option<&K>) -> Option { + match key { + Some(key) => self.map.swap_remove(key), + None => { + let mut ret_val = None; + swap(&mut ret_val, &mut self.null_val); + ret_val + } + } + } + + /// Returns an iterator over references of the key-value pairs of the map. + // TODO: Remove once `NullableIndexMap` is being consumed. + #[allow(dead_code)] + pub fn iter(&self) -> Iter { + Iter { + map: self.map.iter(), + null_value: &self.null_val, + } + } + + /// Returns an iterator over references of the keys present in the map. + pub fn keys(&self) -> Keys { + Keys { + map_keys: self.map.keys(), + null_value: self.null_val.is_some(), + } + } + + /// Returns an iterator over references of all the values present in the map. + pub fn values(&self) -> Values { + Values { + map_values: self.map.values(), + null_value: &self.null_val, + } + } + + /// Returns the number of key-value pairs present in the map. + pub fn len(&self) -> usize { + self.map.len() + self.null_val.is_some() as usize + } +} + +impl IntoIterator for NullableIndexMap +where + K: Eq + Hash + Clone, + V: Clone, +{ + type Item = (Option, V); + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter { + map: self.map.into_iter(), + null_value: self.null_val, + } + } +} + +/// Iterator for the key-value pairs in `NullableIndexMap`. +pub struct Iter<'a, K, V> { + map: BaseIter<'a, K, V>, + null_value: &'a Option, +} + +impl<'a, K, V> Iterator for Iter<'a, K, V> { + type Item = (Option<&'a K>, &'a V); + + fn next(&mut self) -> Option { + if let Some((key, val)) = self.map.next() { + Some((Some(key), val)) + } else if let Some(value) = self.null_value { + let value = value; + self.null_value = &None; + Some((None, value)) + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + ( + self.map.size_hint().0 + self.null_value.is_some() as usize, + self.map + .size_hint() + .1 + .map(|hint| hint + self.null_value.is_some() as usize), + ) + } +} + +impl<'a, K, V> ExactSizeIterator for Iter<'a, K, V> { + fn len(&self) -> usize { + self.map.len() + self.null_value.is_some() as usize + } +} + +/// Owned iterator over the key-value pairs in `NullableIndexMap`. +pub struct IntoIter +where + V: Clone, +{ + map: BaseIntoIter, + null_value: Option, +} + +impl Iterator for IntoIter +where + V: Clone, +{ + type Item = (Option, V); + + fn next(&mut self) -> Option { + if let Some((key, val)) = self.map.next() { + Some((Some(key), val)) + } else if self.null_value.is_some() { + let mut value = None; + swap(&mut value, &mut self.null_value); + Some((None, value.unwrap())) + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + ( + self.map.size_hint().0 + self.null_value.is_some() as usize, + self.map + .size_hint() + .1 + .map(|hint| hint + self.null_value.is_some() as usize), + ) + } +} + +impl ExactSizeIterator for IntoIter +where + V: Clone, +{ + fn len(&self) -> usize { + self.map.len() + self.null_value.is_some() as usize + } +} + +/// Iterator over the keys of a `NullableIndexMap`. +pub struct Keys<'a, K, V> { + map_keys: BaseKeys<'a, K, V>, + null_value: bool, +} + +impl<'a, K, V> Iterator for Keys<'a, K, V> { + type Item = Option<&'a K>; + + fn next(&mut self) -> Option { + if let Some(key) = self.map_keys.next() { + Some(Some(key)) + } else if self.null_value { + self.null_value = false; + Some(None) + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + ( + self.map_keys.size_hint().0 + self.null_value as usize, + self.map_keys + .size_hint() + .1 + .map(|hint| hint + self.null_value as usize), + ) + } +} + +impl<'a, K, V> ExactSizeIterator for Keys<'a, K, V> { + fn len(&self) -> usize { + self.map_keys.len() + self.null_value as usize + } +} + +/// Iterator over the values of a `NullableIndexMap`. +pub struct Values<'a, K, V> { + map_values: BaseValues<'a, K, V>, + null_value: &'a Option, +} + +impl<'a, K, V> Iterator for Values<'a, K, V> { + type Item = &'a V; + + fn next(&mut self) -> Option { + if let Some(value) = self.map_values.next() { + Some(value) + } else if self.null_value.is_some() { + let return_value = self.null_value; + self.null_value = &None; + return_value.as_ref() + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + ( + self.map_values.size_hint().0 + self.null_value.is_some() as usize, + self.map_values + .size_hint() + .1 + .map(|hint| hint + self.null_value.is_some() as usize), + ) + } +} + +impl<'a, K, V> ExactSizeIterator for Values<'a, K, V> { + fn len(&self) -> usize { + self.map_values.len() + self.null_value.is_some() as usize + } +} + +impl Index> for NullableIndexMap +where + K: Eq + Hash + Clone, + V: Clone, +{ + type Output = V; + fn index(&self, index: Option<&K>) -> &Self::Output { + match index { + Some(k) => self.map.index(k), + None => match &self.null_val { + Some(val) => val, + None => panic!("The provided key is not present in map: None"), + }, + } + } +} + +impl Default for NullableIndexMap +where + K: Eq + Hash + Clone, + V: Clone, +{ + fn default() -> Self { + Self { + map: IndexMap::default(), + null_val: None, + } + } +} + +impl<'py, K, V> FromPyObject<'py> for NullableIndexMap +where + K: IntoPy + FromPyObject<'py> + Eq + Hash + Clone, + V: IntoPy + FromPyObject<'py> + Clone, +{ + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let map: IndexMap, V, RandomState> = ob.extract()?; + let mut null_val: Option = None; + let filtered = map + .into_iter() + .filter_map(|(key, value)| match (key, value) { + (Some(key), value) => Some((key, value)), + (None, value) => { + null_val = Some(value); + None + } + }); + Ok(Self { + map: filtered.collect(), + null_val, + }) + } +} + +impl IntoPy for NullableIndexMap +where + K: IntoPy + Eq + Hash + Clone, + V: IntoPy + Clone, +{ + fn into_py(self, py: Python<'_>) -> PyObject { + let map_object = self.map.into_py(py); + let bound_map_obj = map_object.bind(py); + let downcast_dict: &Bound = bound_map_obj.downcast().unwrap(); + if let Some(null_val) = self.null_val { + downcast_dict + .set_item(py.None(), null_val.into_py(py)) + .unwrap(); + } + map_object + } +} + +impl ToPyObject for NullableIndexMap +where + K: ToPyObject + Eq + Hash + Clone, + V: ToPyObject + Clone, +{ + fn to_object(&self, py: Python<'_>) -> PyObject { + let map_object = self.map.to_object(py); + let bound_map_obj = map_object.bind(py); + let downcast_dict: &Bound = bound_map_obj.downcast().unwrap(); + if let Some(null_val) = &self.null_val { + downcast_dict + .set_item(py.None(), null_val.to_object(py)) + .unwrap(); + } + map_object + } +} diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 72f0d759099a..6af99ff04a8d 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -19,8 +19,8 @@ use qiskit_accelerate::{ isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, pauli_exp_val::pauli_expval, results::results, sabre::sabre, sampled_exp_val::sampled_exp_val, sparse_pauli_op::sparse_pauli_op, stochastic_swap::stochastic_swap, synthesis::synthesis, - two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, - vf2_layout::vf2_layout, + target_transpiler::target, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, + utils::utils, vf2_layout::vf2_layout, }; #[pymodule] @@ -42,6 +42,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(sampled_exp_val))?; m.add_wrapped(wrap_pymodule!(sparse_pauli_op))?; m.add_wrapped(wrap_pymodule!(stochastic_swap))?; + m.add_wrapped(wrap_pymodule!(target))?; m.add_wrapped(wrap_pymodule!(two_qubit_decompose))?; m.add_wrapped(wrap_pymodule!(uc_gate))?; m.add_wrapped(wrap_pymodule!(utils))?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index b470d48f9111..d88261cad209 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -78,6 +78,7 @@ sys.modules["qiskit._accelerate.sampled_exp_val"] = _accelerate.sampled_exp_val sys.modules["qiskit._accelerate.sparse_pauli_op"] = _accelerate.sparse_pauli_op sys.modules["qiskit._accelerate.stochastic_swap"] = _accelerate.stochastic_swap +sys.modules["qiskit._accelerate.target"] = _accelerate.target sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose sys.modules["qiskit._accelerate.vf2_layout"] = _accelerate.vf2_layout sys.modules["qiskit._accelerate.synthesis.permutation"] = _accelerate.synthesis.permutation diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 8805deece50e..001e8020962b 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -23,7 +23,6 @@ from typing import Optional, List, Any from collections.abc import Mapping -from collections import defaultdict import datetime import io import logging @@ -31,6 +30,12 @@ import rustworkx as rx +# import target class from the rust side +from qiskit._accelerate.target import ( + BaseTarget, + BaseInstructionProperties, +) + from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.gate import Gate @@ -51,11 +56,10 @@ from qiskit.providers.backend import QubitProperties # pylint: disable=unused-import from qiskit.providers.models.backendproperties import BackendProperties - logger = logging.getLogger(__name__) -class InstructionProperties: +class InstructionProperties(BaseInstructionProperties): """A representation of the properties of a gate implementation. This class provides the optional properties that a backend can provide @@ -65,12 +69,25 @@ class InstructionProperties: custom attributes for those custom/additional properties by the backend. """ - __slots__ = ("duration", "error", "_calibration") + __slots__ = [ + "_calibration", + ] + + def __new__( # pylint: disable=keyword-arg-before-vararg + cls, + duration=None, # pylint: disable=keyword-arg-before-vararg + error=None, # pylint: disable=keyword-arg-before-vararg + *args, # pylint: disable=unused-argument + **kwargs, # pylint: disable=unused-argument + ): + return super(InstructionProperties, cls).__new__( # pylint: disable=too-many-function-args + cls, duration, error + ) def __init__( self, - duration: float | None = None, - error: float | None = None, + duration: float | None = None, # pylint: disable=unused-argument + error: float | None = None, # pylint: disable=unused-argument calibration: Schedule | ScheduleBlock | CalibrationEntry | None = None, ): """Create a new ``InstructionProperties`` object @@ -82,10 +99,8 @@ def __init__( set of qubits. calibration: The pulse representation of the instruction. """ + super().__init__() self._calibration: CalibrationEntry | None = None - - self.duration = duration - self.error = error self.calibration = calibration @property @@ -135,8 +150,16 @@ def __repr__(self): f", calibration={self._calibration})" ) + def __getstate__(self) -> tuple: + return (super().__getstate__(), self.calibration, self._calibration) + + def __setstate__(self, state: tuple): + super().__setstate__(state[0]) + self.calibration = state[1] + self._calibration = state[2] + -class Target(Mapping): +class Target(BaseTarget): """ The intent of the ``Target`` object is to inform Qiskit's compiler about the constraints of a particular backend so the compiler can compile an @@ -221,37 +244,25 @@ class Target(Mapping): """ __slots__ = ( - "num_qubits", "_gate_map", - "_gate_name_map", - "_qarg_gate_map", - "description", "_coupling_graph", "_instruction_durations", "_instruction_schedule_map", - "dt", - "granularity", - "min_length", - "pulse_alignment", - "acquire_alignment", - "_non_global_basis", - "_non_global_strict_basis", - "qubit_properties", - "_global_operations", - "concurrent_measurements", ) - def __init__( - self, - description=None, - num_qubits=0, - dt=None, - granularity=1, - min_length=1, - pulse_alignment=1, - acquire_alignment=1, - qubit_properties=None, - concurrent_measurements=None, + def __new__( # pylint: disable=keyword-arg-before-vararg + cls, + description: str | None = None, + num_qubits: int = 0, + dt: float | None = None, + granularity: int = 1, + min_length: int = 1, + pulse_alignment: int = 1, + acquire_alignment: int = 1, + qubit_properties: list | None = None, + concurrent_measurements: list | None = None, + *args, # pylint: disable=unused-argument disable=keyword-arg-before-vararg + **kwargs, # pylint: disable=unused-argument ): """ Create a new ``Target`` object @@ -295,37 +306,38 @@ def __init__( defined and the value of ``num_qubits`` differs from the length of ``qubit_properties``. """ - self.num_qubits = num_qubits - # A mapping of gate name -> gate instance - self._gate_name_map = {} + if description is not None: + description = str(description) + return super(Target, cls).__new__( # pylint: disable=too-many-function-args + cls, + description, + num_qubits, + dt, + granularity, + min_length, + pulse_alignment, + acquire_alignment, + qubit_properties, + concurrent_measurements, + ) + + def __init__( + self, + description=None, # pylint: disable=unused-argument + num_qubits=0, # pylint: disable=unused-argument + dt=None, # pylint: disable=unused-argument + granularity=1, # pylint: disable=unused-argument + min_length=1, # pylint: disable=unused-argument + pulse_alignment=1, # pylint: disable=unused-argument + acquire_alignment=1, # pylint: disable=unused-argument + qubit_properties=None, # pylint: disable=unused-argument + concurrent_measurements=None, # pylint: disable=unused-argument + ): # A nested mapping of gate name -> qargs -> properties self._gate_map = {} - # A mapping of number of qubits to set of op names which are global - self._global_operations = defaultdict(set) - # A mapping of qarg -> set(gate name) - self._qarg_gate_map = defaultdict(set) - self.dt = dt - self.description = description self._coupling_graph = None self._instruction_durations = None self._instruction_schedule_map = None - self.granularity = granularity - self.min_length = min_length - self.pulse_alignment = pulse_alignment - self.acquire_alignment = acquire_alignment - self._non_global_basis = None - self._non_global_strict_basis = None - if qubit_properties is not None: - if not self.num_qubits: - self.num_qubits = len(qubit_properties) - else: - if self.num_qubits != len(qubit_properties): - raise ValueError( - "The value of num_qubits specified does not match the " - "length of the input qubit_properties list" - ) - self.qubit_properties = qubit_properties - self.concurrent_measurements = concurrent_measurements def add_instruction(self, instruction, properties=None, name=None): """Add a new instruction to the :class:`~qiskit.transpiler.Target` @@ -408,35 +420,15 @@ def add_instruction(self, instruction, properties=None, name=None): "An instruction added globally by class can't have properties set." ) instruction_name = name - if properties is None: + if properties is None or is_class: properties = {None: None} if instruction_name in self._gate_map: raise AttributeError(f"Instruction {instruction_name} is already in the target") - self._gate_name_map[instruction_name] = instruction - if is_class: - qargs_val = {None: None} - else: - if None in properties: - self._global_operations[instruction.num_qubits].add(instruction_name) - qargs_val = {} - for qarg in properties: - if qarg is not None and len(qarg) != instruction.num_qubits: - raise TranspilerError( - f"The number of qubits for {instruction} does not match the number " - f"of qubits in the properties dictionary: {qarg}" - ) - if qarg is not None: - self.num_qubits = max( - self.num_qubits if self.num_qubits is not None else 0, max(qarg) + 1 - ) - qargs_val[qarg] = properties[qarg] - self._qarg_gate_map[qarg].add(instruction_name) - self._gate_map[instruction_name] = qargs_val + super().add_instruction(instruction, instruction_name, properties) + self._gate_map[instruction_name] = properties self._coupling_graph = None self._instruction_durations = None self._instruction_schedule_map = None - self._non_global_basis = None - self._non_global_strict_basis = None def update_instruction_properties(self, instruction, qargs, properties): """Update the property object for an instruction qarg pair already in the Target @@ -448,10 +440,7 @@ def update_instruction_properties(self, instruction, qargs, properties): Raises: KeyError: If ``instruction`` or ``qarg`` are not in the target """ - if instruction not in self._gate_map: - raise KeyError(f"Provided instruction: '{instruction}' not in this Target") - if qargs not in self._gate_map[instruction]: - raise KeyError(f"Provided qarg: '{qargs}' not in this Target for {instruction}") + super().update_instruction_properties(instruction, qargs, properties) self._gate_map[instruction][qargs] = properties self._instruction_durations = None self._instruction_schedule_map = None @@ -574,14 +563,6 @@ def update_from_instruction_schedule_map(self, inst_map, inst_name_map=None, err continue self.update_instruction_properties(inst_name, qargs, prop) - @property - def qargs(self): - """The set of qargs in the target.""" - qargs = set(self._qarg_gate_map) - if len(qargs) == 1 and next(iter(qargs)) is None: - return None - return qargs - def qargs_for_operation_name(self, operation): """Get the qargs for a given operation name @@ -643,236 +624,6 @@ def instruction_schedule_map(self): self._instruction_schedule_map = out_inst_schedule_map return out_inst_schedule_map - def operation_from_name(self, instruction): - """Get the operation class object for a given name - - Args: - instruction (str): The instruction name to get the - :class:`~qiskit.circuit.Instruction` instance for - Returns: - qiskit.circuit.Instruction: The Instruction instance corresponding to the - name. This also can also be the class for globally defined variable with - operations. - """ - return self._gate_name_map[instruction] - - def operations_for_qargs(self, qargs): - """Get the operation class object for a specified qargs tuple - - Args: - qargs (tuple): A qargs tuple of the qubits to get the gates that apply - to it. For example, ``(0,)`` will return the set of all - instructions that apply to qubit 0. If set to ``None`` this will - return any globally defined operations in the target. - Returns: - list: The list of :class:`~qiskit.circuit.Instruction` instances - that apply to the specified qarg. This may also be a class if - a variable width operation is globally defined. - - Raises: - KeyError: If qargs is not in target - """ - if qargs is not None and any(x not in range(0, self.num_qubits) for x in qargs): - raise KeyError(f"{qargs} not in target.") - res = [self._gate_name_map[x] for x in self._qarg_gate_map[qargs]] - if qargs is not None: - res += self._global_operations.get(len(qargs), []) - for op in self._gate_name_map.values(): - if inspect.isclass(op): - res.append(op) - if not res: - raise KeyError(f"{qargs} not in target.") - return list(res) - - def operation_names_for_qargs(self, qargs): - """Get the operation names for a specified qargs tuple - - Args: - qargs (tuple): A ``qargs`` tuple of the qubits to get the gates that apply - to it. For example, ``(0,)`` will return the set of all - instructions that apply to qubit 0. If set to ``None`` this will - return the names for any globally defined operations in the target. - Returns: - set: The set of operation names that apply to the specified ``qargs``. - - Raises: - KeyError: If ``qargs`` is not in target - """ - # if num_qubits == 0, we will return globally defined operations - if self.num_qubits == 0 or self.num_qubits is None: - qargs = None - if qargs is not None and any(x not in range(0, self.num_qubits) for x in qargs): - raise KeyError(f"{qargs} not in target.") - res = self._qarg_gate_map.get(qargs, set()) - if qargs is not None: - res.update(self._global_operations.get(len(qargs), set())) - for name, op in self._gate_name_map.items(): - if inspect.isclass(op): - res.add(name) - if not res: - raise KeyError(f"{qargs} not in target.") - return res - - def instruction_supported( - self, operation_name=None, qargs=None, operation_class=None, parameters=None - ): - """Return whether the instruction (operation + qubits) is supported by the target - - Args: - operation_name (str): The name of the operation for the instruction. Either - this or ``operation_class`` must be specified, if both are specified - ``operation_class`` will take priority and this argument will be ignored. - qargs (tuple): The tuple of qubit indices for the instruction. If this is - not specified then this method will return ``True`` if the specified - operation is supported on any qubits. The typical application will - always have this set (otherwise it's the same as just checking if the - target contains the operation). Normally you would not set this argument - if you wanted to check more generally that the target supports an operation - with the ``parameters`` on any qubits. - operation_class (Type[qiskit.circuit.Instruction]): The operation class to check whether - the target supports a particular operation by class rather - than by name. This lookup is more expensive as it needs to - iterate over all operations in the target instead of just a - single lookup. If this is specified it will supersede the - ``operation_name`` argument. The typical use case for this - operation is to check whether a specific variant of an operation - is supported on the backend. For example, if you wanted to - check whether a :class:`~.RXGate` was supported on a specific - qubit with a fixed angle. That fixed angle variant will - typically have a name different from the object's - :attr:`~.Instruction.name` attribute (``"rx"``) in the target. - This can be used to check if any instances of the class are - available in such a case. - parameters (list): A list of parameters to check if the target - supports them on the specified qubits. If the instruction - supports the parameter values specified in the list on the - operation and qargs specified this will return ``True`` but - if the parameters are not supported on the specified - instruction it will return ``False``. If this argument is not - specified this method will return ``True`` if the instruction - is supported independent of the instruction parameters. If - specified with any :class:`~.Parameter` objects in the list, - that entry will be treated as supporting any value, however parameter names - will not be checked (for example if an operation in the target - is listed as parameterized with ``"theta"`` and ``"phi"`` is - passed into this function that will return ``True``). For - example, if called with:: - - parameters = [Parameter("theta")] - target.instruction_supported("rx", (0,), parameters=parameters) - - will return ``True`` if an :class:`~.RXGate` is supported on qubit 0 - that will accept any parameter. If you need to check for a fixed numeric - value parameter this argument is typically paired with the ``operation_class`` - argument. For example:: - - target.instruction_supported("rx", (0,), RXGate, parameters=[pi / 4]) - - will return ``True`` if an RXGate(pi/4) exists on qubit 0. - - Returns: - bool: Returns ``True`` if the instruction is supported and ``False`` if it isn't. - - """ - - def check_obj_params(parameters, obj): - for index, param in enumerate(parameters): - if isinstance(param, Parameter) and not isinstance(obj.params[index], Parameter): - return False - if param != obj.params[index] and not isinstance(obj.params[index], Parameter): - return False - return True - - # Handle case where num_qubits is None by always checking globally supported operations - if self.num_qubits is None: - qargs = None - # Case a list if passed in by mistake - if qargs is not None: - qargs = tuple(qargs) - if operation_class is not None: - for op_name, obj in self._gate_name_map.items(): - if inspect.isclass(obj): - if obj != operation_class: - continue - # If no qargs operation class is supported - if qargs is None: - return True - # If qargs set then validate no duplicates and all indices are valid on device - elif all(qarg <= self.num_qubits for qarg in qargs) and len(set(qargs)) == len( - qargs - ): - return True - else: - return False - - if isinstance(obj, operation_class): - if parameters is not None: - if len(parameters) != len(obj.params): - continue - if not check_obj_params(parameters, obj): - continue - if qargs is None: - return True - if qargs in self._gate_map[op_name]: - return True - if self._gate_map[op_name] is None or None in self._gate_map[op_name]: - return obj.num_qubits == len(qargs) and all( - x < self.num_qubits for x in qargs - ) - return False - if operation_name in self._gate_map: - if parameters is not None: - obj = self._gate_name_map[operation_name] - if inspect.isclass(obj): - # The parameters argument was set and the operation_name specified is - # defined as a globally supported class in the target. This means - # there is no available validation (including whether the specified - # operation supports parameters), the returned value will not factor - # in the argument `parameters`, - - # If no qargs a operation class is supported - if qargs is None: - return True - # If qargs set then validate no duplicates and all indices are valid on device - elif all(qarg <= self.num_qubits for qarg in qargs) and len(set(qargs)) == len( - qargs - ): - return True - else: - return False - if len(parameters) != len(obj.params): - return False - for index, param in enumerate(parameters): - matching_param = False - if isinstance(obj.params[index], Parameter): - matching_param = True - elif param == obj.params[index]: - matching_param = True - if not matching_param: - return False - return True - if qargs is None: - return True - if qargs in self._gate_map[operation_name]: - return True - if self._gate_map[operation_name] is None or None in self._gate_map[operation_name]: - obj = self._gate_name_map[operation_name] - if inspect.isclass(obj): - if qargs is None: - return True - # If qargs set then validate no duplicates and all indices are valid on device - elif all(qarg <= self.num_qubits for qarg in qargs) and len(set(qargs)) == len( - qargs - ): - return True - else: - return False - else: - return self._gate_name_map[operation_name].num_qubits == len(qargs) and all( - x < self.num_qubits for x in qargs - ) - return False - def has_calibration( self, operation_name: str, @@ -927,11 +678,6 @@ def operation_names(self): """Get the operation names in the target.""" return self._gate_map.keys() - @property - def operations(self): - """Get the operation class objects in the target.""" - return list(self._gate_name_map.values()) - @property def instructions(self): """Get the list of tuples ``(:class:`~qiskit.circuit.Instruction`, (qargs))`` @@ -994,25 +740,30 @@ def _build_coupling_graph(self): for gate, qarg_map in self._gate_map.items(): if qarg_map is None: if self._gate_name_map[gate].num_qubits == 2: - self._coupling_graph = None + self._coupling_graph = None # pylint: disable=attribute-defined-outside-init return continue for qarg, properties in qarg_map.items(): if qarg is None: - if self._gate_name_map[gate].num_qubits == 2: + if self.operation_from_name(gate).num_qubits == 2: self._coupling_graph = None return continue if len(qarg) == 1: - self._coupling_graph[qarg[0]] = properties + self._coupling_graph[qarg[0]] = ( + properties # pylint: disable=attribute-defined-outside-init + ) elif len(qarg) == 2: try: edge_data = self._coupling_graph.get_edge_data(*qarg) edge_data[gate] = properties except rx.NoEdgeBetweenNodes: self._coupling_graph.add_edge(*qarg, {gate: properties}) - if self._coupling_graph.num_edges() == 0 and any(x is None for x in self._qarg_gate_map): - self._coupling_graph = None + qargs = self.qargs + if self._coupling_graph.num_edges() == 0 and ( + qargs is None or any(x is None for x in qargs) + ): + self._coupling_graph = None # pylint: disable=attribute-defined-outside-init def build_coupling_map(self, two_q_gate=None, filter_idle_qubits=False): """Get a :class:`~qiskit.transpiler.CouplingMap` from this target. @@ -1059,7 +810,7 @@ def build_coupling_map(self, two_q_gate=None, filter_idle_qubits=False): if two_q_gate is not None: coupling_graph = rx.PyDiGraph(multigraph=False) coupling_graph.add_nodes_from([None] * self.num_qubits) - for qargs, properties in self._gate_map[two_q_gate].items(): + for qargs, properties in self[two_q_gate].items(): if len(qargs) != 2: raise ValueError( f"Specified two_q_gate: {two_q_gate} is not a 2 qubit instruction" @@ -1090,70 +841,19 @@ def _filter_coupling_graph(self): graph.remove_nodes_from(list(to_remove)) return graph - @property - def physical_qubits(self): - """Returns a sorted list of physical_qubits""" - return list(range(self.num_qubits)) - - def get_non_global_operation_names(self, strict_direction=False): - """Return the non-global operation names for the target - - The non-global operations are those in the target which don't apply - on all qubits (for single qubit operations) or all multi-qubit qargs - (for multi-qubit operations). - - Args: - strict_direction (bool): If set to ``True`` the multi-qubit - operations considered as non-global respect the strict - direction (or order of qubits in the qargs is significant). For - example, if ``cx`` is defined on ``(0, 1)`` and ``ecr`` is - defined over ``(1, 0)`` by default neither would be considered - non-global, but if ``strict_direction`` is set ``True`` both - ``cx`` and ``ecr`` would be returned. - - Returns: - List[str]: A list of operation names for operations that aren't global in this target - """ - if strict_direction: - if self._non_global_strict_basis is not None: - return self._non_global_strict_basis - search_set = self._qarg_gate_map.keys() - else: - if self._non_global_basis is not None: - return self._non_global_basis - - search_set = { - frozenset(qarg) - for qarg in self._qarg_gate_map - if qarg is not None and len(qarg) != 1 - } - incomplete_basis_gates = [] - size_dict = defaultdict(int) - size_dict[1] = self.num_qubits - for qarg in search_set: - if qarg is None or len(qarg) == 1: - continue - size_dict[len(qarg)] += 1 - for inst, qargs in self._gate_map.items(): - qarg_sample = next(iter(qargs)) - if qarg_sample is None: - continue - if not strict_direction: - qargs = {frozenset(qarg) for qarg in qargs} - if len(qargs) != size_dict[len(qarg_sample)]: - incomplete_basis_gates.append(inst) - if strict_direction: - self._non_global_strict_basis = incomplete_basis_gates - else: - self._non_global_basis = incomplete_basis_gates - return incomplete_basis_gates - def __iter__(self): return iter(self._gate_map) def __getitem__(self, key): return self._gate_map[key] + def get(self, key, default=None): + """Gets an item from the Target. If not found return a provided default or `None`.""" + try: + return self[key] + except KeyError: + return default + def __len__(self): return len(self._gate_map) @@ -1161,12 +861,15 @@ def __contains__(self, item): return item in self._gate_map def keys(self): + """Return the keys (operation_names) of the Target""" return self._gate_map.keys() def values(self): + """Return the Property Map (qargs -> InstructionProperties) of every instruction in the Target""" return self._gate_map.values() def items(self): + """Returns pairs of Gate names and its property map (str, dict[tuple, InstructionProperties])""" return self._gate_map.items() def __str__(self): @@ -1188,10 +891,10 @@ def __str__(self): prop_str_pieces = [f"\t\t{qarg}:\n"] duration = getattr(props, "duration", None) if duration is not None: - prop_str_pieces.append(f"\t\t\tDuration: {duration} sec.\n") + prop_str_pieces.append(f"\t\t\tDuration: {duration:g} sec.\n") error = getattr(props, "error", None) if error is not None: - prop_str_pieces.append(f"\t\t\tError Rate: {error}\n") + prop_str_pieces.append(f"\t\t\tError Rate: {error:g}\n") schedule = getattr(props, "_calibration", None) if schedule is not None: prop_str_pieces.append("\t\t\tWith pulse schedule calibration\n") @@ -1205,6 +908,22 @@ def __str__(self): output.write("".join(prop_str_pieces)) return output.getvalue() + def __getstate__(self) -> dict: + return { + "_gate_map": self._gate_map, + "coupling_graph": self._coupling_graph, + "instruction_durations": self._instruction_durations, + "instruction_schedule_map": self._instruction_schedule_map, + "base": super().__getstate__(), + } + + def __setstate__(self, state: tuple): + self._gate_map = state["_gate_map"] + self._coupling_graph = state["coupling_graph"] + self._instruction_durations = state["instruction_durations"] + self._instruction_schedule_map = state["instruction_schedule_map"] + super().__setstate__(state["base"]) + @classmethod def from_configuration( cls, @@ -1442,6 +1161,9 @@ def from_configuration( return target +Mapping.register(Target) + + def target_to_backend_properties(target: Target): """Convert a :class:`~.Target` object into a legacy :class:`~.BackendProperties`"""