From 93d796f6df3717e6aa1c18c1ee897ca18e080729 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 7 Jan 2025 16:58:13 +0100 Subject: [PATCH 1/4] More conservative caching in the ``CommutationChecker`` (#13600) * conservative commutation check * tests and reno * reno in the right location * more tests for custom gates --- crates/accelerate/src/commutation_checker.rs | 48 +++++++----- crates/circuit/src/operations.rs | 5 ++ ...commutation-checking-b728e7b6e1645615.yaml | 18 +++++ .../circuit/test_commutation_checker.py | 77 ++++++++++++++++--- 4 files changed, 117 insertions(+), 31 deletions(-) create mode 100644 releasenotes/notes/conservative-commutation-checking-b728e7b6e1645615.yaml diff --git a/crates/accelerate/src/commutation_checker.rs b/crates/accelerate/src/commutation_checker.rs index fe242c73422f..52d4900efa5b 100644 --- a/crates/accelerate/src/commutation_checker.rs +++ b/crates/accelerate/src/commutation_checker.rs @@ -28,14 +28,21 @@ use qiskit_circuit::circuit_instruction::{ExtraInstructionAttributes, OperationF use qiskit_circuit::dag_node::DAGOpNode; use qiskit_circuit::imports::QI_OPERATOR; use qiskit_circuit::operations::OperationRef::{Gate as PyGateType, Operation as PyOperationType}; -use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; +use qiskit_circuit::operations::{ + get_standard_gate_names, Operation, OperationRef, Param, StandardGate, +}; use qiskit_circuit::{BitType, Clbit, Qubit}; use crate::unitary_compose; use crate::QiskitError; +const TWOPI: f64 = 2.0 * std::f64::consts::PI; + +// These gates do not commute with other gates, we do not check them. static SKIPPED_NAMES: [&str; 4] = ["measure", "reset", "delay", "initialize"]; -static NO_CACHE_NAMES: [&str; 2] = ["annotated", "linear_function"]; + +// We keep a hash-set of operations eligible for commutation checking. This is because checking +// eligibility is not for free. static SUPPORTED_OP: Lazy> = Lazy::new(|| { HashSet::from([ "rxx", "ryy", "rzz", "rzx", "h", "x", "y", "z", "sx", "sxdg", "t", "tdg", "s", "sdg", "cx", @@ -43,9 +50,7 @@ static SUPPORTED_OP: Lazy> = Lazy::new(|| { ]) }); -const TWOPI: f64 = 2.0 * std::f64::consts::PI; - -// map rotation gates to their generators, or to ``None`` if we cannot currently efficiently +// Map rotation gates to their generators, or to ``None`` if we cannot currently efficiently // represent the generator in Rust and store the commutation relation in the commutation dictionary static SUPPORTED_ROTATIONS: Lazy>> = Lazy::new(|| { HashMap::from([ @@ -322,15 +327,17 @@ impl CommutationChecker { (qargs1, qargs2) }; - let skip_cache: bool = NO_CACHE_NAMES.contains(&first_op.name()) || - NO_CACHE_NAMES.contains(&second_op.name()) || - // Skip params that do not evaluate to floats for caching and commutation library - first_params.iter().any(|p| !matches!(p, Param::Float(_))) || - second_params.iter().any(|p| !matches!(p, Param::Float(_))) - && !SUPPORTED_OP.contains(op1.name()) - && !SUPPORTED_OP.contains(op2.name()); - - if skip_cache { + // For our cache to work correctly, we require the gate's definition to only depend on the + // ``params`` attribute. This cannot be guaranteed for custom gates, so we only check + // the cache for our standard gates, which we know are defined by the ``params`` AND + // that the ``params`` are float-only at this point. + let whitelist = get_standard_gate_names(); + let check_cache = whitelist.contains(&first_op.name()) + && whitelist.contains(&second_op.name()) + && first_params.iter().all(|p| matches!(p, Param::Float(_))) + && second_params.iter().all(|p| matches!(p, Param::Float(_))); + + if !check_cache { return self.commute_matmul( py, first_op, @@ -630,21 +637,24 @@ fn map_rotation<'a>( ) -> (&'a OperationRef<'a>, &'a [Param], bool) { let name = op.name(); if let Some(generator) = SUPPORTED_ROTATIONS.get(name) { - // if the rotation angle is below the tolerance, the gate is assumed to + // If the rotation angle is below the tolerance, the gate is assumed to // commute with everything, and we simply return the operation with the flag that - // it commutes trivially + // it commutes trivially. if let Param::Float(angle) = params[0] { if (angle % TWOPI).abs() < tol { return (op, params, true); }; }; - // otherwise, we check if a generator is given -- if not, we'll just return the operation - // itself (e.g. RXX does not have a generator and is just stored in the commutations - // dictionary) + // Otherwise we need to cover two cases -- either a generator is given, in which case + // we return it, or we don't have a generator yet, but we know we have the operation + // stored in the commutation library. For example, RXX does not have a generator in Rust + // yet (PauliGate is not in Rust currently), but it is stored in the library, so we + // can strip the parameters and just return the gate. if let Some(gate) = generator { return (gate, &[], false); }; + return (op, &[], false); } (op, params, false) } diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 59adfd9e0e8c..444d178c6863 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -431,6 +431,11 @@ static STANDARD_GATE_NAME: [&str; STANDARD_GATE_SIZE] = [ "rcccx", // 51 ("rc3x") ]; +/// Get a slice of all standard gate names. +pub fn get_standard_gate_names() -> &'static [&'static str] { + &STANDARD_GATE_NAME +} + impl StandardGate { pub fn create_py_op( &self, diff --git a/releasenotes/notes/conservative-commutation-checking-b728e7b6e1645615.yaml b/releasenotes/notes/conservative-commutation-checking-b728e7b6e1645615.yaml new file mode 100644 index 000000000000..dd741f981110 --- /dev/null +++ b/releasenotes/notes/conservative-commutation-checking-b728e7b6e1645615.yaml @@ -0,0 +1,18 @@ +--- +fixes: + - | + Commutation relations of :class:`~.circuit.Instruction`\ s with float-only ``params`` + were eagerly cached by the :class:`.CommutationChecker`, using the ``params`` as key to + query the relation. This could lead to faulty results, if the instruction's definition + depended on additional information that just the :attr:`~.circuit.Instruction.params` + attribute, such as e.g. the case for :class:`.PauliEvolutionGate`. + This behavior is now fixed, and the commutation checker only conservatively caches + commutations for Qiskit-native standard gates. This can incur a performance cost if you were + relying on your custom gates being cached, however, we cannot guarantee safe caching for + custom gates, as they might rely on information beyond :attr:`~.circuit.Instruction.params`. + - | + Fixed a bug in the :class:`.CommmutationChecker`, where checking commutation of instruction + with non-numeric values in the :attr:`~.circuit.Instruction.params` attribute (such as the + :class:`.PauliGate`) could raise an error. + Fixed `#13570 `__. + diff --git a/test/python/circuit/test_commutation_checker.py b/test/python/circuit/test_commutation_checker.py index 9759b5bffd1e..b4f6a30d904c 100644 --- a/test/python/circuit/test_commutation_checker.py +++ b/test/python/circuit/test_commutation_checker.py @@ -27,6 +27,7 @@ Parameter, QuantumRegister, Qubit, + QuantumCircuit, ) from qiskit.circuit.commutation_library import SessionCommutationChecker as scc from qiskit.circuit.library import ( @@ -37,9 +38,11 @@ CRYGate, CRZGate, CXGate, + CUGate, LinearFunction, MCXGate, Measure, + PauliGate, PhaseGate, Reset, RXGate, @@ -82,6 +85,22 @@ def to_matrix(self): return np.array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]], dtype=complex) +class MyEvilRXGate(Gate): + """A RX gate designed to annoy the caching mechanism (but a realistic gate nevertheless).""" + + def __init__(self, evil_input_not_in_param: float): + """ + Args: + evil_input_not_in_param: The RX rotation angle. + """ + self.value = evil_input_not_in_param + super().__init__("", 1, []) + + def _define(self): + self.definition = QuantumCircuit(1) + self.definition.rx(self.value, 0) + + @ddt class TestCommutationChecker(QiskitTestCase): """Test CommutationChecker class.""" @@ -137,7 +156,7 @@ def test_standard_gates_commutations(self): def test_caching_positive_results(self): """Check that hashing positive results in commutativity checker works as expected.""" scc.clear_cached_commutations() - self.assertTrue(scc.commute(ZGate(), [0], [], NewGateCX(), [0, 1], [])) + self.assertTrue(scc.commute(ZGate(), [0], [], CUGate(1, 2, 3, 0), [0, 1], [])) self.assertGreater(scc.num_cached_entries(), 0) def test_caching_lookup_with_non_overlapping_qubits(self): @@ -150,16 +169,17 @@ def test_caching_lookup_with_non_overlapping_qubits(self): def test_caching_store_and_lookup_with_non_overlapping_qubits(self): """Check that commutations storing and lookup with non-overlapping qubits works as expected.""" scc_lenm = scc.num_cached_entries() - self.assertTrue(scc.commute(NewGateCX(), [0, 2], [], CXGate(), [0, 1], [])) - self.assertFalse(scc.commute(NewGateCX(), [0, 1], [], CXGate(), [1, 2], [])) - self.assertTrue(scc.commute(NewGateCX(), [1, 4], [], CXGate(), [1, 6], [])) - self.assertFalse(scc.commute(NewGateCX(), [5, 3], [], CXGate(), [3, 1], [])) + cx_like = CUGate(np.pi, 0, np.pi, 0) + self.assertTrue(scc.commute(cx_like, [0, 2], [], CXGate(), [0, 1], [])) + self.assertFalse(scc.commute(cx_like, [0, 1], [], CXGate(), [1, 2], [])) + self.assertTrue(scc.commute(cx_like, [1, 4], [], CXGate(), [1, 6], [])) + self.assertFalse(scc.commute(cx_like, [5, 3], [], CXGate(), [3, 1], [])) self.assertEqual(scc.num_cached_entries(), scc_lenm + 2) def test_caching_negative_results(self): """Check that hashing negative results in commutativity checker works as expected.""" scc.clear_cached_commutations() - self.assertFalse(scc.commute(XGate(), [0], [], NewGateCX(), [0, 1], [])) + self.assertFalse(scc.commute(XGate(), [0], [], CUGate(1, 2, 3, 0), [0, 1], [])) self.assertGreater(scc.num_cached_entries(), 0) def test_caching_different_qubit_sets(self): @@ -167,10 +187,11 @@ def test_caching_different_qubit_sets(self): scc.clear_cached_commutations() # All the following should be cached in the same way # though each relation gets cached twice: (A, B) and (B, A) - scc.commute(XGate(), [0], [], NewGateCX(), [0, 1], []) - scc.commute(XGate(), [10], [], NewGateCX(), [10, 20], []) - scc.commute(XGate(), [10], [], NewGateCX(), [10, 5], []) - scc.commute(XGate(), [5], [], NewGateCX(), [5, 7], []) + cx_like = CUGate(np.pi, 0, np.pi, 0) + scc.commute(XGate(), [0], [], cx_like, [0, 1], []) + scc.commute(XGate(), [10], [], cx_like, [10, 20], []) + scc.commute(XGate(), [10], [], cx_like, [10, 5], []) + scc.commute(XGate(), [5], [], cx_like, [5, 7], []) self.assertEqual(scc.num_cached_entries(), 1) def test_zero_rotations(self): @@ -377,12 +398,14 @@ def test_serialization(self): """Test that the commutation checker is correctly serialized""" import pickle + cx_like = CUGate(np.pi, 0, np.pi, 0) + scc.clear_cached_commutations() - self.assertTrue(scc.commute(ZGate(), [0], [], NewGateCX(), [0, 1], [])) + self.assertTrue(scc.commute(ZGate(), [0], [], cx_like, [0, 1], [])) cc2 = pickle.loads(pickle.dumps(scc)) self.assertEqual(cc2.num_cached_entries(), 1) dop1 = DAGOpNode(ZGate(), qargs=[0], cargs=[]) - dop2 = DAGOpNode(NewGateCX(), qargs=[0, 1], cargs=[]) + dop2 = DAGOpNode(cx_like, qargs=[0, 1], cargs=[]) cc2.commute_nodes(dop1, dop2) dop1 = DAGOpNode(ZGate(), qargs=[0], cargs=[]) dop2 = DAGOpNode(CXGate(), qargs=[0, 1], cargs=[]) @@ -430,6 +453,36 @@ def test_rotation_mod_2pi(self, gate_cls): scc.commute(generic_gate, [0], [], gate, list(range(gate.num_qubits)), []) ) + def test_custom_gate(self): + """Test a custom gate.""" + my_cx = NewGateCX() + + self.assertTrue(scc.commute(my_cx, [0, 1], [], XGate(), [1], [])) + self.assertFalse(scc.commute(my_cx, [0, 1], [], XGate(), [0], [])) + self.assertTrue(scc.commute(my_cx, [0, 1], [], ZGate(), [0], [])) + + self.assertFalse(scc.commute(my_cx, [0, 1], [], my_cx, [1, 0], [])) + self.assertTrue(scc.commute(my_cx, [0, 1], [], my_cx, [0, 1], [])) + + def test_custom_gate_caching(self): + """Test a custom gate is correctly handled on consecutive runs.""" + + all_commuter = MyEvilRXGate(0) # this will commute with anything + some_rx = MyEvilRXGate(1.6192) # this should not commute with H + + # the order here is important: we're testing whether the gate that commutes with + # everything is used after the first commutation check, regardless of the internal + # gate parameters + self.assertTrue(scc.commute(all_commuter, [0], [], HGate(), [0], [])) + self.assertFalse(scc.commute(some_rx, [0], [], HGate(), [0], [])) + + def test_nonfloat_param(self): + """Test commutation-checking on a gate that has non-float ``params``.""" + pauli_gate = PauliGate("XX") + rx_gate_theta = RXGate(Parameter("Theta")) + self.assertTrue(scc.commute(pauli_gate, [0, 1], [], rx_gate_theta, [0], [])) + self.assertTrue(scc.commute(rx_gate_theta, [0], [], pauli_gate, [0, 1], [])) + if __name__ == "__main__": unittest.main() From 586d72d0c0fb67124e5ec1b60cc35cd224aaa2f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 08:05:41 +0000 Subject: [PATCH 2/4] Bump pypa/cibuildwheel from 2.21.3 to 2.22.0 in the github_actions group (#13488) * Bump pypa/cibuildwheel from 2.21.3 to 2.22.0 in the github_actions group Bumps the github_actions group with 1 update: [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel). Updates `pypa/cibuildwheel` from 2.21.3 to 2.22.0 - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v2.21.3...v2.22.0) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github_actions ... Signed-off-by: dependabot[bot] * Use Python 3.13 as cibuildwheel host --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jake Lishman --- .github/workflows/wheels-build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/wheels-build.yml b/.github/workflows/wheels-build.yml index 4215d1202647..136d0f94b3e6 100644 --- a/.github/workflows/wheels-build.yml +++ b/.github/workflows/wheels-build.yml @@ -69,7 +69,7 @@ on: python-version: description: "The Python version to use to host the build runner." type: string - default: "3.10" + default: "3.13" required: false pgo: @@ -127,7 +127,7 @@ jobs: env: PGO_WORK_DIR: ${{ github.workspace }}/pgo-data PGO_OUT_PATH: ${{ github.workspace }}/merged.profdata - - uses: pypa/cibuildwheel@v2.21.3 + - uses: pypa/cibuildwheel@v2.22.0 - uses: actions/upload-artifact@v4 with: path: ./wheelhouse/*.whl @@ -152,7 +152,7 @@ jobs: with: components: llvm-tools-preview - name: Build wheels - uses: pypa/cibuildwheel@v2.21.3 + uses: pypa/cibuildwheel@v2.22.0 env: CIBW_SKIP: 'pp* cp36-* cp37-* cp38-* *musllinux* *amd64 *x86_64' - uses: actions/upload-artifact@v4 @@ -174,7 +174,7 @@ jobs: - uses: docker/setup-qemu-action@v3 with: platforms: all - - uses: pypa/cibuildwheel@v2.21.3 + - uses: pypa/cibuildwheel@v2.22.0 env: CIBW_ARCHS_LINUX: s390x CIBW_TEST_SKIP: "cp*" @@ -197,7 +197,7 @@ jobs: - uses: docker/setup-qemu-action@v3 with: platforms: all - - uses: pypa/cibuildwheel@v2.21.3 + - uses: pypa/cibuildwheel@v2.22.0 env: CIBW_ARCHS_LINUX: ppc64le CIBW_TEST_SKIP: "cp*" @@ -219,7 +219,7 @@ jobs: - uses: docker/setup-qemu-action@v3 with: platforms: all - - uses: pypa/cibuildwheel@v2.21.3 + - uses: pypa/cibuildwheel@v2.22.0 env: CIBW_ARCHS_LINUX: aarch64 CIBW_TEST_COMMAND: cp -r {project}/test . && QISKIT_PARALLEL=FALSE stestr --test-path test/python run --abbreviate -n test.python.compiler.test_transpiler From 1cfdf2eb9e6e9fc97bc5811b4fe7be795f69bc23 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:27:18 +0200 Subject: [PATCH 3/4] Allow mcrx, mcry and mcrz gate methods to accept angles of ParameterValueType (#13507) * allow mcrx, mcry and mcrz accept angles of ParamterValueType * update MCPhase gate definition using mcrz where the angle is of ParameterValueType * remove annotated from control function in rx, ry, rz * fix mc without annotation test * add a test for mcrx/mcry/mcrz/mcp with Parameter * add release notes * update release notes * simplify _mscu2_real_diagonal code into one function * update release notes following review * move multi_control_rotation_gates to the synthesis library * replace MCXVChain by synth_mcx_dirty_i15. remove ctrl_state which is not used. * add num_controls=1 in synth_mcx_dirty_i15. * disable cyclic imports * add to_gate to prevent test from failing * updates following review * minor fix * minor fix following review * better docs for mcrx, mcry, mcrz * remove pylint disable --- .../library/standard_gates/__init__.py | 1 - .../multi_control_rotation_gates.py | 405 ------------------ qiskit/circuit/library/standard_gates/p.py | 28 +- qiskit/circuit/library/standard_gates/rx.py | 7 +- qiskit/circuit/library/standard_gates/ry.py | 7 +- qiskit/circuit/library/standard_gates/rz.py | 7 +- qiskit/circuit/quantumcircuit.py | 193 +++++++++ qiskit/synthesis/__init__.py | 2 +- qiskit/synthesis/multi_controlled/__init__.py | 1 + .../multi_controlled/mcx_synthesis.py | 5 +- .../multi_control_rotation_gates.py | 206 +++++++++ ...gates-with-parameter-12a04701d0cd095b.yaml | 9 + test/python/circuit/test_controlled_gate.py | 46 +- 13 files changed, 455 insertions(+), 462 deletions(-) delete mode 100644 qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py create mode 100644 qiskit/synthesis/multi_controlled/multi_control_rotation_gates.py create mode 100644 releasenotes/notes/fix-multi-controlled-rotation-gates-with-parameter-12a04701d0cd095b.yaml diff --git a/qiskit/circuit/library/standard_gates/__init__.py b/qiskit/circuit/library/standard_gates/__init__.py index be0e9dd04449..729772723418 100644 --- a/qiskit/circuit/library/standard_gates/__init__.py +++ b/qiskit/circuit/library/standard_gates/__init__.py @@ -43,7 +43,6 @@ from .y import YGate, CYGate from .z import ZGate, CZGate, CCZGate from .global_phase import GlobalPhaseGate -from .multi_control_rotation_gates import mcrx, mcry, mcrz def get_standard_gate_name_mapping(): diff --git a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py deleted file mode 100644 index 8746e51c48db..000000000000 --- a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py +++ /dev/null @@ -1,405 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2018, 2019. -# -# 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. -""" -Multiple-Controlled U3 gate. Not using ancillary qubits. -""" - -from math import pi -import math -from typing import Optional, Union, Tuple, List -import numpy as np - -from qiskit.circuit import QuantumCircuit, QuantumRegister, Qubit, ParameterExpression -from qiskit.circuit.library.standard_gates.x import MCXGate -from qiskit.circuit.library.standard_gates.u3 import _generate_gray_code -from qiskit.circuit.parameterexpression import ParameterValueType -from qiskit.exceptions import QiskitError - - -def _apply_cu(circuit, theta, phi, lam, control, target, use_basis_gates=True): - if use_basis_gates: - # pylint: disable=cyclic-import - # ┌──────────────┐ - # control: ┤ P(λ/2 + φ/2) ├──■──────────────────────────────────■──────────────── - # ├──────────────┤┌─┴─┐┌────────────────────────────┐┌─┴─┐┌────────────┐ - # target: ┤ P(λ/2 - φ/2) ├┤ X ├┤ U(-0.5*0,0,-0.5*λ - 0.5*φ) ├┤ X ├┤ U(0/2,φ,0) ├ - # └──────────────┘└───┘└────────────────────────────┘└───┘└────────────┘ - circuit.p((lam + phi) / 2, [control]) - circuit.p((lam - phi) / 2, [target]) - circuit.cx(control, target) - circuit.u(-theta / 2, 0, -(phi + lam) / 2, [target]) - circuit.cx(control, target) - circuit.u(theta / 2, phi, 0, [target]) - else: - circuit.cu(theta, phi, lam, 0, control, target) - - -def _apply_mcu_graycode(circuit, theta, phi, lam, ctls, tgt, use_basis_gates): - """Apply multi-controlled u gate from ctls to tgt using graycode - pattern with single-step angles theta, phi, lam.""" - - n = len(ctls) - - gray_code = _generate_gray_code(n) - last_pattern = None - - for pattern in gray_code: - if "1" not in pattern: - continue - if last_pattern is None: - last_pattern = pattern - # find left most set bit - lm_pos = list(pattern).index("1") - - # find changed bit - comp = [i != j for i, j in zip(pattern, last_pattern)] - if True in comp: - pos = comp.index(True) - else: - pos = None - if pos is not None: - if pos != lm_pos: - circuit.cx(ctls[pos], ctls[lm_pos]) - else: - indices = [i for i, x in enumerate(pattern) if x == "1"] - for idx in indices[1:]: - circuit.cx(ctls[idx], ctls[lm_pos]) - # check parity and undo rotation - if pattern.count("1") % 2 == 0: - # inverse CU: u(theta, phi, lamb)^dagger = u(-theta, -lam, -phi) - _apply_cu( - circuit, -theta, -lam, -phi, ctls[lm_pos], tgt, use_basis_gates=use_basis_gates - ) - else: - _apply_cu(circuit, theta, phi, lam, ctls[lm_pos], tgt, use_basis_gates=use_basis_gates) - last_pattern = pattern - - -def _mcsu2_real_diagonal( - unitary: np.ndarray, - num_controls: int, - ctrl_state: Optional[str] = None, - use_basis_gates: bool = False, -) -> QuantumCircuit: - """ - Return a multi-controlled SU(2) gate [1]_ with a real main diagonal or secondary diagonal. - - Args: - unitary: SU(2) unitary matrix with one real diagonal. - num_controls: The number of control qubits. - ctrl_state: The state on which the SU(2) operation is controlled. Defaults to all - control qubits being in state 1. - use_basis_gates: If ``True``, use ``[p, u, cx]`` gates to implement the decomposition. - - Returns: - A :class:`.QuantumCircuit` implementing the multi-controlled SU(2) gate. - - Raises: - QiskitError: If the input matrix is invalid. - - References: - - .. [1]: R. Vale et al. Decomposition of Multi-controlled Special Unitary Single-Qubit Gates - `arXiv:2302.06377 (2023) `__ - - """ - # pylint: disable=cyclic-import - from .x import MCXVChain - from qiskit.circuit.library.generalized_gates import UnitaryGate - from qiskit.quantum_info.operators.predicates import is_unitary_matrix - from qiskit.compiler import transpile - - if unitary.shape != (2, 2): - raise QiskitError(f"The unitary must be a 2x2 matrix, but has shape {unitary.shape}.") - - if not is_unitary_matrix(unitary): - raise QiskitError(f"The unitary in must be an unitary matrix, but is {unitary}.") - - if not np.isclose(1.0, np.linalg.det(unitary)): - raise QiskitError("Invalid Value _mcsu2_real_diagonal requires det(unitary) equal to one.") - - is_main_diag_real = np.isclose(unitary[0, 0].imag, 0.0) and np.isclose(unitary[1, 1].imag, 0.0) - is_secondary_diag_real = np.isclose(unitary[0, 1].imag, 0.0) and np.isclose( - unitary[1, 0].imag, 0.0 - ) - - if not is_main_diag_real and not is_secondary_diag_real: - raise QiskitError("The unitary must have one real diagonal.") - - if is_secondary_diag_real: - x = unitary[0, 1] - z = unitary[1, 1] - else: - x = -unitary[0, 1].real - z = unitary[1, 1] - unitary[0, 1].imag * 1.0j - - if np.isclose(z, -1): - s_op = [[1.0, 0.0], [0.0, 1.0j]] - else: - alpha_r = math.sqrt((math.sqrt((z.real + 1.0) / 2.0) + 1.0) / 2.0) - alpha_i = z.imag / ( - 2.0 * math.sqrt((z.real + 1.0) * (math.sqrt((z.real + 1.0) / 2.0) + 1.0)) - ) - alpha = alpha_r + 1.0j * alpha_i - beta = x / (2.0 * math.sqrt((z.real + 1.0) * (math.sqrt((z.real + 1.0) / 2.0) + 1.0))) - - # S gate definition - s_op = np.array([[alpha, -np.conj(beta)], [beta, np.conj(alpha)]]) - - s_gate = UnitaryGate(s_op) - - k_1 = math.ceil(num_controls / 2.0) - k_2 = math.floor(num_controls / 2.0) - - ctrl_state_k_1 = None - ctrl_state_k_2 = None - - if ctrl_state is not None: - str_ctrl_state = f"{ctrl_state:0{num_controls}b}" - ctrl_state_k_1 = str_ctrl_state[::-1][:k_1][::-1] - ctrl_state_k_2 = str_ctrl_state[::-1][k_1:][::-1] - - circuit = QuantumCircuit(num_controls + 1, name="MCSU2") - controls = list(range(num_controls)) # control indices, defined for code legibility - target = num_controls # target index, defined for code legibility - - if not is_secondary_diag_real: - circuit.h(target) - - mcx_1 = MCXVChain(num_ctrl_qubits=k_1, dirty_ancillas=True, ctrl_state=ctrl_state_k_1) - circuit.append(mcx_1, controls[:k_1] + [target] + controls[k_1 : 2 * k_1 - 2]) - circuit.append(s_gate, [target]) - - mcx_2 = MCXVChain( - num_ctrl_qubits=k_2, - dirty_ancillas=True, - ctrl_state=ctrl_state_k_2, - # action_only=general_su2_optimization # Requires PR #9687 - ) - circuit.append(mcx_2.inverse(), controls[k_1:] + [target] + controls[k_1 - k_2 + 2 : k_1]) - circuit.append(s_gate.inverse(), [target]) - - mcx_3 = MCXVChain(num_ctrl_qubits=k_1, dirty_ancillas=True, ctrl_state=ctrl_state_k_1) - circuit.append(mcx_3, controls[:k_1] + [target] + controls[k_1 : 2 * k_1 - 2]) - circuit.append(s_gate, [target]) - - mcx_4 = MCXVChain(num_ctrl_qubits=k_2, dirty_ancillas=True, ctrl_state=ctrl_state_k_2) - circuit.append(mcx_4, controls[k_1:] + [target] + controls[k_1 - k_2 + 2 : k_1]) - circuit.append(s_gate.inverse(), [target]) - - if not is_secondary_diag_real: - circuit.h(target) - - if use_basis_gates: - circuit = transpile(circuit, basis_gates=["p", "u", "cx"], qubits_initially_zero=False) - - return circuit - - -def mcrx( - self, - theta: ParameterValueType, - q_controls: Union[QuantumRegister, List[Qubit]], - q_target: Qubit, - use_basis_gates: bool = False, -): - """ - Apply Multiple-Controlled X rotation gate - - Args: - self (QuantumCircuit): The QuantumCircuit object to apply the mcrx gate on. - theta (float): angle theta - q_controls (QuantumRegister or list(Qubit)): The list of control qubits - q_target (Qubit): The target qubit - use_basis_gates (bool): use p, u, cx - - Raises: - QiskitError: parameter errors - """ - from .rx import RXGate - - control_qubits = self._qbit_argument_conversion(q_controls) - target_qubit = self._qbit_argument_conversion(q_target) - if len(target_qubit) != 1: - raise QiskitError("The mcrz gate needs a single qubit as target.") - all_qubits = control_qubits + target_qubit - target_qubit = target_qubit[0] - self._check_dups(all_qubits) - - n_c = len(control_qubits) - if n_c == 1: # cu - _apply_cu( - self, - theta, - -pi / 2, - pi / 2, - control_qubits[0], - target_qubit, - use_basis_gates=use_basis_gates, - ) - elif n_c < 4: - theta_step = theta * (1 / (2 ** (n_c - 1))) - _apply_mcu_graycode( - self, - theta_step, - -pi / 2, - pi / 2, - control_qubits, - target_qubit, - use_basis_gates=use_basis_gates, - ) - else: - if isinstance(theta, ParameterExpression): - raise QiskitError(f"Cannot synthesize MCRX with unbound parameter: {theta}.") - - cgate = _mcsu2_real_diagonal( - RXGate(theta).to_matrix(), - num_controls=len(control_qubits), - use_basis_gates=use_basis_gates, - ) - self.compose(cgate, control_qubits + [target_qubit], inplace=True) - - -def mcry( - self, - theta: ParameterValueType, - q_controls: Union[QuantumRegister, List[Qubit]], - q_target: Qubit, - q_ancillae: Optional[Union[QuantumRegister, Tuple[QuantumRegister, int]]] = None, - mode: Optional[str] = None, - use_basis_gates: bool = False, -): - """ - Apply Multiple-Controlled Y rotation gate - - Args: - self (QuantumCircuit): The QuantumCircuit object to apply the mcry gate on. - theta (float): angle theta - q_controls (list(Qubit)): The list of control qubits - q_target (Qubit): The target qubit - q_ancillae (QuantumRegister or tuple(QuantumRegister, int)): The list of ancillary qubits. - mode (string): The implementation mode to use - use_basis_gates (bool): use p, u, cx - - Raises: - QiskitError: parameter errors - """ - from .ry import RYGate - - control_qubits = self._qbit_argument_conversion(q_controls) - target_qubit = self._qbit_argument_conversion(q_target) - if len(target_qubit) != 1: - raise QiskitError("The mcrz gate needs a single qubit as target.") - ancillary_qubits = [] if q_ancillae is None else self._qbit_argument_conversion(q_ancillae) - all_qubits = control_qubits + target_qubit + ancillary_qubits - target_qubit = target_qubit[0] - self._check_dups(all_qubits) - - # auto-select the best mode - if mode is None: - # if enough ancillary qubits are provided, use the 'v-chain' method - additional_vchain = MCXGate.get_num_ancilla_qubits(len(control_qubits), "v-chain") - if len(ancillary_qubits) >= additional_vchain: - mode = "basic" - else: - mode = "noancilla" - - if mode == "basic": - self.ry(theta / 2, q_target) - self.mcx(q_controls, q_target, q_ancillae, mode="v-chain") - self.ry(-theta / 2, q_target) - self.mcx(q_controls, q_target, q_ancillae, mode="v-chain") - elif mode == "noancilla": - n_c = len(control_qubits) - if n_c == 1: # cu - _apply_cu( - self, theta, 0, 0, control_qubits[0], target_qubit, use_basis_gates=use_basis_gates - ) - elif n_c < 4: - theta_step = theta * (1 / (2 ** (n_c - 1))) - _apply_mcu_graycode( - self, - theta_step, - 0, - 0, - control_qubits, - target_qubit, - use_basis_gates=use_basis_gates, - ) - else: - if isinstance(theta, ParameterExpression): - raise QiskitError(f"Cannot synthesize MCRY with unbound parameter: {theta}.") - - cgate = _mcsu2_real_diagonal( - RYGate(theta).to_matrix(), - num_controls=len(control_qubits), - use_basis_gates=use_basis_gates, - ) - self.compose(cgate, control_qubits + [target_qubit], inplace=True) - else: - raise QiskitError(f"Unrecognized mode for building MCRY circuit: {mode}.") - - -def mcrz( - self, - lam: ParameterValueType, - q_controls: Union[QuantumRegister, List[Qubit]], - q_target: Qubit, - use_basis_gates: bool = False, -): - """ - Apply Multiple-Controlled Z rotation gate - - Args: - self (QuantumCircuit): The QuantumCircuit object to apply the mcrz gate on. - lam (float): angle lambda - q_controls (list(Qubit)): The list of control qubits - q_target (Qubit): The target qubit - use_basis_gates (bool): use p, u, cx - - Raises: - QiskitError: parameter errors - """ - from .rz import CRZGate, RZGate - - control_qubits = self._qbit_argument_conversion(q_controls) - target_qubit = self._qbit_argument_conversion(q_target) - if len(target_qubit) != 1: - raise QiskitError("The mcrz gate needs a single qubit as target.") - all_qubits = control_qubits + target_qubit - target_qubit = target_qubit[0] - self._check_dups(all_qubits) - - n_c = len(control_qubits) - if n_c == 1: - if use_basis_gates: - self.u(0, 0, lam / 2, target_qubit) - self.cx(control_qubits[0], target_qubit) - self.u(0, 0, -lam / 2, target_qubit) - self.cx(control_qubits[0], target_qubit) - else: - self.append(CRZGate(lam), control_qubits + [target_qubit]) - else: - if isinstance(lam, ParameterExpression): - raise QiskitError(f"Cannot synthesize MCRZ with unbound parameter: {lam}.") - - cgate = _mcsu2_real_diagonal( - RZGate(lam).to_matrix(), - num_controls=len(control_qubits), - use_basis_gates=use_basis_gates, - ) - self.compose(cgate, control_qubits + [target_qubit], inplace=True) - - -QuantumCircuit.mcrx = mcrx -QuantumCircuit.mcry = mcry -QuantumCircuit.mcrz = mcrz diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index a3ea7167a34f..479d7959f942 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -372,24 +372,16 @@ def _define(self): qc.cp(self.params[0], 0, 1) else: lam = self.params[0] - if type(lam) in [float, int]: - q_controls = list(range(self.num_ctrl_qubits)) - q_target = self.num_ctrl_qubits - new_target = q_target - for k in range(self.num_ctrl_qubits): - # Note: it's better *not* to run transpile recursively - qc.mcrz(lam / (2**k), q_controls, new_target, use_basis_gates=False) - new_target = q_controls.pop() - qc.p(lam / (2**self.num_ctrl_qubits), new_target) - else: # in this case type(lam) is ParameterValueType - from .u3 import _gray_code_chain - - scaled_lam = self.params[0] / (2 ** (self.num_ctrl_qubits - 1)) - bottom_gate = CPhaseGate(scaled_lam) - for operation, qubits, clbits in _gray_code_chain( - qr, self.num_ctrl_qubits, bottom_gate - ): - qc._append(operation, qubits, clbits) + + q_controls = list(range(self.num_ctrl_qubits)) + q_target = self.num_ctrl_qubits + new_target = q_target + for k in range(self.num_ctrl_qubits): + # Note: it's better *not* to run transpile recursively + qc.mcrz(lam / (2**k), q_controls, new_target, use_basis_gates=False) + new_target = q_controls.pop() + qc.p(lam / (2**self.num_ctrl_qubits), new_target) + self.definition = qc def control( diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index cc8a72cd06dd..5c6db8ce7cbc 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -22,7 +22,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit._accelerate.circuit import StandardGate @@ -104,11 +104,6 @@ def control( gate = CRXGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: - # If the gate parameters contain free parameters, we cannot eagerly synthesize - # the controlled gate decomposition. In this case, we annotate the gate per default. - if annotated is None: - annotated = any(isinstance(p, ParameterExpression) for p in self.params) - gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index 6e6ba7142498..67c27007c1a6 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -21,7 +21,7 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit._accelerate.circuit import StandardGate @@ -103,11 +103,6 @@ def control( gate = CRYGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: - # If the gate parameters contain free parameters, we cannot eagerly synthesize - # the controlled gate decomposition. In this case, we annotate the gate per default. - if annotated is None: - annotated = any(isinstance(p, ParameterExpression) for p in self.params) - gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index e7efeafd24c5..ed0207658441 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -19,7 +19,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.parameterexpression import ParameterValueType, ParameterExpression +from qiskit.circuit.parameterexpression import ParameterValueType from qiskit._accelerate.circuit import StandardGate @@ -115,11 +115,6 @@ def control( gate = CRZGate(self.params[0], label=label, ctrl_state=ctrl_state) gate.base_gate.label = self.label else: - # If the gate parameters contain free parameters, we cannot eagerly synthesize - # the controlled gate decomposition. In this case, we annotate the gate per default. - if annotated is None: - annotated = any(isinstance(p, ParameterExpression) for p in self.params) - gate = super().control( num_ctrl_qubits=num_ctrl_qubits, label=label, diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index dee2f3e72276..495d6b36f439 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -37,6 +37,7 @@ Literal, overload, ) +from math import pi import numpy as np from qiskit._accelerate.circuit import CircuitData from qiskit._accelerate.circuit import StandardGate @@ -4683,6 +4684,198 @@ def mcp( copy=False, ) + def mcrx( + self, + theta: ParameterValueType, + q_controls: Sequence[QubitSpecifier], + q_target: QubitSpecifier, + use_basis_gates: bool = False, + ): + """ + Apply Multiple-Controlled X rotation gate + + Args: + theta: The angle of the rotation. + q_controls: The qubits used as the controls. + q_target: The qubit targeted by the gate. + use_basis_gates: use p, u, cx basis gates. + """ + # pylint: disable=cyclic-import + from .library.standard_gates.rx import RXGate + from qiskit.synthesis.multi_controlled import ( + _apply_cu, + _apply_mcu_graycode, + _mcsu2_real_diagonal, + ) + + control_qubits = self._qbit_argument_conversion(q_controls) + target_qubit = self._qbit_argument_conversion(q_target) + if len(target_qubit) != 1: + raise QiskitError("The mcrx gate needs a single qubit as target.") + all_qubits = control_qubits + target_qubit + target_qubit = target_qubit[0] + self._check_dups(all_qubits) + + n_c = len(control_qubits) + if n_c == 1: # cu + _apply_cu( + self, + theta, + -pi / 2, + pi / 2, + control_qubits[0], + target_qubit, + use_basis_gates=use_basis_gates, + ) + elif n_c < 4: + theta_step = theta * (1 / (2 ** (n_c - 1))) + _apply_mcu_graycode( + self, + theta_step, + -pi / 2, + pi / 2, + control_qubits, + target_qubit, + use_basis_gates=use_basis_gates, + ) + else: + cgate = _mcsu2_real_diagonal( + RXGate(theta), + num_controls=len(control_qubits), + use_basis_gates=use_basis_gates, + ) + self.compose(cgate, control_qubits + [target_qubit], inplace=True) + + def mcry( + self, + theta: ParameterValueType, + q_controls: Sequence[QubitSpecifier], + q_target: QubitSpecifier, + q_ancillae: QubitSpecifier | Sequence[QubitSpecifier] | None = None, + mode: str | None = None, + use_basis_gates: bool = False, + ): + """ + Apply Multiple-Controlled Y rotation gate + + Args: + theta: The angle of the rotation. + q_controls: The qubits used as the controls. + q_target: The qubit targeted by the gate. + q_ancillae: The list of ancillary qubits. + mode: The implementation mode to use. + use_basis_gates: use p, u, cx basis gates + """ + # pylint: disable=cyclic-import + from .library.standard_gates.ry import RYGate + from .library.standard_gates.x import MCXGate + from qiskit.synthesis.multi_controlled import ( + _apply_cu, + _apply_mcu_graycode, + _mcsu2_real_diagonal, + ) + + control_qubits = self._qbit_argument_conversion(q_controls) + target_qubit = self._qbit_argument_conversion(q_target) + if len(target_qubit) != 1: + raise QiskitError("The mcry gate needs a single qubit as target.") + ancillary_qubits = [] if q_ancillae is None else self._qbit_argument_conversion(q_ancillae) + all_qubits = control_qubits + target_qubit + ancillary_qubits + target_qubit = target_qubit[0] + self._check_dups(all_qubits) + + # auto-select the best mode + if mode is None: + # if enough ancillary qubits are provided, use the 'v-chain' method + additional_vchain = MCXGate.get_num_ancilla_qubits(len(control_qubits), "v-chain") + if len(ancillary_qubits) >= additional_vchain: + mode = "basic" + else: + mode = "noancilla" + + if mode == "basic": + self.ry(theta / 2, q_target) + self.mcx(list(q_controls), q_target, q_ancillae, mode="v-chain") + self.ry(-theta / 2, q_target) + self.mcx(list(q_controls), q_target, q_ancillae, mode="v-chain") + elif mode == "noancilla": + n_c = len(control_qubits) + if n_c == 1: # cu + _apply_cu( + self, + theta, + 0, + 0, + control_qubits[0], + target_qubit, + use_basis_gates=use_basis_gates, + ) + elif n_c < 4: + theta_step = theta * (1 / (2 ** (n_c - 1))) + _apply_mcu_graycode( + self, + theta_step, + 0, + 0, + control_qubits, + target_qubit, + use_basis_gates=use_basis_gates, + ) + else: + cgate = _mcsu2_real_diagonal( + RYGate(theta), + num_controls=len(control_qubits), + use_basis_gates=use_basis_gates, + ) + self.compose(cgate, control_qubits + [target_qubit], inplace=True) + else: + raise QiskitError(f"Unrecognized mode for building MCRY circuit: {mode}.") + + def mcrz( + self, + lam: ParameterValueType, + q_controls: Sequence[QubitSpecifier], + q_target: QubitSpecifier, + use_basis_gates: bool = False, + ): + """ + Apply Multiple-Controlled Z rotation gate + + Args: + lam: The angle of the rotation. + q_controls: The qubits used as the controls. + q_target: The qubit targeted by the gate. + use_basis_gates: use p, u, cx basis gates. + """ + # pylint: disable=cyclic-import + from .library.standard_gates.rz import CRZGate, RZGate + from qiskit.synthesis.multi_controlled import _mcsu2_real_diagonal + + control_qubits = self._qbit_argument_conversion(q_controls) + target_qubit = self._qbit_argument_conversion(q_target) + if len(target_qubit) != 1: + raise QiskitError("The mcrz gate needs a single qubit as target.") + all_qubits = control_qubits + target_qubit + target_qubit = target_qubit[0] + self._check_dups(all_qubits) + + n_c = len(control_qubits) + if n_c == 1: + if use_basis_gates: + self.u(0, 0, lam / 2, target_qubit) + self.cx(control_qubits[0], target_qubit) + self.u(0, 0, -lam / 2, target_qubit) + self.cx(control_qubits[0], target_qubit) + else: + self.append(CRZGate(lam), control_qubits + [target_qubit]) + else: + cgate = _mcsu2_real_diagonal( + RZGate(lam), + num_controls=len(control_qubits), + use_basis_gates=use_basis_gates, + ) + self.compose(cgate, control_qubits + [target_qubit], inplace=True) + def r( self, theta: ParameterValueType, phi: ParameterValueType, qubit: QubitSpecifier ) -> InstructionSet: diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index adea95d4260c..a86ec6681400 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -147,7 +147,7 @@ Multipliers ----------- -.. autofunction:: multiplier_cumulative_h18 +.. autofunction:: multiplier_cumulative_h18 .. autofunction:: multiplier_qft_r17 """ diff --git a/qiskit/synthesis/multi_controlled/__init__.py b/qiskit/synthesis/multi_controlled/__init__.py index 925793fc5dc1..0fa29553e7ed 100644 --- a/qiskit/synthesis/multi_controlled/__init__.py +++ b/qiskit/synthesis/multi_controlled/__init__.py @@ -22,3 +22,4 @@ synth_c3x, synth_c4x, ) +from .multi_control_rotation_gates import _apply_cu, _apply_mcu_graycode, _mcsu2_real_diagonal diff --git a/qiskit/synthesis/multi_controlled/mcx_synthesis.py b/qiskit/synthesis/multi_controlled/mcx_synthesis.py index 10680f0fee88..221d6adaf736 100644 --- a/qiskit/synthesis/multi_controlled/mcx_synthesis.py +++ b/qiskit/synthesis/multi_controlled/mcx_synthesis.py @@ -53,7 +53,10 @@ def synth_mcx_n_dirty_i15( `arXiv:1501.06911 `_ """ - num_qubits = 2 * num_ctrl_qubits - 1 + if num_ctrl_qubits == 1: + num_qubits = 2 + else: + num_qubits = 2 * num_ctrl_qubits - 1 q = QuantumRegister(num_qubits, name="q") qc = QuantumCircuit(q, name="mcx_vchain") q_controls = q[:num_ctrl_qubits] diff --git a/qiskit/synthesis/multi_controlled/multi_control_rotation_gates.py b/qiskit/synthesis/multi_controlled/multi_control_rotation_gates.py new file mode 100644 index 000000000000..520bf1722a41 --- /dev/null +++ b/qiskit/synthesis/multi_controlled/multi_control_rotation_gates.py @@ -0,0 +1,206 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 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. +""" +Multiple-Controlled U3 gate utilities. Not using ancillary qubits. +""" + +import math +import numpy as np + +from qiskit.circuit import QuantumCircuit, Gate +from qiskit.circuit.library.standard_gates.u3 import _generate_gray_code +from qiskit.exceptions import QiskitError + + +def _apply_cu(circuit, theta, phi, lam, control, target, use_basis_gates=True): + if use_basis_gates: + # ┌──────────────┐ + # control: ┤ P(λ/2 + φ/2) ├──■──────────────────────────────────■──────────────── + # ├──────────────┤┌─┴─┐┌────────────────────────────┐┌─┴─┐┌────────────┐ + # target: ┤ P(λ/2 - φ/2) ├┤ X ├┤ U(-0.5*0,0,-0.5*λ - 0.5*φ) ├┤ X ├┤ U(0/2,φ,0) ├ + # └──────────────┘└───┘└────────────────────────────┘└───┘└────────────┘ + circuit.p((lam + phi) / 2, [control]) + circuit.p((lam - phi) / 2, [target]) + circuit.cx(control, target) + circuit.u(-theta / 2, 0, -(phi + lam) / 2, [target]) + circuit.cx(control, target) + circuit.u(theta / 2, phi, 0, [target]) + else: + circuit.cu(theta, phi, lam, 0, control, target) + + +def _apply_mcu_graycode(circuit, theta, phi, lam, ctls, tgt, use_basis_gates): + """Apply multi-controlled u gate from ctls to tgt using graycode + pattern with single-step angles theta, phi, lam.""" + + n = len(ctls) + + gray_code = _generate_gray_code(n) + last_pattern = None + + for pattern in gray_code: + if "1" not in pattern: + continue + if last_pattern is None: + last_pattern = pattern + # find left most set bit + lm_pos = list(pattern).index("1") + + # find changed bit + comp = [i != j for i, j in zip(pattern, last_pattern)] + if True in comp: + pos = comp.index(True) + else: + pos = None + if pos is not None: + if pos != lm_pos: + circuit.cx(ctls[pos], ctls[lm_pos]) + else: + indices = [i for i, x in enumerate(pattern) if x == "1"] + for idx in indices[1:]: + circuit.cx(ctls[idx], ctls[lm_pos]) + # check parity and undo rotation + if pattern.count("1") % 2 == 0: + # inverse CU: u(theta, phi, lamb)^dagger = u(-theta, -lam, -phi) + _apply_cu( + circuit, -theta, -lam, -phi, ctls[lm_pos], tgt, use_basis_gates=use_basis_gates + ) + else: + _apply_cu(circuit, theta, phi, lam, ctls[lm_pos], tgt, use_basis_gates=use_basis_gates) + last_pattern = pattern + + +def _mcsu2_real_diagonal( + gate: Gate, + num_controls: int, + use_basis_gates: bool = False, +) -> QuantumCircuit: + """ + Return a multi-controlled SU(2) gate [1]_ with a real main diagonal or secondary diagonal. + + Args: + gate: SU(2) Gate whose unitary matrix has one real diagonal. + num_controls: The number of control qubits. + use_basis_gates: If ``True``, use ``[p, u, cx]`` gates to implement the decomposition. + + Returns: + A :class:`.QuantumCircuit` implementing the multi-controlled SU(2) gate. + + Raises: + QiskitError: If the input matrix is invalid. + + References: + + .. [1]: R. Vale et al. Decomposition of Multi-controlled Special Unitary Single-Qubit Gates + `arXiv:2302.06377 (2023) `__ + + """ + # pylint: disable=cyclic-import + from qiskit.circuit.library.standard_gates import RXGate, RYGate, RZGate + from qiskit.circuit.library.generalized_gates import UnitaryGate + from qiskit.quantum_info.operators.predicates import is_unitary_matrix + from qiskit.compiler import transpile + from qiskit.synthesis.multi_controlled import synth_mcx_n_dirty_i15 + + if isinstance(gate, RYGate): + theta = gate.params[0] + s_gate = RYGate(-theta / 4) + is_secondary_diag_real = True + elif isinstance(gate, RZGate): + theta = gate.params[0] + s_gate = RZGate(-theta / 4) + is_secondary_diag_real = True + elif isinstance(gate, RXGate): + theta = gate.params[0] + s_gate = RZGate(-theta / 4) + is_secondary_diag_real = False + + else: + unitary = gate.to_matrix() + if unitary.shape != (2, 2): + raise QiskitError(f"The unitary must be a 2x2 matrix, but has shape {unitary.shape}.") + + if not is_unitary_matrix(unitary): + raise QiskitError(f"The unitary in must be an unitary matrix, but is {unitary}.") + + if not np.isclose(1.0, np.linalg.det(unitary)): + raise QiskitError( + "Invalid Value _mcsu2_real_diagonal requires det(unitary) equal to one." + ) + + is_main_diag_real = np.isclose(unitary[0, 0].imag, 0.0) and np.isclose( + unitary[1, 1].imag, 0.0 + ) + is_secondary_diag_real = np.isclose(unitary[0, 1].imag, 0.0) and np.isclose( + unitary[1, 0].imag, 0.0 + ) + + if not is_main_diag_real and not is_secondary_diag_real: + raise QiskitError("The unitary must have one real diagonal.") + + if is_secondary_diag_real: + x = unitary[0, 1] + z = unitary[1, 1] + else: + x = -unitary[0, 1].real + z = unitary[1, 1] - unitary[0, 1].imag * 1.0j + + if np.isclose(z, -1): + s_op = [[1.0, 0.0], [0.0, 1.0j]] + else: + alpha_r = math.sqrt((math.sqrt((z.real + 1.0) / 2.0) + 1.0) / 2.0) + alpha_i = z.imag / ( + 2.0 * math.sqrt((z.real + 1.0) * (math.sqrt((z.real + 1.0) / 2.0) + 1.0)) + ) + alpha = alpha_r + 1.0j * alpha_i + beta = x / (2.0 * math.sqrt((z.real + 1.0) * (math.sqrt((z.real + 1.0) / 2.0) + 1.0))) + + # S gate definition + s_op = np.array([[alpha, -np.conj(beta)], [beta, np.conj(alpha)]]) + s_gate = UnitaryGate(s_op) + + k_1 = math.ceil(num_controls / 2.0) + k_2 = math.floor(num_controls / 2.0) + + circuit = QuantumCircuit(num_controls + 1, name="MCSU2") + controls = list(range(num_controls)) # control indices, defined for code legibility + target = num_controls # target index, defined for code legibility + + if not is_secondary_diag_real: + circuit.h(target) + + mcx_1 = synth_mcx_n_dirty_i15(num_ctrl_qubits=k_1) + circuit.compose(mcx_1, controls[:k_1] + [target] + controls[k_1 : 2 * k_1 - 2], inplace=True) + circuit.append(s_gate, [target]) + + # TODO: improve CX count by using action_only=True (based on #9687) + mcx_2 = synth_mcx_n_dirty_i15(num_ctrl_qubits=k_2).to_gate() + circuit.compose( + mcx_2.inverse(), controls[k_1:] + [target] + controls[k_1 - k_2 + 2 : k_1], inplace=True + ) + circuit.append(s_gate.inverse(), [target]) + + mcx_3 = synth_mcx_n_dirty_i15(num_ctrl_qubits=k_1).to_gate() + circuit.compose(mcx_3, controls[:k_1] + [target] + controls[k_1 : 2 * k_1 - 2], inplace=True) + circuit.append(s_gate, [target]) + + mcx_4 = synth_mcx_n_dirty_i15(num_ctrl_qubits=k_2).to_gate() + circuit.compose(mcx_4, controls[k_1:] + [target] + controls[k_1 - k_2 + 2 : k_1], inplace=True) + circuit.append(s_gate.inverse(), [target]) + + if not is_secondary_diag_real: + circuit.h(target) + + if use_basis_gates: + circuit = transpile(circuit, basis_gates=["p", "u", "cx"], qubits_initially_zero=False) + + return circuit diff --git a/releasenotes/notes/fix-multi-controlled-rotation-gates-with-parameter-12a04701d0cd095b.yaml b/releasenotes/notes/fix-multi-controlled-rotation-gates-with-parameter-12a04701d0cd095b.yaml new file mode 100644 index 000000000000..7653507c98a2 --- /dev/null +++ b/releasenotes/notes/fix-multi-controlled-rotation-gates-with-parameter-12a04701d0cd095b.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fix a bug in the multi-controlled rotation circuit methods :meth:`.QuantumCircuit.mcrx`, + :meth:`.QuantumCircuit.mcry`, and :meth:`.QuantumCircuit.mcrz`, when the user provides an unbounded parameter, + as well as when calling :meth:`.RXGate.control`, :meth:`.RYGate.control` or :meth:`.RZGate.control` where the + rotation angle is a :class:`.ParameterExpression`. + Previously, the user got an error that this gate cannot be synthesized with unbound parameter, + and now these multi-controlled rotation circuits can be synthesized without raising an error. diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 997fa2fdb034..8ad5ca7d473a 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -86,7 +86,7 @@ ) from qiskit.circuit._utils import _compute_control_matrix import qiskit.circuit.library.standard_gates as allGates -from qiskit.circuit.library.standard_gates.multi_control_rotation_gates import _mcsu2_real_diagonal +from qiskit.synthesis.multi_controlled.multi_control_rotation_gates import _mcsu2_real_diagonal from qiskit.circuit.library.standard_gates.equivalence_library import ( StandardEquivalenceLibrary as std_eqlib, ) @@ -553,9 +553,9 @@ def test_mcsu2_real_diagonal(self): """Test mcsu2_real_diagonal""" num_ctrls = 6 theta = 0.3 - ry_matrix = RYGate(theta).to_matrix() - qc = _mcsu2_real_diagonal(ry_matrix, num_ctrls) + qc = _mcsu2_real_diagonal(RYGate(theta), num_ctrls) + ry_matrix = RYGate(theta).to_matrix() mcry_matrix = _compute_control_matrix(ry_matrix, 6) self.assertTrue(np.allclose(mcry_matrix, Operator(qc).to_matrix())) @@ -685,6 +685,23 @@ def test_mcry_defaults_to_vchain(self): dag = circuit_to_dag(circuit) self.assertEqual(len(list(dag.idle_wires())), 0) + @combine(num_controls=[1, 2, 3], base_gate=[RXGate, RYGate, RZGate, CPhaseGate]) + def test_multi_controlled_rotation_gate_with_parameter(self, num_controls, base_gate): + """Test multi-controlled rotation gates and MCPhase gate with Parameter synthesis.""" + theta = Parameter("theta") + params = [theta] + val = 0.4123 + rot_matrix = base_gate(val).to_matrix() + mc_matrix = _compute_control_matrix(rot_matrix, num_controls) + + mc_gate = base_gate(*params).control(num_controls) + circuit = QuantumCircuit(mc_gate.num_qubits) + circuit.append(mc_gate, circuit.qubits) + + bound = circuit.assign_parameters([val]) + unrolled = transpile(bound, basis_gates=["u", "cx"], optimization_level=0) + self.assertTrue(np.allclose(mc_matrix, Operator(unrolled).to_matrix())) + @data(1, 2) def test_mcx_gates_yield_explicit_gates(self, num_ctrl_qubits): """Test the creating a MCX gate yields the explicit definition if we know it.""" @@ -1443,8 +1460,6 @@ def test_control_zero_operand_gate(self, num_ctrl_qubits): self.assertEqual(Operator(controlled), Operator(target)) @data( - RXGate, - RYGate, RXXGate, RYYGate, RZXGate, @@ -1454,8 +1469,8 @@ def test_control_zero_operand_gate(self, num_ctrl_qubits): XXMinusYYGate, XXPlusYYGate, ) - def test_mc_failure_without_annotation(self, gate_cls): - """Test error for gates that cannot be multi-controlled without annotation.""" + def test_mc_without_annotation(self, gate_cls): + """Test multi-controlled gates with and without annotation.""" theta = Parameter("theta") num_params = len(_get_free_params(gate_cls.__init__, ignore=["self"])) params = [theta] + (num_params - 1) * [1.234] @@ -1463,22 +1478,17 @@ def test_mc_failure_without_annotation(self, gate_cls): for annotated in [False, None]: with self.subTest(annotated=annotated): # if annotated is False, check that a sensible error is raised - if annotated is False: - with self.assertRaisesRegex(QiskitError, "unbound parameter"): - _ = gate_cls(*params).control(5, annotated=False) - # else, check that the gate can be synthesized after all parameters # have been bound - else: - mc_gate = gate_cls(*params).control(5) + mc_gate = gate_cls(*params).control(5) - circuit = QuantumCircuit(mc_gate.num_qubits) - circuit.append(mc_gate, circuit.qubits) + circuit = QuantumCircuit(mc_gate.num_qubits) + circuit.append(mc_gate, circuit.qubits) - bound = circuit.assign_parameters([0.5123]) - unrolled = transpile(bound, basis_gates=["u", "cx"], optimization_level=0) + bound = circuit.assign_parameters([0.5123]) + unrolled = transpile(bound, basis_gates=["u", "cx"], optimization_level=0) - self.assertEqual(unrolled.num_parameters, 0) + self.assertEqual(unrolled.num_parameters, 0) def assertEqualTranslated(self, circuit, unrolled_reference, basis): """Assert that the circuit is equal to the unrolled reference circuit.""" From 55d2da89938349e06e94df743a0272e6a72c3087 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 8 Jan 2025 13:44:06 +0200 Subject: [PATCH 4/4] Add option ``collect_from_back`` to ``CollectMultiQBlocks`` (#13612) * adding option collect_from_back * new option, tests, reno * typo * improving test following review * test fix --- .../optimization/collect_multiqubit_blocks.py | 17 +++++++- ...on-collect-from-back-cde10ee5e2e4ea9f.yaml | 11 +++++ .../transpiler/test_collect_multiq_blocks.py | 40 +++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-option-collect-from-back-cde10ee5e2e4ea9f.yaml diff --git a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py index 34d51a17fe4a..c05d5f023b44 100644 --- a/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py +++ b/qiskit/transpiler/passes/optimization/collect_multiqubit_blocks.py @@ -33,13 +33,18 @@ class CollectMultiQBlocks(AnalysisPass): Some gates may not be present in any block (e.g. if the number of operands is greater than ``max_block_size``) + By default, blocks are collected in the direction from the inputs towards the + outputs of the DAG. The option ``collect_from_back`` allows to change this + direction, that is to collect blocks from the outputs towards the inputs. + Note that the blocks are still reported in a valid topological order. + A Disjoint Set Union data structure (DSU) is used to maintain blocks as gates are processed. This data structure points each qubit to a set at all times and the sets correspond to current blocks. These change over time and the data structure allows these changes to be done quickly. """ - def __init__(self, max_block_size=2): + def __init__(self, max_block_size=2, collect_from_back=False): super().__init__() self.parent = {} # parent array for the union @@ -49,6 +54,7 @@ def __init__(self, max_block_size=2): self.gate_groups = {} # current gate lists for the groups self.max_block_size = max_block_size # maximum block size + self.collect_from_back = collect_from_back # backward collection def find_set(self, index): """DSU function for finding root of set of items @@ -127,6 +133,10 @@ def collect_key(x): op_nodes = dag.topological_op_nodes(key=collect_key) + # When collecting from the back, the order of nodes is reversed + if self.collect_from_back: + op_nodes = reversed(list(op_nodes)) + for nd in op_nodes: can_process = True makes_too_big = False @@ -222,6 +232,11 @@ def collect_key(x): if item == index and len(self.gate_groups[index]) != 0: block_list.append(self.gate_groups[index][:]) + # When collecting from the back, both the order of the blocks + # and the order of nodes in each block should be reversed. + if self.collect_from_back: + block_list = [block[::-1] for block in block_list[::-1]] + self.property_set["block_list"] = block_list return dag diff --git a/releasenotes/notes/add-option-collect-from-back-cde10ee5e2e4ea9f.yaml b/releasenotes/notes/add-option-collect-from-back-cde10ee5e2e4ea9f.yaml new file mode 100644 index 000000000000..684b095905a0 --- /dev/null +++ b/releasenotes/notes/add-option-collect-from-back-cde10ee5e2e4ea9f.yaml @@ -0,0 +1,11 @@ +--- +features_transpiler: + - | + Added a new option, ``collect_from_back``, to + :class:`~qiskit.transpiler.passes.CollectMultiQBlocks`. + When set to ``True``, the blocks are collected in the reverse direction, + from the outputs towards the inputs of the circuit. The blocks are still + reported following the normal topological order. + This leads to an additional flexibility provided by the pass, and + additional optimization opportunities when combined with a circuit + resynthesis method. diff --git a/test/python/transpiler/test_collect_multiq_blocks.py b/test/python/transpiler/test_collect_multiq_blocks.py index 2d4bc8783764..0f7eb5dd9539 100644 --- a/test/python/transpiler/test_collect_multiq_blocks.py +++ b/test/python/transpiler/test_collect_multiq_blocks.py @@ -290,6 +290,46 @@ def test_larger_blocks(self): pass_manager.run(qc) + def test_collect_from_back(self): + """Test the option to collect blocks from the outputs towards + the inputs. + ┌───┐ + q_0: ┤ H ├──■────■────■─────── + └───┘┌─┴─┐ │ │ + q_1: ─────┤ X ├──┼────┼─────── + └───┘┌─┴─┐ │ + q_2: ──────────┤ X ├──┼─────── + └───┘┌─┴─┐┌───┐ + q_3: ───────────────┤ X ├┤ H ├ + └───┘└───┘ + """ + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.h(3) + + dag = circuit_to_dag(qc) + # For the circuit above, the topological order is unique + topo_ops = list(dag.topological_op_nodes()) + + # When collecting blocks of size-3 using the default direction, + # the first block should contain the H-gate and two CX-gates, + # and the second block should contain a single CX-gate and an H-gate. + pass_ = CollectMultiQBlocks(max_block_size=3, collect_from_back=False) + pass_.run(dag) + expected_blocks = [[topo_ops[0], topo_ops[1], topo_ops[2]], [topo_ops[3], topo_ops[4]]] + self.assertEqual(pass_.property_set["block_list"], expected_blocks) + + # When collecting blocks of size-3 using the opposite direction, + # the first block should contain the H-gate and a single CX-gate, + # and the second block should contain two CX-gates and an H-gate. + pass_ = CollectMultiQBlocks(max_block_size=3, collect_from_back=True) + pass_.run(dag) + expected_blocks = [[topo_ops[0], topo_ops[1]], [topo_ops[2], topo_ops[3], topo_ops[4]]] + self.assertEqual(pass_.property_set["block_list"], expected_blocks) + if __name__ == "__main__": unittest.main()