Logilab Constraints Documentation

Some bits of documentation for logilab constraints, by Alexandre Fayolle.

Constraint Propagation in Python

Alexandre Fayolle

Copyright © 2002 by Logilab This manual presents how to use the constraint package to solve constraint propagation problems in Python. It also presents the the extension mechanisms available in the package

Using the constraint package

Sample problem

Let's take a real-life problem to get started. You're organising a an international Python Programming event, and you have to schedule conferences. There are 10 different conferences, you have 3 conference rooms available during 2 days. Each conference is 4 hours long, so you can organize at most 2 conferences per day in a given room.

Conferences 3, 4, 5 and 6 have to take place in room C, because it's the only one with Internet access.

Some of the speakers are not available on both days, so conferences 1, 5 and 10 have to take place on day 1, and conferences 2, 3 4 and 9 on day 2.

You have made a quick poll over the python mailing list, and it turns out that people attending some of the conferences are likely to be attending some other conferences too, so you want to make sure that such conferences are not scheduled at the same time. A careful statistical study has found 4 groups of potential attendees. The first group want to attend conferences 1, 2, 3 and 10, the second conferences 2, 6, 8 and 9 the third group conferences 3, 5, 6 and 7, and the last group conferences 1, 3, 7 and 8.

You've tried to put this on a whiteboard, but this quickly proved to be tedious, so you thought about using the constraint solving package.

Variables, Domains and Constraints

The first thing to find out in order to use the constraint package is what the variables are, what their domains are and what the constraints between variables are.

If we look at our problem, the variables are the conferences' room and time slot, and for each conference, the domain is the cross product of the set of available rooms with the set of available time slots. We could say that the conferences which require Internet access have a different domain, because they need to be in room C. This is perfectly valid. However, we will model this as a constraint.

Variables are manipulated as names, stored in character strings. Domains are instances of the fd.FiniteDomain class, which is instantiated with a list of values. Domains are manipulated through a dictionnary mapping a variable to its domain. Do not use the same domain instance for several variables, because in the current implementation, this is guaranteed to break. The code looks like this:

# import Repository class and fd module,
from logilab.constraint import *
variables = ('c01','c02','c03','c04','c05','c06','c07','c08','c09','c10')
values = [(room,slot)
          for room in ('room A','room B','room C')
          for slot in ('day 1 AM','day 1 PM','day 2 AM','day 2 PM')]
domains = {}
for v in variables:
    domains[v]=fd.FiniteDomain(values)

Constraints, like domains are objects. So far the only class that can be used is fd.Expression and fd.BinaryExpression. We use the fd.make_expression factory function to build an instance of the right class, depending on the number of variables that is passed. This function takes a list of affected variables and a python expression that evaluates to true if the constraint is satisfied.

We have several constraints on our variables. First some conferences need to take place in room C:

constraints = []
for conf in ('c03','c04','c05','c06'):
    constraints.append(fd.make_expression((conf,),
                                          "%s[0] == 'room C'"%conf))

Availability of the speakers impose some more constraints:

for conf in ('c01','c05','c10'):
    constraints.append(fd.make_expression((conf,),
                                          "%s[1].startswith('day 1')"%conf))
for conf in ('c02','c03','c04','c09'):
    constraints.append(fd.make_expression((conf,),
                                          "%s[1].startswith('day 2')"%conf))

Then we want to say that some of the conferences should not be scheduled at the same time:

groups = (('c01','c02','c03','c10'),
          ('c02','c06','c08','c09'),
          ('c03','c05','c06','c07'),
          ('c01','c03','c07','c08'))
for g in groups:
    for conf1 in g:
        for conf2 in g:
            if conf2 > conf1:
                constraints.append(fd.make_expression((conf1,conf2),
                                                      '%s[1] != %s[1]'%\
                                                        (conf1,conf2)))

Finally, no two conferences can be scheduled in the same room at the same time:

for conf1 in variables:
    for conf2 in variables:
        if conf2 > conf1:
            constraints.append(fd.make_expression((conf1,conf2),
                                                  '%s != %s'%(conf1,conf2)))

The Repository class

Repository objects are used to hold the variables, domains and constraints describing the problem. A Solver object can solve the problem described by a Repository.

Here's the final touch:

r = Repository(variables,domains,constraints)
solutions = Solver().solve(r)
print solutions

The code is available in the file conferences.py in the examples directory of the distribution. It finds 64 possible schedules in a couple of seconds on my machine.

Performance considerations

There is still a lot of things to be worked on with this package. Here are a few tips that can help you to use it in its current state:

  • Try to avoid constraints with a lot of variables. It is better to have several binary constraints than one big N-ary constraint with several 'and' conditions
  • If you cannot avoid constraint with lots of variable, put the constraints with less variables first in the list, because they will get evaluated before, will take less time to process, and will hopefully reduce the domains of the variables playing a role in your big constraints