Ejemplo n.º 1
0
def _hardware_aware_reset(
    mapping: ty.Dict[Qubit, int],
    circuit: QuantumCircuit,
    hardware: IBMQHardwareArchitecture,
) -> ty.Dict[Qubit, int]:
    starting_qubit = random.randint(0, hardware.qubit_number - 1)
    qubits: ty.List[int] = [starting_qubit]
    weights: ty.Dict[int, float] = dict()
    while len(qubits) < len(mapping):
        # 1. Update the weights
        for neighbour in hardware.neighbors(qubits[-1]):
            if neighbour not in qubits:
                if neighbour not in weights.keys():
                    weights[neighbour] = 0.5 * (
                        1 - hardware.get_qubit_readout_error(neighbour))
                else:
                    weights[neighbour] += (
                        weights.get(neighbour, 0) + 1 -
                        hardware.get_link_error_rate(qubits[-1], neighbour))
        # Find the best weighted qubit
        best_weight, best_qubit = 0, None
        for qubit, weight in weights.items():
            if weight > best_weight:
                best_qubit = qubit
                best_weight = weight
        # Insert it in the qubit list
        qubits.append(best_qubit)
        del weights[best_qubit]
    # Finally, return a mapping with the chosen qubits
    return {qubit: idx for qubit, idx in zip(mapping.keys(), qubits)}
Ejemplo n.º 2
0
def get_all_swap_candidates(
    layer: QuantumLayer,
    hardware: IBMQHardwareArchitecture,
    current_mapping: ty.Dict[Qubit, int],
    explored_mappings: ty.Set[str],
) -> ty.List[SwapTwoQubitGate]:
    # First compute all the qubits involved in the given layer
    qubits_involved_in_front_layer = set()
    for op in layer.ops:
        qubits_involved_in_front_layer.update(op.qargs)
    inverse_mapping = {val: key for key, val in current_mapping.items()}
    # Then for all the possible links that involve at least one of the qubits used by
    # the gates in the given layer, add this link as a possible SWAP.
    all_swaps = list()
    for involved_qubit in qubits_involved_in_front_layer:
        qubit_index = current_mapping[involved_qubit]
        # For all the links that involve the current qubit.
        for source, sink in hardware.out_edges(qubit_index):
            two_qubit_gate = SwapTwoQubitGate(inverse_mapping[source],
                                              inverse_mapping[sink])
            # Check that the mapping has not already been explored in this
            # SWAP-insertion pass.
            if (mapping_to_str(two_qubit_gate.update_mapping(current_mapping))
                    not in explored_mappings):
                all_swaps.append(two_qubit_gate)
    return all_swaps
Ejemplo n.º 3
0
def _hardware_aware_expand(
    mapping: ty.Dict[Qubit, int],
    circuit: QuantumCircuit,
    hardware: IBMQHardwareArchitecture,
) -> ty.Dict[Qubit, int]:
    qubits = list(mapping.values())
    idle_qubits = {mapping[qubit] for qubit in _get_idle_qubits(circuit)}
    used_qubits = list(set(qubits) - idle_qubits)
    if not idle_qubits:
        return _random_shuffle(mapping, circuit, hardware)
    # Compute a weight for each qubit.
    # A qubit with a lot of links to other qubits in the mapping is good.
    # A qubit with bad links is not that good.
    weights: ty.List[float] = list()
    outside_qubits_weights = dict()
    for qubit in used_qubits:
        weights.append(0.5 * (1 - hardware.get_qubit_readout_error(qubit)))
        for neighbour in hardware.neighbors(qubit):
            # Only count the neighbour if it is also in the mapping.
            if neighbour in used_qubits:
                weights[-1] += 1 - hardware.get_link_error_rate(
                    qubit, neighbour)
            # Else, we keep an eye on the qubits that are not in the mapping because
            # we will need the best of them to add it to the mapping.
            else:
                if neighbour not in outside_qubits_weights.keys():
                    outside_qubits_weights[neighbour] = 0.5 * (
                        1 - hardware.get_qubit_readout_error(qubit))
                else:
                    outside_qubits_weights[neighbour] += (
                        outside_qubits_weights.get(neighbour, 0) + 1 -
                        hardware.get_link_error_rate(qubit, neighbour))
    worst_qubit_index = _argmin(weights)
    best_outside_qubit_index = None
    best_outside_weight = 0
    for neighbour, weight in outside_qubits_weights.items():
        if weight > best_outside_weight:
            best_outside_qubit_index = neighbour
            best_outside_weight = weight
    # Now exchange the 2 qubits
    inverse_mapping = {v: k for k, v in mapping.items()}
    inverse_mapping[worst_qubit_index], inverse_mapping[
        best_outside_qubit_index] = (
            inverse_mapping[best_outside_qubit_index],
            inverse_mapping[worst_qubit_index],
        )
    return {v: k for k, v in inverse_mapping.items()}
