[pypy-svn] r39042 - in pypy/dist/pypy: lib/cslib lib/cslib/doc lib/cslib/examples lib/cslib/test module/_cslib rlib/cslib

auc at codespeak.net auc at codespeak.net
Fri Feb 16 19:33:38 CET 2007


Author: auc
Date: Fri Feb 16 19:33:37 2007
New Revision: 39042

Added:
   pypy/dist/pypy/lib/cslib/
   pypy/dist/pypy/lib/cslib/__init__.py   (contents, props changed)
   pypy/dist/pypy/lib/cslib/app_distributors.py   (contents, props changed)
   pypy/dist/pypy/lib/cslib/app_fd.py   (contents, props changed)
   pypy/dist/pypy/lib/cslib/app_propagation.py   (contents, props changed)
   pypy/dist/pypy/lib/cslib/distributors.py   (contents, props changed)
   pypy/dist/pypy/lib/cslib/doc/
   pypy/dist/pypy/lib/cslib/examples/
   pypy/dist/pypy/lib/cslib/fd.py   (contents, props changed)
   pypy/dist/pypy/lib/cslib/fi.py   (contents, props changed)
   pypy/dist/pypy/lib/cslib/propagation.py   (contents, props changed)
   pypy/dist/pypy/lib/cslib/test/
   pypy/dist/pypy/module/_cslib/
   pypy/dist/pypy/module/_cslib/__init__.py   (contents, props changed)
   pypy/dist/pypy/module/_cslib/constraint.py   (contents, props changed)
   pypy/dist/pypy/module/_cslib/fd.py   (contents, props changed)
   pypy/dist/pypy/module/_cslib/propagation.py   (contents, props changed)
   pypy/dist/pypy/rlib/cslib/
   pypy/dist/pypy/rlib/cslib/__init__.py   (contents, props changed)
   pypy/dist/pypy/rlib/cslib/btree.py   (contents, props changed)
   pypy/dist/pypy/rlib/cslib/rconstraint.py   (contents, props changed)
   pypy/dist/pypy/rlib/cslib/rdistributor.py   (contents, props changed)
   pypy/dist/pypy/rlib/cslib/rdomain.py   (contents, props changed)
   pypy/dist/pypy/rlib/cslib/rpropagation.py   (contents, props changed)
Log:
(alf, auc, ludal) provide constraint solver with logilab.constraint interface,
apropriately stuffed into lib, module, rlib


Added: pypy/dist/pypy/lib/cslib/__init__.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/lib/cslib/__init__.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,7 @@
+"Constraint Solver in Python."
+
+from propagation import Repository, Solver
+#from distributors import DefaultDistributor
+import fd
+#import fi
+__all__ = ['Repository', 'Solver', 'fd']

Added: pypy/dist/pypy/lib/cslib/app_distributors.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/lib/cslib/app_distributors.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,154 @@
+"""
+distributors - part of constraint satisfaction solver.
+"""
+
+import math, random
+
+def make_new_domains(domains):
+    """return a shallow copy of dict of domains passed in argument"""
+    domain = {}
+    for key, value in domains.items():
+        domain[key] = value.copy()
+    return domain
+
+class AbstractDistributor(object):
+    """Implements DistributorInterface but abstract because
+    _distribute is left unimplemented."""
+
+    def __init__(self, nb_subspaces=2):
+        self.nb_subspaces = nb_subspaces
+        self.verbose = 0
+        
+    def findSmallestDomain(self, domains):
+        """returns the variable having the smallest domain.
+        (or one of such varibles if there is a tie)
+        """
+        domlist = [(dom.size(), variable ) for variable, dom in domains.items()
+                                           if dom.size() > 1]
+        domlist.sort()
+        return domlist[0][1]
+
+    def findLargestDomain(self, domains):
+        """returns the variable having the largest domain.
+        (or one of such variables if there is a tie)
+        """
+        domlist = [(dom.size(), variable) for variable, dom in domains.items()
+                                          if dom.size() > 1]
+        domlist.sort()
+        return domlist[-1][1]
+
+    def nb_subdomains(self, domains):
+        """return number of sub domains to explore"""
+        return self.nb_subspaces
+
+    def distribute(self, domains, verbose=0):
+        """do the minimal job and let concrete class distribute variables
+        """
+        self.verbose = verbose
+        replicas = []
+        for i in range(self.nb_subdomains(domains)):
+            replicas.append(make_new_domains(domains))
+        modified_domains = self._distribute(*replicas)
+        for domain in modified_domains:
+            domain.clear_change()
+        return replicas
+
+    def _distribute(self, *args):
+        """ method to implement in concrete class
+
+        take self.nb_subspaces copy of the original domains as argument
+        distribute the domains and return each modified domain
+        """
+        raise NotImplementedError("Use a concrete implementation of "
+                                  "the Distributor interface")
+        
+class NaiveDistributor(AbstractDistributor):
+    """distributes domains by splitting the smallest domain in 2 new domains
+    The first new domain has a size of one,
+    and the second has all the other values"""
+
+    def __init__(self):
+        AbstractDistributor.__init__(self)
+        
+    def _distribute(self, dom1, dom2):
+        """See AbstractDistributor"""
+        variable = self.findSmallestDomain(dom1)
+        values = dom1[variable].get_values()
+        if self.verbose:
+            print 'Distributing domain for variable', variable, \
+                  'at value', values[0]
+        dom1[variable].remove_values(values[1:])
+        dom2[variable].removeValue(values[0])
+        return (dom1[variable], dom2[variable])
+
+
+class RandomizingDistributor(AbstractDistributor):
+    """distributes domains as the NaiveDistrutor, except that the unique
+    value of the first domain is picked at random."""
+
+    def __init__(self):
+        AbstractDistributor.__init__(self)
+        
+    def _distribute(self, dom1, dom2):
+        """See AbstractDistributor"""
+        variable = self.findSmallestDomain(dom1)
+        values = dom1[variable].get_values()
+        distval = random.choice(values)
+        values.remove(distval)
+        if self.verbose:
+            print 'Distributing domain for variable', variable, \
+                  'at value', distval
+        dom1[variable].remove_values(values)
+        dom2[variable].removeValue(distval)
+        return (dom1[variable], dom2[variable])
+    
+
+class SplitDistributor(AbstractDistributor):
+    """distributes domains by splitting the smallest domain in
+    nb_subspaces equal parts or as equal as possible.
+    If nb_subspaces is 0, then the smallest domain is split in
+    domains of size 1"""
+    
+    def __init__(self, nb_subspaces=3):
+        AbstractDistributor.__init__(self, nb_subspaces)
+        self.__to_split = None
+    def nb_subdomains(self, domains):
+        """See AbstractDistributor"""
+        self.__to_split = self.findSmallestDomain(domains)
+        if self.nb_subspaces:
+            return min(self.nb_subspaces, domains[self.__to_split].size())
+        else:
+            return domains[self.__to_split].size()
+    
+    def _distribute(self, *args):
+        """See AbstractDistributor"""
+        variable = self.__to_split
+        nb_subspaces = len(args)
+        values = args[0][variable].get_values()
+        nb_elts = max(1, len(values)*1./nb_subspaces)
+        slices = [(int(math.floor(index * nb_elts)),
+                   int(math.floor((index + 1) * nb_elts)))
+                  for index in range(nb_subspaces)]
+        if self.verbose:
+            print 'Distributing domain for variable', variable
+        modified = []
+        for (dom, (end, start)) in zip(args, slices) :
+            dom[variable].remove_values(values[:end])
+            dom[variable].remove_values(values[start:])
+            modified.append(dom[variable])
+        return modified
+
+class DichotomyDistributor(SplitDistributor):
+    """distributes domains by splitting the smallest domain in
+    two equal parts or as equal as possible"""
+    def __init__(self):
+        SplitDistributor.__init__(self, 2)
+
+
+class EnumeratorDistributor(SplitDistributor):
+    """distributes domains by splitting the smallest domain
+    in domains of size 1."""
+    def __init__(self):
+        SplitDistributor.__init__(self, 0)
+
+DefaultDistributor = DichotomyDistributor

