Wednesday, February 18, 2015

Wednesday Night Hack #11 - Experimenting with PyEDA

Not much updates lately.  Work and life are good.  I have probably been working on this for about 2 weeks.

Here is a short write up on an experiment I did using PyEDA for the purpose of developing a "poor-man's" random constraint solver using Python.

At it's core, PyEDA is a library that provides support for the specification of complex boolean functions.

It provides a very clean interface to PicoSAT, a popular SAT solver.

The first realization I made was that PyEDA did not support integer values.  With the help of the author of PyEDA, the following solution was devised.

You can specify integers using function arrays :

class Int:
    def __init__(self, name, width=3, value=None):
        self.name = name
        self.width = width
        if value != None:
            self.farray = int2exprs(value, self.width)  # a real value
        else:
            self.farray = exprvars(name, width)  # empty variables

A function is array is nothing but an array of boolean variables.

The next step is to add operators.

    def __eq__(self, other):
        name = "{}eq{}".format(self.name, other.name)
        f = Int(name, self.width)
        f.farray = farray([Equal(i, j) for i, j in zip(self.farray, other.farray)])
        return f

    def __add__(self, other):
        sum, carry = ripple_carry_add(self.farray, other.farray)
        name = "{}add{}".format(self.name, other.name)
        f = Int(name, self.width)
        f.farray = farray([i & ~(carry[-1] & carry[-2]) for i in sum])  # this is completely incorrect, need to fix overflow issue
        return f

A wrapper to the SAT solve function call and a __str__ method :

    def satisfy_all(self):
       return self.farray.uand().satisfy_all()

    def __str__(self):
        return self.name + " : " + str(self.farray)
Finally, we need a way to convert the result from the SAT solver back to our Int class.


    def from_soln(self, soln):
        value = 0
        for boolvar in sorted(soln.keys(), reverse=True):
            tmp = str(boolvar)
            if tmp.startswith(self.name):
                value = value | (int(soln[boolvar]) * pow(2,int(tmp[-2])))
        return value

I'll conclude this knowledge share post with 3 simple examples :

1. A == B

This works very well.

>>> a = Int('A', 3)
>>> b = Int('B', 3)
>>> f = (a == b)
>>> for soln in f.satisfy_all():
>>>     print("a="+str(a.from_soln(soln))+" b="+str(b.from_soln(soln)))
a=0 b=0
a=4 b=4
a=2 b=2
a=6 b=6
a=1 b=1
a=5 b=5
a=3 b=3
a=7 b=7

2. F == (A + B)

This works well, but there is an overflow bug that I have chosen not to fix.

>>> a = Int('A', 3)
>>> b = Int('B', 3)
>>> f = Int('F', 3)
>>> g = (f == (a + b))
>>> for soln in g.satisfy_all():
>>>     print("a="+str(a.from_soln(soln))+" b="+str(b.from_soln(soln))+" f="+str(f.from_soln(soln)))

a=0 b=0 f=0
a=0 b=4 f=4
a=0 b=2 f=2
a=0 b=6 f=6
a=0 b=1 f=1
[..]
a=2 b=6 f=0 <-- overflow bug

2. F == (A + B), F == 3

The same overflow bug happens here, but I also identified a more serious issue.  Since F is kept constant, the SAT solver does not return it's var.

a = Int('A', 3)
b = Int('B', 3)
f = Int('F', 3, 3)
g = (f == (a + b))
for soln in g.satisfy_all():
    print("a="+str(a.from_soln(soln))+" b="+str(b.from_soln(soln))+" f="+str(f.from_soln(soln)))


a=0 b=3 f=0 <-- f is not returned by SAT solver
a=2 b=1 f=0
a=1 b=2 f=0
a=4 b=7 f=0 <-- overflow bug
[..]

This concludes my experiment with PyEDA.  It was an worthwhile and interesting experiment, but it's time to move on.

As far as "poor-man's" constraint solver goes, this doesn't come close.  I would point interested individuals to the CRAVE project (or, commercial tools!).