From b6c61661272c2e242963c416e64a9a2e050ea25d Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 20 Jun 2024 18:15:25 +0100 Subject: [PATCH 01/15] Invalidate `parameters` cache on circuit copy (#12619) Previously, the caching of the parameter view could persist between copies of the circuit because it was part of the `copy.copy`. --- qiskit/circuit/quantumcircuit.py | 2 ++ .../fix-parameter-cache-05eac2f24477ccb8.yaml | 7 ++++++ test/python/circuit/test_parameters.py | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index a88dfd43ea4b..ee52e3308a94 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -3674,6 +3674,8 @@ def copy_empty_like( cpy._data = CircuitData( self._data.qubits, self._data.clbits, global_phase=self._data.global_phase ) + # Invalidate parameters caching. + cpy._parameters = None cpy._calibrations = _copy.deepcopy(self._calibrations) cpy._metadata = _copy.deepcopy(self._metadata) diff --git a/releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml b/releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml new file mode 100644 index 000000000000..05ac759569f7 --- /dev/null +++ b/releasenotes/notes/fix-parameter-cache-05eac2f24477ccb8.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + The :attr:`.QuantumCircuit.parameters` attribute will now correctly be empty + when using :meth:`.QuantumCircuit.copy_empty_like` on a parametric circuit. + Previously, an internal cache would be copied over without invalidation. + Fix `#12617 `__. diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 0095f87be9ae..f841f969c00c 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -203,6 +203,29 @@ def test_parameters_property_by_index(self): for i, vi in enumerate(v): self.assertEqual(vi, qc.parameters[i]) + def test_parameters_property_independent_after_copy(self): + """Test that any `parameters` property caching is invalidated after a copy operation.""" + a = Parameter("a") + b = Parameter("b") + c = Parameter("c") + + qc1 = QuantumCircuit(1) + qc1.rz(a, 0) + self.assertEqual(set(qc1.parameters), {a}) + + qc2 = qc1.copy_empty_like() + self.assertEqual(set(qc2.parameters), set()) + + qc3 = qc1.copy() + self.assertEqual(set(qc3.parameters), {a}) + qc3.rz(b, 0) + self.assertEqual(set(qc3.parameters), {a, b}) + self.assertEqual(set(qc1.parameters), {a}) + + qc1.rz(c, 0) + self.assertEqual(set(qc1.parameters), {a, c}) + self.assertEqual(set(qc3.parameters), {a, b}) + def test_get_parameter(self): """Test the `get_parameter` method.""" x = Parameter("x") From b8de17f9082089275ae979c21cf03b8e99e245b3 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 21 Jun 2024 11:15:07 +0100 Subject: [PATCH 02/15] Localise `py_op` caching data in `RefCell` (#12594) This localises the caching concerns of the `PackedInstruction::py_op` field into a method `unpack_py_op`, which can now update the cache through an immutable reference (if no other immutable references are taken out). Having the new method to encapsulate the `cache_pyops` feature simplifies the large `#[cfg(feature = "cache_pyop")]` gates. --- crates/circuit/src/circuit_data.rs | 363 ++-------------------- crates/circuit/src/circuit_instruction.rs | 82 ++++- 2 files changed, 102 insertions(+), 343 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index da35787e3207..07f4579a4cd3 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -10,10 +10,13 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +#[cfg(feature = "cache_pygates")] +use std::cell::RefCell; + use crate::bit_data::BitData; use crate::circuit_instruction::{ - convert_py_to_operation_type, operation_type_and_data_to_py, CircuitInstruction, - ExtraInstructionAttributes, OperationInput, PackedInstruction, + convert_py_to_operation_type, CircuitInstruction, ExtraInstructionAttributes, OperationInput, + PackedInstruction, }; use crate::imports::{BUILTIN_LIST, QUBIT}; use crate::interner::{IndexedInterner, Interner, InternerKey}; @@ -489,66 +492,40 @@ impl CircuitData { .getattr(intern!(py, "deepcopy"))?; for inst in &mut res.data { match &mut inst.op { - OperationType::Standard(_) => { - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } - } + OperationType::Standard(_) => {} OperationType::Gate(ref mut op) => { op.gate = deepcopy.call1((&op.gate,))?.unbind(); - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } OperationType::Instruction(ref mut op) => { op.instruction = deepcopy.call1((&op.instruction,))?.unbind(); - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } OperationType::Operation(ref mut op) => { op.operation = deepcopy.call1((&op.operation,))?.unbind(); - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } }; + #[cfg(feature = "cache_pygates")] + { + *inst.py_op.borrow_mut() = None; + } } } else if copy_instructions { for inst in &mut res.data { match &mut inst.op { - OperationType::Standard(_) => { - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } - } + OperationType::Standard(_) => {} OperationType::Gate(ref mut op) => { op.gate = op.gate.call_method0(py, intern!(py, "copy"))?; - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } OperationType::Instruction(ref mut op) => { op.instruction = op.instruction.call_method0(py, intern!(py, "copy"))?; - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } OperationType::Operation(ref mut op) => { op.operation = op.operation.call_method0(py, intern!(py, "copy"))?; - #[cfg(feature = "cache_pygates")] - { - inst.py_op = None; - } } }; + #[cfg(feature = "cache_pygates")] + { + *inst.py_op.borrow_mut() = None; + } } } Ok(res) @@ -589,87 +566,10 @@ impl CircuitData { /// Args: /// func (Callable[[:class:`~.Operation`], None]): /// The callable to invoke. - #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn foreach_op(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter() { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - - let op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )?; - func.call1((op,))?; - } - Ok(()) - } - - /// Invokes callable ``func`` with each instruction's operation. - /// - /// Args: - /// func (Callable[[:class:`~.Operation`], None]): - /// The callable to invoke. - #[cfg(feature = "cache_pygates")] - #[pyo3(signature = (func))] - pub fn foreach_op(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { - for inst in self.data.iter_mut() { - let op = match &inst.py_op { - Some(op) => op.clone_ref(py), - None => { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - let new_op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )?; - inst.py_op = Some(new_op.clone_ref(py)); - new_op - } - }; - func.call1((op,))?; + func.call1((inst.unpack_py_op(py)?,))?; } Ok(()) } @@ -680,88 +580,10 @@ impl CircuitData { /// Args: /// func (Callable[[int, :class:`~.Operation`], None]): /// The callable to invoke. - #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn foreach_op_indexed(&self, py: Python<'_>, func: &Bound) -> PyResult<()> { for (index, inst) in self.data.iter().enumerate() { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - - let op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )?; - func.call1((index, op))?; - } - Ok(()) - } - - /// Invokes callable ``func`` with the positional index and operation - /// of each instruction. - /// - /// Args: - /// func (Callable[[int, :class:`~.Operation`], None]): - /// The callable to invoke. - #[cfg(feature = "cache_pygates")] - #[pyo3(signature = (func))] - pub fn foreach_op_indexed(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { - for (index, inst) in self.data.iter_mut().enumerate() { - let op = match &inst.py_op { - Some(op) => op.clone_ref(py), - None => { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - let new_op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )?; - inst.py_op = Some(new_op.clone_ref(py)); - new_op - } - }; - func.call1((index, op))?; + func.call1((index, inst.unpack_py_op(py)?))?; } Ok(()) } @@ -779,49 +601,23 @@ impl CircuitData { /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): /// A callable used to map original operation to their /// replacements. - #[cfg(not(feature = "cache_pygates"))] #[pyo3(signature = (func))] pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { for inst in self.data.iter_mut() { - let old_op = match &inst.op { - OperationType::Standard(op) => { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - if condition.is_some() { - operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )? - } else { - op.into_py(py) + let py_op = { + if let OperationType::Standard(op) = inst.op { + match inst.extra_attrs.as_deref() { + None + | Some(ExtraInstructionAttributes { + condition: None, .. + }) => op.into_py(py), + _ => inst.unpack_py_op(py)?, } + } else { + inst.unpack_py_op(py)? } - OperationType::Gate(op) => op.gate.clone_ref(py), - OperationType::Instruction(op) => op.instruction.clone_ref(py), - OperationType::Operation(op) => op.operation.clone_ref(py), }; - let result: OperationInput = func.call1((old_op,))?.extract()?; + let result: OperationInput = func.call1((py_op,))?.extract()?; match result { OperationInput::Standard(op) => { inst.op = OperationType::Standard(op); @@ -836,7 +632,7 @@ impl CircuitData { inst.op = OperationType::Operation(op); } OperationInput::Object(new_op) => { - let new_inst_details = convert_py_to_operation_type(py, new_op)?; + let new_inst_details = convert_py_to_operation_type(py, new_op.clone_ref(py))?; inst.op = new_inst_details.operation; inst.params = new_inst_details.params; if new_inst_details.label.is_some() @@ -851,103 +647,10 @@ impl CircuitData { condition: new_inst_details.condition, })) } - } - } - } - Ok(()) - } - - /// Invokes callable ``func`` with each instruction's operation, - /// replacing the operation with the result. - /// - /// .. note:: - /// - /// This is only to be used by map_vars() in quantumcircuit.py it - /// assumes that a full Python instruction will only be returned from - /// standard gates iff a condition is set. - /// - /// Args: - /// func (Callable[[:class:`~.Operation`], :class:`~.Operation`]): - /// A callable used to map original operation to their - /// replacements. - #[cfg(feature = "cache_pygates")] - #[pyo3(signature = (func))] - pub fn map_ops(&mut self, py: Python<'_>, func: &Bound) -> PyResult<()> { - for inst in self.data.iter_mut() { - let old_op = match &inst.py_op { - Some(op) => op.clone_ref(py), - None => match &inst.op { - OperationType::Standard(op) => { - let label; - let duration; - let unit; - let condition; - match &inst.extra_attrs { - Some(extra_attrs) => { - label = &extra_attrs.label; - duration = &extra_attrs.duration; - unit = &extra_attrs.unit; - condition = &extra_attrs.condition; - } - None => { - label = &None; - duration = &None; - unit = &None; - condition = &None; - } - } - if condition.is_some() { - let new_op = operation_type_and_data_to_py( - py, - &inst.op, - &inst.params, - label, - duration, - unit, - condition, - )?; - inst.py_op = Some(new_op.clone_ref(py)); - new_op - } else { - op.into_py(py) - } - } - OperationType::Gate(op) => op.gate.clone_ref(py), - OperationType::Instruction(op) => op.instruction.clone_ref(py), - OperationType::Operation(op) => op.operation.clone_ref(py), - }, - }; - let result: OperationInput = func.call1((old_op,))?.extract()?; - match result { - OperationInput::Standard(op) => { - inst.op = OperationType::Standard(op); - } - OperationInput::Gate(op) => { - inst.op = OperationType::Gate(op); - } - OperationInput::Instruction(op) => { - inst.op = OperationType::Instruction(op); - } - OperationInput::Operation(op) => { - inst.op = OperationType::Operation(op); - } - OperationInput::Object(new_op) => { - let new_inst_details = convert_py_to_operation_type(py, new_op.clone_ref(py))?; - inst.op = new_inst_details.operation; - inst.params = new_inst_details.params; - if new_inst_details.label.is_some() - || new_inst_details.duration.is_some() - || new_inst_details.unit.is_some() - || new_inst_details.condition.is_some() + #[cfg(feature = "cache_pygates")] { - inst.extra_attrs = Some(Box::new(ExtraInstructionAttributes { - label: new_inst_details.label, - duration: new_inst_details.duration, - unit: new_inst_details.unit, - condition: new_inst_details.condition, - })) + *inst.py_op.borrow_mut() = Some(new_op); } - inst.py_op = Some(new_op); } } } @@ -1537,7 +1240,7 @@ impl CircuitData { params: inst.params.clone(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] - py_op: inst.py_op.clone(), + py_op: RefCell::new(inst.py_op.clone()), }) } @@ -1557,7 +1260,7 @@ impl CircuitData { params: inst.params.clone(), extra_attrs: inst.extra_attrs.clone(), #[cfg(feature = "cache_pygates")] - py_op: inst.py_op.clone(), + py_op: RefCell::new(inst.py_op.clone()), }) } } diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 2bb90367082d..5179190d8aa2 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -10,6 +10,9 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +#[cfg(feature = "cache_pygates")] +use std::cell::RefCell; + use pyo3::basic::CompareOp; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; @@ -47,8 +50,61 @@ pub(crate) struct PackedInstruction { pub clbits_id: Index, pub params: SmallVec<[Param; 3]>, pub extra_attrs: Option>, + #[cfg(feature = "cache_pygates")] - pub py_op: Option, + /// This is hidden in a `RefCell` because, while that has additional memory-usage implications + /// while we're still building with the feature enabled, we intend to remove the feature in the + /// future, and hiding the cache within a `RefCell` lets us keep the cache transparently in our + /// interfaces, without needing various functions to unnecessarily take `&mut` references. + pub py_op: RefCell>, +} + +impl PackedInstruction { + /// Build a reference to the Python-space operation object (the `Gate`, etc) packed into this + /// instruction. This may construct the reference if the `PackedInstruction` is a standard + /// gate with no already stored operation. + /// + /// A standard-gate operation object returned by this function is disconnected from the + /// containing circuit; updates to its label, duration, unit and condition will not be + /// propagated back. + pub fn unpack_py_op(&self, py: Python) -> PyResult> { + #[cfg(feature = "cache_pygates")] + { + if let Some(cached_op) = self.py_op.borrow().as_ref() { + return Ok(cached_op.clone_ref(py)); + } + } + let (label, duration, unit, condition) = match self.extra_attrs.as_deref() { + Some(ExtraInstructionAttributes { + label, + duration, + unit, + condition, + }) => ( + label.as_deref(), + duration.as_ref(), + unit.as_deref(), + condition.as_ref(), + ), + None => (None, None, None, None), + }; + let out = operation_type_and_data_to_py( + py, + &self.op, + &self.params, + label, + duration, + unit, + condition, + )?; + #[cfg(feature = "cache_pygates")] + { + if let Ok(mut cell) = self.py_op.try_borrow_mut() { + cell.get_or_insert_with(|| out.clone_ref(py)); + } + } + Ok(out) + } } /// A single instruction in a :class:`.QuantumCircuit`, comprised of the :attr:`operation` and @@ -666,20 +722,20 @@ pub(crate) fn operation_type_to_py( let (label, duration, unit, condition) = match &circuit_inst.extra_attrs { None => (None, None, None, None), Some(extra_attrs) => ( - extra_attrs.label.clone(), - extra_attrs.duration.clone(), - extra_attrs.unit.clone(), - extra_attrs.condition.clone(), + extra_attrs.label.as_deref(), + extra_attrs.duration.as_ref(), + extra_attrs.unit.as_deref(), + extra_attrs.condition.as_ref(), ), }; operation_type_and_data_to_py( py, &circuit_inst.operation, &circuit_inst.params, - &label, - &duration, - &unit, - &condition, + label, + duration, + unit, + condition, ) } @@ -692,10 +748,10 @@ pub(crate) fn operation_type_and_data_to_py( py: Python, operation: &OperationType, params: &[Param], - label: &Option, - duration: &Option, - unit: &Option, - condition: &Option, + label: Option<&str>, + duration: Option<&PyObject>, + unit: Option<&str>, + condition: Option<&PyObject>, ) -> PyResult { match &operation { OperationType::Standard(op) => { From 91f0c70885e6b58d1168e43c330373197b1c19ee Mon Sep 17 00:00:00 2001 From: "Tiago R. Cunha" <155388148+cstiago@users.noreply.github.com> Date: Fri, 21 Jun 2024 08:29:48 -0300 Subject: [PATCH 03/15] Fix type hint in SolovayKitaevDecomposition (#12627) The return type hint of `find_basic_approximation` method changes from `Gate` to `GateSequence` in `SolovayKitaevDecomposition` class, as implied by `self.basic_approximations`. With no remaining mentions of `Gate`, its corresponding import statement is removed. --- qiskit/synthesis/discrete_basis/solovay_kitaev.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/qiskit/synthesis/discrete_basis/solovay_kitaev.py b/qiskit/synthesis/discrete_basis/solovay_kitaev.py index 2e5cfeafcecd..2c8df5bd1b6d 100644 --- a/qiskit/synthesis/discrete_basis/solovay_kitaev.py +++ b/qiskit/synthesis/discrete_basis/solovay_kitaev.py @@ -16,8 +16,6 @@ import numpy as np -from qiskit.circuit.gate import Gate - from .gate_sequence import GateSequence from .commutator_decompose import commutator_decompose from .generate_basis_approximations import generate_basic_approximations, _1q_gates, _1q_inverses @@ -157,14 +155,14 @@ def _recurse(self, sequence: GateSequence, n: int, check_input: bool = True) -> w_n1 = self._recurse(w_n, n - 1, check_input=check_input) return v_n1.dot(w_n1).dot(v_n1.adjoint()).dot(w_n1.adjoint()).dot(u_n1) - def find_basic_approximation(self, sequence: GateSequence) -> Gate: - """Finds gate in ``self._basic_approximations`` that best represents ``sequence``. + def find_basic_approximation(self, sequence: GateSequence) -> GateSequence: + """Find ``GateSequence`` in ``self._basic_approximations`` that approximates ``sequence``. Args: - sequence: The gate to find the approximation to. + sequence: ``GateSequence`` to find the approximation to. Returns: - Gate in basic approximations that is closest to ``sequence``. + ``GateSequence`` in ``self._basic_approximations`` that approximates ``sequence``. """ # TODO explore using a k-d tree here From 8752f900f090bb46bc1e2468d76f99a797b9182f Mon Sep 17 00:00:00 2001 From: Jim Garrison Date: Fri, 21 Jun 2024 08:18:28 -0400 Subject: [PATCH 04/15] Add remaining tests for `ParameterVector` and tweak its `repr` (#12597) --- qiskit/circuit/parametervector.py | 2 +- test/python/circuit/test_parameters.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/qiskit/circuit/parametervector.py b/qiskit/circuit/parametervector.py index 151e3e7fea73..7b32395e1430 100644 --- a/qiskit/circuit/parametervector.py +++ b/qiskit/circuit/parametervector.py @@ -87,7 +87,7 @@ def __str__(self): return f"{self.name}, {[str(item) for item in self.params]}" def __repr__(self): - return f"{self.__class__.__name__}(name={self.name}, length={len(self)})" + return f"{self.__class__.__name__}(name={repr(self.name)}, length={len(self)})" def resize(self, length): """Resize the parameter vector. If necessary, new elements are generated. diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index f841f969c00c..f580416eccf5 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1398,6 +1398,21 @@ def test_parametervector_resize(self): self.assertEqual(element, vec[1]) self.assertListEqual([param.name for param in vec], _paramvec_names("x", 3)) + def test_parametervector_repr(self): + """Test the __repr__ method of the parameter vector.""" + vec = ParameterVector("x", 2) + self.assertEqual(repr(vec), "ParameterVector(name='x', length=2)") + + def test_parametervector_str(self): + """Test the __str__ method of the parameter vector.""" + vec = ParameterVector("x", 2) + self.assertEqual(str(vec), "x, ['x[0]', 'x[1]']") + + def test_parametervector_index(self): + """Test the index method of the parameter vector.""" + vec = ParameterVector("x", 2) + self.assertEqual(vec.index(vec[1]), 1) + def test_raise_if_sub_unknown_parameters(self): """Verify we raise if asked to sub a parameter not in self.""" x = Parameter("x") From 22c145aa0e978e6cdefd59d0ff43e371b2d6487b Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 21 Jun 2024 13:28:57 +0100 Subject: [PATCH 05/15] Fix `CircuitInstruction` legacy iterable typing (#12630) The legacy 3-tuple format of `CircuitInstruction` still exposes the object in the old tuple-like way of `(Operation, list[Qubit], list[Clbit])`, rather than the new-style attribute access using tuples for the qargs and cargs. This was inadvertantly changed when it moved to Rust. --- crates/circuit/src/circuit_instruction.rs | 12 ++++++++++-- test/python/circuit/test_circuit_data.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 5179190d8aa2..93e73ccbc42f 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -542,7 +542,11 @@ impl CircuitInstruction { Ok(PyTuple::new_bound( py, - [op, self.qubits.to_object(py), self.clbits.to_object(py)], + [ + op, + self.qubits.bind(py).to_list().into(), + self.clbits.bind(py).to_list().into(), + ], )) } @@ -558,7 +562,11 @@ impl CircuitInstruction { }; Ok(PyTuple::new_bound( py, - [op, self.qubits.to_object(py), self.clbits.to_object(py)], + [ + op, + self.qubits.bind(py).to_list().into(), + self.clbits.bind(py).to_list().into(), + ], )) } diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 6fc6e8e72bd7..35ae27b2fcfb 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -403,6 +403,22 @@ class TestQuantumCircuitInstructionData(QiskitTestCase): # but are included as tests to maintain compatability with the previous # list interface of circuit.data. + def test_iteration_of_data_entry(self): + """Verify that the base types of the legacy tuple iteration are correct, since they're + different to attribute access.""" + qc = QuantumCircuit(3, 3) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 2) + qc.measure([0, 1, 2], [0, 1, 2]) + + def to_legacy(instruction): + return (instruction.operation, list(instruction.qubits), list(instruction.clbits)) + + expected = [to_legacy(instruction) for instruction in qc.data] + actual = [tuple(instruction) for instruction in qc.data] + self.assertEqual(actual, expected) + def test_getitem_by_insertion_order(self): """Verify one can get circuit.data items in insertion order.""" qr = QuantumRegister(2) From 87aa89c19387ef7ff4177ef5144f60119da392e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Fri, 21 Jun 2024 17:25:59 +0200 Subject: [PATCH 06/15] Add Rust representation for `XXMinusYYGate` and `XXPlusYYGate` (#12606) * Add XXMinusYYGate and XXPlusYYGate, implement parameter multiplication function (naive approach). Co-authored by: Julien Gacon jul@zurich.ibm.com * * Format code * Use multiply_param in RZGate * Fix signs and indices --- crates/circuit/src/gate_matrix.rs | 46 +++++ crates/circuit/src/imports.rs | 14 +- crates/circuit/src/operations.rs | 167 ++++++++++++++---- .../library/standard_gates/xx_minus_yy.py | 3 + .../library/standard_gates/xx_plus_yy.py | 3 + 5 files changed, 194 insertions(+), 39 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index ad8c918e73bc..23ce94869227 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -212,6 +212,7 @@ pub static ISWAP_GATE: [[Complex64; 4]; 4] = [ ]; pub static S_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(0., 1.)]]; + pub static SDG_GATE: [[Complex64; 2]; 2] = [[c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(0., -1.)]]; @@ -219,6 +220,7 @@ pub static T_GATE: [[Complex64; 2]; 2] = [ [c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(FRAC_1_SQRT_2, FRAC_1_SQRT_2)], ]; + pub static TDG_GATE: [[Complex64; 2]; 2] = [ [c64(1., 0.), c64(0., 0.)], [c64(0., 0.), c64(FRAC_1_SQRT_2, -FRAC_1_SQRT_2)], @@ -246,3 +248,47 @@ pub fn u_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] { [c64(0., phi).exp() * sin, c64(0., phi + lam).exp() * cos], ] } + +#[inline] +pub fn xx_minus_yy_gate(theta: f64, beta: f64) -> [[Complex64; 4]; 4] { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [ + c64(cos, 0.), + c64(0., 0.), + c64(0., 0.), + c64(0., -sin) * c64(0., -beta).exp(), + ], + [c64(0., 0.), c64(1., 0.), c64(0., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., 0.), c64(1., 0.), c64(0., 0.)], + [ + c64(0., -sin) * c64(0., beta).exp(), + c64(0., 0.), + c64(0., 0.), + c64(cos, 0.), + ], + ] +} + +#[inline] +pub fn xx_plus_yy_gate(theta: f64, beta: f64) -> [[Complex64; 4]; 4] { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [c64(1., 0.), c64(0., 0.), c64(0., 0.), c64(0., 0.)], + [ + c64(0., 0.), + c64(cos, 0.), + c64(0., -sin) * c64(0., -beta).exp(), + c64(0., 0.), + ], + [ + c64(0., 0.), + c64(0., -sin) * c64(0., beta).exp(), + c64(cos, 0.), + c64(0., 0.), + ], + [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], + ] +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 8db3b88fd7d2..3a9a942db8df 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -124,13 +124,23 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ // SdgGate = 19 ["qiskit.circuit.library.standard_gates.s", "SdgGate"], // TGate = 20 - ["qiskit.circuit.library.standard_gates.s", "TGate"], + ["qiskit.circuit.library.standard_gates.t", "TGate"], // TdgGate = 21 - ["qiskit.circuit.library.standard_gates.s", "TdgGate"], + ["qiskit.circuit.library.standard_gates.t", "TdgGate"], // SXdgGate = 22 ["qiskit.circuit.library.standard_gates.sx", "SXdgGate"], // iSWAPGate = 23 ["qiskit.circuit.library.standard_gates.iswap", "iSwapGate"], + //XXMinusYYGate = 24 + [ + "qiskit.circuit.library.standard_gates.xx_minus_yy", + "XXMinusYYGate", + ], + //XXPlusYYGate = 25 + [ + "qiskit.circuit.library.standard_gates.xx_plus_yy", + "XXPlusYYGate", + ], ]; /// A mapping from the enum variant in crate::operations::StandardGate to the python object for the diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 9048c55d9d48..6dedd3ac2061 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -203,14 +203,16 @@ pub enum StandardGate { TdgGate = 21, SXdgGate = 22, ISwapGate = 23, + XXMinusYYGate = 24, + XXPlusYYGate = 25, } static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ - 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, + 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, ]; static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ - 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 2, 2, ]; static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ @@ -238,6 +240,8 @@ static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ "tdg", "sxdg", "iswap", + "xx_minus_yy", + "xx_plus_yy", ]; #[pymethods] @@ -287,7 +291,7 @@ impl StandardGate { // Remove this when std::mem::variant_count() is stabilized (see // https://github.com/rust-lang/rust/issues/73662 ) -pub const STANDARD_GATE_SIZE: usize = 24; +pub const STANDARD_GATE_SIZE: usize = 26; impl Operation for StandardGate { fn name(&self) -> &str { @@ -416,6 +420,18 @@ impl Operation for StandardGate { [] => Some(aview2(&gate_matrix::ISWAP_GATE).to_owned()), _ => None, }, + Self::XXMinusYYGate => match params { + [Param::Float(theta), Param::Float(beta)] => { + Some(aview2(&gate_matrix::xx_minus_yy_gate(*theta, *beta)).to_owned()) + } + _ => None, + }, + Self::XXPlusYYGate => match params { + [Param::Float(theta), Param::Float(beta)] => { + Some(aview2(&gate_matrix::xx_plus_yy_gate(*theta, *beta)).to_owned()) + } + _ => None, + }, } } @@ -502,6 +518,7 @@ impl Operation for StandardGate { }), Self::CXGate => None, Self::CCXGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; let q1 = smallvec![Qubit(1)]; let q2 = smallvec![Qubit(2)]; let q0_1 = smallvec![Qubit(0), Qubit(1)]; @@ -524,7 +541,7 @@ impl Operation for StandardGate { (Self::TGate, smallvec![], q2.clone()), (Self::HGate, smallvec![], q2), (Self::CXGate, smallvec![], q0_1.clone()), - (Self::TGate, smallvec![], smallvec![Qubit(0)]), + (Self::TGate, smallvec![], q0), (Self::TdgGate, smallvec![], q1), (Self::CXGate, smallvec![], q0_1), ], @@ -536,39 +553,20 @@ impl Operation for StandardGate { Self::RXGate => todo!("Add when we have R"), Self::RYGate => todo!("Add when we have R"), Self::RZGate => Python::with_gil(|py| -> Option { - match ¶ms[0] { - Param::Float(theta) => Some( - CircuitData::from_standard_gates( - py, - 1, - [( - Self::PhaseGate, - smallvec![Param::Float(*theta)], - smallvec![Qubit(0)], - )], - Param::Float(-0.5 * theta), - ) - .expect("Unexpected Qiskit python bug"), - ), - Param::ParameterExpression(theta) => Some( - CircuitData::from_standard_gates( - py, - 1, - [( - Self::PhaseGate, - smallvec![Param::ParameterExpression(theta.clone_ref(py))], - smallvec![Qubit(0)], - )], - Param::ParameterExpression( - theta - .call_method1(py, intern!(py, "__rmul__"), (-0.5,)) - .expect("Parameter expression for global phase failed"), - ), - ) - .expect("Unexpected Qiskit python bug"), - ), - Param::Obj(_) => unreachable!(), - } + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + smallvec![theta.clone()], + smallvec![Qubit(0)], + )], + multiply_param(theta, -0.5, py), + ) + .expect("Unexpected Qiskit python bug"), + ) }), Self::ECRGate => todo!("Add when we have RZX"), Self::SwapGate => Python::with_gil(|py| -> Option { @@ -732,6 +730,88 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::XXMinusYYGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q0_1 = smallvec![Qubit(0), Qubit(1)]; + let theta = ¶ms[0]; + let beta = ¶ms[1]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + ( + Self::RZGate, + smallvec![multiply_param(beta, -1.0, py)], + q1.clone(), + ), + (Self::RZGate, smallvec![Param::Float(-PI2)], q0.clone()), + (Self::SXGate, smallvec![], q0.clone()), + (Self::RZGate, smallvec![Param::Float(PI2)], q0.clone()), + (Self::SGate, smallvec![], q1.clone()), + (Self::CXGate, smallvec![], q0_1.clone()), + ( + Self::RYGate, + smallvec![multiply_param(theta, 0.5, py)], + q0.clone(), + ), + ( + Self::RYGate, + smallvec![multiply_param(theta, -0.5, py)], + q1.clone(), + ), + (Self::CXGate, smallvec![], q0_1), + (Self::SdgGate, smallvec![], q1.clone()), + (Self::RZGate, smallvec![Param::Float(-PI2)], q0.clone()), + (Self::SXdgGate, smallvec![], q0.clone()), + (Self::RZGate, smallvec![Param::Float(PI2)], q0), + (Self::RZGate, smallvec![beta.clone()], q1), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::XXPlusYYGate => Python::with_gil(|py| -> Option { + let q0 = smallvec![Qubit(0)]; + let q1 = smallvec![Qubit(1)]; + let q1_0 = smallvec![Qubit(1), Qubit(0)]; + let theta = ¶ms[0]; + let beta = ¶ms[1]; + Some( + CircuitData::from_standard_gates( + py, + 2, + [ + (Self::RZGate, smallvec![beta.clone()], q0.clone()), + (Self::RZGate, smallvec![Param::Float(-PI2)], q1.clone()), + (Self::SXGate, smallvec![], q1.clone()), + (Self::RZGate, smallvec![Param::Float(PI2)], q1.clone()), + (Self::SGate, smallvec![], q0.clone()), + (Self::CXGate, smallvec![], q1_0.clone()), + ( + Self::RYGate, + smallvec![multiply_param(theta, -0.5, py)], + q1.clone(), + ), + ( + Self::RYGate, + smallvec![multiply_param(theta, -0.5, py)], + q0.clone(), + ), + (Self::CXGate, smallvec![], q1_0), + (Self::SdgGate, smallvec![], q0.clone()), + (Self::RZGate, smallvec![Param::Float(-PI2)], q1.clone()), + (Self::SXdgGate, smallvec![], q1.clone()), + (Self::RZGate, smallvec![Param::Float(PI2)], q1), + (Self::RZGate, smallvec![multiply_param(beta, -1.0, py)], q0), + ], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), } } @@ -742,6 +822,19 @@ impl Operation for StandardGate { const FLOAT_ZERO: Param = Param::Float(0.0); +fn multiply_param(param: &Param, mult: f64, py: Python) -> Param { + match param { + Param::Float(theta) => Param::Float(*theta * mult), + Param::ParameterExpression(theta) => Param::ParameterExpression( + theta + .clone_ref(py) + .call_method1(py, intern!(py, "__rmul__"), (mult,)) + .expect("Parameter expression for global phase failed"), + ), + Param::Obj(_) => unreachable!(), + } +} + /// This class is used to wrap a Python side Instruction that is not in the standard library #[derive(Clone, Debug)] #[pyclass(freelist = 20, module = "qiskit._accelerate.circuit")] diff --git a/qiskit/circuit/library/standard_gates/xx_minus_yy.py b/qiskit/circuit/library/standard_gates/xx_minus_yy.py index 4bf4ab80eca2..db3c3dc89153 100644 --- a/qiskit/circuit/library/standard_gates/xx_minus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_minus_yy.py @@ -27,6 +27,7 @@ from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class XXMinusYYGate(Gate): @@ -91,6 +92,8 @@ class XXMinusYYGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.XXMinusYYGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/xx_plus_yy.py b/qiskit/circuit/library/standard_gates/xx_plus_yy.py index a82316ed7b09..7920454d0b98 100644 --- a/qiskit/circuit/library/standard_gates/xx_plus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_plus_yy.py @@ -21,6 +21,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class XXPlusYYGate(Gate): @@ -87,6 +88,8 @@ class XXPlusYYGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.XXPlusYYGate + def __init__( self, theta: ParameterValueType, From de6c6eb2f944c58ecf2258801e921e0846d41d89 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 24 Jun 2024 10:02:31 +0200 Subject: [PATCH 07/15] Follow up on #12327: circuit construction in Rust (#12605) * Follow up on #12327 also port circuit construction to rust and add a reno * move _get_ordered_swap to Rust only * drop redundant Ok(expect()) * proper synthesis structure --- crates/accelerate/src/lib.rs | 2 +- crates/accelerate/src/synthesis/mod.rs | 22 ++++++ .../src/synthesis/permutation/mod.rs | 68 +++++++++++++++++++ .../permutation/utils.rs} | 58 ++++------------ crates/pyext/src/lib.rs | 10 +-- qiskit/__init__.py | 2 +- .../library/generalized_gates/permutation.py | 17 +++-- .../synthesis/permutation/permutation_full.py | 14 +--- .../permutation/permutation_utils.py | 3 +- .../oxidize-permbasic-be27578187ac472f.yaml | 4 ++ .../synthesis/test_permutation_synthesis.py | 14 ---- 11 files changed, 124 insertions(+), 90 deletions(-) create mode 100644 crates/accelerate/src/synthesis/mod.rs create mode 100644 crates/accelerate/src/synthesis/permutation/mod.rs rename crates/accelerate/src/{permutation.rs => synthesis/permutation/utils.rs} (66%) create mode 100644 releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 3924c1de4092..dcfbdc9f1878 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -23,12 +23,12 @@ pub mod isometry; pub mod nlayout; pub mod optimize_1q_gates; pub mod pauli_exp_val; -pub mod permutation; pub mod results; pub mod sabre; pub mod sampled_exp_val; pub mod sparse_pauli_op; pub mod stochastic_swap; +pub mod synthesis; pub mod two_qubit_decompose; pub mod uc_gate; pub mod utils; diff --git a/crates/accelerate/src/synthesis/mod.rs b/crates/accelerate/src/synthesis/mod.rs new file mode 100644 index 000000000000..f1a720459211 --- /dev/null +++ b/crates/accelerate/src/synthesis/mod.rs @@ -0,0 +1,22 @@ +// 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 permutation; + +use pyo3::prelude::*; +use pyo3::wrap_pymodule; + +#[pymodule] +pub fn synthesis(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pymodule!(permutation::permutation))?; + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/permutation/mod.rs b/crates/accelerate/src/synthesis/permutation/mod.rs new file mode 100644 index 000000000000..bf0ff97848f2 --- /dev/null +++ b/crates/accelerate/src/synthesis/permutation/mod.rs @@ -0,0 +1,68 @@ +// 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 numpy::PyArrayLike1; +use smallvec::smallvec; + +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::operations::{Param, StandardGate}; +use qiskit_circuit::Qubit; + +mod utils; + +/// Checks whether an array of size N is a permutation of 0, 1, ..., N - 1. +#[pyfunction] +#[pyo3(signature = (pattern))] +pub fn _validate_permutation(py: Python, pattern: PyArrayLike1) -> PyResult { + let view = pattern.as_array(); + utils::validate_permutation(&view)?; + Ok(py.None()) +} + +/// Finds inverse of a permutation pattern. +#[pyfunction] +#[pyo3(signature = (pattern))] +pub fn _inverse_pattern(py: Python, pattern: PyArrayLike1) -> PyResult { + let view = pattern.as_array(); + let inverse_i64: Vec = utils::invert(&view).iter().map(|&x| x as i64).collect(); + Ok(inverse_i64.to_object(py)) +} + +#[pyfunction] +#[pyo3(signature = (pattern))] +pub fn _synth_permutation_basic(py: Python, pattern: PyArrayLike1) -> PyResult { + let view = pattern.as_array(); + let num_qubits = view.len(); + CircuitData::from_standard_gates( + py, + num_qubits as u32, + utils::get_ordered_swap(&view).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)?)?; + Ok(()) +} diff --git a/crates/accelerate/src/permutation.rs b/crates/accelerate/src/synthesis/permutation/utils.rs similarity index 66% rename from crates/accelerate/src/permutation.rs rename to crates/accelerate/src/synthesis/permutation/utils.rs index 31ba433ddd30..a78088bfbfa9 100644 --- a/crates/accelerate/src/permutation.rs +++ b/crates/accelerate/src/synthesis/permutation/utils.rs @@ -11,12 +11,11 @@ // that they have been altered from the originals. use ndarray::{Array1, ArrayView1}; -use numpy::PyArrayLike1; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use std::vec::Vec; -fn validate_permutation(pattern: &ArrayView1) -> PyResult<()> { +pub fn validate_permutation(pattern: &ArrayView1) -> PyResult<()> { let n = pattern.len(); let mut seen: Vec = vec![false; n]; @@ -47,7 +46,7 @@ fn validate_permutation(pattern: &ArrayView1) -> PyResult<()> { Ok(()) } -fn invert(pattern: &ArrayView1) -> Array1 { +pub fn invert(pattern: &ArrayView1) -> Array1 { let mut inverse: Array1 = Array1::zeros(pattern.len()); pattern.iter().enumerate().for_each(|(ii, &jj)| { inverse[jj as usize] = ii; @@ -55,7 +54,16 @@ fn invert(pattern: &ArrayView1) -> Array1 { inverse } -fn get_ordered_swap(pattern: &ArrayView1) -> Vec<(i64, i64)> { +/// Sorts the input permutation by iterating through the permutation list +/// and putting each element to its correct position via a SWAP (if it's not +/// at the correct position already). If ``n`` is the length of the input +/// permutation, this requires at most ``n`` SWAPs. +/// +/// More precisely, if the input permutation is a cycle of length ``m``, +/// 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)> { let mut permutation: Vec = pattern.iter().map(|&x| x as usize).collect(); let mut index_map = invert(pattern); @@ -76,45 +84,3 @@ fn get_ordered_swap(pattern: &ArrayView1) -> Vec<(i64, i64)> { swaps[..].reverse(); swaps } - -/// Checks whether an array of size N is a permutation of 0, 1, ..., N - 1. -#[pyfunction] -#[pyo3(signature = (pattern))] -fn _validate_permutation(py: Python, pattern: PyArrayLike1) -> PyResult { - let view = pattern.as_array(); - validate_permutation(&view)?; - Ok(py.None()) -} - -/// Finds inverse of a permutation pattern. -#[pyfunction] -#[pyo3(signature = (pattern))] -fn _inverse_pattern(py: Python, pattern: PyArrayLike1) -> PyResult { - let view = pattern.as_array(); - let inverse_i64: Vec = invert(&view).iter().map(|&x| x as i64).collect(); - Ok(inverse_i64.to_object(py)) -} - -/// Sorts the input permutation by iterating through the permutation list -/// and putting each element to its correct position via a SWAP (if it's not -/// at the correct position already). If ``n`` is the length of the input -/// permutation, this requires at most ``n`` SWAPs. -/// -/// More precisely, if the input permutation is a cycle of length ``m``, -/// 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. -#[pyfunction] -#[pyo3(signature = (permutation_in))] -fn _get_ordered_swap(py: Python, permutation_in: PyArrayLike1) -> PyResult { - let view = permutation_in.as_array(); - Ok(get_ordered_swap(&view).to_object(py)) -} - -#[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!(_get_ordered_swap, m)?)?; - Ok(()) -} diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index b80aad1a7a45..72f0d759099a 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -17,10 +17,10 @@ use qiskit_accelerate::{ convert_2q_block_matrix::convert_2q_block_matrix, dense_layout::dense_layout, error_map::error_map, euler_one_qubit_decomposer::euler_one_qubit_decomposer, isometry::isometry, nlayout::nlayout, optimize_1q_gates::optimize_1q_gates, - pauli_exp_val::pauli_expval, permutation::permutation, results::results, sabre::sabre, - sampled_exp_val::sampled_exp_val, sparse_pauli_op::sparse_pauli_op, - stochastic_swap::stochastic_swap, two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, - utils::utils, vf2_layout::vf2_layout, + pauli_exp_val::pauli_expval, results::results, sabre::sabre, sampled_exp_val::sampled_exp_val, + sparse_pauli_op::sparse_pauli_op, stochastic_swap::stochastic_swap, synthesis::synthesis, + two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, + vf2_layout::vf2_layout, }; #[pymodule] @@ -36,7 +36,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(nlayout))?; m.add_wrapped(wrap_pymodule!(optimize_1q_gates))?; m.add_wrapped(wrap_pymodule!(pauli_expval))?; - m.add_wrapped(wrap_pymodule!(permutation))?; + m.add_wrapped(wrap_pymodule!(synthesis))?; m.add_wrapped(wrap_pymodule!(results))?; m.add_wrapped(wrap_pymodule!(sabre))?; m.add_wrapped(wrap_pymodule!(sampled_exp_val))?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index fce544333478..5b8505654428 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -80,7 +80,7 @@ sys.modules["qiskit._accelerate.stochastic_swap"] = _accelerate.stochastic_swap sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose sys.modules["qiskit._accelerate.vf2_layout"] = _accelerate.vf2_layout -sys.modules["qiskit._accelerate.permutation"] = _accelerate.permutation +sys.modules["qiskit._accelerate.synthesis.permutation"] = _accelerate.synthesis.permutation from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/circuit/library/generalized_gates/permutation.py b/qiskit/circuit/library/generalized_gates/permutation.py index 776c69d94f01..b2d17d2bed23 100644 --- a/qiskit/circuit/library/generalized_gates/permutation.py +++ b/qiskit/circuit/library/generalized_gates/permutation.py @@ -80,15 +80,13 @@ def __init__( name = "permutation_" + np.array_str(pattern).replace(" ", ",") - circuit = QuantumCircuit(num_qubits, name=name) - super().__init__(num_qubits, name=name) # pylint: disable=cyclic-import - from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap + from qiskit.synthesis.permutation import synth_permutation_basic - for i, j in _get_ordered_swap(pattern): - circuit.swap(i, j) + circuit = synth_permutation_basic(pattern) + circuit.name = name all_qubits = self.qubits self.append(circuit.to_gate(), all_qubits) @@ -184,10 +182,11 @@ def inverse(self, annotated: bool = False): def _qasm2_decomposition(self): # pylint: disable=cyclic-import - from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap + from qiskit.synthesis.permutation import synth_permutation_basic name = f"permutation__{'_'.join(str(n) for n in self.pattern)}_" - out = QuantumCircuit(self.num_qubits, name=name) - for i, j in _get_ordered_swap(self.pattern): - out.swap(i, j) + + out = synth_permutation_basic(self.pattern) + out.name = name + return out.to_gate() diff --git a/qiskit/synthesis/permutation/permutation_full.py b/qiskit/synthesis/permutation/permutation_full.py index ff014cb3a051..c280065c2a57 100644 --- a/qiskit/synthesis/permutation/permutation_full.py +++ b/qiskit/synthesis/permutation/permutation_full.py @@ -16,8 +16,8 @@ import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit._accelerate.synthesis.permutation import _synth_permutation_basic from .permutation_utils import ( - _get_ordered_swap, _inverse_pattern, _pattern_to_cycles, _decompose_cycles, @@ -44,17 +44,7 @@ def synth_permutation_basic(pattern: list[int] | np.ndarray[int]) -> QuantumCirc Returns: The synthesized quantum circuit. """ - # This is the very original Qiskit algorithm for synthesizing permutations. - - num_qubits = len(pattern) - qc = QuantumCircuit(num_qubits) - - swaps = _get_ordered_swap(pattern) - - for swap in swaps: - qc.swap(swap[0], swap[1]) - - return qc + return QuantumCircuit._from_circuit_data(_synth_permutation_basic(pattern)) def synth_permutation_acg(pattern: list[int] | np.ndarray[int]) -> QuantumCircuit: diff --git a/qiskit/synthesis/permutation/permutation_utils.py b/qiskit/synthesis/permutation/permutation_utils.py index dbd73bfe8111..4520e18f4d06 100644 --- a/qiskit/synthesis/permutation/permutation_utils.py +++ b/qiskit/synthesis/permutation/permutation_utils.py @@ -13,9 +13,8 @@ """Utility functions for handling permutations.""" # pylint: disable=unused-import -from qiskit._accelerate.permutation import ( +from qiskit._accelerate.synthesis.permutation import ( _inverse_pattern, - _get_ordered_swap, _validate_permutation, ) diff --git a/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml b/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml new file mode 100644 index 000000000000..e770aa1ca31b --- /dev/null +++ b/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml @@ -0,0 +1,4 @@ +--- +upgrade_synthesis: + - | + Port :func:`.synth_permutation_basic`, used to synthesize qubit permutations, to Rust. diff --git a/test/python/synthesis/test_permutation_synthesis.py b/test/python/synthesis/test_permutation_synthesis.py index 050df5a3fe1c..b6a1ca9e1857 100644 --- a/test/python/synthesis/test_permutation_synthesis.py +++ b/test/python/synthesis/test_permutation_synthesis.py @@ -27,7 +27,6 @@ ) from qiskit.synthesis.permutation.permutation_utils import ( _inverse_pattern, - _get_ordered_swap, _validate_permutation, ) from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -47,19 +46,6 @@ def test_inverse_pattern(self, width): for ii, jj in enumerate(pattern): self.assertTrue(inverse[jj] == ii) - @data(4, 5, 10, 15, 20) - def test_get_ordered_swap(self, width): - """Test _get_ordered_swap function produces correct swap list.""" - np.random.seed(1) - for _ in range(5): - pattern = np.random.permutation(width) - swap_list = _get_ordered_swap(pattern) - output = list(range(width)) - for i, j in swap_list: - output[i], output[j] = output[j], output[i] - self.assertTrue(np.array_equal(pattern, output)) - self.assertLess(len(swap_list), width) - @data(10, 20) def test_invalid_permutations(self, width): """Check that _validate_permutation raises exceptions when the From bf8f398fa4ddf287c6182b39bd27b324ab11dda0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 24 Jun 2024 10:05:06 -0400 Subject: [PATCH 08/15] Add rust representation for the u1, u2, and u3 gates (#12572) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add rust representation for the u1, u2, and u3 gates This commit adds the rust representation of the U1Gate, U2Gate, and U3Gate to the `StandardGates` enum in rust. Part of #12566 * Update crates/circuit/src/imports.rs Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Fix test failures * Fix pylint pedantry --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- crates/circuit/src/gate_matrix.rs | 32 +++++ crates/circuit/src/imports.rs | 6 + crates/circuit/src/operations.rs | 74 +++++++++++- qiskit/circuit/library/standard_gates/u1.py | 3 + qiskit/circuit/library/standard_gates/u2.py | 3 + qiskit/circuit/library/standard_gates/u3.py | 3 + test/python/circuit/test_rust_equivalence.py | 25 ++-- test/python/qasm3/test_export.py | 114 +++++++++--------- .../transpiler/test_optimize_1q_gates.py | 36 +++++- 9 files changed, 224 insertions(+), 72 deletions(-) diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 23ce94869227..2e5f55d6ddcb 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -271,6 +271,38 @@ pub fn xx_minus_yy_gate(theta: f64, beta: f64) -> [[Complex64; 4]; 4] { ] } +#[inline] +pub fn u1_gate(lam: f64) -> [[Complex64; 2]; 2] { + [ + [c64(1., 0.), c64(0., 0.)], + [c64(0., 0.), c64(0., lam).exp()], + ] +} + +#[inline] +pub fn u2_gate(phi: f64, lam: f64) -> [[Complex64; 2]; 2] { + [ + [ + c64(FRAC_1_SQRT_2, 0.), + (-c64(0., lam).exp()) * FRAC_1_SQRT_2, + ], + [ + c64(0., phi).exp() * FRAC_1_SQRT_2, + c64(0., phi + lam).exp() * FRAC_1_SQRT_2, + ], + ] +} + +#[inline] +pub fn u3_gate(theta: f64, phi: f64, lam: f64) -> [[Complex64; 2]; 2] { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + [ + [c64(cos, 0.), -(c64(0., lam).exp()) * sin], + [c64(0., phi).exp() * sin, c64(0., phi + lam).exp() * cos], + ] +} + #[inline] pub fn xx_plus_yy_gate(theta: f64, beta: f64) -> [[Complex64; 4]; 4] { let cos = (theta / 2.).cos(); diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 3a9a942db8df..7160798f56bb 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -141,6 +141,12 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ "qiskit.circuit.library.standard_gates.xx_plus_yy", "XXPlusYYGate", ], + // U1Gate = 26 + ["qiskit.circuit.library.standard_gates.u1", "U1Gate"], + // U2Gate = 27 + ["qiskit.circuit.library.standard_gates.u2", "U2Gate"], + // U3Gate = 28 + ["qiskit.circuit.library.standard_gates.u3", "U3Gate"], ]; /// A mapping from the enum variant in crate::operations::StandardGate to the python object for the diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 6dedd3ac2061..451b04947388 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -205,14 +205,17 @@ pub enum StandardGate { ISwapGate = 23, XXMinusYYGate = 24, XXPlusYYGate = 25, + U1Gate = 26, + U2Gate = 27, + U3Gate = 28, } static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ - 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, + 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, ]; static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ - 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 2, 2, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 2, 2, 1, 2, 3, ]; static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ @@ -242,6 +245,9 @@ static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ "iswap", "xx_minus_yy", "xx_plus_yy", + "u1", + "u2", + "u3", ]; #[pymethods] @@ -290,8 +296,7 @@ impl StandardGate { // // Remove this when std::mem::variant_count() is stabilized (see // https://github.com/rust-lang/rust/issues/73662 ) - -pub const STANDARD_GATE_SIZE: usize = 26; +pub const STANDARD_GATE_SIZE: usize = 29; impl Operation for StandardGate { fn name(&self) -> &str { @@ -432,6 +437,22 @@ impl Operation for StandardGate { } _ => None, }, + Self::U1Gate => match params[0] { + Param::Float(val) => Some(aview2(&gate_matrix::u1_gate(val)).to_owned()), + _ => None, + }, + Self::U2Gate => match params { + [Param::Float(phi), Param::Float(lam)] => { + Some(aview2(&gate_matrix::u2_gate(*phi, *lam)).to_owned()) + } + _ => None, + }, + Self::U3Gate => match params { + [Param::Float(theta), Param::Float(phi), Param::Float(lam)] => { + Some(aview2(&gate_matrix::u3_gate(*theta, *phi, *lam)).to_owned()) + } + _ => None, + }, } } @@ -667,6 +688,21 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::U1Gate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::PhaseGate, + params.iter().cloned().collect(), + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::SdgGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( @@ -682,6 +718,21 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::U2Gate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![Param::Float(PI / 2.), params[0].clone(), params[1].clone()], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::TGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( @@ -697,6 +748,21 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::U3Gate => Python::with_gil(|py| -> Option { + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + params.iter().cloned().collect(), + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::TdgGate => Python::with_gil(|py| -> Option { Some( CircuitData::from_standard_gates( diff --git a/qiskit/circuit/library/standard_gates/u1.py b/qiskit/circuit/library/standard_gates/u1.py index 1d59cabae1f6..f141146b72dc 100644 --- a/qiskit/circuit/library/standard_gates/u1.py +++ b/qiskit/circuit/library/standard_gates/u1.py @@ -19,6 +19,7 @@ from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _ctrl_state_to_int +from qiskit._accelerate.circuit import StandardGate class U1Gate(Gate): @@ -92,6 +93,8 @@ class U1Gate(Gate): `1612.00858 `_ """ + _standard_gate = StandardGate.U1Gate + def __init__( self, theta: ParameterValueType, label: str | None = None, *, duration=None, unit="dt" ): diff --git a/qiskit/circuit/library/standard_gates/u2.py b/qiskit/circuit/library/standard_gates/u2.py index c8e4de96efec..9e59cd4c5bbd 100644 --- a/qiskit/circuit/library/standard_gates/u2.py +++ b/qiskit/circuit/library/standard_gates/u2.py @@ -18,6 +18,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class U2Gate(Gate): @@ -86,6 +87,8 @@ class U2Gate(Gate): using two X90 pulses. """ + _standard_gate = StandardGate.U2Gate + def __init__( self, phi: ParameterValueType, diff --git a/qiskit/circuit/library/standard_gates/u3.py b/qiskit/circuit/library/standard_gates/u3.py index 0eef2518a85a..f191609ea8f1 100644 --- a/qiskit/circuit/library/standard_gates/u3.py +++ b/qiskit/circuit/library/standard_gates/u3.py @@ -19,6 +19,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister +from qiskit._accelerate.circuit import StandardGate class U3Gate(Gate): @@ -80,6 +81,8 @@ class U3Gate(Gate): U3(\theta, 0, 0) = RY(\theta) """ + _standard_gate = StandardGate.U3Gate + def __init__( self, theta: ParameterValueType, diff --git a/test/python/circuit/test_rust_equivalence.py b/test/python/circuit/test_rust_equivalence.py index bb09ae4caf3f..8d6d159c0b64 100644 --- a/test/python/circuit/test_rust_equivalence.py +++ b/test/python/circuit/test_rust_equivalence.py @@ -79,12 +79,23 @@ def test_definitions(self): ) # Rust uses P but python still uses u1 elif rs_inst.operation.name == "p": - self.assertEqual(py_inst.operation.name, "u1") - self.assertEqual(rs_inst.operation.params, py_inst.operation.params) - self.assertEqual( - [py_def.find_bit(x).index for x in py_inst.qubits], - [rs_def.find_bit(x).index for x in rs_inst.qubits], - ) + if py_inst.operation.name == "u1": + self.assertEqual(py_inst.operation.name, "u1") + self.assertEqual(rs_inst.operation.params, py_inst.operation.params) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + else: + self.assertEqual(py_inst.operation.name, "u3") + self.assertEqual( + rs_inst.operation.params[0], py_inst.operation.params[2] + ) + self.assertEqual( + [py_def.find_bit(x).index for x in py_inst.qubits], + [rs_def.find_bit(x).index for x in rs_inst.qubits], + ) + else: self.assertEqual(py_inst.operation.name, rs_inst.operation.name) self.assertEqual(rs_inst.operation.params, py_inst.operation.params) @@ -102,7 +113,7 @@ def test_matrix(self): continue with self.subTest(name=name): - params = [pi] * standard_gate._num_params() + params = [0.1] * standard_gate._num_params() py_def = gate_class.base_class(*params).to_matrix() rs_def = standard_gate._to_matrix(params) np.testing.assert_allclose(rs_def, py_def) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 048c5d7852b4..6df041420882 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -2018,65 +2018,63 @@ def test_teleportation(self): qc.z(2).c_if(qc.clbits[0], 1) transpiled = transpile(qc, initial_layout=[0, 1, 2]) - first_h = transpiled.data[0].operation - u2 = first_h.definition.data[0].operation - u3_1 = u2.definition.data[0].operation - first_x = transpiled.data[-2].operation - u3_2 = first_x.definition.data[0].operation - first_z = transpiled.data[-1].operation - u1 = first_z.definition.data[0].operation - u3_3 = u1.definition.data[0].operation + id_len = len(str(id(transpiled.data[0].operation))) - expected_qasm = "\n".join( - [ - "OPENQASM 3.0;", - f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi/2, 0, pi) _gate_q_0;", - "}", - f"gate u2_{id(u2)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", - f" u3_{id(u3_1)}(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - f" u2_{id(u2)}(0, pi) _gate_q_0;", - "}", - "gate cx c, t {", - " ctrl @ U(pi, 0, pi) c, t;", - "}", - f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(pi, 0, pi) _gate_q_0;", - "}", - "gate x _gate_q_0 {", - f" u3_{id(u3_2)}(pi, 0, pi) _gate_q_0;", - "}", - f"gate u3_{id(u3_3)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", - " U(0, 0, pi) _gate_q_0;", - "}", - f"gate u1_{id(u1)}(_gate_p_0) _gate_q_0 {{", - f" u3_{id(u3_3)}(0, 0, pi) _gate_q_0;", - "}", - "gate z _gate_q_0 {", - f" u1_{id(u1)}(pi) _gate_q_0;", - "}", - "bit[2] c;", - "h $1;", - "cx $1, $2;", - "barrier $0, $1, $2;", - "cx $0, $1;", - "h $0;", - "barrier $0, $1, $2;", - "c[0] = measure $0;", - "c[1] = measure $1;", - "barrier $0, $1, $2;", - "if (c[1]) {", - " x $2;", - "}", - "if (c[0]) {", - " z $2;", - "}", - "", - ] - ) - self.assertEqual(Exporter(includes=[]).dumps(transpiled), expected_qasm) + expected_qasm = [ + "OPENQASM 3.0;", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi/2, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate h _gate_q_0 {", + re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), + "}", + "gate cx c, t {", + " ctrl @ U(pi, 0, pi) c, t;", + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(pi, 0, pi) _gate_q_0;", + "}", + "gate x _gate_q_0 {", + re.compile(r" u3_\d{%s}\(pi, 0, pi\) _gate_q_0;" % id_len), + "}", + re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), + " U(0, 0, pi) _gate_q_0;", + "}", + re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), + re.compile(r" u3_\d{%s}\(0, 0, pi\) _gate_q_0;" % id_len), + "}", + "gate z _gate_q_0 {", + re.compile(r" u1_\d{%s}\(pi\) _gate_q_0;" % id_len), + "}", + "bit[2] c;", + "h $1;", + "cx $1, $2;", + "barrier $0, $1, $2;", + "cx $0, $1;", + "h $0;", + "barrier $0, $1, $2;", + "c[0] = measure $0;", + "c[1] = measure $1;", + "barrier $0, $1, $2;", + "if (c[1]) {", + " x $2;", + "}", + "if (c[0]) {", + " z $2;", + "}", + "", + ] + res = Exporter(includes=[]).dumps(transpiled).splitlines() + for result, expected in zip(res, expected_qasm): + if isinstance(expected, str): + self.assertEqual(result, expected) + else: + self.assertTrue( + expected.search(result), f"Line {result} doesn't match regex: {expected}" + ) def test_custom_gate_with_params_bound_main_call(self): """Custom gate with unbound parameters that are bound in the main circuit""" diff --git a/test/python/transpiler/test_optimize_1q_gates.py b/test/python/transpiler/test_optimize_1q_gates.py index 9253130bedbf..e5483dd47499 100644 --- a/test/python/transpiler/test_optimize_1q_gates.py +++ b/test/python/transpiler/test_optimize_1q_gates.py @@ -19,7 +19,7 @@ from qiskit.transpiler import PassManager from qiskit.transpiler.passes import Optimize1qGates, BasisTranslator from qiskit.converters import circuit_to_dag -from qiskit.circuit import Parameter +from qiskit.circuit import Parameter, Gate from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, UGate, PhaseGate from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.target import Target @@ -323,9 +323,24 @@ def test_parameterized_expressions_in_circuits(self): def test_global_phase_u3_on_left(self): """Check proper phase accumulation with instruction with no definition.""" + + class CustomGate(Gate): + """Custom u1 gate definition.""" + + def __init__(self, lam): + super().__init__("u1", 1, [lam]) + + def _define(self): + qc = QuantumCircuit(1) + qc.p(*self.params, 0) + self.definition = qc + + def _matrix(self): + return U1Gate(*self.params).to_matrix() + qr = QuantumRegister(1) qc = QuantumCircuit(qr) - u1 = U1Gate(0.1) + u1 = CustomGate(0.1) u1.definition.global_phase = np.pi / 2 qc.append(u1, [0]) qc.global_phase = np.pi / 3 @@ -337,9 +352,24 @@ def test_global_phase_u3_on_left(self): def test_global_phase_u_on_left(self): """Check proper phase accumulation with instruction with no definition.""" + + class CustomGate(Gate): + """Custom u1 gate.""" + + def __init__(self, lam): + super().__init__("u1", 1, [lam]) + + def _define(self): + qc = QuantumCircuit(1) + qc.p(*self.params, 0) + self.definition = qc + + def _matrix(self): + return U1Gate(*self.params).to_matrix() + qr = QuantumRegister(1) qc = QuantumCircuit(qr) - u1 = U1Gate(0.1) + u1 = CustomGate(0.1) u1.definition.global_phase = np.pi / 2 qc.append(u1, [0]) qc.global_phase = np.pi / 3 From 35f6297f20be6d9b58671948adc179dad77894af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:23:42 -0400 Subject: [PATCH 09/15] Bump faer from 0.19.0 to 0.19.1 (#12645) Bumps [faer](https://github.com/sarah-ek/faer-rs) from 0.19.0 to 0.19.1. - [Changelog](https://github.com/sarah-ek/faer-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/sarah-ek/faer-rs/commits) --- updated-dependencies: - dependency-name: faer 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 ++-- crates/accelerate/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aefa3c932a04..454823748e8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,9 +307,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "faer" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ef9e1a4098a9e3a03c47bc5061406c04820552d869fd0fcd92587d07b271f0" +checksum = "41543c4de4bfb32efdffdd75cbcca5ef41b800e8a811ea4a41fb9393c6ef3bc0" dependencies = [ "bytemuck", "coe-rs", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index d9865d545437..b377a9b38a6d 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -20,7 +20,7 @@ num-traits = "0.2" num-complex.workspace = true num-bigint = "0.4" rustworkx-core = "0.14" -faer = "0.19.0" +faer = "0.19.1" itertools = "0.13.0" qiskit-circuit.workspace = true From b20a7ceb58b0ec7e49004d3ce81bd3f2144e6f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Mon, 24 Jun 2024 20:16:16 +0200 Subject: [PATCH 10/15] Add placeholders for all mising standard gates in Rust (#12646) * Add placeholders for all gates, mark TODOs * Update name for CPhase * Remove todo from Ux gates --- crates/circuit/src/imports.rs | 53 ++++++++++- crates/circuit/src/operations.rs | 156 ++++++++++++++++++++++++------- 2 files changed, 175 insertions(+), 34 deletions(-) diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 7160798f56bb..76e808d1b308 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -79,6 +79,7 @@ pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = /// /// NOTE: the order here is significant, the StandardGate variant's number must match /// index of it's entry in this table. This is all done statically for performance +// TODO: replace placeholders with actual implementation static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ // ZGate = 0 ["qiskit.circuit.library.standard_gates.z", "ZGate"], @@ -131,12 +132,12 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ ["qiskit.circuit.library.standard_gates.sx", "SXdgGate"], // iSWAPGate = 23 ["qiskit.circuit.library.standard_gates.iswap", "iSwapGate"], - //XXMinusYYGate = 24 + // XXMinusYYGate = 24 [ "qiskit.circuit.library.standard_gates.xx_minus_yy", "XXMinusYYGate", ], - //XXPlusYYGate = 25 + // XXPlusYYGate = 25 [ "qiskit.circuit.library.standard_gates.xx_plus_yy", "XXPlusYYGate", @@ -147,6 +148,54 @@ static STDGATE_IMPORT_PATHS: [[&str; 2]; STANDARD_GATE_SIZE] = [ ["qiskit.circuit.library.standard_gates.u2", "U2Gate"], // U3Gate = 28 ["qiskit.circuit.library.standard_gates.u3", "U3Gate"], + // CRXGate = 29 + ["placeholder", "placeholder"], + // CRYGate = 30 + ["placeholder", "placeholder"], + // CRZGate = 31 + ["placeholder", "placeholder"], + // RGate 32 + ["placeholder", "placeholder"], + // CHGate = 33 + ["qiskit.circuit.library.standard_gates.h", "CHGate"], + // CPhaseGate = 34 + ["qiskit.circuit.library.standard_gates.p", "CPhaseGate"], + // CSGate = 35 + ["qiskit.circuit.library.standard_gates.s", "CSGate"], + // CSdgGate = 36 + ["qiskit.circuit.library.standard_gates.s", "CSdgGate"], + // CSXGate = 37 + ["qiskit.circuit.library.standard_gates.sx", "CSXGate"], + // CSwapGate = 38 + ["qiskit.circuit.library.standard_gates.swap", "CSwapGate"], + // CUGate = 39 + ["qiskit.circuit.library.standard_gates.u", "CUGate"], + // CU1Gate = 40 + ["qiskit.circuit.library.standard_gates.u1", "CU1Gate"], + // CU3Gate = 41 + ["qiskit.circuit.library.standard_gates.u3", "CU3Gate"], + // C3XGate = 42 + ["placeholder", "placeholder"], + // C3SXGate = 43 + ["placeholder", "placeholder"], + // C4XGate = 44 + ["placeholder", "placeholder"], + // DCXGate = 45 + ["placeholder", "placeholder"], + // CCZGate = 46 + ["placeholder", "placeholder"], + // RCCXGate = 47 + ["placeholder", "placeholder"], + // RC3XGate = 48 + ["placeholder", "placeholder"], + // RXXGate = 49 + ["placeholder", "placeholder"], + // RYYGate = 50 + ["placeholder", "placeholder"], + // RZZGate = 51 + ["placeholder", "placeholder"], + // RZXGate = 52 + ["placeholder", "placeholder"], ]; /// A mapping from the enum variant in crate::operations::StandardGate to the python object for the diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 451b04947388..af7dabc86216 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -208,46 +208,106 @@ pub enum StandardGate { U1Gate = 26, U2Gate = 27, U3Gate = 28, + CRXGate = 29, + CRYGate = 30, + CRZGate = 31, + RGate = 32, + CHGate = 33, + CPhaseGate = 34, + CSGate = 35, + CSdgGate = 36, + CSXGate = 37, + CSwapGate = 38, + CUGate = 39, + CU1Gate = 40, + CU3Gate = 41, + C3XGate = 42, + C3SXGate = 43, + C4XGate = 44, + DCXGate = 45, + CCZGate = 46, + RCCXGate = 47, + RC3XGate = 48, + RXXGate = 49, + RYYGate = 50, + RZZGate = 51, + RZXGate = 52, } +// 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, 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, + 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, // 0-9 + 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, // 10-19 + 1, 1, 1, 2, 2, 2, 1, 1, 1, 34, // 20-29 + 34, 34, 34, 2, 2, 2, 2, 2, 3, 2, // 30-39 + 2, 2, 34, 34, 34, 34, 34, 34, 34, 34, // 40-49 + 34, 34, 34, // 50-52 ]; +// TODO: replace all 34s (placeholders) with actual number static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ - 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 2, 2, 1, 2, 3, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, // 0-9 + 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, // 10-19 + 0, 0, 0, 0, 2, 2, 1, 2, 3, 34, // 20-29 + 34, 34, 34, 0, 1, 0, 0, 0, 0, 3, // 30-39 + 1, 3, 34, 34, 34, 34, 34, 34, 34, 34, // 40-49 + 34, 34, 34, // 50-52 ]; static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ - "z", - "y", - "x", - "cz", - "cy", - "cx", - "ccx", - "rx", - "ry", - "rz", - "ecr", - "swap", - "sx", - "global_phase", - "id", - "h", - "p", - "u", - "s", - "sdg", - "t", - "tdg", - "sxdg", - "iswap", - "xx_minus_yy", - "xx_plus_yy", - "u1", - "u2", - "u3", + "z", // 0 + "y", // 1 + "x", // 2 + "cz", // 3 + "cy", // 4 + "cx", // 5 + "ccx", // 6 + "rx", // 7 + "ry", // 8 + "rz", // 9 + "ecr", // 10 + "swap", // 11 + "sx", // 12 + "global_phase", // 13 + "id", // 14 + "h", // 15 + "p", // 16 + "u", // 17 + "s", // 18 + "sdg", // 19 + "t", // 20 + "tdg", // 21 + "sxdg", // 22 + "iswap", // 23 + "xx_minus_yy", // 24 + "xx_plus_yy", // 25 + "u1", // 26 + "u2", // 27 + "u3", // 28 + "crx", // 29 + "cry", // 30 + "crz", // 31 + "r", // 32 + "ch", // 33 + "cp", // 34 + "cs", // 35 + "csdg", // 36 + "csx", // 37 + "cswap", // 38 + "cu", // 39 + "cu1", // 40 + "cu3", // 41 + "c3x", // 42 + "c3sx", // 43 + "c4x", // 44 + "dcx", // 45 + "ccz", // 46 + "rccx", // 47 + "rc3x", // 48 + "rxx", // 49 + "ryy", // 50 + "rzz", // 51 + "rzx", // 52 ]; #[pymethods] @@ -296,7 +356,7 @@ impl StandardGate { // // Remove this when std::mem::variant_count() is stabilized (see // https://github.com/rust-lang/rust/issues/73662 ) -pub const STANDARD_GATE_SIZE: usize = 29; +pub const STANDARD_GATE_SIZE: usize = 53; impl Operation for StandardGate { fn name(&self) -> &str { @@ -453,6 +513,21 @@ impl Operation for StandardGate { } _ => None, }, + Self::CRXGate | Self::CRYGate | Self::CRZGate => todo!(), + Self::RGate => todo!(), + Self::CHGate => todo!(), + Self::CPhaseGate => todo!(), + Self::CSGate => todo!(), + Self::CSdgGate => todo!(), + Self::CSXGate => todo!(), + Self::CSwapGate => todo!(), + Self::CUGate | Self::CU1Gate | Self::CU3Gate => todo!(), + Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), + Self::DCXGate => todo!(), + Self::CCZGate => todo!(), + Self::RCCXGate | Self::RC3XGate => todo!(), + Self::RXXGate | Self::RYYGate | Self::RZZGate => todo!(), + Self::RZXGate => todo!(), } } @@ -878,6 +953,23 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), + Self::CRXGate | Self::CRYGate | Self::CRZGate => todo!(), + Self::RGate => todo!(), + Self::CHGate => todo!(), + Self::CPhaseGate => todo!(), + Self::CSGate => todo!(), + Self::CSdgGate => todo!(), + Self::CSXGate => todo!(), + Self::CSwapGate => todo!(), + Self::CUGate => todo!(), + Self::CU1Gate => todo!(), + Self::CU3Gate => todo!(), + Self::C3XGate | Self::C3SXGate | Self::C4XGate => todo!(), + Self::DCXGate => todo!(), + Self::CCZGate => todo!(), + Self::RCCXGate | Self::RC3XGate => todo!(), + Self::RXXGate | Self::RYYGate | Self::RZZGate => todo!(), + Self::RZXGate => todo!(), } } From 8b1f75ffafc70596bcf45480aa2f6d59d822e337 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 24 Jun 2024 19:21:20 +0100 Subject: [PATCH 11/15] Deprecate tuple-like access to `CircuitInstruction` (#12640) This has been the legacy path since `CircuitInstruction` was added in gh-8093. It's more performant to use the attribute-access patterns, and with more of the internals moving to Rust and potentially needing more use of additional class methods and attributes, we need to start shifting people away from the old form. --- crates/circuit/src/circuit_instruction.rs | 39 +++++++++++++++++-- crates/circuit/src/imports.rs | 2 + ...-circuit-instruction-8a332ab09de73766.yaml | 23 +++++++++++ test/python/circuit/test_circuit_data.py | 6 ++- test/utils/base.py | 9 +++++ 5 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index 93e73ccbc42f..781a776c1566 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -14,7 +14,7 @@ use std::cell::RefCell; use pyo3::basic::CompareOp; -use pyo3::exceptions::PyValueError; +use pyo3::exceptions::{PyDeprecationWarning, PyValueError}; use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyList, PyTuple, PyType}; use pyo3::{intern, IntoPy, PyObject, PyResult}; @@ -22,7 +22,7 @@ use smallvec::{smallvec, SmallVec}; use crate::imports::{ get_std_gate_class, populate_std_gate_map, GATE, INSTRUCTION, OPERATION, - SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, + SINGLETON_CONTROLLED_GATE, SINGLETON_GATE, WARNINGS_WARN, }; use crate::interner::Index; use crate::operations::{OperationType, Param, PyGate, PyInstruction, PyOperation, StandardGate}; @@ -572,26 +572,31 @@ impl CircuitInstruction { #[cfg(not(feature = "cache_pygates"))] pub fn __getitem__(&self, py: Python<'_>, key: &Bound) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) } #[cfg(feature = "cache_pygates")] pub fn __getitem__(&mut self, py: Python<'_>, key: &Bound) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; Ok(self._legacy_format(py)?.as_any().get_item(key)?.into_py(py)) } #[cfg(not(feature = "cache_pygates"))] pub fn __iter__(&self, py: Python<'_>) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) } #[cfg(feature = "cache_pygates")] pub fn __iter__(&mut self, py: Python<'_>) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; Ok(self._legacy_format(py)?.as_any().iter()?.into_py(py)) } - pub fn __len__(&self) -> usize { - 3 + pub fn __len__(&self, py: Python) -> PyResult { + warn_on_legacy_circuit_instruction_iteration(py)?; + Ok(3) } pub fn __richcmp__( @@ -939,3 +944,29 @@ pub(crate) fn convert_py_to_operation_type( } Err(PyValueError::new_err(format!("Invalid input: {}", py_op))) } + +/// Issue a Python `DeprecationWarning` about using the legacy tuple-like interface to +/// `CircuitInstruction`. +/// +/// Beware the `stacklevel` here doesn't work quite the same way as it does in Python as Rust-space +/// calls are completely transparent to Python. +#[inline] +fn warn_on_legacy_circuit_instruction_iteration(py: Python) -> PyResult<()> { + WARNINGS_WARN + .get_bound(py) + .call1(( + intern!( + py, + concat!( + "Treating CircuitInstruction as an iterable is deprecated legacy behavior", + " since Qiskit 1.2, and will be removed in Qiskit 2.0.", + " Instead, use the `operation`, `qubits` and `clbits` named attributes." + ) + ), + py.get_type_bound::(), + // Stack level. Compared to Python-space calls to `warn`, this is unusually low + // beacuse all our internal call structure is now Rust-space and invisible to Python. + 1, + )) + .map(|_| ()) +} diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 76e808d1b308..92700f3274e7 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -72,6 +72,8 @@ pub static SINGLETON_GATE: ImportOnceCell = pub static SINGLETON_CONTROLLED_GATE: ImportOnceCell = ImportOnceCell::new("qiskit.circuit.singleton", "SingletonControlledGate"); +pub static WARNINGS_WARN: ImportOnceCell = ImportOnceCell::new("warnings", "warn"); + /// A mapping from the enum variant in crate::operations::StandardGate to the python /// module path and class name to import it. This is used to populate the conversion table /// when a gate is added directly via the StandardGate path and there isn't a Python object diff --git a/releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml b/releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml new file mode 100644 index 000000000000..d656ee5cb823 --- /dev/null +++ b/releasenotes/notes/deprecate-legacy-circuit-instruction-8a332ab09de73766.yaml @@ -0,0 +1,23 @@ +--- +deprecations_circuits: + - | + Treating :class:`.CircuitInstruction` as a tuple-like iterable is deprecated, and this legacy + path way will be removed in Qiskit 2.0. You should use the attribute-access fields + :attr:`~.CircuitInstruction.operation`, :attr:`~.CircuitInstruction.qubits`, and + :attr:`~.CircuitInstruction.clbits` instead. For example:: + + from qiskit.circuit import QuantumCircuit + + qc = QuantumCircuit(2, 2) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + + # Deprecated. + for op, qubits, clbits in qc.data: + pass + # New style. + for instruction in qc.data: + op = instruction.operation + qubits = instruction.qubits + clbits = instruction.clbits diff --git a/test/python/circuit/test_circuit_data.py b/test/python/circuit/test_circuit_data.py index 35ae27b2fcfb..55028c8e883e 100644 --- a/test/python/circuit/test_circuit_data.py +++ b/test/python/circuit/test_circuit_data.py @@ -416,7 +416,11 @@ def to_legacy(instruction): return (instruction.operation, list(instruction.qubits), list(instruction.clbits)) expected = [to_legacy(instruction) for instruction in qc.data] - actual = [tuple(instruction) for instruction in qc.data] + + with self.assertWarnsRegex( + DeprecationWarning, "Treating CircuitInstruction as an iterable is deprecated" + ): + actual = [tuple(instruction) for instruction in qc.data] self.assertEqual(actual, expected) def test_getitem_by_insertion_order(self): diff --git a/test/utils/base.py b/test/utils/base.py index 63a8bf4384f0..bebf03008858 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -215,6 +215,15 @@ def setUpClass(cls): module=r"seaborn(\..*)?", ) + # Safe to remove once https://github.com/Qiskit/qiskit-aer/pull/2179 is in a release version + # of Aer. + warnings.filterwarnings( + "default", + category=DeprecationWarning, + message="Treating CircuitInstruction as an iterable is deprecated", + module=r"qiskit_aer(\.[a-zA-Z0-9_]+)*", + ) + allow_DeprecationWarning_modules = [ "test.python.pulse.test_builder", "test.python.pulse.test_block", From 1ed5951a98b594808525c8428e06178c160cfcbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:53:54 +0200 Subject: [PATCH 12/15] Pin scipy to 1.13.1 to bypass CI failures (#12654) * Pin scipy to 1.13.1 to bypass CI failures * whoops * double whoops --- constraints.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/constraints.txt b/constraints.txt index d3985581d362..6681de226d93 100644 --- a/constraints.txt +++ b/constraints.txt @@ -3,6 +3,10 @@ # https://github.com/Qiskit/qiskit-terra/issues/10345 for current details. scipy<1.11; python_version<'3.12' +# Temporary pin to avoid CI issues caused by scipy 1.14.0 +# See https://github.com/Qiskit/qiskit/issues/12655 for current details. +scipy==1.13.1; python_version=='3.12' + # z3-solver from 4.12.3 onwards upped the minimum macOS API version for its # wheels to 11.7. The Azure VM images contain pre-built CPythons, of which at # least CPython 3.8 was compiled for an older macOS, so does not match a From 6974b4500f6c716b407b599e5cec80afbb757516 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 26 Jun 2024 08:29:34 +0200 Subject: [PATCH 13/15] Fix some bugs in loading Solovay Kitaev decompositions (#12579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix store & load - fix access via .item() - fix storing of global phase - fix storing ofgate sequence labels * undangle a dangling print * fix import order * Update releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- .../generate_basis_approximations.py | 2 +- .../discrete_basis/solovay_kitaev.py | 24 ++++++++++---- ...ix-sk-load-from-file-02c6eabbbd7fcda3.yaml | 10 ++++++ test/python/transpiler/test_solovay_kitaev.py | 31 +++++++++++++++++++ 4 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml diff --git a/qiskit/synthesis/discrete_basis/generate_basis_approximations.py b/qiskit/synthesis/discrete_basis/generate_basis_approximations.py index 672d0eb9e8ef..da9708c24559 100644 --- a/qiskit/synthesis/discrete_basis/generate_basis_approximations.py +++ b/qiskit/synthesis/discrete_basis/generate_basis_approximations.py @@ -156,7 +156,7 @@ def generate_basic_approximations( data = {} for sequence in sequences: gatestring = sequence.name - data[gatestring] = sequence.product + data[gatestring] = (sequence.product, sequence.global_phase) np.save(filename, data) diff --git a/qiskit/synthesis/discrete_basis/solovay_kitaev.py b/qiskit/synthesis/discrete_basis/solovay_kitaev.py index 2c8df5bd1b6d..f367f6c0f0b5 100644 --- a/qiskit/synthesis/discrete_basis/solovay_kitaev.py +++ b/qiskit/synthesis/discrete_basis/solovay_kitaev.py @@ -51,14 +51,19 @@ def __init__( self.basic_approximations = self.load_basic_approximations(basic_approximations) - def load_basic_approximations(self, data: list | str | dict) -> list[GateSequence]: + @staticmethod + def load_basic_approximations(data: list | str | dict) -> list[GateSequence]: """Load basic approximations. Args: data: If a string, specifies the path to the file from where to load the data. - If a dictionary, directly specifies the decompositions as ``{gates: matrix}``. - There ``gates`` are the names of the gates producing the SO(3) matrix ``matrix``, - e.g. ``{"h t": np.array([[0, 0.7071, -0.7071], [0, -0.7071, -0.7071], [-1, 0, 0]]}``. + If a dictionary, directly specifies the decompositions as ``{gates: matrix}`` + or ``{gates: (matrix, global_phase)}``. There, ``gates`` are the names of the gates + producing the SO(3) matrix ``matrix``, e.g. + ``{"h t": np.array([[0, 0.7071, -0.7071], [0, -0.7071, -0.7071], [-1, 0, 0]]}`` + and the ``global_phase`` can be given to account for a global phase difference + between the U(2) matrix of the quantum gates and the stored SO(3) matrix. + If not given, the ``global_phase`` will be assumed to be 0. Returns: A list of basic approximations as type ``GateSequence``. @@ -72,13 +77,20 @@ def load_basic_approximations(self, data: list | str | dict) -> list[GateSequenc # if a file, load the dictionary if isinstance(data, str): - data = np.load(data, allow_pickle=True) + data = np.load(data, allow_pickle=True).item() sequences = [] - for gatestring, matrix in data.items(): + for gatestring, matrix_and_phase in data.items(): + if isinstance(matrix_and_phase, tuple): + matrix, global_phase = matrix_and_phase + else: + matrix, global_phase = matrix_and_phase, 0 + sequence = GateSequence() sequence.gates = [_1q_gates[element] for element in gatestring.split()] + sequence.labels = [gate.name for gate in sequence.gates] sequence.product = np.asarray(matrix) + sequence.global_phase = global_phase sequences.append(sequence) return sequences diff --git a/releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml b/releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml new file mode 100644 index 000000000000..d995af06bccb --- /dev/null +++ b/releasenotes/notes/fix-sk-load-from-file-02c6eabbbd7fcda3.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Fix the :class:`.SolovayKitaev` transpiler pass when loading basic + approximations from an exising ``.npy`` file. Previously, loading + a stored approximation which allowed for further reductions (e.g. due + to gate cancellations) could cause a runtime failure. + Additionally, the global phase difference of the U(2) gate product + and SO(3) representation was lost during a save-reload procedure. + Fixes `Qiskit/qiskit#12576 `_. diff --git a/test/python/transpiler/test_solovay_kitaev.py b/test/python/transpiler/test_solovay_kitaev.py index e15a080f6f03..62b811c8e3bd 100644 --- a/test/python/transpiler/test_solovay_kitaev.py +++ b/test/python/transpiler/test_solovay_kitaev.py @@ -12,8 +12,10 @@ """Test the Solovay Kitaev transpilation pass.""" +import os import unittest import math +import tempfile import numpy as np import scipy @@ -230,6 +232,35 @@ def test_u_gates_work(self): included_gates = set(discretized.count_ops().keys()) self.assertEqual(set(basis_gates), included_gates) + def test_load_from_file(self): + """Test loading basic approximations from a file works. + + Regression test of Qiskit/qiskit#12576. + """ + filename = "approximations.npy" + + with tempfile.TemporaryDirectory() as tmp_dir: + fullpath = os.path.join(tmp_dir, filename) + + # dump approximations to file + generate_basic_approximations(basis_gates=["h", "s", "sdg"], depth=3, filename=fullpath) + + # circuit to decompose and reference decomp + circuit = QuantumCircuit(1) + circuit.rx(0.8, 0) + + reference = QuantumCircuit(1, global_phase=3 * np.pi / 4) + reference.h(0) + reference.s(0) + reference.h(0) + + # load the decomp and compare to reference + skd = SolovayKitaev(basic_approximations=fullpath) + # skd = SolovayKitaev(basic_approximations=filename) + discretized = skd(circuit) + + self.assertEqual(discretized, reference) + @ddt class TestGateSequence(QiskitTestCase): From e36027c01a5d18b72225502c0fd5021613893623 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Wed, 26 Jun 2024 09:41:11 +0200 Subject: [PATCH 14/15] GenericBackendV2 should fail when the backend cannot allocate the basis gate because its size (#12653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * GenericBackendV2 should fail when the backend cannot allocate the basis gate because its size Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * reno * Update releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * another single qubit backend --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- qiskit/providers/fake_provider/generic_backend_v2.py | 5 +++++ .../notes/fixes_GenericBackendV2-668e40596e1f070d.yaml | 4 ++++ .../providers/fake_provider/test_generic_backend_v2.py | 10 ++++++++++ test/visual/mpl/graph/test_graph_matplotlib_drawer.py | 2 +- 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index 1ac0484d775d..214754080e57 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -375,6 +375,11 @@ def _build_generic_target(self): f"in the standard qiskit circuit library." ) gate = self._supported_gates[name] + if self.num_qubits < gate.num_qubits: + raise QiskitError( + f"Provided basis gate {name} needs more qubits than {self.num_qubits}, " + f"which is the size of the backend." + ) noise_params = self._get_noise_defaults(name, gate.num_qubits) self._add_noisy_instruction_to_target(gate, noise_params, calibration_inst_map) diff --git a/releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml b/releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml new file mode 100644 index 000000000000..9d297125e3c2 --- /dev/null +++ b/releasenotes/notes/fixes_GenericBackendV2-668e40596e1f070d.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + The constructor :class:`.GenericBackendV2` was allowing to create malformed backends because it accepted basis gates that couldn't be allocated in the backend size . That is, a backend with a single qubit should not accept a basis with two-qubit gates. diff --git a/test/python/providers/fake_provider/test_generic_backend_v2.py b/test/python/providers/fake_provider/test_generic_backend_v2.py index b4fbe944c332..cd7c611b2212 100644 --- a/test/python/providers/fake_provider/test_generic_backend_v2.py +++ b/test/python/providers/fake_provider/test_generic_backend_v2.py @@ -35,6 +35,16 @@ def test_supported_basis_gates(self): with self.assertRaises(QiskitError): GenericBackendV2(num_qubits=8, basis_gates=["cx", "id", "rz", "sx", "zz"]) + def test_cx_1Q(self): + """Test failing with a backend with single qubit but with a two-qubit basis gate""" + with self.assertRaises(QiskitError): + GenericBackendV2(num_qubits=1, basis_gates=["cx", "id"]) + + def test_ccx_2Q(self): + """Test failing with a backend with two qubits but with a three-qubit basis gate""" + with self.assertRaises(QiskitError): + GenericBackendV2(num_qubits=2, basis_gates=["ccx", "id"]) + def test_operation_names(self): """Test that target basis gates include "delay", "measure" and "reset" even if not provided by user.""" diff --git a/test/visual/mpl/graph/test_graph_matplotlib_drawer.py b/test/visual/mpl/graph/test_graph_matplotlib_drawer.py index ae69f212f89c..20fae107d30d 100644 --- a/test/visual/mpl/graph/test_graph_matplotlib_drawer.py +++ b/test/visual/mpl/graph/test_graph_matplotlib_drawer.py @@ -389,7 +389,7 @@ def test_plot_1_qubit_gate_map(self): """Test plot_gate_map using 1 qubit backend""" # getting the mock backend from FakeProvider - backend = GenericBackendV2(num_qubits=1) + backend = GenericBackendV2(num_qubits=1, basis_gates=["id", "rz", "sx", "x"]) fname = "1_qubit_gate_map.png" self.graph_plot_gate_map(backend=backend, filename=fname) From 4d3821b01a70c40e8542646ad92759aae877a096 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 26 Jun 2024 08:06:23 -0400 Subject: [PATCH 15/15] Simplify QuantumCircuit._from_circuit_data bit handling (#12661) * Simplify QuantumCircuit._from_circuit_data bit handling This commit simplifies the logic around bit handling in the `QuantumCircuit._from_circuit_data()` constructor. Previously it was calling `add_bits()` for each bit in the `CircuitData` object to update the output circuit's accounting for each qubit. But this was needlessly heavy as the `CircuitData` is already the source of truth for the bits in a circuit and we just need to update the indices dictionary. The `add_bits()` method attempts to add the bits to the `CircuitData` too but this is wasted overhead because the `CircuitData` already has the bits as that's where the came from. This changes the constructor to just directly set the bit indices as needed and return the circuit. * Use a dict comprehension instead of a for loop --- qiskit/circuit/quantumcircuit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index ee52e3308a94..8b3ff7bf1979 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1161,9 +1161,9 @@ def __init__( def _from_circuit_data(cls, data: CircuitData) -> typing.Self: """A private constructor from rust space circuit data.""" out = QuantumCircuit() - out.add_bits(data.qubits) - out.add_bits(data.clbits) out._data = data + out._qubit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.qubits)} + out._clbit_indices = {bit: BitLocations(index, []) for index, bit in enumerate(data.clbits)} return out @staticmethod