Added: pypy/dist/pypy/lib/cslib/app_fd.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/lib/cslib/app_fd.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,351 @@
+"""Tools to work with finite domain variables and constraints
+
+This module provides the following usable classes:
+ * FiniteDomain: a class for storing FiniteDomains
+ * Expression: a constraint represented as an expression
+ * BinaryExpression: a binary constraint represented as an expression
+ * various BasicConstraint classes
+
+The Expression and BinaryExpression classes can be constructed using the
+make_expression factory function.  """
+
+import operator
+
+from propagation import AbstractDomain, BasicConstraint, \
+     ConsistencyFailure, AbstractConstraint
+
+
+class FiniteDomain(AbstractDomain):
+    """
+    Variable Domain with a finite set of possible values
+    """
+
+    _copy_count = 0
+    _write_count = 0
+    
+    def __init__(self, values):
+        """values is a list of values in the domain
+        This class uses a dictionnary to make sure that there are
+        no duplicate values"""
+        AbstractDomain.__init__(self)
+        if isinstance(values, FiniteDomain):
+            # do a copy on write
+            self._cow = True
+            values._cow = True
+            FiniteDomain._copy_count += 1
+            self._values = values._values
+        else:
+            assert len(values) > 0
+            self.setValues(values)
+            
+        ##self.getValues = self._values.keys
+
+    def setValues(self, values):
+        self._cow = False
+        FiniteDomain._write_count += 1
+        self._values = {}
+        for val in values:
+            self._values[val] = 0
+        
+    def removeValue(self, value):
+        """Remove value of domain and check for consistency"""
+##         print "removing", value, "from", self._values.keys()
+        if self._cow:
+            self.setValues(self._values)
+        del self._values[value]
+        self._valueRemoved()
+
+    def removeValues(self, values):
+        """Remove values of domain and check for consistency"""
+        if self._cow:
+            self.setValues(self._values)
+        if values:
+##             print "removing", values, "from", self._values.keys()
+            for val in values :
+                del self._values[val]
+            self._valueRemoved()
+    __delitem__ = removeValue
+    
+    def size(self):
+        """computes the size of a finite domain"""
+        return len(self._values)
+    __len__ = size
+    
+    def getValues(self):
+        """return all the values in the domain"""
+        return self._values.keys()
+
+    def __iter__(self):
+        return iter(self._values)
+    
+    def copy(self):
+        """clone the domain"""
+        return FiniteDomain(self)
+    
+    def __repr__(self):
+        return '<FiniteDomain %s>' % str(self.getValues())
+
+##
+## Constraints
+##    
+class AllDistinct(AbstractConstraint):
+    """Contraint: all values must be distinct"""
+
+    def __init__(self, variables):
+        assert len(variables)>1
+        AbstractConstraint.__init__(self, variables)
+        # worst case complexity
+        self.__cost = len(variables) * (len(variables) - 1) / 2 
+
+    def __repr__(self):
+        return '<AllDistinct %s>' % str(self._variables)
+
+    def estimateCost(self, domains):
+        """return cost"""
+        return self.__cost
+
+    def narrow(self, domains):
+        """narrowing algorithm for the constraint"""
+        variables = [(domains[variable].size(), variable, domains[variable])
+                     for variable in self._variables]
+        
+        variables.sort()
+        # if a domain has a size of 1,
+        # then the value must be removed from the other domains
+        for size, var, dom in variables:
+            if dom.size() == 1:
+                for _siz, _var, _dom in variables:
+                    if _var != var:
+                        try:
+                            _dom.removeValue(dom.getValues()[0])
+                        except KeyError:
+                            # we ignore errors caused by the removal of
+                            # non existing values
+                            pass
+
+        # if there are less values than variables, the constraint fails
+        values = {}
+        for size, var, dom in variables:
+            for val in dom:
+                values[val] = 0
+        if len(values) < len(variables):
+            raise ConsistencyFailure()
+            
+        # the constraint is entailed if all domains have a size of 1
+        for variable in variables:
+            if variable[2].size() != 1:
+                return 0
+        return 1
+
+
+
+
+class Expression(AbstractConstraint):
+    """A constraint represented as a python expression."""
+    _FILTER_CACHE = {}
+
+    def __init__(self, variables, formula, type='fd.Expression'):
+        """variables is a list of variables which appear in the formula
+        formula is a python expression that will be evaluated as a boolean"""
+        AbstractConstraint.__init__(self, variables)
+        self.formula = formula
+        self.type = type
+        try:
+            self.filterFunc = Expression._FILTER_CACHE[formula]
+        except KeyError:
+            self.filterFunc = eval('lambda %s: %s' % \
+                                        (','.join(variables), formula), {}, {})
+            Expression._FILTER_CACHE[formula] = self.filterFunc
+
+    def _init_result_cache(self):
+        """key = (variable,value), value = [has_success,has_failure]"""
+        result_cache = {}
+        for var_name in self._variables:
+            result_cache[var_name] = {}
+        return result_cache
+
+
+    def _assign_values(self, domains):
+        variables = []
+        kwargs = {}
+        for variable in self._variables:
+            domain = domains[variable]
+            values = domain.get_values()
+            variables.append((domain.size(), [variable, values, 0, len(values)]))
+            kwargs[variable] = values[0]
+        # sort variables to instanciate those with fewer possible values first
+        variables.sort()
+
+        go_on = 1
+        while go_on:
+            yield kwargs
+            # try to instanciate the next variable
+            for size, curr in variables:
+                if (curr[2] + 1) < curr[-1]:
+                    curr[2] += 1
+                    kwargs[curr[0]] = curr[1][curr[2]]
+                    break
+                else:
+                    curr[2] = 0
+                    kwargs[curr[0]] = curr[1][0]
+            else:
+                # it's over
+                go_on = 0
+            
+        
+    def narrow(self, domains):
+        """generic narrowing algorithm for n-ary expressions"""
+        maybe_entailed = 1
+        ffunc = self.filterFunc
+        result_cache = self._init_result_cache()
+        for kwargs in self._assign_values(domains):
+            if maybe_entailed:
+                for var, val in kwargs.iteritems():
+                    if val not in result_cache[var]:
+                        break
+                else:
+                    continue
+            if ffunc(**kwargs):
+                for var, val in kwargs.items():
+                    result_cache[var][val] = 1
+            else:
+                maybe_entailed = 0
+
+        try:
+            for var, keep in result_cache.iteritems():
+                domain = domains[var]
+                domain.remove_values([val for val in domain.get_values()
+                                      if val not in keep])                
+        except ConsistencyFailure:
+            raise ConsistencyFailure('Inconsistency while applying %s' % \
+                                     repr(self))
+        except KeyError:
+            # There are no more value in result_cache
+            pass
+
+        return maybe_entailed
+
+    def __repr__(self):
+        return '<%s "%s">' % (self.type, self.formula)
+
+class BinaryExpression(Expression):
+    """A binary constraint represented as a python expression
+
+    This implementation uses a narrowing algorithm optimized for
+    binary constraints."""
+    
+    def __init__(self, variables, formula, type = 'fd.BinaryExpression'):
+        assert len(variables) == 2
+        Expression.__init__(self, variables, formula, type)
+
+    def narrow(self, domains):
+        """specialized narrowing algorithm for binary expressions
+        Runs much faster than the generic version"""
+        maybe_entailed = 1
+        var1 = self._variables[0]
+        dom1 = domains[var1]
+        values1 = dom1.get_values()
+        var2 = self._variables[1]
+        dom2 = domains[var2]
+        values2 = dom2.get_values()
+        ffunc = self.filterFunc
+        if dom2.size() < dom1.size():
+            var1, var2 = var2, var1
+            dom1, dom2 = dom2, dom1
+            values1, values2 = values2, values1
+            
+        kwargs = {}
+        keep1 = {}
+        keep2 = {}
+        maybe_entailed = 1
+        try:
+            # iterate for all values
+            for val1 in values1:
+                kwargs[var1] = val1
+                for val2 in values2:
+                    kwargs[var2] = val2
+                    if val1 in keep1 and val2 in keep2 and maybe_entailed == 0:
+                        continue
+                    if ffunc(**kwargs):
+                        keep1[val1] = 1
+                        keep2[val2] = 1
+                    else:
+                        maybe_entailed = 0
+
+            dom1.remove_values([val for val in values1 if val not in keep1])
+            dom2.remove_values([val for val in values2 if val not in keep2])
+            
+        except ConsistencyFailure:
+            raise ConsistencyFailure('Inconsistency while applying %s' % \
+                                     repr(self))
+        except Exception:
+            print self, kwargs
+            raise 
+        return maybe_entailed
+
+
+def make_expression(variables, formula, constraint_type=None):
+    """create a new constraint of type Expression or BinaryExpression
+    The chosen class depends on the number of variables in the constraint"""
+    # encode unicode
+    vars = []
+    for var in variables:
+        if type(var) == type(u''):
+            vars.append(var.encode())
+        else:
+            vars.append(var)
+    if len(vars) == 2:
+        if constraint_type is not None:
+            return BinaryExpression(vars, formula, constraint_type)
+        else:
+            return BinaryExpression(vars, formula)
+
+    else:
+        if constraint_type is not None:
+            return Expression(vars, formula, constraint_type)
+        else:
+            return Expression(vars, formula)
+
+
+class Equals(BasicConstraint):
+    """A basic constraint variable == constant value"""
+    def __init__(self, variable, reference):
+        BasicConstraint.__init__(self, variable, reference, operator.eq)
+
+class NotEquals(BasicConstraint):
+    """A basic constraint variable != constant value"""
+    def __init__(self, variable, reference):
+        BasicConstraint.__init__(self, variable, reference, operator.ne)
+
+class LesserThan(BasicConstraint):
+    """A basic constraint variable < constant value"""
+    def __init__(self, variable, reference):
+        BasicConstraint.__init__(self, variable, reference, operator.lt)
+
+class LesserOrEqual(BasicConstraint):
+    """A basic constraint variable <= constant value"""
+    def __init__(self, variable, reference):
+        BasicConstraint.__init__(self, variable, reference, operator.le)
+
+class GreaterThan(BasicConstraint):
+    """A basic constraint variable > constant value"""
+    def __init__(self, variable, reference):
+        BasicConstraint.__init__(self, variable, reference, operator.gt)
+
+class GreaterOrEqual(BasicConstraint):
+    """A basic constraint variable >= constant value"""
+    def __init__(self, variable, reference):
+        BasicConstraint.__init__(self, variable, reference, operator.ge)
+
+def _in(v, set):
+    """test presence of v in set"""
+    return v in set
+
+class InSet(BasicConstraint):
+    """A basic contraint variable in set value"""
+    def __init__(self, variable, set):
+        BasicConstraint.__init__(self, variable, set, _in )
+
+
+
+

