From 079f69f7dd906299dfdf8c1262fa9bc1d75e478b Mon Sep 17 00:00:00 2001 From: abbycross Date: Thu, 19 Dec 2024 15:20:11 -0500 Subject: [PATCH 01/15] fix typo and wordsmith (#13587) --- qiskit/transpiler/preset_passmanagers/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index cad512c07a52..fbbd5a6c202a 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -23,11 +23,11 @@ transformations as part of Qiskit's compiler inside the :func:`~.transpile` function at the different optimization levels, but can also be used in a standalone manner. -The functionality here is divided into two parts, the first includes the -functions used generate the entire pass manager which is used by -:func:`~.transpile` (:ref:`preset_pass_manager_generators`) and the -second includes functions which are used to build (either entirely or in -part) the stages which the preset pass managers are composed of +The functionality here is divided into two parts. The first includes the +functions used to generate the entire pass manager, which is used by +:func:`~.transpile` (:ref:`preset_pass_manager_generators`), and the +second includes functions that are used to build (either entirely or in +part) the stages that comprise the preset pass managers (:ref:`stage_generators`). .. _preset_pass_manager_generators: From eced049c79b4f2c5934bd48f98333029f3fe4c38 Mon Sep 17 00:00:00 2001 From: Divyanshu Singh <55018955+divshacker@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:08:11 +0530 Subject: [PATCH 02/15] fixed_typo #13576 (#13607) --- qiskit/synthesis/evolution/qdrift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/synthesis/evolution/qdrift.py b/qiskit/synthesis/evolution/qdrift.py index 13b6b6dd79e6..02e06edae8f7 100644 --- a/qiskit/synthesis/evolution/qdrift.py +++ b/qiskit/synthesis/evolution/qdrift.py @@ -32,7 +32,7 @@ class QDrift(ProductFormula): - r"""The QDrift Trotterization method, which selects each each term in the + r"""The QDrift Trotterization method, which selects each term in the Trotterization randomly, with a probability proportional to its weight. Based on the work of Earl Campbell in Ref. [1]. From 136aaff14fb53f68e31029a51e9aaddb1b4cad15 Mon Sep 17 00:00:00 2001 From: trigpolynom Date: Mon, 6 Jan 2025 03:01:01 -0500 Subject: [PATCH 03/15] Fix ``SparsePauliOp`` initialization with dense ``Y`` labels (#13580) * used modular arithmetic to fix floating point error in heavy-weight sparse_pauli_op * fix comments * reformatted * added release notes and addressed comments * addressed comments * formatted * addressed comments * addressed comments * addressed all comments * formatted --- .../operators/symplectic/sparse_pauli_op.py | 11 ++++++++++- ...se-pauli-op-heavy-weight-fix-aa822428643d642a.yaml | 7 +++++++ .../operators/symplectic/test_sparse_pauli_op.py | 5 +++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/sparse-pauli-op-heavy-weight-fix-aa822428643d642a.yaml diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 34e524348bf8..50b80ec89051 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -167,7 +167,16 @@ def __init__( # move the phase of `pauli_list` to `self._coeffs` phase = pauli_list._phase count_y = pauli_list._count_y() - self._coeffs = np.asarray((-1j) ** (phase - count_y) * coeffs, dtype=coeffs.dtype) + + # Compute exponentiation via integer arithmetic and lookup table to avoid + # floating point errors + exponent = (phase - count_y) % 4 + lookup = np.array([1 + 0j, -1j, -1 + 0j, 1j], dtype=coeffs.dtype) + + vals = lookup[exponent] + self._coeffs = vals * coeffs + + # Update pauli_list phase pauli_list._phase = np.mod(count_y, 4) self._pauli_list = pauli_list diff --git a/releasenotes/notes/sparse-pauli-op-heavy-weight-fix-aa822428643d642a.yaml b/releasenotes/notes/sparse-pauli-op-heavy-weight-fix-aa822428643d642a.yaml new file mode 100644 index 000000000000..d285c6fb6857 --- /dev/null +++ b/releasenotes/notes/sparse-pauli-op-heavy-weight-fix-aa822428643d642a.yaml @@ -0,0 +1,7 @@ +fixes: + - | + Fixed a bug where a initializing :class:`.SparsePauliOp` with a large + number of Pauli-``Y`` terms (typically :math:`\geq 100`) and no explicit + ``coeffs`` would result in a coefficient close to 1 but with a floating point + error. The coefficient is now correctly 1 per default. + Fixed `#13522 `__. \ No newline at end of file diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index 766011042e43..3602492030c4 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -140,6 +140,11 @@ def test_sparse_pauli_op_init(self): coeffs[:] = 0 self.assertEqual(spp_op, ref_op) + def test_sparse_pauli_op_init_long_ys(self): + """Test heavy-weight SparsePauliOp initialization.""" + y = SparsePauliOp("Y" * 1000) + self.assertEqual(1, y.coeffs[0]) + @ddt.ddt class TestSparsePauliOpConversions(QiskitTestCase): From cc932b89fcc11c016966966857af627902cba5c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:42:34 -0500 Subject: [PATCH 04/15] Bump bytemuck from 1.20.0 to 1.21.0 (#13598) Bumps [bytemuck](https://github.com/Lokathor/bytemuck) from 1.20.0 to 1.21.0. - [Changelog](https://github.com/Lokathor/bytemuck/blob/main/changelog.md) - [Commits](https://github.com/Lokathor/bytemuck/compare/v1.20.0...v1.21.0) --- updated-dependencies: - dependency-name: bytemuck dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2170ee3a9945..74ef042e64d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" dependencies = [ "bytemuck_derive", ] diff --git a/Cargo.toml b/Cargo.toml index 14b2504000a4..35fa7cb3c39d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ license = "Apache-2.0" # # Each crate can add on specific features freely as it inherits. [workspace.dependencies] -bytemuck = "1.20" +bytemuck = "1.21" indexmap.version = "2.7.0" hashbrown.version = "0.14.5" num-bigint = "0.4" From 0d4ee23bdd410ee94055e9abc780323f7e9803ab Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 7 Jan 2025 11:10:16 +0200 Subject: [PATCH 05/15] Fix `random_clifford` (#13606) * fix and tests * Using a fixed rather than random clifford in one of the tests * reno * restoring previous docstring that was changed accidentally * updating in-code comment * also checking decompositions for random 3q cliffords --- .../src/synthesis/clifford/random_clifford.rs | 8 ++--- .../fix-random-clifford-c0394becbdd7db50.yaml | 5 ++++ .../operators/symplectic/test_clifford.py | 11 +++++-- .../quantum_info/operators/test_random.py | 30 +++++++++++++++++++ 4 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/fix-random-clifford-c0394becbdd7db50.yaml diff --git a/crates/accelerate/src/synthesis/clifford/random_clifford.rs b/crates/accelerate/src/synthesis/clifford/random_clifford.rs index fee3ac8e9650..57531efcea55 100644 --- a/crates/accelerate/src/synthesis/clifford/random_clifford.rs +++ b/crates/accelerate/src/synthesis/clifford/random_clifford.rs @@ -125,17 +125,15 @@ pub fn random_clifford_tableau_inner(num_qubits: usize, seed: Option) -> Ar // Compute the full stabilizer tableau - // The code below is identical to the Python implementation, but is based on the original - // code in the paper. - + // The code below is based on the original code in the referenced paper. let mut table = Array2::from_elem((2 * num_qubits, 2 * num_qubits), false); // Apply qubit permutation for i in 0..num_qubits { - replace_row_inner(table.view_mut(), i, table2.slice(s![i, ..])); + replace_row_inner(table.view_mut(), i, table2.slice(s![perm[i], ..])); replace_row_inner( table.view_mut(), - perm[i] + num_qubits, + i + num_qubits, table2.slice(s![perm[i] + num_qubits, ..]), ); } diff --git a/releasenotes/notes/fix-random-clifford-c0394becbdd7db50.yaml b/releasenotes/notes/fix-random-clifford-c0394becbdd7db50.yaml new file mode 100644 index 000000000000..42a77da5210b --- /dev/null +++ b/releasenotes/notes/fix-random-clifford-c0394becbdd7db50.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :func:`~qiskit.quantum_info.random_clifford` that stopped it + from sampling the full Clifford group. diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index 253bc15852b6..177f9a9ed77d 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -473,7 +473,12 @@ def test_from_circuit_with_all_types(self): # and even circuits with other clifford objects. linear_function = LinearFunction([[0, 1], [1, 1]]) pauli_gate = PauliGate("YZ") - cliff = random_clifford(2, seed=777) + + qc_cliff = QuantumCircuit(2) + qc_cliff.h(0) + qc_cliff.cx(0, 1) + cliff = Clifford(qc_cliff) + qc = QuantumCircuit(2) qc.cx(0, 1) qc.append(random_clifford(1, seed=999), [1]) @@ -493,8 +498,8 @@ def test_from_circuit_with_all_types(self): # Additionally, make sure that it produces the correct clifford. expected_clifford_dict = { - "stabilizer": ["-IZX", "+XXZ", "-YYZ"], - "destabilizer": ["-YYI", "-XZI", "-ZXY"], + "stabilizer": ["-IZX", "+ZYZ", "+XZI"], + "destabilizer": ["+XZZ", "-XII", "+IXY"], } expected_clifford = Clifford.from_dict(expected_clifford_dict) self.assertEqual(combined_clifford, expected_clifford) diff --git a/test/python/quantum_info/operators/test_random.py b/test/python/quantum_info/operators/test_random.py index cb7e85ffabe3..bb1e65ba3d7d 100644 --- a/test/python/quantum_info/operators/test_random.py +++ b/test/python/quantum_info/operators/test_random.py @@ -190,6 +190,36 @@ def test_not_global_seed(self): rng_after = np.random.randint(1000, size=test_cases) self.assertFalse(np.all(rng_before == rng_after)) + def test_cliffords_2q(self): + """Test that we get all 2-qubit Cliffords (actually symplectic + matrices) with sufficiently many trials. + """ + seen = set() + for seed in range(10000): + cliff = random_clifford(2, seed) + seen.add(cliff.symplectic_matrix.tobytes()) + self.assertEqual(len(seen), 720) + + def test_clifford_2q_decompositions(self): + """Test that we get all possible CX-counts for 2q-random cliffords + with sufficiently many trials. + """ + seen = set() + for seed in range(100): + cliff = random_clifford(2, seed) + seen.add(cliff.to_circuit().count_ops().get("cx", 0)) + self.assertEqual(seen, {0, 1, 2, 3}) + + def test_clifford_3q_decompositions(self): + """Test that we get all possible CX-counts for 3q-random cliffords + with sufficiently many trials. + """ + seen = set() + for seed in range(10000): + cliff = random_clifford(3, seed) + seen.add(cliff.to_circuit().count_ops().get("cx", 0)) + self.assertEqual(seen, {0, 1, 2, 3, 4, 5, 6}) + @ddt class TestRandomPauliList(QiskitTestCase): From b9cba9f1490f1a9684d205b7915800a77abdec24 Mon Sep 17 00:00:00 2001 From: gadial Date: Tue, 7 Jan 2025 12:21:15 +0200 Subject: [PATCH 06/15] ObservablesArray docstring improvement (#13423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ObservablesArray docstring improvement * Update qiskit/primitives/containers/observables_array.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update qiskit/primitives/containers/observables_array.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Linting * Added `ObservableLike` as possible return type --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- .../containers/observables_array.py | 21 +++++++++++++++++-- ...bles_array_docstring-d6e74b1871e3145c.yaml | 6 ++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/improve_observables_array_docstring-d6e74b1871e3145c.yaml diff --git a/qiskit/primitives/containers/observables_array.py b/qiskit/primitives/containers/observables_array.py index 21c415d75899..f339589ac79d 100644 --- a/qiskit/primitives/containers/observables_array.py +++ b/qiskit/primitives/containers/observables_array.py @@ -97,8 +97,25 @@ def __repr__(self): array = np.array2string(self._array, prefix=prefix, suffix=suffix, threshold=50) return prefix + array + suffix - def tolist(self) -> list: - """Convert to a nested list""" + def tolist(self) -> list | ObservableLike: + """Convert to a nested list. + + Similar to Numpy's ``tolist`` method, the level of nesting + depends on the dimension of the observables array. In the + case of dimension 0 the method returns a single observable + (``dict`` in the case of a weighted sum of Paulis) instead of a list. + + Examples:: + Return values for a one-element list vs one element: + + >>> from qiskit.primitives.containers.observables_array import ObservablesArray + >>> oa = ObservablesArray.coerce(["Z"]) + >>> print(type(oa.tolist())) + + >>> oa = ObservablesArray.coerce("Z") + >>> print(type(oa.tolist())) + + """ return self._array.tolist() def __array__(self, dtype=None, copy=None): diff --git a/releasenotes/notes/improve_observables_array_docstring-d6e74b1871e3145c.yaml b/releasenotes/notes/improve_observables_array_docstring-d6e74b1871e3145c.yaml new file mode 100644 index 000000000000..061dc8cfa4ce --- /dev/null +++ b/releasenotes/notes/improve_observables_array_docstring-d6e74b1871e3145c.yaml @@ -0,0 +1,6 @@ +--- +features_primitives: + - | + Expanded the docstring of :meth:`.ObservablesArray.tolist` + to make it clear it might return a scalar in the case + the observables array is of dimension 0. From 0eeba0d2560524f37918e23e1eb153971349fd50 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 7 Jan 2025 12:14:13 +0100 Subject: [PATCH 07/15] Only apply MCMT plugin on ``MCMTGate`` (#13596) * Only apply MCMT plugin on MCMTGate * add checks for other gates * fix reno * use == instead of equiv --- .../passes/synthesis/hls_plugins.py | 57 ++++++++++++++++--- .../fix-mcmt-to-gate-ec84e1c625312444.yaml | 15 +++++ test/python/circuit/library/test_mcmt.py | 13 +++++ .../python/transpiler/test_clifford_passes.py | 11 ++++ .../transpiler/test_high_level_synthesis.py | 33 +++++++++++ 5 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/fix-mcmt-to-gate-ec84e1c625312444.yaml diff --git a/qiskit/transpiler/passes/synthesis/hls_plugins.py b/qiskit/transpiler/passes/synthesis/hls_plugins.py index eef996e636eb..11f9958f076c 100644 --- a/qiskit/transpiler/passes/synthesis/hls_plugins.py +++ b/qiskit/transpiler/passes/synthesis/hls_plugins.py @@ -420,12 +420,13 @@ C3XGate, C4XGate, PauliEvolutionGate, + PermutationGate, + MCMTGate, ModularAdderGate, HalfAdderGate, FullAdderGate, MultiplierGate, ) -from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.coupling import CouplingMap from qiskit.synthesis.clifford import ( @@ -467,6 +468,7 @@ multiplier_qft_r17, multiplier_cumulative_h18, ) +from qiskit.quantum_info.operators import Clifford from qiskit.transpiler.passes.routing.algorithms import ApproximateTokenSwapper from .plugin import HighLevelSynthesisPlugin @@ -484,6 +486,9 @@ class DefaultSynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + decomposition = synth_clifford_full(high_level_object) return decomposition @@ -497,6 +502,9 @@ class AGSynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + decomposition = synth_clifford_ag(high_level_object) return decomposition @@ -513,10 +521,14 @@ class BMSynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + if high_level_object.num_qubits <= 3: decomposition = synth_clifford_bm(high_level_object) else: decomposition = None + return decomposition @@ -530,6 +542,9 @@ class GreedySynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + decomposition = synth_clifford_greedy(high_level_object) return decomposition @@ -544,6 +559,9 @@ class LayerSynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + decomposition = synth_clifford_layers(high_level_object) return decomposition @@ -559,6 +577,9 @@ class LayerLnnSynthesisClifford(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Clifford.""" + if not isinstance(high_level_object, Clifford): + return None + decomposition = synth_clifford_depth_lnn(high_level_object) return decomposition @@ -572,6 +593,9 @@ class DefaultSynthesisLinearFunction(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given LinearFunction.""" + if not isinstance(high_level_object, LinearFunction): + return None + decomposition = synth_cnot_count_full_pmh(high_level_object.linear) return decomposition @@ -595,11 +619,8 @@ class KMSSynthesisLinearFunction(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given LinearFunction.""" - if not isinstance(high_level_object, LinearFunction): - raise TranspilerError( - "PMHSynthesisLinearFunction only accepts objects of type LinearFunction" - ) + return None use_inverted = options.get("use_inverted", False) use_transposed = options.get("use_transposed", False) @@ -646,11 +667,8 @@ class PMHSynthesisLinearFunction(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given LinearFunction.""" - if not isinstance(high_level_object, LinearFunction): - raise TranspilerError( - "PMHSynthesisLinearFunction only accepts objects of type LinearFunction" - ) + return None section_size = options.get("section_size", 2) use_inverted = options.get("use_inverted", False) @@ -682,6 +700,9 @@ class KMSSynthesisPermutation(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Permutation.""" + if not isinstance(high_level_object, PermutationGate): + return None + decomposition = synth_permutation_depth_lnn_kms(high_level_object.pattern) return decomposition @@ -695,6 +716,9 @@ class BasicSynthesisPermutation(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Permutation.""" + if not isinstance(high_level_object, PermutationGate): + return None + decomposition = synth_permutation_basic(high_level_object.pattern) return decomposition @@ -708,6 +732,9 @@ class ACGSynthesisPermutation(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Permutation.""" + if not isinstance(high_level_object, PermutationGate): + return None + decomposition = synth_permutation_acg(high_level_object.pattern) return decomposition @@ -858,6 +885,9 @@ class TokenSwapperSynthesisPermutation(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): """Run synthesis for the given Permutation.""" + if not isinstance(high_level_object, PermutationGate): + return None + trials = options.get("trials", 5) seed = options.get("seed", 0) parallel_threshold = options.get("parallel_threshold", 50) @@ -1156,6 +1186,9 @@ class MCMTSynthesisDefault(HighLevelSynthesisPlugin): def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): # first try to use the V-chain synthesis if enough auxiliary qubits are available + if not isinstance(high_level_object, MCMTGate): + return None + if ( decomposition := MCMTSynthesisVChain().run( high_level_object, coupling_map, target, qubits, **options @@ -1170,6 +1203,9 @@ class MCMTSynthesisNoAux(HighLevelSynthesisPlugin): """A V-chain based synthesis for ``MCMTGate``.""" def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, MCMTGate): + return None + base_gate = high_level_object.base_gate ctrl_state = options.get("ctrl_state", None) @@ -1195,6 +1231,9 @@ class MCMTSynthesisVChain(HighLevelSynthesisPlugin): """A V-chain based synthesis for ``MCMTGate``.""" def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + if not isinstance(high_level_object, MCMTGate): + return None + if options.get("num_clean_ancillas", 0) < high_level_object.num_ctrl_qubits - 1: return None # insufficient number of auxiliary qubits diff --git a/releasenotes/notes/fix-mcmt-to-gate-ec84e1c625312444.yaml b/releasenotes/notes/fix-mcmt-to-gate-ec84e1c625312444.yaml new file mode 100644 index 000000000000..04fb9cc3f1b5 --- /dev/null +++ b/releasenotes/notes/fix-mcmt-to-gate-ec84e1c625312444.yaml @@ -0,0 +1,15 @@ +--- +fixes: + - | + Fixed a bug where any instruction called ``"mcmt"`` would be passed into the high-level + synthesis routine for a :class:`.MCMTGate`, which causes a failure or invalid result. + In particular, this could happen accidentally when handling the :class:`.MCMT` _circuit_, + named ``"mcmt"``, and implicitly converting it into an instruction e.g. when appending + it to a circuit. + Fixed `#13563 `__. +upgrade_synthesis: + - | + The plugins for :class:`.LinearFunction` no longer raise an error if another object + than :class:`.LinearFunction` is passed into the ``run`` method. Instead, ``None`` is + returned, which is consistent with the other plugins. If you relied on this error being raised, + you can manually perform an instance-check. \ No newline at end of file diff --git a/test/python/circuit/library/test_mcmt.py b/test/python/circuit/library/test_mcmt.py index 73befb19db46..435ee0629593 100644 --- a/test/python/circuit/library/test_mcmt.py +++ b/test/python/circuit/library/test_mcmt.py @@ -285,6 +285,19 @@ def test_gate_with_parameters_vchain(self): self.assertEqual(circuit.num_parameters, 1) self.assertIs(circuit.parameters[0], theta) + def test_mcmt_circuit_as_gate(self): + """Test the MCMT plugin is only triggered for the gate, not the same-named circuit. + + Regression test of #13563. + """ + circuit = QuantumCircuit(2) + gate = RYGate(0.1) + mcmt = MCMT(gate=gate, num_ctrl_qubits=1, num_target_qubits=1) + circuit.append(mcmt, circuit.qubits) # append the MCMT circuit as gate called "MCMT" + + transpiled = transpile(circuit, basis_gates=["u", "cx"]) + self.assertEqual(Operator(transpiled), Operator(gate.control(1))) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_clifford_passes.py b/test/python/transpiler/test_clifford_passes.py index 2a39c45bc48a..50bf2ecb493e 100644 --- a/test/python/transpiler/test_clifford_passes.py +++ b/test/python/transpiler/test_clifford_passes.py @@ -831,6 +831,17 @@ def test_collect_all_clifford_gates(self): qct = PassManager(CollectCliffords(matrix_based=True)).run(qc) self.assertEqual(qct.count_ops()["clifford"], 1) + def test_plugin_unfortunate_name(self): + """Test the synthesis is not triggered for a custom gate with the same name.""" + intruder = QuantumCircuit(2, name="clifford") + circuit = QuantumCircuit(2) + circuit.append(intruder.to_gate(), [0, 1]) + + hls = HighLevelSynthesis() + synthesized = hls(circuit) + + self.assertIn("clifford", synthesized.count_ops()) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 11aa8501afd0..b7bc31132a48 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -827,6 +827,17 @@ def test_plugin_selection_all_with_metrix(self): self.assertEqual(qct.size(), 24) self.assertEqual(qct.depth(), 13) + def test_unfortunate_name(self): + """Test the synthesis is not triggered for a custom gate with the same name.""" + intruder = QuantumCircuit(2, name="linear_function") + circuit = QuantumCircuit(2) + circuit.append(intruder.to_gate(), [0, 1]) + + hls = HighLevelSynthesis() + synthesized = hls(circuit) + + self.assertIn("linear_function", synthesized.count_ops()) + class TestKMSSynthesisLinearFunctionPlugin(QiskitTestCase): """Tests for the KMSSynthesisLinearFunction plugin for synthesizing linear functions.""" @@ -877,6 +888,17 @@ def test_invert_and_transpose(self): self.assertEqual(qct.size(), 87) self.assertEqual(qct.depth(), 32) + def test_unfortunate_name(self): + """Test the synthesis is not triggered for a custom gate with the same name.""" + intruder = QuantumCircuit(2, name="linear_function") + circuit = QuantumCircuit(2) + circuit.append(intruder.to_gate(), [0, 1]) + + hls = HighLevelSynthesis() + synthesized = hls(circuit) + + self.assertIn("linear_function", synthesized.count_ops()) + class TestTokenSwapperPermutationPlugin(QiskitTestCase): """Tests for the token swapper plugin for synthesizing permutation gates.""" @@ -1059,6 +1081,17 @@ def test_concrete_synthesis_all_permutations(self): qubits = tuple(qc_transpiled.find_bit(q).index for q in inst.qubits) self.assertIn(qubits, edges) + def test_unfortunate_name(self): + """Test the synthesis is not triggered for a custom gate with the same name.""" + intruder = QuantumCircuit(2, name="permutation") + circuit = QuantumCircuit(2) + circuit.append(intruder.to_gate(), [0, 1]) + + hls = HighLevelSynthesis() + synthesized = hls(circuit) + + self.assertIn("permutation", synthesized.count_ops()) + class TestHighLevelSynthesisModifiers(QiskitTestCase): """Tests for high-level-synthesis pass.""" From 86a5325ef77ce8fbc9a02f3ada8e0d65e9c2c98c Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 7 Jan 2025 13:25:06 +0100 Subject: [PATCH 08/15] Fix `UnitarySynthesis` for 3+ qubits when compiled for a backend (#13591) * Fix unitary synthesis for 3+ q * Update releasenotes/notes/fix-unitary-synthesis-3q-2b2de5305bfd11ff.yaml Co-authored-by: Alexander Ivrii * add more tests * use == over equiv --------- Co-authored-by: Alexander Ivrii --- crates/accelerate/src/unitary_synthesis.rs | 3 +- ...unitary-synthesis-3q-2b2de5305bfd11ff.yaml | 8 +++ .../transpiler/test_unitary_synthesis.py | 51 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-unitary-synthesis-3q-2b2de5305bfd11ff.yaml diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 62f41c78084c..fa5880b1697a 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -372,7 +372,8 @@ fn py_run_main_loop( None, None, )?; - out_dag = synth_dag; + let out_qargs = dag.get_qargs(packed_instr.qubits); + apply_synth_dag(py, &mut out_dag, out_qargs, &synth_dag)?; } } } diff --git a/releasenotes/notes/fix-unitary-synthesis-3q-2b2de5305bfd11ff.yaml b/releasenotes/notes/fix-unitary-synthesis-3q-2b2de5305bfd11ff.yaml new file mode 100644 index 000000000000..248ff0bc2195 --- /dev/null +++ b/releasenotes/notes/fix-unitary-synthesis-3q-2b2de5305bfd11ff.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed a bug in the :class:`.UnitarySynthesis` transpiler pass, where blocks of + :class:`.UnitaryGate`\s on 3 qubits or more were not correctly synthesized. + This led, e.g., to the circuit being overwritten with the last processed block or + to internal panics when encountering measurements after such a block. + Fixed `#13586 `__. diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 5e39c9d982c0..7f2bb2f89911 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -57,7 +57,9 @@ RYYGate, RZZGate, RXXGate, + PauliEvolutionGate, ) +from qiskit.quantum_info import SparsePauliOp from qiskit.circuit import Measure from qiskit.circuit.controlflow import IfElseOp from qiskit.circuit import Parameter, Gate @@ -1050,6 +1052,55 @@ def test_qsd(self, opt): qc_transpiled = transpile(qc, target=target, optimization_level=opt) self.assertTrue(np.allclose(mat, Operator(qc_transpiled).data)) + def test_3q_with_measure(self): + """Test 3-qubit synthesis with measurements.""" + backend = FakeBackend5QV2() + + qc = QuantumCircuit(3, 1) + qc.unitary(np.eye(2**3), range(3)) + qc.measure(0, 0) + + qc_transpiled = transpile(qc, backend) + self.assertTrue(qc_transpiled.size, 1) + self.assertTrue(qc_transpiled.count_ops().get("measure", 0), 1) + + def test_3q_series(self): + """Test a series of 3-qubit blocks.""" + backend = GenericBackendV2(5, basis_gates=["u", "cx"]) + + x = QuantumCircuit(3) + x.x(2) + x_mat = Operator(x) + + qc = QuantumCircuit(3) + qc.unitary(x_mat, range(3)) + qc.unitary(np.eye(2**3), range(3)) + + tqc = transpile(qc, backend, optimization_level=0, initial_layout=[0, 1, 2]) + + expected = np.kron(np.eye(2**2), x_mat) + self.assertEqual(Operator(tqc), Operator(expected)) + + def test_3q_measure_all(self): + """Regression test of #13586.""" + hamiltonian = SparsePauliOp.from_list( + [("IXX", 1), ("IYY", 1), ("IZZ", 1), ("XXI", 1), ("YYI", 1), ("ZZI", 1)] + ) + + qc = QuantumCircuit(3) + qc.x([1, 2]) + op = PauliEvolutionGate(hamiltonian, time=1) + qc.append(op.power(8), [0, 1, 2]) + qc.measure_all() + + backend = GenericBackendV2(5, basis_gates=["u", "cx"]) + tqc = transpile(qc, backend) + + ops = tqc.count_ops() + self.assertIn("u", ops) + self.assertIn("cx", ops) + self.assertIn("measure", ops) + if __name__ == "__main__": unittest.main() From 93d796f6df3717e6aa1c18c1ee897ca18e080729 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 7 Jan 2025 16:58:13 +0100 Subject: [PATCH 09/15] 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 10/15] 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 11/15] 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 12/15] 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() From 8b2f6bd83ce8839c3efcb24d784dbb5f888db307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:48:39 +0100 Subject: [PATCH 13/15] Close previous figures in docs plots. (#13635) --- qiskit/circuit/library/grover_operator.py | 6 +++--- qiskit/circuit/library/n_local/efficient_su2.py | 2 +- qiskit/circuit/library/n_local/excitation_preserving.py | 2 +- qiskit/circuit/library/n_local/n_local.py | 8 ++++---- qiskit/circuit/library/n_local/real_amplitudes.py | 6 +++--- qiskit/transpiler/__init__.py | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/qiskit/circuit/library/grover_operator.py b/qiskit/circuit/library/grover_operator.py index d55a59249a3d..8b6aeaf2db8c 100644 --- a/qiskit/circuit/library/grover_operator.py +++ b/qiskit/circuit/library/grover_operator.py @@ -118,7 +118,7 @@ def grover_operator( .. plot:: :include-source: - :context: + :context: close-figs oracle = QuantumCircuit(1) oracle.z(0) # the qubit state |1> is the good state @@ -133,7 +133,7 @@ def grover_operator( .. plot:: :include-source: - :context: + :context: close-figs oracle = QuantumCircuit(4) oracle.z(3) @@ -150,7 +150,7 @@ def grover_operator( .. plot:: :include-source: - :context: + :context: close-figs from qiskit.quantum_info import Statevector, DensityMatrix, Operator diff --git a/qiskit/circuit/library/n_local/efficient_su2.py b/qiskit/circuit/library/n_local/efficient_su2.py index c9589a22c9fa..954e74a3d296 100644 --- a/qiskit/circuit/library/n_local/efficient_su2.py +++ b/qiskit/circuit/library/n_local/efficient_su2.py @@ -85,7 +85,7 @@ def efficient_su2( .. plot:: :include-source: - :context: + :context: close-figs circuit = efficient_su2(4, su2_gates=["rx", "y"], entanglement="circular", reps=1) circuit.draw("mpl") diff --git a/qiskit/circuit/library/n_local/excitation_preserving.py b/qiskit/circuit/library/n_local/excitation_preserving.py index b2fd3f0cdc43..3623834f2b6d 100644 --- a/qiskit/circuit/library/n_local/excitation_preserving.py +++ b/qiskit/circuit/library/n_local/excitation_preserving.py @@ -71,7 +71,7 @@ def excitation_preserving( .. plot:: :include-source: - :context: + :context: close-figs from qiskit.circuit.library import excitation_preserving diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py index 8c0b4d285086..a714a4699737 100644 --- a/qiskit/circuit/library/n_local/n_local.py +++ b/qiskit/circuit/library/n_local/n_local.py @@ -161,7 +161,7 @@ def n_local( .. plot:: :include-source: - :context: + :context: close-figs circuit = n_local(3, ["ry", "rz"], "cz", "full", reps=1, insert_barriers=True) circuit.draw("mpl") @@ -170,7 +170,7 @@ def n_local( .. plot:: :include-source: - :context: + :context: close-figs circuit = n_local(4, [], "cry", reps=2) circuit.draw("mpl") @@ -179,7 +179,7 @@ def n_local( .. plot:: :include-source: - :context: + :context: close-figs entangler_map = [[0, 1], [2, 0]] circuit = n_local(3, "x", "crx", entangler_map, reps=2) @@ -191,7 +191,7 @@ def n_local( .. plot: :include-source: - :context: + :context: close-figs def entanglement(layer_index): if layer_index % 2 == 0: diff --git a/qiskit/circuit/library/n_local/real_amplitudes.py b/qiskit/circuit/library/n_local/real_amplitudes.py index 572cc036ad73..79d85db5b603 100644 --- a/qiskit/circuit/library/n_local/real_amplitudes.py +++ b/qiskit/circuit/library/n_local/real_amplitudes.py @@ -78,21 +78,21 @@ def real_amplitudes( .. plot:: :include-source: - :context: + :context: close-figs ansatz = real_amplitudes(3, entanglement="full", reps=2) # it is the same unitary as above ansatz.draw("mpl") .. plot:: :include-source: - :context: + :context: close-figs ansatz = real_amplitudes(3, entanglement="linear", reps=2, insert_barriers=True) ansatz.draw("mpl") .. plot:: :include-source: - :context: + :context: close-figs ansatz = real_amplitudes(4, reps=2, entanglement=[[0,3], [0,2]], skip_unentangled_qubits=True) ansatz.draw("mpl") diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 7924d4034aef..b5c64bc95a95 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -620,7 +620,7 @@ ['id', 'rz', 'sx', 'x', 'cx', 'measure', 'delay'] - .. plot: + .. plot:: :include-source: from qiskit.circuit import QuantumCircuit From 682cf3b7887d88d187164ec5dc723874c4e1c294 Mon Sep 17 00:00:00 2001 From: Hank Greenburg Date: Wed, 8 Jan 2025 10:04:34 -0800 Subject: [PATCH 14/15] build: update hashbrown to fix security vuln (#13622) --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74ef042e64d3..133299e5a34a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -558,9 +558,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -581,7 +581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.2", "rayon", ] From 408741ca1ae4d291f6ae12d3f419ecd841ad8b39 Mon Sep 17 00:00:00 2001 From: aeddins-ibm <60495383+aeddins-ibm@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:25:11 -0800 Subject: [PATCH 15/15] Fix phase of `pauli_list.insert(..., qubit=True)` for length-1 `pauli_list` (#13624) * switch order of if-clauses `len(value) == 1` is the simplest case, and is also the case where the problematic clause `len(value) == size` fails. This commit switches the order, so we check for `len(value) == 1` first. This ensures that when the `len(value) == size` clause runs, we know that `size != 1`, avoiding the bug in #13623. * add test * Simplify `value.phase` broadcasting Here, `phase` should be a 1D array. `np.vstack()` docs say explicitly output will be at least 2D, so we should not use that to create `phase`. The intent of using `np.vstack()` was essentially to broadcast `phase` to properly add to `self.phase`. But, this happens automatically and correctly if we just add as-is. So this commit simplifies the code accordingly. * Verify that `phase` is 1D in `from_symplectic()` Verifying the input arg `phase`, since otherwise `from_symplectic()` will silently create PauliLists with malformed `phase` attributes (i.e. not 1D). Zero-dimensional is OK (e.g. `phase` defaults to `0`) since that can broadcast per the shape of the `z` and `x` arrays. * remove vestigial line * lint * release note * Update releasenotes/notes/fix-paulilist-length1-phase-688d0e3a64ec9a9f.yaml Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- .../operators/symplectic/pauli_list.py | 16 ++++++++-------- ...paulilist-length1-phase-688d0e3a64ec9a9f.yaml | 6 ++++++ .../operators/symplectic/test_pauli_list.py | 9 +++++++++ 3 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/fix-paulilist-length1-phase-688d0e3a64ec9a9f.yaml diff --git a/qiskit/quantum_info/operators/symplectic/pauli_list.py b/qiskit/quantum_info/operators/symplectic/pauli_list.py index af2a0ed9407f..8e23b6f31a87 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli_list.py +++ b/qiskit/quantum_info/operators/symplectic/pauli_list.py @@ -451,16 +451,14 @@ def insert(self, ind: int, value: PauliList, qubit: bool = False) -> PauliList: f"Index {ind} is greater than number of qubits" f" in the PauliList ({self.num_qubits})" ) - if len(value) == 1: - # Pad blocks to correct size - value_x = np.vstack(size * [value.x]) - value_z = np.vstack(size * [value.z]) - value_phase = np.vstack(size * [value.phase]) - elif len(value) == size: + if len(value) == size: # Blocks are already correct size value_x = value.x value_z = value.z - value_phase = value.phase + elif len(value) == 1: + # Pad blocks to correct size + value_x = np.vstack(size * [value.x]) + value_z = np.vstack(size * [value.z]) else: # Blocks are incorrect size raise QiskitError( @@ -471,7 +469,7 @@ def insert(self, ind: int, value: PauliList, qubit: bool = False) -> PauliList: # Build new array by blocks z = np.hstack([self.z[:, :ind], value_z, self.z[:, ind:]]) x = np.hstack([self.x[:, :ind], value_x, self.x[:, ind:]]) - phase = self.phase + value_phase + phase = self.phase + value.phase return PauliList.from_symplectic(z, x, phase) @@ -1131,6 +1129,8 @@ def from_symplectic( Returns: PauliList: the constructed PauliList. """ + if isinstance(phase, np.ndarray) and np.ndim(phase) > 1: + raise ValueError(f"phase should be at most 1D but has {np.ndim(phase)} dimensions.") base_z, base_x, base_phase = cls._from_array(z, x, phase) return cls(BasePauli(base_z, base_x, base_phase)) diff --git a/releasenotes/notes/fix-paulilist-length1-phase-688d0e3a64ec9a9f.yaml b/releasenotes/notes/fix-paulilist-length1-phase-688d0e3a64ec9a9f.yaml new file mode 100644 index 000000000000..a1a59708003c --- /dev/null +++ b/releasenotes/notes/fix-paulilist-length1-phase-688d0e3a64ec9a9f.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed a bug that caused :meth:`.PauliList.insert` with ``qubit=True`` to produce a `phase` + attribute with the wrong shape when the original object was length 1. + Fixed `#13623 `__. diff --git a/test/python/quantum_info/operators/symplectic/test_pauli_list.py b/test/python/quantum_info/operators/symplectic/test_pauli_list.py index 8c96f63c4ddf..9abc473dc333 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli_list.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli_list.py @@ -1560,6 +1560,15 @@ def test_insert(self): value1 = pauli.insert(1, insert) self.assertEqual(value1, target1) + # Insert single column to length-1 PauliList: + with self.subTest(msg="length-1, single-column, single-val"): + pauli = PauliList(["X"]) + insert = PauliList(["Y"]) + target0 = PauliList(["YX"]) + value0 = pauli.insert(1, insert, qubit=True) + self.assertEqual(value0, target0) + self.assertEqual(value0.phase.shape, (1,)) + # Insert single column pauli = PauliList(["X", "Y", "Z", "-iI"]) for i in ["I", "X", "Y", "Z", "iY"]: