From a4f28f2635fa944f8f1a0b1dc5116c0cf20d3b9c Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:36:17 -0400 Subject: [PATCH 1/6] Add `get_parameter_by_uuid` to `CircuitData` (#12926) * Initial: Add `parameter_by_uuid` to `CircuitData` - This method receives a `uuid` instance from Python and returns the parameter object identified by said `uuid`. * Fix: Make `get_parameter_by_uuid` a rust only method. --- crates/circuit/src/circuit_data.rs | 5 +++++ crates/circuit/src/parameter_table.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index d87efd03ac5d..3249b609e154 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -1284,6 +1284,11 @@ impl CircuitData { } Ok(()) } + + /// Retrieves the python `Param` object based on its `ParameterUuid`. + pub fn get_parameter_by_uuid(&self, uuid: ParameterUuid) -> Option<&Py> { + self.param_table.py_parameter_by_uuid(uuid) + } } /// Helper struct for `assign_parameters` to allow use of `Param::extract_no_coerce` in diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs index 8825fbd71772..38cabf10c69f 100644 --- a/crates/circuit/src/parameter_table.rs +++ b/crates/circuit/src/parameter_table.rs @@ -225,6 +225,11 @@ impl ParameterTable { .map(|uuid| &self.by_uuid[uuid].object) } + /// Lookup the Python parameter object by uuid. + pub fn py_parameter_by_uuid(&self, uuid: ParameterUuid) -> Option<&Py> { + self.by_uuid.get(&uuid).map(|param| ¶m.object) + } + /// Get the (maybe cached) Python list of the sorted `Parameter` objects. pub fn py_parameters<'py>(&mut self, py: Python<'py>) -> Bound<'py, PyList> { if let Some(py_parameters) = self.py_parameters.as_ref() { From 6aa933cc0627d0d64cfcda53055db4f460f21b28 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 9 Aug 2024 18:21:29 -0400 Subject: [PATCH 2/6] Use ahash for IndexMap and IndexSet hasher (#12935) The ahash hasher offers better performace as a drop in replacement for the stdlib hash algorithm. This is a large reason we use the hashbrown crate for our HashMap type in Qiskit (along with rayon support). The indexmap crate doesn't use ahash by default however, so we typically switch to it anywhere we use IndexMap or IndexSet to match the lookup performance of hashbrown in places where we need to preserve insertion order. However, there were a couple of spots where we missed this in the rust code, this commit corrects these oversights. --- Cargo.lock | 1 + Cargo.toml | 1 + crates/accelerate/Cargo.toml | 2 +- crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs | 5 +++-- crates/qasm3/Cargo.toml | 1 + crates/qasm3/src/build.rs | 5 ++++- 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4858af1971e9..5b2ee803ee4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1222,6 +1222,7 @@ dependencies = [ name = "qiskit-qasm3" version = "1.3.0" dependencies = [ + "ahash 0.8.11", "hashbrown 0.14.5", "indexmap", "oq3_semantics", diff --git a/Cargo.toml b/Cargo.toml index a505a4e7c22b..5fffe8ff4dd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ ndarray = "^0.15.6" numpy = "0.21.0" smallvec = "1.13" thiserror = "1.0" +ahash = "0.8.11" # Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an # actual C extension (the feature disables linking in `libpython`, which is forbidden in Python diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 0b23cf08743e..854c1ed05706 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -15,7 +15,7 @@ numpy.workspace = true rand = "0.8" rand_pcg = "0.3" rand_distr = "0.4.3" -ahash = "0.8.11" +ahash.workspace = true num-traits = "0.2" num-complex.workspace = true num-bigint.workspace = true diff --git a/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs b/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs index e53e25282005..81ddfb1d6503 100644 --- a/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs +++ b/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs @@ -10,6 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. +use ahash::RandomState; use indexmap::IndexSet; use ndarray::{s, ArrayView2}; use smallvec::smallvec; @@ -102,7 +103,7 @@ pub struct GreedyCliffordSynthesis<'a> { symplectic_matrix: SymplecticMatrix, /// Unprocessed qubits. - unprocessed_qubits: IndexSet, + unprocessed_qubits: IndexSet, } impl GreedyCliffordSynthesis<'_> { @@ -121,7 +122,7 @@ impl GreedyCliffordSynthesis<'_> { smat: tableau.slice(s![.., 0..2 * num_qubits]).to_owned(), }; - let unprocessed_qubits: IndexSet = (0..num_qubits).collect(); + let unprocessed_qubits = (0..num_qubits).collect(); Ok(GreedyCliffordSynthesis { tableau, diff --git a/crates/qasm3/Cargo.toml b/crates/qasm3/Cargo.toml index 4dd0d977786e..8e42f3f0701b 100644 --- a/crates/qasm3/Cargo.toml +++ b/crates/qasm3/Cargo.toml @@ -14,3 +14,4 @@ pyo3.workspace = true indexmap.workspace = true hashbrown.workspace = true oq3_semantics = "0.6.0" +ahash.workspace = true diff --git a/crates/qasm3/src/build.rs b/crates/qasm3/src/build.rs index f5cf2fd4efca..912066df839e 100644 --- a/crates/qasm3/src/build.rs +++ b/crates/qasm3/src/build.rs @@ -13,6 +13,8 @@ use pyo3::prelude::*; use pyo3::types::{PySequence, PyString, PyTuple}; +use ahash::RandomState; + use hashbrown::HashMap; use indexmap::IndexMap; @@ -190,8 +192,9 @@ impl BuilderState { let qubits = if let Some(asg_qubits) = barrier.qubits().as_ref() { // We want any deterministic order for easier circuit reproducibility in Python space, // and to include each seen qubit once. This simply maintains insertion order. - let mut qubits = IndexMap::<*const ::pyo3::ffi::PyObject, Py>::with_capacity( + let mut qubits = IndexMap::<*const ::pyo3::ffi::PyObject, Py, RandomState>::with_capacity_and_hasher( asg_qubits.len(), + RandomState::default() ); for qarg in asg_qubits.iter() { let qarg = expr::expect_gate_operand(qarg)?; From c7e7016419183c788cc384202694ee14baea8baf Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Sun, 11 Aug 2024 14:09:27 +0300 Subject: [PATCH 3/6] Move mcx synthesis methods with ancillas to the synthesis library (#12904) * move mcx synthesis method with dirty ancillas to the synthesis library * minor lint updates * move mcx synthesis method with several clean ancillas to the synthesis library * move handling up to 3 controls to the synthesis code * move handling up to 3 controls to the synthesis code * handle cyclic imports * add mcx synthesis method with one clean ancilla to the synthesis library * update input to synth_mcx functions * refactor test for mcx method modes * update circuit names. add references * reduce num_controls in tests * revert circuit names to old ones * refactor functions names * add docstrings * update year * add synthesis functions to API docs * add release notes * fix docs * update docs and release notes following review * update imports following review --- qiskit/circuit/library/standard_gates/x.py | 138 ++--------- qiskit/synthesis/__init__.py | 12 + qiskit/synthesis/multi_controlled/__init__.py | 19 ++ .../mcx_with_ancillas_synth.py | 232 ++++++++++++++++++ ...th-mcx-with-ancillas-6a92078d6b0e1de4.yaml | 17 ++ test/python/circuit/test_controlled_gate.py | 125 +++------- 6 files changed, 330 insertions(+), 213 deletions(-) create mode 100644 qiskit/synthesis/multi_controlled/__init__.py create mode 100644 qiskit/synthesis/multi_controlled/mcx_with_ancillas_synth.py create mode 100644 releasenotes/notes/add-synth-mcx-with-ancillas-6a92078d6b0e1de4.yaml diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index f3f7b5ebdb72..3ee5551b599d 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -13,7 +13,7 @@ """X, CX, CCX and multi-controlled X gates.""" from __future__ import annotations from typing import Optional, Union, Type -from math import ceil, pi +from math import pi import numpy from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.singleton import SingletonGate, SingletonControlledGate, stdlib_singleton_key @@ -1371,47 +1371,12 @@ def inverse(self, annotated: bool = False): def _define(self): """Define the MCX gate using recursion.""" - # pylint: disable=cyclic-import - from qiskit.circuit.quantumcircuit import QuantumCircuit - q = QuantumRegister(self.num_qubits, name="q") - qc = QuantumCircuit(q, name=self.name) - if self.num_qubits == 4: - qc._append(C3XGate(), q[:], []) - self.definition = qc - elif self.num_qubits == 5: - qc._append(C4XGate(), q[:], []) - self.definition = qc - else: - num_ctrl_qubits = len(q) - 1 - q_ancilla = q[-1] - q_target = q[-2] - middle = ceil(num_ctrl_qubits / 2) - first_half = [*q[:middle]] - second_half = [*q[middle : num_ctrl_qubits - 1], q_ancilla] - - qc._append( - MCXVChain(num_ctrl_qubits=len(first_half), dirty_ancillas=True), - qargs=[*first_half, q_ancilla, *q[middle : middle + len(first_half) - 2]], - cargs=[], - ) - qc._append( - MCXVChain(num_ctrl_qubits=len(second_half), dirty_ancillas=True), - qargs=[*second_half, q_target, *q[: len(second_half) - 2]], - cargs=[], - ) - qc._append( - MCXVChain(num_ctrl_qubits=len(first_half), dirty_ancillas=True), - qargs=[*first_half, q_ancilla, *q[middle : middle + len(first_half) - 2]], - cargs=[], - ) - qc._append( - MCXVChain(num_ctrl_qubits=len(second_half), dirty_ancillas=True), - qargs=[*second_half, q_target, *q[: len(second_half) - 2]], - cargs=[], - ) + # pylint: disable=cyclic-import + from qiskit.synthesis.multi_controlled import synth_mcx_1_clean_b95 - self.definition = qc + qc = synth_mcx_1_clean_b95(self.num_ctrl_qubits) + self.definition = qc class MCXVChain(MCXGate): @@ -1513,92 +1478,21 @@ def get_num_ancilla_qubits(num_ctrl_qubits: int, mode: str = "v-chain"): def _define(self): """Define the MCX gate using a V-chain of CX gates.""" - # pylint: disable=cyclic-import - from qiskit.circuit.quantumcircuit import QuantumCircuit - - q = QuantumRegister(self.num_qubits, name="q") - qc = QuantumCircuit(q, name=self.name) - q_controls = q[: self.num_ctrl_qubits] - q_target = q[self.num_ctrl_qubits] - q_ancillas = q[self.num_ctrl_qubits + 1 :] if self._dirty_ancillas: - if self.num_ctrl_qubits < 3: - qc.mcx(q_controls, q_target) - elif not self._relative_phase and self.num_ctrl_qubits == 3: - qc._append(C3XGate(), [*q_controls, q_target], []) - else: - num_ancillas = self.num_ctrl_qubits - 2 - targets = [q_target] + q_ancillas[:num_ancillas][::-1] - - for j in range(2): - for i in range(self.num_ctrl_qubits): # action part - if i < self.num_ctrl_qubits - 2: - if targets[i] != q_target or self._relative_phase: - # gate cancelling - - # cancel rightmost gates of action part - # with leftmost gates of reset part - if self._relative_phase and targets[i] == q_target and j == 1: - qc.cx(q_ancillas[num_ancillas - i - 1], targets[i]) - qc.t(targets[i]) - qc.cx(q_controls[self.num_ctrl_qubits - i - 1], targets[i]) - qc.tdg(targets[i]) - qc.h(targets[i]) - else: - qc.h(targets[i]) - qc.t(targets[i]) - qc.cx(q_controls[self.num_ctrl_qubits - i - 1], targets[i]) - qc.tdg(targets[i]) - qc.cx(q_ancillas[num_ancillas - i - 1], targets[i]) - else: - controls = [ - q_controls[self.num_ctrl_qubits - i - 1], - q_ancillas[num_ancillas - i - 1], - ] - - qc.ccx(controls[0], controls[1], targets[i]) - else: - # implements an optimized toffoli operation - # up to a diagonal gate, akin to lemma 6 of arXiv:1501.06911 - qc.h(targets[i]) - qc.t(targets[i]) - qc.cx(q_controls[self.num_ctrl_qubits - i - 2], targets[i]) - qc.tdg(targets[i]) - qc.cx(q_controls[self.num_ctrl_qubits - i - 1], targets[i]) - qc.t(targets[i]) - qc.cx(q_controls[self.num_ctrl_qubits - i - 2], targets[i]) - qc.tdg(targets[i]) - qc.h(targets[i]) - - break - - for i in range(num_ancillas - 1): # reset part - qc.cx(q_ancillas[i], q_ancillas[i + 1]) - qc.t(q_ancillas[i + 1]) - qc.cx(q_controls[2 + i], q_ancillas[i + 1]) - qc.tdg(q_ancillas[i + 1]) - qc.h(q_ancillas[i + 1]) - - if self._action_only: - qc.ccx(q_controls[-1], q_ancillas[-1], q_target) - - break - else: - qc.rccx(q_controls[0], q_controls[1], q_ancillas[0]) - i = 0 - for j in range(2, self.num_ctrl_qubits - 1): - qc.rccx(q_controls[j], q_ancillas[i], q_ancillas[i + 1]) + # pylint: disable=cyclic-import + from qiskit.synthesis.multi_controlled import synth_mcx_n_dirty_i15 - i += 1 - - qc.ccx(q_controls[-1], q_ancillas[i], q_target) - - for j in reversed(range(2, self.num_ctrl_qubits - 1)): - qc.rccx(q_controls[j], q_ancillas[i - 1], q_ancillas[i]) + qc = synth_mcx_n_dirty_i15( + self.num_ctrl_qubits, + self._relative_phase, + self._action_only, + ) - i -= 1 + else: # use clean ancillas + # pylint: disable=cyclic-import + from qiskit.synthesis.multi_controlled import synth_mcx_n_clean_m15 - qc.rccx(q_controls[0], q_controls[1], q_ancillas[i]) + qc = synth_mcx_n_clean_m15(self.num_ctrl_qubits) self.definition = qc diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index cfe5f0b304cb..f1d1e3b28359 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -122,6 +122,13 @@ .. autofunction:: two_qubit_cnot_decompose +Multi Controlled Synthesis +========================== + +.. autofunction:: synth_mcx_n_dirty_i15 +.. autofunction:: synth_mcx_n_clean_m15 +.. autofunction:: synth_mcx_1_clean_b95 + """ from .evolution import ( @@ -173,3 +180,8 @@ two_qubit_cnot_decompose, TwoQubitWeylDecomposition, ) +from .multi_controlled.mcx_with_ancillas_synth import ( + synth_mcx_n_dirty_i15, + synth_mcx_n_clean_m15, + synth_mcx_1_clean_b95, +) diff --git a/qiskit/synthesis/multi_controlled/__init__.py b/qiskit/synthesis/multi_controlled/__init__.py new file mode 100644 index 000000000000..0c04823a537a --- /dev/null +++ b/qiskit/synthesis/multi_controlled/__init__.py @@ -0,0 +1,19 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Module containing multi-controlled circuits synthesis""" + +from .mcx_with_ancillas_synth import ( + synth_mcx_n_dirty_i15, + synth_mcx_n_clean_m15, + synth_mcx_1_clean_b95, +) diff --git a/qiskit/synthesis/multi_controlled/mcx_with_ancillas_synth.py b/qiskit/synthesis/multi_controlled/mcx_with_ancillas_synth.py new file mode 100644 index 000000000000..cf2325764569 --- /dev/null +++ b/qiskit/synthesis/multi_controlled/mcx_with_ancillas_synth.py @@ -0,0 +1,232 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Module containing multi-controlled circuits synthesis with ancillary qubits.""" + +from math import ceil +from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.library.standard_gates.x import C3XGate, C4XGate + + +def synth_mcx_n_dirty_i15( + num_ctrl_qubits: int, + relative_phase: bool = False, + action_only: bool = False, +): + """ + Synthesize a multi-controlled X gate with :math:`k` controls using :math:`k - 2` + dirty ancillary qubits producing a circuit with :math:`2 * k - 1` qubits and at most + :math:`8 * k - 6` CX gates, by Iten et. al. [1]. + + Args: + num_ctrl_qubits: The number of control qubits. + + relative_phase: when set to ``True``, the method applies the optimized multi-controlled X gate + up to a relative phase, in a way that, by lemma 8 of [1], the relative + phases of the ``action part`` cancel out with the phases of the ``reset part``. + + action_only: when set to ``True``, the method applies only the ``action part`` of lemma 8 of [1]. + + Returns: + The synthesized quantum circuit. + + References: + 1. Iten et. al., *Quantum Circuits for Isometries*, Phys. Rev. A 93, 032318 (2016), + `arXiv:1501.06911 `_ + """ + + 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] + q_target = q[num_ctrl_qubits] + q_ancillas = q[num_ctrl_qubits + 1 :] + + if num_ctrl_qubits == 1: + qc.cx(q_controls, q_target) + return qc + elif num_ctrl_qubits == 2: + qc.ccx(q_controls[0], q_controls[1], q_target) + return qc + elif not relative_phase and num_ctrl_qubits == 3: + qc._append(C3XGate(), [*q_controls, q_target], []) + return qc + + num_ancillas = num_ctrl_qubits - 2 + targets = [q_target] + q_ancillas[:num_ancillas][::-1] + + for j in range(2): + for i in range(num_ctrl_qubits): # action part + if i < num_ctrl_qubits - 2: + if targets[i] != q_target or relative_phase: + # gate cancelling + + # cancel rightmost gates of action part + # with leftmost gates of reset part + if relative_phase and targets[i] == q_target and j == 1: + qc.cx(q_ancillas[num_ancillas - i - 1], targets[i]) + qc.t(targets[i]) + qc.cx(q_controls[num_ctrl_qubits - i - 1], targets[i]) + qc.tdg(targets[i]) + qc.h(targets[i]) + else: + qc.h(targets[i]) + qc.t(targets[i]) + qc.cx(q_controls[num_ctrl_qubits - i - 1], targets[i]) + qc.tdg(targets[i]) + qc.cx(q_ancillas[num_ancillas - i - 1], targets[i]) + else: + controls = [ + q_controls[num_ctrl_qubits - i - 1], + q_ancillas[num_ancillas - i - 1], + ] + + qc.ccx(controls[0], controls[1], targets[i]) + else: + # implements an optimized toffoli operation + # up to a diagonal gate, akin to lemma 6 of arXiv:1501.06911 + qc.h(targets[i]) + qc.t(targets[i]) + qc.cx(q_controls[num_ctrl_qubits - i - 2], targets[i]) + qc.tdg(targets[i]) + qc.cx(q_controls[num_ctrl_qubits - i - 1], targets[i]) + qc.t(targets[i]) + qc.cx(q_controls[num_ctrl_qubits - i - 2], targets[i]) + qc.tdg(targets[i]) + qc.h(targets[i]) + + break + + for i in range(num_ancillas - 1): # reset part + qc.cx(q_ancillas[i], q_ancillas[i + 1]) + qc.t(q_ancillas[i + 1]) + qc.cx(q_controls[2 + i], q_ancillas[i + 1]) + qc.tdg(q_ancillas[i + 1]) + qc.h(q_ancillas[i + 1]) + + if action_only: + qc.ccx(q_controls[-1], q_ancillas[-1], q_target) + + break + + return qc + + +def synth_mcx_n_clean_m15(num_ctrl_qubits: int): + """ + Synthesize a multi-controlled X gate with :math:`k` controls using :math:`k - 2` + clean ancillary qubits with producing a circuit with :math:`2 * k - 1` qubits + and at most :math:`6 * k - 6` CX gates, by Maslov [1]. + + Args: + num_ctrl_qubits: The number of control qubits. + + Returns: + The synthesized quantum circuit. + + References: + 1. Maslov., Phys. Rev. A 93, 022311 (2016), + `arXiv:1508.03273 `_ + """ + + 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] + q_target = q[num_ctrl_qubits] + q_ancillas = q[num_ctrl_qubits + 1 :] + + qc.rccx(q_controls[0], q_controls[1], q_ancillas[0]) + i = 0 + for j in range(2, num_ctrl_qubits - 1): + qc.rccx(q_controls[j], q_ancillas[i], q_ancillas[i + 1]) + + i += 1 + + qc.ccx(q_controls[-1], q_ancillas[i], q_target) + + for j in reversed(range(2, num_ctrl_qubits - 1)): + qc.rccx(q_controls[j], q_ancillas[i - 1], q_ancillas[i]) + + i -= 1 + + qc.rccx(q_controls[0], q_controls[1], q_ancillas[i]) + + return qc + + +def synth_mcx_1_clean_b95(num_ctrl_qubits: int): + """ + Synthesize a multi-controlled X gate with :math:`k` controls using a single + clean ancillary qubit producing a circuit with :math:`k + 2` qubits and at most + :math:`16 * k - 8` CX gates, by Barenco et al. [1]. + + Args: + num_ctrl_qubits: The number of control qubits. + + Returns: + The synthesized quantum circuit. + + References: + 1. Barenco et. al., Phys.Rev. A52 3457 (1995), + `arXiv:quant-ph/9503016 `_ + """ + + if num_ctrl_qubits == 3: + q = QuantumRegister(4, name="q") + qc = QuantumCircuit(q, name="mcx") + qc._append(C3XGate(), q[:], []) + return qc + + elif num_ctrl_qubits == 4: + q = QuantumRegister(5, name="q") + qc = QuantumCircuit(q, name="mcx") + qc._append(C4XGate(), q[:], []) + return qc + + num_qubits = num_ctrl_qubits + 2 + q = QuantumRegister(num_qubits, name="q") + qc = QuantumCircuit(q, name="mcx_recursive") + + num_ctrl_qubits = len(q) - 1 + q_ancilla = q[-1] + q_target = q[-2] + middle = ceil(num_ctrl_qubits / 2) + first_half = [*q[:middle]] + second_half = [*q[middle : num_ctrl_qubits - 1], q_ancilla] + + qc_first_half = synth_mcx_n_dirty_i15(num_ctrl_qubits=len(first_half)) + qc_second_half = synth_mcx_n_dirty_i15(num_ctrl_qubits=len(second_half)) + + qc.append( + qc_first_half, + qargs=[*first_half, q_ancilla, *q[middle : middle + len(first_half) - 2]], + cargs=[], + ) + qc.append( + qc_second_half, + qargs=[*second_half, q_target, *q[: len(second_half) - 2]], + cargs=[], + ) + qc.append( + qc_first_half, + qargs=[*first_half, q_ancilla, *q[middle : middle + len(first_half) - 2]], + cargs=[], + ) + qc.append( + qc_second_half, + qargs=[*second_half, q_target, *q[: len(second_half) - 2]], + cargs=[], + ) + + return qc diff --git a/releasenotes/notes/add-synth-mcx-with-ancillas-6a92078d6b0e1de4.yaml b/releasenotes/notes/add-synth-mcx-with-ancillas-6a92078d6b0e1de4.yaml new file mode 100644 index 000000000000..47719c60b89c --- /dev/null +++ b/releasenotes/notes/add-synth-mcx-with-ancillas-6a92078d6b0e1de4.yaml @@ -0,0 +1,17 @@ +--- +features_synthesis: + - | + Add a synthesis function :func:`.synth_mcx_n_dirty_i15` that + synthesizes a multi-controlled X gate with :math:`k` controls using :math:`k - 2` + dirty ancillary qubits producing a circuit with at most :math:`8 * k - 6` CX gates, + by Iten et. al. (arXiv:1501.06911). + - | + Add a synthesis function :func:`.synth_mcx_n_clean_m15` that + synthesizes a multi-controlled X gate with :math:`k` controls using :math:`k - 2` + clean ancillary qubits producing a circuit with at most :math:`6 * k - 6` CX gates, + by Maslov (arXiv:1508.03273). + - | + Add a synthesis function :func:`.synth_mcx_1_clean_b95` that + synthesizes a multi-controlled X gate with :math:`k` controls using a single + clean ancillary qubit producing a circuit with at most :math:`16 * k - 8` CX gates, + by Barenco et al. (arXiv:quant-ph/9503016). diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 707f9d32cb94..a517d5d1e4a4 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -507,89 +507,37 @@ def test_multi_controlled_u1_matrix(self, num_controls): with self.subTest(msg=f"control state = {ctrl_state}"): self.assertTrue(matrix_equal(simulated, expected)) - @data(1, 2, 3, 4) - def test_multi_control_toffoli_matrix_clean_ancillas(self, num_controls): - """Test the multi-control Toffoli gate with clean ancillas. - - Based on the test moved here from Aqua: - https://github.com/Qiskit/qiskit-aqua/blob/769ca8f/test/aqua/test_mct.py - """ - # set up circuit - q_controls = QuantumRegister(num_controls) - q_target = QuantumRegister(1) - qc = QuantumCircuit(q_controls, q_target) - - if num_controls > 2: - num_ancillas = num_controls - 2 - q_ancillas = QuantumRegister(num_controls) - qc.add_register(q_ancillas) - else: - num_ancillas = 0 - q_ancillas = None - - # apply hadamard on control qubits and toffoli gate - qc.mcx(q_controls, q_target[0], q_ancillas, mode="basic") - - # obtain unitary for circuit - simulated = Operator(qc).data - - # compare to expectation - if num_ancillas > 0: - simulated = simulated[: 2 ** (num_controls + 1), : 2 ** (num_controls + 1)] - - base = XGate().to_matrix() - expected = _compute_control_matrix(base, num_controls) - self.assertTrue(matrix_equal(simulated, expected)) - - @data(1, 2, 3, 4, 5) - def test_multi_control_toffoli_matrix_basic_dirty_ancillas(self, num_controls): - """Test the multi-control Toffoli gate with dirty ancillas (basic-dirty-ancilla). - - Based on the test moved here from Aqua: - https://github.com/Qiskit/qiskit-aqua/blob/769ca8f/test/aqua/test_mct.py - """ + @combine( + num_controls=[2, 3, 4, 5, 6], + mode=[ + "noancilla", + "recursion", + "v-chain", + "v-chain-dirty", + "advanced", + "basic", + "basic-dirty-ancilla", + ], + ) + def test_multi_control_toffoli_matrix_advanced_num_ancillas(self, num_controls, mode): + """Test the multi-control Toffoli gate methods with and w/o ancillas.""" q_controls = QuantumRegister(num_controls) q_target = QuantumRegister(1) qc = QuantumCircuit(q_controls, q_target) q_ancillas = None - if num_controls <= 2: + if mode == "noancilla": num_ancillas = 0 - else: - num_ancillas = num_controls - 2 + if mode in ["recursion", "advanced"]: + num_ancillas = int(num_controls > 4) q_ancillas = QuantumRegister(num_ancillas) qc.add_register(q_ancillas) - - qc.mcx(q_controls, q_target[0], q_ancillas, mode="basic-dirty-ancilla") - - simulated = Operator(qc).data - if num_ancillas > 0: - simulated = simulated[: 2 ** (num_controls + 1), : 2 ** (num_controls + 1)] - - base = XGate().to_matrix() - expected = _compute_control_matrix(base, num_controls) - self.assertTrue(matrix_equal(simulated, expected, atol=1e-8)) - - @data(1, 2, 3, 4, 5) - def test_multi_control_toffoli_matrix_advanced_dirty_ancillas(self, num_controls): - """Test the multi-control Toffoli gate with dirty ancillas (advanced). - - Based on the test moved here from Aqua: - https://github.com/Qiskit/qiskit-aqua/blob/769ca8f/test/aqua/test_mct.py - """ - q_controls = QuantumRegister(num_controls) - q_target = QuantumRegister(1) - qc = QuantumCircuit(q_controls, q_target) - - q_ancillas = None - if num_controls <= 4: - num_ancillas = 0 - else: - num_ancillas = 1 + if mode[:7] == "v-chain" or mode[:5] == "basic": + num_ancillas = max(0, num_controls - 2) q_ancillas = QuantumRegister(num_ancillas) qc.add_register(q_ancillas) - qc.mcx(q_controls, q_target[0], q_ancillas, mode="advanced") + qc.mcx(q_controls, q_target[0], q_ancillas, mode=mode) simulated = Operator(qc).data if num_ancillas > 0: @@ -599,25 +547,6 @@ def test_multi_control_toffoli_matrix_advanced_dirty_ancillas(self, num_controls expected = _compute_control_matrix(base, num_controls) self.assertTrue(matrix_equal(simulated, expected, atol=1e-8)) - @data(1, 2, 3) - def test_multi_control_toffoli_matrix_noancilla_dirty_ancillas(self, num_controls): - """Test the multi-control Toffoli gate with dirty ancillas (noancilla). - - Based on the test moved here from Aqua: - https://github.com/Qiskit/qiskit-aqua/blob/769ca8f/test/aqua/test_mct.py - """ - q_controls = QuantumRegister(num_controls) - q_target = QuantumRegister(1) - qc = QuantumCircuit(q_controls, q_target) - - qc.mcx(q_controls, q_target[0], None, mode="noancilla") - - simulated = Operator(qc) - - base = XGate().to_matrix() - expected = _compute_control_matrix(base, num_controls) - self.assertTrue(matrix_equal(simulated, expected, atol=1e-8)) - def test_mcsu2_real_diagonal(self): """Test mcsu2_real_diagonal""" num_ctrls = 6 @@ -823,6 +752,20 @@ def test_mcxvchain_dirty_ancilla_cx_count(self, num_ctrl_qubits): self.assertLessEqual(cx_count, 8 * num_ctrl_qubits - 6) + @data(5, 10, 15) + def test_mcxvchain_clean_ancilla_cx_count(self, num_ctrl_qubits): + """Test if cx count of the v-chain mcx with clean ancilla + is less than upper bound.""" + mcx_vchain = MCXVChain(num_ctrl_qubits, dirty_ancillas=False) + qc = QuantumCircuit(mcx_vchain.num_qubits) + + qc.append(mcx_vchain, list(range(mcx_vchain.num_qubits))) + + tr_mcx_vchain = transpile(qc, basis_gates=["u", "cx"]) + cx_count = tr_mcx_vchain.count_ops()["cx"] + + self.assertLessEqual(cx_count, 6 * num_ctrl_qubits - 6) + @data(7, 10, 15) def test_mcxrecursive_clean_ancilla_cx_count(self, num_ctrl_qubits): """Test if cx count of the mcx with one clean ancilla From b1e7ffeff000d4034d70e691da92062bb5b0730d Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 12 Aug 2024 15:42:30 +0200 Subject: [PATCH 4/6] Allow `CircuitData` construction from `PackedOperation`s (#12943) * ``CircuitData::from_packed_operations`` * missing import * remove redundant `to_vec` --- crates/circuit/src/circuit_data.rs | 67 +++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index 3249b609e154..68267121b5a8 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -18,7 +18,7 @@ use crate::circuit_instruction::{CircuitInstruction, OperationFromPython}; use crate::imports::{ANNOTATED_OPERATION, CLBIT, QUANTUM_CIRCUIT, QUBIT}; use crate::interner::{IndexedInterner, Interner, InternerKey}; use crate::operations::{Operation, OperationRef, Param, StandardGate}; -use crate::packed_instruction::PackedInstruction; +use crate::packed_instruction::{PackedInstruction, PackedOperation}; use crate::parameter_table::{ParameterTable, ParameterTableError, ParameterUse, ParameterUuid}; use crate::slice::{PySequenceIndex, SequenceIndex}; use crate::{Clbit, Qubit}; @@ -104,6 +104,71 @@ pub struct CircuitData { } impl CircuitData { + /// An alternate constructor to build a new `CircuitData` from an iterator + /// of packed operations. This can be used to build a circuit from a sequence + /// of `PackedOperation` without needing to involve Python. + /// + /// This can be connected with the Python space + /// QuantumCircuit.from_circuit_data() constructor to build a full + /// QuantumCircuit from Rust. + /// + /// # Arguments + /// + /// * py: A GIL handle this is needed to instantiate Qubits in Python space + /// * num_qubits: The number of qubits in the circuit. These will be created + /// in Python as loose bits without a register. + /// * num_clbits: The number of classical bits in the circuit. These will be created + /// in Python as loose bits without a register. + /// * instructions: An iterator of the (packed operation, params, qubits, clbits) to + /// add to the circuit + /// * global_phase: The global phase to use for the circuit + pub fn from_packed_operations( + py: Python, + num_qubits: u32, + num_clbits: u32, + instructions: I, + global_phase: Param, + ) -> PyResult + where + I: IntoIterator< + Item = ( + PackedOperation, + SmallVec<[Param; 3]>, + Vec, + Vec, + ), + >, + { + let instruction_iter = instructions.into_iter(); + let mut res = Self::with_capacity( + py, + num_qubits, + num_clbits, + instruction_iter.size_hint().0, + global_phase, + )?; + for (operation, params, qargs, cargs) in instruction_iter { + let qubits = (&mut res.qargs_interner) + .intern(InternerKey::Value(qargs))? + .index; + let clbits = (&mut res.cargs_interner) + .intern(InternerKey::Value(cargs))? + .index; + let params = (!params.is_empty()).then(|| Box::new(params)); + res.data.push(PackedInstruction { + op: operation, + qubits, + clbits, + params, + extra_attrs: None, + #[cfg(feature = "cache_pygates")] + py_op: RefCell::new(None), + }); + res.track_instruction_parameters(py, res.data.len() - 1)?; + } + Ok(res) + } + /// An alternate constructor to build a new `CircuitData` from an iterator /// of standard gates. This can be used to build a circuit from a sequence /// of standard gates, such as for a `StandardGate` definition or circuit From fb9c0dbf89732d1c1e155f132305002bf82c89b5 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 12 Aug 2024 15:11:12 +0100 Subject: [PATCH 5/6] Improve error message on bad OpenQASM 3 `basis_gates` argument (#12945) If the user requests a basis-gate name that cannot be used (like a keyword), there is nothing sensible we can output for a circuit that contains one of those operations. The exporter was already correctly erroring in these cases, but the error message was quite opaque. --- qiskit/qasm3/exporter.py | 8 +++++++- .../notes/qasm3-basis-gates-keyword-c5998bff1e178715.yaml | 5 +++++ test/python/qasm3/test_export.py | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/qasm3-basis-gates-keyword-c5998bff1e178715.yaml diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 098ab8578d48..561e59a72242 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -626,7 +626,13 @@ def build_program(self): if builtin in _BUILTIN_GATES: # It's built into the langauge; we don't need to re-add it. continue - self.symbols.register_gate_without_definition(builtin, None) + try: + self.symbols.register_gate_without_definition(builtin, None) + except QASM3ExporterError as exc: + raise QASM3ExporterError( + f"Cannot use '{builtin}' as a basis gate for the reason in the prior exception." + " Consider renaming the gate if needed, or omitting this basis gate if not." + ) from exc header = ast.Header(ast.Version("3.0"), list(self.build_includes())) diff --git a/releasenotes/notes/qasm3-basis-gates-keyword-c5998bff1e178715.yaml b/releasenotes/notes/qasm3-basis-gates-keyword-c5998bff1e178715.yaml new file mode 100644 index 000000000000..82347fd920fc --- /dev/null +++ b/releasenotes/notes/qasm3-basis-gates-keyword-c5998bff1e178715.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + The OpenQASM 3 exporter will now correctly error when asked to use a keyword or other invalid + identifier as a "basis gate", as it has no way of putting out correct output in these cases. diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 703b5f8cac12..868846925753 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -2699,3 +2699,11 @@ def test_disallow_export_of_inner_scope(self): QASM3ExporterError, "cannot export an inner scope.*as a top-level program" ): dumps(qc) + + def test_no_basis_gate_with_keyword(self): + """Test that keyword cannot be used as a basis gate.""" + qc = QuantumCircuit() + with self.assertRaisesRegex(QASM3ExporterError, "Cannot use 'reset' as a basis gate") as cm: + dumps(qc, basis_gates=["U", "reset"]) + self.assertIsInstance(cm.exception.__cause__, QASM3ExporterError) + self.assertRegex(cm.exception.__cause__.message, "cannot use the keyword 'reset'") From 61adcf9d5fb2cf59afb76f5184c7c62665ed4c8d Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Mon, 12 Aug 2024 13:32:01 -0400 Subject: [PATCH 6/6] Fix version parsing issue in bloch.py. (#12928) --- qiskit/visualization/bloch.py | 45 ++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/qiskit/visualization/bloch.py b/qiskit/visualization/bloch.py index 2855c6ba9651..ae7083a9fd61 100644 --- a/qiskit/visualization/bloch.py +++ b/qiskit/visualization/bloch.py @@ -50,6 +50,7 @@ import math import os +import re import numpy as np import matplotlib import matplotlib.pyplot as plt @@ -60,6 +61,47 @@ from .utils import matplotlib_close_if_inline +# This version pattern is taken from the pypa packaging project: +# https://github.com/pypa/packaging/blob/21.3/packaging/version.py#L223-L254 +# which is dual licensed Apache 2.0 and BSD see the source for the original +# authors and other details +VERSION_PATTERN = ( + "^" + + r""" + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+            [-_\.]?
+            (?P(a|b|c|rc|alpha|beta|pre|preview))
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+        (?P                                         # post release
+            (?:-(?P[0-9]+))
+            |
+            (?:
+                [-_\.]?
+                (?Ppost|rev|r)
+                [-_\.]?
+                (?P[0-9]+)?
+            )
+        )?
+        (?P                                          # dev release
+            [-_\.]?
+            (?Pdev)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+    )
+    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+"""
+    + "$"
+)
+VERSION_PATTERN_REGEX = re.compile(VERSION_PATTERN, re.VERBOSE | re.IGNORECASE)
+
+
 class Arrow3D(Patch3D, FancyArrowPatch):
     """Makes a fancy arrow"""
 
@@ -419,7 +461,8 @@ def render(self, title=""):
             self.fig = plt.figure(figsize=self.figsize)
 
         if not self._ext_axes:
-            if tuple(int(x) for x in matplotlib.__version__.split(".")) >= (3, 4, 0):
+            version_match = VERSION_PATTERN_REGEX.search(matplotlib.__version__)
+            if tuple(int(x) for x in version_match.group("release").split(".")) >= (3, 4, 0):
                 self.axes = Axes3D(
                     self.fig, azim=self.view[0], elev=self.view[1], auto_add_to_figure=False
                 )