Ejemplo n.º 4
0
def get_all_bridge_candidates(
    layer: QuantumLayer,
    hardware: IBMQHardwareArchitecture,
    initial_mapping: ty.Dict[Qubit, int],
    trans_mapping: ty.Dict[Qubit, int],
    current_mapping: ty.Dict[Qubit, int],
    explored_mappings: ty.Set[str],
) -> ty.List[BridgeTwoQubitGate]:
    all_bridges = []

    inverse_trans_mapping = {val: key for key, val in trans_mapping.items()}
    inverse_mapping = {val: key for key, val in initial_mapping.items()}
    for op in layer.ops:
        if len(op.qargs) < 2:
            # We just pass 1 qubit gates because they do not participate in the
            # Bridge operation
            continue
        if len(op.qargs) != 2:
            logger.warning(
                "A 3-qubit or more gate has been found in the circuit.")
            continue

        control, target = op.qargs
        control_index = initial_mapping[inverse_trans_mapping[
            initial_mapping[control]]]
        target_index = initial_mapping[inverse_trans_mapping[
            initial_mapping[target]]]
        # For each qubit q linked with control, check if target is linked with q.
        for _, potential_middle_index in hardware.out_edges(control_index):
            for _, potential_target_index in hardware.out_edges(
                    potential_middle_index):
                if potential_target_index == target_index:
                    two_qubit_gate = BridgeTwoQubitGate(
                        inverse_trans_mapping[initial_mapping[control]],
                        inverse_mapping[potential_middle_index],
                        inverse_trans_mapping[initial_mapping[target]],
                    )
                    # Check that the mapping has not already been explored in this
                    # SWAP-insertion pass.
                    if (mapping_to_str(
                            two_qubit_gate.update_mapping(current_mapping))
                            not in explored_mappings):
                        all_bridges.append(two_qubit_gate)
    return all_bridges
Ejemplo n.º 5
0
def main():
    parser = argparse.ArgumentParser(
        "Compare the Bridge+SWAP approach to the simple SWAP one.")

    parser.add_argument(
        "N",
        type=int,
        help=
        "Number of initial mapping that will be explored. Should be strictly "
        "over 1 (i.e. 2 or more).",
    )
    parser.add_argument("circuit_name",
                        type=str,
                        help="Name of the quantum circuit to map.")
    parser.add_argument("hardware",
                        type=str,
                        help="Name of the hardware to consider.")

    args = parser.parse_args()

    N = args.N
    if N < 1:
        raise RuntimeError("N should be 2 or more.")
    hardware = IBMQHardwareArchitecture.load(args.hardware)
    circuit = add_qubits_to_quantum_circuit(
        read_benchmark_circuit("sabre", args.circuit_name), hardware)

    initial_mappings = [{
        qubit: i
        for qubit, i in zip(circuit.qubits,
                            permutation(range(hardware.qubit_number)))
    } for _ in range(N)]

    # using_swap_bridge_strategy_tup([circuit, hardware, initial_mappings[0]])
    with ProcessPoolExecutor(max_workers=cpu_count()) as executor:
        best_swap_results, swap_timings = separate_lists(
            executor.map(
                using_only_swap_strategy_tup,
                zip(
                    itertools.repeat(circuit, N),
                    itertools.repeat(hardware, N),
                    initial_mappings,
                ),
            ))
        print_statistics("SWAP", best_swap_results, swap_timings)
        best_swap_bridge_results, swap_bridge_timings = separate_lists(
            executor.map(
                using_swap_bridge_strategy_tup,
                zip(
                    itertools.repeat(circuit, N),
                    itertools.repeat(hardware, N),
                    initial_mappings,
                ),
            ))
        print_statistics("SWAP+Bridge", best_swap_bridge_results,
                         swap_bridge_timings)
