2Extended explanation of the RelationalGraph class:
4The RelationalGraph class is used to generate a graph of the relationships between the DALs in the configuration file.
5The class is used to generate a topological ordering of the DALs, and to calculate the longest path in the graph.
6The reasoning is that the configuration should naturally group similar objects together based on how far they are from the session object
8Current this is only really used to find "top level" objects
13from numpy.typing
import NDArray
14from collections
import deque
16from daqconf.cider.data_structures.configuration_handler
import ConfigurationHandler
19 def __init__(self, config_handler: ConfigurationHandler):
25 """Generates adjacency matrix from configuration handler object i.e. finds connected DALs
29 for i, dal
in enumerate(self.
_handler.conf_obj_list):
30 for connection_category
in self.
_handler.get_relationships_for_conf_object(dal):
32 for connection
in list(connection_category.values())[0]:
48HW: Leaving here for now for posterity, this is a great way to organise a FIXED configuration
49but is horribly slow for constant updates
52 def __init__(self, config_handler: ConfigurationHandler):
53 """Construct relational graph
56 config_handler -- ConfigurationHandler object
59 # Configuration handler
60 self._handler = config_handler
63 def generate_graph(self):
64 # Matrices etc. we require [maybe don't need to be defined at the constructor level, could be
66 self._topological_ordered_matrix = np.array([[]])
68 # Adjacency matrix has 1 for direct connection, 0 for no connection
69 self._adjacency_matrix = np.zeros((self._handler.n_dals, self._handler.n_dals))
70 # Maximum distance from the "top level" to a given node
71 self._max_distance = np.zeros(self._handler.n_dals)-np.inf
74 self.__generate_adjacency_matrix()
75 # Sort topologically and get longest paths
76 self.__calculate_longest_paths()
79 def __generate_adjacency_matrix(self):
80 """Generates adjacency matrix from configuration handler object i.e. finds connected DALs
82 for i, dal in enumerate(self._handler.conf_obj_list):
83 for connection_category in self._handler.get_relationships_for_conf_object(dal):
84 # Allows for multiply connected nodes
85 for connection in list(connection_category.values())[0]:
86 # Loop over just conf objects
87 self._adjacency_matrix[i][self._handler.conf_obj_list.index(connection)] += 1
90 def __compute_degree(self):
91 """Get number of incoming nodes for each node"""
92 return np.sum(self._adjacency_matrix !=0, axis=0)
94 def __get_topological_order(self):
96 Topological sort of the adjacency graph
98 Algorithm implementation roughly based on: https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
101 in_degree = self.__compute_degree()
102 queue = deque(np.where(in_degree == 0)[0])
103 topological_ordering = []
106 node = queue.popleft()
107 # Add node to topological ordering
108 topological_ordering.append(node)
109 # Check out going edges
110 outgoing_edges = np.where(self._adjacency_matrix != 0)[0]
111 # Reduce the number of incoming edges for each outgoing edge
112 in_degree[outgoing_edges] -= 1
113 # Change the number of nodes with no outgoing edges
114 zero_in_degree_nodes = outgoing_edges[in_degree[outgoing_edges] == 0]
116 queue.extend(zero_in_degree_nodes)
118 return topological_ordering
120 def __update_distances(self, distance: NDArray, node_id: int):
121 """Update maximum distance to each node
124 distance -- List of distancezs to each node
125 node_id -- ID of a node
127 outgoing_edges = np.where(self._adjacency_matrix[node_id] != 0)[0]
128 distance[outgoing_edges] = np.maximum(distance[outgoing_edges], distance[node_id] + self._adjacency_matrix[node_id][outgoing_edges])
130 def __longest_path(self, start_id: int):
131 """Calculate the longest path in a DAG from the start node."""
132 dist = np.full(self._handler.n_dals, -np.inf)
135 for u in self._topological_ordered_matrix:
136 if dist[u] != -np.inf:
137 self.__update_distances(dist, u)
140 def __calculate_longest_paths(self)->None:
142 Idea is to find shortest paths on -G for each top level node where G is the connection graph.
143 Layer each item lives on is then simply max(longest_path) for each top level item
146 self._topological_ordered_matrix = self.__get_topological_order()
147 for node_id in range(self._handler.n_dals):
148 self._max_distance = np.maximum(self._max_distance, self.__longest_path(node_id))
150 self._max_distance = self._max_distance.astype(int)
152 def add_node(self, node_dal):
155 def remove_node(self, node_dal):
159 def top_level_nodes(self):
160 # Means we automatically rebuild the graph
161 if len(self._max_distance!=len(self._handler.conf_obj_list)):
162 self.generate_graph()
164 return [dal for i, dal in enumerate(self._handler.conf_obj_list) if self._max_distance[i]==0]
__generate_adjacency_matrix(self)
NDArray adjacency_matrix(self)
__init__(self, ConfigurationHandler config_handler)