diff --git a/.pylintdict b/.pylintdict index fdbf66fef..03384ce34 100644 --- a/.pylintdict +++ b/.pylintdict @@ -24,6 +24,7 @@ catol cartan chu chvátal +cmap cobyla coeff coeffs @@ -108,6 +109,7 @@ makefile matplotlib maxcut maxfun +maxkcut maxiter mdl minimizer @@ -150,6 +152,7 @@ py pxd qaoa qasm +qce qiskit qiskit's qn @@ -164,6 +167,8 @@ qubits qubo readme representable +rgb +rgba rhobeg rhoend rhs @@ -195,6 +200,7 @@ subspaces sys subproblem summands +tabi tavernelli terra th @@ -215,6 +221,8 @@ undirected upperbound variational vartype +vmax +vmin vqe writelines xixj diff --git a/docs/tutorials/09_application_classes.ipynb b/docs/tutorials/09_application_classes.ipynb index fe1f3111a..932b9d5db 100644 --- a/docs/tutorials/09_application_classes.ipynb +++ b/docs/tutorials/09_application_classes.ipynb @@ -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 -} +} \ No newline at end of file diff --git a/qiskit_optimization/applications/__init__.py b/qiskit_optimization/applications/__init__.py index 27fda9223..f4e942635 100644 --- a/qiskit_optimization/applications/__init__.py +++ b/qiskit_optimization/applications/__init__.py @@ -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", diff --git a/qiskit_optimization/applications/max_k_cut.py b/qiskit_optimization/applications/max_k_cut.py new file mode 100644 index 000000000..3f51c7698 --- /dev/null +++ b/qiskit_optimization/applications/max_k_cut.py @@ -0,0 +1,247 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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 docplex.mp.model import Model + +from qiskit.utils import algorithm_globals +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 + +try: + from matplotlib.pyplot import cm + from matplotlib.colors import to_rgba + + _HAS_MATPLOTLIB = True +except ImportError: + + _HAS_MATPLOTLIB = False + + +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" + 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, + ) -> None: + """ + Args: + graph: A graph representing a problem. It can be specified directly as a + `NetworkX `_ graph, + or as an array or list format suitable to build out a NetworkX graph. + k: The number of colors + """ + super().__init__(graph=graph) + self._subsets_num = k + self._colors: Union[List[Tuple[float, float, float, float]], List[str]] = None + self._seed: int = None + + 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))) + second_penalty = mdl.sum( + mdl.sum(self._graph.edges[v, w]["weight"] * x[v, i] * x[w, i] for i in range(k)) + 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 + ) -> Union[List[Tuple[float, float, float, float]], List[str]]: + # Return a list of colors for draw. + + n = self._graph.number_of_nodes() + + # k colors chosen (randomly or from cm.rainbow), or from given color list + if self._colors is None: + if _HAS_MATPLOTLIB: + colors = cm.rainbow(np.linspace(0, 1, self._subsets_num)) + else: + if self._seed: + algorithm_globals.random_seed = self._seed + colors = [ + "#" + + "".join( + [algorithm_globals.random.choice("0123456789ABCDEF") for i in range(6)] + ) + for j in range(self._subsets_num) + ] + else: + colors = self._colors + + gray = to_rgba("lightgray") if _HAS_MATPLOTLIB else "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]]) + if _HAS_MATPLOTLIB + else 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 + + Raises: + ValueError: if the size of the colors is different than the k parameter. + """ + self._subsets_num = k + if self._colors and len(self._colors) != self._subsets_num: + self._colors = None + raise ValueError( + f"Number of colors in the list is different than the parameter" + f" k = {self._subsets_num} specified for this problem," + f" the colors have not been assigned" + ) + + @property + def colors(self) -> Union[List[Tuple[float, float, float, float]], List[str]]: + """Getter of colors list + + Returns: + The k size color list + """ + return self._colors + + @colors.setter + def colors(self, colors: Union[List[Tuple[float, float, float, float]], List[str]]) -> None: + """Setter of colors list + Colors list must be the same length as the k parameter. Color can be a string or rgb or + rgba tuple of floats from 0-1. If numeric values are specified, they will be mapped to + colors using the cmap and vmin, vmax parameters. See matplotlib colors docs for more + details (https://matplotlib.org/stable/gallery/color/named_colors.html). + + Examples: + [[0.0, 0.5, 0.0, 1.0], [1.0, 0.0, 0.0, 1.0], ...] + ["g", "r", "b", ...] + ["cyan", "purple", ...] + + Args: + colors: The k size color list + + Raises: + ValueError: if the size of the colors is different than the k parameter. + """ + if colors and len(colors) == self._subsets_num: + self._colors = colors + else: + self._colors = None + raise ValueError( + f"Number of colors in the list is different than the parameter" + f" k = {self._subsets_num} specified for this problem," + f" the colors have not been assigned" + ) + + @property + def seed(self) -> int: + """Getter of seed + + Returns: + The seed value for random generation of colors + """ + return self._seed + + @seed.setter + def seed(self, seed: int) -> None: + """Setter of seed + + Args: + seed: The seed value for random generation of colors + """ + self._seed = seed diff --git a/releasenotes/notes/add-max-k-cut-app-7e451a5993171175.yaml b/releasenotes/notes/add-max-k-cut-app-7e451a5993171175.yaml new file mode 100644 index 000000000..2ceaa2c30 --- /dev/null +++ b/releasenotes/notes/add-max-k-cut-app-7e451a5993171175.yaml @@ -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 diff --git a/test/applications/test_max_k_cut.py b/test/applications/test_max_k_cut.py new file mode 100644 index 000000000..acea6d681 --- /dev/null +++ b/test/applications/test_max_k_cut.py @@ -0,0 +1,188 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# 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 + +try: + import matplotlib as _ + + _HAS_MATPLOTLIB = True +except ImportError: + _HAS_MATPLOTLIB = False + + +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 + if _HAS_MATPLOTLIB: + 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) + maxkcut.colors = ["r", "g", "b"] + if _HAS_MATPLOTLIB: + 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], + ], + ) + else: + self.assertEqual( + [list(i) for i in maxkcut._node_color(self.result.x)], + [["g"], ["r"], ["b"], ["r"]], + ) + + 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() + + def test_set_colors(self): + """Test set colors list""" + maxkcut = Maxkcut(self.graph, self.k) + # set different quantity of colors + with self.assertRaises(ValueError): + maxkcut.colors = ["g", "r"] + # change k value + maxkcut.colors = ["g", "r", "b"] + with self.assertRaises(ValueError): + maxkcut.k = 8