Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding the Max-k-Cut application #299

Closed
wants to merge 29 commits into from
Closed
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3bb2d53
Adding the Max-k-cut application
clausia Sep 23, 2021
57208a7
validate if it is possible to use the matplotlib library
clausia Dec 14, 2021
4db521a
minor fix (for library validations)
clausia Dec 14, 2021
11d6f96
Merge branch 'main' into max-k-cut-app
clausia Dec 16, 2021
5fa9126
Merge branch 'main' into max-k-cut-app
manoelmarques Dec 16, 2021
6cb7536
Merge branch 'main' into max-k-cut-app
manoelmarques Dec 16, 2021
9fb9458
Merge branch 'main' into max-k-cut-app
manoelmarques Dec 16, 2021
d457732
Merge branch 'main' into max-k-cut-app
clausia Dec 23, 2021
6408781
Merge branch 'main' into max-k-cut-app
clausia Jan 6, 2022
afdabab
Merge branch 'main' into max-k-cut-app
fedonman Jan 9, 2022
2ccae04
Get colors parameter out of init, using algorithm_globals
clausia Jan 10, 2022
63ba6b1
update copyright year
fedonman Jan 10, 2022
c42e061
Merge branch 'main' into max-k-cut-app
clausia Jan 14, 2022
82c67bd
Merge branch 'main' into max-k-cut-app
clausia Jan 20, 2022
6e64655
Merge branch 'main' into max-k-cut-app
clausia Jan 23, 2022
6bec1f9
Merge branch 'main' into max-k-cut-app
manoelmarques Jan 26, 2022
4aa90a8
Merge branch 'main' into max-k-cut-app
clausia Feb 7, 2022
c8a8107
Merge branch 'main' into max-k-cut-app
clausia Feb 9, 2022
a74c686
Merge branch 'main' into max-k-cut-app
clausia Feb 11, 2022
323ec1e
Merge branch 'main' into max-k-cut-app
clausia Feb 28, 2022
980df66
Merge branch 'main' into max-k-cut-app
clausia Mar 1, 2022
e696d23
Merge branch 'main' into max-k-cut-app
clausia Mar 14, 2022
647028f
Merge branch 'main' into max-k-cut-app
clausia Mar 28, 2022
c869735
Merge branch 'main' into max-k-cut-app
clausia Apr 3, 2022
5cf0eee
Merge branch 'main' into max-k-cut-app
clausia Apr 4, 2022
2997c45
Merge branch 'main' into max-k-cut-app
clausia Apr 10, 2022
91f390c
Merge branch 'main' into max-k-cut-app
clausia Apr 19, 2022
2d1e302
Merge branch 'main' into max-k-cut-app
clausia Apr 23, 2022
d19b1d1
Merge branch 'main' into max-k-cut-app
clausia May 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Adding the Max-k-cut application
* Add new Maxkcut class in qiskit_optimization/applications/max_k_cut.py

* Add tests for new Maxkcut class in test/applications/test_max_k_cut.py

* Add reference to Maxkcut class in qiskit_optimization/applications/__init__.py file

* Mention Maxkcut application in docs/tutorials/09_application_classes.ipynb

* Add necessary words in .pylintdict

* Add release notes regarding new Maxkcut class in releasenotes/notes/add-max-k-cut-app-7e451a5993171175.yaml

