From bab232dbb8a167d39305ea19e9e6dd2a2e9ca631 Mon Sep 17 00:00:00 2001 From: Anne-Liza <a.m.r.m.bruggeman@student.tudelft.nl> Date: Mon, 21 May 2018 11:25:00 +0200 Subject: [PATCH] Added runtime - feedback trade-off to partitioning. Fixed small bug in sequencing algorithms. Former-commit-id: 3341b0086223aa911b04a4bbcd00b4603369f5fb --- kadmos/graph/graph_data.py | 466 +++++++++++++++++++++++-------------- 1 file changed, 293 insertions(+), 173 deletions(-) diff --git a/kadmos/graph/graph_data.py b/kadmos/graph/graph_data.py index a2c8e5a18..78aa56874 100644 --- a/kadmos/graph/graph_data.py +++ b/kadmos/graph/graph_data.py @@ -594,10 +594,7 @@ class DataGraph(KadmosGraph): # Merge the nodes in 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) - - # Remove selfloops that arise due to the merging - function_graph.remove_edges_from(function_graph.selfloop_edges()) + function_graph = nx.contracted_nodes(function_graph, 'super_node', function_id, self_loops=False) # Check if all coupled nodes are assigned to a partition if not nx.is_directed_acyclic_graph(function_graph): @@ -622,8 +619,8 @@ class DataGraph(KadmosGraph): for function_id in functions_in_cycle: if function_id != 'super_node': coupled_functions.append(function_id) - function_graph = nx.contracted_nodes(function_graph, 'super_node', function_id) - function_graph.remove_edges_from(function_graph.selfloop_edges()) + function_graph = nx.contracted_nodes(function_graph, 'super_node', function_id, + self_loops=False) # Find a topological function order function_order = list(nx.topological_sort(function_graph)) @@ -641,10 +638,12 @@ class DataGraph(KadmosGraph): partitions[partition] = nodes coupled_functions_order.extend(nodes) self.graph['problem_formulation']['coupled_functions_groups'] = partitions - else: + elif coupled_functions: coupled_functions_order = self.minimize_feedback(coupled_functions, method, multi_start=multi_start, rcb=rcb, use_runtime_info=use_runtime_info, coupling_dict=coupling_dict) + else: + coupled_functions_order = [] # Get post-coupling functions and sort post_coupling_functions = function_order[function_order.index('super_node') + 1:] @@ -783,8 +782,8 @@ class DataGraph(KadmosGraph): coupling_dict = self.get_coupling_dictionary() # Get total number of couplings and total runtime - total_couplings = sum([coupling_dict[function1][function2] for function1 in nodes for function2 in nodes if - function2 in coupling_dict[function1]]) + total_couplings = max(sum([coupling_dict[function1][function2] for function1 in nodes for function2 in nodes if + function2 in coupling_dict[function1]]), 1) total_time = sum([self.nodes[node]['performance_info']['run_time'] for node in nodes]) if use_runtime_info else\ len(nodes) @@ -875,8 +874,8 @@ class DataGraph(KadmosGraph): 'resources') # Get total number of couplings and total runtime - total_couplings = sum([coupling_dict[function1][function2] for function1 in nodes for function2 in nodes if - function2 in coupling_dict[function1]]) + total_couplings = max(sum([coupling_dict[function1][function2] for function1 in nodes for function2 in nodes if + function2 in coupling_dict[function1]]), 1) total_time = sum([self.nodes[node]['performance_info']['run_time'] for node in nodes]) if use_runtime_info \ else len(nodes) @@ -914,8 +913,8 @@ class DataGraph(KadmosGraph): converged = False # Get total number of couplings and total runtime - total_couplings = sum([coupling_dict[function1][function2] for function1 in nodes for function2 in nodes if - function2 in coupling_dict[function1]]) + total_couplings = max(sum([coupling_dict[function1][function2] for function1 in nodes for function2 in nodes if + function2 in coupling_dict[function1]]), 1) total_time = sum([self.nodes[node]['performance_info']['run_time'] for node in nodes]) if use_runtime_info \ else len(nodes) @@ -985,8 +984,8 @@ class DataGraph(KadmosGraph): n_eval = 0 # Get total number of couplings and total runtime - total_couplings = sum([coupling_dict[function1][function2] for function1 in nodes for function2 in nodes if - function2 in coupling_dict[function1]]) + total_couplings = max(sum([coupling_dict[function1][function2] for function1 in nodes for function2 in nodes if + function2 in coupling_dict[function1]]), 1) total_time = sum([self.nodes[node]['performance_info']['run_time'] for node in nodes]) if use_runtime_info \ else len(nodes) @@ -1046,8 +1045,8 @@ class DataGraph(KadmosGraph): n_eval = 0 # Get total number of couplings and maximum runtime of graph - total_couplings = sum([coupling_dict[function1][function2] for function1 in nodes for function2 in nodes if - function2 in coupling_dict[function1]]) + total_couplings = max(sum([coupling_dict[function1][function2] for function1 in nodes for function2 in nodes if + function2 in coupling_dict[function1]]), 1) total_time = sum([self.nodes[node]['performance_info']['run_time'] for node in nodes]) if use_runtime_info \ else len(nodes) @@ -1125,7 +1124,10 @@ class DataGraph(KadmosGraph): return n_feedback, run_time_branch - def _genetic_algorithm(self, nodes, coupling_dict): + # noinspection PyUnresolvedReferences + def _genetic_algorithm(self, nodes, coupling_dict, rcb, use_runtime_info): + + import array from deap import base from deap import creator @@ -1133,18 +1135,27 @@ class DataGraph(KadmosGraph): from deap import algorithms # Settings GA - CXPB = 0.63 - MUTPB = 0.4 - NGEN = 250 - INDPB = 0.11 - pop_size = 300 + cxpb = 0.5 + mutpb = 0.65 + ngen = 750 + indpb = 0.02 + pop_size = 250 # Make mapping of nodes to sequence of integers mapping = copy.deepcopy(nodes) - # Create fitness and individual class - creator.create('FitnessMin', base.Fitness, weights=(-1.0,)) - creator.create('Individual', list, fitness=creator.FitnessMin) + # Get total number of couplings and maximum runtime of graph + total_couplings = max(sum([coupling_dict[function1][function2] for function1 in nodes for function2 in nodes if + function2 in coupling_dict[function1]]), 1) + total_time = sum([self.nodes[node]['performance_info']['run_time'] for node in nodes]) if use_runtime_info \ + else len(nodes) + + # Create fitness class based on the optimization objective (= rcb value) + creator.create('Fitness', base.Fitness, weights=(-0.99, -0.01)) if rcb in [0, 1] else \ + creator.create('Fitness', base.Fitness, weights=(-1.0,)) + + # Create individual class + creator.create('Individual', array.array, typecode='i', fitness=creator.Fitness) # Get toolbox toolbox = base.Toolbox() @@ -1154,10 +1165,11 @@ class DataGraph(KadmosGraph): toolbox.register('individual', tools.initIterate, creator.Individual, toolbox.indices) toolbox.register('population', tools.initRepeat, list, toolbox.individual) toolbox.register('mate', tools.cxOrdered) - toolbox.register('mutate', tools.mutShuffleIndexes, indpb=INDPB) - #toolbox.register('select', tools.selBest) + toolbox.register('mutate', tools.mutShuffleIndexes, indpb=indpb) toolbox.register('select', tools.selTournament, tournsize=3) - toolbox.register('evaluate', self._get_fitness_individual, mapping=mapping, coupling_dict=coupling_dict) + toolbox.register('evaluate', self._get_fitness_individual, mapping=mapping, coupling_dict=coupling_dict, + rcb=rcb, use_runtime_info=use_runtime_info, total_couplings=total_couplings, + total_time=total_time) population = toolbox.population(pop_size) hof = tools.HallOfFame(1) @@ -1166,24 +1178,35 @@ class DataGraph(KadmosGraph): stats.register("std", np.std) stats.register("min", np.min) stats.register("max", np.max) - algorithms.eaSimple(population, toolbox, CXPB, MUTPB, NGEN, halloffame=hof, - verbose=True, stats=stats) + pop, logbook = algorithms.eaSimple(population, toolbox, cxpb, mutpb, ngen, halloffame=hof, verbose=True, + stats=stats) + n_eval = sum([logbook[generation]['nevals'] for generation in range(len(logbook))]) + + # Get best results function_order = [] for idx in hof[0]: function_order.append(mapping[idx]) - return function_order + return function_order, n_eval - def _get_fitness_individual(self, individual, mapping, coupling_dict): + def _get_fitness_individual(self, individual, mapping, coupling_dict, rcb, use_runtime_info, total_couplings, + total_time): """ Function to evaluate the fitness of an individual. Needed for the genetic algorithm """ function_order = [] for idx in individual: function_order.append(mapping[idx]) - feedback, time = self.get_feedback_info(function_order, coupling_dict) + feedback, time = self.get_feedback_info(function_order, coupling_dict, use_runtime_info) - return feedback, + f = rcb * (feedback / float(total_couplings)) + (1 - rcb) * (time / float(total_time)) + + if rcb == 1: + return f, time + elif rcb == 0: + return f, feedback + else: + return f, def get_feedback_info(self, function_order, coupling_dict=None, use_runtime_info=False): """Function to determine the number of feedback loops and a runtime estimation for a given function order @@ -1467,8 +1490,11 @@ class RepositoryConnectivityGraph(DataGraph): self.add_node('D{0}'.format(discipline + 1), category='function', instance=1, function_type='regular') if 'runtime_range' in kwargs: runtime_range = kwargs['runtime_range'] - self.nodes['D{0}'.format(discipline + 1)]['performance_info'] = {'run_time': random.randint( - runtime_range[0], runtime_range[1])} + if runtime_range: + self.nodes['D{0}'.format(discipline + 1)]['performance_info'] = {'run_time': random.randint( + runtime_range[0], runtime_range[1])} + else: + self.nodes['D{0}'.format(discipline + 1)]['performance_info'] = {'run_time': 1} 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') @@ -2760,8 +2786,8 @@ class FundamentalProblemGraph(DataGraph, KeChainMixin): if 'doe_runs' in doe_settings: self.graph['problem_formulation']['doe_settings']['doe_runs'] = doe_settings['doe_runs'] - def partition_graph(self, n_parts, include_run_time=False, tpwgts=None, recursive=True, contig=False, - local_convergers=False): + def partition_graph(self, n_parts, use_runtime_info=False, tpwgts=None, recursive=True, contig=False, + local_convergers=False, coupling_dict=None, rcb=1.0, node_selection=None): """Partition a graph using the Metis algorithm (http://glaros.dtc.umn.edu/gkhome/metis/metis/overview). Note that partitioning can only be performed on undirected graphs. Therefore every graph input is translated @@ -2769,8 +2795,8 @@ class FundamentalProblemGraph(DataGraph, KeChainMixin): :param n_parts: number of partitions requested (algorithm might provide less) :type n_parts: int - :param include_run_time: - :type include_run_time: bool + :param use_runtime_info: + :type use_runtime_info: bool :param tpwgts: list of target partition weights :type tpwgts: list :param recursive: option to use the recursive bisection or k-way partitioning algorithm @@ -2784,160 +2810,229 @@ class FundamentalProblemGraph(DataGraph, KeChainMixin): import metis # Input assertions - assert 'function_ordering' in self.graph['problem_formulation'], 'Function ordering is missing' - pre_couplings = self.graph['problem_formulation']['function_ordering'][self.FUNCTION_ROLES[0]] - coupled_nodes = self.graph['problem_formulation']['function_ordering'][self.FUNCTION_ROLES[1]] - post_couplings = self.graph['problem_formulation']['function_ordering'][self.FUNCTION_ROLES[2]] - if include_run_time: - for node in coupled_nodes: - assert 'run_time' in self.nodes[node]['performance_info'], 'Run time missing for function ' \ - '{}'.format(node) + if not node_selection: + assert 'function_ordering' in self.graph['problem_formulation'], 'Function ordering is missing' - # Get subgraph - if 'system_architecture' in self.graph['problem_formulation'] and \ + # Get coupling dictionary + if not coupling_dict: + coupling_dict = self.get_coupling_dictionary() + + # Get the nodes that need to be partitioned + if node_selection: + nodes_to_partition = list(node_selection) + # For IDF, all functions in the optimizer loop are partitioned except for the objective and constraint functions + elif 'system_architecture' in self.graph['problem_formulation'] and \ self.graph['problem_formulation']['system_architecture'] == self.OPTIONS_ARCHITECTURES[2]: + + # Get post-design-variables, coupled and post-coupling functions + mg_function_ordering = self.get_mg_function_ordering() + post_des_vars = mg_function_ordering[self.FUNCTION_ROLES[4]] + post_couplings = mg_function_ordering[self.FUNCTION_ROLES[2]] + coupled_nodes = mg_function_ordering[self.FUNCTION_ROLES[1]] + + # Get objective and constraint functions constr_obj_vars = self.find_all_nodes(attr_cond=['problem_role', '==', self.PROBLEM_ROLES_VARS[2]]) + \ - self.find_all_nodes(attr_cond=['problem_role', '==', self.PROBLEM_ROLES_VARS[1]]) - constr_obj_funcs = [] - for variable in constr_obj_vars: - constr_obj_funcs.extend(self.get_sources(variable)) - post_coupling_disciplines = [node for node in post_couplings if node not in constr_obj_funcs] - pre_coupling_disciplines = [node for node in pre_couplings if node not in constr_obj_funcs] - subgraph = self.get_subgraph_by_function_nodes(pre_coupling_disciplines + coupled_nodes + - post_coupling_disciplines) + self.find_all_nodes(attr_cond=['problem_role', '==', self.PROBLEM_ROLES_VARS[1]]) + constr_obj_funcs = [self.get_sources(variable)[0] for variable in constr_obj_vars] + + # Get the nodes that need to be partitioned + nodes_to_partition = [node for node in post_des_vars if node not in constr_obj_funcs] + coupled_nodes + \ + [node for node in post_couplings if node not in constr_obj_funcs] + # When a converger is present or no system architecture selected, only the coupled functions are partitioned else: - subgraph = self.get_subgraph_by_function_nodes(coupled_nodes) + function_ordering = self.graph['problem_formulation']['function_ordering'] + coupled_nodes = function_ordering[self.FUNCTION_ROLES[1]] + nodes_to_partition = coupled_nodes - graph = subgraph.deepcopy() - coupling_dict = graph.get_coupling_dictionary() + # Check runtime + if use_runtime_info: + for node in nodes_to_partition: + assert 'run_time' in self.nodes[node]['performance_info'], 'Run time missing for function ' \ + '{}'.format(node) - best_solution = {'partitions': [], 'variables': float("inf"), 'run_time': float("inf")} + # Get initial function graph of the nodes that need to be partitioned + subgraph = self.get_subgraph_by_function_nodes(nodes_to_partition) + initial_function_graph = subgraph.get_function_graph() + + # Get total number of couplings and the maximum runtime of the graph + total_couplings = sum([coupling_dict[function1][function2] for function1 in nodes_to_partition for function2 in + nodes_to_partition if function2 in coupling_dict[function1]]) + total_time = sum([self.nodes[node]['performance_info']['run_time'] for node in nodes_to_partition]) if \ + use_runtime_info else len(nodes_to_partition) + + # Initialize variables + best_partitions = [] + min_f, min_variables, min_time = float("inf"), float("inf"), float("inf") number_of_iterations_not_improved = 0 + function_graph = initial_function_graph.deepcopy() while True: + # Combine coupling strengths of feedforward and feedback connections between two nodes to get an undirected + # graph with the correct edge weights + remove_edges = [] + for edge in function_graph.edges(): + if (edge[0], edge[1]) in remove_edges: + continue + elif (edge[1], edge[0]) in function_graph.edges(): + function_graph.edges[edge[0], edge[1]]['coupling_strength'] += function_graph.edges[ + edge[1], edge[0]]['coupling_strength'] + remove_edges.append((edge[1], edge[0])) + for edge in remove_edges: + function_graph.remove_edge(edge[0], edge[1]) # Get undirected graph - g_und = graph.to_undirected() + g_und = function_graph.to_undirected() - # Add run time to the nodes of the undirected graph + # Add runtime to the nodes of the undirected graph for metis for node in g_und.nodes(): - if g_und.nodes[node]['category'] == 'variable': - g_und.nodes[node]['run_time'] = 0 - elif g_und.nodes[node]['category'] == 'function': - g_und.nodes[node]['run_time'] = g_und.nodes[node]['performance_info']['run_time'] if \ - include_run_time else 1 + g_und.nodes[node]['run_time'] = g_und.nodes[node]['performance_info']['run_time'] if \ + use_runtime_info else 1 + + # Set the runtime as node weight and the coupling strength as edge weight for metis g_und.graph['node_weight_attr'] = 'run_time' + g_und.graph['edge_weight_attr'] = 'coupling_strength' - # Partition graph + # Partition graph using metis (edgecuts, parts) = metis.part_graph(g_und, n_parts, tpwgts=tpwgts, recursive=recursive, contig=contig) - # Get partition dict + # Create a list with the nodes in each partition partitions = [] for part in range(n_parts): + + # Get nodes in this partition nodes = [] - # Get function nodes in partition for idx, node in enumerate(g_und.nodes): - if parts[idx] == part and graph.nodes[node]['category'] == 'function': + if parts[idx] == part: nodes.extend(node.split('--') if '--' in node else [node]) + # Minimize feedback within the partition - graph_partition = self.get_subgraph_by_function_nodes(nodes) - if local_convergers and not nx.is_directed_acyclic_graph(graph_partition): - nodes = self.get_possible_function_order('single-swap', node_selection=nodes) + if not nodes: + logger.warning('Metis returned less than {} partitions. Some partitions will be empty'.format( + n_parts)) else: - nodes = self.minimize_feedback(nodes, 'single-swap') - # Add nodes to the partition dict - partitions.append(nodes) + nodes = self.get_possible_function_order('single-swap', node_selection=nodes, rcb=rcb, + coupling_dict=coupling_dict, + use_runtime_info=use_runtime_info) if local_convergers \ + else self.minimize_feedback(nodes, 'single-swap', rcb=rcb, coupling_dict=coupling_dict, + use_runtime_info=use_runtime_info) - # Reset graph - graph = subgraph.deepcopy() + # Add nodes to the partition list + partitions.append(nodes) # Evaluate the properties of the partitioning - partition_variables, system_variables, runtime = graph.get_partition_info(partitions, - include_run_time=include_run_time) + partition_variables, system_variables, runtime = subgraph.get_partition_info( + partitions, coupling_dict=coupling_dict, use_runtime_info=use_runtime_info, + local_convergers=local_convergers) + n_variables = system_variables + sum(partition_variables) - # Merge nodes that can be merged based on process and calculate runtime of each partition + # Decide whether new solution is better than the best solution found so far + f = rcb * (n_variables / float(total_couplings)) + (1 - rcb) * (max(runtime) / float(total_time)) + if (n_variables == min_variables and max(runtime) < min_time) or \ + (max(runtime) == min_time and n_variables < min_variables) or (f < min_f): + best_partitions, min_f, min_variables, min_time = partitions, f, n_variables, max(runtime) + number_of_iterations_not_improved = 0 + else: + number_of_iterations_not_improved += 1 + + # If the third iteration does not give an improvement the iterations are stopped + if number_of_iterations_not_improved > 2: + break + + # Merge the nodes that can be merged based on process + function_graph = initial_function_graph.deepcopy() for partition in partitions: nodes = list(partition) while nodes: merge_nodes, run_times = [], [] for idx, node in enumerate(nodes): + # If the nodes before the current node do not supply input to the current node, the nodes can + # be merged if not set(nodes[:idx]).intersection(coupling_dict[node]): merge_nodes.append(node) - run_times.append(self.nodes[node]['performance_info']['run_time'] if include_run_time else + run_times.append(self.nodes[node]['performance_info']['run_time'] if use_runtime_info else 1) else: break if len(merge_nodes) > 1: - new_node = '--'.join(merge_nodes) + new_node_label = '--'.join(merge_nodes) try: - graph = graph.merge_parallel_functions(merge_nodes, new_label=new_node) - graph.nodes[new_node]['performance_info'] = {'run_time': max(run_times)} + function_graph = function_graph.merge_parallel_functions(merge_nodes, + new_label=new_node_label) + function_graph.nodes[new_node_label]['performance_info'] = {'run_time': max(run_times)} except AssertionError: pass for node in merge_nodes: nodes.pop(nodes.index(node)) - n_variables = len(system_variables) + sum([len(variables) for variables in partition_variables]) - - # Decide whether new solution is better than the best solution found so far - accept_solution = False - variable_change = abs((best_solution['variables'] - n_variables)/float(best_solution['variables'])) - run_time_change = abs((best_solution['run_time'] - max(runtime))/float(best_solution['run_time'])) - if n_variables <= best_solution['variables']: - if max(runtime) < best_solution['run_time']: - accept_solution = True - else: - if variable_change*1.5 > run_time_change: - accept_solution = True - elif max(runtime) <= best_solution['run_time']: - if run_time_change > variable_change*1.5: - accept_solution = True - - if accept_solution: - best_solution = {'partitions': partitions, 'variables': n_variables, 'run_time': max(runtime)} - number_of_iterations_not_improved = 0 - else: - number_of_iterations_not_improved += 1 - - # Remember current partition - if accept_solution: - best_solution['partitions'] = partitions - best_solution['variables'] = n_variables - best_solution['runtime'] = max(runtime) - - # If the third iteration does not give an improvement the iterations are stopped - if number_of_iterations_not_improved > 2: - break + # Get correct coupling strengths in case merged nodes exist in the graph + for node1 in function_graph.nodes(): + for node2 in function_graph.nodes(): + coupling_strength = 0 + source_nodes = node1.split('--') if '--' in node1 else [node1] + target_nodes = node2.split('--') if '--' in node2 else [node2] + for source in source_nodes: + for target in target_nodes: + if (source, target) in initial_function_graph.edges(): + coupling_strength += initial_function_graph.edges[source, target][ + 'coupling_strength'] + if coupling_strength != 0: + function_graph.edges[node1, node2]['coupling_strength'] = coupling_strength # Add local convergers if there are feedback loops in the partitions convergers = [] if local_convergers: - for part_nr, partition in enumerate(best_solution['partitions']): - subgraph = self.get_subgraph_by_function_nodes(partition) - if not nx.is_directed_acyclic_graph(subgraph): + for part_nr, partition in enumerate(best_partitions): + converger = False + for idx, node in enumerate(partition): + if not set(partition[idx:]).intersection(coupling_dict[node]): + continue + else: + converger = True + if converger: convergers.append(part_nr) - # Update the function order - + # Update the function order # todo: check whether the non partitioned nodes were already in the right sequence? + if node_selection: + function_order = [node for partition in partitions for node in partition] + elif 'system_architecture' in self.graph['problem_formulation'] and \ + self.graph['problem_formulation']['system_architecture'] == self.OPTIONS_ARCHITECTURES[2]: + # noinspection PyUnboundLocalVariable + pre_desvars_order = self.sort_nodes_for_process(mg_function_ordering[self.FUNCTION_ROLES[3]]) + partitioned_nodes_order = [node for partition in partitions for node in partition] + # noinspection PyUnboundLocalVariable + constr_obj_order = self.sort_nodes_for_process(constr_obj_funcs) + function_order = pre_desvars_order + partitioned_nodes_order + constr_obj_order + else: + pre_coupled_order = self.sort_nodes_for_process(function_ordering[self.FUNCTION_ROLES[0]]) + partitioned_nodes_order = [node for partition in partitions for node in partition] + post_coupling_order = self.sort_nodes_for_process(function_ordering[self.FUNCTION_ROLES[2]]) + function_order = pre_coupled_order + partitioned_nodes_order + post_coupling_order - function_order = [] + # Add partition to the input graph + if not 'problem_formulation' in self.graph: + self.graph['problem_formulation'] = dict() - self.graph['problem_formulation']['coupled_functions_groups'] = best_solution['partitions'] + self.graph['problem_formulation']['coupled_functions_groups'] = best_partitions self.graph['problem_formulation']['local_convergers'] = convergers self.graph['problem_formulation']['partition_convergence'] = [] self.graph['problem_formulation']['sequence_partitions'] = [] - self.graph['problem_formulation']['function_order'] = function_order + if not node_selection: + self.graph['problem_formulation']['function_order'] = function_order return - def get_partition_info(self, partitions, include_run_time=False, coupling_dict=None): + def get_partition_info(self, partitions, coupling_dict=None, use_runtime_info=False, local_convergers=False): """ Function to get the number of feedback variables in the partitions and the number system variables (feedback and feedforward variables between partitions) :param partitions: list which indicates which nodes are in which partition :type partitions: list - :param include_run_time: - :type include_run_time: bool + :param coupling_dict: + :type coupling_dict: dict + :param use_runtime_info: + :type use_runtime_info: bool + :param local_convergers: + :param local_convergers: bool :return partition_variables: :rtype partition_variables: list of sets :return system_variables: @@ -2948,7 +3043,7 @@ class FundamentalProblemGraph(DataGraph, KeChainMixin): function_order = [func for partition in partitions for func in partition] # Input assertions - if include_run_time: + if use_runtime_info: for node in function_order: assert 'run_time' in self.nodes[node]['performance_info'], 'Run time missing for function ' \ '{}'.format(node) @@ -2959,46 +3054,57 @@ class FundamentalProblemGraph(DataGraph, KeChainMixin): # For each node in the partitions check whether its input comes from the same partition, another partition or # neither - partition_variables = [set() for _ in partitions] - system_variables = set() + system_connections = 0 + partition_connections = [] for partition, nodes in enumerate(partitions): + partition_feedback = 0 for idx, target in enumerate(nodes): for source in coupling_dict[target]: if source in nodes[idx+1:]: - paths = nx.all_shortest_paths(self, source, target) - for path in paths: - partition_variables[partition].update([path[1].split('/')[-1]]) + partition_feedback += coupling_dict[target][source] elif source in function_order and source not in nodes: - paths = nx.all_shortest_paths(self, source, target) - for path in paths: - system_variables.update([path[1].split('/')[-1]]) + system_connections += coupling_dict[target][source] + partition_connections.append(partition_feedback) - # Calculate run time of each partition + # Calculate runtime # todo: can this more simple? run_time_partitions = [] - for partition in partitions: - nodes = list(partition) - run_time_partition = 0 - while nodes: - parallel_nodes = [] - run_times = [] + for partition, nodes in enumerate(partitions): + if local_convergers and self.check_for_coupling(nodes, only_feedback=True): + # Get runtime pre-coupled nodes + pre_coupling_nodes = [] for idx, node in enumerate(nodes): - if not set(nodes[:idx]).intersection(coupling_dict[node]): - parallel_nodes.append(node) - if include_run_time: - run_times.append(self.nodes[node]['performance_info']['run_time']) - else: - run_times.append(1) + if not set(nodes[idx + 1:]).intersection(coupling_dict[node]): + pre_coupling_nodes = nodes[:idx + 1] + else: + break + # Get runtime post-coupled nodes + nodes = [node for node in nodes if node not in pre_coupling_nodes] + input_nodes = [] + post_coupling_nodes = [] + coupled_nodes = list(nodes) + input_nodes.extend([node for node in coupling_dict[node] if node in nodes[idx:]] for idx, node in + enumerate(nodes)) + for idx, node in reversed(list(enumerate(nodes))): + if node not in input_nodes: + post_coupling_nodes = nodes[idx:] + coupled_nodes = nodes[:idx] else: break - run_time_partition += max(run_times) - for node in parallel_nodes: - nodes.pop(nodes.index(node)) + run_time_partition = self.get_runtime_sequence(pre_coupling_nodes, coupling_dict=coupling_dict, + use_runtime_info=use_runtime_info) + \ + self.get_runtime_sequence(coupled_nodes, coupling_dict=coupling_dict, + use_runtime_info=use_runtime_info) + \ + self.get_runtime_sequence(post_coupling_nodes, coupling_dict=coupling_dict, + use_runtime_info=use_runtime_info) + else: + run_time_partition = self.get_runtime_sequence(nodes, coupling_dict=coupling_dict, + use_runtime_info=use_runtime_info) run_time_partitions.append(run_time_partition) - return partition_variables, system_variables, run_time_partitions + return partition_connections, system_connections, run_time_partitions - def select_number_of_partitions(self, partition_range, include_run_time=False, plot_pareto_front=False, - local_convergers=False): + def select_number_of_partitions(self, partition_range, use_runtime_info=False, plot_pareto_front=False, + local_convergers=False, coupling_dict=None, rcb=1.0): """ Function to evaluate the properties of different number of partitions and to select the best one. :param partition_range: range of number of partitions that need to be evaluated @@ -3013,7 +3119,7 @@ class FundamentalProblemGraph(DataGraph, KeChainMixin): # Input assertions assert 'function_ordering' in self.graph['problem_formulation'], 'Function ordering is missing' coupled_nodes = self.graph['problem_formulation']['function_ordering'][self.FUNCTION_ROLES[1]] - if include_run_time: + if use_runtime_info: for node in coupled_nodes: assert 'run_time' in self.nodes[node]['performance_info'], 'Run time missing for function ' \ '{}'.format(node) @@ -3021,24 +3127,32 @@ class FundamentalProblemGraph(DataGraph, KeChainMixin): partition_info = [] partition_results = dict() + if not coupling_dict: + coupling_dict = self.get_coupling_dictionary() + for idx, n_partitions in enumerate(partition_range): + # Partition graph graph = self.deepcopy() - partitions, convergers = graph.partition_graph(n_partitions, - include_run_time=include_run_time, - local_convergers=local_convergers) + print 'Number of partitions: ', n_partitions + graph.partition_graph(n_partitions, coupling_dict=coupling_dict, use_runtime_info=use_runtime_info, + local_convergers=local_convergers, rcb=rcb) + partitions = graph.graph['problem_formulation']['coupled_functions_groups'] + local_convergers = graph.graph['problem_formulation']['local_convergers'] # Evaluate graph partition_variables, system_variables, runtime = graph.get_partition_info(partitions, - include_run_time=include_run_time) - total_var = len(system_variables) + sum([len(variables) for variables in partition_variables]) + use_runtime_info=use_runtime_info, + coupling_dict=coupling_dict, + local_convergers=local_convergers) + total_var = system_variables + sum(partition_variables) # Save partition information - partition_info.append([idx, n_partitions, [len(variables) for variables in partition_variables], - len(system_variables), total_var, max(runtime)]) + partition_info.append([idx, n_partitions, partition_variables, system_variables, total_var, max(runtime)]) partition_results[n_partitions] = dict() partition_results[n_partitions]['partitions'] = partitions - partition_results[n_partitions]['convergers'] = convergers + partition_results[n_partitions]['local_convergers'] = local_convergers + partition_results[n_partitions]['function_order'] = graph.graph['problem_formulation']['function_order'] # Print partition information in table header = ['Option', '# partitions', '# feedback in partitions', '# system variables', 'Total # variables', @@ -3064,16 +3178,21 @@ class FundamentalProblemGraph(DataGraph, KeChainMixin): plt.show() # Select the number of partitions - selmsg = 'Please slect number of partitions:' + selmsg = 'Please select number of partitions:' sel = prompting.user_prompt_select_options(*partition_range, message=selmsg, allow_empty=False, allow_multi=False) idx = partition_range.index(int(sel[0])) # Get result - partitions = partition_results[partition_range[idx]]['partitions'] - convergers = partition_results[partition_range[idx]]['convergers'] + self.graph['problem_formulation']['coupled_functions_groups'] = partition_results[partition_range[idx]][ + 'partitions'] + self.graph['problem_formulation']['local_convergers'] = partition_results[partition_range[idx]][ + 'local_convergers'] + self.graph['problem_formulation']['partition_convergence'] = [] + self.graph['problem_formulation']['sequence_partitions'] = [] + self.graph['problem_formulation']['function_order'] = partition_results[partition_range[idx]]['function_order'] - return partitions, convergers + return def select_distributed_architecture(self): """ Function for easy selection of a distributed architecture for a partitioned graph. @@ -3093,7 +3212,7 @@ class FundamentalProblemGraph(DataGraph, KeChainMixin): partitions = self.graph['problem_formulation']['coupled_functions_groups'] coupling_dict = self.get_coupling_dictionary() - # Select system architecture + # Select system architecture # todo: remove? System acrhictecture should be given before graph is partitioned system_architectures = ['unconverged-MDA', 'converged-MDA', 'MDF', 'IDF', 'unconverged-OPT'] options = [] for idx, arch in enumerate(system_architectures): @@ -3148,6 +3267,7 @@ class FundamentalProblemGraph(DataGraph, KeChainMixin): continue break + # todo: remove or improve print 'local converger:', local_convergers print 'system architecture:', system_architecture print 'jacobi convergence:', jacobi_convergence -- GitLab