Skip to content

Demo: Solving 3-SAT Using Grover's Algorithm

View on GitHub Experiment in the IDE

Grover's algorithm [1,2] is a quantum search algorithm that enables searching a dataset of \(N\) items using \(O(\sqrt{N})\) queries, in contrast to classical algorithms that require \(\Omega(N)\) queries in the worst case. Using the algorithm allows us to speed up various computations that are considered classically intractable. For example, the 3-SAT problem [3] is a famous \(\text{NP-Complete}\) problem, a solution of which allows solving any problem in the complexity class \(\text{NP}\).

In this demo, we show how the Classiq platform enables us to solve an instance of 3-SAT using Grover's algorithm at a higher functional level, avoiding the implementation details you encounter when using other platforms.

The 3-SAT Formula

We now specify the 3-CNF [4] formula that requires a solution:

\[(x_1 \lor x_2 \lor x_3) \land (\neg x_1 \lor x_2 \lor x_3) \land (\neg x_1 \lor \neg x_2 \lor \neg x_3) \land (\neg x_1 \lor \neg x_2 \lor x_3) \land (x_1 \lor x_2 \lor \neg x_3) \land (\neg x_1 \lor x_2 \lor \neg x_3)\]
def _not(y):
    return y ^ 1


def sat_formula(x1, x2, x3):
    return (
        ((x1) | (x2) | (x3))
        & ((_not(x1)) | (x2) | (x3))
        & ((_not(x1)) | (_not(x2)) | (_not(x3)))
        & ((_not(x1)) | (_not(x2)) | (x3))
        & ((x1) | (x2) | (_not(x3)))
        & ((_not(x1)) | (x2) | (_not(x3)))
    )


NUM_VARIABLES = 3
import itertools


def print_truth_table(num_variables, boolean_func):
    variables = [f"x{i}" for i in range(num_variables)]
    combinations = list(itertools.product([0, 1], repeat=len(variables)))

    header = "  ".join([f"{var:<5}" for var in variables]) + " | Result"
    print(header)
    print("-" * len(header))

    for combination in combinations:
        result = boolean_func(*combination)
        values_str = "  ".join([f"{str(val):<5}" for val in combination])
        print(f"{values_str} | {str(result):<5}")

We can see that the formula has two possible solutions:

print_truth_table(NUM_VARIABLES, sat_formula)
x0     x1     x2    | Result
----------------------------
0      0      0     | 0    
0      0      1     | 0    
0      1      0     | 1    
0      1      1     | 1    
1      0      0     | 0    
1      0      1     | 0    
1      1      0     | 0    
1      1      1     | 0

We note that since there are multiple satisfying assignments for the formula, the goal of the Grover quantum algorithm is to find one of them. This is contrast to the usual description of the search problem (found in 2), in which there is a single solution (out of \(N\) possibilities).

Loading the Grover Search Model

We load the Grover search model for finding the solution. To specify the model, we define the oracle it uses. Each oracle call is analogous to a classical query. In this case, we define an oracle based on the 3-CNF formula defined earlier, using the standard phase_oracle that transforms 'digital' oracle; i.e., \(|x\rangle|0\rangle \rightarrow |x\rangle|f(x)\rangle\) to a phase oracle \(|x\rangle \rightarrow (-1)^{f(x)}|x\rangle\).

In addition, we define the number of Grover operator repetitions in the model (which is based on the frequency of solutions in the search space). This time, only one repetition is needed.

from classiq import *


@qfunc
def sat_oracle(x: QArray[QBit], res: QBit):
    res ^= sat_formula(*[x[i] for i in range(NUM_VARIABLES)])


@qfunc
def main(x: Output[QArray[QBit, NUM_VARIABLES]]):
    allocate(x.len, x)
    grover_search(
        reps=1,
        oracle=lambda vars: phase_oracle(sat_oracle, vars),
        packed_vars=x,
    )


qmod = create_model(
    main, constraints=Constraints(max_width=20), out_file="3_sat_grover"
)

Synthesizing the Circuit

We synthesize the circuit using the Classiq synthesis engine. The synthesis takes several seconds:

