Source code for qlauncher.problems.optimization.graph_coloring

  1"""This module contains the Graph Coloring class."""
  2
  3import pickle
  4from itertools import product
  5from random import randint
  6from typing import Literal
  7
  8import matplotlib.pyplot as plt
  9import networkx as nx
 10import numpy as np
 11from pyqubo import Array, Binary
 12
 13from qlauncher import models
 14from qlauncher.base import Problem
 15from qlauncher.hampy import Equation
 16
 17
[docs] 18class GraphColoring(Problem): 19 """ 20 Class for Graph Coloring Problem which is a combinatorial problem involving assigning labels to vertices of the graph such that no two adjacent vertices share the same label. 21 22 Attributes: 23 instance (nx.Graph): The graph for which the coloring problem is to be solved. 24 25 26 """ 27 28 def __init__( 29 self, 30 instance: nx.Graph, 31 num_colors: int, 32 instance_name: str = 'unnamed', 33 ) -> None: 34 super().__init__(instance=instance, instance_name=instance_name) 35 self.pos = None 36 self.num_colors = num_colors 37 38 @property 39 def setup(self) -> dict: 40 return {'instance_name': self.instance_name} 41
[docs] 42 @staticmethod 43 def from_preset(instance_name: Literal['default', 'small'], **kwargs) -> 'GraphColoring': 44 match instance_name: 45 case 'default': 46 graph = nx.petersen_graph() 47 gc = GraphColoring(graph, instance_name=instance_name, num_colors=3) 48 gc.pos = nx.shell_layout(gc.instance, nlist=[list(range(5, 10)), list(range(5))]) 49 return gc 50 case 'small': 51 graph = nx.Graph() 52 graph.add_edges_from([(0, 1), (1, 2), (0, 2), (3, 2)]) 53 return GraphColoring(graph, instance_name=instance_name, num_colors=3) 54 case _: 55 raise ValueError(f'Preset f{instance_name} not defined')
56
[docs] 57 @classmethod 58 def from_file(cls: type['GraphColoring'], path: str) -> 'Problem': 59 with open(path, 'rb') as f: 60 graph, num_colors = pickle.load(f) 61 return cls(graph, instance_name=path, num_colors=num_colors)
62 63 # def to_file(self, path: str) -> None: 64 # with open(path, 'wb') as f: 65 # pickle.dump((self.instance, self.num_colors), f, pickle.HIGHEST_PROTOCOL) 66 67 def _get_path(self) -> str: 68 return f'{self.name}@{self.instance_name}' 69
[docs] 70 def visualize(self, solution: list[int] | None = None) -> None: 71 if self.pos is None: 72 self.pos = nx.spring_layout(self.instance, seed=42) # set seed for same node graphs in plt 73 plt.figure(figsize=(8, 6)) 74 if solution is not None: 75 nx.draw_networkx_nodes(self.instance, self.pos, node_size=500, node_color=solution, cmap='Accent') 76 colors = [] 77 for n1, n2 in self.instance.edges: 78 if solution[n1] == solution[n2]: 79 colors.append('r') 80 else: 81 colors.append('gray') 82 nx.draw_networkx_edges(self.instance, self.pos, edge_color=colors, width=4) 83 else: 84 nx.draw_networkx_nodes(self.instance, self.pos, node_size=500, node_color='skyblue') 85 nx.draw_networkx_edges(self.instance, self.pos, edge_color='gray') 86 nx.draw_networkx_labels(self.instance, self.pos, font_size=10, font_weight='bold') 87 plt.title(f'Graph {self.num_colors}-Coloring Problem Instance Visualization') 88 plt.show()
89
[docs] 90 @staticmethod 91 def generate_graph_coloring_instance(num_vertices: int, edge_probability: int, num_colors: int) -> 'GraphColoring': 92 graph = nx.gnp_random_graph(num_vertices, edge_probability) 93 return GraphColoring(graph, num_colors=num_colors)
94
[docs] 95 @staticmethod 96 def randomly_choose_a_graph(num_colors: int) -> 'GraphColoring': 97 graphs = nx.graph_atlas_g() 98 graph = graphs[randint(0, len(graphs) - 1)] 99 return GraphColoring(graph, num_colors=num_colors)
100 101 # Penalty for assigning the same colors to neighboring vertices 102 def _color_duplication_hamiltonian(self, num_qubits: int, color_bit_length: int) -> Equation: 103 eq = Equation(num_qubits) 104 for node1, node2 in self.instance.edges: 105 for ind, comb in enumerate(product(range(2), repeat=color_bit_length)): 106 if ind >= self.num_colors: 107 break 108 eq_inner = Equation(num_qubits) 109 for i in range(color_bit_length): 110 qubit1 = eq[node1 * color_bit_length + i] 111 qubit2 = eq[node2 * color_bit_length + i] 112 exp = qubit1 & qubit2 if comb[i] else ~qubit1 & ~qubit2 113 eq_inner &= exp 114 eq += eq_inner 115 return eq 116 117 # Penalty for using excessive colors 118 def _excessive_colors_use_hamiltonian(self, num_qubits: int, color_bit_length: int) -> Equation: 119 eq = Equation(num_qubits) 120 for node in self.instance.nodes: 121 for ind, comb in enumerate(product(range(2), repeat=color_bit_length)): 122 if ind < self.num_colors: 123 continue 124 eq_inner = Equation(num_qubits) 125 for i in range(color_bit_length): 126 qubit = eq[node * color_bit_length + i] 127 exp = qubit if comb[i] else ~qubit 128 eq_inner &= exp 129 eq += eq_inner 130 return eq 131
[docs] 132 def to_hamiltonian(self, constraints_weight: float = 1, costs_weight: float = 1) -> models.Hamiltonian: 133 color_bit_length = int(np.ceil(np.log2(self.num_colors))) 134 num_qubits = self.instance.number_of_nodes() * color_bit_length 135 136 eq = self._color_duplication_hamiltonian(num_qubits, color_bit_length) 137 eq2 = self._excessive_colors_use_hamiltonian(num_qubits, color_bit_length) 138 139 return models.Hamiltonian(eq * costs_weight + eq2 * constraints_weight)
140
[docs] 141 def to_bqm(self) -> models.BQM: 142 """Returns BQM""" 143 x = Array.create('x', shape=(self.instance.number_of_nodes(), self.num_colors), vartype='BINARY') 144 qubo: Binary = 0 145 for n1, n2 in self.instance.edges: 146 for c in range(self.num_colors): 147 qubo += x[n1, c] * x[n2, c] 148 for node in self.instance.nodes: 149 expression: Binary = 1 - sum(x[node, i] for i in range(self.num_colors)) 150 qubo += expression * expression 151 return models.BQM(qubo.compile())
152
[docs] 153 def to_qubo(self) -> models.QUBO: 154 return self.to_bqm().to_qubo()