Added: pypy/dist/pypy/lib/cslib/app_propagation.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/lib/cslib/app_propagation.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,283 @@
+"""The code of the constraint propagation algorithms"""
+
+import operator
+#from time import strftime
+
+class ConsistencyFailure(Exception):
+    """The repository is not in a consistent state"""
+    pass
+
+class Repository(object):
+    """Stores variables, domains and constraints
+    Propagates domain changes to constraints
+    Manages the constraint evaluation queue"""
+    
+    def __init__(self, variables, domains, constraints = None):
+        # encode unicode
+        for i, var in enumerate(variables):
+            if type(var) == type(u''):
+                variables[i] = var.encode()
+                
+        self._variables = variables   # list of variable names
+        self._domains = domains    # maps variable name to domain object
+        self._constraints = [] # list of constraint objects
+#        self._queue = []       # queue of constraints waiting to be processed
+        self._variableListeners = {}
+        for var in self._variables:
+            self._variableListeners[var] = []
+            assert self._domains.has_key(var)
+        for constr in constraints or ():
+            self.addConstraint(constr)
+
+    def __repr__(self):
+        return '<Repository nb_constraints=%d domains=%s>' % \
+                               (len(self._constraints), self._domains)
+
+    def addConstraint(self, constraint):
+        if isinstance(constraint, BasicConstraint):
+            # Basic constraints are processed just once
+            # because they are straight away entailed
+            var = constraint.getVariable()
+            constraint.narrow({var: self._domains[var]})
+        else:
+            self._constraints.append(constraint)
+            for var in constraint.affectedVariables():
+                self._variableListeners[var].append(constraint)
+        
+    def _removeConstraint(self, constraint):
+        self._constraints.remove(constraint)
+        for var in constraint.affectedVariables():
+            try:
+                self._variableListeners[var].remove(constraint)
+            except ValueError:
+                raise ValueError('Error removing constraint from listener',
+                                 var,
+                                 self._variableListeners[var],
+                                 constraint)
+
+    def getDomains(self):
+        return self._domains
+
+    def distribute(self, distributor, verbose=0):
+        """Create new repository using the distributor and self """
+        for domains in distributor.distribute(self._domains, verbose):
+            yield Repository(self._variables, domains, self._constraints) 
+    
+    def revise(self, verbose=0):
+        """Prunes the domains of the variables
+        This method calls constraint.narrow() and queues constraints
+        that are affected by recent changes in the domains.
+        Returns True if a solution was found"""
+        if verbose:
+            print '** Consistency **'
+
+        _queue = [ (constr.estimateCost(self._domains),
+                           constr) for constr in self._constraints ]
+        _queue.sort()
+        _affected_constraints = {}
+        while True:
+            if not _queue:
+                # refill the queue if some constraints have been affected
+                _queue = [(constr.estimateCost(self._domains),
+                           constr) for constr in _affected_constraints]
+                if not _queue:
+                    break
+                _queue.sort()
+                _affected_constraints.clear()
+            if verbose > 2:
+                print 'Queue', _queue
+            cost, constraint = _queue.pop(0)
+            if verbose > 1:
+                print 'Trying to entail constraint',
+                print constraint, '[cost:%d]' % cost
+            entailed = constraint.narrow(self._domains)
+            for var in constraint.affectedVariables():
+                # affected constraints are listeners of
+                # affected variables of this constraint
+                dom = self._domains[var]
+                if not dom.has_changed():
+                    continue
+                if verbose > 1 :
+                    print ' -> New domain for variable', var, 'is', dom
+                for constr in self._variableListeners[var]:
+                    if constr is not constraint:
+                        _affected_constraints[constr] = True
+                dom.clear_change()
+            if entailed:
+                if verbose:
+                    print "--> Entailed constraint", constraint
+                self._removeConstraint(constraint)
+                if constraint in _affected_constraints:
+                    del _affected_constraints[constraint]
+                
+        for domain in self._domains.itervalues():
+            if domain.size() != 1:
+                return 0
+        return 1
+
+class Solver(object):
+    """Top-level object used to manage the search"""
+
+    def __init__(self, distributor=None):
+        """if no distributer given, will use the default one"""
+        if distributor is None:
+            from distributors import DefaultDistributor
+            distributor = DefaultDistributor()
+        self._distributor = distributor
+        self.verbose = 1
+        self.max_depth = 0
+
+    def solve_one(self, repository, verbose=0):
+        """Generates only one solution"""
+        self.verbose = verbose
+        self.max_depth = 0
+        try:
+            return  self._solve(repository).next()
+        except StopIteration:
+            return
+        
+    def solve_best(self, repository, cost_func, verbose=0):
+        """Generates solution with an improving cost"""
+        self.verbose = verbose
+        self.max_depth = 0
+        best_cost = None
+        for solution in self._solve(repository):
+            cost = cost_func(**solution)
+            if best_cost is None or cost <= best_cost:
+                best_cost = cost
+                yield solution, cost            
+        
+    def solve_all(self, repository, verbose=0):
+        """Generates all solutions"""
+        self.verbose = verbose
+        self.max_depth = 0
+        for solution in self._solve(repository):
+            yield solution
+
+    def solve(self, repository, verbose=0):
+        """return list of all solutions"""
+        self.max_depth = 0
+        solutions = []
+        for solution in self.solve_all(repository, verbose):
+            solutions.append(solution)
+        return solutions
+        
+    def _solve(self, repository, recursion_level=0):
+        """main generator"""
+        solve = self._solve
+        verbose = self.verbose
+        if recursion_level > self.max_depth:
+            self.max_depth = recursion_level
+        if verbose:
+            print '*** [%d] Solve called with repository' % recursion_level,
+            print repository
+        try:
+            foundSolution = repository.revise(verbose)
+        except ConsistencyFailure, exc:
+            if verbose:
+                print exc
+            pass
+        else:
+            if foundSolution: 
+                solution = {}
+                for variable, domain in repository.getDomains().items():
+                    solution[variable] = domain.get_values()[0]
+                if verbose:
+                    print '### Found Solution', solution
+                    print '-'*80
+                yield solution
+            else:
+                for repo in repository.distribute(self._distributor,
+                                                  verbose):
+                    for solution in solve(repo, recursion_level+1):
+                        if solution is not None:
+                            yield solution
+                            
+        if recursion_level == 0 and self.verbose:
+            print 'Finished search'
+            print 'Maximum recursion depth = ', self.max_depth
+
+        
+
+
+
+
+class BasicConstraint(object):
+    """A BasicConstraint, which is never queued by the Repository
+    A BasicConstraint affects only one variable, and will be entailed
+    on the first call to narrow()"""
+
+    def __init__(self, variable, reference, operator):
+        """variables is a list of variables on which
+        the constraint is applied"""
+        self._variable = variable
+        self._reference = reference
+        self._operator = operator
+
+    def __repr__(self):
+        return '<%s %s %s>'% (self.__class__, self._variable, self._reference)
+
+    def isVariableRelevant(self, variable):
+        return variable == self._variable
+
+    def estimateCost(self, domains):
+        return 0 # get in the first place in the queue
+    
+    def affectedVariables(self):
+        return [self._variable]
+    
+    def getVariable(self):
+        return self._variable
+        
+    def narrow(self, domains):
+        domain = domains[self._variable]
+        operator = self._operator
+        ref = self._reference
+        try:
+            for val in domain.getValues() :
+                if not operator(val, ref) :
+                    domain.removeValue(val)
+        except ConsistencyFailure:
+            raise ConsistencyFailure('inconsistency while applying %s' % \
+                                     repr(self))
+        return 1
+
+
+class AbstractDomain(object):
+    """Implements the functionnality related to the changed flag.
+    Can be used as a starting point for concrete domains"""
+
+    def __init__(self):
+        self.__changed = 0
+
+    def resetFlags(self):
+        self.__changed = 0
+    
+    def hasChanged(self):
+        return self.__changed
+
+    def _valueRemoved(self):
+        """The implementation of removeValue should call this method"""
+        self.__changed = 1
+        if self.size() == 0:
+            raise ConsistencyFailure()
+    
+class AbstractConstraint(object):
+    
+    def __init__(self, variables):
+        """variables is a list of variables which appear in the formula"""
+        self._variables = variables
+
+    def affectedVariables(self):
+        """ Return a list of all variables affected by this constraint """
+        return self._variables
+
+    def isVariableRelevant(self, variable):
+        return variable in self._variables
+
+    def estimateCost(self, domains):
+        """Return an estimate of the cost of the narrowing of the constraint"""
+        return reduce(operator.mul,
+                      [domains[var].size() for var in self._variables])
+
+    