Ejemplo n.º 6
0
def main():
    N = 10
    hardware = IBMQHardwareArchitecture.load("ibmq_16_melbourne")
    circuit = add_qubits_to_quantum_circuit(
        read_benchmark_circuit("sabre", "ising_model_10"), hardware
    )
    initial_mapping = get_random_mapping(circuit)

    for i in range(1, N):
        print(
            f"Swap number {i}: {test_iterated_sabre(circuit, hardware, i, initial_mapping)}"
        )
Ejemplo n.º 7
0
def _gate_op_cost(
    op: DAGNode,
    distance_matrix: numpy.ndarray,
    mapping: ty.Dict[Qubit, int],
    hardware: IBMQHardwareArchitecture,
) -> float:
    if hardware.is_ignored_operation(op):
        return 0
    if len(op.qargs) == 1:
        # SABRE ignores 1-qubit gates
        return 0
    elif len(op.qargs) == 2:
        # This is a CNOT
        source, sink = op.qargs
        return distance_matrix[mapping[source], mapping[sink]]
    else:
        logger.warning(
            f"Found a quantum operation applied on '{len(op.qargs)}' qubits. This "
            f"operation will be excluded from the cost computation."
        )
        return 0
Ejemplo n.º 8
0
def get_distance_matrix_error_cost(
        hardware: IBMQHardwareArchitecture) -> numpy.ndarray:
    hardware.weight_function = _get_swap_error_cost
    return nx.floyd_warshall_numpy(hardware)
Ejemplo n.º 9
0
def _get_swap_error_cost(node, hardware: IBMQHardwareArchitecture) -> float:
    source, sink = node
    cnot_fidelity = 1 - hardware.get_link_error_rate(source, sink)
    reversed_cnot_fidelity = 1 - hardware.get_link_error_rate(sink, source)
    return 1 - cnot_fidelity * reversed_cnot_fidelity * max(
        cnot_fidelity, reversed_cnot_fidelity)
Ejemplo n.º 10
0
def _get_swap_execution_time_cost(node,
                                  hardware: IBMQHardwareArchitecture) -> float:
    source, sink = node
    cnot_cost = hardware.get_link_execution_time(source, sink)
    reversed_cnot_cost = hardware.get_link_execution_time(sink, source)
    return cnot_cost + reversed_cnot_cost + min(cnot_cost, reversed_cnot_cost)
