commit d5966e21f133072b661378be5d4223e0449bd890 Author: Arjan Molenaar Date: Sun Mar 19 19:57:19 2006 +0000 Moved Gaphor canvas (Gaphas in the right location) diff --git a/constraint.py b/constraint.py new file mode 100644 index 0000000..775b267 --- /dev/null +++ b/constraint.py @@ -0,0 +1,103 @@ +'''equation solver using attributes and introspection. + +Class Constraint from + http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/303396 +''' + +from __future__ import division + +class Constraint(object): + '''takes a function, named arg value (opt.) and returns a Constraint object''' + + def __init__(self,f,**args): + self._f=f + self._args={} + # see important note on order of operations in __setattr__ below. + for arg in f.func_code.co_varnames[0:f.func_code.co_argcount]: + self._args[arg]=None + self.set(**args) + + def __repr__(self): + argstring=','.join(['%s=%s' % (arg,str(value)) for (arg,value) in + self._args.items()]) + if argstring: + return 'Constraint(%s,%s)' % (self._f.func_code.co_name, argstring) + else: + return 'Constraint(%s)' % self._f.func_code.co_name + + def __getattr__(self,name): + '''used to extract function argument values''' + self._args[name] + return self.solve_for(name) + + def __setattr__(self,name,value): + '''sets function argument values''' + # Note - once self._args is created, no new attributes can + # be added to self.__dict__. This is a good thing as it throws + # an exception if you try to assign to an arg which is inappropriate + # for the function in the solver. + if self.__dict__.has_key('_args'): + if name in self._args: + self._args[name]=value + else: + raise KeyError, name + else: + object.__setattr__(self,name,value) + + def set(self, **args): + '''sets values of function arguments''' + for arg in args: + self._args[arg] # raise exception if arg not in _args + setattr(self,arg,args[arg]) + + def solve_for(self,arg): + '''Newton's method solver''' + TOL=0.0000001 # tolerance + ITERLIMIT=1000 # iteration limit + CLOSE_RUNS=10 # after getting close, do more passes + args=self._args + if self._args[arg]: + x0=self._args[arg] + else: + x0=1 + if x0==0: + x1=1 + else: + x1=x0*1.1 + def f(x): + '''function to solve''' + args[arg]=x + return self._f(**args) + fx0=f(x0) + n=0 + while 1: # Newton's method loop here + fx1 = f(x1) + if fx1==0 or x1==x0: # managed to nail it exactly + break + if abs(fx1-fx0)ITERLIMIT: + print "Failed to converge; exceeded iteration limit" + break + slope=(fx1-fx0)/(x1-x0) + if slope==0: + if close_flag: # we're close but have zero slope, finish + break + else: + print 'Zero slope and not close enough to solution' + break + x2=x0-fx0/slope # New 'x1' + fx0 = fx1 + x0=x1 + x1=x2 + n+=1 + self._args[arg]=x1 + return x1 + + diff --git a/solver.py b/solver.py new file mode 100644 index 0000000..d679665 --- /dev/null +++ b/solver.py @@ -0,0 +1,184 @@ + +from operator import isCallable +from constraint import Constraint + +# Variable Strengths: +VERY_WEAK = 0 +WEAK = 10 +NORMAL = 20 +STRONG = 30 +VERY_STRONG = 40 +REQUIRED = 100 + +class Variable(object): + """Representation of a variable in the constraint solver. + Each Variable has a @value and a @strength. Ina constraint the + weakest variables are changed. + """ + + def __init__(self, value=0.0, strength=NORMAL): + self._value = value + self._strength = strength + + # These variables are set by the Solver: + self._solver = None + self._constraints = set() + + strength = property(lambda s: s._strength) + + def set_value(self, value): + self._value = value + if self._solver: + self._solver.mark_dirty(self) + + value = property(lambda s: s._value, set_value) + + def __str__(self): + return 'Variable(%f, %d)' % (self._value, self._strength) + __repr__ = __str__ + + +class Solver(object): + """Solve constraints. A constraint should have accompanying + variables. + """ + + def __init__(self): + # a dict of constraint -> name/variable mappings + self._constraints = {} + self._marked_vars = [] + self._marked_cons = [] + + def mark_dirty(self, variable): + """Mark a variable as "dirty". This means it it solved the next time + the constraints are resolved. + + Example: + >>> a,b,c = Variable(1.0), Variable(2.0), Variable(3.0) + >>> s=Solver() + >>> s.add_constraint(lambda a,b: a+b, a=a, b=b) + Constraint(,a=None,b=None) + >>> s._marked_vars + [] + >>> s._marked_cons + [Constraint(,a=None,b=None)] + >>> a.value=5.0 + >>> s._marked_vars + [Variable(5.000000, 20)] + >>> b.value=2.0 + >>> s._marked_vars + [Variable(5.000000, 20), Variable(2.000000, 20)] + >>> a.value=5.0 + >>> s._marked_vars + [Variable(2.000000, 20), Variable(5.000000, 20)] + """ + if variable in self._marked_vars: + self._marked_vars.remove(variable) + self._marked_vars.append(variable) + for c in variable._constraints: + if c in self._marked_cons: + self._marked_cons.remove(c) + self._marked_cons.append(c) + + def add_constraint(self, constraint, **variables): + """Add a constraint. + The actual constraint is returned, so the constraint can be removed + later on. + + Example: + >>> s = Solver() + >>> a, b = Variable(), Variable(2.0) + >>> s.add_constraint(lambda a, b: a -b, a=a, b=b) + Constraint(,a=None,b=None) + >>> a.value + 0.0 + >>> b.value + 2.0 + >>> len(s._constraints) + 1 + """ + if isCallable(constraint): + constraint = Constraint(constraint) + self._constraints[constraint] = dict(variables) + self._marked_cons.append(constraint) + for v in variables.values(): + v._constraints.add(constraint) + v._solver = self + return constraint + + def remove_constraint(self, constraint): + """ Remove a constraint from the solver + """ + for v in self._constraints[constraint]: v._constraints.remove(constraint) + del self._constraints[constraint] + if self._marked_cons.get(constraint): + del self._marked_cons[constraint] + + def weakest_variable(self, variables): + """Returns the name(!) of the weakest variable. + + Example: + >>> s = Solver() + >>> s.weakest_variable({'a': Variable(2.0, 30), 'b': Variable(2.0, 20)}) + ('b', Variable(2.000000, 20)) + >>> a,b,c = Variable(), Variable(), Variable() + >>> s._marked_vars = [a, b, c] + """ + marked_vars = self._marked_vars + wname, wvar = None, None + for n, v in variables.items(): + if not wvar or \ + (v.strength < wvar.strength) or \ + (v.strength == wvar.strength and \ + (v not in marked_vars + or wvar in marked_vars and \ + marked_vars.index(v) < marked_vars.index(wvar))): + wname, wvar = n, v + return wname, wvar + + def solve(self): + """ + Example: + >>> a,b,c = Variable(1.0), Variable(2.0), Variable(3.0) + >>> s=Solver() + >>> s.add_constraint(lambda a,b: a+b, a=a, b=b) + Constraint(,a=None,b=None) + >>> a.value=5.0 + >>> s.solve() + >>> len(s._marked_cons) + 0 + >>> b._value + -5.0 + >>> s.add_constraint(lambda a,b: a+b, a=b, b=c) + Constraint(,a=None,b=None) + >>> len(s._constraints) + 2 + >>> len(s._marked_cons) + 1 + >>> s.solve() + >>> b._value + -5.0 + """ + constraints = self._constraints + marked_cons = self._marked_cons + + # Solve each constraint. Using a counter makes it + # possible to also solve constraints that are marked as + # a result of other variabled being solved. + n = 0 + while n < len(marked_cons): + c = marked_cons[n] + wname, wvar = self.weakest_variable(constraints[c]) + xx = {} + for nm, v in constraints[c].items(): + xx[nm] = v.value + c.set(**xx) + wvar.value = c.solve_for(wname) + n += 1 + + self._marked_cons = [] + self._marked_vars = [] + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/tree.py b/tree.py new file mode 100644 index 0000000..fdfd124 --- /dev/null +++ b/tree.py @@ -0,0 +1,169 @@ +""" +The canvas base class. +""" + + +class Tree(object): + """A Tree structure. + None is the root node. + + @invariant: len(self._children) == len(self._nodes) + 1 + """ + + def __init__(self): + # List of nodes in the tree, sorted in the order they ought to be + # rendered + self._nodes = [] + + # Per entry a list of children is maintained. + self._children = { None: [] } + + def get_parent(self, node): + """Return the parent item of @node. + """ + for item, children in self._children.items(): + if node in children: + return item + + def get_siblings(self, node): + parent = self.get_parent(node) + return self._children[parent] + + def get_next_sibling(self, node): + parent = self.get_parent(node) + siblings = self._children[parent] + return siblings[siblings.index(node) + 1] + + def get_previous_sibling(self, node): + parent = self.get_parent(node) + siblings = self._children[parent] + return siblings[siblings.index(node) - 1] + + + def _add_to_nodes(self, node, parent): + """Called only from add() + """ + nodes = self._nodes + if parent: + try: + next_uncle = self.get_next_sibling(parent) + except IndexError: + # parent has no younger brothers.. + # place it before the next uncle of grant_parent: + self._add_to_nodes(node, self.get_parent(parent)) + else: + nodes.insert(nodes.index(next_uncle), node) + else: + # append to root node: + nodes.append(node) + + def add(self, node, parent=None): + """Add @node to the tree. @parent is the parent node, which may + be None if the item should be added to the root item. + """ + assert not self._children.get(node) + nodes = self._nodes + siblings = self._children[parent] + self._add_to_nodes(node, parent) + siblings.append(node) + # Create new entry for it's own children: + self._children[node] = [] + + def remove(self, node): + """Remove @node from the tree. + """ + # First remove children: + children = self._children[node] + for c in children: + self.remove(c) + # Remove from parent item + self.get_siblings(node).remove(node) + # Remove data entries: + del self._children[node] + self._nodes.remove(node) + + +def test_add(): + print 'test_add' + + tree = Tree() + n1 = 'n1' + n2 = 'n2' + n3 = 'n3' + + tree.add(n1) + assert len(tree._nodes) == 1 + assert len(tree._children) == 2 + assert len(tree._children[None]) == 1 + assert len(tree._children[n1]) == 0 + + tree.add(n2) + tree.add(n3, parent=n1) + assert len(tree._nodes) == 3 + assert len(tree._children) == 4 + assert len(tree._children[None]) == 2 + assert len(tree._children[n1]) == 1 + assert len(tree._children[n2]) == 0 + assert len(tree._children[n2]) == 0 + print tree._nodes + assert tree._nodes == [n1, n3, n2] + + n4 = 'n4' + tree.add(n4, parent=n3) + print tree._nodes + assert tree._nodes == [n1, n3, n4, n2] + + n5 = 'n5' + tree.add(n5, parent=n3) + print tree._nodes + assert tree._nodes == [n1, n3, n4, n5, n2] + + n6 = 'n6' + tree.add(n6, parent=n2) + print tree._nodes + assert tree._nodes == [n1, n3, n4, n5, n2, n6] + + n7 = 'n7' + tree.add(n7, parent=n1) + print tree._nodes + assert len(tree._children) == 8 + assert tree._nodes == [n1, n3, n4, n5, n7, n2, n6] + assert tree.get_parent(n7) is n1 + assert tree.get_parent(n6) is n2 + assert tree.get_parent(n5) is n3 + assert tree.get_parent(n4) is n3 + assert tree.get_parent(n3) is n1 + assert tree.get_parent(n2) is None + assert tree.get_parent(n1) is None + +def test_remove(): + print 'test_remove' + + tree = Tree() + n1 = 'n1' + n2 = 'n2' + n3 = 'n3' + n4 = 'n4' + n5 = 'n5' + + tree.add(n1) + tree.add(n2) + tree.add(n3, parent=n1) + tree.add(n4, parent=n3) + tree.add(n5, parent=n4) + + assert tree._nodes == [n1, n3, n4, n5, n2] + + tree.remove(n4) + assert tree._nodes == [n1, n3, n2] + + tree.remove(n1) + assert len(tree._children) == 2 + assert tree._children[None] == [n2] + assert tree._children[n2] == [] + assert tree._nodes == [n2] + + +if __name__ == '__main__': + test_add() + test_remove()