Added: pypy/dist/pypy/lib/cslib/distributors.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/lib/cslib/distributors.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,2 @@
+from _cslib import DefaultDistributor
+

Added: pypy/dist/pypy/lib/cslib/fd.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/lib/cslib/fd.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,7 @@
+from _cslib import FiniteDomain, _make_expression, AllDistinct
+
+
+def make_expression(variables, formula):
+    func = 'lambda %s:%s' % (','.join(variables),
+                             formula)
+    return _make_expression(variables, formula, eval(func))

Added: pypy/dist/pypy/lib/cslib/fi.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/lib/cslib/fi.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,326 @@
+"""Tools to work with finite interval domain interval and constraints
+"""
+
+from propagation import AbstractDomain, BasicConstraint, \
+     ConsistencyFailure, AbstractConstraint
+
+from distributors import AbstractDistributor
+
+class Interval:
+    """representation of an interval
+    This class is used to give back results from a FiniteIntervalDomain
+    """
+    def __init__(self, start, end):
+        self._start = start
+        self._end = end
+
+    def __repr__(self):
+        return "<Interval [%.2f %.2f[>" % (self._start, self._end)
+
+    def __eq__(self, other):
+        return self._start == other._start and \
+               self._end == other._end
+
+class FiniteIntervalDomain(AbstractDomain):
+    """
+    Domain for a variable with interval values. 
+    """
+    
+    def __init__(self, lowestMin, highestMax,
+                 min_length, max_length=None, resolution=1):
+        """
+        lowestMin is the lowest value of a low boundary for a variable (inclusive).
+        highestMax is the highest value of a high boundary for a variable (exclusive).
+        min_length is the minimum width of the interval.
+        max_length is the maximum width of the interval.
+                   Use None to have max = min.
+        resolution is the precision to use for constraint satisfaction. Defaults to 1
+        """
+        assert highestMax >= lowestMin
+        if max_length is None:
+            max_length = min_length
+        assert 0 <= min_length <= max_length
+        assert min_length <= highestMax - lowestMin
+        assert resolution > 0
+        AbstractDomain.__init__(self)
+        self.lowestMin = lowestMin
+        self.highestMax = highestMax
+        self._min_length = min_length
+        max_length = min(max_length, highestMax - lowestMin)
+        self._max_length = max_length
+        self._resolution = resolution
+
+    def __eq__(self, other):
+        
+        return  self.lowestMin == other.lowestMin and \
+               self.highestMax == other.highestMax and \
+               self._min_length == other._min_length and \
+               self._max_length == other._max_length and \
+               self._resolution == other._resolution
+
+    def getValues(self):
+        return list(self.iter_values())
+
+    def iter_values(self):
+        length = self._min_length
+        while length <= self._max_length:
+            start = self.lowestMin
+            while start + length <= self.highestMax:
+                yield Interval(start, start+length)
+                start += self._resolution
+            length += self._resolution
+        
+        
+    def size(self):
+        """computes the size of a finite interval"""
+        size = 0
+        length = self._min_length 
+        while length <= self._max_length :
+            size += ((self.highestMax - length) - self.lowestMin) / self._resolution + 1
+            length += self._resolution
+        return size
+
+    def _highestMin(self):
+        return self.highestMax - self._min_length
+
+    def _lowestMax(self):
+        return self.lowestMin + self._min_length
+
+    lowestMax = property(_lowestMax, None, None, "")
+    
+    highestMin = property(_highestMin, None, None, "")
+
+    def copy(self):
+        """clone the domain"""
+        return FiniteIntervalDomain(self.lowestMin, self.highestMax,
+                                    self._min_length, self._max_length,
+                                    self._resolution)
+
+    def setLowestMin(self, new_lowestMin):
+        self.lowestMin = new_lowestMin
+        self._valueRemoved()
+
+    def setHighestMax(self, new_highestMax):
+        self.highestMax = new_highestMax
+        self._valueRemoved()
+
+    def setMinLength(self, new_min):
+        self._min_length = new_min
+        self._valueRemoved()
+
+    def setMaxLength(self, new_max):
+        self._max_length = new_max
+        self._valueRemoved()
+
+    def overlap(self, other):
+        return other.highestMax > self.lowestMin and \
+               other.lowestMin < self.highestMax
+
+    def no_overlap_impossible(self, other):
+        return self.lowestMax > other.highestMin and \
+               other.lowestMax > self.highestMin
+
+    def hasSingleLength(self):
+        return self._max_length == self._min_length
+
+    def _valueRemoved(self):
+        if self.lowestMin >= self.highestMax:
+            raise ConsistencyFailure("earliest start [%.2f] higher than latest end  [%.2f]" %
+                                     (self.lowestMin, self.highestMax))
+        if self._min_length > self._max_length:
+            raise ConsistencyFailure("min length [%.2f] greater than max length [%.2f]" %
+                                     (self._min_length, self._max_length))
+
+        self._max_length = min(self._max_length, self.highestMax - self.lowestMin)
+
+        if self.size() == 0:
+            raise ConsistencyFailure('size is 0')
+        
+                                 
+    
+    def __repr__(self):
+        return '<FiniteIntervalDomain from [%.2f, %.2f[ to [%.2f, %.2f[>' % (self.lowestMin,
+                                                                             self.lowestMax,
+                                                                             self.highestMin,
+                                                                             self.highestMax)
+
+##
+## Distributors
+##    
+
+class FiniteIntervalDistributor(AbstractDistributor):
+    """Distributes a set of FiniteIntervalDomain
+    The distribution strategy is the following:
+     - the smallest domain of size > 1 is picked
+     - if its max_length is greater than its min_length, a subdomain if size
+       min_length is distributed, with the same boundaries
+     - otherwise, a subdomain [lowestMin, lowestMax[ is distributed
+    """
+    def __init__(self):
+        AbstractDistributor.__init__(self)
+
+    def _split_values(self, copy1, copy2):
+        if copy1.hasSingleLength():
+            copy1.highestMax = copy1.lowestMin + copy1._min_length
+            copy2.lowestMin += copy2._resolution
+        else:
+            copy1._max_length = copy1._min_length
+            copy2._min_length += copy2._resolution
+        
+
+    def _distribute(self, dom1, dom2):
+        variable = self.findSmallestDomain(dom1)
+        if self.verbose:
+            print 'Distributing domain for variable', variable
+        splitted = dom1[variable]
+        cpy1 = splitted.copy()
+        cpy2 = splitted.copy()
+
+        self._split_values(cpy1, cpy2)
+            
+        dom1[variable] = cpy1
+        dom2[variable] = cpy2
+        
+        return cpy1, cpy2
+            
+        
+
+##
+## Constraints
+##    
+
+class AbstractFIConstraint(AbstractConstraint):
+    def __init__(self, var1, var2):
+        AbstractConstraint.__init__(self, (var1, var2))
+    
+    def estimateCost(self, domains):
+        return 1
+
+    def __repr__(self):
+        return '<%s %s>' % (self.__class__.__name__, str(self._variables))
+
+    def __eq__(self, other):
+        return repr(self) == repr(other)
+
+    def __hash__(self):
+        # FIXME: to be able to add constraints in Sets (and compare them)
+        # FIXME: improve implementation
+        variables = tuple(sorted(self._variables))
+        return hash((self.__class__.__name__, variables))
+    
+    def narrow(self, domains):
+        """narrowing algorithm for the constraint"""
+        dom1 = domains[self._variables[0]]
+        dom2 = domains[self._variables[1]]
+
+        return self._doNarrow(dom1, dom2)
+
+    def _doNarrow(self, dom1, dom2):
+        """virtual method which does the real work"""
+        raise NotImplementedError
+
+
+
+# FIXME: deal with more than 2 domains at once ?
+class NoOverlap(AbstractFIConstraint):
+
+    def __eq__(self, other):
+        return isinstance(other, NoOverlap) and \
+               set(self._variables) == set(other._variables)
+
+    def _doNarrow(self, dom1, dom2):
+        if not dom1.overlap(dom2):
+            return 1
+        elif dom1.no_overlap_impossible(dom2) :
+            raise ConsistencyFailure
+        elif dom1.lowestMax == dom2.highestMin and dom2.lowestMax > dom1.highestMin :
+            dom1.setHighestMax(dom2.highestMin)
+            dom2.setLowestMin(dom1.lowestMax)
+            return 1
+        elif dom1.lowestMax > dom2.highestMin and dom2.lowestMax == dom1.highestMin :
+            dom2.setHighestMax(dom1.highestMin)
+            dom1.setLowestMin(dom2.lowestMax)
+            return 1
+        return 0
+        
+class StartsBeforeStart(AbstractFIConstraint):
+
+    def _doNarrow(self, dom1, dom2):
+        
+        if dom1.lowestMin > dom2.highestMin:
+            raise ConsistencyFailure
+        if dom1.highestMin < dom2.lowestMin:
+            return 1
+        return 0
+
+class StartsBeforeEnd(AbstractFIConstraint):
+
+    def _doNarrow(self, dom1, dom2):
+        if dom1.lowestMin > dom2.highestMax:
+            raise ConsistencyFailure
+        if dom1.highestMin < dom2.lowestMax:
+            return 1
+        return 0 
+    
+class EndsBeforeStart(AbstractFIConstraint):
+
+    def _doNarrow(self, dom1, dom2):
+        if dom1.lowestMax > dom2.highestMin:
+            raise ConsistencyFailure
+        if dom1.highestMax < dom2.lowestMin:
+            return 1
+        if dom1.highestMax > dom2.highestMin:
+            dom1.setHighestMax(dom2.highestMin)
+        return 0 
+
+class EndsBeforeEnd(AbstractFIConstraint):
+
+    def _doNarrow(self, dom1, dom2):
+        if dom1.lowestMax > dom2.highestMax:
+            raise ConsistencyFailure
+        if dom1.highestMax < dom2.lowestMax:
+            return 1
+        if dom1.highestMax > dom2.highestMax:
+            dom1.setHighestMax(dom2.highestMax)
+        return 0 
+
+class StartsAfterStart(AbstractFIConstraint):
+
+    def _doNarrow(self, dom1, dom2):
+        if dom1.highestMin < dom2.lowestMin:
+            raise ConsistencyFailure
+        if dom1.lowestMin > dom2.highestMin:
+            return 1
+        if dom1.lowestMin < dom2.lowestMin:
+            dom1.setLowestMin(dom2.lowestMin)
+        return 0 
+        
+class StartsAfterEnd(AbstractFIConstraint):
+
+    def _doNarrow(self, dom1, dom2):
+        if dom1.highestMin < dom2.lowestMax:
+            raise ConsistencyFailure
+        if dom1.lowestMin > dom2.highestMax:
+            return 1
+        if dom1.lowestMin < dom2.lowestMax:
+            dom1.setLowestMin(dom2.lowestMax)
+        return 0 
+
+class EndsAfterStart(AbstractFIConstraint):
+
+    def _doNarrow(self, dom1, dom2):
+        if dom1.highestMax < dom2.lowestMin:
+            raise ConsistencyFailure
+        if dom1.lowestMax > dom2.highestMin:
+            return 1
+        return 0 
+    
+class EndsAfterEnd(AbstractFIConstraint):
+
+    def _doNarrow(self, dom1, dom2):
+        if dom1.highestMax < dom2.lowestMax:
+            raise ConsistencyFailure
+        if dom1.lowestMax > dom2.highestMax:
+            return 1
+        return 0
+    