Ejemplo n.º 11
0
def main():
    parser = argparse.ArgumentParser(
        "Compare the annealing method to pure random.")

    parser.add_argument(
        "N",
        type=int,
        help=
        "Number of allowed call to the mapping procedure. Should be strictly "
        "over 1 (i.e. 2 or more).",
    )
    parser.add_argument("M",
                        type=int,
                        help="Number of repetitions for statistics.")
    parser.add_argument("Nstep",
                        type=int,
                        help="Steps used to increase N from step "
                        "to N.")
    parser.add_argument("circuit_name",
                        type=str,
                        help="Name of the quantum circuit to map.")
    parser.add_argument("hardware",
                        type=str,
                        help="Name of the hardware to consider.")

    args = parser.parse_args()

    N = args.N
    if N <= 1:
        raise RuntimeError("N should be 2 or more.")
    M = args.M
    Nstep = args.Nstep
    hardware = IBMQHardwareArchitecture.load(args.hardware)
    circuit = add_qubits_to_quantum_circuit(
        read_benchmark_circuit("sabre", args.circuit_name), hardware)

    results = dict()

    with ProcessPoolExecutor(max_workers=cpu_count()) as executor:
        N_values = list(range(Nstep, N + 1, Nstep))
        print("Computing random...")
        best_random_results, random_timings = separate_lists_all_values_of_n(
            executor.map(
                random_tuple_strategy_results,
                itertools.repeat([N, circuit, hardware, N_values], M),
            ))
        print("Computing SABRE...")
        best_sabre_results, sabre_timings = separate_lists_all_values_of_n(
            executor.map(
                sabre_tuple_strategy_results,
                itertools.repeat([N, circuit, hardware, N_values], M),
            ))
        for i, n in enumerate(N_values):
            print(f"Computing for {n}:")
            print("\tannealing...")
            best_annealing_results, annealing_timings = separate_lists(
                executor.map(
                    annealing_tuple_strategy_results,
                    itertools.repeat([n, circuit, hardware], M),
                ))
            # print_statistics("Annealing", best_annealing_results, annealing_timings)
            print("\tSABRE-annealing...")
            best_sabre_annealing_results, sabre_annealing_timings = separate_lists(
                executor.map(
                    sabre_annealing_tuple_strategy_results,
                    itertools.repeat([n, circuit, hardware], M),
                ))
            # print_statistics(
            #     "SABRE + Annealing",
            #     best_sabre_annealing_results,
            #     sabre_annealing_timings,
            # )
            print("\tforward-backward...")
            (
                best_forward_backward_results,
                forward_backward_timings,
            ) = separate_lists(
                executor.map(
                    forward_backward_tuple_strategy_results,
                    itertools.repeat([n, circuit, hardware], M),
                ))
            # print_statistics(
            #     "Forward-backward",
            #     best_forward_backward_results,
            #     forward_backward_timings,
            # )
            print("\tforward-backward annealing...")
            (
                best_forward_backward_annealing_results,
                forward_backward_annealing_timings,
            ) = separate_lists(
                executor.map(
                    forward_backward_tuple_strategy_results,
                    itertools.repeat([n, circuit, hardware], M),
                ))
            # print_statistics(
            #     "Forward-backward + Annealing",
            #     best_forward_backward_annealing_results,
            #     forward_backward_annealing_timings,
            # )
            print("\tforward-backward neighbour...")
            (
                best_forward_backward_neighbour_results,
                forward_backward_neighbour_timings,
            ) = separate_lists(
                executor.map(
                    forward_backward_neighbour_tuple_strategy_results,
                    itertools.repeat([n, circuit, hardware], M),
                ))
            results[n] = {
                "random": {
                    "results": best_random_results[i],
                    "times": random_timings[i],
                },
                "annealing": {
                    "results": best_annealing_results,
                    "times": annealing_timings,
                },
                "sabre": {
                    "results": best_sabre_results[i],
                    "times": sabre_timings[i]
                },
                "sabre_annealing": {
                    "results": best_sabre_annealing_results,
                    "times": sabre_annealing_timings,
                },
                "iterated": {
                    "results": best_forward_backward_results,
                    "times": forward_backward_timings,
                },
                "iterated_annealing": {
                    "results": best_forward_backward_annealing_results,
                    "times": forward_backward_annealing_timings,
                },
                "iterated_neighbour": {
                    "results": best_forward_backward_neighbour_results,
                    "times": forward_backward_neighbour_timings,
                },
            }

    with open(
            f"results-{N}-{Nstep}-{M}-{args.circuit_name}-{args.hardware}.pkl",
            "wb") as f:
        pickle.dump(results, f)
