diff options
Diffstat (limited to 'ero1/src/drone')
| -rw-r--r-- | ero1/src/drone/couverture_sommets.py | 59 | ||||
| -rw-r--r-- | ero1/src/drone/postier_chinoisV1.py | 209 | ||||
| -rw-r--r-- | ero1/src/drone/postier_chinoisV2.py | 258 | ||||
| -rw-r--r-- | ero1/src/drone/postier_chinoisV3.py | 253 | ||||
| -rw-r--r-- | ero1/src/drone/postier_chinois_Martial.py | 122 | ||||
| -rw-r--r-- | ero1/src/drone/postier_chinois_montreal.py | 89 |
6 files changed, 990 insertions, 0 deletions
diff --git a/ero1/src/drone/couverture_sommets.py b/ero1/src/drone/couverture_sommets.py new file mode 100644 index 0000000..80a1fe6 --- /dev/null +++ b/ero1/src/drone/couverture_sommets.py @@ -0,0 +1,59 @@ +from src.helper.debug_printer import debug_print +import parameters as params +import networkx as nx + +def vertex_cover_glouton(graph): + """ + Renvoie un ensemble de sommets couvrant toutes les arêtes du graphe. + Approche gloutonne. + """ + G = graph.copy() + cover = set() + + while G.number_of_edges() > 0: + u = max(G.degree, key=lambda x: x[1])[0] # Max degree + cover.add(u) + G.remove_node(u) + + return cover + +def get_snowed_edges(graph, cover_nodes, snow_min, snow_max): + """ + Retourne les arêtes enneigées couvertes par les sommets sélectionnés. + """ + snowy_edges = [] + no_snowy_edges = [] + + for u, v, key in graph.edges(keys=True): # toutes les arretes du graphe + if u in cover_nodes or v in cover_nodes: + data = graph.get_edge_data(u, v, key) + if 'snow' in data and snow_min <= data['snow'] <= snow_max: + snowy_edges.append((u, v, key)) + else: + no_snowy_edges.append((u, v, key)) + + return (snowy_edges, no_snowy_edges) + +def vertex_cover(graph, debug_mode=False): + """ + Stratégie : + Trouver un ensemble minimum de sommets pour couvrir toutes les arêtes. + """ + debug_print(f"Passage du graphe en mode : undirected", debug_mode) + graph = nx.to_undirected(graph) + + # debug_print(f"List Sommets total : {graph.nodes}", debug_mode) + # debug_print(f"List Routes total : {graph.edges}", debug_mode) + debug_print(f"Nombre de sommets total : {len(graph.nodes)}", debug_mode) + debug_print(f"Nombre de routes total : {len(graph.edges)}", debug_mode) + + debug_print("Calcul de la couverture par sommet ...", debug_mode) + nodes = vertex_cover_glouton(graph) + debug_print(f"Nombre de sommets sélectionnés : {len(nodes)}", debug_mode) + + snowy_edges = get_snowed_edges(graph, + nodes, + params.SNOW_THRESHOLD, + params.SNOW_MAX) + + return snowy_edges diff --git a/ero1/src/drone/postier_chinoisV1.py b/ero1/src/drone/postier_chinoisV1.py new file mode 100644 index 0000000..6afe1a0 --- /dev/null +++ b/ero1/src/drone/postier_chinoisV1.py @@ -0,0 +1,209 @@ +import osmnx as ox +import networkx as nx +import parameters as params + +from src.helper.debug_printer import debug_print +from src.helper.display_graph import display_graph +from src.helper.duplicate_removal import remove_duplicates +from src.helper.export_import_yaml import save_paths_to_yaml + +arrondissements = [ + 'Ahuntsic-Cartierville', + 'Anjou', + 'Côte-des-Neiges–Notre-Dame-de-Grâce', + 'Lachine', + 'LaSalle', + 'Le Plateau-Mont-Royal', + 'Le Sud-Ouest', + 'L\'Île-Bizard–Sainte-Geneviève', + 'Mercier–Hochelaga-Maisonneuve', + 'Montréal-Nord', + 'Outremont', + 'Pierrefonds-Roxboro', + 'Rivière-des-Prairies–Pointe-aux-Trembles', + 'Rosemont–La Petite-Patrie', + 'Saint-Laurent', + 'Saint-Léonard', + 'Verdun', + 'Ville-Marie', + 'Villeray–Saint-Michel–Parc-Extension' +] # first list, we changed its order manually to make a "smart path" for the drone + +connection_order = [ + 'Rivière-des-Prairies–Pointe-aux-Trembles', + 'Montréal-Nord', + 'Saint-Léonard', + 'Anjou', + 'Mercier–Hochelaga-Maisonneuve', + 'Rosemont–La Petite-Patrie', + 'Villeray–Saint-Michel–Parc-Extension', + 'Outremont', + 'Le Sud-Ouest', + 'Ville-Marie', + 'L\'Île-Bizard–Sainte-Geneviève', + 'Verdun', + 'LaSalle', + 'Côte-des-Neiges–Notre-Dame-de-Grâce', + 'Le Plateau-Mont-Royal', + 'Saint-Laurent', + 'Ahuntsic-Cartierville', + 'Pierrefonds-Roxboro', + 'Lachine' +] + +def find_circuit(G_undirected, debug_mode): + if nx.is_eulerian(G_undirected): + G_eulerian = G_undirected + else: + G_eulerian = nx.eulerize(G_undirected) + return nx.eulerian_circuit(G_eulerian), G_eulerian + +def generate_graph(name, debug_mode=False): + """ + permet de charger un graphe + d'une localisation a partir du nom de cette derniere. + """ + G = ox.graph_from_place(name, network_type='drive') + G = remove_duplicates(G, False) + G = ox.project_graph(G) + return G + +def total_length_of_circuit(G, circuit): + return sum(G[u][v][0]['length'] for u, v in circuit) / 1000 + +def total_length_of_arrondissement(G): + distance = 0 + for u, v, data in G.edges(data=True): + distance += data["length"] + return distance / 1000 + +def process_graphs(name, debug_mode): + """ + Création des paths + """ + paths = {} + for i in arrondissements: + sub_name = i + ", Montréal, Québec, Canada" + debug_print(f"Génération : {sub_name}", debug_mode) + sub_graph = generate_graph(sub_name, debug_mode) + G_undirected = sub_graph.to_undirected() + + circuit, _ = find_circuit(G_undirected, debug_mode) + c = [edge for edge in circuit] + # print(f"distance kilometre du quartier = {total_length_of_arrondissement(G_undirected):.2f}km") + # print(f"distance kilometre du drone = {total_length_of_circuit(G_eulerian, circuit):.2f}km") + # debug_print(f"Nombre de Route du Path : {len(c)}", debug_mode) + + paths[i] = c + + del sub_graph # to save as much memory as possible + del circuit + return paths + +def connect_circuits(G, path_dict, arrondissement_order=connection_order): + """ + La fonction trouve le chemin le plus cours entre 2 arrondissements + """ + connections = [] + for i in range(len(arrondissement_order) - 1): + from_name = arrondissement_order[i] + to_name = arrondissement_order[i + 1] + + from_path = path_dict[from_name] + to_path = path_dict[to_name] + + start_node = from_path[0][0] + end_node = from_path[-1][1] + + next_start_node = to_path[0][0] + + try: + path = nx.shortest_path(G, source=end_node, target=next_start_node, weight='length') + connections.append((from_name, to_name, path)) + except nx.NetworkXNoPath: + print(f"Aucun chemin trouvé entre {from_name} et {to_name}") + connections.append((from_name, to_name, None)) + return connections + +def final_path(graph, paths, connection_order=connection_order): + full_path = [] + + connections = connect_circuits(graph, paths) + + for i, arr_name in enumerate(connection_order): + full_path.extend(paths[arr_name]) + + if i < len(connection_order) - 1: + next_arr = connection_order[i+1] + + for conn in connections: + if conn[0] == arr_name and conn[1] == next_arr: + node_path = conn[2] + for j in range(len(node_path) - 1): + full_path.append((node_path[j], node_path[j+1])) + break + + return full_path + +def distance_total(G, paths, connections): + G_undir = G.to_undirected() + total_distance = 0.0 + used_edges = set() + + for arrondissement, edges in paths.items(): + for u, v in edges: + key = (min(u, v), max(u, v)) + used_edges.add(key) + total_distance += G_undir[u][v][0]['length'] + + for from_arr, to_arr, node_path in connections: + for i in range(len(node_path) - 1): + u = node_path[i] + v = node_path[i+1] + key = (min(u, v), max(u, v)) + used_edges.add(key) + total_distance += G_undir[u][v][0]['length'] + + unused_distance = 0.0 + for u, v, data in G_undir.edges(data=True): + key = (min(u, v), max(u, v)) + if key not in used_edges: + unused_distance += data['length'] + + total_distance /= 1000 + unused_distance /= 1000 + return total_distance, unused_distance + +def distance_optimal(G): + """ + Calcule la somme des longueurs de toutes les routes dans les arrondissements + """ + covered_edges = set() + for i in arrondissements: + sub_name = i + ", Montréal, Québec, Canada" + sub_graph = generate_graph(sub_name) + for u, v, data in sub_graph.edges(data=True): + covered_edges.add((min(u, v), max(u, v))) + del sub_graph + + G_undir = G.to_undirected() + total = 0.0 + for u, v in covered_edges: + total += G_undir[u][v][0]['length'] + + + return total / 1000 + +def postier_chinois_process_v1(G, debug_mode): + name = "Montréal, Québec, Canada" + # initially, we ran find circuit on the whole graph + # => processes all suburbs instead of the whole graph + paths = process_graphs(name, debug_mode) + # it was our closest attempt to get an optimal answer but way to long (ran for more than 12h and did not finish) + + finalPath = final_path(G, paths) + + # Save Path with YML + save_paths_to_yaml(paths, "paths-PostierChinoisV1.yml") + + return paths, finalPath diff --git a/ero1/src/drone/postier_chinoisV2.py b/ero1/src/drone/postier_chinoisV2.py new file mode 100644 index 0000000..01f8643 --- /dev/null +++ b/ero1/src/drone/postier_chinoisV2.py @@ -0,0 +1,258 @@ +import osmnx as ox +import networkx as nx +import parameters as params + +from src.helper.debug_printer import debug_print +from src.helper.display_graph import display_graph +from src.helper.duplicate_removal import remove_duplicates +from src.helper.export_import_yaml import save_paths_to_yaml + +arrondissements = [ + 'Ahuntsic-Cartierville', + 'Anjou', + 'Côte-des-Neiges–Notre-Dame-de-Grâce', + 'Lachine', + 'LaSalle', + 'Le Plateau-Mont-Royal', + 'Le Sud-Ouest', + 'L\'Île-Bizard–Sainte-Geneviève', + 'Mercier–Hochelaga-Maisonneuve', + 'Montréal-Nord', + 'Outremont', + 'Pierrefonds-Roxboro', + 'Rivière-des-Prairies–Pointe-aux-Trembles', + 'Rosemont–La Petite-Patrie', + 'Saint-Laurent', + 'Saint-Léonard', + 'Verdun', + 'Ville-Marie', + 'Villeray–Saint-Michel–Parc-Extension' +] # first list, we changed its order manually to make a "smart path" for the drone + +connection_order = [ + 'Rivière-des-Prairies–Pointe-aux-Trembles', + 'Montréal-Nord', + 'Saint-Léonard', + 'Anjou', + 'Mercier–Hochelaga-Maisonneuve', + 'Rosemont–La Petite-Patrie', + 'Villeray–Saint-Michel–Parc-Extension', + 'Outremont', + 'Le Sud-Ouest', + 'Ville-Marie', + 'L\'Île-Bizard–Sainte-Geneviève', + 'Verdun', + 'LaSalle', + 'Côte-des-Neiges–Notre-Dame-de-Grâce', + 'Le Plateau-Mont-Royal', + 'Saint-Laurent', + 'Ahuntsic-Cartierville', + 'Pierrefonds-Roxboro', + 'Lachine' +] + +def find_pairs_odd(G, odd_nodes): + """ + L'objectif de cette fonction est de trouver des paires de nœuds de degré impair dans un graphe. + Cela, en les reliant via les plus courst chemins (avec "length" <=> distance réel). + """ + pairs = [] + used = set() + distances = {} + paths = {} + for u in odd_nodes: + dists, path = nx.single_source_dijkstra(G, u, weight='length') + distances[u] = dists + paths[u] = path + + for u in odd_nodes: + if u in used: + continue + min_dist = float('inf') + best_v = None + for v in odd_nodes: + if v == u or v in used: + continue + if distances[u][v] < min_dist: + min_dist = distances[u][v] + best_v = v + if best_v is not None: + used.add(u) + used.add(best_v) + pairs.append((u, best_v, paths[u][best_v])) + return pairs + +def eulerization_maison(G): + """ + L'objectif de cette fonction est de transformer un graphe non eulérien en un graphe eulérien. + La fonction rajoute des arretes dans le graphe. Les arretes sont ajouté entre les noeuds de degrée impaires. + Avec l'utilisation de find_pairs_odd, on trouve le chemin le plus cours (en distance réel) entre 2 sommets. + """ + G_update = G.copy() + odd_nodes = [v for (v, d) in G.degree() if d % 2 == 1] + pairs = find_pairs_odd(G, odd_nodes) + for u, v, path in pairs: + for i in range(len(path) - 1): + edge_data = G.get_edge_data(path[i], path[i+1]) + l = edge_data.get('length') + G_update.add_edge(path[i], path[i+1], length=l) + return G_update + +def find_circuit(G_undirected, debug_mode): + # debug_print(f"Avant eulérisation : {len(G_undirected.edges)} arêtes", debug_mode) + if nx.is_eulerian(G_undirected): + G_eulerian = G_undirected + else: + G_eulerian = eulerization_maison(G_undirected) + # debug_print(f"Apres eulérisation : {len(G_eulerian.edges)} arêtes", debug_mode) + return list(nx.eulerian_circuit(G_eulerian)), G_eulerian + +def generate_graph(name, debug_mode=False): + """ + permet de charger un graphe + d'une localisation a partir du nom de cette derniere. + """ + G = ox.graph_from_place(name, network_type='drive') + G = remove_duplicates(G, False) + G = ox.project_graph(G) + return G + +def total_length_of_circuit(G, circuit): + return sum(G[u][v][0]['length'] for u, v in circuit) / 1000 + +def total_length_of_arrondissement(G): + distance = 0 + for u, v, data in G.edges(data=True): + distance += data["length"] + return distance / 1000 + +def process_graphs(name, debug_mode): + """ + Création des paths + """ + paths = {} + for i in arrondissements: + sub_name = i + ", Montréal, Québec, Canada" + debug_print(f"Génération : {sub_name}", debug_mode) + sub_graph = generate_graph(sub_name, debug_mode) + G_undirected = sub_graph.to_undirected() + + circuit, _ = find_circuit(G_undirected, debug_mode) + c = [edge for edge in circuit] + # print(f"distance kilometre du quartier = {total_length_of_arrondissement(G_undirected):.2f}km") + # print(f"distance kilometre du drone = {total_length_of_circuit(G_eulerian, circuit):.2f}km") + # debug_print(f"Nombre de Route du Path : {len(c)}", debug_mode) + + paths[i] = c + + del sub_graph # to save as much memory as possible + del circuit + return paths + +def connect_circuits(G, path_dict, arrondissement_order=connection_order): + """ + La fonction trouve le chemin le plus cours entre 2 arrondissements + """ + connections = [] + for i in range(len(arrondissement_order) - 1): + from_name = arrondissement_order[i] + to_name = arrondissement_order[i + 1] + + from_path = path_dict[from_name] + to_path = path_dict[to_name] + + start_node = from_path[0][0] + end_node = from_path[-1][1] + + next_start_node = to_path[0][0] + + try: + path = nx.shortest_path(G, source=end_node, target=next_start_node, weight='length') + connections.append((from_name, to_name, path)) + except nx.NetworkXNoPath: + print(f"Aucun chemin trouvé entre {from_name} et {to_name}") + connections.append((from_name, to_name, None)) + return connections + +def final_path(graph, paths, connection_order=connection_order): + full_path = [] + + connections = connect_circuits(graph, paths) + + for i, arr_name in enumerate(connection_order): + full_path.extend(paths[arr_name]) + + if i < len(connection_order) - 1: + next_arr = connection_order[i+1] + + for conn in connections: + if conn[0] == arr_name and conn[1] == next_arr: + node_path = conn[2] + for j in range(len(node_path) - 1): + full_path.append((node_path[j], node_path[j+1])) + break + + return full_path + +def distance_total(G, paths, connections): + G_undir = G.to_undirected() + total_distance = 0.0 + used_edges = set() + + for arrondissement, edges in paths.items(): + for u, v in edges: + key = (min(u, v), max(u, v)) + used_edges.add(key) + total_distance += G_undir[u][v][0]['length'] + + for from_arr, to_arr, node_path in connections: + for i in range(len(node_path) - 1): + u = node_path[i] + v = node_path[i+1] + key = (min(u, v), max(u, v)) + used_edges.add(key) + total_distance += G_undir[u][v][0]['length'] + + unused_distance = 0.0 + for u, v, data in G_undir.edges(data=True): + key = (min(u, v), max(u, v)) + if key not in used_edges: + unused_distance += data['length'] + + total_distance /= 1000 + unused_distance /= 1000 + return total_distance, unused_distance + +def distance_optimal(G): + """ + Calcule la somme des longueurs de toutes les routes dans les arrondissements + """ + covered_edges = set() + for i in arrondissements: + sub_name = i + ", Montréal, Québec, Canada" + sub_graph = generate_graph(sub_name) + for u, v, data in sub_graph.edges(data=True): + covered_edges.add((min(u, v), max(u, v))) + del sub_graph + + G_undir = G.to_undirected() + total = 0.0 + for u, v in covered_edges: + total += G_undir[u][v][0]['length'] + + + return total / 1000 + +def postier_chinois_process_v2(G, debug_mode): + name = "Montréal, Québec, Canada" + # initially, we ran find circuit on the whole graph + # => processes all suburbs instead of the whole graph + paths = process_graphs(name, debug_mode) + # it was our closest attempt to get an optimal answer but way to long (ran for more than 12h and did not finish) + + finalPath = final_path(G, paths) + + # Save Path with YML + save_paths_to_yaml(paths, "paths-PostierChinoisV2.yml") + + return paths, finalPath diff --git a/ero1/src/drone/postier_chinoisV3.py b/ero1/src/drone/postier_chinoisV3.py new file mode 100644 index 0000000..fc74a26 --- /dev/null +++ b/ero1/src/drone/postier_chinoisV3.py @@ -0,0 +1,253 @@ +import osmnx as ox +import networkx as nx +import parameters as params +import yaml +import itertools + +from src.helper.debug_printer import debug_print +from src.helper.display_graph import display_graph +from src.helper.duplicate_removal import remove_duplicates +from src.helper.export_import_yaml import save_paths_to_yaml +from itertools import combinations + +arrondissements = [ + 'Ahuntsic-Cartierville', + 'Anjou', + 'Côte-des-Neiges–Notre-Dame-de-Grâce', + 'Lachine', + 'LaSalle', + 'Le Plateau-Mont-Royal', + 'Le Sud-Ouest', + 'L\'Île-Bizard–Sainte-Geneviève', + 'Mercier–Hochelaga-Maisonneuve', + 'Montréal-Nord', + 'Outremont', + 'Pierrefonds-Roxboro', + 'Rivière-des-Prairies–Pointe-aux-Trembles', + 'Rosemont–La Petite-Patrie', + 'Saint-Laurent', + 'Saint-Léonard', + 'Verdun', + 'Ville-Marie', + 'Villeray–Saint-Michel–Parc-Extension' +] # first list, we changed its order manually to make a "smart path" for the drone + +connection_order = [ + 'Rivière-des-Prairies–Pointe-aux-Trembles', + 'Montréal-Nord', + 'Saint-Léonard', + 'Anjou', + 'Mercier–Hochelaga-Maisonneuve', + 'Rosemont–La Petite-Patrie', + 'Villeray–Saint-Michel–Parc-Extension', + 'Outremont', + 'Le Sud-Ouest', + 'Ville-Marie', + 'L\'Île-Bizard–Sainte-Geneviève', + 'Verdun', + 'LaSalle', + 'Côte-des-Neiges–Notre-Dame-de-Grâce', + 'Le Plateau-Mont-Royal', + 'Saint-Laurent', + 'Ahuntsic-Cartierville', + 'Pierrefonds-Roxboro', + 'Lachine' +] + +def eulerization(G): + """ + Eulérise le graphe G (non orienté) en dupliquant des arêtes existantes. + Retourne un MultiGraph eulérien. + """ + + if not nx.is_connected(G): + return None + + odd_nodes = [v for v in G.nodes if G.degree(v) % 2 == 1] + + all_pairs = {} + for u in odd_nodes: + dist, path = nx.single_source_dijkstra(G, u, weight="length") + all_pairs[u] = (dist, path) + + H = nx.Graph() + for u, v in itertools.combinations(odd_nodes, 2): + if v in all_pairs[u][0]: + length = all_pairs[u][0][v] + H.add_edge(u, v, length=length) + else: + return None + + pairs = sorted(H.edges(data=True), key=lambda e: e[2]['length']) + matched = set() + min_matching = [] + for u, v, d in pairs: + if u not in matched and v not in matched: + matched.update([u, v]) + min_matching.append((u, v)) + + MG = nx.MultiGraph(G) + for u, v in min_matching: + path = all_pairs[u][1][v] + for i in range(len(path) - 1): + MG.add_edge(path[i], path[i+1]) + + return MG + +def find_circuit(G_undirected, debug_mode): + # debug_print(f"Avant eulérisation : {len(G_undirected.edges)} arêtes", debug_mode) + if nx.is_eulerian(G_undirected): + G_eulerian = G_undirected + else: + G_eulerian = eulerization(G_undirected) + # debug_print(f"Apres eulérisation : {len(G_eulerian.edges)} arêtes", debug_mode) + return list(nx.eulerian_circuit(G_eulerian)), G_eulerian + +def generate_graph(name, debug_mode=False): + """ + permet de charger un graphe + d'une localisation a partir du nom de cette derniere. + """ + G = ox.graph_from_place(name, network_type='drive') + G = remove_duplicates(G, False) + G = ox.project_graph(G) + return G + +def total_length_of_circuit(G, circuit): + return sum(G[u][v][0]['length'] for u, v in circuit) / 1000 + +def total_length_of_arrondissement(G): + distance = 0 + for u, v, data in G.edges(data=True): + distance += data["length"] + return distance / 1000 + +def process_graphs(name, debug_mode): + """ + Création des paths + """ + paths = {} + for i in arrondissements: + sub_name = i + ", Montréal, Québec, Canada" + debug_print(f"Génération : {sub_name}", debug_mode) + sub_graph = generate_graph(sub_name, debug_mode) + G_undirected = sub_graph.to_undirected() + + circuit, G_eulerian = find_circuit(G_undirected, debug_mode) + c = [edge for edge in circuit] + + # print(f"distance kilometre du quartier = {total_length_of_arrondissement(G_undirected):.2f}km") + # print(f"distance kilometre du drone = {total_length_of_circuit(G_eulerian, circuit):.2f}km") + + paths[i] = c + + del sub_graph # to save as much memory as possible + del circuit + return paths + +def connect_circuits(G, path_dict, arrondissement_order=connection_order): + """ + La fonction trouve le chemin le plus cours entre 2 arrondissements + """ + connections = [] + for i in range(len(arrondissement_order) - 1): + from_name = arrondissement_order[i] + to_name = arrondissement_order[i + 1] + + from_path = path_dict[from_name] + to_path = path_dict[to_name] + + start_node = from_path[0][0] + end_node = from_path[-1][1] + + next_start_node = to_path[0][0] + + try: + path = nx.shortest_path(G, source=end_node, target=next_start_node, weight='length') + connections.append((from_name, to_name, path)) + except nx.NetworkXNoPath: + print(f"Aucun chemin trouvé entre {from_name} et {to_name}") + connections.append((from_name, to_name, None)) + return connections + +def final_path(graph, paths, connection_order=connection_order): + full_path = [] + + connections = connect_circuits(graph, paths) + + for i, arr_name in enumerate(connection_order): + full_path.extend(paths[arr_name]) + + if i < len(connection_order) - 1: + next_arr = connection_order[i+1] + + for conn in connections: + if conn[0] == arr_name and conn[1] == next_arr: + node_path = conn[2] + for j in range(len(node_path) - 1): + full_path.append((node_path[j], node_path[j+1])) + break + + return full_path + +def distance_total(G, paths, connections): + G_undir = G.to_undirected() + total_distance = 0.0 + used_edges = set() + + for arrondissement, edges in paths.items(): + for u, v in edges: + key = (min(u, v), max(u, v)) + used_edges.add(key) + total_distance += G_undir[u][v][0]['length'] + + for from_arr, to_arr, node_path in connections: + for i in range(len(node_path) - 1): + u = node_path[i] + v = node_path[i+1] + key = (min(u, v), max(u, v)) + used_edges.add(key) + total_distance += G_undir[u][v][0]['length'] + + unused_distance = 0.0 + for u, v, data in G_undir.edges(data=True): + key = (min(u, v), max(u, v)) + if key not in used_edges: + unused_distance += data['length'] + + total_distance /= 1000 + unused_distance /= 1000 + return total_distance, unused_distance + +def distance_optimal(G): + """ + Calcule la somme des longueurs de toutes les routes dans les arrondissements + """ + covered_edges = set() + for i in arrondissements: + sub_name = i + ", Montréal, Québec, Canada" + sub_graph = generate_graph(sub_name) + for u, v, data in sub_graph.edges(data=True): + covered_edges.add((min(u, v), max(u, v))) + del sub_graph + + G_undir = G.to_undirected() + total = 0.0 + for u, v in covered_edges: + total += G_undir[u][v][0]['length'] + + + return total / 1000 + +def postier_chinois_process_v3(G, debug_mode): + name = "Montréal, Québec, Canada" + # initially, we ran find circuit on the whole graph + # => processes all suburbs instead of the whole graph + paths = process_graphs(name, debug_mode) + # it was our closest attempt to get an optimal answer but way to long (ran for more than 12h and did not finish) + finalPath = final_path(G, paths) + + # Save Path with YML + save_paths_to_yaml(paths, "paths-PostierChinoisV3.yml") + + return paths, finalPath diff --git a/ero1/src/drone/postier_chinois_Martial.py b/ero1/src/drone/postier_chinois_Martial.py new file mode 100644 index 0000000..a0afb14 --- /dev/null +++ b/ero1/src/drone/postier_chinois_Martial.py @@ -0,0 +1,122 @@ +import parameters as params +import itertools +from src.helper.debug_printer import debug_print +import networkx as nx +import pandas as pd + +def postier_chinois(G, debug_mode=False): + """ + Parcourt le graphe en résolvant le problème du postier chinois + Source: https://brooksandrew.github.io/simpleblog/articles/intro-to-graph-optimization-solving-cpp/#solving-the-chinese-postman-problem + Parameters: + G: Le graphe des routes + debug_mode: Logs de debug + Returns: + La liste des arêtes (routes) avec entre 2.5 et 15 cm de neige + """ + gu = nx.to_undirected(G) + nodes_odd_degree = [v for v, d in gu.degree() if d % 2 == 1] + debug_print('Number of nodes of odd degree: {}'.format(len(nodes_odd_degree)), debug_mode) + debug_print('Number of total nodes: {}'.format(len(gu.nodes())), debug_mode) + + odd_node_pairs = list(itertools.combinations(nodes_odd_degree, 2)) + debug_print('Number of odd degree node pairs: {}'.format(len(odd_node_pairs)), debug_mode) + + def get_shortest_paths_distances(graph, pairs, edge_weight_name): + """ + Compute shortest distance between each pair of nodes in a graph. Return a dictionary keyed on node pairs (tuples). + """ + distances = {} + for pair in pairs: + distances[pair] = nx.dijkstra_path_length(graph, pair[0], pair[1], weight=edge_weight_name) + return distances + odd_node_pairs_shortest_paths = get_shortest_paths_distances(gu, odd_node_pairs, 'length') + + def create_complete_graph(pair_weights, flip_weights=True): + """ + Create a completely connected graph using a list of vertex pairs and the shortest path distances between them + Parameters: + pair_weights: list[tuple] from the output of get_shortest_paths_distances + flip_weights: Boolean. Should we negate the edge attribute in pair_weights? + """ + g = nx.Graph() + for k, v in pair_weights.items(): + wt_i = - v if flip_weights else v + # g.add_edge(k[0], k[1], {'distance': v, 'weight': wt_i}) # deprecated after NX 1.11 + g.add_edge(k[0], k[1], **{'distance': v, 'weight': wt_i}) + return g + g_odd_complete = create_complete_graph(odd_node_pairs_shortest_paths, flip_weights=True) + debug_print('Number of nodes in complete graph of odd nodes: {}'.format(len(g_odd_complete.nodes())), debug_mode) + debug_print('Number of edges in complete graph of odd nodes: {}'.format(len(g_odd_complete.edges())), debug_mode) + + odd_matching_dupes = nx.algorithms.max_weight_matching(g_odd_complete, True) + odd_matching = list(pd.unique([tuple(sorted([k, v])) for k, v in odd_matching_dupes])) + debug_print('Number of edges in max weight matching: {}'.format(len(odd_matching)), debug_mode) + + def add_augmenting_path_to_graph(graph, min_weight_pairs): + """ + Add the min weight matching edges to the original graph + Parameters: + graph: NetworkX graph (original graph from trailmap) + min_weight_pairs: list[tuples] of node pairs from min weight matching + Returns: + augmented NetworkX graph + """ + + # We need to make the augmented graph a MultiGraph so we can add parallel edges + graph_aug = nx.MultiGraph(graph.copy()) + for pair in min_weight_pairs: + graph_aug.add_edge(pair[0], + pair[1], + **{'length': nx.dijkstra_path_length(graph, pair[0], pair[1]), 'name': 'augmented'} + # attr_dict={'distance': nx.dijkstra_path_length(graph, pair[0], pair[1]), + # 'trail': 'augmented'} # deprecated after 1.11 + ) + return graph_aug + g_aug = add_augmenting_path_to_graph(gu, odd_matching) + + # Counts + debug_print('Number of edges in original graph: {}'.format(len(gu.edges())), debug_mode) + debug_print('Number of edges in augmented graph: {}'.format(len(g_aug.edges())), debug_mode) + + def create_eulerian_circuit(graph_augmented, graph_original): + """ + Create the eulerian path using only edges from the original graph. + """ + debug_print("Creating eulerian circuit", debug_mode) + euler_circuit = [] + naive_circuit = list(nx.eulerian_circuit(graph_augmented)) + dbg = False + + for edge in naive_circuit: + edge_data = graph_augmented.get_edge_data(edge[0], edge[1]) + + if len(edge_data) == 1 and 'name' in edge_data[0] and edge_data[0]['name'] != 'augmented': + # If `edge` exists in original graph, grab the edge attributes and add to eulerian circuit. + edge_att = graph_original[edge[0]][edge[1]] + euler_circuit.append((edge[0], edge[1], edge_att)) + else: + aug_path = nx.shortest_path(graph_original, edge[0], edge[1], weight='length') + aug_path_pairs = list(zip(aug_path[:-1], aug_path[1:])) + + debug_print('Filling in edges for augmented edge: {}'.format(edge), dbg) + debug_print('Augmenting path: {}'.format(' => '.join(str(aug_path))), dbg) + debug_print('Augmenting path pairs: {}\n'.format(aug_path_pairs), dbg) + + # If `edge` does not exist in original graph, find the shortest path between its nodes and + # add the edge attributes for each link in the shortest path. + for edge_aug in aug_path_pairs: + edge_aug_att = graph_original[edge_aug[0]][edge_aug[1]] + euler_circuit.append((edge_aug[0], edge_aug[1], edge_aug_att)) + + return euler_circuit + euler_circuit = create_eulerian_circuit(g_aug, gu) + debug_print('Length of Eulerian circuit: {}'.format(len(euler_circuit)), debug_mode) + + debug_print("Extraction des la météo des neiges depuis le parcours eulérien...", debug_mode) + + routes_enneigées = [] + for edge in euler_circuit: + if params.SNOW_THRESHOLD <= edge[2][0]['snow'] <= params.SNOW_MAX: + routes_enneigées.append(edge) + return routes_enneigées
\ No newline at end of file diff --git a/ero1/src/drone/postier_chinois_montreal.py b/ero1/src/drone/postier_chinois_montreal.py new file mode 100644 index 0000000..04ade41 --- /dev/null +++ b/ero1/src/drone/postier_chinois_montreal.py @@ -0,0 +1,89 @@ +import osmnx as ox +import networkx as nx +import parameters as params + +from src.helper.debug_printer import debug_print +from src.helper.display_graph import display_graph +from src.helper.duplicate_removal import remove_duplicates +from src.helper.export_import_yaml import save_paths_to_yaml + +arrondissements = [ + 'Ahuntsic-Cartierville', + 'Anjou', + 'Côte-des-Neiges–Notre-Dame-de-Grâce', + 'Lachine', + 'LaSalle', + 'Le Plateau-Mont-Royal', + 'Le Sud-Ouest', + 'L\'Île-Bizard–Sainte-Geneviève', + 'Mercier–Hochelaga-Maisonneuve', + 'Montréal-Nord', + 'Outremont', + 'Pierrefonds-Roxboro', + 'Rivière-des-Prairies–Pointe-aux-Trembles', + 'Rosemont–La Petite-Patrie', + 'Saint-Laurent', + 'Saint-Léonard', + 'Verdun', + 'Ville-Marie', + 'Villeray–Saint-Michel–Parc-Extension' +] # first list, we changed its order manually to make a "smart path" for the drone + +connection_order = [ + 'Rivière-des-Prairies–Pointe-aux-Trembles', + 'Montréal-Nord', + 'Saint-Léonard', + 'Anjou', + 'Mercier–Hochelaga-Maisonneuve', + 'Rosemont–La Petite-Patrie', + 'Villeray–Saint-Michel–Parc-Extension', + 'Outremont', + 'Le Sud-Ouest', + 'Ville-Marie', + 'L\'Île-Bizard–Sainte-Geneviève', + 'Verdun', + 'LaSalle', + 'Côte-des-Neiges–Notre-Dame-de-Grâce', + 'Le Plateau-Mont-Royal', + 'Saint-Laurent', + 'Ahuntsic-Cartierville', + 'Pierrefonds-Roxboro', + 'Lachine' +] + +def find_circuit(G_undirected, debug_mode): + if nx.is_eulerian(G_undirected): + G_eulerian = G_undirected + else: + G_eulerian = nx.eulerize(G_undirected) + return nx.eulerian_circuit(G_eulerian), G_eulerian + +def generate_graph(name, debug_mode=False): + """ + permet de charger un graphe + d'une localisation a partir du nom de cette derniere. + """ + G = ox.graph_from_place(name, network_type='drive') + G = remove_duplicates(G, False) + G = ox.project_graph(G) + return G + +def process_graphs(Graphe_montreal, debug_mode): + """ + Création des paths + """ + paths = {} + debug_print(f"Génération : Montréal", debug_mode) + G_undirected = Graphe_montreal.to_undirected() + circuit, _ = find_circuit(G_undirected, debug_mode) + c = [edge for edge in circuit] + paths["Montréal"] = c + return paths + +def postier_chinois_process_montreal(Graphe_montreal, debug_mode): + paths = process_graphs(Graphe_montreal, debug_mode) + + # Save Path with YML + save_paths_to_yaml(paths, "paths-PostierChinoisMontreal.yml") + + return paths, finalPath |