Added: pypy/dist/pypy/lib/cslib/propagation.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/lib/cslib/propagation.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1 @@
+from _cslib import Repository, Solver

Added: pypy/dist/pypy/module/_cslib/__init__.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/module/_cslib/__init__.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,16 @@
+# Package initialisation
+from pypy.interpreter.mixedmodule import MixedModule
+
+#print '_csp module'
+
+class Module(MixedModule):
+    appleveldefs = {
+    }
+
+    interpleveldefs = {
+        'FiniteDomain'    : 'fd.make_fd',
+        'AllDistinct'     : 'constraint.make_alldistinct',
+        '_make_expression': 'constraint.interp_make_expression',
+        'Repository'      : 'propagation.make_repo',
+        'Solver'          : 'propagation.make_solver'
+    }

Added: pypy/dist/pypy/module/_cslib/constraint.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/module/_cslib/constraint.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,185 @@
+from pypy.interpreter.baseobjspace import Wrappable
+from pypy.interpreter import baseobjspace, typedef, gateway
+from pypy.interpreter.gateway import interp2app
+from pypy.interpreter.function import Function
+from pypy.interpreter.error import OperationError
+
+from pypy.objspace.std.listobject import W_ListObject
+from pypy.objspace.std.tupleobject import W_TupleObject
+from pypy.objspace.std.stringobject import W_StringObject
+from pypy.objspace.std.dictobject import W_DictObject
+
+from pypy.module._cslib.fd import _FiniteDomain
+from pypy.rlib import rconstraint as rc
+
+class W_AbstractConstraint(baseobjspace.Wrappable):
+    
+    def __init__(self, space, constraint):
+        """variables is a list of variables which appear in the formula"""
+        self.space = space
+        assert isinstance( constraint, rc.AbstractConstraint )
+        self.constraint = constraint
+
+    def w_affected_variables(self):
+        """ Return a list of all variables affected by this constraint """
+        return self.space.wrap(self._variables)
+
+    def affected_variables(self):
+        return self._variables
+
+    def w_revise(self, w_domains):
+        assert isinstance(w_domains, W_DictObject)
+        doms = {}
+        spc = self.space
+        for var, dom in w_domains.content.items():
+            doms[spc.str_w(var)] = dom
+        return self.space.newbool(self.revise(doms))
+
+    def w_estimate_cost(self, w_domains):
+        assert isinstance(w_domains, W_DictObject)
+        cost = 1
+        doms = w_domains.content
+        for var in self._variables:
+            dom = doms[self.space.wrap(var)]
+            assert isinstance(dom, W_AbstractDomain)
+            cost = cost * dom.size()
+        return self.space.newint(cost)
+                
+W_AbstractConstraint.typedef = typedef.TypeDef(
+    "W_AbstractConstraint")
+
+
+
+class _Expression(rc.Expression):
+    """A constraint represented as a python expression."""
+
+    def __init__(self, space, w_variables, w_formula, w_filter_func):
+        """variables is a list of variables which appear in the formula
+        formula is a python expression that will be evaluated as a boolean"""
+        self.space = space
+        variables = []
+        for w_var in space.unpackiterable( w_variables ):
+            variables.append( space.str_w(w_var) )
+        if len(variables)==0:
+            raise OperationError( space.w_ValueError,
+                                  space.wrap("need at least one variable") )
+        rc.Expression.__init__(self, variables )
+        self.formula = self.space.str_w(w_formula)
+        # self.filter_func is a function taking keyword arguments and returning a boolean
+        self.w_filter_func = w_filter_func
+
+    def filter_func(self, kwargs):
+        space = self.space
+        w_kwargs = space.newdict()
+        for var, value in kwargs.items():
+            dom = self.doms[var]
+            assert isinstance( dom, _FiniteDomain )
+            #print '!!!!', dom.w_values, value
+            w_val = space.getitem( dom.w_values, space.wrap(value) )
+            w_kwargs.content[space.wrap(var)] = w_val
+        return space.is_true(space.call(self.w_filter_func,
+                                        space.newlist([]),
+                                        w_kwargs))
+
+
+    def __repr__(self):
+        return '<%s>' % self.formula
+
+
+class _BinaryExpression(rc.BinaryExpression):
+    """A constraint represented as a python expression."""
+
+    def __init__(self, space, w_variables, w_formula, w_filter_func):
+        """variables is a list of variables which appear in the formula
+        formula is a python expression that will be evaluated as a boolean"""
+        self.space = space
+        variables = []
+        for w_var in space.unpackiterable( w_variables ):
+            variables.append( space.str_w(w_var) )
+        if len(variables)==0:
+            raise OperationError( space.w_ValueError,
+                                  space.wrap("need at least one variable") )
+        rc.BinaryExpression.__init__(self, variables )
+        self.formula = self.space.str_w(w_formula)
+        # self.filter_func is a function taking keyword arguments and returning a boolean
+        self.w_filter_func = w_filter_func
+        self.kwcache = {}
+
+    def filter_func(self, kwargs):
+        space = self.space
+        var1 = self._variables[0]
+        var2 = self._variables[1]
+        arg1 = kwargs[var1]
+        arg2 = kwargs[var2]
+        t = (arg1,arg2)
+        if t in self.kwcache:
+            return self.kwcache[t]
+        w_kwargs = space.newdict()
+        
+        dom = self.doms[var1]
+        w_val = space.getitem( dom.w_values, space.wrap(arg1) )
+        w_kwargs.content[space.wrap(var1)] = w_val
+        
+        dom = self.doms[var2]
+        w_val = space.getitem( dom.w_values, space.wrap(arg2) )
+        w_kwargs.content[space.wrap(var2)] = w_val
+        
+        res = space.is_true(space.call(self.w_filter_func,
+                                       space.newlist([]),
+                                       w_kwargs))
+        self.kwcache[t] = res
+        return res
+
+
+class W_Expression(W_AbstractConstraint):
+
+    def __init__(self, space, w_variables, w_formula, w_filter_func):
+        if space.int_w(space.len(w_variables)) == 2:
+            constraint = _BinaryExpression(space, w_variables, w_formula, w_filter_func)
+        else:
+            constraint = _Expression(space, w_variables, w_formula, w_filter_func)
+        W_AbstractConstraint.__init__(self, space, constraint)
+
+
+W_Expression.typedef = typedef.TypeDef("W_Expression",
+    W_AbstractConstraint.typedef)
+    
+
+def interp_make_expression(space, w_variables, w_formula, w_callable):
+    """create a new constraint of type Expression or BinaryExpression
+    The chosen class depends on the number of variables in the constraint"""
+    if not isinstance(w_formula, W_StringObject):
+        raise OperationError(space.w_TypeError,
+                             space.wrap('formula must be a string.'))
+    return W_Expression(space, w_variables, w_formula, w_callable)
+
+
+#--- Alldistinct
+
+class _AllDistinct(rc.AllDistinct):
+    """Contraint: all values must be distinct"""
+
+    def __init__(self, space, w_variables):
+        variables = []
+        for w_var in space.unpackiterable( w_variables ):
+            variables.append( space.str_w(w_var) )
+        if len(variables)==0:
+            raise OperationError( space.w_ValueError,
+                                  space.wrap("need at least one variable") )
+        rc.AllDistinct.__init__(self, variables)
+
+
+class W_AllDistinct(W_AbstractConstraint):
+    
+    def __init__(self, space, w_variables):
+        constraint = _AllDistinct(space, w_variables)
+        W_AbstractConstraint.__init__(self, space, constraint)
+
+
+W_AllDistinct.typedef = typedef.TypeDef(
+    "W_AllDistinct", W_AbstractConstraint.typedef)
+
+#function bolted into the space to serve as constructor
+def make_alldistinct(space, w_variables):
+    return space.wrap(W_AllDistinct(space, w_variables))
+