Ejemplo n.º 12
0
def main():
    parser = argparse.ArgumentParser(
        "Compare the annealing method to pure random.")

    parser.add_argument(
        "N",
        type=int,
        help=
        "Number of allowed call to the mapping procedure. Should be strictly "
        "over 1 (i.e. 2 or more).",
    )
    parser.add_argument("M",
                        type=int,
                        help="Number of repetitions for statistics.")
    parser.add_argument("Nstep",
                        type=int,
                        help="Steps used to increase N from step to N.")
    parser.add_argument("circuit_name",
                        type=str,
                        help="Name of the quantum circuit to map.")
    parser.add_argument("hardware",
                        type=str,
                        help="Name of the hardware to consider.")

    args = parser.parse_args()

    N = args.N
    if N <= 1:
        raise RuntimeError("N should be 2 or more.")
    M = args.M
    Nstep = args.Nstep
    hardware = IBMQHardwareArchitecture.load(args.hardware)
    circuit = add_qubits_to_quantum_circuit(
        read_benchmark_circuit("sabre", args.circuit_name), hardware)

    results = dict()
    N_values = list(range(Nstep, N + 1, Nstep))

    with ProcessPoolExecutor(max_workers=cpu_count()) as executor:
        for i, n in enumerate(N_values):
            print(f"Computing for {n}:")
            print("\tRandom...")
            best_random_results = list(
                executor.map(
                    random_tuple_strategy_results,
                    itertools.repeat([circuit, hardware, n], M),
                ))

            print("\tSABRE...")
            best_sabre_results = list(
                executor.map(
                    sabre_tuple_strategy_results,
                    itertools.repeat([circuit, hardware, n], M),
                ))

            print("\tAnnealing random...")
            best_annealing_random_results = list(
                executor.map(
                    annealing_random_tuple_strategy_results,
                    itertools.repeat([circuit, hardware, n], M),
                ))

            print("\tAnnealing hardware...")
            best_annealing_hardware_results = list(
                executor.map(
                    annealing_hardware_aware_tuple_strategy_results,
                    itertools.repeat([circuit, hardware, n], M),
                ))

            results[n] = {
                "random": {
                    "results": deepcopy(best_random_results)
                },
                "annealing_random": {
                    "results": deepcopy(best_annealing_random_results)
                },
                "sabre": {
                    "results": deepcopy(best_sabre_results)
                },
                "annealing_hardware": {
                    "results": deepcopy(best_annealing_hardware_results)
                },
            }

    print(
        f"Saving to results-{N}-{Nstep}-{M}-{args.circuit_name}-{args.hardware}.pkl"
    )
    with open(
            f"results-{N}-{Nstep}-{M}-{args.circuit_name}-{args.hardware}.pkl",
            "wb") as f:
        pickle.dump(results, f)
