Skip to content

Graph Cut Search Problem with Grover Oracle

View on GitHub Experiment in the IDE

The "Maximum Cut Problem" (MaxCut) [1] is an example of a combinatorial optimization problem. It refers to finding a partition of a graph into two sets, such that the number of edges between the two sets is the maximum.

Mathematical Formulation

Given a graph \(G=(V,E)\) with \(|V|=n\) nodes and \(E\) edges, a cut is defined as a partition of the graph into two complementary subsets of nodes. In the MaxCut problem we look for a cut where the number of edges between the two subsets is the maximum. We can represent a cut of the graph by a binary vector \(x\) of size \(n\), assigning 0 and 1 to nodes in the first and second subsets, respectively. The number of connecting edges for a given cut is simply given by summing over \(x_i (1-x_j)+x_j (1-x_i)\) for every pair of connected nodes \((i,j)\).

Solving with the Classiq Platform

In this tutorial we define a search problem instead: Given a graph and number of edges, we check if there is a cut in the graph larger than a certain size. We solve the problem using the Grover algorithm.

Defining a Specific Problem Input

We initiate a specific graph whose maximum cut is 5:

import networkx as nx

# Create graph
G = nx.Graph()
G.add_nodes_from([0, 1, 2, 3, 4])
G.add_edges_from([(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4)])
pos = nx.planar_layout(G)
nx.draw_networkx(G, pos=pos, with_labels=True, alpha=0.8, node_size=500)

png

Creating a Quantum Circuit and Solving It Using the Classiq Platform

We use the grover_search function for the quantum circuit. The oracle function is a phase_oracle, which applies a \((-1)\) phase to each state for which the inner function delivered to phase_oracle returns 1. The cut_oracle is the inner function in this case, calculating whether a given graph partition`s cut is larger than some constant.

Here we look for a cut of size 4 to the graph:

from classiq import *

CUT_SIZE = 4


# cut formulas
def is_cross_cut_edge(x1: int, x2: int) -> int:
    return x1 * (1 - x2) + x2 * (1 - x1)


def cut(x):
    return sum(is_cross_cut_edge(x[node1], x[node2]) for (node1, node2) in G.edges)


# quantum functions
@qfunc
def cut_oracle(cut_size: CInt, nodes: Const[QArray], res: Permutable[QBit]):
    res ^= cut(nodes) >= cut_size


@qfunc
def main(nodes: Output[QArray[QBit, len(G.nodes)]]):
    allocate(nodes)
    grover_search(
        reps=3,
        oracle=lambda vars: phase_oracle(
            lambda vars, res: cut_oracle(CUT_SIZE, vars, res), vars
        ),
        packed_vars=nodes,
    )

Synthesizing the Circuit

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

qmod = create_model(main, constraints=Constraints(max_width=22))
write_qmod(qmod, "grover_max_cut")

qprog = synthesize(qmod)

Showing the Resulting Circuit

After the Classiq synthesis engine finishes the job, we display the resulting circuit in the interactive GUI:

show(qprog)
Quantum program link: https://platform.classiq.io/circuit/2yd4FlAFkOqVH9TVKcHMVWPpDb1
print(qprog.transpiled_circuit.depth)
2666

Executing on a Simulator to Find a Valid Solution

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

optimization_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:

optimization_result.parsed_counts
[{'nodes': [1, 0, 1, 1, 0]}: 170,
 {'nodes': [1, 0, 0, 0, 1]}: 162,
 {'nodes': [1, 0, 0, 1, 1]}: 161,
 {'nodes': [0, 0, 1, 1, 0]}: 156,
 {'nodes': [1, 0, 0, 1, 0]}: 152,
 {'nodes': [0, 1, 1, 0, 0]}: 151,
 {'nodes': [0, 1, 1, 1, 0]}: 149,
 {'nodes': [1, 1, 0, 0, 1]}: 146,
 {'nodes': [0, 1, 1, 0, 1]}: 136,
 {'nodes': [0, 1, 0, 0, 1]}: 133,
 {'nodes': [1, 0, 1, 0, 0]}: 33,
 {'nodes': [0, 0, 0, 0, 1]}: 31,
 {'nodes': [0, 0, 0, 0, 0]}: 28,
 {'nodes': [1, 1, 0, 1, 0]}: 28,
 {'nodes': [0, 1, 0, 0, 0]}: 27,
 {'nodes': [1, 1, 1, 0, 1]}: 27,
 {'nodes': [0, 1, 1, 1, 1]}: 26,
 {'nodes': [0, 0, 1, 0, 0]}: 25,
 {'nodes': [0, 1, 0, 1, 0]}: 25,
 {'nodes': [1, 1, 1, 1, 0]}: 25,
 {'nodes': [0, 1, 0, 1, 1]}: 24,
 {'nodes': [1, 0, 1, 1, 1]}: 24,
 {'nodes': [0, 0, 1, 1, 1]}: 24,
 {'nodes': [0, 0, 0, 1, 0]}: 23,
 {'nodes': [1, 1, 0, 1, 1]}: 23,
 {'nodes': [0, 0, 0, 1, 1]}: 22,
 {'nodes': [1, 1, 0, 0, 0]}: 21,
 {'nodes': [1, 1, 1, 0, 0]}: 20,
 {'nodes': [1, 0, 0, 0, 0]}: 20,
 {'nodes': [1, 1, 1, 1, 1]}: 19,
 {'nodes': [0, 0, 1, 0, 1]}: 19,
 {'nodes': [1, 0, 1, 0, 1]}: 18]

The satisfying assignments are ~6 times more probable than the unsatisfying assignments. We print one:

most_probable_result = optimization_result.parsed_counts[0]
import matplotlib.pyplot as plt

result_parsed = most_probable_result.state["nodes"]
edge_widths = [
    is_cross_cut_edge(
        int(result_parsed[i]),
        int(result_parsed[j]),
    )
    + 0.5
    for i, j in G.edges
]
node_colors = [int(c) for c in result_parsed]
nx.draw_networkx(
    G,
    pos=pos,
    with_labels=True,
    alpha=0.8,
    node_size=500,
    node_color=node_colors,
    width=edge_widths,
    cmap=plt.cm.rainbow,
)

png

References

[1]: Maximum Cut Problem (Wikipedia)