Co-authored-by: fedonman <[email protected]>
clausia and fedonman committed Dec 13, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 3bb2d53aacec5aa63f8b8d4bff64b4242b3bede5
4 changes: 4 additions & 0 deletions .pylintdict
Original file line number Diff line number Diff line change
@@ -106,6 +106,7 @@ makefile
matplotlib
maxcut
maxfun
maxkcut
maxiter
mdl
minimizer
@@ -146,6 +147,7 @@ py
pxd
qaoa
qasm
qce
qiskit
qiskit's
qn
@@ -160,6 +162,7 @@ qubits
qubo
readme
representable
rgba
rhobeg
rhoend
rhs
@@ -190,6 +193,7 @@ subspaces
sys
subproblem
summands
tabi
terra
th
toctree
4 changes: 3 additions & 1 deletion docs/tutorials/09_application_classes.ipynb
Original file line number Diff line number Diff line change
@@ -40,6 +40,8 @@
" - Given a graph, a depot node, and the number of vehicles (routes), find a set of routes such that each node is covered exactly once except the depot and the total distance of the routes is minimized.\n",
"11. Vertex cover problem\n",
" - Given an undirected graph, find a subset of nodes with the minimum size such that each edge has at least one endpoint in the subsets.\n",
"12. Max-k-Cut problem\n",
" - Given an undirected graph, find a partition of nodes into at most k subsets such that the total weight of the edges between the k subsets is maximized.\n",
"\n",
"The application classes for graph problems (`GraphOptimizationApplication`) provide a functionality to draw graphs of an instance and a result.\n",
"Note that you need to install `matplotlib` beforehand to utilize the functionality."
@@ -1518,4 +1520,4 @@
},
"nbformat": 4,
"nbformat_minor": 2
}
}
3 changes: 3 additions & 0 deletions qiskit_optimization/applications/__init__.py
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@
GraphPartition
Knapsack
Maxcut
Maxkcut
NumberPartition
SetPacking
SKModel
@@ -56,6 +57,7 @@
from .graph_partition import GraphPartition
from .knapsack import Knapsack
from .max_cut import Maxcut
from .max_k_cut import Maxkcut
from .number_partition import NumberPartition
from .optimization_application import OptimizationApplication
from .set_packing import SetPacking
@@ -72,6 +74,7 @@
"GraphOptimizationApplication",
"Knapsack",
"Maxcut",
"Maxkcut",
"NumberPartition",
"OptimizationApplication",
"SetPacking",
177 changes: 177 additions & 0 deletions qiskit_optimization/applications/max_k_cut.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.


"""An application class for the Max-k-cut."""

from typing import List, Dict, Tuple, Optional, Union
import networkx as nx
import numpy as np
from matplotlib.pyplot import cm
from matplotlib.colors import to_rgba
from docplex.mp.model import Model

from qiskit_optimization.algorithms import OptimizationResult
from qiskit_optimization.problems.quadratic_program import QuadraticProgram
from qiskit_optimization.translators import from_docplex_mp
from .graph_optimization_application import GraphOptimizationApplication


class Maxkcut(GraphOptimizationApplication):
"""Optimization application for the "max-k-cut" [1] problem based on a NetworkX graph.

References:
[1]: Z. Tabi et al.,
"Quantum Optimization for the Graph Coloring Problem with Space-Efficient Embedding"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paper addresses a graph coloring problem and does not mention max-k-cut problem.
Could you refer an appropriate paper or page?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @t-imamichi, graph colouring problem is a decision variant of max-k-cut. Graph coloring problem is about checking if there "exists colouring of the nodes which make every edge connecting nodes with a different colour." Contrary, Max-K-Cut is about finding the colouring for which the number of such edges is maximized. You could say that graph colouring to max-K-Cut shows similar relation as SAT to MAX-SAT. For this reason, and the fact max cut is already used in the package we decided to choose max-k-cut name

Copy link
Collaborator

@t-imamichi t-imamichi Feb 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as the paper mentions only coloring problem, I don't think it's an appropriate citation of max-k-cut. It would confuse users. Why not rename the class "k-coloring problem" as the paper addresses?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moreover, there are many papers directly addressing max-k-cut problem. Why do you need to refer that paper (Tabi et al.) for max-k-cut class?

Copy link

@adamglos92 adamglos92 Feb 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notion of feasibility changes. For Max-Cut every solution is feasible, for graph colouring only those with all edges connecting different nodes are feasible. This is obviously correct but inconsistent with max-cut used in the qiskit-optimization.
Since this is for optimization purposes mostly we found Max-K-Cut more accurate. But we can also move it to k-graph-coloring
We are not aware of older papers that propose the given formulation. If there is one, we're open to replacing the reference.

Copy link
Collaborator

@t-imamichi t-imamichi Feb 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned

if there "exists colouring of the nodes which make every edge connecting nodes with a different colour."

But, coloring does not take care of edge weights. So, I don't think the optimal solution of coloring does not corresponds to the optimal solution to max-k-cut problem.

You must make a mixed integer programming model of max-k-cut. Here is the definition of k-cut value (though this page addresses minimization)
https://en.wikipedia.org/wiki/Minimum_k-cut

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may perhaps try to use an solution of coloring problem as a heuristic solution of max-k-cut problem?
If so, I ask you to make a proper mixed integer programming model of max-k-cut problem.
The model of application classes should be exact. Otherwise, even if we apply CPLEX or Gurobi to the model, we may not be able to obtain the optimal solution.

2020 IEEE International Conference on Quantum Computing and Engineering (QCE),
2020, pp. 56-62, doi: 10.1109/QCE49297.2020.00018.,
https://ieeexplore.ieee.org/document/9259934
"""

