├── .gitignore ├── 01_generate_instances.py ├── 02_generate_dataset.py ├── 03_train_competitor.py ├── 03_train_gcnn.py ├── 04_test.py ├── 05_evaluate.py ├── INSTALL.md ├── LICENSE ├── README.md ├── __init__.py ├── models ├── baseline │ └── model.py ├── mean_convolution │ └── model.py └── no_prenorm │ └── model.py ├── scip_patch └── vanillafullstrong.patch ├── setup.py ├── utilities.py └── utilities_tf.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by this project 2 | data 3 | trained_models 4 | results 5 | 6 | # Created by https://www.toptal.com/developers/gitignore/api/python 7 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 8 | 9 | ### Python ### 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | pip-wheel-metadata/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | *.py,cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | pytestdebug.log 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | db.sqlite3 72 | db.sqlite3-journal 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | doc/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | pythonenv* 124 | 125 | # Spyder project settings 126 | .spyderproject 127 | .spyproject 128 | 129 | # Rope project settings 130 | .ropeproject 131 | 132 | # mkdocs documentation 133 | /site 134 | 135 | # mypy 136 | .mypy_cache/ 137 | .dmypy.json 138 | dmypy.json 139 | 140 | # Pyre type checker 141 | .pyre/ 142 | 143 | # pytype static type analyzer 144 | .pytype/ 145 | 146 | # profiling data 147 | .prof 148 | 149 | # End of https://www.toptal.com/developers/gitignore/api/python 150 | -------------------------------------------------------------------------------- /01_generate_instances.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | import numpy as np 4 | import scipy.sparse 5 | import utilities 6 | from itertools import combinations 7 | 8 | 9 | class Graph: 10 | """ 11 | Container for a graph. 12 | 13 | Parameters 14 | ---------- 15 | number_of_nodes : int 16 | The number of nodes in the graph. 17 | edges : set of tuples (int, int) 18 | The edges of the graph, where the integers refer to the nodes. 19 | degrees : numpy array of integers 20 | The degrees of the nodes in the graph. 21 | neighbors : dictionary of type {int: set of ints} 22 | The neighbors of each node in the graph. 23 | """ 24 | def __init__(self, number_of_nodes, edges, degrees, neighbors): 25 | self.number_of_nodes = number_of_nodes 26 | self.edges = edges 27 | self.degrees = degrees 28 | self.neighbors = neighbors 29 | 30 | def __len__(self): 31 | """ 32 | The number of nodes in the graph. 33 | """ 34 | return self.number_of_nodes 35 | 36 | def greedy_clique_partition(self): 37 | """ 38 | Partition the graph into cliques using a greedy algorithm. 39 | 40 | Returns 41 | ------- 42 | list of sets 43 | The resulting clique partition. 44 | """ 45 | cliques = [] 46 | leftover_nodes = (-self.degrees).argsort().tolist() 47 | 48 | while leftover_nodes: 49 | clique_center, leftover_nodes = leftover_nodes[0], leftover_nodes[1:] 50 | clique = {clique_center} 51 | neighbors = self.neighbors[clique_center].intersection(leftover_nodes) 52 | densest_neighbors = sorted(neighbors, key=lambda x: -self.degrees[x]) 53 | for neighbor in densest_neighbors: 54 | # Can you add it to the clique, and maintain cliqueness? 55 | if all([neighbor in self.neighbors[clique_node] for clique_node in clique]): 56 | clique.add(neighbor) 57 | cliques.append(clique) 58 | leftover_nodes = [node for node in leftover_nodes if node not in clique] 59 | 60 | return cliques 61 | 62 | @staticmethod 63 | def erdos_renyi(number_of_nodes, edge_probability, random): 64 | """ 65 | Generate an Erdös-Rényi random graph with a given edge probability. 66 | 67 | Parameters 68 | ---------- 69 | number_of_nodes : int 70 | The number of nodes in the graph. 71 | edge_probability : float in [0,1] 72 | The probability of generating each edge. 73 | random : numpy.random.RandomState 74 | A random number generator. 75 | 76 | Returns 77 | ------- 78 | Graph 79 | The generated graph. 80 | """ 81 | edges = set() 82 | degrees = np.zeros(number_of_nodes, dtype=int) 83 | neighbors = {node: set() for node in range(number_of_nodes)} 84 | for edge in combinations(np.arange(number_of_nodes), 2): 85 | if random.uniform() < edge_probability: 86 | edges.add(edge) 87 | degrees[edge[0]] += 1 88 | degrees[edge[1]] += 1 89 | neighbors[edge[0]].add(edge[1]) 90 | neighbors[edge[1]].add(edge[0]) 91 | graph = Graph(number_of_nodes, edges, degrees, neighbors) 92 | return graph 93 | 94 | @staticmethod 95 | def barabasi_albert(number_of_nodes, affinity, random): 96 | """ 97 | Generate a Barabási-Albert random graph with a given edge probability. 98 | 99 | Parameters 100 | ---------- 101 | number_of_nodes : int 102 | The number of nodes in the graph. 103 | affinity : integer >= 1 104 | The number of nodes each new node will be attached to, in the sampling scheme. 105 | random : numpy.random.RandomState 106 | A random number generator. 107 | 108 | Returns 109 | ------- 110 | Graph 111 | The generated graph. 112 | """ 113 | assert affinity >= 1 and affinity < number_of_nodes 114 | 115 | edges = set() 116 | degrees = np.zeros(number_of_nodes, dtype=int) 117 | neighbors = {node: set() for node in range(number_of_nodes)} 118 | for new_node in range(affinity, number_of_nodes): 119 | # first node is connected to all previous ones (star-shape) 120 | if new_node == affinity: 121 | neighborhood = np.arange(new_node) 122 | # remaining nodes are picked stochastically 123 | else: 124 | neighbor_prob = degrees[:new_node] / (2*len(edges)) 125 | neighborhood = random.choice(new_node, affinity, replace=False, p=neighbor_prob) 126 | for node in neighborhood: 127 | edges.add((node, new_node)) 128 | degrees[node] += 1 129 | degrees[new_node] += 1 130 | neighbors[node].add(new_node) 131 | neighbors[new_node].add(node) 132 | 133 | graph = Graph(number_of_nodes, edges, degrees, neighbors) 134 | return graph 135 | 136 | 137 | def generate_indset(graph, filename): 138 | """ 139 | Generate a Maximum Independent Set (also known as Maximum Stable Set) instance 140 | in CPLEX LP format from a previously generated graph. 141 | 142 | Parameters 143 | ---------- 144 | graph : Graph 145 | The graph from which to build the independent set problem. 146 | filename : str 147 | Path to the file to save. 148 | """ 149 | cliques = graph.greedy_clique_partition() 150 | inequalities = set(graph.edges) 151 | for clique in cliques: 152 | clique = tuple(sorted(clique)) 153 | for edge in combinations(clique, 2): 154 | inequalities.remove(edge) 155 | if len(clique) > 1: 156 | inequalities.add(clique) 157 | 158 | # Put trivial inequalities for nodes that didn't appear 159 | # in the constraints, otherwise SCIP will complain 160 | used_nodes = set() 161 | for group in inequalities: 162 | used_nodes.update(group) 163 | for node in range(10): 164 | if node not in used_nodes: 165 | inequalities.add((node,)) 166 | 167 | with open(filename, 'w') as lp_file: 168 | lp_file.write("maximize\nOBJ:" + "".join([f" + 1 x{node+1}" for node in range(len(graph))]) + "\n") 169 | lp_file.write("\nsubject to\n") 170 | for count, group in enumerate(inequalities): 171 | lp_file.write(f"C{count+1}:" + "".join([f" + x{node+1}" for node in sorted(group)]) + " <= 1\n") 172 | lp_file.write("\nbinary\n" + " ".join([f"x{node+1}" for node in range(len(graph))]) + "\n") 173 | 174 | 175 | def generate_setcover(nrows, ncols, density, filename, rng, max_coef=100): 176 | """ 177 | Generates a setcover instance with specified characteristics, and writes 178 | it to a file in the LP format. 179 | 180 | Approach described in: 181 | E.Balas and A.Ho, Set covering algorithms using cutting planes, heuristics, 182 | and subgradient optimization: A computational study, Mathematical 183 | Programming, 12 (1980), 37-60. 184 | 185 | Parameters 186 | ---------- 187 | nrows : int 188 | Desired number of rows 189 | ncols : int 190 | Desired number of columns 191 | density: float between 0 (excluded) and 1 (included) 192 | Desired density of the constraint matrix 193 | filename: str 194 | File to which the LP will be written 195 | rng: numpy.random.RandomState 196 | Random number generator 197 | max_coef: int 198 | Maximum objective coefficient (>=1) 199 | """ 200 | nnzrs = int(nrows * ncols * density) 201 | 202 | assert nnzrs >= nrows # at least 1 col per row 203 | assert nnzrs >= 2 * ncols # at leats 2 rows per col 204 | 205 | # compute number of rows per column 206 | indices = rng.choice(ncols, size=nnzrs) # random column indexes 207 | indices[:2 * ncols] = np.repeat(np.arange(ncols), 2) # force at leats 2 rows per col 208 | _, col_nrows = np.unique(indices, return_counts=True) 209 | 210 | # for each column, sample random rows 211 | indices[:nrows] = rng.permutation(nrows) # force at least 1 column per row 212 | i = 0 213 | indptr = [0] 214 | for n in col_nrows: 215 | 216 | # empty column, fill with random rows 217 | if i >= nrows: 218 | indices[i:i+n] = rng.choice(nrows, size=n, replace=False) 219 | 220 | # partially filled column, complete with random rows among remaining ones 221 | elif i + n > nrows: 222 | remaining_rows = np.setdiff1d(np.arange(nrows), indices[i:nrows], assume_unique=True) 223 | indices[nrows:i+n] = rng.choice(remaining_rows, size=i+n-nrows, replace=False) 224 | 225 | i += n 226 | indptr.append(i) 227 | 228 | # objective coefficients 229 | c = rng.randint(max_coef, size=ncols) + 1 230 | 231 | # sparce CSC to sparse CSR matrix 232 | A = scipy.sparse.csc_matrix( 233 | (np.ones(len(indices), dtype=int), indices, indptr), 234 | shape=(nrows, ncols)).tocsr() 235 | indices = A.indices 236 | indptr = A.indptr 237 | 238 | # write problem 239 | with open(filename, 'w') as file: 240 | file.write("minimize\nOBJ:") 241 | file.write("".join([f" +{c[j]} x{j+1}" for j in range(ncols)])) 242 | 243 | file.write("\n\nsubject to\n") 244 | for i in range(nrows): 245 | row_cols_str = "".join([f" +1 x{j+1}" for j in indices[indptr[i]:indptr[i+1]]]) 246 | file.write(f"C{i}:" + row_cols_str + f" >= 1\n") 247 | 248 | file.write("\nbinary\n") 249 | file.write("".join([f" x{j+1}" for j in range(ncols)])) 250 | 251 | 252 | def generate_cauctions(random, filename, n_items=100, n_bids=500, min_value=1, max_value=100, 253 | value_deviation=0.5, add_item_prob=0.7, max_n_sub_bids=5, 254 | additivity=0.2, budget_factor=1.5, resale_factor=0.5, 255 | integers=False, warnings=False): 256 | """ 257 | Generate a Combinatorial Auction problem following the 'arbitrary' scheme found in section 4.3. of 258 | Kevin Leyton-Brown, Mark Pearson, and Yoav Shoham. (2000). 259 | Towards a universal test suite for combinatorial auction algorithms. 260 | Proceedings of ACM Conference on Electronic Commerce (EC-00) 66-76. 261 | 262 | Saves it as a CPLEX LP file. 263 | 264 | Parameters 265 | ---------- 266 | random : numpy.random.RandomState 267 | A random number generator. 268 | filename : str 269 | Path to the file to save. 270 | n_items : int 271 | The number of items. 272 | n_bids : int 273 | The number of bids. 274 | min_value : int 275 | The minimum resale value for an item. 276 | max_value : int 277 | The maximum resale value for an item. 278 | value_deviation : int 279 | The deviation allowed for each bidder's private value of an item, relative from max_value. 280 | add_item_prob : float in [0, 1] 281 | The probability of adding a new item to an existing bundle. 282 | max_n_sub_bids : int 283 | The maximum number of substitutable bids per bidder (+1 gives the maximum number of bids per bidder). 284 | additivity : float 285 | Additivity parameter for bundle prices. Note that additivity < 0 gives sub-additive bids, while additivity > 0 gives super-additive bids. 286 | budget_factor : float 287 | The budget factor for each bidder, relative to their initial bid's price. 288 | resale_factor : float 289 | The resale factor for each bidder, relative to their initial bid's resale value. 290 | integers : logical 291 | Should bid's prices be integral ? 292 | warnings : logical 293 | Should warnings be printed ? 294 | """ 295 | 296 | assert min_value >= 0 and max_value >= min_value 297 | assert add_item_prob >= 0 and add_item_prob <= 1 298 | 299 | def choose_next_item(bundle_mask, interests, compats, add_item_prob, random): 300 | n_items = len(interests) 301 | prob = (1 - bundle_mask) * interests * compats[bundle_mask, :].mean(axis=0) 302 | prob /= prob.sum() 303 | return random.choice(n_items, p=prob) 304 | 305 | # common item values (resale price) 306 | values = min_value + (max_value - min_value) * random.rand(n_items) 307 | 308 | # item compatibilities 309 | compats = np.triu(random.rand(n_items, n_items), k=1) 310 | compats = compats + compats.transpose() 311 | compats = compats / compats.sum(1) 312 | 313 | bids = [] 314 | n_dummy_items = 0 315 | 316 | # create bids, one bidder at a time 317 | while len(bids) < n_bids: 318 | 319 | # bidder item values (buy price) and interests 320 | private_interests = random.rand(n_items) 321 | private_values = values + max_value * value_deviation * (2 * private_interests - 1) 322 | 323 | # substitutable bids of this bidder 324 | bidder_bids = {} 325 | 326 | # generate initial bundle, choose first item according to bidder interests 327 | prob = private_interests / private_interests.sum() 328 | item = random.choice(n_items, p=prob) 329 | bundle_mask = np.full(n_items, 0) 330 | bundle_mask[item] = 1 331 | 332 | # add additional items, according to bidder interests and item compatibilities 333 | while random.rand() < add_item_prob: 334 | # stop when bundle full (no item left) 335 | if bundle_mask.sum() == n_items: 336 | break 337 | item = choose_next_item(bundle_mask, private_interests, compats, add_item_prob, random) 338 | bundle_mask[item] = 1 339 | 340 | bundle = np.nonzero(bundle_mask)[0] 341 | 342 | # compute bundle price with value additivity 343 | price = private_values[bundle].sum() + np.power(len(bundle), 1 + additivity) 344 | if integers: 345 | price = int(price) 346 | 347 | # drop negativaly priced bundles 348 | if price < 0: 349 | if warnings: 350 | print("warning: negatively priced bundle avoided") 351 | continue 352 | 353 | # bid on initial bundle 354 | bidder_bids[frozenset(bundle)] = price 355 | 356 | # generate candidates substitutable bundles 357 | sub_candidates = [] 358 | for item in bundle: 359 | 360 | # at least one item must be shared with initial bundle 361 | bundle_mask = np.full(n_items, 0) 362 | bundle_mask[item] = 1 363 | 364 | # add additional items, according to bidder interests and item compatibilities 365 | while bundle_mask.sum() < len(bundle): 366 | item = choose_next_item(bundle_mask, private_interests, compats, add_item_prob, random) 367 | bundle_mask[item] = 1 368 | 369 | sub_bundle = np.nonzero(bundle_mask)[0] 370 | 371 | # compute bundle price with value additivity 372 | sub_price = private_values[sub_bundle].sum() + np.power(len(sub_bundle), 1 + additivity) 373 | if integers: 374 | sub_price = int(sub_price) 375 | 376 | sub_candidates.append((sub_bundle, sub_price)) 377 | 378 | # filter valid candidates, higher priced candidates first 379 | budget = budget_factor * price 380 | min_resale_value = resale_factor * values[bundle].sum() 381 | for bundle, price in [ 382 | sub_candidates[i] for i in np.argsort([-price for bundle, price in sub_candidates])]: 383 | 384 | if len(bidder_bids) >= max_n_sub_bids + 1 or len(bids) + len(bidder_bids) >= n_bids: 385 | break 386 | 387 | if price < 0: 388 | if warnings: 389 | print("warning: negatively priced substitutable bundle avoided") 390 | continue 391 | 392 | if price > budget: 393 | if warnings: 394 | print("warning: over priced substitutable bundle avoided") 395 | continue 396 | 397 | if values[bundle].sum() < min_resale_value: 398 | if warnings: 399 | print("warning: substitutable bundle below min resale value avoided") 400 | continue 401 | 402 | if frozenset(bundle) in bidder_bids: 403 | if warnings: 404 | print("warning: duplicated substitutable bundle avoided") 405 | continue 406 | 407 | bidder_bids[frozenset(bundle)] = price 408 | 409 | # add XOR constraint if needed (dummy item) 410 | if len(bidder_bids) > 2: 411 | dummy_item = [n_items + n_dummy_items] 412 | n_dummy_items += 1 413 | else: 414 | dummy_item = [] 415 | 416 | # place bids 417 | for bundle, price in bidder_bids.items(): 418 | bids.append((list(bundle) + dummy_item, price)) 419 | 420 | # generate the LP file 421 | with open(filename, 'w') as file: 422 | bids_per_item = [[] for item in range(n_items + n_dummy_items)] 423 | 424 | file.write("maximize\nOBJ:") 425 | for i, bid in enumerate(bids): 426 | bundle, price = bid 427 | file.write(f" +{price} x{i+1}") 428 | for item in bundle: 429 | bids_per_item[item].append(i) 430 | 431 | file.write("\n\nsubject to\n") 432 | for item_bids in bids_per_item: 433 | if item_bids: 434 | for i in item_bids: 435 | file.write(f" +1 x{i+1}") 436 | file.write(f" <= 1\n") 437 | 438 | file.write("\nbinary\n") 439 | for i in range(len(bids)): 440 | file.write(f" x{i+1}") 441 | 442 | 443 | def generate_capacited_facility_location(random, filename, n_customers, n_facilities, ratio): 444 | """ 445 | Generate a Capacited Facility Location problem following 446 | Cornuejols G, Sridharan R, Thizy J-M (1991) 447 | A Comparison of Heuristics and Relaxations for the Capacitated Plant Location Problem. 448 | European Journal of Operations Research 50:280-297. 449 | 450 | Saves it as a CPLEX LP file. 451 | 452 | Parameters 453 | ---------- 454 | random : numpy.random.RandomState 455 | A random number generator. 456 | filename : str 457 | Path to the file to save. 458 | n_customers: int 459 | The desired number of customers. 460 | n_facilities: int 461 | The desired number of facilities. 462 | ratio: float 463 | The desired capacity / demand ratio. 464 | """ 465 | c_x = rng.rand(n_customers) 466 | c_y = rng.rand(n_customers) 467 | 468 | f_x = rng.rand(n_facilities) 469 | f_y = rng.rand(n_facilities) 470 | 471 | demands = rng.randint(5, 35+1, size=n_customers) 472 | capacities = rng.randint(10, 160+1, size=n_facilities) 473 | fixed_costs = rng.randint(100, 110+1, size=n_facilities) * np.sqrt(capacities) \ 474 | + rng.randint(90+1, size=n_facilities) 475 | fixed_costs = fixed_costs.astype(int) 476 | 477 | total_demand = demands.sum() 478 | total_capacity = capacities.sum() 479 | 480 | # adjust capacities according to ratio 481 | capacities = capacities * ratio * total_demand / total_capacity 482 | capacities = capacities.astype(int) 483 | total_capacity = capacities.sum() 484 | 485 | # transportation costs 486 | trans_costs = np.sqrt( 487 | (c_x.reshape((-1, 1)) - f_x.reshape((1, -1))) ** 2 \ 488 | + (c_y.reshape((-1, 1)) - f_y.reshape((1, -1))) ** 2) * 10 * demands.reshape((-1, 1)) 489 | 490 | # write problem 491 | with open(filename, 'w') as file: 492 | file.write("minimize\nobj:") 493 | file.write("".join([f" +{trans_costs[i, j]} x_{i+1}_{j+1}" for i in range(n_customers) for j in range(n_facilities)])) 494 | file.write("".join([f" +{fixed_costs[j]} y_{j+1}" for j in range(n_facilities)])) 495 | 496 | file.write("\n\nsubject to\n") 497 | for i in range(n_customers): 498 | file.write(f"demand_{i+1}:" + "".join([f" -1 x_{i+1}_{j+1}" for j in range(n_facilities)]) + f" <= -1\n") 499 | for j in range(n_facilities): 500 | file.write(f"capacity_{j+1}:" + "".join([f" +{demands[i]} x_{i+1}_{j+1}" for i in range(n_customers)]) + f" -{capacities[j]} y_{j+1} <= 0\n") 501 | 502 | # optional constraints for LP relaxation tightening 503 | file.write("total_capacity:" + "".join([f" -{capacities[j]} y_{j+1}" for j in range(n_facilities)]) + f" <= -{total_demand}\n") 504 | for i in range(n_customers): 505 | for j in range(n_facilities): 506 | file.write(f"affectation_{i+1}_{j+1}: +1 x_{i+1}_{j+1} -1 y_{j+1} <= 0") 507 | 508 | file.write("\nbounds\n") 509 | for i in range(n_customers): 510 | for j in range(n_facilities): 511 | file.write(f"0 <= x_{i+1}_{j+1} <= 1\n") 512 | 513 | file.write("\nbinary\n") 514 | file.write("".join([f" y_{j+1}" for j in range(n_facilities)])) 515 | 516 | 517 | if __name__ == '__main__': 518 | parser = argparse.ArgumentParser() 519 | parser.add_argument( 520 | 'problem', 521 | help='MILP instance type to process.', 522 | choices=['setcover', 'cauctions', 'facilities', 'indset'], 523 | ) 524 | parser.add_argument( 525 | '-s', '--seed', 526 | help='Random generator seed (default 0).', 527 | type=utilities.valid_seed, 528 | default=0, 529 | ) 530 | args = parser.parse_args() 531 | 532 | rng = np.random.RandomState(args.seed) 533 | 534 | if args.problem == 'setcover': 535 | nrows = 500 536 | ncols = 1000 537 | dens = 0.05 538 | max_coef = 100 539 | 540 | filenames = [] 541 | nrowss = [] 542 | ncolss = [] 543 | denss = [] 544 | 545 | # train instances 546 | n = 10000 547 | lp_dir = f'data/instances/setcover/train_{nrows}r_{ncols}c_{dens}d' 548 | print(f"{n} instances in {lp_dir}") 549 | os.makedirs(lp_dir) 550 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 551 | nrowss.extend([nrows] * n) 552 | ncolss.extend([ncols] * n) 553 | denss.extend([dens] * n) 554 | 555 | # validation instances 556 | n = 2000 557 | lp_dir = f'data/instances/setcover/valid_{nrows}r_{ncols}c_{dens}d' 558 | print(f"{n} instances in {lp_dir}") 559 | os.makedirs(lp_dir) 560 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 561 | nrowss.extend([nrows] * n) 562 | ncolss.extend([ncols] * n) 563 | denss.extend([dens] * n) 564 | 565 | # small transfer instances 566 | n = 100 567 | nrows = 500 568 | lp_dir = f'data/instances/setcover/transfer_{nrows}r_{ncols}c_{dens}d' 569 | print(f"{n} instances in {lp_dir}") 570 | os.makedirs(lp_dir) 571 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 572 | nrowss.extend([nrows] * n) 573 | ncolss.extend([ncols] * n) 574 | denss.extend([dens] * n) 575 | 576 | # medium transfer instances 577 | n = 100 578 | nrows = 1000 579 | lp_dir = f'data/instances/setcover/transfer_{nrows}r_{ncols}c_{dens}d' 580 | print(f"{n} instances in {lp_dir}") 581 | os.makedirs(lp_dir) 582 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 583 | nrowss.extend([nrows] * n) 584 | ncolss.extend([ncols] * n) 585 | denss.extend([dens] * n) 586 | 587 | # big transfer instances 588 | n = 100 589 | nrows = 2000 590 | lp_dir = f'data/instances/setcover/transfer_{nrows}r_{ncols}c_{dens}d' 591 | print(f"{n} instances in {lp_dir}") 592 | os.makedirs(lp_dir) 593 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 594 | nrowss.extend([nrows] * n) 595 | ncolss.extend([ncols] * n) 596 | denss.extend([dens] * n) 597 | 598 | # test instances 599 | n = 2000 600 | nrows = 500 601 | ncols = 1000 602 | lp_dir = f'data/instances/setcover/test_{nrows}r_{ncols}c_{dens}d' 603 | print(f"{n} instances in {lp_dir}") 604 | os.makedirs(lp_dir) 605 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 606 | nrowss.extend([nrows] * n) 607 | ncolss.extend([ncols] * n) 608 | denss.extend([dens] * n) 609 | 610 | # actually generate the instances 611 | for filename, nrows, ncols, dens in zip(filenames, nrowss, ncolss, denss): 612 | print(f' generating file {filename} ...') 613 | generate_setcover(nrows=nrows, ncols=ncols, density=dens, filename=filename, rng=rng, max_coef=max_coef) 614 | 615 | print('done.') 616 | 617 | elif args.problem == 'indset': 618 | number_of_nodes = 500 619 | affinity = 4 620 | 621 | filenames = [] 622 | nnodess = [] 623 | 624 | # train instances 625 | n = 10000 626 | lp_dir = f'data/instances/indset/train_{number_of_nodes}_{affinity}' 627 | print(f"{n} instances in {lp_dir}") 628 | os.makedirs(lp_dir) 629 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 630 | nnodess.extend([number_of_nodes] * n) 631 | 632 | # validation instances 633 | n = 2000 634 | lp_dir = f'data/instances/indset/valid_{number_of_nodes}_{affinity}' 635 | print(f"{n} instances in {lp_dir}") 636 | os.makedirs(lp_dir) 637 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 638 | nnodess.extend([number_of_nodes] * n) 639 | 640 | # small transfer instances 641 | n = 100 642 | number_of_nodes = 500 643 | lp_dir = f'data/instances/indset/transfer_{number_of_nodes}_{affinity}' 644 | print(f"{n} instances in {lp_dir}") 645 | os.makedirs(lp_dir) 646 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 647 | nnodess.extend([number_of_nodes] * n) 648 | 649 | # medium transfer instances 650 | n = 100 651 | number_of_nodes = 1000 652 | lp_dir = f'data/instances/indset/transfer_{number_of_nodes}_{affinity}' 653 | print(f"{n} instances in {lp_dir}") 654 | os.makedirs(lp_dir) 655 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 656 | nnodess.extend([number_of_nodes] * n) 657 | 658 | # big transfer instances 659 | n = 100 660 | number_of_nodes = 1500 661 | lp_dir = f'data/instances/indset/transfer_{number_of_nodes}_{affinity}' 662 | print(f"{n} instances in {lp_dir}") 663 | os.makedirs(lp_dir) 664 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 665 | nnodess.extend([number_of_nodes] * n) 666 | 667 | # test instances 668 | n = 2000 669 | number_of_nodes = 500 670 | lp_dir = f'data/instances/indset/test_{number_of_nodes}_{affinity}' 671 | print(f"{n} instances in {lp_dir}") 672 | os.makedirs(lp_dir) 673 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 674 | nnodess.extend([number_of_nodes] * n) 675 | 676 | # actually generate the instances 677 | for filename, nnodes in zip(filenames, nnodess): 678 | print(f" generating file {filename} ...") 679 | graph = Graph.barabasi_albert(nnodes, affinity, rng) 680 | generate_indset(graph, filename) 681 | 682 | print("done.") 683 | 684 | elif args.problem == 'cauctions': 685 | number_of_items = 100 686 | number_of_bids = 500 687 | filenames = [] 688 | nitemss = [] 689 | nbidss = [] 690 | 691 | # train instances 692 | n = 10000 693 | lp_dir = f'data/instances/cauctions/train_{number_of_items}_{number_of_bids}' 694 | print(f"{n} instances in {lp_dir}") 695 | os.makedirs(lp_dir) 696 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 697 | nitemss.extend([number_of_items] * n) 698 | nbidss.extend([number_of_bids ] * n) 699 | 700 | # validation instances 701 | n = 2000 702 | lp_dir = f'data/instances/cauctions/valid_{number_of_items}_{number_of_bids}' 703 | print(f"{n} instances in {lp_dir}") 704 | os.makedirs(lp_dir) 705 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 706 | nitemss.extend([number_of_items] * n) 707 | nbidss.extend([number_of_bids ] * n) 708 | 709 | # small transfer instances 710 | n = 100 711 | number_of_items = 100 712 | number_of_bids = 500 713 | lp_dir = f'data/instances/cauctions/transfer_{number_of_items}_{number_of_bids}' 714 | print(f"{n} instances in {lp_dir}") 715 | os.makedirs(lp_dir) 716 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 717 | nitemss.extend([number_of_items] * n) 718 | nbidss.extend([number_of_bids ] * n) 719 | 720 | # medium transfer instances 721 | n = 100 722 | number_of_items = 200 723 | number_of_bids = 1000 724 | lp_dir = f'data/instances/cauctions/transfer_{number_of_items}_{number_of_bids}' 725 | print(f"{n} instances in {lp_dir}") 726 | os.makedirs(lp_dir) 727 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 728 | nitemss.extend([number_of_items] * n) 729 | nbidss.extend([number_of_bids ] * n) 730 | 731 | # big transfer instances 732 | n = 100 733 | number_of_items = 300 734 | number_of_bids = 1500 735 | lp_dir = f'data/instances/cauctions/transfer_{number_of_items}_{number_of_bids}' 736 | print(f"{n} instances in {lp_dir}") 737 | os.makedirs(lp_dir) 738 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 739 | nitemss.extend([number_of_items] * n) 740 | nbidss.extend([number_of_bids ] * n) 741 | 742 | # test instances 743 | n = 2000 744 | number_of_items = 100 745 | number_of_bids = 500 746 | lp_dir = f'data/instances/cauctions/test_{number_of_items}_{number_of_bids}' 747 | print(f"{n} instances in {lp_dir}") 748 | os.makedirs(lp_dir) 749 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 750 | nitemss.extend([number_of_items] * n) 751 | nbidss.extend([number_of_bids ] * n) 752 | 753 | # actually generate the instances 754 | for filename, nitems, nbids in zip(filenames, nitemss, nbidss): 755 | print(f" generating file {filename} ...") 756 | generate_cauctions(rng, filename, n_items=nitems, n_bids=nbids, add_item_prob=0.7) 757 | 758 | print("done.") 759 | 760 | elif args.problem == 'facilities': 761 | number_of_customers = 100 762 | number_of_facilities = 100 763 | ratio = 5 764 | filenames = [] 765 | ncustomerss = [] 766 | nfacilitiess = [] 767 | ratios = [] 768 | 769 | # train instances 770 | n = 10000 771 | lp_dir = f'data/instances/facilities/train_{number_of_customers}_{number_of_facilities}_{ratio}' 772 | print(f"{n} instances in {lp_dir}") 773 | os.makedirs(lp_dir) 774 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 775 | ncustomerss.extend([number_of_customers] * n) 776 | nfacilitiess.extend([number_of_facilities] * n) 777 | ratios.extend([ratio] * n) 778 | 779 | # validation instances 780 | n = 2000 781 | lp_dir = f'data/instances/facilities/valid_{number_of_customers}_{number_of_facilities}_{ratio}' 782 | print(f"{n} instances in {lp_dir}") 783 | os.makedirs(lp_dir) 784 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 785 | ncustomerss.extend([number_of_customers] * n) 786 | nfacilitiess.extend([number_of_facilities] * n) 787 | ratios.extend([ratio] * n) 788 | 789 | # small transfer instances 790 | n = 100 791 | number_of_customers = 100 792 | number_of_facilities = 100 793 | lp_dir = f'data/instances/facilities/transfer_{number_of_customers}_{number_of_facilities}_{ratio}' 794 | print(f"{n} instances in {lp_dir}") 795 | os.makedirs(lp_dir) 796 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 797 | ncustomerss.extend([number_of_customers] * n) 798 | nfacilitiess.extend([number_of_facilities] * n) 799 | ratios.extend([ratio] * n) 800 | 801 | # medium transfer instances 802 | n = 100 803 | number_of_customers = 200 804 | lp_dir = f'data/instances/facilities/transfer_{number_of_customers}_{number_of_facilities}_{ratio}' 805 | print(f"{n} instances in {lp_dir}") 806 | os.makedirs(lp_dir) 807 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 808 | ncustomerss.extend([number_of_customers] * n) 809 | nfacilitiess.extend([number_of_facilities] * n) 810 | ratios.extend([ratio] * n) 811 | 812 | # big transfer instances 813 | n = 100 814 | number_of_customers = 400 815 | lp_dir = f'data/instances/facilities/transfer_{number_of_customers}_{number_of_facilities}_{ratio}' 816 | print(f"{n} instances in {lp_dir}") 817 | os.makedirs(lp_dir) 818 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 819 | ncustomerss.extend([number_of_customers] * n) 820 | nfacilitiess.extend([number_of_facilities] * n) 821 | ratios.extend([ratio] * n) 822 | 823 | # test instances 824 | n = 2000 825 | number_of_customers = 100 826 | number_of_facilities = 100 827 | lp_dir = f'data/instances/facilities/test_{number_of_customers}_{number_of_facilities}_{ratio}' 828 | print(f"{n} instances in {lp_dir}") 829 | os.makedirs(lp_dir) 830 | filenames.extend([os.path.join(lp_dir, f'instance_{i+1}.lp') for i in range(n)]) 831 | ncustomerss.extend([number_of_customers] * n) 832 | nfacilitiess.extend([number_of_facilities] * n) 833 | ratios.extend([ratio] * n) 834 | 835 | # actually generate the instances 836 | for filename, ncs, nfs, r in zip(filenames, ncustomerss, nfacilitiess, ratios): 837 | print(f" generating file {filename} ...") 838 | generate_capacited_facility_location(rng, filename, n_customers=ncs, n_facilities=nfs, ratio=r) 839 | 840 | print("done.") 841 | -------------------------------------------------------------------------------- /02_generate_dataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | import multiprocessing as mp 4 | import pickle 5 | import glob 6 | import numpy as np 7 | import shutil 8 | import gzip 9 | 10 | import pyscipopt as scip 11 | import utilities 12 | 13 | 14 | class SamplingAgent(scip.Branchrule): 15 | 16 | def __init__(self, episode, instance, seed, out_queue, exploration_policy, query_expert_prob, out_dir, follow_expert=True): 17 | self.episode = episode 18 | self.instance = instance 19 | self.seed = seed 20 | self.out_queue = out_queue 21 | self.exploration_policy = exploration_policy 22 | self.query_expert_prob = query_expert_prob 23 | self.out_dir = out_dir 24 | self.follow_expert = follow_expert 25 | 26 | self.rng = np.random.RandomState(seed) 27 | self.new_node = True 28 | self.sample_counter = 0 29 | 30 | def branchinit(self): 31 | self.khalil_root_buffer = {} 32 | 33 | def branchexeclp(self, allowaddcons): 34 | 35 | if self.model.getNNodes() == 1: 36 | # initialize root buffer for Khalil features extraction 37 | utilities.extract_khalil_variable_features(self.model, [], self.khalil_root_buffer) 38 | 39 | # once in a while, also run the expert policy and record the (state, action) pair 40 | query_expert = self.rng.rand() < self.query_expert_prob 41 | if query_expert: 42 | state = utilities.extract_state(self.model) 43 | cands, *_ = self.model.getPseudoBranchCands() 44 | state_khalil = utilities.extract_khalil_variable_features(self.model, cands, self.khalil_root_buffer) 45 | 46 | result = self.model.executeBranchRule('vanillafullstrong', allowaddcons) 47 | cands_, scores, npriocands, bestcand = self.model.getVanillafullstrongData() 48 | 49 | assert result == scip.SCIP_RESULT.DIDNOTRUN 50 | assert all([c1.getCol().getLPPos() == c2.getCol().getLPPos() for c1, c2 in zip(cands, cands_)]) 51 | 52 | action_set = [c.getCol().getLPPos() for c in cands] 53 | expert_action = action_set[bestcand] 54 | 55 | data = [state, state_khalil, expert_action, action_set, scores] 56 | 57 | # Do not record inconsistent scores. May happen if SCIP was early stopped (time limit). 58 | if not any([s < 0 for s in scores]): 59 | 60 | filename = f'{self.out_dir}/sample_{self.episode}_{self.sample_counter}.pkl' 61 | with gzip.open(filename, 'wb') as f: 62 | pickle.dump({ 63 | 'episode': self.episode, 64 | 'instance': self.instance, 65 | 'seed': self.seed, 66 | 'node_number': self.model.getCurrentNode().getNumber(), 67 | 'node_depth': self.model.getCurrentNode().getDepth(), 68 | 'data': data, 69 | }, f) 70 | 71 | self.out_queue.put({ 72 | 'type': 'sample', 73 | 'episode': self.episode, 74 | 'instance': self.instance, 75 | 'seed': self.seed, 76 | 'node_number': self.model.getCurrentNode().getNumber(), 77 | 'node_depth': self.model.getCurrentNode().getDepth(), 78 | 'filename': filename, 79 | }) 80 | 81 | self.sample_counter += 1 82 | 83 | # if exploration and expert policies are the same, prevent running it twice 84 | if not query_expert or (not self.follow_expert and self.exploration_policy != 'vanillafullstrong'): 85 | result = self.model.executeBranchRule(self.exploration_policy, allowaddcons) 86 | 87 | # apply 'vanillafullstrong' branching decision if needed 88 | if query_expert and self.follow_expert or self.exploration_policy == 'vanillafullstrong': 89 | assert result == scip.SCIP_RESULT.DIDNOTRUN 90 | cands, scores, npriocands, bestcand = self.model.getVanillafullstrongData() 91 | self.model.branchVar(cands[bestcand]) 92 | result = scip.SCIP_RESULT.BRANCHED 93 | 94 | return {"result": result} 95 | 96 | 97 | def make_samples(in_queue, out_queue): 98 | """ 99 | Worker loop: fetch an instance, run an episode and record samples. 100 | 101 | Parameters 102 | ---------- 103 | in_queue : multiprocessing.Queue 104 | Input queue from which orders are received. 105 | out_queue : multiprocessing.Queue 106 | Output queue in which to send samples. 107 | """ 108 | 109 | while True: 110 | episode, instance, seed, exploration_policy, query_expert_prob, time_limit, out_dir = in_queue.get() 111 | print(f'[w {os.getpid()}] episode {episode}, seed {seed}, processing instance \'{instance}\'...') 112 | 113 | m = scip.Model() 114 | m.setIntParam('display/verblevel', 0) 115 | m.readProblem(f'{instance}') 116 | utilities.init_scip_params(m, seed=seed) 117 | m.setIntParam('timing/clocktype', 2) 118 | m.setRealParam('limits/time', time_limit) 119 | 120 | branchrule = SamplingAgent( 121 | episode=episode, 122 | instance=instance, 123 | seed=seed, 124 | out_queue=out_queue, 125 | exploration_policy=exploration_policy, 126 | query_expert_prob=query_expert_prob, 127 | out_dir=out_dir) 128 | 129 | m.includeBranchrule( 130 | branchrule=branchrule, 131 | name="Sampling branching rule", desc="", 132 | priority=666666, maxdepth=-1, maxbounddist=1) 133 | 134 | m.setBoolParam('branching/vanillafullstrong/integralcands', True) 135 | m.setBoolParam('branching/vanillafullstrong/scoreall', True) 136 | m.setBoolParam('branching/vanillafullstrong/collectscores', True) 137 | m.setBoolParam('branching/vanillafullstrong/donotbranch', True) 138 | m.setBoolParam('branching/vanillafullstrong/idempotent', True) 139 | 140 | out_queue.put({ 141 | 'type': 'start', 142 | 'episode': episode, 143 | 'instance': instance, 144 | 'seed': seed, 145 | }) 146 | 147 | m.optimize() 148 | m.freeProb() 149 | 150 | print(f"[w {os.getpid()}] episode {episode} done, {branchrule.sample_counter} samples") 151 | 152 | out_queue.put({ 153 | 'type': 'done', 154 | 'episode': episode, 155 | 'instance': instance, 156 | 'seed': seed, 157 | }) 158 | 159 | 160 | def send_orders(orders_queue, instances, seed, exploration_policy, query_expert_prob, time_limit, out_dir): 161 | """ 162 | Continuously send sampling orders to workers (relies on limited 163 | queue capacity). 164 | 165 | Parameters 166 | ---------- 167 | orders_queue : multiprocessing.Queue 168 | Queue to which to send orders. 169 | instances : list 170 | Instance file names from which to sample episodes. 171 | seed : int 172 | Random seed for reproducibility. 173 | exploration_policy : str 174 | Branching strategy for exploration. 175 | query_expert_prob : float in [0, 1] 176 | Probability of running the expert strategy and collecting samples. 177 | time_limit : float in [0, 1e+20] 178 | Maximum running time for an episode, in seconds. 179 | out_dir: str 180 | Output directory in which to write samples. 181 | """ 182 | rng = np.random.RandomState(seed) 183 | 184 | episode = 0 185 | while True: 186 | instance = rng.choice(instances) 187 | seed = rng.randint(2**32) 188 | orders_queue.put([episode, instance, seed, exploration_policy, query_expert_prob, time_limit, out_dir]) 189 | episode += 1 190 | 191 | 192 | def collect_samples(instances, out_dir, rng, n_samples, n_jobs, 193 | exploration_policy, query_expert_prob, time_limit): 194 | """ 195 | Runs branch-and-bound episodes on the given set of instances, and collects 196 | randomly (state, action) pairs from the 'vanilla-fullstrong' expert 197 | brancher. 198 | 199 | Parameters 200 | ---------- 201 | instances : list 202 | Instance files from which to collect samples. 203 | out_dir : str 204 | Directory in which to write samples. 205 | rng : numpy.random.RandomState 206 | A random number generator for reproducibility. 207 | n_samples : int 208 | Number of samples to collect. 209 | n_jobs : int 210 | Number of jobs for parallel sampling. 211 | exploration_policy : str 212 | Exploration policy (branching rule) for sampling. 213 | query_expert_prob : float in [0, 1] 214 | Probability of using the expert policy and recording a (state, action) 215 | pair. 216 | time_limit : float in [0, 1e+20] 217 | Maximum running time for an episode, in seconds. 218 | """ 219 | os.makedirs(out_dir, exist_ok=True) 220 | 221 | # start workers 222 | orders_queue = mp.Queue(maxsize=2*n_jobs) 223 | answers_queue = mp.SimpleQueue() 224 | workers = [] 225 | for i in range(n_jobs): 226 | p = mp.Process( 227 | target=make_samples, 228 | args=(orders_queue, answers_queue), 229 | daemon=True) 230 | workers.append(p) 231 | p.start() 232 | 233 | tmp_samples_dir = f'{out_dir}/tmp' 234 | os.makedirs(tmp_samples_dir, exist_ok=True) 235 | 236 | # start dispatcher 237 | dispatcher = mp.Process( 238 | target=send_orders, 239 | args=(orders_queue, instances, rng.randint(2**32), exploration_policy, query_expert_prob, time_limit, tmp_samples_dir), 240 | daemon=True) 241 | dispatcher.start() 242 | 243 | # record answers and write samples 244 | buffer = {} 245 | current_episode = 0 246 | i = 0 247 | in_buffer = 0 248 | while i < n_samples: 249 | sample = answers_queue.get() 250 | 251 | # add received sample to buffer 252 | if sample['type'] == 'start': 253 | buffer[sample['episode']] = [] 254 | else: 255 | buffer[sample['episode']].append(sample) 256 | if sample['type'] == 'sample': 257 | in_buffer += 1 258 | 259 | # if any, write samples from current episode 260 | while current_episode in buffer and buffer[current_episode]: 261 | samples_to_write = buffer[current_episode] 262 | buffer[current_episode] = [] 263 | 264 | for sample in samples_to_write: 265 | 266 | # if no more samples here, move to next episode 267 | if sample['type'] == 'done': 268 | del buffer[current_episode] 269 | current_episode += 1 270 | 271 | # else write sample 272 | else: 273 | os.rename(sample['filename'], f'{out_dir}/sample_{i+1}.pkl') 274 | in_buffer -= 1 275 | i += 1 276 | print(f"[m {os.getpid()}] {i} / {n_samples} samples written, ep {sample['episode']} ({in_buffer} in buffer).") 277 | 278 | # early stop dispatcher (hard) 279 | if in_buffer + i >= n_samples and dispatcher.is_alive(): 280 | dispatcher.terminate() 281 | print(f"[m {os.getpid()}] dispatcher stopped...") 282 | 283 | # as soon as enough samples are collected, stop 284 | if i == n_samples: 285 | buffer = {} 286 | break 287 | 288 | # stop all workers (hard) 289 | for p in workers: 290 | p.terminate() 291 | 292 | shutil.rmtree(tmp_samples_dir, ignore_errors=True) 293 | 294 | 295 | if __name__ == '__main__': 296 | parser = argparse.ArgumentParser() 297 | parser.add_argument( 298 | 'problem', 299 | help='MILP instance type to process.', 300 | choices=['setcover', 'cauctions', 'facilities', 'indset'], 301 | ) 302 | parser.add_argument( 303 | '-s', '--seed', 304 | help='Random generator seed.', 305 | type=utilities.valid_seed, 306 | default=0, 307 | ) 308 | parser.add_argument( 309 | '-j', '--njobs', 310 | help='Number of parallel jobs.', 311 | type=int, 312 | default=1, 313 | ) 314 | args = parser.parse_args() 315 | 316 | print(f"seed {args.seed}") 317 | 318 | train_size = 100000 319 | valid_size = 20000 320 | test_size = 20000 321 | exploration_strategy = 'pscost' 322 | node_record_prob = 0.05 323 | time_limit = 3600 324 | 325 | if args.problem == 'setcover': 326 | instances_train = glob.glob('data/instances/setcover/train_500r_1000c_0.05d/*.lp') 327 | instances_valid = glob.glob('data/instances/setcover/valid_500r_1000c_0.05d/*.lp') 328 | instances_test = glob.glob('data/instances/setcover/test_500r_1000c_0.05d/*.lp') 329 | out_dir = 'data/samples/setcover/500r_1000c_0.05d' 330 | 331 | elif args.problem == 'cauctions': 332 | instances_train = glob.glob('data/instances/cauctions/train_100_500/*.lp') 333 | instances_valid = glob.glob('data/instances/cauctions/valid_100_500/*.lp') 334 | instances_test = glob.glob('data/instances/cauctions/test_100_500/*.lp') 335 | out_dir = 'data/samples/cauctions/100_500' 336 | 337 | elif args.problem == 'indset': 338 | instances_train = glob.glob('data/instances/indset/train_500_4/*.lp') 339 | instances_valid = glob.glob('data/instances/indset/valid_500_4/*.lp') 340 | instances_test = glob.glob('data/instances/indset/test_500_4/*.lp') 341 | out_dir = 'data/samples/indset/500_4' 342 | 343 | elif args.problem == 'facilities': 344 | instances_train = glob.glob('data/instances/facilities/train_100_100_5/*.lp') 345 | instances_valid = glob.glob('data/instances/facilities/valid_100_100_5/*.lp') 346 | instances_test = glob.glob('data/instances/facilities/test_100_100_5/*.lp') 347 | out_dir = 'data/samples/facilities/100_100_5' 348 | time_limit = 600 349 | 350 | else: 351 | raise NotImplementedError 352 | 353 | print(f"{len(instances_train)} train instances for {train_size} samples") 354 | print(f"{len(instances_valid)} validation instances for {valid_size} samples") 355 | print(f"{len(instances_test)} test instances for {test_size} samples") 356 | 357 | # create output directory, throws an error if it already exists 358 | os.makedirs(out_dir) 359 | 360 | rng = np.random.RandomState(args.seed) 361 | collect_samples(instances_train, out_dir + '/train', rng, train_size, 362 | args.njobs, exploration_policy=exploration_strategy, 363 | query_expert_prob=node_record_prob, 364 | time_limit=time_limit) 365 | 366 | rng = np.random.RandomState(args.seed + 1) 367 | collect_samples(instances_valid, out_dir + '/valid', rng, test_size, 368 | args.njobs, exploration_policy=exploration_strategy, 369 | query_expert_prob=node_record_prob, 370 | time_limit=time_limit) 371 | 372 | rng = np.random.RandomState(args.seed + 2) 373 | collect_samples(instances_test, out_dir + '/test', rng, test_size, 374 | args.njobs, exploration_policy=exploration_strategy, 375 | query_expert_prob=node_record_prob, 376 | time_limit=time_limit) 377 | -------------------------------------------------------------------------------- /03_train_competitor.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import os 3 | import argparse 4 | import numpy as np 5 | import utilities 6 | import pathlib 7 | 8 | from utilities import log, load_flat_samples 9 | 10 | 11 | def load_samples(filenames, feat_type, label_type, augment, qbnorm, size_limit, logfile=None): 12 | x, y, ncands = [], [], [] 13 | total_ncands = 0 14 | 15 | for i, filename in enumerate(filenames): 16 | cand_x, cand_y, best = load_flat_samples(filename, feat_type, label_type, augment, qbnorm) 17 | 18 | x.append(cand_x) 19 | y.append(cand_y) 20 | ncands.append(cand_x.shape[0]) 21 | total_ncands += ncands[-1] 22 | 23 | if (i + 1) % 100 == 0: 24 | log(f" {i+1}/{len(filenames)} files processed ({total_ncands} candidate variables)", logfile) 25 | 26 | if total_ncands >= size_limit: 27 | log(f" dataset size limit reached ({size_limit} candidate variables)", logfile) 28 | break 29 | 30 | x = np.concatenate(x) 31 | y = np.concatenate(y) 32 | ncands = np.asarray(ncands) 33 | 34 | if total_ncands > size_limit: 35 | x = x[:size_limit] 36 | y = y[:size_limit] 37 | ncands[-1] -= total_ncands - size_limit 38 | 39 | return x, y, ncands 40 | 41 | 42 | if __name__ == '__main__': 43 | parser = argparse.ArgumentParser() 44 | parser.add_argument( 45 | 'problem', 46 | help='MILP instance type to process.', 47 | choices=['setcover', 'cauctions', 'facilities', 'indset'], 48 | ) 49 | parser.add_argument( 50 | '-m', '--model', 51 | help='Model to be trained.', 52 | type=str, 53 | choices=['svmrank', 'extratrees', 'lambdamart'], 54 | ) 55 | parser.add_argument( 56 | '-s', '--seed', 57 | help='Random generator seed.', 58 | type=utilities.valid_seed, 59 | default=0, 60 | ) 61 | args = parser.parse_args() 62 | 63 | feats_type = 'nbr_maxminmean' 64 | 65 | problem_folders = { 66 | 'setcover': 'setcover/500r_1000c_0.05d', 67 | 'cauctions': 'cauctions/100_500', 68 | 'facilities': 'facilities/100_100_5', 69 | 'indset': 'indset/500_4', 70 | } 71 | problem_folder = problem_folders[args.problem] 72 | 73 | if args.model == 'extratrees': 74 | train_max_size = 250000 75 | valid_max_size = 100000 76 | feat_type = 'gcnn_agg' 77 | feat_qbnorm = False 78 | feat_augment = False 79 | label_type = 'scores' 80 | 81 | elif args.model == 'lambdamart': 82 | train_max_size = 250000 83 | valid_max_size = 100000 84 | feat_type = 'khalil' 85 | feat_qbnorm = True 86 | feat_augment = False 87 | label_type = 'bipartite_ranks' 88 | 89 | elif args.model == 'svmrank': 90 | train_max_size = 250000 91 | valid_max_size = 100000 92 | feat_type = 'khalil' 93 | feat_qbnorm = True 94 | feat_augment = True 95 | label_type = 'bipartite_ranks' 96 | 97 | rng = np.random.RandomState(args.seed) 98 | 99 | running_dir = f"trained_models/{args.problem}/{args.model}_{feat_type}/{args.seed}" 100 | os.makedirs(running_dir) 101 | 102 | logfile = f"{running_dir}/log.txt" 103 | log(f"Logfile for {args.model} model on {args.problem} with seed {args.seed}", logfile) 104 | 105 | # Data loading 106 | train_files = list(pathlib.Path(f'data/samples/{problem_folder}/train').glob('sample_*.pkl')) 107 | valid_files = list(pathlib.Path(f'data/samples/{problem_folder}/valid').glob('sample_*.pkl')) 108 | 109 | log(f"{len(train_files)} training files", logfile) 110 | log(f"{len(valid_files)} validation files", logfile) 111 | 112 | log("Loading training samples", logfile) 113 | train_x, train_y, train_ncands = load_samples( 114 | rng.permutation(train_files), 115 | feat_type, label_type, feat_augment, feat_qbnorm, 116 | train_max_size, logfile) 117 | log(f" {train_x.shape[0]} training samples", logfile) 118 | 119 | log("Loading validation samples", logfile) 120 | valid_x, valid_y, valid_ncands = load_samples( 121 | valid_files, 122 | feat_type, label_type, feat_augment, feat_qbnorm, 123 | valid_max_size, logfile) 124 | log(f" {valid_x.shape[0]} validation samples", logfile) 125 | 126 | # Data normalization 127 | log("Normalizing datasets", logfile) 128 | x_shift = train_x.mean(axis=0) 129 | x_scale = train_x.std(axis=0) 130 | x_scale[x_scale == 0] = 1 131 | 132 | valid_x = (valid_x - x_shift) / x_scale 133 | train_x = (train_x - x_shift) / x_scale 134 | 135 | # Saving feature parameters 136 | with open(f"{running_dir}/feat_specs.pkl", "wb") as file: 137 | pickle.dump({ 138 | 'type': feat_type, 139 | 'augment': feat_augment, 140 | 'qbnorm': feat_qbnorm, 141 | }, file) 142 | 143 | # save normalization parameters 144 | with open(f"{running_dir}/normalization.pkl", "wb") as f: 145 | pickle.dump((x_shift, x_scale), f) 146 | 147 | log("Starting training", logfile) 148 | if args.model == 'extratrees': 149 | from sklearn.ensemble import ExtraTreesRegressor 150 | 151 | # Training 152 | model = ExtraTreesRegressor( 153 | n_estimators=100, 154 | random_state=rng,) 155 | model.verbose = True 156 | model.fit(train_x, train_y) 157 | model.verbose = False 158 | 159 | # Saving model 160 | with open(f"{running_dir}/model.pkl", "wb") as file: 161 | pickle.dump(model, file) 162 | 163 | # Testing 164 | loss = np.mean((model.predict(valid_x) - valid_y) ** 2) 165 | log(f"Validation RMSE: {np.sqrt(loss):.2f}", logfile) 166 | 167 | elif args.model == 'lambdamart': 168 | import pyltr 169 | 170 | train_qids = np.repeat(np.arange(len(train_ncands)), train_ncands) 171 | valid_qids = np.repeat(np.arange(len(valid_ncands)), valid_ncands) 172 | 173 | # Training 174 | model = pyltr.models.LambdaMART(verbose=1, random_state=rng, n_estimators=500) 175 | model.fit(train_x, train_y, train_qids, 176 | monitor=pyltr.models.monitors.ValidationMonitor( 177 | valid_x, valid_y, valid_qids, metric=model.metric)) 178 | 179 | # Saving model 180 | with open(f"{running_dir}/model.pkl", "wb") as file: 181 | pickle.dump(model, file) 182 | 183 | # Testing 184 | loss = model.metric.calc_mean(valid_qids, valid_y, model.predict(valid_x)) 185 | log(f"Validation log-NDCG: {np.log(loss)}", logfile) 186 | 187 | elif args.model == 'svmrank': 188 | import svmrank 189 | 190 | train_qids = np.repeat(np.arange(len(train_ncands)), train_ncands) 191 | valid_qids = np.repeat(np.arange(len(valid_ncands)), valid_ncands) 192 | 193 | # Training (includes hyper-parameter tuning) 194 | best_loss = np.inf 195 | best_model = None 196 | for c in (1e-3, 1e-2, 1e-1, 1e0): 197 | log(f"C: {c}", logfile) 198 | model = svmrank.Model({ 199 | '-c': c * len(train_ncands), # c_light = c_rank / n 200 | '-v': 1, 201 | '-y': 0, 202 | '-l': 2, 203 | }) 204 | model.fit(train_x, train_y, train_qids) 205 | loss = model.loss(train_y, model(train_x, train_qids), train_qids) 206 | log(f" training loss: {loss}", logfile) 207 | loss = model.loss(valid_y, model(valid_x, valid_qids), valid_qids) 208 | log(f" validation loss: {loss}", logfile) 209 | if loss < best_loss: 210 | best_model = model 211 | best_loss = loss 212 | best_c = c 213 | # save model 214 | model.write(f"{running_dir}/model.txt") 215 | 216 | log(f"Best model with C={best_c}, validation loss: {best_loss}", logfile) 217 | -------------------------------------------------------------------------------- /03_train_gcnn.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | import argparse 4 | import sys 5 | import pathlib 6 | import pickle 7 | import numpy as np 8 | from time import strftime 9 | from shutil import copyfile 10 | import gzip 11 | 12 | import tensorflow as tf 13 | import tensorflow.contrib.eager as tfe 14 | 15 | import utilities 16 | from utilities import log 17 | 18 | from utilities_tf import load_batch_gcnn 19 | 20 | 21 | def load_batch_tf(x): 22 | return tf.py_func( 23 | load_batch_gcnn, 24 | [x], 25 | [tf.float32, tf.int32, tf.float32, tf.float32, tf.int32, tf.int32, tf.int32, tf.int32, tf.int32, tf.float32]) 26 | 27 | 28 | def pretrain(model, dataloader): 29 | """ 30 | Pre-normalizes a model (i.e., PreNormLayer layers) over the given samples. 31 | 32 | Parameters 33 | ---------- 34 | model : model.BaseModel 35 | A base model, which may contain some model.PreNormLayer layers. 36 | dataloader : tf.data.Dataset 37 | Dataset to use for pre-training the model. 38 | Return 39 | ------ 40 | number of PreNormLayer layers processed. 41 | """ 42 | model.pre_train_init() 43 | i = 0 44 | while True: 45 | for batch in dataloader: 46 | c, ei, ev, v, n_cs, n_vs, n_cands, cands, best_cands, cand_scores = batch 47 | batched_states = (c, ei, ev, v, n_cs, n_vs) 48 | 49 | if not model.pre_train(batched_states, tf.convert_to_tensor(True)): 50 | break 51 | 52 | res = model.pre_train_next() 53 | if res is None: 54 | break 55 | else: 56 | layer, name = res 57 | 58 | i += 1 59 | 60 | return i 61 | 62 | 63 | def process(model, dataloader, top_k, optimizer=None): 64 | mean_loss = 0 65 | mean_kacc = np.zeros(len(top_k)) 66 | 67 | n_samples_processed = 0 68 | for batch in dataloader: 69 | c, ei, ev, v, n_cs, n_vs, n_cands, cands, best_cands, cand_scores = batch 70 | batched_states = (c, ei, ev, v, tf.reduce_sum(n_cs, keepdims=True), tf.reduce_sum(n_vs, keepdims=True)) # prevent padding 71 | batch_size = len(n_cs.numpy()) 72 | 73 | if optimizer: 74 | with tf.GradientTape() as tape: 75 | logits = model(batched_states, tf.convert_to_tensor(True)) # training mode 76 | logits = tf.expand_dims(tf.gather(tf.squeeze(logits, 0), cands), 0) # filter candidate variables 77 | logits = model.pad_output(logits, n_cands.numpy()) # apply padding now 78 | loss = tf.losses.sparse_softmax_cross_entropy(labels=best_cands, logits=logits) 79 | grads = tape.gradient(target=loss, sources=model.variables) 80 | optimizer.apply_gradients(zip(grads, model.variables)) 81 | else: 82 | logits = model(batched_states, tf.convert_to_tensor(False)) # eval mode 83 | logits = tf.expand_dims(tf.gather(tf.squeeze(logits, 0), cands), 0) # filter candidate variables 84 | logits = model.pad_output(logits, n_cands.numpy()) # apply padding now 85 | loss = tf.losses.sparse_softmax_cross_entropy(labels=best_cands, logits=logits) 86 | 87 | true_scores = model.pad_output(tf.reshape(cand_scores, (1, -1)), n_cands) 88 | true_bestscore = tf.reduce_max(true_scores, axis=-1, keepdims=True) 89 | true_scores = true_scores.numpy() 90 | true_bestscore = true_bestscore.numpy() 91 | 92 | kacc = [] 93 | for k in top_k: 94 | pred_top_k = tf.nn.top_k(logits, k=k)[1].numpy() 95 | pred_top_k_true_scores = np.take_along_axis(true_scores, pred_top_k, axis=1) 96 | kacc.append(np.mean(np.any(pred_top_k_true_scores == true_bestscore, axis=1))) 97 | kacc = np.asarray(kacc) 98 | 99 | mean_loss += loss.numpy() * batch_size 100 | mean_kacc += kacc * batch_size 101 | n_samples_processed += batch_size 102 | 103 | mean_loss /= n_samples_processed 104 | mean_kacc /= n_samples_processed 105 | 106 | return mean_loss, mean_kacc 107 | 108 | 109 | if __name__ == '__main__': 110 | parser = argparse.ArgumentParser() 111 | parser.add_argument( 112 | 'problem', 113 | help='MILP instance type to process.', 114 | choices=['setcover', 'cauctions', 'facilities', 'indset'], 115 | ) 116 | parser.add_argument( 117 | '-m', '--model', 118 | help='GCNN model to be trained.', 119 | type=str, 120 | default='baseline', 121 | ) 122 | parser.add_argument( 123 | '-s', '--seed', 124 | help='Random generator seed.', 125 | type=utilities.valid_seed, 126 | default=0, 127 | ) 128 | parser.add_argument( 129 | '-g', '--gpu', 130 | help='CUDA GPU id (-1 for CPU).', 131 | type=int, 132 | default=0, 133 | ) 134 | args = parser.parse_args() 135 | 136 | ### HYPER PARAMETERS ### 137 | max_epochs = 1000 138 | epoch_size = 312 139 | batch_size = 32 140 | pretrain_batch_size = 128 141 | valid_batch_size = 128 142 | lr = 0.001 143 | patience = 10 144 | early_stopping = 20 145 | top_k = [1, 3, 5, 10] 146 | train_ncands_limit = np.inf 147 | valid_ncands_limit = np.inf 148 | 149 | problem_folders = { 150 | 'setcover': 'setcover/500r_1000c_0.05d', 151 | 'cauctions': 'cauctions/100_500', 152 | 'facilities': 'facilities/100_100_5', 153 | 'indset': 'indset/500_4', 154 | } 155 | problem_folder = problem_folders[args.problem] 156 | 157 | running_dir = f"trained_models/{args.problem}/{args.model}/{args.seed}" 158 | 159 | os.makedirs(running_dir) 160 | 161 | ### LOG ### 162 | logfile = os.path.join(running_dir, 'log.txt') 163 | 164 | log(f"max_epochs: {max_epochs}", logfile) 165 | log(f"epoch_size: {epoch_size}", logfile) 166 | log(f"batch_size: {batch_size}", logfile) 167 | log(f"pretrain_batch_size: {pretrain_batch_size}", logfile) 168 | log(f"valid_batch_size : {valid_batch_size }", logfile) 169 | log(f"lr: {lr}", logfile) 170 | log(f"patience : {patience }", logfile) 171 | log(f"early_stopping : {early_stopping }", logfile) 172 | log(f"top_k: {top_k}", logfile) 173 | log(f"problem: {args.problem}", logfile) 174 | log(f"gpu: {args.gpu}", logfile) 175 | log(f"seed {args.seed}", logfile) 176 | 177 | ### NUMPY / TENSORFLOW SETUP ### 178 | if args.gpu == -1: 179 | os.environ['CUDA_VISIBLE_DEVICES'] = '' 180 | else: 181 | os.environ['CUDA_VISIBLE_DEVICES'] = f'{args.gpu}' 182 | config = tf.ConfigProto() 183 | config.gpu_options.allow_growth = True 184 | tf.enable_eager_execution(config) 185 | tf.executing_eagerly() 186 | 187 | rng = np.random.RandomState(args.seed) 188 | tf.set_random_seed(rng.randint(np.iinfo(int).max)) 189 | 190 | ### SET-UP DATASET ### 191 | train_files = list(pathlib.Path(f'data/samples/{problem_folder}/train').glob('sample_*.pkl')) 192 | valid_files = list(pathlib.Path(f'data/samples/{problem_folder}/valid').glob('sample_*.pkl')) 193 | 194 | 195 | def take_subset(sample_files, cands_limit): 196 | nsamples = 0 197 | ncands = 0 198 | for filename in sample_files: 199 | with gzip.open(filename, 'rb') as file: 200 | sample = pickle.load(file) 201 | 202 | _, _, _, cands, _ = sample['data'] 203 | ncands += len(cands) 204 | nsamples += 1 205 | 206 | if ncands >= cands_limit: 207 | log(f" dataset size limit reached ({cands_limit} candidate variables)", logfile) 208 | break 209 | 210 | return sample_files[:nsamples] 211 | 212 | 213 | if train_ncands_limit < np.inf: 214 | train_files = take_subset(rng.permutation(train_files), train_ncands_limit) 215 | log(f"{len(train_files)} training samples", logfile) 216 | if valid_ncands_limit < np.inf: 217 | valid_files = take_subset(valid_files, valid_ncands_limit) 218 | log(f"{len(valid_files)} validation samples", logfile) 219 | 220 | train_files = [str(x) for x in train_files] 221 | valid_files = [str(x) for x in valid_files] 222 | 223 | valid_data = tf.data.Dataset.from_tensor_slices(valid_files) 224 | valid_data = valid_data.batch(valid_batch_size) 225 | valid_data = valid_data.map(load_batch_tf) 226 | valid_data = valid_data.prefetch(1) 227 | 228 | pretrain_files = [f for i, f in enumerate(train_files) if i % 10 == 0] 229 | pretrain_data = tf.data.Dataset.from_tensor_slices(pretrain_files) 230 | pretrain_data = pretrain_data.batch(pretrain_batch_size) 231 | pretrain_data = pretrain_data.map(load_batch_tf) 232 | pretrain_data = pretrain_data.prefetch(1) 233 | 234 | ### MODEL LOADING ### 235 | sys.path.insert(0, os.path.abspath(f'models/{args.model}')) 236 | import model 237 | importlib.reload(model) 238 | model = model.GCNPolicy() 239 | del sys.path[0] 240 | 241 | ### TRAINING LOOP ### 242 | optimizer = tf.train.AdamOptimizer(learning_rate=lambda: lr) # dynamic LR trick 243 | best_loss = np.inf 244 | for epoch in range(max_epochs + 1): 245 | log(f"EPOCH {epoch}...", logfile) 246 | epoch_loss_avg = tfe.metrics.Mean() 247 | epoch_accuracy = tfe.metrics.Accuracy() 248 | 249 | # TRAIN 250 | if epoch == 0: 251 | n = pretrain(model=model, dataloader=pretrain_data) 252 | log(f"PRETRAINED {n} LAYERS", logfile) 253 | # model compilation 254 | model.call = tfe.defun(model.call, input_signature=model.input_signature) 255 | else: 256 | # bugfix: tensorflow's shuffle() seems broken... 257 | epoch_train_files = rng.choice(train_files, epoch_size * batch_size, replace=True) 258 | train_data = tf.data.Dataset.from_tensor_slices(epoch_train_files) 259 | train_data = train_data.batch(batch_size) 260 | train_data = train_data.map(load_batch_tf) 261 | train_data = train_data.prefetch(1) 262 | train_loss, train_kacc = process(model, train_data, top_k, optimizer) 263 | log(f"TRAIN LOSS: {train_loss:0.3f} " + "".join([f" acc@{k}: {acc:0.3f}" for k, acc in zip(top_k, train_kacc)]), logfile) 264 | 265 | # TEST 266 | valid_loss, valid_kacc = process(model, valid_data, top_k, None) 267 | log(f"VALID LOSS: {valid_loss:0.3f} " + "".join([f" acc@{k}: {acc:0.3f}" for k, acc in zip(top_k, valid_kacc)]), logfile) 268 | 269 | if valid_loss < best_loss: 270 | plateau_count = 0 271 | best_loss = valid_loss 272 | model.save_state(os.path.join(running_dir, 'best_params.pkl')) 273 | log(f" best model so far", logfile) 274 | else: 275 | plateau_count += 1 276 | if plateau_count % early_stopping == 0: 277 | log(f" {plateau_count} epochs without improvement, early stopping", logfile) 278 | break 279 | if plateau_count % patience == 0: 280 | lr *= 0.2 281 | log(f" {plateau_count} epochs without improvement, decreasing learning rate to {lr}", logfile) 282 | 283 | model.restore_state(os.path.join(running_dir, 'best_params.pkl')) 284 | valid_loss, valid_kacc = process(model, valid_data, top_k, None) 285 | log(f"BEST VALID LOSS: {valid_loss:0.3f} " + "".join([f" acc@{k}: {acc:0.3f}" for k, acc in zip(top_k, valid_kacc)]), logfile) 286 | 287 | -------------------------------------------------------------------------------- /04_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import importlib 4 | import argparse 5 | import csv 6 | import numpy as np 7 | import time 8 | import pickle 9 | import pathlib 10 | import gzip 11 | 12 | import tensorflow as tf 13 | import tensorflow.contrib.eager as tfe 14 | 15 | import svmrank 16 | 17 | import utilities 18 | 19 | from utilities_tf import load_batch_gcnn 20 | 21 | 22 | def load_batch_flat(sample_files, feats_type, augment_feats, normalize_feats): 23 | cand_features = [] 24 | cand_choices = [] 25 | cand_scoress = [] 26 | 27 | for i, filename in enumerate(sample_files): 28 | cand_states, cand_scores, cand_choice = utilities.load_flat_samples(filename, feats_type, 'scores', augment_feats, normalize_feats) 29 | 30 | cand_features.append(cand_states) 31 | cand_choices.append(cand_choice) 32 | cand_scoress.append(cand_scores) 33 | 34 | n_cands_per_sample = [v.shape[0] for v in cand_features] 35 | 36 | cand_features = np.concatenate(cand_features, axis=0).astype(np.float32, copy=False) 37 | cand_choices = np.asarray(cand_choices).astype(np.int32, copy=False) 38 | cand_scoress = np.concatenate(cand_scoress, axis=0).astype(np.float32, copy=False) 39 | n_cands_per_sample = np.asarray(n_cands_per_sample).astype(np.int32, copy=False) 40 | 41 | return cand_features, n_cands_per_sample, cand_choices, cand_scoress 42 | 43 | 44 | def padding(output, n_vars_per_sample, fill=-1e8): 45 | n_vars_max = tf.reduce_max(n_vars_per_sample) 46 | 47 | output = tf.split( 48 | value=output, 49 | num_or_size_splits=n_vars_per_sample, 50 | axis=1, 51 | ) 52 | output = tf.concat([ 53 | tf.pad( 54 | x, 55 | paddings=[[0, 0], [0, n_vars_max - tf.shape(x)[1]]], 56 | mode='CONSTANT', 57 | constant_values=fill) 58 | for x in output 59 | ], axis=0) 60 | 61 | return output 62 | 63 | 64 | def process(policy, dataloader, top_k): 65 | mean_kacc = np.zeros(len(top_k)) 66 | 67 | n_samples_processed = 0 68 | for batch in dataloader: 69 | 70 | if policy['type'] == 'gcnn': 71 | c, ei, ev, v, n_cs, n_vs, n_cands, cands, best_cands, cand_scores = batch 72 | 73 | pred_scores = policy['model']((c, ei, ev, v, tf.reduce_sum(n_cs, keepdims=True), tf.reduce_sum(n_vs, keepdims=True)), tf.convert_to_tensor(False)) 74 | 75 | # filter candidate variables 76 | pred_scores = tf.expand_dims(tf.gather(tf.squeeze(pred_scores, 0), cands), 0) 77 | 78 | elif policy['type'] == 'ml-competitor': 79 | cand_feats, n_cands, best_cands, cand_scores = batch 80 | 81 | # move to numpy 82 | cand_feats = cand_feats.numpy() 83 | n_cands = n_cands.numpy() 84 | 85 | # feature normalization 86 | cand_feats = (cand_feats - policy['feat_shift']) / policy['feat_scale'] 87 | 88 | pred_scores = policy['model'].predict(cand_feats) 89 | 90 | # move back to TF 91 | pred_scores = tf.convert_to_tensor(pred_scores.reshape((1, -1)), dtype=tf.float32) 92 | 93 | # padding 94 | pred_scores = padding(pred_scores, n_cands) 95 | true_scores = padding(tf.reshape(cand_scores, (1, -1)), n_cands) 96 | true_bestscore = tf.reduce_max(true_scores, axis=-1, keepdims=True) 97 | 98 | assert all(true_bestscore.numpy() == np.take_along_axis(true_scores.numpy(), best_cands.numpy().reshape((-1, 1)), axis=1)) 99 | 100 | kacc = [] 101 | for k in top_k: 102 | pred_top_k = tf.nn.top_k(pred_scores, k=k)[1].numpy() 103 | pred_top_k_true_scores = np.take_along_axis(true_scores.numpy(), pred_top_k, axis=1) 104 | kacc.append(np.mean(np.any(pred_top_k_true_scores == true_bestscore.numpy(), axis=1))) 105 | kacc = np.asarray(kacc) 106 | 107 | batch_size = int(n_cands.shape[0]) 108 | mean_kacc += kacc * batch_size 109 | n_samples_processed += batch_size 110 | 111 | mean_kacc /= n_samples_processed 112 | 113 | return mean_kacc 114 | 115 | 116 | if __name__ == '__main__': 117 | parser = argparse.ArgumentParser() 118 | parser.add_argument( 119 | 'problem', 120 | help='MILP instance type to process.', 121 | choices=['setcover', 'cauctions', 'facilities', 'indset'], 122 | ) 123 | parser.add_argument( 124 | '-g', '--gpu', 125 | help='CUDA GPU id (-1 for CPU).', 126 | type=int, 127 | default=0, 128 | ) 129 | args = parser.parse_args() 130 | 131 | print(f"problem: {args.problem}") 132 | print(f"gpu: {args.gpu}") 133 | 134 | os.makedirs("results", exist_ok=True) 135 | result_file = f"results/{args.problem}_validation_{time.strftime('%Y%m%d-%H%M%S')}.csv" 136 | seeds = [0, 1, 2, 3, 4] 137 | gcnn_models = ['baseline'] 138 | other_models = ['extratrees_gcnn_agg', 'lambdamart_khalil', 'svmrank_khalil'] 139 | test_batch_size = 128 140 | top_k = [1, 3, 5, 10] 141 | 142 | problem_folders = { 143 | 'setcover': 'setcover/500r_1000c_0.05d', 144 | 'cauctions': 'cauctions/100_500', 145 | 'facilities': 'facilities/100_100_5', 146 | 'indset': 'indset/500_4', 147 | } 148 | problem_folder = problem_folders[args.problem] 149 | 150 | if args.problem == 'setcover': 151 | gcnn_models += ['mean_convolution', 'no_prenorm'] 152 | 153 | result_file = f"results/{args.problem}_test_{time.strftime('%Y%m%d-%H%M%S')}" 154 | 155 | result_file = result_file + '.csv' 156 | os.makedirs('results', exist_ok=True) 157 | 158 | ### TENSORFLOW SETUP ### 159 | if args.gpu == -1: 160 | os.environ['CUDA_VISIBLE_DEVICES'] = '' 161 | else: 162 | os.environ['CUDA_VISIBLE_DEVICES'] = f'{args.gpu}' 163 | config = tf.ConfigProto() 164 | config.gpu_options.allow_growth = True 165 | tf.enable_eager_execution(config) 166 | tf.executing_eagerly() 167 | 168 | test_files = list(pathlib.Path(f"data/samples/{problem_folder}/test").glob('sample_*.pkl')) 169 | test_files = [str(x) for x in test_files] 170 | 171 | print(f"{len(test_files)} test samples") 172 | 173 | evaluated_policies = [['gcnn', model] for model in gcnn_models] + \ 174 | [['ml-competitor', model] for model in other_models] 175 | 176 | fieldnames = [ 177 | 'policy', 178 | 'seed', 179 | ] + [ 180 | f'acc@{k}' for k in top_k 181 | ] 182 | with open(result_file, 'w', newline='') as csvfile: 183 | writer = csv.DictWriter(csvfile, fieldnames=fieldnames) 184 | writer.writeheader() 185 | for policy_type, policy_name in evaluated_policies: 186 | print(f"{policy_type}:{policy_name}...") 187 | for seed in seeds: 188 | rng = np.random.RandomState(seed) 189 | tf.set_random_seed(rng.randint(np.iinfo(int).max)) 190 | 191 | policy = {} 192 | policy['name'] = policy_name 193 | policy['type'] = policy_type 194 | 195 | if policy['type'] == 'gcnn': 196 | # load model 197 | sys.path.insert(0, os.path.abspath(f"models/{policy['name']}")) 198 | import model 199 | importlib.reload(model) 200 | del sys.path[0] 201 | policy['model'] = model.GCNPolicy() 202 | policy['model'].restore_state(f"trained_models/{args.problem}/{policy['name']}/{seed}/best_params.pkl") 203 | policy['model'].call = tfe.defun(policy['model'].call, input_signature=policy['model'].input_signature) 204 | policy['batch_datatypes'] = [tf.float32, tf.int32, tf.float32, 205 | tf.float32, tf.int32, tf.int32, tf.int32, tf.int32, tf.int32, tf.float32] 206 | policy['batch_fun'] = load_batch_gcnn 207 | else: 208 | # load feature normalization parameters 209 | try: 210 | with open(f"trained_models/{args.problem}/{policy['name']}/{seed}/normalization.pkl", 'rb') as f: 211 | policy['feat_shift'], policy['feat_scale'] = pickle.load(f) 212 | except: 213 | policy['feat_shift'], policy['feat_scale'] = 0, 1 214 | 215 | # load model 216 | if policy_name.startswith('svmrank'): 217 | policy['model'] = svmrank.Model().read(f"trained_models/{args.problem}/{policy['name']}/{seed}/model.txt") 218 | else: 219 | with open(f"trained_models/{args.problem}/{policy['name']}/{seed}/model.pkl", 'rb') as f: 220 | policy['model'] = pickle.load(f) 221 | 222 | # load feature specifications 223 | with open(f"trained_models/{args.problem}/{policy['name']}/{seed}/feat_specs.pkl", 'rb') as f: 224 | feat_specs = pickle.load(f) 225 | 226 | policy['batch_datatypes'] = [tf.float32, tf.int32, tf.int32, tf.float32] 227 | policy['batch_fun'] = lambda x: load_batch_flat(x, feat_specs['type'], feat_specs['augment'], feat_specs['qbnorm']) 228 | 229 | test_data = tf.data.Dataset.from_tensor_slices(test_files) 230 | test_data = test_data.batch(test_batch_size) 231 | test_data = test_data.map(lambda x: tf.py_func( 232 | policy['batch_fun'], [x], policy['batch_datatypes'])) 233 | test_data = test_data.prefetch(2) 234 | 235 | test_kacc = process(policy, test_data, top_k) 236 | print(f" {seed} " + " ".join([f"acc@{k}: {100*acc:4.1f}" for k, acc in zip(top_k, test_kacc)])) 237 | 238 | writer.writerow({ 239 | **{ 240 | 'policy': f"{policy['type']}:{policy['name']}", 241 | 'seed': seed, 242 | }, 243 | **{ 244 | f'acc@{k}': test_kacc[i] for i, k in enumerate(top_k) 245 | }, 246 | }) 247 | csvfile.flush() 248 | -------------------------------------------------------------------------------- /05_evaluate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import importlib 4 | import argparse 5 | import csv 6 | import numpy as np 7 | import time 8 | import pickle 9 | 10 | import pyscipopt as scip 11 | 12 | import tensorflow as tf 13 | import tensorflow.contrib.eager as tfe 14 | 15 | import svmrank 16 | 17 | import utilities 18 | 19 | 20 | class PolicyBranching(scip.Branchrule): 21 | 22 | def __init__(self, policy): 23 | super().__init__() 24 | 25 | self.policy_type = policy['type'] 26 | self.policy_name = policy['name'] 27 | 28 | if self.policy_type == 'gcnn': 29 | model = policy['model'] 30 | model.restore_state(policy['parameters']) 31 | self.policy = tfe.defun(model.call, input_signature=model.input_signature) 32 | 33 | elif self.policy_type == 'internal': 34 | self.policy = policy['name'] 35 | 36 | elif self.policy_type == 'ml-competitor': 37 | self.policy = policy['model'] 38 | 39 | # feature parameterization 40 | self.feat_shift = policy['feat_shift'] 41 | self.feat_scale = policy['feat_scale'] 42 | self.feat_specs = policy['feat_specs'] 43 | 44 | else: 45 | raise NotImplementedError 46 | 47 | def branchinitsol(self): 48 | self.ndomchgs = 0 49 | self.ncutoffs = 0 50 | self.state_buffer = {} 51 | self.khalil_root_buffer = {} 52 | 53 | def branchexeclp(self, allowaddcons): 54 | 55 | # SCIP internal branching rule 56 | if self.policy_type == 'internal': 57 | result = self.model.executeBranchRule(self.policy, allowaddcons) 58 | 59 | # custom policy branching 60 | else: 61 | candidate_vars, *_ = self.model.getPseudoBranchCands() 62 | candidate_mask = [var.getCol().getLPPos() for var in candidate_vars] 63 | 64 | # initialize root buffer for Khalil features extraction 65 | if self.model.getNNodes() == 1 \ 66 | and self.policy_type == 'ml-competitor' \ 67 | and self.feat_specs['type'] in ('khalil', 'all'): 68 | utilities.extract_khalil_variable_features(self.model, [], self.khalil_root_buffer) 69 | 70 | if len(candidate_vars) == 1: 71 | best_var = candidate_vars[0] 72 | 73 | elif self.policy_type == 'gcnn': 74 | state = utilities.extract_state(self.model, self.state_buffer) 75 | 76 | # convert state to tensors 77 | c, e, v = state 78 | state = ( 79 | tf.convert_to_tensor(c['values'], dtype=tf.float32), 80 | tf.convert_to_tensor(e['indices'], dtype=tf.int32), 81 | tf.convert_to_tensor(e['values'], dtype=tf.float32), 82 | tf.convert_to_tensor(v['values'], dtype=tf.float32), 83 | tf.convert_to_tensor([c['values'].shape[0]], dtype=tf.int32), 84 | tf.convert_to_tensor([v['values'].shape[0]], dtype=tf.int32), 85 | ) 86 | 87 | var_logits = self.policy(state, tf.convert_to_tensor(False)).numpy().squeeze(0) 88 | 89 | candidate_scores = var_logits[candidate_mask] 90 | best_var = candidate_vars[candidate_scores.argmax()] 91 | 92 | elif self.policy_type == 'ml-competitor': 93 | 94 | # build candidate features 95 | candidate_states = [] 96 | if self.feat_specs['type'] in ('all', 'gcnn_agg'): 97 | state = utilities.extract_state(self.model, self.state_buffer) 98 | candidate_states.append(utilities.compute_extended_variable_features(state, candidate_mask)) 99 | if self.feat_specs['type'] in ('all', 'khalil'): 100 | candidate_states.append(utilities.extract_khalil_variable_features(self.model, candidate_vars, self.khalil_root_buffer)) 101 | candidate_states = np.concatenate(candidate_states, axis=1) 102 | 103 | # feature preprocessing 104 | candidate_states = utilities.preprocess_variable_features(candidate_states, self.feat_specs['augment'], self.feat_specs['qbnorm']) 105 | 106 | # feature normalization 107 | candidate_states = (candidate_states - self.feat_shift) / self.feat_scale 108 | 109 | candidate_scores = self.policy.predict(candidate_states) 110 | best_var = candidate_vars[candidate_scores.argmax()] 111 | 112 | else: 113 | raise NotImplementedError 114 | 115 | self.model.branchVar(best_var) 116 | result = scip.SCIP_RESULT.BRANCHED 117 | 118 | # fair node counting 119 | if result == scip.SCIP_RESULT.REDUCEDDOM: 120 | self.ndomchgs += 1 121 | elif result == scip.SCIP_RESULT.CUTOFF: 122 | self.ncutoffs += 1 123 | 124 | return {'result': result} 125 | 126 | 127 | if __name__ == '__main__': 128 | parser = argparse.ArgumentParser() 129 | parser.add_argument( 130 | 'problem', 131 | help='MILP instance type to process.', 132 | choices=['setcover', 'cauctions', 'facilities', 'indset'], 133 | ) 134 | parser.add_argument( 135 | '-g', '--gpu', 136 | help='CUDA GPU id (-1 for CPU).', 137 | type=int, 138 | default=0, 139 | ) 140 | args = parser.parse_args() 141 | 142 | result_file = f"{args.problem}_{time.strftime('%Y%m%d-%H%M%S')}.csv" 143 | instances = [] 144 | seeds = [0, 1, 2, 3, 4] 145 | gcnn_models = ['baseline'] 146 | other_models = ['extratrees_gcnn_agg', 'lambdamart_khalil', 'svmrank_khalil'] 147 | internal_branchers = ['relpscost'] 148 | time_limit = 3600 149 | 150 | if args.problem == 'setcover': 151 | instances += [{'type': 'small', 'path': f"data/instances/setcover/transfer_500r_1000c_0.05d/instance_{i+1}.lp"} for i in range(20)] 152 | instances += [{'type': 'medium', 'path': f"data/instances/setcover/transfer_1000r_1000c_0.05d/instance_{i+1}.lp"} for i in range(20)] 153 | instances += [{'type': 'big', 'path': f"data/instances/setcover/transfer_2000r_1000c_0.05d/instance_{i+1}.lp"} for i in range(20)] 154 | gcnn_models += ['mean_convolution', 'no_prenorm'] 155 | 156 | elif args.problem == 'cauctions': 157 | instances += [{'type': 'small', 'path': f"data/instances/cauctions/transfer_100_500/instance_{i+1}.lp"} for i in range(20)] 158 | instances += [{'type': 'medium', 'path': f"data/instances/cauctions/transfer_200_1000/instance_{i+1}.lp"} for i in range(20)] 159 | instances += [{'type': 'big', 'path': f"data/instances/cauctions/transfer_300_1500/instance_{i+1}.lp"} for i in range(20)] 160 | 161 | elif args.problem == 'facilities': 162 | instances += [{'type': 'small', 'path': f"data/instances/facilities/transfer_100_100_5/instance_{i+1}.lp"} for i in range(20)] 163 | instances += [{'type': 'medium', 'path': f"data/instances/facilities/transfer_200_100_5/instance_{i+1}.lp"} for i in range(20)] 164 | instances += [{'type': 'big', 'path': f"data/instances/facilities/transfer_400_100_5/instance_{i+1}.lp"} for i in range(20)] 165 | 166 | elif args.problem == 'indset': 167 | instances += [{'type': 'small', 'path': f"data/instances/indset/transfer_500_4/instance_{i+1}.lp"} for i in range(20)] 168 | instances += [{'type': 'medium', 'path': f"data/instances/indset/transfer_1000_4/instance_{i+1}.lp"} for i in range(20)] 169 | instances += [{'type': 'big', 'path': f"data/instances/indset/transfer_1500_4/instance_{i+1}.lp"} for i in range(20)] 170 | 171 | else: 172 | raise NotImplementedError 173 | 174 | branching_policies = [] 175 | 176 | # SCIP internal brancher baselines 177 | for brancher in internal_branchers: 178 | for seed in seeds: 179 | branching_policies.append({ 180 | 'type': 'internal', 181 | 'name': brancher, 182 | 'seed': seed, 183 | }) 184 | # ML baselines 185 | for model in other_models: 186 | for seed in seeds: 187 | branching_policies.append({ 188 | 'type': 'ml-competitor', 189 | 'name': model, 190 | 'seed': seed, 191 | 'model': f'trained_models/{args.problem}/{model}/{seed}', 192 | }) 193 | # GCNN models 194 | for model in gcnn_models: 195 | for seed in seeds: 196 | branching_policies.append({ 197 | 'type': 'gcnn', 198 | 'name': model, 199 | 'seed': seed, 200 | 'parameters': f'trained_models/{args.problem}/{model}/{seed}/best_params.pkl' 201 | }) 202 | 203 | print(f"problem: {args.problem}") 204 | print(f"gpu: {args.gpu}") 205 | print(f"time limit: {time_limit} s") 206 | 207 | ### TENSORFLOW SETUP ### 208 | if args.gpu == -1: 209 | os.environ['CUDA_VISIBLE_DEVICES'] = '' 210 | else: 211 | os.environ['CUDA_VISIBLE_DEVICES'] = f'{args.gpu}' 212 | config = tf.ConfigProto() 213 | config.gpu_options.allow_growth = True 214 | tf.enable_eager_execution(config) 215 | tf.executing_eagerly() 216 | 217 | # load and assign tensorflow models to policies (share models and update parameters) 218 | loaded_models = {} 219 | for policy in branching_policies: 220 | if policy['type'] == 'gcnn': 221 | if policy['name'] not in loaded_models: 222 | sys.path.insert(0, os.path.abspath(f"models/{policy['name']}")) 223 | import model 224 | importlib.reload(model) 225 | loaded_models[policy['name']] = model.GCNPolicy() 226 | del sys.path[0] 227 | policy['model'] = loaded_models[policy['name']] 228 | 229 | # load ml-competitor models 230 | for policy in branching_policies: 231 | if policy['type'] == 'ml-competitor': 232 | try: 233 | with open(f"{policy['model']}/normalization.pkl", 'rb') as f: 234 | policy['feat_shift'], policy['feat_scale'] = pickle.load(f) 235 | except: 236 | policy['feat_shift'], policy['feat_scale'] = 0, 1 237 | 238 | with open(f"{policy['model']}/feat_specs.pkl", 'rb') as f: 239 | policy['feat_specs'] = pickle.load(f) 240 | 241 | if policy['name'].startswith('svmrank'): 242 | policy['model'] = svmrank.Model().read(f"{policy['model']}/model.txt") 243 | else: 244 | with open(f"{policy['model']}/model.pkl", 'rb') as f: 245 | policy['model'] = pickle.load(f) 246 | 247 | print("running SCIP...") 248 | 249 | fieldnames = [ 250 | 'policy', 251 | 'seed', 252 | 'type', 253 | 'instance', 254 | 'nnodes', 255 | 'nlps', 256 | 'stime', 257 | 'gap', 258 | 'status', 259 | 'ndomchgs', 260 | 'ncutoffs', 261 | 'walltime', 262 | 'proctime', 263 | ] 264 | os.makedirs('results', exist_ok=True) 265 | with open(f"results/{result_file}", 'w', newline='') as csvfile: 266 | writer = csv.DictWriter(csvfile, fieldnames=fieldnames) 267 | writer.writeheader() 268 | 269 | for instance in instances: 270 | print(f"{instance['type']}: {instance['path']}...") 271 | 272 | for policy in branching_policies: 273 | tf.set_random_seed(policy['seed']) 274 | 275 | m = scip.Model() 276 | m.setIntParam('display/verblevel', 0) 277 | m.readProblem(f"{instance['path']}") 278 | utilities.init_scip_params(m, seed=policy['seed']) 279 | m.setIntParam('timing/clocktype', 1) # 1: CPU user seconds, 2: wall clock time 280 | m.setRealParam('limits/time', time_limit) 281 | 282 | brancher = PolicyBranching(policy) 283 | m.includeBranchrule( 284 | branchrule=brancher, 285 | name=f"{policy['type']}:{policy['name']}", 286 | desc=f"Custom PySCIPOpt branching policy.", 287 | priority=666666, maxdepth=-1, maxbounddist=1) 288 | 289 | walltime = time.perf_counter() 290 | proctime = time.process_time() 291 | 292 | m.optimize() 293 | 294 | walltime = time.perf_counter() - walltime 295 | proctime = time.process_time() - proctime 296 | 297 | stime = m.getSolvingTime() 298 | nnodes = m.getNNodes() 299 | nlps = m.getNLPs() 300 | gap = m.getGap() 301 | status = m.getStatus() 302 | ndomchgs = brancher.ndomchgs 303 | ncutoffs = brancher.ncutoffs 304 | 305 | writer.writerow({ 306 | 'policy': f"{policy['type']}:{policy['name']}", 307 | 'seed': policy['seed'], 308 | 'type': instance['type'], 309 | 'instance': instance['path'], 310 | 'nnodes': nnodes, 311 | 'nlps': nlps, 312 | 'stime': stime, 313 | 'gap': gap, 314 | 'status': status, 315 | 'ndomchgs': ndomchgs, 316 | 'ncutoffs': ncutoffs, 317 | 'walltime': walltime, 318 | 'proctime': proctime, 319 | }) 320 | 321 | csvfile.flush() 322 | m.freeProb() 323 | 324 | print(f" {policy['type']}:{policy['name']} {policy['seed']} - {nnodes} ({nnodes+2*(ndomchgs+ncutoffs)}) nodes {nlps} lps {stime:.2f} ({walltime:.2f} wall {proctime:.2f} proc) s. {status}") 325 | 326 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # SCIP solver 2 | 3 | Set-up a desired installation path for SCIP / SoPlex (e.g., `/opt/scip`): 4 | ``` 5 | export SCIPOPTDIR='/opt/scip' 6 | ``` 7 | 8 | ## SoPlex 9 | 10 | SoPlex 4.0.1 (free for academic uses) 11 | 12 | https://soplex.zib.de/download.php?fname=soplex-4.0.1.tgz 13 | 14 | ``` 15 | tar -xzf soplex-4.0.1.tgz 16 | cd soplex-4.0.1/ 17 | mkdir build 18 | cmake -S . -B build -DCMAKE_INSTALL_PREFIX=$SCIPOPTDIR 19 | make -C ./build -j 4 20 | make -C ./build install 21 | cd .. 22 | ``` 23 | 24 | # SCIP 25 | 26 | SCIP 6.0.1 (free for academic uses) 27 | 28 | https://scip.zib.de/download.php?fname=scip-6.0.1.tgz 29 | 30 | ``` 31 | tar -xzf scip-6.0.1.tgz 32 | cd scip-6.0.1/ 33 | ``` 34 | 35 | Apply patch file in `learn2branch/scip_patch/` 36 | 37 | ``` 38 | patch -p1 < ../learn2branch/scip_patch/vanillafullstrong.patch 39 | ``` 40 | 41 | ``` 42 | mkdir build 43 | cmake -S . -B build -DSOPLEX_DIR=$SCIPOPTDIR -DCMAKE_INSTALL_PREFIX=$SCIPOPTDIR 44 | make -C ./build -j 4 45 | make -C ./build install 46 | cd .. 47 | ``` 48 | 49 | For reference, original installation instructions [here](http://scip.zib.de/doc/html/CMAKE.php). 50 | 51 | # Python dependencies 52 | 53 | Recommended setup: conda + python 3 54 | 55 | https://docs.conda.io/en/latest/miniconda.html 56 | 57 | ## Cython 58 | 59 | Required to compile PySCIPOpt and PySVMRank 60 | ``` 61 | conda install cython 62 | ``` 63 | 64 | ## PySCIPOpt 65 | 66 | SCIP's python interface (modified version) 67 | 68 | ``` 69 | pip install git+https://github.com/ds4dm/PySCIPOpt.git@ml-branching 70 | ``` 71 | 72 | ## ExtraTrees 73 | ``` 74 | conda install scikit-learn=0.20.2 # ExtraTrees 75 | ``` 76 | 77 | ## LambdaMART 78 | ``` 79 | pip install git+https://github.com/jma127/pyltr@78fa0ebfef67d6594b8415aa5c6136e30a5e3395 # LambdaMART 80 | ``` 81 | 82 | ## SVMrank 83 | ``` 84 | git clone https://github.com/ds4dm/PySVMRank.git 85 | cd PySVMRank 86 | wget http://download.joachims.org/svm_rank/current/svm_rank.tar.gz # get SVMrank original source code 87 | mkdir src/c 88 | tar -xzf svm_rank.tar.gz -C src/c 89 | pip install . 90 | ``` 91 | 92 | ## Tensorflow 93 | ``` 94 | conda install tensorflow-gpu=1.12.0 95 | ``` 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Data Science for Real-Time Decision Making - Canada Excellence Research Chair 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | __Update__: check out library [Ecole](https://doc.ecole.ai), which reimplements everything you'll need for learning to branch, in a nice and clean Python package ([paper here](https://arxiv.org/abs/2011.06069)). 2 | 3 | # Exact Combinatorial Optimization with Graph Convolutional Neural Networks 4 | 5 | Maxime Gasse, Didier Chételat, Nicola Ferroni, Laurent Charlin, Andrea Lodi 6 | 7 | This is the official implementation of our NeurIPS 2019 [paper](https://arxiv.org/abs/1906.01629). 8 | 9 | ## Installation 10 | 11 | See installation instructions [here](INSTALL.md). 12 | 13 | ## Running the experiments 14 | 15 | ### Set Covering 16 | ``` 17 | # Generate MILP instances 18 | python 01_generate_instances.py setcover 19 | # Generate supervised learning datasets 20 | python 02_generate_samples.py setcover -j 4 # number of available CPUs 21 | # Training 22 | for i in {0..4} 23 | do 24 | python 03_train_gcnn.py setcover -m baseline -s $i 25 | python 03_train_gcnn.py setcover -m mean_convolution -s $i 26 | python 03_train_gcnn.py setcover -m no_prenorm -s $i 27 | python 03_train_competitor.py setcover -m extratrees -s $i 28 | python 03_train_competitor.py setcover -m svmrank -s $i 29 | python 03_train_competitor.py setcover -m lambdamart -s $i 30 | done 31 | # Test 32 | python 04_test.py setcover 33 | # Evaluation 34 | python 05_evaluate.py setcover 35 | ``` 36 | 37 | ### Combinatorial Auction 38 | ``` 39 | # Generate MILP instances 40 | python 01_generate_instances.py cauctions 41 | # Generate supervised learning datasets 42 | python 02_generate_samples.py cauctions -j 4 # number of available CPUs 43 | # Training 44 | for i in {0..4} 45 | do 46 | python 03_train_gcnn.py cauctions -m baseline -s $i 47 | python 03_train_competitor.py cauctions -m extratrees -s $i 48 | python 03_train_competitor.py cauctions -m svmrank -s $i 49 | python 03_train_competitor.py cauctions -m lambdamart -s $i 50 | done 51 | # Test 52 | python 04_test.py cauctions 53 | # Evaluation 54 | python 05_evaluate.py cauctions 55 | ``` 56 | 57 | ### Capacitated Facility Location 58 | ``` 59 | # Generate MILP instances 60 | python 01_generate_instances.py facilities 61 | # Generate supervised learning datasets 62 | python 02_generate_samples.py facilities -j 4 # number of available CPUs 63 | # Training 64 | for i in {0..4} 65 | do 66 | python 03_train_gcnn.py facilities -m baseline -s $i 67 | python 03_train_competitor.py facilities -m extratrees -s $i 68 | python 03_train_competitor.py facilities -m svmrank -s $i 69 | python 03_train_competitor.py facilities -m lambdamart -s $i 70 | done 71 | # Test 72 | python 04_test.py facilities 73 | # Evaluation 74 | python 05_evaluate.py facilities 75 | ``` 76 | 77 | ### Maximum Independent Set 78 | ``` 79 | # Generate MILP instances 80 | python 01_generate_instances.py indset 81 | # Generate supervised learning datasets 82 | python 02_generate_samples.py indset -j 4 # number of available CPUs 83 | # Training 84 | for i in {0..4} 85 | do 86 | python 03_train_gcnn.py indset -m baseline -s $i 87 | python 03_train_competitor.py indset -m extratrees -s $i 88 | python 03_train_competitor.py indset -m svmrank -s $i 89 | python 03_train_competitor.py indset -m lambdamart -s $i 90 | done 91 | # Test 92 | python 04_test.py indset 93 | # Evaluation 94 | python 05_evaluate.py indset 95 | ``` 96 | 97 | ## Citation 98 | Please cite our paper if you use this code in your work. 99 | ``` 100 | @inproceedings{conf/nips/GasseCFCL19, 101 | title={Exact Combinatorial Optimization with Graph Convolutional Neural Networks}, 102 | author={Gasse, Maxime and Chételat, Didier and Ferroni, Nicola and Charlin, Laurent and Lodi, Andrea}, 103 | booktitle={Advances in Neural Information Processing Systems 32}, 104 | year={2019} 105 | } 106 | ``` 107 | 108 | ## Questions / Bugs 109 | Please feel free to submit a Github issue if you have any questions or find any bugs. We do not guarantee any support, but will do our best if we can help. 110 | 111 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ds4dm/learn2branch/57e82603fba39c34ff81baf9bd0b5768f5a1a860/__init__.py -------------------------------------------------------------------------------- /models/baseline/model.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | import tensorflow.keras as K 3 | import numpy as np 4 | import pickle 5 | 6 | 7 | class PreNormException(Exception): 8 | pass 9 | 10 | class PreNormLayer(K.layers.Layer): 11 | """ 12 | Our pre-normalization layer, whose purpose is to normalize an input layer 13 | to zero mean and unit variance to speed-up and stabilize GCN training. The 14 | layer's parameters are aimed to be computed during the pre-training phase. 15 | """ 16 | 17 | def __init__(self, n_units, shift=True, scale=True): 18 | super().__init__() 19 | assert shift or scale 20 | 21 | if shift: 22 | self.shift = self.add_weight( 23 | name=f'{self.name}/shift', 24 | shape=(n_units,), 25 | trainable=False, 26 | initializer=tf.keras.initializers.constant(value=np.zeros((n_units,)), 27 | dtype=tf.float32), 28 | ) 29 | else: 30 | self.shift = None 31 | 32 | if scale: 33 | self.scale = self.add_weight( 34 | name=f'{self.name}/scale', 35 | shape=(n_units,), 36 | trainable=False, 37 | initializer=tf.keras.initializers.constant(value=np.ones((n_units,)), 38 | dtype=tf.float32), 39 | ) 40 | else: 41 | self.scale = None 42 | 43 | self.n_units = n_units 44 | self.waiting_updates = False 45 | self.received_updates = False 46 | 47 | def build(self, input_shapes): 48 | self.built = True 49 | 50 | def call(self, input): 51 | if self.waiting_updates: 52 | self.update_stats(input) 53 | self.received_updates = True 54 | raise PreNormException 55 | 56 | if self.shift is not None: 57 | input = input + self.shift 58 | 59 | if self.scale is not None: 60 | input = input * self.scale 61 | 62 | return input 63 | 64 | def start_updates(self): 65 | """ 66 | Initializes the pre-training phase. 67 | """ 68 | self.avg = 0 69 | self.var = 0 70 | self.m2 = 0 71 | self.count = 0 72 | self.waiting_updates = True 73 | self.received_updates = False 74 | 75 | def update_stats(self, input): 76 | """ 77 | Online mean and variance estimation. See: Chan et al. (1979) Updating 78 | Formulae and a Pairwise Algorithm for Computing Sample Variances. 79 | https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm 80 | """ 81 | assert self.n_units == 1 or input.shape[-1] == self.n_units, f"Expected input dimension of size {self.n_units}, got {input.shape[-1]}." 82 | 83 | input = tf.reshape(input, [-1, self.n_units]) 84 | sample_avg = tf.reduce_mean(input, 0) 85 | sample_var = tf.reduce_mean((input - sample_avg) ** 2, axis=0) 86 | sample_count = tf.cast(tf.size(input=input) / self.n_units, tf.float32) 87 | 88 | delta = sample_avg - self.avg 89 | 90 | self.m2 = self.var * self.count + sample_var * sample_count + delta ** 2 * self.count * sample_count / ( 91 | self.count + sample_count) 92 | 93 | self.count += sample_count 94 | self.avg += delta * sample_count / self.count 95 | self.var = self.m2 / self.count if self.count > 0 else 1 96 | 97 | def stop_updates(self): 98 | """ 99 | Ends pre-training for that layer, and fixes the layers's parameters. 100 | """ 101 | assert self.count > 0 102 | if self.shift is not None: 103 | self.shift.assign(-self.avg) 104 | 105 | if self.scale is not None: 106 | self.var = tf.where(tf.equal(self.var, 0), tf.ones_like(self.var), self.var) # NaN check trick 107 | self.scale.assign(1 / np.sqrt(self.var)) 108 | 109 | del self.avg, self.var, self.m2, self.count 110 | self.waiting_updates = False 111 | self.trainable = False 112 | 113 | 114 | class BipartiteGraphConvolution(K.Model): 115 | """ 116 | Partial bipartite graph convolution (either left-to-right or right-to-left). 117 | """ 118 | 119 | def __init__(self, emb_size, activation, initializer, right_to_left=False): 120 | super().__init__() 121 | self.emb_size = emb_size 122 | self.activation = activation 123 | self.initializer = initializer 124 | self.right_to_left = right_to_left 125 | 126 | # feature layers 127 | self.feature_module_left = K.Sequential([ 128 | K.layers.Dense(units=self.emb_size, activation=None, use_bias=True, kernel_initializer=self.initializer) 129 | ]) 130 | self.feature_module_edge = K.Sequential([ 131 | K.layers.Dense(units=self.emb_size, activation=None, use_bias=False, kernel_initializer=self.initializer) 132 | ]) 133 | self.feature_module_right = K.Sequential([ 134 | K.layers.Dense(units=self.emb_size, activation=None, use_bias=False, kernel_initializer=self.initializer) 135 | ]) 136 | self.feature_module_final = K.Sequential([ 137 | PreNormLayer(1, shift=False), # normalize after summation trick 138 | K.layers.Activation(self.activation), 139 | K.layers.Dense(units=self.emb_size, activation=None, kernel_initializer=self.initializer) 140 | ]) 141 | 142 | self.post_conv_module = K.Sequential([ 143 | PreNormLayer(1, shift=False), # normalize after convolution 144 | ]) 145 | 146 | # output_layers 147 | self.output_module = K.Sequential([ 148 | K.layers.Dense(units=self.emb_size, activation=None, kernel_initializer=self.initializer), 149 | K.layers.Activation(self.activation), 150 | K.layers.Dense(units=self.emb_size, activation=None, kernel_initializer=self.initializer), 151 | ]) 152 | 153 | def build(self, input_shapes): 154 | l_shape, ei_shape, ev_shape, r_shape = input_shapes 155 | 156 | self.feature_module_left.build(l_shape) 157 | self.feature_module_edge.build(ev_shape) 158 | self.feature_module_right.build(r_shape) 159 | self.feature_module_final.build([None, self.emb_size]) 160 | self.post_conv_module.build([None, self.emb_size]) 161 | self.output_module.build([None, self.emb_size + (l_shape[1] if self.right_to_left else r_shape[1])]) 162 | self.built = True 163 | 164 | def call(self, inputs, training): 165 | """ 166 | Perfoms a partial graph convolution on the given bipartite graph. 167 | 168 | Inputs 169 | ------ 170 | left_features: 2D float tensor 171 | Features of the left-hand-side nodes in the bipartite graph 172 | edge_indices: 2D int tensor 173 | Edge indices in left-right order 174 | edge_features: 2D float tensor 175 | Features of the edges 176 | right_features: 2D float tensor 177 | Features of the right-hand-side nodes in the bipartite graph 178 | scatter_out_size: 1D int tensor 179 | Output size (left_features.shape[0] or right_features.shape[0], unknown at compile time) 180 | 181 | Other parameters 182 | ---------------- 183 | training: boolean 184 | Training mode indicator 185 | """ 186 | left_features, edge_indices, edge_features, right_features, scatter_out_size = inputs 187 | 188 | if self.right_to_left: 189 | scatter_dim = 0 190 | prev_features = left_features 191 | else: 192 | scatter_dim = 1 193 | prev_features = right_features 194 | 195 | # compute joint features 196 | joint_features = self.feature_module_final( 197 | tf.gather( 198 | self.feature_module_left(left_features), 199 | axis=0, 200 | indices=edge_indices[0] 201 | ) + 202 | self.feature_module_edge(edge_features) + 203 | tf.gather( 204 | self.feature_module_right(right_features), 205 | axis=0, 206 | indices=edge_indices[1]) 207 | ) 208 | 209 | # perform convolution 210 | conv_output = tf.scatter_nd( 211 | updates=joint_features, 212 | indices=tf.expand_dims(edge_indices[scatter_dim], axis=1), 213 | shape=[scatter_out_size, self.emb_size] 214 | ) 215 | conv_output = self.post_conv_module(conv_output) 216 | 217 | # apply final module 218 | output = self.output_module(tf.concat([ 219 | conv_output, 220 | prev_features, 221 | ], axis=1)) 222 | 223 | return output 224 | 225 | 226 | class BaseModel(K.Model): 227 | """ 228 | Our base model class, which implements basic save/restore and pre-training 229 | methods. 230 | """ 231 | 232 | def pre_train_init(self): 233 | self.pre_train_init_rec(self, self.name) 234 | 235 | @staticmethod 236 | def pre_train_init_rec(model, name): 237 | for layer in model.layers: 238 | if isinstance(layer, K.Model): 239 | BaseModel.pre_train_init_rec(layer, f"{name}/{layer.name}") 240 | elif isinstance(layer, PreNormLayer): 241 | layer.start_updates() 242 | 243 | def pre_train_next(self): 244 | return self.pre_train_next_rec(self, self.name) 245 | 246 | @staticmethod 247 | def pre_train_next_rec(model, name): 248 | for layer in model.layers: 249 | if isinstance(layer, K.Model): 250 | result = BaseModel.pre_train_next_rec(layer, f"{name}/{layer.name}") 251 | if result is not None: 252 | return result 253 | elif isinstance(layer, PreNormLayer) and layer.waiting_updates and layer.received_updates: 254 | layer.stop_updates() 255 | return layer, f"{name}/{layer.name}" 256 | return None 257 | 258 | def pre_train(self, *args, **kwargs): 259 | try: 260 | self.call(*args, **kwargs) 261 | return False 262 | except PreNormException: 263 | return True 264 | 265 | def save_state(self, path): 266 | with open(path, 'wb') as f: 267 | for v_name in self.variables_topological_order: 268 | v = [v for v in self.variables if v.name == v_name][0] 269 | pickle.dump(v.numpy(), f) 270 | 271 | def restore_state(self, path): 272 | with open(path, 'rb') as f: 273 | for v_name in self.variables_topological_order: 274 | v = [v for v in self.variables if v.name == v_name][0] 275 | v.assign(pickle.load(f)) 276 | 277 | 278 | class GCNPolicy(BaseModel): 279 | """ 280 | Our bipartite Graph Convolutional neural Network (GCN) model. 281 | """ 282 | 283 | def __init__(self): 284 | super().__init__() 285 | 286 | self.emb_size = 64 287 | self.cons_nfeats = 5 288 | self.edge_nfeats = 1 289 | self.var_nfeats = 19 290 | 291 | self.activation = K.activations.relu 292 | self.initializer = K.initializers.Orthogonal() 293 | 294 | # CONSTRAINT EMBEDDING 295 | self.cons_embedding = K.Sequential([ 296 | PreNormLayer(n_units=self.cons_nfeats), 297 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 298 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 299 | ]) 300 | 301 | # EDGE EMBEDDING 302 | self.edge_embedding = K.Sequential([ 303 | PreNormLayer(self.edge_nfeats), 304 | ]) 305 | 306 | # VARIABLE EMBEDDING 307 | self.var_embedding = K.Sequential([ 308 | PreNormLayer(n_units=self.var_nfeats), 309 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 310 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 311 | ]) 312 | 313 | # GRAPH CONVOLUTIONS 314 | self.conv_v_to_c = BipartiteGraphConvolution(self.emb_size, self.activation, self.initializer, right_to_left=True) 315 | self.conv_c_to_v = BipartiteGraphConvolution(self.emb_size, self.activation, self.initializer) 316 | 317 | # OUTPUT 318 | self.output_module = K.Sequential([ 319 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 320 | K.layers.Dense(units=1, activation=None, kernel_initializer=self.initializer, use_bias=False), 321 | ]) 322 | 323 | # build model right-away 324 | self.build([ 325 | (None, self.cons_nfeats), 326 | (2, None), 327 | (None, self.edge_nfeats), 328 | (None, self.var_nfeats), 329 | (None, ), 330 | (None, ), 331 | ]) 332 | 333 | # save / restore fix 334 | self.variables_topological_order = [v.name for v in self.variables] 335 | 336 | # save input signature for compilation 337 | self.input_signature = [ 338 | ( 339 | tf.contrib.eager.TensorSpec(shape=[None, self.cons_nfeats], dtype=tf.float32), 340 | tf.contrib.eager.TensorSpec(shape=[2, None], dtype=tf.int32), 341 | tf.contrib.eager.TensorSpec(shape=[None, self.edge_nfeats], dtype=tf.float32), 342 | tf.contrib.eager.TensorSpec(shape=[None, self.var_nfeats], dtype=tf.float32), 343 | tf.contrib.eager.TensorSpec(shape=[None], dtype=tf.int32), 344 | tf.contrib.eager.TensorSpec(shape=[None], dtype=tf.int32), 345 | ), 346 | tf.contrib.eager.TensorSpec(shape=[], dtype=tf.bool), 347 | ] 348 | 349 | def build(self, input_shapes): 350 | c_shape, ei_shape, ev_shape, v_shape, nc_shape, nv_shape = input_shapes 351 | emb_shape = [None, self.emb_size] 352 | 353 | if not self.built: 354 | self.cons_embedding.build(c_shape) 355 | self.edge_embedding.build(ev_shape) 356 | self.var_embedding.build(v_shape) 357 | self.conv_v_to_c.build((emb_shape, ei_shape, ev_shape, emb_shape)) 358 | self.conv_c_to_v.build((emb_shape, ei_shape, ev_shape, emb_shape)) 359 | self.output_module.build(emb_shape) 360 | self.built = True 361 | 362 | @staticmethod 363 | def pad_output(output, n_vars_per_sample, pad_value=-1e8): 364 | n_vars_max = tf.reduce_max(n_vars_per_sample) 365 | 366 | output = tf.split( 367 | value=output, 368 | num_or_size_splits=n_vars_per_sample, 369 | axis=1, 370 | ) 371 | output = tf.concat([ 372 | tf.pad( 373 | x, 374 | paddings=[[0, 0], [0, n_vars_max - tf.shape(x)[1]]], 375 | mode='CONSTANT', 376 | constant_values=pad_value) 377 | for x in output 378 | ], axis=0) 379 | 380 | return output 381 | 382 | def call(self, inputs, training): 383 | """ 384 | Accepts stacked mini-batches, i.e. several bipartite graphs aggregated 385 | as one. In that case the number of variables per samples has to be 386 | provided, and the output consists in a padded dense tensor. 387 | 388 | Parameters 389 | ---------- 390 | inputs: list of tensors 391 | Model input as a bipartite graph. May be batched into a stacked graph. 392 | 393 | Inputs 394 | ------ 395 | constraint_features: 2D float tensor 396 | Constraint node features (n_constraints x n_constraint_features) 397 | edge_indices: 2D int tensor 398 | Edge constraint and variable indices (2, n_edges) 399 | edge_features: 2D float tensor 400 | Edge features (n_edges, n_edge_features) 401 | variable_features: 2D float tensor 402 | Variable node features (n_variables, n_variable_features) 403 | n_cons_per_sample: 1D int tensor 404 | Number of constraints for each of the samples stacked in the batch. 405 | n_vars_per_sample: 1D int tensor 406 | Number of variables for each of the samples stacked in the batch. 407 | 408 | Other parameters 409 | ---------------- 410 | training: boolean 411 | Training mode indicator 412 | """ 413 | constraint_features, edge_indices, edge_features, variable_features, n_cons_per_sample, n_vars_per_sample = inputs 414 | n_cons_total = tf.reduce_sum(n_cons_per_sample) 415 | n_vars_total = tf.reduce_sum(n_vars_per_sample) 416 | 417 | # EMBEDDINGS 418 | constraint_features = self.cons_embedding(constraint_features) 419 | edge_features = self.edge_embedding(edge_features) 420 | variable_features = self.var_embedding(variable_features) 421 | 422 | # GRAPH CONVOLUTIONS 423 | constraint_features = self.conv_v_to_c(( 424 | constraint_features, edge_indices, edge_features, variable_features, n_cons_total), training) 425 | constraint_features = self.activation(constraint_features) 426 | 427 | variable_features = self.conv_c_to_v(( 428 | constraint_features, edge_indices, edge_features, variable_features, n_vars_total), training) 429 | variable_features = self.activation(variable_features) 430 | 431 | # OUTPUT 432 | output = self.output_module(variable_features) 433 | output = tf.reshape(output, [1, -1]) 434 | 435 | if n_vars_per_sample.shape[0] > 1: 436 | output = self.pad_output(output, n_vars_per_sample) 437 | 438 | return output 439 | 440 | 441 | -------------------------------------------------------------------------------- /models/mean_convolution/model.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | import tensorflow.keras as K 3 | import numpy as np 4 | import pickle 5 | 6 | 7 | class PreNormException(Exception): 8 | pass 9 | 10 | class PreNormLayer(K.layers.Layer): 11 | """ 12 | Our pre-normalization layer, whose purpose is to normalize an input layer 13 | to zero mean and unit variance to speed-up and stabilize GCN training. The 14 | layer's parameters are aimed to be computed during the pre-training phase. 15 | """ 16 | 17 | def __init__(self, n_units, shift=True, scale=True): 18 | super().__init__() 19 | assert shift or scale 20 | 21 | if shift: 22 | self.shift = self.add_weight( 23 | name=f'{self.name}/shift', 24 | shape=(n_units,), 25 | trainable=False, 26 | initializer=tf.keras.initializers.constant(value=np.zeros((n_units,)), 27 | dtype=tf.float32), 28 | ) 29 | else: 30 | self.shift = None 31 | 32 | if scale: 33 | self.scale = self.add_weight( 34 | name=f'{self.name}/scale', 35 | shape=(n_units,), 36 | trainable=False, 37 | initializer=tf.keras.initializers.constant(value=np.ones((n_units,)), 38 | dtype=tf.float32), 39 | ) 40 | else: 41 | self.scale = None 42 | 43 | self.n_units = n_units 44 | self.waiting_updates = False 45 | self.received_updates = False 46 | 47 | def build(self, input_shapes): 48 | self.built = True 49 | 50 | def call(self, input): 51 | if self.waiting_updates: 52 | self.update_stats(input) 53 | self.received_updates = True 54 | raise PreNormException 55 | 56 | if self.shift is not None: 57 | input = input + self.shift 58 | 59 | if self.scale is not None: 60 | input = input * self.scale 61 | 62 | return input 63 | 64 | def start_updates(self): 65 | """ 66 | Initializes the pre-training phase. 67 | """ 68 | self.avg = 0 69 | self.var = 0 70 | self.m2 = 0 71 | self.count = 0 72 | self.waiting_updates = True 73 | self.received_updates = False 74 | 75 | def update_stats(self, input): 76 | """ 77 | Online mean and variance estimation. See: Chan et al. (1979) Updating 78 | Formulae and a Pairwise Algorithm for Computing Sample Variances. 79 | https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm 80 | """ 81 | assert self.n_units == 1 or input.shape[-1] == self.n_units, f"Expected input dimension of size {self.n_units}, got {input.shape[-1]}." 82 | 83 | input = tf.reshape(input, [-1, self.n_units]) 84 | sample_avg = tf.reduce_mean(input, 0) 85 | sample_var = tf.reduce_mean((input - sample_avg) ** 2, axis=0) 86 | sample_count = tf.cast(tf.size(input=input) / self.n_units, tf.float32) 87 | 88 | delta = sample_avg - self.avg 89 | 90 | self.m2 = self.var * self.count + sample_var * sample_count + delta ** 2 * self.count * sample_count / ( 91 | self.count + sample_count) 92 | 93 | self.count += sample_count 94 | self.avg += delta * sample_count / self.count 95 | self.var = self.m2 / self.count if self.count > 0 else 1 96 | 97 | def stop_updates(self): 98 | """ 99 | Ends pre-training for that layer, and fixes the layers's parameters. 100 | """ 101 | assert self.count > 0 102 | if self.shift is not None: 103 | self.shift.assign(-self.avg) 104 | 105 | if self.scale is not None: 106 | self.var = tf.where(tf.equal(self.var, 0), tf.ones_like(self.var), self.var) # NaN check trick 107 | self.scale.assign(1 / np.sqrt(self.var)) 108 | 109 | del self.avg, self.var, self.m2, self.count 110 | self.waiting_updates = False 111 | self.trainable = False 112 | 113 | 114 | class BipartiteGraphConvolution(K.Model): 115 | """ 116 | Partial bipartite graph convolution (either left-to-right or right-to-left). 117 | """ 118 | 119 | def __init__(self, emb_size, activation, initializer, right_to_left=False): 120 | super().__init__() 121 | self.emb_size = emb_size 122 | self.activation = activation 123 | self.initializer = initializer 124 | self.right_to_left = right_to_left 125 | 126 | # feature layers 127 | self.feature_module_left = K.Sequential([ 128 | K.layers.Dense(units=self.emb_size, activation=None, use_bias=True, kernel_initializer=self.initializer) 129 | ]) 130 | self.feature_module_edge = K.Sequential([ 131 | K.layers.Dense(units=self.emb_size, activation=None, use_bias=False, kernel_initializer=self.initializer) 132 | ]) 133 | self.feature_module_right = K.Sequential([ 134 | K.layers.Dense(units=self.emb_size, activation=None, use_bias=False, kernel_initializer=self.initializer) 135 | ]) 136 | self.feature_module_final = K.Sequential([ 137 | PreNormLayer(1, shift=False), # normalize after summation trick 138 | K.layers.Activation(self.activation), 139 | K.layers.Dense(units=self.emb_size, activation=None, kernel_initializer=self.initializer) 140 | ]) 141 | 142 | # output_layers 143 | self.output_module = K.Sequential([ 144 | K.layers.Dense(units=self.emb_size, activation=None, kernel_initializer=self.initializer), 145 | K.layers.Activation(self.activation), 146 | K.layers.Dense(units=self.emb_size, activation=None, kernel_initializer=self.initializer), 147 | ]) 148 | 149 | def build(self, input_shapes): 150 | l_shape, ei_shape, ev_shape, r_shape = input_shapes 151 | 152 | self.feature_module_left.build(l_shape) 153 | self.feature_module_edge.build(ev_shape) 154 | self.feature_module_right.build(r_shape) 155 | self.feature_module_final.build([None, self.emb_size]) 156 | self.output_module.build([None, self.emb_size + (l_shape[1] if self.right_to_left else r_shape[1])]) 157 | self.built = True 158 | 159 | def call(self, inputs): 160 | """ 161 | Perfoms a partial graph convolution on the given bipartite graph. 162 | 163 | Inputs 164 | ------ 165 | left_features: 2D float tensor 166 | Features of the left-hand-side nodes in the bipartite graph 167 | edge_indices: 2D int tensor 168 | Edge indices in left-right order 169 | edge_features: 2D float tensor 170 | Features of the edges 171 | right_features: 2D float tensor 172 | Features of the right-hand-side nodes in the bipartite graph 173 | scatter_out_size: 1D int tensor 174 | Output size (left_features.shape[0] or right_features.shape[0], unknown at compile time) 175 | """ 176 | left_features, edge_indices, edge_features, right_features, scatter_out_size = inputs 177 | 178 | if self.right_to_left: 179 | scatter_dim = 0 180 | prev_features = left_features 181 | else: 182 | scatter_dim = 1 183 | prev_features = right_features 184 | 185 | # compute joint features 186 | joint_features = self.feature_module_final( 187 | tf.gather( 188 | self.feature_module_left(left_features), 189 | axis=0, 190 | indices=edge_indices[0] 191 | ) + 192 | self.feature_module_edge(edge_features) + 193 | tf.gather( 194 | self.feature_module_right(right_features), 195 | axis=0, 196 | indices=edge_indices[1]) 197 | ) 198 | 199 | # perform convolution 200 | conv_output = tf.scatter_nd( 201 | updates=joint_features, 202 | indices=tf.expand_dims(edge_indices[scatter_dim], axis=1), 203 | shape=[scatter_out_size, self.emb_size] 204 | ) 205 | 206 | # mean convolution 207 | neighbour_count = tf.scatter_nd( 208 | updates=tf.ones(shape=[tf.shape(edge_indices)[1], 1], dtype=tf.float32), 209 | indices=tf.expand_dims(edge_indices[scatter_dim], axis=1), 210 | shape=[scatter_out_size, 1]) 211 | neighbour_count = tf.where( 212 | tf.equal(neighbour_count, 0), 213 | tf.ones_like(neighbour_count), 214 | neighbour_count) # NaN safety trick 215 | conv_output = conv_output / neighbour_count 216 | 217 | # apply final module 218 | output = self.output_module(tf.concat([ 219 | conv_output, 220 | prev_features, 221 | ], axis=1)) 222 | 223 | return output 224 | 225 | 226 | class BaseModel(K.Model): 227 | """ 228 | Our base model class, which implements basic save/restore and pre-training 229 | methods. 230 | """ 231 | 232 | def pre_train_init(self): 233 | self.pre_train_init_rec(self, self.name) 234 | 235 | @staticmethod 236 | def pre_train_init_rec(model, name): 237 | for layer in model.layers: 238 | if isinstance(layer, K.Model): 239 | BaseModel.pre_train_init_rec(layer, f"{name}/{layer.name}") 240 | elif isinstance(layer, PreNormLayer): 241 | layer.start_updates() 242 | 243 | def pre_train_next(self): 244 | return self.pre_train_next_rec(self, self.name) 245 | 246 | @staticmethod 247 | def pre_train_next_rec(model, name): 248 | for layer in model.layers: 249 | if isinstance(layer, K.Model): 250 | result = BaseModel.pre_train_next_rec(layer, f"{name}/{layer.name}") 251 | if result is not None: 252 | return result 253 | elif isinstance(layer, PreNormLayer) and layer.waiting_updates and layer.received_updates: 254 | layer.stop_updates() 255 | return layer, f"{name}/{layer.name}" 256 | return None 257 | 258 | def pre_train(self, *args, **kwargs): 259 | try: 260 | self.call(*args, **kwargs) 261 | return False 262 | except PreNormException: 263 | return True 264 | 265 | def save_state(self, path): 266 | with open(path, 'wb') as f: 267 | for v_name in self.variables_topological_order: 268 | v = [v for v in self.variables if v.name == v_name][0] 269 | pickle.dump(v.numpy(), f) 270 | 271 | def restore_state(self, path): 272 | with open(path, 'rb') as f: 273 | for v_name in self.variables_topological_order: 274 | v = [v for v in self.variables if v.name == v_name][0] 275 | v.assign(pickle.load(f)) 276 | 277 | 278 | class GCNPolicy(BaseModel): 279 | """ 280 | Our bipartite Graph Convolutional neural Network (GCN) model. 281 | """ 282 | 283 | def __init__(self): 284 | super().__init__() 285 | 286 | self.emb_size = 64 287 | self.cons_nfeats = 5 288 | self.edge_nfeats = 1 289 | self.var_nfeats = 19 290 | 291 | self.activation = K.activations.relu 292 | self.initializer = K.initializers.Orthogonal() 293 | 294 | # CONSTRAINT EMBEDDING 295 | self.cons_embedding = K.Sequential([ 296 | PreNormLayer(n_units=self.cons_nfeats), 297 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 298 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 299 | ]) 300 | 301 | # EDGE EMBEDDING 302 | self.edge_embedding = K.Sequential([ 303 | PreNormLayer(self.edge_nfeats), 304 | ]) 305 | 306 | # VARIABLE EMBEDDING 307 | self.var_embedding = K.Sequential([ 308 | PreNormLayer(n_units=self.var_nfeats), 309 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 310 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 311 | ]) 312 | 313 | # GRAPH CONVOLUTIONS 314 | self.conv_v_to_c = BipartiteGraphConvolution(self.emb_size, self.activation, self.initializer, right_to_left=True) 315 | self.conv_c_to_v = BipartiteGraphConvolution(self.emb_size, self.activation, self.initializer) 316 | 317 | # OUTPUT 318 | self.output_module = K.Sequential([ 319 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 320 | K.layers.Dense(units=1, activation=None, kernel_initializer=self.initializer, use_bias=False), 321 | ]) 322 | 323 | # build model right-away 324 | self.build([ 325 | (None, self.cons_nfeats), 326 | (2, None), 327 | (None, self.edge_nfeats), 328 | (None, self.var_nfeats), 329 | (None, ), 330 | (None, ), 331 | ]) 332 | 333 | # save / restore fix 334 | self.variables_topological_order = [v.name for v in self.variables] 335 | 336 | # save input signature for compilation 337 | self.input_signature = [ 338 | ( 339 | tf.contrib.eager.TensorSpec(shape=[None, self.cons_nfeats], dtype=tf.float32), 340 | tf.contrib.eager.TensorSpec(shape=[2, None], dtype=tf.int32), 341 | tf.contrib.eager.TensorSpec(shape=[None, self.edge_nfeats], dtype=tf.float32), 342 | tf.contrib.eager.TensorSpec(shape=[None, self.var_nfeats], dtype=tf.float32), 343 | tf.contrib.eager.TensorSpec(shape=[None], dtype=tf.int32), 344 | tf.contrib.eager.TensorSpec(shape=[None], dtype=tf.int32), 345 | ), 346 | tf.contrib.eager.TensorSpec(shape=[], dtype=tf.bool), 347 | ] 348 | 349 | def build(self, input_shapes): 350 | c_shape, ei_shape, ev_shape, v_shape, nc_shape, nv_shape = input_shapes 351 | emb_shape = [None, self.emb_size] 352 | 353 | if not self.built: 354 | self.cons_embedding.build(c_shape) 355 | self.edge_embedding.build(ev_shape) 356 | self.var_embedding.build(v_shape) 357 | self.conv_v_to_c.build((emb_shape, ei_shape, ev_shape, emb_shape)) 358 | self.conv_c_to_v.build((emb_shape, ei_shape, ev_shape, emb_shape)) 359 | self.output_module.build(emb_shape) 360 | self.built = True 361 | 362 | @staticmethod 363 | def pad_output(output, n_vars_per_sample, pad_value=-1e8): 364 | n_vars_max = tf.reduce_max(n_vars_per_sample) 365 | 366 | output = tf.split( 367 | value=output, 368 | num_or_size_splits=n_vars_per_sample, 369 | axis=1, 370 | ) 371 | output = tf.concat([ 372 | tf.pad( 373 | x, 374 | paddings=[[0, 0], [0, n_vars_max - tf.shape(x)[1]]], 375 | mode='CONSTANT', 376 | constant_values=pad_value) 377 | for x in output 378 | ], axis=0) 379 | 380 | return output 381 | 382 | def call(self, inputs, training): 383 | """ 384 | Accepts stacked mini-batches, i.e. several bipartite graphs aggregated 385 | as one. In that case the number of variables per samples has to be 386 | provided, and the output consists in a padded dense tensor. 387 | 388 | Parameters 389 | ---------- 390 | inputs: list of tensors 391 | Model input as a bipartite graph. May be batched into a stacked graph. 392 | n_cons_per_sample: list of ints 393 | Number of constraints for each of the samples stacked in the batch. 394 | n_vars_per_sample: list of ints 395 | Number of variables for each of the samples stacked in the batch. 396 | 397 | Inputs 398 | ------ 399 | constraint_features: 2D float tensor 400 | Constraint node features (n_constraints x n_constraint_features) 401 | edge_indices: 2D int tensor 402 | Edge constraint and variable indices (2, n_edges) 403 | edge_features: 2D float tensor 404 | Edge features (n_edges, n_edge_features) 405 | variable_features: 2D float tensor 406 | Variable node features (n_variables, n_variable_features) 407 | n_cons_per_sample: 1D int tensor 408 | Number of constraints for each of the samples stacked in the batch. 409 | n_vars_per_sample: 1D int tensor 410 | Number of variables for each of the samples stacked in the batch. 411 | 412 | Other parameters 413 | ---------------- 414 | training: boolean 415 | Training mode indicator 416 | """ 417 | constraint_features, edge_indices, edge_features, variable_features, n_cons_per_sample, n_vars_per_sample = inputs 418 | n_cons_total = tf.reduce_sum(n_cons_per_sample) 419 | n_vars_total = tf.reduce_sum(n_vars_per_sample) 420 | 421 | # EMBEDDINGS 422 | constraint_features = self.cons_embedding(constraint_features) 423 | edge_features = self.edge_embedding(edge_features) 424 | variable_features = self.var_embedding(variable_features) 425 | 426 | # GRAPH CONVOLUTIONS 427 | constraint_features = self.conv_v_to_c(( 428 | constraint_features, edge_indices, edge_features, variable_features, n_cons_total)) 429 | constraint_features = self.activation(constraint_features) 430 | 431 | variable_features = self.conv_c_to_v(( 432 | constraint_features, edge_indices, edge_features, variable_features, n_vars_total)) 433 | variable_features = self.activation(variable_features) 434 | 435 | # OUTPUT 436 | output = self.output_module(variable_features) 437 | output = tf.reshape(output, [1, -1]) 438 | 439 | if n_vars_per_sample.shape[0] > 1: 440 | output = self.pad_output(output, n_vars_per_sample) 441 | 442 | return output 443 | 444 | 445 | -------------------------------------------------------------------------------- /models/no_prenorm/model.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | import tensorflow.keras as K 3 | import numpy as np 4 | import pickle 5 | 6 | 7 | class PreNormException(Exception): 8 | pass 9 | 10 | class PreNormLayer(K.layers.Layer): 11 | """ 12 | Our pre-normalization layer, whose purpose is to normalize an input layer 13 | to zero mean and unit variance to speed-up and stabilize GCN training. The 14 | layer's parameters are aimed to be computed during the pre-training phase. 15 | """ 16 | 17 | def __init__(self, n_units, shift=True, scale=True): 18 | super().__init__() 19 | assert shift or scale 20 | 21 | if shift: 22 | self.shift = self.add_weight( 23 | name=f'{self.name}/shift', 24 | shape=(n_units,), 25 | trainable=False, 26 | initializer=tf.keras.initializers.constant(value=np.zeros((n_units,)), 27 | dtype=tf.float32), 28 | ) 29 | else: 30 | self.shift = None 31 | 32 | if scale: 33 | self.scale = self.add_weight( 34 | name=f'{self.name}/scale', 35 | shape=(n_units,), 36 | trainable=False, 37 | initializer=tf.keras.initializers.constant(value=np.ones((n_units,)), 38 | dtype=tf.float32), 39 | ) 40 | else: 41 | self.scale = None 42 | 43 | self.n_units = n_units 44 | self.waiting_updates = False 45 | self.received_updates = False 46 | 47 | def build(self, input_shapes): 48 | self.built = True 49 | 50 | def call(self, input): 51 | if self.waiting_updates: 52 | self.update_stats(input) 53 | self.received_updates = True 54 | raise PreNormException 55 | 56 | if self.shift is not None: 57 | input = input + self.shift 58 | 59 | if self.scale is not None: 60 | input = input * self.scale 61 | 62 | return input 63 | 64 | def start_updates(self): 65 | """ 66 | Initializes the pre-training phase. 67 | """ 68 | self.avg = 0 69 | self.var = 0 70 | self.m2 = 0 71 | self.count = 0 72 | self.waiting_updates = True 73 | self.received_updates = False 74 | 75 | def update_stats(self, input): 76 | """ 77 | Online mean and variance estimation. See: Chan et al. (1979) Updating 78 | Formulae and a Pairwise Algorithm for Computing Sample Variances. 79 | https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm 80 | """ 81 | assert self.n_units == 1 or input.shape[-1] == self.n_units, f"Expected input dimension of size {self.n_units}, got {input.shape[-1]}." 82 | 83 | input = tf.reshape(input, [-1, self.n_units]) 84 | sample_avg = tf.reduce_mean(input, 0) 85 | sample_var = tf.reduce_mean((input - sample_avg) ** 2, axis=0) 86 | sample_count = tf.cast(tf.size(input=input) / self.n_units, tf.float32) 87 | 88 | delta = sample_avg - self.avg 89 | 90 | self.m2 = self.var * self.count + sample_var * sample_count + delta ** 2 * self.count * sample_count / ( 91 | self.count + sample_count) 92 | 93 | self.count += sample_count 94 | self.avg += delta * sample_count / self.count 95 | self.var = self.m2 / self.count if self.count > 0 else 1 96 | 97 | def stop_updates(self): 98 | """ 99 | Ends pre-training for that layer, and fixes the layers's parameters. 100 | """ 101 | assert self.count > 0 102 | if self.shift is not None: 103 | self.shift.assign(-self.avg) 104 | 105 | if self.scale is not None: 106 | self.var = tf.where(tf.equal(self.var, 0), tf.ones_like(self.var), self.var) # NaN check trick 107 | self.scale.assign(1 / np.sqrt(self.var)) 108 | 109 | del self.avg, self.var, self.m2, self.count 110 | self.waiting_updates = False 111 | self.trainable = False 112 | 113 | 114 | class BipartiteGraphConvolution(K.Model): 115 | """ 116 | Partial bipartite graph convolution (either left-to-right or right-to-left). 117 | """ 118 | 119 | def __init__(self, emb_size, activation, initializer, right_to_left=False): 120 | super().__init__() 121 | self.emb_size = emb_size 122 | self.activation = activation 123 | self.initializer = initializer 124 | self.right_to_left = right_to_left 125 | 126 | # feature layers 127 | self.feature_module_left = K.Sequential([ 128 | K.layers.Dense(units=self.emb_size, activation=None, use_bias=True, kernel_initializer=self.initializer) 129 | ]) 130 | self.feature_module_edge = K.Sequential([ 131 | K.layers.Dense(units=self.emb_size, activation=None, use_bias=False, kernel_initializer=self.initializer) 132 | ]) 133 | self.feature_module_right = K.Sequential([ 134 | K.layers.Dense(units=self.emb_size, activation=None, use_bias=False, kernel_initializer=self.initializer) 135 | ]) 136 | self.feature_module_final = K.Sequential([ 137 | PreNormLayer(1, shift=False), # normalize after summation trick 138 | K.layers.Activation(self.activation), 139 | K.layers.Dense(units=self.emb_size, activation=None, kernel_initializer=self.initializer) 140 | ]) 141 | 142 | # output_layers 143 | self.output_module = K.Sequential([ 144 | K.layers.Dense(units=self.emb_size, activation=None, kernel_initializer=self.initializer), 145 | K.layers.Activation(self.activation), 146 | K.layers.Dense(units=self.emb_size, activation=None, kernel_initializer=self.initializer), 147 | ]) 148 | 149 | def build(self, input_shapes): 150 | l_shape, ei_shape, ev_shape, r_shape = input_shapes 151 | 152 | self.feature_module_left.build(l_shape) 153 | self.feature_module_edge.build(ev_shape) 154 | self.feature_module_right.build(r_shape) 155 | self.feature_module_final.build([None, self.emb_size]) 156 | self.output_module.build([None, self.emb_size + (l_shape[1] if self.right_to_left else r_shape[1])]) 157 | self.built = True 158 | 159 | def call(self, inputs): 160 | """ 161 | Perfoms a partial graph convolution on the given bipartite graph. 162 | 163 | Inputs 164 | ------ 165 | left_features: 2D float tensor 166 | Features of the left-hand-side nodes in the bipartite graph 167 | edge_indices: 2D int tensor 168 | Edge indices in left-right order 169 | edge_features: 2D float tensor 170 | Features of the edges 171 | right_features: 2D float tensor 172 | Features of the right-hand-side nodes in the bipartite graph 173 | scatter_out_size: 1D int tensor 174 | Output size (left_features.shape[0] or right_features.shape[0], unknown at compile time) 175 | """ 176 | left_features, edge_indices, edge_features, right_features, scatter_out_size = inputs 177 | 178 | if self.right_to_left: 179 | scatter_dim = 0 180 | prev_features = left_features 181 | else: 182 | scatter_dim = 1 183 | prev_features = right_features 184 | 185 | # compute joint features 186 | joint_features = self.feature_module_final( 187 | tf.gather( 188 | self.feature_module_left(left_features), 189 | axis=0, 190 | indices=edge_indices[0] 191 | ) + 192 | self.feature_module_edge(edge_features) + 193 | tf.gather( 194 | self.feature_module_right(right_features), 195 | axis=0, 196 | indices=edge_indices[1]) 197 | ) 198 | 199 | # perform convolution 200 | conv_output = tf.scatter_nd( 201 | updates=joint_features, 202 | indices=tf.expand_dims(edge_indices[scatter_dim], axis=1), 203 | shape=[scatter_out_size, self.emb_size] 204 | ) 205 | 206 | # apply final module 207 | output = self.output_module(tf.concat([ 208 | conv_output, 209 | prev_features, 210 | ], axis=1)) 211 | 212 | return output 213 | 214 | 215 | class BaseModel(K.Model): 216 | """ 217 | Our base model class, which implements basic save/restore and pre-training 218 | methods. 219 | """ 220 | 221 | def pre_train_init(self): 222 | self.pre_train_init_rec(self, self.name) 223 | 224 | @staticmethod 225 | def pre_train_init_rec(model, name): 226 | for layer in model.layers: 227 | if isinstance(layer, K.Model): 228 | BaseModel.pre_train_init_rec(layer, f"{name}/{layer.name}") 229 | elif isinstance(layer, PreNormLayer): 230 | layer.start_updates() 231 | 232 | def pre_train_next(self): 233 | return self.pre_train_next_rec(self, self.name) 234 | 235 | @staticmethod 236 | def pre_train_next_rec(model, name): 237 | for layer in model.layers: 238 | if isinstance(layer, K.Model): 239 | result = BaseModel.pre_train_next_rec(layer, f"{name}/{layer.name}") 240 | if result is not None: 241 | return result 242 | elif isinstance(layer, PreNormLayer) and layer.waiting_updates and layer.received_updates: 243 | layer.stop_updates() 244 | return layer, f"{name}/{layer.name}" 245 | return None 246 | 247 | def pre_train(self, *args, **kwargs): 248 | try: 249 | self.call(*args, **kwargs) 250 | return False 251 | except PreNormException: 252 | return True 253 | 254 | def save_state(self, path): 255 | with open(path, 'wb') as f: 256 | for v_name in self.variables_topological_order: 257 | v = [v for v in self.variables if v.name == v_name][0] 258 | pickle.dump(v.numpy(), f) 259 | 260 | def restore_state(self, path): 261 | with open(path, 'rb') as f: 262 | for v_name in self.variables_topological_order: 263 | v = [v for v in self.variables if v.name == v_name][0] 264 | v.assign(pickle.load(f)) 265 | 266 | 267 | class GCNPolicy(BaseModel): 268 | """ 269 | Our bipartite Graph Convolutional neural Network (GCN) model. 270 | """ 271 | 272 | def __init__(self): 273 | super().__init__() 274 | 275 | self.emb_size = 64 276 | self.cons_nfeats = 5 277 | self.edge_nfeats = 1 278 | self.var_nfeats = 19 279 | 280 | self.activation = K.activations.relu 281 | self.initializer = K.initializers.Orthogonal() 282 | 283 | # CONSTRAINT EMBEDDING 284 | self.cons_embedding = K.Sequential([ 285 | PreNormLayer(n_units=self.cons_nfeats), 286 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 287 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 288 | ]) 289 | 290 | # EDGE EMBEDDING 291 | self.edge_embedding = K.Sequential([ 292 | PreNormLayer(self.edge_nfeats), 293 | ]) 294 | 295 | # VARIABLE EMBEDDING 296 | self.var_embedding = K.Sequential([ 297 | PreNormLayer(n_units=self.var_nfeats), 298 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 299 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 300 | ]) 301 | 302 | # GRAPH CONVOLUTIONS 303 | self.conv_v_to_c = BipartiteGraphConvolution(self.emb_size, self.activation, self.initializer, right_to_left=True) 304 | self.conv_c_to_v = BipartiteGraphConvolution(self.emb_size, self.activation, self.initializer) 305 | 306 | # OUTPUT 307 | self.output_module = K.Sequential([ 308 | K.layers.Dense(units=self.emb_size, activation=self.activation, kernel_initializer=self.initializer), 309 | K.layers.Dense(units=1, activation=None, kernel_initializer=self.initializer, use_bias=False), 310 | ]) 311 | 312 | # build model right-away 313 | 314 | self.build([ 315 | (None, self.cons_nfeats), 316 | (2, None), 317 | (None, self.edge_nfeats), 318 | (None, self.var_nfeats), 319 | (None, ), 320 | (None, ), 321 | ]) 322 | 323 | # save / restore fix 324 | self.variables_topological_order = [v.name for v in self.variables] 325 | 326 | # save input signature for compilation 327 | self.input_signature = [ 328 | ( 329 | tf.contrib.eager.TensorSpec(shape=[None, self.cons_nfeats], dtype=tf.float32), 330 | tf.contrib.eager.TensorSpec(shape=[2, None], dtype=tf.int32), 331 | tf.contrib.eager.TensorSpec(shape=[None, self.edge_nfeats], dtype=tf.float32), 332 | tf.contrib.eager.TensorSpec(shape=[None, self.var_nfeats], dtype=tf.float32), 333 | tf.contrib.eager.TensorSpec(shape=[None], dtype=tf.int32), 334 | tf.contrib.eager.TensorSpec(shape=[None], dtype=tf.int32), 335 | ), 336 | tf.contrib.eager.TensorSpec(shape=[], dtype=tf.bool), 337 | ] 338 | 339 | def build(self, input_shapes): 340 | c_shape, ei_shape, ev_shape, v_shape, nc_shape, nv_shape = input_shapes 341 | emb_shape = [None, self.emb_size] 342 | 343 | if not self.built: 344 | self.cons_embedding.build(c_shape) 345 | self.edge_embedding.build(ev_shape) 346 | self.var_embedding.build(v_shape) 347 | self.conv_v_to_c.build((emb_shape, ei_shape, ev_shape, emb_shape)) 348 | self.conv_c_to_v.build((emb_shape, ei_shape, ev_shape, emb_shape)) 349 | self.output_module.build(emb_shape) 350 | self.built = True 351 | 352 | @staticmethod 353 | def pad_output(output, n_vars_per_sample, pad_value=-1e8): 354 | n_vars_max = tf.reduce_max(n_vars_per_sample) 355 | 356 | output = tf.split( 357 | value=output, 358 | num_or_size_splits=n_vars_per_sample, 359 | axis=1, 360 | ) 361 | output = tf.concat([ 362 | tf.pad( 363 | x, 364 | paddings=[[0, 0], [0, n_vars_max - tf.shape(x)[1]]], 365 | mode='CONSTANT', 366 | constant_values=pad_value) 367 | for x in output 368 | ], axis=0) 369 | 370 | return output 371 | 372 | def call(self, inputs, training): 373 | """ 374 | Accepts stacked mini-batches, i.e. several bipartite graphs aggregated 375 | as one. In that case the number of variables per samples has to be 376 | provided, and the output consists in a padded dense tensor. 377 | 378 | Parameters 379 | ---------- 380 | inputs: list of tensors 381 | Model input as a bipartite graph. May be batched into a stacked graph. 382 | n_cons_per_sample: list of ints 383 | Number of constraints for each of the samples stacked in the batch. 384 | n_vars_per_sample: list of ints 385 | Number of variables for each of the samples stacked in the batch. 386 | 387 | Inputs 388 | ------ 389 | constraint_features: 2D float tensor 390 | Constraint node features (n_constraints x n_constraint_features) 391 | edge_indices: 2D int tensor 392 | Edge constraint and variable indices (2, n_edges) 393 | edge_features: 2D float tensor 394 | Edge features (n_edges, n_edge_features) 395 | variable_features: 2D float tensor 396 | Variable node features (n_variables, n_variable_features) 397 | n_cons_per_sample: 1D int tensor 398 | Number of constraints for each of the samples stacked in the batch. 399 | n_vars_per_sample: 1D int tensor 400 | Number of variables for each of the samples stacked in the batch. 401 | 402 | Other parameters 403 | ---------------- 404 | training: boolean 405 | Training mode indicator 406 | """ 407 | constraint_features, edge_indices, edge_features, variable_features, n_cons_per_sample, n_vars_per_sample = inputs 408 | n_cons_total = tf.reduce_sum(n_cons_per_sample) 409 | n_vars_total = tf.reduce_sum(n_vars_per_sample) 410 | 411 | # EMBEDDINGS 412 | constraint_features = self.cons_embedding(constraint_features) 413 | edge_features = self.edge_embedding(edge_features) 414 | variable_features = self.var_embedding(variable_features) 415 | 416 | # GRAPH CONVOLUTIONS 417 | constraint_features = self.conv_v_to_c(( 418 | constraint_features, edge_indices, edge_features, variable_features, n_cons_total)) 419 | constraint_features = self.activation(constraint_features) 420 | 421 | variable_features = self.conv_c_to_v(( 422 | constraint_features, edge_indices, edge_features, variable_features, n_vars_total)) 423 | variable_features = self.activation(variable_features) 424 | 425 | # OUTPUT 426 | output = self.output_module(variable_features) 427 | output = tf.reshape(output, [1, -1]) 428 | 429 | if n_vars_per_sample.shape[0] > 1: 430 | output = self.pad_output(output, n_vars_per_sample) 431 | 432 | return output 433 | 434 | 435 | -------------------------------------------------------------------------------- /scip_patch/vanillafullstrong.patch: -------------------------------------------------------------------------------- 1 | diff --git a/Makefile b/Makefile 2 | index 4a2b4dc8c1..58d7d62fec 100644 3 | --- a/Makefile 4 | +++ b/Makefile 5 | @@ -512,6 +512,7 @@ SCIPPLUGINLIBOBJ= scip/benders_default.o \ 6 | scip/branch_pscost.o \ 7 | scip/branch_random.o \ 8 | scip/branch_relpscost.o \ 9 | + scip/branch_vanillafullstrong.o \ 10 | scip/cons_abspower.o \ 11 | scip/compr_largestrepr.o \ 12 | scip/compr_weakcompr.o \ 13 | diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt 14 | index 9bf9eec46b..c753588d0e 100644 15 | --- a/src/CMakeLists.txt 16 | +++ b/src/CMakeLists.txt 17 | @@ -38,6 +38,7 @@ set(scipsources 18 | scip/branch_pscost.c 19 | scip/branch_random.c 20 | scip/branch_relpscost.c 21 | + scip/branch_vanillafullstrong.c 22 | scip/cons_abspower.c 23 | scip/compr_largestrepr.c 24 | scip/compr_weakcompr.c 25 | @@ -452,6 +453,7 @@ set(scipheaders 26 | scip/branch_pscost.h 27 | scip/branch_random.h 28 | scip/branch_relpscost.h 29 | + scip/branch_vanillafullstrong.h 30 | scip/clock.h 31 | scip/compr.h 32 | scip/compr_largestrepr.h 33 | diff --git a/src/scip/branch_allfullstrong.c b/src/scip/branch_allfullstrong.c 34 | index 77e301825d..d8f82ebc8b 100644 35 | --- a/src/scip/branch_allfullstrong.c 36 | +++ b/src/scip/branch_allfullstrong.c 37 | @@ -397,12 +397,12 @@ SCIP_RETCODE SCIPselectVarPseudoStrongBranching( 38 | 39 | if( integral ) 40 | { 41 | - SCIP_CALL( SCIPgetVarStrongbranchInt(scip, pseudocands[c], INT_MAX, 42 | + SCIP_CALL( SCIPgetVarStrongbranchInt(scip, pseudocands[c], INT_MAX, FALSE, 43 | skipdown[c] ? NULL : &down, skipup[c] ? NULL : &up, &downvalid, &upvalid, &downinf, &upinf, &downconflict, &upconflict, &lperror) ); 44 | } 45 | else 46 | { 47 | - SCIP_CALL( SCIPgetVarStrongbranchFrac(scip, pseudocands[c], INT_MAX, 48 | + SCIP_CALL( SCIPgetVarStrongbranchFrac(scip, pseudocands[c], INT_MAX, FALSE, 49 | skipdown[c] ? NULL : &down, skipup[c] ? NULL : &up, &downvalid, &upvalid, &downinf, &upinf, &downconflict, &upconflict, &lperror) ); 50 | } 51 | nsbcalls++; 52 | diff --git a/src/scip/branch_fullstrong.c b/src/scip/branch_fullstrong.c 53 | index a94084ab86..89c4a7ba75 100644 54 | --- a/src/scip/branch_fullstrong.c 55 | +++ b/src/scip/branch_fullstrong.c 56 | @@ -343,7 +343,7 @@ SCIP_RETCODE SCIPselectVarStrongBranching( 57 | } 58 | else 59 | { 60 | - SCIP_CALL( SCIPgetVarStrongbranchFrac(scip, lpcands[c], INT_MAX, 61 | + SCIP_CALL( SCIPgetVarStrongbranchFrac(scip, lpcands[c], INT_MAX, FALSE, 62 | skipdown[i] ? NULL : &down, skipup[i] ? NULL : &up, &downvalid, &upvalid, &downinf, &upinf, 63 | &downconflict, &upconflict, &lperror) ); 64 | } 65 | diff --git a/src/scip/branch_relpscost.c b/src/scip/branch_relpscost.c 66 | index 72d468e569..5385936cdb 100644 67 | --- a/src/scip/branch_relpscost.c 68 | +++ b/src/scip/branch_relpscost.c 69 | @@ -1076,7 +1076,7 @@ SCIP_RETCODE execRelpscost( 70 | else 71 | { 72 | /* apply strong branching */ 73 | - SCIP_CALL( SCIPgetVarStrongbranchFrac(scip, branchcands[c], inititer, 74 | + SCIP_CALL( SCIPgetVarStrongbranchFrac(scip, branchcands[c], inititer, FALSE, 75 | &down, &up, &downvalid, &upvalid, &downinf, &upinf, &downconflict, &upconflict, &lperror) ); 76 | 77 | ndomredsdown = ndomredsup = 0; 78 | diff --git a/src/scip/branch_vanillafullstrong.c b/src/scip/branch_vanillafullstrong.c 79 | new file mode 100644 80 | index 0000000000..e188291991 81 | --- /dev/null 82 | +++ b/src/scip/branch_vanillafullstrong.c 83 | @@ -0,0 +1,592 @@ 84 | +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 85 | +/* */ 86 | +/* This file is part of the program and library */ 87 | +/* SCIP --- Solving Constraint Integer Programs */ 88 | +/* */ 89 | +/* Copyright (C) 2002-2019 Konrad-Zuse-Zentrum */ 90 | +/* fuer Informationstechnik Berlin */ 91 | +/* */ 92 | +/* SCIP is distributed under the terms of the ZIB Academic License. */ 93 | +/* */ 94 | +/* You should have received a copy of the ZIB Academic License */ 95 | +/* along with SCIP; see the file COPYING. If not visit scip.zib.de. */ 96 | +/* */ 97 | +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 98 | + 99 | +/**@file branch_vanillafullstrong.c 100 | + * @ingroup DEFPLUGINS_BRANCH 101 | + * @brief vanilla full strong LP branching rule 102 | + * @author Tobias Achterberg 103 | + * @author Maxime Gasse 104 | + */ 105 | + 106 | +/*---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0----+----1----+----2*/ 107 | + 108 | +#include "blockmemshell/memory.h" 109 | +#include "scip/branch_vanillafullstrong.h" 110 | +#include "scip/pub_branch.h" 111 | +#include "scip/pub_message.h" 112 | +#include "scip/pub_tree.h" 113 | +#include "scip/pub_var.h" 114 | +#include "scip/scip_branch.h" 115 | +#include "scip/scip_general.h" 116 | +#include "scip/scip_lp.h" 117 | +#include "scip/scip_mem.h" 118 | +#include "scip/scip_message.h" 119 | +#include "scip/scip_numerics.h" 120 | +#include "scip/scip_param.h" 121 | +#include "scip/scip_prob.h" 122 | +#include "scip/scip_solvingstats.h" 123 | +#include "scip/scip_tree.h" 124 | +#include "scip/scip_var.h" 125 | +#include 126 | + 127 | + 128 | +#define BRANCHRULE_NAME "vanillafullstrong" 129 | +#define BRANCHRULE_DESC "vanilla full strong branching" 130 | +#define BRANCHRULE_PRIORITY -2000 131 | +#define BRANCHRULE_MAXDEPTH -1 132 | +#define BRANCHRULE_MAXBOUNDDIST 1.0 133 | + 134 | +#define DEFAULT_INTEGRALCANDS FALSE /**< should integral variables in the current LP solution be considered as 135 | + * branching candidates ? */ 136 | +#define DEFAULT_SCOREALL FALSE /**< should strong branching scores be computed for all candidates, or can 137 | + * we early stop when a variable has infinite score ? */ 138 | +#define DEFAULT_IDEMPOTENT FALSE /**< should strong branching side-effects be prevented (e.g., domain 139 | + * changes, stat updates etc.) ? */ 140 | +#define DEFAULT_COLLECTSCORES FALSE /**< should strong branching scores be collected ? */ 141 | +#define DEFAULT_DONOTBRANCH FALSE /**< should branching be done ? */ 142 | + 143 | + 144 | +/** branching rule data */ 145 | +struct SCIP_BranchruleData 146 | +{ 147 | + SCIP_Bool integralcands; /**< should integral variables in the current LP solution be considered 148 | + * as branching candidates ? */ 149 | + SCIP_Bool scoreall; /**< should strong branching scores be computed for all candidates, or 150 | + * can we early stop when a node is detected infeasible ? */ 151 | + SCIP_Bool idempotent; /**< should strong branching side-effects be prevented (e.g., domain 152 | + * changes, stat updates etc.) ? */ 153 | + SCIP_Bool collectscores; /**< should strong branching scores be collected ? */ 154 | + SCIP_Bool donotbranch; /**< should branching be done ? */ 155 | + SCIP_VAR** cands; /**< candidate variables */ 156 | + SCIP_Real* candscores; /**< candidate scores */ 157 | + int ncands; /**< number of candidates */ 158 | + int npriocands; /**< number of priority candidates */ 159 | + int bestcand; /**< best branching candidate */ 160 | + int candcapacity; /**< capacity of candidate arrays */ 161 | +}; 162 | + 163 | + 164 | +/* 165 | + * local methods 166 | + */ 167 | + 168 | + 169 | +/** selects a variable from a set of candidates by strong branching */ 170 | +static 171 | +SCIP_RETCODE runVanillaStrongBranching( 172 | + SCIP* scip, /**< SCIP data structure */ 173 | + SCIP_VAR** cands, /**< branching candidates */ 174 | + int ncands, /**< number of branching candidates */ 175 | + int npriocands, /**< number of branching candidates with highest priority */ 176 | + SCIP_Bool scoreall, /**< should strong branching scores be computed for all candidates, or can 177 | + * we early stop when a node is detected infeasible ? */ 178 | + SCIP_Bool idempotent, /**< should strong branching side-effects be prevented (e.g., domain 179 | + * changes, stat updates etc.) ? */ 180 | + SCIP_Real* scores, /**< candidate scores */ 181 | + int* bestcand, /**< best candidate for branching */ 182 | + SCIP_Real* bestdown, /**< objective value of the down branch for bestcand */ 183 | + SCIP_Real* bestup, /**< objective value of the up branch for bestcand */ 184 | + SCIP_Real* bestscore, /**< score for bestcand */ 185 | + SCIP_Bool* bestdownvalid, /**< is bestdown a valid dual bound for the down branch? */ 186 | + SCIP_Bool* bestupvalid, /**< is bestup a valid dual bound for the up branch? */ 187 | + SCIP_Real* provedbound /**< proved dual bound for current subtree */ 188 | + ) 189 | +{ /*lint --e{715}*/ 190 | + SCIP_Real lpobjval; 191 | + int nsbcalls; 192 | + int c; 193 | + 194 | + assert(scip != NULL); 195 | + assert(cands != NULL); 196 | + assert(bestcand != NULL); 197 | + assert(bestdown != NULL); 198 | + assert(bestup != NULL); 199 | + assert(bestscore != NULL); 200 | + assert(bestdownvalid != NULL); 201 | + assert(bestupvalid != NULL); 202 | + assert(provedbound != NULL); 203 | + assert(ncands > 0); 204 | + 205 | + /* get current LP objective bound of the local sub problem and global cutoff bound */ 206 | + lpobjval = SCIPgetLPObjval(scip); 207 | + *provedbound = lpobjval; 208 | + 209 | + *bestcand = 0; 210 | + *bestdown = lpobjval; 211 | + *bestup = lpobjval; 212 | + *bestdownvalid = TRUE; 213 | + *bestupvalid = TRUE; 214 | + *bestscore = -SCIPinfinity(scip); 215 | + 216 | + if( scores != NULL ) 217 | + for( c = 0; c < ncands; ++c ) 218 | + scores[c] = -SCIPinfinity(scip); 219 | + 220 | + /* if only one candidate exists, choose this one without applying strong branching; also, when SCIP is about to be 221 | + * stopped, all strongbranching evaluations will be aborted anyway, thus we can return immediately 222 | + */ 223 | + if( (!scoreall && ncands == 1) || SCIPisStopped(scip) ) 224 | + return SCIP_OKAY; 225 | + 226 | + /* this assert may not hold if SCIP is stopped, thus we only check it here */ 227 | + assert(SCIPgetLPSolstat(scip) == SCIP_LPSOLSTAT_OPTIMAL); 228 | + 229 | + /* initialize strong branching without propagation */ 230 | + SCIP_CALL( SCIPstartStrongbranch(scip, FALSE) ); 231 | + 232 | + /* compute strong branching scores */ 233 | + nsbcalls = 0; 234 | + for( c = 0; c < ncands ; ++c ) 235 | + { 236 | + SCIP_VAR* var; 237 | + SCIP_Real val; 238 | + SCIP_Bool integral; 239 | + SCIP_Real down, up; 240 | + SCIP_Real downgain, upgain; 241 | + SCIP_Bool downvalid, upvalid; 242 | + SCIP_Bool downinf, upinf; 243 | + SCIP_Bool downconflict, upconflict; 244 | + SCIP_Bool lperror; 245 | + SCIP_Real gains[3]; 246 | + SCIP_Real score; 247 | + 248 | + var = cands[c]; 249 | + assert(var != NULL); 250 | + 251 | + val = SCIPvarGetLPSol(var); 252 | + integral = SCIPisFeasIntegral(scip, val); 253 | + 254 | + up = -SCIPinfinity(scip); 255 | + down = -SCIPinfinity(scip); 256 | + 257 | + SCIPdebugMsg(scip, "applying vanilla strong branching on variable <%s> with solution %g\n", 258 | + SCIPvarGetName(var), val); 259 | + 260 | + /* apply strong branching */ 261 | + if( integral ) 262 | + { 263 | + SCIP_CALL( SCIPgetVarStrongbranchInt(scip, cands[c], INT_MAX, idempotent, 264 | + &down, &up, &downvalid, &upvalid, &downinf, &upinf, &downconflict, &upconflict, &lperror) ); 265 | + } 266 | + else 267 | + { 268 | + SCIP_CALL( SCIPgetVarStrongbranchFrac(scip, cands[c], INT_MAX, idempotent, 269 | + &down, &up, &downvalid, &upvalid, &downinf, &upinf, &downconflict, &upconflict, &lperror) ); 270 | + } 271 | + nsbcalls++; 272 | + 273 | + /* check for an error in strong branching */ 274 | + if( lperror ) 275 | + { 276 | + SCIPverbMessage(scip, SCIP_VERBLEVEL_HIGH, NULL, 277 | + "(node %" SCIP_LONGINT_FORMAT ") error in strong branching call for variable <%s> with solution %g\n", 278 | + SCIPgetNNodes(scip), SCIPvarGetName(var), val); 279 | + break; 280 | + } 281 | + 282 | + /* evaluate strong branching */ 283 | + down = MAX(down, lpobjval); 284 | + up = MAX(up, lpobjval); 285 | + downgain = down - lpobjval; 286 | + upgain = up - lpobjval; 287 | + 288 | + assert(!SCIPallColsInLP(scip) || SCIPisExactSolve(scip) || !downvalid || downinf == SCIPisGE(scip, down, SCIPgetCutoffbound(scip))); 289 | + assert(!SCIPallColsInLP(scip) || SCIPisExactSolve(scip) || !upvalid || upinf == SCIPisGE(scip, up, SCIPgetCutoffbound(scip))); 290 | + assert(downinf || !downconflict); 291 | + assert(upinf || !upconflict); 292 | + 293 | + if( !idempotent ) 294 | + { 295 | + /* display node information line */ 296 | + if( SCIPgetDepth(scip) == 0 && nsbcalls % 100 == 0 ) 297 | + { 298 | + SCIP_CALL( SCIPprintDisplayLine(scip, NULL, SCIP_VERBLEVEL_HIGH, TRUE) ); 299 | + } 300 | + /* update variable pseudo cost values */ 301 | + if( !downinf && downvalid ) 302 | + { 303 | + SCIP_CALL( SCIPupdateVarPseudocost(scip, var, integral ? -1.0 : 0.0 - SCIPfrac(scip, val), downgain, 1.0) ); 304 | + } 305 | + if( !upinf && upvalid ) 306 | + { 307 | + SCIP_CALL( SCIPupdateVarPseudocost(scip, var, integral ? +1.0 : 1.0 - SCIPfrac(scip, val), upgain, 1.0) ); 308 | + } 309 | + } 310 | + 311 | + /* compute strong branching score */ 312 | + gains[0] = downgain; 313 | + gains[1] = upgain; 314 | + gains[2] = 0.0; 315 | + score = SCIPgetBranchScoreMultiple(scip, var, integral ? 3 : 2, gains); 316 | + 317 | + /* collect scores if requested */ 318 | + if( scores != NULL ) 319 | + scores[c] = score; 320 | + 321 | + /* check for a better score */ 322 | + if( score > *bestscore ) 323 | + { 324 | + *bestcand = c; 325 | + *bestdown = down; 326 | + *bestup = up; 327 | + *bestdownvalid = downvalid; 328 | + *bestupvalid = upvalid; 329 | + *bestscore = score; 330 | + } 331 | + 332 | + SCIPdebugMsg(scip, " -> cand %d/%d (prio:%d) var <%s> (solval=%g, downgain=%g, upgain=%g, score=%g) -- best: <%s> (%g)\n", 333 | + c, ncands, npriocands, SCIPvarGetName(var), val, downgain, upgain, score, 334 | + SCIPvarGetName(cands[*bestcand]), *bestscore); 335 | + 336 | + /* node is infeasible -> early stopping (highest score) */ 337 | + if( !integral && !scoreall && downinf && upinf ) 338 | + { 339 | + /* we should only detect infeasibility if the LP is a valid relaxation */ 340 | + assert(SCIPallColsInLP(scip)); 341 | + assert(!SCIPisExactSolve(scip)); 342 | + 343 | + SCIPdebugMsg(scip, " -> variable <%s> is infeasible in both directions\n", SCIPvarGetName(var)); 344 | + break; 345 | + } 346 | + } 347 | + 348 | + /* end strong branching */ 349 | + SCIP_CALL( SCIPendStrongbranch(scip) ); 350 | + 351 | + return SCIP_OKAY; 352 | +} 353 | + 354 | +/* 355 | + * Callback methods 356 | + */ 357 | + 358 | +/** copy method for branchrule plugins (called when SCIP copies plugins) */ 359 | +static 360 | +SCIP_DECL_BRANCHCOPY(branchCopyVanillafullstrong) 361 | +{ /*lint --e{715}*/ 362 | + assert(scip != NULL); 363 | + assert(branchrule != NULL); 364 | + assert(strcmp(SCIPbranchruleGetName(branchrule), BRANCHRULE_NAME) == 0); 365 | + 366 | + /* call inclusion method of branchrule */ 367 | + SCIP_CALL( SCIPincludeBranchruleVanillafullstrong(scip) ); 368 | + 369 | + return SCIP_OKAY; 370 | +} 371 | + 372 | +/** destructor of branching rule to free user data (called when SCIP is exiting) */ 373 | +static 374 | +SCIP_DECL_BRANCHFREE(branchFreeVanillafullstrong) 375 | +{ /*lint --e{715}*/ 376 | + SCIP_BRANCHRULEDATA* branchruledata; 377 | + 378 | + /* free branching rule data */ 379 | + branchruledata = SCIPbranchruleGetData(branchrule); 380 | + assert(branchruledata != NULL); 381 | + 382 | + SCIPfreeBlockMemoryNull(scip, &branchruledata); 383 | + 384 | + return SCIP_OKAY; 385 | +} 386 | + 387 | +/** initialization method of branching rule (called after problem was transformed) */ 388 | +static 389 | +SCIP_DECL_BRANCHINIT(branchInitVanillafullstrong) 390 | +{ /*lint --e{715}*/ 391 | +#ifndef NDEBUG 392 | + SCIP_BRANCHRULEDATA* branchruledata; 393 | + 394 | + /* initialize branching rule data */ 395 | + branchruledata = SCIPbranchruleGetData(branchrule); 396 | +#endif 397 | + assert(branchruledata != NULL); 398 | + assert(branchruledata->candscores == NULL); 399 | + assert(branchruledata->cands == NULL); 400 | + 401 | + return SCIP_OKAY; 402 | +} 403 | + 404 | +/** deinitialization method of branching rule (called before transformed problem is freed) */ 405 | +static 406 | +SCIP_DECL_BRANCHEXIT(branchExitVanillafullstrong) 407 | +{ /*lint --e{715}*/ 408 | + SCIP_BRANCHRULEDATA* branchruledata; 409 | + 410 | + /* initialize branching rule data */ 411 | + branchruledata = SCIPbranchruleGetData(branchrule); 412 | + assert(branchruledata != NULL); 413 | + 414 | + /* free candidate arrays if any */ 415 | + if( branchruledata->candscores != NULL ) 416 | + { 417 | + SCIPfreeBlockMemoryArrayNull(scip, &branchruledata->candscores, branchruledata->candcapacity); 418 | + } 419 | + if( branchruledata->cands != NULL ) 420 | + { 421 | + SCIPfreeBlockMemoryArrayNull(scip, &branchruledata->cands, branchruledata->candcapacity); 422 | + } 423 | + 424 | + branchruledata->candcapacity = -1; 425 | + branchruledata->ncands = -1; 426 | + branchruledata->npriocands = -1; 427 | + branchruledata->bestcand = -1; 428 | + 429 | + return SCIP_OKAY; 430 | +} 431 | + 432 | +/** branching execution method */ 433 | +static 434 | +SCIP_DECL_BRANCHEXECLP(branchExeclpVanillafullstrong) 435 | +{ /*lint --e{715}*/ 436 | + SCIP_BRANCHRULEDATA* branchruledata; 437 | + SCIP_Real bestdown; 438 | + SCIP_Real bestup; 439 | + SCIP_Real bestscore; 440 | + SCIP_Real provedbound; 441 | + SCIP_Bool bestdownvalid; 442 | + SCIP_Bool bestupvalid; 443 | + SCIP_VAR** cands; 444 | + int ncands; 445 | + int npriocands; 446 | + int i; 447 | + 448 | + assert(branchrule != NULL); 449 | + assert(strcmp(SCIPbranchruleGetName(branchrule), BRANCHRULE_NAME) == 0); 450 | + assert(scip != NULL); 451 | + assert(result != NULL); 452 | + 453 | + SCIPdebugMsg(scip, "Execlp method of vanilla fullstrong branching\n"); 454 | + 455 | + *result = SCIP_DIDNOTRUN; 456 | + 457 | + /* get branching rule data */ 458 | + branchruledata = SCIPbranchruleGetData(branchrule); 459 | + assert(branchruledata != NULL); 460 | + 461 | + /* get branching candidates, either all non-fixed variables or only the 462 | + * fractional ones */ 463 | + if( branchruledata->integralcands ) 464 | + { 465 | + SCIP_CALL( SCIPgetPseudoBranchCands(scip, &cands, &ncands, &npriocands) ); 466 | + } 467 | + else 468 | + { 469 | + SCIP_CALL( SCIPgetLPBranchCands(scip, &cands, NULL, NULL, &ncands, &npriocands, NULL) ); 470 | + } 471 | + 472 | + assert(ncands > 0); 473 | + assert(npriocands > 0); 474 | + 475 | + /* increase candidate arrays capacity if needed */ 476 | + if( ncands > branchruledata->candcapacity ) 477 | + { 478 | + /* free previously allocated arrays if any */ 479 | + if( branchruledata->candscores != NULL) 480 | + { 481 | + SCIPfreeBlockMemoryArrayNull(scip, &branchruledata->candscores, branchruledata->candcapacity); 482 | + branchruledata->candscores = NULL; 483 | + } 484 | + if( branchruledata->cands != NULL) 485 | + { 486 | + SCIPfreeBlockMemoryArrayNull(scip, &branchruledata->cands, branchruledata->candcapacity); 487 | + branchruledata->cands = NULL; 488 | + } 489 | + 490 | + /* update capacity */ 491 | + branchruledata->candcapacity = SCIPgetNBinVars(scip) + SCIPgetNIntVars(scip) + SCIPgetNImplVars(scip); 492 | + } 493 | + assert(branchruledata->candcapacity >= ncands); 494 | + 495 | + /* allocate new candidate arrays if needed */ 496 | + if( branchruledata->cands == NULL ) 497 | + { 498 | + SCIP_CALL( SCIPallocBlockMemoryArray(scip, &branchruledata->cands, branchruledata->candcapacity) ); 499 | + } 500 | + if( branchruledata->candscores == NULL && branchruledata->collectscores ) 501 | + { 502 | + SCIP_CALL( SCIPallocBlockMemoryArray(scip, &branchruledata->candscores, branchruledata->candcapacity) ); 503 | + } 504 | + 505 | + /* copy candidates */ 506 | + branchruledata->ncands = ncands; 507 | + branchruledata->npriocands = npriocands; 508 | + 509 | + for( i = 0; i < ncands; i++ ) 510 | + branchruledata->cands[i] = cands[i]; 511 | + 512 | + SCIP_CALL( runVanillaStrongBranching(scip, branchruledata->cands, branchruledata->ncands, branchruledata->npriocands, 513 | + branchruledata->scoreall, branchruledata->idempotent, branchruledata->candscores, 514 | + &branchruledata->bestcand, &bestdown, &bestup, &bestscore, &bestdownvalid, 515 | + &bestupvalid, &provedbound) ); 516 | + 517 | + if( !branchruledata->donotbranch ) 518 | + { 519 | + SCIP_VAR* var; 520 | + SCIP_Real val; 521 | + SCIP_NODE* downchild; 522 | + SCIP_NODE* eqchild; 523 | + SCIP_NODE* upchild; 524 | + SCIP_Bool allcolsinlp; 525 | + SCIP_Bool exactsolve; 526 | + 527 | + assert(0 <= branchruledata->bestcand && branchruledata->bestcand < branchruledata->ncands); 528 | + assert(SCIPisLT(scip, provedbound, SCIPgetCutoffbound(scip))); 529 | + 530 | + var = branchruledata->cands[branchruledata->bestcand]; 531 | + val = SCIPvarGetLPSol(var); 532 | + 533 | + /* perform the branching */ 534 | + SCIPdebugMsg(scip, " -> %d candidates, selected candidate %d: variable <%s>[%g,%g] (solval=%g, down=%g, up=%g, score=%g)\n", 535 | + branchruledata->ncands, branchruledata->bestcand, SCIPvarGetName(var), SCIPvarGetLbLocal(var), 536 | + SCIPvarGetUbLocal(var), val, bestdown, bestup, bestscore); 537 | + SCIP_CALL( SCIPbranchVarVal(scip, var, val, &downchild, &eqchild, &upchild) ); 538 | + 539 | + /* check, if we want to solve the problem exactly, meaning that strong branching information is not useful 540 | + * for cutting off sub problems and improving lower bounds of children 541 | + */ 542 | + exactsolve = SCIPisExactSolve(scip); 543 | + 544 | + /* check, if all existing columns are in LP, and thus the strong branching results give lower bounds */ 545 | + allcolsinlp = SCIPallColsInLP(scip); 546 | + 547 | + /* update the lower bounds in the children */ 548 | + if( !branchruledata->idempotent && allcolsinlp && !exactsolve ) 549 | + { 550 | + if( downchild != NULL ) 551 | + { 552 | + SCIP_CALL( SCIPupdateNodeLowerbound(scip, downchild, bestdownvalid ? MAX(bestdown, provedbound) : provedbound) ); 553 | + SCIPdebugMsg(scip, " -> down child's lowerbound: %g\n", SCIPnodeGetLowerbound(downchild)); 554 | + } 555 | + if( eqchild != NULL ) 556 | + { 557 | + SCIP_CALL( SCIPupdateNodeLowerbound(scip, eqchild, provedbound) ); 558 | + SCIPdebugMsg(scip, " -> eq child's lowerbound: %g\n", SCIPnodeGetLowerbound(eqchild)); 559 | + } 560 | + if( upchild != NULL ) 561 | + { 562 | + SCIP_CALL( SCIPupdateNodeLowerbound(scip, upchild, bestupvalid ? MAX(bestup, provedbound) : provedbound) ); 563 | + SCIPdebugMsg(scip, " -> up child's lowerbound: %g\n", SCIPnodeGetLowerbound(upchild)); 564 | + } 565 | + } 566 | + 567 | + *result = SCIP_BRANCHED; 568 | + } 569 | + 570 | + return SCIP_OKAY; 571 | +} 572 | + 573 | + 574 | +/* 575 | + * branching specific interface methods 576 | + */ 577 | + 578 | +/** creates the vanilla full strong LP branching rule and includes it in SCIP */ 579 | +SCIP_RETCODE SCIPincludeBranchruleVanillafullstrong( 580 | + SCIP* scip /**< SCIP data structure */ 581 | + ) 582 | +{ 583 | + SCIP_BRANCHRULEDATA* branchruledata; 584 | + SCIP_BRANCHRULE* branchrule; 585 | + 586 | + /* create fullstrong branching rule data */ 587 | + SCIP_CALL( SCIPallocBlockMemory(scip, &branchruledata) ); 588 | + branchruledata->cands = NULL; 589 | + branchruledata->candscores = NULL; 590 | + branchruledata->candcapacity = -1; 591 | + branchruledata->ncands = -1; 592 | + branchruledata->npriocands = -1; 593 | + branchruledata->bestcand = -1; 594 | + 595 | + /* include branching rule */ 596 | + SCIP_CALL( SCIPincludeBranchruleBasic(scip, &branchrule, BRANCHRULE_NAME, BRANCHRULE_DESC, BRANCHRULE_PRIORITY, 597 | + BRANCHRULE_MAXDEPTH, BRANCHRULE_MAXBOUNDDIST, branchruledata) ); 598 | + 599 | + assert(branchrule != NULL); 600 | + 601 | + /* set non-fundamental callbacks via specific setter functions*/ 602 | + SCIP_CALL( SCIPsetBranchruleCopy(scip, branchrule, branchCopyVanillafullstrong) ); 603 | + SCIP_CALL( SCIPsetBranchruleFree(scip, branchrule, branchFreeVanillafullstrong) ); 604 | + SCIP_CALL( SCIPsetBranchruleInit(scip, branchrule, branchInitVanillafullstrong) ); 605 | + SCIP_CALL( SCIPsetBranchruleExit(scip, branchrule, branchExitVanillafullstrong) ); 606 | + SCIP_CALL( SCIPsetBranchruleExecLp(scip, branchrule, branchExeclpVanillafullstrong) ); 607 | + 608 | + /* fullstrong branching rule parameters */ 609 | + SCIP_CALL( SCIPaddBoolParam(scip, 610 | + "branching/vanillafullstrong/integralcands", 611 | + "should integral variables in the current LP solution be considered as branching candidates?", 612 | + &branchruledata->integralcands, FALSE, DEFAULT_INTEGRALCANDS, NULL, NULL) ); 613 | + SCIP_CALL( SCIPaddBoolParam(scip, 614 | + "branching/vanillafullstrong/idempotent", 615 | + "should strong branching side-effects be prevented (e.g., domain changes, stat updates etc.)?", 616 | + &branchruledata->idempotent, FALSE, DEFAULT_IDEMPOTENT, NULL, NULL) ); 617 | + SCIP_CALL( SCIPaddBoolParam(scip, 618 | + "branching/vanillafullstrong/scoreall", 619 | + "should strong branching scores be computed for all candidates, or can we early stop when a variable has infinite score?", 620 | + &branchruledata->scoreall, TRUE, DEFAULT_SCOREALL, NULL, NULL) ); 621 | + SCIP_CALL( SCIPaddBoolParam(scip, 622 | + "branching/vanillafullstrong/collectscores", 623 | + "should strong branching scores be collected?", 624 | + &branchruledata->collectscores, TRUE, DEFAULT_COLLECTSCORES, NULL, NULL) ); 625 | + SCIP_CALL( SCIPaddBoolParam(scip, 626 | + "branching/vanillafullstrong/donotbranch", 627 | + "should candidates only be scored, but no branching be performed?", 628 | + &branchruledata->donotbranch, TRUE, DEFAULT_DONOTBRANCH, NULL, NULL) ); 629 | + 630 | + return SCIP_OKAY; 631 | +} 632 | + 633 | + 634 | +/** recovers candidate variables and their scores from last vanilla full strong branching call */ 635 | +SCIP_RETCODE SCIPgetVanillafullstrongData( 636 | + SCIP* scip, /**< SCIP data structure */ 637 | + SCIP_VAR*** cands, /**< pointer to store candidate variables; or NULL */ 638 | + SCIP_Real** candscores, /**< pointer to store candidate scores; or NULL */ 639 | + int* ncands, /**< pointer to store number of candidates; or NULL */ 640 | + int* npriocands, /**< pointer to store number of priority candidates; or NULL */ 641 | + int* bestcand /**< pointer to store best branching candidate; or NULL */ 642 | + ) 643 | +{ 644 | + SCIP_BRANCHRULEDATA* branchruledata; 645 | + SCIP_BRANCHRULE* branchrule; 646 | + 647 | + assert(scip != NULL); 648 | + 649 | + branchrule = SCIPfindBranchrule(scip, BRANCHRULE_NAME); 650 | + branchruledata = SCIPbranchruleGetData(branchrule); 651 | + 652 | + if( cands ) 653 | + { 654 | + *cands = branchruledata->cands; 655 | + } 656 | + if( candscores && branchruledata->collectscores ) 657 | + { 658 | + *candscores = branchruledata->candscores; 659 | + } 660 | + if( ncands ) 661 | + { 662 | + *ncands = branchruledata->ncands; 663 | + } 664 | + if( npriocands ) 665 | + { 666 | + *npriocands = branchruledata->npriocands; 667 | + } 668 | + if( bestcand ) 669 | + { 670 | + *bestcand = branchruledata->bestcand; 671 | + } 672 | + 673 | + return SCIP_OKAY; 674 | +} 675 | + 676 | diff --git a/src/scip/branch_vanillafullstrong.h b/src/scip/branch_vanillafullstrong.h 677 | new file mode 100644 678 | index 0000000000..a75bf4dd26 679 | --- /dev/null 680 | +++ b/src/scip/branch_vanillafullstrong.h 681 | @@ -0,0 +1,78 @@ 682 | +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 683 | +/* */ 684 | +/* This file is part of the program and library */ 685 | +/* SCIP --- Solving Constraint Integer Programs */ 686 | +/* */ 687 | +/* Copyright (C) 2002-2019 Konrad-Zuse-Zentrum */ 688 | +/* fuer Informationstechnik Berlin */ 689 | +/* */ 690 | +/* SCIP is distributed under the terms of the ZIB Academic License. */ 691 | +/* */ 692 | +/* You should have received a copy of the ZIB Academic License */ 693 | +/* along with SCIP; see the file COPYING. If not visit scip.zib.de. */ 694 | +/* */ 695 | +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 696 | + 697 | +/**@file branch_vanillafullstrong.h 698 | + * @ingroup BRANCHINGRULES 699 | + * @brief vanilla full strong LP branching rule 700 | + * @author Tobias Achterberg 701 | + * @author Maxime Gasse 702 | + * 703 | + * The vanilla full strong branching rule is a purged implementation of full strong branching, for academic purposes. 704 | + * It implements full strong branching with the following specific features: 705 | + * - no cutoff or domain reduction: only branching. 706 | + * - idempotent (optional): leave SCIP, as much as possible, in the same state before / after the strong branching 707 | + * calls. Basically, do not update any statistic. 708 | + * - donotbranch (optional): do no perform branching. So that the brancher can be called as an oracle only (on which 709 | + * variable would you branch ? But do not branch please). 710 | + * - scoreall (optional): continue scoring variables, even if infeasibility is detected along the way. 711 | + * - collectscores (optional): store the candidate scores from the last call, which can then be retrieved by calling 712 | + * SCIPgetVanillafullstrongData(). 713 | + * - integralcands (optional): get candidates from SCIPgetPseudoBranchCands() instead of SCIPgetLPBranchCands(), i.e., 714 | + * consider all non-fixed variables as branching candidates, not only fractional ones. 715 | + * 716 | + */ 717 | + 718 | +/*---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0----+----1----+----2*/ 719 | + 720 | +#ifndef __SCIP_BRANCH_VANILLAFULLSTRONG_H__ 721 | +#define __SCIP_BRANCH_VANILLAFULLSTRONG_H__ 722 | + 723 | + 724 | +#include "scip/def.h" 725 | +#include "scip/type_result.h" 726 | +#include "scip/type_retcode.h" 727 | +#include "scip/type_scip.h" 728 | +#include "scip/type_var.h" 729 | + 730 | +#ifdef __cplusplus 731 | +extern "C" { 732 | +#endif 733 | + 734 | +/** creates the vanilla full strong branching rule and includes it in SCIP 735 | + * 736 | + * @ingroup BranchingRuleIncludes 737 | + */ 738 | +extern 739 | +SCIP_RETCODE SCIPincludeBranchruleVanillafullstrong( 740 | + SCIP* scip /**< SCIP data structure */ 741 | + ); 742 | + 743 | +/** recovers candidate variables and their scores from last vanilla full strong branching call */ 744 | +extern 745 | +SCIP_RETCODE SCIPgetVanillafullstrongData( 746 | + SCIP* scip, /**< SCIP data structure */ 747 | + SCIP_VAR*** cands, /**< pointer to store candidate variables; or NULL */ 748 | + SCIP_Real** candscores, /**< pointer to store candidate scores; or NULL */ 749 | + int* ncands, /**< pointer to store number of candidates; or NULL */ 750 | + int* npriocands, /**< pointer to store number of priority candidates; or NULL */ 751 | + int* bestcand /**< pointer to store best branching candidate; or NULL */ 752 | + ); 753 | + 754 | + 755 | +#ifdef __cplusplus 756 | +} 757 | +#endif 758 | + 759 | +#endif 760 | diff --git a/src/scip/lp.c b/src/scip/lp.c 761 | index 9cb4adcbb1..e9d8eb5fd1 100644 762 | --- a/src/scip/lp.c 763 | +++ b/src/scip/lp.c 764 | @@ -4241,6 +4241,8 @@ SCIP_RETCODE SCIPcolGetStrongbranch( 765 | SCIP_PROB* prob, /**< problem data */ 766 | SCIP_LP* lp, /**< LP data */ 767 | int itlim, /**< iteration limit for strong branchings */ 768 | + SCIP_Bool updatecol, /**< should col be updated, or should it stay in its current state ? */ 769 | + SCIP_Bool updatestat, /**< should stat be updated, or should it stay in its current state ? */ 770 | SCIP_Real* down, /**< stores dual bound after branching column down */ 771 | SCIP_Real* up, /**< stores dual bound after branching column up */ 772 | SCIP_Bool* downvalid, /**< stores whether the returned down value is a valid dual bound, or NULL; 773 | @@ -4250,6 +4252,17 @@ SCIP_RETCODE SCIPcolGetStrongbranch( 774 | SCIP_Bool* lperror /**< pointer to store whether an unresolved LP error occurred */ 775 | ) 776 | { 777 | + SCIP_Real sbdown; 778 | + SCIP_Real sbup; 779 | + SCIP_Bool sbdownvalid; 780 | + SCIP_Bool sbupvalid; 781 | + SCIP_Longint validsblp; 782 | + SCIP_Real sbsolval; 783 | + SCIP_Real sblpobjval; 784 | + SCIP_Longint sbnode; 785 | + int sbitlim; 786 | + int nsbcalls; 787 | + 788 | assert(col != NULL); 789 | assert(col->var != NULL); 790 | assert(SCIPcolIsIntegral(col)); 791 | @@ -4277,29 +4290,36 @@ SCIP_RETCODE SCIPcolGetStrongbranch( 792 | 793 | *lperror = FALSE; 794 | 795 | - if( col->validsblp != stat->nlps || itlim > col->sbitlim ) 796 | - { 797 | - col->validsblp = stat->nlps; 798 | - col->sbsolval = col->primsol; 799 | - col->sblpobjval = SCIPlpGetObjval(lp, set, prob); 800 | - col->sbnode = stat->nnodes; 801 | + sbdown = col->sbdown; 802 | + sbup = col->sbup; 803 | + sbdownvalid = col->sbdownvalid; 804 | + sbupvalid = col->sbupvalid; 805 | + validsblp = col->validsblp; 806 | + sbsolval = col->sbsolval; 807 | + sblpobjval = col->sblpobjval; 808 | + sbnode = col->sbnode; 809 | + sbitlim = col->sbitlim; 810 | + nsbcalls = col->nsbcalls; 811 | + 812 | + if( validsblp != stat->nlps || itlim > sbitlim ) 813 | + { 814 | + validsblp = stat->nlps; 815 | + sbsolval = col->primsol; 816 | + sblpobjval = SCIPlpGetObjval(lp, set, prob); 817 | + sbnode = stat->nnodes; 818 | assert(integral || !SCIPsetIsFeasIntegral(set, col->primsol)); 819 | 820 | /* if a loose variables has an infinite best bound, the LP bound is -infinity and no gain can be achieved */ 821 | if( lp->looseobjvalinf > 0 ) 822 | { 823 | - col->sbdown = -SCIPsetInfinity(set); 824 | - col->sbup = -SCIPsetInfinity(set); 825 | - col->sbdownvalid = FALSE; 826 | - col->sbupvalid = FALSE; 827 | + sbdown = -SCIPsetInfinity(set); 828 | + sbup = -SCIPsetInfinity(set); 829 | + sbdownvalid = FALSE; 830 | + sbupvalid = FALSE; 831 | } 832 | else 833 | { 834 | SCIP_RETCODE retcode; 835 | - SCIP_Real sbdown; 836 | - SCIP_Real sbup; 837 | - SCIP_Bool sbdownvalid; 838 | - SCIP_Bool sbupvalid; 839 | int iter; 840 | 841 | SCIPsetDebugMsg(set, "performing strong branching on variable <%s>(%g) with %d iterations\n", 842 | @@ -4309,8 +4329,8 @@ SCIP_RETCODE SCIPcolGetStrongbranch( 843 | SCIPclockStart(stat->strongbranchtime, set); 844 | 845 | /* call LPI strong branching */ 846 | - col->sbitlim = itlim; 847 | - col->nsbcalls++; 848 | + sbitlim = itlim; 849 | + nsbcalls++; 850 | 851 | sbdown = lp->lpobjval; 852 | sbup = lp->lpobjval; 853 | @@ -4327,14 +4347,14 @@ SCIP_RETCODE SCIPcolGetStrongbranch( 854 | if( retcode == SCIP_LPERROR ) 855 | { 856 | *lperror = TRUE; 857 | - col->sbdown = SCIP_INVALID; 858 | - col->sbup = SCIP_INVALID; 859 | - col->sbdownvalid = FALSE; 860 | - col->sbupvalid = FALSE; 861 | - col->validsblp = -1; 862 | - col->sbsolval = SCIP_INVALID; 863 | - col->sblpobjval = SCIP_INVALID; 864 | - col->sbnode = -1; 865 | + sbdown = SCIP_INVALID; 866 | + sbup = SCIP_INVALID; 867 | + sbdownvalid = FALSE; 868 | + sbupvalid = FALSE; 869 | + validsblp = -1; 870 | + sbsolval = SCIP_INVALID; 871 | + sblpobjval = SCIP_INVALID; 872 | + sbnode = -1; 873 | } 874 | else 875 | { 876 | @@ -4344,30 +4364,30 @@ SCIP_RETCODE SCIPcolGetStrongbranch( 877 | SCIP_CALL( retcode ); 878 | 879 | looseobjval = getFiniteLooseObjval(lp, set, prob); 880 | - col->sbdown = MIN(sbdown + looseobjval, lp->cutoffbound); 881 | - col->sbup = MIN(sbup + looseobjval, lp->cutoffbound); 882 | - 883 | - col->sbdownvalid = sbdownvalid; 884 | - col->sbupvalid = sbupvalid; 885 | + sbdown = MIN(sbdown + looseobjval, lp->cutoffbound); 886 | + sbup = MIN(sbup + looseobjval, lp->cutoffbound); 887 | 888 | /* update strong branching statistics */ 889 | - if( iter == -1 ) 890 | + if( updatestat ) 891 | { 892 | - /* calculate average iteration number */ 893 | - iter = stat->ndualresolvelps > 0 ? (int)(2*stat->ndualresolvelpiterations / stat->ndualresolvelps) 894 | - : stat->nduallps > 0 ? (int)((stat->nduallpiterations / stat->nduallps) / 5) 895 | - : stat->nprimalresolvelps > 0 ? (int)(2*stat->nprimalresolvelpiterations / stat->nprimalresolvelps) 896 | - : stat->nprimallps > 0 ? (int)((stat->nprimallpiterations / stat->nprimallps) / 5) 897 | - : 0; 898 | - if( iter/2 >= itlim ) 899 | - iter = 2*itlim; 900 | - } 901 | - SCIPstatIncrement(stat, set, nstrongbranchs); 902 | - SCIPstatAdd(stat, set, nsblpiterations, iter); 903 | - if( stat->nnodes == 1 ) 904 | - { 905 | - SCIPstatIncrement(stat, set, nrootstrongbranchs); 906 | - SCIPstatAdd(stat, set, nrootsblpiterations, iter); 907 | + if( iter == -1 ) 908 | + { 909 | + /* calculate average iteration number */ 910 | + iter = stat->ndualresolvelps > 0 ? (int)(2*stat->ndualresolvelpiterations / stat->ndualresolvelps) 911 | + : stat->nduallps > 0 ? (int)((stat->nduallpiterations / stat->nduallps) / 5) 912 | + : stat->nprimalresolvelps > 0 ? (int)(2*stat->nprimalresolvelpiterations / stat->nprimalresolvelps) 913 | + : stat->nprimallps > 0 ? (int)((stat->nprimallpiterations / stat->nprimallps) / 5) 914 | + : 0; 915 | + if( iter/2 >= itlim ) 916 | + iter = 2*itlim; 917 | + } 918 | + SCIPstatIncrement(stat, set, nstrongbranchs); 919 | + SCIPstatAdd(stat, set, nsblpiterations, iter); 920 | + if( stat->nnodes == 1 ) 921 | + { 922 | + SCIPstatIncrement(stat, set, nrootstrongbranchs); 923 | + SCIPstatAdd(stat, set, nrootsblpiterations, iter); 924 | + } 925 | } 926 | } 927 | 928 | @@ -4375,17 +4395,31 @@ SCIP_RETCODE SCIPcolGetStrongbranch( 929 | SCIPclockStop(stat->strongbranchtime, set); 930 | } 931 | } 932 | - assert(*lperror || col->sbdown < SCIP_INVALID); 933 | - assert(*lperror || col->sbup < SCIP_INVALID); 934 | + assert(*lperror || sbdown < SCIP_INVALID); 935 | + assert(*lperror || sbup < SCIP_INVALID); 936 | 937 | if( down != NULL) 938 | - *down = col->sbdown; 939 | + *down = sbdown; 940 | if( up != NULL ) 941 | - *up = col->sbup; 942 | + *up = sbup; 943 | if( downvalid != NULL ) 944 | - *downvalid = col->sbdownvalid; 945 | + *downvalid = sbdownvalid; 946 | if( upvalid != NULL ) 947 | - *upvalid = col->sbupvalid; 948 | + *upvalid = sbupvalid; 949 | + 950 | + if( updatecol ) 951 | + { 952 | + col->sbdown = sbdown; 953 | + col->sbup = sbup; 954 | + col->sbdownvalid = sbdownvalid; 955 | + col->sbupvalid = sbupvalid; 956 | + col->validsblp = validsblp; 957 | + col->sbsolval = sbsolval; 958 | + col->sblpobjval = sblpobjval; 959 | + col->sbnode = sbnode; 960 | + col->sbitlim = sbitlim; 961 | + col->nsbcalls = nsbcalls; 962 | + } 963 | 964 | return SCIP_OKAY; 965 | } 966 | diff --git a/src/scip/lp.h b/src/scip/lp.h 967 | index 65b5093f8f..0ce1622e8c 100644 968 | --- a/src/scip/lp.h 969 | +++ b/src/scip/lp.h 970 | @@ -253,6 +253,8 @@ SCIP_RETCODE SCIPcolGetStrongbranch( 971 | SCIP_PROB* prob, /**< problem data */ 972 | SCIP_LP* lp, /**< LP data */ 973 | int itlim, /**< iteration limit for strong branchings */ 974 | + SCIP_Bool updatecol, /**< should col be updated, or should it stay in its current state ? */ 975 | + SCIP_Bool updatestat, /**< should stat be updated, or should it stay in its current state ? */ 976 | SCIP_Real* down, /**< stores dual bound after branching column down */ 977 | SCIP_Real* up, /**< stores dual bound after branching column up */ 978 | SCIP_Bool* downvalid, /**< stores whether the returned down value is a valid dual bound, or NULL; 979 | diff --git a/src/scip/scip_var.c b/src/scip/scip_var.c 980 | index 4582b02f02..263dc255f2 100644 981 | --- a/src/scip/scip_var.c 982 | +++ b/src/scip/scip_var.c 983 | @@ -2910,6 +2910,7 @@ SCIP_RETCODE SCIPgetVarStrongbranchFrac( 984 | SCIP* scip, /**< SCIP data structure */ 985 | SCIP_VAR* var, /**< variable to get strong branching values for */ 986 | int itlim, /**< iteration limit for strong branchings */ 987 | + SCIP_Bool idempotent, /**< should scip's state remain the same after the call (statistics, column states...), or should it be updated ? */ 988 | SCIP_Real* down, /**< stores dual bound after branching column down */ 989 | SCIP_Real* up, /**< stores dual bound after branching column up */ 990 | SCIP_Bool* downvalid, /**< stores whether the returned down value is a valid dual bound, or NULL; 991 | @@ -2927,6 +2928,10 @@ SCIP_RETCODE SCIPgetVarStrongbranchFrac( 992 | ) 993 | { 994 | SCIP_COL* col; 995 | + SCIP_Real localdown; 996 | + SCIP_Real localup; 997 | + SCIP_Bool localdownvalid; 998 | + SCIP_Bool localupvalid; 999 | 1000 | assert(scip != NULL); 1001 | assert(var != NULL); 1002 | @@ -2973,17 +2978,36 @@ SCIP_RETCODE SCIPgetVarStrongbranchFrac( 1003 | } 1004 | 1005 | /* call strong branching for column with fractional value */ 1006 | - SCIP_CALL( SCIPcolGetStrongbranch(col, FALSE, scip->set, scip->stat, scip->transprob, scip->lp, itlim, 1007 | - down, up, downvalid, upvalid, lperror) ); 1008 | + SCIP_CALL( SCIPcolGetStrongbranch(col, FALSE, scip->set, scip->stat, scip->transprob, scip->lp, itlim, !idempotent, !idempotent, 1009 | + &localdown, &localup, &localdownvalid, &localupvalid, lperror) ); 1010 | 1011 | /* check, if the branchings are infeasible; in exact solving mode, we cannot trust the strong branching enough to 1012 | * declare the sub nodes infeasible 1013 | */ 1014 | if( !(*lperror) && SCIPprobAllColsInLP(scip->transprob, scip->set, scip->lp) && !scip->set->misc_exactsolve ) 1015 | { 1016 | - SCIP_CALL( analyzeStrongbranch(scip, var, downinf, upinf, downconflict, upconflict) ); 1017 | + if( !idempotent ) 1018 | + { 1019 | + SCIP_CALL( analyzeStrongbranch(scip, var, downinf, upinf, downconflict, upconflict) ); 1020 | + } 1021 | + else 1022 | + { 1023 | + if( downinf != NULL ) 1024 | + *downinf = localdownvalid && SCIPsetIsGE(scip->set, localdown, scip->lp->cutoffbound); 1025 | + if( upinf != NULL ) 1026 | + *upinf = localupvalid && SCIPsetIsGE(scip->set, localup, scip->lp->cutoffbound); 1027 | + } 1028 | } 1029 | 1030 | + if( down != NULL ) 1031 | + *down = localdown; 1032 | + if( up != NULL ) 1033 | + *up = localup; 1034 | + if( downvalid != NULL ) 1035 | + *downvalid = localdownvalid; 1036 | + if( upvalid != NULL ) 1037 | + *upvalid = localupvalid; 1038 | + 1039 | return SCIP_OKAY; 1040 | } 1041 | 1042 | @@ -3627,6 +3651,7 @@ SCIP_RETCODE SCIPgetVarStrongbranchInt( 1043 | SCIP* scip, /**< SCIP data structure */ 1044 | SCIP_VAR* var, /**< variable to get strong branching values for */ 1045 | int itlim, /**< iteration limit for strong branchings */ 1046 | + SCIP_Bool idempotent, /**< should scip's state remain the same after the call (statistics, column states...), or should it be updated ? */ 1047 | SCIP_Real* down, /**< stores dual bound after branching column down */ 1048 | SCIP_Real* up, /**< stores dual bound after branching column up */ 1049 | SCIP_Bool* downvalid, /**< stores whether the returned down value is a valid dual bound, or NULL; 1050 | @@ -3644,6 +3669,10 @@ SCIP_RETCODE SCIPgetVarStrongbranchInt( 1051 | ) 1052 | { 1053 | SCIP_COL* col; 1054 | + SCIP_Real localdown; 1055 | + SCIP_Real localup; 1056 | + SCIP_Bool localdownvalid; 1057 | + SCIP_Bool localupvalid; 1058 | 1059 | SCIP_CALL( SCIPcheckStage(scip, "SCIPgetVarStrongbranchInt", FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE) ); 1060 | 1061 | @@ -3687,17 +3716,36 @@ SCIP_RETCODE SCIPgetVarStrongbranchInt( 1062 | } 1063 | 1064 | /* call strong branching for column */ 1065 | - SCIP_CALL( SCIPcolGetStrongbranch(col, TRUE, scip->set, scip->stat, scip->transprob, scip->lp, itlim, 1066 | - down, up, downvalid, upvalid, lperror) ); 1067 | + SCIP_CALL( SCIPcolGetStrongbranch(col, TRUE, scip->set, scip->stat, scip->transprob, scip->lp, itlim, !idempotent, !idempotent, 1068 | + &localdown, &localup, &localdownvalid, &localupvalid, lperror) ); 1069 | 1070 | /* check, if the branchings are infeasible; in exact solving mode, we cannot trust the strong branching enough to 1071 | * declare the sub nodes infeasible 1072 | */ 1073 | if( !(*lperror) && SCIPprobAllColsInLP(scip->transprob, scip->set, scip->lp) && !scip->set->misc_exactsolve ) 1074 | { 1075 | - SCIP_CALL( analyzeStrongbranch(scip, var, downinf, upinf, downconflict, upconflict) ); 1076 | + if( !idempotent ) 1077 | + { 1078 | + SCIP_CALL( analyzeStrongbranch(scip, var, downinf, upinf, downconflict, upconflict) ); 1079 | + } 1080 | + else 1081 | + { 1082 | + if( downinf != NULL ) 1083 | + *downinf = localdownvalid && SCIPsetIsGE(scip->set, localdown, scip->lp->cutoffbound); 1084 | + if( upinf != NULL ) 1085 | + *upinf = localupvalid && SCIPsetIsGE(scip->set, localup, scip->lp->cutoffbound); 1086 | + } 1087 | } 1088 | 1089 | + if( down != NULL ) 1090 | + *down = localdown; 1091 | + if( up != NULL ) 1092 | + *up = localup; 1093 | + if( downvalid != NULL ) 1094 | + *downvalid = localdownvalid; 1095 | + if( upvalid != NULL ) 1096 | + *upvalid = localupvalid; 1097 | + 1098 | return SCIP_OKAY; 1099 | } 1100 | 1101 | diff --git a/src/scip/scip_var.h b/src/scip/scip_var.h 1102 | index f155fa6442..039e6f5722 100644 1103 | --- a/src/scip/scip_var.h 1104 | +++ b/src/scip/scip_var.h 1105 | @@ -1201,6 +1201,7 @@ SCIP_RETCODE SCIPgetVarStrongbranchFrac( 1106 | SCIP* scip, /**< SCIP data structure */ 1107 | SCIP_VAR* var, /**< variable to get strong branching values for */ 1108 | int itlim, /**< iteration limit for strong branchings */ 1109 | + SCIP_Bool idempotent, /**< should scip's state remain the same after the call (statistics, column states...), or should it be updated ? */ 1110 | SCIP_Real* down, /**< stores dual bound after branching column down */ 1111 | SCIP_Real* up, /**< stores dual bound after branching column up */ 1112 | SCIP_Bool* downvalid, /**< stores whether the returned down value is a valid dual bound, or NULL; 1113 | @@ -1284,6 +1285,7 @@ SCIP_RETCODE SCIPgetVarStrongbranchInt( 1114 | SCIP* scip, /**< SCIP data structure */ 1115 | SCIP_VAR* var, /**< variable to get strong branching values for */ 1116 | int itlim, /**< iteration limit for strong branchings */ 1117 | + SCIP_Bool idempotent, /**< should scip's state remain the same after the call (statistics, column states...), or should it be updated ? */ 1118 | SCIP_Real* down, /**< stores dual bound after branching column down */ 1119 | SCIP_Real* up, /**< stores dual bound after branching column up */ 1120 | SCIP_Bool* downvalid, /**< stores whether the returned down value is a valid dual bound, or NULL; 1121 | diff --git a/src/scip/scipdefplugins.c b/src/scip/scipdefplugins.c 1122 | index a6804158a7..dc9d4f3dac 100644 1123 | --- a/src/scip/scipdefplugins.c 1124 | +++ b/src/scip/scipdefplugins.c 1125 | @@ -123,6 +123,7 @@ SCIP_RETCODE SCIPincludeDefaultPlugins( 1126 | SCIP_CALL( SCIPincludeBranchrulePscost(scip) ); 1127 | SCIP_CALL( SCIPincludeBranchruleRandom(scip) ); 1128 | SCIP_CALL( SCIPincludeBranchruleRelpscost(scip) ); 1129 | + SCIP_CALL( SCIPincludeBranchruleVanillafullstrong(scip) ); 1130 | SCIP_CALL( SCIPincludeEventHdlrSolvingphase(scip) ); 1131 | SCIP_CALL( SCIPincludeComprLargestrepr(scip) ); 1132 | SCIP_CALL( SCIPincludeComprWeakcompr(scip) ); 1133 | diff --git a/src/scip/scipdefplugins.h b/src/scip/scipdefplugins.h 1134 | index 351167fdf3..e5857c3884 100644 1135 | --- a/src/scip/scipdefplugins.h 1136 | +++ b/src/scip/scipdefplugins.h 1137 | @@ -42,6 +42,7 @@ 1138 | #include "scip/branch_pscost.h" 1139 | #include "scip/branch_random.h" 1140 | #include "scip/branch_relpscost.h" 1141 | +#include "scip/branch_vanillafullstrong.h" 1142 | #include "scip/compr_largestrepr.h" 1143 | #include "scip/compr_weakcompr.h" 1144 | #include "scip/cons_abspower.h" 1145 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | setup( 5 | name="learn2branch", 6 | version="1.0.0", 7 | author="Maxime Gasse, Didier Chételat, Nicola Ferroni, Laurent Charlin, Andrea Lodi", 8 | install_requires=["numpy", "scipy"], 9 | packages=["learn2branch"], 10 | package_dir={"learn2branch": "."}, 11 | ) 12 | -------------------------------------------------------------------------------- /utilities.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import numpy as np 3 | import scipy.sparse as sp 4 | import pyscipopt as scip 5 | import pickle 6 | import gzip 7 | 8 | def log(str, logfile=None): 9 | str = f'[{datetime.datetime.now()}] {str}' 10 | print(str) 11 | if logfile is not None: 12 | with open(logfile, mode='a') as f: 13 | print(str, file=f) 14 | 15 | 16 | def init_scip_params(model, seed, heuristics=True, presolving=True, separating=True, conflict=True): 17 | 18 | seed = seed % 2147483648 # SCIP seed range 19 | 20 | # set up randomization 21 | model.setBoolParam('randomization/permutevars', True) 22 | model.setIntParam('randomization/permutationseed', seed) 23 | model.setIntParam('randomization/randomseedshift', seed) 24 | 25 | # separation only at root node 26 | model.setIntParam('separating/maxrounds', 0) 27 | 28 | # no restart 29 | model.setIntParam('presolving/maxrestarts', 0) 30 | 31 | # if asked, disable presolving 32 | if not presolving: 33 | model.setIntParam('presolving/maxrounds', 0) 34 | model.setIntParam('presolving/maxrestarts', 0) 35 | 36 | # if asked, disable separating (cuts) 37 | if not separating: 38 | model.setIntParam('separating/maxroundsroot', 0) 39 | 40 | # if asked, disable conflict analysis (more cuts) 41 | if not conflict: 42 | model.setBoolParam('conflict/enable', False) 43 | 44 | # if asked, disable primal heuristics 45 | if not heuristics: 46 | model.setHeuristics(scip.SCIP_PARAMSETTING.OFF) 47 | 48 | 49 | def extract_state(model, buffer=None): 50 | """ 51 | Compute a bipartite graph representation of the solver. In this 52 | representation, the variables and constraints of the MILP are the 53 | left- and right-hand side nodes, and an edge links two nodes iff the 54 | variable is involved in the constraint. Both the nodes and edges carry 55 | features. 56 | 57 | Parameters 58 | ---------- 59 | model : pyscipopt.scip.Model 60 | The current model. 61 | buffer : dict 62 | A buffer to avoid re-extracting redundant information from the solver 63 | each time. 64 | Returns 65 | ------- 66 | variable_features : dictionary of type {'names': list, 'values': np.ndarray} 67 | The features associated with the variable nodes in the bipartite graph. 68 | edge_features : dictionary of type ('names': list, 'indices': np.ndarray, 'values': np.ndarray} 69 | The features associated with the edges in the bipartite graph. 70 | This is given as a sparse matrix in COO format. 71 | constraint_features : dictionary of type {'names': list, 'values': np.ndarray} 72 | The features associated with the constraint nodes in the bipartite graph. 73 | """ 74 | if buffer is None or model.getNNodes() == 1: 75 | buffer = {} 76 | 77 | # update state from buffer if any 78 | s = model.getState(buffer['scip_state'] if 'scip_state' in buffer else None) 79 | buffer['scip_state'] = s 80 | 81 | if 'state' in buffer: 82 | obj_norm = buffer['state']['obj_norm'] 83 | else: 84 | obj_norm = np.linalg.norm(s['col']['coefs']) 85 | obj_norm = 1 if obj_norm <= 0 else obj_norm 86 | 87 | row_norms = s['row']['norms'] 88 | row_norms[row_norms == 0] = 1 89 | 90 | # Column features 91 | n_cols = len(s['col']['types']) 92 | 93 | if 'state' in buffer: 94 | col_feats = buffer['state']['col_feats'] 95 | else: 96 | col_feats = {} 97 | col_feats['type'] = np.zeros((n_cols, 4)) # BINARY INTEGER IMPLINT CONTINUOUS 98 | col_feats['type'][np.arange(n_cols), s['col']['types']] = 1 99 | col_feats['coef_normalized'] = s['col']['coefs'].reshape(-1, 1) / obj_norm 100 | 101 | col_feats['has_lb'] = ~np.isnan(s['col']['lbs']).reshape(-1, 1) 102 | col_feats['has_ub'] = ~np.isnan(s['col']['ubs']).reshape(-1, 1) 103 | col_feats['sol_is_at_lb'] = s['col']['sol_is_at_lb'].reshape(-1, 1) 104 | col_feats['sol_is_at_ub'] = s['col']['sol_is_at_ub'].reshape(-1, 1) 105 | col_feats['sol_frac'] = s['col']['solfracs'].reshape(-1, 1) 106 | col_feats['sol_frac'][s['col']['types'] == 3] = 0 # continuous have no fractionality 107 | col_feats['basis_status'] = np.zeros((n_cols, 4)) # LOWER BASIC UPPER ZERO 108 | col_feats['basis_status'][np.arange(n_cols), s['col']['basestats']] = 1 109 | col_feats['reduced_cost'] = s['col']['redcosts'].reshape(-1, 1) / obj_norm 110 | col_feats['age'] = s['col']['ages'].reshape(-1, 1) / (s['stats']['nlps'] + 5) 111 | col_feats['sol_val'] = s['col']['solvals'].reshape(-1, 1) 112 | col_feats['inc_val'] = s['col']['incvals'].reshape(-1, 1) 113 | col_feats['avg_inc_val'] = s['col']['avgincvals'].reshape(-1, 1) 114 | 115 | col_feat_names = [[k, ] if v.shape[1] == 1 else [f'{k}_{i}' for i in range(v.shape[1])] for k, v in col_feats.items()] 116 | col_feat_names = [n for names in col_feat_names for n in names] 117 | col_feat_vals = np.concatenate(list(col_feats.values()), axis=-1) 118 | 119 | variable_features = { 120 | 'names': col_feat_names, 121 | 'values': col_feat_vals,} 122 | 123 | # Row features 124 | 125 | if 'state' in buffer: 126 | row_feats = buffer['state']['row_feats'] 127 | has_lhs = buffer['state']['has_lhs'] 128 | has_rhs = buffer['state']['has_rhs'] 129 | else: 130 | row_feats = {} 131 | has_lhs = np.nonzero(~np.isnan(s['row']['lhss']))[0] 132 | has_rhs = np.nonzero(~np.isnan(s['row']['rhss']))[0] 133 | row_feats['obj_cosine_similarity'] = np.concatenate(( 134 | -s['row']['objcossims'][has_lhs], 135 | +s['row']['objcossims'][has_rhs])).reshape(-1, 1) 136 | row_feats['bias'] = np.concatenate(( 137 | -(s['row']['lhss'] / row_norms)[has_lhs], 138 | +(s['row']['rhss'] / row_norms)[has_rhs])).reshape(-1, 1) 139 | 140 | row_feats['is_tight'] = np.concatenate(( 141 | s['row']['is_at_lhs'][has_lhs], 142 | s['row']['is_at_rhs'][has_rhs])).reshape(-1, 1) 143 | 144 | row_feats['age'] = np.concatenate(( 145 | s['row']['ages'][has_lhs], 146 | s['row']['ages'][has_rhs])).reshape(-1, 1) / (s['stats']['nlps'] + 5) 147 | 148 | # # redundant with is_tight 149 | # tmp = s['row']['basestats'] # LOWER BASIC UPPER ZERO 150 | # tmp[s['row']['lhss'] == s['row']['rhss']] = 4 # LOWER == UPPER for equality constraints 151 | # tmp_l = tmp[has_lhs] 152 | # tmp_l[tmp_l == 2] = 1 # LHS UPPER -> BASIC 153 | # tmp_l[tmp_l == 4] = 2 # EQU UPPER -> UPPER 154 | # tmp_l[tmp_l == 0] = 2 # LHS LOWER -> UPPER 155 | # tmp_r = tmp[has_rhs] 156 | # tmp_r[tmp_r == 0] = 1 # RHS LOWER -> BASIC 157 | # tmp_r[tmp_r == 4] = 2 # EQU LOWER -> UPPER 158 | # tmp = np.concatenate((tmp_l, tmp_r)) - 1 # BASIC UPPER ZERO 159 | # row_feats['basis_status'] = np.zeros((len(has_lhs) + len(has_rhs), 3)) 160 | # row_feats['basis_status'][np.arange(len(has_lhs) + len(has_rhs)), tmp] = 1 161 | 162 | tmp = s['row']['dualsols'] / (row_norms * obj_norm) 163 | row_feats['dualsol_val_normalized'] = np.concatenate(( 164 | -tmp[has_lhs], 165 | +tmp[has_rhs])).reshape(-1, 1) 166 | 167 | row_feat_names = [[k, ] if v.shape[1] == 1 else [f'{k}_{i}' for i in range(v.shape[1])] for k, v in row_feats.items()] 168 | row_feat_names = [n for names in row_feat_names for n in names] 169 | row_feat_vals = np.concatenate(list(row_feats.values()), axis=-1) 170 | 171 | constraint_features = { 172 | 'names': row_feat_names, 173 | 'values': row_feat_vals,} 174 | 175 | # Edge features 176 | if 'state' in buffer: 177 | edge_row_idxs = buffer['state']['edge_row_idxs'] 178 | edge_col_idxs = buffer['state']['edge_col_idxs'] 179 | edge_feats = buffer['state']['edge_feats'] 180 | else: 181 | coef_matrix = sp.csr_matrix( 182 | (s['nzrcoef']['vals'] / row_norms[s['nzrcoef']['rowidxs']], 183 | (s['nzrcoef']['rowidxs'], s['nzrcoef']['colidxs'])), 184 | shape=(len(s['row']['nnzrs']), len(s['col']['types']))) 185 | coef_matrix = sp.vstack(( 186 | -coef_matrix[has_lhs, :], 187 | coef_matrix[has_rhs, :])).tocoo(copy=False) 188 | 189 | edge_row_idxs, edge_col_idxs = coef_matrix.row, coef_matrix.col 190 | edge_feats = {} 191 | 192 | edge_feats['coef_normalized'] = coef_matrix.data.reshape(-1, 1) 193 | 194 | edge_feat_names = [[k, ] if v.shape[1] == 1 else [f'{k}_{i}' for i in range(v.shape[1])] for k, v in edge_feats.items()] 195 | edge_feat_names = [n for names in edge_feat_names for n in names] 196 | edge_feat_indices = np.vstack([edge_row_idxs, edge_col_idxs]) 197 | edge_feat_vals = np.concatenate(list(edge_feats.values()), axis=-1) 198 | 199 | edge_features = { 200 | 'names': edge_feat_names, 201 | 'indices': edge_feat_indices, 202 | 'values': edge_feat_vals,} 203 | 204 | if 'state' not in buffer: 205 | buffer['state'] = { 206 | 'obj_norm': obj_norm, 207 | 'col_feats': col_feats, 208 | 'row_feats': row_feats, 209 | 'has_lhs': has_lhs, 210 | 'has_rhs': has_rhs, 211 | 'edge_row_idxs': edge_row_idxs, 212 | 'edge_col_idxs': edge_col_idxs, 213 | 'edge_feats': edge_feats, 214 | } 215 | 216 | return constraint_features, edge_features, variable_features 217 | 218 | 219 | def valid_seed(seed): 220 | """Check whether seed is a valid random seed or not.""" 221 | seed = int(seed) 222 | if seed < 0 or seed > 2**32 - 1: 223 | raise argparse.ArgumentTypeError( 224 | "seed must be any integer between 0 and 2**32 - 1 inclusive") 225 | return seed 226 | 227 | 228 | def compute_extended_variable_features(state, candidates): 229 | """ 230 | Utility to extract variable features only from a bipartite state representation. 231 | 232 | Parameters 233 | ---------- 234 | state : dict 235 | A bipartite state representation. 236 | candidates: list of ints 237 | List of candidate variables for which to compute features (given as indexes). 238 | 239 | Returns 240 | ------- 241 | variable_states : np.array 242 | The resulting variable states. 243 | """ 244 | constraint_features, edge_features, variable_features = state 245 | constraint_features = constraint_features['values'] 246 | edge_indices = edge_features['indices'] 247 | edge_features = edge_features['values'] 248 | variable_features = variable_features['values'] 249 | 250 | cand_states = np.zeros(( 251 | len(candidates), 252 | variable_features.shape[1] + 3*(edge_features.shape[1] + constraint_features.shape[1]), 253 | )) 254 | 255 | # re-order edges according to variable index 256 | edge_ordering = edge_indices[1].argsort() 257 | edge_indices = edge_indices[:, edge_ordering] 258 | edge_features = edge_features[edge_ordering] 259 | 260 | # gather (ordered) neighbourhood features 261 | nbr_feats = np.concatenate([ 262 | edge_features, 263 | constraint_features[edge_indices[0]] 264 | ], axis=1) 265 | 266 | # split neighborhood features by variable, along with the corresponding variable 267 | var_cuts = np.diff(edge_indices[1]).nonzero()[0]+1 268 | nbr_feats = np.split(nbr_feats, var_cuts) 269 | nbr_vars = np.split(edge_indices[1], var_cuts) 270 | assert all([all(vs[0] == vs) for vs in nbr_vars]) 271 | nbr_vars = [vs[0] for vs in nbr_vars] 272 | 273 | # process candidate variable neighborhoods only 274 | for var, nbr_id, cand_id in zip(*np.intersect1d(nbr_vars, candidates, return_indices=True)): 275 | cand_states[cand_id, :] = np.concatenate([ 276 | variable_features[var, :], 277 | nbr_feats[nbr_id].min(axis=0), 278 | nbr_feats[nbr_id].mean(axis=0), 279 | nbr_feats[nbr_id].max(axis=0)]) 280 | 281 | cand_states[np.isnan(cand_states)] = 0 282 | 283 | return cand_states 284 | 285 | 286 | def extract_khalil_variable_features(model, candidates, root_buffer): 287 | """ 288 | Extract features following Khalil et al. (2016) Learning to Branch in Mixed Integer Programming. 289 | 290 | Parameters 291 | ---------- 292 | model : pyscipopt.scip.Model 293 | The current model. 294 | candidates : list of pyscipopt.scip.Variable's 295 | A list of variables for which to compute the variable features. 296 | root_buffer : dict 297 | A buffer to avoid re-extracting redundant root node information (None to deactivate buffering). 298 | 299 | Returns 300 | ------- 301 | variable_features : 2D np.ndarray 302 | The features associated with the candidate variables. 303 | """ 304 | # update state from state_buffer if any 305 | scip_state = model.getKhalilState(root_buffer, candidates) 306 | 307 | variable_feature_names = sorted(scip_state) 308 | variable_features = np.stack([scip_state[feature_name] for feature_name in variable_feature_names], axis=1) 309 | 310 | return variable_features 311 | 312 | 313 | def preprocess_variable_features(features, interaction_augmentation, normalization): 314 | """ 315 | Features preprocessing following Khalil et al. (2016) Learning to Branch in Mixed Integer Programming. 316 | 317 | Parameters 318 | ---------- 319 | features : 2D np.ndarray 320 | The candidate variable features to preprocess. 321 | interaction_augmentation : bool 322 | Whether to augment features with 2-degree interactions (useful for linear models such as SVMs). 323 | normalization : bool 324 | Wether to normalize features in [0, 1] (i.e., query-based normalization). 325 | 326 | Returns 327 | ------- 328 | variable_features : 2D np.ndarray 329 | The preprocessed variable features. 330 | """ 331 | # 2-degree polynomial feature augmentation 332 | if interaction_augmentation: 333 | interactions = ( 334 | np.expand_dims(features, axis=-1) * \ 335 | np.expand_dims(features, axis=-2) 336 | ).reshape((features.shape[0], -1)) 337 | features = np.concatenate([features, interactions], axis=1) 338 | 339 | # query-based normalization in [0, 1] 340 | if normalization: 341 | features -= features.min(axis=0, keepdims=True) 342 | max_val = features.max(axis=0, keepdims=True) 343 | max_val[max_val == 0] = 1 344 | features /= max_val 345 | 346 | return features 347 | 348 | 349 | def load_flat_samples(filename, feat_type, label_type, augment_feats, normalize_feats): 350 | with gzip.open(filename, 'rb') as file: 351 | sample = pickle.load(file) 352 | 353 | state, khalil_state, best_cand, cands, cand_scores = sample['data'] 354 | 355 | cands = np.array(cands) 356 | cand_scores = np.array(cand_scores) 357 | 358 | cand_states = [] 359 | if feat_type in ('all', 'gcnn_agg'): 360 | cand_states.append(compute_extended_variable_features(state, cands)) 361 | if feat_type in ('all', 'khalil'): 362 | cand_states.append(khalil_state) 363 | cand_states = np.concatenate(cand_states, axis=1) 364 | 365 | best_cand_idx = np.where(cands == best_cand)[0][0] 366 | 367 | # feature preprocessing 368 | cand_states = preprocess_variable_features(cand_states, interaction_augmentation=augment_feats, normalization=normalize_feats) 369 | 370 | if label_type == 'scores': 371 | cand_labels = cand_scores 372 | 373 | elif label_type == 'ranks': 374 | cand_labels = np.empty(len(cand_scores), dtype=int) 375 | cand_labels[cand_scores.argsort()] = np.arange(len(cand_scores)) 376 | 377 | elif label_type == 'bipartite_ranks': 378 | # scores quantile discretization as in 379 | # Khalil et al. (2016) Learning to Branch in Mixed Integer Programming 380 | cand_labels = np.empty(len(cand_scores), dtype=int) 381 | cand_labels[cand_scores >= 0.8 * cand_scores.max()] = 1 382 | cand_labels[cand_scores < 0.8 * cand_scores.max()] = 0 383 | 384 | else: 385 | raise ValueError(f"Invalid label type: '{label_type}'") 386 | 387 | return cand_states, cand_labels, best_cand_idx 388 | -------------------------------------------------------------------------------- /utilities_tf.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import gzip 3 | import numpy as np 4 | 5 | import tensorflow as tf 6 | 7 | 8 | def load_batch_gcnn(sample_files): 9 | """ 10 | Loads and concatenates a bunch of samples into one mini-batch. 11 | """ 12 | c_features = [] 13 | e_indices = [] 14 | e_features = [] 15 | v_features = [] 16 | candss = [] 17 | cand_choices = [] 18 | cand_scoress = [] 19 | 20 | # load samples 21 | for filename in sample_files: 22 | with gzip.open(filename, 'rb') as f: 23 | sample = pickle.load(f) 24 | 25 | sample_state, _, sample_action, sample_cands, cand_scores = sample['data'] 26 | 27 | sample_cands = np.array(sample_cands) 28 | cand_choice = np.where(sample_cands == sample_action)[0][0] # action index relative to candidates 29 | 30 | c, e, v = sample_state 31 | c_features.append(c['values']) 32 | e_indices.append(e['indices']) 33 | e_features.append(e['values']) 34 | v_features.append(v['values']) 35 | candss.append(sample_cands) 36 | cand_choices.append(cand_choice) 37 | cand_scoress.append(cand_scores) 38 | 39 | n_cs_per_sample = [c.shape[0] for c in c_features] 40 | n_vs_per_sample = [v.shape[0] for v in v_features] 41 | n_cands_per_sample = [cds.shape[0] for cds in candss] 42 | 43 | # concatenate samples in one big graph 44 | c_features = np.concatenate(c_features, axis=0) 45 | v_features = np.concatenate(v_features, axis=0) 46 | e_features = np.concatenate(e_features, axis=0) 47 | # edge indices have to be adjusted accordingly 48 | cv_shift = np.cumsum([ 49 | [0] + n_cs_per_sample[:-1], 50 | [0] + n_vs_per_sample[:-1] 51 | ], axis=1) 52 | e_indices = np.concatenate([e_ind + cv_shift[:, j:(j+1)] 53 | for j, e_ind in enumerate(e_indices)], axis=1) 54 | # candidate indices as well 55 | candss = np.concatenate([cands + shift 56 | for cands, shift in zip(candss, cv_shift[1])]) 57 | cand_choices = np.array(cand_choices) 58 | cand_scoress = np.concatenate(cand_scoress, axis=0) 59 | 60 | # convert to tensors 61 | c_features = tf.convert_to_tensor(c_features, dtype=tf.float32) 62 | e_indices = tf.convert_to_tensor(e_indices, dtype=tf.int32) 63 | e_features = tf.convert_to_tensor(e_features, dtype=tf.float32) 64 | v_features = tf.convert_to_tensor(v_features, dtype=tf.float32) 65 | n_cs_per_sample = tf.convert_to_tensor(n_cs_per_sample, dtype=tf.int32) 66 | n_vs_per_sample = tf.convert_to_tensor(n_vs_per_sample, dtype=tf.int32) 67 | candss = tf.convert_to_tensor(candss, dtype=tf.int32) 68 | cand_choices = tf.convert_to_tensor(cand_choices, dtype=tf.int32) 69 | cand_scoress = tf.convert_to_tensor(cand_scoress, dtype=tf.float32) 70 | n_cands_per_sample = tf.convert_to_tensor(n_cands_per_sample, dtype=tf.int32) 71 | 72 | return c_features, e_indices, e_features, v_features, n_cs_per_sample, n_vs_per_sample, n_cands_per_sample, candss, cand_choices, cand_scoress 73 | --------------------------------------------------------------------------------