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