diff --git a/Cargo.lock b/Cargo.lock index 6ce26b3baea5..125ce6abe876 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1392,9 +1392,9 @@ checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "rustworkx-core" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2b9aa5926b35dd3029530aef27eac0926b544c78f8e8f1aad4d37854b132fe9" +checksum = "ef8108bdaf5b590d2ea261c6ca9b1795cbf253d0733b2e209b7990c95ed23843" dependencies = [ "ahash 0.8.11", "fixedbitset", @@ -1527,18 +1527,18 @@ checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", diff --git a/crates/accelerate/src/synthesis/permutation/mod.rs b/crates/accelerate/src/synthesis/permutation/mod.rs index bf0ff97848f2..0e3900a4b8bf 100644 --- a/crates/accelerate/src/synthesis/permutation/mod.rs +++ b/crates/accelerate/src/synthesis/permutation/mod.rs @@ -59,10 +59,34 @@ pub fn _synth_permutation_basic(py: Python, pattern: PyArrayLike1) -> PyRes ) } +#[pyfunction] +#[pyo3(signature = (pattern))] +fn _synth_permutation_acg(py: Python, pattern: PyArrayLike1) -> PyResult { + let inverted = utils::invert(&pattern.as_array()); + let view = inverted.view(); + let num_qubits = view.len(); + let cycles = utils::pattern_to_cycles(&view); + let swaps = utils::decompose_cycles(&cycles); + + CircuitData::from_standard_gates( + py, + num_qubits as u32, + swaps.iter().map(|(i, j)| { + ( + StandardGate::SwapGate, + smallvec![], + smallvec![Qubit(*i as u32), Qubit(*j as u32)], + ) + }), + Param::Float(0.0), + ) +} + #[pymodule] pub fn permutation(m: &Bound) -> PyResult<()> { m.add_function(wrap_pyfunction!(_validate_permutation, m)?)?; m.add_function(wrap_pyfunction!(_inverse_pattern, m)?)?; m.add_function(wrap_pyfunction!(_synth_permutation_basic, m)?)?; + m.add_function(wrap_pyfunction!(_synth_permutation_acg, m)?)?; Ok(()) } diff --git a/crates/accelerate/src/synthesis/permutation/utils.rs b/crates/accelerate/src/synthesis/permutation/utils.rs index a78088bfbfa9..620ce4628741 100644 --- a/crates/accelerate/src/synthesis/permutation/utils.rs +++ b/crates/accelerate/src/synthesis/permutation/utils.rs @@ -15,6 +15,8 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use std::vec::Vec; +use qiskit_circuit::slice::{PySequenceIndex, PySequenceIndexError, SequenceIndex}; + pub fn validate_permutation(pattern: &ArrayView1) -> PyResult<()> { let n = pattern.len(); let mut seen: Vec = vec![false; n]; @@ -63,19 +65,19 @@ pub fn invert(pattern: &ArrayView1) -> Array1 { /// then this creates a quantum circuit with ``m-1`` SWAPs (and of depth ``m-1``); /// if the input permutation consists of several disjoint cycles, then each cycle /// is essentially treated independently. -pub fn get_ordered_swap(pattern: &ArrayView1) -> Vec<(i64, i64)> { +pub fn get_ordered_swap(pattern: &ArrayView1) -> Vec<(usize, usize)> { let mut permutation: Vec = pattern.iter().map(|&x| x as usize).collect(); let mut index_map = invert(pattern); let n = permutation.len(); - let mut swaps: Vec<(i64, i64)> = Vec::with_capacity(n); + let mut swaps: Vec<(usize, usize)> = Vec::with_capacity(n); for ii in 0..n { let val = permutation[ii]; if val == ii { continue; } let jj = index_map[ii]; - swaps.push((ii as i64, jj as i64)); + swaps.push((ii, jj)); (permutation[ii], permutation[jj]) = (permutation[jj], permutation[ii]); index_map[val] = jj; index_map[ii] = ii; @@ -84,3 +86,68 @@ pub fn get_ordered_swap(pattern: &ArrayView1) -> Vec<(i64, i64)> { swaps[..].reverse(); swaps } + +/// Explore cycles in a permutation pattern. This is probably best explained in an +/// example: let a pattern be [1, 2, 3, 0, 4, 6, 5], then it contains the two +/// cycles [1, 2, 3, 0] and [6, 5]. The index [4] does not perform a permutation and does +/// therefore not create a cycle. +pub fn pattern_to_cycles(pattern: &ArrayView1) -> Vec> { + // vector keeping track of which elements in the permutation pattern have been visited + let mut explored: Vec = vec![false; pattern.len()]; + + // vector to store the cycles + let mut cycles: Vec> = Vec::new(); + + for pos in pattern { + let mut cycle: Vec = Vec::new(); + + // follow the cycle until we reached an entry we saw before + let mut i = *pos; + while !explored[i] { + cycle.push(i); + explored[i] = true; + i = pattern[i]; + } + // cycles must have more than 1 element + if cycle.len() > 1 { + cycles.push(cycle); + } + } + + cycles +} + +/// Periodic (or Python-like) access to a vector. +/// Util used below in ``decompose_cycles``. +#[inline] +fn pget(vec: &Vec, index: isize) -> Result { + let SequenceIndex::Int(wrapped) = PySequenceIndex::Int(index).with_len(vec.len())? else {unreachable!()}; + Ok(vec[wrapped]) +} + +/// Given a disjoint cycle decomposition of a permutation pattern (see the function +/// ``pattern_to_cycles``), decomposes every cycle into a series of SWAPs to implement it. +/// In combination with ``pattern_to_cycle``, this function allows to implement a +/// full permutation pattern by applying SWAP gates on the returned index-pairs. +pub fn decompose_cycles(cycles: &Vec>) -> Vec<(usize, usize)> { + let mut swaps: Vec<(usize, usize)> = Vec::new(); + + for cycle in cycles { + let length = cycle.len() as isize; + + for idx in 0..(length - 1) / 2 { + swaps.push(( + pget(cycle, idx - 1).unwrap(), + pget(cycle, length - 3 - idx).unwrap(), + )); + } + for idx in 0..length / 2 { + swaps.push(( + pget(cycle, idx - 1).unwrap(), + pget(cycle, length - 2 - idx).unwrap(), + )); + } + } + + swaps +} diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 10e0691021a1..f10911cc440f 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -18,7 +18,7 @@ use crate::circuit_instruction::{ convert_py_to_operation_type, CircuitInstruction, ExtraInstructionAttributes, OperationInput, PackedInstruction, }; -use crate::imports::{BUILTIN_LIST, QUBIT}; +use crate::imports::{BUILTIN_LIST, DEEPCOPY, QUBIT}; use crate::interner::{IndexedInterner, Interner, InternerKey}; use crate::operations::{Operation, OperationType, Param, StandardGate}; use crate::parameter_table::{ParamEntry, ParamTable, GLOBAL_PHASE_INDEX}; @@ -488,20 +488,17 @@ impl CircuitData { res.param_table.clone_from(&self.param_table); if deepcopy { - let deepcopy = py - .import_bound(intern!(py, "copy"))? - .getattr(intern!(py, "deepcopy"))?; for inst in &mut res.data { match &mut inst.op { OperationType::Standard(_) => {} OperationType::Gate(ref mut op) => { - op.gate = deepcopy.call1((&op.gate,))?.unbind(); + op.gate = DEEPCOPY.get_bound(py).call1((&op.gate,))?.unbind(); } OperationType::Instruction(ref mut op) => { - op.instruction = deepcopy.call1((&op.instruction,))?.unbind(); + op.instruction = DEEPCOPY.get_bound(py).call1((&op.instruction,))?.unbind(); } OperationType::Operation(ref mut op) => { - op.operation = deepcopy.call1((&op.operation,))?.unbind(); + op.operation = DEEPCOPY.get_bound(py).call1((&op.operation,))?.unbind(); } }; #[cfg(feature = "cache_pygates")] diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index ffa6bb0c652c..d6516722fbac 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -13,6 +13,7 @@ #[cfg(feature = "cache_pygates")] use std::cell::RefCell; +use numpy::IntoPyArray; use pyo3::basic::CompareOp; use pyo3::exceptions::{PyDeprecationWarning, PyValueError}; use pyo3::prelude::*; @@ -25,7 +26,9 @@ use crate::imports::{ SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, WARNINGS_WARN, }; use crate::interner::Index; -use crate::operations::{OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate}; +use crate::operations::{ + Operation, OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate, +}; /// These are extra mutable attributes for a circuit instruction's state. In general we don't /// typically deal with this in rust space and the majority of the time they're not used in Python @@ -407,6 +410,56 @@ impl CircuitInstruction { }) } + #[getter] + fn _raw_op(&self, py: Python) -> PyObject { + self.operation.clone().into_py(py) + } + + /// 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) + } + + #[getter] + fn get_params(&self, py: Python) -> PyObject { + self.params.to_object(py) + } + + #[getter] + fn matrix(&self, py: Python) -> Option { + let matrix = self.operation.matrix(&self.params); + matrix.map(|mat| mat.into_pyarray_bound(py).into()) + } + + #[getter] + fn label(&self) -> Option<&str> { + self.extra_attrs + .as_ref() + .and_then(|attrs| attrs.label.as_deref()) + } + + #[getter] + fn condition(&self, py: Python) -> Option { + self.extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref().map(|x| x.clone_ref(py))) + } + + #[getter] + fn duration(&self, py: Python) -> Option { + self.extra_attrs + .as_ref() + .and_then(|attrs| attrs.duration.as_ref().map(|x| x.clone_ref(py))) + } + + #[getter] + fn unit(&self) -> Option<&str> { + self.extra_attrs + .as_ref() + .and_then(|attrs| attrs.unit.as_deref()) + } + /// Creates a shallow copy with the given fields replaced. /// /// Returns: diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index c8b6a4c8b082..ffd7920a36fd 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -15,9 +15,11 @@ use crate::circuit_instruction::{ ExtraInstructionAttributes, }; use crate::operations::Operation; +use numpy::IntoPyArray; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; -use pyo3::{intern, PyObject, PyResult}; +use pyo3::{intern, IntoPy, PyObject, PyResult}; +use smallvec::smallvec; /// Parent class for DAGOpNode, DAGInNode, and DAGOutNode. #[pyclass(module = "qiskit._accelerate.circuit", subclass)] @@ -70,12 +72,19 @@ 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))] fn new( py: Python, - op: PyObject, + op: crate::circuit_instruction::OperationInput, 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 = @@ -110,34 +119,16 @@ impl DAGOpNode { } None => qargs.str()?.into_any(), }; - let res = convert_py_to_operation_type(py, op.clone_ref(py))?; - let extra_attrs = if res.label.is_some() - || res.duration.is_some() - || res.unit.is_some() - || res.condition.is_some() - { - Some(Box::new(ExtraInstructionAttributes { - label: res.label, - duration: res.duration, - unit: res.unit, - condition: res.condition, - })) - } else { - None - }; + 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: CircuitInstruction { - operation: res.operation, - qubits: qargs.unbind(), - clbits: cargs.unbind(), - params: res.params, - extra_attrs, - #[cfg(feature = "cache_pygates")] - py_op: Some(op), - }, + instruction, sort_key: sort_key.unbind(), }, DAGNode { _node_id: -1 }, @@ -219,6 +210,77 @@ impl DAGOpNode { self.instruction.operation.name().to_object(py) } + #[getter] + fn get_params(&self, py: Python) -> PyObject { + self.instruction.params.to_object(py) + } + + #[getter] + fn matrix(&self, py: Python) -> Option { + let matrix = self.instruction.operation.matrix(&self.instruction.params); + matrix.map(|mat| mat.into_pyarray_bound(py).into()) + } + + #[getter] + fn label(&self) -> Option<&str> { + self.instruction + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.label.as_deref()) + } + + #[getter] + fn condition(&self, py: Python) -> Option { + self.instruction + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.condition.as_ref().map(|x| x.clone_ref(py))) + } + + #[getter] + fn duration(&self, py: Python) -> Option { + self.instruction + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.duration.as_ref().map(|x| x.clone_ref(py))) + } + + #[getter] + fn unit(&self) -> Option<&str> { + self.instruction + .extra_attrs + .as_ref() + .and_then(|attrs| attrs.unit.as_deref()) + } + + #[setter] + fn set_label(&mut self, val: Option) { + match self.instruction.extra_attrs.as_mut() { + Some(attrs) => attrs.label = val, + None => { + if val.is_some() { + self.instruction.extra_attrs = Some(Box::new( + crate::circuit_instruction::ExtraInstructionAttributes { + label: val, + duration: None, + unit: None, + condition: None, + }, + )) + } + } + }; + if let Some(attrs) = &self.instruction.extra_attrs { + if attrs.label.is_none() + && attrs.duration.is_none() + && attrs.unit.is_none() + && attrs.condition.is_none() + { + self.instruction.extra_attrs = None; + } + } + } + /// Sets the Instruction name corresponding to the op for this node #[setter] fn set_name(&mut self, py: Python, new_name: PyObject) -> PyResult<()> { @@ -229,6 +291,11 @@ impl DAGOpNode { 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!( diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 53fee34f486e..554f553d9427 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -71,6 +71,7 @@ pub static SINGLETON_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonGate"); pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonControlledGate"); +pub static DEEPCOPY: ImportOnceCell = ImportOnceCell::new("copy", "deepcopy"); pub static WARNINGS_WARN: ImportOnceCell = ImportOnceCell::new("warnings", "warn"); diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index f4019753e258..c752c0af494a 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -13,7 +13,7 @@ use std::f64::consts::PI; use crate::circuit_data::CircuitData; -use crate::imports::{PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; +use crate::imports::{DEEPCOPY, PARAMETER_EXPRESSION, QUANTUM_CIRCUIT}; use crate::{gate_matrix, Qubit}; use ndarray::{aview2, Array2}; @@ -35,6 +35,17 @@ pub enum OperationType { Operation(PyOperation), } +impl IntoPy for OperationType { + fn into_py(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), + } + } +} + impl Operation for OperationType { fn name(&self) -> &str { match self { @@ -1455,6 +1466,16 @@ impl PyInstruction { 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 { @@ -1534,6 +1555,16 @@ impl PyGate { 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 { @@ -1626,6 +1657,16 @@ impl PyOperation { 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 { diff --git a/qiskit/converters/circuit_to_dag.py b/qiskit/converters/circuit_to_dag.py index b2c1df2a037b..88d9c72f1d61 100644 --- a/qiskit/converters/circuit_to_dag.py +++ b/qiskit/converters/circuit_to_dag.py @@ -13,7 +13,8 @@ """Helper function for converting a circuit to a dag""" import copy -from qiskit.dagcircuit.dagcircuit import DAGCircuit +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): @@ -93,10 +94,24 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord dagcircuit.add_creg(register) for instruction in circuit.data: - op = instruction.operation - if copy_operations: - op = copy.deepcopy(op) - dagcircuit.apply_operation_back(op, instruction.qubits, instruction.clbits, check=False) + 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.duration = circuit.duration dagcircuit.unit = circuit.unit diff --git a/qiskit/converters/dag_to_circuit.py b/qiskit/converters/dag_to_circuit.py index ede026c247c9..3667c2183eae 100644 --- a/qiskit/converters/dag_to_circuit.py +++ b/qiskit/converters/dag_to_circuit.py @@ -14,6 +14,7 @@ import copy from qiskit.circuit import QuantumCircuit, CircuitInstruction +from qiskit._accelerate.circuit import StandardGate def dag_to_circuit(dag, copy_operations=True): @@ -71,10 +72,24 @@ def dag_to_circuit(dag, copy_operations=True): circuit.calibrations = dag.calibrations for node in dag.topological_op_nodes(): - op = node.op - if copy_operations: - op = copy.deepcopy(op) - circuit._append(CircuitInstruction(op, node.qargs, node.cargs)) + 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.duration = dag.duration circuit.unit = dag.unit diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 944e2df625b0..a0d2c42be17e 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -717,6 +717,27 @@ def copy_empty_like(self, *, vars_mode: _VarsMode = "alike"): return target_dag + def _apply_op_node_back(self, node: DAGOpNode): + 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) + + node._node_id = self._multi_graph.add_node(node) + self._increment_op(node) + + # 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 (node.qargs, node.cargs, additional) + for bit in bits + ], + ) + def apply_operation_back( self, op: Operation, diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py index 7ccd5053e6e0..c89fe3b4e306 100644 --- a/qiskit/pulse/schedule.py +++ b/qiskit/pulse/schedule.py @@ -252,7 +252,7 @@ def children(self) -> tuple[tuple[int, "ScheduleComponent"], ...]: Notes: Nested schedules are returned as-is. If you want to collect only instructions, - use py:meth:`~Schedule.instructions` instead. + use :py:meth:`~Schedule.instructions` instead. Returns: A tuple, where each element is a two-tuple containing the initial @@ -490,7 +490,7 @@ def exclude( ) -> "Schedule": """Return a ``Schedule`` with only the instructions from this Schedule *failing* at least one of the provided filters. - This method is the complement of py:meth:`~self.filter`, so that:: + This method is the complement of :py:meth:`~Schedule.filter`, so that:: self.filter(args) | self.exclude(args) == self @@ -1300,7 +1300,7 @@ def exclude( ): """Return a new ``ScheduleBlock`` with only the instructions from this ``ScheduleBlock`` *failing* at least one of the provided filters. - This method is the complement of py:meth:`~self.filter`, so that:: + This method is the complement of :py:meth:`~ScheduleBlock.filter`, so that:: self.filter(args) + self.exclude(args) == self in terms of instructions included. diff --git a/qiskit/synthesis/permutation/permutation_full.py b/qiskit/synthesis/permutation/permutation_full.py index c280065c2a57..2fd892a0427e 100644 --- a/qiskit/synthesis/permutation/permutation_full.py +++ b/qiskit/synthesis/permutation/permutation_full.py @@ -16,11 +16,9 @@ import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit._accelerate.synthesis.permutation import _synth_permutation_basic -from .permutation_utils import ( - _inverse_pattern, - _pattern_to_cycles, - _decompose_cycles, +from qiskit._accelerate.synthesis.permutation import ( + _synth_permutation_basic, + _synth_permutation_acg, ) @@ -77,16 +75,4 @@ def synth_permutation_acg(pattern: list[int] | np.ndarray[int]) -> QuantumCircui *Routing Permutations on Graphs Via Matchings.*, `(Full paper) `_ """ - - num_qubits = len(pattern) - qc = QuantumCircuit(num_qubits) - - # invert pattern (Qiskit notation is opposite) - cur_pattern = _inverse_pattern(pattern) - cycles = _pattern_to_cycles(cur_pattern) - swaps = _decompose_cycles(cycles) - - for swap in swaps: - qc.swap(swap[0], swap[1]) - - return qc + return QuantumCircuit._from_circuit_data(_synth_permutation_acg(pattern)) diff --git a/qiskit/synthesis/permutation/permutation_utils.py b/qiskit/synthesis/permutation/permutation_utils.py index 4520e18f4d06..a8d18b8a8196 100644 --- a/qiskit/synthesis/permutation/permutation_utils.py +++ b/qiskit/synthesis/permutation/permutation_utils.py @@ -13,36 +13,4 @@ """Utility functions for handling permutations.""" # pylint: disable=unused-import -from qiskit._accelerate.synthesis.permutation import ( - _inverse_pattern, - _validate_permutation, -) - - -def _pattern_to_cycles(pattern): - """Given a permutation pattern, creates its disjoint cycle decomposition.""" - nq = len(pattern) - explored = [False] * nq - cycles = [] - for i in pattern: - cycle = [] - while not explored[i]: - cycle.append(i) - explored[i] = True - i = pattern[i] - if len(cycle) >= 2: - cycles.append(cycle) - return cycles - - -def _decompose_cycles(cycles): - """Given a disjoint cycle decomposition, decomposes every cycle into a SWAP - circuit of depth 2.""" - swap_list = [] - for cycle in cycles: - m = len(cycle) - for i in range((m - 1) // 2): - swap_list.append((cycle[i - 1], cycle[m - 3 - i])) - for i in range(m // 2): - swap_list.append((cycle[i - 1], cycle[m - 2 - i])) - return swap_list +from qiskit._accelerate.synthesis.permutation import _inverse_pattern, _validate_permutation diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 7db48d6d1395..ab7c5e04649f 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -32,17 +32,17 @@ from qiskit.transpiler import CouplingMap, Target from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError -from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagcircuit import DAGCircuit, DAGOpNode from qiskit.synthesis.one_qubit import one_qubit_decompose from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import _possible_decomposers from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer, XXEmbodiments from qiskit.synthesis.two_qubit.two_qubit_decompose import ( TwoQubitBasisDecomposer, TwoQubitWeylDecomposition, - GATE_NAME_MAP, ) from qiskit.quantum_info import Operator -from qiskit.circuit import ControlFlowOp, Gate, Parameter +from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES +from qiskit.circuit import Gate, Parameter from qiskit.circuit.library.standard_gates import ( iSwapGate, CXGate, @@ -50,6 +50,17 @@ RXXGate, RZXGate, ECRGate, + RXGate, + SXGate, + XGate, + RZGate, + UGate, + PhaseGate, + U1Gate, + U2Gate, + U3Gate, + RYGate, + RGate, ) from qiskit.transpiler.passes.synthesis import plugin from qiskit.transpiler.passes.optimization.optimize_1q_decomposition import ( @@ -60,6 +71,22 @@ from qiskit.exceptions import QiskitError +GATE_NAME_MAP = { + "cx": CXGate._standard_gate, + "rx": RXGate._standard_gate, + "sx": SXGate._standard_gate, + "x": XGate._standard_gate, + "rz": RZGate._standard_gate, + "u": UGate._standard_gate, + "p": PhaseGate._standard_gate, + "u1": U1Gate._standard_gate, + "u2": U2Gate._standard_gate, + "u3": U3Gate._standard_gate, + "ry": RYGate._standard_gate, + "r": RGate._standard_gate, +} + + KAK_GATE_NAMES = { "cx": CXGate(), "cz": CZGate(), @@ -479,7 +506,9 @@ def _run_main_loop( self, dag, qubit_indices, plugin_method, plugin_kwargs, default_method, default_kwargs ): """Inner loop for the optimizer, after all DAG-independent set-up has been completed.""" - for node in dag.op_nodes(ControlFlowOp): + for node in dag.op_nodes(): + if node.name not in CONTROL_FLOW_OP_NAMES: + continue node.op = node.op.replace_blocks( [ dag_to_circuit( @@ -502,9 +531,9 @@ def _run_main_loop( out_dag = dag.copy_empty_like() for node in dag.topological_op_nodes(): - if node.op.name == "unitary" and len(node.qargs) >= self._min_qubits: + if node.name == "unitary" and len(node.qargs) >= self._min_qubits: synth_dag = None - unitary = node.op.to_matrix() + unitary = node.matrix n_qubits = len(node.qargs) if ( plugin_method.max_qubits is not None and n_qubits > plugin_method.max_qubits @@ -519,35 +548,41 @@ def _run_main_loop( ) synth_dag = method.run(unitary, **kwargs) if synth_dag is None: - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + out_dag._apply_op_node_back(node) continue if isinstance(synth_dag, DAGCircuit): qubit_map = dict(zip(synth_dag.qubits, node.qargs)) for node in synth_dag.topological_op_nodes(): - out_dag.apply_operation_back( - node.op, (qubit_map[x] for x in node.qargs), check=False - ) + node.qargs = tuple(qubit_map[x] for x in node.qargs) + out_dag._apply_op_node_back(node) out_dag.global_phase += synth_dag.global_phase else: node_list, global_phase, gate = synth_dag qubits = node.qargs + user_gate_node = DAGOpNode(gate) for ( op_name, params, qargs, ) in node_list: if op_name == "USER_GATE": - op = gate + node = DAGOpNode( + user_gate_node._raw_op, + params=user_gate_node.params, + qargs=tuple(qubits[x] for x in qargs), + dag=out_dag, + ) else: - op = GATE_NAME_MAP[op_name](*params) - out_dag.apply_operation_back( - op, - (qubits[x] for x in qargs), - check=False, - ) + node = DAGOpNode( + GATE_NAME_MAP[op_name], + params=params, + qargs=tuple(qubits[x] for x in qargs), + dag=out_dag, + ) + out_dag._apply_op_node_back(node) out_dag.global_phase += global_phase else: - out_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False) + out_dag._apply_op_node_back(node) return out_dag @@ -1008,5 +1043,6 @@ 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) - out_dag.apply_operation_back(node.op, qubits, check=False) + node = DAGOpNode(node._raw_op, qargs=qubits, params=node.params) + out_dag._apply_op_node_back(node) return out_dag diff --git a/qiskit/transpiler/passes/utils/check_gate_direction.py b/qiskit/transpiler/passes/utils/check_gate_direction.py index 1ddfd40124b5..e797be95c4a1 100644 --- a/qiskit/transpiler/passes/utils/check_gate_direction.py +++ b/qiskit/transpiler/passes/utils/check_gate_direction.py @@ -12,7 +12,7 @@ """Check if the gates follow the right direction with respect to the coupling map.""" -from qiskit.circuit import ControlFlowOp +from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.converters import circuit_to_dag from qiskit.transpiler.basepasses import AnalysisPass @@ -39,7 +39,7 @@ def _coupling_map_visit(self, dag, wire_map, edges=None): edges = self.coupling_map.get_edges() # Don't include directives to avoid things like barrier, which are assumed always supported. for node in dag.op_nodes(include_directives=False): - if isinstance(node.op, ControlFlowOp): + if node.name in CONTROL_FLOW_OP_NAMES: for block in node.op.blocks: inner_wire_map = { inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits) @@ -57,7 +57,7 @@ def _coupling_map_visit(self, dag, wire_map, edges=None): def _target_visit(self, dag, wire_map): # Don't include directives to avoid things like barrier, which are assumed always supported. for node in dag.op_nodes(include_directives=False): - if isinstance(node.op, ControlFlowOp): + if node.name in CONTROL_FLOW_OP_NAMES: for block in node.op.blocks: inner_wire_map = { inner: wire_map[outer] for outer, inner in zip(node.qargs, block.qubits) @@ -65,7 +65,7 @@ def _target_visit(self, dag, wire_map): if not self._target_visit(circuit_to_dag(block), inner_wire_map): return False elif len(node.qargs) == 2 and not self.target.instruction_supported( - node.op.name, (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) + node.name, (wire_map[node.qargs[0]], wire_map[node.qargs[1]]) ): return False return True diff --git a/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml b/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml index 551ea9e918c6..d29089ef9491 100644 --- a/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml +++ b/releasenotes/notes/1.1/parameter_assignment_by_name_for_pulse_schedules-3a27bbbbf235fb9e.yaml @@ -1,7 +1,7 @@ --- features_pulse: - | - It is now possible to assign parameters to pulse :class:`.Schedule`and :class:`.ScheduleBlock` objects by specifying + It is now possible to assign parameters to pulse :class:`.Schedule` and :class:`.ScheduleBlock` objects by specifying the parameter name as a string. The parameter name can be used to assign values to all parameters within the `Schedule` or `ScheduleBlock` that have the same name. Moreover, the parameter name of a `ParameterVector` can be used to assign all values of the vector simultaneously (the list of values should therefore match the diff --git a/releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml b/releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml index f3f69ce5cb19..d1be6c450ee3 100644 --- a/releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml +++ b/releasenotes/notes/1.1/pauli-apply-layout-cdcbc1bce724a150.yaml @@ -3,29 +3,29 @@ features_quantum_info: - | Added a new :meth:`~.Pauli.apply_layout` method that is equivalent to :meth:`~.SparsePauliOp.apply_layout`. This method is used to apply - a :class:`~.TranspileLayout` layout from the transpiler to a :class:~.Pauli` + a :class:`~.TranspileLayout` layout from the transpiler to a :class:`~.Pauli` observable that was built for an input circuit. This enables working with :class:`~.BaseEstimator` / :class:`~.BaseEstimatorV2` implementations and local transpilation when the input is of type :class:`~.Pauli`. For example:: - from qiskit.circuit.library import RealAmplitudes - from qiskit.primitives import BackendEstimatorV2 - from qiskit.providers.fake_provider import GenericBackendV2 - from qiskit.quantum_info import Pauli - from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + from qiskit.circuit.library import RealAmplitudes + from qiskit.primitives import BackendEstimatorV2 + from qiskit.providers.fake_provider import GenericBackendV2 + from qiskit.quantum_info import Pauli + from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + + psi = RealAmplitudes(num_qubits=2, reps=2) + H1 = Pauli("XI") + backend = GenericBackendV2(num_qubits=7) + estimator = BackendEstimatorV2(backend=backend) + thetas = [0, 1, 1, 2, 3, 5] + pm = generate_preset_pass_manager(optimization_level=3, backend=backend) + transpiled_psi = pm.run(psi) + permuted_op = H1.apply_layout(transpiled_psi.layout) + res = estimator.run([(transpiled_psi, permuted_op, thetas)]).result() - psi = RealAmplitudes(num_qubits=2, reps=2) - H1 = Pauli("XI") - backend = GenericBackendV2(num_qubits=7) - estimator = BackendEstimatorV2(backend=backend) - thetas = [0, 1, 1, 2, 3, 5] - pm = generate_preset_pass_manager(optimization_level=3, backend=backend) - transpiled_psi = pm.run(psi) - permuted_op = H1.apply_layout(transpiled_psi.layout) - res = estimator.run([(transpiled_psi, permuted_op, thetas)]).result() - - where an input circuit is transpiled locally before it's passed to - :class:`~.BaseEstimator.run`. Transpilation expands the original - circuit from 2 to 7 qubits (the size of ``backend``) and permutes its layout, - which is then applied to ``H1`` using :meth:`~.Pauli.apply_layout` - to reflect the transformations performed by ``pm.run()``. \ No newline at end of file + where an input circuit is transpiled locally before it's passed to + :class:`~.BaseEstimator.run`. Transpilation expands the original + circuit from 2 to 7 qubits (the size of ``backend``) and permutes its layout, + which is then applied to ``H1`` using :meth:`~.Pauli.apply_layout` + to reflect the transformations performed by ``pm.run()``. \ No newline at end of file diff --git a/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml index 6fa548d9245c..d0466b8f75d8 100644 --- a/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml +++ b/releasenotes/notes/outcome_bitstring_target_for_probabilities_dict-e53f524d115bbcfc.yaml @@ -1,10 +1,10 @@ --- features: - | - The :class:'.StabilizerState' class now has a new method - :meth:'~.StabilizerState.probabilities_dict_from_bitstring' allowing the + The :class:`.StabilizerState` class now has a new method + :meth:`~.StabilizerState.probabilities_dict_from_bitstring` allowing the user to pass single bitstring to measure an outcome for. Previouslly the - :meth:'~.StabilizerState.probabilities_dict' would be utilized and would + :meth:`~.StabilizerState.probabilities_dict` would be utilized and would at worst case calculate (2^n) number of probability calculations (depending on the state), even if a user wanted a single result. With this new method the user can calculate just the single outcome bitstring value a user passes diff --git a/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml b/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml new file mode 100644 index 000000000000..6bd6761e0355 --- /dev/null +++ b/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml @@ -0,0 +1,5 @@ +--- +upgrade_synthesis: + - | + Port :func:`.synth_permutation_acg`, used to synthesize qubit permutations, to Rust. + This produces an approximate 3x performance improvement on 1000 qubit circuits.