From 05d5b98304eecca05feb6a2aeb2411911455d42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Abad=20L=C3=B3pez?= <109400222+GuillermoAbadLopez@users.noreply.github.com> Date: Mon, 1 Jul 2024 22:50:06 +0200 Subject: [PATCH 1/5] Fix bad Sphinx syntax in several release notes and in API documentaiton (#12604) * Fix ' into ` * More typos on release notes and on API documentation * change `self.filter` for `Schedule.filter` * change `Schedule` -> `ScheduleBlock` in its `filter()` reference --- qiskit/pulse/schedule.py | 6 +-- ..._for_pulse_schedules-3a27bbbbf235fb9e.yaml | 2 +- .../pauli-apply-layout-cdcbc1bce724a150.yaml | 42 +++++++++---------- ...r_probabilities_dict-e53f524d115bbcfc.yaml | 6 +-- 4 files changed, 28 insertions(+), 28 deletions(-) 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/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 From d2ab4dfb480dbe77c42d01dc9a9c6d11cb9aa12c Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 2 Jul 2024 04:13:26 -0400 Subject: [PATCH 2/5] Enable avoiding Python operation creation in transpiler (#12692) * Avoid Python operation creation in transpiler Since #12459 accessing `node.op` in the transpiler eagerly creates a Python object on access. This is because we now are no longer storing a Python object internally and we need to rebuild the object to return the python object as expected by the api. This is causing a significant performance regression because of the extra overhead. The longer term goal is to move as much of the performance critical passes to operate in rust which will eliminate this overhead. But in the meantime we can mitigate the performance overhead by changing the Python access patterns to avoid the operation object creation. This commit adds some new getter methods to DAGOpNode to give access to the inner rust data so that we can avoid the extra overhead. As a proof of concept this updates the unitary synthesis pass in isolation. Doing this fixes the regression caused by #12459 for that pass. We can continue this migration for everything else in follow up PRs. This commit is mostly to establish the pattern and add the python space access methods. * Remove unused import * Add path to avoid StandardGate conversion in circuit_to_dag * Add fast path through dag_to_circuit --- crates/circuit/src/circuit_data.rs | 11 +- crates/circuit/src/circuit_instruction.rs | 55 +++++++- crates/circuit/src/dag_node.rs | 119 ++++++++++++++---- crates/circuit/src/imports.rs | 1 + crates/circuit/src/operations.rs | 43 ++++++- qiskit/converters/circuit_to_dag.py | 25 +++- qiskit/converters/dag_to_circuit.py | 23 +++- qiskit/dagcircuit/dagcircuit.py | 21 ++++ .../passes/synthesis/unitary_synthesis.py | 74 ++++++++--- .../passes/utils/check_gate_direction.py | 8 +- 10 files changed, 313 insertions(+), 67 deletions(-) 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 df15b4abb415..8935e72e0ad5 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 { @@ -1418,6 +1429,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 { @@ -1497,6 +1518,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 { @@ -1589,6 +1620,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/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 From b399ec30d69d8e0681e8dfab8f83816c77b96f37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:00:47 +0000 Subject: [PATCH 3/5] Bump rustworkx-core from 0.15.0 to 0.15.1 (#12708) Bumps [rustworkx-core](https://github.com/Qiskit/rustworkx) from 0.15.0 to 0.15.1. - [Release notes](https://github.com/Qiskit/rustworkx/releases) - [Commits](https://github.com/Qiskit/rustworkx/compare/0.15.0...0.15.1) --- updated-dependencies: - dependency-name: rustworkx-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ce26b3baea5..a69ed15a19db 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", From 283a880329d2fd4a3058c49e58829d63a803c8c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:06:12 +0000 Subject: [PATCH 4/5] Bump thiserror from 1.0.59 to 1.0.61 (#12707) Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.59 to 1.0.61. - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/1.0.59...1.0.61) --- updated-dependencies: - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a69ed15a19db..125ce6abe876 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", From c674913e688dba7bbe531fbd9e2255e5e2cc7a4d Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 2 Jul 2024 17:09:30 +0200 Subject: [PATCH 5/5] Oxidize `synth_permutation_acg` (#12543) * Move utility functions _inverse_pattern and _get_ordered_swap to Rust * fix formatting and pylint issues * Changed input type to `PyArrayLike1` * Refactor `permutation.rs`, clean up imports, fix coverage error * fix docstring for `_inverse_pattern` Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> * fix docstring for `_get_ordered_swap` Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> * remove pymodule nesting * remove explicit `AllowTypeChange` * Move input validation out of `_inverse_pattern` and `_get_ordered_swap` * oxidization attempt no. 1 * working version! maybe faster possible... * move more into rust & fix clones * layouting & comments * dangling comment * circuit construction in rust * remove dangling code * more lint * add reno * drop redundant Ok(expect()) * Implements Shelly's suggestions * simplify code a little --------- Co-authored-by: jpacold Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- .../src/synthesis/permutation/mod.rs | 24 ++++++ .../src/synthesis/permutation/utils.rs | 73 ++++++++++++++++++- .../synthesis/permutation/permutation_full.py | 22 +----- .../permutation/permutation_utils.py | 34 +-------- .../notes/oxidize-acg-0294a87c0d5974fa.yaml | 5 ++ 5 files changed, 104 insertions(+), 54 deletions(-) create mode 100644 releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml 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/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/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.