diff --git a/qiskit/circuit/library/generalized_gates/linear_function.py b/qiskit/circuit/library/generalized_gates/linear_function.py index f1a3c25c1ee0..e8c94084e133 100644 --- a/qiskit/circuit/library/generalized_gates/linear_function.py +++ b/qiskit/circuit/library/generalized_gates/linear_function.py @@ -171,6 +171,18 @@ def permutation_pattern(self): locs = np.where(linear == 1) return locs[1] + def permute(self, perm): + """Returns a linear function obtained by permuting rows and columns of a given linear function + using the permutation pattern ``perm``. + """ + mat = self.linear + nq = mat.shape[0] + permuted_mat = np.zeros(mat.shape, dtype=bool) + for i in range(nq): + for j in range(nq): + permuted_mat[i, j] = mat[perm[i], perm[j]] + return LinearFunction(permuted_mat) + def _linear_quantum_circuit_to_mat(qc: QuantumCircuit): """This creates a n x n matrix corresponding to the given linear quantum circuit.""" diff --git a/qiskit/synthesis/linear/linear_circuits_utils.py b/qiskit/synthesis/linear/linear_circuits_utils.py index d1eda40e1f05..a4d232c0237f 100644 --- a/qiskit/synthesis/linear/linear_circuits_utils.py +++ b/qiskit/synthesis/linear/linear_circuits_utils.py @@ -13,12 +13,15 @@ """Utility functions for handling linear reversible circuits.""" import copy -from typing import Callable +from typing import Callable, List import numpy as np -from qiskit import QuantumCircuit +from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.exceptions import QiskitError from qiskit.circuit.exceptions import CircuitError -from . import calc_inverse_matrix, check_invertible_binary_matrix +from qiskit.synthesis.linear.linear_matrix_utils import ( + calc_inverse_matrix, + check_invertible_binary_matrix, +) def transpose_cx_circ(qc: QuantumCircuit): @@ -42,7 +45,7 @@ def transpose_cx_circ(qc: QuantumCircuit): return transposed_circ -def optimize_cx_4_options(function: Callable, mat: np.ndarray, optimize_count: bool = True): +def _optimize_cx_4_options(function: Callable, mat: np.ndarray, optimize_count: bool = True): """Get the best implementation of a circuit implementing a binary invertible matrix M, by considering all four options: M,M^(-1),M^T,M^(-1)^T. Optimizing either the CX count or the depth. @@ -61,10 +64,25 @@ def optimize_cx_4_options(function: Callable, mat: np.ndarray, optimize_count: b if not check_invertible_binary_matrix(mat): raise QiskitError("The matrix is not invertible.") + circuits = _cx_circuits_4_options(function, mat) + best_qc = _choose_best_linear_circuit(circuits, optimize_count) + return best_qc + + +def _cx_circuits_4_options(function: Callable, mat: np.ndarray) -> List[QuantumCircuit]: + """Construct different circuits implementing a binary invertible matrix M, + by considering all four options: M,M^(-1),M^T,M^(-1)^T. + + Args: + function: the synthesis function. + mat: a binary invertible matrix. + + Returns: + List[QuantumCircuit]: constructed circuits. + """ + circuits = [] qc = function(mat) - best_qc = qc - best_depth = qc.depth() - best_count = qc.count_ops()["cx"] + circuits.append(qc) for i in range(1, 4): mat_cpy = copy.deepcopy(mat) @@ -73,35 +91,104 @@ def optimize_cx_4_options(function: Callable, mat: np.ndarray, optimize_count: b mat_cpy = calc_inverse_matrix(mat_cpy) qc = function(mat_cpy) qc = qc.inverse() + circuits.append(qc) elif i == 2: mat_cpy = np.transpose(mat_cpy) qc = function(mat_cpy) qc = transpose_cx_circ(qc) + circuits.append(qc) elif i == 3: mat_cpy = calc_inverse_matrix(np.transpose(mat_cpy)) qc = function(mat_cpy) qc = transpose_cx_circ(qc) qc = qc.inverse() + circuits.append(qc) + + return circuits - new_depth = qc.depth() - new_count = qc.count_ops()["cx"] - # Prioritize count, and if it has the same count, then also consider depth - better_count = (optimize_count and best_count > new_count) or ( - not optimize_count and best_depth == new_depth and best_count > new_count - ) - # Prioritize depth, and if it has the same depth, then also consider count - better_depth = (not optimize_count and best_depth > new_depth) or ( - optimize_count and best_count == new_count and best_depth > new_depth - ) - - if better_count or better_depth: - best_count = new_count - best_depth = new_depth - best_qc = qc +def _choose_best_linear_circuit( + circuits: List[QuantumCircuit], optimize_count: bool = True +) -> QuantumCircuit: + """Returns the best quantum circuit either in terms of gate count or depth. + + Args: + circuits: a list of quantum circuits + optimize_count: True if the number of CX gates is optimized, False if the depth is optimized. + + Returns: + QuantumCircuit: the best quantum circuit out of the given circuits. + """ + best_qc = circuits[0] + for circuit in circuits[1:]: + if _compare_linear_circuits(best_qc, circuit, optimize_count=optimize_count): + best_qc = circuit return best_qc +def _linear_circuit_depth(qc: QuantumCircuit): + """Computes the depth of a linear circuit (that is, a circuit that only contains CX and SWAP + gates). This is similar to quantum circuit's depth method except that SWAPs are counted as + depth-3 gates. + """ + qubit_depths = [0] * qc.num_qubits + bit_indices = {bit: idx for idx, bit in enumerate(qc.qubits)} + for instruction in qc.data: + if instruction.operation.name not in ["cx", "swap"]: + raise CircuitError("The circuit contains non-linear gates.") + new_depth = max(qubit_depths[bit_indices[q]] for q in instruction.qubits) + new_depth += 3 if instruction.operation.name == "swap" else 1 + for q in instruction.qubits: + qubit_depths[bit_indices[q]] = new_depth + return max(qubit_depths) + + +def _compare_linear_circuits( + qc1: QuantumCircuit, qc2: QuantumCircuit, optimize_count: bool = True +) -> bool: + """Compares two quantum circuits either in terms of gate count or depth. + + Args: + qc1: the first quantum circuit + qc2: the second quantum circuit + optimize_count: True if the number of CX gates is optimized, False if the depth is optimized. + + Returns: + bool: ``False`` means that the first quantum circuit is "better", ``True`` means the second. + """ + count1 = qc1.size() + depth1 = _linear_circuit_depth(qc1) + count2 = qc2.size() + depth2 = _linear_circuit_depth(qc2) + + # Prioritize count, and if it has the same count, then also consider depth + count2_is_better = (optimize_count and count1 > count2) or ( + not optimize_count and depth1 == depth2 and count1 > count2 + ) + # Prioritize depth, and if it has the same depth, then also consider count + depth2_is_better = (not optimize_count and depth1 > depth2) or ( + optimize_count and count1 == count2 and depth1 > depth2 + ) + + return count2_is_better or depth2_is_better + + +def _linear_circuit_check_map(qc: QuantumCircuit, coupling_list: list) -> bool: + """Returns whether a linear quantum circuit (consisting of CX and SWAP gates) + only has connections from the coupling_list. + """ + bit_indices = {bit: idx for idx, bit in enumerate(qc.qubits)} + coupling_list_set = set(coupling_list) + for circuit_instruction in qc.data: + if len(circuit_instruction.qubits) != 2: + return False + q0 = bit_indices[circuit_instruction.qubits[0]] + q1 = bit_indices[circuit_instruction.qubits[1]] + if (q0, q1) not in coupling_list_set: + return False + return True + + def check_lnn_connectivity(qc: QuantumCircuit) -> bool: """Check that the synthesized circuit qc fits linear nearest neighbor connectivity. diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index c3b56c3bc7cc..3b72a92bed1c 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -13,11 +13,19 @@ """Synthesize higher-level objects.""" +from typing import Union, List from qiskit.converters import circuit_to_dag from qiskit.transpiler.basepasses import TransformationPass from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.coupling import CouplingMap +from qiskit.synthesis.linear.linear_circuits_utils import ( + _optimize_cx_4_options, + _compare_linear_circuits, + _linear_circuit_check_map, +) +from qiskit.transpiler.target import Target from qiskit.synthesis.clifford import ( synth_clifford_full, @@ -119,9 +127,24 @@ class HighLevelSynthesis(TransformationPass): ``default`` methods for all other high-level objects, including ``op_a``-objects. """ - def __init__(self, hls_config=None): + def __init__(self, hls_config=None, coupling_map=None, target=None): + """ + HighLevelSynthesis initializer. + + Args: + hls_config (HLSConfig): the high-level-synthesis config file + specifying synthesis methods and parameters. + coupling_map (CouplingMap): the coupling map of the backend + in case synthesis is done on a physical circuit. + target (Target): A target representing the target backend. + """ super().__init__() + self._coupling_map = coupling_map + self._target = target + if target is not None: + self._coupling_map = self._target.build_coupling_map() + if hls_config is not None: self.hls_config = hls_config else: @@ -142,6 +165,8 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: TranspilerError: when the specified synthesis method is not available. """ + dag_bit_indices = {bit: i for i, bit in enumerate(dag.qubits)} + for node in dag.op_nodes(): if node.name in self.hls_config.methods.keys(): # the operation's name appears in the user-provided config, @@ -187,14 +212,25 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: else: plugin_method = plugin_specifier - # ToDo: similarly to UnitarySynthesis, we should pass additional parameters - # e.g. coupling_map to the synthesis algorithm. + if self._coupling_map: + plugin_args["coupling_map"] = self._coupling_map + plugin_args["qubits"] = [dag_bit_indices[x] for x in node.qargs] + decomposition = plugin_method.run(node.op, **plugin_args) - # The synthesis methods that are not suited for the given higher-level-object - # will return None, in which case the next method in the list will be used. + # The above synthesis method may return: + # - None, if the synthesis algorithm is not suited for the given higher-level-object + # (in which case we consider the next method in the list if available). + # - decomposition when the order of qubits is not important + # - a tuple (decomposition, wires) when the node's qubits need to be reordered if decomposition is not None: - dag.substitute_node_with_dag(node, circuit_to_dag(decomposition)) + if isinstance(decomposition, tuple): + decomposition_dag = circuit_to_dag(decomposition[0]) + wires = [decomposition_dag.wires[i] for i in decomposition[1]] + dag.substitute_node_with_dag(node, decomposition_dag, wires=wires) + else: + dag.substitute_node_with_dag(node, circuit_to_dag(decomposition)) + break return dag @@ -328,10 +364,238 @@ class PMHSynthesisLinearFunction(HighLevelSynthesisPlugin): def run(self, high_level_object, **options): """Run synthesis for the given LinearFunction.""" - decomposition = synth_cnot_count_full_pmh(high_level_object.linear) + # For now, use PMH algorithm by default + decomposition = PMHSynthesisLinearFunction().run(high_level_object, **options) return decomposition +class KMSSynthesisLinearFunction(HighLevelSynthesisPlugin): + """Linear function synthesis plugin based on the Kutin-Moulton-Smithline method. + + The plugin also supports the following plugin-specific options that can be passed + in ``__init__`` and ``run`` methods: + + * coupling_map: CouplingMap of the target backend or ``None``. If this coupling map + is not ``None``, then the synthesized quantum circuit should adhere to the imposed + connectivity constraints. Note that the synthesis algorithm is allowed to return + ``None`` when it is unable to synthesize the desired circuit. + * qubits: the sequence of qubits over which the ``high_level_object`` is defined. + This is only used in the case that the coupling map is not ``None``. + * opt_count: an option to prioritize count over depth when multiple synthesized + circuits are available. + * all_mats: an option to synthesize quantum circuits for the matrix, its transpose, + its inverse, and its inverse transpose, choosing the best implementation. + * max_paths: when the coupling map is not ``None``, the internally used synthesis + algorithm needs to pick a hamiltonian path through ``qubits``. By default, only one + hamiltonian pass is considered. This option allows to increase the maximum number + of paths that should be considered, choosing the best implementation. + * orig_circuit: an option to also consider the original implementation of the linear + function when available, choosing the best implementation. Note that the original + implementation might not adhere to the coupling map. + + The output of the ``run`` method should either be ``None`` if the circuit could + not be synthesized, a ``QuantumCircuit``, or a pair consisting of a quantum circuit + and an ordered list of qubits. + """ + + def __init__(self, **options): + self._options = options + + def run(self, high_level_object, **options): + """Run synthesis for the given LinearFunction.""" + + # Combine the options passed in the initializer and now, + # prioritizing values passed now. + run_options = self._options.copy() + run_options.update(options) + + # options supported by this plugin + coupling_map = run_options.get("coupling_map", None) + qubits = run_options.get("qubits", None) + consider_all_mats = run_options.get("all_mats", 0) + max_paths = run_options.get("max_paths", 1) + consider_original_circuit = run_options.get("orig_circuit", 1) + optimize_count = run_options.get("opt_count", 1) + + # At the end, if not none, represents the best decomposition adhering to LNN architecture. + best_decomposition = None + + # At the end, if not none, represents the path of qubits through the coupling map + # over which the LNN synthesis is applied. + best_path = None + + if not coupling_map: + if consider_original_circuit: + best_decomposition = high_level_object.original_circuit + + if not consider_all_mats: + decomposition = synth_cnot_depth_line_kms(high_level_object.linear) + else: + decomposition = _optimize_cx_4_options( + synth_cnot_depth_line_kms, + high_level_object.linear, + optimize_count=optimize_count, + ) + + if not best_decomposition or _compare_linear_circuits( + best_decomposition, decomposition, optimize_count=optimize_count + ): + best_decomposition = decomposition + + else: + # Consider the coupling map over the qubits on which the linear function is applied. + reduced_map = coupling_map.reduce(qubits) + + # We can consider the original definition if it's compliant with the reduced map. + if consider_original_circuit and _linear_circuit_check_map( + high_level_object.original_circuit, reduced_map + ): + best_decomposition = high_level_object.original_circuit + + # Find one or more paths through the coupling map (when such exist). + considered_paths = _hamiltonian_paths(reduced_map, max_paths) + + for path in considered_paths: + permuted_linear_function = high_level_object.permute(path) + + if not consider_all_mats: + decomposition = synth_cnot_depth_line_kms(permuted_linear_function.linear) + else: + decomposition = _optimize_cx_4_options( + synth_cnot_depth_line_kms, + permuted_linear_function.linear, + optimize_count=False, + ) + + if not best_decomposition or _compare_linear_circuits( + best_decomposition, decomposition, optimize_count=False + ): + best_decomposition = decomposition + best_path = path + + if best_path is None: + return best_decomposition + + return best_decomposition, best_path + + +class PMHSynthesisLinearFunction(HighLevelSynthesisPlugin): + """Linear function synthesis plugin based on the Patel-Markov-Hayes method. + + The plugin also supports the following plugin-specific options that can be passed + in ``__init__`` and ``run`` methods: + + * coupling_map: CouplingMap of the target backend or ``None``. If this coupling map + is not ``None``, then the synthesized quantum circuit should adhere to the imposed + connectivity constraints. Note that the synthesis algorithm is allowed to return + ``None`` when it is unable to synthesize the desired circuit. + * opt_count: an option to prioritize count over depth when multiple synthesized + circuits are available. + * all_mats: an option to synthesize quantum circuits for the matrix, its transpose, + its inverse, and its inverse transpose, choosing the best implementation. + * orig_circuit: an option to also consider the original implementation of the linear + function when available, choosing the best implementation. Note that the original + implementation might not adhere to the coupling map. + + The output of the ``run`` method should either be ``None`` if the circuit could + not be synthesized, a ``QuantumCircuit``, or a pair consisting of a quantum circuit + and an ordered list of qubits. + """ + + def __init__(self, **options): + self._options = options + + def run(self, high_level_object, **options): + """Run synthesis for the given LinearFunction.""" + + # Combine the options passed in the initializer and now, + # prioritizing values passed now. + # Note: run_options = self._options | options is only supported for python >= 3.9. + run_options = self._options.copy() + run_options.update(options) + + # options supported by this plugin + coupling_map = run_options.get("coupling_map", None) + consider_all_mats = run_options.get("all_mats", 0) + consider_original_circuit = run_options.get("orig_circuit", 1) + optimize_count = run_options.get("opt_count", 1) + + # At the end, if not none, represents the best decomposition. + best_decomposition = None + + if consider_original_circuit: + best_decomposition = high_level_object.original_circuit + + # This synthesis method is not aware of the coupling map, so we cannot apply + # this method when the coupling map is not None. + # (Though, technically, we could check if the reduced coupling map is + # fully-connected). + + if not coupling_map: + if not consider_all_mats: + decomposition = synth_cnot_count_full_pmh(high_level_object.linear) + else: + decomposition = _optimize_cx_4_options( + synth_cnot_count_full_pmh, + high_level_object.linear, + optimize_count=optimize_count, + ) + + if not best_decomposition or _compare_linear_circuits( + best_decomposition, decomposition, optimize_count=optimize_count + ): + best_decomposition = decomposition + + return best_decomposition + + +def _hamiltonian_paths( + coupling_map: CouplingMap, cutoff: Union[None, int] = None +) -> List[List[int]]: + """Returns a list of all Hamiltonian paths in ``coupling_map`` (stopping the enumeration when + the number of already discovered paths exceeds the ``cutoff`` value, when specified). + In particular, returns an empty list if there are no Hamiltonian paths. + """ + + # This is a temporary function, the plan is to move it to rustworkx + + def _should_stop(): + return cutoff is not None and len(all_paths) >= cutoff + + def _recurse(current_node): + current_path.append(current_node) + on_path[current_node] = True + + if len(current_path) == coupling_map.size(): + # Discovered a new Hamiltonian path + all_paths.append(current_path.copy()) + + if _should_stop(): + return + + unvisited_neighbors = [ + node for node in coupling_map.neighbors(current_node) if not on_path[node] + ] + for node in unvisited_neighbors: + _recurse(node) + if _should_stop(): + return + + current_path.pop() + on_path[current_node] = False + + all_paths = [] + qubits = coupling_map.physical_qubits + current_path = [] + on_path = [False] * len(qubits) + + for qubit in qubits: + _recurse(qubit) + if _should_stop(): + break + return all_paths + + class KMSSynthesisPermutation(HighLevelSynthesisPlugin): """The permutation synthesis plugin based on the Kutin, Moulton, Smithline method. diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 4645b4ca1385..180a017e359f 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -219,7 +219,7 @@ def generate_unroll_3q( target=target, ) ) - unroll_3q.append(HighLevelSynthesis(hls_config=hls_config)) + unroll_3q.append(HighLevelSynthesis(hls_config=hls_config, target=target)) unroll_3q.append(Unroll3qOrMore(target=target, basis_gates=basis_gates)) return unroll_3q @@ -424,7 +424,7 @@ def generate_translation_passmanager( method=unitary_synthesis_method, target=target, ), - HighLevelSynthesis(hls_config=hls_config), + HighLevelSynthesis(hls_config=hls_config, coupling_map=coupling_map, target=target), UnrollCustomDefinitions(sel, basis_gates=basis_gates, target=target), BasisTranslator(sel, basis_gates, target), ] @@ -442,7 +442,7 @@ def generate_translation_passmanager( min_qubits=3, target=target, ), - HighLevelSynthesis(hls_config=hls_config), + HighLevelSynthesis(hls_config=hls_config, coupling_map=coupling_map, target=target), Unroll3qOrMore(target=target, basis_gates=basis_gates), Collect2qBlocks(), Collect1qRuns(), @@ -458,7 +458,7 @@ def generate_translation_passmanager( method=unitary_synthesis_method, target=target, ), - HighLevelSynthesis(hls_config=hls_config), + HighLevelSynthesis(hls_config=hls_config, coupling_map=coupling_map, target=target), ] else: raise TranspilerError("Invalid translation method %s." % method) diff --git a/releasenotes/notes/add-coupling-map-to-hls-f8d377ca98f5e1fc.yaml b/releasenotes/notes/add-coupling-map-to-hls-f8d377ca98f5e1fc.yaml new file mode 100644 index 000000000000..1c7d1ef887f1 --- /dev/null +++ b/releasenotes/notes/add-coupling-map-to-hls-f8d377ca98f5e1fc.yaml @@ -0,0 +1,50 @@ +--- +features: + - | + Added the ability to pass the coupling map to :class:`.HighLevelSynthesis` + transpiler pass. When the coupling map is not ``None``, each synthesis method + (built on top of :class:`.HighLevelSynthesisPlugin`) is required to synthesize + a given high-level object adhering to this coupling map, or return ``None`` if + unable to do so. + - | + Added two high-level-synthesis plugins for :class:`.LinearFunction`. The + :class:`.PMHSynthesisLinearFunction` is the synthesis plugin built on top + of the Patel-Markov-Hayes method. As the underlying method does not take the + coupling map into account, the plugin only synthesizes the given linear function + when the coupling map is ``None``, and returns ``None`` otherwise. The + :class:`.KMSSynthesisLinearFunction` is the synthesis plugin built on top of the + Kutin-Moulton-Smithline method. The underlying synthesis method synthesizes with + respect to linear nearest-neighbour (LNN) architecture. The plugin synthesizes + the given linear function either when the coupling map is ``None``, or when there + exists a hamiltonian path through the qubits over which this linear function is + defined. When the coupling map is not ``None`` and the hamiltonian path does + not exist, this plugin returns ``None``. +upgrade: + - | + Added an alternative way to specify the synthesis methods used for a given + high-level-object in :class:`.HighLevelSynthesis` transpiler pass. Now, each + synthesis method can be either a tuple consisting of the name of the method + and additional arguments, or an instance of :class:`.HighLevelSynthesisPlugin`. + The following example shows how one can do both, even using both forms in the + list of synthesis methods:: + + from qiskit import QuantumCircuit + from qiskit.transpiler import PassManager, CouplingMap + from qiskit.transpiler.passes import HighLevelSynthesis + from qiskit.circuit.library.generalized_gates import LinearFunction + from qiskit.transpiler.passes.synthesis.high_level_synthesis import KMSSynthesisLinearFunction + + mat = [[1, 0, 0, 0], [1, 1, 0, 0], [1, 1, 1, 0], [1, 1, 1, 1]] + linear_function = LinearFunction(mat) + qc = QuantumCircuit(6) + qc.append(linear_function, [1, 2, 3, 4]) + coupling_map = CouplingMap.from_line(6) + + config = HLSConfig( + linear_function=[ + ("pmh", {"orig_circuit": 0}), + KMSSynthesisLinearFunction(orig_circuit=0), + ] + ) + pm = PassManager(HighLevelSynthesis(hls_config=config, coupling_map=coupling_map)) + qc_synthesized = pm.run(qc) diff --git a/test/python/circuit/library/test_linear_function.py b/test/python/circuit/library/test_linear_function.py index f9b7e5dce21e..87385668ccd6 100644 --- a/test/python/circuit/library/test_linear_function.py +++ b/test/python/circuit/library/test_linear_function.py @@ -239,6 +239,22 @@ def test_no_original_definition(self): linear_function = LinearFunction(mat) self.assertIsNone(linear_function.original_circuit) + def test_permute(self): + """Tests correctness of ``permute`` method.""" + mat = [[1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]] + + # This means: 2->0, 0->1, 1->2, 3->3 + perm = [2, 0, 1, 3] + + permuted_matrix = LinearFunction(mat).permute(perm).linear.astype(int) + + # expected matrix is obtained by first reordering columns + # according to the permutation pattern, i.e. + # [[0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 1], [1, 0, 0, 0]], + # and then reordering rows of the matrix. + expected_matrix = [[0, 0, 0, 1], [0, 1, 1, 0], [0, 0, 1, 0], [1, 0, 0, 0]] + self.assertTrue(np.all(permuted_matrix == expected_matrix)) + if __name__ == "__main__": unittest.main() diff --git a/test/python/synthesis/test_linear_synthesis.py b/test/python/synthesis/test_linear_synthesis.py index 0448414e6b70..590bbcfb1534 100644 --- a/test/python/synthesis/test_linear_synthesis.py +++ b/test/python/synthesis/test_linear_synthesis.py @@ -25,7 +25,13 @@ check_invertible_binary_matrix, calc_inverse_matrix, ) -from qiskit.synthesis.linear.linear_circuits_utils import transpose_cx_circ, optimize_cx_4_options +from qiskit.synthesis.linear.linear_circuits_utils import ( + transpose_cx_circ, + _optimize_cx_4_options, + _linear_circuit_depth, + _compare_linear_circuits, + _linear_circuit_check_map, +) from qiskit.test import QiskitTestCase @@ -43,7 +49,7 @@ def test_lnn_circuit(self): mat = LinearFunction(qc).linear for optimized in [True, False]: - optimized_qc = optimize_cx_4_options( + optimized_qc = _optimize_cx_4_options( synth_cnot_count_full_pmh, mat, optimize_count=optimized ) self.assertEqual(optimized_qc.depth(), 4) @@ -60,7 +66,7 @@ def test_full_circuit(self): mat = LinearFunction(qc).linear for optimized in [True, False]: - optimized_qc = optimize_cx_4_options( + optimized_qc = _optimize_cx_4_options( synth_cnot_count_full_pmh, mat, optimize_count=optimized ) self.assertEqual(optimized_qc.depth(), 4) @@ -94,11 +100,11 @@ def test_example_circuit(self): qc.cx(1, 0) mat = LinearFunction(qc).linear - optimized_qc = optimize_cx_4_options(synth_cnot_count_full_pmh, mat, optimize_count=True) + optimized_qc = _optimize_cx_4_options(synth_cnot_count_full_pmh, mat, optimize_count=True) self.assertEqual(optimized_qc.depth(), 17) self.assertEqual(optimized_qc.count_ops()["cx"], 20) - optimized_qc = optimize_cx_4_options(synth_cnot_count_full_pmh, mat, optimize_count=False) + optimized_qc = _optimize_cx_4_options(synth_cnot_count_full_pmh, mat, optimize_count=False) self.assertEqual(optimized_qc.depth(), 15) self.assertEqual(optimized_qc.count_ops()["cx"], 23) @@ -136,6 +142,77 @@ def test_synth_lnn_kms(self, num_qubits): dist = abs(q0 - q1) self.assertEqual(dist, 1) + def test_linear_circuit_depth(self): + """Test the ``_linear_circuit_depth`` utility method, in particular that it + returns the same depth when SWAPs are replaced by three CXs.""" + + qc1 = QuantumCircuit(4) + qc1.cx(0, 1) + qc1.cx(0, 2) + qc1.swap(1, 3) + qc1.cx(0, 3) + + qc2 = QuantumCircuit(4) + qc2.cx(0, 1) + qc2.cx(0, 2) + qc2.cx(1, 3) + qc2.cx(3, 1) + qc2.cx(1, 3) + qc2.cx(0, 3) + + depth1 = _linear_circuit_depth(qc1) + depth2 = _linear_circuit_depth(qc2) + + self.assertEqual(depth1, depth2) + + def test_compare_linear_circuits_count(self): + """Test the ``_compare_linear_circuits`` utility method.""" + + # Create two quantum circuits, one with better count, another with better depth + # (ignoring the fact that they do not implement the same linear function). + qc1 = QuantumCircuit(4) + qc1.cx(0, 1) + qc1.cx(2, 3) + qc1.cx(0, 2) + qc1.cx(1, 3) + self.assertEqual(qc1.size(), 4) + self.assertEqual(qc1.depth(), 2) + + qc2 = QuantumCircuit(4) + qc2.cx(0, 1) + qc2.cx(0, 2) + qc2.cx(0, 3) + self.assertEqual(qc2.size(), 3) + self.assertEqual(qc2.depth(), 3) + + qc2_count_is_better = _compare_linear_circuits(qc1, qc2, optimize_count=True) + qc2_depth_is_better = _compare_linear_circuits(qc1, qc2, optimize_count=False) + + self.assertTrue(qc2_count_is_better) + self.assertFalse(qc2_depth_is_better) + + def test_linear_circuit_check_map(self): + """Test the ``_linear_circuit_check_map`` utility method.""" + coupling_list = [(0, 1), (1, 2), (2, 3)] + + qc1 = QuantumCircuit(4) + qc1.cx(1, 2) + qc1.cx(2, 3) + qc1.cx(1, 2) + self.assertTrue(_linear_circuit_check_map(qc1, coupling_list)) + + qc2 = QuantumCircuit(4) + qc2.cx(1, 2) + qc2.cx(1, 3) + qc2.cx(1, 2) + self.assertFalse(_linear_circuit_check_map(qc2, coupling_list)) + + qc3 = QuantumCircuit(4) + qc3.cx(1, 2) + qc3.cx(3, 2) + qc3.cx(1, 2) + self.assertFalse(_linear_circuit_check_map(qc3, coupling_list)) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_linear_functions_passes.py b/test/python/transpiler/test_linear_functions_passes.py index 5c09f6d46ea5..9ae09f227c29 100644 --- a/test/python/transpiler/test_linear_functions_passes.py +++ b/test/python/transpiler/test_linear_functions_passes.py @@ -23,11 +23,18 @@ LinearFunctionsSynthesis, HighLevelSynthesis, LinearFunctionsToPermutations, + HighLevelSynthesis, + HLSConfig, +) +from qiskit.transpiler.passes.synthesis.high_level_synthesis import ( + PMHSynthesisLinearFunction, + KMSSynthesisLinearFunction, + _hamiltonian_paths, ) from qiskit.test import QiskitTestCase from qiskit.circuit.library.generalized_gates import LinearFunction from qiskit.circuit.library import RealAmplitudes -from qiskit.transpiler import PassManager +from qiskit.transpiler import PassManager, CouplingMap from qiskit.quantum_info import Operator @@ -595,6 +602,212 @@ def test_do_not_merge_conditional_gates(self): # Make sure that the condition on the middle gate is not lost self.assertIsNotNone(qct.data[1].operation.condition) + def test_synthesis_using_pmh(self): + """Test high level synthesis of linear functions using the PMHSynthesisLinearFunction + plugin. The synthesis method is specified as a tuple consisting of the method to run + and additional parameters. + """ + qc = QuantumCircuit(6) + mat = [[1, 0, 0, 0], [1, 1, 0, 0], [1, 1, 1, 0], [1, 1, 1, 1]] + linear_function = LinearFunction(mat) + qc.append(linear_function, [1, 2, 3, 4]) + + # Identifies plugin + config = HLSConfig(linear_function=[("pmh", {"all_mats": 1})]) + pm = PassManager(HighLevelSynthesis(hls_config=config)) + qct = pm.run(qc) + self.assertNotIn("linear_function", qct.count_ops().keys()) + + def test_synthesis_using_pmh_alternate_form(self): + """Test high level synthesis of linear functions using the PMHSynthesisLinearFunction + plugin. The synthesis method is specified as a class instance.""" + qc = QuantumCircuit(6) + mat = [[1, 0, 0, 0], [1, 1, 0, 0], [1, 1, 1, 0], [1, 1, 1, 1]] + linear_function = LinearFunction(mat) + qc.append(linear_function, [1, 2, 3, 4]) + + # Test that running plugin specifying class method works correctly + pmh = PMHSynthesisLinearFunction(all_mats=1) + config = HLSConfig(linear_function=[pmh]) + pm = PassManager(HighLevelSynthesis(hls_config=config)) + qct = pm.run(qc) + self.assertNotIn("linear_function", qct.count_ops().keys()) + + def test_synthesis_using_pmh_with_coupling_map(self): + """Test high level synthesis of linear functions using the PMHSynthesisLinearFunction + plugin when the coupling map is specified, in which case the linear function should + not be synthesized. + """ + qc = QuantumCircuit(6) + mat = [[1, 0, 0, 0], [1, 1, 0, 0], [1, 1, 1, 0], [1, 1, 1, 1]] + linear_function = LinearFunction(mat) + qc.append(linear_function, [1, 2, 3, 4]) + + coupling_map = CouplingMap.from_line(6) + config = HLSConfig(linear_function=[("pmh", {"all_mats": 1})]) + pm = PassManager(HighLevelSynthesis(hls_config=config, coupling_map=coupling_map)) + qct = pm.run(qc) + self.assertIn("linear_function", qct.count_ops().keys()) + + def test_synthesis_using_pmh_with_original_circuit(self): + """Test high level synthesis of linear functions using the PMHSynthesisLinearFunction + plugin when the linear function is created from some linear circuit. If the coupling map + is specified and ``orig_circuit`` option is 0, the linear function should not be + synthesized. However, if the ``orig_circuit`` option is 1, the linear function should + be replaced by this linear circuit.""" + qcl = QuantumCircuit(4) + qcl.cx(0, 1) + qcl.cx(0, 2) + qcl.cx(0, 3) + linear_function = LinearFunction(qcl) + + qc = QuantumCircuit(6) + qc.append(linear_function, [1, 2, 3, 4]) + + # First, setting orig_circuit to 0 + coupling_map = CouplingMap.from_line(6) + config = HLSConfig(linear_function=[("pmh", {"orig_circuit": 0})]) + pm = PassManager(HighLevelSynthesis(hls_config=config, coupling_map=coupling_map)) + qct = pm.run(qc) + self.assertIn("linear_function", qct.count_ops().keys()) + + # Second, setting orig_circuit to 1 + config = HLSConfig(linear_function=[("pmh", {"orig_circuit": 1})]) + pm = PassManager(HighLevelSynthesis(hls_config=config, coupling_map=coupling_map)) + qct = pm.run(qc) + self.assertNotIn("linear_function", qct.count_ops().keys()) + + def test_synthesis_using_kms(self): + """Test high level synthesis of linear functions using the KMSSynthesisLinearFunction + plugin. The synthesis method is specified as a tuple consisting of the method to run + and additional parameters. + """ + qc = QuantumCircuit(6) + mat = [[1, 0, 0, 0], [1, 1, 0, 0], [1, 1, 1, 0], [1, 1, 1, 1]] + linear_function = LinearFunction(mat) + qc.append(linear_function, [1, 2, 3, 4]) + + # Identifies plugin + config = HLSConfig(linear_function=[("kms", {"all_mats": 1})]) + pm = PassManager(HighLevelSynthesis(hls_config=config)) + qct = pm.run(qc) + self.assertNotIn("linear_function", qct.count_ops().keys()) + + def test_synthesis_using_kms_with_coupling_map(self): + """Test high level synthesis of linear functions using the KMSSynthesisLinearFunction + plugin when the coupling map is specified. The linear function should be synthesized + if the coupling map restricted to the set of qubits over which the linear function + is defined contains a hamiltonian path. + """ + mat = [[1, 0, 0, 0], [1, 1, 0, 0], [1, 1, 1, 0], [1, 1, 1, 1]] + linear_function = LinearFunction(mat) + + coupling_map = CouplingMap([(0, 1), (1, 2), (1, 3), (4, 3), (3, 5)]) + coupling_map.make_symmetric() + + config = HLSConfig(linear_function=[("kms", {"orig_circuit": 0})]) + pm = PassManager(HighLevelSynthesis(hls_config=config, coupling_map=coupling_map)) + + # First, consider the case that the linear function is defined over a set of qubits + # with a hamiltonian path + qc1 = QuantumCircuit(6) + qc1.append(linear_function, [1, 2, 3, 4]) + qct1 = pm.run(qc1) + self.assertNotIn("linear_function", qct1.count_ops().keys()) + + # Second, consider the case that the linear function is defined over a set of qubits + # without a Hamiltonian path + qc2 = QuantumCircuit(6) + qc2.append(linear_function, [1, 3, 4, 5]) + qct2 = pm.run(qc2) + self.assertIn("linear_function", qct2.count_ops().keys()) + + def test_synthesis_using_kms_with_compliant_original_circuit(self): + """Test high level synthesis of linear functions using the KMSSynthesisLinearFunction + plugin when the linear function is created from some linear circuit. If the coupling + map does not have a hamiltonian path but the ``orig_circuit`` option is 1 and the linear + function is adheres to the coupling map, the linear function should be synthesized.""" + qcl = QuantumCircuit(4) + qcl.cx(0, 1) + qcl.cx(0, 2) + qcl.cx(0, 3) + linear_function = LinearFunction(qcl) + + coupling_map = CouplingMap([(0, 1), (1, 2), (1, 3), (4, 3), (3, 5)]) + coupling_map.make_symmetric() + + qc = QuantumCircuit(6) + qc.append(linear_function, [3, 1, 4, 5]) + + # First, the case that synthesis does not apply and orig_circuit option is 0. + config = HLSConfig(linear_function=[("kms", {"orig_circuit": 0})]) + pm = PassManager(HighLevelSynthesis(hls_config=config, coupling_map=coupling_map)) + qct = pm.run(qc) + self.assertIn("linear_function", qct.count_ops().keys()) + + # Second, the case that synthesis does not apply but orig_circuit option is 1. + config = HLSConfig(linear_function=[("kms", {"orig_circuit": 1})]) + pm = PassManager(HighLevelSynthesis(hls_config=config, coupling_map=coupling_map)) + qct = pm.run(qc) + self.assertNotIn("linear_function", qct.count_ops().keys()) + + def test_synthesis_using_kms_with_non_compliant_original_circuit(self): + """Test high level synthesis of linear functions using the KMSSynthesisLinearFunction + plugin when the linear function is created from some linear circuit. If the coupling + map does not have a hamiltonian path but the ``orig_circuit`` option is 1 yet the linear + function does not adhere to the coupling map, the linear function should not be synthesized.""" + qcl = QuantumCircuit(4) + qcl.cx(0, 1) + qcl.cx(0, 2) + qcl.cx(0, 3) + linear_function = LinearFunction(qcl) + + coupling_map = CouplingMap([(0, 1), (1, 2), (1, 3), (4, 3), (3, 5)]) + coupling_map.make_symmetric() + + qc = QuantumCircuit(6) + qc.append(linear_function, [1, 3, 4, 5]) + + # First, the case that synthesis does not apply and orig_circuit option is 0. + config = HLSConfig(linear_function=[("kms", {"orig_circuit": 0})]) + pm = PassManager(HighLevelSynthesis(hls_config=config, coupling_map=coupling_map)) + qct = pm.run(qc) + self.assertIn("linear_function", qct.count_ops().keys()) + + # Second, the case that synthesis does not apply but orig_circuit option is 1. + config = HLSConfig(linear_function=[("kms", {"orig_circuit": 1})]) + pm = PassManager(HighLevelSynthesis(hls_config=config, coupling_map=coupling_map)) + qct = pm.run(qc) + self.assertIn("linear_function", qct.count_ops().keys()) + + def test_synthesis_with_multiple_methods(self): + """Test high level synthesis with multiple methods.""" + mat = [[1, 0, 0, 0], [1, 1, 0, 0], [1, 1, 1, 0], [1, 1, 1, 1]] + linear_function = LinearFunction(mat) + qc = QuantumCircuit(6) + qc.append(linear_function, [1, 2, 3, 4]) + coupling_map = CouplingMap.from_line(6) + # This specifies a sequence of different methods to use (additionally, specified using + # different forms). Only the last method synthesizes the function. + config = HLSConfig( + linear_function=[ + ("pmh", {"orig_circuit": 0}), + PMHSynthesisLinearFunction(orig_circuit=1), + KMSSynthesisLinearFunction(orig_circuit=0), + ] + ) + pm = PassManager(HighLevelSynthesis(hls_config=config, coupling_map=coupling_map)) + qct = pm.run(qc) + self.assertNotIn("linear_function", qct.count_ops().keys()) + + def test_hamiltonian_paths(self): + """Test the utility ``_hamiltonian_paths`` method.""" + coupling_list = [(0, 1), (1, 2), (2, 3), (1, 3), (2, 0)] + computed_paths = _hamiltonian_paths(CouplingMap(coupling_list)) + computed_paths_set = {tuple(path) for path in computed_paths} + expected_paths_set = {tuple([0, 1, 2, 3]), tuple([2, 0, 1, 3])} + self.assertEqual(computed_paths_set, expected_paths_set) + if __name__ == "__main__": unittest.main()