Ejemplo n.º 13
0
def ha_mapping(
    quantum_circuit: QuantumCircuit,
    initial_mapping: ty.Dict[Qubit, int],
    hardware: IBMQHardwareArchitecture,
    swap_cost_heuristic: ty.Callable[
        [
            IBMQHardwareArchitecture,  # Hardware information
            QuantumLayer,  # Current front layer
            ty.List[DAGNode],  # Topologically sorted list of nodes
            int,  # Index of the first non-processed gate.
            ty.Dict[Qubit, int],  # The mapping before applying the tested SWAP/Bridge
            ty.Dict[Qubit, int],  # The initial mapping
            ty.Dict[Qubit, int],  # The trans mapping
            numpy.ndarray,  # The distance matrix between each qubits
            TwoQubitGate,  # The SWAP/Bridge we want to rank
        ],
        float,
    ] = sabre_heuristic,
    get_candidates: ty.Callable[
        [QuantumLayer, IBMQHardwareArchitecture, ty.Dict[Qubit, int], ty.Dict[Qubit, int], ty.Dict[Qubit, int], ty.Set[str],],
        ty.List[TwoQubitGate],
    ] = get_all_swap_bridge_candidates,
    get_distance_matrix: ty.Callable[
        [IBMQHardwareArchitecture], numpy.ndarray
    ] = get_distance_matrix_swap_number_and_error,
) -> ty.Tuple[QuantumCircuit, ty.Dict[Qubit, int]]:
    """Map the given quantum circuit to the hardware topology provided.

    :param quantum_circuit: the quantum circuit to map.
    :param initial_mapping: the initial mapping used to start the iterative mapping
        algorithm.
    :param hardware: hardware data such as connectivity, gate time, gate errors, ...
    :param swap_cost_heuristic: the heuristic cost function that will estimate the cost
        of a given SWAP/Bridge according to the current state of the circuit.
    :param get_candidates: a function that takes as input the current front
        layer and the hardware description and that returns a list of tuples
        representing the SWAP/Bridge that should be considered by the heuristic.
    :param get_distance_matrix: a function that takes as first (and only) parameter the
        hardware representation and that outputs a numpy array containing the cost of
        performing a SWAP between each pair of qubits.
    :return: The final circuit along with the mapping obtained at the end of the
        iterative procedure.
    """
    _adapt_quantum_circuit_and_mapping_arity(quantum_circuit, initial_mapping, hardware)
    # Creating the internal data structures that will be used in this function.
    dag_circuit = circuit_to_dag(quantum_circuit)
    distance_matrix = get_distance_matrix(hardware)
    resulting_dag_quantum_circuit = _create_empty_dagcircuit_from_existing(dag_circuit)
    current_mapping = initial_mapping
    explored_mappings: ty.Set[str] = set()
    # Sorting all the quantum operations in topological order once for all.
    # May require significant memory on large circuits...
    topological_nodes: ty.List[DAGNode] = list(dag_circuit.topological_op_nodes())
    current_node_index = 0
    # Creating the initial front layer.
    front_layer = QuantumLayer()
    current_node_index = update_layer(
        front_layer, topological_nodes, current_node_index
    )
    trans_mapping = initial_mapping.copy()

    # Start of the iterative algorithm
    while not front_layer.is_empty():
        execute_gate_list = QuantumLayer()
        for op in front_layer.ops:
            if hardware.can_natively_execute_operation(op, current_mapping):
                execute_gate_list.add_operation(op)
                # Delaying the remove operation because we do not want to remove from
                # a container we are iterating on.
                # front_layer.remove_operation(op)
        if not execute_gate_list.is_empty():
            front_layer.remove_operations_from_layer(execute_gate_list)
            execute_gate_list.apply_back_to_dag_circuit(
                resulting_dag_quantum_circuit, initial_mapping, trans_mapping
            )
            # Empty the explored mappings because at least one gate has been executed.
            explored_mappings.clear()
        else:
            inverse_mapping = {val: key for key, val in initial_mapping.items()}
            # We cannot execute any gate, that means that we should insert at least
            # one SWAP/Bridge to make some gates executable.
            # First list all the SWAPs/Bridges that may help us make some gates
            # executable.
            swap_candidates = get_candidates(
                front_layer, hardware, initial_mapping, current_mapping, trans_mapping, explored_mappings
            )
            # Then rank the SWAPs/Bridge and take the best one.
            best_swap_qubits = None
            best_cost = float("inf")
            for potential_swap in swap_candidates:
                cost = swap_cost_heuristic(
                    hardware,
                    front_layer,
                    topological_nodes,
                    current_node_index,
                    current_mapping,
                    initial_mapping,
                    trans_mapping,
                    distance_matrix,
                    potential_swap,
                )
                if cost < best_cost:
                    best_cost = cost
                    best_swap_qubits = potential_swap
            # We now have our best SWAP/Bridge, let's perform it!
            current_mapping = best_swap_qubits.update_mapping(current_mapping)
            if isinstance(best_swap_qubits, SwapTwoQubitGate):
                control, target = current_mapping[best_swap_qubits.left], current_mapping[best_swap_qubits.right]
                swap_control, swap_target = inverse_mapping[control], inverse_mapping[target]
                best_swap_qubits = SwapTwoQubitGate(
                    swap_control, swap_target
                )
                #print("swap gates is :", best_swap_qubits.left, best_swap_qubits.right)
                trans_mapping[best_swap_qubits.left], trans_mapping[best_swap_qubits.right] = (
                    trans_mapping[best_swap_qubits.right],
                    trans_mapping[best_swap_qubits.left],
                )
            else:
                #print("brige gate is :", best_swap_qubits.left, best_swap_qubits.middle, best_swap_qubits.right)
                pass
            explored_mappings.add(mapping_to_str(current_mapping))
            best_swap_qubits.apply(resulting_dag_quantum_circuit, front_layer, initial_mapping, trans_mapping)
        # Anyway, update the current front_layer
        current_node_index = update_layer(
            front_layer, topological_nodes, current_node_index
        )

    # We are done here, we just need to return the results
    # resulting_dag_quantum_circuit.draw(scale=1, filename="qcirc.dot")
    resulting_circuit = dag_to_circuit(resulting_dag_quantum_circuit)
    return resulting_circuit, current_mapping