Added: pypy/dist/pypy/module/_cslib/fd.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/module/_cslib/fd.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,50 @@
+from pypy.interpreter.error import OperationError
+
+from pypy.interpreter import typedef, gateway, baseobjspace
+from pypy.interpreter.gateway import interp2app
+
+from pypy.objspace.std.listobject import W_ListObject, W_TupleObject
+from pypy.objspace.std.intobject import W_IntObject
+
+from pypy.rlib import rdomain as rd
+
+class _FiniteDomain(rd.BaseFiniteDomain):
+    """
+    Variable Domain with a finite set of possible values
+    """
+
+    def __init__(self, w_values, values):
+        """values is a list of values in the domain
+        This class uses a dictionnary to make sure that there are
+        no duplicate values"""
+        
+        assert isinstance(w_values, W_ListObject)
+        self.w_values = w_values
+        self._values = {}
+
+        if values is None:
+            for k in range(len(w_values.wrappeditems)):
+                self._values[k] = True
+        else:
+            self._values = values.copy()
+        
+        self._changed = False
+
+    def copy(self):
+        return _FiniteDomain(self.w_values, self._values)
+
+class W_FiniteDomain(baseobjspace.Wrappable):
+    def __init__(self, w_values, values):
+        assert isinstance(w_values, W_ListObject)
+        self.domain = _FiniteDomain( w_values, values )
+
+def make_fd(space, w_values):
+    if not isinstance(w_values, W_ListObject):
+        if not isinstance(w_values, W_TupleObject):
+            raise OperationError(space.w_TypeError,
+                                 space.wrap('first argument must be a list.'))
+    return W_FiniteDomain(w_values, None)
+
+W_FiniteDomain.typedef = typedef.TypeDef(
+    "W_FiniteDomain")
+

Added: pypy/dist/pypy/module/_cslib/propagation.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/module/_cslib/propagation.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,86 @@
+from pypy.rlib.rpropagation import Repository, Solver
+import pypy.rlib.rdistributor as rd
+from pypy.module._cslib import fd
+from pypy.module._cslib.constraint import W_AbstractConstraint
+
+from pypy.interpreter.error import OperationError
+
+from pypy.interpreter import typedef, gateway, baseobjspace
+from pypy.interpreter.gateway import interp2app
+
+#from pypy.objspace.std.listobject import W_ListObject, W_TupleObject
+from pypy.objspace.std.intobject import W_IntObject
+from pypy.objspace.std.listobject import W_ListObject
+from pypy.objspace.std.tupleobject import W_TupleObject
+from pypy.objspace.std.stringobject import W_StringObject
+from pypy.objspace.std.dictobject import W_DictObject
+
+
+class _Repository(Repository):
+
+    def __init__(self, space, w_variables, w_domains, w_constraints):
+        # let's just give everything unwrapped to our parent
+        doms = {}
+        for var, dom in w_domains.content.items():
+            assert isinstance( dom, fd.W_FiniteDomain )
+            assert isinstance( var, W_StringObject )
+            doms[space.str_w(var)] = dom.domain
+        constraints = []
+        for w_constraint in space.unpackiterable( w_constraints ):
+            if not isinstance( w_constraint, W_AbstractConstraint ):
+                raise OperationError( space.w_TypeError,
+                                      space.wrap("constraints needs to be a sequence of constraints" ) )
+            constraints.append( w_constraint.constraint )
+        Repository.__init__(self, doms, constraints)
+
+
+class W_Repository(baseobjspace.Wrappable):
+
+    def __init__(self, space, w_variables, w_domains, w_constraints):
+        self.repo = _Repository(space, w_variables, w_domains, w_constraints)
+
+
+W_Repository.typedef = typedef.TypeDef("W_Repository")
+
+def make_repo(space, w_variables, w_domains, w_constraints):
+    if not isinstance(w_domains,W_DictObject):
+        raise OperationError(space.w_TypeError,
+                               space.wrap('domains must be a dictionary'))
+    return W_Repository(space, w_variables, w_domains, w_constraints)
+
+                            
+class W_Solver(baseobjspace.Wrappable):
+
+    def __init__(self, space):
+        self.space = space
+        self.solver = Solver(rd.DefaultDistributor())
+
+    def w_solve(self, w_repo, w_verbosity):
+        space = self.space
+        if not isinstance(w_repo, W_Repository):
+            raise OperationError(space.w_TypeError,
+                                 space.wrap('first argument must be a repository.'))
+        if not isinstance(w_verbosity, W_IntObject):
+            raise OperationError(space.w_TypeError,
+                                 space.wrap('second argument must be an int.'))
+        self._verb = w_verbosity.intval
+        sols = self.solver.solve_all(w_repo.repo)
+        sols_w = []
+        for sol in sols:
+            w_dict = space.newdict()
+            for var,value in sol.items():
+                domain = w_repo.repo._domains[var]
+                assert isinstance( domain, fd._FiniteDomain )
+                w_var = space.wrap(var)
+                w_value = space.getitem( domain.w_values, space.wrap(value) )
+                space.setitem( w_dict, w_var, w_value )
+            sols_w.append( w_dict )
+        return space.newlist(sols_w)
+
+W_Solver.typedef = typedef.TypeDef(
+    'W_Solver',
+    solve = interp2app(W_Solver.w_solve))
+                                   
+
+def make_solver(space):
+    return W_Solver(space)

Added: pypy/dist/pypy/rlib/cslib/__init__.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/rlib/cslib/__init__.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1 @@
+#

Added: pypy/dist/pypy/rlib/cslib/btree.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/rlib/cslib/btree.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,43 @@
+"""
+A minimalist binary tree implementation
+whose values are (descendants of) BTreeNodes.
+This alleviates some typing difficulties when
+using TimSort on lists of the form [(key, Thing), ...]
+"""
+
+class BTreeNode:
+
+    def __init__(self, key):
+        self.key = key
+        self.left = None
+        self.right = None
+
+    def add(self, val):
+        key = val.key
+        assert isinstance(key, int)
+        assert isinstance(val, BTreeNode)
+        
+        if key > self.key:
+            if self.right:
+                self.right.add(val)
+            else:
+                self.right = val
+        else:
+            if self.left:
+                self.left.add(val)
+            else:
+                self.left = val
+
+
+    def _values(self, dest):
+        if self.left:
+            self.left._values(dest)
+        dest.append(self)
+        if self.right:
+            self.right._values(dest)
+
+    def get_values(self):
+        dest = []
+        self._values( dest )
+        return dest
+

