Kelson Martins

Boolean Satisfiability Problem Checker

Introduction

Kelson

Kelson

Software engineer. Geek. Traveller. Wannabe athlete. Lifelong student. Works at IBM and hosts the @HardcodeCast.


LATEST POSTS

Binary Search Algorithms with Examples 07th June, 2022

Configuring Openshift Identity Providers 06th February, 2022

Programming

Boolean Satisfiability Problem Checker

Posted on .

Introduction

Recently I have worked with Boolean Satisfiability Problems and techniques to solve them efficiently. I found the topic extremely interesting, so I produced this article as the first of a series on how to solve SAT problems with a few different meta-heuristic techniques.

All SAT solver algorithms have something in common: they need some sort of procedure to evaluate whether the generated solution satisfies all clauses of an SAT problem or not.

Given this information, this article presents the implementation of a python SAT checker, which has the goal of reading SAT Problem File in the CNF DIMACS file format, and evaluate how many clauses it satisfies given a solution file.

The SAT Problem File

An SAT problem comprises 2 components: variables and clauses, and the CNF DIMACS format was created as a way of defining Boolean expressions written in conjunctive normal form.

An example of a CNF DIMACS format file is:

c cnf comment line
p cnf 6 4
1 -5 3 0
-2 4 5 0
4 -6 0
3 0

Problem File Breakdown

Comment lines
The first line starts with a c, which provides a comment line. Comment lines can provide any information, and we can use 0 or more comment lines.

Problem Line
The second line starts with a p, denoting a problem line. The problem line follows the following pattern: p FORMAT VARIABLES CLAUSES.

  • Format: This is the file format, useful for programs to detect the format the file uses. For our scenario, the format is cnf.
  • Variables: The number of unique variables our SAT problem contains.
  • Clauses: The total number of clauses our problem contains.

Clauses Lines

Subsequent Lines in our file are the Clauses lines.

In the second line, we specified our problem file contains 4 clauses, so we must express these clauses as 4 distinct lines where each number represents a variable.

  • Having a positive number represents the variable having the value of True.
  • Having a negative number represents the variable having the value of False.
  • The value 0, at the end of the line. A value of 0 does not represent a variable.

Based on our problem file (which has 6 vars and 4 clauses), our Boolean expression is:

( (x1) OR NOT(x5) OR (x3) OR (x4) )
AND 
( NOT(x2) OR (x4) OR (x5) )
AND
( (x4) OR NOT(x6) )
AND
(x3)

The SAT Solution File

Having the problem file available is the first step in producing our SAT checker. But this is just the first component, as we also require the solution file.

An example of a solution file for the problem previously detailed is:

c cnf solution file comment
v  -1 2 3 4 -5 -6
v  0

Solution File Breakdown

Comment lines

Similar to the Problem file, solution files can also contain comments, which also start with a c character as shown in the first line of our file.

Variables lines

Note that we then have a line starting with v, which denotes the variables from our problem with their respective values.

Again:

  • Having a positive number represents the variable having a value of True.
  • Having a negative number represents the variable having a value of False.

Variables lines end with a line containing an individual 0 character, as shown in the third line of our solution file.

Show Me the Code

Now that we have the details about SAT Problem and Solution files, we can then develop a python parser to check whether the solution file completely satisfies all the clauses of the problem.

First, here is the function that reads the Problem file to break it down and extract all of its clauses:

def read_clauses(path):
    clauses = []

    try:
        file = open(path,"r")

        clauses_lines = False

        for line in file:
            if line.startswith("p"):
                clauses_lines = True
                continue

            if line.startswith("%"):
                clauses_lines = False

            if clauses_lines:                    
                # removing trailing spaces, removing last 0 items and converting to number
                clause = [int(s) for s in re.split('[\s]+',line.strip())][:-1] 

                clauses.append(clause)            

        file.close()       

    except IOError:
        print("Can not find input file or read data")
        exit()

    return clauses

This function is key since it reads the problem file and parses it so all the clauses get extracted and added into a list. The function then returns a list where each element is a clause.

The function returns the following when applied to our problem file:

[[1, -5, 3], [-2, 4, 5], [4, -6], [3]]

Similarly, the function to read and parse the solution file is shown below:

