[pypy-svn] r29003 - pypy/dist/pypy/doc

auc at codespeak.net auc at codespeak.net
Tue Jun 20 17:10:56 CEST 2006


Author: auc
Date: Tue Jun 20 17:10:55 2006
New Revision: 29003

Modified:
   pypy/dist/pypy/doc/howto-logicobjspace-0.9.txt
Log:
more of the howto (on constraint stuff)

Modified: pypy/dist/pypy/doc/howto-logicobjspace-0.9.txt
==============================================================================
--- pypy/dist/pypy/doc/howto-logicobjspace-0.9.txt	(original)
+++ pypy/dist/pypy/doc/howto-logicobjspace-0.9.txt	Tue Jun 20 17:10:55 2006
@@ -21,7 +21,7 @@
 The 0.9 preview comes without logic programming ; the constraint
 solver is only lightly tested, is not equipped with some specialized
 but important propagators for linear relations on numeric variables,
-and *might* support concurrency -- but that would be an accident ; the
+and *might* support concurrency -- but that would be an accident; the
 dataflow scheduling of coroutines is known to fail in at least one
 basic and important case.
 
@@ -29,7 +29,7 @@
 information and examples for an uninformed user to understand what is
 going on and how to use the provided functionnality.
 
-To fire up a working PyPy with the LO, please type :
+To fire up a working PyPy with the LO, please type::
 
 /root-of-pypy-dist/pypy/bin/py.py -o logic --usemodules=_stackless
 
@@ -43,9 +43,9 @@
 Description and examples
 ------------------------
 
-Logic variables are similar to Python variables in the following sense
-: they map names to values in a defined scope. But unlike normal
-Python variables, they have two states : free and bound. A bound logic
+Logic variables are similar to Python variables in the following
+sense:: they map names to values in a defined scope. But unlike normal
+Python variables, they have two states:: free and bound. A bound logic
 variable is indistinguishable from a normal Python value which it
 wraps. A free variable can only be bound once (it is also said to be a
 single-assignment variable). It is good practice to denote these
@@ -64,7 +64,7 @@
   bind(X, 42)
   assert X / 2 == 24
 
-The single-assignment property is easily checked :
+The single-assignment property is easily checked::
 
   bind(X, 'hello') # would raise a FailureException
 
@@ -89,7 +89,7 @@
   assert [1, 2] == [1, 2]   
   assert 42 == 43           
 
-A basic example involving logic variables embedded into a dictionnary::
+A basic example involving logic variables embedded into dictionnaries::
 
   Z, W = newvar(), newvar()
   unify({'a': 42, 'b': Z},
@@ -104,7 +104,7 @@
 Assignment or aliasing of variables is provided underneath by the
 'bind' operator.
 
-An example involving custom data types :
+An example involving custom data types::
 
   class Foo(object):
       def __init__(self, a):
@@ -125,15 +125,15 @@
 
 Logic variables support the following operators (with their arity) :
 
-Predicates : 
+Predicates:: 
  is_free/1   # applies to anything
  is_bound/1  # applies to anything
  alias_of/2  # wants logic variables
 
-Creation:
+Variable Creation::
  newvar/0
  
-Mutators :
+Mutators::
  bind/2      # wants a logic variable in first position
  unify/2     # applies to anything
 
@@ -148,12 +148,11 @@
 language. With respect to behaviour under concurrency conditions,
 logic variables come with two operators :
 
-* wait : this suspends the current thread until the variable is bound,
+* wait:: this suspends the current thread until the variable is bound,
   it returns the value otherwise (impl. note : in the logic
-  objectspace, all operators make an implicit wait on their value
-  arguments)
+  objectspace, all operators make an implicit wait on their arguments)
 
-* wait_needed : this suspends the current thread until the variable
+* wait_needed:: this suspends the current thread until the variable
   has received a wait message. It has to be used explicitely,
   typically by a producer thread that wants to produce data only when
   needed.
@@ -245,3 +244,205 @@
 
 This program currently blocks on the first call to sleep (we don't
 even get a chance to reach the first unify operation).
