Commit fed3268a authored by cyts's avatar cyts
Browse files

Rewrote Nested Edge Turn and Delta Change as recursive algorithms

parent 7138163a
...@@ -67,19 +67,15 @@ def main(): ...@@ -67,19 +67,15 @@ def main():
results = gpd.GeoDataFrame(sink) results = gpd.GeoDataFrame(sink)
#List of algorithms to test #List of algorithms to test
# algos = [local_search,vns,tabu,delta_change, algos = [local_search,vns,tabu,delta_change,
# edge_turn,high_valency_shuffle_edge_turn] edge_turn,high_valency_shuffle_edge_turn]
# algo_strings = ["local", algo_strings = ["local",
# "vns", "vns",
# "tabu", "tabu",
# "delta", "delta",
# "edge", "edge",
# "shuffle_edge"] "shuffle_edge"]
algos = [high_valency_shuffle_local_search]
algo_strings = ["shuffle_local"]
#Run optimization heuristics #Run optimization heuristics
...@@ -95,6 +91,6 @@ def main(): ...@@ -95,6 +91,6 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
results = main() results = main()
results.to_pickle(Path('../results_paper/large_graph/dataframe_cluster6_local.pkl')) results.to_pickle(Path('../results_paper/large_graph/dataframe_cluster6.pkl'))
\ No newline at end of file
...@@ -16,17 +16,11 @@ from shapely import wkt ...@@ -16,17 +16,11 @@ from shapely import wkt
filepath = "../results_paper/large_graph/dataframe_cluster6.pkl" filepath = "../results_paper/large_graph/dataframe_cluster6.pkl"
#filepath2 = "../results_paper/grid_search/grid_dataframe_cluster6_new.pkl"
filepath3 = '../results_paper/large_graph/dataframe_cluster6_local.pkl'
all_dfs = pd.read_pickle(Path(filepath).resolve()) all_dfs = pd.read_pickle(Path(filepath).resolve())
#all_dfs2 = pd.read_pickle(Path(filepath2).resolve())
all_dfs3 = pd.read_pickle(Path(filepath3).resolve())
mst = graph_with_calc_edge_capacity(nx.minimum_spanning_tree(create_compl_graph_from_other(all_dfs.graphs_vns.iloc[0]))) mst = graph_with_calc_edge_capacity(nx.minimum_spanning_tree(create_compl_graph_from_other(all_dfs.graphs_vns.iloc[0])))
# all_dfs=all_dfs.drop(["distance_to_com",'geometry'],axis = 1)
# all_dfs.to_pickle(r"C:\Users\yeates\Documents\Git\network-optimization\Results\large_graph\dataframe_cluster6.pkl")
func = lambda x: x**0.6 func = lambda x: x**0.6
...@@ -47,7 +41,7 @@ max_edge_cap = 0 ...@@ -47,7 +41,7 @@ max_edge_cap = 0
for s_orig in [all_dfs.graphs_local.values[0], all_dfs.graphs_delta.values[0], for s_orig in [all_dfs.graphs_local.values[0], all_dfs.graphs_delta.values[0],
all_dfs.graphs_edge.values[0], all_dfs.graphs_shuffle_edge.values[0], all_dfs.graphs_edge.values[0], all_dfs.graphs_shuffle_edge.values[0],
all_dfs.graphs_tabu.values[0], all_dfs.graphs_vns.values[0],all_dfs3.graphs_shuffle_local.values[0]]: all_dfs.graphs_tabu.values[0], all_dfs.graphs_vns.values[0]]:
df = nx.to_pandas_edgelist(s_orig) df = nx.to_pandas_edgelist(s_orig)
...@@ -60,7 +54,7 @@ for s_orig in [all_dfs.graphs_local.values[0], all_dfs.graphs_delta.values[0], ...@@ -60,7 +54,7 @@ for s_orig in [all_dfs.graphs_local.values[0], all_dfs.graphs_delta.values[0],
com = (np.sum(emittersgdf_filt_one['x'].values*emittersgdf_filt_one['em_mean'].values)/emittersgdf_filt_one['em_mean'].sum(), com = (np.sum(emittersgdf_filt_one['x'].values*emittersgdf_filt_one['em_mean'].values)/emittersgdf_filt_one['em_mean'].sum(),
np.sum(emittersgdf_filt_one['y'].values*emittersgdf_filt_one['em_mean'].values)/emittersgdf_filt_one['em_mean'].sum()) np.sum(emittersgdf_filt_one['y'].values*emittersgdf_filt_one['em_mean'].values)/emittersgdf_filt_one['em_mean'].sum())
s_best = all_dfs3.graphs_shuffle_local.values[0] s_best = all_dfs.graphs_shuffle_edge.values[0]
fig, ax = plt.subplots(figsize=(7,7), dpi = 200,sharex=True, sharey=True) fig, ax = plt.subplots(figsize=(7,7), dpi = 200,sharex=True, sharey=True)
ax.set_xlim(frame_limsx[0],frame_limsx[1]) ax.set_xlim(frame_limsx[0],frame_limsx[1])
...@@ -78,8 +72,8 @@ stor = gpd.GeoDataFrame( geometry= [Point(com)]) ...@@ -78,8 +72,8 @@ stor = gpd.GeoDataFrame( geometry= [Point(com)])
for s_orig,title in zip([all_dfs.graphs_local.values[0], all_dfs.graphs_delta.values[0], for s_orig,title in zip([all_dfs.graphs_local.values[0], all_dfs.graphs_delta.values[0],
all_dfs.graphs_edge.values[0], all_dfs.graphs_shuffle_edge.values[0], all_dfs.graphs_edge.values[0], all_dfs.graphs_shuffle_edge.values[0],
all_dfs.graphs_tabu.values[0], all_dfs.graphs_vns.values[0],all_dfs3.graphs_shuffle_local.values[0],mst], all_dfs.graphs_tabu.values[0], all_dfs.graphs_vns.values[0],mst],
['LS','DC','ET','VSET','Tabu','VNS','VSLS','MST']): ['LS','DC','ET','VSET','Tabu','VNS','MST']):
fig, ax = plt.subplots(figsize=(7,7), dpi = 300,sharex=True, sharey=True) fig, ax = plt.subplots(figsize=(7,7), dpi = 300,sharex=True, sharey=True)
ax.set_xlim(frame_limsx[0],frame_limsx[1]) ax.set_xlim(frame_limsx[0],frame_limsx[1])
ax.set_ylim(frame_limsy[0],frame_limsy[1]) ax.set_ylim(frame_limsy[0],frame_limsy[1])
......
...@@ -94,7 +94,6 @@ def get_starting_trees_grid(emitters_fixed,sink_grid): ...@@ -94,7 +94,6 @@ def get_starting_trees_grid(emitters_fixed,sink_grid):
#Combine sink and sources in a single DataFrame #Combine sink and sources in a single DataFrame
full["x"] = full.geometry.apply(lambda p: p.x) full["x"] = full.geometry.apply(lambda p: p.x)
full["y"] = full.geometry.apply(lambda p: p.y) full["y"] = full.geometry.apply(lambda p: p.y)
## fixx### ^ needed?
#Add nodes to graph #Add nodes to graph
for j, point in full.iterrows(): for j, point in full.iterrows():
...@@ -154,7 +153,7 @@ def main(chosen_clust,n_pointsx): ...@@ -154,7 +153,7 @@ def main(chosen_clust,n_pointsx):
"shuffle_local"] "shuffle_local"]
if chosen_clust in [1,2,3]: if chosen_clust in [1,2,3]:
algos.extend([delta_change_nested,edge_turn_nested]) algos.extend([delta_change_recursive,edge_turn_recursive])
algo_strings.extend(["delta2","edge2"]) algo_strings.extend(["delta2","edge2"])
if chosen_clust == 1: if chosen_clust == 1:
......
...@@ -79,8 +79,8 @@ def main(number_rand,size_graph): ...@@ -79,8 +79,8 @@ def main(number_rand,size_graph):
results = pd.DataFrame() results = pd.DataFrame()
#List of algorithms to test #List of algorithms to test
algos = [local_search,vns,tabu,delta_change,delta_change_nested, algos = [local_search,vns,tabu,delta_change,delta_change_recursive,
edge_turn,edge_turn_nested,high_valency_shuffle_edge_turn, edge_turn,edge_turn_recursive,high_valency_shuffle_edge_turn,
high_valency_shuffle_delta_change,high_valency_shuffle_local_search] high_valency_shuffle_delta_change,high_valency_shuffle_local_search]
algo_strings = ["local", algo_strings = ["local",
......
...@@ -70,12 +70,13 @@ for algo in algos: ...@@ -70,12 +70,13 @@ for algo in algos:
yfit = lambda x: np.exp(poly(np.log(x))) yfit = lambda x: np.exp(poly(np.log(x)))
#Print predicted calculation times for a grpah of 94 nodes #Print predicted calculation times for a grpah of 94 sources
print(algo +" : "+ str(yfit(94)/3600)) print(algo +" : "+ str(yfit(94)/3600))
coeffs_list_linspace = [(exponent,np.exp(constant)) for exponent,constant in coeffs_list] coeffs_list_linspace = [(exponent,np.exp(constant)) for exponent,constant in coeffs_list]
#Plotting
al = 0.7 al = 0.7
small_mark = 40 small_mark = 40
big_mark = 130 big_mark = 130
......
...@@ -188,7 +188,6 @@ def delta_change(input_graph, cost_function): ...@@ -188,7 +188,6 @@ def delta_change(input_graph, cost_function):
""" """
complete_graph = create_compl_graph_from_other(input_graph) complete_graph = create_compl_graph_from_other(input_graph)
initial_cost = graph_cost(cost_function, input_graph)
nodes_i = list(input_graph.nodes()) nodes_i = list(input_graph.nodes())
...@@ -203,6 +202,7 @@ def delta_change(input_graph, cost_function): ...@@ -203,6 +202,7 @@ def delta_change(input_graph, cost_function):
while better_found == True: while better_found == True:
better_found = False better_found = False
initial_cost = graph_cost(cost_function, candidate_graph)
for node_iter_i in nodes_i: for node_iter_i in nodes_i:
...@@ -226,7 +226,6 @@ def delta_change(input_graph, cost_function): ...@@ -226,7 +226,6 @@ def delta_change(input_graph, cost_function):
nodes_ij.remove(node_neighbour) nodes_ij.remove(node_neighbour)
#Create cycle by connecting candidate nodes #Create cycle by connecting candidate nodes
for node_ij_iter in nodes_ij: for node_ij_iter in nodes_ij:
candidate_graph2 = candidate_graph.copy() candidate_graph2 = candidate_graph.copy()
...@@ -253,10 +252,12 @@ def delta_change(input_graph, cost_function): ...@@ -253,10 +252,12 @@ def delta_change(input_graph, cost_function):
#As soon as better solution is found break all loops and restart: #As soon as better solution is found break all loops and restart:
if new_cost < initial_cost: if new_cost < initial_cost:
candidate_graph = candidate_graph3.copy() candidate_graph = candidate_graph3.copy()
initial_cost = new_cost initial_cost = new_cost
better_found = True better_found = True
break break
if better_found == True: if better_found == True:
break break
if better_found == True: if better_found == True:
...@@ -264,20 +265,100 @@ def delta_change(input_graph, cost_function): ...@@ -264,20 +265,100 @@ def delta_change(input_graph, cost_function):
return candidate_graph, graph_cost(cost_function, candidate_graph) return candidate_graph, graph_cost(cost_function, candidate_graph)
def delta_change_nested(input_graph, cost_function):
def delta_change_recursive_under(input_graph, cost_function,recursion_level):
complete_graph = create_compl_graph_from_other(input_graph)
nodes_i = list(input_graph.nodes())
candidate_graph = input_graph.copy()
better_found = False
initial_cost = graph_cost(cost_function, candidate_graph)
for node_iter_i in nodes_i:
#Find all potential neighbours to node_i
nodes_ij = list(complete_graph[node_iter_i])
#Get candiate nodes that aren't connected to node_i
existing_neighbours = list(candidate_graph.neighbors(node_iter_i))
for node_neighbour in existing_neighbours:
nodes_ij.remove(node_neighbour)
#Create cycle by connecting candidate nodes
for node_ij_iter in nodes_ij:
candidate_graph2 = candidate_graph.copy()
candidate_graph2.add_edge(
node_ij_iter,
node_iter_i,
weight=complete_graph[node_ij_iter][node_iter_i]["weight"],
)
edges_cycle = nx.find_cycle(candidate_graph2)
try:
edges_cycle.remove((node_ij_iter, node_iter_i))
except ValueError:
edges_cycle.remove((node_iter_i, node_ij_iter))
#Test removing each edge in the new cycle and calculate graph cost
for candidate_edge_remove in edges_cycle:
candidate_graph3 = candidate_graph2.copy()
candidate_graph3.remove_edge(*candidate_edge_remove)
candidate_graph3 = graph_with_calc_edge_capacity(candidate_graph3)
new_cost = graph_cost(cost_function, candidate_graph3)
#As soon as better solution is found break all loops and restart:
if new_cost < initial_cost:
candidate_graph = candidate_graph3.copy()
initial_cost = new_cost
better_found = True
break
if recursion_level >1:
nested_candidate,nested_cost = delta_change_recursive(candidate_graph3, cost_function,recursion_level-1)
if nested_cost < initial_cost:
candidate_graph = nested_candidate.copy()
initial_cost = nested_cost
better_found = True
break
if better_found == True:
break
if better_found == True:
break
return candidate_graph, graph_cost(cost_function, candidate_graph)
def delta_change_recursive(input_graph, cost_function,recursion_level = 2):
""" """
Heuristic to provide minimum-cost tree from another tree, Heuristic to provide minimum-cost tree from another tree,
by creating and breaking cycles in the trees before calculating cost. by creating and breaking cycles in the trees before calculating cost.
First-descent algorithm. Not guaranteed to provide global minima solutions. First-descent algorithm. Not guaranteed to provide global minima solutions.
This algorithm is a nested version of a simpler algorithm "delta_change". This algorithm is a recursive version of a simpler algorithm "delta_change".
In this algorithm the delta_change is performed, In this algorithm the delta_change is performed,
and on each intermediate candidate tree, a second delta_change is performed, and on each intermediate candidate tree, a nested delta_change is performed, and so on...
allowing jumps into the 2-neigbourhood solution space in one step allowing jumps into the neigbourhood solution space of the recursion level in one step.
In this implementation we remove the Andre's (2013) additions the delta_change algorithm (and references to them), In this implementation we remove the Andre's (2013) additions the delta_change algorithm (and references to them),
although including them (notably only taking closest candidate nodes) could reduce calculation time. although including them (notably only taking closest candidate nodes) could reduce calculation time.
Note that calcaulation time becomes usually becomes impratical for input recusion levels above 3 and node numbers above 10.
Args: Args:
input_graph: NetworkX Graph input_graph: NetworkX Graph
cost_function: a cost function of the capacity cost_function: a cost function of the capacity
...@@ -290,6 +371,9 @@ def delta_change_nested(input_graph, cost_function): ...@@ -290,6 +371,9 @@ def delta_change_nested(input_graph, cost_function):
complete_graph = create_compl_graph_from_other(input_graph) complete_graph = create_compl_graph_from_other(input_graph)
nodes_i = list(input_graph.nodes()) nodes_i = list(input_graph.nodes())
#Randomisation of node list as indicated in Andre 2013:
random.shuffle(nodes_i)
#This variable serves to stop the while loop if no better solution is found #This variable serves to stop the while loop if no better solution is found
better_found = True better_found = True
...@@ -300,7 +384,7 @@ def delta_change_nested(input_graph, cost_function): ...@@ -300,7 +384,7 @@ def delta_change_nested(input_graph, cost_function):
initial_cost = graph_cost(cost_function, candidate_graph) initial_cost = graph_cost(cost_function, candidate_graph)
for node_iter_i in nodes_i: for node_iter_i in nodes_i:
#if node_iter
#Find all potential neighbours to node_i #Find all potential neighbours to node_i
nodes_ij = list(complete_graph[node_iter_i]) nodes_ij = list(complete_graph[node_iter_i])
#Get candiate nodes that aren't connected to node_i #Get candiate nodes that aren't connected to node_i
...@@ -313,8 +397,6 @@ def delta_change_nested(input_graph, cost_function): ...@@ -313,8 +397,6 @@ def delta_change_nested(input_graph, cost_function):
#Create cycle by connecting candidate nodes #Create cycle by connecting candidate nodes
for node_ij_iter in nodes_ij: for node_ij_iter in nodes_ij:
results = []
results.append((candidate_graph, initial_cost))
candidate_graph2 = candidate_graph.copy() candidate_graph2 = candidate_graph.copy()
candidate_graph2.add_edge( candidate_graph2.add_edge(
node_ij_iter, node_ij_iter,
...@@ -346,58 +428,18 @@ def delta_change_nested(input_graph, cost_function): ...@@ -346,58 +428,18 @@ def delta_change_nested(input_graph, cost_function):
break break
#The nested delta change starts here if recursion_level >1:
#variables named as previously have the same function in the nested loop
for node_iter_i2 in nodes_i:
#No point iterating over the same node again:
if node_iter_i == node_iter_i2:
continue
nodes_ij2 = list(complete_graph[node_iter_i2])
existing_neighbours2 = list(candidate_graph3.neighbors(node_iter_i2))
for node_neighbour2 in existing_neighbours2:
nodes_ij2.remove(node_neighbour2)
for node_ij2_iter in nodes_ij2: nested_candidate,nested_cost = delta_change_recursive_under(candidate_graph3, cost_function,recursion_level-1)
candidate_graph4 = candidate_graph3.copy() if nested_cost < initial_cost:
candidate_graph4.add_edge(
node_ij2_iter,
node_iter_i2,
weight=complete_graph[node_ij2_iter][node_iter_i2][
"weight"
],
)
edges_cycle2 = nx.find_cycle(candidate_graph4)
try: candidate_graph = nested_candidate.copy()
edges_cycle2.remove((node_ij2_iter, node_iter_i2)) initial_cost = nested_cost
except ValueError:
edges_cycle2.remove((node_iter_i2, node_ij2_iter))
for candidate_edge_remove2 in edges_cycle2:
candidate_graph5 = candidate_graph4.copy()
candidate_graph5.remove_edge(*candidate_edge_remove2)
candidate_graph5 = graph_with_calc_edge_capacity(candidate_graph5)
new_cost2 = graph_cost(cost_function, candidate_graph5)
#As soon as better solution is found break all loops and restart:
if new_cost2 < initial_cost:
initial_cost = new_cost2
better_found = True better_found = True
candidate_graph = candidate_graph5.copy()
break
if better_found == True:
break
if better_found == True:
break
if better_found == True:
break break
if better_found == True: if better_found == True:
break break
if better_found == True: if better_found == True:
...@@ -483,17 +525,84 @@ def edge_turn(input_graph, cost_function): ...@@ -483,17 +525,84 @@ def edge_turn(input_graph, cost_function):
return input_graph, graph_cost(cost_function, input_graph) return input_graph, graph_cost(cost_function, input_graph)
def edge_turn_nested(input_graph, cost_function):
def edge_turn_recursive_under(input_graph, cost_function,recursion_level):
#This variable serves to stop the while loop if no better solution is found
candidate_graph = input_graph.copy()
better_found = False
initial_cost = graph_cost(cost_function, candidate_graph)
#Potential list of cheaper trees:
results = []
#Add current best solution
results.append([initial_cost, candidate_graph.copy()])
#Iterates over the edges to remove
for edge in candidate_graph.edges:
candidate_graph2 = candidate_graph.copy()
candidate_graph2 = graph_with_calc_edge_capacity(candidate_graph2)
candidate_graph2.remove_edge(*edge)
#For each removed edge, all nodes of either connected component are re-attached
#to the node in the oher connected compoenent from the removed edge.
#The following codeblock in then repeated only twice in the for loop: once for each connected component
#Iterates over both connected components:
for component in list(nx.connected_components(candidate_graph2)):
connected = list(component)
#Finding the terminal in the removed edge that is not in the connected component considered
if edge[0] not in connected:
term = edge[0]
else:
term = edge[1]
for comp in connected:
candidate_graph_test = candidate_graph2.copy()
candidate_graph_test.add_edge(term, comp)
candidate_graph_test = graph_with_calc_edge_capacity(candidate_graph_test)
new_cost = graph_cost(cost_function, candidate_graph_test)
if new_cost < initial_cost:
results.append([new_cost, candidate_graph_test])
if recursion_level >1:
nested_candidate,nested_cost = edge_turn_recursive_under(candidate_graph_test, cost_function,recursion_level-1)
if nested_cost < initial_cost:
results.append([nested_cost, nested_candidate])
#If better solution found:
if len(results) > 1:
better_found = True
results.sort(key=lambda x: x[0])
candidate_graph = results[0][1]
#Otherwise return the incumbent solution
else:
candidate_graph = results[0][1]
input_graph = candidate_graph.copy()
return input_graph, graph_cost(cost_function, input_graph)
def edge_turn_recursive(input_graph, cost_function,recursion_level = 2):
""" """
Heuristic to provide minimum-cost tree from another tree, Heuristic to provide minimum-cost tree from another tree,
by creating and breaking cycles in the trees before calculating cost. by creating and breaking cycles in the trees before calculating cost.
Steepest-descent algorithm. Not guaranteed to provide global minima solutions. Steepest-descent algorithm. Not guaranteed to provide global minima solutions.
This algorithm is a nested version of a simpler algorithm "edge_turn". This algorithm is a recursive version of a simpler algorithm "edge_turn".
In this algorithm the edge turn algorithm is performed, In this algorithm the edge turn algorithm is performed,
and on each intermediate candidate tree, a second edge turn algorithm is performed, and on each intermediate candidate tree, a nested turn algorithm is performed, and so on...
allowing jumps into the 2-neigbourhood solution space in one step allowing jumps into the neigbourhood solution space of the recursion level in one step
In our implementation, the steepest descent applies to both the initial In our implementation, the steepest descent applies to both the initial
and to the nested edge turn algorithm, and the chosen best tree is chosen and to the nested edge turn algorithm, and the chosen best tree is chosen
over all the initial and nested edge turns. over all the initial and nested edge turns.
...@@ -552,32 +661,11 @@ def edge_turn_nested(input_graph, cost_function): ...@@ -552,32 +661,11 @@ def edge_turn_nested(input_graph, cost_function):
if new_cost < initial_cost: if new_cost < initial_cost:
results.append([new_cost, candidate_graph_test]) results.append([new_cost, candidate_graph_test])
#The nested edge turn algorithm starts here if recursion_level >1:
#variables named as previously have the same function in the nested loop nested_candidate,nested_cost = edge_turn_recursive_under(candidate_graph_test, cost_function,recursion_level-1)
for edge2 in candidate_graph_test.edges:
candidate_graph_test2 = candidate_graph_test.copy()
if edge2 == edge:
continue
candidate_graph_test2.remove_edge(*edge2)
for component2 in list(nx.connected_components(candidate_graph_test2)):
connected2 = list(component2)
if edge2[0] not in connected2:
term2 = edge2[0]
else:
term2 = edge2[1]
for comp2 in connected2:
candidate_graph_test3 = candidate_graph_test2.copy()
candidate_graph_test3.add_edge(term2, comp2)
candidate_graph_test3 = graph_with_calc_edge_capacity(candidate_graph_test3)
new_cost2 = graph_cost(cost_function, candidate_graph_test3)
if new_cost2 < initial_cost:
results.append([new_cost2, candidate_graph_test3])
if nested_cost < initial_cost:
results.append([nested_cost, nested_candidate])