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 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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. From 3fb175390d5fa007150fb76a23b914059010788a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 2 Jul 2024 15:13:48 -0400 Subject: [PATCH 06/15] Fix cargo clippy and rustfmt for Rust 1.79 (#12710) After the recently merged #12543 when working with Rust 1.79 cargo fmt makes a small formatting change that rust 1.70 wouldn't and clippy makes flags a &Vec<_> that should be a slice &[_] instead. This commit makes these two small chagnes so they're not an issue for people building with the latest stable version of rust, not just our MSRV. --- crates/accelerate/src/synthesis/permutation/utils.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/accelerate/src/synthesis/permutation/utils.rs b/crates/accelerate/src/synthesis/permutation/utils.rs index 620ce4628741..47a9e1c3a7a9 100644 --- a/crates/accelerate/src/synthesis/permutation/utils.rs +++ b/crates/accelerate/src/synthesis/permutation/utils.rs @@ -120,8 +120,10 @@ pub fn pattern_to_cycles(pattern: &ArrayView1) -> Vec> { /// 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!()}; +fn pget(vec: &[usize], index: isize) -> Result { + let SequenceIndex::Int(wrapped) = PySequenceIndex::Int(index).with_len(vec.len())? else { + unreachable!() + }; Ok(vec[wrapped]) } From bfc69a499c035bc3b8af31b37ba0469d7e89e67a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 3 Jul 2024 00:18:40 -0400 Subject: [PATCH 07/15] Use rust gates for Optimize1QGatesDecomposition (#12650) * Use rust gates for Optimize1QGatesDecomposition This commit moves to using rust gates for the Optimize1QGatesDecomposition transpiler pass. It takes in a sequence of runs (which are a list of DAGOpNodes) from the python side of the transpiler pass which are generated from DAGCircuit.collect_1q_runs() (which in the future should be moved to rust after #12550 merges). The rust portion of the pass now iterates over each run, performs the matrix multiplication to compute the unitary of the run, then synthesizes that unitary, computes the estimated error of the circuit synthesis and returns a tuple of the circuit sequence in terms of rust StandardGate enums. The python portion of the code then takes those sequences and does inplace substitution of each run with the sequence returned from rust. Once #12550 merges we should be able to move the input collect_1q_runs() call and perform the output node substitions in rust making the full pass execute in the rust domain without any python interaction. Additionally, the OneQubitEulerDecomposer class is updated to use rust for circuit generation instead of doing this python side. The internal changes done to use rust gates in the transpiler pass meant we were half way to this already by emitting rust StandardGates instead of python gate objects. The dag handling is still done in Python however until #12550 merges. This also includes an implementation of the r gate, I temporarily added this to unblock this effort as it was the only gate missing needed to complete this. We can rebase this if a standalone implementation of the gate merges before this. * Cache target decompositions for each qubit Previously this PR was re-computing the target bases to synthesize with for each run found in the circuit. But in cases where there were multiple runs repeated on a qubit this was unecessary work. Prior to moving this code to rust there was already caching code to make this optimization, but the rust path short circuited around this. This commit fixes this so we're caching the target bases for each qubit and only computing it once. * Optimize rust implementation slightly * Avoid extra allocations by inlining matrix multiplication * Remove unnecessary comment * Remove stray code block * Add import path for rust gate * Use rust gate in circuit constructor * Remove duplicated op_name getter and just use existing name getter * Apply suggestions from code review Co-authored-by: John Lapeyre * Simplify construction of target_basis_vec * Fix rebase issue * Update crates/accelerate/src/euler_one_qubit_decomposer.rs Co-authored-by: John Lapeyre * Update crates/accelerate/src/euler_one_qubit_decomposer.rs --------- Co-authored-by: John Lapeyre --- .../src/euler_one_qubit_decomposer.rs | 301 +++++++++++++++--- crates/accelerate/src/two_qubit_decompose.rs | 19 +- crates/circuit/src/dag_node.rs | 50 ++- crates/circuit/src/operations.rs | 6 + qiskit/dagcircuit/dagcircuit.py | 57 ++-- .../one_qubit/one_qubit_decompose.py | 50 ++- .../optimization/optimize_1q_decomposition.py | 105 +++--- 7 files changed, 431 insertions(+), 157 deletions(-) diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 01725269bb84..8c3a87ce51ec 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -18,7 +18,6 @@ use num_complex::{Complex64, ComplexFloat}; use smallvec::{smallvec, SmallVec}; use std::cmp::Ordering; use std::f64::consts::PI; -use std::ops::Deref; use std::str::FromStr; use pyo3::exceptions::PyValueError; @@ -31,8 +30,12 @@ use ndarray::prelude::*; use numpy::PyReadonlyArray2; use pyo3::pybacked::PyBackedStr; +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::dag_node::DAGOpNode; +use qiskit_circuit::operations::{Operation, Param, StandardGate}; use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::c64; +use qiskit_circuit::Qubit; pub const ANGLE_ZERO_EPSILON: f64 = 1e-12; @@ -68,12 +71,12 @@ impl OneQubitGateErrorMap { #[pyclass(sequence)] pub struct OneQubitGateSequence { - pub gates: Vec<(String, SmallVec<[f64; 3]>)>, + pub gates: Vec<(StandardGate, SmallVec<[f64; 3]>)>, #[pyo3(get)] pub global_phase: f64, } -type OneQubitGateSequenceState = (Vec<(String, SmallVec<[f64; 3]>)>, f64); +type OneQubitGateSequenceState = (Vec<(StandardGate, SmallVec<[f64; 3]>)>, f64); #[pymethods] impl OneQubitGateSequence { @@ -115,15 +118,15 @@ fn circuit_kak( phi: f64, lam: f64, phase: f64, - k_gate: &str, - a_gate: &str, + k_gate: StandardGate, + a_gate: StandardGate, simplify: bool, atol: Option, ) -> OneQubitGateSequence { let mut lam = lam; let mut theta = theta; let mut phi = phi; - let mut circuit: Vec<(String, SmallVec<[f64; 3]>)> = Vec::with_capacity(3); + let mut circuit: Vec<(StandardGate, SmallVec<[f64; 3]>)> = Vec::with_capacity(3); let mut atol = match atol { Some(atol) => atol, None => ANGLE_ZERO_EPSILON, @@ -139,7 +142,7 @@ fn circuit_kak( // slippage coming from _mod_2pi injecting multiples of 2pi. lam = mod_2pi(lam, atol); if lam.abs() > atol { - circuit.push((String::from(k_gate), smallvec![lam])); + circuit.push((k_gate, smallvec![lam])); global_phase += lam / 2.; } return OneQubitGateSequence { @@ -160,13 +163,13 @@ fn circuit_kak( lam = mod_2pi(lam, atol); if lam.abs() > atol { global_phase += lam / 2.; - circuit.push((String::from(k_gate), smallvec![lam])); + circuit.push((k_gate, smallvec![lam])); } - circuit.push((String::from(a_gate), smallvec![theta])); + circuit.push((a_gate, smallvec![theta])); phi = mod_2pi(phi, atol); if phi.abs() > atol { global_phase += phi / 2.; - circuit.push((String::from(k_gate), smallvec![phi])); + circuit.push((k_gate, smallvec![phi])); } OneQubitGateSequence { gates: circuit, @@ -190,7 +193,7 @@ fn circuit_u3( let phi = mod_2pi(phi, atol); let lam = mod_2pi(lam, atol); if !simplify || theta.abs() > atol || phi.abs() > atol || lam.abs() > atol { - circuit.push((String::from("u3"), smallvec![theta, phi, lam])); + circuit.push((StandardGate::U3Gate, smallvec![theta, phi, lam])); } OneQubitGateSequence { gates: circuit, @@ -217,16 +220,16 @@ fn circuit_u321( if theta.abs() < atol { let tot = mod_2pi(phi + lam, atol); if tot.abs() > atol { - circuit.push((String::from("u1"), smallvec![tot])); + circuit.push((StandardGate::U1Gate, smallvec![tot])); } } else if (theta - PI / 2.).abs() < atol { circuit.push(( - String::from("u2"), + StandardGate::U2Gate, smallvec![mod_2pi(phi, atol), mod_2pi(lam, atol)], )); } else { circuit.push(( - String::from("u3"), + StandardGate::U3Gate, smallvec![theta, mod_2pi(phi, atol), mod_2pi(lam, atol)], )); } @@ -255,7 +258,7 @@ fn circuit_u( let phi = mod_2pi(phi, atol); let lam = mod_2pi(lam, atol); if theta.abs() > atol || phi.abs() > atol || lam.abs() > atol { - circuit.push((String::from("u"), smallvec![theta, phi, lam])); + circuit.push((StandardGate::UGate, smallvec![theta, phi, lam])); } OneQubitGateSequence { gates: circuit, @@ -358,7 +361,7 @@ fn circuit_rr( // This can be expressed as a single R gate if theta.abs() > atol { circuit.push(( - String::from("r"), + StandardGate::RGate, smallvec![theta, mod_2pi(PI / 2. + phi, atol)], )); } @@ -366,12 +369,12 @@ fn circuit_rr( // General case: use two R gates if (theta - PI).abs() > atol { circuit.push(( - String::from("r"), + StandardGate::RGate, smallvec![theta - PI, mod_2pi(PI / 2. - lam, atol)], )); } circuit.push(( - String::from("r"), + StandardGate::RGate, smallvec![PI, mod_2pi(0.5 * (phi - lam + PI), atol)], )); } @@ -393,10 +396,46 @@ pub fn generate_circuit( atol: Option, ) -> PyResult { let res = match target_basis { - EulerBasis::ZYZ => circuit_kak(theta, phi, lam, phase, "rz", "ry", simplify, atol), - EulerBasis::ZXZ => circuit_kak(theta, phi, lam, phase, "rz", "rx", simplify, atol), - EulerBasis::XZX => circuit_kak(theta, phi, lam, phase, "rx", "rz", simplify, atol), - EulerBasis::XYX => circuit_kak(theta, phi, lam, phase, "rx", "ry", simplify, atol), + EulerBasis::ZYZ => circuit_kak( + theta, + phi, + lam, + phase, + StandardGate::RZGate, + StandardGate::RYGate, + simplify, + atol, + ), + EulerBasis::ZXZ => circuit_kak( + theta, + phi, + lam, + phase, + StandardGate::RZGate, + StandardGate::RXGate, + simplify, + atol, + ), + EulerBasis::XZX => circuit_kak( + theta, + phi, + lam, + phase, + StandardGate::RXGate, + StandardGate::RZGate, + simplify, + atol, + ), + EulerBasis::XYX => circuit_kak( + theta, + phi, + lam, + phase, + StandardGate::RXGate, + StandardGate::RYGate, + simplify, + atol, + ), EulerBasis::U3 => circuit_u3(theta, phi, lam, phase, simplify, atol), EulerBasis::U321 => circuit_u321(theta, phi, lam, phase, simplify, atol), EulerBasis::U => circuit_u(theta, phi, lam, phase, simplify, atol), @@ -411,11 +450,13 @@ pub fn generate_circuit( let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { - circuit.gates.push((String::from("p"), smallvec![phi])); + circuit + .gates + .push((StandardGate::PhaseGate, smallvec![phi])); } }; let fnx = |circuit: &mut OneQubitGateSequence| { - circuit.gates.push((String::from("sx"), SmallVec::new())); + circuit.gates.push((StandardGate::SXGate, SmallVec::new())); }; circuit_psx_gen( @@ -441,12 +482,12 @@ pub fn generate_circuit( let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { - circuit.gates.push((String::from("rz"), smallvec![phi])); + circuit.gates.push((StandardGate::RZGate, smallvec![phi])); circuit.global_phase += phi / 2.; } }; let fnx = |circuit: &mut OneQubitGateSequence| { - circuit.gates.push((String::from("sx"), SmallVec::new())); + circuit.gates.push((StandardGate::SXGate, SmallVec::new())); }; circuit_psx_gen( theta, @@ -471,12 +512,14 @@ pub fn generate_circuit( let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { - circuit.gates.push((String::from("u1"), smallvec![phi])); + circuit.gates.push((StandardGate::U1Gate, smallvec![phi])); } }; let fnx = |circuit: &mut OneQubitGateSequence| { circuit.global_phase += PI / 4.; - circuit.gates.push((String::from("rx"), smallvec![PI / 2.])); + circuit + .gates + .push((StandardGate::RXGate, smallvec![PI / 2.])); }; circuit_psx_gen( theta, @@ -501,15 +544,15 @@ pub fn generate_circuit( let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { - circuit.gates.push((String::from("rz"), smallvec![phi])); + circuit.gates.push((StandardGate::RZGate, smallvec![phi])); circuit.global_phase += phi / 2.; } }; let fnx = |circuit: &mut OneQubitGateSequence| { - circuit.gates.push((String::from("sx"), SmallVec::new())); + circuit.gates.push((StandardGate::SXGate, SmallVec::new())); }; let fnxpi = |circuit: &mut OneQubitGateSequence| { - circuit.gates.push((String::from("x"), SmallVec::new())); + circuit.gates.push((StandardGate::XGate, SmallVec::new())); }; circuit_psx_gen( theta, @@ -633,7 +676,7 @@ fn compare_error_fn( let fidelity_product: f64 = circuit .gates .iter() - .map(|x| 1. - err_map.get(&x.0).unwrap_or(&0.)) + .map(|gate| 1. - err_map.get(gate.0.name()).unwrap_or(&0.)) .product(); (1. - fidelity_product, circuit.gates.len()) } @@ -642,6 +685,28 @@ fn compare_error_fn( } fn compute_error( + gates: &[(StandardGate, SmallVec<[f64; 3]>)], + error_map: Option<&OneQubitGateErrorMap>, + qubit: usize, +) -> (f64, usize) { + match error_map { + Some(err_map) => { + let num_gates = gates.len(); + let gate_fidelities: f64 = gates + .iter() + .map(|gate| 1. - err_map.error_map[qubit].get(gate.0.name()).unwrap_or(&0.)) + .product(); + (1. - gate_fidelities, num_gates) + } + None => (gates.len() as f64, gates.len()), + } +} + +fn compute_error_term(gate: &str, error_map: &OneQubitGateErrorMap, qubit: usize) -> f64 { + 1. - error_map.error_map[qubit].get(gate).unwrap_or(&0.) +} + +fn compute_error_str( gates: &[(String, SmallVec<[f64; 3]>)], error_map: Option<&OneQubitGateErrorMap>, qubit: usize, @@ -651,7 +716,7 @@ fn compute_error( let num_gates = gates.len(); let gate_fidelities: f64 = gates .iter() - .map(|x| 1. - err_map.error_map[qubit].get(&x.0).unwrap_or(&0.)) + .map(|gate| compute_error_term(gate.0.as_str(), err_map, qubit)) .product(); (1. - gate_fidelities, num_gates) } @@ -670,11 +735,20 @@ pub fn compute_error_one_qubit_sequence( #[pyfunction] pub fn compute_error_list( - circuit: Vec<(String, SmallVec<[f64; 3]>)>, + circuit: Vec>, qubit: usize, error_map: Option<&OneQubitGateErrorMap>, ) -> (f64, usize) { - compute_error(&circuit, error_map, qubit) + let circuit_list: Vec<(String, SmallVec<[f64; 3]>)> = circuit + .iter() + .map(|node| { + ( + node.instruction.operation.name().to_string(), + smallvec![], // Params not needed in this path + ) + }) + .collect(); + compute_error_str(&circuit_list, error_map, qubit) } #[pyfunction] @@ -687,15 +761,13 @@ pub fn unitary_to_gate_sequence( simplify: bool, atol: Option, ) -> PyResult> { - let mut target_basis_vec: Vec = Vec::with_capacity(target_basis_list.len()); - for basis in target_basis_list { - let basis_enum = EulerBasis::__new__(basis.deref())?; - target_basis_vec.push(basis_enum) - } - let unitary_mat = unitary.as_array(); + let target_basis_vec: PyResult> = target_basis_list + .iter() + .map(|basis| EulerBasis::__new__(basis)) + .collect(); Ok(unitary_to_gate_sequence_inner( - unitary_mat, - &target_basis_vec, + unitary.as_array(), + &target_basis_vec?, qubit, error_map, simplify, @@ -725,6 +797,46 @@ pub fn unitary_to_gate_sequence_inner( }) } +#[pyfunction] +#[pyo3(signature = (unitary, target_basis_list, qubit, error_map=None, simplify=true, atol=None))] +pub fn unitary_to_circuit( + py: Python, + unitary: PyReadonlyArray2, + target_basis_list: Vec, + qubit: usize, + error_map: Option<&OneQubitGateErrorMap>, + simplify: bool, + atol: Option, +) -> PyResult> { + let target_basis_vec: PyResult> = target_basis_list + .iter() + .map(|basis| EulerBasis::__new__(basis)) + .collect(); + let circuit_sequence = unitary_to_gate_sequence_inner( + unitary.as_array(), + &target_basis_vec?, + qubit, + error_map, + simplify, + atol, + ); + Ok(circuit_sequence.map(|seq| { + CircuitData::from_standard_gates( + py, + 1, + seq.gates.into_iter().map(|(gate, params)| { + ( + gate, + params.into_iter().map(Param::Float).collect(), + smallvec![Qubit(0)], + ) + }), + Param::Float(seq.global_phase), + ) + .expect("Unexpected Qiskit python bug") + })) +} + #[inline] pub fn det_one_qubit(mat: ArrayView2) -> Complex64 { mat[[0, 0]] * mat[[1, 1]] - mat[[0, 1]] * mat[[1, 0]] @@ -853,6 +965,106 @@ pub fn params_zxz(unitary: PyReadonlyArray2) -> [f64; 4] { params_zxz_inner(mat) } +type OptimizeDecompositionReturn = Option<((f64, usize), (f64, usize), OneQubitGateSequence)>; + +#[pyfunction] +pub fn optimize_1q_gates_decomposition( + runs: Vec>>, + qubits: Vec, + bases: Vec>, + simplify: bool, + error_map: Option<&OneQubitGateErrorMap>, + atol: Option, +) -> Vec { + runs.iter() + .enumerate() + .map(|(index, raw_run)| -> OptimizeDecompositionReturn { + let mut error = match error_map { + Some(_) => 1., + None => raw_run.len() as f64, + }; + let qubit = qubits[index]; + let operator = &raw_run + .iter() + .map(|node| { + if let Some(err_map) = error_map { + error *= + compute_error_term(node.instruction.operation.name(), err_map, qubit) + } + node.instruction + .operation + .matrix(&node.instruction.params) + .expect("No matrix defined for operation") + }) + .fold( + [ + [Complex64::new(1., 0.), Complex64::new(0., 0.)], + [Complex64::new(0., 0.), Complex64::new(1., 0.)], + ], + |mut operator, node| { + matmul_1q(&mut operator, node); + operator + }, + ); + let old_error = if error_map.is_some() { + (1. - error, raw_run.len()) + } else { + (error, raw_run.len()) + }; + let target_basis_vec: Vec = bases[index] + .iter() + .map(|basis| EulerBasis::__new__(basis).unwrap()) + .collect(); + unitary_to_gate_sequence_inner( + aview2(operator), + &target_basis_vec, + qubit, + error_map, + simplify, + atol, + ) + .map(|out_seq| { + let new_error = compute_error_one_qubit_sequence(&out_seq, qubit, error_map); + (old_error, new_error, out_seq) + }) + }) + .collect() +} + +fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { + *operator = [ + [ + other[[0, 0]] * operator[0][0] + other[[0, 1]] * operator[1][0], + other[[0, 0]] * operator[0][1] + other[[0, 1]] * operator[1][1], + ], + [ + other[[1, 0]] * operator[0][0] + other[[1, 1]] * operator[1][0], + other[[1, 0]] * operator[0][1] + other[[1, 1]] * operator[1][1], + ], + ]; +} + +#[pyfunction] +pub fn collect_1q_runs_filter(py: Python, node: PyObject) -> bool { + let op_node = node.extract::>(py); + match op_node { + Ok(node) => { + node.instruction.operation.num_qubits() == 1 + && node.instruction.operation.num_clbits() == 0 + && node + .instruction + .operation + .matrix(&node.instruction.params) + .is_some() + && match &node.instruction.extra_attrs { + None => true, + Some(attrs) => attrs.condition.is_none(), + } + } + Err(_) => false, + } +} + #[pymodule] pub fn euler_one_qubit_decomposer(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(params_zyz))?; @@ -863,8 +1075,11 @@ pub fn euler_one_qubit_decomposer(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(params_u1x))?; m.add_wrapped(wrap_pyfunction!(generate_circuit))?; m.add_wrapped(wrap_pyfunction!(unitary_to_gate_sequence))?; + m.add_wrapped(wrap_pyfunction!(unitary_to_circuit))?; m.add_wrapped(wrap_pyfunction!(compute_error_one_qubit_sequence))?; m.add_wrapped(wrap_pyfunction!(compute_error_list))?; + m.add_wrapped(wrap_pyfunction!(optimize_1q_gates_decomposition))?; + m.add_wrapped(wrap_pyfunction!(collect_1q_runs_filter))?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 37061d5159f4..568206925c2b 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -52,6 +52,7 @@ use rand_distr::StandardNormal; use rand_pcg::Pcg64Mcg; use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; +use qiskit_circuit::operations::Operation; use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::{c64, GateArray1Q, GateArray2Q, C_M_ONE, C_ONE, C_ZERO, IM, M_IM}; @@ -1045,7 +1046,7 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c2r.gates { - gate_sequence.push((gate.0, gate.1, smallvec![0])) + gate_sequence.push((gate.0.name().to_string(), gate.1, smallvec![0])) } global_phase += c2r.global_phase; let c2l = unitary_to_gate_sequence_inner( @@ -1058,7 +1059,7 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c2l.gates { - gate_sequence.push((gate.0, gate.1, smallvec![1])) + gate_sequence.push((gate.0.name().to_string(), gate.1, smallvec![1])) } global_phase += c2l.global_phase; self.weyl_gate( @@ -1077,7 +1078,7 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c1r.gates { - gate_sequence.push((gate.0, gate.1, smallvec![0])) + gate_sequence.push((gate.0.name().to_string(), gate.1, smallvec![0])) } global_phase += c2r.global_phase; let c1l = unitary_to_gate_sequence_inner( @@ -1090,7 +1091,7 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c1l.gates { - gate_sequence.push((gate.0, gate.1, smallvec![1])) + gate_sequence.push((gate.0.name().to_string(), gate.1, smallvec![1])) } Ok(TwoQubitGateSequence { gates: gate_sequence, @@ -1459,7 +1460,7 @@ impl TwoQubitBasisDecomposer { if let Some(sequence) = sequence { *global_phase += sequence.global_phase; for gate in sequence.gates { - gates.push((gate.0, gate.1, smallvec![qubit])); + gates.push((gate.0.name().to_string(), gate.1, smallvec![qubit])); } } } @@ -1847,13 +1848,13 @@ impl TwoQubitBasisDecomposer { for i in 0..best_nbasis as usize { if let Some(euler_decomp) = &euler_decompositions[2 * i] { for gate in &euler_decomp.gates { - gates.push((gate.0.clone(), gate.1.clone(), smallvec![0])); + gates.push((gate.0.name().to_string(), gate.1.clone(), smallvec![0])); } global_phase += euler_decomp.global_phase } if let Some(euler_decomp) = &euler_decompositions[2 * i + 1] { for gate in &euler_decomp.gates { - gates.push((gate.0.clone(), gate.1.clone(), smallvec![1])); + gates.push((gate.0.name().to_string(), gate.1.clone(), smallvec![1])); } global_phase += euler_decomp.global_phase } @@ -1861,13 +1862,13 @@ impl TwoQubitBasisDecomposer { } if let Some(euler_decomp) = &euler_decompositions[2 * best_nbasis as usize] { for gate in &euler_decomp.gates { - gates.push((gate.0.clone(), gate.1.clone(), smallvec![0])); + gates.push((gate.0.name().to_string(), gate.1.clone(), smallvec![0])); } global_phase += euler_decomp.global_phase } if let Some(euler_decomp) = &euler_decompositions[2 * best_nbasis as usize + 1] { for gate in &euler_decomp.gates { - gates.push((gate.0.clone(), gate.1.clone(), smallvec![1])); + gates.push((gate.0.name().to_string(), gate.1.clone(), smallvec![1])); } global_phase += euler_decomp.global_phase } diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index ffd7920a36fd..44ba5f7a6bf0 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -18,7 +18,7 @@ use crate::operations::Operation; use numpy::IntoPyArray; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; -use pyo3::{intern, IntoPy, PyObject, PyResult}; +use pyo3::{intern, IntoPy, PyObject, PyResult, ToPyObject}; use smallvec::smallvec; /// Parent class for DAGOpNode, DAGInNode, and DAGOutNode. @@ -135,6 +135,50 @@ impl DAGOpNode { )) } + #[staticmethod] + fn from_instruction( + py: Python, + instruction: CircuitInstruction, + dag: Option<&Bound>, + ) -> PyResult { + let qargs = instruction.qubits.clone_ref(py).into_bound(py); + let cargs = instruction.clbits.clone_ref(py).into_bound(py); + + let sort_key = match dag { + Some(dag) => { + let cache = dag + .getattr(intern!(py, "_key_cache"))? + .downcast_into_exact::()?; + let cache_key = PyTuple::new_bound(py, [&qargs, &cargs]); + match cache.get_item(&cache_key)? { + Some(key) => key, + None => { + let indices: PyResult> = qargs + .iter() + .chain(cargs.iter()) + .map(|bit| { + dag.call_method1(intern!(py, "find_bit"), (bit,))? + .getattr(intern!(py, "index")) + }) + .collect(); + let index_strs: Vec<_> = + indices?.into_iter().map(|i| format!("{:04}", i)).collect(); + let key = PyString::new_bound(py, index_strs.join(",").as_str()); + cache.set_item(&cache_key, &key)?; + key.into_any() + } + } + } + None => qargs.str()?.into_any(), + }; + let base = PyClassInitializer::from(DAGNode { _node_id: -1 }); + let sub = base.add_subclass(DAGOpNode { + instruction, + sort_key: sort_key.unbind(), + }); + Ok(Py::new(py, sub)?.to_object(py)) + } + fn __reduce__(slf: PyRef, py: Python) -> PyResult { let state = (slf.as_ref()._node_id, &slf.sort_key); Ok(( @@ -206,8 +250,8 @@ impl DAGOpNode { /// Returns the Instruction name corresponding to the op for this node #[getter] - fn get_name(&self, py: Python) -> PyObject { - self.instruction.operation.name().to_object(py) + fn get_name(&self) -> &str { + self.instruction.operation.name() } #[getter] diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 8935e72e0ad5..77458e2fc35f 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -242,6 +242,12 @@ pub enum StandardGate { RZXGate = 52, } +impl ToPyObject for StandardGate { + fn to_object(&self, py: Python) -> PyObject { + self.into_py(py) + } +} + // TODO: replace all 34s (placeholders) with actual number static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, // 0-9 diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index a0d2c42be17e..b93a90e47f7b 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -54,6 +54,7 @@ from qiskit.dagcircuit.dagnode import DAGNode, DAGOpNode, DAGInNode, DAGOutNode from qiskit.circuit.bit import Bit from qiskit.pulse import Schedule +from qiskit._accelerate.euler_one_qubit_decomposer import collect_1q_runs_filter BitLocations = namedtuple("BitLocations", ("index", "registers")) # The allowable arguments to :meth:`DAGCircuit.copy_empty_like`'s ``vars_mode``. @@ -642,17 +643,17 @@ def _check_wires(self, args: Iterable[Bit | expr.Var], amap: dict[Bit | expr.Var if wire not in amap: raise DAGCircuitError(f"wire {wire} not found in {amap}") - def _increment_op(self, op): - if op.name in self._op_names: - self._op_names[op.name] += 1 + def _increment_op(self, op_name): + if op_name in self._op_names: + self._op_names[op_name] += 1 else: - self._op_names[op.name] = 1 + self._op_names[op_name] = 1 - def _decrement_op(self, op): - if self._op_names[op.name] == 1: - del self._op_names[op.name] + def _decrement_op(self, op_name): + if self._op_names[op_name] == 1: + del self._op_names[op_name] else: - self._op_names[op.name] -= 1 + self._op_names[op_name] -= 1 def copy_empty_like(self, *, vars_mode: _VarsMode = "alike"): """Return a copy of self with the same structure but empty. @@ -724,7 +725,7 @@ def _apply_op_node_back(self, node: DAGOpNode): additional = set(_additional_wires(node)).difference(node.cargs) node._node_id = self._multi_graph.add_node(node) - self._increment_op(node) + self._increment_op(node.name) # Add new in-edges from predecessors of the output nodes to the # operation node while deleting the old in-edges of the output nodes @@ -780,7 +781,7 @@ def apply_operation_back( node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) node._node_id = self._multi_graph.add_node(node) - self._increment_op(op) + self._increment_op(op.name) # Add new in-edges from predecessors of the output nodes to the # operation node while deleting the old in-edges of the output nodes @@ -832,7 +833,7 @@ def apply_operation_front( node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) node._node_id = self._multi_graph.add_node(node) - self._increment_op(op) + self._increment_op(op.name) # Add new out-edges to successors of the input nodes from the # operation node while deleting the old out-edges of the input nodes @@ -1379,10 +1380,10 @@ def replace_block_with_op( "Replacing the specified node block would introduce a cycle" ) from ex - self._increment_op(op) + self._increment_op(op.name) for nd in node_block: - self._decrement_op(nd.op) + self._decrement_op(nd.name) return new_node @@ -1593,7 +1594,7 @@ def edge_weight_map(wire): node_map = self._multi_graph.substitute_node_with_subgraph( node._node_id, in_dag._multi_graph, edge_map_fn, filter_fn, edge_weight_map ) - self._decrement_op(node.op) + self._decrement_op(node.name) variable_mapper = _classical_resource_map.VariableMapper( self.cregs.values(), wire_map, add_register=self.add_creg @@ -1624,7 +1625,7 @@ def edge_weight_map(wire): new_node = DAGOpNode(m_op, qargs=m_qargs, cargs=m_cargs, dag=self) new_node._node_id = new_node_index self._multi_graph[new_node_index] = new_node - self._increment_op(new_node.op) + self._increment_op(new_node.name) return {k: self._multi_graph[v] for k, v in node_map.items()} @@ -1696,17 +1697,17 @@ def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_ if inplace: if op.name != node.op.name: - self._increment_op(op) - self._decrement_op(node.op) + self._increment_op(op.name) + self._decrement_op(node.name) node.op = op return node new_node = copy.copy(node) new_node.op = op self._multi_graph[node._node_id] = new_node - if op.name != node.op.name: - self._increment_op(op) - self._decrement_op(node.op) + if op.name != node.name: + self._increment_op(op.name) + self._decrement_op(node.name) return new_node def separable_circuits( @@ -1987,7 +1988,7 @@ def remove_op_node(self, node): self._multi_graph.remove_node_retain_edges( node._node_id, use_outgoing=False, condition=lambda edge1, edge2: edge1 == edge2 ) - self._decrement_op(node.op) + self._decrement_op(node.name) def remove_ancestors_of(self, node): """Remove all of the ancestor operation nodes of node.""" @@ -2152,19 +2153,7 @@ def filter_fn(node): def collect_1q_runs(self) -> list[list[DAGOpNode]]: """Return a set of non-conditional runs of 1q "op" nodes.""" - - def filter_fn(node): - return ( - isinstance(node, DAGOpNode) - and len(node.qargs) == 1 - and len(node.cargs) == 0 - and isinstance(node.op, Gate) - and hasattr(node.op, "__array__") - and getattr(node.op, "condition", None) is None - and not node.op.is_parameterized() - ) - - return rx.collect_runs(self._multi_graph, filter_fn) + return rx.collect_runs(self._multi_graph, collect_1q_runs_filter) def collect_2q_runs(self): """Return a set of non-conditional runs of 2q "op" nodes.""" diff --git a/qiskit/synthesis/one_qubit/one_qubit_decompose.py b/qiskit/synthesis/one_qubit/one_qubit_decompose.py index c84db761b7f0..f60f20f9524e 100644 --- a/qiskit/synthesis/one_qubit/one_qubit_decompose.py +++ b/qiskit/synthesis/one_qubit/one_qubit_decompose.py @@ -161,29 +161,19 @@ def build_circuit(self, gates, global_phase) -> QuantumCircuit | DAGCircuit: if len(gates) > 0 and isinstance(gates[0], tuple): lookup_gate = True - if self.use_dag: - from qiskit.dagcircuit import dagcircuit - - dag = dagcircuit.DAGCircuit() - dag.global_phase = global_phase - dag.add_qubits(qr) - for gate_entry in gates: - if lookup_gate: - gate = NAME_MAP[gate_entry[0]](*gate_entry[1]) - else: - gate = gate_entry - - dag.apply_operation_back(gate, (qr[0],), check=False) - return dag - else: - circuit = QuantumCircuit(qr, global_phase=global_phase) - for gate_entry in gates: - if lookup_gate: - gate = NAME_MAP[gate_entry[0]](*gate_entry[1]) - else: - gate = gate_entry - circuit._append(gate, [qr[0]], []) - return circuit + from qiskit.dagcircuit import dagcircuit + + dag = dagcircuit.DAGCircuit() + dag.global_phase = global_phase + dag.add_qubits(qr) + for gate_entry in gates: + if lookup_gate: + gate = NAME_MAP[gate_entry[0].name](*gate_entry[1]) + else: + gate = gate_entry.name + + dag.apply_operation_back(gate, (qr[0],), check=False) + return dag def __call__( self, @@ -225,11 +215,17 @@ def __call__( return self._decompose(unitary, simplify=simplify, atol=atol) def _decompose(self, unitary, simplify=True, atol=DEFAULT_ATOL): - circuit_sequence = euler_one_qubit_decomposer.unitary_to_gate_sequence( - unitary, [self.basis], 0, None, simplify, atol + if self.use_dag: + circuit_sequence = euler_one_qubit_decomposer.unitary_to_gate_sequence( + unitary, [self.basis], 0, None, simplify, atol + ) + circuit = self.build_circuit(circuit_sequence, circuit_sequence.global_phase) + return circuit + return QuantumCircuit._from_circuit_data( + euler_one_qubit_decomposer.unitary_to_circuit( + unitary, [self.basis], 0, None, simplify, atol + ) ) - circuit = self.build_circuit(circuit_sequence, circuit_sequence.global_phase) - return circuit @property def basis(self): diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 3f8d07839c0f..04d95312aa6d 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -33,6 +33,7 @@ XGate, ) from qiskit.circuit import Qubit +from qiskit.circuit.quantumcircuitdata import CircuitInstruction from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.dagcircuit.dagnode import DAGOpNode @@ -110,16 +111,7 @@ def _build_error_map(self): else: return None - def _resynthesize_run(self, matrix, qubit=None): - """ - Re-synthesizes one 2x2 `matrix`, typically extracted via `dag.collect_1q_runs`. - - Returns the newly synthesized circuit in the indicated basis, or None - if no synthesis routine applied. - - When multiple synthesis options are available, it prefers the one with the lowest - error when the circuit is applied to `qubit`. - """ + def _get_decomposer(self, qubit=None): # include path for when target exists but target.num_qubits is None (BasicSimulator) if self._target is not None and self._target.num_qubits is not None: if qubit is not None: @@ -133,6 +125,19 @@ def _resynthesize_run(self, matrix, qubit=None): decomposers = _possible_decomposers(available_1q_basis) else: decomposers = self._global_decomposers + return decomposers + + def _resynthesize_run(self, matrix, qubit=None): + """ + Re-synthesizes one 2x2 `matrix`, typically extracted via `dag.collect_1q_runs`. + + Returns the newly synthesized circuit in the indicated basis, or None + if no synthesis routine applied. + + When multiple synthesis options are available, it prefers the one with the lowest + error when the circuit is applied to `qubit`. + """ + decomposers = self._get_decomposer(qubit) best_synth_circuit = euler_one_qubit_decomposer.unitary_to_gate_sequence( matrix, @@ -149,10 +154,13 @@ def _gate_sequence_to_dag(self, best_synth_circuit): out_dag.global_phase = best_synth_circuit.global_phase for gate_name, angles in best_synth_circuit: - out_dag.apply_operation_back(NAME_MAP[gate_name](*angles), qubits, check=False) + op = CircuitInstruction(gate_name, qubits=qubits, params=angles) + out_dag.apply_operation_back(op.operation, qubits, check=False) return out_dag - def _substitution_checks(self, dag, old_run, new_circ, basis, qubit): + def _substitution_checks( + self, dag, old_run, new_circ, basis, qubit, old_error=None, new_error=None + ): """ Returns `True` when it is recommended to replace `old_run` with `new_circ` over `basis`. """ @@ -176,11 +184,14 @@ def _substitution_checks(self, dag, old_run, new_circ, basis, qubit): # if we're outside of the basis set, we're obligated to logically decompose. # if we're outside of the set of gates for which we have physical definitions, # then we _try_ to decompose, using the results if we see improvement. - new_error = 0.0 - old_error = 0.0 if not uncalibrated_and_not_basis_p: - new_error = self._error(new_circ, qubit) - old_error = self._error(old_run, qubit) + if new_error is None: + new_error = self._error(new_circ, qubit) + if old_error is None: + old_error = self._error(old_run, qubit) + else: + new_error = 0.0 + old_error = 0.0 return ( uncalibrated_and_not_basis_p @@ -198,32 +209,47 @@ def run(self, dag): Returns: DAGCircuit: the optimized DAG. """ - runs = dag.collect_1q_runs() - for run in runs: + runs = [] + qubits = [] + bases = [] + for run in dag.collect_1q_runs(): qubit = dag.find_bit(run[0].qargs[0]).index - operator = run[0].op.to_matrix() - for node in run[1:]: - operator = node.op.to_matrix().dot(operator) - best_circuit_sequence = self._resynthesize_run(operator, qubit) - + runs.append(run) + qubits.append(qubit) + bases.append(self._get_decomposer(qubit)) + best_sequences = euler_one_qubit_decomposer.optimize_1q_gates_decomposition( + runs, qubits, bases, simplify=True, error_map=self.error_map + ) + for index, best_circuit_sequence in enumerate(best_sequences): + run = runs[index] + qubit = qubits[index] if self._target is None: basis = self._basis_gates else: basis = self._target.operation_names_for_qargs((qubit,)) - - if best_circuit_sequence is not None and self._substitution_checks( - dag, run, best_circuit_sequence, basis, qubit - ): - for gate_name, angles in best_circuit_sequence: - op = NAME_MAP[gate_name](*angles) - node = DAGOpNode(NAME_MAP[gate_name](*angles), run[0].qargs, dag=dag) - node._node_id = dag._multi_graph.add_node(node) - dag._increment_op(op) - dag._multi_graph.insert_node_on_in_edges(node._node_id, run[0]._node_id) - dag.global_phase += best_circuit_sequence.global_phase - # Delete the other nodes in the run - for current_node in run: - dag.remove_op_node(current_node) + if best_circuit_sequence is not None: + (old_error, new_error, best_circuit_sequence) = best_circuit_sequence + if self._substitution_checks( + dag, + run, + best_circuit_sequence, + basis, + qubit, + old_error=old_error, + new_error=new_error, + ): + first_node_id = run[0]._node_id + qubit = run[0].qargs + for gate, angles in best_circuit_sequence: + op = CircuitInstruction(gate, qubits=qubit, params=angles) + node = DAGOpNode.from_instruction(op, dag=dag) + node._node_id = dag._multi_graph.add_node(node) + dag._increment_op(gate.name) + dag._multi_graph.insert_node_on_in_edges(node._node_id, first_node_id) + dag.global_phase += best_circuit_sequence.global_phase + # Delete the other nodes in the run + for current_node in run: + dag.remove_op_node(current_node) return dag @@ -241,10 +267,7 @@ def _error(self, circuit, qubit): circuit, qubit, self.error_map ) else: - circuit_list = [(x.op.name, []) for x in circuit] - return euler_one_qubit_decomposer.compute_error_list( - circuit_list, qubit, self.error_map - ) + return euler_one_qubit_decomposer.compute_error_list(circuit, qubit, self.error_map) def _possible_decomposers(basis_set): From 0f585bd75d3a06445960cc4d19439e6b931094cc Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 3 Jul 2024 09:42:41 +0300 Subject: [PATCH 08/15] Port `synth_clifford_greedy` to Rust (#12601) * starting to experiment * porting code * messy code porting * printing statements to enable debugging * fixes * fixing phase * removing some of the printing statements * fixing inaccuracy for cost computation * Moving some of the functionality to SymplecticMatrix class * reducing the number of warnings * formatting * replacing expensive adjoint and compose operations for symplectic matrices by significantly cheaper in-place prepend operations * resolving merge conflicts * cleanup * code cleanup * cleanup * cleanup * cleanup * cleanup * cleanup * cleanup * cleanup * using fast lookup * cleanup * clippy * including params in gate_seq to avoid mapping * removing unnecessary inner function * cleanup * renaming * changes on the python side * reno * adding error handling * improved error handling * removing redundant Ok(Some(...)) * using random_clifford in tests * reorganizing clifford code * fixes * formatting * improved error handling * do not panic * formatting * Applying refactoring suggestions d/utils.rs from code review * release notes update * adding comment --- .../synthesis/clifford/greedy_synthesis.rs | 441 ++++++++++++++++++ .../accelerate/src/synthesis/clifford/mod.rs | 48 ++ .../src/synthesis/clifford/utils.rs | 289 ++++++++++++ crates/accelerate/src/synthesis/linear/mod.rs | 2 +- crates/accelerate/src/synthesis/mod.rs | 6 +- qiskit/__init__.py | 1 + .../clifford/clifford_decompose_greedy.py | 312 +------------ ...ynth-clifford-greedy-0739e9688bc4eedd.yaml | 6 + .../synthesis/test_clifford_sythesis.py | 4 +- 9 files changed, 801 insertions(+), 308 deletions(-) create mode 100644 crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs create mode 100644 crates/accelerate/src/synthesis/clifford/mod.rs create mode 100644 crates/accelerate/src/synthesis/clifford/utils.rs create mode 100644 releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml diff --git a/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs b/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs new file mode 100644 index 000000000000..e53e25282005 --- /dev/null +++ b/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs @@ -0,0 +1,441 @@ +// 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 indexmap::IndexSet; +use ndarray::{s, ArrayView2}; +use smallvec::smallvec; + +use crate::synthesis::clifford::utils::CliffordGatesVec; +use crate::synthesis::clifford::utils::{adjust_final_pauli_gates, SymplecticMatrix}; +use qiskit_circuit::operations::StandardGate; +use qiskit_circuit::Qubit; + +/// Converts a pair of Paulis pauli_x and pauli_z acting on a specific qubit +/// to the corresponding index in [PauliPairsClass] or [SingleQubitGate] classes. +/// The input is given as a 4-tuple: (pauli_x stabilizer, pauli_x destabilizer, +/// pauli_z stabilizer, pauli_z destabilizer), and the output is an unsigned +/// integer from 0 to 15. +fn pauli_pair_to_index(xs: bool, xd: bool, zs: bool, zd: bool) -> usize { + ((xs as usize) << 3) | ((xd as usize) << 2) | ((zs as usize) << 1) | (zd as usize) +} + +/// The five classes of Pauli 2-qubit operators as described in the paper. +#[derive(Clone, Copy)] +enum PauliPairsClass { + ClassA, + ClassB, + ClassC, + ClassD, + ClassE, +} + +/// The 16 Pauli 2-qubit operators are divided into 5 equivalence classes +/// under the action of single-qubit Cliffords. +static PAULI_INDEX_TO_CLASS: [PauliPairsClass; 16] = [ + PauliPairsClass::ClassE, // 'II' + PauliPairsClass::ClassD, // 'IX' + PauliPairsClass::ClassD, // 'IZ' + PauliPairsClass::ClassD, // 'IY' + PauliPairsClass::ClassC, // 'XI' + PauliPairsClass::ClassB, // 'XX' + PauliPairsClass::ClassA, // 'XZ' + PauliPairsClass::ClassA, // 'XY' + PauliPairsClass::ClassC, // 'ZI' + PauliPairsClass::ClassA, // 'ZX' + PauliPairsClass::ClassB, // 'ZZ' + PauliPairsClass::ClassA, // 'ZY' + PauliPairsClass::ClassC, // 'YI' + PauliPairsClass::ClassA, // 'YX' + PauliPairsClass::ClassA, // 'YZ' + PauliPairsClass::ClassB, // 'YY' +]; + +/// Single-qubit Clifford gates modulo Paulis. +#[derive(Clone, Copy)] +enum SingleQubitGate { + GateI, + GateS, + GateH, + GateSH, + GateHS, + GateSHS, +} + +/// Maps pair of pauli operators to the single-qubit gate required +/// for the decoupling step. +static PAULI_INDEX_TO_1Q_GATE: [SingleQubitGate; 16] = [ + SingleQubitGate::GateI, // 'II' + SingleQubitGate::GateH, // 'IX' + SingleQubitGate::GateI, // 'IZ' + SingleQubitGate::GateSH, // 'IY' + SingleQubitGate::GateI, // 'XI' + SingleQubitGate::GateI, // 'XX' + SingleQubitGate::GateI, // 'XZ' + SingleQubitGate::GateSHS, // 'XY' + SingleQubitGate::GateH, // 'ZI' + SingleQubitGate::GateH, // 'ZX' + SingleQubitGate::GateH, // 'ZZ' + SingleQubitGate::GateSH, // 'ZY' + SingleQubitGate::GateS, // 'YI' + SingleQubitGate::GateHS, // 'YX' + SingleQubitGate::GateS, // 'YZ' + SingleQubitGate::GateS, // 'YY' +]; + +pub struct GreedyCliffordSynthesis<'a> { + /// The Clifford tableau to be synthesized. + tableau: ArrayView2<'a, bool>, + + /// The total number of qubits. + num_qubits: usize, + + /// Symplectic matrix being reduced. + symplectic_matrix: SymplecticMatrix, + + /// Unprocessed qubits. + unprocessed_qubits: IndexSet, +} + +impl GreedyCliffordSynthesis<'_> { + pub(crate) fn new(tableau: ArrayView2) -> Result, String> { + let tableau_shape = tableau.shape(); + if (tableau_shape[0] % 2 == 1) || (tableau_shape[1] != tableau_shape[0] + 1) { + return Err("The shape of the Clifford tableau is invalid".to_string()); + } + + let num_qubits = tableau_shape[0] / 2; + + // We are going to modify symplectic_matrix in-place until it + // becomes the identity. + let symplectic_matrix = SymplecticMatrix { + num_qubits, + smat: tableau.slice(s![.., 0..2 * num_qubits]).to_owned(), + }; + + let unprocessed_qubits: IndexSet = (0..num_qubits).collect(); + + Ok(GreedyCliffordSynthesis { + tableau, + num_qubits, + symplectic_matrix, + unprocessed_qubits, + }) + } + + /// Computes the CX cost of decoupling the symplectic matrix on the + /// given qubit. + fn compute_cost(&self, qubit: usize) -> Result { + let mut a_num = 0; + let mut b_num = 0; + let mut c_num = 0; + let mut d_num = 0; + + let mut qubit_is_in_a = false; + + for q in &self.unprocessed_qubits { + let pauli_pair_index = pauli_pair_to_index( + self.symplectic_matrix.smat[[*q, qubit + self.num_qubits]], + self.symplectic_matrix.smat[[*q + self.num_qubits, qubit + self.num_qubits]], + self.symplectic_matrix.smat[[*q, qubit]], + self.symplectic_matrix.smat[[*q + self.num_qubits, qubit]], + ); + let pauli_class = PAULI_INDEX_TO_CLASS[pauli_pair_index]; + + match pauli_class { + PauliPairsClass::ClassA => { + a_num += 1; + if *q == qubit { + qubit_is_in_a = true; + } + } + PauliPairsClass::ClassB => { + b_num += 1; + } + PauliPairsClass::ClassC => { + c_num += 1; + } + PauliPairsClass::ClassD => { + d_num += 1; + } + PauliPairsClass::ClassE => {} + } + } + + if a_num % 2 == 0 { + return Err("Symplectic Gaussian elimination failed.".to_string()); + } + + let mut cnot_cost: usize = + 3 * (a_num - 1) / 2 + (b_num + 1) * ((b_num > 0) as usize) + c_num + d_num; + + if !qubit_is_in_a { + cnot_cost += 3; + } + + Ok(cnot_cost) + } + + /// Calculate a decoupling operator D: + /// D^{-1} * Ox * D = x1 + /// D^{-1} * Oz * D = z1 + /// and reduces the clifford such that it will act trivially on min_qubit. + fn decouple_qubit( + &mut self, + gate_seq: &mut CliffordGatesVec, + min_qubit: usize, + ) -> Result<(), String> { + let mut a_qubits = IndexSet::new(); + let mut b_qubits = IndexSet::new(); + let mut c_qubits = IndexSet::new(); + let mut d_qubits = IndexSet::new(); + + for qubit in &self.unprocessed_qubits { + let pauli_pair_index = pauli_pair_to_index( + self.symplectic_matrix.smat[[*qubit, min_qubit + self.num_qubits]], + self.symplectic_matrix.smat + [[*qubit + self.num_qubits, min_qubit + self.num_qubits]], + self.symplectic_matrix.smat[[*qubit, min_qubit]], + self.symplectic_matrix.smat[[*qubit + self.num_qubits, min_qubit]], + ); + + let single_qubit_gate = PAULI_INDEX_TO_1Q_GATE[pauli_pair_index]; + match single_qubit_gate { + SingleQubitGate::GateS => { + gate_seq.push(( + StandardGate::SGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_s(*qubit); + } + SingleQubitGate::GateH => { + gate_seq.push(( + StandardGate::HGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_h(*qubit); + } + SingleQubitGate::GateSH => { + gate_seq.push(( + StandardGate::SGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + gate_seq.push(( + StandardGate::HGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_s(*qubit); + self.symplectic_matrix.prepend_h(*qubit); + } + SingleQubitGate::GateHS => { + gate_seq.push(( + StandardGate::HGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + gate_seq.push(( + StandardGate::SGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_h(*qubit); + self.symplectic_matrix.prepend_s(*qubit); + } + SingleQubitGate::GateSHS => { + gate_seq.push(( + StandardGate::SGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + gate_seq.push(( + StandardGate::HGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + gate_seq.push(( + StandardGate::SGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_s(*qubit); + self.symplectic_matrix.prepend_h(*qubit); + self.symplectic_matrix.prepend_s(*qubit); + } + SingleQubitGate::GateI => {} + } + + let pauli_class = PAULI_INDEX_TO_CLASS[pauli_pair_index]; + match pauli_class { + PauliPairsClass::ClassA => { + a_qubits.insert(*qubit); + } + PauliPairsClass::ClassB => { + b_qubits.insert(*qubit); + } + PauliPairsClass::ClassC => { + c_qubits.insert(*qubit); + } + PauliPairsClass::ClassD => { + d_qubits.insert(*qubit); + } + PauliPairsClass::ClassE => {} + } + } + + if a_qubits.len() % 2 != 1 { + return Err("Symplectic Gaussian elimination failed.".to_string()); + } + + if !a_qubits.contains(&min_qubit) { + let qubit_a = a_qubits[0]; + gate_seq.push(( + StandardGate::SwapGate, + smallvec![], + smallvec![Qubit(min_qubit as u32), Qubit(qubit_a as u32)], + )); + self.symplectic_matrix.prepend_swap(min_qubit, qubit_a); + + if b_qubits.contains(&min_qubit) { + b_qubits.swap_remove(&min_qubit); + b_qubits.insert(qubit_a); + } else if c_qubits.contains(&min_qubit) { + c_qubits.swap_remove(&min_qubit); + c_qubits.insert(qubit_a); + } else if d_qubits.contains(&min_qubit) { + d_qubits.swap_remove(&min_qubit); + d_qubits.insert(qubit_a); + } + + a_qubits.swap_remove(&qubit_a); + a_qubits.insert(min_qubit); + } + + for qubit in c_qubits { + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(min_qubit as u32), Qubit(qubit as u32)], + )); + self.symplectic_matrix.prepend_cx(min_qubit, qubit); + } + + for qubit in d_qubits { + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(qubit as u32), Qubit(min_qubit as u32)], + )); + self.symplectic_matrix.prepend_cx(qubit, min_qubit); + } + + if b_qubits.len() > 1 { + let qubit_b = b_qubits[0]; + for qubit in &b_qubits[1..] { + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(qubit_b as u32), Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_cx(qubit_b, *qubit); + } + } + + if !b_qubits.is_empty() { + let qubit_b = b_qubits[0]; + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(min_qubit as u32), Qubit(qubit_b as u32)], + )); + self.symplectic_matrix.prepend_cx(min_qubit, qubit_b); + + gate_seq.push(( + StandardGate::HGate, + smallvec![], + smallvec![Qubit(qubit_b as u32)], + )); + self.symplectic_matrix.prepend_h(qubit_b); + + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(qubit_b as u32), Qubit(min_qubit as u32)], + )); + self.symplectic_matrix.prepend_cx(qubit_b, min_qubit); + } + + let a_len: usize = (a_qubits.len() - 1) / 2; + if a_len > 0 { + a_qubits.swap_remove(&min_qubit); + } + + for qubit in 0..a_len { + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![ + Qubit(a_qubits[2 * qubit + 1] as u32), + Qubit(a_qubits[2 * qubit] as u32) + ], + )); + self.symplectic_matrix + .prepend_cx(a_qubits[2 * qubit + 1], a_qubits[2 * qubit]); + + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(a_qubits[2 * qubit] as u32), Qubit(min_qubit as u32)], + )); + self.symplectic_matrix + .prepend_cx(a_qubits[2 * qubit], min_qubit); + + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![ + Qubit(min_qubit as u32), + Qubit(a_qubits[2 * qubit + 1] as u32) + ], + )); + self.symplectic_matrix + .prepend_cx(min_qubit, a_qubits[2 * qubit + 1]); + } + + Ok(()) + } + + /// The main synthesis function. + pub(crate) fn run(&mut self) -> Result<(usize, CliffordGatesVec), String> { + let mut clifford_gates = CliffordGatesVec::new(); + + while !self.unprocessed_qubits.is_empty() { + let costs: Vec<(usize, usize)> = self + .unprocessed_qubits + .iter() + .map(|q| self.compute_cost(*q).map(|cost| (cost, *q))) + .collect::, _>>()?; + + let min_cost_qubit = costs.iter().min_by_key(|(cost, _)| cost).unwrap().1; + + self.decouple_qubit(&mut clifford_gates, min_cost_qubit)?; + + self.unprocessed_qubits.swap_remove(&min_cost_qubit); + } + + adjust_final_pauli_gates(&mut clifford_gates, self.tableau, self.num_qubits)?; + + Ok((self.num_qubits, clifford_gates)) + } +} diff --git a/crates/accelerate/src/synthesis/clifford/mod.rs b/crates/accelerate/src/synthesis/clifford/mod.rs new file mode 100644 index 000000000000..6772228acf88 --- /dev/null +++ b/crates/accelerate/src/synthesis/clifford/mod.rs @@ -0,0 +1,48 @@ +// 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. + +mod greedy_synthesis; +mod utils; + +use crate::synthesis::clifford::greedy_synthesis::GreedyCliffordSynthesis; +use crate::QiskitError; +use numpy::PyReadonlyArray2; +use pyo3::prelude::*; +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::operations::Param; + +/// Create a circuit that synthesizes a given Clifford operator represented as a tableau. +/// +/// This is an implementation of the "greedy Clifford compiler" presented in +/// Appendix A of the paper "Clifford Circuit Optimization with Templates and Symbolic +/// Pauli Gates" by Bravyi, Shaydulin, Hu, and Maslov (2021), ``__. +/// +/// This method typically yields better CX cost compared to the Aaronson-Gottesman method. +/// +/// Note that this function only implements the greedy Clifford compiler and not the +/// templates and symbolic Pauli gates optimizations that are also described in the paper. +#[pyfunction] +#[pyo3(signature = (clifford))] +fn synth_clifford_greedy(py: Python, clifford: PyReadonlyArray2) -> PyResult { + let tableau = clifford.as_array(); + let mut greedy_synthesis = + GreedyCliffordSynthesis::new(tableau.view()).map_err(QiskitError::new_err)?; + let (num_qubits, clifford_gates) = greedy_synthesis.run().map_err(QiskitError::new_err)?; + + CircuitData::from_standard_gates(py, num_qubits as u32, clifford_gates, Param::Float(0.0)) +} + +#[pymodule] +pub fn clifford(m: &Bound) -> PyResult<()> { + m.add_function(wrap_pyfunction!(synth_clifford_greedy, m)?)?; + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/clifford/utils.rs b/crates/accelerate/src/synthesis/clifford/utils.rs new file mode 100644 index 000000000000..766d84ed179d --- /dev/null +++ b/crates/accelerate/src/synthesis/clifford/utils.rs @@ -0,0 +1,289 @@ +// 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 crate::synthesis::linear::utils::calc_inverse_matrix_inner; +use ndarray::{azip, s, Array1, Array2, ArrayView2}; +use qiskit_circuit::operations::{Param, StandardGate}; +use qiskit_circuit::Qubit; +use smallvec::{smallvec, SmallVec}; + +/// Symplectic matrix. +/// Currently this class is internal to the synthesis library. +pub struct SymplecticMatrix { + /// Number of qubits. + pub num_qubits: usize, + /// Matrix with dimensions (2 * num_qubits) x (2 * num_qubits). + pub smat: Array2, +} + +/// Clifford. +/// Currently this class is internal to the synthesis library and +/// has a very different functionality from Qiskit's python-based +/// Clifford class. +pub struct Clifford { + /// Number of qubits. + pub num_qubits: usize, + /// Matrix with dimensions (2 * num_qubits) x (2 * num_qubits + 1). + pub tableau: Array2, +} + +impl SymplecticMatrix { + /// Modifies the matrix in-place by appending S-gate + #[allow(dead_code)] + pub fn append_s(&mut self, qubit: usize) { + let (x, mut z) = self + .smat + .multi_slice_mut((s![.., qubit], s![.., self.num_qubits + qubit])); + azip!((z in &mut z, &x in &x) *z ^= x); + } + + /// Modifies the matrix in-place by prepending S-gate + pub fn prepend_s(&mut self, qubit: usize) { + let (x, mut z) = self + .smat + .multi_slice_mut((s![self.num_qubits + qubit, ..], s![qubit, ..])); + azip!((z in &mut z, &x in &x) *z ^= x); + } + + /// Modifies the matrix in-place by appending H-gate + #[allow(dead_code)] + pub fn append_h(&mut self, qubit: usize) { + let (mut x, mut z) = self + .smat + .multi_slice_mut((s![.., qubit], s![.., self.num_qubits + qubit])); + azip!((x in &mut x, z in &mut z) (*x, *z) = (*z, *x)); + } + + /// Modifies the matrix in-place by prepending H-gate + pub fn prepend_h(&mut self, qubit: usize) { + let (mut x, mut z) = self + .smat + .multi_slice_mut((s![qubit, ..], s![self.num_qubits + qubit, ..])); + azip!((x in &mut x, z in &mut z) (*x, *z) = (*z, *x)); + } + + /// Modifies the matrix in-place by appending SWAP-gate + #[allow(dead_code)] + pub fn append_swap(&mut self, qubit0: usize, qubit1: usize) { + let (mut x0, mut z0, mut x1, mut z1) = self.smat.multi_slice_mut(( + s![.., qubit0], + s![.., self.num_qubits + qubit0], + s![.., qubit1], + s![.., self.num_qubits + qubit1], + )); + azip!((x0 in &mut x0, x1 in &mut x1) (*x0, *x1) = (*x1, *x0)); + azip!((z0 in &mut z0, z1 in &mut z1) (*z0, *z1) = (*z1, *z0)); + } + + /// Modifies the matrix in-place by prepending SWAP-gate + pub fn prepend_swap(&mut self, qubit0: usize, qubit1: usize) { + let (mut x0, mut z0, mut x1, mut z1) = self.smat.multi_slice_mut(( + s![qubit0, ..], + s![self.num_qubits + qubit0, ..], + s![qubit1, ..], + s![self.num_qubits + qubit1, ..], + )); + azip!((x0 in &mut x0, x1 in &mut x1) (*x0, *x1) = (*x1, *x0)); + azip!((z0 in &mut z0, z1 in &mut z1) (*z0, *z1) = (*z1, *z0)); + } + + /// Modifies the matrix in-place by appending CX-gate + #[allow(dead_code)] + pub fn append_cx(&mut self, qubit0: usize, qubit1: usize) { + let (x0, mut z0, mut x1, z1) = self.smat.multi_slice_mut(( + s![.., qubit0], + s![.., self.num_qubits + qubit0], + s![.., qubit1], + s![.., self.num_qubits + qubit1], + )); + azip!((x1 in &mut x1, &x0 in &x0) *x1 ^= x0); + azip!((z0 in &mut z0, &z1 in &z1) *z0 ^= z1); + } + + /// Modifies the matrix in-place by prepending CX-gate + pub fn prepend_cx(&mut self, qubit0: usize, qubit1: usize) { + let (x0, mut z0, mut x1, z1) = self.smat.multi_slice_mut(( + s![qubit1, ..], + s![self.num_qubits + qubit1, ..], + s![qubit0, ..], + s![self.num_qubits + qubit0, ..], + )); + azip!((x1 in &mut x1, &x0 in &x0) *x1 ^= x0); + azip!((z0 in &mut z0, &z1 in &z1) *z0 ^= z1); + } +} + +impl Clifford { + /// Modifies the tableau in-place by appending S-gate + pub fn append_s(&mut self, qubit: usize) { + let (x, mut z, mut p) = self.tableau.multi_slice_mut(( + s![.., qubit], + s![.., self.num_qubits + qubit], + s![.., 2 * self.num_qubits], + )); + + azip!((p in &mut p, &x in &x, &z in &z) *p ^= x & z); + azip!((z in &mut z, &x in &x) *z ^= x); + } + + /// Modifies the tableau in-place by appending Sdg-gate + #[allow(dead_code)] + pub fn append_sdg(&mut self, qubit: usize) { + let (x, mut z, mut p) = self.tableau.multi_slice_mut(( + s![.., qubit], + s![.., self.num_qubits + qubit], + s![.., 2 * self.num_qubits], + )); + + azip!((p in &mut p, &x in &x, &z in &z) *p ^= x & !z); + azip!((z in &mut z, &x in &x) *z ^= x); + } + + /// Modifies the tableau in-place by appending H-gate + pub fn append_h(&mut self, qubit: usize) { + let (mut x, mut z, mut p) = self.tableau.multi_slice_mut(( + s![.., qubit], + s![.., self.num_qubits + qubit], + s![.., 2 * self.num_qubits], + )); + + azip!((p in &mut p, &x in &x, &z in &z) *p ^= x & z); + azip!((x in &mut x, z in &mut z) (*x, *z) = (*z, *x)); + } + + /// Modifies the tableau in-place by appending SWAP-gate + pub fn append_swap(&mut self, qubit0: usize, qubit1: usize) { + let (mut x0, mut z0, mut x1, mut z1) = self.tableau.multi_slice_mut(( + s![.., qubit0], + s![.., self.num_qubits + qubit0], + s![.., qubit1], + s![.., self.num_qubits + qubit1], + )); + azip!((x0 in &mut x0, x1 in &mut x1) (*x0, *x1) = (*x1, *x0)); + azip!((z0 in &mut z0, z1 in &mut z1) (*z0, *z1) = (*z1, *z0)); + } + + /// Modifies the tableau in-place by appending CX-gate + pub fn append_cx(&mut self, qubit0: usize, qubit1: usize) { + let (x0, mut z0, mut x1, z1, mut p) = self.tableau.multi_slice_mut(( + s![.., qubit0], + s![.., self.num_qubits + qubit0], + s![.., qubit1], + s![.., self.num_qubits + qubit1], + s![.., 2 * self.num_qubits], + )); + azip!((p in &mut p, &x0 in &x0, &z0 in &z0, &x1 in &x1, &z1 in &z1) *p ^= (x1 ^ z0 ^ true) & z1 & x0); + azip!((x1 in &mut x1, &x0 in &x0) *x1 ^= x0); + azip!((z0 in &mut z0, &z1 in &z1) *z0 ^= z1); + } + + /// Creates a Clifford from a given sequence of Clifford gates. + /// In essence, starts from the identity tableau and modifies it + /// based on the gates in the sequence. + pub fn from_gate_sequence( + gate_seq: &CliffordGatesVec, + num_qubits: usize, + ) -> Result { + // create the identity + let mut clifford = Clifford { + num_qubits, + tableau: Array2::from_shape_fn((2 * num_qubits, 2 * num_qubits + 1), |(i, j)| i == j), + }; + + gate_seq + .iter() + .try_for_each(|(gate, _params, qubits)| match *gate { + StandardGate::SGate => { + clifford.append_s(qubits[0].0 as usize); + Ok(()) + } + StandardGate::HGate => { + clifford.append_h(qubits[0].0 as usize); + Ok(()) + } + StandardGate::CXGate => { + clifford.append_cx(qubits[0].0 as usize, qubits[1].0 as usize); + Ok(()) + } + StandardGate::SwapGate => { + clifford.append_swap(qubits[0].0 as usize, qubits[1].0 as usize); + Ok(()) + } + _ => Err(format!("Unsupported gate {:?}", gate)), + })?; + Ok(clifford) + } +} + +/// A sequence of Clifford gates. +/// Represents the return type of Clifford synthesis algorithms. +pub type CliffordGatesVec = Vec<(StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>)>; + +/// Given a sequence of Clifford gates that correctly implements the symplectic matrix +/// of the target clifford tableau, adds the Pauli gates to also match the phase of +/// the tableau. +pub fn adjust_final_pauli_gates( + gate_seq: &mut CliffordGatesVec, + target_tableau: ArrayView2, + num_qubits: usize, +) -> Result<(), String> { + // simulate the clifford circuit that we have constructed + let simulated_clifford = Clifford::from_gate_sequence(gate_seq, num_qubits)?; + + // compute the phase difference + let target_phase = target_tableau.column(2 * num_qubits); + let sim_phase = simulated_clifford.tableau.column(2 * num_qubits); + + let delta_phase: Vec = target_phase + .iter() + .zip(sim_phase.iter()) + .map(|(&a, &b)| a ^ b) + .collect(); + + // compute inverse of the symplectic matrix + let smat = target_tableau.slice(s![.., ..2 * num_qubits]); + let smat_inv = calc_inverse_matrix_inner(smat, false)?; + + // compute smat_inv * delta_phase + let arr1 = smat_inv.map(|v| *v as usize); + let vec2: Vec = delta_phase.into_iter().map(|v| v as usize).collect(); + let arr2 = Array1::from(vec2); + let delta_phase_pre = arr1.dot(&arr2).map(|v| v % 2 == 1); + + // add pauli gates + for qubit in 0..num_qubits { + if delta_phase_pre[qubit] && delta_phase_pre[qubit + num_qubits] { + // println!("=> Adding Y-gate on {}", qubit); + gate_seq.push(( + StandardGate::YGate, + smallvec![], + smallvec![Qubit(qubit as u32)], + )); + } else if delta_phase_pre[qubit] { + // println!("=> Adding Z-gate on {}", qubit); + gate_seq.push(( + StandardGate::ZGate, + smallvec![], + smallvec![Qubit(qubit as u32)], + )); + } else if delta_phase_pre[qubit + num_qubits] { + // println!("=> Adding X-gate on {}", qubit); + gate_seq.push(( + StandardGate::XGate, + smallvec![], + smallvec![Qubit(qubit as u32)], + )); + } + } + + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/linear/mod.rs b/crates/accelerate/src/synthesis/linear/mod.rs index 2fa158ea761f..b184a170fa5f 100644 --- a/crates/accelerate/src/synthesis/linear/mod.rs +++ b/crates/accelerate/src/synthesis/linear/mod.rs @@ -14,7 +14,7 @@ use crate::QiskitError; use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2, PyReadwriteArray2}; use pyo3::prelude::*; -mod utils; +pub mod utils; #[pyfunction] #[pyo3(signature = (mat, ncols=None, full_elim=false))] diff --git a/crates/accelerate/src/synthesis/mod.rs b/crates/accelerate/src/synthesis/mod.rs index db28751437f6..1b9908ef80cf 100644 --- a/crates/accelerate/src/synthesis/mod.rs +++ b/crates/accelerate/src/synthesis/mod.rs @@ -10,7 +10,8 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -mod linear; +mod clifford; +pub mod linear; mod permutation; use pyo3::prelude::*; @@ -18,7 +19,8 @@ use pyo3::wrap_pymodule; #[pymodule] pub fn synthesis(m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pymodule!(permutation::permutation))?; m.add_wrapped(wrap_pymodule!(linear::linear))?; + m.add_wrapped(wrap_pymodule!(permutation::permutation))?; + m.add_wrapped(wrap_pymodule!(clifford::clifford))?; Ok(()) } diff --git a/qiskit/__init__.py b/qiskit/__init__.py index aca555da8cb8..9aaa7a76a68e 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -82,6 +82,7 @@ sys.modules["qiskit._accelerate.vf2_layout"] = _accelerate.vf2_layout sys.modules["qiskit._accelerate.synthesis.permutation"] = _accelerate.synthesis.permutation sys.modules["qiskit._accelerate.synthesis.linear"] = _accelerate.synthesis.linear +sys.modules["qiskit._accelerate.synthesis.clifford"] = _accelerate.synthesis.clifford from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/synthesis/clifford/clifford_decompose_greedy.py b/qiskit/synthesis/clifford/clifford_decompose_greedy.py index 784e6706d62e..0a679a8a7a6f 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_greedy.py +++ b/qiskit/synthesis/clifford/clifford_decompose_greedy.py @@ -12,22 +12,16 @@ """ Circuit synthesis for the Clifford class. """ -# pylint: disable=invalid-name # --------------------------------------------------------------------- # Synthesis based on Bravyi et. al. greedy clifford compiler # --------------------------------------------------------------------- - -import numpy as np from qiskit.circuit import QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.quantum_info import Clifford, Pauli -from qiskit.quantum_info.operators.symplectic.clifford_circuits import ( - _append_cx, - _append_h, - _append_s, - _append_swap, +from qiskit.quantum_info import Clifford + +from qiskit._accelerate.synthesis.clifford import ( + synth_clifford_greedy as synth_clifford_greedy_inner, ) @@ -56,296 +50,8 @@ def synth_clifford_greedy(clifford: Clifford) -> QuantumCircuit: *Clifford Circuit Optimization with Templates and Symbolic Pauli Gates*, `arXiv:2105.02291 [quant-ph] `_ """ - - num_qubits = clifford.num_qubits - circ = QuantumCircuit(num_qubits, name=str(clifford)) - qubit_list = list(range(num_qubits)) - clifford_cpy = clifford.copy() - - # Reducing the original Clifford to identity - # via symplectic Gaussian elimination - while len(qubit_list) > 0: - # Calculate the adjoint of clifford_cpy without the phase - clifford_adj = clifford_cpy.copy() - tmp = clifford_adj.destab_x.copy() - clifford_adj.destab_x = clifford_adj.stab_z.T - clifford_adj.destab_z = clifford_adj.destab_z.T - clifford_adj.stab_x = clifford_adj.stab_x.T - clifford_adj.stab_z = tmp.T - - list_greedy_cost = [] - for qubit in qubit_list: - pauli_x = Pauli("I" * (num_qubits - qubit - 1) + "X" + "I" * qubit) - pauli_x = pauli_x.evolve(clifford_adj, frame="s") - - pauli_z = Pauli("I" * (num_qubits - qubit - 1) + "Z" + "I" * qubit) - pauli_z = pauli_z.evolve(clifford_adj, frame="s") - list_pairs = [] - pauli_count = 0 - - # Compute the CNOT cost in order to find the qubit with the minimal cost - for i in qubit_list: - typeq = _from_pair_paulis_to_type(pauli_x, pauli_z, i) - list_pairs.append(typeq) - pauli_count += 1 - cost = _compute_greedy_cost(list_pairs) - list_greedy_cost.append([cost, qubit]) - - _, min_qubit = (sorted(list_greedy_cost))[0] - - # Gaussian elimination step for the qubit with minimal CNOT cost - pauli_x = Pauli("I" * (num_qubits - min_qubit - 1) + "X" + "I" * min_qubit) - pauli_x = pauli_x.evolve(clifford_adj, frame="s") - - pauli_z = Pauli("I" * (num_qubits - min_qubit - 1) + "Z" + "I" * min_qubit) - pauli_z = pauli_z.evolve(clifford_adj, frame="s") - - # Compute the decoupling operator of cliff_ox and cliff_oz - decouple_circ, decouple_cliff = _calc_decoupling( - pauli_x, pauli_z, qubit_list, min_qubit, num_qubits, clifford_cpy - ) - circ = circ.compose(decouple_circ) - - # Now the clifford acts trivially on min_qubit - clifford_cpy = decouple_cliff.adjoint().compose(clifford_cpy) - qubit_list.remove(min_qubit) - - # Add the phases (Pauli gates) to the Clifford circuit - for qubit in range(num_qubits): - stab = clifford_cpy.stab_phase[qubit] - destab = clifford_cpy.destab_phase[qubit] - if destab and stab: - circ.y(qubit) - elif not destab and stab: - circ.x(qubit) - elif destab and not stab: - circ.z(qubit) - - return circ - - -# --------------------------------------------------------------------- -# Helper functions for Bravyi et. al. greedy clifford compiler -# --------------------------------------------------------------------- - -# Global arrays of the 16 pairs of Pauli operators -# divided into 5 equivalence classes under the action of single-qubit Cliffords - -# Class A - canonical representative is 'XZ' -A_class = [ - [[False, True], [True, True]], # 'XY' - [[False, True], [True, False]], # 'XZ' - [[True, True], [False, True]], # 'YX' - [[True, True], [True, False]], # 'YZ' - [[True, False], [False, True]], # 'ZX' - [[True, False], [True, True]], -] # 'ZY' - -# Class B - canonical representative is 'XX' -B_class = [ - [[True, False], [True, False]], # 'ZZ' - [[False, True], [False, True]], # 'XX' - [[True, True], [True, True]], -] # 'YY' - -# Class C - canonical representative is 'XI' -C_class = [ - [[True, False], [False, False]], # 'ZI' - [[False, True], [False, False]], # 'XI' - [[True, True], [False, False]], -] # 'YI' - -# Class D - canonical representative is 'IZ' -D_class = [ - [[False, False], [False, True]], # 'IX' - [[False, False], [True, False]], # 'IZ' - [[False, False], [True, True]], -] # 'IY' - -# Class E - only 'II' -E_class = [[[False, False], [False, False]]] # 'II' - - -def _from_pair_paulis_to_type(pauli_x, pauli_z, qubit): - """Converts a pair of Paulis pauli_x and pauli_z into a type""" - - type_x = [pauli_x.z[qubit], pauli_x.x[qubit]] - type_z = [pauli_z.z[qubit], pauli_z.x[qubit]] - return [type_x, type_z] - - -def _compute_greedy_cost(list_pairs): - """Compute the CNOT cost of one step of the algorithm""" - - A_num = 0 - B_num = 0 - C_num = 0 - D_num = 0 - - for pair in list_pairs: - if pair in A_class: - A_num += 1 - elif pair in B_class: - B_num += 1 - elif pair in C_class: - C_num += 1 - elif pair in D_class: - D_num += 1 - - if (A_num % 2) == 0: - raise QiskitError("Symplectic Gaussian elimination fails.") - - # Calculate the CNOT cost - cost = 3 * (A_num - 1) / 2 + (B_num + 1) * (B_num > 0) + C_num + D_num - if list_pairs[0] not in A_class: # additional SWAP - cost += 3 - - return cost - - -def _calc_decoupling(pauli_x, pauli_z, qubit_list, min_qubit, num_qubits, cliff): - """Calculate a decoupling operator D: - D^{-1} * Ox * D = x1 - D^{-1} * Oz * D = z1 - and reduce the clifford such that it will act trivially on min_qubit - """ - - circ = QuantumCircuit(num_qubits) - - # decouple_cliff is initialized to an identity clifford - decouple_cliff = cliff.copy() - num_qubits = decouple_cliff.num_qubits - decouple_cliff.phase = np.zeros(2 * num_qubits) - decouple_cliff.symplectic_matrix = np.eye(2 * num_qubits) - - qubit0 = min_qubit # The qubit for the symplectic Gaussian elimination - - # Reduce the pair of Paulis to a representative in the equivalence class - # ['XZ', 'XX', 'XI', 'IZ', 'II'] by adding single-qubit gates - for qubit in qubit_list: - - typeq = _from_pair_paulis_to_type(pauli_x, pauli_z, qubit) - - if typeq in [ - [[True, True], [False, False]], # 'YI' - [[True, True], [True, True]], # 'YY' - [[True, True], [True, False]], - ]: # 'YZ': - circ.s(qubit) - _append_s(decouple_cliff, qubit) - - elif typeq in [ - [[True, False], [False, False]], # 'ZI' - [[True, False], [True, False]], # 'ZZ' - [[True, False], [False, True]], # 'ZX' - [[False, False], [False, True]], - ]: # 'IX' - circ.h(qubit) - _append_h(decouple_cliff, qubit) - - elif typeq in [ - [[False, False], [True, True]], # 'IY' - [[True, False], [True, True]], - ]: # 'ZY' - circ.s(qubit) - circ.h(qubit) - _append_s(decouple_cliff, qubit) - _append_h(decouple_cliff, qubit) - - elif typeq == [[True, True], [False, True]]: # 'YX' - circ.h(qubit) - circ.s(qubit) - _append_h(decouple_cliff, qubit) - _append_s(decouple_cliff, qubit) - - elif typeq == [[False, True], [True, True]]: # 'XY' - circ.s(qubit) - circ.h(qubit) - circ.s(qubit) - _append_s(decouple_cliff, qubit) - _append_h(decouple_cliff, qubit) - _append_s(decouple_cliff, qubit) - - # Reducing each pair of Paulis (except of qubit0) to 'II' - # by adding two-qubit gates and single-qubit gates - A_qubits = [] - B_qubits = [] - C_qubits = [] - D_qubits = [] - - for qubit in qubit_list: - typeq = _from_pair_paulis_to_type(pauli_x, pauli_z, qubit) - if typeq in A_class: - A_qubits.append(qubit) - elif typeq in B_class: - B_qubits.append(qubit) - elif typeq in C_class: - C_qubits.append(qubit) - elif typeq in D_class: - D_qubits.append(qubit) - - if len(A_qubits) % 2 != 1: - raise QiskitError("Symplectic Gaussian elimination fails.") - - if qubit0 not in A_qubits: # SWAP qubit0 and qubitA - qubitA = A_qubits[0] - circ.swap(qubit0, qubitA) - _append_swap(decouple_cliff, qubit0, qubitA) - if qubit0 in B_qubits: - B_qubits.remove(qubit0) - B_qubits.append(qubitA) - A_qubits.remove(qubitA) - A_qubits.append(qubit0) - elif qubit0 in C_qubits: - C_qubits.remove(qubit0) - C_qubits.append(qubitA) - A_qubits.remove(qubitA) - A_qubits.append(qubit0) - elif qubit0 in D_qubits: - D_qubits.remove(qubit0) - D_qubits.append(qubitA) - A_qubits.remove(qubitA) - A_qubits.append(qubit0) - else: - A_qubits.remove(qubitA) - A_qubits.append(qubit0) - - # Reduce pairs in Class C to 'II' - for qubit in C_qubits: - circ.cx(qubit0, qubit) - _append_cx(decouple_cliff, qubit0, qubit) - - # Reduce pairs in Class D to 'II' - for qubit in D_qubits: - circ.cx(qubit, qubit0) - _append_cx(decouple_cliff, qubit, qubit0) - - # Reduce pairs in Class B to 'II' - if len(B_qubits) > 1: - for qubit in B_qubits[1:]: - qubitB = B_qubits[0] - circ.cx(qubitB, qubit) - _append_cx(decouple_cliff, qubitB, qubit) - - if len(B_qubits) > 0: - qubitB = B_qubits[0] - circ.cx(qubit0, qubitB) - circ.h(qubitB) - circ.cx(qubitB, qubit0) - _append_cx(decouple_cliff, qubit0, qubitB) - _append_h(decouple_cliff, qubitB) - _append_cx(decouple_cliff, qubitB, qubit0) - - # Reduce pairs in Class A (except of qubit0) to 'II' - Alen = int((len(A_qubits) - 1) / 2) - if Alen > 0: - A_qubits.remove(qubit0) - for qubit in range(Alen): - circ.cx(A_qubits[2 * qubit + 1], A_qubits[2 * qubit]) - circ.cx(A_qubits[2 * qubit], qubit0) - circ.cx(qubit0, A_qubits[2 * qubit + 1]) - _append_cx(decouple_cliff, A_qubits[2 * qubit + 1], A_qubits[2 * qubit]) - _append_cx(decouple_cliff, A_qubits[2 * qubit], qubit0) - _append_cx(decouple_cliff, qubit0, A_qubits[2 * qubit + 1]) - - return circ, decouple_cliff + circuit = QuantumCircuit._from_circuit_data( + synth_clifford_greedy_inner(clifford.tableau.astype(bool)) + ) + circuit.name = str(clifford) + return circuit diff --git a/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml b/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml new file mode 100644 index 000000000000..044fb7d44315 --- /dev/null +++ b/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml @@ -0,0 +1,6 @@ +--- +upgrade_synthesis: + - | + The function :func:`.synth_clifford_greedy` that synthesizes :class:`.Clifford` operators + was ported to Rust, leading to a significant increase in performance for all numbers of + qubits. For Cliffords over 50 qubits, the speedup is on the order of 1000 times. diff --git a/test/python/synthesis/test_clifford_sythesis.py b/test/python/synthesis/test_clifford_sythesis.py index 887f1af5ad99..8ca11f1ef251 100644 --- a/test/python/synthesis/test_clifford_sythesis.py +++ b/test/python/synthesis/test_clifford_sythesis.py @@ -16,6 +16,7 @@ import numpy as np from ddt import ddt from qiskit.circuit.random import random_clifford_circuit +from qiskit.quantum_info import random_clifford from qiskit.quantum_info.operators import Clifford from qiskit.synthesis.clifford import ( synth_clifford_full, @@ -99,8 +100,7 @@ def test_synth_greedy(self, num_qubits): rng = np.random.default_rng(1234) samples = 50 for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) + target = random_clifford(num_qubits, rng) synth_circ = synth_clifford_greedy(target) value = Clifford(synth_circ) self.assertEqual(value, target) From ba486b739daf8ba410f7c0943fb035d689110dc2 Mon Sep 17 00:00:00 2001 From: Hirmay Sandesara <56473003+Hirmay@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:17:55 +0530 Subject: [PATCH 09/15] Add Uniform State Preparation (#12112) * Update state_preparation.py * Update state_preparation.py * Add files via upload * Update __init__.py * Update test_generalized_uniform_superposition_gate.py * Update test_generalized_uniform_superposition_gate.py * Update test_gate_definitions.py * made the format consistent with StatePreparation class * Put description and arguments in init * replaced assert with raise ValueError * small mistake in Returns * added test cases for ValueError cases * Update state_preparation.py * incorporate Steve's helpful suggestions * small bug * test_case fix * function for returning M * oops..forgot "" marks * Update state_preparation.py * removed get methods for num_qubit and M * added power of 2 condition * included test function when num_qubits is None * blacked init * blacked state_prep * blacked test_generalized_uniform_superposition_gate.py * reblacked state_prep * reblacked test_generalized_uniform_superposition_gate.py * shorterned max line length * reblacked state_prep * reblacked state_prep * for pyline * pylinted state_preparation.py * pylinted test_generalized_uniform_superposition_gate.py * pylinted test_gate_definitions.py * pylinted test_generalized_uniform_superposition_gate.py * Added GeneralizedUniformSuperposition gate Added GeneralizedUniformSuperposition gate class to the StatePreparation file in Circuit library. * modified: releasenotes/notes/generalized-uniform-superposition-gate-3bd95ffdf05ef18c.yaml * Updated release notes based on Steve's suggestions * Update release note * implemented the changes * fixed error * Update test_uniform_superposition_gate.py * Update uniform-superposition-gate-3bd95ffdf05ef18c.yaml * Update uniform-superposition-gate-3bd95ffdf05ef18c.yaml * Update qiskit/circuit/library/data_preparation/state_preparation.py Sounds good! Co-authored-by: Julien Gacon * Update qiskit/circuit/library/data_preparation/state_preparation.py Okay! Co-authored-by: Julien Gacon * Update qiskit/circuit/library/data_preparation/state_preparation.py oh that's interesting...I didn't know that. Thanks for the info! Co-authored-by: Julien Gacon * Update releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml Ahhh...that's nice! Co-authored-by: Julien Gacon * Update releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml You're quite right, will help for others who might look at the code in future. Co-authored-by: Luciano Bello * Update state_preparation.py incorporated Julien's changes! * Update test_uniform_superposition_gate.py Incorporated Julien's optimization suggestion. * Update uniform-superposition-gate-3bd95ffdf05ef18c.yaml implemented both reviewers' suggestions. * Update uniform-superposition-gate-3bd95ffdf05ef18c.yaml removed SV24 * Update state_preparation.py * Update state_preparation.py * Update test_uniform_superposition_gate.py * Update uniform-superposition-gate-3bd95ffdf05ef18c.yaml * Update qiskit/circuit/library/data_preparation/state_preparation.py Co-authored-by: Julien Gacon * blacked state_preparation.py * Update test_uniform_superposition_gate.py * pylinted state_preparation.py * removed transpile test_uniform_superposition_gate.py --------- Co-authored-by: Julien Gacon Co-authored-by: Luciano Bello --- .../library/data_preparation/__init__.py | 11 ++- .../data_preparation/state_preparation.py | 96 +++++++++++++++++- ...m-superposition-gate-3bd95ffdf05ef18c.yaml | 27 +++++ test/python/circuit/test_gate_definitions.py | 1 + .../test_uniform_superposition_gate.py | 98 +++++++++++++++++++ 5 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml create mode 100644 test/python/circuit/test_uniform_superposition_gate.py diff --git a/qiskit/circuit/library/data_preparation/__init__.py b/qiskit/circuit/library/data_preparation/__init__.py index 38611c911fa4..192308a3a7f5 100644 --- a/qiskit/circuit/library/data_preparation/__init__.py +++ b/qiskit/circuit/library/data_preparation/__init__.py @@ -41,7 +41,14 @@ from .pauli_feature_map import PauliFeatureMap from .z_feature_map import ZFeatureMap from .zz_feature_map import ZZFeatureMap -from .state_preparation import StatePreparation +from .state_preparation import StatePreparation, UniformSuperpositionGate from .initializer import Initialize -__all__ = ["PauliFeatureMap", "ZFeatureMap", "ZZFeatureMap", "StatePreparation", "Initialize"] +__all__ = [ + "PauliFeatureMap", + "ZFeatureMap", + "ZZFeatureMap", + "StatePreparation", + "UniformSuperpositionGate", + "Initialize", +] diff --git a/qiskit/circuit/library/data_preparation/state_preparation.py b/qiskit/circuit/library/data_preparation/state_preparation.py index 1d9ad7f7b082..26c37334cfe4 100644 --- a/qiskit/circuit/library/data_preparation/state_preparation.py +++ b/qiskit/circuit/library/data_preparation/state_preparation.py @@ -25,7 +25,9 @@ from qiskit.circuit.library.standard_gates.s import SGate, SdgGate from qiskit.circuit.library.generalized_gates import Isometry from qiskit.circuit.exceptions import CircuitError -from qiskit.quantum_info.states.statevector import Statevector # pylint: disable=cyclic-import +from qiskit.quantum_info.states.statevector import ( + Statevector, +) # pylint: disable=cyclic-import _EPS = 1e-10 # global variable used to chop very small numbers to zero @@ -240,3 +242,95 @@ def validate_parameter(self, parameter): def _return_repeat(self, exponent: float) -> "Gate": return Gate(name=f"{self.name}*{exponent}", num_qubits=self.num_qubits, params=[]) + + +class UniformSuperpositionGate(Gate): + r"""Implements a uniform superposition state. + + This gate is used to create the uniform superposition state + :math:`\frac{1}{\sqrt{M}} \sum_{j=0}^{M-1} |j\rangle` when it acts on an input + state :math:`|0...0\rangle`. Note, that `M` is not required to be + a power of 2, in which case the uniform superposition could be + prepared by a single layer of Hadamard gates. + + .. note:: + + This class uses the Shukla-Vedula algorithm [1], which only needs + :math:`O(\log_2 (M))` qubits and :math:`O(\log_2 (M))` gates, + to prepare the superposition. + + **References:** + [1]: A. Shukla and P. Vedula (2024), An efficient quantum algorithm for preparation + of uniform quantum superposition states, `Quantum Inf Process 23, 38 + `_. + """ + + def __init__( + self, + num_superpos_states: int = 2, + num_qubits: Optional[int] = None, + ): + r""" + Args: + num_superpos_states (int): + A positive integer M = num_superpos_states (> 1) representing the number of computational + basis states with an amplitude of 1/sqrt(M) in the uniform superposition + state (:math:`\frac{1}{\sqrt{M}} \sum_{j=0}^{M-1} |j\rangle`, where + :math:`1< M <= 2^n`). Note that the remaining (:math:`2^n - M`) computational basis + states have zero amplitudes. Here M need not be an integer power of 2. + + num_qubits (int): + A positive integer representing the number of qubits used. If num_qubits is None + or is not specified, then num_qubits is set to ceil(log2(num_superpos_states)). + + Raises: + ValueError: num_qubits must be an integer greater than or equal to log2(num_superpos_states). + + """ + if num_superpos_states <= 1: + raise ValueError("num_superpos_states must be a positive integer greater than 1.") + if num_qubits is None: + num_qubits = int(math.ceil(math.log2(num_superpos_states))) + else: + if not (isinstance(num_qubits, int) and (num_qubits >= math.log2(num_superpos_states))): + raise ValueError( + "num_qubits must be an integer greater than or equal to log2(num_superpos_states)." + ) + super().__init__("USup", num_qubits, [num_superpos_states]) + + def _define(self): + + qc = QuantumCircuit(self._num_qubits) + + num_superpos_states = self.params[0] + + if ( + num_superpos_states & (num_superpos_states - 1) + ) == 0: # if num_superpos_states is an integer power of 2 + m = int(math.log2(num_superpos_states)) + qc.h(range(m)) + self.definition = qc + return + + n_value = [int(x) for x in reversed(np.binary_repr(num_superpos_states))] + k = len(n_value) + l_value = [index for (index, item) in enumerate(n_value) if item == 1] # Locations of '1's + + qc.x(l_value[1:k]) + m_current_value = 2 ** l_value[0] + theta = -2 * np.arccos(np.sqrt(m_current_value / num_superpos_states)) + + if l_value[0] > 0: # if num_superpos_states is even + qc.h(range(l_value[0])) + qc.ry(theta, l_value[1]) + qc.ch(l_value[1], range(l_value[0], l_value[1]), ctrl_state="0") + + for m in range(1, len(l_value) - 1): + theta = -2 * np.arccos( + np.sqrt(2 ** l_value[m] / (num_superpos_states - m_current_value)) + ) + qc.cry(theta, l_value[m], l_value[m + 1], ctrl_state="0") + qc.ch(l_value[m + 1], range(l_value[m], l_value[m + 1]), ctrl_state="0") + m_current_value = m_current_value + 2 ** l_value[m] + + self.definition = qc diff --git a/releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml b/releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml new file mode 100644 index 000000000000..6017979748ea --- /dev/null +++ b/releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml @@ -0,0 +1,27 @@ +--- +features: + - | + Implemented :class:`.UniformSuperpositionGate` class, which allows + the creation of a uniform superposition state using + the Shukla-Vedula algorithm. This feature facilitates the + creation of quantum circuits that produce a uniform superposition + state :math:`\frac{1}{\sqrt{M}} \sum_{j=0}^{M-1} |j\rangle`, where + :math:`M` is a positive integer representing the number of + computational basis states with an amplitude of + :math:`\frac{1}{\sqrt{M}}`. This implementation supports the + efficient creation of uniform superposition states, + requiring only :math:`O(\log_2 (M))` qubits and + :math:`O(\log_2 (M))` gates. Usage example: + + .. code-block:: python + + from qiskit import QuantumCircuit + from qiskit.circuit.library.data_preparation import UniformSuperpositionGate + + M = 5 + num_qubits = 3 + usp_gate = UniformSuperpositionGate(M, num_qubits) + qc = QuantumCircuit(num_qubits) + qc.append(usp_gate, list(range(num_qubits))) + + qc.draw() diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 38bf7046cae5..c5df22a0e8a1 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -283,6 +283,7 @@ class TestGateEquivalenceEqual(QiskitTestCase): "ClassicalFunction", "ClassicalElement", "StatePreparation", + "UniformSuperpositionGate", "LinearFunction", "PermutationGate", "Commuting2qBlock", diff --git a/test/python/circuit/test_uniform_superposition_gate.py b/test/python/circuit/test_uniform_superposition_gate.py new file mode 100644 index 000000000000..019d6144ac5e --- /dev/null +++ b/test/python/circuit/test_uniform_superposition_gate.py @@ -0,0 +1,98 @@ +# 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. + +""" +Uniform Superposition Gate test. +""" + +import unittest +import math +from test import QiskitTestCase +import numpy as np +from ddt import ddt, data + +from qiskit import QuantumCircuit +from qiskit.quantum_info import Operator, Statevector + +from qiskit.circuit.library.data_preparation import ( + UniformSuperpositionGate, +) + + +@ddt +class TestUniformSuperposition(QiskitTestCase): + """Test initialization with UniformSuperpositionGate class""" + + @data(2, 3, 5) + def test_uniform_superposition_gate(self, num_superpos_states): + """Test Uniform Superposition Gate""" + n = int(math.ceil(math.log2(num_superpos_states))) + desired_sv = (1 / np.sqrt(num_superpos_states)) * np.array( + [1.0] * num_superpos_states + [0.0] * (2**n - num_superpos_states) + ) + gate = UniformSuperpositionGate(num_superpos_states, n) + actual_sv = Statevector(gate) + np.testing.assert_allclose(desired_sv, actual_sv) + + @data(2, 3, 5, 13) + def test_inverse_uniform_superposition_gate(self, num_superpos_states): + """Test Inverse Uniform Superposition Gate""" + n = int(math.ceil(math.log2(num_superpos_states))) + gate = UniformSuperpositionGate(num_superpos_states, n) + qc = QuantumCircuit(n) + qc.append(gate, list(range(n))) + qc.append(gate.inverse(annotated=True), list(range(n))) + actual_unitary_matrix = np.array(Operator(qc).data) + desired_unitary_matrix = np.eye(2**n) + np.testing.assert_allclose(desired_unitary_matrix, actual_unitary_matrix, atol=1e-14) + + @data(-2, -1, 0, 1) + def test_incompatible_num_superpos_states(self, num_superpos_states): + """Test error raised if num_superpos_states not valid""" + n = 1 + with self.assertRaises(ValueError): + UniformSuperpositionGate(num_superpos_states, n) + + @data(1, 2, 3, 4) + def test_incompatible_int_num_superpos_states_and_qubit_args(self, n): + """Test error raised if number of qubits not compatible with integer + state num_superpos_states (n >= log2(num_superpos_states) )""" + num_superpos_states = 50 + with self.assertRaises(ValueError): + UniformSuperpositionGate(num_superpos_states, n) + + @data(2, 3, 5) + def test_extra_qubits(self, num_superpos_states): + """Tests for cases where n >= log2(num_superpos_states)""" + num_extra_qubits = 2 + n = int(math.ceil(math.log2(num_superpos_states))) + num_extra_qubits + desired_sv = (1 / np.sqrt(num_superpos_states)) * np.array( + [1.0] * num_superpos_states + [0.0] * (2**n - num_superpos_states) + ) + gate = UniformSuperpositionGate(num_superpos_states, n) + actual_sv = Statevector(gate) + np.testing.assert_allclose(desired_sv, actual_sv) + + @data(2, 3, 5) + def test_no_qubit_args(self, num_superpos_states): + """Test Uniform Superposition Gate without passing the number of qubits as an argument""" + n = int(math.ceil(math.log2(num_superpos_states))) + desired_sv = (1 / np.sqrt(num_superpos_states)) * np.array( + [1.0] * num_superpos_states + [0.0] * (2**n - num_superpos_states) + ) + gate = UniformSuperpositionGate(num_superpos_states) + actual_sv = Statevector(gate) + np.testing.assert_allclose(desired_sv, actual_sv) + + +if __name__ == "__main__": + unittest.main() From 419f40e9e72b4981a6536d08d6b0fa91c13021d5 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 3 Jul 2024 14:04:47 +0300 Subject: [PATCH 10/15] fixing synthesis release notes (#12715) --- releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml | 2 +- releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml | 2 +- .../notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml b/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml index 6bd6761e0355..532d3e8fa55e 100644 --- a/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml +++ b/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml @@ -1,5 +1,5 @@ --- -upgrade_synthesis: +features_synthesis: - | Port :func:`.synth_permutation_acg`, used to synthesize qubit permutations, to Rust. This produces an approximate 3x performance improvement on 1000 qubit circuits. diff --git a/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml b/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml index e770aa1ca31b..bd8c969be934 100644 --- a/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml +++ b/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml @@ -1,4 +1,4 @@ --- -upgrade_synthesis: +features_synthesis: - | Port :func:`.synth_permutation_basic`, used to synthesize qubit permutations, to Rust. diff --git a/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml b/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml index 044fb7d44315..ea492e29a7fc 100644 --- a/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml +++ b/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml @@ -1,5 +1,5 @@ --- -upgrade_synthesis: +features_synthesis: - | The function :func:`.synth_clifford_greedy` that synthesizes :class:`.Clifford` operators was ported to Rust, leading to a significant increase in performance for all numbers of From 9571ea1b3aea4c3ff89ee71f03b31b53d5bfa49a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 3 Jul 2024 10:00:10 -0400 Subject: [PATCH 11/15] Avoid Python op creation in commutative cancellation (#12701) * Avoid Python op creation in commutative cancellation This commit updates the commutative cancellation and commutation analysis transpiler pass. It builds off of #12692 to adjust access patterns in the python transpiler path to avoid eagerly creating a Python space operation object. The goal of this PR is to mitigate the performance regression on these passes introduced by the extra conversion cost of #12459. * Remove stray print * Don't add __array__ to DAGOpNode or CircuitInstruction --- crates/circuit/src/circuit_instruction.rs | 6 ++++ crates/circuit/src/dag_node.rs | 30 ++++++++++++++++ crates/circuit/src/operations.rs | 34 ++++++++++++++++-- qiskit/circuit/commutation_checker.py | 35 ++++++++++++++++++- .../optimization/commutation_analysis.py | 9 +---- .../optimization/commutative_cancellation.py | 14 ++++---- 6 files changed, 111 insertions(+), 17 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index d6516722fbac..ed1c358cbc5b 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -460,6 +460,12 @@ impl CircuitInstruction { .and_then(|attrs| attrs.unit.as_deref()) } + pub fn is_parameterized(&self) -> bool { + self.params + .iter() + .any(|x| matches!(x, Param::ParameterExpression(_))) + } + /// 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 44ba5f7a6bf0..55a40c83dc39 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -14,6 +14,7 @@ use crate::circuit_instruction::{ convert_py_to_operation_type, operation_type_to_py, CircuitInstruction, ExtraInstructionAttributes, }; +use crate::imports::QUANTUM_CIRCUIT; use crate::operations::Operation; use numpy::IntoPyArray; use pyo3::prelude::*; @@ -228,6 +229,16 @@ impl DAGOpNode { Ok(()) } + #[getter] + fn num_qubits(&self) -> u32 { + self.instruction.operation.num_qubits() + } + + #[getter] + fn num_clbits(&self) -> u32 { + self.instruction.operation.num_clbits() + } + #[getter] fn get_qargs(&self, py: Python) -> Py { self.instruction.qubits.clone_ref(py) @@ -259,6 +270,10 @@ impl DAGOpNode { self.instruction.params.to_object(py) } + pub fn is_parameterized(&self) -> bool { + self.instruction.is_parameterized() + } + #[getter] fn matrix(&self, py: Python) -> Option { let matrix = self.instruction.operation.matrix(&self.instruction.params); @@ -325,6 +340,21 @@ impl DAGOpNode { } } + #[getter] + fn definition<'py>(&self, py: Python<'py>) -> PyResult>> { + let definition = self + .instruction + .operation + .definition(&self.instruction.params); + definition + .map(|data| { + QUANTUM_CIRCUIT + .get_bound(py) + .call_method1(intern!(py, "_from_circuit_data"), (data,)) + }) + .transpose() + } + /// Sets the Instruction name corresponding to the op for this node #[setter] fn set_name(&mut self, py: Python, new_name: PyObject) -> PyResult<()> { diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 77458e2fc35f..3bfef81d29ce 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -715,8 +715,38 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::RXGate => todo!("Add when we have R"), - Self::RYGate => todo!("Add when we have R"), + Self::RXGate => Python::with_gil(|py| -> Option { + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::RGate, + smallvec![theta.clone(), FLOAT_ZERO], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RYGate => Python::with_gil(|py| -> Option { + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::RGate, + smallvec![theta.clone(), Param::Float(PI / 2.0)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::RZGate => Python::with_gil(|py| -> Option { let theta = ¶ms[0]; Some( diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py index e758674829f8..79f04a65714d 100644 --- a/qiskit/circuit/commutation_checker.py +++ b/qiskit/circuit/commutation_checker.py @@ -21,6 +21,7 @@ from qiskit.circuit.operation import Operation from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.quantum_info.operators import Operator +from qiskit._accelerate.circuit import StandardGate _skipped_op_names = {"measure", "reset", "delay", "initialize"} _no_cache_op_names = {"annotated"} @@ -57,6 +58,23 @@ def __init__(self, standard_gate_commutations: dict = None, cache_max_entries: i self._cache_miss = 0 self._cache_hit = 0 + def commute_nodes( + self, + op1, + op2, + max_num_qubits: int = 3, + ) -> bool: + """Checks if two DAGOpNodes commute.""" + qargs1 = op1.qargs + cargs1 = op2.cargs + if not isinstance(op1._raw_op, StandardGate): + op1 = op1.op + qargs2 = op2.qargs + cargs2 = op2.cargs + if not isinstance(op2._raw_op, StandardGate): + op2 = op2.op + return self.commute(op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits) + def commute( self, op1: Operation, @@ -255,9 +273,15 @@ def is_commutation_skipped(op, qargs, max_num_qubits): if getattr(op, "is_parameterized", False) and op.is_parameterized(): return True + from qiskit.dagcircuit.dagnode import DAGOpNode + # we can proceed if op has defined: to_operator, to_matrix and __array__, or if its definition can be # recursively resolved by operations that have a matrix. We check this by constructing an Operator. - if (hasattr(op, "to_matrix") and hasattr(op, "__array__")) or hasattr(op, "to_operator"): + if ( + isinstance(op, DAGOpNode) + or (hasattr(op, "to_matrix") and hasattr(op, "__array__")) + or hasattr(op, "to_operator") + ): return False return False @@ -409,6 +433,15 @@ def _commute_matmul( first_qarg = tuple(qarg[q] for q in first_qargs) second_qarg = tuple(qarg[q] for q in second_qargs) + from qiskit.dagcircuit.dagnode import DAGOpNode + + # If we have a DAGOpNode here we've received a StandardGate definition from + # rust and we can manually pull the matrix to use for the Operators + if isinstance(first_ops, DAGOpNode): + first_ops = first_ops.matrix + if isinstance(second_op, DAGOpNode): + second_op = second_op.matrix + # try to generate an Operator out of op, if this succeeds we can determine commutativity, otherwise # return false try: diff --git a/qiskit/transpiler/passes/optimization/commutation_analysis.py b/qiskit/transpiler/passes/optimization/commutation_analysis.py index eddb659f0a25..61c77de552b9 100644 --- a/qiskit/transpiler/passes/optimization/commutation_analysis.py +++ b/qiskit/transpiler/passes/optimization/commutation_analysis.py @@ -72,14 +72,7 @@ def run(self, dag): does_commute = ( isinstance(current_gate, DAGOpNode) and isinstance(prev_gate, DAGOpNode) - and self.comm_checker.commute( - current_gate.op, - current_gate.qargs, - current_gate.cargs, - prev_gate.op, - prev_gate.qargs, - prev_gate.cargs, - ) + and self.comm_checker.commute_nodes(current_gate, prev_gate) ) if not does_commute: break diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index 4c6c487a0ea3..5c0b7317aabd 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -24,7 +24,7 @@ from qiskit.circuit.library.standard_gates.rx import RXGate from qiskit.circuit.library.standard_gates.p import PhaseGate from qiskit.circuit.library.standard_gates.rz import RZGate -from qiskit.circuit import ControlFlowOp +from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES _CUTOFF_PRECISION = 1e-5 @@ -138,14 +138,14 @@ def run(self, dag): total_phase = 0.0 for current_node in run: if ( - getattr(current_node.op, "condition", None) is not None + current_node.condition is not None or len(current_node.qargs) != 1 or current_node.qargs[0] != run_qarg ): raise RuntimeError("internal error") if current_node.name in ["p", "u1", "rz", "rx"]: - current_angle = float(current_node.op.params[0]) + current_angle = float(current_node.params[0]) elif current_node.name in ["z", "x"]: current_angle = np.pi elif current_node.name == "t": @@ -159,8 +159,8 @@ def run(self, dag): # Compose gates total_angle = current_angle + total_angle - if current_node.op.definition: - total_phase += current_node.op.definition.global_phase + if current_node.definition: + total_phase += current_node.definition.global_phase # Replace the data of the first node in the run if cancel_set_key[0] == "z_rotation": @@ -200,7 +200,9 @@ def _handle_control_flow_ops(self, dag): """ pass_manager = PassManager([CommutationAnalysis(), self]) - for node in dag.op_nodes(ControlFlowOp): + for node in dag.op_nodes(): + if node.name not in CONTROL_FLOW_OP_NAMES: + continue mapped_blocks = [] for block in node.op.blocks: new_circ = pass_manager.run(block) From 5a4d598966f4ffea84dfc7871543d3e6268f90ce Mon Sep 17 00:00:00 2001 From: Shraddha Aangiras <63237790+shraddha-aangiras@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:54:31 +0530 Subject: [PATCH 12/15] Fixed typo leading to missing examples (#12689) --- qiskit/transpiler/layout.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/layout.py b/qiskit/transpiler/layout.py index 4117e2987bb6..bece19671794 100644 --- a/qiskit/transpiler/layout.py +++ b/qiskit/transpiler/layout.py @@ -454,7 +454,7 @@ class TranspileLayout: qubits in the circuit as it fits the circuit to the target backend. For example, let the input circuit be: - .. plot: + .. plot:: :include-source: from qiskit.circuit import QuantumCircuit, QuantumRegister @@ -469,7 +469,7 @@ class TranspileLayout: Suppose that during the layout stage the transpiler reorders the qubits to be: - .. plot: + .. plot:: :include-source: from qiskit import QuantumCircuit @@ -497,7 +497,7 @@ class TranspileLayout: the transpiler needs to insert swap gates, and the output circuit becomes: - .. plot: + .. plot:: :include-source: from qiskit import QuantumCircuit From eed8f452457c8e9fc93cded53a467a4b1a2ba506 Mon Sep 17 00:00:00 2001 From: Hwa Date: Wed, 3 Jul 2024 23:53:25 +0800 Subject: [PATCH 13/15] Add warning about Tweedledum support in ClassicalFunction (#12652) * Add warning about Tweedledum support in ClassicalFunction * Update qiskit/circuit/classicalfunction/__init__.py Co-authored-by: Luciano Bello * Update __init__.py line break to comply to pylint --------- Co-authored-by: Luciano Bello --- qiskit/circuit/classicalfunction/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qiskit/circuit/classicalfunction/__init__.py b/qiskit/circuit/classicalfunction/__init__.py index a072d910f97a..b045227b167e 100644 --- a/qiskit/circuit/classicalfunction/__init__.py +++ b/qiskit/circuit/classicalfunction/__init__.py @@ -51,6 +51,14 @@ def grover_oracle(a: Int1, b: Int1, c: Int1, d: Int1) -> Int1: Following Qiskit's little-endian bit ordering convention, the left-most bit (``a``) is the most significant bit and the right-most bit (``d``) is the least significant bit. +.. warning:: + + The functionality of `qiskit.circuit.classicalfunction` requires `tweedledum`, + which isn't available on all platforms (up to Python version 3.11). + See `tweedledum installation guide + `_ + for more details. + Supplementary Information ========================= From a897d707f76205615a2403fdc9aad8cf9a9f3673 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Wed, 3 Jul 2024 12:26:15 -0400 Subject: [PATCH 14/15] Accept `Option<&str>` instead of `&Option`, etc (#12593) * Accept Option<&str> instead of &Option, etc In a few places, this removes unnecessary object copies. Accept a wider range of input types, and more idiomatic input types in a few functions. This affects code added in the gates-in-rust PR. The great majority of the changes here were obsoleted by #12594. The original commit has been cherry picked on top of main. * Remove unnecessary as_ref() --- crates/circuit/src/circuit_data.rs | 8 +------- crates/circuit/src/parameter_table.rs | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index f10911cc440f..501645415874 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -1048,13 +1048,7 @@ impl CircuitData { Ok(PySet::new_bound(py, self.param_table.uuid_map.values())?.unbind()) } - pub fn pop_param( - &mut self, - py: Python, - uuid: u128, - name: String, - default: PyObject, - ) -> PyObject { + pub fn pop_param(&mut self, py: Python, uuid: u128, name: &str, default: PyObject) -> PyObject { match self.param_table.pop(uuid, name) { Some(res) => res.into_py(py), None => default.clone_ref(py), diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs index 48c779eed3a3..9e5b31245391 100644 --- a/crates/circuit/src/parameter_table.rs +++ b/crates/circuit/src/parameter_table.rs @@ -153,8 +153,8 @@ impl ParamTable { self.uuid_map.clear(); } - pub fn pop(&mut self, key: u128, name: String) -> Option { - self.names.remove(&name); + pub fn pop(&mut self, key: u128, name: &str) -> Option { + self.names.remove(name); self.uuid_map.remove(&key); self.table.remove(&key) } From 51bf91e6ebdfef20eca0667eb51687119f10ea4b Mon Sep 17 00:00:00 2001 From: Shraddha Aangiras <63237790+shraddha-aangiras@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:34:10 +0530 Subject: [PATCH 15/15] Fixed typo in RZZGate example eqn (#12678) Co-authored-by: Luciano Bello --- qiskit/circuit/library/standard_gates/rzz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/library/standard_gates/rzz.py b/qiskit/circuit/library/standard_gates/rzz.py index 119dd370e20c..554ad4954a31 100644 --- a/qiskit/circuit/library/standard_gates/rzz.py +++ b/qiskit/circuit/library/standard_gates/rzz.py @@ -72,7 +72,7 @@ class RZZGate(Gate): .. math:: - R_{ZZ}(\theta = \pi) = - Z \otimes Z + R_{ZZ}(\theta = \pi) = - i Z \otimes Z .. math::