+
+
+Constraint Programming
+======================
+
+The LO comes stuffed with a flexible, extensible constraint solver
+engine. While regular search strategies such as depth-first or
+breadth-first search are provided, you can write better, specialized
+strategies (an exemple would be best-search). We therein describe how
+to use the solver to specify and get the solutions of a constraint
+satisfaction problem, and then highlight how to extend the solver with
+new strategies.
+
+Specification of a problem, getting solutions
+---------------------------------------------
+
+A constraint satisfaction problem is defined by a triple (X, D, C)
+where X is a set of finite domains variables, D the set of domains
+assocated to the variables in X, and C the set of constraints, or
+relations, that bind together the variables of X.
+
+Note that the constraint variables are NOT logic variables. Not yet
+anyway.
+
+So we basically need a way to declare variables, their domains and
+relations ; and something to hold these together. The later is what we
+call a "computation space". The notion of computation space is broad
+enough to encompass constraint and logic programming, but we use it
+there only as a box that holds the elements of our constraint
+satisfaction problem.
+
+A problem is a one-argument procedure defined as follow::
+
+  def simple_problem(cs):
+      cs.var('x', FiniteDomain(['spam', 'egg', 'ham']))
+      cs.var('y', FiniteDomain([3, 4, 5]))
+
+This snippet defines a couple of variables and their domains. Note
+that we didn't take a reference of the created variables. We can query
+the space to get these back if needed, and then complete the
+definition of our problem.
+
+      # ... continued ...
+      x = cs.find_var('x')
+      y = cs.find_var('y')
+      cs.tell(make_expression([x,y], 'len(x) == y'))
+      
+      return x, y
+      
+We must be careful to return the set of variables whose candidate
+values we are interested in. The rest should be sufficiently
+self-describing ... Now to print solutions out of this, we must::
+
+  import solver
+  cs = newspace()
+  cs.define_problem(simple_problem)
+  
+  for sol in solver.solve(cs):
+      print sol
+
+The builtin solve function returns a generator. You will note with
+pleasure how slow the search can be on a solver running on a Python
+interpreter written in Python and running on top of cpython... It is
+expected that the compiled version of PyPy + LO will provide decent
+performance.
+
+Operators for CSP specification
+-------------------------------
+
+Note that below, "variable/expression designators" really are strings.
+
+Space creation::
+ newspace/0
+
+Finite domain creation::
+ FiniteDomain/1    # a list/tuple of arbitrary objects
+
+Expressions::
+ make_expression/2 # a list of var. designators,
+                   # an expression encoded as a string
+ AllDistinct/0
+
+Space methods::
+ var/2             # variable designator (string) and a finite domain
+ find_var/1        # variable designator
+ tell/1            # an expression (built with make_expression
+                   # or AllDistinct)
+ define_problem/1  # an unary procedure (1)
+
+(1) takes a computation space as argument, returns tuple of constraint
+    variables.
+
+
+Extending the engine
+--------------------
+
+Here we show how the primitives allow you to write, in pure Python, a
+very basic solver that will search depth-first and return the first
+found solution.
+
+As we've seen, a CSP is encapsulated into a so-called "computation
+space". The space object has additional methods that allow the solver
+implementor to drive the search. First, let us see some code driving a
+binary depth-first search::
+
+1   def first_solution_dfs(space):
+2       status = space.ask()
+3       if status == 0:
+4           return None
+5       elif status == 1:
+6           return space
+7       else:
+8           new_space = space.clone()
+9           space.commit(1)
+10          outcome = first_solution_dfs(space)
+11          if outcome is None:
+13              new_space.commit(2)
+14              outcome = first_solution_dfs(new_space)
+15          return outcome
+
+This recursive solver takes a space as argument, and returns the first
+space containing a solution or None. Let us examine it piece by piece
+and discover the basics of the solver protocol.
+
+The first thing to do is "asking" the space about its status. This may
+force the "inside" of the space to check that the values of the
+domains are compatibles with the constraints. Every inconsistent value
+is removed from the variable domains. This phase is called "constraint
+propagation". It is crucial because it prunes as much as possible of
+the search space. Then, the call to ask returns a positive integer
+value, which means that all (possibly concurrent) computations of the
+space are terminated.
+
+At this point, either::
+
+* the space is failed (status == 0), which means that there is no set
+  of values of the finite domains that can satisfy the constraints,
+
+* one solution has been found (status == 1):: there is exactly one
+  valuation of the variables that satisfy the constraints,
+
+* several branches of the search space can be taken (status represents
+  the exact number of available alternatives, or branches).
+
+Now, we have written this toy solver as if there could be a maximum of
+two alternatives. This assumption holds for the simple_problem we
+defined above, where a binary "distributor" (see below for an
+explanation of this term) has been chosen automatically for us, but
+not in the general case. See the sources (applevel/solver.py) for a
+general-purpose solver.
+
+In line 8, we take a clone of the space; nothing is shared between
+space and newspace. Having taken a clone, we now have two identical
+versions of the space that we received. This will allow us to explore
+the two alternatives. This step is done, line 9 and 13, with the call
+to commit, each time with a different integer value representing the
+branch to be taken. The rest should be sufficiently self-describing.
+
+This shows the two important space methods used by a search engine::
+ask, which waits for the stability of the space and informs the solver
+of its status, and commit, which tells a space which road to take in
+case of a fork. 
+
+Now, earlier, we talked of a "distributor":: it is a program running
+in a computation space. It could be anything, and in fact, in the
+final version of the LO, it will be any Python program, augmented with
+calls to non-deterministic choice points. Each time a program embedded
+in a computation space reaches such a point, it blocks until some Deus
+ex machina make the choice for him. Only a solver can be responsible
+for the actual choice (that is the reason for the name
+"non-deterministic":: the decision does not belong to the embedded
+program, only to the solver that drives it).
+
+In the case of a CSP, the distributor is a simple piece of code, which
+works only after the propagation phase. The standard way of CSP
+solving is typically using binary search, so there is a default,
+binary distributor set up in any new space.
+
+Here are two examples of distribution strategies::
+
+* take the variable with the biggest domain, and remove exactly one
+  value from its domain,
+
+* take a variable with a small domain, and keep only one value in the
+  domain (in other words, we "instantiate" the variable).
+
+There are a great many ways to distribute ... Some of them perform
+better, depending on the caracteristics of the problem to be
+solved. But there is no absolutely better distribution strategy. Note
+that the second strategy given as example there is what is used (and
+hard-wired) in the MAC algorithm.
+
+Remaining space operators
+-------------------------
+
+For solver writers::
+ ask/0           # returns i| 0 <= i <= maxint
+ commit/1        # takes integer in [1, i]
+
+For distributor writers::
+ choose
+



More information about the Pypy-commit mailing list