Added: pypy/dist/pypy/rlib/cslib/rconstraint.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/rlib/cslib/rconstraint.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,262 @@
+from pypy.rlib.btree import BTreeNode
+from pypy.rlib.rdomain import BaseFiniteDomain, ConsistencyError
+
+
+class AbstractConstraint:
+    
+    def __init__(self, variables):
+        """variables is a list of variables which appear in the formula"""
+        assert isinstance(variables, list)
+
+        self._variables = variables
+
+    def revise(self, domains):
+        "domains : {'var':Domain}"
+        return False
+
+    def estimate_cost(self, domains):
+        cost = 1
+        for var in self._variables:
+            dom = domains[var]
+            assert isinstance(dom, BaseFiniteDomain)
+            cost = cost * dom.size()
+        return cost
+
+
+class Quadruple(BTreeNode):
+    def __init__(self, key, varname, values, index):
+        BTreeNode.__init__( self, key )
+        self.var = varname
+        self.values = values
+        self.index = index
+
+class Expression(AbstractConstraint):
+    """A constraint represented as a functional expression."""
+
+    def __init__(self, variables):
+        AbstractConstraint.__init__(self, variables)
+        self.doms = {}
+        
+    def filter_func(self, kwargs):
+        return False
+
+    def _init_result_cache(self):
+        """key = (variable,value), value = [has_success,has_failure]"""
+        result_cache = {}
+        for var in self._variables:
+            result_cache[var] = {}
+        return result_cache
+
+    def _assign_values(self, doms):
+        kwargs = {}
+        sorted_vars = None
+        for variable in self._variables:
+            domain = doms[variable]
+            assert isinstance(domain, BaseFiniteDomain)
+            values = domain.get_values()
+            node = Quadruple(domain.size(),
+                             variable,
+                             values,
+                             0)
+            if sorted_vars is None:
+                sorted_vars = node
+            else:
+                sorted_vars.add( node )
+            kwargs[variable] = values[0]
+
+        # get sorted variables to instanciate those with fewer possible values first
+        assert sorted_vars is not None
+        self._assign_values_state = sorted_vars.get_values()
+        return kwargs
+        
+    def _next_value(self, kwargs):
+
+        # try to instanciate the next variable
+        variables = self._assign_values_state
+
+        for curr in variables:
+            if curr.index < curr.key:
+                kwargs[curr.var] = curr.values[curr.index]
+                curr.index += 1
+                break
+            else:
+                curr.index = 0
+                kwargs[curr.var] = curr.values[0]
+        else:
+            # it's over
+            return None
+        return kwargs
+
+    def revise(self, doms):
+        """generic propagation algorithm for n-ary expressions"""
+        self.doms = doms
+        maybe_entailed = True
+        result_cache = self._init_result_cache()
+
+        kwargs = self._assign_values(doms)
+
+        while 1:
+            kwargs = self._next_value(kwargs)
+            if kwargs is None:
+                break
+                                   
+            if maybe_entailed:
+                for varname, val in kwargs.iteritems():
+                    val_dict = result_cache[varname]
+                    if val not in val_dict:
+                        break
+                else:
+                    continue
+            if self.filter_func(kwargs):
+                for var, val in kwargs.items():
+                    var_dict = result_cache[var]
+                    var_dict[val] = True
+            else:
+                maybe_entailed = False
+
+        try: # XXX domains in rlib, too
+            for varname, keep in result_cache.items():
+                domain = doms[varname]
+                assert isinstance(domain, BaseFiniteDomain)
+                domain.remove_values([val
+                                      for val in domain.get_values()
+                                      if val not in keep])
+        except KeyError:
+            # There are no more value in result_cache
+            pass
+
+        return maybe_entailed
+        
+
+    def __repr__(self):
+        return '<%s>' % self.formula
+
+    
+#--- Alldistinct
+
+class VarDom(BTreeNode):
+    def __init__(self, key, var, dom):
+        BTreeNode.__init__(self, key)
+        self.var = var
+        self.dom = dom
+
+class AllDistinct(AbstractConstraint):
+    """Contraint: all values must be distinct"""
+
+    def __init__(self, variables):
+        AbstractConstraint.__init__(self, variables)
+        # worst case complexity
+        self._cost = len(self._variables) * (len(self._variables) - 1) / 2
+
+    def estimate_cost(self, domains):
+        return self._cost
+        
+    def revise(self, doms):
+
+        sorted_vars = None
+        for var in self._variables:
+            dom = doms[var]
+            assert isinstance(dom, BaseFiniteDomain)
+            node = VarDom(dom.size(), var, dom)
+            if sorted_vars is None:
+                sorted_vars = node
+            else:
+                sorted_vars.add(node)
+
+        assert sorted_vars is not None
+        variables = sorted_vars.get_values()
+        
+        # if a domain has a size of 1,
+        # then the value must be removed from the other domains
+        for var_dom in variables:
+            if var_dom.dom.size() == 1:
+                #print "AllDistinct removes values"
+                for var_dom2 in variables:
+                    if var_dom2.var != var_dom.var:
+                        try:
+                            var_dom2.dom.remove_value(var_dom.dom.get_values()[0])
+                        except KeyError, e:
+                            # we ignore errors caused by the removal of
+                            # non existing values
+                            pass
+
+        # if there are less values than variables, the constraint fails
+        values = {}
+        for var_dom in variables:
+            for val in var_dom.dom.get_values():
+                values[val] = 0
+
+        if len(values) < len(variables):
+            #print "AllDistinct failed"
+            raise ConsistencyError
+
+        # the constraint is entailed if all domains have a size of 1
+        for var_dom in variables:
+            if not var_dom.dom.size() == 1:
+                return False
+
+        #print "All distinct entailed"
+        return True
+
+
+
+#--- Binary expressions
+
+class BinaryExpression(Expression):
+    """A binary constraint represented as a python expression
+
+    This implementation uses a narrowing algorithm optimized for
+    binary constraints."""
+    
+    def __init__(self, variables):
+        assert len(variables) == 2
+        Expression.__init__(self, variables)
+
+    def revise(self, domains):
+        """specialized pruning algorithm for binary expressions
+        Runs much faster than the generic version"""
+        self.doms = domains
+        maybe_entailed = True
+        var1 = self._variables[0]
+        dom1 = domains[var1]
+        values1 = dom1.get_values()
+        var2 = self._variables[1]
+        dom2 = domains[var2]
+        values2 = dom2.get_values()
+        if dom2.size() < dom1.size():
+            var1, var2 = var2, var1
+            dom1, dom2 = dom2, dom1
+            values1, values2 = values2, values1
+            
+        kwargs = {}
+        keep1 = {}
+        keep2 = {}
+        maybe_entailed = True
+        # iterate for all values
+        for val1 in values1:
+            kwargs[var1] = val1
+            for val2 in values2:
+                kwargs[var2] = val2
+                if val1 in keep1 and val2 in keep2 and not maybe_entailed:
+                    continue
+                if self.filter_func(kwargs):
+                    keep1[val1] = 1
+                    keep2[val2] = 1
+                else:
+                    maybe_entailed = False
+
+        dom1.remove_values([val for val in values1 if val not in keep1])
+        dom2.remove_values([val for val in values2 if val not in keep2])
+
+        return maybe_entailed
+
+
+class BinEq(BinaryExpression):
+    def filter_func(self, kwargs):
+        values = kwargs.values()
+        return values[0]==values[1]
+
+class BinLt(BinaryExpression):
+    def filter_func(self, kwargs):
+        values = kwargs.values()
+        return values[0] < values[1]