qprog = synthesize(qmod)

Showing the Resulting Circuit

Once the Classiq synthesis engine finishes the job, we can show the resulting circuit in the interactive GUI:

show(qprog)

Executing the Circuit

Lastly, we run the resulting circuit with the Classiq execute interface using the execute function.

result = execute(qprog).result_value()

Upon printing the result, we see that our execution of Grover's algorithm successfully found the satisfying assignments for the input formula:

result.parsed_counts
[{'x': [0, 1, 0]}: 510, {'x': [0, 1, 1]}: 490]
assert sat_formula(*result.parsed_counts[0].state["x"])

Large 3-SAT Example

We now demonstrate on a larger example:

def sat_formula_large(x1, x2, x3, x4):
    return (
        (x2 | x3 | x4)
        & (_not(x1) | x2 | x3)
        & (_not(x1) | x2 | _not(x3))
        & (_not(x1) | _not(x2) | x3)
        & (x1 | _not(x2) | _not(x3))
        & (x1 | _not(x2) | x3)
        & (_not(x1) | _not(x2) | _not(x4))
        & (_not(x1) | _not(x2) | x4)
        & (_not(x2) | _not(x3) | _not(x4))
        & (x2 | _not(x3) | x4)
        & (x1 | _not(x3) | x4)
        & (x1 | _not(x2) | _not(x4))
        & (_not(x1) | _not(x2) | _not(x3))
    )


NUM_VARIABLES_LARGE = 4
print_truth_table(NUM_VARIABLES_LARGE, sat_formula_large)
x0     x1     x2     x3    | Result
-----------------------------------
0      0      0      0     | 0    
0      0      0      1     | 1    
0      0      1      0     | 0    
0      0      1      1     | 1    
0      1      0      0     | 0    
0      1      0      1     | 0    
0      1      1      0     | 0    
0      1      1      1     | 0    
1      0      0      0     | 0    
1      0      0      1     | 0    
1      0      1      0     | 0    
1      0      1      1     | 0    
1      1      0      0     | 0    
1      1      0      1     | 0    
1      1      1      0     | 0    
1      1      1      1     | 0
@qfunc
def sat_oracle_large(x: QArray[QBit], res: QBit):
    res ^= sat_formula_large(*[x[i] for i in range(NUM_VARIABLES_LARGE)])


@qfunc
def main(x: Output[QArray[QBit, NUM_VARIABLES_LARGE]]):
    allocate(x.len, x)
    grover_search(
        reps=2,
        oracle=lambda vars: phase_oracle(sat_oracle_large, vars),
        packed_vars=x,
    )


qmod_large = create_model(
    main, constraints=Constraints(max_width=24), out_file="3_sat_grover_large"
)
qprog_large = synthesize(qmod_large)
show(qprog_large)
result_large = execute(qprog_large).result_value()
result_large.parsed_counts
[{'x': [0, 0, 0, 1]}: 492,
 {'x': [0, 0, 1, 1]}: 458,
 {'x': [1, 1, 0, 0]}: 6,
 {'x': [0, 1, 1, 0]}: 5,
 {'x': [0, 1, 1, 1]}: 5,
 {'x': [0, 0, 0, 0]}: 5,
 {'x': [1, 0, 0, 1]}: 4,
 {'x': [1, 1, 0, 1]}: 4,
 {'x': [0, 1, 0, 0]}: 4,
 {'x': [0, 1, 0, 1]}: 3,
 {'x': [1, 1, 1, 1]}: 3,
 {'x': [1, 0, 0, 0]}: 3,
 {'x': [1, 0, 1, 0]}: 3,
 {'x': [1, 1, 1, 0]}: 3,
 {'x': [1, 0, 1, 1]}: 2]
assert sat_formula_large(*result_large.parsed_counts[0].state["x"])

References

[1]: Grover, Lov K. "Quantum mechanics helps in searching for a needle in a haystack." Physical Review Letters 79.2 (1997): 325.

[2]: Grover's algorithm (Wikipedia)

[3]: 3-SAT Problem (Wikipedia)

[4]: Conjunctive Normal Form (Wikipedia)