[graph] compute and store nodes depths on graph topology change

This commit is contained in:
Yann Lanthony 2018-01-05 12:42:55 +01:00
parent b97090430c
commit b3ab554bc9

View file

@ -1038,6 +1038,23 @@ class Visitor:
pass pass
def changeTopology(func):
"""
Graph methods modifying the graph topology (add/remove edges or nodes)
must be decorated with 'changeTopology' for update mechanism to work as intended.
"""
def decorator(self, *args, **kwargs):
assert isinstance(self, Graph)
# call method
result = func(self, *args, **kwargs)
# mark graph dirty
self.dirtyTopology = True
# request graph update
self.update()
return result
return decorator
class Graph(BaseObject): class Graph(BaseObject):
""" """
_________________ _________________ _________________ _________________ _________________ _________________
@ -1059,6 +1076,8 @@ class Graph(BaseObject):
self.name = name self.name = name
self._updateEnabled = True self._updateEnabled = True
self._updateRequested = False self._updateRequested = False
self.dirtyTopology = False
self._nodesMinMaxDepths = {}
self._nodes = DictModel(keyAttrName='name', parent=self) self._nodes = DictModel(keyAttrName='name', parent=self)
self._edges = DictModel(keyAttrName='dst', parent=self) # use dst attribute as unique key since it can only have one input connection self._edges = DictModel(keyAttrName='dst', parent=self) # use dst attribute as unique key since it can only have one input connection
self._cacheDir = meshroom.core.defaultCacheFolder self._cacheDir = meshroom.core.defaultCacheFolder
@ -1102,6 +1121,7 @@ class Graph(BaseObject):
self.update() self.update()
self._updateRequested = False self._updateRequested = False
@changeTopology
def _addNode(self, node, uniqueName): def _addNode(self, node, uniqueName):
""" """
Internal method to add the given node to this Graph, with the given name (must be unique). Internal method to add the given node to this Graph, with the given name (must be unique).
@ -1142,6 +1162,7 @@ class Graph(BaseObject):
""" Return the list of edges starting from this node """ """ Return the list of edges starting from this node """
return [edge for edge in self.edges if edge.src.node == node] return [edge for edge in self.edges if edge.src.node == node]
@changeTopology
def removeNode(self, nodeName): def removeNode(self, nodeName):
""" """
Remove the node identified by 'nodeName' from the graph Remove the node identified by 'nodeName' from the graph
@ -1224,6 +1245,7 @@ class Graph(BaseObject):
nodesWithOutput = set([edge.src.node for edge in self.edges]) nodesWithOutput = set([edge.src.node for edge in self.edges])
return set(self._nodes) - nodesWithOutput return set(self._nodes) - nodesWithOutput
@changeTopology
def addEdge(self, srcAttr, dstAttr): def addEdge(self, srcAttr, dstAttr):
assert isinstance(srcAttr, Attribute) assert isinstance(srcAttr, Attribute)
assert isinstance(dstAttr, Attribute) assert isinstance(dstAttr, Attribute)
@ -1235,7 +1257,6 @@ class Graph(BaseObject):
self.edges.add(edge) self.edges.add(edge)
dstAttr.valueChanged.emit() dstAttr.valueChanged.emit()
dstAttr.isLinkChanged.emit() dstAttr.isLinkChanged.emit()
self.update()
return edge return edge
def addEdges(self, *edges): def addEdges(self, *edges):
@ -1243,21 +1264,26 @@ class Graph(BaseObject):
for edge in edges: for edge in edges:
self.addEdge(*edge) self.addEdge(*edge)
@changeTopology
def removeEdge(self, dstAttr): def removeEdge(self, dstAttr):
if dstAttr not in self.edges.keys(): if dstAttr not in self.edges.keys():
raise RuntimeError('Attribute "{}" is not connected'.format(dstAttr.fullName())) raise RuntimeError('Attribute "{}" is not connected'.format(dstAttr.fullName()))
self.edges.pop(dstAttr) self.edges.pop(dstAttr)
dstAttr.valueChanged.emit() dstAttr.valueChanged.emit()
dstAttr.isLinkChanged.emit() dstAttr.isLinkChanged.emit()
self.update()
def getDepth(self, node): def getDepth(self, node):
# TODO: would be better to use bfs instead of recursive function """ Return node's (max) depth in this Graph.
inputEdges = self.getInputEdges(node)
if not inputEdges: Args:
return 0 node (Node): the node to consider.
inputDepths = [e.src.node.depth for e in inputEdges] Returns:
return max(inputDepths) + 1 int: the node's max depth in this Graph.
"""
assert node.graph == self
assert not self.dirtyTopology
minDepth, maxDepth = self._nodesMinMaxDepths[node]
return maxDepth
def getInputEdges(self, node): def getInputEdges(self, node):
return set([edge for edge in self.edges if edge.dst.node is node]) return set([edge for edge in self.edges if edge.dst.node is node])
@ -1272,36 +1298,35 @@ class Graph(BaseObject):
def dfs(self, visitor, startNodes=None, longestPathFirst=False): def dfs(self, visitor, startNodes=None, longestPathFirst=False):
nodeChildren = self._getInputEdgesPerNode() nodeChildren = self._getInputEdgesPerNode()
minMaxDepthPerNode = self.minMaxDepthPerNode() if longestPathFirst else None # Initialize color map
colors = {} colors = {}
for u in self._nodes: for u in self._nodes:
colors[u] = WHITE colors[u] = WHITE
if startNodes:
if longestPathFirst:
startNodes = sorted(startNodes, key=lambda item: item.depth)
for startNode in startNodes:
self.dfsVisit(startNode, visitor, colors, nodeChildren, longestPathFirst, minMaxDepthPerNode)
else:
leaves = self.getLeaves()
if longestPathFirst:
leaves = sorted(leaves, key=lambda item: item.depth)
for u in leaves:
if colors[u] == WHITE:
self.dfsVisit(u, visitor, colors, nodeChildren, longestPathFirst, minMaxDepthPerNode)
def dfsVisit(self, u, visitor, colors, nodeChildren, longestPathFirst, minMaxDepthPerNode): nodes = startNodes or self.getLeaves()
if longestPathFirst:
# Graph topology must be known and node depths up-to-date
assert not self.dirtyTopology
nodes = sorted(nodes, key=lambda item: item.depth)
for node in nodes:
self.dfsVisit(node, visitor, colors, nodeChildren, longestPathFirst)
def dfsVisit(self, u, visitor, colors, nodeChildren, longestPathFirst):
colors[u] = GRAY colors[u] = GRAY
visitor.discoverVertex(u, self) visitor.discoverVertex(u, self)
# d_time[u] = time = time + 1 # d_time[u] = time = time + 1
children = nodeChildren[u] children = nodeChildren[u]
if longestPathFirst: if longestPathFirst:
children = sorted(children, reverse=True, key=lambda item: minMaxDepthPerNode[item][1]) assert not self.dirtyTopology
children = sorted(children, reverse=True, key=lambda item: self._nodesMinMaxDepths[item][1])
for v in children: for v in children:
visitor.examineEdge((u, v), self) visitor.examineEdge((u, v), self)
if colors[v] == WHITE: if colors[v] == WHITE:
visitor.treeEdge((u, v), self) visitor.treeEdge((u, v), self)
# (u,v) is a tree edge # (u,v) is a tree edge
self.dfsVisit(v, visitor, colors, nodeChildren, longestPathFirst, minMaxDepthPerNode) # TODO: avoid recursion self.dfsVisit(v, visitor, colors, nodeChildren, longestPathFirst) # TODO: avoid recursion
elif colors[v] == GRAY: elif colors[v] == GRAY:
visitor.backEdge((u, v), self) visitor.backEdge((u, v), self)
pass # (u,v) is a back edge pass # (u,v) is a back edge
@ -1362,8 +1387,11 @@ class Graph(BaseObject):
""" """
Compute the min and max depth for each node. Compute the min and max depth for each node.
:param startNodes: list of starting nodes. Use all leaves if empty. Args:
:return: {node: (minDepth, maxDepth)} startNodes: list of starting nodes. Use all leaves if empty.
Returns:
dict: {node: (minDepth, maxDepth)}
""" """
depthPerNode = {} depthPerNode = {}
for node in self.nodes: for node in self.nodes:
@ -1490,6 +1518,13 @@ class Graph(BaseObject):
# To do the update once for multiple changes # To do the update once for multiple changes
self._updateRequested = True self._updateRequested = True
return return
# Graph topology has changed
if self.dirtyTopology:
# Update nodes depths cache
self._nodesMinMaxDepths = self.minMaxDepthPerNode()
self.dirtyTopology = False
self.updateInternals() self.updateInternals()
if os.path.exists(self._cacheDir): if os.path.exists(self._cacheDir):
self.updateStatusFromCache() self.updateStatusFromCache()