Ejemplo n.º 14
0
def ha_mapping_paper_compliant(
    quantum_circuit: QuantumCircuit,
    initial_mapping: ty.Dict[Qubit, int],
    hardware: IBMQHardwareArchitecture,
    swap_cost_and_effect_heuristic: ty.Callable[
        [
            IBMQHardwareArchitecture,  # Hardware information
            QuantumLayer,  # Current front layer
            ty.List[DAGNode],  # Topologically sorted list of nodes
            int,  # Index of the first non-processed gate.
            ty.Dict[Qubit, int],  # The mapping before applying the tested SWAP/Bridge
            ty.Dict[Qubit, int],  # The initial mapping
            ty.Dict[Qubit, int],  # The trans mapping
            numpy.ndarray,  # The distance matrix between each qubits
            TwoQubitGate,  # The SWAP/Bridge we want to rank
        ],
        ty.Tuple[
            float,  # Cost of the SWAP pair
            float,  # Effect of the SWAP pair on the other gates
        ],
    ] = sabre_heuristic_with_effect,
    get_candidates: ty.Callable[
        [QuantumLayer, IBMQHardwareArchitecture, ty.Dict[Qubit, int], ty.Set[str],],
        ty.List[SwapTwoQubitGate],
    ] = get_all_swap_candidates,
    get_distance_matrix: ty.Callable[
        [IBMQHardwareArchitecture], numpy.ndarray
    ] = get_distance_matrix_swap_number_and_error,
) -> ty.Tuple[QuantumCircuit, ty.Dict[Qubit, int]]:
    """Map the given quantum circuit to the hardware topology provided.

    This implementation uses the exact same algorithm described in the associated
    scientific paper. Another implementation using a different method to choose
    between SWAP and Bridge is available as `:py:func:`~hamap.mapping.ha_mapping`.

    :param quantum_circuit: the quantum circuit to map.
    :param initial_mapping: the initial mapping used to start the iterative mapping
        algorithm.
    :param hardware: hardware data such as connectivity, gate time, gate errors, ...
    :param swap_cost_and_effect_heuristic: the heuristic cost function that will
        estimate the cost of a given SWAP according to the current state of the circuit
        and the effect of the SWAP on the following quantum gates. The two floats
        should be returned as a tuple (cost, effect).
    :param get_candidates: a function that takes as input the current front
        layer and the hardware description and that returns a list of tuples
        representing the SWAP that should be considered by the heuristic.
    :param get_distance_matrix: a function that takes as first (and only) parameter the
        hardware representation and that outputs a numpy array containing the cost of
        performing a SWAP between each pair of qubits.
    :return: The final circuit along with the mapping obtained at the end of the
        iterative procedure.
    """
    _adapt_quantum_circuit_and_mapping_arity(quantum_circuit, initial_mapping, hardware)
    # Creating the internal data structures that will be used in this function.
    dag_circuit = circuit_to_dag(quantum_circuit)
    distance_matrix = get_distance_matrix(hardware)
    resulting_dag_quantum_circuit = _create_empty_dagcircuit_from_existing(dag_circuit)
    current_mapping = initial_mapping
    explored_mappings: ty.Set[str] = set()
    # Sorting all the quantum operations in topological order once for all.
    # May require significant memory on large circuits...
    topological_nodes: ty.List[DAGNode] = list(dag_circuit.topological_op_nodes())
    current_node_index = 0
    # Creating the initial front layer.
    front_layer = QuantumLayer()
    current_node_index = update_layer(
        front_layer, topological_nodes, current_node_index
    )

    swap_distance_matrix = get_distance_matrix_swap_number(hardware)
    trans_mapping = initial_mapping.copy()
    # Start of the iterative algorithm
    while not front_layer.is_empty():
        execute_gate_list = QuantumLayer()
        for op in front_layer.ops:
            if hardware.can_natively_execute_operation(op, current_mapping):
                execute_gate_list.add_operation(op)
                # Delaying the remove operation because we do not want to remove from
                # a container we are iterating on.
                # front_layer.remove_operation(op)
        if not execute_gate_list.is_empty():
            front_layer.remove_operations_from_layer(execute_gate_list)
            execute_gate_list.apply_back_to_dag_circuit(
                resulting_dag_quantum_circuit, initial_mapping, trans_mapping
            )
            # Empty the explored mappings because at least one gate has been executed.
            explored_mappings.clear()
        else:
            # We cannot execute any gate, that means that we should insert at least
            # one SWAP/Bridge to make some gates executable.
            # First list all the SWAPs/Bridges that may help us make some gates
            # executable.
            inverse_mapping = {val: key for key, val in initial_mapping.items()}
            swap_candidates = get_candidates(
                front_layer, hardware, current_mapping, explored_mappings
            )
            # Then rank the SWAPs/Bridge and take the best one.
            best_swap_qubits = None
            best_cost = float("inf")
            best_effect = 0.0
            for potential_swap in swap_candidates:
                cost, swap_effect = swap_cost_and_effect_heuristic(
                    hardware,
                    front_layer,
                    topological_nodes,
                    current_node_index,
                    current_mapping,
                    initial_mapping,
                    trans_mapping,
                    distance_matrix,
                    potential_swap,
                )
                if cost < best_cost:
                    best_cost = cost
                    best_effect = swap_effect
                    best_swap_qubits = potential_swap
            # We now have our best SWAP, let's check if a Bridge is not better
            # if (
            #     best_effect < 0
            #     and swap_distance_matrix[best_swap_qubits.left][best_swap_qubits.right]
            #     == 2
            # ):
            #     i, j = best_swap_qubits
            #     common_neighbours = set(hardware.neighbors(i)) & set(
            #         hardware.neighbors(j)
            #     )
            #     if len(common_neighbours) < 1:
            #         raise RuntimeError("Less than one common neighbour")
            #     common_neighbour = list(common_neighbours)[0]
            #     best_gate = BridgeTwoQubitGate(i, common_neighbour, j)
            if best_effect < 0:
                inverse_trans_mapping = {val: key for key, val in trans_mapping.items()}
                for op in front_layer.ops:
                    if len(op.qargs) < 2:
                        # We just pass 1 qubit gates because they do not participate in the
                        # Bridge operation
                        continue
                    if len(op.qargs) != 2:
                        logger.warning("A 3-qubit or more gate has been found in the circuit.")
                        continue
                    control, target = op.qargs
                    control_index = initial_mapping[inverse_trans_mapping[initial_mapping[control]]]
                    target_index = initial_mapping[inverse_trans_mapping[initial_mapping[target]]]
                    for _, potential_middle_index in hardware.out_edges(control_index):
                        for _, potential_target_index in hardware.out_edges(potential_middle_index):
                            if potential_target_index == target_index:
                                best_swap_qubits = BridgeTwoQubitGate(
                                    inverse_trans_mapping[initial_mapping[control]],
                                    inverse_mapping[potential_middle_index],
                                    inverse_trans_mapping[initial_mapping[target]],
                                )

            current_mapping = best_swap_qubits.update_mapping(current_mapping)
            if isinstance(best_swap_qubits, SwapTwoQubitGate):
                control, target = current_mapping[best_swap_qubits.left], current_mapping[best_swap_qubits.right]
                swap_control, swap_target = inverse_mapping[control], inverse_mapping[target]
                best_swap_qubits = SwapTwoQubitGate(
                    swap_control, swap_target
                )
                #print("swap gates is :", best_swap_qubits.left, best_swap_qubits.right)
                trans_mapping[best_swap_qubits.left], trans_mapping[best_swap_qubits.right] = (
                    trans_mapping[best_swap_qubits.right],
                    trans_mapping[best_swap_qubits.left],
                )
            else:
                #print("brige gate is :", best_swap_qubits.left, best_swap_qubits.middle, best_swap_qubits.right)
                pass

            explored_mappings.add(mapping_to_str(current_mapping))
            best_swap_qubits.apply(resulting_dag_quantum_circuit, front_layer, initial_mapping, trans_mapping)
        # Anyway, update the current front_layer
        current_node_index = update_layer(
            front_layer, topological_nodes, current_node_index
        )

    # We are done here, we just need to return the results
    # resulting_dag_quantum_circuit.draw(scale=1, filename="qcirc.dot")
    resulting_circuit = dag_to_circuit(resulting_dag_quantum_circuit)
    return resulting_circuit, current_mapping