Added: pypy/dist/pypy/rlib/cslib/rdistributor.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/rlib/cslib/rdistributor.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,77 @@
+"""
+distributors - part of constraint satisfaction solver.
+"""
+
+def make_new_domains(domains):
+    """return a shallow copy of dict of domains passed in argument"""
+    new_domains = {}
+    for key, domain in domains.items():
+        new_domains[key] = domain.copy()
+    return new_domains
+
+class AbstractDistributor:
+    """Implements DistributorInterface but abstract because
+    _distribute is left unimplemented."""
+        
+    def find_smallest_domain(self, domains):
+        """returns the variable having the smallest domain.
+        (or one of such varibles if there is a tie)
+        """
+        k = 0
+        doms = domains.items()
+        while k<len(doms):
+            var, dom = doms[k]
+            sz = dom.size()
+            if sz>1:
+                min_size = sz
+                min_var = var
+                break
+            k += 1
+        else:
+            raise RuntimeError, "should not be here"
+        while k<len(doms):
+            var, dom = doms[k]
+            if 1 < dom.size() < min_size:
+                min_var = var
+                min_size = dom.size()
+            k += 1
+        return min_var
+
+    def distribute(self, domains):
+        """
+        domains -> two variants of the same modified domain
+        do the minimal job and let concrete class distribute variables
+        """
+        doms1 = make_new_domains(domains)
+        doms2 = make_new_domains(domains)
+        for modified_domain in self._distribute(doms1,doms2):
+            modified_domain._changed = False
+        return [doms1,doms2]
+        
+class AllOrNothingDistributor(AbstractDistributor):
+    """distributes domains by splitting the smallest domain in 2 new domains
+    The first new domain has a size of one,
+    and the second has all the other values"""
+
+    def _distribute(self, doms1, doms2):
+        """See AbstractDistributor"""
+        variable = self.find_smallest_domain(doms1)
+        values = doms1[variable].get_values()
+        doms1[variable].remove_values(values[1:])
+        doms2[variable].remove_value(values[0])
+        return [doms1[variable], doms2[variable]]
+
+class DichotomyDistributor(AbstractDistributor):
+    """distributes domains by splitting the smallest domain in
+    two equal parts or as equal as possible."""
+    
+    def _distribute(self, doms1, doms2):
+        """See AbstractDistributor"""
+        variable = self.find_smallest_domain(doms1)
+        values = doms1[variable].get_values()
+        middle = len(values)/2
+        doms1[variable].remove_values(values[:middle])
+        doms2[variable].remove_values(values[middle:])
+        return [doms1[variable], doms2[variable]]
+
+DefaultDistributor = DichotomyDistributor

Added: pypy/dist/pypy/rlib/cslib/rdomain.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/rlib/cslib/rdomain.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,93 @@
+
+class ConsistencyError(Exception):
+    pass
+
+class BaseFiniteDomain:
+    """
+    Variable Domain with a finite set of int values
+    """
+
+    def __init__(self, values):
+        """values is a list of values in the domain
+        This class uses a dictionnary to make sure that there are
+        no duplicate values"""
+        #assert isinstance(values, dict)
+        self._values = values.copy()
+        self._changed = False
+
+    def copy(self):
+        return BaseFiniteDomain(self._values)
+            
+    def _value_removed(self):
+        "The implementation of remove_value should call this method"
+        if self.size() == 0:
+            raise ConsistencyError, "tried to make a domain empty"
+        
+    def remove_value(self, value):
+        """Remove value of domain and check for consistency"""
+        del self._values[value]
+        self._value_removed()
+        self._changed = True
+
+    def remove_values(self, values):
+        assert isinstance(values, list)
+        if len(values) > 0:
+            for val in values:
+                del self._values[val]
+            self._value_removed()
+            self._changed = True
+
+    def size(self):
+        """computes the size of a finite domain"""
+        return len(self._values)
+    
+    def get_values(self):
+        return self._values.keys()
+
+
+    def __repr__(self):
+        return "<Domain %s>" % self._values.keys()
+
+# XXX finish this
+class TodoBaseFiniteDomain:
+    """
+    Variable Domain with a finite set of int values
+    """
+
+    def __init__(self, values):
+        """values is a list of values in the domain
+        This class uses a dictionnary to make sure that there are
+        no duplicate values"""
+        assert isinstance(values, int)
+        self._values = [True] * values
+
+    def copy(self):
+        dom = BaseFiniteDomain(len(self._values))
+        for i, v in enumerate(self._values):
+            dom._values[i] = v
+
+    def _value_removed(self):
+        "The implementation of remove_value should call this method"
+        if self.size() == 0:
+            raise ConsistencyError, "tried to make a domain empty"
+        
+    def remove_value(self, value):
+        """Remove value of domain and check for consistency"""
+        assert isinstance(value, int)
+        del self._values[value]
+        self._value_removed()
+
+    def remove_values(self, values):
+        assert isinstance(values, list)
+        if len(values) > 0:
+            for val in values:
+                del self._values[val]
+            self._value_removed()
+
+    def size(self):
+        """computes the size of a finite domain"""
+        return len(self._values)
+    
+    def get_values(self):
+        return self._values.keys()
+

Added: pypy/dist/pypy/rlib/cslib/rpropagation.py
==============================================================================
--- (empty file)
+++ pypy/dist/pypy/rlib/cslib/rpropagation.py	Fri Feb 16 19:33:37 2007
@@ -0,0 +1,151 @@
+"""The code of the constraint propagation algorithms"""
+from pypy.rlib.rconstraint import AbstractConstraint, ConsistencyError
+
+class Repository:
+    """Stores variables, domains and constraints
+    Propagates domain changes to constraints
+    Manages the constraint evaluation queue"""
+
+    def __init__(self, domains, constraints):
+        self._variables = domains.keys()   # list of variable names
+        self._domains = domains    # maps variable name to domain object
+        self._constraints = [] # list of constraint objects
+        self._variableListeners = {}
+        for var in self._variables:
+            self._variableListeners[var] = []
+        for constr in constraints:
+            self.add_constraint( constr )
+
+    def __repr__(self):
+        return '<Repository nb_constraints=%d domains=%s>' % \
+                               (len(self._constraints), self._domains)
+
+    def add_constraint(self, constraint):
+        assert isinstance( constraint, AbstractConstraint )
+        if 0: # isinstance(constraint, BasicConstraint):
+            # Basic constraints are processed just once
+            # because they are straight away entailed
+            var = constraint.getVariable()
+            constraint.revise({var: self._domains[var]})
+        else:
+            self._constraints.append(constraint)
+            for var in constraint._variables:
+                self._variableListeners[var].append(constraint)
+        
+    def _remove_constraint(self, constraint):
+        self._constraints.remove(constraint)
+        for var in constraint._variables:
+            try:
+                self._variableListeners[var].remove(constraint)
+            except ValueError:
+                raise ValueError('Error removing constraint from listener',
+                                 var,
+                                 self._variableListeners[var],
+                                 constraint)
+
+    def get_domains(self):
+        return self._domains
+
+    def distribute(self, distributor):
+        """
+        create new repository using the distributor and self
+        using changed domains
+        """
+        d1, d2 = distributor.distribute(self._domains)
+        return [Repository(d1, self._constraints),
+                Repository(d2, self._constraints)]
+    
+    def propagate(self):
+        """Prunes the domains of the variables
+        This method calls constraint.narrow() and queues constraints
+        that are affected by recent changes in the domains.
+        Returns True if a solution was found"""
+
+        _queue = [(constr.estimate_cost(self._domains), constr)
+                  for constr in self._constraints ]
+        # XXX : _queue.sort()
+        _affected_constraints = {}
+        while True:
+            if not _queue:
+                # refill the queue if some constraints have been affected
+                _queue = [(constr.estimate_cost(self._domains), constr)
+                          for constr in _affected_constraints]
+                if not _queue:
+                    break
+                # XXX _queue.sort()
+                _affected_constraints.clear()
+            cost, constraint = _queue.pop(0)
+            entailed = constraint.revise(self._domains)
+            for var in constraint._variables:
+                # affected constraints are listeners of
+                # affected variables of this constraint
+                dom = self._domains[var]
+                if not dom._changed: # XXX
+                    continue
+                for constr in self._variableListeners[var]:
+                    if constr is not constraint:
+                        _affected_constraints[constr] = True
+                dom._changed = False
+            if entailed:
+                self._remove_constraint(constraint)
+                if constraint in _affected_constraints:
+                    del _affected_constraints[constraint]
+                
+        for domain in self._domains.values():
+            if domain.size() != 1:
+                return 0
+        return 1
+
+
+    def solve_all(self, distributor):
+        solver = Solver(distributor)
+        return solver.solve_all(self)
+
+
+import os
+
+class Solver:
+    """Top-level object used to manage the search"""
+
+    def __init__(self, distributor):
+        """if no distributer given, will use the default one"""
+        self._verb = 0
+        self._distributor = distributor
+        self.todo = []
+
+    def solve_one(self, repository):
+        self.todo = [repository]
+        return self.next_sol()
+
+    def solve_all(self, repository):
+        self.todo = [repository]
+        solutions = []
+        while True:
+            sol = self.next_sol()
+            if sol is not None:
+                solutions.append( sol )
+                if self._verb:
+                    os.write(1, 'found solution : %s\n' % sol)
+            else:
+                break
+        return solutions
+
+    def next_sol(self):
+        found_solution = False
+        todo = self.todo
+        while todo:
+            repo = todo.pop()
+            try:
+                found_solution = repo.propagate()
+            except ConsistencyError:
+                continue
+            if found_solution:
+                solution = {}
+                for variable, domain in repo.get_domains().items():
+                    solution[variable] = domain.get_values()[0]
+                return solution
+            else:
+                for rep in repo.distribute(self._distributor):
+                    todo.append( rep )
+        return None
+    



More information about the Pypy-commit mailing list