From 257269ff9664528bb872ff48564a59b0a52699d6 Mon Sep 17 00:00:00 2001 From: Anne-Liza <a.m.r.m.bruggeman@student.tudelft.nl> Date: Tue, 3 Jul 2018 14:48:52 +0200 Subject: [PATCH] Fixed some small issues in the sequencing algorithms Former-commit-id: 0fe0f76ce87883d1ec615fdaed06fed5e0f34750 --- kadmos/graph/graph_data.py | 223 +++++++++++++++++++++++++------------ 1 file changed, 150 insertions(+), 73 deletions(-) diff --git a/kadmos/graph/graph_data.py b/kadmos/graph/graph_data.py index e4d23e3f0..b67e0227b 100644 --- a/kadmos/graph/graph_data.py +++ b/kadmos/graph/graph_data.py @@ -583,7 +583,9 @@ class DataGraph(KadmosGraph): def get_possible_function_order(self, method, multi_start=None, check_graph=False, coupling_dict=None, node_selection=None, rcb=1.0, use_runtime_info=False): - """ Method to find a possible function order, in the order: pre-coupled, coupled, post-coupled functions + """ Method to find a possible function order, in the order: pre-coupling, coupled, post-coupling functions. + If partitions have already been set, the partitions will be taken into account and the function order in each + partition will be determined as well. :param method: algorithm which will be used to minimize the feedback loops :type method: str @@ -591,11 +593,15 @@ class DataGraph(KadmosGraph): :type multi_start: int :param check_graph: check whether graph has problematic variables :type check_graph: bool - :param node_selection: possibility to get the order of only a selection of nodes instead of the entire graph + :param coupling_dict: coupling dictionary of the graph + :type coupling_dict: dict + :param node_selection: option to get the order of only a selection of nodes instead of the entire graph :type node_selection: list :param rcb: runtime-coupling balance, relative importance between feedback and runtime while optimizing function order. 1: min feedback, 0: min runtime :type rcb: float + :param use_runtime_info: option to use the runtime of the disciplines while determining the function order + :type use_runtime_info: bool :return Possible function order :rtype list """ @@ -605,6 +611,19 @@ class DataGraph(KadmosGraph): assert not self.find_all_nodes(subcategory='all problematic variables'), \ 'Graph still has problematic variables.' assert 0 <= rcb <= 1, 'Runtime-coupling balance should be between zero and one.' + if use_runtime_info: + nodes = list(node_selection) if node_selection else self.find_all_nodes(category='function') + for node in nodes: + assert 'performance_info' in self.nodes[node], 'Performance info missing for node {}'.format(node) + assert 'run_time' in self.nodes[node]['performance_info'], 'Runtime missing for node {}'.format(node) + + # If zero or one node is present, the solution can be given immediately + if node_selection is None: + if len(self.find_all_nodes(category='function')) < 2: + return self.find_all_nodes(category='function') + else: + if len(node_selection) < 2: + return node_selection # Get coupling dictionary if not coupling_dict: @@ -630,7 +649,7 @@ class DataGraph(KadmosGraph): coupled_functions = [] if partitions: - # Merge the nodes in the partitions into the super node + # Merge the nodes of the partitions into the super node for partition in partitions: for function_id in partition: function_graph = nx.contracted_nodes(function_graph, 'super_node', function_id, self_loops=False) @@ -662,20 +681,18 @@ class DataGraph(KadmosGraph): self_loops=False) # Find a topological function order - function_order = list(nx.topological_sort(function_graph)) - - # Get pre-coupling functions and sort - pre_coupling_functions = function_order[:function_order.index('super_node')] - pre_coupling_functions_order = self.sort_nodes_for_process(pre_coupling_functions) + initial_function_order = list(nx.topological_sort(function_graph)) # Sort coupled functions to minimize feedback if partitions: coupled_functions_order = [] for partition, nodes in enumerate(partitions): - nodes = self.minimize_feedback(list(nodes), method, multi_start=multi_start, rcb=rcb, - use_runtime_info=use_runtime_info, coupling_dict=coupling_dict) + if len(nodes) > 1: + nodes = self.minimize_feedback(list(nodes), method, multi_start=multi_start, rcb=rcb, + use_runtime_info=use_runtime_info, coupling_dict=coupling_dict) partitions[partition] = nodes coupled_functions_order.extend(nodes) + # Make sure the function orders in the partitions are consistent with the overall function order self.graph['problem_formulation']['coupled_functions_groups'] = partitions elif coupled_functions: coupled_functions_order = self.minimize_feedback(coupled_functions, method, multi_start=multi_start, @@ -684,12 +701,22 @@ class DataGraph(KadmosGraph): else: coupled_functions_order = [] - # Get post-coupling functions and sort - post_coupling_functions = function_order[function_order.index('super_node') + 1:] - post_coupling_functions_order = self.sort_nodes_for_process(post_coupling_functions) + if coupled_functions_order: + # Get pre-coupling functions and sort + pre_coupling_functions = initial_function_order[:initial_function_order.index('super_node')] + pre_coupling_functions_order = self.sort_nodes_for_process(pre_coupling_functions) + + # Get post-coupling functions and sort + post_coupling_functions = initial_function_order[initial_function_order.index('super_node') + 1:] + post_coupling_functions_order = self.sort_nodes_for_process(post_coupling_functions) - # Get function_order - function_order = pre_coupling_functions_order + coupled_functions_order + post_coupling_functions_order + # Get function_order + function_order = pre_coupling_functions_order + coupled_functions_order + post_coupling_functions_order + else: + # If no coupled functions are present, the number of feedback is zero and the nodes need to be sorted for an + # optimal process only + initial_function_order.pop(initial_function_order.index('super_node')) + function_order = self.sort_nodes_for_process(initial_function_order) return function_order @@ -715,12 +742,14 @@ class DataGraph(KadmosGraph): else: return highest_instance - def sort_nodes_for_process(self, nodes): + def sort_nodes_for_process(self, nodes, coupling_dict=None): """ Method to sort function nodes such that the process order is optimized, while not increasing the number of feedback loops :param nodes: function nodes to sort :type nodes: list + :param coupling_dict: coupling dictionary of the graph + :type coupling_dict: dict :return: nodes in sorted order :rtype: list """ @@ -730,17 +759,23 @@ class DataGraph(KadmosGraph): assert func in self, "Function node {} must be present in graph.".format(func) assert self.nodes[func]['category'] == 'function', "Node {} is not a function node".format(func) - # When nodes have no feedback, get topological order (e.g. when sorting pre or post coupled functions) + # If zero or one node, return immediately + if len(nodes) < 2: + return nodes + + # When nodes have no feedback, get topological order (e.g. when sorting pre or post-coupling functions) subgraph = self.get_subgraph_by_function_nodes(nodes) subgraph = subgraph.get_function_graph() + subgraph.remove_edges_from(subgraph.selfloop_edges()) if nx.is_directed_acyclic_graph(subgraph): - nodes = nx.topological_sort(subgraph) + nodes = list(nx.topological_sort(subgraph)) nodes_to_sort = list(nodes) function_order = [] # Get couplings between nodes - coupling_dict = self.get_coupling_dictionary() + if not coupling_dict: + coupling_dict = self.get_coupling_dictionary() while nodes_to_sort: sorted_nodes = [] @@ -757,7 +792,15 @@ class DataGraph(KadmosGraph): return function_order def get_runtime_sequence(self, sequence, use_runtime_info=False, coupling_dict=None): - """Function to calculate the runtime of a sequence of nodes""" + """Function to calculate the runtime of a sequence of nodes + + :param sequence: Sequence of nodes for which the runtime needs to be calculated + :type sequence: list + :param use_runtime_info: option to use the runtime of the disciplines while determining the function order + :type use_runtime_info: bool + :param coupling_dict: coupling dictionary of the graph + :type coupling_dict: dict + """ # Input assertion if use_runtime_info: @@ -765,6 +808,10 @@ class DataGraph(KadmosGraph): assert 'performance_info' in self.nodes[node] assert 'run_time' in self.nodes[node]['performance_info'], 'Run time missing for node {}'.format(node) + # If zero nodes, return zero runtime + if not sequence: + return 0 + # Get coupling dictionary if not coupling_dict: coupling_dict = self.get_coupling_dictionary() @@ -788,7 +835,7 @@ class DataGraph(KadmosGraph): def minimize_feedback(self, nodes, method, multi_start=None, get_evaluations=False, coupling_dict=None, rcb=1, use_runtime_info=False): - """Function to find the function order with minimum feedback + """Function to find the function order with minimum feedback or minimum runtime :param nodes: nodes for which the feedback needs to be minimized :type nodes: list @@ -796,16 +843,18 @@ class DataGraph(KadmosGraph): :type method: str :param multi_start: start the algorithm from multiple starting points :type multi_start: int - :param get_evaluations: option to give the number of evaluations as output + :param get_evaluations: option to get the number of evaluations needed by the algorithm as output :type get_evaluations: bool - :param coupling_dict: + :param coupling_dict: coupling dictionary of the graph :type coupling_dict: dict :param rcb: runtime-coupling balance, relative importance between feedback and runtime while optimizing function order. 1: min feedback, 0: min runtime :type rcb: float + :param use_runtime_info: option to use the runtime of the disciplines while determining the function order + :type use_runtime_info: bool :return function order :rtype list - :return number of evaluations + :return number of evaluations (optional) :rtype int """ @@ -813,9 +862,13 @@ class DataGraph(KadmosGraph): assert 0 <= rcb <= 1, 'Runtime-coupling balance should be between zero and one.' if use_runtime_info: for node in nodes: - assert 'performance_info' in self.nodes[node] + assert 'performance_info' in self.nodes[node], 'Performance info missing for node {}'.format(node) assert 'run_time' in self.nodes[node]['performance_info'], 'Run time missing for node {}'.format(node) + # If zero or one node is given, the solution can be returned immediately + if len(nodes) < 2: + return nodes + # Get coupling dictionary if not coupling_dict: coupling_dict = self.get_coupling_dictionary() @@ -833,64 +886,53 @@ class DataGraph(KadmosGraph): random.shuffle(nodes) start_points[i][:] = nodes multi_start = start_points + elif multi_start is None: + multi_start = [nodes] - if multi_start: - best_order = list(nodes) - min_f, min_feedback, min_time = float("inf"), float("inf"), float("inf") - - # Start algorithm for each starting point - n_eval = 0 - for start_point in range(len(multi_start)): - if method == 'brute-force' or method == 'branch-and-bound': - raise IOError('No multi start possible for an exact algorithm') - elif method == 'single-swap': - function_order, n_eval_iter = self._single_swap(multi_start[start_point], coupling_dict, rcb, - use_runtime_info) - elif method == 'two-swap': - function_order, n_eval_iter = self._two_swap(multi_start[start_point], coupling_dict, rcb, - use_runtime_info) - elif method == 'hybrid-swap': - function_order, n_eval_iter1 = self._two_swap(multi_start[start_point], coupling_dict, rcb, - use_runtime_info) - function_order, n_eval_iter2 = self._single_swap(function_order, coupling_dict, rcb, - use_runtime_info) - n_eval_iter = n_eval_iter1 + n_eval_iter2 - else: - raise IOError('Selected method (' + method + ') is not a valid method for sequencing, supported ' + - 'methods are: brute-force, single-swap, two-swap, hybrid-swap, branch-and-bound') - - n_eval += n_eval_iter - - # Get feedback info - feedback, time = self.get_feedback_info(function_order, coupling_dict, use_runtime_info) - - # Remember best order found - f = rcb*(feedback/float(total_couplings)) + (1-rcb)*(time/float(total_time)) - if (feedback == min_feedback and time < min_time) or (time == min_time and feedback < min_feedback) or \ - (f < min_f): - best_order, min_f, min_feedback, min_time = list(function_order), f, feedback, time - - function_order = list(best_order) + best_order = list(nodes) + min_f, min_feedback, min_time = float("inf"), float("inf"), float("inf") - else: + # Start algorithm for each starting point + n_eval = 0 + for start_point in range(len(multi_start)): + if start_point > 0 and method in ['brute-force', 'branch-and-bound']: + logger.warning('Multi-start is useless when using an exact algorithm to determine the function order.') + break if method == 'brute-force': - function_order, n_eval = self._brute_force(nodes, coupling_dict, rcb, use_runtime_info) + function_order, n_eval_iter = self._brute_force(nodes, coupling_dict, rcb, use_runtime_info) elif method == 'branch-and-bound': - function_order, n_eval = self._branch_and_bound(nodes, coupling_dict, rcb, use_runtime_info) + function_order, n_eval_iter = self._branch_and_bound(nodes, coupling_dict, rcb, use_runtime_info) elif method == 'single-swap': - function_order, n_eval = self._single_swap(nodes, coupling_dict, rcb, use_runtime_info) + function_order, n_eval_iter = self._single_swap(multi_start[start_point], coupling_dict, rcb, + use_runtime_info) elif method == 'two-swap': - function_order, n_eval = self._two_swap(nodes, coupling_dict, rcb, use_runtime_info) + function_order, n_eval_iter = self._two_swap(multi_start[start_point], coupling_dict, rcb, + use_runtime_info) elif method == 'hybrid-swap': - function_order, n_eval1 = self._two_swap(nodes, coupling_dict, rcb, use_runtime_info) - function_order, n_eval2 = self._single_swap(function_order, coupling_dict, rcb, use_runtime_info) - n_eval = n_eval1 + n_eval2 + function_order, n_eval_iter1 = self._two_swap(multi_start[start_point], coupling_dict, rcb, + use_runtime_info) + function_order, n_eval_iter2 = self._single_swap(function_order, coupling_dict, rcb, + use_runtime_info) + n_eval_iter = n_eval_iter1 + n_eval_iter2 elif method == 'genetic-algorithm': - function_order, n_eval = self._genetic_algorithm(nodes, coupling_dict, rcb, use_runtime_info) + function_order, n_eval_iter = self._genetic_algorithm(nodes, coupling_dict, rcb, use_runtime_info) else: raise IOError('Selected method (' + method + ') is not a valid method for sequencing, supported ' + 'methods are: brute-force, single-swap, two-swap, hybrid-swap, branch-and-bound') + n_eval += n_eval_iter + + # Get feedback info + feedback, time = self.get_feedback_info(function_order, coupling_dict, use_runtime_info) + + # Remember best order found + f = rcb*(feedback/float(total_couplings)) + (1-rcb)*(time/float(total_time)) + if (feedback == min_feedback and time < min_time) or (time == min_time and feedback < min_feedback) or \ + (f < min_f): + best_order, min_f, min_feedback, min_time = list(function_order), f, feedback, time + + function_order = list(best_order) + if get_evaluations: return function_order, n_eval else: @@ -902,6 +944,13 @@ class DataGraph(KadmosGraph): :param nodes: nodes that need to be ordered :type nodes: list + :param coupling_dict: coupling dictionary of the graph + :type coupling_dict: dict + :param rcb: runtime-coupling balance, relative importance between feedback and runtime while optimizing + function order. 1: min feedback, 0: min runtime + :type rcb: float + :param use_runtime_info: option to use the runtime of the disciplines while determining the function order + :type use_runtime_info: bool :return: function order :rtype: list """ @@ -945,6 +994,13 @@ class DataGraph(KadmosGraph): :param nodes: nodes that need to be ordered :type nodes: list + :param coupling_dict: coupling dictionary of the graph + :type coupling_dict: dict + :param rcb: runtime-coupling balance, relative importance between feedback and runtime while optimizing + function order. 1: min feedback, 0: min runtime + :type rcb: float + :param use_runtime_info: option to use the runtime of the disciplines while determining the function order + :type use_runtime_info: bool :return: function order :rtype: list """ @@ -1013,6 +1069,13 @@ class DataGraph(KadmosGraph): :param nodes: nodes that need to be ordered :type nodes: list + :param coupling_dict: coupling dictionary of the graph + :type coupling_dict: dict + :param rcb: runtime-coupling balance, relative importance between feedback and runtime while optimizing + function order. 1: min feedback, 0: min runtime + :type rcb: float + :param use_runtime_info: option to use the runtime of the disciplines while determining the function order + :type use_runtime_info: bool :return: function order :rtype: list """ @@ -1074,6 +1137,13 @@ class DataGraph(KadmosGraph): :param nodes: nodes that need to be ordered :type nodes: list + :param coupling_dict: coupling dictionary of the graph + :type coupling_dict: dict + :param rcb: runtime-coupling balance, relative importance between feedback and runtime while optimizing + function order. 1: min feedback, 0: min runtime + :type rcb: float + :param use_runtime_info: option to use the runtime of the disciplines while determining the function order + :type use_runtime_info: bool :return: function order :rtype: list """ @@ -1146,6 +1216,10 @@ class DataGraph(KadmosGraph): :type branch: list :param nodes: the nodes that are considered in the sequencing problem :type nodes: list + :param coupling_dict: coupling dictionary of the graph + :type coupling_dict: dict + :param use_runtime_info: option to use the runtime of the disciplines while determining the function order + :type use_runtime_info: bool :return: lower bound :rtype: int """ @@ -1263,7 +1337,7 @@ class DataGraph(KadmosGraph): # Input assertions if use_runtime_info: for node in function_order: - assert 'performance_info' in self.nodes[node] + assert 'performance_info' in self.nodes[node], 'Performance info missing for node {}'.format(node) assert 'run_time' in self.nodes[node]['performance_info'], 'Run time missing for node {}'.format(node) # Get coupling dictionary @@ -1569,9 +1643,12 @@ class RepositoryConnectivityGraph(DataGraph): for local_constraint in range(n_local_constraints[discipline]): # Local constraints self.add_node('G{0}_{1}'.format(discipline+1, local_constraint+1), category='function', instance=1, function_type='regular') + self.nodes['G{0}_{1}'.format(discipline+1, local_constraint+1)]['performance_info'] = {'run_time': 0} self.add_node('F', category='function', instance=1, function_type='regular') # Objective + self.nodes['F']['performance_info'] = {'run_time': 0} for constraint in range(n_global_constraints): # Global constraints self.add_node('G0{0}'.format(constraint + 1), category='function', instance=1, function_type='regular') + self.nodes['G0{0}'.format(constraint + 1)]['performance_info'] = {'run_time': 0} # All variable nodes are defined for global_var in range(n_global_var): # Global design variables @@ -4736,7 +4813,7 @@ class MdaoDataGraph(DataGraph, MdaoMixin): instance = int(self.nodes[graph_parameter_node].get('instance')) if instance > 1: cmdows_parameter_node.add('relatedInstanceUID', - self.get_first_node_instance(self.nodes[graph_parameter_node])) + self.get_first_node_instance(graph_parameter_node)) else: cmdows_parameter_node.add('relatedParameterUID', self.nodes[graph_parameter_node].get('related_to_schema_node')) -- GitLab