Skip to content

Commit

Permalink
Move common operations out of rule matching.
Browse files Browse the repository at this point in the history
Only renumber the graph once.
Cache the nodes and edges that match a given tag.
  • Loading branch information
mgritter committed Dec 13, 2019
1 parent 5420be1 commit 07ab4a5
Show file tree
Hide file tree
Showing 6 changed files with 414 additions and 41 deletions.
13 changes: 11 additions & 2 deletions soffit/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,16 @@ def chooseAndApply( grammar, graph, timing = None, verbose = False,
ruleAttemptOrder = random.sample( grammar.rules, nRules )

rule_count = 0


# Only convert the graph once!
# FIXME: could we do it even less often than that, maybe carry over from
# one interaction to the next?
graph = nx.convert_node_labels_to_integers( graph, label_attribute="orig" )
for n in graph:
graph.node[n]['orig'] = n
graph.graph['node_tag_cache'] = {}
graph.graph['edge_tag_cache'] = {}

for r in ruleAttemptOrder:
left = r.leftSide()
for right in r.rightSide():
Expand All @@ -106,7 +115,7 @@ def chooseAndApply( grammar, graph, timing = None, verbose = False,
(left, right, graph) = makeAllDirected( left, right, graph )

start = time.time()
finder = MatchFinder( graph )
finder = MatchFinder( graph, already_labeled = True )
if pick_first:
finder.maxMatches = 1
finder.leftSide( left )
Expand Down
169 changes: 168 additions & 1 deletion soffit/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# soffit/constraint.py
#
# Copyright 2018 Mark Gritter
# Copyright 2018-2019 Mark Gritter
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -19,6 +19,173 @@

from constraint import Constraint, Unassigned, Domain

# These two Constraint implementations are much, much slower than using
# TupleConstraint. I don't understand why.
# Profiling shows a lot of hideValue calls by the Edge constraint.

class NodeTagConstraint(Constraint):
"""Given a graph and a tag, make sure any variables identify a node
with matching tag. This seems more efficient than enumerating all the
nodes to find one up front, at least in some cases."""
def __init__( self, graph, tag ):
self.graph = graph
self.tag = tag
self.mismatch_cache = set()

def preProcess(self, variables, domains, constraints, vconstraints):
if 'node_tag_cache' not in self.graph.graph:
return

cache = self.graph.graph['node_tag_cache']
if self.tag not in cache:
nodes = []
for n,t in self.graph.nodes( data="tag" ):
if t == self.tag:
nodes.append( n )
cache[self.tag] = nodes
else:
nodes = cache[self.tag]

for v in variables:
domains[v] = Domain( nodes )

def __call__(self, variables, domains, assignments, forwardcheck=False):
current = [ (v,assignments.get( v, Unassigned )) for v in variables ]

for v,n in current:
if n is not Unassigned:
if n not in self.graph.nodes:
self.mismatch_cache.add( n )
return False
if self.graph.nodes[n].get( 'tag', None ) != self.tag:
self.mismatch_cache.add( n )
return False
elif forwardcheck:
domainValues = set( domains[v] )
for x in self.mismatch_cache.intersection( domainValues ):
domains[v].hideValue( x )

return True

class EdgeTagConstraint(Constraint):
"""Given a graph and a tag, make sure any pairs of variables identify an
edge with matching tag."""
def __init__( self, graph, tag ):
self.graph = graph
self.tag = tag

def preProcess(self, variables, domains, constraints, vconstraints):
if 'edge_tag_cache' not in self.graph.graph:
return

cache = self.graph.graph['edge_tag_cache']
if self.tag not in cache:
edges = []
e_0 = set()
e_1 = set()
for i,j,t in self.graph.edges( data="tag" ):
if t == self.tag:
e_0.add( i )
e_1.add( j )
edges.append( (i,j) )
cache[self.tag] = edges
else:
# FIXME: would it be better to switch to representing edges as
# edge variables, instead of a pair of node variables?
edges = cache[self.tag]
e_0 = set( e[0] for e in edges )
e_1 = set( e[1] for e in edges )

vb = iter( variables )
for i, j in zip( vb, vb ):
if self.graph.is_directed():
domains[i] = Domain( e_0 )
domains[j] = Domain( e_1 )
else:
all_nodes = e_0.union( e_1 )
domains[i] = Domain( all_nodes )
domains[j] = Domain( all_nodes )


def edge_view( self, i, j ):
if self.graph.is_directed():
if i is Unassigned:
for (i2,j2,t2) in self.graph.in_edges( nbunch=[j], data='tag' ):
if t2 == self.tag:
yield (i2,j2)
elif j is Unassigned:
for (i2,j2,t2) in self.graph.out_edges( nbunch=[i], data='tag' ):
if t2 == self.tag:
yield (i2,j2)
else:
assert False
else:
if i is Unassigned:
for (i2,j2,t2) in self.graph.edges( nbunch=[j], data='tag' ):
if t2 == self.tag:
if j2 == j:
yield (i2,j2)
else:
yield (j2,i2)
elif j is Unassigned:
for (i2,j2,t2) in self.graph.edges( nbunch=[i], data='tag' ):
if t2 == self.tag:
if i2 == i:
yield (i2,j2)
else:
yield (j2,i2)
else:
assert False

def __call__(self, variables, domains, assignments, forwardcheck=False):
assert len(variables) % 2 == 0

current = iter( (v, assignments.get( v, Unassigned ))
for v in variables )

for (v_i,i),(v_j,j) in zip( current, current ):
if i is not Unassigned and j is not Unassigned:
if (i,j) not in self.graph.edges:
return False
if self.graph.edges[(i,j)].get( 'tag', None ) != self.tag:
return False
else:
if i is Unassigned:
prev_i = set( domains[v_i] )
allowed_i = set()
for (i2,j2) in self.edge_view( i, j ):
allowed_i.add( i2 )
if not forwardcheck:
return True

if len( allowed_i ) == 0:
return False

if forwardcheck:
for i2 in prev_i.difference( allowed_i ):
domains[v_i].hideValue( i2 )
if not domains[v_i]:
return False

if j is Unassigned:
prev_j = set( domains[v_j] )
allowed_j = set()
for (i2,j2) in self.edge_view( i, j ):
allowed_j.add( j2 )
if not forwardcheck:
return True

if len( allowed_j ) == 0:
return False

if forwardcheck:
for j2 in prev_j.difference( allowed_j ):
domains[v_j].hideValue( j2 )
if not domains[v_j]:
return False

return True

class TupleConstraint(Constraint):
"""Provided a collection of tuples, verify that the variable values
appear as a tuple (in the order specified for the variables.)."""
Expand Down
92 changes: 60 additions & 32 deletions soffit/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# soffit/graph.py
#
# Copyright 2018 Mark Gritter
# Copyright 2018-2019 Mark Gritter
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -87,13 +87,24 @@ class MatchFinder(object):
"""
An object which finds matches for graph grammar rules.
"""
def __init__( self, graph, verbose = False ):
def __init__( self, graph, verbose = False, already_labeled = False ):
"""Initialize the finder with the graph in which matches are to
be found."""

self.originalGraph = graph

# Relabel for compactness, nodes numbered 0..(n-1)
self.graph = nx.convert_node_labels_to_integers( graph, label_attribute="orig" )
if already_labeled:
self.graph = graph
else:
self.graph = nx.convert_node_labels_to_integers( graph, label_attribute="orig" )
self.graph.graph['node_tag_cache'] = {}
self.graph.graph['edge_tag_cache'] = {}

# FIXME: track number of misses so we can tell if it's worth
# walking the whole graph?
# self.graph.graph['node_tag_misses'] = {}
# self.graph.graph['edge_tag_misses'] = {}

self.model = Problem()
self.impossible = False
Expand All @@ -108,6 +119,24 @@ def __init__( self, graph, verbose = False ):
def checkCompatible( self, lr ):
if nx.is_directed( self.graph ) != nx.is_directed( lr ):
raise MatchError( "Convert both graphs to directed first." )

def nodesForTag( self, tag ):
if 'node_tag_cache' in self.graph.graph and tag in self.graph.graph['node_tag_cache']:
nodes_matching_tag = self.graph.graph['node_tag_cache'][tag]
else:
nodes_matching_tag = [ (n,) for n,t in self.graph.nodes( data="tag" ) if t == tag ]
if 'node_tag_cache' in self.graph.graph:
self.graph.graph['node_tag_cache'][tag] = nodes_matching_tag
return nodes_matching_tag

def edgesForTag( self, tag ):
if 'edge_tag_cache' in self.graph.graph and tag in self.graph.graph['edge_tag_cache']:
edges_matching_tag = self.graph.graph['edge_tag_cache'][tag]
else:
edges_matching_tag = [ (i,j) for i,j,t in self.graph.edges( data="tag" ) if t == tag ]
if 'edge_tag_cache' in self.graph.graph:
self.graph.graph['edge_tag_cache'][tag] = edges_matching_tag
return edges_matching_tag

def leftSide( self, leftGraph ):
"""Specify the left side of a rule; that is, a graph to match."""
Expand All @@ -123,46 +152,45 @@ def leftSide( self, leftGraph ):
for n in leftGraph.nodes:
self.model.addVariable( n, range( 0, maxVertex + 1 ) )

# Add a contraint to only assign to nodes with identical tag.
# FIXME: no tag is *not* a wildcard, does that match expectations?
tag = leftGraph.nodes[n].get( 'tag', None )
matchingTag = [ (i,) for i in self.graph.nodes
if self.graph.nodes[i].get( 'tag', None ) == tag ]
if self.verbose:
print( "node", n, "matchingTag", matchingTag )
if len( matchingTag ) == 0:
self.impossible = True
return

# If node choice is unconstrained, don't bother adding it as
# a constraint!
if len( matchingTag ) != len( self.graph.nodes ):
self.model.addConstraint( TupleConstraint( matchingTag ),
[n] )
if False:
# Add a contraint to only assign to nodes with identical tag.
self.model.addConstraint( NodeTagConstraint( self.graph, tag ), [n] )
else:
nodes_matching_tag = self.nodesForTag( tag )
if self.verbose:
print( "Nodes matching", tag, nodes_matching_tag )
if len( nodes_matching_tag ) == 0:
self.impossible = True
return

if len( nodes_matching_tag ) != len( self.graph.nodes ):
self.model.addConstraint( TupleConstraint( nodes_matching_tag ), [n] )

self.model.addConstraint( AllDifferentConstraint(), list( leftGraph.nodes ) )

# Add an allowed assignment for each edge that must be matched,
# again limiting to just exact matching tags.
for (a,b) in leftGraph.edges:
tag = leftGraph.edges[a,b].get( 'tag', None )

matchingTag = [ i for i in self.graph.edges
if self.graph.edges[i].get( 'tag', None ) == tag ]
if self.verbose:
print( "edge", (a,b), "matchingTag", matchingTag )
if len( matchingTag ) == 0:
self.impossible = True
return

# Allow reverse matches if the graph is undirected
if not nx.is_directed( leftGraph ):
revEdges = [ (b,a) for (a,b) in matchingTag ]
matchingTag += revEdges
if False:
self.model.addConstraint( EdgeTagConstraint( self.graph, tag ), [a,b] )
else:
edges_matching_tag = self.edgesForTag( tag )
if self.verbose:
print( "Edges matching", tag, edges_matching_tag )
if len( edges_matching_tag ) == 0:
self.impossible = True
return
if not nx.is_directed( leftGraph ):
revEdges = [ (b,a) for (a,b) in edges_matching_tag ]
edges_matching_tag += revEdges

self.model.addConstraint( TupleConstraint( matchingTag ),
[ a, b ] )

self.model.addConstraint( TupleConstraint( edges_matching_tag ), [a,b] )

def addConditionalTupleConstraint( self, first, rest, variables ):
if self.currentConstraintVariables != variables:
self.finishTupleConstraint()
Expand Down
Loading

0 comments on commit 07ab4a5

Please sign in to comment.