def __init__(
self,
graph: Union[nx.Graph, np.ndarray, List],
k: int,
colors: Optional[Union[List[str], List[List[int]]]] = None,
) -> None:
"""
Args:
graph: A graph representing a problem. It can be specified directly as a
`NetworkX <https://networkx.org/>`_ graph,
or as an array or list format suitable to build out a NetworkX graph.
k: The number of colors
colors: List of strings or list of colors in rgba lists to be assigned to each
resulting subset, there must be as many colors as the number k
"""
super().__init__(graph=graph)
self._subsets_num = k
self._colors = colors if colors and len(colors) >= k else None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For colors I see that more are allowed to be defined and presumably ignored; however if colors were given but less than k then they are discarded. In looking further, from what can see, these are just used if/when the user the draws the result.

I am wondering whether it might be better to move this off the constructor and just say have a method call something like set_draw_colors that takes the same thing and stores it for the rendering. That makes this feel less like part of the problem definition - at least to me. I would have said pass them on draw but then this is implementing logic defined in the base class and it does not seem to support customizing the rendering in this way.

In terms of the colors I think it should define what the string and int formats are - if its simply matplot color format then I think that can be stated perhaps with a link to matplot colot doc that has the info plus perhaps an example in the code to help people.

Also I wonder since this is a set of colors should they be passed in as a set so that it contains unique items - otherwise a user can specify the same color again. In regards of the behavior or more or less colors given than k maybe you prefer the less severe mechanism of discarding all or part of them depending on size. I think at least the code should emit a warning to alert the user - ideally they should be passing in k colors right not some other sized set. If this is a separate method just to set draw colors then maybe allowing only None (to reset to internal default) or a k sized set and raising a ValueError if the size if not equal to k might be more acceptable?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your comments, they have already been addressed:

  • The importance of including the definition of colors in the constructor is that this implementation is based on the "Graph Coloring Problem", but I understand your concern, we can have the solution of the max-k-cut problem without the need to draw the resulting graph. To change (or add) the colors you have the methods set/get (colors, called like that for the same reason, that we are thinking about the "Graph Coloring Problem").
    So, I have removed the colors parameter from the constructor, (before it was optional), I have already done this update.

  • As for the documentation, certainly including an example is much better for people who use this class, that update is ready, please review.

  • Regarding that the user can specify a repeated color, it seems to me that we should allow this type of flexibility, that this decision is up to the user, finally if it was not his/her intention, she/he will be able to see the result when he requests the drawing of the graph (the user will see fewer colors than 'k').

  • Now it is being validated that the size of the color list is equal to the k parameter, and if it is not, the ValueError is thrown indicating that the colors have not been assigned because they are not the correct amount.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@woodsp-ibm, what do you think of the changes?


def to_quadratic_program(self) -> QuadraticProgram:
"""Convert a Max-k-cut problem instance into a
:class:`~qiskit_optimization.problems.QuadraticProgram`

Returns:
The :class:`~qiskit_optimization.problems.QuadraticProgram` created
from the Max-k-cut problem instance.
"""
for w, v in self._graph.edges:
self._graph.edges[w, v].setdefault("weight", 1)

