├── LICENSE ├── README.md ├── SCIP_settings └── randomness_control.set ├── context.py ├── data.py ├── data ├── binpacking-66-132 │ └── .gitkeep ├── capfac-100-100 │ └── .gitkeep ├── combauct-100-500 │ └── .gitkeep ├── indset-500 │ └── .gitkeep ├── load_balancing │ └── .gitkeep ├── maxcut-54-134 │ └── .gitkeep ├── miplib │ ├── .gitkeep │ └── sequence.npy ├── nnv │ └── .gitkeep ├── packing-60-60 │ └── .gitkeep └── raw │ └── .gitkeep ├── data_generation ├── data_filter.py ├── generate_ecole.py └── generate_tang.py ├── dataset.py ├── example ├── model │ ├── capfac-100-100 │ │ ├── Z_k1 │ │ ├── Z_k2 │ │ ├── model_k1 │ │ └── model_k2 │ ├── combauct-100-500 │ │ ├── Z_k1 │ │ ├── Z_k2 │ │ ├── model_k1 │ │ └── model_k2 │ └── indset-500 │ │ ├── Z_k1 │ │ ├── Z_k2 │ │ ├── model_k1 │ │ └── model_k2 └── restricted_space │ ├── capfac-100-100 │ ├── inst_agnostic_action.npy │ └── restricted_actions_space.npy │ ├── combauct-100-500 │ ├── inst_agnostic_action.npy │ └── restricted_actions_space.npy │ └── indset-500 │ ├── inst_agnostic_action.npy │ └── restricted_actions_space.npy ├── model.py ├── requirements.txt ├── restricted_space ├── binpacking-66-132 │ └── .gitkeep ├── capfac-100-100 │ └── .gitkeep ├── combauct-100-500 │ └── .gitkeep ├── indset-500 │ └── .gitkeep ├── load_balancing │ └── .gitkeep ├── maxcut-54-134 │ └── .gitkeep ├── miplib │ └── .gitkeep ├── nnv │ └── .gitkeep └── packing-60-60 │ └── .gitkeep ├── restricted_space_generation ├── explore_subspace.py ├── find_best_ins_agnostic.py ├── get_actions.py └── restricted_space_generation.ipynb ├── states.py ├── states_helpers.py ├── test_k2.py ├── train_k1.py ├── train_k2.py └── utils.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 mit-wu-lab 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 | # Learning to Configure Separators in Branch-and-Cut 2 | 3 | This directory is the official implementation for our NeurIPS 2023 paper *Learning to Configure Separators in Branch-and-Cut*. This README file provides instructions on environment setup, data collection, restricted configuration space construction, model training and testing. 4 | 5 | ## Relevant Links 6 | You may find this project at: [Project Website](https://mit-wu-lab.github.io/learning-to-configure-separators/), [arXiv](https://arxiv.org/abs/2311.05650), [OpenReview](https://openreview.net/forum?id=gf5xJVQS5p). 7 | ``` 8 | @inproceedings{li2023learning, 9 | title={Learning to Configure Separators in Branch-and-Cut}, 10 | author={Sirui Li and Wenbin Ouyang and Max B. Paulus and Cathy Wu}, 11 | booktitle={Thirty-seventh Conference on Neural Information Processing Systems}, 12 | year={2023}, 13 | url={https://openreview.net/forum?id=gf5xJVQS5p} 14 | } 15 | ``` 16 | 17 | ## Environment Setup 18 | Our implementation uses python 3.8.13 and Pytorch 1.9.0. The other package dependencies are listed in `requirement.txt` and can be installed with the following command: 19 | ``` 20 | pip install -r requirements.txt 21 | ``` 22 | Beside the listed python packages, we also use a custom version of the [SCIP](https://www.scipopt.org) solver (v7.0.2) and the [PySCIPOpt](https://github.com/scipopt/PySCIPOpt) interface (v3.3.0), kindly provided by the authors of [1], that allows activating and deactivating separators at intermediate separation rounds during each solve (see Appendix A.6.3 for details). We are informed that the authors of [1] will make the code publicly available soon. The SCIP and PySCIPOpt plug-in follows a similar design to the one found at https://github.com/avrech/learning2cut, which is publicly available. 23 | 24 | > [1] Paulus, Max B., et al. "Learning to cut by looking ahead: Cutting plane selection via imitation learning." International conference on machine learning. PMLR, 2022. 25 | 26 | ## Data Collection 27 | We provide code for data generation in the `./data_generation` folder for Tang et al. [2] and Ecole [3] benchmarks. We also provide download instructions and data filtering code for real-world large-scale benchmarks (NNV, MIPLIB, Load Balancing). The collected data are stored in the `./data` folder. 28 | 29 | ### Generating instances from Tang et al. and Ecole 30 | One can generate the data simply by: 31 | ``` 32 | cd data_generation 33 | python generate_tang.py 34 | python generate_ecole.py 35 | ``` 36 | The default save directory is `./data`, and can be changed by specifying the `--path_to_data_dir` argument of two programs. One can refer to [2] and [3] for changing MILP parameters. We do not filter instances from Tange et al. and Ecole. 37 | 38 | - An example Packing instance from Tang et al. can be found in `./example/example_packing_instance.mps`. 39 | - We include the size of the instances in the suffix. For example, if the save directory is `./data`, packing instances will be saved in `./data/packing-60-60` 40 | 41 | > [2] Tang, Yunhao, Shipra Agrawal, and Yuri Faenza. "Reinforcement learning for integer programming: Learning to cut." International conference on machine learning. PMLR, 2020. 42 | > [3] Prouvost, Antoine, et al. "Ecole: A gym-like library for machine learning in combinatorial optimization solvers." arXiv preprint arXiv:2011.06069 (2020). 43 | 44 | ### Downloading instances for the real-world benchmarks 45 | The NN Verification dataset can be downloaded at https://github.com/deepmind/deepmind-research/tree/master/neural_mip_solving. 46 | The MIPLIB dataset can be downloaded at https://miplib.zib.de/download.html. 47 | The Load Balancing dataset can be downloaded at https://github.com/ds4dm/ml4co-competition/blob/main/START.md. 48 | 49 | We filter out some of the instances from the real-world benchmarks with the following code (see Appendix A.6.5 for details): 50 | ``` 51 | cd data_generation 52 | python data_filter.py --instances [MILP class to be filtered] \ 53 | --path_to_raw_data_dir [directory of the downloaded data] \ 54 | --path_to_data_dir [save directory] \ 55 | --time_limit [time limit] \ 56 | --gap_limit [gap limit] \ 57 | --index_end [total number of the instances to consider] 58 | ``` 59 | Specifically, we can run the following commands to filter NNV, MIPLIB, and Load Balancing, assuming the raw data is saved in the `./data/raw/{instances}` directory: 60 | ``` 61 | cd data_generation 62 | python data_filter.py --instances nnv --path_to_raw_data_dir ../data/raw --path_to_data_dir ../data --time_limit 120 --index_end 1800 63 | python data_filter.py --instances load_balancing --path_to_raw_data_dir ../data/raw --path_to_data_dir ../data --time_limit 300 --gap_limit 0.1 --index_end 1600 64 | python data_filter.py --instances miplib --path_to_raw_data_dir ../data/raw --path_to_data_dir ../data --time_limit 300 --gap_limit 0.1 --index_end 1065 65 | ``` 66 | The dataset after filtering will be stored in the `./data` folder. 67 | 68 | 69 | ### Splitting datasets into train, validation and test sets 70 | After generating or filtering the problem instances, one can separate the datasets into train, validation and test sets. The default saving directories are `./data/{instance}_train`, `./data/{instance}_val` and `./data/{instance}_test`. And the default ratio for these three sets is 8:1:1. 71 | 72 | For example, after generating 1000 packing instances, copy and paste the first 800 instances from `./data/packing-60-60` to `./data/packing-60-60_train`, 801st to 900th instances to `./data/packing-60-60_val` and the last 100 instances to`./data/packing-60-60_test`. 73 | 74 | For the hetergeneous dataset MIPLIB, to ensure the distributions of the train, validation and test sets are roughly the same, a random shuffle of the instances are needed. Assume after filtering, $n$ instances are selected. Then, a randomize permutation from 1 to n should be stored in `./data/miplib/sequence.npy` to indicate the sequence of the instances after shuffling. 75 | 76 | ---------------------------------------------- 77 | 78 | ## Restricted Space Construction 79 | We provide instructions on constructing the restricted configuration space, following Alg.1 in Appendix A.3. 80 | ### Searching a good instance-agnostic configuration 81 | We first use random search to find a good instance-agnostic configuration, which we later use to construct the "Near Best Random" initial configuration subset (See Appendix A.3). 82 | ``` 83 | cd restricted_space_generation 84 | python find_best_ins_agnostic.py --instances [the MILP class] \ 85 | --path_to_data_dir [directory of the data] \ 86 | --gap_limit [gap limit] 87 | ``` 88 | - For example, one can run the following code for the Packing MILP class in Tang et al.: 89 | ``` 90 | cd restricted_space_generation 91 | python find_best_ins_agnostic.py --instances packing-60-60 --path_to_data_dir ../data --gap_limit 0.0 92 | ``` 93 | The result will be stored as a `best_ins_agnostic.npy` file in the `./restricted_space/{instances}` folder. 94 | 95 | ### Sampling the large initial configuration subset 96 | We apply the "Near Zero" and "Near Best Random" strategies as described in Appendix A.3.1 to construct the large initial configuration subset for later constructing the restricted space. 97 | ``` 98 | cd restricted_space_generation 99 | python explore_subspace.py --instances [the MILP class] \ 100 | --path_to_data_dir [directory of the data] \ 101 | --path_to_ins_agnst [directory of the best instance-agnostic configuration] \ 102 | --mode [the strategy to apply] \ 103 | ``` 104 | - For example, one can run the following code for applying the "Near Zero" strategy to the Packing MILP class in Tang et al.: 105 | ``` 106 | cd restricted_space_generation 107 | python explore_subspace.py --instances packing-60-60 --path_to_data_dir ../data --path_to_ins_agnst ../restricted_space/ --mode near_zero 108 | ``` 109 | - and the following code for applying "Near Best Random": 110 | ``` 111 | cd restricted_space_generation 112 | python explore_subspace.py --instances packing-60-60 --path_to_data_dir ../data --path_to_ins_agnst ../restricted_space/ --mode near_mask 113 | ``` 114 | 115 | The results will be stored in the `./restricted_space/{instances}/{strategy_name}` folder. 116 | 117 | We then run the following code to combine the above sampled configurations into a single configuration subset $S$: 118 | ``` 119 | cd restricted_space_generation 120 | python get_actions.py --instances [the MILP class] \ 121 | --path_to_space_dir [directory of the sampled configurations] 122 | ``` 123 | The code will generate two `.npy` files: `action_space.npy` contains all the configurations in this set $S$; `action_scores.npy` includes the relative time improvement of each configuration in $S$ on each MILP instance in $\mathcal{K}_{small}$. 124 | 125 | - For example, one can run the following code to abtain the configuration subset $S$ for Packing MILP class. 126 | ``` 127 | cd restricted_space_generation 128 | python get_actions.py --instances packing-60-60 --path_to_space_dir ../restricted_space/ 129 | ``` 130 | 131 | ### Constructing the restricted space $A$ 132 | We construct the restricted space $A$ by running `./restricted_space/restricted_space_generation.ipynb` which analyzes `action_space.npy` and `action_scores.npy`. The MILP class can be specified by changing the `instances` variable in the first cell. 133 | 134 | By adjusting `our_size` (candidate size of $A$) and `Bs` (candidate threshold $b$) in the $5^{th}$ cell, different plots will be generated from the $6^{th}$ and $7^{th}$ cells (similar to Fig. 6 and 7 in Appendix A.3.2). One should choose $|A|$ and $b$ based on the plots, as described in Appendix A.3.2. 135 | 136 | Finally, we run the last cell to store the restricted space $A$ in `./restricted_space/{instances}/restricted_actions_space.npy`, and the instance-agonstic ERM configuration (one of ours heuristic variants) will be stored to `./restricted_space/{instances}/inst_agnostic_action.npy`. 137 | 138 | - Example restricted action space could be found in `./example/restricted_space/{instances}/restricted_actions_space.npy` 139 | 140 | ## Training 141 | We provide instructions for training the models for $k = 1, 2$ at separation rounds $n_1, n_2$ within the restricted configuration space $A$. 142 | 143 | For training the model $\tilde{f}^{1}$ at $k = 1$ (we set $n_1=0$ across all benchmarks), we run the following: 144 | ``` 145 | python train_k1.py --instances [the MILP class] 146 | ``` 147 | The default directory of the restricted action space is `./restricted_space/{instance}/restricted_actions_space.npy`. One can change that by modifying the `--actiondir` and the `--actions_name`. The default directory for saving the model and $Z$ matrix is `./model/{instances}_k1`. One can change the save directory by modifying the `--savedir` argument. 148 | 149 | For training the model $\tilde{f}^{2}$ at $k = 2$, we then run: 150 | ``` 151 | python train_k2.py --instances [the MILP class] \ 152 | --model_0_path [path to the model of k=1] \ 153 | --Z_0_path [Matrix Z for k=1] \ 154 | --step_k2 [the value of n_2] 155 | ``` 156 | - For example, one can run the following code to train the model $\tilde{f}^{2}$ at $k = 2$ for the Packing MILP class in Tang et al.: 157 | ``` 158 | python train_k2.py --instances packing-60-60 --model_0_path ./model/packing-60-60_k1/model-42 --Z_0_path ./model/packing-60-60_k1/Z-42 --step_k2 5 159 | ``` 160 | The default directory for saving the model and $Z$ matrix is at $k_2$ is `./model/{instances}_k2`. One can change the save directory by modifying the `--savedir` argument. 161 | 162 | - Example pretrained models $\tilde{f}^{1}$, $\tilde{f}^{2}$ and the associated $Z^1, Z^2$ matrices can be found at: `./example/model/{instances}/model_k1`, `./example/model/{instances}/model_k2`, `./example/model/{instances}/Z_k1`, and `./example/model/{instances}/Z_k2`. 163 | 164 | ## Testing 165 | 166 | To test the models $\tilde{f}^{1}$ and $\tilde{f}^{2}$ at $k = 1, 2$, we can run: 167 | ``` 168 | python test_k2.py --instances [the MILP class] \ 169 | --model_0_path [path the model of k=1] \ 170 | --Z_0_path [Matrix Z for k=1] \ 171 | --model_path [path the model of k=2] \ 172 | --Z_path [Matrix Z for k=2] \ 173 | --step_k2 [the value of n_2] 174 | ``` 175 | - For example, one can run the following code to test the models for the Packing MILP class in Tang et al.: 176 | ``` 177 | python test_k2.py --instances packing-60-60 --model_0_path ./model/packing-60-60_k1/model-42 --Z_0_path ./model/packing-60-60_k1/Z-42 --model_path ./model/packing-60-60_k2/model-42 --Z_path ./model/packing-60-60_k2/Z-42 --step_k2 5 178 | ``` 179 | The testing results will be printed to the console. 180 | -------------------------------------------------------------------------------- /SCIP_settings/randomness_control.set: -------------------------------------------------------------------------------- 1 | # SCIP version 7.0.2 2 | 3 | # global shift of all random seeds in the plugins and the LP random seed 4 | # [type: int, advanced: FALSE, range: [0,2147483647], default: 0] 5 | randomization/randomseedshift = 0 6 | 7 | # seed value for permuting the problem after reading/transformation (0: no permutation) 8 | # [type: int, advanced: FALSE, range: [0,2147483647], default: 0] 9 | randomization/permutationseed = 0 10 | 11 | # should order of constraints be permuted (depends on permutationseed)? 12 | # [type: bool, advanced: TRUE, range: {TRUE,FALSE}, default: TRUE] 13 | randomization/permuteconss = TRUE 14 | 15 | # should order of variables be permuted (depends on permutationseed)? 16 | # [type: bool, advanced: TRUE, range: {TRUE,FALSE}, default: FALSE] 17 | randomization/permutevars = FALSE 18 | 19 | # random seed for LP solver, e.g. for perturbations in the simplex (0: LP default) 20 | # [type: int, advanced: FALSE, range: [0,2147483647], default: 0] 21 | randomization/lpseed = 43 22 | 23 | # set different random seeds in each concurrent solver? 24 | # [type: bool, advanced: FALSE, range: {TRUE,FALSE}, default: TRUE] 25 | concurrent/changeseeds = TRUE 26 | 27 | # maximum number of solutions that will be shared in a one synchronization 28 | # [type: int, advanced: FALSE, range: [0,2147483647], default: 5131912] 29 | concurrent/initseed = 5131912 30 | 31 | # initial random seed value 32 | # [type: int, advanced: FALSE, range: [0,2147483647], default: 41] 33 | branching/random/seed = 43 34 | 35 | # start seed for random number generation 36 | # [type: int, advanced: TRUE, range: [0,2147483647], default: 5] 37 | branching/relpscost/startrandseed = 43 38 | 39 | # initial random seed for bandit algorithms and random decisions by neighborhoods 40 | # [type: int, advanced: FALSE, range: [0,2147483647], default: 113] 41 | heuristics/alns/seed = 43 42 | 43 | # should random seeds of sub-SCIPs be altered to increase diversification? 44 | # [type: bool, advanced: TRUE, range: {TRUE,FALSE}, default: FALSE] 45 | heuristics/alns/subsciprandseeds = FALSE 46 | 47 | # initial seed used for random tie-breaking in cut selection 48 | # [type: int, advanced: FALSE, range: [0,2147483647], default: 24301] 49 | separating/zerohalf/initseed = 43 -------------------------------------------------------------------------------- /context.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import utils as _utils 3 | import data as _data 4 | import states_helpers as _helpers 5 | import torch 6 | import torch.nn as nn 7 | import pyscipopt as pyopt 8 | 9 | path_to_randomness_control_set = './SCIP_settings/randomness_control.set' 10 | 11 | def getActionContext(statestr, input_context, action): 12 | if statestr == 'bandit': 13 | action_context = input_context 14 | action_context.x_sepas = torch.tensor(action, dtype=torch.float) 15 | 16 | else: 17 | raise ValueError(f'Unknown state identifier: {statestr}') 18 | 19 | return action_context 20 | 21 | def getUpdatedInputContext(model, instance_type, cuts=None, round_num=0, max_sepa_round_num=10, mask=None): 22 | rows = model.getLPRowsData() 23 | cols = model.getLPColsData() 24 | 25 | if type(cuts) != type(None): 26 | n_cuts = len(cuts) 27 | else: 28 | n_cuts = 0 29 | n_row = len(rows) 30 | n_cols = len(cols) 31 | 32 | # the model might not use all of this, in this case, we just create 33 | # mock inputs, so we don't have to rewrite the rest of the code. 34 | # this makes good sense, even though it is computationally stupid. 35 | 36 | cut_input_scores = _helpers.computeInputScores(cuts, model) 37 | 38 | row_input_scores = _helpers.computeInputScores(rows, model) 39 | 40 | # cut_lookahead_scores = _helpers.computeLookaheadScores(cuts, model) 41 | cut_lookahead_scores = np.ones((n_cuts, 3)) 42 | cut_lookahead_scores[:, 2] = 2 # not sure if there were any checks for consistency 43 | 44 | row_features = _helpers.computeRowFeatures1(rows, model, round_num=round_num / max_sepa_round_num) 45 | col_features = _helpers.computeColFeatures1(cols, model, round_num=round_num / max_sepa_round_num) 46 | cut_features = _helpers.computeRowFeatures1(cuts, model, round_num=round_num / max_sepa_round_num) 47 | 48 | cut_parallelism = _helpers.computeCutParallelism(cuts, model) 49 | 50 | cutrow_parallelism = _helpers.computeCutRowParallelism(cuts, rows, model) 51 | 52 | row_coefs = _helpers.computeCoefs(rows, cols, model) 53 | 54 | # dictionary, int: tuple(list, list) 55 | # key is position of cut, then ixs, vals 56 | cut_coefs = _helpers.computeCoefs(cuts, cols, model) 57 | 58 | sepa_settings = np.ones(len(_helpers.SCIP_CUT_IDENTIFIERS_TO_NUMS)) 59 | 60 | sepa_features = _helpers.computeSepaFeatures1(model, round_num=round_num / max_sepa_round_num) 61 | 62 | raw_data = { 63 | 'cut_input_scores.npy': cut_input_scores, 64 | 'row_input_scores.npy': row_input_scores, 65 | 'cut_lookahead_scores.npy': cut_lookahead_scores, 66 | 'row_features.pkl': row_features, 67 | 'col_features.pkl': col_features, 68 | 'cut_features.pkl': cut_features, 69 | 'cut_parallelism.npy': cut_parallelism, 70 | 'cutrow_parallelism.npy': cutrow_parallelism, 71 | 'row_coefs.pkl': row_coefs, 72 | 'cut_coefs.pkl': cut_coefs, 73 | 'sepa_settings.pkl': sepa_settings, 74 | 'masks.npy': np.zeros(len(sepa_settings)) if mask is None else mask, 75 | 'sepa_features.pkl': sepa_features 76 | } 77 | 78 | min_parall = _data.MyData.get_minparallelism(instance_type) 79 | maxnum_cutixs, maxnum_rowixs, maxnum_rowcolixs = _data.MyData.get_maxnums(instance_type) 80 | processed_data =_data.MyData.from_rawdata(raw_data, min_parall, maxnum_cutixs, maxnum_rowixs, maxnum_rowcolixs) 81 | inp = _data.MyData(*processed_data) 82 | return inp 83 | 84 | class SepaManager_SM_Context(pyopt.Sepa): 85 | # Defaults (shouldn't matter) 86 | SEPA_NAME = '#SM' 87 | SEPA_DESC = 'special sepa manager' 88 | SEPA_FREQ = 1 89 | SEPA_MAXBOUNDDIST = 1.0 90 | SEPA_USESSUBSCIP = False 91 | SEPA_DELAY = False 92 | SEPA_PRIORITY = 1 93 | 94 | # Info record 95 | def __init__(self): 96 | super().__init__() 97 | self.History_SM = [] 98 | 99 | def sepaexeclp(self): 100 | self.sepa_round += 1 101 | if self.sepa_round == self.context_step: 102 | self.contextAtStep = getUpdatedInputContext( 103 | self.model, 104 | self.instance_type, 105 | [], 106 | round_num=self.context_step, 107 | ) 108 | self.model.interruptSolve() 109 | self.interrupt_flag = True 110 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 111 | if self.interrupt_flag == True: 112 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 113 | 114 | if self.sepa_round in self.orders: 115 | for index in range(len(self.sepa_list)): 116 | on_or_off = 1 117 | # data collection: search trajectory 118 | if self.orders[self.sepa_round][index,0] == 0: 119 | on_or_off = -1 120 | self.model.setParam(f'separating/{self.sepa_list[index]}/freq', on_or_off) 121 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 122 | 123 | def sepaexecsol(self): 124 | # actual behaviour is implemented in method self.main 125 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 126 | 127 | def addModel(self, model, orders={}, context_step=-1, instance_type="packing-60-60"): 128 | r''' 129 | Call self.addModel(model) instead of model.addSeparator(self, **kwargs) 130 | # max_sepa_round_norm is the constant used to normalize the round feature in the network. It is only used when 131 | # a network is used to select sepa-settings or we need to save_state for neural network input. 132 | ''' 133 | self._check_inputs(model) 134 | model.includeSepa( 135 | self, 136 | self.SEPA_NAME, 137 | self.SEPA_DESC, 138 | self.SEPA_PRIORITY, 139 | self.SEPA_FREQ, 140 | self.SEPA_MAXBOUNDDIST, 141 | self.SEPA_USESSUBSCIP, 142 | self.SEPA_DELAY) 143 | self.model.setParam(f'separating/#SM/freq', 1) 144 | self.model.setParam(f'separating/closecuts/freq', -1) 145 | self.sepa_round = 0 146 | self.orders = orders 147 | self.context_step = context_step 148 | self.interrupt_flag = False 149 | self.contextAtStep = None 150 | self.instance_type = instance_type 151 | self.sepa_list = [ 152 | # 'closecuts', 153 | 'disjunctive', 154 | # '#SM', 155 | # '#CS', 156 | 'convexproj', 157 | 'gauge', 158 | 'impliedbounds', 159 | 'intobj', 160 | 'gomory', 161 | 'cgmip', 162 | 'strongcg', 163 | 'aggregation', 164 | 'clique', 165 | 'zerohalf', 166 | 'mcf', 167 | 'eccuts', 168 | 'oddcycle', 169 | 'flowcover', 170 | 'cmir', 171 | 'rapidlearning' 172 | ] 173 | assert self.model == model # PySCIPOpt sets that in pyopt.Sepa 174 | 175 | def get_sepa_round(self): 176 | return self.sepa_round 177 | 178 | def get_context(self): 179 | return self.contextAtStep 180 | 181 | def _check_inputs(self, model): 182 | assert isinstance(model, pyopt.Model) 183 | # this checks that all attributes to includeSepa are correctly specified. 184 | assert isinstance(self.SEPA_NAME, str) 185 | assert isinstance(self.SEPA_DESC, str) 186 | assert isinstance(self.SEPA_PRIORITY, int) 187 | assert isinstance(self.SEPA_FREQ, int) 188 | assert isinstance(self.SEPA_MAXBOUNDDIST, float) 189 | assert isinstance(self.SEPA_USESSUBSCIP, bool) 190 | assert isinstance(self.SEPA_DELAY, bool) 191 | 192 | def getInputContextAtStep(action_step0, path_to_problem, step, instance_type, gap_limit=0.00): 193 | sepa_list = [ 194 | # 'closecuts', 195 | 'disjunctive', 196 | # '#SM', 197 | # '#CS', 198 | 'convexproj', 199 | 'gauge', 200 | 'impliedbounds', 201 | 'intobj', 202 | 'gomory', 203 | 'cgmip', 204 | 'strongcg', 205 | 'aggregation', 206 | 'clique', 207 | 'zerohalf', 208 | 'mcf', 209 | 'eccuts', 210 | 'oddcycle', 211 | 'flowcover', 212 | 'cmir', 213 | 'rapidlearning' 214 | ] 215 | 216 | model = pyopt.Model() 217 | model.hideOutput(1) 218 | model.readProblem(path_to_problem) 219 | model.readParams(path_to_randomness_control_set) 220 | model.setRealParam('limits/gap', gap_limit) 221 | if type(action_step0) != type(None): 222 | for index in range(len(sepa_list)): 223 | on_or_off = 1 224 | # data collection: search trajectory 225 | if action_step0[index,0] == 0: 226 | on_or_off = -1 227 | model.setParam(f'separating/{sepa_list[index]}/freq', on_or_off) 228 | sepa_manager_SM = SepaManager_SM_Context() 229 | sepa_manager_SM.addModel(model, context_step=step, instance_type=instance_type) 230 | model.optimize() 231 | return sepa_manager_SM.get_context() 232 | 233 | class SepaManager_SM_Context_Actions(pyopt.Sepa): 234 | # Defaults (shouldn't matter) 235 | SEPA_NAME = '#SM' 236 | SEPA_DESC = 'special sepa manager' 237 | SEPA_FREQ = 1 238 | SEPA_MAXBOUNDDIST = 1.0 239 | SEPA_USESSUBSCIP = False 240 | SEPA_DELAY = False 241 | SEPA_PRIORITY = 1 242 | 243 | # Info record 244 | def __init__(self): 245 | super().__init__() 246 | self.History_SM = [] 247 | 248 | def sepaexeclp(self): 249 | self.sepa_round += 1 250 | if self.sepa_round == self.context_step: 251 | self.contextAtStep = getUpdatedInputContext( 252 | self.model, 253 | self.instance_type, 254 | [], 255 | round_num=self.context_step, 256 | ) 257 | self.model.interruptSolve() 258 | self.interrupt_flag = True 259 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 260 | if self.interrupt_flag == True: 261 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 262 | 263 | for Bandit, bandit_step in self.Bandits: 264 | if self.sepa_round == bandit_step: 265 | cur_input_context = getUpdatedInputContext( 266 | self.model, 267 | self.instance_type, 268 | [], 269 | round_num=self.sepa_round, 270 | ) 271 | action_tmp, _ = Bandit.getActions(cur_input_context, 1, eva=1) 272 | action = action_tmp[0] 273 | for index in range(len(self.sepa_list)): 274 | on_or_off = 1 275 | # data collection: search trajectory 276 | if action[index, 0] < 0.5: 277 | on_or_off = -1 278 | self.model.setParam(f'separating/{self.sepa_list[index]}/freq', on_or_off) 279 | self.actions[self.sepa_round] = action 280 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 281 | 282 | def sepaexecsol(self): 283 | # actual behaviour is implemented in method self.main 284 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 285 | 286 | def addModel(self, model, Bandits, context_step, instance_type): 287 | r''' 288 | Call self.addModel(model) instead of model.addSeparator(self, **kwargs) 289 | # max_sepa_round_norm is the constant used to normalize the round feature in the network. It is only used when 290 | # a network is used to select sepa-settings or we need to save_state for neural network input. 291 | ''' 292 | self._check_inputs(model) 293 | model.includeSepa( 294 | self, 295 | self.SEPA_NAME, 296 | self.SEPA_DESC, 297 | self.SEPA_PRIORITY, 298 | self.SEPA_FREQ, 299 | self.SEPA_MAXBOUNDDIST, 300 | self.SEPA_USESSUBSCIP, 301 | self.SEPA_DELAY) 302 | self.model.setParam(f'separating/#SM/freq', 1) 303 | self.model.setParam(f'separating/closecuts/freq', -1) 304 | self.sepa_round = -1 305 | self.Bandits = Bandits 306 | self.context_step = context_step 307 | self.interrupt_flag = False 308 | self.contextAtStep = None 309 | self.instance_type = instance_type 310 | self.actions = {} 311 | self.sepa_list = [ 312 | # 'closecuts', 313 | 'disjunctive', 314 | # '#SM', 315 | # '#CS', 316 | 'convexproj', 317 | 'gauge', 318 | 'impliedbounds', 319 | 'intobj', 320 | 'gomory', 321 | 'cgmip', 322 | 'strongcg', 323 | 'aggregation', 324 | 'clique', 325 | 'zerohalf', 326 | 'mcf', 327 | 'eccuts', 328 | 'oddcycle', 329 | 'flowcover', 330 | 'cmir', 331 | 'rapidlearning' 332 | ] 333 | assert self.model == model # PySCIPOpt sets that in pyopt.Sepa 334 | 335 | def get_sepa_round(self): 336 | return self.sepa_round 337 | 338 | def get_context(self): 339 | return self.contextAtStep 340 | 341 | def get_actions(self): 342 | return self.actions 343 | 344 | def _check_inputs(self, model): 345 | assert isinstance(model, pyopt.Model) 346 | # this checks that all attributes to includeSepa are correctly specified. 347 | assert isinstance(self.SEPA_NAME, str) 348 | assert isinstance(self.SEPA_DESC, str) 349 | assert isinstance(self.SEPA_PRIORITY, int) 350 | assert isinstance(self.SEPA_FREQ, int) 351 | assert isinstance(self.SEPA_MAXBOUNDDIST, float) 352 | assert isinstance(self.SEPA_USESSUBSCIP, bool) 353 | assert isinstance(self.SEPA_DELAY, bool) 354 | 355 | def getInputContextAndInstruction(path_to_problem, step, instance_type, Bandits, gap_limit=0.00): 356 | model = pyopt.Model() 357 | model.hideOutput(1) 358 | model.readProblem(path_to_problem) 359 | model.readParams(path_to_randomness_control_set) 360 | model.setRealParam('limits/gap', gap_limit) 361 | sepa_manager_SM = SepaManager_SM_Context_Actions() 362 | sepa_manager_SM.addModel(model, Bandits=Bandits, context_step=step, instance_type=instance_type) 363 | model.optimize() 364 | return sepa_manager_SM.get_context(), sepa_manager_SM.get_actions() -------------------------------------------------------------------------------- /data.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import numpy as np 3 | import torch 4 | from torch_geometric.data import Data 5 | import json 6 | import numpy as np 7 | import os 8 | 9 | import utils as _utils 10 | 11 | X_CUTS_DIM = 26 12 | LPVALS_DIM = 2 13 | 14 | ROW_DIM = 39 + 1 + 2 # prev 39 # prev 39 + 1 lol 15 | CUT_DIM = 39 + 1 + 2 # prev 39 # prev 39 + 1 lol 16 | EDGE_DIM_CUTS = 2 17 | EDGE_DIM_ROWS = 1 18 | COL_DIM = 17 + 1 # prev 17 19 | N_SEPAS = 17 # 20 | SEPA_DIM = 22 + 1 # 21 | EDGE_DIM_SEPAS = 1 # 22 | 23 | 24 | class MyData(Data): 25 | ### 26 | x_cuts_dim = X_CUTS_DIM 27 | lpvals_dim = LPVALS_DIM 28 | 29 | row_dim = ROW_DIM 30 | cut_dim = CUT_DIM 31 | edge_dim_cuts = EDGE_DIM_CUTS 32 | edge_dim_rows = EDGE_DIM_ROWS 33 | col_dim = COL_DIM 34 | sepa_dim = SEPA_DIM # 35 | edge_dim_sepas = EDGE_DIM_SEPAS # 36 | ### 37 | 38 | def __init__( 39 | self, 40 | x_cuts, 41 | x_rows, 42 | x_cols, 43 | edge_index_cuts, # 44 | edge_vals_cuts, 45 | edge_index_rows, 46 | edge_vals_rows, 47 | edge_index_self, 48 | edge_vals_self, 49 | edge_index_rowcols, 50 | edge_vals_rowcols, 51 | lpvals, 52 | sepa_settings, # 53 | masks, # 54 | x_sepas, edge_index_sepa_cols, edge_vals_sepa_cols, 55 | edge_index_sepa_rows, edge_vals_sepa_rows, edge_index_sepa_self, edge_vals_sepa_self): # 56 | 57 | super().__init__() 58 | self.x_cuts = x_cuts 59 | self.x_rows = x_rows 60 | self.x_cols = x_cols 61 | 62 | self.edge_index_cuts = edge_index_cuts 63 | self.edge_vals_cuts = edge_vals_cuts 64 | 65 | self.edge_index_rows = edge_index_rows 66 | self.edge_vals_rows = edge_vals_rows 67 | 68 | self.edge_index_self = edge_index_self 69 | self.edge_vals_self = edge_vals_self 70 | 71 | self.edge_index_rowcols = edge_index_rowcols 72 | self.edge_vals_rowcols = edge_vals_rowcols 73 | 74 | self.lpvals = lpvals # first col is old_lp, second col is new_lp 75 | 76 | self.sepa_settings = sepa_settings # 77 | self.masks = masks # 78 | 79 | self.x_sepas = x_sepas # 80 | self.edge_index_sepa_cols = edge_index_sepa_cols # 81 | self.edge_vals_sepa_cols = edge_vals_sepa_cols # 82 | self.edge_index_sepa_rows = edge_index_sepa_rows # 83 | self.edge_vals_sepa_rows = edge_vals_sepa_rows # 84 | self.edge_index_sepa_self = edge_index_sepa_self # 85 | self.edge_vals_sepa_self = edge_vals_sepa_self # 86 | 87 | def __inc__(self, key, value, *args, **kwargs): 88 | if key == 'x_cuts': 89 | inc = 0 90 | elif key == 'x_rows': 91 | inc = 0 92 | elif key == 'x_cols': 93 | inc = 0 94 | elif key == 'lpvals': 95 | inc = 0 96 | elif key == 'sepa_settings': # 97 | inc = 0 98 | elif key == 'masks': # 99 | inc = 0 100 | elif key == 'x_sepas': # 101 | inc = 0 102 | elif key == 'edge_index_sepa_cols': # 103 | inc = torch.tensor([[N_SEPAS], 104 | [self.x_cols.size(0)]]) 105 | elif key == 'edge_vals_sepa_cols': # 106 | inc = 0 107 | elif key == 'edge_index_sepa_rows': # 108 | inc = torch.tensor([[N_SEPAS], 109 | [self.x_rows.size(0)]]) 110 | elif key == 'edge_vals_sepa_rows': # 111 | inc = 0 112 | elif key == 'edge_index_sepa_self': # 113 | inc = torch.tensor([[N_SEPAS], 114 | [N_SEPAS]]) 115 | elif key == 'edge_vals_sepa_self': # 116 | inc = 0 117 | elif key == 'edge_index_cuts': 118 | inc = torch.tensor([ 119 | [self.x_cuts.size(0)], 120 | [self.x_cols.size(0)]]) 121 | elif key == 'edge_vals_cuts': 122 | inc = 0 123 | elif key == 'edge_index_rows': 124 | inc = torch.tensor([ 125 | [self.x_cuts.size(0)], 126 | [self.x_rows.size(0)]]) 127 | elif key == 'edge_vals_rows': 128 | inc = 0 129 | elif key == 'edge_index_self': 130 | inc = torch.tensor([ 131 | [self.x_cuts.size(0)], 132 | [self.x_cuts.size(0)]]) 133 | elif key == 'edge_vals_self': 134 | inc = 0 135 | elif key == 'edge_index_rowcols': 136 | inc = torch.tensor([ 137 | [self.x_rows.size(0)], 138 | [self.x_cols.size(0)]]) 139 | elif key == 'edge_vals_rowcols': 140 | inc = 0 141 | else: 142 | print('Resorting to default') 143 | inc = super().__inc__(key, value, *args, **kwargs) 144 | return inc 145 | 146 | def __cat_dim__(self, key, value, *args, **kwargs): 147 | if key == 'x_cuts': 148 | cat_dim = 0 149 | elif key == 'x_rows': 150 | cat_dim = 0 151 | elif key == 'x_cols': 152 | cat_dim = 0 153 | elif key == 'edge_index_cuts': 154 | cat_dim = 1 155 | elif key == 'edge_vals_cuts': 156 | cat_dim = 0 157 | elif key == 'edge_index_rows': 158 | cat_dim = 1 159 | elif key == 'edge_vals_rows': 160 | cat_dim = 0 161 | elif key == 'edge_index_self': 162 | cat_dim = 1 163 | elif key == 'edge_vals_self': 164 | cat_dim = 0 165 | elif key == 'edge_index_rowcols': 166 | cat_dim = 1 167 | elif key == 'edge_vals_rowcols': 168 | cat_dim = 0 169 | elif key == 'lpvals': 170 | cat_dim = 0 171 | elif key == 'sepa_settings': # 172 | cat_dim = 0 173 | elif key == 'masks': # 174 | cat_dim = 0 175 | elif key == 'x_sepas': # 176 | cat_dim = 0 177 | elif key == 'edge_index_sepa_cols': # 178 | cat_dim = 1 179 | elif key == 'edge_vals_sepa_cols': # 180 | cat_dim = 0 181 | elif key == 'edge_index_sepa_rows': # 182 | cat_dim = 1 183 | elif key == 'edge_vals_sepa_rows': # 184 | cat_dim = 0 185 | elif key == 'edge_index_sepa_self': # 186 | cat_dim = 1 187 | elif key == 'edge_vals_sepa_self': # 188 | cat_dim = 0 189 | else: 190 | print('Resorting to default') 191 | cat_dim = super().__cat_dim__(key, value, *args, **kwargs) 192 | return cat_dim 193 | 194 | @classmethod 195 | def from_path(cls, path): 196 | raw_data = cls.load_rawdata(path) 197 | min_parall = cls.get_minparallelism(path) 198 | maxnum_cutixs, maxnum_rowixs, maxnum_rowcolixs = cls.get_maxnums(path) 199 | processed_data = cls.from_rawdata(raw_data, min_parall, maxnum_cutixs, maxnum_rowixs, maxnum_rowcolixs) 200 | self = cls(*processed_data) 201 | return self 202 | 203 | @classmethod 204 | def get_minparallelism(cls, path): 205 | if 'binpacking-66-66' in path: 206 | min_parall = 0.9 207 | elif 'packing-60-60' in path: 208 | min_parall = 0.85 209 | elif 'maxcut-14-40' in path: 210 | min_parall = 0.0 211 | elif 'planning-40' in path: 212 | min_parall = 0.0 213 | else: 214 | min_parall = 0.0 215 | # raise ValueError('What is minparallelism?') 216 | return min_parall 217 | 218 | @classmethod 219 | def get_maxnums(cls, path): 220 | # for cut <-> col 221 | if 'nn' in path: 222 | maxnum_cutixs = 200 223 | maxnum_rowixs = 200 224 | maxnum_rowcolixs = 200 225 | else: 226 | maxnum_cutixs, maxnum_rowixs, maxnum_rowcolixs = 1e6, 1e6, 1e6 227 | # raise ValueError('What is maxnum_cuixs?') 228 | return maxnum_cutixs, maxnum_rowixs, maxnum_rowcolixs 229 | 230 | 231 | @classmethod 232 | def load_rawdata(cls, path): 233 | raw_data = {} 234 | for file in [ 235 | 'cut_input_scores.npy', 236 | 'row_input_scores.npy', 237 | 'cut_lookahead_scores.npy', 238 | 'cutrow_parallelism.npy', 239 | 'cut_parallelism.npy', 240 | 'masks.npy']: # 241 | 242 | arr = _utils.load_numpy(os.path.join(path, file)) 243 | raw_data[file] = arr 244 | 245 | for file in [ 246 | 'cut_features.pkl', 247 | 'row_features.pkl', 248 | 'col_features.pkl', 249 | 'cut_coefs.pkl', 250 | 'row_coefs.pkl', 251 | 'sepa_settings.pkl', # 252 | 'sepa_features.pkl']: # 253 | 254 | feat_dicts = _utils.load_pickle(os.path.join(path, file)) 255 | raw_data[file] = feat_dicts 256 | 257 | return raw_data 258 | 259 | @classmethod 260 | def from_rawdata(cls, raw_data, min_parall, maxnum_cutixs=1e6, maxnum_rowixs=1e6, maxnum_rowcolixs=1e6, args=None): 261 | 262 | # Make x_cuts 263 | ### cut scores 264 | scores = torch.FloatTensor(raw_data['cut_input_scores.npy']) 265 | relative_scores = (scores - scores.mean(0) ) / (scores.std(0) + 1e-5) 266 | relative_scores[~torch.isfinite(relative_scores)] = -1.0 # overwrite 267 | 268 | ## cut features 269 | x_cuts = cls.make_xcuts( 270 | raw_data['cut_features.pkl'], 271 | raw_data['cut_input_scores.npy']) 272 | 273 | x_rows = cls.make_xrows( 274 | raw_data['row_features.pkl'], 275 | raw_data['row_input_scores.npy'], 276 | ) 277 | 278 | x_cols = cls.make_xcols( 279 | raw_data['col_features.pkl'], 280 | ) 281 | 282 | edge_index_cuts, edge_vals_cuts = cls.make_edge_cols( 283 | raw_data['cut_coefs.pkl'], maxnum_cutixs 284 | ) 285 | 286 | edge_index_rows, edge_vals_rows = cls.make_edge_rows( 287 | raw_data['cutrow_parallelism.npy'], 288 | min_parall, maxnum_rowixs 289 | ) 290 | 291 | edge_index_self = cls.make_edge_index_self(x_cuts.size(0)) 292 | edge_vals_self = cls.make_edge_vals_self( 293 | raw_data['cut_parallelism.npy'] 294 | ) 295 | 296 | edge_index_rowcols, edge_vals_rowcols = cls.make_edge_cols( 297 | raw_data['row_coefs.pkl'], maxnum_rowcolixs 298 | ) 299 | # lpvals 300 | lpvals = torch.empty( size=(len(x_cuts), 2) ) 301 | lpvals[:, 0] = torch.FloatTensor(raw_data['cut_lookahead_scores.npy'][:, 0]) 302 | lpvals[:, 1] = torch.FloatTensor(raw_data['cut_lookahead_scores.npy'][:, 2]) 303 | 304 | sepa_settings = torch.FloatTensor(raw_data['sepa_settings.pkl']) # 305 | masks = torch.BoolTensor(raw_data['masks.npy']) # 306 | 307 | # 308 | x_sepas = cls.make_xsepas( 309 | raw_data['sepa_features.pkl'], 310 | args 311 | ) 312 | 313 | # 314 | edge_index_sepa_cols, edge_vals_sepa_cols = cls.make_edge_sepas_cols( 315 | N_SEPAS, x_cols.size(0) 316 | ) 317 | 318 | # 319 | edge_index_sepa_rows, edge_vals_sepa_rows = cls.make_edge_sepa_rows( 320 | N_SEPAS, x_rows.size(0) 321 | ) 322 | 323 | # 324 | edge_index_sepa_self, edge_vals_sepa_self = cls.make_edge_sepa_self(N_SEPAS) 325 | 326 | if torch.any(torch.isinf(x_rows)): 327 | print(torch.where(torch.isinf(x_rows))) 328 | 329 | # 330 | return (x_cuts, x_rows, x_cols, edge_index_cuts, edge_vals_cuts, edge_index_rows, edge_vals_rows, 331 | edge_index_self, edge_vals_self, edge_index_rowcols, edge_vals_rowcols, lpvals, 332 | sepa_settings, masks, x_sepas, edge_index_sepa_cols, edge_vals_sepa_cols, 333 | edge_index_sepa_rows, edge_vals_sepa_rows, edge_index_sepa_self, edge_vals_sepa_self) 334 | 335 | @classmethod 336 | def make_xcuts(cls, features, scores): 337 | 338 | vecs = [] 339 | for feat_dict in features: 340 | vec = cls.get_cut_vec(feat_dict) 341 | vecs.append(vec) 342 | 343 | vecs = np.array(vecs) 344 | if len(vecs) == 0: 345 | x = vecs.reshape(0, CUT_DIM) 346 | return torch.FloatTensor(x) 347 | x = torch.FloatTensor(np.concatenate([vecs, scores], axis=-1)) 348 | x[:, 22][torch.isinf(x[:, 22])] = -1.0 # if we want to add a new cut type, need to update from 20 to 21 # add 2 349 | return x 350 | 351 | @classmethod 352 | def make_xrows(cls, features, scores): 353 | 354 | vecs = [] 355 | for feat_dict in features: 356 | vec = cls.get_row_vec(feat_dict) 357 | vecs.append(vec) 358 | 359 | vecs = np.array(vecs) 360 | 361 | # relative normalization of these scores 362 | # import pdb; pdb.set_trace() 363 | try: 364 | scores[:, -8] = np.clip( (scores[:, -8] - np.mean(scores[:, -8])) / (np.std(scores[:, -8]) + 1e-5), a_min = -10, a_max=10) 365 | scores[:, -3] = np.clip( (scores[:, -3] - np.mean(scores[:, -3])) / (np.std(scores[:, -3]) + 1e-5), a_min = -10, a_max=10) 366 | except: 367 | import pdb; pdb.set_trace() 368 | 369 | x = torch.FloatTensor(np.concatenate([vecs, scores], axis=-1)) 370 | # import pdb; pdb.set_trace() 371 | # this feature is inf for one row often. 372 | x[:, 22][torch.isinf(x[:, 22])] = -1.0 # if we want to add a new cut type, need to update from 20 to 21 373 | # import pdb; pdb.set_trace() 374 | return x 375 | 376 | @classmethod 377 | def make_xcols(cls, features): 378 | 379 | vecs = [] 380 | for feat_dict in features: 381 | vec = cls.get_col_vec(feat_dict) 382 | vecs.append(vec) 383 | 384 | x = torch.FloatTensor(np.array(vecs)) 385 | 386 | return x 387 | 388 | @classmethod 389 | def make_edge_cols(cls, coefs, maxnum=1e6): 390 | 391 | edge_ixs_top = [] 392 | edge_ixs_bottom = [] 393 | edge_vals_raw = [] 394 | edge_vals_norm = [] 395 | 396 | for cut_ix, (col_ix, vals) in coefs.items(): 397 | edge_ixs_top.append( np.ones(len(col_ix)) * cut_ix ) 398 | edge_ixs_bottom.append( np.array(col_ix) ) 399 | 400 | vals = np.array(vals) 401 | edge_vals_raw.append( vals ) 402 | edge_vals_norm.append( vals / np.linalg.norm(vals)) 403 | 404 | if len(edge_ixs_top) == 0: # 405 | edge_ixs = np.stack([[], []]) 406 | edge_vals = np.stack([[], []]) 407 | else: 408 | edge_ixs = np.stack([ 409 | np.concatenate(edge_ixs_top), 410 | np.concatenate(edge_ixs_bottom)]) 411 | edge_vals = np.stack([ 412 | np.concatenate(edge_vals_raw), 413 | np.concatenate(edge_vals_norm)]) 414 | 415 | edge_ixs = torch.LongTensor(edge_ixs) 416 | edge_vals = torch.FloatTensor(edge_vals.T) 417 | 418 | # Filter.. 419 | if maxnum < 1e6: # nnv 420 | if len(edge_vals) > maxnum: 421 | threshold = torch.sort(edge_vals[:, 0], descending=True).values[maxnum] 422 | mask = (edge_vals[:, 0] > threshold) 423 | edge_vals = torch.stack( [ 424 | torch.masked_select(edge_vals[:,0], mask), 425 | torch.masked_select(edge_vals[:,1], mask), 426 | ], dim=-1) 427 | edge_ixs = torch.stack( [ 428 | torch.masked_select(edge_ixs[0, :], mask), 429 | torch.masked_select(edge_ixs[1, :], mask), 430 | ], dim=0) 431 | else: 432 | pass 433 | 434 | return edge_ixs, edge_vals 435 | 436 | @classmethod 437 | def make_edge_rows(cls, parallelism, min_parall, maxnum_rowixs=1e6): 438 | # cut ix is top, row ix is bottom 439 | 440 | if maxnum_rowixs < 1e6: # nnv 441 | max_edges = (parallelism.shape[0] * parallelism.shape[1]) 442 | if max_edges < maxnum_rowixs: 443 | min_parall = 0.0 444 | else: 445 | min_parall = np.sort(parallelism.flatten())[::-1][maxnum_rowixs] 446 | 447 | edge_ixs = np.where(parallelism > min_parall) 448 | if len(edge_ixs[0]) == 0: # 449 | # print('No edge ixs.') 450 | edge_ixs = np.stack([[], []]) 451 | edge_vals = np.stack([[]]) 452 | edge_ixs = torch.LongTensor(edge_ixs) 453 | edge_vals = torch.FloatTensor(edge_vals.T) 454 | return edge_ixs, edge_vals 455 | 456 | edge_vals = parallelism[edge_ixs][:, None] 457 | 458 | edge_ixs = torch.LongTensor(np.stack(edge_ixs)) 459 | edge_vals = torch.FloatTensor(edge_vals) 460 | 461 | # Clip.. 462 | edge_vals = torch.clamp(edge_vals, max=2.0) # max = 2.0 so have indication for wrong value.. 463 | 464 | return edge_ixs, edge_vals 465 | 466 | @classmethod 467 | def make_edge_index_self(cls, n_cuts): 468 | # no self-loops.. 469 | _temp = np.triu_indices(n_cuts - 1) 470 | triu_ixs = np.stack((_temp[0], _temp[1] + 1)) 471 | rev = np.stack([triu_ixs[1, :], triu_ixs[0, :]]) 472 | edge_ixs = np.concatenate((triu_ixs, rev), axis=-1) # no self-loops 473 | edge_ixs = torch.LongTensor(edge_ixs) 474 | 475 | return edge_ixs 476 | 477 | @classmethod 478 | def make_edge_vals_self(cls, parallelism): 479 | vals = torch.FloatTensor(parallelism) 480 | vals = vals.repeat(2).unsqueeze(1) 481 | return vals 482 | 483 | @classmethod 484 | def get_cut_vec(cls, feat_dict): 485 | vec = [] 486 | # Type 487 | vec.append(1.0 if feat_dict['origin_type'] == 0 else 0.0 ) 488 | vec.append(1.0 if feat_dict['origin_type'] == 1 else 0.0 ) 489 | vec.append(1.0 if feat_dict['origin_type'] == 2 else 0.0 ) 490 | vec.append(1.0 if feat_dict['origin_type'] == 3 else 0.0 ) 491 | vec.append(1.0 if feat_dict['origin_type'] == 4 else 0.0 ) 492 | # Cut Type 493 | vec.append( 1.0 if 'cmir' in feat_dict['rname'] else 0.0 ) 494 | vec.append( 1.0 if 'flowcover' in feat_dict['rname'] else 0.0 ) 495 | vec.append( 1.0 if 'clique' in feat_dict['rname'] else 0.0 ) 496 | vec.append( 1.0 if 'dis' in feat_dict['rname'] else 0.0 ) # ? 497 | vec.append( 1.0 if 'gom' in feat_dict['rname'] else 0.0 ) 498 | vec.append( 1.0 if 'implbd' in feat_dict['rname'] else 0.0 ) 499 | vec.append( 1.0 if 'mcf' in feat_dict['rname'] else 0.0 ) 500 | vec.append( 1.0 if 'oddcycle' in feat_dict['rname'] else 0.0 ) 501 | vec.append( 1.0 if 'scg' in feat_dict['rname'] else 0.0 ) 502 | vec.append( 1.0 if 'zerohalf' in feat_dict['rname'] else 0.0 ) 503 | vec.append( 1.0 if 'cgcut' in feat_dict['rname'] else 0.0 ) 504 | vec.append( 1.0 if 'obj' in feat_dict['rname'] else 0.0 ) 505 | 506 | # basis status 507 | vec.append( 1.0 if feat_dict['basisstatus'] == 0 else 0.0 ) # basestat one-hot {lower: 0, basic: 1, upper: 2, zero: 3} 508 | vec.append( 1.0 if feat_dict['basisstatus'] == 1 else 0.0 ) 509 | vec.append( 1.0 if feat_dict['basisstatus'] == 2 else 0.0 ) 510 | vec.append( 1.0 if feat_dict['basisstatus'] == 3 else 0.0 ) 511 | # other info 512 | vec.append( feat_dict['rank'] ) 513 | # normalization (following Giulia) 514 | lhs = feat_dict['lhs'] 515 | rhs = feat_dict['rhs'] 516 | cst = feat_dict['rhs'] 517 | nlps = feat_dict['nlps'] 518 | cste = feat_dict['cste'] 519 | 520 | activity = feat_dict['activity'] 521 | row_norm = feat_dict['row_norm'] 522 | obj_norm = feat_dict['obj_norm'] 523 | dualsol = feat_dict['dualsol'] 524 | 525 | unshifted_lhs = None if np.isinf(lhs) else lhs - cst 526 | unshifted_rhs = None if np.isinf(rhs) else rhs - cst 527 | 528 | if unshifted_lhs is not None: 529 | bias = -1. * unshifted_lhs / row_norm 530 | dualsol = -1. * dualsol / (row_norm * obj_norm) 531 | if unshifted_rhs is not None: 532 | bias = unshifted_rhs / row_norm 533 | dualsol = dualsol / (row_norm * obj_norm) 534 | # values 535 | vec.append( bias ) 536 | vec.append( dualsol ) 537 | vec.append( 1.0 if np.isclose(activity, lhs) else 0.0 ) # at_lhs 538 | vec.append( 1.0 if np.isclose(activity, rhs) else 0.0 ) # at_rhs 539 | vec.append( feat_dict['nlpnonz'] / feat_dict['ncols'] ) 540 | vec.append( feat_dict['age'] / (feat_dict['nlps'] + feat_dict['cste']) ) 541 | vec.append( feat_dict['nlpsaftercreation'] / (feat_dict['nlps'] + feat_dict['cste']) ) 542 | vec.append( feat_dict['intcols'] / feat_dict['ncols'] ) 543 | # flags 544 | vec.append( 1.0 if feat_dict['is_integral'] else 0.0 ) 545 | vec.append( 1.0 if feat_dict['is_removable'] else 0.0 ) # could be removed if we have binary identifier for cuts 546 | vec.append( 1.0 if feat_dict['is_in_lp'] else 0.0 ) # could be removed if we have binary identifier for cuts 547 | 548 | #round feature 549 | vec.append(feat_dict['round_num'] if 'round_num' in feat_dict else 0) 550 | 551 | return vec 552 | 553 | @classmethod 554 | def get_row_vec(cls, feat_dict): 555 | vec = [] 556 | # Type 557 | vec.append(1.0 if feat_dict['origin_type'] == 0 else 0.0 ) 558 | vec.append(1.0 if feat_dict['origin_type'] == 1 else 0.0 ) 559 | vec.append(1.0 if feat_dict['origin_type'] == 2 else 0.0 ) 560 | vec.append(1.0 if feat_dict['origin_type'] == 3 else 0.0 ) 561 | vec.append(1.0 if feat_dict['origin_type'] == 4 else 0.0 ) 562 | # Cut Type 563 | vec.append( 1.0 if 'cmir' in feat_dict['rname'] else 0.0 ) 564 | vec.append( 1.0 if 'flowcover' in feat_dict['rname'] else 0.0 ) 565 | vec.append( 1.0 if 'clique' in feat_dict['rname'] else 0.0 ) 566 | vec.append( 1.0 if 'dis' in feat_dict['rname'] else 0.0 ) # ? 567 | vec.append( 1.0 if 'gom' in feat_dict['rname'] else 0.0 ) 568 | vec.append( 1.0 if 'implbd' in feat_dict['rname'] else 0.0 ) 569 | vec.append( 1.0 if 'mcf' in feat_dict['rname'] else 0.0 ) 570 | vec.append( 1.0 if 'oddcycle' in feat_dict['rname'] else 0.0 ) 571 | vec.append( 1.0 if 'scg' in feat_dict['rname'] else 0.0 ) 572 | vec.append( 1.0 if 'zerohalf' in feat_dict['rname'] else 0.0 ) 573 | vec.append( 1.0 if 'cgcut' in feat_dict['rname'] else 0.0 ) 574 | vec.append( 1.0 if 'objrow' in feat_dict['rname'] else 0.0 ) # intobj ? 575 | 576 | # basis status 577 | vec.append( 1.0 if feat_dict['basisstatus'] == 0 else 0.0 ) # basestat one-hot {lower: 0, basic: 1, upper: 2, zero: 3} 578 | vec.append( 1.0 if feat_dict['basisstatus'] == 1 else 0.0 ) 579 | vec.append( 1.0 if feat_dict['basisstatus'] == 2 else 0.0 ) 580 | vec.append( 1.0 if feat_dict['basisstatus'] == 3 else 0.0 ) 581 | # other info 582 | vec.append( feat_dict['rank'] ) 583 | # normalization (following Giulia) 584 | lhs = feat_dict['lhs'] 585 | rhs = feat_dict['rhs'] 586 | cst = feat_dict['rhs'] 587 | nlps = feat_dict['nlps'] 588 | cste = feat_dict['cste'] 589 | 590 | activity = feat_dict['activity'] 591 | row_norm = feat_dict['row_norm'] 592 | obj_norm = feat_dict['obj_norm'] 593 | dualsol = feat_dict['dualsol'] 594 | 595 | unshifted_lhs = None if np.isinf(lhs) else lhs - cst 596 | unshifted_rhs = None if np.isinf(rhs) else rhs - cst 597 | 598 | if unshifted_lhs is not None: 599 | bias = -1. * unshifted_lhs / row_norm 600 | dualsol = -1. * dualsol / (row_norm * obj_norm) 601 | if unshifted_rhs is not None: 602 | bias = unshifted_rhs / row_norm 603 | dualsol = dualsol / (row_norm * obj_norm) 604 | # values 605 | vec.append( bias ) 606 | vec.append( dualsol ) 607 | vec.append( 1.0 if np.isclose(activity, lhs) else 0.0 ) # at_lhs 608 | vec.append( 1.0 if np.isclose(activity, rhs) else 0.0 ) # at_rhs 609 | vec.append( feat_dict['nlpnonz'] / feat_dict['ncols'] ) 610 | vec.append( feat_dict['age'] / (feat_dict['nlps'] + feat_dict['cste']) ) 611 | vec.append( feat_dict['nlpsaftercreation'] / (feat_dict['nlps'] + feat_dict['cste']) ) 612 | vec.append( feat_dict['intcols'] / feat_dict['ncols'] ) 613 | 614 | # flags 615 | vec.append( 1.0 if feat_dict['is_integral'] else 0.0 ) 616 | vec.append( 1.0 if feat_dict['is_removable'] else 0.0 ) # could be removed if we have binary identifier for cuts 617 | vec.append( 1.0 if feat_dict['is_in_lp'] else 0.0 ) # could be removed if we have binary identifier for cuts 618 | 619 | #round feature 620 | vec.append(feat_dict['round_num'] if 'round_num' in feat_dict else 0) 621 | 622 | return vec 623 | 624 | @classmethod 625 | def get_col_vec(cls, feat_dict): 626 | vec = [] 627 | # type 628 | vec.append( 1.0 if feat_dict['type'] == 0 else 0.0 ) # binary 629 | vec.append( 1.0 if feat_dict['type'] == 1 else 0.0 ) # integer 630 | vec.append( 1.0 if feat_dict['type'] == 2 else 0.0 ) # implicit-int 631 | vec.append( 1.0 if feat_dict['type'] == 3 else 0.0 ) # continuous 632 | # bounds 633 | vec.append( 1.0 if feat_dict['lb'] is not None else 0.0 ) # has_lower_bound 634 | vec.append( 1.0 if feat_dict['ub'] is not None else 0.0 ) # has_upper_bound 635 | # basis status 636 | vec.append( 1.0 if feat_dict['basestat'] == 0 else 0.0 ) 637 | vec.append( 1.0 if feat_dict['basestat'] == 1 else 0.0 ) 638 | vec.append( 1.0 if feat_dict['basestat'] == 2 else 0.0 ) 639 | vec.append( 1.0 if feat_dict['basestat'] == 3 else 0.0 ) 640 | # values 641 | vec.append( feat_dict['norm_coef']) 642 | vec.append( feat_dict['norm_redcost'] ) # was already normalized :() 643 | vec.append( feat_dict['norm_age'] ) # war already normalized :() 644 | vec.append( feat_dict['solval']) 645 | vec.append( feat_dict['solfrac']) 646 | vec.append( 1.0 if feat_dict['sol_is_at_lb'] else 0.0 ) 647 | vec.append( 1.0 if feat_dict['sol_is_at_ub'] else 0.0 ) 648 | 649 | # round feature 650 | vec.append(feat_dict['round_num'] if 'round_num' in feat_dict else 0) 651 | 652 | return vec 653 | 654 | # 655 | @classmethod 656 | def get_sepa_vec(cls, feat_dict, args): 657 | vec = [] 658 | # Cut Type 659 | vec.append(1.0 if 'disjunctive' in feat_dict['name'] else 0.0) 660 | vec.append(1.0 if 'convexproj' in feat_dict['name'] else 0.0 ) 661 | vec.append(1.0 if 'gauge' in feat_dict['name'] else 0.0 ) 662 | vec.append(1.0 if 'impliedbounds' in feat_dict['name'] else 0.0) 663 | vec.append(1.0 if 'intobj' in feat_dict['name'] else 0.0 ) 664 | vec.append(1.0 if 'gomory' in feat_dict['name'] else 0.0) 665 | vec.append(1.0 if 'cgmip' in feat_dict['name'] else 0.0 ) 666 | vec.append(1.0 if 'strongcg' in feat_dict['name'] else 0.0) 667 | vec.append(1.0 if 'aggregation' in feat_dict['name'] else 0.0 ) 668 | vec.append(1.0 if 'clique' in feat_dict['name'] else 0.0) 669 | vec.append(1.0 if 'zerohalf' in feat_dict['name'] else 0.0) 670 | vec.append(1.0 if 'mcf' in feat_dict['name'] else 0.0) 671 | vec.append(1.0 if 'eccuts' in feat_dict['name'] else 0.0 ) 672 | vec.append(1.0 if 'oddcycle' in feat_dict['name'] else 0.0) 673 | vec.append(1.0 if 'flowcover' in feat_dict['name'] else 0.0) 674 | vec.append(1.0 if 'cmir' in feat_dict['name'] else 0.0) 675 | vec.append(1.0 if 'rapidlearning' in feat_dict['name'] else 0.0 ) 676 | 677 | return vec 678 | 679 | # 680 | @classmethod 681 | def make_xsepas(cls, features, args): 682 | 683 | vecs = [] 684 | for feat_dict in features: 685 | vec = cls.get_sepa_vec(feat_dict, args) 686 | vecs.append(vec) 687 | 688 | x = torch.FloatTensor(np.array(vecs)) 689 | 690 | return x 691 | 692 | 693 | # 694 | @classmethod 695 | def make_edge_sepas_cols(cls, n_sepas, n_cols): 696 | # top: sepa idx; bottom: col idx -> fully connected 697 | edge_ixs_top = [] 698 | edge_ixs_bottom = [] 699 | edge_vals_raw = [] 700 | edge_vals_norm = [] 701 | 702 | for sepa_ix in range(n_sepas): 703 | edge_ixs_top.append(np.ones(n_cols) * sepa_ix) 704 | edge_ixs_bottom.append(np.arange(n_cols)) 705 | edge_vals_raw.append(np.ones(n_cols)) 706 | edge_vals_norm.append(np.ones(n_cols)) 707 | 708 | if len(edge_ixs_top) == 0: # 709 | edge_ixs = np.stack([[], []]) 710 | edge_vals = np.stack([[], []]) 711 | else: 712 | edge_ixs = np.stack([ 713 | np.concatenate(edge_ixs_top), 714 | np.concatenate(edge_ixs_bottom)]) 715 | edge_vals = np.stack([ 716 | np.concatenate(edge_vals_raw), 717 | ]) # np.concatenate(edge_vals_norm) 718 | 719 | edge_ixs = torch.LongTensor(edge_ixs) 720 | edge_vals = torch.FloatTensor(edge_vals.T) 721 | 722 | return edge_ixs, edge_vals 723 | 724 | # 725 | @classmethod 726 | def make_edge_sepa_rows(cls, n_sepas, n_rows): 727 | # sepa ix is top, row ix is bottom 728 | edge_ixs_top = [] 729 | edge_ixs_bottom = [] 730 | edge_vals_raw = [] 731 | edge_vals_norm = [] 732 | 733 | for sepa_ix in range(n_sepas): 734 | edge_ixs_top.append(np.ones(n_rows) * sepa_ix) 735 | edge_ixs_bottom.append(np.arange(n_rows)) 736 | edge_vals_raw.append(np.ones(n_rows)) 737 | edge_vals_norm.append(np.ones(n_rows)) 738 | 739 | if len(edge_ixs_top) == 0: # 740 | edge_ixs = np.stack([[], []]) 741 | edge_vals = np.stack([[], []]) 742 | else: 743 | edge_ixs = np.stack([ 744 | np.concatenate(edge_ixs_top), 745 | np.concatenate(edge_ixs_bottom)]) 746 | edge_vals = np.stack([ 747 | np.concatenate(edge_vals_raw), 748 | ]) # np.concatenate(edge_vals_norm) 749 | 750 | edge_ixs = torch.LongTensor(edge_ixs) 751 | edge_vals = torch.FloatTensor(edge_vals.T) 752 | 753 | return edge_ixs, edge_vals 754 | 755 | # 756 | @classmethod 757 | def make_edge_sepa_self(cls, n_sepas): 758 | # no self-loops.. 759 | _temp = np.triu_indices(n_sepas - 1) 760 | triu_ixs = np.stack((_temp[0], _temp[1] + 1)) 761 | rev = np.stack([triu_ixs[1, :], triu_ixs[0, :]]) 762 | edge_ixs = np.concatenate((triu_ixs, rev), axis=-1) # no self-loops 763 | edge_ixs = torch.LongTensor(edge_ixs) 764 | 765 | edge_vals = torch.ones((edge_ixs.size(-1), 1)) 766 | return edge_ixs, edge_vals 767 | -------------------------------------------------------------------------------- /data/binpacking-66-132/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/data/binpacking-66-132/.gitkeep -------------------------------------------------------------------------------- /data/capfac-100-100/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/data/capfac-100-100/.gitkeep -------------------------------------------------------------------------------- /data/combauct-100-500/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/data/combauct-100-500/.gitkeep -------------------------------------------------------------------------------- /data/indset-500/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/data/indset-500/.gitkeep -------------------------------------------------------------------------------- /data/load_balancing/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/data/load_balancing/.gitkeep -------------------------------------------------------------------------------- /data/maxcut-54-134/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/data/maxcut-54-134/.gitkeep -------------------------------------------------------------------------------- /data/miplib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/data/miplib/.gitkeep -------------------------------------------------------------------------------- /data/miplib/sequence.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/data/miplib/sequence.npy -------------------------------------------------------------------------------- /data/nnv/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/data/nnv/.gitkeep -------------------------------------------------------------------------------- /data/packing-60-60/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/data/packing-60-60/.gitkeep -------------------------------------------------------------------------------- /data/raw/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/data/raw/.gitkeep -------------------------------------------------------------------------------- /data_generation/data_filter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pyscipopt as pyopt 3 | import argparse 4 | from joblib import Parallel, delayed 5 | from multiprocessing import cpu_count as _cpu_count 6 | import numpy as np 7 | import shutil 8 | 9 | path_to_randomness_control_set = '../SCIP_settings/randomness_control.set' 10 | instances_list = [] 11 | 12 | def get_LIST(indexes): 13 | global instances_list 14 | path_to_miplib = os.path.join( 15 | args.path_to_raw_data_dir, 16 | args.instances 17 | ) 18 | tmp_list = os.listdir(path_to_miplib) 19 | for i in indexes: 20 | instances_list.append(tmp_list[i]) 21 | 22 | def multiprocess_helper(helper_args): 23 | index, args = helper_args 24 | print(f"{index}th {args.instances} instance starts!") 25 | instance_suffix = f"model-{index}/model.mps" 26 | if args.instances == "nnv": 27 | instance_suffix = f"model-{index}/model.proto.lp" 28 | elif args.instances == "load_balancing": 29 | instance_suffix = f"load_balancing_{index}.mps.gz" 30 | elif args.instances == "miplib": 31 | instance_suffix = instances_list[index] 32 | 33 | path_to_problem = os.path.join( 34 | args.path_to_raw_data_dir, 35 | args.instances, 36 | instance_suffix 37 | ) 38 | 39 | model = pyopt.Model() 40 | model.hideOutput(1) 41 | model.readProblem(path_to_problem) 42 | model.readParams(path_to_randomness_control_set) 43 | model.setParam("limits/time", args.time_limit) 44 | model.optimize() 45 | 46 | if model.getGap() > args.gap_limit+1e-6: 47 | print(f"{index}th instance is flitered out!") 48 | else: 49 | dst_path = os.path.join( 50 | args.path_to_data_dir, 51 | args.instances, 52 | instance_suffix 53 | ) 54 | os.makedirs(dst_path, exist_ok=True) 55 | shutil.copy(path_to_problem, dst_path) 56 | print(f"{index}th instance is selected!") 57 | return model.getGap() 58 | 59 | if __name__ == "__main__": 60 | parser = argparse.ArgumentParser() 61 | parser.add_argument('--instances', type=str, help="MILP class to be filtered") 62 | parser.add_argument('--n_cpus', type=int, default=48, help="number of available CPUs (for parallel processing)") 63 | parser.add_argument('--path_to_raw_data_dir', type=str, default="../data/raw", help="directory of the downloaded raw data") 64 | parser.add_argument('--path_to_data_dir', type=str, default="../data", help="save directory of the filtered data") 65 | parser.add_argument('--time_limit', type=float, default=float('inf'), help="time limit for solving each instance") 66 | parser.add_argument('--gap_limit', type=float, default=0.0, help="gap limit for filtering instances") 67 | parser.add_argument('--index_start', type=int, default=0, help="index of the first instance to consider") 68 | parser.add_argument('--index_end', type=int, default=1000, help="total number of the instances to consider") 69 | args = parser.parse_args() 70 | 71 | indexes = range(args.index_start, args.index_end) 72 | 73 | if args.instances == "miplib": 74 | get_LIST(indexes) 75 | 76 | outputs = Parallel(n_jobs=args.n_cpus)( 77 | delayed(multiprocess_helper)(args_) for args_ in list( 78 | zip( 79 | indexes, 80 | [args] * len(indexes) 81 | ) 82 | ) 83 | ) -------------------------------------------------------------------------------- /data_generation/generate_ecole.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import ecole 3 | import os 4 | import numpy as np 5 | import json 6 | import argparse 7 | # import pyscipopt as pyopt 8 | 9 | INSTANCES = [ 10 | ('combauct', OrderedDict({'n_items': 100, 'n_bids': 500}), 'max'), 11 | ('capfac', OrderedDict({'n_customers': 100, 'n_facilities': 100}), 'min'), 12 | ('indset', OrderedDict({'n_nodes': 500}), 'max'), 13 | ] 14 | MAXNUM = 1000 15 | 16 | def main(args): 17 | for ptype, kwargs, sense in INSTANCES: 18 | spec = '-'.join([str(i) for i in kwargs.values()]) 19 | path = f'{args.path_to_data_dir}/{ptype}-{spec}' 20 | 21 | for i in range(MAXNUM): 22 | generator, new_kwargs = get_generator(ptype, **kwargs) 23 | modeldir = os.path.join(path, f'model-{i}') 24 | os.makedirs(modeldir, exist_ok=True) 25 | json_object = json.dumps(new_kwargs) 26 | #Writing to sample.json 27 | with open(os.path.join(modeldir, 'info.json'), "w") as outfile: 28 | outfile.write(json_object) 29 | model = next(generator) # .as_pyscipopt() 30 | if sense == 'max': # Revert sense, so we always have minimization problems for code. 31 | new_obj = -1 * model.getObjective() 32 | model.setObjective(new_obj, 'minimize') 33 | model.write_problem(os.path.join(modeldir, 'model.mps')) 34 | 35 | print(f'Generated {MAXNUM} instances of {ptype}-{spec}.') 36 | 37 | 38 | def get_generator(ptype, **kwargs): 39 | 40 | GENERATORS = { 41 | 'setcover': ecole.instance.SetCoverGenerator, 42 | 'combauct': ecole.instance.CombinatorialAuctionGenerator, 43 | 'capfac': ecole.instance.CapacitatedFacilityLocationGenerator, 44 | 'indset': ecole.instance.IndependentSetGenerator, 45 | } 46 | 47 | ecole.seed(0) 48 | if ptype == "setcover": 49 | kwargs['density'] = 0.05 + np.random.rand() * 0.1 50 | kwargs['max_coef'] = 100 + np.random.randint(0,200) 51 | elif ptype == "indset": 52 | type = "barabasi_albert" if np.random.randint(0,2) == 0 else "erdos_renyi" 53 | if type == "barabasi_albert": 54 | kwargs['affinity'] = np.random.randint(2,7) 55 | else: 56 | kwargs['graph_type'] = type 57 | kwargs['edge_probability'] = np.random.rand() * 0.005 + 0.005 58 | elif ptype == "combauct": 59 | kwargs['value_deviation'] = 0.25 + np.random.rand() * 0.5 60 | kwargs['add_item_prob'] = 0.5 + np.random.rand() * 0.25 61 | kwargs['max_n_sub_bids'] = np.random.randint(3,8) 62 | kwargs['additivity'] = -0.1 + np.random.rand() * 0.5 63 | kwargs['budget_factor'] = 1.25 + np.random.rand() * 0.5 64 | kwargs['resale_factor'] = 0.35 + np.random.rand() * 0.3 65 | generator = GENERATORS[ptype](**kwargs) 66 | generator.seed(0) 67 | 68 | return generator, kwargs 69 | 70 | 71 | if __name__ == '__main__': 72 | parser = argparse.ArgumentParser() 73 | parser.add_argument('--path_to_data_dir', type=str, 74 | default='../data',) 75 | args = parser.parse_args() 76 | main(args) -------------------------------------------------------------------------------- /data_generation/generate_tang.py: -------------------------------------------------------------------------------- 1 | # """ 2 | # Instance generation of Tang et al. 2020 3 | # https://arxiv.org/pdf/1906.04859.pdf 4 | # """ 5 | # 6 | from abc import ABC, abstractmethod 7 | from collections import OrderedDict 8 | from itertools import combinations 9 | from numpy.random import default_rng 10 | import os 11 | import pyscipopt as pyopt 12 | import argparse 13 | 14 | INSTANCES = [ 15 | ('maxcut', OrderedDict({'nV': 54, 'nE': 134}), 'max'), 16 | ('packing', OrderedDict({'n': 60, 'm': 60}), 'max'), 17 | ('binpacking', OrderedDict({'n': 66, 'm': 132}), 'max'), 18 | ] 19 | 20 | MAXNUM = 1000 21 | 22 | def main(): 23 | 24 | for ptype, kwargs, sense in INSTANCES: 25 | generator = get_generator(ptype, **kwargs) 26 | spec = '-'.join([str(i) for i in kwargs.values()]) 27 | path = f'{args.path_to_data_dir}/{ptype}-{spec}' 28 | 29 | for i, instance in enumerate(generator): 30 | model = instance # .as_pyscipopt() not needed 31 | 32 | if sense == 'max': # Revert sense, so we always have minimization problems for code. 33 | new_obj = -1 * model.getObjective() 34 | model.setObjective(new_obj, 'minimize') 35 | 36 | modeldir = os.path.join(path, f'model-{i}') 37 | os.makedirs(modeldir, exist_ok=True) 38 | model.writeProblem(os.path.join(modeldir, 'model.mps')) 39 | 40 | if (i + 1) >= MAXNUM: 41 | break 42 | 43 | print(f'Generated {MAXNUM} instances of {ptype}-{spec}.') 44 | 45 | 46 | def get_generator(ptype, **kwargs): 47 | 48 | GENERATORS = { 49 | 'maxcut': MaxCutGenerator, 50 | 'packing': PackingGenerator, 51 | 'binpacking': BinPackingGenerator, 52 | 'planning': PlanningGenerator, 53 | 'maxcutplus': MaxCutGeneratorPlus 54 | } 55 | 56 | generator = GENERATORS[ptype](**kwargs, seed=42) 57 | 58 | return generator 59 | 60 | 61 | class Generator(ABC): 62 | 63 | def __init__(self, seed=42): 64 | self.num = 0 65 | self.rng = default_rng(seed=seed) 66 | 67 | def __iter__(self): 68 | return self 69 | 70 | @abstractmethod 71 | def __next__(self): 72 | pass 73 | 74 | @staticmethod 75 | @abstractmethod 76 | def generate_instance(self, *args, **kwargs): 77 | pass 78 | 79 | 80 | # Max Cut 81 | # The size (n,m) of the final relaxation (excluding non-negativity constraints) 82 | # is determined by the underlying graph G=(V,E), as n = |V|+|E| and m = 3|E|+|V|. 83 | # Medium size instances have |V|=7 and |E|=20 (note: it basically keeps all but one edge?). 84 | 85 | class MaxCutGenerator(Generator): 86 | 87 | def __init__(self, nV, nE, seed=42): 88 | super().__init__(seed=seed) 89 | self.nV = nV 90 | self.nE = nE 91 | 92 | def __next__(self): 93 | instance = self.generate_instance(nV=self.nV, nE=self.nE, num=self.num, rng=self.rng) 94 | self.num += 1 95 | return instance 96 | 97 | @staticmethod 98 | def generate_instance(nV, nE, num, rng): 99 | 100 | name = 'model-'+str(num) 101 | # sample nE edges from all the possible ones 102 | V = list(range(1, nV+1)) 103 | allE = list(combinations(V, 2)) 104 | E = list(map(tuple, rng.choice(allE, nE, replace=False))) 105 | E.sort() 106 | 107 | # sample weights for E 108 | W = dict.fromkeys(E) 109 | for e in W.keys(): 110 | W[e] = rng.integers(low=0, high=10, endpoint=True) # as in Tang et al. 111 | 112 | # build the optimization model 113 | model = pyopt.Model(name) 114 | 115 | x, y = {}, {} 116 | for u in V: 117 | x[u] = model.addVar(vtype='B', lb=0.0, ub=1, name="x(%s)" % u) 118 | for e in E: # e=(u,v) 119 | y[e] = model.addVar(vtype='B', lb=0.0, ub=1, name="y(%s,%s)" % (e[0], e[1])) 120 | model.addCons(y[e] <= x[e[0]] + x[e[1]], "C1(%s,%s)" % (e[0], e[1])) 121 | model.addCons(y[e] <= 2 - x[e[0]] - x[e[1]], "C2(%s,%s)" % (e[0], e[1])) 122 | 123 | # objective is max(c^T x) 124 | model.setObjective(pyopt.quicksum(W[e]*y[e] for e in E), "maximize") 125 | 126 | return model 127 | 128 | class MaxCutGeneratorPlus(Generator): 129 | 130 | def __init__(self, nV, nE, seed=42): 131 | super().__init__(seed=seed) 132 | self.nV = nV 133 | self.nE = nE 134 | 135 | def __next__(self): 136 | instance = self.generate_instance(nV=self.nV, nE=self.nE, num=self.num, rng=self.rng) 137 | self.num += 1 138 | return instance 139 | 140 | @staticmethod 141 | def generate_instance(nV, nE, num, rng): 142 | 143 | name = 'model-'+str(num) 144 | # sample nE edges from all the possible ones 145 | V = list(range(1, nV+1)) 146 | allE = list(combinations(V, 2)) 147 | E = list(map(tuple, rng.choice(allE, nE, replace=False))) 148 | E.sort() 149 | 150 | # sample weights for E 151 | W = dict.fromkeys(E) 152 | for e in W.keys(): 153 | W[e] = rng.integers(low=0, high=100, endpoint=True) # as in Tang et al. 154 | 155 | # build the optimization model 156 | model = pyopt.Model(name) 157 | 158 | x, y = {}, {} 159 | for u in V: 160 | x[u] = model.addVar(vtype='B', lb=0.0, ub=1, name="x(%s)" % u) 161 | for e in E: # e=(u,v) 162 | y[e] = model.addVar(vtype='B', lb=0.0, ub=1, name="y(%s,%s)" % (e[0], e[1])) 163 | model.addCons(y[e] <= x[e[0]] + x[e[1]], "C1(%s,%s)" % (e[0], e[1])) 164 | model.addCons(y[e] <= 2 - x[e[0]] - x[e[1]], "C2(%s,%s)" % (e[0], e[1])) 165 | 166 | # objective is max(c^T x) 167 | model.setObjective(pyopt.quicksum(W[e]*y[e] for e in E), "maximize") 168 | 169 | return model 170 | 171 | # Packing 172 | # Packing problem with non-negative general integer variables. 173 | # Dimension is directly determined by n=number of items and m=number of resource constraints. 174 | 175 | class PackingGenerator(Generator): 176 | 177 | def __init__(self, n, m, seed=42): 178 | super().__init__(seed=seed) 179 | self.n = n 180 | self.m = m 181 | 182 | def __next__(self): 183 | instance = self.generate_instance(n=self.n, m=self.m, num=self.num, rng=self.rng) 184 | self.num += 1 185 | return instance 186 | 187 | @staticmethod 188 | def generate_instance(n, m, num, rng): 189 | 190 | name = 'model-'+str(num) 191 | 192 | # sample coefficients, ranges as in Tang et al. 193 | A = rng.integers(low=0, high=5, size=(m, n), endpoint=True) 194 | b = rng.integers(low=9*n, high=10*n, size=m, endpoint=True) 195 | c = rng.integers(low=1, high=10, size=n, endpoint=True) 196 | 197 | # build the optimization model 198 | model = pyopt.Model(name) 199 | 200 | x = {} 201 | for j in range(n): 202 | x[j] = model.addVar(vtype='I', lb=0.0, ub=None, name="x(%s)" % (j+1)) 203 | for i in range(m): 204 | model.addCons(pyopt.quicksum(A[i][j]*x[j] for j in range(n)) <= b[i], "Resource(%s)" % (i+1)) 205 | 206 | # objective is max(c^T x) 207 | model.setObjective(pyopt.quicksum(c[j]*x[j] for j in range(n)), "maximize") 208 | 209 | return model 210 | 211 | 212 | # Bin Packing 213 | # Packing problem with binary variables. 214 | # Dimension is determined by n=number of items and m=number of resource constraints. 215 | # With respect to packing method, bounds are added for binary variables, and ranges for coefficients changed. 216 | 217 | class BinPackingGenerator(Generator): 218 | 219 | def __init__(self, n, m, seed=42): 220 | super().__init__(seed=seed) 221 | self.m = m 222 | self.n = n 223 | 224 | def __next__(self): 225 | instance = self.generate_instance(n=self.n, m=self.m, num=self.num, rng=self.rng) 226 | self.num += 1 227 | return instance 228 | 229 | @staticmethod 230 | def generate_instance(n, m, num, rng): 231 | 232 | name = 'model-'+str(num) 233 | 234 | # sample coefficients, ranges as in Tang et al. 235 | A = rng.integers(low=5, high=30, size=(m, n), endpoint=True) 236 | b = rng.integers(low=10*n, high=20*n, size=m, endpoint=True) 237 | c = rng.integers(low=1, high=10, size=n, endpoint=True) 238 | 239 | # build the optimization model 240 | model = pyopt.Model(name) 241 | 242 | x = {} 243 | for j in range(n): 244 | x[j] = model.addVar(vtype='B', lb=0.0, ub=1, name="x(%s)" % (j+1)) 245 | for i in range(m): 246 | model.addCons(pyopt.quicksum(A[i][j]*x[j] for j in range(n)) <= b[i], "Resource(%s)" % (i+1)) 247 | 248 | # objective is max(c^T x) 249 | model.setObjective(pyopt.quicksum(c[j]*x[j] for j in range(n)), "maximize") 250 | 251 | return model 252 | 253 | 254 | # Production Planning 255 | # Production planning problem over planning horizon T. 256 | # Dimension is determined as n=3T+1 and m=4T+1 (not sure how constraints are counted, maybe as 4(T+1)?) 257 | 258 | class PlanningGenerator(Generator): 259 | 260 | def __init__(self, T, seed=42): 261 | super().__init__(seed=seed) 262 | self.T = T 263 | 264 | def __next__(self): 265 | instance = self.generate_instance(T=self.T, num=self.num, rng=self.rng) 266 | self.num += 1 267 | return instance 268 | 269 | @staticmethod 270 | def generate_instance(T, num, rng): 271 | 272 | name = 'model-'+str(num) 273 | 274 | # constant parameters as in Tang et al. 275 | # s0, sT = 0, 20 are enforced via bounds 276 | M = 100 277 | 278 | # sample coefficients, ranges as in Tang et al. 279 | p = rng.integers(low=1, high=10, size=T, endpoint=True) 280 | h = rng.integers(low=1, high=10, size=T+1, endpoint=True) 281 | q = rng.integers(low=1, high=10, size=T, endpoint=True) # indices should run 1...T in objective 282 | # demands are not specified in the paper, so we get them as other parameters 283 | dval = rng.integers(low=1, high=10, size=T, endpoint=True) 284 | d = {} # this makes indexing easier for constraints 285 | for i in range(1, T+1): 286 | d[i] = dval[i-1] 287 | 288 | # build the optimization model 289 | model = pyopt.Model(name) 290 | 291 | x, y, s = {}, {}, {} 292 | for i in range(T+1): 293 | if i == 0: 294 | s[i] = model.addVar(vtype='I', lb=0.0, ub=0, name="s(%s)" % i) # fixed as in Tang et al. 295 | elif i == T: 296 | s[i] = model.addVar(vtype='I', lb=20, ub=20, name="s(%s)" % i) # fixed as in Tang et al. 297 | else: 298 | s[i] = model.addVar(vtype='I', lb=0.0, ub=None, name="s(%s)" % i) 299 | 300 | for i in range(1, T+1): 301 | x[i] = model.addVar(vtype='I', lb=0.0, ub=None, name="x(%s)" % i) 302 | y[i] = model.addVar(vtype='B', lb=0.0, ub=1, name="y(%s)" % i) 303 | 304 | model.addCons(s[i-1] + x[i] == d[i] + s[i], "Demand(%s)" % i) 305 | model.addCons(x[i] <= M*y[i], "BigM(%s)" % i) 306 | 307 | model.setObjective(pyopt.quicksum(p[i-1]*x[i] for i in range(1, T+1)) + 308 | pyopt.quicksum(h[i]*s[i] for i in range(T+1)) + 309 | pyopt.quicksum(q[i-1]*y[i] for i in range(1, T+1)), "minimize") 310 | 311 | return model 312 | 313 | 314 | if __name__ == "__main__": 315 | parser = argparse.ArgumentParser() 316 | parser.add_argument('--path_to_data_dir', type=str, 317 | default='../data',) 318 | args = parser.parse_args() 319 | main() 320 | -------------------------------------------------------------------------------- /dataset.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import json 3 | import os 4 | import torch 5 | import torch_geometric 6 | from torch_geometric.data import Dataset 7 | import pickle 8 | import glob 9 | import platform 10 | from torch_geometric.data import Data 11 | 12 | import data as _data 13 | import utils as _utils 14 | from copy import deepcopy as dp 15 | 16 | class bandit_dataset(torch_geometric.data.Dataset): 17 | def __init__(self, train_set): 18 | super().__init__(root=None, transform=None, pre_transform=None) 19 | self.train_set = dp(train_set) 20 | 21 | def len(self): 22 | return len(self.train_set) 23 | 24 | def get(self, index): 25 | self.train_set[index][0].labels = self.train_set[index][1] 26 | return bandit_data( 27 | self.train_set[index][0].x_rows, 28 | self.train_set[index][0].x_cols, 29 | self.train_set[index][0].x_sepas, 30 | self.train_set[index][0].edge_index_rowcols, 31 | self.train_set[index][0].edge_vals_rowcols, 32 | self.train_set[index][0].edge_index_sepa_cols, 33 | self.train_set[index][0].edge_vals_sepa_cols, 34 | self.train_set[index][0].edge_index_sepa_rows, 35 | self.train_set[index][0].edge_vals_sepa_rows, 36 | self.train_set[index][0].edge_index_sepa_self, 37 | self.train_set[index][0].edge_vals_sepa_self, 38 | self.train_set[index][0].labels 39 | ) 40 | 41 | class offline_dataset(torch_geometric.data.Dataset): 42 | def __init__(self, train_set, intputs_context): 43 | super().__init__(root=None, transform=None, pre_transform=None) 44 | self.train_set = dp(train_set) 45 | self.inputs_context = dp(intputs_context) 46 | 47 | def len(self): 48 | return len(self.train_set) 49 | 50 | def get(self, index, device='cuda:0'): 51 | def sc(index): 52 | return self.inputs_context[self.train_set[index][0]] 53 | return bandit_data( 54 | sc(index).x_rows, 55 | sc(index).x_cols, 56 | torch.tensor(self.train_set[index][1]).to(torch.float32), 57 | sc(index).edge_index_rowcols, 58 | sc(index).edge_vals_rowcols, 59 | sc(index).edge_index_sepa_cols, 60 | sc(index).edge_vals_sepa_cols, 61 | sc(index).edge_index_sepa_rows, 62 | sc(index).edge_vals_sepa_rows, 63 | sc(index).edge_index_sepa_self, 64 | sc(index).edge_vals_sepa_self, 65 | torch.tensor(self.train_set[index][2]).to(torch.float32), 66 | ) 67 | 68 | N_SEPAS = 17 69 | 70 | class bandit_data(Data): 71 | 72 | def __init__( 73 | self, 74 | x_rows, 75 | x_cols, 76 | x_sepas, 77 | edge_index_rowcols, 78 | edge_vals_rowcols, 79 | edge_index_sepa_cols, 80 | edge_vals_sepa_cols, 81 | edge_index_sepa_rows, 82 | edge_vals_sepa_rows, 83 | edge_index_sepa_self, 84 | edge_vals_sepa_self, 85 | labels 86 | ): 87 | super().__init__() 88 | self.x_rows = x_rows 89 | self.x_cols = x_cols 90 | self.x_sepas = x_sepas 91 | self.edge_index_rowcols = edge_index_rowcols 92 | self.edge_vals_rowcols = edge_vals_rowcols 93 | self.edge_index_sepa_cols = edge_index_sepa_cols 94 | self.edge_vals_sepa_cols = edge_vals_sepa_cols 95 | self.edge_index_sepa_rows = edge_index_sepa_rows 96 | self.edge_vals_sepa_rows = edge_vals_sepa_rows 97 | self.edge_index_sepa_self = edge_index_sepa_self 98 | self.edge_vals_sepa_self = edge_vals_sepa_self 99 | self.labels = labels 100 | 101 | def __inc__(self, key, value, *args, **kwargs): 102 | if key == 'x_cuts': 103 | inc = 0 104 | elif key == 'x_rows': 105 | inc = 0 106 | elif key == 'x_cols': 107 | inc = 0 108 | elif key == 'lpvals': 109 | inc = 0 110 | elif key == 'sepa_settings': 111 | inc = 0 112 | elif key == 'masks': 113 | inc = 0 114 | elif key == 'x_sepas': 115 | inc = 0 116 | elif key == 'edge_index_sepa_cols': 117 | inc = torch.tensor([[N_SEPAS], 118 | [self.x_cols.size(0)]]) 119 | elif key == 'edge_vals_sepa_cols': 120 | inc = 0 121 | elif key == 'edge_index_sepa_rows': 122 | inc = torch.tensor([[N_SEPAS], 123 | [self.x_rows.size(0)]]) 124 | elif key == 'edge_vals_sepa_rows': 125 | inc = 0 126 | elif key == 'edge_index_sepa_self': 127 | inc = torch.tensor([[N_SEPAS], 128 | [N_SEPAS]]) 129 | elif key == 'edge_vals_sepa_self': 130 | inc = 0 131 | elif key == 'edge_index_cuts': 132 | inc = torch.tensor([ 133 | [self.x_cuts.size(0)], 134 | [self.x_cols.size(0)]]) 135 | elif key == 'edge_vals_cuts': 136 | inc = 0 137 | elif key == 'edge_index_rows': 138 | inc = torch.tensor([ 139 | [self.x_cuts.size(0)], 140 | [self.x_rows.size(0)]]) 141 | elif key == 'edge_vals_rows': 142 | inc = 0 143 | elif key == 'edge_index_self': 144 | inc = torch.tensor([ 145 | [self.x_cuts.size(0)], 146 | [self.x_cuts.size(0)]]) 147 | elif key == 'edge_vals_self': 148 | inc = 0 149 | elif key == 'edge_index_rowcols': 150 | inc = torch.tensor([ 151 | [self.x_rows.size(0)], 152 | [self.x_cols.size(0)]]) 153 | elif key == 'edge_vals_rowcols': 154 | inc = 0 155 | else: 156 | # print('Resorting to default') 157 | inc = super().__inc__(key, value, *args, **kwargs) 158 | return inc 159 | 160 | def __cat_dim__(self, key, value, *args, **kwargs): 161 | if key == 'x_cuts': 162 | cat_dim = 0 163 | elif key == 'x_rows': 164 | cat_dim = 0 165 | elif key == 'x_cols': 166 | cat_dim = 0 167 | elif key == 'edge_index_cuts': 168 | cat_dim = 1 169 | elif key == 'edge_vals_cuts': 170 | cat_dim = 0 171 | elif key == 'edge_index_rows': 172 | cat_dim = 1 173 | elif key == 'edge_vals_rows': 174 | cat_dim = 0 175 | elif key == 'edge_index_self': 176 | cat_dim = 1 177 | elif key == 'edge_vals_self': 178 | cat_dim = 0 179 | elif key == 'edge_index_rowcols': 180 | cat_dim = 1 181 | elif key == 'edge_vals_rowcols': 182 | cat_dim = 0 183 | elif key == 'lpvals': 184 | cat_dim = 0 185 | elif key == 'sepa_settings': 186 | cat_dim = 0 187 | elif key == 'masks': 188 | cat_dim = 0 189 | elif key == 'x_sepas': 190 | cat_dim = 0 191 | elif key == 'edge_index_sepa_cols': 192 | cat_dim = 1 193 | elif key == 'edge_vals_sepa_cols': 194 | cat_dim = 0 195 | elif key == 'edge_index_sepa_rows': 196 | cat_dim = 1 197 | elif key == 'edge_vals_sepa_rows': 198 | cat_dim = 0 199 | elif key == 'edge_index_sepa_self': 200 | cat_dim = 1 201 | elif key == 'edge_vals_sepa_self': 202 | cat_dim = 0 203 | else: 204 | # print('Resorting to default') 205 | cat_dim = super().__cat_dim__(key, value, *args, **kwargs) 206 | return cat_dim 207 | 208 | def getDataloaders(train_set, args, batch_size=64, shuffle_flag=True): 209 | num_workers = args.n_cpus 210 | pin_memory=False 211 | follow_batch = ['x_sepas', 'x_rows', 'x_cols'] 212 | 213 | trainloader = torch_geometric.loader.DataLoader( 214 | bandit_dataset(train_set), 215 | batch_size=batch_size, 216 | shuffle=shuffle_flag, 217 | follow_batch=follow_batch, 218 | num_workers=0, 219 | pin_memory=pin_memory 220 | ) 221 | 222 | return trainloader 223 | -------------------------------------------------------------------------------- /example/model/capfac-100-100/Z_k1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/capfac-100-100/Z_k1 -------------------------------------------------------------------------------- /example/model/capfac-100-100/Z_k2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/capfac-100-100/Z_k2 -------------------------------------------------------------------------------- /example/model/capfac-100-100/model_k1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/capfac-100-100/model_k1 -------------------------------------------------------------------------------- /example/model/capfac-100-100/model_k2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/capfac-100-100/model_k2 -------------------------------------------------------------------------------- /example/model/combauct-100-500/Z_k1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/combauct-100-500/Z_k1 -------------------------------------------------------------------------------- /example/model/combauct-100-500/Z_k2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/combauct-100-500/Z_k2 -------------------------------------------------------------------------------- /example/model/combauct-100-500/model_k1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/combauct-100-500/model_k1 -------------------------------------------------------------------------------- /example/model/combauct-100-500/model_k2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/combauct-100-500/model_k2 -------------------------------------------------------------------------------- /example/model/indset-500/Z_k1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/indset-500/Z_k1 -------------------------------------------------------------------------------- /example/model/indset-500/Z_k2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/indset-500/Z_k2 -------------------------------------------------------------------------------- /example/model/indset-500/model_k1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/indset-500/model_k1 -------------------------------------------------------------------------------- /example/model/indset-500/model_k2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/model/indset-500/model_k2 -------------------------------------------------------------------------------- /example/restricted_space/capfac-100-100/inst_agnostic_action.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/restricted_space/capfac-100-100/inst_agnostic_action.npy -------------------------------------------------------------------------------- /example/restricted_space/capfac-100-100/restricted_actions_space.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/restricted_space/capfac-100-100/restricted_actions_space.npy -------------------------------------------------------------------------------- /example/restricted_space/combauct-100-500/inst_agnostic_action.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/restricted_space/combauct-100-500/inst_agnostic_action.npy -------------------------------------------------------------------------------- /example/restricted_space/combauct-100-500/restricted_actions_space.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/restricted_space/combauct-100-500/restricted_actions_space.npy -------------------------------------------------------------------------------- /example/restricted_space/indset-500/inst_agnostic_action.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/restricted_space/indset-500/inst_agnostic_action.npy -------------------------------------------------------------------------------- /example/restricted_space/indset-500/restricted_actions_space.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/example/restricted_space/indset-500/restricted_actions_space.npy -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch_geometric 4 | import data as _data 5 | import context as _context 6 | import numpy as np 7 | 8 | def getModel(modelstr="Neural_UCB"): 9 | if modelstr == 'Neural_UCB': 10 | return Neural_UCB() 11 | else: 12 | raise ValueError('Unknown model.') 13 | 14 | # https://pytorch-geometric.readthedocs.io/en/latest/modules/nn.html#torch_geometric.nn.conv.message_passing.MessagePassing 15 | class BipartiteGraphConvolution(torch_geometric.nn.MessagePassing): 16 | """ 17 | The bipartite graph convolution is already provided by pytorch geometric and we merely need 18 | to provide the exact form of the messages being passed. 19 | """ 20 | def __init__(self, edge_dim): 21 | super().__init__('add') 22 | emb_size = 64 23 | self.edge_dim = edge_dim 24 | 25 | self.feature_module_left = torch.nn.Sequential( 26 | torch.nn.Linear(emb_size, emb_size) 27 | ) 28 | self.feature_module_edge = torch.nn.Sequential( 29 | torch.nn.Linear(edge_dim, emb_size, bias=False) 30 | ) 31 | self.feature_module_right = torch.nn.Sequential( 32 | torch.nn.Linear(emb_size, emb_size, bias=False) 33 | ) 34 | self.feature_module_final = torch.nn.Sequential( 35 | torch.nn.LayerNorm(emb_size), 36 | torch.nn.ReLU(), 37 | torch.nn.Linear(emb_size, emb_size) 38 | ) 39 | 40 | self.post_conv_module = torch.nn.Sequential( 41 | torch.nn.LayerNorm(emb_size) 42 | ) 43 | 44 | # output_layers 45 | self.output_module = torch.nn.Sequential( 46 | torch.nn.Linear(2*emb_size, emb_size), 47 | torch.nn.ReLU(), 48 | torch.nn.Linear(emb_size, emb_size), 49 | ) 50 | 51 | def forward(self, left_features, edge_indices, edge_features, right_features): 52 | """ 53 | This method sends the messages, computed in the message method. 54 | """ 55 | output = self.propagate(edge_indices, size=(left_features.shape[0], right_features.shape[0]), 56 | node_features=(left_features, right_features), edge_features=edge_features) 57 | return self.output_module(torch.cat([self.post_conv_module(output), right_features], dim=-1)) 58 | 59 | def message(self, node_features_i, node_features_j, edge_features): 60 | 61 | output = self.feature_module_final(self.feature_module_left(node_features_i) 62 | + self.feature_module_edge(edge_features) 63 | + self.feature_module_right(node_features_j)) 64 | return output 65 | 66 | class Neural_UCB(torch.nn.Module): 67 | emb_size = 64 68 | row_dim = _data.MyData.row_dim 69 | edge_dim_cuts = _data.MyData.edge_dim_cuts 70 | edge_dim_rows = _data.MyData.edge_dim_rows 71 | col_dim = _data.MyData.col_dim 72 | num_sepa = 17 # 10 73 | sepa_dim = 1 74 | edge_dim_sepas = _data.MyData.edge_dim_sepas 75 | 76 | def __init__(self): 77 | super().__init__() 78 | # ROW EMBEDDING 79 | self.row_embedding = torch.nn.Sequential( 80 | torch.nn.BatchNorm1d(self.row_dim), 81 | torch.nn.Linear(self.row_dim, self.emb_size), 82 | torch.nn.ReLU(), 83 | torch.nn.Linear(self.emb_size, self.emb_size), 84 | torch.nn.ReLU(), 85 | ) 86 | 87 | self.sepa_embedding = torch.nn.Sequential( 88 | torch.nn.LayerNorm(self.sepa_dim), 89 | torch.nn.Linear(self.sepa_dim, self.emb_size), 90 | torch.nn.ReLU(), 91 | torch.nn.Linear(self.emb_size, self.emb_size), 92 | torch.nn.ReLU(), 93 | ) 94 | 95 | # EDGE EMBEDDING 96 | self.edge_norm_sepas = torch.nn.Sequential( 97 | torch.nn.BatchNorm1d(self.edge_dim_sepas), 98 | ) 99 | 100 | self.edge_norm_rowcols = torch.nn.Sequential( 101 | torch.nn.BatchNorm1d(self.edge_dim_cuts), 102 | ) 103 | 104 | self.edge_norm_rows = torch.nn.Sequential( 105 | torch.nn.BatchNorm1d(self.edge_dim_rows), 106 | ) 107 | 108 | # colIABLE EMBEDDING 109 | self.col_embedding = torch.nn.Sequential( 110 | torch.nn.BatchNorm1d(self.col_dim), 111 | torch.nn.Linear(self.col_dim, self.emb_size), 112 | torch.nn.ReLU(), 113 | torch.nn.Linear(self.emb_size, self.emb_size), 114 | torch.nn.ReLU(), 115 | ) 116 | 117 | self.conv_col_to_row = BipartiteGraphConvolution(edge_dim=2) 118 | self.conv_row_to_col = BipartiteGraphConvolution(edge_dim=2) 119 | 120 | self.conv_sepa_to_col = BipartiteGraphConvolution(edge_dim=1) 121 | self.conv_col_to_sepa = BipartiteGraphConvolution(edge_dim=1) 122 | 123 | self.conv_sepa_to_row = BipartiteGraphConvolution(edge_dim=1) 124 | self.conv_row_to_sepa = BipartiteGraphConvolution(edge_dim=1) 125 | 126 | self.transformer_conv = torch_geometric.nn.conv.TransformerConv( 127 | in_channels = self.emb_size, 128 | out_channels = self.emb_size // 4, 129 | heads=4, 130 | concat=True, 131 | dropout=0.1, 132 | edge_dim=1, 133 | ) 134 | 135 | self.sepa_output_embed_module = torch.nn.Sequential( 136 | torch.nn.Linear(2*self.emb_size+1, self.emb_size), 137 | torch.nn.ReLU(), 138 | ) 139 | 140 | self.row_output_embed_module = torch.nn.Sequential( 141 | torch.nn.Linear(self.emb_size, self.emb_size), 142 | torch.nn.ReLU(), 143 | ) 144 | 145 | self.output_module = torch.nn.Sequential( 146 | torch.nn.Linear(self.emb_size*3, self.emb_size), 147 | torch.nn.ReLU(), 148 | torch.nn.Linear(self.emb_size, 1), 149 | torch.nn.Sigmoid(), 150 | ) 151 | 152 | def get_device(self): 153 | return self.row_embedding[1].weight.device 154 | 155 | def load_grad_dict(self, grad_dict): 156 | state_dict = self.state_dict() 157 | assert isinstance(grad_dict, type(state_dict)) 158 | 159 | for (name, param) in self.named_parameters(): 160 | if name in grad_dict: 161 | 162 | grad = grad_dict[name] 163 | param.grad = grad 164 | else: 165 | param.grad = torch.zeros_like(param.data) 166 | 167 | def forward( 168 | self, 169 | inp, 170 | ): 171 | device = self.get_device() 172 | x_rows = inp.x_rows.to(device) 173 | x_cols = inp.x_cols.to(device) 174 | x_sepas = inp.x_sepas.to(device) 175 | 176 | edge_index_rowcols = inp.edge_index_rowcols.to(device) # row ix is top, col ix is bottom 177 | edge_vals_rowcols = inp.edge_vals_rowcols.to(device) # currently fully connected, all 1 weight 178 | 179 | edge_index_sepa_cols = inp.edge_index_sepa_cols.to(device) # sepa ix is top, col ix is bottom 180 | edge_vals_sepa_cols = inp.edge_vals_sepa_cols.to(device) # currently fully connected, all 1 weight 181 | 182 | edge_index_sepa_rows = inp.edge_index_sepa_rows.to(device) # sepa ix is top, row ix is bottom 183 | edge_vals_sepa_rows = inp.edge_vals_sepa_rows.to(device) # currently fully connected, all 1 weight 184 | 185 | edge_index_sepa_self = inp.edge_index_sepa_self.to(device) # sepa and sepa self edge 186 | edge_vals_sepa_self = inp.edge_vals_sepa_self.to(device) # currently fully connected, all 1 weight 187 | 188 | if hasattr(inp, 'x_sepas_batch'): 189 | x_sepas_batch = inp.x_sepas_batch.to(device) 190 | batch_size = inp.num_graphs 191 | else: 192 | x_sepas_batch = torch.zeros(x_sepas.shape[0], dtype=torch.long).to(device) 193 | batch_size = 1 194 | 195 | if hasattr(inp, 'x_cols_batch'): 196 | x_cols_batch = inp.x_cols_batch.to(device) 197 | batch_size = inp.num_graphs 198 | else: 199 | x_cols_batch = torch.zeros(x_cols.shape[0], dtype=torch.long).to(device) 200 | batch_size = 1 201 | 202 | if hasattr(inp, 'x_rows_batch'): 203 | x_rows_batch = inp.x_rows_batch.to(device) 204 | else: 205 | x_rows_batch = torch.zeros(x_rows.shape[0], dtype=torch.long).to(device) 206 | 207 | r_edge_index_sepa_cols = torch.stack([edge_index_sepa_cols[1], edge_index_sepa_cols[0]], dim=0) 208 | r_edge_index_sepa_rows = torch.stack([edge_index_sepa_rows[1], edge_index_sepa_rows[0]], dim=0) 209 | r_edge_index_rowcols = torch.stack([edge_index_rowcols[1], edge_index_rowcols[0]], dim=0) 210 | 211 | # First step: linear embedding layers to a common dimension (64) 212 | row_embd = self.row_embedding(x_rows) 213 | sepa_embd = self.sepa_embedding(x_sepas) 214 | 215 | col_embd = self.col_embedding(x_cols) 216 | 217 | edge_embd_sepas = self.edge_norm_sepas(edge_vals_sepa_cols) 218 | edge_embd_sepa_rows = self.edge_norm_rows(edge_vals_sepa_rows) 219 | edge_embd_rowcols = self.edge_norm_rowcols(edge_vals_rowcols) 220 | 221 | row_embd = self.conv_col_to_row(col_embd, r_edge_index_rowcols, edge_embd_rowcols, row_embd) 222 | col_embd = self.conv_row_to_col(row_embd, edge_index_rowcols, edge_embd_rowcols, col_embd) 223 | 224 | sepa_embd = self.conv_col_to_sepa(col_embd, r_edge_index_sepa_cols, edge_embd_sepas, sepa_embd) 225 | 226 | row_embd = self.conv_sepa_to_row(sepa_embd, edge_index_sepa_rows, edge_embd_sepa_rows, row_embd) 227 | sepa_embd = self.conv_row_to_sepa(row_embd, r_edge_index_sepa_rows, edge_embd_sepa_rows, sepa_embd) 228 | 229 | sepa_att = self.transformer_conv(sepa_embd, edge_index_sepa_self, edge_vals_sepa_self) 230 | 231 | sepa_att = self.sepa_output_embed_module(torch.cat([sepa_embd, sepa_att, x_sepas], dim=-1)) # nodes * feature 232 | row_att = self.row_output_embed_module(row_embd) # nodes * feature 233 | output = torch.cat([torch_geometric.nn.global_mean_pool(sepa_att, x_sepas_batch, size=batch_size), 234 | torch_geometric.nn.global_mean_pool(row_att, x_rows_batch, size=batch_size), 235 | torch_geometric.nn.global_mean_pool(col_embd, x_cols_batch, size=batch_size)], 236 | dim=-1 237 | ) 238 | 239 | output = self.output_module(output) # n_batch * n_cuts 240 | 241 | return output 242 | 243 | class NeuralUCB: 244 | def __init__(self, modelstr, lamb, nu, actions, args): 245 | self.model = getModel(modelstr) 246 | self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 247 | self.model.to(self.device) 248 | self.lamb = lamb 249 | self.total_param = sum(p.numel() for p in self.model.parameters() if p.requires_grad) 250 | self.U = lamb * torch.ones((self.total_param,)) 251 | self.nu = nu 252 | self.actions = actions 253 | self.ucb_on = args.ucb_on 254 | self.ucb_val_on = args.ucb_val_on 255 | self.wrong_time = 0 256 | 257 | def getActions(self, input_context, num=1, eva=False): 258 | self.model.train(False) 259 | UCBs = [] 260 | g_list = [] 261 | actions_context = [] 262 | for action in self.actions: 263 | action_context = _context.getActionContext("bandit", input_context, action) 264 | actions_context.append(action_context) 265 | score = self.model(action_context) 266 | UCB = score.item() 267 | self.model.zero_grad() 268 | if self.ucb_on and (not eva or self.ucb_val_on): 269 | score.backward(retain_graph=True) 270 | tmp = [] 271 | for p in self.model.parameters(): 272 | if p.grad is None: 273 | continue 274 | tmp.append(p.grad.flatten().detach()) 275 | g = torch.cat(tmp) 276 | g_list.append(g) 277 | if (g*g).shape != self.U.shape: 278 | self.U = self.lamb * torch.ones((g*g).shape)# .to(self.device) 279 | self.wrong_time += 1 280 | sigma = torch.sqrt(torch.sum(self.lamb * self.nu * g * g / self.U)) 281 | UCB += sigma.item() 282 | UCBs.append(UCB) 283 | 284 | UCBs = np.array(UCBs) 285 | if eva == True: 286 | UCBs[UCBs < UCBs.max()-1e-9] = -np.inf 287 | 288 | softmax = torch.nn.Softmax(dim=0) 289 | probs = softmax(torch.tensor(UCBs)) 290 | dist = torch.distributions.categorical.Categorical(probs) 291 | is_sample = np.zeros(len(self.actions)) 292 | sampled_actions = [] 293 | sampled_actions_context = [] 294 | sampled_actions_score = [] 295 | while num > 0: 296 | arm = dist.sample() 297 | if is_sample[arm] == 0: 298 | sampled_actions.append(self.actions[arm]) 299 | sampled_actions_context.append(actions_context[arm]) 300 | sampled_actions_score.append(UCBs[arm]) 301 | is_sample[arm] = 1 302 | num -= 1 303 | if self.ucb_on and not eva: 304 | self.U += g_list[arm] * g_list[arm] 305 | return sampled_actions, sampled_actions_context 306 | 307 | def getActionsScores(self, input_context): 308 | self.model.train(False) 309 | UCBs = [] 310 | g_list = [] 311 | actions_context = [] 312 | for action in self.actions: 313 | action_context = _context.getActionContext("bandit", input_context, action) 314 | actions_context.append(action_context) 315 | score = self.model(action_context) 316 | UCB = score.item() 317 | self.model.zero_grad() 318 | if self.ucb_on and self.ucb_val_on: 319 | score.backward(retain_graph=True) 320 | tmp = [] 321 | for p in self.model.parameters(): 322 | if p.grad is None: 323 | continue 324 | tmp.append(p.grad.flatten().detach()) 325 | g = torch.cat(tmp) 326 | g_list.append(g) 327 | if (g*g).shape != self.U.shape: 328 | self.U = self.lamb * torch.ones((g*g).shape)# .to(self.device) 329 | self.wrong_time += 1 330 | sigma = torch.sqrt(torch.sum(self.lamb * self.nu * g * g / self.U)) 331 | UCB += sigma.item() 332 | UCBs.append(UCB) 333 | 334 | return UCBs -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ecole==0.8.1 2 | joblib==1.2.0 3 | numpy==1.19.0 4 | PySCIPOpt==3.3.0 5 | torch==1.9.0 6 | torch_geometric==1.7.2 7 | tqdm==4.65.0 -------------------------------------------------------------------------------- /restricted_space/binpacking-66-132/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/restricted_space/binpacking-66-132/.gitkeep -------------------------------------------------------------------------------- /restricted_space/capfac-100-100/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/restricted_space/capfac-100-100/.gitkeep -------------------------------------------------------------------------------- /restricted_space/combauct-100-500/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/restricted_space/combauct-100-500/.gitkeep -------------------------------------------------------------------------------- /restricted_space/indset-500/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/restricted_space/indset-500/.gitkeep -------------------------------------------------------------------------------- /restricted_space/load_balancing/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/restricted_space/load_balancing/.gitkeep -------------------------------------------------------------------------------- /restricted_space/maxcut-54-134/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/restricted_space/maxcut-54-134/.gitkeep -------------------------------------------------------------------------------- /restricted_space/miplib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/restricted_space/miplib/.gitkeep -------------------------------------------------------------------------------- /restricted_space/nnv/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/restricted_space/nnv/.gitkeep -------------------------------------------------------------------------------- /restricted_space/packing-60-60/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mit-wu-lab/learning-to-configure-separators/f53c59e0225660217b5454db1866d00042d3f828/restricted_space/packing-60-60/.gitkeep -------------------------------------------------------------------------------- /restricted_space_generation/explore_subspace.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | import random 5 | 6 | from joblib import Parallel, delayed 7 | from multiprocessing import cpu_count as _cpu_count 8 | import pyscipopt as pyopt 9 | import numpy as np 10 | import copy 11 | import math 12 | 13 | path_to_randomness_control_set = '../SCIP_settings/randomness_control.set' 14 | error_signal = -999999999 15 | instances_list = [] 16 | 17 | def solve(path_to_problem, action=None,default=-1): 18 | sepa_list = [ 19 | # 'closecuts', 20 | 'disjunctive', 21 | # '#SM', 22 | # '#CS', 23 | 'convexproj', 24 | 'gauge', 25 | 'impliedbounds', 26 | 'intobj', 27 | 'gomory', 28 | 'cgmip', 29 | 'strongcg', 30 | 'aggregation', 31 | 'clique', 32 | 'zerohalf', 33 | 'mcf', 34 | 'eccuts', 35 | 'oddcycle', 36 | 'flowcover', 37 | 'cmir', 38 | 'rapidlearning' 39 | ] 40 | 41 | # get the solve time of SCIP 42 | model = pyopt.Model() 43 | model.hideOutput(1) 44 | model.readProblem(path_to_problem) 45 | model.readParams(path_to_randomness_control_set) 46 | model.setParam("limits/gap", args.gap_limit) 47 | 48 | if default == -1: 49 | model.optimize() 50 | SCIP_time = model.getSolvingTime() 51 | return SCIP_time 52 | 53 | for index in range(len(sepa_list)): 54 | on_or_off = 1 55 | # data collection: search trajectory 56 | if action[index] < 0.5: 57 | on_or_off = -1 58 | model.setParam(f'separating/{sepa_list[index]}/freq', on_or_off) 59 | model.setParam("limits/time", default*2.5) 60 | model.optimize() 61 | time = model.getSolvingTime() 62 | 63 | improv = (default - time)/default 64 | return improv, time 65 | 66 | def multiprocess_helper(helper_args): 67 | index, action, action_index = helper_args 68 | print(f"process {index}: start!") 69 | 70 | global args 71 | instance_suffix = f"model-{index}/model.mps" 72 | if args.instances == "nnv": 73 | instance_suffix = f"model-{index}/model.proto.lp" 74 | elif args.instances == "load_balancing": 75 | instance_suffix = f"load_balancing_{index}.mps.gz" 76 | elif args.instances == "miplib": 77 | instance_suffix = instances_list[index] 78 | 79 | path_to_problem = os.path.join( 80 | args.path_to_data_dir, 81 | args.instances, 82 | instance_suffix 83 | ) 84 | 85 | if os.path.exists(path_to_problem) == False: 86 | print(path_to_problem) 87 | return error_signal, error_signal, index, action_index 88 | 89 | default_time = 0 90 | repeat_time = 3 91 | for i in range(repeat_time): 92 | default_time += solve( 93 | path_to_problem, 94 | action=None, 95 | default=-1 96 | ) / repeat_time 97 | print(f"process {index}, {action_index}: finish default") 98 | 99 | action_improv = 0 100 | action_time = 0 101 | for i in range(repeat_time): 102 | action_improv_cur, action_time_cur = solve( 103 | path_to_problem, 104 | action=action, 105 | default=default_time 106 | ) 107 | action_improv += action_improv_cur / repeat_time 108 | action_time += action_time_cur / repeat_time 109 | print(f"process {index}, {action_index}: finish emprical mask") 110 | return action_improv, action_time, index, action_index 111 | 112 | def find_actions_near_zero(factor, index, action, actions): 113 | if index >= 17 or action.sum() >= factor: 114 | actions.append(copy.deepcopy(action)) 115 | return actions 116 | 117 | action[index] = 0 118 | actions = find_actions_near_zero(factor, index+1, copy.deepcopy(action), copy.deepcopy(actions)) 119 | action[index] = 1 120 | actions = find_actions_near_zero(factor, index+1, copy.deepcopy(action), copy.deepcopy(actions)) 121 | 122 | return actions 123 | 124 | def getMasks(args): 125 | actions = [] 126 | path_to_constant = args.path_to_ins_agnst + '/' + args.instances + "/best_ins_agnostic.npy" 127 | best_constant = np.load(path_to_constant) 128 | if args.mode == "mask_space": 129 | one_index = [] 130 | for i in range(17): 131 | if best_constant[i] > 0.5: 132 | one_index.append(i) 133 | for i in range(2**len(one_index)): 134 | action = np.zeros((17,)) 135 | now = i 136 | for j in one_index: 137 | action[j] = now % 2 138 | now = int(now/2) 139 | actions.append(action) 140 | elif args.mode == "near_zero" or args.mode == "near_mask": 141 | actions_helper = find_actions_near_zero(args.near_factor, 0, np.zeros(17), []) 142 | if args.mode == "near_mask": 143 | actions = np.array([best_constant] * len(actions_helper)) 144 | for i, indicator in enumerate(actions_helper): 145 | for j in range(17): 146 | if indicator[j] > 0.5: 147 | actions[i][j] = 1 - actions[i][j] 148 | else: 149 | actions = actions_helper 150 | 151 | print("finish finding the actions, the number of actions is: ", len(actions)) 152 | return actions 153 | 154 | def get_LIST(args): 155 | path_to_miplib = os.path.join( 156 | args.path_to_data_dir, 157 | args.instance 158 | ) 159 | tmp_list = os.listdir(path_to_miplib) 160 | path_to_sequence = os.path.join( 161 | args.path_to_data_dir, 162 | args.instance, 163 | "sequence.npy" 164 | ) 165 | sequence = np.load(path_to_sequence) 166 | for i in range(args.index_start, args.index_end): 167 | instances_list.append(tmp_list[sequence[i]]) 168 | args.index_start = 0 169 | args.index_end = len(instances_list) 170 | 171 | return args 172 | 173 | if __name__ == '__main__': 174 | parser = argparse.ArgumentParser() 175 | # Data Settings. 176 | parser.add_argument('--instances', type=str, help="the MILP class") 177 | parser.add_argument('--mode', type=str, help="the sample strategy to apply") 178 | parser.add_argument('--n_cpus', type=int, default=48, help="number of available CPUs (for parallel processing)") 179 | parser.add_argument('--path_to_data_dir', type=str, default="../data", help="directory of the data") 180 | parser.add_argument('--path_to_ins_agnst', type=str, default="../restricted_space/", help="directory of the best instance-agnostic configuration") 181 | parser.add_argument('--save_dir', type=str, default="../restricted_space/", help="save directory of sampled configurations") 182 | parser.add_argument('--gap_limit', type=float, default=0.0, help="gap limit for solving instances") 183 | parser.add_argument('--near_factor', type=int, default=3, help="this parameter controls how large we explore the space") 184 | parser.add_argument('--index_start', type=int, default=0, help="index of the first instance to consider") 185 | parser.add_argument('--index_end', type=int, default=100, help="total number of the instances to consider") 186 | args = parser.parse_args() 187 | args.n_cpus = min(args.n_cpus, _cpu_count()) 188 | 189 | if args.instances == "miplib": 190 | args = get_LIST(args) 191 | args.index_end = 30 192 | 193 | index_start = args.index_start 194 | index_end = args.index_end 195 | 196 | # For the masks 197 | actions = getMasks(args) 198 | a_len = len(actions) 199 | t_len = (index_end - index_start) * a_len 200 | best_constants = [] 201 | indexes = [] 202 | masks_index = [] 203 | for i in range(a_len): 204 | for j in range(index_start, index_end): 205 | best_constants.append(actions[i]) 206 | indexes.append(j) 207 | masks_index.append(i) 208 | 209 | improvs = None 210 | outputs = Parallel(n_jobs=args.n_cpus)( 211 | delayed(multiprocess_helper)(args_) for args_ in list( 212 | zip( 213 | indexes, 214 | best_constants, 215 | masks_index 216 | ) 217 | ) 218 | ) 219 | 220 | improvs, time_abs, return_index, return_mask_index = zip(*outputs) 221 | stats = np.zeros((a_len, index_end-index_start)) 222 | for i in range(t_len): 223 | stats[return_mask_index[i], return_index[i] - index_start] = improvs[i] 224 | for i in range(index_start, index_end): 225 | if stats[:,index_end-i-1].sum() <= error_signal+1: 226 | stats = np.delete(stats, index_end-i-1, axis=1) 227 | path_to_save_dir = args.save_dir + "/" + args.instances + "/" + args.mode 228 | os.makedirs(path_to_save_dir, exist_ok=True) 229 | with open(path_to_save_dir + "/stats.npy", "wb") as f: 230 | np.save(f, stats) 231 | with open(path_to_save_dir + "/configs.npy", "wb") as ff: 232 | np.save(ff, np.array(actions)) -------------------------------------------------------------------------------- /restricted_space_generation/find_best_ins_agnostic.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | import torch.nn as nn 4 | import pyscipopt as pyopt 5 | import argparse 6 | import os 7 | from joblib import Parallel, delayed 8 | from multiprocessing import cpu_count as _cpu_count 9 | 10 | path_to_randomness_control_set = '../SCIP_settings/randomness_control.set' 11 | instances_list = [] 12 | 13 | def solve(path_to_problem, action=None, default=-1): 14 | sepa_list = [ 15 | # 'closecuts', 16 | 'disjunctive', 17 | # '#SM', 18 | # '#CS', 19 | 'convexproj', 20 | 'gauge', 21 | 'impliedbounds', 22 | 'intobj', 23 | 'gomory', 24 | 'cgmip', 25 | 'strongcg', 26 | 'aggregation', 27 | 'clique', 28 | 'zerohalf', 29 | 'mcf', 30 | 'eccuts', 31 | 'oddcycle', 32 | 'flowcover', 33 | 'cmir', 34 | 'rapidlearning' 35 | ] 36 | 37 | # get the solve time of SCIP 38 | model = pyopt.Model() 39 | model.hideOutput(1) 40 | model.readProblem(path_to_problem) 41 | model.readParams(path_to_randomness_control_set) 42 | model.setParam("limits/gap", args.gap_limit) 43 | if default == -1: 44 | model.optimize() 45 | SCIP_time = model.getSolvingTime() 46 | return SCIP_time 47 | 48 | for index in range(len(sepa_list)): 49 | on_or_off = 1 50 | # data collection: search trajectory 51 | if action[index] < 0.5: 52 | on_or_off = -1 53 | model.setParam(f'separating/{sepa_list[index]}/freq', on_or_off) 54 | model.setParam("limits/time", default*2.5) 55 | model.optimize() 56 | time = model.getSolvingTime() 57 | 58 | improv = (default - time)/default 59 | return improv 60 | 61 | def multiprocess_helper(helper_args): 62 | index, action = helper_args 63 | global args 64 | instance_suffix = f"model-{index}/model.mps" 65 | if args.instances == "nnv": 66 | instance_suffix = f"model-{index}/model.proto.lp" 67 | elif args.instances == "load_balancing": 68 | instance_suffix = f"load_balancing_{index}.mps.gz" 69 | elif args.instances == "miplib": 70 | instance_suffix = instances_list[index] 71 | 72 | path_to_problem = os.path.join( 73 | args.path_to_data_dir, 74 | args.instances, 75 | instance_suffix 76 | ) 77 | 78 | # for default 79 | default_time = 0 80 | repeat_time = 3 81 | for i in range(repeat_time): 82 | default_time += solve(path_to_problem,action=None,default=-1) / repeat_time 83 | 84 | # for current configuration 85 | improv = 0 86 | for i in range(repeat_time): 87 | improv += solve( 88 | path_to_problem, 89 | action=action, 90 | default=default_time 91 | ) / repeat_time 92 | 93 | return improv, default_time 94 | 95 | def get_LIST(args): 96 | path_to_miplib = os.path.join( 97 | args.path_to_data_dir, 98 | args.instances 99 | ) 100 | tmp_list = os.listdir(path_to_miplib) 101 | path_to_sequence = os.path.join( 102 | args.path_to_data_dir, 103 | args.instances, 104 | "sequence.npy" 105 | ) 106 | sequence = np.load(path_to_sequence) 107 | for i in range(args.index_start, args.index_end): 108 | instances_list.append(tmp_list[sequence[i]]) 109 | args.index_start = 0 110 | args.index_end = len(instances_list) 111 | 112 | return args 113 | 114 | if __name__ == '__main__': 115 | parser = argparse.ArgumentParser() 116 | # Data Settings. 117 | parser.add_argument('--instances', type=str, help="the MILP class") 118 | parser.add_argument('--n_cpus', type=int, default=48, help="number of available CPUs (for parallel processing)") 119 | parser.add_argument('--path_to_data_dir', type=str, default="../data/", help="directory of the data") 120 | parser.add_argument('--configs_num', type=int, default=1000, help="number of configurations to sample") 121 | parser.add_argument('--index_start', type=int, default=0, help="index of the first instance to consider") 122 | parser.add_argument('--index_end', type=int, default=100, help="total number of the instances to consider") 123 | parser.add_argument('--gap_limit', type=float, default=0.0, help="gap limit for solving instances") 124 | parser.add_argument('--savedir', type=str, default="../restricted_space/", help="save directory of best instance-agonistic configuration") 125 | args = parser.parse_args() 126 | 127 | if args.instances == "miplib": 128 | args = get_LIST(args) 129 | args.index_end = 30 130 | 131 | best_constant_action = None 132 | best_score = -float("inf") 133 | path = args.savedir + "/" + args.instances 134 | os.makedirs(path, exist_ok=True) 135 | path_to_action = path + "/best_ins_agnostic.npy" 136 | path_to_score = path + "/best_improv.npy" 137 | for _ in range(args.configs_num): 138 | constant_action = np.random.randint(2,size=17) 139 | outputs = Parallel(n_jobs=args.n_cpus)( 140 | delayed(multiprocess_helper)(args_) for args_ in list( 141 | zip( 142 | range(args.index_start, args.index_end), 143 | [constant_action] * (args.index_end - args.index_start) 144 | ) 145 | ) 146 | ) 147 | raw_data, _ = zip(*outputs) 148 | score = np.mean(np.array(raw_data)) 149 | print("----------------------") 150 | print(f"Current configuration is") 151 | print(constant_action) 152 | print(f"Current improv is {score}") 153 | if score > best_score: 154 | best_constant_action = constant_action 155 | best_score = score 156 | np.save(path_to_action, best_constant_action) 157 | np.save(path_to_score, np.array(best_score)) 158 | 159 | print(f"Best instance agnostic is {best_constant_action}.") 160 | print(f"Best improvement is {best_score}.") 161 | -------------------------------------------------------------------------------- /restricted_space_generation/get_actions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import argparse 3 | import os 4 | 5 | def get_action_space(args): 6 | path_prefix = f"{args.path_to_space_dir}/{args.instances}/" 7 | spaces = ["near_zero", "near_mask", "mask_space"] 8 | all_actions = [] 9 | all_scores = [] 10 | all_scores_detail = [] 11 | for space in spaces: 12 | path_to_mask = path_prefix + space + "/configs.npy" 13 | path_to_score = path_prefix + space + "/stats.npy" 14 | 15 | if os.path.exists(path_to_mask) == False: 16 | continue 17 | 18 | masks = np.load(path_to_mask) 19 | scores = np.load(path_to_score) 20 | 21 | all_actions += masks.tolist() 22 | all_scores += scores.mean(axis=1).tolist() 23 | all_scores_detail += scores.tolist() 24 | rank = np.argsort(-np.array(all_scores)) 25 | actions = [] 26 | return_scores = [] 27 | for i in range(min(args.action_space_size, len(all_actions))): 28 | actions.append(np.array(all_actions[rank[i]]).reshape(-1,1)) 29 | return_scores.append(np.array(all_scores_detail[rank[i]])) 30 | return actions, return_scores 31 | 32 | if __name__ == "__main__": 33 | parser = argparse.ArgumentParser() 34 | parser.add_argument("--instances", type=str, help="the MILP class") 35 | parser.add_argument("--action_space_size", type=int, default=999999999, help="the size of the action space") 36 | parser.add_argument('--path_to_space_dir', type=str, default="../restricted_space/", help="the directory of the subspace") 37 | args = parser.parse_args() 38 | actions, return_scores = get_action_space(args) 39 | np.save(f"{args.path_to_space_dir}/{args.instances}/action_space.npy", np.array(actions)) 40 | np.save(f"{args.path_to_space_dir}/{args.instances}/action_scores.npy", np.array(return_scores)) -------------------------------------------------------------------------------- /restricted_space_generation/restricted_space_generation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "instances = \"binpacking-66-132\" # specify the instance class here" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 2, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "all_actions = np.load(f\"../restricted_space/{instances}/action_space.npy\")\n", 20 | "all_scores = np.load(f\"../restricted_space/{instances}/action_scores.npy\")\n", 21 | "# one may change the path to the action space and action scores above" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "import matplotlib.pylab as plt\n", 31 | "import seaborn as sns" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "def check_action(action, actions):\n", 41 | " flag = 0\n", 42 | " for pre_action in actions:\n", 43 | " flag = 1\n", 44 | " for jj in range(17):\n", 45 | " if abs(pre_action[jj,0] - action[jj,0]) > 1e-9:\n", 46 | " flag = 0\n", 47 | " break\n", 48 | " if flag == 1:\n", 49 | " break\n", 50 | " return flag\n", 51 | "\n", 52 | "def construct_smart_space(all_actions, all_scores, b, sas_size=30):\n", 53 | " # smart action space / mask upper bound action space\n", 54 | " smart_actions = [all_actions[0,:,:]]\n", 55 | " smart_actions_scores = [all_scores[0,:]]\n", 56 | " selected = np.zeros(all_actions.shape[0])\n", 57 | " selected[0] = 1\n", 58 | " for i in range(1,sas_size):\n", 59 | " index = None\n", 60 | " improv = -1\n", 61 | " cur_score = np.array(smart_actions_scores)[:i,:].max(axis=0).mean()\n", 62 | " for j in range(1,all_actions.shape[0]):\n", 63 | " if all_scores[j,:].mean() < b: # 0.3 default\n", 64 | " continue\n", 65 | " if selected[j] == 1:\n", 66 | " continue\n", 67 | " if check_action(all_actions[j,:,:], smart_actions) == True:\n", 68 | " selected[j] = 1\n", 69 | " continue\n", 70 | " new_score = np.array(smart_actions_scores[:i] + [all_scores[j,:]]).max(axis=0).mean()\n", 71 | " if new_score - cur_score > improv:\n", 72 | " index = j\n", 73 | " improv = new_score - cur_score\n", 74 | " if index == None:\n", 75 | " break\n", 76 | " smart_actions.append(all_actions[index,:,:])\n", 77 | " smart_actions_scores.append(all_scores[index,:])\n", 78 | " return smart_actions_scores" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "our_size = 20\n", 88 | "Bs = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.525]\n", 89 | "# adjust the size of the restricted space and the threshold above\n", 90 | "means = []\n", 91 | "ubs = []\n", 92 | "all_stats = []\n", 93 | "for b in Bs:\n", 94 | " smart_actions_scores = construct_smart_space(all_actions, all_scores, b, our_size * 3)\n", 95 | " means.append(np.array(smart_actions_scores[:our_size]).mean(axis=1).mean())\n", 96 | " ubs.append(np.array(smart_actions_scores[:our_size]).max(axis=0).mean())\n", 97 | " all_stats.append(smart_actions_scores)\n", 98 | "print(means)\n", 99 | "print(ubs)" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "st = 0\n", 109 | "\n", 110 | "sns.set_palette(\"colorblind\")\n", 111 | "\n", 112 | "plt.figure(figsize=(8, 4))\n", 113 | "\n", 114 | "num_colors = 6\n", 115 | "\n", 116 | "colorblind_palette = sns.color_palette(\"colorblind\")\n", 117 | "\n", 118 | "color = colorblind_palette[0] # rocket_palette[2]\n", 119 | "plt.plot(Bs[st:], ubs[st:], label = \"1st\", color= color, marker='o')\n", 120 | "plt.plot(Bs[st:], means[st:], label = \"2nd\", color= color, linestyle = '--', marker='o') # , linestyle = '--'\n", 121 | "\n", 122 | "\n", 123 | "plt.legend()\n", 124 | "\n", 125 | "fontsize = 15\n", 126 | "# Set the plot title and labels\n", 127 | "plt.xlabel('threshold $b$', fontsize=fontsize)\n", 128 | "plt.ylabel('performace', fontsize=fontsize)\n", 129 | "\n", 130 | "# Increase the legend font size\n", 131 | "plt.legend(fontsize=fontsize)\n", 132 | "\n", 133 | "# Set the font size of tick labels\n", 134 | "plt.xticks(fontsize=fontsize)\n", 135 | "plt.yticks(fontsize=fontsize)\n", 136 | "\n", 137 | "plt.grid(True)\n", 138 | "\n", 139 | "\n", 140 | "plt.show()" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [ 149 | "mean_bs = []\n", 150 | "ub_bs = []\n", 151 | "show_Bs = Bs[2:-1]\n", 152 | "c_id = 0\n", 153 | "\n", 154 | "num_colors = len([b for b in Bs if b in show_Bs])\n", 155 | "\n", 156 | "colorblind_palette = sns.color_palette(\"colorblind\")\n", 157 | "\n", 158 | "rocket_palette = sns.color_palette(\"rocket\", n_colors=5)[::-1]\n", 159 | "\n", 160 | "mako_palette = sns.color_palette(\"mako\", n_colors=num_colors)[::-1]\n", 161 | "linestyle_cycler = [\"-\", \"--\", \"-.\", \":\", \"-\", \"--\", \"-.\"]\n", 162 | "color_list = [\"#045275\", \"#089099\", \"#7CCBA2\", \"#FCDE9C\", \"#FD746E\", \"#DC3977\", \"#7C1D6F\"]\n", 163 | "\n", 164 | "plt.figure(figsize=(8, 4))\n", 165 | "\n", 166 | "for b_id, b in enumerate(Bs):\n", 167 | " if b not in show_Bs:\n", 168 | " continue\n", 169 | " mean_b = []\n", 170 | " ub_b = []\n", 171 | " for size_A in range(len(all_stats[b_id])):\n", 172 | " mean_b.append(np.array(all_stats[b_id][:size_A+1]).mean(axis=0).mean())\n", 173 | " ub_b.append(np.array(all_stats[b_id][:size_A+1]).max(axis=0).mean())\n", 174 | " \n", 175 | " color = mako_palette[c_id] # colorblind_palette[2*c_id+1]\n", 176 | " \n", 177 | " c_id += 1\n", 178 | " linewidth = 1.6\n", 179 | " alpha=0.8\n", 180 | " ub_label = f\"$b=${b}; 1st\"\n", 181 | " mean_label = f\"$b=${b}; 2nd\"\n", 182 | " \n", 183 | " line, = plt.plot(range(1,1+len(all_stats[b_id])), ub_b, label = ub_label, \n", 184 | " color=color, alpha=alpha, linewidth=linewidth, marker='.') # \n", 185 | " plt.plot(range(1,1+len(all_stats[b_id])), mean_b, label = mean_label, \n", 186 | " color=color, linestyle='--', alpha=alpha, linewidth=linewidth, marker='.') #\n", 187 | "\n", 188 | " \n", 189 | " mean_bs.append(mean_b)\n", 190 | " ub_bs.append(ub_b)\n", 191 | "\n", 192 | "plt.grid(True)\n", 193 | "\n", 194 | "fontsize = 15\n", 195 | "# Set the plot title and labels\n", 196 | "plt.xlabel('subspace size $|A|$', fontsize=fontsize)\n", 197 | "plt.ylabel('performance', fontsize=fontsize)\n", 198 | "\n", 199 | "# Increase the legend font size\n", 200 | "plt.legend(fontsize=fontsize, loc='center right', bbox_to_anchor=(1.43, 0.5), )\n", 201 | "\n", 202 | "# Set the font size of tick labels\n", 203 | "plt.xticks(fontsize=fontsize)\n", 204 | "plt.yticks(fontsize=fontsize)\n", 205 | "plt.xlim([0, 25])\n", 206 | "\n", 207 | "plt.show()" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 45, 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [ 216 | "final_size = 15\n", 217 | "final_b = 0.4\n", 218 | "# finalize the restricted subspace size and the threshold above\n", 219 | "restricted_actions_space, _ = construct_smart_space(all_actions, all_scores, final_b, final_size)\n", 220 | "np.save(f\"../restricted_space/{instances}/restricted_actions_space.npy\", np.array(all_actions)[:final_size,:,:])\n", 221 | "np.save(f\"../restricted_space/{instances}/inst_agnostic_action.npy\", np.array(all_actions)[0,:,:])\n", 222 | "# one may change the save directory above" 223 | ] 224 | } 225 | ], 226 | "metadata": { 227 | "kernelspec": { 228 | "display_name": "Python 3 (ipykernel)", 229 | "language": "python", 230 | "name": "python3" 231 | }, 232 | "language_info": { 233 | "codemirror_mode": { 234 | "name": "ipython", 235 | "version": 3 236 | }, 237 | "file_extension": ".py", 238 | "mimetype": "text/x-python", 239 | "name": "python", 240 | "nbconvert_exporter": "python", 241 | "pygments_lexer": "ipython3", 242 | "version": "3.8.11" 243 | } 244 | }, 245 | "nbformat": 4, 246 | "nbformat_minor": 4 247 | } 248 | -------------------------------------------------------------------------------- /states.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import states_helpers as _helpers 3 | 4 | def getState(statestr, model, round_num=0): 5 | ### immediate steps 6 | # log_lookahead_score 7 | # lpobjval in state 8 | # lookahead num_branching candidates (for interest) 9 | # transform pickle -> numpy array to be more storage efficient 10 | 11 | # 1000 instances, 50 rounds, 1 cut per round (but code should support more cuts per round) 12 | ## let's assume 500 cuts per round (!), 13 | # 500 cuts per round, for parallelism give 0.5 MB 14 | # so 1000 instances give 0.5 GB for parallelism 15 | # for state, depends on size of instance, 16 | # we get 1000 x 50 x 1 states. so it'd be nice if it wasn't super large.. 17 | 18 | if statestr == 'learn1': 19 | # should contain all data we want. 20 | ### assertion that col names are unique ### 21 | rows = model.getLPRowsData() 22 | cuts = model.getPoolCuts() + model.getCuts() 23 | cols = model.getLPColsData() 24 | 25 | row_features = _helpers.computeRowFeatures1(rows, model, round_num=round_num) 26 | col_features = _helpers.computeColFeatures1(cols, model, round_num=round_num) 27 | cut_features = _helpers.computeRowFeatures1(cuts, model, round_num=round_num) 28 | sepa_features = _helpers.computeSepaFeatures1(model, round_num=round_num) 29 | 30 | state = { 31 | 'cut_input_scores': _helpers.computeInputScores(cuts, model), 32 | 'row_input_scores': _helpers.computeInputScores(rows, model), 33 | 'cut_lookahead_scores': np.random.rand(len(cuts), 3), # _helpers.computeLookaheadScores(cuts, model), -> this triggers some bug 34 | 'row_features': row_features, 35 | 'col_features': col_features, 36 | 'cut_features': cut_features, 37 | 'cut_parallelism': _helpers.computeCutParallelism(cuts, model), 38 | 'cutrow_parallelism': _helpers.computeCutRowParallelism(cuts, rows, model), 39 | 'row_coefs': _helpers.computeCoefs(rows, cols, model), 40 | 'cut_coefs': _helpers.computeCoefs(cuts, cols, model), 41 | 'sepa_features': sepa_features 42 | } 43 | 44 | elif statestr == 'learn2': 45 | # should contain all data we want. 46 | ### assertion that col names are unique ### 47 | rows = model.getLPRowsData() 48 | cuts = model.getOptPoolCuts() 49 | cols = model.getLPColsData() 50 | 51 | state = { 52 | 'cut_input_scores': _helpers.computeInputScores(cuts, model), 53 | 'row_input_scores': _helpers.computeInputScores(rows, model), 54 | 'cut_lookahead_scores': _helpers.computeLookaheadScores(cuts, model), 55 | 'row_features': _helpers.computeRowFeatures1(rows, model), 56 | 'col_features': _helpers.computeColFeatures1(cols, model), 57 | 'cut_features': _helpers.computeRowFeatures1(cuts, model), 58 | 'cut_parallelism': _helpers.computeCutParallelism(cuts, model), 59 | 'cutrow_parallelism': _helpers.computeCutRowParallelism(cuts, rows, model), 60 | 'row_coefs': _helpers.computeCoefs(rows, cols, model), 61 | 'cut_coefs': _helpers.computeCoefs(cuts, cols, model), 62 | } 63 | 64 | elif statestr == 'scores': 65 | rows = model.getLPRowsData() 66 | cuts = model.getOptPoolCuts() 67 | 68 | state = { 69 | 'cut_input_scores': _helpers.computeInputScores(cuts, model), 70 | 'cut_lookahead_scores': _helpers.computeLookaheadScores(cuts, model), 71 | 'cut_types': _helpers.computeCutTypes(cuts), 72 | } 73 | 74 | elif statestr == 'scores_parallelism': 75 | # this is the stuff we might want to consider for population-based 76 | # scoring only.. 77 | 78 | rows = model.getLPRowsData() 79 | cuts = model.getOptPoolCuts() 80 | 81 | state = { 82 | 'cut_input_scores': _helpers.computeInputScores(cuts, model), 83 | 'cut_lookahead_scores': _helpers.computeLookaheadScores(cuts, model), 84 | 'cut_types': _helpers.computeCutTypes(cuts), 85 | 'cut_parallelism': _helpers.computeCutParallelism(cuts, model), 86 | 'cutrow_parallelism': _helpers.computeCutRowParallelism(cuts, rows, model), 87 | } 88 | 89 | else: 90 | raise ValueError(f'Unknown state identifier: {statestr}') 91 | 92 | return state 93 | -------------------------------------------------------------------------------- /states_helpers.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | 4 | CUT_IDENTIFIERS_TO_NUMS = { 5 | 'cmir': 1, 6 | 'flowcover': 2, 7 | 'clique': 3, 8 | 'dis': 4, #. ? 9 | 'gom': 5, 10 | 'implbd': 6, 11 | 'mcf': 7, 12 | 'oddcycle': 8, 13 | 'scg': 9, 14 | 'zerohalf': 10 15 | } 16 | 17 | SCIP_CUT_IDENTIFIERS_TO_NUMS = { 18 | # 'closecuts', 19 | 'disjunctive': 1, 20 | # '#SM', 21 | # '#CS', 22 | 'convexproj': 2, 23 | 'gauge': 3, 24 | 'impliedbounds': 4, 25 | 'intobj': 5, 26 | 'gomory': 6, 27 | 'cgmip': 7, 28 | 'strongcg': 8, 29 | 'aggregation': 9, 30 | 'clique': 10, 31 | 'zerohalf': 11, 32 | 'mcf': 12, 33 | 'eccuts': 13, 34 | 'oddcycle': 14, 35 | 'flowcover': 15, 36 | 'cmir': 16, 37 | 'rapidlearning': 17 38 | } 39 | 40 | def computeSepas(sepa_states): 41 | res = [None for _ in range(len(sepa_states))] 42 | for sepa, v in sepa_states.items(): 43 | res[CUT_IDENTIFIERS_TO_NUMS[sepa]-1] = v 44 | return res 45 | 46 | def computeSCIPSepas(sepa_states): 47 | res = [None for _ in range(len(sepa_states))] 48 | for sepa, v in sepa_states.items(): 49 | res[SCIP_CUT_IDENTIFIERS_TO_NUMS[sepa]-1] = v 50 | return res 51 | 52 | 53 | def getCutTypeFromName(cut_name): 54 | 55 | for k, v in CUT_IDENTIFIERS_TO_NUMS.items(): 56 | if k in cut_name: 57 | return v 58 | # unknown, return zero.. 59 | return 0 60 | 61 | def get_names(model): 62 | # we stick to the order of names this function returns 63 | row_names = [] 64 | col_names = [] 65 | cut_names = [] 66 | 67 | for row in model.getLPRowsData(): 68 | row_names.append(row.name) 69 | 70 | for col in model.getCols(): 71 | col_names.append(col.name) 72 | 73 | for cut in model.getOptPoolCuts(): 74 | cut_names.append(cut.name) 75 | 76 | return row_names, col_names, cut_names 77 | 78 | def computeInputScores(cuts, model): 79 | score_funcs = [ 80 | model.getCutViolation, 81 | model.getCutRelViolation, 82 | model.getCutObjParallelism, 83 | model.getCutEfficacy, 84 | # model.getCutDirectedCutoffDistance, 85 | # model.getCutAdjustedDirectedCutoffDistance, 86 | model.getCutSCIPScore, 87 | model.getCutExpImprov, 88 | model.getCutSupportScore, 89 | model.getCutIntSupport, 90 | ] 91 | 92 | scores = np.empty((len(cuts), len(score_funcs)), dtype=np.float32) 93 | for i, cut in enumerate(cuts): 94 | for j, score_func in enumerate(score_funcs): 95 | scores[i, j] = score_func(cut) 96 | 97 | return scores 98 | 99 | def computeLookaheadScores(cuts, model): 100 | 101 | lpobjval = model.getLPObjVal() 102 | scores = np.empty([len(cuts), 3]) 103 | scores[:, 0] = lpobjval 104 | 105 | for (i, cut) in enumerate(cuts): 106 | scores[i, 1] = model.getCutLookaheadScore(cut) 107 | scores[i, 2] = model.getCutLookaheadLPObjval(cut) 108 | 109 | return scores 110 | 111 | 112 | def computeSepaFeatures1(model, round_num=0): 113 | features = [] 114 | stats = model.getSepaCumulatedStatics() 115 | for sepa_name, sepa_freq in SCIP_CUT_IDENTIFIERS_TO_NUMS.items(): 116 | ft = {'#calls': stats[sepa_name]['time'], 'time': stats[sepa_name]['time'], 117 | '#cuts': stats[sepa_name]['#cuts'], '#cutoffs': stats[sepa_name]['#cutoffs'], 118 | '#applied': stats[sepa_name]['#applied'], 119 | 'round_num': round_num, 'name': sepa_name} 120 | 121 | features.append(ft) 122 | return features 123 | 124 | 125 | def computeRowFeatures1(rows, model, round_num=0): 126 | features = [] 127 | for row in rows: 128 | ft = model.getRowFeatures1(row) 129 | ft['round_num'] = round_num 130 | features.append(ft) 131 | return features 132 | 133 | def computeColFeatures1(cols, model, round_num=0): 134 | features = [] 135 | for col in cols: 136 | ft = model.getColFeatures1(col) 137 | ft['round_num'] = round_num 138 | features.append(ft) 139 | return features 140 | 141 | def computeCoefs(rows, cols, model): 142 | # hash col position for fast retrieval.. 143 | col_dict = {} 144 | for j, col in enumerate(cols): 145 | colname = col.getVar().name 146 | assert not (colname in col_dict) 147 | col_dict[colname] = j 148 | 149 | coefs = {} 150 | for (i, row) in enumerate(rows): 151 | row_cols = row.getCols() 152 | row_js = [col_dict[col.getVar().name] for col in row_cols if col.getVar().name in col_dict] 153 | row_coefs = [val for val, col in zip(row.getVals(), row_cols) if col.getVar().name in col_dict] 154 | coefs[i] = (row_js, row_coefs) 155 | 156 | return coefs 157 | 158 | def computeCutTypes(cuts): 159 | 160 | cut_types = np.empty((len(cuts), ), dtype=np.int32) 161 | 162 | for i, cut in enumerate(cuts): 163 | cut_types[i] = getCutTypeFromName(cut.name) 164 | 165 | return cut_types 166 | 167 | def computeCutParallelism(cuts, model): 168 | 169 | cut_parallelism = [] 170 | for i, cut1 in enumerate(cuts[:-1]): # exclude the very last 171 | for cut2 in cuts[(i+1):]: 172 | cut_parallelism.append(model.getRowParallelism(cut1, cut2)) 173 | 174 | cut_parallelism = np.array(cut_parallelism, dtype=np.float32) #1D 175 | return cut_parallelism 176 | 177 | def computeCutRowParallelism(cuts, rows, model): 178 | 179 | row_parallelism = np.empty((len(cuts), len(rows)), dtype=np.float32) 180 | 181 | for i, cut1 in enumerate(cuts): # exclude the very last [:-1] 182 | for j, row in enumerate(rows): 183 | row_parallelism[i, j] = model.getRowParallelism(cut1, row) 184 | 185 | return row_parallelism -------------------------------------------------------------------------------- /test_k2.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | import random 5 | 6 | import utils as _utils 7 | import torch 8 | import torch.nn as nn 9 | 10 | from joblib import Parallel, delayed 11 | from multiprocessing import cpu_count as _cpu_count 12 | import pyscipopt as pyopt 13 | import numpy as np 14 | import copy 15 | import math 16 | import time 17 | 18 | import context as _context 19 | 20 | import model as _b_model 21 | 22 | path_to_randomness_control_set = './SCIP_settings/randomness_control.set' 23 | error_signal = -999999999 24 | instances_list = [] 25 | 26 | class SepaManager_SM(pyopt.Sepa): 27 | # Defaults (shouldn't matter) 28 | SEPA_NAME = '#SM' 29 | SEPA_DESC = 'special sepa manager' 30 | SEPA_FREQ = 1 31 | SEPA_MAXBOUNDDIST = 1.0 32 | SEPA_USESSUBSCIP = False 33 | SEPA_DELAY = False 34 | SEPA_PRIORITY = 1 35 | 36 | # Info record 37 | def __init__(self): 38 | super().__init__() 39 | self.History_SM = [] 40 | 41 | def sepaexeclp(self): 42 | execution_time_tmp = time.time() 43 | if self.sepa_round in self.actions.keys(): 44 | action = self.actions[self.sepa_round] 45 | for index in range(len(self.sepa_list)): 46 | on_or_off = 1 47 | if action[index, 0] == 0: 48 | on_or_off = -1 49 | self.model.setParam(f'separating/{self.sepa_list[index]}/freq', on_or_off) 50 | self.sepa_round += 1 51 | self.execution_time += time.time() - execution_time_tmp 52 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 53 | 54 | def sepaexecsol(self): 55 | # actual behaviour is implemented in method self.main 56 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 57 | 58 | def addModel(self, model, actions): 59 | r''' 60 | Call self.addModel(model) instead of model.addSeparator(self, **kwargs) 61 | # max_sepa_round_norm is the constant used to normalize the round feature in the network. It is only used when 62 | # a network is used to select sepa-settings or we need to save_state for neural network input. 63 | ''' 64 | self._check_inputs(model) 65 | model.includeSepa( 66 | self, 67 | self.SEPA_NAME, 68 | self.SEPA_DESC, 69 | self.SEPA_PRIORITY, 70 | self.SEPA_FREQ, 71 | self.SEPA_MAXBOUNDDIST, 72 | self.SEPA_USESSUBSCIP, 73 | self.SEPA_DELAY) 74 | self.sepa_list = [ 75 | # 'closecuts', 76 | 'disjunctive', 77 | # '#SM', 78 | # '#CS', 79 | 'convexproj', 80 | 'gauge', 81 | 'impliedbounds', 82 | 'intobj', 83 | 'gomory', 84 | 'cgmip', 85 | 'strongcg', 86 | 'aggregation', 87 | 'clique', 88 | 'zerohalf', 89 | 'mcf', 90 | 'eccuts', 91 | 'oddcycle', 92 | 'flowcover', 93 | 'cmir', 94 | 'rapidlearning' 95 | ] 96 | self.model.setParam(f'separating/#SM/freq', 1) 97 | self.model.setParam(f'separating/closecuts/freq', -1) 98 | self.sepa_round = 0 99 | self.actions = actions 100 | self.execution_time = 0 101 | assert self.model == model # PySCIPOpt sets that in pyopt.Sepa 102 | 103 | def get_sepa_round(self): 104 | return self.sepa_round 105 | 106 | def _check_inputs(self, model): 107 | assert isinstance(model, pyopt.Model) 108 | # this checks that all attributes to includeSepa are correctly specified. 109 | assert isinstance(self.SEPA_NAME, str) 110 | assert isinstance(self.SEPA_DESC, str) 111 | assert isinstance(self.SEPA_PRIORITY, int) 112 | assert isinstance(self.SEPA_FREQ, int) 113 | assert isinstance(self.SEPA_MAXBOUNDDIST, float) 114 | assert isinstance(self.SEPA_USESSUBSCIP, bool) 115 | assert isinstance(self.SEPA_DELAY, bool) 116 | 117 | def solve(path_to_problem, actions=None,default=-1): 118 | # get the solve time of SCIP 119 | model = pyopt.Model() 120 | model.hideOutput(1) 121 | model.readProblem(path_to_problem) 122 | model.readParams(path_to_randomness_control_set) 123 | if default == -1: 124 | sepa_manager_SM = SepaManager_SM() 125 | sepa_manager_SM.addModel(model, actions={}) 126 | model.optimize() 127 | SCIP_time = model.getSolvingTime() - sepa_manager_SM.execution_time 128 | return SCIP_time 129 | 130 | sepa_manager_SM = SepaManager_SM() 131 | sepa_manager_SM.addModel(model, actions=actions) 132 | model.setParam(f'limits/time', default * 4) 133 | model.optimize() 134 | time = model.getSolvingTime() - sepa_manager_SM.execution_time 135 | 136 | improv = (default - time)/default 137 | return improv, time 138 | 139 | def multiprocess_helper(helper_args): 140 | index, Bandit, Bandits = helper_args 141 | print(f"process {index}: start!") 142 | 143 | repeat_time = 3 144 | instance_suffix = f"model-{index}/model.mps" 145 | if args.instances == "nnv": 146 | instance_suffix = f"model-{index}/model.proto.lp" 147 | elif args.instances == "load_balancing": 148 | instance_suffix = f"load_balancing_{index}.mps.gz" 149 | elif args.instances == "miplib": 150 | instance_suffix = instances_list[index] 151 | 152 | path_to_problem = os.path.join( 153 | args.path_to_data_dir, 154 | args.instances+'_test', 155 | instance_suffix 156 | ) 157 | 158 | if os.path.exists(path_to_problem) == False: 159 | return error_signal, error_signal 160 | 161 | actions = None 162 | input_context, actions = _context.getInputContextAndInstruction(path_to_problem, args.step_k2, args.instances, Bandits=Bandits) 163 | if input_context == None: 164 | return error_signal, error_signal 165 | best_action, _ = Bandit.getActions(input_context, num=1, eva=True) 166 | best_action = best_action[0] 167 | actions[args.step_k2] = best_action 168 | 169 | print(f"For model-{index}, the best action:") 170 | print(f"step 0: {actions[0][:,0]}") 171 | print(f"step {args.step_k2}: {actions[args.step_k2][:,0]}") 172 | 173 | default_time = 0 174 | for i in range(repeat_time): 175 | default_time += solve( 176 | path_to_problem, 177 | actions=None, 178 | default=-1 179 | ) / repeat_time 180 | print(f"process {index}: finish default") 181 | 182 | our_improv = 0 183 | our_time = 0 184 | for i in range(repeat_time): 185 | our_improv_cur, our_time_cur = solve( 186 | path_to_problem, 187 | actions=actions, 188 | default=default_time 189 | ) 190 | our_improv += our_improv_cur / repeat_time 191 | our_time += our_time_cur / repeat_time 192 | print(f"process {index}: finish emprical mask") 193 | return our_improv, our_time 194 | 195 | def get_LIST(args): 196 | path_to_miplib = os.path.join( 197 | args.path_to_data_dir, 198 | args.instance 199 | ) 200 | tmp_list = os.listdir(path_to_miplib) 201 | path_to_sequence = os.path.join( 202 | args.path_to_data_dir, 203 | args.instance, 204 | "sequence.npy" 205 | ) 206 | sequence = np.load(path_to_sequence) 207 | for i in range(args.index_start, args.index_end): 208 | instances_list.append(tmp_list[sequence[i]]) 209 | args.index_start = 0 210 | args.index_end = len(instances_list) 211 | 212 | return args 213 | 214 | if __name__ == '__main__': 215 | parser = argparse.ArgumentParser() 216 | # Data Settings. 217 | parser.add_argument('--instances', type=str, help="the MILP class") 218 | parser.add_argument('--n_cpus', type=int, default=48, help="number of available CPUs (for parallel processing)") 219 | parser.add_argument('--path_to_data_dir', type=str, default="./data", help="directory of the data") 220 | 221 | parser.add_argument('--actionsdir', type=str, default='./restricted_space', help="the directory to the restricted action space") 222 | parser.add_argument('--actions_name', type=str, default='restricted_actions_space.npy', help="the name of the restricted action space") 223 | parser.add_argument('--index_start', type=int, default=900, help="the start index of the test instances") 224 | parser.add_argument('--index_end', type=int, default=1000, help="the end index of the test instances") 225 | parser.add_argument('--ucb_nu', type=float, default=1.5/1.6, help='nu for control variance') 226 | parser.add_argument('--ucb_lamb', type=float, default=0.001, help='lambda for regularzation') 227 | parser.add_argument('--ucb_on', type=int, default=1, help='whether to use ucb during training; set to 1') 228 | parser.add_argument('--ucb_val_on', type=int, default=1, help='whether to use ucb during validation') 229 | 230 | # k = 2 231 | parser.add_argument('--Z_1_path', type=str, help="the path to the Z matrix at step n2") 232 | parser.add_argument('--model_1_path', type=str, help="the path to the model at step n2") 233 | parser.add_argument('--Z_0_path', type=str, help="the path to the Z matrix at step n1") 234 | parser.add_argument('--model_0_path', type=str, help="the path to the model at step n1") 235 | parser.add_argument('--step_k2', type=int, default=5, help="the value of n2") 236 | 237 | args = parser.parse_args() 238 | args.n_cpus = min(args.n_cpus, _cpu_count()) 239 | 240 | if args.instances == "miplib": 241 | args = get_LIST(args) 242 | 243 | np.random.seed(42) 244 | torch.manual_seed(42) 245 | action_space = None 246 | nn_model = None 247 | Bandit = None 248 | Bandits = None 249 | 250 | path_to_actions = args.actionsdir + f"/{args.instances}/{args.actions_name}" 251 | action_space = np.load(path_to_actions) 252 | nn_model = _b_model.getModel("Neural_UCB") 253 | nn_model.load_state_dict(torch.load(args.model_1_path)) 254 | nn_model.train(False) 255 | U = torch.load(args.Z_1_path) 256 | Bandit = _b_model.NeuralUCB("Neural_UCB", args.ucb_lamb, args.ucb_nu, action_space, args) 257 | Bandit.U = U 258 | Bandit.model = nn_model 259 | 260 | Bandit0 = _b_model.NeuralUCB("Neural_UCB", args.ucb_lamb, args.ucb_nu, action_space, args) 261 | Bandit0.model = _b_model.getModel("Neural_UCB") 262 | Bandit0.model.load_state_dict(torch.load(args.model_0_path)) 263 | Bandit0.model.train(False) 264 | Bandit0.U = torch.load(args.Z_0_path) 265 | Bandits = [(Bandit0, 0)] 266 | 267 | index_start = args.index_start 268 | index_end = args.index_end 269 | outputs = Parallel(n_jobs=args.n_cpus)( 270 | delayed(multiprocess_helper)(args_) for args_ in list( 271 | zip( 272 | range(index_start, index_end), 273 | [Bandit] * (index_end - index_start), 274 | [Bandits] * (index_end - index_start), 275 | ) 276 | ) 277 | ) 278 | 279 | improvs, time_abs = zip(*outputs) 280 | improvs = np.array(improvs) 281 | time_abs = np.array(time_abs) 282 | improvs = improvs[time_abs>-1e-6] 283 | time_abs = time_abs[time_abs>-1e-6] 284 | 285 | name = "ours k=2" 286 | data = improvs 287 | data = np.array(data) 288 | q3, q1 = np.percentile(data, [75 ,25]) 289 | data_IQR = data[(data > q1)*(data < q3)] 290 | print(f"---------------------------------") 291 | print(f"For {name}:") 292 | print(f"The mean: {np.around(data.mean(),3)}") 293 | print(f"The median: {np.around(np.median(data),3)}") 294 | print(f"The IQM: {np.around(data_IQR.mean(),3)}") 295 | print(f"The % of improvement: {np.around((data > -1e-9).sum()/data.size,3)}") 296 | print(f"The std: {np.around(data.std(),3)}") 297 | # print(data) 298 | 299 | q3_abs, q1_abs = np.percentile(time_abs, [75 ,25]) 300 | time_abs_IQR = time_abs[(time_abs > q1_abs)*(time_abs < q3_abs)] 301 | print(f"The abs mean: {np.around(time_abs.mean(),3)}") 302 | print(f"The abs median: {np.around(np.median(time_abs),3)}") 303 | print(f"The abs IQM: {np.around(time_abs_IQR.mean(),3)}") 304 | print(f"The abs std: {np.around(time_abs.std(),3)}") 305 | -------------------------------------------------------------------------------- /train_k1.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | import random 5 | 6 | os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0' 7 | import utils as _utils 8 | import torch 9 | import torch.nn as nn 10 | 11 | from joblib import Parallel, delayed 12 | from multiprocessing import cpu_count as _cpu_count 13 | import pyscipopt as pyopt 14 | import numpy as np 15 | import copy 16 | import math 17 | 18 | import context as _context 19 | import dataset as _dataset 20 | import model as _b_model 21 | from torch.utils.tensorboard import SummaryWriter 22 | 23 | writer = None 24 | writer_train_len = 0 25 | # parameter 26 | path_to_randomness_control_set = './SCIP_settings/randomness_control.set' 27 | embsize = None 28 | instances_list = [] 29 | 30 | def solve(path_to_problem, action): 31 | global args 32 | sepa_list = [ 33 | # 'closecuts', 34 | 'disjunctive', 35 | # '#SM', 36 | # '#CS', 37 | 'convexproj', 38 | 'gauge', 39 | 'impliedbounds', 40 | 'intobj', 41 | 'gomory', 42 | 'cgmip', 43 | 'strongcg', 44 | 'aggregation', 45 | 'clique', 46 | 'zerohalf', 47 | 'mcf', 48 | 'eccuts', 49 | 'oddcycle', 50 | 'flowcover', 51 | 'cmir', 52 | 'rapidlearning' 53 | ] 54 | 55 | # get the solve time of SCIP 56 | SCIP_time = 0 57 | for i in range(args.reward_avg_time): 58 | model = pyopt.Model() 59 | model.hideOutput(1) 60 | model.readProblem(path_to_problem) 61 | model.readParams(path_to_randomness_control_set) 62 | if args.instances == "load_balancing" or args.instances == "miplib": 63 | model.setParam("limits/gap", 0.1) 64 | model.optimize() 65 | SCIP_time += model.getSolvingTime() / args.reward_avg_time 66 | 67 | # get the solve time of the action 68 | our_time = 0 69 | for i in range(args.reward_avg_time): 70 | model = pyopt.Model() 71 | model.hideOutput(1) 72 | model.readProblem(path_to_problem) 73 | model.readParams(path_to_randomness_control_set) 74 | for index in range(len(sepa_list)): 75 | on_or_off = 1 76 | if action[index, 0] == 0: 77 | on_or_off = -1 78 | model.setParam(f'separating/{sepa_list[index]}/freq', on_or_off) 79 | model.setParam("limits/time", SCIP_time * 2.5) 80 | if args.instances == "load_balancing" or args.instances == "miplib": 81 | model.setParam("limits/gap", 0.1) 82 | model.optimize() 83 | our_time += model.getSolvingTime() / args.reward_avg_time 84 | 85 | # clipping 86 | clipping = 1.5 87 | 88 | improv_time = (SCIP_time - our_time) / SCIP_time 89 | reward = improv_time 90 | if reward < -clipping: 91 | reward = -clipping 92 | return improv_time, reward 93 | 94 | 95 | def train(train_set, val_set, nn_model, args): 96 | nn_model.train(True) 97 | optimizer = torch.optim.Adam( 98 | nn_model.parameters(), 99 | lr=args.lr 100 | ) 101 | scheduler = torch.optim.lr_scheduler.StepLR( 102 | optimizer, 103 | step_size=3, 104 | gamma=args.lr_decay 105 | ) 106 | loss_fuction = torch.nn.MSELoss() 107 | train_epoch = args.train_epoch 108 | train_loader = _dataset.getDataloaders(train_set[max(0,len(train_set)-args.max_train_set):], args) 109 | for epoch in range(train_epoch): 110 | global writer_train_len 111 | # log: val 112 | if epoch % args.val_loss_freq == 0: 113 | with torch.no_grad(): 114 | val_predicts = [] 115 | val_labels = [] 116 | for item in val_set: 117 | val_action_context, val_label = item 118 | val_labels.append(val_label) 119 | val_predict = nn_model(val_action_context) 120 | val_predicts.append(val_predict) 121 | val_loss = loss_fuction(torch.tensor(val_labels).float(), torch.tensor(val_predicts).float()) 122 | 123 | writer.add_scalar('val_loss', val_loss, writer_train_len) 124 | writer.add_scalar('val_avg_bias', math.sqrt(val_loss), writer_train_len) 125 | print(f"epoch: {epoch}, val loss: {val_loss}, val avg bias: {math.sqrt(val_loss)}") 126 | 127 | loss_avg = 0 128 | for _, action_contexts in enumerate(train_loader): 129 | num = action_contexts.num_graphs 130 | predicts = nn_model(action_contexts) 131 | labels = action_contexts.labels 132 | loss = loss_fuction(torch.tensor(labels).float(), predicts[:, 0]) 133 | loss_avg += loss * num / len(train_set) 134 | optimizer.zero_grad() 135 | loss.backward() 136 | optimizer.step() 137 | scheduler.step() 138 | 139 | # log: train 140 | writer.add_scalar('train_loss', loss_avg, writer_train_len) 141 | writer.add_scalar('train_avg_bias', math.sqrt(loss_avg), writer_train_len) 142 | writer_train_len += 1 143 | print(f"epoch: {epoch}, loss: {loss_avg}, avg bias: {math.sqrt(loss_avg)}") 144 | nn_model.train(False) 145 | return nn_model 146 | 147 | def get_path_to_problem(index, split='train'): 148 | instance_suffix = f"model-{index}/model.mps" 149 | if args.instances == "nnv": 150 | instance_suffix = f"model-{index}/model.proto.lp" 151 | elif args.instances == "load_balancing": 152 | instance_suffix = f"load_balancing_{index}.mps.gz" 153 | elif args.instances == "miplib": 154 | instance_suffix = instances_list[index] 155 | 156 | path_to_problem = os.path.join( 157 | args.path_to_data_dir, 158 | args.instances+'_'+split, 159 | instance_suffix 160 | ) 161 | 162 | return path_to_problem 163 | 164 | def multiprocess_context_helper(helper_args): 165 | process_num, Bandit, val_flag = helper_args 166 | # sample problem 167 | print(f"process {process_num}: sampling the problem") 168 | model_num = None 169 | path_to_problem = None 170 | 171 | if val_flag == False: 172 | while True: 173 | global args 174 | model_num = np.random.randint(0, args.train_index_end) 175 | path_to_problem = get_path_to_problem(model_num, "train") 176 | if os.path.exists(path_to_problem): 177 | break 178 | else: 179 | model_num = process_num 180 | path_to_problem = get_path_to_problem(model_num, "val") 181 | 182 | # get context for the problem 183 | input_context = _context.getInputContextAtStep(None, path_to_problem, 1, args.instances) 184 | if input_context == None: 185 | if val_flag == True: 186 | my_len = 1 187 | else: 188 | my_len = args.action_sampled_num 189 | return [-1] * my_len, \ 190 | [-1] * my_len, \ 191 | [-1] * my_len, \ 192 | [-1] * my_len 193 | 194 | actions_sampled_num = 1 195 | if val_flag == False: 196 | actions_sampled_num = args.action_sampled_num 197 | actions_play, actions_context = Bandit.getActions(input_context, actions_sampled_num, val_flag) 198 | return actions_context, \ 199 | actions_play, \ 200 | [path_to_problem] * len(actions_play), \ 201 | [process_num] * len(actions_play), \ 202 | 203 | 204 | def multiprocess_solve_helper(helper_args): 205 | subprocess_num, process_num, action, path_to_problem = helper_args 206 | print(f"process {process_num}-{subprocess_num}: solving the problem with the chosen arm...") 207 | improv, reward = solve(path_to_problem, action) 208 | print(f"process {process_num}-{subprocess_num}: at this iteration, the chosen arm improves {improv} r.s.t. SCIP, and get reward {reward}") 209 | return reward, improv 210 | 211 | 212 | def saveModel(model, modelpath): 213 | assert isinstance(model, nn.Module) 214 | torch.save(model.state_dict(), modelpath) 215 | 216 | def unpack_action_contexts(elements, num): 217 | A, B, C, D = [], [], [], [] 218 | a, b, c, d = elements 219 | for i in range(num): 220 | if type(a[i][0]) == type(-1): 221 | continue 222 | A += a[i] 223 | B += b[i] 224 | C += c[i] 225 | D += d[i] 226 | return A, B, C, D 227 | 228 | def unpack_reward(elements): 229 | a, b = elements 230 | return np.array(a), np.array(b) 231 | 232 | def get_LIST(args): 233 | path_to_miplib = os.path.join( 234 | args.path_to_data_dir, 235 | args.instance 236 | ) 237 | tmp_list = os.listdir(path_to_miplib) 238 | path_to_sequence = os.path.join( 239 | args.path_to_data_dir, 240 | args.instance, 241 | "sequence.npy" 242 | ) 243 | sequence = np.load(path_to_sequence) 244 | for i in range(args.index_start, args.index_end): 245 | instances_list.append(tmp_list[sequence[i]]) 246 | args.index_start = 0 247 | args.index_end = len(instances_list) 248 | 249 | return args 250 | 251 | if __name__ == '__main__': 252 | parser = argparse.ArgumentParser() 253 | # Data Settings. 254 | parser.add_argument('--instances', type=str, help="the MILP class") 255 | parser.add_argument('--savedir', type=str, default='./model', help="the directory to save the model") 256 | parser.add_argument('--n_cpus', type=int, default=48, help="number of available CPUs (for parallel processing)") 257 | parser.add_argument('--path_to_data_dir', type=str, default="./data", help="directory of the data") 258 | 259 | # training & bandit settings 260 | parser.add_argument('--lr', type=float, default=0.001, help="the learning rate") 261 | parser.add_argument('--bandit_ins_num', type=int, default=6, help="the number of instances sampled per iteration") 262 | parser.add_argument('--bandit_epoch', type=int, default=100, help="the number of epochs for training the bandit") 263 | parser.add_argument('--train_epoch', type=int, default=100, help="the number of epochs for training the model") 264 | parser.add_argument('--logdir', type=str, default='./log/', help="the directory to save the log") 265 | parser.add_argument('--action_sampled_num', type=int, default=8, help="the number of actions sampled per instance") 266 | parser.add_argument('--lr_decay', type=float, default=1, help="the learning rate decay") 267 | parser.add_argument('--train_index_end', type=int, default=800, help="the number of instances used for training") 268 | parser.add_argument('--reward_avg_time', type=int, default=3, help="the number of interactions with the environment to average the reward") 269 | parser.add_argument('--max_train_set', type=int, default=999999999, help="the maximum number of context-action-reward pairs stored in the training set") 270 | parser.add_argument('--ucb_nu', type=float, default=1.5/1.6, help='nu for control variance') 271 | parser.add_argument('--ucb_lamb', type=float, default=0.001, help='lambda for regularzation') 272 | parser.add_argument('--ucb_on', type=int, default=1, help='whether to use ucb durting training; set to 1') 273 | parser.add_argument('--ucb_val_on', type=int, default=0, help='whether to use ucb during validation') 274 | parser.add_argument('--actionsdir', type=str, default='./restricted_space', help='the directory to save the restricted action space') 275 | parser.add_argument('--actions_name', type=str, default='restricted_actions_space.npy', help='the name of the restricted action space') 276 | 277 | # NN architecture settings 278 | parser.add_argument('--embsize', type=int, default=64, help='the embedding size') 279 | 280 | # validation settings 281 | parser.add_argument('--val_index_start', type=int, default=800, help='the index of the first instance used for validation') 282 | parser.add_argument('--val_index_end', type=int, default=900, help='the index of the last instance used for validation') 283 | parser.add_argument('--val_loss_freq', type=int, default=10, help='the frequency of calculating the validation loss') 284 | 285 | args = parser.parse_args() 286 | args.n_cpus = min(args.n_cpus, _cpu_count()) 287 | print(f'{args.n_cpus} cpus available') 288 | 289 | if args.instances == "miplib": 290 | args = get_LIST(args) 291 | 292 | np.random.seed(42) 293 | torch.manual_seed(42) 294 | if not os.path.exists(args.savedir): 295 | os.makedirs(args.savedir) 296 | path_to_logdir = args.logdir + f"/{args.instances}_k1" 297 | if not os.path.exists(path_to_logdir): 298 | os.makedirs(path_to_logdir) 299 | writer = SummaryWriter(path_to_logdir) 300 | writer_train_len = 0 301 | embsize = args.embsize 302 | 303 | path_to_actions = args.actionsdir + f"/{args.instances}/{args.actions_name}" 304 | action_space = np.load(path_to_actions) 305 | 306 | bandit_epoch = args.bandit_epoch 307 | bandit_ins_num = args.bandit_ins_num 308 | Bandit = _b_model.NeuralUCB("Neural_UCB", args.ucb_lamb, args.ucb_nu, action_space, args) 309 | train_set = [] 310 | for t in range(bandit_epoch): 311 | print("iteration: ", t) 312 | 313 | # log: val 314 | index_val = range(args.val_index_start, args.val_index_end) 315 | outputs = Parallel(n_jobs=args.n_cpus)( 316 | delayed(multiprocess_context_helper)(args_) for args_ in list( 317 | zip( 318 | index_val, 319 | [Bandit] * len(index_val), 320 | [True] * len(index_val) 321 | ) 322 | ) 323 | ) 324 | action_contexts, actions, paths, process_nums = unpack_action_contexts(zip(*outputs), len(index_val)) 325 | outputs = Parallel(n_jobs=args.n_cpus)( 326 | delayed(multiprocess_solve_helper)(args_) for args_ in list( 327 | zip( 328 | range(len(action_contexts)), 329 | process_nums, 330 | actions, 331 | paths 332 | ) 333 | ) 334 | ) 335 | rewards, improvs = unpack_reward(zip(*outputs)) 336 | improvs_val = np.array(improvs) 337 | q3, q1 = np.percentile(improvs_val, [75, 25]) 338 | improvs_val_IQR = improvs_val[(improvs_val > q1) * (improvs_val < q3)] 339 | writer.add_scalars( 340 | 'val_mean_median_IQM', 341 | { 342 | "mean": improvs_val.mean(), 343 | "median": np.median(improvs_val), 344 | "IQM": improvs_val_IQR.mean() 345 | }, 346 | t 347 | ) 348 | writer.add_scalar('val_%', (improvs_val > -1e-9).sum() / improvs_val.size, t) 349 | writer.add_scalar('val_std', improvs_val.std(), t) 350 | 351 | val_set = [] 352 | for action_context, reward in zip(action_contexts, rewards): 353 | val_set.append([action_context, reward]) 354 | 355 | # train: parallel sample different problems 356 | outputs = Parallel(n_jobs=args.n_cpus)( 357 | delayed(multiprocess_context_helper)(args_) for args_ in list( 358 | zip( 359 | range(bandit_ins_num), 360 | [Bandit] * bandit_ins_num, 361 | [False] * bandit_ins_num 362 | ) 363 | ) 364 | ) 365 | action_contexts, actions, paths, process_nums = unpack_action_contexts(zip(*outputs), bandit_ins_num) 366 | outputs = Parallel(n_jobs=args.n_cpus)( 367 | delayed(multiprocess_solve_helper)(args_) for args_ in list( 368 | zip( 369 | range(len(action_contexts)), 370 | process_nums, 371 | actions, 372 | paths 373 | ) 374 | ) 375 | ) 376 | rewards, improvs = unpack_reward(zip(*outputs)) 377 | for index in range(len(action_contexts)): 378 | train_set.append([action_contexts[index], rewards[index]]) 379 | 380 | print("training the model...") 381 | Bandit.model = train(train_set, val_set, Bandit.model, args) 382 | # save the model 383 | os.makedirs(args.savedir + f"/{args.instances}_k1", exist_ok=True) 384 | model_path = args.savedir + f"/{args.instances}_k1/model-{t}" 385 | Z_path = args.savedir + f"/{args.instances}_k1/Z-{t}" 386 | saveModel(Bandit.model, model_path) 387 | torch.save(Bandit.U, Z_path) -------------------------------------------------------------------------------- /train_k2.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | import random 5 | 6 | os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0' 7 | import utils as _utils 8 | import torch 9 | import torch.nn as nn 10 | 11 | from joblib import Parallel, delayed 12 | from multiprocessing import cpu_count as _cpu_count 13 | import pyscipopt as pyopt 14 | import numpy as np 15 | import copy 16 | import math 17 | 18 | import context as _context 19 | import dataset as _dataset 20 | import model as _b_model 21 | from torch.utils.tensorboard import SummaryWriter 22 | 23 | writer = None 24 | writer_train_len = 0 25 | # parameter 26 | path_to_randomness_control_set = './SCIP_settings/randomness_control.set' 27 | embsize = None 28 | instances_list = [] 29 | 30 | class SepaManager_SM(pyopt.Sepa): 31 | # Defaults (shouldn't matter) 32 | SEPA_NAME = '#SM' 33 | SEPA_DESC = 'special sepa manager' 34 | SEPA_FREQ = 1 35 | SEPA_MAXBOUNDDIST = 1.0 36 | SEPA_USESSUBSCIP = False 37 | SEPA_DELAY = False 38 | SEPA_PRIORITY = 1 39 | 40 | # Info record 41 | def __init__(self): 42 | super().__init__() 43 | self.History_SM = [] 44 | 45 | def sepaexeclp(self): 46 | if self.sepa_round in self.actions.keys(): 47 | action = self.actions[self.sepa_round] 48 | for index in range(len(self.sepa_list)): 49 | on_or_off = 1 50 | if action[index, 0] == 0: 51 | on_or_off = -1 52 | self.model.setParam(f'separating/{self.sepa_list[index]}/freq', on_or_off) 53 | self.sepa_round += 1 54 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 55 | 56 | def sepaexecsol(self): 57 | # actual behaviour is implemented in method self.main 58 | return {'result': pyopt.SCIP_RESULT.DIDNOTRUN} 59 | 60 | def addModel(self, model, actions): 61 | r''' 62 | Call self.addModel(model) instead of model.addSeparator(self, **kwargs) 63 | # max_sepa_round_norm is the constant used to normalize the round feature in the network. It is only used when 64 | # a network is used to select sepa-settings or we need to save_state for neural network input. 65 | ''' 66 | self._check_inputs(model) 67 | model.includeSepa( 68 | self, 69 | self.SEPA_NAME, 70 | self.SEPA_DESC, 71 | self.SEPA_PRIORITY, 72 | self.SEPA_FREQ, 73 | self.SEPA_MAXBOUNDDIST, 74 | self.SEPA_USESSUBSCIP, 75 | self.SEPA_DELAY) 76 | self.sepa_list = [ 77 | # 'closecuts', 78 | 'disjunctive', 79 | # '#SM', 80 | # '#CS', 81 | 'convexproj', 82 | 'gauge', 83 | 'impliedbounds', 84 | 'intobj', 85 | 'gomory', 86 | 'cgmip', 87 | 'strongcg', 88 | 'aggregation', 89 | 'clique', 90 | 'zerohalf', 91 | 'mcf', 92 | 'eccuts', 93 | 'oddcycle', 94 | 'flowcover', 95 | 'cmir', 96 | 'rapidlearning' 97 | ] 98 | self.model.setParam(f'separating/#SM/freq', 1) 99 | self.model.setParam(f'separating/closecuts/freq', -1) 100 | self.sepa_round = 0 101 | self.actions = actions 102 | assert self.model == model # PySCIPOpt sets that in pyopt.Sepa 103 | 104 | def get_sepa_round(self): 105 | return self.sepa_round 106 | 107 | def _check_inputs(self, model): 108 | assert isinstance(model, pyopt.Model) 109 | # this checks that all attributes to includeSepa are correctly specified. 110 | assert isinstance(self.SEPA_NAME, str) 111 | assert isinstance(self.SEPA_DESC, str) 112 | assert isinstance(self.SEPA_PRIORITY, int) 113 | assert isinstance(self.SEPA_FREQ, int) 114 | assert isinstance(self.SEPA_MAXBOUNDDIST, float) 115 | assert isinstance(self.SEPA_USESSUBSCIP, bool) 116 | assert isinstance(self.SEPA_DELAY, bool) 117 | 118 | 119 | def solve(path_to_problem, actions): 120 | global args 121 | 122 | # get the solve time of SCIP 123 | SCIP_time = 0 124 | for i in range(args.reward_avg_time): 125 | model = pyopt.Model() 126 | model.hideOutput(1) 127 | model.readProblem(path_to_problem) 128 | model.readParams(path_to_randomness_control_set) 129 | sepa_manager_SM = SepaManager_SM() 130 | sepa_manager_SM.addModel(model, actions={}) 131 | if args.instances == "load_balancing" or args.instances == "miplib": 132 | model.setParam("limits/gap", 0.1) 133 | model.optimize() 134 | SCIP_time += model.getSolvingTime() / args.reward_avg_time 135 | 136 | # get the solve time of the action 137 | our_time = 0 138 | for i in range(args.reward_avg_time): 139 | model = pyopt.Model() 140 | model.hideOutput(1) 141 | model.readProblem(path_to_problem) 142 | model.readParams(path_to_randomness_control_set) 143 | sepa_manager_SM = SepaManager_SM() 144 | sepa_manager_SM.addModel(model, actions=actions) 145 | model.setParam("limits/time", SCIP_time * 2.5) 146 | if args.instances == "load_balancing" or args.instances == "miplib": 147 | model.setParam("limits/gap", 0.1) 148 | model.optimize() 149 | our_time += model.getSolvingTime() / args.reward_avg_time 150 | 151 | # clipping 152 | clipping = 1.5 153 | 154 | improv_time = (SCIP_time - our_time) / SCIP_time 155 | reward = improv_time 156 | 157 | if reward < -clipping: 158 | reward = -clipping 159 | return improv_time, reward 160 | 161 | def train(train_set, val_set, nn_model, args): 162 | nn_model.train(True) 163 | optimizer = torch.optim.Adam( 164 | nn_model.parameters(), 165 | lr=args.lr 166 | ) 167 | scheduler = torch.optim.lr_scheduler.StepLR( 168 | optimizer, 169 | step_size=3, 170 | gamma=args.lr_decay 171 | ) 172 | loss_fuction = torch.nn.MSELoss() 173 | train_epoch = args.train_epoch 174 | train_loader = _dataset.getDataloaders(train_set[max(0,len(train_set)-args.max_train_set):], args) 175 | for epoch in range(train_epoch): 176 | global writer_train_len 177 | # log: val 178 | if epoch % args.val_loss_freq == 0: 179 | with torch.no_grad(): 180 | val_predicts = [] 181 | val_labels = [] 182 | for item in val_set: 183 | val_action_context, val_label = item 184 | val_labels.append(val_label) 185 | val_predict = nn_model(val_action_context) 186 | val_predicts.append(val_predict) 187 | val_loss = loss_fuction(torch.tensor(val_labels).float(), torch.tensor(val_predicts).float()) 188 | 189 | writer.add_scalar('val_loss', val_loss, writer_train_len) 190 | writer.add_scalar('val_avg_bias', math.sqrt(val_loss), writer_train_len) 191 | print(f"epoch: {epoch}, val loss: {val_loss}, val avg bias: {math.sqrt(val_loss)}") 192 | 193 | loss_avg = 0 194 | for _, action_contexts in enumerate(train_loader): 195 | num = action_contexts.num_graphs 196 | predicts = nn_model(action_contexts) 197 | labels = action_contexts.labels 198 | loss = loss_fuction(torch.tensor(labels).float(), predicts[:, 0]) 199 | loss_avg += loss * num / len(train_set) 200 | optimizer.zero_grad() 201 | loss.backward() 202 | optimizer.step() 203 | scheduler.step() 204 | 205 | # log: train 206 | writer.add_scalar('train_loss', loss_avg, writer_train_len) 207 | writer.add_scalar('train_avg_bias', math.sqrt(loss_avg), writer_train_len) 208 | writer_train_len += 1 209 | print(f"epoch: {epoch}, loss: {loss_avg}, avg bias: {math.sqrt(loss_avg)}") 210 | nn_model.train(False) 211 | return nn_model 212 | 213 | def get_path_to_problem(index, split='train'): 214 | instance_suffix = f"model-{index}/model.mps" 215 | if args.instances == "nnv": 216 | instance_suffix = f"model-{index}/model.proto.lp" 217 | elif args.instances == "load_balancing": 218 | instance_suffix = f"load_balancing_{index}.mps.gz" 219 | elif args.instances == "miplib": 220 | instance_suffix = instances_list[index] 221 | 222 | path_to_problem = os.path.join( 223 | args.path_to_data_dir, 224 | args.instances+'_'+split, 225 | instance_suffix 226 | ) 227 | 228 | return path_to_problem 229 | 230 | def multiprocess_context_helper(helper_args): 231 | process_num, Bandits, Bandit, val_flag = helper_args 232 | # sample problem 233 | print(f"process {process_num}: sampling the problem") 234 | model_num = None 235 | path_to_problem = None 236 | 237 | if val_flag == False: 238 | while True: 239 | global args 240 | model_num = np.random.randint(0, args.train_index_end) 241 | path_to_problem = get_path_to_problem(model_num, "train") 242 | if os.path.exists(path_to_problem): 243 | break 244 | else: 245 | model_num = process_num 246 | path_to_problem = get_path_to_problem(model_num, "val") 247 | 248 | # get context for the problem 249 | input_context, actions = _context.getInputContextAndInstruction(path_to_problem, args.step_k2, args.instances, Bandits=Bandits) 250 | if input_context == None: 251 | if val_flag == True: 252 | my_len = 1 253 | else: 254 | my_len = args.action_sampled_num 255 | return [-1] * my_len, \ 256 | [-1] * my_len, \ 257 | [-1] * my_len, \ 258 | [-1] * my_len 259 | 260 | actions_sampled_num = 1 261 | if val_flag == False: 262 | actions_sampled_num = args.action_sampled_num 263 | actions_play, actions_context = Bandit.getActions(input_context, actions_sampled_num, val_flag) 264 | actions = [actions] * len(actions_play) 265 | for idx, action in enumerate(actions_play): 266 | actions[idx][args.step_k2] = action 267 | print("------------------------") 268 | for key in actions[idx].keys(): 269 | print(f"step {key}, {actions[idx][key][:,0]}") 270 | print("------------------------") 271 | return actions_context, \ 272 | actions, \ 273 | [path_to_problem] * len(actions_play), \ 274 | [process_num] * len(actions_play), \ 275 | 276 | 277 | def multiprocess_solve_helper(helper_args): 278 | subprocess_num, process_num, actions, path_to_problem = helper_args 279 | print(f"process {process_num}-{subprocess_num}: solving the problem with the chosen arm...") 280 | improv, reward = solve(path_to_problem, actions) 281 | print(f"process {process_num}-{subprocess_num}: at this iteration, the chosen arm improves {improv} r.s.t. SCIP, and get reward {reward}") 282 | return reward, improv 283 | 284 | 285 | def saveModel(model, modelpath): 286 | assert isinstance(model, nn.Module) 287 | torch.save(model.state_dict(), modelpath) 288 | 289 | 290 | def unpack_action_contexts(elements, num): 291 | A, B, C, D = [], [], [], [] 292 | a, b, c, d = elements 293 | for i in range(num): 294 | if type(a[i][0]) == type(-1): 295 | continue 296 | A += a[i] 297 | B += b[i] 298 | C += c[i] 299 | D += d[i] 300 | return A, B, C, D 301 | 302 | 303 | def unpack_reward(elements): 304 | a, b = elements 305 | return np.array(a), np.array(b) 306 | 307 | def get_LIST(args): 308 | path_to_miplib = os.path.join( 309 | args.path_to_data_dir, 310 | args.instance 311 | ) 312 | tmp_list = os.listdir(path_to_miplib) 313 | path_to_sequence = os.path.join( 314 | args.path_to_data_dir, 315 | args.instance, 316 | "sequence.npy" 317 | ) 318 | sequence = np.load(path_to_sequence) 319 | for i in range(args.index_start, args.index_end): 320 | instances_list.append(tmp_list[sequence[i]]) 321 | args.index_start = 0 322 | args.index_end = len(instances_list) 323 | 324 | return args 325 | 326 | 327 | if __name__ == '__main__': 328 | parser = argparse.ArgumentParser() 329 | # Data Settings. 330 | parser.add_argument('--instances', type=str, help="the MILP class") 331 | parser.add_argument('--savedir', type=str, default='./model', help="the directory to save the model") 332 | parser.add_argument('--n_cpus', type=int, default=48, help="number of available CPUs (for parallel processing)") 333 | parser.add_argument('--path_to_data_dir', type=str, default="./data", help="directory of the data") 334 | 335 | # training & bandit settings 336 | parser.add_argument('--lr', type=float, default=0.001, help="the learning rate") 337 | parser.add_argument('--bandit_ins_num', type=int, default=6, help="the number of instances sampled per iteration") 338 | parser.add_argument('--bandit_epoch', type=int, default=100, help="the number of epochs for training the bandit") 339 | parser.add_argument('--train_epoch', type=int, default=100, help="the number of epochs for training the model") 340 | parser.add_argument('--logdir', type=str, default='./log/', help="the directory to save the log") 341 | parser.add_argument('--action_sampled_num', type=int, default=8, help="the number of actions sampled per instance") 342 | parser.add_argument('--lr_decay', type=float, default=1, help="the learning rate decay") 343 | parser.add_argument('--train_index_end', type=int, default=800, help="the number of instances used for training") 344 | parser.add_argument('--reward_avg_time', type=int, default=3, help="the number of interactions with the environment to average the reward") 345 | parser.add_argument('--max_train_set', type=int, default=999999999, help="the maximum number of context-action-reward pairs stored in the training set") 346 | parser.add_argument('--ucb_nu', type=float, default=1.5/1.6, help='nu for control variance') 347 | parser.add_argument('--ucb_lamb', type=float, default=0.001, help='lambda for regularzation') 348 | parser.add_argument('--ucb_on', type=int, default=1, help='whether to use ucb durting training; set to 1') 349 | parser.add_argument('--ucb_val_on', type=int, default=1, help='whether to use ucb during validation') 350 | parser.add_argument('--actionsdir', type=str, default='./restricted_space', help="the directory to the restricted action space") 351 | parser.add_argument('--actions_name', type=str, default='restricted_actions_space.npy', help="the name of the restricted action space") 352 | 353 | # NN architecture settings 354 | parser.add_argument('--embsize', type=int, default=64, help='the embedding size') 355 | 356 | # validation settings 357 | parser.add_argument('--val_index_start', type=int, default=800, help='the index of the first instance used for validation') 358 | parser.add_argument('--val_index_end', type=int, default=900, help='the index of the last instance used for validation') 359 | parser.add_argument('--val_loss_freq', type=int, default=10, help='the frequency of calculating the validation loss') 360 | 361 | # setting for k2 362 | parser.add_argument('--Z_0_path', type=str, help="the path to the Z matrix for the first configuration") 363 | parser.add_argument('--model_0_path', type=str, help="the path to the model for the first configuration") 364 | parser.add_argument('--step_k2', type=int, default=5, help="the value of n2") 365 | 366 | args = parser.parse_args() 367 | args.n_cpus = min(args.n_cpus, _cpu_count()) 368 | print(f'{args.n_cpus} cpus available') 369 | 370 | if args.instances == "miplib": 371 | args = get_LIST(args) 372 | 373 | np.random.seed(42) 374 | torch.manual_seed(42) 375 | if not os.path.exists(args.savedir): 376 | os.makedirs(args.savedir) 377 | if not os.path.exists(args.logdir): 378 | os.makedirs(args.logdir) 379 | writer = SummaryWriter(args.logdir) 380 | writer_train_len = 0 381 | embsize = args.embsize 382 | 383 | path_to_actions = args.actionsdir + f"/{args.instances}/{args.actions_name}" 384 | action_space = np.load(path_to_actions) 385 | 386 | bandit_epoch = args.bandit_epoch 387 | bandit_ins_num = args.bandit_ins_num 388 | Bandit0 = _b_model.NeuralUCB("Neural_UCB", args.ucb_lamb, args.ucb_nu, action_space, args) 389 | Bandit0.model = _b_model.getModel("Neural_UCB") 390 | Bandit0.model.load_state_dict(torch.load(args.model_0_path)) 391 | Bandit0.model.train(False) 392 | Bandit0.U = torch.load(args.Z_0_path) 393 | Bandits = [(Bandit0, 0)] 394 | Bandit = _b_model.NeuralUCB("Neural_UCB", args.ucb_lamb, args.ucb_nu, action_space, args) 395 | train_set = [] 396 | for t in range(bandit_epoch): 397 | print("iteration: ", t) 398 | 399 | # log: val 400 | index_val = range(args.val_index_start, args.val_index_end) 401 | outputs = Parallel(n_jobs=args.n_cpus)( 402 | delayed(multiprocess_context_helper)(args_) for args_ in list( 403 | zip( 404 | index_val, 405 | [Bandits] * len(index_val), 406 | [Bandit] * len(index_val), 407 | [True] * len(index_val) 408 | ) 409 | ) 410 | ) 411 | action_contexts, actions, paths, process_nums = unpack_action_contexts(zip(*outputs), len(index_val)) 412 | outputs = Parallel(n_jobs=args.n_cpus)( 413 | delayed(multiprocess_solve_helper)(args_) for args_ in list( 414 | zip( 415 | range(len(action_contexts)), 416 | process_nums, 417 | actions, 418 | paths 419 | ) 420 | ) 421 | ) 422 | rewards, improvs = unpack_reward(zip(*outputs)) 423 | improvs_val = np.array(improvs) 424 | q3, q1 = np.percentile(improvs_val, [75, 25]) 425 | improvs_val_IQR = improvs_val[(improvs_val > q1) * (improvs_val < q3)] 426 | writer.add_scalars( 427 | 'val_mean_median_IQM', 428 | { 429 | "mean": improvs_val.mean(), 430 | "median": np.median(improvs_val), 431 | "IQM": improvs_val_IQR.mean() 432 | }, 433 | t 434 | ) 435 | writer.add_scalar('val_%', (improvs_val > -1e-9).sum() / improvs_val.size, t) 436 | writer.add_scalar('val_std', improvs_val.std(), t) 437 | 438 | val_set = [] 439 | for action_context, reward in zip(action_contexts, rewards): 440 | val_set.append([action_context, reward]) 441 | 442 | # train: parallel sample different problems 443 | outputs = Parallel(n_jobs=args.n_cpus)( 444 | delayed(multiprocess_context_helper)(args_) for args_ in list( 445 | zip( 446 | range(bandit_ins_num), 447 | [Bandits] * bandit_ins_num, 448 | [Bandit] * bandit_ins_num, 449 | [False] * bandit_ins_num 450 | ) 451 | ) 452 | ) 453 | action_contexts, actions, paths, process_nums = unpack_action_contexts(zip(*outputs), bandit_ins_num) 454 | outputs = Parallel(n_jobs=args.n_cpus)( 455 | delayed(multiprocess_solve_helper)(args_) for args_ in list( 456 | zip( 457 | range(len(action_contexts)), 458 | process_nums, 459 | actions, 460 | paths 461 | ) 462 | ) 463 | ) 464 | rewards, improvs = unpack_reward(zip(*outputs)) 465 | for index in range(len(action_contexts)): 466 | train_set.append([action_contexts[index], rewards[index]]) 467 | 468 | print("training the model...") 469 | Bandit.model = train(train_set, val_set, Bandit.model, args) 470 | 471 | # save the model 472 | os.makedirs(args.savedir + f"/{args.instances}_k2", exist_ok=True) 473 | model_path = args.savedir + f"/{args.instances}_k2/model-{t}" 474 | Z_path = args.savedir + f"/{args.instances}_k2/Z-{t}" 475 | saveModel(Bandit.model, model_path) 476 | torch.save(Bandit.U, Z_path) 477 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import numpy as np 3 | import os 4 | import pickle 5 | import joblib 6 | import platform 7 | import torch 8 | 9 | import tqdm 10 | from multiprocessing import Pool 11 | from multiprocessing.dummy import Pool as ThreadPool 12 | 13 | def multiprocess(func, tasks, cpus=None): 14 | if cpus == 1 or len(tasks) == 1: 15 | return [func(t) for t in tasks] 16 | with Pool(cpus or os.cpu_count()) as pool: 17 | return list(pool.imap(func, tasks)) 18 | 19 | def multithread(func, tasks, cpus=None, show_bar=True): 20 | bar = lambda x: tqdm(x, total=len(tasks)) if show_bar else x 21 | if cpus == 1 or len(tasks) == 1: 22 | return [func(t) for t in bar(tasks)] 23 | with ThreadPool(cpus or os.cpu_count()) as pool: 24 | return list(bar(pool.imap(func, tasks))) 25 | 26 | SEPA2IX = { 27 | 'aggregation': 0, 28 | 'clique': 1, 29 | 'disjunctive': 2, 30 | 'gomory': 3, 31 | 'impliedbounds': 4, 32 | 'mcf': 5, 33 | 'oddcycle': 6, 34 | 'strongcg': 7, 35 | 'zerohalf': 8, 36 | } 37 | 38 | IX2SEPA = {ix: sepa for sepa, ix in SEPA2IX.items()} 39 | 40 | SAVE_DIR = '../exp_optlearn/runs/exp_optdata' # TODO: need to double check 41 | 42 | def get_data_directory(args): # TODO: need to double check 43 | instances = os.path.basename(os.path.normpath(os.path.dirname(args.path_to_instance))) 44 | model = os.path.basename(os.path.normpath(args.path_to_instance)) 45 | data_directory = os.path.join( 46 | SAVE_DIR, 47 | instances, 48 | args.config, 49 | model, 50 | args.state, 51 | args.samplingstrategy, 52 | f'offset-{args.random_offset}', 53 | ) 54 | os.makedirs(data_directory, exist_ok=True) 55 | return data_directory 56 | 57 | def save_state(path, state): 58 | os.makedirs(path, exist_ok=True) 59 | 60 | for k, obj in state.items(): 61 | obj_path = os.path.join(path, k) 62 | 63 | if isinstance(obj, np.ndarray): 64 | save_func = save_numpy 65 | obj_path += '.npy' 66 | else: 67 | save_func = save_pickle 68 | obj_path += '.pkl' 69 | 70 | save_func(obj_path, obj) 71 | 72 | 73 | def save_json(path, d): 74 | with open(path, 'w') as file: 75 | json.dump(d, file, indent=4) 76 | 77 | def load_json(path): 78 | with open(path, 'r') as f: 79 | d = json.load(f) 80 | return d 81 | 82 | def save_numpy(path, arr): 83 | with open(path, 'wb') as file: 84 | np.save(file, arr) 85 | 86 | def load_numpy(path): 87 | with open(path, 'rb') as file: 88 | arr = np.load(path) 89 | return arr 90 | 91 | def save_pickle(path, obj): 92 | with open(path, 'wb') as file: 93 | pickle.dump(obj, file, protocol=5) 94 | 95 | def load_pickle(path): 96 | with open(path, 'rb') as file: 97 | obj = pickle.load(file) 98 | return obj 99 | 100 | def save_joblib(path, obj): 101 | joblib.dump(obj, path) 102 | 103 | def load_joblib(path): 104 | return joblib.load(path) 105 | 106 | 107 | class ExitStatus: 108 | CONVERGED_DUAL = 0 109 | CONVERGED_INTEGRAL = 1 110 | MAXITER = 2 111 | ERROR_TRIVIAL = 3 112 | ERROR_SEPARATION = 4 113 | ERROR_NOCUTS = 5 114 | ERROR_DUAL = 6 115 | ERROR_SIGN_SWITCH = 7 116 | ERROR_IGC_MESS = 8 117 | ERROR_DEF_JSON_MISSING = 9 118 | ERROR_EXP_JSON_MISSING = 10 119 | 120 | 121 | ############################################################# 122 | # exp_optlearn util 123 | ############################################################# 124 | def save_json(path, d): 125 | with open(path, 'w') as file: 126 | json.dump(d, file, indent=4) 127 | 128 | 129 | def load_json(path): 130 | with open(path, 'r') as f: 131 | d = json.load(f) 132 | return d 133 | 134 | 135 | def save_pickle(path, d): 136 | with open(path, 'wb') as file: 137 | pickle.dump(d, file, protocol=pickle.HIGHEST_PROTOCOL) 138 | 139 | 140 | def load_pickle(path): 141 | with open(path, 'rb') as file: 142 | d = pickle.load(file) 143 | return d 144 | 145 | def save_numpy(path, arr): 146 | with open(path, 'wb') as file: 147 | np.save(file, arr) 148 | 149 | def load_numpy(path): 150 | with open(path, 'rb') as file: 151 | arr = np.load(path) 152 | return arr 153 | 154 | def save_joblib(path, obj): 155 | joblib.dump(obj, path) 156 | 157 | def load_joblib(path): 158 | return joblib.load(path) 159 | 160 | def find_all_paths_sepa( 161 | split, 162 | instances, 163 | state, 164 | data_dir='runs' 165 | ): 166 | 167 | paths = [] 168 | 169 | basepath = f'./{data_dir}/{instances}_{split}' 170 | 171 | models = [] 172 | if not os.path.exists(basepath): return paths 173 | for f in os.listdir(basepath): 174 | if f.startswith('model-'): 175 | models.append(f) 176 | 177 | for model in models: 178 | model_path = f'{basepath}/{model}/{state}/' 179 | for root, dirs, _ in os.walk(model_path): 180 | for d in dirs: 181 | if d.startswith('nsepacut'): 182 | path = f'{root}/{d}' 183 | paths.append(path) 184 | 185 | return paths 186 | 187 | def find_all_paths( 188 | split, 189 | instances, 190 | config, 191 | state, 192 | samplingstrategy, 193 | ): 194 | 195 | paths = [] 196 | 197 | basepath = f'./runs/exp_optdata/{instances}_{split}' 198 | if is_local(): 199 | basepath += 'mock' 200 | basepath = f'{basepath}/{config}' 201 | 202 | models = [] 203 | if not os.path.exists(basepath): return paths 204 | for f in os.listdir(basepath): 205 | if f.startswith('model-'): 206 | models.append(f) 207 | 208 | for model in models: 209 | model_path = f'{basepath}/{model}/{state}/{samplingstrategy}' 210 | for root, dirs, _ in os.walk(model_path): 211 | for d in dirs: 212 | if d.startswith('nsepacut'): 213 | path = f'{root}/{d}' 214 | paths.append(path) 215 | 216 | return paths 217 | 218 | class AverageMeter(): 219 | """Computes and stores the average and current value""" 220 | def __init__(self): 221 | self.reset() 222 | 223 | def reset(self): 224 | self.val = 0 225 | self.avg = 0 226 | self.sum = 0 227 | self.count = 0 228 | 229 | def update(self, val, n=1): 230 | self.val = val 231 | self.sum += val * n 232 | self.count += n 233 | self.avg = self.sum / self.count 234 | 235 | def is_local(): 236 | return platform.system() == 'Darwin' 237 | 238 | """ 239 | Utility functions to load and save torch model checkpoints 240 | """ 241 | def load_checkpoint(net, optimizer=None, step='max', save_dir='checkpoints'): 242 | os.makedirs(save_dir, exist_ok=True) 243 | 244 | checkpoints = [x for x in os.listdir(save_dir) if not x.startswith('events')] 245 | if step == 'max': 246 | step = 0 247 | if checkpoints: 248 | step, last_checkpoint = max([(int(x.split('.')[0]), x) for x in checkpoints]) 249 | else: 250 | last_checkpoint = str(step) + '.pth' 251 | if step: 252 | save_path = os.path.join(save_dir, last_checkpoint) 253 | state = torch.load(save_path, map_location='cpu') 254 | net.load_state_dict(state['net']) 255 | if optimizer: 256 | optimizer.load_state_dict(state['optimizer']) 257 | print('Loaded checkpoint %s' % save_path) 258 | return step 259 | 260 | def save_checkpoint(net, optimizer, step, save_dir='checkpoints'): 261 | if not os.path.exists(save_dir): 262 | os.makedirs(save_dir) 263 | save_path = os.path.join(save_dir, str(step) + '.pth') 264 | 265 | torch.save(dict(net=net.state_dict(), optimizer=optimizer.state_dict()), save_path) 266 | print('Saved checkpoint %s' % save_path) 267 | 268 | 269 | if __name__ == '__main__': 270 | 271 | paths = find_all_paths( 272 | 'train', 273 | 'binpacking-66-66', 274 | 'all-def-def', 275 | 'learn1', 276 | 'mixed1' 277 | ) 278 | print(len(paths)) 279 | import pdb; pdb.set_trace() 280 | --------------------------------------------------------------------------------