3

This question is about the python package constraint (see http://labix.org/python-constraint), in particular the built-in "AllEqualConstraint". In a problem involving 4 variables, I would like to enforce the first two and the second two to be equal, i.e.:

p = Problem()
p.addVariables([1,2,3,4], [0,1])
p.addConstraint(AllEqualConstraint(), [1,2])
p.addConstraint(AllEqualConstraint(), [3,4])

I only get two solutions:

for sol in p.getSolutions():
  print sol

> {1: 1, 2: 1, 3: 1, 4: 1}
> {1: 0, 2: 0, 3: 0, 4: 0}

where I would expect to see four, namely:

> {1: 1, 2: 1, 3: 1, 4: 1}
> {1: 1, 2: 1, 3: 0, 4: 0}
> {1: 0, 2: 0, 3: 1, 4: 1}
> {1: 0, 2: 0, 3: 0, 4: 0}

My question is: Can anyone confirm that this is what the package intends to compute and what the reasoning behind it is?

Ps: I have contacted the authors of this package, but got no reply yet. I know that this package is fairly well known and that there have been questions about it on StackOverflow before.

In answer to LVC: The constraint does not always apply the constraint to all variables:

p = Problem()
p.addVariables([1,2,3], [0,1])
p.addConstraint(AllEqualConstraint(), [1,2])

gives

> {1: 1, 2: 1, 3: 1}
> {1: 1, 2: 1, 3: 0}
> {1: 0, 2: 0, 3: 1}
> {1: 0, 2: 0, 3: 0}

as expected. If the AllEqualConstraint didn't respect variables, it would be very limited.

Leevi L
  • 1,538
  • 2
  • 13
  • 28

2 Answers2

2

The module's source code contains this in how it enforeces AllEqualConstraint:

def __call__(self, variables, domains, assignments, forwardcheck=False,
             _unassigned=Unassigned):
    singlevalue = _unassigned
    for value in assignments.values():
        if singlevalue is _unassigned:
            singlevalue = value
        elif value != singlevalue:
            return False

The code that calls this in each Solver does this:

for constraint, variables in vconstraints[variable]:
    if not constraint(variables, domains, assignments,
                      pushdomains):
        # Value is not good.
        break

assignments contains all assignments for all variables, not just the variables affected by the constraint. This means that AllEqualConstraint doesn't care about the variables you put in addConstraint - it always tests that all the variables in the potential solution are equal, which is why you're missing the two solutions where that isn't the case.

The only time AllEqualConstraint looks at the variables argument is when it is called with forwardcheck being truthy. In that case, it does this:

if forwardcheck and singlevalue is not _unassigned:
    for variable in variables:
        if variable not in assignments:
            domain = domains[variable]
            if singlevalue not in domain:
                return False
            for value in domain[:]:
                if value != singlevalue:
                    domain.hideValue(value)

But none of the provided solvers appear to ever call a constraint with any forwardcheck value other than the default - which, in the case of AllEqualConstraint is False.

So, you will have to specify your own manual constraint - but this isn't too hard, since they can just be functions that take the appropriate number of variables (it gets wrapped in a FunctionConstraint, so you don't need to care about all the other stuff that gets passed to Constraint.__call__).

What you want as a Constraint, therefore, is a function that takes two variables, and returns whether they are equal. operator.eq fits the bill nicely:

p.addConstraint(operator.eq, [1, 2])
p.addConstraint(operator.eq, [3, 4])

Should give you the result you're looking for.

So scale this to more than two variables, you can write your own function:

def all_equal(a, b, c):
    return a == b == c

Or, more generally:

def all_equal(*vars):
    if not vars:
       return True
    return all(var == var[0] for var in vars)
lvc
  • 34,233
  • 10
  • 73
  • 98
  • Good stuff. I know about the `FunctionConstraint`. Its just that if I have the choice of using a built-in constraint or the equivalent function constraint, I would always go for the built-in version. But you're right: In this case I would have to use a function constraint. – Leevi L Aug 29 '12 at 13:04
  • The `AllEqualConstraint` _does_ respect variables, see edit of my question. – Leevi L Aug 29 '12 at 13:27
  • @LeeviL are you using a different solver than the provided ones by any chance? If not, then I have no idea how the `AllEqualConstraint` is working in the example you give - as far as my ability to follow the code goes (at least, this side of local midnight..), the `AllEqualConstraint` will respect variables only sometimes (and I've updated my answer with all the gory details), but none of the solvers in that module will call it that way. – lvc Aug 29 '12 at 14:39
1

[Too long for a comment.]

The edit you made doesn't show that AllEqualConstraint respects the variable restrictions, only that it doesn't always manage to ignore them completely. The code segment that @lvc noted does what he said it does. If you instrument it, you can see (using a,b,c,d as variable names for clarity) that

from constraint import *
p = Problem()
p.addVariables(list("abcd"), [0, 1])
p.addConstraint(AllEqualConstraint(), list("ab"))
p.addConstraint(AllEqualConstraint(), list("cd"))
print p.getSolutions()

eventually produces

variables: ['c', 'd']
domains: {'a': [0, 1], 'c': [0, 1], 'b': [1], 'd': [1, 0]}
assignments: {'a': 1, 'c': 0, 'b': 1}
forwardcheck: [[1, 0]]
_unassigned: Unassigned
setting singlevalue to 1 from variable a
returning False because assigned variable c with value 0 != 1

And indeed, as @lvc said, it's imposing a constraint that a should equal c, even though it shouldn't. I think the relevant loop should be something more like

    for key, value in assignments.items():
        if key not in variables: continue

which produces

[{'a': 1, 'c': 1, 'b': 1, 'd': 1}, {'a': 1, 'c': 0, 'b': 1, 'd': 0},
 {'a': 0, 'c': 1, 'b': 0, 'd': 1}, {'a': 0, 'c': 0, 'b': 0, 'd': 0}]

although I don't know how the code really works so it's hard to be sure. This feels like a bug to me. That may seem unlikely, but it doesn't look like AllEqualConstraint is used anywhere in the code base (in none of the examples) except in its own docstring, where the example tests a case where every variable participates in the constraint, and so the bug wouldn't appear.

DSM
  • 342,061
  • 65
  • 592
  • 494
  • I agree that it feels like a bug, given that loop guarded by `if forwarcheck...` *does* pay attention to variables. It should either respect them or ignore them - changing its mind sometimes certainly sounds buggy! – lvc Aug 29 '12 at 14:45
  • Especially when you compare the logic of `AllEqualConstraint` to `AllDifferentConstraint`. `ADC` explicitly does a `for variable in variables:` loop in exactly the place where I think `AEC` should.. – DSM Aug 29 '12 at 14:46