mdl = Model(name="Max-k-cut")
n = self._graph.number_of_nodes()
k = self._subsets_num
x = {(v, i): mdl.binary_var(name=f"x_{v}_{i}") for v in range(n) for i in range(k)}
first_penalty = mdl.sum_squares((1 - mdl.sum(x[v, i] for i in range(k)) for v in range(n)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make a model with a standard formulation of the problem? Please do not penalize constraints by yourself.
In the application classes, we make a model with a standard formulation. The conversion to QUBO will be done within algorithm classes.

second_penalty = mdl.sum(
mdl.sum(self._graph.edges[v, w]["weight"] * x[v, i] * x[w, i] for i in range(k))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this takes sum of edges whose end nodes are same color (i). Is it a correct k-cut value?
I think k-cut value is the sum of edges whose end nodes have different colors.
Anyways, could you refer an appropriate paper or page and make a model (obj func + constraints).

for v, w in self._graph.edges
)
objective = first_penalty + second_penalty
mdl.minimize(objective)

op = from_docplex_mp(mdl)
return op

def interpret(self, result: Union[OptimizationResult, np.ndarray]) -> List[List[int]]:
"""Interpret a result as k lists of node indices

Args:
result : The calculated result of the problem

Returns:
k lists of node indices correspond to k node sets for the Max-k-cut
"""
x = self._result_to_x(result)
n = self._graph.number_of_nodes()
cut = [[] for i in range(self._subsets_num)] # type: List[List[int]]

n_selected = x.reshape((n, self._subsets_num))
for i in range(n):
node_in_subset = np.where(n_selected[i] == 1)[0] # one-hot encoding
if len(node_in_subset) != 0:
cut[node_in_subset[0]].append(i)

return cut

def _draw_result(
self,
result: Union[OptimizationResult, np.ndarray],
pos: Optional[Dict[int, np.ndarray]] = None,
) -> None:
"""Draw the result with colors

Args:
result : The calculated result for the problem
pos: The positions of nodes
"""
x = self._result_to_x(result)
nx.draw(self._graph, node_color=self._node_color(x), pos=pos, with_labels=True)

def _node_color(self, x: np.ndarray) -> List[Tuple[float, float, float, float]]:
# Return a list of colors for draw.

n = self._graph.number_of_nodes()

# k colors chosen from cm.rainbow, or from given color list
colors = (
cm.rainbow(np.linspace(0, 1, self._subsets_num))
if self._colors is None
else self._colors
)
gray = to_rgba("lightgray")
node_colors = [gray for _ in range(n)]

n_selected = x.reshape((n, self._subsets_num))
for i in range(n):
node_in_subset = np.where(n_selected[i] == 1) # one-hot encoding
if len(node_in_subset[0]) != 0:
node_colors[i] = to_rgba(colors[node_in_subset[0][0]])

return node_colors

@property
def k(self) -> int:
"""Getter of k

Returns:
The number of colors
"""
return self._subsets_num

@k.setter
def k(self, k: int) -> None:
"""Setter of k

Args:
k: The number of colors
"""
self._subsets_num = k
self._colors = self._colors if self._colors and len(self._colors) >= k else None

@property
def colors(self) -> Union[List[str], List[List[int]]]:
"""Getter of colors list

Returns:
The k size color list
"""
return self._colors

@colors.setter
def colors(self, colors: Union[List[str], List[List[int]]]) -> None:
"""Setter of colors list

Args:
colors: The k size color list
"""
self._colors = colors if colors and len(colors) >= self._subsets_num else None
14 changes: 14 additions & 0 deletions releasenotes/notes/add-max-k-cut-app-7e451a5993171175.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
features:
- |
Adding the Max-k-Cut application :class:`qiskit_optimization.applications.Maxkcut`.

Problem: Given an undirected graph, find a partition of nodes into at most k subsets such
that the total weight of the edges between the k subsets is maximized.

To solve this problem, the space-efficient quantum optimization representation (or encoding)
for the graph coloring problem proposed in [1] is used.

[1]: Z. Tabi et al., "Quantum Optimization for the Graph Coloring Problem with Space-Efficient
Embedding," 2020 IEEE International Conference on Quantum Computing and Engineering (QCE),
2020, pp. 56-62, doi: 10.1109/QCE49297.2020.00018., https://ieeexplore.ieee.org/document/9259934
162 changes: 162 additions & 0 deletions test/applications/test_max_k_cut.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

""" Test Maxkcut class"""

from test.optimization_test_case import QiskitOptimizationTestCase

import networkx as nx

from qiskit.exceptions import MissingOptionalLibraryError
from qiskit_optimization import QuadraticProgram
from qiskit_optimization.algorithms import OptimizationResult, OptimizationResultStatus
from qiskit_optimization.applications.max_k_cut import Maxkcut
from qiskit_optimization.problems import QuadraticObjective, VarType


class TestMaxkcut(QiskitOptimizationTestCase):
"""Test Maxkcut class"""

def setUp(self):
super().setUp()
self.graph = nx.gnm_random_graph(4, 5, 123)
self.k = 3
op = QuadraticProgram()
for _ in range(12):
op.binary_var()
self.result = OptimizationResult(
x=[0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0],
fval=0,
variables=op.variables,
status=OptimizationResultStatus.SUCCESS,
)

def test_to_quadratic_program(self):
"""Test to_quadratic_program"""
maxkcut = Maxkcut(self.graph, self.k)
op = maxkcut.to_quadratic_program()
# Test name
self.assertEqual(op.name, "Max-k-cut")
# Test variables
self.assertEqual(op.get_num_vars(), 12)
for var in op.variables:
self.assertEqual(var.vartype, VarType.BINARY)
# Test objective
obj = op.objective
self.assertEqual(obj.sense, QuadraticObjective.Sense.MINIMIZE)
self.assertEqual(obj.constant, 4)
self.assertDictEqual(
obj.linear.to_dict(),
{
0: -2.0,
1: -2.0,
2: -2.0,
3: -2.0,
4: -2.0,
5: -2.0,
6: -2.0,
7: -2.0,
8: -2.0,
9: -2.0,
10: -2.0,
11: -2.0,
},
)
self.assertDictEqual(
obj.quadratic.to_dict(),
{
(0, 0): 1.0,
(0, 1): 2.0,
(1, 1): 1.0,
(0, 2): 2.0,
(1, 2): 2.0,
(2, 2): 1.0,
(0, 3): 1.0,
(3, 3): 1.0,
(1, 4): 1.0,
(3, 4): 2.0,
(4, 4): 1.0,
(2, 5): 1.0,
(3, 5): 2.0,
(4, 5): 2.0,
(5, 5): 1.0,
(0, 6): 1.0,
(3, 6): 1.0,
(6, 6): 1.0,
(1, 7): 1.0,
(4, 7): 1.0,
(6, 7): 2.0,
(7, 7): 1.0,
(2, 8): 1.0,
(5, 8): 1.0,
(6, 8): 2.0,
(7, 8): 2.0,
(8, 8): 1.0,
(0, 9): 1.0,
(6, 9): 1.0,
(9, 9): 1.0,
(1, 10): 1.0,
(7, 10): 1.0,
(9, 10): 2.0,
(10, 10): 1.0,
(2, 11): 1.0,
(8, 11): 1.0,
(9, 11): 2.0,
(10, 11): 2.0,
(11, 11): 1.0,
},
)
# Test constraint
lin = op.linear_constraints
self.assertEqual(len(lin), 0)

def test_interpret(self):
"""Test interpret"""
maxkcut = Maxkcut(self.graph, self.k)
self.assertEqual(maxkcut.interpret(self.result), [[1, 3], [0], [2]])

def test_node_color(self):
"""Test _node_color"""
# default colors
maxkcut = Maxkcut(self.graph, self.k)
self.assertEqual(
[[round(num, 2) for num in i] for i in maxkcut._node_color(self.result.x)],
[
[0.5, 1.0, 0.7, 1.0],
[0.5, 0.0, 1.0, 1.0],
[1.0, 0.0, 0.0, 1.0],
[0.5, 0.0, 1.0, 1.0],
],
)
# given colors
maxkcut = Maxkcut(self.graph, self.k, colors=["r", "g", "b"])
self.assertEqual(
[[round(num, 2) for num in i] for i in maxkcut._node_color(self.result.x)],
[
[0.0, 0.5, 0.0, 1.0],
[1.0, 0.0, 0.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
[1.0, 0.0, 0.0, 1.0],
],
)

def test_draw(self):
"""Test whether draw raises an error if matplotlib is not installed"""
maxkcut = Maxkcut(self.graph, self.k)
try:
import matplotlib as _

maxkcut.draw()

except ImportError:
with self.assertRaises(MissingOptionalLibraryError):
maxkcut.draw()