geosolver/geosolver/method.py
kwikrick 4fe301a5e6 Bugfix: Fix-constraint now works.
Implemented a mechanism for setting the root cluster so that cluster-merging
takes place in the correct order. For each cluster, a root variable is 
created, which is used in cluster-merging methods to choose which 
cluster to transform, and which too keep fixed. The root-variable 
of the output cluster is related to root-variables of the input
clusters by adding a logical OR method (OrMethod) to the 
method graph.

Rick
2009-10-06 15:26:25 +00:00

471 lines
14 KiB
Python

"""
Module for method graphs
Copyright Rick van der Meiden, 2003, 2004
Created: 1 Nov 2003.
A method graph contains variables and methods. Methods are objects that
specify input and output variables and an 'execute' method. Whenever the
value of a variable is changed, one or more methods are executed to update
the value of 'upstream' variables.
Changes:
23 Nov 2004 - added Error classes, updated naming and doc conventions (PEP 8, 257)
"""
from graph import Graph
# ----------- misc stuff -----------
def _strseq(seq):
"""print string rep of items in a sequence, seperated by commas.
It realy sucks that str(list/dict) uses the __repr__ method of items
in the list/dict. Ergo, this function.
"""
s = ""
for el in seq:
s += str(el)
s += ','
if len(s) > 0:
s = s[:-1]
return s
# ----------- Exceptions -----------
class ValidityError(Exception):
"""Error indicating operation violated MethodGraph validity"""
def __init__(self, message):
"""Constructor for ValidityError
arguments:
message - message to be displayed
"""
self._message = message
def __str__(self):
return "ValidityError: " + self._message
# ----------- class Method -----------
class Method:
"""Abstract method
A Method is an object that defines input variables, output variables
and an execute method. This class should be considered abstract.
Instances (of subclasses of) Method must be non-mutable, hashable objects.
"""
def inputs(self):
"""return a list of input variables
If an attribute '_inputs' has been defined, a new list
with the contents of that attribute will be returned.
Subclasses may choose to initialise this variable or to
override this function.
"""
if hasattr(self, "_inputs"):
return list(self._inputs)
else:
raise NotImplementedError
def outputs(self):
"""return a list of output variables
If an attribute '_outputs' has been defined, a new list
with the contents of that attribute will be returned.
Subclasses may choose to initialise this variable or to
override this function.
"""
if hasattr(self, "_outputs"):
return list(self._outputs)
else:
raise NotImplementedError
def execute(self, inmap):
"""Execute method.
Returns a mapping (dictionary) of output variables to values,
given an input map, mapping input variables to values (dictionary)
The previous value of the output variable should also be in inmap.
If the method cannot be executed, it should return an empty map.
"""
raise NotImplementedError
# ----------- class MethodGraph -------
class MethodGraph:
"""Implementation of a method graph
A method graph is represented by
a directed bi-partite graph: nodes are either varables or methods.
Edges run from input variables to methods and from methods to ouput
variables.
A method graph may not contain cycles. Every variable must be
determined by at most one constraint.
Methods must be instances of (subclasses of) class Method.
Variables are basically just names, and may be any
non-mutable, hashable object, e.g. strings.
Values associated with variables may be of any type.
If no value is explicitly associated with a variable, it defaults to None.
"""
def __init__(self):
self._map = {}
"""A map from variable names to values"""
self._methods = {}
"""A set of methods"""
self._graph = Graph()
"""A graph for fast navigation"""
self._changed = {}
"""Set of changed variables since last propagation"""
def variables(self):
"""return a list of variables"""
return self._map.keys()
def methods(self):
"""return a list of methods"""
return self._methods.keys()
def contains(self, object):
return self._graph.has_vertex(object)
def add_variable(self, varname, value = None):
"""Add a variable, optionally with a value"""
if not varname in self._map:
self._map[varname] = value
self._graph.add_vertex(varname)
def rem_variable(self, varname):
"""Remove a variable and all methods on that variable"""
if varname in self._map:
del self._map[varname]
if varname in self._changed:
del self._changed[varname]
# delete al methods on it
for met in self._graph.ingoing_vertices(varname):
self.rem_method(met)
for met in self._graph.outgoing_vertices(varname):
self.rem_method(met)
# remove it from graph
self._graph.rem_vertex(varname)
else:
raise StandardError, "variable not in graph"
# end rem variable
def get(self,varname):
"""get the value of a variable"""
return self._map[varname]
def set(self, varname, value, prop = True):
"""Set the value of a variable.
Iff prop is true then this change and any pending
changes will be propagated.
"""
self._map[varname] = value
self._changed[varname] = 1
if prop:
self.propagate()
def add_method(self, met, prop = True):
"""Add a method.
Iff prop is true then this change and any pending
changes will be propagated.
"""
if met in self._methods:
return
self._methods[met] = 1
# update graph
for var in met.inputs():
self.add_variable(var)
self._graph.add_edge(var, met)
for var in met.outputs():
self.add_variable(var)
self._graph.add_edge(met, var)
# check validity of graph
for var in met.outputs():
if len(self._graph.ingoing_vertices(var)) > 1:
self.rem_method(met)
raise ValidityError, "variable "+str(var)+" determined by multiple methods"
elif len(self._graph.path(var, var)) != 0:
self.rem_method(met)
raise ValidityError, "cylce in graph not allowed (variable "+str(var)+")"
# end for
if prop:
self._execute(met)
self.propagate()
def rem_method(self, met):
"""Remove a method"""
if met in self._methods:
del self._methods[met]
self._graph.rem_vertex(met)
else:
raise StandardError, "method not in graph"
def propagate(self):
"""Propagate any pending changes.
Changes are propagated until no changes are left or until
no more changes can be propagated. This method is called
from set() and add_method() by default. However, if the
user so chooses, the methods will not call propagate, and
the user should call this fucntion at a convenient time.
"""
while len(self._changed) != 0:
pick = self._changed.keys()[0]
methods = self._graph.outgoing_vertices(pick)
for met in methods:
self._execute(met)
#end for
if pick in self._changed:
del self._changed[pick]
#end while
#end def propagate
def clear(self):
"""clear methodgraph by removing all variables"""
while (len(self._map) > 0):
var = self._map.keys()[0]
self.rem_variable(var)
#wend
#def
def execute(self, met):
"""Execute a method and proagate changes. Method must be in Methodgraph"""
if met in self._methods:
self._execute(met)
self.propagate()
else:
raise StandardError, "method not in graph"
def _execute(self, met):
"""Execute a method.
Method is executed only if all inputvariable values are not None
Updates mapping and change flags.
"""
# create input map and check for None-values
inmap = {}
hasNoneValues = False
for var in met.inputs():
value = self._map[var]
if value == None:
hasNoneValues = True
inmap[var] = value
for var in met.outputs():
inmap[var] = self._map[var]
# call method.execute
if hasNoneValues:
outmap = {}
else:
outmap = met.execute(inmap)
# update values in self._map
# set output variables changed
for var in met.outputs():
if var in outmap:
self._map[var] = outmap[var]
self._changed[var] = 1
else:
if self._map[var] != None:
self._changed[var] = 1
self._map[var] = None
#end for
# clear change flag on input variables
for var in met.inputs():
if var in self._changed:
del self._changed[var]
#end for
# end def execute
def __str__(self):
s = "MethodGraph(variables=["
s += _strseq(self._map.keys())
s += "], methods=["
s += _strseq(self._methods.keys())
s += "])"
return s;
# end class MethodGraph
# ----------- various Methods ---------
class OrMethod(Method):
def __init__(self, inputs, output):
"""new method output := input[0] | input[1] | ... """
self._inputs = list(inputs)
self._outputs = [output]
def execute(self, inmap):
result = False
for input in self._inputs:
result = result | inmap[input]
outmap = {}
outmap[self._outputs[0]] = result
return outmap
def __str__(self):
s = "OrMethod("
s += str(self._inputs[0])
s += ','
s += str(self._inputs[1])
s += ','
s += str(self._outputs[0])
s += ')'
return s
class AddMethod(Method):
def __init__(self, a, b, c):
"""new method c := a + b"""
self._inputs = [a,b]
self._outputs = [c]
def execute(self, inmap):
outmap = {}
a = self._inputs[0]
b = self._inputs[1]
c = self._outputs[0]
if a in inmap and b in inmap and \
inmap[a] != None and inmap[b] != None:
outmap[c] = inmap[a] + inmap[b]
#fi
return outmap
def __str__(self):
s = "AddMethod("
s += str(self._inputs[0])
s += ','
s += str(self._inputs[1])
s += ','
s += str(self._outputs[0])
s += ')'
return s
class SubMethod(Method):
def __init__(self, a, b, c):
"""new method c := a - b"""
self._inputs = [a,b]
self._outputs = [c]
def execute(self, inmap):
outmap = {}
a = self._inputs[0]
b = self._inputs[1]
c = self._outputs[0]
if a in inmap and b in inmap and \
inmap[a] != None and inmap[b] != None:
outmap[c] = inmap[a] - inmap[b]
#fi
return outmap
def __str__(self):
s = "SubMethod("
s += str(self._inputs[0])
s += ','
s += str(self._inputs[1])
s += ','
s += str(self._outputs[0])
s += ')'
return s
class SetMethod(Method):
def __init__(self, var, value):
"""new method var := value
keyword arguments:
var - variable name
value - any object to be associated with var
"""
self._inputs = []
self._outputs = [var]
self._value = value
def execute(self, inmap):
return {self._outputs[0]:self._value}
def __str__(self):
s = "SetMethod("
s += str(self._outputs[0])
s += ','
s += str(self._value)
s += ')'
return s
class AssignMethod(Method):
def __init__(self, a, b):
"""new method a := b
keyword arguments:
a - variable name
b - variable name
"""
self._inputs = [b]
self._outputs = [a]
def execute(self, inmap):
if self._inputs[0] in inmap:
return {self._outputs[0]:inmap(self._inputs[0])}
else:
return {}
def __str__(self):
s = "SetMethod("
s += str(self._inputs[0])
s += ','
s += str(self._value)
s += ')'
return s
# ---------- test ----------
def test():
print "-- testing method graph"
mg = MethodGraph()
print "set a = 3"
mg.add_variable('a', 3)
print "set b = 4"
mg.add_variable('b', 4)
print "c := a + b"
mg.add_method(AddMethod('a','b','c'))
print "c = "+str(mg.get('c'))
print "set a = 10"
mg.set('a', 10)
print "c = "+str(mg.get('c'))
mg.add_method(AddMethod('a','c','d'))
print "d := a + c"
mg.add_method(AddMethod('b','d','e'))
print "e := b + d"
print "d = "+str(mg.get('d'))
print "e = "+str(mg.get('e'))
print "a := d + e"
import sys, traceback
try:
mg.add_method(AddMethod('d','e','a'))
print "success: should not be possible"
except Exception, e:
print e
print "e := a + b"
try:
mg.add_method(AddMethod('a','b','e'))
print "success: should not be possible"
except Exception, e:
print e
if __name__ == "__main__":
test()