def read_solution(path)
    solution = []

    try:
        file = open(path,"r")

        for line in file:
            if line.startswith("v"):
                
                split_solution = line[3:].split(" ")

                for i in range(len(split_solution)):
                    solution.append(split_solution[i])

        # converting list to int and removing last item "0"
        solution = list(map(int, solution))[:-1]

        file.close()       

    except IOError:
        print("Can not find input file or read data")
        exit()

    return solution

The read_solution function has similar functionality to the read_clauses function previously shown, the difference being that the read_solution parses the solution file and returns a list with all variables with their corresponding value.

The function returns the following when applied to our solution file:

[-1, 2, 3, 4, -5, -6]

Having read and parsed our Problem and Solution files, we have now all the tools required to write a checker that will evaluate whether all problem clauses from the problem are satisfied.
The following function does the job:

def check_satisfation(clauses, solution):

    total_clauses = len(clauses)
    total_satistied = 0
    total_non_satisfied = 0

    # iterating over all clauses
    for i in range(len(clauses)):
        
        satistied = False

        for j in range(len(clauses[i])):
                
            if clauses[i][j] in solution:
                satistied = True
        
        if satistied:
            total_satistied += 1
        else:
            total_non_satisfied += 1    
    
    print("Total Clauses: " + str(total_clauses))
    print("Satisfied Clauses: " + str(total_satistied))
    print("Non Satisfied Clauses: " + str(total_non_satisfied))

First, note that our function expects 2 parameters: [clauses, solution].

The clauses parameter is a list of clauses and their variables, which is what the read_solution() function returns.

The solution parameter is a list of variables that represents an attempt to solve the problem. This is what the read_solution() function returns.

Our function iterates over all clauses, evaluating whether any variable in the clause evaluates to True, that serves as proof that it satisfies the clause.

Once all clauses evaluate, the program displays the summary on the console. Quite simple, is it not?

Putting it all together

Having the 3 functions that are the core of our project ready, we can just put it all together with something like:

def main():
    
    clauses = read_clauses(PROBLEM_FILEPATH)
    solution = read_solution(SOLUTION_FILEPATH)

    check_satisfation(clauses,solution)


if __name__ == "__main__":
    main()

Note that the entry point of our script expects 2 files, one for the problem and one for the solution files. We then pass the file to its respective function, before calling the main check_satisfaction() function, which provides our results.

Testing the Code

Let’s now perform some tests assuming the following Problem and Solution files:

problem_1.cnf

c cnf comment line
p cnf 6 4
1 -5 3 0
-2 4 5 0
4 -6 0
3 0

solution_1.cnf

c cnf solution file comment
v  -1 2 3 4 -5 -6
v  0

Expect the following when calling our SAT checker with the given files as parameters:

$ python sat_checker.py problem_1.cnf solution_1.cnf

Total Clauses: 4
Satisfied Clauses: 4
Non Satistied Clauses: 0

If we analyze the problem and the solution presented, indeed we can confirm that the solution satisfies all clauses.

Shall we perform another test?

problem_2.cnf

c cnf comment line
p cnf 6 5
1 -2 6 0
-1 4 -3 0
4 -2 0
3 -5 2 0
-6 3 4 0

solution_2.cnf

c cnf solution file comment
v  -1 2 3 4 -5 -6
v  0

By testing the new problem files, expect the following:

$ python sat_checker.py problem_2.cnf solution_2.cnf

Total Clauses: 5
Satisfied Clauses: 4
Non Satistied Clauses: 1

Analyzing the output, we can identify that there is one clause not being satisfied.

By performing further analysis, we can identify that the first clause [1, -2, 6] is the clause that can not be satisfied given the presented solution.

Conclusion

Boolean Satisfiability Problem (SAT) is a popular topic in Computer Science. SAT problem is also the first problem that was proven to be NP-Complete.
Before going through techniques of how to develop solutions for SAT Problems, this article presented the implementation of an SAT checker that determines whether a Solution satisfies all clauses of a Problem.

This article used Problem files in the CNF DIMACS format. A few different tools can generate these files, with cnfgen being a popular one.

You can find the full code from this first SAT article at my GitHub repository here. Future articles on this topic will approach techniques for the creation of solutions that satisfy all clauses, not just checking them. Stay Tuned!

Kelson

Kelson

//iamkel.dev

Software engineer. Geek. Traveller. Wannabe athlete. Lifelong student. Works at IBM and hosts the @HardcodeCast.

Navigation