├── .gitignore ├── INSTALL.md ├── LICENSE ├── README.md ├── build-requirements.txt ├── cpp ├── Makefile ├── lp.cpp ├── lp.hpp ├── lp_generators_ext.pyx └── tests.cpp ├── examples ├── generate.py └── search.py ├── lp_generators ├── __init__.py ├── features.py ├── instance.py ├── lhs_generators.py ├── lp_ext.py ├── neighbours_common.py ├── neighbours_encoded.py ├── neighbours_unsolved.py ├── performance.py ├── search.py ├── solution_generators.py ├── utils.py └── writers.py ├── scripts ├── .gitignore ├── Makefile ├── figures.ipynb ├── naive_performance_search.py ├── naive_random.py ├── naive_search.py ├── parameter-feature-relations.ipynb ├── parameterised_performance_search.py ├── parameterised_random.py ├── parameterised_search.py ├── performance_search_common.py ├── search_common.py ├── search_operators.py └── seeds.py ├── setup.py └── tests ├── __init__.py ├── test_constructor.py ├── test_encode_decode.py ├── test_features.py ├── test_lp_ext.py ├── test_neighbours_common.py ├── test_writers.py └── testing.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .coverage 34 | .coverage.* 35 | .cache 36 | nosetests.xml 37 | coverage.xml 38 | *,cover 39 | 40 | # C++ build products, cythonized files, data files 41 | cpp/bin/ 42 | cpp/obj/ 43 | cpp/*.mps.gz 44 | cpp/lp_generators_ext.cpp 45 | scripts/data/ 46 | scripts/result_figures 47 | examples/generated/ 48 | examples/search/ 49 | 50 | # Notebooks 51 | .ipynb_checkpoints/ 52 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | 2 | This package has been developed and tested in Python 3.5.2 on an Ubuntu 1604 LTS system. 3 | The following is the easiest installation sequence for Ubuntu/Debian in a python virtul environment (may need sudo). 4 | 5 | Install required system libraries: 6 | 7 | apt install pkg-config zlib1g-dev libbz2-dev coinor-clp coinor-libclp-dev coinor-libosi-dev 8 | 9 | Install Python3, set up virtualenv: 10 | 11 | apt install python3-dev python-pip 12 | pip install virtualenvwrapper 13 | source /usr/local/bin/virtualenvwrapper.sh 14 | mkvirtualenv --python=$(which python3.5) lp_generators 15 | workon lp_generators 16 | 17 | Installation requires numpy, cython and pkgconfig in the local python3 environment. 18 | It is easiest to install these first via pip: 19 | 20 | pip install -r build-requirements.txt 21 | 22 | Followed by installing this package: 23 | 24 | pip install . 25 | 26 | And extra requirements for scripts and analysis: 27 | 28 | pip install .[scripts,analysis] 29 | 30 | Running the scripts additionally requires CLP and SCIP executables available on the system path. 31 | CLP is provided on Ubuntu as the coinor-clp package. 32 | 33 | # Tests 34 | 35 | Tests of the python package and C++ extension require pytest and mock, installable 36 | from the module setup: 37 | 38 | pip install .[tests] 39 | 40 | Running the tests: 41 | 42 | pytest tests/ 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Simon Bowly, Monash University 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 | 2 | # About 3 | 4 | This package is a collection of algorithms for generating random MIP instances by constructing relaxations with known solutions. 5 | Scripts are included to generate random instance distributions and instance space search results. 6 | 7 | # Install 8 | 9 | This package is developed in Python 3.5.2. 10 | Building the C++ extension requires numpy, cython, pkgconfig python packages, and the COIN-LP callable library. 11 | 12 | Basic functionality is installed using: 13 | 14 | pip install . 15 | 16 | Extra python requirements for generating results and analysis: 17 | 18 | pip install .[scripts,analysis] 19 | 20 | Algorithm performance evaluation functions require clp and scip on the system path. 21 | 22 | See INSTALL.md for detailed installation instructions. 23 | 24 | # Examples 25 | 26 | The examples/ directory contains sample scripts for setting up generation and search algorithms in instance space. 27 | 28 | python generate.py # Generates instances using constructor approach 29 | python search.py # Make generated instances harder to solve 30 | 31 | # Running the Experiments 32 | 33 | Scripts to reproduce experimental results are contained in the scripts/ directory. 34 | To generate results using system random seeds, run `make` from this directory. 35 | Figures showing feature and performance distributions of the generated instance sets can be produced using the Jupyter notebook `scripts/figures.ipynb`. 36 | 37 | # Citing this work 38 | 39 | Paper published in Mathematical Programming Computation (MPC) where we describe 40 | the generator and investigate properties of the generated instances: 41 | 42 | ``` 43 | @Article{Bowly.Smith-Miles.ea_2020_Generation-techniques, 44 | author = {Bowly, Simon and Smith-Miles, Kate and Baatar, Davaatseren 45 | and Mittelmann, Hans}, 46 | title = {Generation techniques for linear programming instances 47 | with controllable properties}, 48 | journal = {Mathematical Programming Computation}, 49 | year = 2020, 50 | volume = 12, 51 | number = 3, 52 | pages = {389--415}, 53 | month = sep, 54 | } 55 | ``` 56 | 57 | Direct citation of the version of the code used in the MPC paper: 58 | 59 | ``` 60 | @software{Bowly_2018_MIP-instance, 61 | author = {Simon Bowly}, 62 | title = {simonbowly/lp-generators: v0.2-beta}, 63 | month = apr, 64 | year = 2018, 65 | publisher = {Zenodo}, 66 | version = {v0.2-beta}, 67 | doi = {10.5281/zenodo.1220448}, 68 | url = {https://doi.org/10.5281/zenodo.1220448} 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /build-requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | cython 3 | pkgconfig 4 | -------------------------------------------------------------------------------- /cpp/Makefile: -------------------------------------------------------------------------------- 1 | # This makefile and tests are not meant to do more then check the basics. 2 | # Logic tests, performance tests, etc are performed via python. 3 | 4 | CXX = g++ 5 | CXXFLAGS = -std=c++11 -Wunused-variable 6 | CXXLINKFLAGS = 7 | 8 | INCL = -I$(GTEST_HOME)/include -I$(COINOR_HOME)/include/coin 9 | LIBS = -L$(GTEST_HOME)/build -lgtest -lpthread 10 | 11 | LIBS += -L$(COINOR_HOME)/lib -lCbcSolver -lCbc -lCgl -lOsiClp -lClpSolver -lClp -lOsi -lCoinUtils -lz -lm 12 | 13 | 14 | all: test 15 | 16 | clean: 17 | @echo "-> cleaning objects and binary" 18 | @rm -f bin/tests obj/*.o 19 | 20 | bin/tests: obj/tests.o obj/lp.o 21 | @echo "-> linking test program" 22 | @mkdir -p bin 23 | @$(CXX) $(CXXFLAGS) $(INCL) $(CXXLINKFLAGS) obj/lp.o obj/tests.o -o bin/tests $(LIBS) 24 | 25 | obj/%.o: %.cpp 26 | @echo "-> compiling $@" 27 | @mkdir -p obj 28 | @$(CXX) $(CXXFLAGS) $(INCL) $(MACROS) $< -o $@ -c 29 | 30 | test: bin/tests 31 | @echo "-> running tests" 32 | @bin/tests 33 | 34 | obj/lp.o: lp.hpp 35 | -------------------------------------------------------------------------------- /cpp/lp.cpp: -------------------------------------------------------------------------------- 1 | 2 | #ifndef LP_CPP 3 | #define LP_CPP 4 | 5 | #include "lp.hpp" 6 | 7 | 8 | LP::LP() { 9 | numVariables = -1; 10 | numConstraints = -1; 11 | numLHSElements = -1; 12 | lhsMatrixDense = NULL; 13 | rhsVector = NULL; 14 | objVector = NULL; 15 | simplexModel = NULL; 16 | } 17 | 18 | 19 | LP::~LP() { 20 | delete [] lhsMatrixDense; 21 | delete [] rhsVector; 22 | delete [] objVector; 23 | delete simplexModel; 24 | } 25 | 26 | 27 | void LP::constructDenseCanonical(int nv, int nc, double* A, double* b, double* c) { 28 | // Construction method by copying dense arrays. 29 | 30 | numVariables = nv; 31 | numConstraints = nc; 32 | numLHSElements = 0; 33 | 34 | lhsMatrixDense = new double[nv * nc]; 35 | rhsVector = new double[nc]; 36 | objVector = new double[nv]; 37 | 38 | for (int col = 0; col < nv; col++) { 39 | objVector[col] = c[col]; 40 | } 41 | 42 | int index = 0; 43 | for (int row = 0; row < nc; row++) { 44 | rhsVector[row] = b[row]; 45 | for (int col = 0; col < nv; col++) { 46 | index = row * nv + col; 47 | if (A[index] != 0) { 48 | numLHSElements++; 49 | } 50 | lhsMatrixDense[index] = A[index]; 51 | } 52 | } 53 | } 54 | 55 | 56 | CoinPackedMatrix* LP::getCoinPackedMatrix() { 57 | // Allocate and return pointer to a COIN matrix. 58 | 59 | int* rowIndices = new int[numLHSElements]; 60 | int* colIndices = new int[numLHSElements]; 61 | double* elements = new double[numLHSElements]; 62 | 63 | int index; 64 | int elem = 0; 65 | for (int row = 0; row < numConstraints; row++) { 66 | for (int col = 0; col < numVariables; col++) { 67 | index = row * numVariables + col; 68 | if (lhsMatrixDense[index] != 0) { 69 | rowIndices[elem] = row; 70 | colIndices[elem] = col; 71 | elements[elem] = lhsMatrixDense[index]; 72 | elem++; 73 | } 74 | } 75 | } 76 | 77 | CoinPackedMatrix* matrix = new CoinPackedMatrix( 78 | true, rowIndices, colIndices, elements, numLHSElements); 79 | 80 | delete[] rowIndices; 81 | delete[] colIndices; 82 | delete[] elements; 83 | 84 | return matrix; 85 | } 86 | 87 | 88 | ClpModel* LP::getClpModel() { 89 | // Allocate and return pointer to a COIN LP model 90 | // 91 | // Problem is stored in canonical form 92 | // max cTx 93 | // s.t. Ax <= b 94 | // x >= 0 95 | // 96 | // Clp model is created as a minimisation model 97 | // min -cTx 98 | // s.t. Ax <= b 99 | // x >= 0 100 | 101 | CoinPackedMatrix* matrix = getCoinPackedMatrix(); 102 | 103 | double* lowerColumn = new double[numVariables]; 104 | double* upperColumn = new double[numVariables]; 105 | double* objective = new double[numVariables]; 106 | double* lowerRow = new double[numConstraints]; 107 | double* upperRow = new double[numConstraints]; 108 | 109 | for (int col = 0; col < numVariables; col++) { 110 | lowerColumn[col] = 0; 111 | upperColumn[col] = COIN_DBL_MAX; 112 | objective[col] = objVector[col] * -1; 113 | } 114 | 115 | for (int row = 0; row < numConstraints; row++) { 116 | lowerRow[row] = -COIN_DBL_MAX; 117 | upperRow[row] = rhsVector[row]; 118 | } 119 | 120 | ClpModel* model = new ClpModel(); 121 | model->loadProblem( 122 | *matrix, lowerColumn, upperColumn, objective, lowerRow, upperRow); 123 | 124 | delete[] lowerColumn; 125 | delete[] upperColumn; 126 | delete[] objective; 127 | delete[] lowerRow; 128 | delete[] upperRow; 129 | delete matrix; 130 | 131 | return model; 132 | } 133 | 134 | 135 | OsiClpSolverInterface* LP::getOsiClpModel() { 136 | // Allocate and return pointer to a COIN LP model 137 | // 138 | // Problem is stored in canonical form 139 | // max cTx 140 | // s.t. Ax <= b 141 | // x >= 0 142 | // 143 | // Clp model is created as a minimisation model 144 | // min -cTx 145 | // s.t. Ax <= b 146 | // x >= 0 147 | 148 | CoinPackedMatrix* matrix = getCoinPackedMatrix(); 149 | 150 | double* lowerColumn = new double[numVariables]; 151 | double* upperColumn = new double[numVariables]; 152 | double* objective = new double[numVariables]; 153 | double* lowerRow = new double[numConstraints]; 154 | double* upperRow = new double[numConstraints]; 155 | 156 | for (int col = 0; col < numVariables; col++) { 157 | lowerColumn[col] = 0; 158 | upperColumn[col] = COIN_DBL_MAX; 159 | objective[col] = objVector[col] * -1; 160 | } 161 | 162 | for (int row = 0; row < numConstraints; row++) { 163 | lowerRow[row] = -COIN_DBL_MAX; 164 | upperRow[row] = rhsVector[row]; 165 | } 166 | 167 | OsiClpSolverInterface* model = new OsiClpSolverInterface(); 168 | model->loadProblem( 169 | *matrix, lowerColumn, upperColumn, objective, lowerRow, upperRow); 170 | 171 | delete[] lowerColumn; 172 | delete[] upperColumn; 173 | delete[] objective; 174 | delete[] lowerRow; 175 | delete[] upperRow; 176 | delete matrix; 177 | 178 | return model; 179 | } 180 | 181 | 182 | void LP::writeMps(string fileName) { 183 | // Generate a COIN model for writing to MPS file. 184 | OsiClpSolverInterface* model; 185 | model = getOsiClpModel(); 186 | model->writeMps(fileName.c_str(), "", 0.0); 187 | delete model; 188 | } 189 | 190 | 191 | void LP::writeMpsIP(string fileName) { 192 | // Generate a COIN model for writing to MPS file. 193 | OsiClpSolverInterface* model; 194 | model = getOsiClpModel(); 195 | for (int i = 0; i < numVariables; i++) { 196 | model->setInteger(i); 197 | } 198 | model->writeMps(fileName.c_str(), "", 0.0); 199 | delete model; 200 | } 201 | 202 | 203 | void LP::getLhsMatrixDense(double* buffer) { 204 | // Copy stored dense constraints to an array. 205 | // Input array must have getNumVariables() * getNumConstraints() elements. 206 | for (int i=0; isetLogLevel(0); 237 | simplexModel->dual(); 238 | delete model; 239 | } 240 | 241 | 242 | int LP::getSolutionStatus() { 243 | return simplexModel->status(); 244 | } 245 | 246 | 247 | void LP::getSolutionPrimals(double* buffer) { 248 | // Copy stored primal solution for solved model to an array. 249 | // Input array must have getNumVariables() elements. 250 | const double* primalValues = simplexModel->getColSolution(); 251 | for (int i=0; igetRowActivity(); 261 | const double* rhsValues = simplexModel->getRowUpper(); 262 | for (int i=0; igetRowPrice(); 270 | for (int i=0; igetReducedCost(); 278 | for (int i=0; igetColumnStatus(i) == ClpSimplex::Status::basic); 288 | } 289 | 290 | for (int i = 0; i < numConstraints; i++) { 291 | buffer[i + numVariables] = double(simplexModel->getRowStatus(i) == ClpSimplex::Status::basic); 292 | } 293 | 294 | } 295 | 296 | #endif 297 | -------------------------------------------------------------------------------- /cpp/lp.hpp: -------------------------------------------------------------------------------- 1 | // Wrapper class around COIN CLP callable library to solve instances and 2 | // write MPS files. 3 | 4 | #ifndef LP_HPP 5 | #define LP_HPP 6 | 7 | #include 8 | using std::string; 9 | #include 10 | 11 | #include "ClpModel.hpp" 12 | #include "ClpSimplex.hpp" 13 | #include "OsiClpSolverInterface.hpp" 14 | 15 | 16 | class LP { 17 | 18 | public: 19 | LP(); 20 | ~LP(); 21 | 22 | // Construction methods (overwrite existing data) 23 | void constructDenseCanonical(int nv, int nc, double* A, double* b, double* c); 24 | 25 | // Conversion to COIN models (return pointers requiring cleanup) 26 | ClpModel* getClpModel(); 27 | OsiClpSolverInterface* getOsiClpModel(); 28 | CoinPackedMatrix* getCoinPackedMatrix(); 29 | 30 | // Model writers 31 | void writeMps(string fileName); 32 | void writeMpsIP(string fileName); 33 | 34 | // Property accessors 35 | int getNumVariables() { return numVariables; } 36 | int getNumConstraints() { return numConstraints; } 37 | int getNumLHSElements() { return numLHSElements; } 38 | 39 | // Read data into arrays 40 | void getLhsMatrixDense(double* buffer); 41 | void getRhsVector(double* buffer); 42 | void getObjVector(double* buffer); 43 | 44 | // Solution 45 | void solve(); 46 | int getSolutionStatus(); 47 | void getSolutionPrimals(double* buffer); 48 | void getSolutionSlacks(double* buffer); 49 | void getSolutionDuals(double* buffer); 50 | void getSolutionReducedCosts(double* buffer); 51 | void getSolutionBasis(double* buffer); 52 | 53 | private: 54 | int numVariables; 55 | int numConstraints; 56 | int numLHSElements; 57 | 58 | // Requiring cleanup 59 | double* lhsMatrixDense; 60 | double* rhsVector; 61 | double* objVector; 62 | ClpSimplex* simplexModel; 63 | 64 | }; 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /cpp/lp_generators_ext.pyx: -------------------------------------------------------------------------------- 1 | # Cython interface to C++ class connecting the COIN-CLP callable library. 2 | 3 | from libcpp.string cimport string 4 | from cython.operator cimport dereference as deref 5 | 6 | import numpy as np 7 | cimport numpy as np 8 | 9 | 10 | cdef extern from "lp.hpp": 11 | cdef cppclass LP: 12 | LP() 13 | void constructDenseCanonical(int, int, double*, double*, double*) 14 | void writeMps(string) 15 | void writeMpsIP(string) 16 | int getNumVariables() 17 | int getNumConstraints() 18 | int getNumLHSElements() 19 | void getLhsMatrixDense(double*) 20 | void getRhsVector(double*) 21 | void getObjVector(double*) 22 | void solve() 23 | int getSolutionStatus(); 24 | void getSolutionPrimals(double*) 25 | void getSolutionSlacks(double*) 26 | void getSolutionDuals(double*) 27 | void getSolutionReducedCosts(double*) 28 | void getSolutionBasis(double*) 29 | 30 | 31 | cdef class LPCy(object): 32 | cdef LP *wrapped 33 | 34 | def __cinit__(self): 35 | self.wrapped = new LP() 36 | 37 | def __dealloc__(self): 38 | del self.wrapped 39 | 40 | def construct_dense_canonical(self, variables, constraints, A, b, c): 41 | deref(self.wrapped).constructDenseCanonical( 42 | variables, constraints, 43 | contiguous_2d_handle(A), 44 | contiguous_1d_handle(b), 45 | contiguous_1d_handle(c)) 46 | 47 | def write_mps(self, file_name): 48 | cdef string strfilename = file_name.encode('UTF-8') 49 | deref(self.wrapped).writeMps(strfilename) 50 | 51 | def write_mps_ip(self, file_name): 52 | cdef string strfilename = file_name.encode('UTF-8') 53 | deref(self.wrapped).writeMpsIP(strfilename) 54 | 55 | def get_dense_lhs(self): 56 | variables = deref(self.wrapped).getNumVariables() 57 | constraints = deref(self.wrapped).getNumConstraints() 58 | result = np.zeros(shape=(constraints, variables)) 59 | deref(self.wrapped).getLhsMatrixDense(contiguous_2d_handle(result)) 60 | return result 61 | 62 | def get_rhs(self): 63 | constraints = deref(self.wrapped).getNumConstraints() 64 | result = np.zeros(shape=(constraints)) 65 | deref(self.wrapped).getRhsVector(contiguous_1d_handle(result)) 66 | return result 67 | 68 | def get_obj(self): 69 | variables = deref(self.wrapped).getNumVariables() 70 | result = np.zeros(shape=(variables)) 71 | deref(self.wrapped).getObjVector(contiguous_1d_handle(result)) 72 | return result 73 | 74 | def solve(self): 75 | deref(self.wrapped).solve() 76 | 77 | def get_solution_status(self): 78 | return deref(self.wrapped).getSolutionStatus() 79 | 80 | def get_solution_primals(self): 81 | variables = deref(self.wrapped).getNumVariables() 82 | result = np.zeros(shape=(variables)) 83 | deref(self.wrapped).getSolutionPrimals(contiguous_1d_handle(result)) 84 | return result 85 | 86 | def get_solution_slacks(self): 87 | constraints = deref(self.wrapped).getNumConstraints() 88 | result = np.zeros(shape=(constraints)) 89 | deref(self.wrapped).getSolutionSlacks(contiguous_1d_handle(result)) 90 | return result 91 | 92 | def get_solution_duals(self): 93 | constraints = deref(self.wrapped).getNumConstraints() 94 | result = np.zeros(shape=(constraints)) 95 | deref(self.wrapped).getSolutionDuals(contiguous_1d_handle(result)) 96 | return result 97 | 98 | def get_solution_reduced_costs(self): 99 | variables = deref(self.wrapped).getNumVariables() 100 | result = np.zeros(shape=(variables)) 101 | deref(self.wrapped).getSolutionReducedCosts(contiguous_1d_handle(result)) 102 | return result 103 | 104 | def get_solution_basis(self): 105 | elements = deref(self.wrapped).getNumVariables() + deref(self.wrapped).getNumConstraints() 106 | result = np.zeros(shape=(elements)) 107 | deref(self.wrapped).getSolutionBasis(contiguous_1d_handle(result)) 108 | return result 109 | 110 | 111 | cdef double* contiguous_1d_handle(np.ndarray[np.double_t, ndim=1, mode='c'] py_array): 112 | ''' Return c handle for contiguous 1d numpy array. ''' 113 | cdef np.ndarray[np.double_t, ndim=1, mode='c'] np_buff = np.ascontiguousarray( 114 | py_array, dtype=np.double) 115 | cdef double* im_buff = py_array.data 116 | return im_buff 117 | 118 | 119 | cdef double* contiguous_2d_handle(np.ndarray[np.double_t, ndim=2, mode='c'] py_array): 120 | ''' Return c handle for contiguous 2d numpy array. ''' 121 | cdef np.ndarray[np.double_t, ndim=2, mode='c'] np_buff = np.ascontiguousarray( 122 | py_array, dtype=np.double) 123 | cdef double* im_buff = py_array.data 124 | return im_buff 125 | -------------------------------------------------------------------------------- /cpp/tests.cpp: -------------------------------------------------------------------------------- 1 | // Very basic tests to check for successful compile. 2 | 3 | #ifndef TESTS_CPP 4 | #define TESTS_CPP 5 | 6 | #include "lp.hpp" 7 | #include 8 | 9 | 10 | TEST(LPTest, ConstructDense) { 11 | 12 | double A[] = { 13 | 1,0,2,0,1, 14 | 0,1,0,1,0, 15 | 1,-1,0,1,0, 16 | 0,0,-1,1,0, 17 | }; 18 | double b[] = {1, 2, 3, 4}; 19 | double c[] = {1, 2, 3, 4, 5}; 20 | 21 | // Construct model 22 | LP lp; 23 | lp.constructDenseCanonical(5, 4, A, b, c); 24 | 25 | // Check accessors 26 | ASSERT_EQ(5, lp.getNumVariables()); 27 | ASSERT_EQ(4, lp.getNumConstraints()); 28 | ASSERT_EQ(10, lp.getNumLHSElements()); 29 | 30 | // Check copiers 31 | double* buffer = new double[20]; 32 | lp.getLhsMatrixDense(buffer); 33 | for (int i = 0; i < 20; i++) { 34 | ASSERT_EQ(A[i], buffer[i]); 35 | } 36 | delete[] buffer; 37 | 38 | buffer = new double[4]; 39 | lp.getRhsVector(buffer); 40 | for (int i = 0; i < 4; i++) { 41 | ASSERT_EQ(b[i], buffer[i]); 42 | } 43 | delete[] buffer; 44 | 45 | buffer = new double[5]; 46 | lp.getObjVector(buffer); 47 | for (int i = 0; i < 5; i++) { 48 | ASSERT_EQ(c[i], buffer[i]); 49 | } 50 | delete[] buffer; 51 | 52 | } 53 | 54 | 55 | TEST(LPTest, Model) { 56 | 57 | double A[] = { 58 | 1,0,2,0,1, 59 | 0,1,0,1,0, 60 | 1,-1,0,1,0, 61 | 0,0,-1,1,0, 62 | }; 63 | double b[] = {1, 2, 3, 4}; 64 | double c[] = {1, 2, 3, 4, 5}; 65 | 66 | // Construct model 67 | LP lp; 68 | lp.constructDenseCanonical(5, 4, A, b, c); 69 | 70 | // COIN matrix 71 | CoinPackedMatrix* matrix = lp.getCoinPackedMatrix(); 72 | ASSERT_EQ(5, matrix->getNumCols()); 73 | ASSERT_EQ(4, matrix->getNumRows()); 74 | ASSERT_EQ(10, matrix->getNumElements()); 75 | delete matrix; 76 | 77 | // COIN clp model 78 | ClpModel* model = lp.getClpModel(); 79 | ASSERT_EQ(5, model->getNumCols()); 80 | ASSERT_EQ(4, model->getNumRows()); 81 | ASSERT_EQ(10, model->getNumElements()); 82 | delete model; 83 | 84 | } 85 | 86 | 87 | TEST(LPTest, Write) { 88 | 89 | double A[] = {1, 3, 3, 1}; 90 | double b[] = {4, 4}; 91 | double c[] = {1, 1}; 92 | 93 | // Construct model 94 | LP lp; 95 | lp.constructDenseCanonical(2, 2, A, b, c); 96 | 97 | // Write to file 98 | lp.writeMps("cpp_test_lp.mps.gz"); 99 | lp.writeMpsIP("cpp_test_ip.mps.gz"); 100 | 101 | } 102 | 103 | 104 | TEST(LPTest, Solve) { 105 | 106 | double A[] = {1, 3, 3, 1}; 107 | double b[] = {4, 4}; 108 | double c[] = {1, 1}; 109 | 110 | // Construct model 111 | LP lp; 112 | lp.constructDenseCanonical(2, 2, A, b, c); 113 | 114 | // Solve and copy results 115 | lp.solve(); 116 | 117 | double* primals = new double[2]; 118 | double* slacks = new double[2]; 119 | double* costs = new double[2]; 120 | double* duals = new double[2]; 121 | double* basis = new double[4]; 122 | 123 | lp.getSolutionPrimals(primals); 124 | lp.getSolutionSlacks(slacks); 125 | lp.getSolutionReducedCosts(costs); 126 | lp.getSolutionDuals(duals); 127 | lp.getSolutionBasis(basis); 128 | 129 | ASSERT_EQ(1, primals[0]); ASSERT_EQ(1, primals[1]); 130 | ASSERT_EQ(0, slacks[0]); ASSERT_EQ(0, slacks[1]); 131 | ASSERT_EQ(0, costs[0]); ASSERT_EQ(0, costs[1]); 132 | ASSERT_EQ(.25, duals[0]); ASSERT_EQ(.25, duals[1]); 133 | 134 | ASSERT_EQ(1, basis[0]); 135 | ASSERT_EQ(1, basis[1]); 136 | ASSERT_EQ(0, basis[2]); 137 | ASSERT_EQ(0, basis[3]); 138 | 139 | delete[] primals; 140 | delete[] slacks; 141 | delete[] costs; 142 | delete[] duals; 143 | delete[] basis; 144 | 145 | } 146 | 147 | 148 | int main(int argc, char **argv) { 149 | testing::InitGoogleTest(&argc, argv); 150 | return RUN_ALL_TESTS(); 151 | } 152 | 153 | #endif 154 | -------------------------------------------------------------------------------- /examples/generate.py: -------------------------------------------------------------------------------- 1 | ''' Example script generating instances with varying properties. ''' 2 | 3 | import numpy as np 4 | from tqdm import tqdm 5 | import pandas as pd 6 | 7 | from lp_generators.lhs_generators import generate_lhs 8 | from lp_generators.solution_generators import generate_alpha, generate_beta 9 | from lp_generators.instance import EncodedInstance 10 | from lp_generators.features import coeff_features, solution_features 11 | from lp_generators.performance import clp_simplex_performance 12 | from lp_generators.utils import calculate_data, write_instance 13 | from lp_generators.writers import write_tar_encoded, write_mps 14 | 15 | 16 | @write_instance(write_mps, 'generated/inst_{seed}.mps.gz') 17 | @write_instance(write_tar_encoded, 'generated/inst_{seed}.tar') 18 | @calculate_data(coeff_features, solution_features, clp_simplex_performance) 19 | def generate(seed): 20 | ''' Generating function taking a seed value and producing an instance 21 | using the constructor method. The decorators used for this function 22 | calculate and attach feature and performance data to each generated 23 | instance, write each instance to a .tar file so they can be loaded 24 | later to start a search, and write each instance to .mps format. ''' 25 | 26 | # Seeded random number generator used in all processes. 27 | random_state = np.random.RandomState(seed) 28 | 29 | # Generation parameters to be passed to generating functions. 30 | size_params = dict( 31 | variables=random_state.randint(50, 100), 32 | constraints=random_state.randint(50, 100)) 33 | beta_params = dict( 34 | basis_split=random_state.uniform(low=0.0, high=1.0)) 35 | alpha_params = dict( 36 | frac_violations=random_state.uniform(low=0.0, high=1.0), 37 | beta_param=random_state.lognormal(mean=-0.2, sigma=1.8), 38 | mean_primal=0, 39 | std_primal=1, 40 | mean_dual=0, 41 | std_dual=1) 42 | lhs_params = dict( 43 | density=random_state.uniform(low=0.3, high=0.7), 44 | pv=random_state.uniform(low=0.0, high=1.0), 45 | pc=random_state.uniform(low=0.0, high=1.0), 46 | coeff_loc=random_state.uniform(low=-2.0, high=2.0), 47 | coeff_scale=random_state.uniform(low=0.1, high=1.0)) 48 | 49 | # Run the generating functions to produce encoding components and 50 | # return the constructed instance. 51 | instance = EncodedInstance( 52 | lhs=generate_lhs(random_state=random_state, **size_params, **lhs_params).todense(), 53 | alpha=generate_alpha(random_state=random_state, **size_params, **alpha_params), 54 | beta=generate_beta(random_state=random_state, **size_params, **beta_params)) 55 | instance.data = dict(seed=seed) 56 | return instance 57 | 58 | # Create a generator of instance objects from random seeds. 59 | seeds = [ 60 | 3072533601, 3601421711, 3085167720, 3845318791, 4240653839, 61 | 956837294, 1416930261, 2883862718, 3309288948, 641946696] 62 | instances = (generate(seed) for seed in seeds) 63 | 64 | # Run the generator, keeping only the feature/performance data at each step. 65 | # tqdm is used for progress monitoring. 66 | data = pd.DataFrame([instance.data for instance in tqdm(instances)]) 67 | 68 | # Print some feature and performance results. 69 | print(data[['seed', 'variables', 'constraints', 'nonzeros', 'clp_primal_iterations']]) 70 | -------------------------------------------------------------------------------- /examples/search.py: -------------------------------------------------------------------------------- 1 | ''' Example script using search to make an instance harder to solve by 2 | primal simplex. ''' 3 | 4 | import functools 5 | 6 | import numpy as np 7 | from tqdm import tqdm 8 | import pandas as pd 9 | 10 | import lp_generators.neighbours_encoded as neighbours_encoded 11 | from lp_generators.performance import clp_simplex_performance 12 | from lp_generators.writers import read_tar_encoded, write_mps 13 | from lp_generators.utils import calculate_data 14 | from lp_generators.search import local_search, write_steps 15 | 16 | 17 | # Decorator/wrapper to calculate performance data on any instance 18 | # as it is generated or read. 19 | calc_wrapper = calculate_data(clp_simplex_performance) 20 | 21 | # List of neighbourhood operators, function to make a random choice 22 | # of operator to apply at each step. 23 | NEIGHBOURS = [ 24 | functools.partial(neighbours_encoded.exchange_basis, count=5), 25 | functools.partial(neighbours_encoded.scale_optvalue, mean=0, sigma=1, count=5), 26 | functools.partial(neighbours_encoded.remove_lhs_entry, count=10), 27 | functools.partial(neighbours_encoded.add_lhs_entry, mean=0, sigma=1, count=10), 28 | functools.partial(neighbours_encoded.scale_lhs_entry, mean=0, sigma=1, count=10), 29 | ] 30 | 31 | @calc_wrapper 32 | def neighbour(instance, random_state): 33 | func = random_state.choice(NEIGHBOURS) 34 | return func(instance, random_state) 35 | 36 | # Loads an instance to start the search, calculating performance data. 37 | load_start = calc_wrapper(read_tar_encoded) 38 | 39 | 40 | @write_steps(write_mps, 'search/step_{step:04d}.mps.gz', new_only=True) 41 | def search(): 42 | ''' Search process as a generator/iterator. Returns step_info, instance 43 | at each step. This search maximises the number of iterations required for 44 | primal simplex to solve the instance. Writes any improved instance found 45 | to mps format.''' 46 | return local_search( 47 | objective=lambda instance: instance.data['clp_primal_iterations'], 48 | sense='max', 49 | neighbour=neighbour, 50 | start_instance=load_start('generated/inst_3072533601.tar'), 51 | steps=100, 52 | random_state=np.random.RandomState(1640241240)) 53 | 54 | # Run search, display improvement steps 55 | data = pd.DataFrame([ 56 | dict(**step_info, **instance.data) for step_info, instance 57 | in tqdm(search()) if step_info['search_update'] == 'improved']) 58 | print(data[['search_step', 'clp_primal_iterations', 'clp_dual_iterations']]) 59 | -------------------------------------------------------------------------------- /lp_generators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbowly/lp-generators/937c44074c234333b6a5408c3e18f498c2205948/lp_generators/__init__.py -------------------------------------------------------------------------------- /lp_generators/features.py: -------------------------------------------------------------------------------- 1 | ''' Feature calculation functions for a canonical form LP instance. ''' 2 | 3 | import numpy as np 4 | 5 | from .lp_ext import LPCy 6 | 7 | 8 | def coeff_features(instance): 9 | ''' Features based on variable/constraint degree and coefficient 10 | value distributions. ''' 11 | lhs = instance.lhs() 12 | nonzeros = lhs != 0 13 | rhs = instance.rhs() 14 | objective = instance.objective() 15 | result = dict( 16 | variables=int(instance.variables), 17 | constraints=int(instance.constraints), 18 | nonzeros=int(np.sum(nonzeros)), 19 | lhs_std=float(lhs[np.where(nonzeros)].std()), 20 | lhs_mean=float(lhs[np.where(nonzeros)].mean()), 21 | lhs_abs_mean=float(np.abs(lhs[np.where(nonzeros)]).mean()), 22 | rhs_std=float(rhs.std()), 23 | rhs_mean=float(rhs.mean()), 24 | obj_std=float(objective.std()), 25 | obj_mean=float(objective.mean())) 26 | result['coefficient_density'] = float( 27 | result['nonzeros'] / (instance.variables * instance.constraints)) 28 | var_degree, cons_degree = degree_seq(lhs) 29 | result.update( 30 | cons_degree_min=int(cons_degree.min()), 31 | cons_degree_max=int(cons_degree.max()), 32 | var_degree_min=int(var_degree.min()), 33 | var_degree_max=int(var_degree.max())) 34 | result.update( 35 | rhs_mean_normed=result['rhs_mean'] / result['lhs_abs_mean'], 36 | obj_mean_normed=result['obj_mean'] / result['lhs_abs_mean']) 37 | return result 38 | 39 | 40 | def solution_features(instance): 41 | ''' Solve the instance (using extension module) and retrieve solution 42 | data to calculate features of the LP relaxation solution. ''' 43 | model = LPCy() 44 | model.construct_dense_canonical( 45 | instance.variables, instance.constraints, 46 | np.asarray(instance.lhs()), 47 | np.asarray(instance.rhs()), 48 | np.asarray(instance.objective())) 49 | model.solve() 50 | if (model.get_solution_status() != 0): 51 | return dict(solvable=False) 52 | # features specific to instances with a relaxation solution 53 | primals = model.get_solution_primals() 54 | fractional_components = np.abs(primals - np.round(primals)) 55 | slacks = model.get_solution_slacks() 56 | return dict( 57 | solvable=True, 58 | binding_constraints=int(np.sum(np.abs(slacks < 10 ** -10))), 59 | fractional_primal=int(np.sum(fractional_components > 10 ** -10)), 60 | total_fractionality=float(np.sum(fractional_components))) 61 | 62 | 63 | def degree_seq(lhs): 64 | ''' Return variable and constraint degree coefficients as numpy arrays. ''' 65 | lhs = np.array(lhs) 66 | nonzeros = lhs != 0 67 | return nonzeros.sum(axis=0), nonzeros.sum(axis=1) 68 | -------------------------------------------------------------------------------- /lp_generators/instance.py: -------------------------------------------------------------------------------- 1 | ''' Storage methods for instances. 2 | 3 | Incomplete classes providing common methods: 4 | Constructor: build rhs and objective from solution 5 | DenseLHS: store constraint left hand side as dense numpy matrix 6 | SolutionEncoder: build alpha/beta from solution 7 | 8 | Complete classes implementing the entire interface: 9 | EncodedInstance: store as lhs, alpha, beta 10 | SolvedInstance: store as lhs, solution 11 | UnsolvedInstance: store as A, b, c 12 | 13 | Note that UnsolvedInstance may not be decodable (if it does not have a 14 | solution) so attempting to solve will throw a value error. 15 | 16 | All complete classes implement the interface given by the abstract base 17 | class LPInstance. 18 | ''' 19 | 20 | import collections 21 | from abc import ABC, abstractproperty 22 | 23 | import numpy as np 24 | 25 | from .lp_ext import LPCy 26 | 27 | 28 | Solution = collections.namedtuple('Solution', ['x', 'y', 'r', 's', 'basis']) 29 | 30 | 31 | class LPInstance(ABC): 32 | ''' All complete LP instance classes use this as a base. ''' 33 | 34 | @abstractproperty 35 | def variables(self): 36 | ''' Number of variables (n). ''' 37 | pass 38 | 39 | @abstractproperty 40 | def constraints(self): 41 | ''' Number of constraints (m). ''' 42 | pass 43 | 44 | def lhs(self): 45 | ''' Returns an m x n matrix-like representation of constraints. ''' 46 | pass 47 | 48 | def rhs(self): 49 | ''' Length m vector of constraint right hand sides. ''' 50 | pass 51 | 52 | def objective(self): 53 | ''' Length n vector of objective coefficients. ''' 54 | pass 55 | 56 | def alpha(self): 57 | ''' Length m+n vector of encoded solution values. ''' 58 | pass 59 | 60 | def beta(self): 61 | ''' Length m+n vector encoding the optimal basis. ''' 62 | pass 63 | 64 | def solution(self): 65 | ''' Solution object. ''' 66 | pass 67 | 68 | 69 | class Constructor(object): 70 | ''' Use the result of lhs() and solution() methods to construct an 71 | instance with the required optimal solution. ''' 72 | 73 | def rhs(self): 74 | solution = self.solution() 75 | A = self.lhs() 76 | x = np.matrix(solution.x).transpose() 77 | s = np.matrix(solution.s).transpose() 78 | _rhs = A * x + s 79 | return np.asarray(_rhs.transpose(), dtype=np.float)[0] 80 | 81 | def objective(self): 82 | solution = self.solution() 83 | A = self.lhs() 84 | y = np.matrix(solution.y).transpose() 85 | r = np.matrix(solution.r).transpose() 86 | _obj = A.transpose() * y - r 87 | return np.asarray(_obj.transpose(), dtype=np.float)[0] 88 | 89 | 90 | class SolutionEncoder(object): 91 | ''' Use the result of solution() to build alpha and beta vectors. ''' 92 | 93 | def alpha(self): 94 | solution = self.solution() 95 | primal = np.concatenate([solution.x, solution.s]) 96 | dual = np.concatenate([solution.r, solution.y]) 97 | # should verify complementarity somewhere...? 98 | return primal + dual 99 | 100 | def beta(self): 101 | return self.solution().basis 102 | 103 | 104 | class DenseLHS(object): 105 | ''' Store the left hand side of the constraints. The result of lhs() must 106 | be able to be transposed and matrix multiplied. ''' 107 | 108 | def __init__(self, lhs, **kwargs): 109 | super().__init__(**kwargs) 110 | self._lhs_matrix = np.matrix(lhs, dtype=np.float) 111 | 112 | @property 113 | def variables(self): 114 | return self._lhs_matrix.shape[1] 115 | 116 | @property 117 | def constraints(self): 118 | return self._lhs_matrix.shape[0] 119 | 120 | def lhs(self): 121 | return self._lhs_matrix 122 | 123 | 124 | class EncodedInstance(Constructor, DenseLHS, LPInstance): 125 | ''' Full instance class storing data as (A, alpha, beta). ''' 126 | 127 | def __init__(self, alpha, beta, **kwargs): 128 | super().__init__(**kwargs) 129 | n, m = self.variables, self.constraints 130 | assert alpha.shape == (n + m, ) 131 | assert np.all(alpha >= 0) 132 | assert beta.shape == (n + m, ) 133 | assert np.sum(beta == 1) == m 134 | assert np.sum(beta == 0) == n 135 | self._alpha = np.array(alpha, dtype=np.float) 136 | self._beta = np.array(beta, dtype=np.float) 137 | 138 | def alpha(self): 139 | return self._alpha 140 | 141 | def beta(self): 142 | return self._beta 143 | 144 | def solution(self): 145 | # Extract primal variables and reduced costs (complete solution) 146 | n, m = self.variables, self.constraints 147 | x = self._beta[:n] * self._alpha[:n] 148 | r = (1 - self._beta[:n]) * self._alpha[:n] 149 | y = (1 - self._beta[n:]) * self._alpha[n:] 150 | s = self._beta[n:] * self._alpha[n:] 151 | return Solution(x=x, r=r, y=y, s=s, basis=self._beta) 152 | 153 | 154 | class SolvedInstance(Constructor, SolutionEncoder, DenseLHS, LPInstance): 155 | ''' Full instance class storing data as (A, x, r, y, s). ''' 156 | 157 | def __init__(self, solution, **kwargs): 158 | super().__init__(**kwargs) 159 | n, m = self.variables, self.constraints 160 | assert solution.x.shape == (n, ) 161 | assert solution.r.shape == (n, ) 162 | assert solution.y.shape == (m, ) 163 | assert solution.s.shape == (m, ) 164 | assert solution.basis.shape == (n + m, ) 165 | self._solution = solution 166 | 167 | def solution(self): 168 | return self._solution 169 | 170 | 171 | class UnsolvedInstance(SolutionEncoder, DenseLHS, LPInstance): 172 | ''' Full instance class storing data as (A, b, c). The instance may or 173 | may not have a solution. ''' 174 | 175 | def __init__(self, rhs, objective, **kwargs): 176 | super().__init__(**kwargs) 177 | assert rhs.shape == (self.constraints, ) 178 | assert objective.shape == (self.variables, ) 179 | self._rhs = rhs 180 | self._objective = objective 181 | 182 | def rhs(self): 183 | return self._rhs 184 | 185 | def objective(self): 186 | return self._objective 187 | 188 | def solution(self): 189 | model = LPCy() 190 | model.construct_dense_canonical( 191 | self.variables, self.constraints, 192 | self.lhs(), self.rhs(), self.objective()) 193 | model.solve() 194 | 195 | if model.get_solution_status() != 0: 196 | raise ValueError('Instance could not be decoded as it could not be solved.') 197 | 198 | x = model.get_solution_primals() 199 | s = model.get_solution_slacks() 200 | y = model.get_solution_duals() 201 | r = model.get_solution_reduced_costs() 202 | basis = model.get_solution_basis() 203 | return Solution(x=x, s=s, y=y, r=r, basis=basis) 204 | -------------------------------------------------------------------------------- /lp_generators/lhs_generators.py: -------------------------------------------------------------------------------- 1 | ''' Randomly generate lhs matrices by varying degree and coefficient 2 | statistics. Main function to be called is generate_lhs. 3 | 4 | This process is a bit of a bottleneck in generation, since it is implemented 5 | in pure python and uses a quadratic algorithm to achieve the required 6 | expected frequencies of edges. This is fine for small scale experimental 7 | purposes, but further work needed (equivalent efficient algorithm and/or C++ 8 | implementation) before using this for larger instances. 9 | ''' 10 | 11 | import itertools 12 | import collections 13 | import operator 14 | 15 | import numpy as np 16 | import scipy.sparse as sparsemat 17 | 18 | 19 | def degree_dist(vertices, edges, max_degree, param, random_state): 20 | ''' Gives degree values for :vertices vertices. 21 | For each iteration, deterministic weights are given by the current vertex 22 | degree, random weights are chosen uniformly. 23 | The final weights for the next choice are weighted by :param on [0, 1], 24 | after being normalised by their sum. 25 | Vertices are excluded once they reach :max_degree, so :edges can be at 26 | most :vertices x :max_degree ''' 27 | if (edges > vertices * max_degree): 28 | raise ValueError('edges > vertices * max_degree') 29 | degree = [0] * vertices 30 | indices = list(range(vertices)) 31 | for _ in range(edges): 32 | deterministic_weights = np.array([degree[i] + 0.0001 for i in indices]) 33 | random_weights = random_state.uniform(0, 1, len(indices)) 34 | weights = ( 35 | deterministic_weights / deterministic_weights.sum() * param + 36 | random_weights / random_weights.sum() * (1 - param)) 37 | ind = random_state.choice(a=indices, p=weights) 38 | degree[ind] += 1 39 | if degree[ind] >= max_degree: 40 | indices.remove(ind) 41 | return degree 42 | 43 | 44 | def expected_bipartite_degree(degree1, degree2, random_state): 45 | # Generates edges with probability d1 * d2 / sum(d1), asserting that 46 | # sum(d1) = sum(d2). 47 | # There is a more efficient way, right? 48 | if abs(sum(degree1) - sum(degree2)) > 10 ** -5: 49 | raise ValueError('You\'ve unbalanced the force!') 50 | rho = 1 / sum(degree1) 51 | for i, di in enumerate(degree1): 52 | for j, dj in enumerate(degree2): 53 | if random_state.uniform(0, 1) < (di * dj * rho): 54 | # print('yield', i, j) 55 | yield i, j 56 | 57 | 58 | def generate_by_degree(n1, n2, density, p1, p2, random_state): 59 | ''' Join together two vertex distributions to create a bipartite graph. ''' 60 | nedges = max(int(round(n1 * n2 * density)), 1) 61 | degree1 = degree_dist(n1, nedges, n2, p1, random_state) 62 | degree2 = degree_dist(n2, nedges, n1, p2, random_state) 63 | return expected_bipartite_degree(degree1, degree2, random_state) 64 | 65 | 66 | def connect_remaining(n1, n2, edges, random_state): 67 | ''' Finds any isolated vertices in the bipartite graph and connects them. ''' 68 | degree1 = collections.Counter(map(operator.itemgetter(0), edges)) 69 | degree2 = collections.Counter(map(operator.itemgetter(1), edges)) 70 | missing1 = [i for i in range(n1) if degree1[i] == 0] 71 | missing2 = [j for j in range(n2) if degree2[j] == 0] 72 | random_state.shuffle(missing1) 73 | random_state.shuffle(missing2) 74 | for v1, v2 in itertools.zip_longest(missing1, missing2): 75 | try: 76 | if v1 is None: 77 | v1 = random_state.choice( 78 | [i for i in range(n1) if degree1[i] < n2]) 79 | if v2 is None: 80 | v2 = random_state.choice( 81 | [j for j in range(n2) if degree2[j] < n1]) 82 | except ValueError: 83 | print(degree1) 84 | print(degree2) 85 | raise 86 | yield v1, v2 87 | degree1[v1] += 1 88 | degree2[v2] += 2 89 | 90 | 91 | def generate_edges(n1, n2, density, p1, p2, random_state): 92 | ''' Generate edges using size and weight parameters. ''' 93 | edges = set(generate_by_degree(n1, n2, density, p1, p2, random_state)) 94 | edges.update(connect_remaining(n1, n2, edges, random_state)) 95 | return edges 96 | 97 | 98 | def generate_lhs(variables, constraints, density, pv, pc, 99 | coeff_loc, coeff_scale, random_state): 100 | ''' Generate lhs constraint matrix using sparsity parameters and 101 | coefficient value distribution. ''' 102 | ind_var, ind_cons = zip(*generate_edges( 103 | variables, constraints, density, 104 | pv, pc, random_state)) 105 | data = random_state.normal( 106 | loc=coeff_loc, scale=coeff_scale, size=len(ind_var)) 107 | return sparsemat.coo_matrix((data, (ind_cons, ind_var))) 108 | -------------------------------------------------------------------------------- /lp_generators/lp_ext.py: -------------------------------------------------------------------------------- 1 | ''' Convenience wrapper importing classes from C++ extension 2 | into the package namespace. ''' 3 | 4 | from lp_generators_ext import LPCy 5 | -------------------------------------------------------------------------------- /lp_generators/neighbours_common.py: -------------------------------------------------------------------------------- 1 | ''' Elementwise modifiers to instance data. Functions here take a matrix of 2 | instance data and modify in place. Implementors at the instance level should 3 | copy the data. ''' 4 | 5 | import numpy as np 6 | 7 | 8 | def apply_repeat(func): 9 | ''' Add a count argument to the decorated modifier function to allow its 10 | operation to be applied repeatedly. ''' 11 | def apply_repeat_fn(arr, random_state, *args, count, **kwargs): 12 | for _ in range(count): 13 | func(arr, random_state, *args, **kwargs) 14 | return apply_repeat_fn 15 | 16 | 17 | @apply_repeat 18 | def _exchange_basis(beta, random_state): 19 | ''' Exchange elements in a basis vector. ''' 20 | incoming = random_state.choice(np.where(beta == 0)[0]) 21 | outgoing = random_state.choice(np.where(beta == 1)[0]) 22 | beta[incoming] = 1 23 | beta[outgoing] = 0 24 | 25 | 26 | @apply_repeat 27 | def _scale_vector_entry(vector, random_state, mean, sigma, dist): 28 | ''' Scale element in a one dimensional vector. ''' 29 | scale_index = random_state.choice(vector.shape[0]) 30 | if dist == 'normal': 31 | scale_value = random_state.normal(loc=mean, scale=sigma) 32 | elif dist == 'lognormal': 33 | scale_value = random_state.lognormal(mean=mean, sigma=sigma) 34 | else: 35 | raise ValueError('Vector entry scales only with normal or lognormal') 36 | vector[scale_index] = scale_value * vector[scale_index] 37 | 38 | 39 | @apply_repeat 40 | def _remove_lhs_entry(lhs, random_state): 41 | ''' Remove element from lhs matrix. ''' 42 | nz_rows, nz_cols = np.where(lhs != 0) 43 | if len(nz_rows) == 0: 44 | return 45 | remove_index = random_state.choice(nz_rows.shape[0]) 46 | lhs[nz_rows[remove_index], nz_cols[remove_index]] = 0 47 | 48 | 49 | @apply_repeat 50 | def _add_lhs_entry(lhs, random_state, mean, sigma): 51 | ''' Add an element to lhs matrix. ''' 52 | zero_rows, zero_cols = np.where(lhs == 0) 53 | if len(zero_rows) == 0: 54 | return 55 | add_index = random_state.choice(zero_rows.shape[0]) 56 | add_value = random_state.normal(loc=mean, scale=sigma) 57 | lhs[zero_rows[add_index], zero_cols[add_index]] = add_value 58 | 59 | 60 | @apply_repeat 61 | def _scale_lhs_entry(lhs, random_state, mean, sigma): 62 | ''' Scale an element of the constraint matrix. ''' 63 | nz_rows, nz_cols = np.where(lhs != 0) 64 | if len(nz_rows) == 0: 65 | return lhs 66 | scale_index = random_state.choice(nz_rows.shape[0]) 67 | scale_value = random_state.normal(loc=mean, scale=sigma) 68 | lhs[nz_rows[scale_index], nz_cols[scale_index]] = ( 69 | scale_value * lhs[nz_rows[scale_index], nz_cols[scale_index]]) 70 | -------------------------------------------------------------------------------- /lp_generators/neighbours_encoded.py: -------------------------------------------------------------------------------- 1 | ''' Neighbourhood modifiers that return EncodedInstance objects. 2 | Input instances must be able to return alpha() and beta() results to be 3 | copied in this scheme. ''' 4 | 5 | import numpy as np 6 | 7 | from .instance import EncodedInstance 8 | from .neighbours_common import ( 9 | _scale_vector_entry, _exchange_basis, _scale_lhs_entry, 10 | _remove_lhs_entry, _add_lhs_entry) 11 | 12 | 13 | def copied_neighbour(func): 14 | ''' Intercept call to decorated function, copying the instance first. 15 | The wrapper calls :func on the instance, then returns the copy. 16 | Wrapped function can be sure the :instance argument in an EncodedInstance, 17 | with data stored as _lhs_matrix, _alpha, _beta. ''' 18 | def copied_neighbour_fn(instance, random_state, *args, **kwargs): 19 | instance = EncodedInstance( 20 | lhs=np.copy(instance.lhs()), 21 | alpha=np.copy(instance.alpha()), 22 | beta=np.copy(instance.beta())) 23 | func(instance, random_state, *args, **kwargs) 24 | return instance 25 | return copied_neighbour_fn 26 | 27 | 28 | @copied_neighbour 29 | def exchange_basis(instance, random_state, count): 30 | _exchange_basis(instance._beta, random_state, count=count) 31 | 32 | 33 | @copied_neighbour 34 | def scale_optvalue(instance, random_state, count, mean, sigma): 35 | _scale_vector_entry(instance._alpha, random_state, mean, sigma, dist='lognormal', count=count) 36 | 37 | 38 | @copied_neighbour 39 | def remove_lhs_entry(instance, random_state, count): 40 | _remove_lhs_entry(instance._lhs_matrix, random_state, count=count) 41 | 42 | 43 | @copied_neighbour 44 | def add_lhs_entry(instance, random_state, count, mean, sigma): 45 | _add_lhs_entry(instance._lhs_matrix, random_state, mean, sigma, count=count) 46 | 47 | 48 | @copied_neighbour 49 | def scale_lhs_entry(instance, random_state, count, mean, sigma): 50 | _scale_lhs_entry(instance._lhs_matrix, random_state, mean, sigma, count=count) 51 | -------------------------------------------------------------------------------- /lp_generators/neighbours_unsolved.py: -------------------------------------------------------------------------------- 1 | ''' Neighbourhood modifiers that return UnsolvedInstance objects. 2 | Input instances must be able to return objective() and rhs() results to be 3 | copied in this scheme. ''' 4 | 5 | import numpy as np 6 | 7 | from .instance import UnsolvedInstance 8 | from .neighbours_common import ( 9 | _scale_vector_entry, _scale_lhs_entry, _remove_lhs_entry, _add_lhs_entry) 10 | 11 | 12 | def copied_neighbour(func): 13 | ''' Intercept call to decorated function, copying the instance first. 14 | The wrapper calls :func on the instance, then returns the copy. 15 | Wrapped function can be sure the :instance argument in an UnsolvedInstance, 16 | with data stored as _lhs_matrix, _rhs, _objective. ''' 17 | def copied_neighbour_fn(instance, random_state, *args, **kwargs): 18 | new_instance = UnsolvedInstance( 19 | lhs=np.copy(instance.lhs()), 20 | rhs=np.copy(instance.rhs()), 21 | objective=np.copy(instance.objective())) 22 | func(new_instance, random_state, *args, **kwargs) 23 | return new_instance 24 | return copied_neighbour_fn 25 | 26 | 27 | @copied_neighbour 28 | def scale_obj_entry(instance, random_state, count, mean, sigma): 29 | _scale_vector_entry(instance._objective, random_state, mean, sigma, dist='normal', count=count) 30 | 31 | 32 | @copied_neighbour 33 | def scale_rhs_entry(instance, random_state, count, mean, sigma): 34 | _scale_vector_entry(instance._rhs, random_state, mean, sigma, dist='normal', count=count) 35 | 36 | 37 | @copied_neighbour 38 | def remove_lhs_entry(instance, random_state, count): 39 | _remove_lhs_entry(instance._lhs_matrix, random_state, count=count) 40 | 41 | 42 | @copied_neighbour 43 | def add_lhs_entry(instance, random_state, count, mean, sigma): 44 | _add_lhs_entry(instance._lhs_matrix, random_state, mean, sigma, count=count) 45 | 46 | 47 | @copied_neighbour 48 | def scale_lhs_entry(instance, random_state, count, mean, sigma): 49 | _scale_lhs_entry(instance._lhs_matrix, random_state, mean, sigma, count=count) 50 | -------------------------------------------------------------------------------- /lp_generators/performance.py: -------------------------------------------------------------------------------- 1 | ''' Performance calculation functions. Calls SCIP and CLP solvers as 2 | subprocesses, so both must be available on the system path. ''' 3 | 4 | import subprocess 5 | import re 6 | 7 | from .writers import write_mps, write_mps_ip 8 | from .utils import temp_file_path 9 | 10 | 11 | def clp_solve_file(file, method): 12 | ''' Solve with a clp method and return statistics. ''' 13 | result = subprocess.run( 14 | ['clp', file, '-{}'.format(method)], 15 | stdout=subprocess.PIPE, 16 | stderr=subprocess.PIPE) 17 | stdout = result.stdout.decode('utf-8') 18 | regex = 'Optimal objective +([0-9e\-\.\+]+) +- +([0-9]+) +iterations +time +([0-9\.]+)' 19 | match = re.search(regex, stdout) 20 | if match is None: 21 | # There are iteration counts to check here. 22 | # This occurs in infeasible/unbounded cases. 23 | return dict(objective=None, iterations=-1, time=-1) 24 | result = dict( 25 | objective=float(match.group(1)), 26 | iterations=int(match.group(2)), 27 | time=float(match.group(3))) 28 | regex = 'flop count +([0-9]+)' 29 | match = re.search(regex, stdout) 30 | if match is not None: 31 | result['flops'] = int(match.group(1)) 32 | return result 33 | 34 | 35 | def scip_strongbranch_file(file): 36 | ''' Run SCIP and force all full strong branching. 37 | Terminate at the root node, return strong branching stats. 38 | Gives a measure of reoptimisation effort. ''' 39 | result = subprocess.run( 40 | [ 41 | 'scip', '-c', 'read {}'.format(file), 42 | '-c', 'set limits nodes 1', 43 | '-c', 'set branching allfullstrong priority 1000000', 44 | '-c', 'opt', 45 | '-c', 'display statistics', 46 | '-c', 'quit'], 47 | stdout=subprocess.PIPE, 48 | stderr=subprocess.PIPE) 49 | stdout = result.stdout.decode('utf-8') 50 | regex = 'strong branching +: +([0-9\.]+) +([0-9]+) +([0-9]+) +([0-9\.]+)' 51 | match = re.search(regex, stdout) 52 | return dict( 53 | time=float(match.group(1)), 54 | calls=int(match.group(2)), 55 | iterations=int(match.group(3)), 56 | percall=float(match.group(4))) 57 | 58 | 59 | def clp_simplex_performance(instance): 60 | ''' Write an instance as LP, report primal simplex results. ''' 61 | with temp_file_path('.mps.gz') as file: 62 | write_mps(instance, file) 63 | primal_result = clp_solve_file(file, 'primalsimplex') 64 | dual_result = clp_solve_file(file, 'dualsimplex') 65 | barrier_result = clp_solve_file(file, 'barrier') 66 | if 'flops' not in barrier_result: 67 | barrier_result['flops'] = -1 68 | return dict( 69 | clp_primal_objective=primal_result['objective'], 70 | clp_primal_iterations=primal_result['iterations'], 71 | clp_primal_time=primal_result['time'], 72 | clp_dual_objective=dual_result['objective'], 73 | clp_dual_iterations=dual_result['iterations'], 74 | clp_dual_time=dual_result['time'], 75 | clp_barrier_objective=barrier_result['objective'], 76 | clp_barrier_iterations=barrier_result['iterations'], 77 | clp_barrier_time=barrier_result['time'], 78 | clp_barrier_flops=barrier_result['flops']) 79 | 80 | 81 | def strbr_performance(instance): 82 | ''' Write an instance as pure IP, report strong branching results. ''' 83 | with temp_file_path('.mps.gz') as file: 84 | # integrality conversion 85 | write_mps_ip(instance, file) 86 | result = scip_strongbranch_file(file) 87 | return dict( 88 | strbr_time=result['time'], 89 | strbr_calls=result['calls'], 90 | strbr_iterations=result['iterations'], 91 | strbr_percall=result['percall']) 92 | -------------------------------------------------------------------------------- /lp_generators/search.py: -------------------------------------------------------------------------------- 1 | ''' Instance space search function, which applies neighbourhood operators to 2 | generate new instances to check for improvement against the given objective. 3 | The search function returns a generator which iterates over step results. ''' 4 | 5 | import os 6 | from contextlib import suppress 7 | import functools 8 | 9 | 10 | def local_search(objective, sense, neighbour, start_instance, steps, random_state): 11 | ''' Start from a given instance, generating a random neighbour at each step 12 | and accepting it if it improves the objective function for the given sense. 13 | Result is a generator, where each step yields a tuple step_info, instance. 14 | step info is a dict: 15 | search_step: step count 16 | search_objective: current objective function value 17 | search_update: 'improved' if the current step is new, 'reject_poor' otherwise 18 | instance is the current instance object at this step ''' 19 | 20 | if sense == 'min': 21 | def accept_next(c_new, c_old): 22 | return c_new < c_old 23 | elif sense == 'max': 24 | def accept_next(c_new, c_old): 25 | return c_new > c_old 26 | else: 27 | raise ValueError('Sense must be max or min') 28 | 29 | # initial state 30 | instance = start_instance 31 | next_instance = start_instance 32 | c_old = 1e+20 if sense == 'min' else -1e+20 33 | is_new = True 34 | step_info = dict(step='start') 35 | 36 | for step in range(steps): 37 | 38 | # data and objective calculation 39 | c_new = objective(next_instance) 40 | 41 | # step update rule 42 | if accept_next(c_new, c_old): 43 | instance = next_instance 44 | c_old = c_new 45 | is_new = True 46 | state = 'improved' 47 | else: 48 | state = 'reject_poor' 49 | 50 | step_info = dict( 51 | search_step=step, 52 | search_objective=c_old, 53 | search_update=state) 54 | yield step_info, instance 55 | 56 | # next candidate 57 | next_instance = neighbour(instance, random_state) 58 | is_new = False 59 | 60 | 61 | def write_steps(write_func, name_format, new_only): 62 | ''' Write the results of a search function, passing the current step 63 | count to name_format. Reads from step_info whether the instance is new 64 | or not, so the function can optionally write new instances only. ''' 65 | def write_steps_decorator(func): 66 | @functools.wraps(func) 67 | def write_steps_fn(*args, **kwargs): 68 | # ensure the directory exists 69 | directory, _ = os.path.split(name_format) 70 | with suppress(FileExistsError): 71 | os.makedirs(directory) 72 | # write each instance as it is yielded, pass on 73 | for step_info, instance in func(*args, **kwargs): 74 | if new_only is False or step_info['search_update'] == 'improved': 75 | write_func(instance, name_format.format(step=step_info['search_step'])) 76 | yield step_info, instance 77 | return write_steps_fn 78 | return write_steps_decorator 79 | -------------------------------------------------------------------------------- /lp_generators/solution_generators.py: -------------------------------------------------------------------------------- 1 | ''' Generates encoded primal-dual solutions for instances by generating 2 | a basis vector and values for the resulting non-zero x, y, r, s values. ''' 3 | 4 | import numpy as np 5 | 6 | 7 | def generate_beta(variables, constraints, basis_split, random_state): 8 | ''' Generate a vector specifying a random basis for an instance. ''' 9 | primal_count = int(round(basis_split * min(variables, constraints))) 10 | slack_count = constraints - primal_count 11 | primals_in_basis = random_state.choice( 12 | np.arange(0, variables), 13 | size=primal_count, replace=False) 14 | slacks_in_basis = random_state.choice( 15 | np.arange(variables, variables + constraints), 16 | size=slack_count, replace=False) 17 | beta_vector = np.zeros(variables + constraints) 18 | basis = np.concatenate([primals_in_basis, slacks_in_basis]) 19 | beta_vector[basis] = 1 20 | return beta_vector 21 | 22 | 23 | def generate_alpha(variables, constraints, frac_violations, beta_param, 24 | mean_primal, std_primal, mean_dual, std_dual, random_state): 25 | ''' Generate a non-negative vector of values for an optimal solution. ''' 26 | # choosing which solution values will be non-integral 27 | num_violations = int(round(frac_violations * variables)) 28 | ind_frac = random_state.choice( 29 | np.arange(0, variables), size=num_violations, replace=False) 30 | # fractional components 31 | frac_values = random_state.beta( 32 | a=beta_param, b=beta_param, size=num_violations) 33 | # subtract fractional components from a base vector of integers 34 | primal_alpha_vector = np.ceil(random_state.lognormal( 35 | mean=mean_primal, sigma=std_primal, size=variables)).astype(np.float) 36 | primal_alpha_vector[ind_frac] = primal_alpha_vector[ind_frac] - frac_values 37 | # slack values 38 | dual_alpha_vector = random_state.lognormal( 39 | mean=mean_dual, sigma=std_dual, 40 | size=constraints).astype(np.float) 41 | alpha_vector = np.concatenate([primal_alpha_vector, dual_alpha_vector]) 42 | return alpha_vector 43 | -------------------------------------------------------------------------------- /lp_generators/utils.py: -------------------------------------------------------------------------------- 1 | ''' Utilities for attaching data to instances, writing generated files, 2 | using temporary files for performance calculations, and randomly seeding 3 | processes. ''' 4 | 5 | import tempfile 6 | import os 7 | from contextlib import contextmanager, suppress 8 | import sys 9 | import functools 10 | import random 11 | 12 | 13 | @contextmanager 14 | def temp_file_path(ext=''): 15 | ''' Context manager returning a unique temporary file path without 16 | actually creating the file. Deletes the file on exit of the context 17 | if it exists. ''' 18 | path = tempfile.mktemp() + ext 19 | yield path 20 | with suppress(FileNotFoundError): 21 | os.remove(path) 22 | 23 | 24 | def calculate_data(*calculators): 25 | ''' Wrap a function which generates instances, passing instances to 26 | calculation functions before returning. Results from the calculation 27 | functions are added to the instances data dictionary. ''' 28 | def calculate_data_decorator(func): 29 | @functools.wraps(func) 30 | def calculate_data_fn(*args, **kwargs): 31 | instance = func(*args, **kwargs) 32 | if not hasattr(instance, 'data'): 33 | instance.data = dict() 34 | for calculator in calculators: 35 | instance.data.update(calculator(instance)) 36 | return instance 37 | return calculate_data_fn 38 | return calculate_data_decorator 39 | 40 | 41 | def write_instance(write_func, name_format): 42 | ''' Wrap a function which generates instances, passing the instance to a 43 | writer function before returning it. :name_format should use members of the 44 | :data dictionary of the instance to generate a unique name. ''' 45 | def write_instance_decorator(func): 46 | @functools.wraps(func) 47 | def write_instance_fn(*args, **kwargs): 48 | # ensure the directory exists 49 | directory, _ = os.path.split(name_format) 50 | with suppress(FileExistsError): 51 | os.makedirs(directory) 52 | # generate, write, and pass the instance on 53 | instance = func(*args, **kwargs) 54 | write_func(instance, name_format.format(**instance.data)) 55 | return instance 56 | return write_instance_fn 57 | return write_instance_decorator 58 | 59 | 60 | def system_random_seeds(n, bits): 61 | ''' Generator for a list of system random seeds of the specified bit size. ''' 62 | rand = random.SystemRandom() 63 | for _ in range(n): 64 | yield rand.getrandbits(bits) 65 | -------------------------------------------------------------------------------- /lp_generators/writers.py: -------------------------------------------------------------------------------- 1 | ''' Write instance data in various formats. Supports MPS for external use of 2 | generated instances and a tar format used internally to read and write 3 | instance data for generation and search where required. ''' 4 | 5 | import io 6 | import tarfile 7 | 8 | import numpy as np 9 | 10 | from .lp_ext import LPCy 11 | from .instance import EncodedInstance, UnsolvedInstance 12 | 13 | 14 | def write_mps(instance, file_name): 15 | ''' Write an LP instance to MPS format (using A, b, c). ''' 16 | writer = LPCy() 17 | writer.construct_dense_canonical( 18 | instance.variables, instance.constraints, 19 | np.asarray(instance.lhs()), 20 | np.asarray(instance.rhs()), 21 | np.asarray(instance.objective())) 22 | writer.write_mps(file_name) 23 | 24 | 25 | def write_mps_ip(instance, file_name): 26 | ''' Write an LP instance to MPS format (using A, b, c). ''' 27 | writer = LPCy() 28 | writer.construct_dense_canonical( 29 | instance.variables, instance.constraints, 30 | np.asarray(instance.lhs()), 31 | np.asarray(instance.rhs()), 32 | np.asarray(instance.objective())) 33 | writer.write_mps_ip(file_name) 34 | 35 | 36 | def save_matrix_to_tar(tarstore, matrix, name): 37 | ''' Helper function encodes matrix to bytes with numpy and adds to tarball. ''' 38 | fp = io.BytesIO() 39 | np.save(fp, matrix, allow_pickle=False) 40 | info = tarfile.TarInfo(name=name) 41 | info.size = fp.tell() 42 | fp.seek(0) 43 | tarstore.addfile(tarinfo=info, fileobj=fp) 44 | 45 | 46 | def extract_matrix_from_tar(tarstore, name): 47 | ''' Helper reads file object from tarball and decodes with numpy. ''' 48 | if name in tarstore.getnames(): 49 | fp = tarstore.extractfile(name) 50 | fpio = io.BytesIO(fp.read()) 51 | return np.load(fpio, allow_pickle=False) 52 | return None 53 | 54 | 55 | def write_tar_encoded(instance, filename): 56 | ''' Internal use format: write the encoded form matrices as a tarball. ''' 57 | with tarfile.TarFile(filename, mode='w') as store: 58 | save_matrix_to_tar(store, instance.lhs(), 'canonical_lhs.npy') 59 | save_matrix_to_tar(store, instance.alpha(), 'canonical_alpha.npy') 60 | save_matrix_to_tar(store, instance.beta(), 'canonical_beta.npy') 61 | 62 | 63 | def read_tar_encoded(filename): 64 | ''' Internal use format: read the encoded form matrices from a tarball. ''' 65 | with tarfile.TarFile(filename, mode='r') as store: 66 | lhs = extract_matrix_from_tar(store, 'canonical_lhs.npy') 67 | alpha = extract_matrix_from_tar(store, 'canonical_alpha.npy') 68 | beta = extract_matrix_from_tar(store, 'canonical_beta.npy') 69 | return EncodedInstance(lhs=lhs, alpha=alpha, beta=beta) 70 | 71 | 72 | def write_tar_lp(instance, filename): 73 | ''' Internal use format: write the encoded form matrices as a tarball. ''' 74 | with tarfile.TarFile(filename, mode='w') as store: 75 | save_matrix_to_tar(store, instance.lhs(), 'canonical_lhs.npy') 76 | save_matrix_to_tar(store, instance.rhs(), 'canonical_rhs.npy') 77 | save_matrix_to_tar(store, instance.objective(), 'canonical_objective.npy') 78 | 79 | 80 | def read_tar_lp(filename): 81 | ''' Internal use format: read the encoded form matrices from a tarball. ''' 82 | with tarfile.TarFile(filename, mode='r') as store: 83 | lhs = extract_matrix_from_tar(store, 'canonical_lhs.npy') 84 | rhs = extract_matrix_from_tar(store, 'canonical_rhs.npy') 85 | objective = extract_matrix_from_tar(store, 'canonical_objective.npy') 86 | return UnsolvedInstance(lhs=lhs, rhs=rhs, objective=objective) 87 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | figures/ 3 | -------------------------------------------------------------------------------- /scripts/Makefile: -------------------------------------------------------------------------------- 1 | # Recreate exact experimental results using the seed information in seed_files/ directory. 2 | 3 | all: naive parameterised \ 4 | naive_search parameterised_search \ 5 | naive_performance_search parameterised_performance_search 6 | 7 | clean: 8 | rm -rf data/ 9 | 10 | data/naive_random.json: 11 | @mkdir -p data 12 | python naive_random.py --system-seeds 3000 13 | 14 | data/parameterised_random.json: 15 | @mkdir -p data 16 | python parameterised_random.py --system-seeds 1000 17 | 18 | data/naive_search.json: data/naive_random.json 19 | python naive_search.py --system-seeds 100 20 | 21 | data/parameterised_search.json: data/naive_random.json 22 | python parameterised_search.py --system-seeds 100 23 | 24 | data/naive_performance_search_clp_dual_iterations.json: data/naive_random.json 25 | python naive_performance_search.py --system-seeds 100 --perf-field clp_dual_iterations 26 | 27 | data/naive_performance_search_clp_primal_iterations.json: data/naive_random.json 28 | python naive_performance_search.py --system-seeds 100 --perf-field clp_primal_iterations 29 | 30 | data/naive_performance_search_clp_barrier_iterations.json: data/naive_random.json 31 | python naive_performance_search.py --system-seeds 100 --perf-field clp_barrier_iterations 32 | 33 | data/naive_performance_search_clp_barrier_flops.json: data/naive_random.json 34 | python naive_performance_search.py --system-seeds 100 --perf-field clp_barrier_flops 35 | 36 | data/parameterised_performance_search_clp_dual_iterations.json: data/naive_random.json 37 | python parameterised_performance_search.py --system-seeds 100 --perf-field clp_dual_iterations 38 | 39 | data/parameterised_performance_search_clp_primal_iterations.json: data/naive_random.json 40 | python parameterised_performance_search.py --system-seeds 100 --perf-field clp_primal_iterations 41 | 42 | data/parameterised_performance_search_clp_barrier_iterations.json: data/naive_random.json 43 | python parameterised_performance_search.py --system-seeds 100 --perf-field clp_barrier_iterations 44 | 45 | data/parameterised_performance_search_clp_barrier_flops.json: data/naive_random.json 46 | python parameterised_performance_search.py --system-seeds 100 --perf-field clp_barrier_flops 47 | 48 | naive: data/naive_random.json 49 | parameterised: data/parameterised_random.json 50 | naive_search: data/naive_search.json 51 | parameterised_search: data/parameterised_search.json 52 | naive_performance_search: \ 53 | data/naive_performance_search_clp_primal_iterations.json \ 54 | data/naive_performance_search_clp_dual_iterations.json \ 55 | data/naive_performance_search_clp_barrier_iterations.json \ 56 | data/naive_performance_search_clp_barrier_flops.json 57 | parameterised_performance_search: \ 58 | data/parameterised_performance_search_clp_primal_iterations.json \ 59 | data/parameterised_performance_search_clp_dual_iterations.json \ 60 | data/parameterised_performance_search_clp_barrier_iterations.json \ 61 | data/parameterised_performance_search_clp_barrier_flops.json 62 | -------------------------------------------------------------------------------- /scripts/figures.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Dataset Analysis\n", 8 | "\n", 9 | "This notebook reproduces all figures from the paper using the datasets produced by running `make` in the scripts directory.\n", 10 | "Brief description of the datasets:\n", 11 | "\n", 12 | "* `naive_random.json` and `parameterised_random.json`:\n", 13 | "Store parameters, features and performance metrics for instances generated using the naive and controlled generators.\n", 14 | "In JSON format, each entry is an instance, with keys storing feature values.\n", 15 | "The complete instance data is also generated and stored by the scripts (as compressed serialised numpy matrices) so they can be used as seed points for local search runs.\n", 16 | "\n", 17 | "* `naive_search.json` and `parameterised_search.json`:\n", 18 | "Store feature-space search records.\n", 19 | "Each row records a search step.\n", 20 | "The scripts record the full feature and performance metric set of the current instance found by local search every 100 steps.\n", 21 | "\n", 22 | "* `naive_performance_search_*.json` and `parameterised_performance_search_*.json`:\n", 23 | "Store performance-space search records.\n", 24 | "File names indicate the performance metric used for search (e.g. `naive_performance_search_clp_primal_iterations.json`) stores the results of local search which aims to increase the number of iterations required by primal simplex to solve the instance.\n", 25 | "Each row records a search step.\n", 26 | "The scripts record the full feature and performance metric set of the current instance found by local search every 100 steps." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "import contextlib\n", 36 | "import os\n", 37 | "import json\n", 38 | "from functools import partial\n", 39 | "import numpy as np\n", 40 | "%matplotlib inline\n", 41 | "import matplotlib.pyplot as plt\n", 42 | "from matplotlib import cm\n", 43 | "import pandas as pd\n", 44 | "import seaborn as sns\n", 45 | "sns.set()\n", 46 | "sns.set_style('white')\n", 47 | "\n", 48 | "rename_columns = {\n", 49 | " 'var_degree_max': 'Variable Degree Max',\n", 50 | " 'cons_degree_max': 'Constraint Degree Max',\n", 51 | " 'var_degree_min': 'Variable Degree Min',\n", 52 | " 'cons_degree_min': 'Constraint Degree Min',\n", 53 | " 'coefficient_density': 'Coefficient Density',\n", 54 | " 'total_fractionality': 'Total Fractionality',\n", 55 | " 'binding_constraints': 'Number of Binding Constraints',\n", 56 | " 'rhs_mean': 'Constraint RHS Values Mean',\n", 57 | " 'obj_mean': 'Objective Coefficients Mean',\n", 58 | " 'clp_dual_iterations': 'Dual Simplex Iterations',\n", 59 | " 'clp_barrier_iterations': 'Barrier Method Iterations',\n", 60 | " 'clp_primal_iterations': 'Primal Simplex Iterations'\n", 61 | "}\n", 62 | "\n", 63 | "# Randomly generated datasets - rows are instances and columns are\n", 64 | "# features/parameters/performance metrics.\n", 65 | "df_naive_random = pd.read_json('data/naive_random.json').rename(columns=rename_columns)\n", 66 | "df_naive_random['Generator'] = 'Naive'\n", 67 | "df_param_random = pd.read_json('data/parameterised_random.json').rename(columns=rename_columns)\n", 68 | "df_param_random['Generator'] = 'Controlled'\n", 69 | "\n", 70 | "# Feature-space search datasets. Rows record the state of a search run at step N.\n", 71 | "# Each dataframe contains step records for 100 runs.\n", 72 | "df_naive_search = pd.read_json('data/naive_search.json').rename(columns=rename_columns)\n", 73 | "df_param_search = pd.read_json('data/parameterised_search.json').rename(columns=rename_columns)\n", 74 | "\n", 75 | "with contextlib.suppress(FileExistsError):\n", 76 | " os.mkdir('figures')\n", 77 | "\n", 78 | "def save(name):\n", 79 | " plt.savefig(f'figures/{name}.pdf')\n", 80 | "\n", 81 | "palette = sns.color_palette()" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "metadata": {}, 87 | "source": [ 88 | "## Naive generator parameters\n", 89 | "\n", 90 | "3000 instances generated, each with a new random seed and the following parameters.\n", 91 | "Each 'sample' generates parameters from these distributions and runs the naive algorithm with those parameters.\n", 92 | "\n", 93 | "```py\n", 94 | "size_params = dict(variables=50, constraints=50)\n", 95 | "rhs_params = dict(\n", 96 | " rhs_mean=uniform(low=-100.0, high=100.0),\n", 97 | " rhs_std=uniform(low=1.0, high=30.0))\n", 98 | "objective_params = dict(\n", 99 | " obj_mean=uniform(low=-100.0, high=100.0),\n", 100 | " obj_std=uniform(low=1.0, high=10.0))\n", 101 | "lhs_params = dict(\n", 102 | " density=uniform(low=0.0, high=1.0),\n", 103 | " pv=uniform(low=0.0, high=1.0),\n", 104 | " pc=uniform(low=0.0, high=1.0),\n", 105 | " coeff_loc=uniform(low=-2.0, high=2.0),\n", 106 | " coeff_scale=uniform(low=0.1, high=1.0))\n", 107 | "```\n", 108 | "\n", 109 | "## Controlled generator parameters\n", 110 | "\n", 111 | "1000 instances generated, as above, using the controlled algorithm.\n", 112 | "Generated more naive instances to ensure we had at least 1000 feasible, bounded instances as a comparison set.\n", 113 | "\n", 114 | "```py\n", 115 | "size_params = dict(variables=50, constraints=50)\n", 116 | "beta_params = dict(\n", 117 | " basis_split=random_state.uniform(low=0.0, high=1.0))\n", 118 | "alpha_params = dict(\n", 119 | " frac_violations=random_state.uniform(low=0.0, high=1.0),\n", 120 | " beta_param=random_state.lognormal(mean=-0.2, sigma=1.8),\n", 121 | " mean_primal=0,\n", 122 | " std_primal=1,\n", 123 | " mean_dual=0,\n", 124 | " std_dual=1)\n", 125 | "lhs_params = dict(\n", 126 | " density=random_state.uniform(low=0.0, high=1.0),\n", 127 | " pv=random_state.uniform(low=0.0, high=1.0),\n", 128 | " pc=random_state.uniform(low=0.0, high=1.0),\n", 129 | " coeff_loc=random_state.uniform(low=-2.0, high=2.0),\n", 130 | " coeff_scale=random_state.uniform(low=0.1, high=1.0))\n", 131 | "```\n", 132 | "\n", 133 | "`df_naive_random` and `df_param_random` store features and performance metrics of randomly generated instances.\n", 134 | "Each instance is a row generated using the parameters described above." 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "print(f'Controlled generator samples: {df_param_random.shape[0]}')\n", 144 | "print(f'Naive generator samples: {df_naive_random.shape[0]}')\n", 145 | "print(f'Naive generator feasible bounded samples: {df_naive_random.solvable.sum()}')\n", 146 | "print(f'Probability naive feasible and bounded: {df_naive_random.solvable.mean():.3f}')\n", 147 | "df_param_random.head()" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "## Constraint Features\n", 155 | "\n", 156 | "Plots below show features of the constraint matrix, which do not differ between generators, so we can just use the results in `df_param_random`.\n", 157 | "Each point in the plots below represent an instance." 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "sns.lmplot(\n", 167 | " data=df_param_random,\n", 168 | " x='Coefficient Density',\n", 169 | " y='Variable Degree Max',\n", 170 | " fit_reg=False)\n", 171 | "save('feature-dist-density')" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": null, 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "ax = sns.lmplot(\n", 181 | " data=df_param_random,\n", 182 | " x='Constraint Degree Max',\n", 183 | " y='Variable Degree Max',\n", 184 | " fit_reg=False)\n", 185 | "save('feature-dist-degree')" 186 | ] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "metadata": {}, 191 | "source": [ 192 | "# Solution Features\n", 193 | "\n", 194 | "Distribution of solution features differs between generators because the controlled generator explictly sets the solution.\n", 195 | "These features are only relevant for feasible and bounded instances so the naive generator results are filtered." 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": null, 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "ax = sns.lmplot(\n", 205 | " data=pd.concat([\n", 206 | " df_param_random,\n", 207 | " df_naive_random[df_naive_random.solvable]]),\n", 208 | " x='Number of Binding Constraints',\n", 209 | " y='Total Fractionality',\n", 210 | " hue='Generator',\n", 211 | " hue_order=['Controlled', 'Naive'],\n", 212 | " palette=reversed(palette[0:2]),\n", 213 | " fit_reg=False)\n", 214 | "ax.ax.set_ybound(lower=-1, upper=26)\n", 215 | "save('feature-dist-relaxation')" 216 | ] 217 | }, 218 | { 219 | "cell_type": "code", 220 | "execution_count": null, 221 | "metadata": {}, 222 | "outputs": [], 223 | "source": [ 224 | "bins = [ 0., 5., 10., 15., 20., 25., 30., 35., 40., 45., 50.]\n", 225 | "df_param_random[\"Number of Binding Constraints\"].hist(\n", 226 | " bins=bins, alpha=0.5)\n", 227 | "df_naive_random[df_naive_random.solvable][\"Number of Binding Constraints\"].hist(\n", 228 | " bins=bins, alpha=0.5)\n", 229 | "plt.gca().grid(False)\n", 230 | "sns.despine()\n", 231 | "plt.legend([\"Controllable\", \"Naive\"])\n", 232 | "plt.xlabel(\"Number of Binding Constraints\")\n", 233 | "plt.ylabel(\"Number of Generated Instances\")\n", 234 | "save('feature-dist-binding')" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "metadata": {}, 240 | "source": [ 241 | "# Objective/RHS Features\n", 242 | "\n", 243 | "Plot below compares feasible and bounded instances from both generators by their positions in objective/rhs feature space." 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "ax = sns.lmplot(\n", 253 | " data=pd.concat([\n", 254 | " df_param_random,\n", 255 | " df_naive_random[df_naive_random.solvable]]),\n", 256 | " x='Constraint RHS Values Mean',\n", 257 | " y='Objective Coefficients Mean',\n", 258 | " hue='Generator',\n", 259 | " hue_order=['Naive', 'Controlled'],\n", 260 | " palette=palette[0:2],\n", 261 | " fit_reg=False)\n", 262 | "save('feature-dist-obj-rhs')" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "metadata": {}, 268 | "source": [ 269 | "# Feasibility Phase Transition\n", 270 | "\n", 271 | "The naive generator produces instances where the mean values of objective and rhs coefficients are uniformly distributed.\n", 272 | "The plots below show feasibility and boundedness by position in this space." 273 | ] 274 | }, 275 | { 276 | "cell_type": "code", 277 | "execution_count": null, 278 | "metadata": {}, 279 | "outputs": [], 280 | "source": [ 281 | "df_naive_random['Feasible and Bounded'] = df_naive_random['solvable'].apply(\n", 282 | " lambda v: 'Yes' if v else 'No')\n", 283 | "sns.lmplot(\n", 284 | " data=df_naive_random,\n", 285 | " x='Constraint RHS Values Mean',\n", 286 | " y='Objective Coefficients Mean',\n", 287 | " hue='Feasible and Bounded',\n", 288 | " hue_order=['Yes', 'No'],\n", 289 | " palette=palette[2:],\n", 290 | " size=5, fit_reg=False, scatter_kws={'alpha': 0.6})\n", 291 | "save('naive-transition-scatter')" 292 | ] 293 | }, 294 | { 295 | "cell_type": "code", 296 | "execution_count": null, 297 | "metadata": {}, 298 | "outputs": [], 299 | "source": [ 300 | "df = df_naive_random[df_naive_random.solvable]\n", 301 | "sns.jointplot(\n", 302 | " x=df['Constraint RHS Values Mean'],\n", 303 | " y=df['Objective Coefficients Mean'],\n", 304 | " kind='kde', color='k', size=5, stat_func=None)\n", 305 | "save('naive-transition-prob')" 306 | ] 307 | }, 308 | { 309 | "cell_type": "markdown", 310 | "metadata": {}, 311 | "source": [ 312 | "# Generated Instance Difficulty\n", 313 | "\n", 314 | "Plots below indicate difficulty of instances for dual simplex in objective/rhs feature space." 315 | ] 316 | }, 317 | { 318 | "cell_type": "code", 319 | "execution_count": null, 320 | "metadata": {}, 321 | "outputs": [], 322 | "source": [ 323 | "df = df_naive_random[df_naive_random.solvable].sample(1000)\n", 324 | "print(f'Naive Generator. F+B samples: {df.shape[0]}')\n", 325 | "fig, ax = plt.subplots()\n", 326 | "sc = ax.scatter(\n", 327 | " x=df['Constraint RHS Values Mean'],\n", 328 | " y=df['Objective Coefficients Mean'],\n", 329 | " c=df['Dual Simplex Iterations'], cmap=cm.coolwarm)\n", 330 | "ax.set_xbound([-125, 125])\n", 331 | "ax.set_ybound([-125, 125])\n", 332 | "ax.set_xlabel('Constraint RHS Values Mean')\n", 333 | "ax.set_ylabel('Objective Coefficients Mean')\n", 334 | "cb = plt.colorbar(sc)\n", 335 | "cb.set_label('Dual Simplex Iterations')\n", 336 | "sns.despine()\n", 337 | "save('hardness-naive')" 338 | ] 339 | }, 340 | { 341 | "cell_type": "code", 342 | "execution_count": null, 343 | "metadata": {}, 344 | "outputs": [], 345 | "source": [ 346 | "df = df_param_random[df_param_random.solvable]\n", 347 | "print(f'Parameterised Generator. Samples: {df.shape[0]}')\n", 348 | "fig, ax = plt.subplots()\n", 349 | "sc = ax.scatter(\n", 350 | " x=df['Constraint RHS Values Mean'],\n", 351 | " y=df['Objective Coefficients Mean'],\n", 352 | " c=df['Dual Simplex Iterations'], cmap=cm.coolwarm)\n", 353 | "ax.set_xbound([-125, 125])\n", 354 | "ax.set_ybound([-125, 125])\n", 355 | "ax.set_xlabel('Constraint RHS Values Mean')\n", 356 | "ax.set_ylabel('Objective Coefficients Mean')\n", 357 | "cb = plt.colorbar(sc)\n", 358 | "cb.set_label('Dual Simplex Iterations')\n", 359 | "sns.despine()\n", 360 | "save('hardness-controlled')" 361 | ] 362 | }, 363 | { 364 | "cell_type": "markdown", 365 | "metadata": {}, 366 | "source": [ 367 | "# Instance Difficulty\n", 368 | "\n", 369 | "Shows instance difficulty achieved by the controllable generator as a direct result of setting the number of binding constraints in generated instances." 370 | ] 371 | }, 372 | { 373 | "cell_type": "code", 374 | "execution_count": null, 375 | "metadata": {}, 376 | "outputs": [], 377 | "source": [ 378 | "df1 = df_naive_random[df_naive_random.solvable].sample(1000)\n", 379 | "df2 = df_param_random[df_param_random.solvable]\n", 380 | "df1['Generator'] = 'Naive'\n", 381 | "df2['Generator'] = 'Controlled'\n", 382 | "sns.lmplot(\n", 383 | " data=pd.concat([df1, df2]),\n", 384 | " x='Number of Binding Constraints',\n", 385 | " y='Dual Simplex Iterations',\n", 386 | " hue='Generator',\n", 387 | " hue_order=['Controlled', 'Naive'],\n", 388 | " palette=reversed(palette[0:2]),\n", 389 | " fit_reg=False,\n", 390 | " scatter_kws={'alpha': 0.4, 'linewidths': 0, 's': 60})\n", 391 | "save('binding-constraints-hardness')" 392 | ] 393 | }, 394 | { 395 | "cell_type": "markdown", 396 | "metadata": {}, 397 | "source": [ 398 | "# Search" 399 | ] 400 | }, 401 | { 402 | "cell_type": "markdown", 403 | "metadata": {}, 404 | "source": [ 405 | "## Searching feature space\n", 406 | "\n", 407 | "In all runs, the objective is simply to reach the target point (-100, 100) in the obj/rhs feature space (our missing region).\n", 408 | "A start point is selected by randomly sampling 10 instances from the existing feasible-bounded instance set and choosing the closest point of those to the target.\n", 409 | "At each local search step:\n", 410 | "* generates a neighbour (using either naive or controllable method)\n", 411 | "* accepts that neighbour if it is an improvement.\n", 412 | "\n", 413 | "If the naive operator produces an infeasible or unbounded instance, it is rejected.\n", 414 | "Search continues for 10000 of these steps and we repeat each search process 100 times.\n", 415 | "The figure below shows state of play after 1000 steps.\n", 416 | "Subsequent figure shows reduced distance to target after N steps." 417 | ] 418 | }, 419 | { 420 | "cell_type": "code", 421 | "execution_count": null, 422 | "metadata": {}, 423 | "outputs": [], 424 | "source": [ 425 | "df1 = df_naive_random[df_naive_random.solvable].sample(1000).copy()\n", 426 | "df1['Source'] = 'Naive Generator'\n", 427 | "df2 = df_param_random[df_param_random.solvable].copy()\n", 428 | "df2['Source'] = 'Controlled Generator'\n", 429 | "\n", 430 | "step = 1000\n", 431 | "df3 = df_naive_search[df_naive_search.solvable & (df_naive_search.step == step)].copy()\n", 432 | "df3['Source'] = 'Naive Search'\n", 433 | "df4 = df_param_search[df_param_search.solvable & (df_param_search.step == step)].copy()\n", 434 | "df4['Source'] = 'Controlled Search'\n", 435 | "\n", 436 | "g = sns.lmplot(\n", 437 | " data=pd.concat([df1, df2, df3, df4]),\n", 438 | " x='Constraint RHS Values Mean',\n", 439 | " y='Objective Coefficients Mean',\n", 440 | " hue='Source',\n", 441 | " fit_reg=False,\n", 442 | " hue_order=[\n", 443 | " 'Naive Generator', 'Controlled Generator',\n", 444 | " 'Naive Search', 'Controlled Search'],\n", 445 | " palette=palette\n", 446 | ")\n", 447 | "g.ax.set_xbound([-125, 125])\n", 448 | "g.ax.set_ybound([-125, 125])\n", 449 | "save('feature-search-target')" 450 | ] 451 | }, 452 | { 453 | "cell_type": "code", 454 | "execution_count": null, 455 | "metadata": {}, 456 | "outputs": [], 457 | "source": [ 458 | "def calc_objective(df):\n", 459 | " objective = (\n", 460 | " (df['Constraint RHS Values Mean'] + 100) ** 2 +\n", 461 | " (df['Objective Coefficients Mean'] - 100) ** 2) ** 0.5\n", 462 | " return pd.DataFrame(dict(step=df.step, objective=objective))\n", 463 | "\n", 464 | "def compare_trajectory(labelled_series, ax, palette):\n", 465 | " ''' Expects series (step, objective) for comparison. '''\n", 466 | " assert len(labelled_series.items()) == 2\n", 467 | " for (label, data), color, linestyle in zip(labelled_series.items(), palette, ['--', '-']):\n", 468 | " df = data.groupby('step').objective.apply(\n", 469 | " lambda x: {\n", 470 | " 'median': x.median(),\n", 471 | " 'lower': x.quantile(0.25),\n", 472 | " 'upper': x.quantile(0.75)}\n", 473 | " ).unstack(1).reset_index()\n", 474 | " ax.fill_between(df['step'], df['lower'], df['upper'], alpha=0.3, color=color)\n", 475 | " ax.plot(df['step'], df['median'], linestyle, color=color, label=label)\n", 476 | " ax.collections[0].set_hatch('xx')\n", 477 | " ax.set_xlabel('Search Step')\n", 478 | " ax.legend()\n", 479 | " return ax" 480 | ] 481 | }, 482 | { 483 | "cell_type": "code", 484 | "execution_count": null, 485 | "metadata": {}, 486 | "outputs": [], 487 | "source": [ 488 | "fig, ax = plt.subplots()\n", 489 | "ax = compare_trajectory({\n", 490 | " 'Naive': calc_objective(df_naive_search),\n", 491 | " 'Controlled': calc_objective(df_param_search)\n", 492 | " }, ax, palette)\n", 493 | "ax.semilogy()\n", 494 | "ax.set_ylabel('Distance to Target')\n", 495 | "sns.despine(fig)\n", 496 | "save('feature-search-progression')" 497 | ] 498 | }, 499 | { 500 | "cell_type": "markdown", 501 | "metadata": {}, 502 | "source": [ 503 | "# Instance Difficulty\n", 504 | "\n", 505 | "Distributions below compare the original naive and controlled generator datasets compared with the sets of instances found at the 10000th search step of each of the 100 feature space search processes by each operator." 506 | ] 507 | }, 508 | { 509 | "cell_type": "code", 510 | "execution_count": null, 511 | "metadata": {}, 512 | "outputs": [], 513 | "source": [ 514 | "df1 = df_naive_random[df_naive_random.solvable].sample(1000).copy()\n", 515 | "df2 = df_param_random[df_param_random.solvable].copy()\n", 516 | "\n", 517 | "step = 10000\n", 518 | "df3 = df_naive_search[df_naive_search.solvable & (df_naive_search.step == step)].copy()\n", 519 | "df4 = df_param_search[df_param_search.solvable & (df_param_search.step == step)].copy()\n", 520 | "\n", 521 | "key = 'Dual Simplex Iterations'\n", 522 | "sns.boxplot(data=pd.DataFrame({\n", 523 | " 'Naive\\nGenerator': df1[key].reset_index(drop=True),\n", 524 | " 'Controlled\\nGenerator': df2[key].reset_index(drop=True),\n", 525 | " 'Naive\\nSearch': df3[key].reset_index(drop=True),\n", 526 | " 'Controlled\\nSearch': df4[key].reset_index(drop=True),\n", 527 | "}), orient='h')\n", 528 | "plt.xlabel('Dual Simplex Iterations')\n", 529 | "plt.tight_layout()\n", 530 | "sns.despine()\n", 531 | "save('feature-search-hardness-dual-boxplot')" 532 | ] 533 | }, 534 | { 535 | "cell_type": "code", 536 | "execution_count": null, 537 | "metadata": {}, 538 | "outputs": [], 539 | "source": [ 540 | "key = 'Primal Simplex Iterations'\n", 541 | "sns.boxplot(data=pd.DataFrame({\n", 542 | " 'Naive\\nGenerator': df1[key].reset_index(drop=True),\n", 543 | " 'Controlled\\nGenerator': df2[key].reset_index(drop=True),\n", 544 | " 'Naive\\nSearch': df3[key].reset_index(drop=True),\n", 545 | " 'Controlled\\nSearch': df4[key].reset_index(drop=True),\n", 546 | "}), orient='h')\n", 547 | "plt.xlabel('Primal Simplex Iterations')\n", 548 | "plt.tight_layout()\n", 549 | "sns.despine()\n", 550 | "save('feature-search-hardness-primal-boxplot')" 551 | ] 552 | }, 553 | { 554 | "cell_type": "markdown", 555 | "metadata": {}, 556 | "source": [ 557 | "## Searching performance space\n", 558 | "\n", 559 | "Operates in the same manner as feature space search, where the objective is to increase the number of iterations required by a target algorithm to solve the given feasible bounded instance.\n", 560 | "The plots below show the search progression summarised over 100 runs with the same objective." 561 | ] 562 | }, 563 | { 564 | "cell_type": "code", 565 | "execution_count": null, 566 | "metadata": {}, 567 | "outputs": [], 568 | "source": [ 569 | "def plot_for_field(ax, field):\n", 570 | " datasets = {\n", 571 | " 'Naive': pd.read_json(\n", 572 | " f'data/naive_performance_search_{field}.json')[['step', field]].rename(\n", 573 | " columns={field: 'objective'}),\n", 574 | " 'Controlled': pd.read_json(\n", 575 | " f'data/parameterised_performance_search_{field}.json')[['step', field]].rename(\n", 576 | " columns={field: 'objective'})}\n", 577 | " ax = compare_trajectory(datasets, ax, palette)\n", 578 | " return ax" 579 | ] 580 | }, 581 | { 582 | "cell_type": "code", 583 | "execution_count": null, 584 | "metadata": {}, 585 | "outputs": [], 586 | "source": [ 587 | "fig, ax = plt.subplots()\n", 588 | "ax = plot_for_field(ax, 'clp_primal_iterations')\n", 589 | "ax.set_ylabel('Primal Simplex Iterations')\n", 590 | "sns.despine(fig)\n", 591 | "plt.legend(loc=4)\n", 592 | "save('search-primal')" 593 | ] 594 | }, 595 | { 596 | "cell_type": "code", 597 | "execution_count": null, 598 | "metadata": {}, 599 | "outputs": [], 600 | "source": [ 601 | "fig, ax = plt.subplots()\n", 602 | "ax = plot_for_field(ax, 'clp_dual_iterations')\n", 603 | "ax.set_ylabel('Dual Simplex Iterations')\n", 604 | "sns.despine(fig)\n", 605 | "save('search-dual')\n", 606 | "plt.legend(loc=4)" 607 | ] 608 | }, 609 | { 610 | "cell_type": "code", 611 | "execution_count": null, 612 | "metadata": {}, 613 | "outputs": [], 614 | "source": [ 615 | "fig, ax = plt.subplots()\n", 616 | "ax = plot_for_field(ax, 'clp_barrier_iterations')\n", 617 | "ax.set_ylabel('Barrier Method Iterations')\n", 618 | "sns.despine(fig)\n", 619 | "save('search-barrier')\n", 620 | "plt.legend(loc=4)" 621 | ] 622 | }, 623 | { 624 | "cell_type": "code", 625 | "execution_count": null, 626 | "metadata": {}, 627 | "outputs": [], 628 | "source": [ 629 | "cols = ['Primal Simplex Iterations', 'Dual Simplex Iterations', 'Barrier Method Iterations']\n", 630 | "\n", 631 | "df1 = df_naive_random.loc[df_naive_random.solvable, cols]\n", 632 | "df2 = df_param_random.loc[df_param_random.solvable, cols]\n", 633 | "\n", 634 | "result1 = pd.DataFrame({\n", 635 | " ('Naive Generator', None, 'Q1'): df1.quantile(.25),\n", 636 | " ('Naive Generator', None, 'Q2'): df1.median(),\n", 637 | " ('Naive Generator', None, 'Q3'): df1.quantile(.75),\n", 638 | " ('Controllable Generator', None, 'Q1'): df2.quantile(.25),\n", 639 | " ('Controllable Generator', None, 'Q2'): df2.median(),\n", 640 | " ('Controllable Generator', None, 'Q3'): df2.quantile(.75),\n", 641 | "}).transpose().unstack()\n", 642 | "result1" 643 | ] 644 | }, 645 | { 646 | "cell_type": "code", 647 | "execution_count": null, 648 | "metadata": { 649 | "scrolled": false 650 | }, 651 | "outputs": [], 652 | "source": [ 653 | "def _read(method, field):\n", 654 | " full_df = pd.read_json(f'data/{method}_performance_search_{field}.json')\n", 655 | " for step in [0, 200, 500, 1000]:\n", 656 | " df = full_df.loc[full_df.step == step, field]\n", 657 | " yield dict(\n", 658 | " Q1=df.quantile(.25), Q2=df.median(), Q3=df.quantile(.75),\n", 659 | " step=step, field=field, method=method)\n", 660 | "\n", 661 | "import itertools\n", 662 | "\n", 663 | "result = pd.DataFrame(list(itertools.chain(*(\n", 664 | " _read(method, field)\n", 665 | " for method, field in itertools.product(\n", 666 | " ['naive', 'parameterised'],\n", 667 | " ['clp_primal_iterations', 'clp_dual_iterations', 'clp_barrier_iterations']\n", 668 | " )))))\n", 669 | "result['field'] = result['field'].map({\n", 670 | " 'clp_dual_iterations': 'Dual Simplex Iterations',\n", 671 | " 'clp_barrier_iterations': 'Barrier Method Iterations',\n", 672 | " 'clp_primal_iterations': 'Primal Simplex Iterations'\n", 673 | "})\n", 674 | "result['method'] = result['method'].map({\n", 675 | " 'naive': 'Naive Search',\n", 676 | " 'parameterised': 'Controllable Search'\n", 677 | "})\n", 678 | "result = result.set_index(['field', 'method', 'step']).stack().unstack(0).unstack(2)\n", 679 | "result = result[[\n", 680 | " 'Primal Simplex Iterations',\n", 681 | " 'Dual Simplex Iterations',\n", 682 | " 'Barrier Method Iterations',\n", 683 | " ]]\n", 684 | "result = pd.concat([result, result1]).sort_index()\n", 685 | "result = (\n", 686 | " result\n", 687 | " .rename(columns=lambda col: col.replace(' Iterations', ''))\n", 688 | " .astype('int')\n", 689 | ")\n", 690 | "print(result.to_latex())\n", 691 | "result" 692 | ] 693 | }, 694 | { 695 | "cell_type": "code", 696 | "execution_count": null, 697 | "metadata": {}, 698 | "outputs": [], 699 | "source": [ 700 | "# TODO add random dist values to each of these.\n", 701 | "\n", 702 | "steps = [0, 100, 1000, 10000]\n", 703 | "\n", 704 | "def _read():\n", 705 | " full_df = calc_objective(df_param_search)\n", 706 | " for step in steps:\n", 707 | " df = full_df.loc[full_df.step == step, 'objective']\n", 708 | " yield dict(method='Controllable Search', min=df.min(), median=df.median(), step=step)\n", 709 | " full_df = calc_objective(df_naive_search)\n", 710 | " for step in steps:\n", 711 | " df = full_df.loc[full_df.step == step, 'objective']\n", 712 | " yield dict(method='Naive Search', min=df.min(), median=df.median(), step=step)\n", 713 | "\n", 714 | "result = pd.DataFrame(list(_read())).set_index(['method', 'step'])\n", 715 | "print(result.round(3).to_latex())" 716 | ] 717 | }, 718 | { 719 | "cell_type": "markdown", 720 | "metadata": {}, 721 | "source": [ 722 | "# Design Solution Correctness\n", 723 | "\n", 724 | "Generates an additional figure which shows whether the design solution features were produced correctly by the controlled generator.\n", 725 | "The controlled generator attempts to set the number of nonzeros/binding constraints in the LP solutions.\n", 726 | "This value may differ if the generated constraints lead to multiple solutions for the generated instance.\n", 727 | "Figure below uses a rolling average to estimate probability that the solution found by simplex differs from the design solution, as a function of constraint matrix density (which affects likelihood of degeneracy)." 728 | ] 729 | }, 730 | { 731 | "cell_type": "code", 732 | "execution_count": null, 733 | "metadata": {}, 734 | "outputs": [], 735 | "source": [ 736 | "with open('data/parameterised_random.json') as infile:\n", 737 | " data = json.load(infile)\n", 738 | "\n", 739 | "df = pd.DataFrame([\n", 740 | " {\n", 741 | " 'basis_split': instance['params']['beta_params']['basis_split'],\n", 742 | " 'binding_constraints': instance['binding_constraints'],\n", 743 | " 'nonzeros': instance['nonzeros'],\n", 744 | " }\n", 745 | " for instance in data\n", 746 | "])\n", 747 | "\n", 748 | "solution_different = (df.binding_constraints - (df.basis_split * 50).round()).abs() > 0\n", 749 | "(\n", 750 | " pd.DataFrame({\n", 751 | " 'different': solution_different,\n", 752 | " 'nonzeros': df.nonzeros\n", 753 | " }).groupby('nonzeros').different.mean().sort_index()\n", 754 | " .rolling(150, center=True).mean().dropna()\n", 755 | ").plot()\n", 756 | "plt.xlabel('Number of Nonzeros')\n", 757 | "plt.ylabel('Pr (Solution Different)')\n", 758 | "plt.xlim([150, 2300])\n", 759 | "sns.despine()\n", 760 | "save('pr-solution-different')" 761 | ] 762 | }, 763 | { 764 | "cell_type": "code", 765 | "execution_count": null, 766 | "metadata": {}, 767 | "outputs": [], 768 | "source": [] 769 | } 770 | ], 771 | "metadata": { 772 | "kernelspec": { 773 | "display_name": "Python 3", 774 | "language": "python", 775 | "name": "python3" 776 | }, 777 | "language_info": { 778 | "codemirror_mode": { 779 | "name": "ipython", 780 | "version": 3 781 | }, 782 | "file_extension": ".py", 783 | "mimetype": "text/x-python", 784 | "name": "python", 785 | "nbconvert_exporter": "python", 786 | "pygments_lexer": "ipython3", 787 | "version": "3.7.3" 788 | } 789 | }, 790 | "nbformat": 4, 791 | "nbformat_minor": 2 792 | } 793 | -------------------------------------------------------------------------------- /scripts/naive_performance_search.py: -------------------------------------------------------------------------------- 1 | 2 | import itertools 3 | import multiprocessing 4 | import json 5 | 6 | import click 7 | import numpy as np 8 | from tqdm import tqdm 9 | 10 | from lp_generators.features import coeff_features, solution_features 11 | from lp_generators.performance import clp_simplex_performance 12 | 13 | from search_operators import lp_column_neighbour, lp_row_neighbour 14 | from seeds import cli_seeds 15 | from performance_search_common import condition, objective, start_instance, calculate_features 16 | 17 | 18 | def generate_by_search(arg): 19 | seed, perf_field = arg 20 | results = [] 21 | pass_condition = 0 22 | step_change = 0 23 | random_state = np.random.RandomState(seed) 24 | current_instance = start_instance(random_state, perf_field) 25 | current_features = calculate_features(current_instance) 26 | for step in range(10001): 27 | if (step % 100) == 0: 28 | results.append(dict( 29 | **coeff_features(current_instance), 30 | **solution_features(current_instance), 31 | **clp_simplex_performance(current_instance), 32 | pass_condition=pass_condition, 33 | step_change=step_change, 34 | step=step, seed=seed)) 35 | if (step % 2) == 0: 36 | new_instance = lp_row_neighbour(random_state, current_instance, 5) 37 | else: 38 | new_instance = lp_column_neighbour(random_state, current_instance, 5) 39 | new_features = calculate_features(new_instance) 40 | if condition(new_features): 41 | pass_condition += 1 42 | if objective(new_features, perf_field) < objective(current_features, perf_field): 43 | step_change += 1 44 | current_instance = new_instance 45 | current_features = new_features 46 | return results 47 | 48 | 49 | @cli_seeds 50 | @click.option('--perf-field', type=str) 51 | def run(seed_values, perf_field): 52 | assert perf_field is not None 53 | ''' Generate the required number of instances and store feature results. ''' 54 | pool = multiprocessing.Pool() 55 | mapper = pool.imap_unordered 56 | print('Generating instances by naive search.') 57 | features = list(tqdm( 58 | mapper(generate_by_search, zip(seed_values, itertools.repeat(perf_field))), 59 | total=len(seed_values), smoothing=0)) 60 | features = list(itertools.chain(*features)) 61 | with open('data/naive_performance_search_{}.json'.format(perf_field), 'w') as outfile: 62 | json.dump(features, outfile, indent=4, sort_keys=True) 63 | 64 | run() 65 | -------------------------------------------------------------------------------- /scripts/naive_random.py: -------------------------------------------------------------------------------- 1 | ''' Generate a set of instances using the naive method. ''' 2 | 3 | import multiprocessing 4 | import json 5 | 6 | import numpy as np 7 | from tqdm import tqdm 8 | 9 | from lp_generators.lhs_generators import generate_lhs 10 | from lp_generators.instance import UnsolvedInstance 11 | from lp_generators.features import coeff_features, solution_features 12 | from lp_generators.performance import clp_simplex_performance 13 | from lp_generators.utils import calculate_data, write_instance 14 | from lp_generators.writers import write_tar_lp 15 | 16 | from seeds import cli_seeds 17 | 18 | 19 | def generate_rhs(variables, constraints, rhs_mean, rhs_std, random_state): 20 | return random_state.normal(loc=rhs_mean, scale=rhs_std, size=constraints) 21 | 22 | 23 | def generate_objective(variables, constraints, obj_mean, obj_std, random_state): 24 | return random_state.normal(loc=obj_mean, scale=obj_std, size=variables) 25 | 26 | 27 | @calculate_data(coeff_features, solution_features, clp_simplex_performance) 28 | @write_instance(write_tar_lp, 'data/naive_random/inst_{seed}.tar') 29 | def generate(seed): 30 | ''' Creates a distribution of fixed size instances using the 31 | 'naive' strategy: 32 | - loc/scale parameters for rhs and objective distributions are 33 | distributed uniformly (at the instance level) 34 | - rhs/objective values are chosen from uniform distributions 35 | using the chosen instance-level parameters 36 | ''' 37 | 38 | random_state = np.random.RandomState(seed) 39 | 40 | size_params = dict(variables=50, constraints=50) 41 | rhs_params = dict( 42 | rhs_mean=random_state.uniform(low=-100.0, high=100.0), 43 | rhs_std=random_state.uniform(low=1.0, high=30.0)) 44 | objective_params = dict( 45 | obj_mean=random_state.uniform(low=-100.0, high=100.0), 46 | obj_std=random_state.uniform(low=1.0, high=10.0)) 47 | lhs_params = dict( 48 | density=random_state.uniform(low=0.0, high=1.0), 49 | pv=random_state.uniform(low=0.0, high=1.0), 50 | pc=random_state.uniform(low=0.0, high=1.0), 51 | coeff_loc=random_state.uniform(low=-2.0, high=2.0), 52 | coeff_scale=random_state.uniform(low=0.1, high=1.0)) 53 | 54 | instance = UnsolvedInstance( 55 | lhs=generate_lhs(random_state=random_state, **size_params, **lhs_params).todense(), 56 | rhs=generate_rhs(random_state=random_state, **size_params, **rhs_params), 57 | objective=generate_objective(random_state=random_state, **size_params, **objective_params)) 58 | instance.data = dict(seed=seed) 59 | return instance 60 | 61 | 62 | @cli_seeds 63 | def run(seed_values): 64 | ''' Generate the required number of instances and store feature results. ''' 65 | pool = multiprocessing.Pool() 66 | mapper = pool.imap_unordered 67 | print('Generating fixed size naive random instances.') 68 | instances = tqdm( 69 | mapper(generate, seed_values), 70 | total=len(seed_values), smoothing=0) 71 | features = [ 72 | instance.data 73 | for instance in instances 74 | ] 75 | with open('data/naive_random.json', 'w') as outfile: 76 | json.dump(features, outfile, indent=4, sort_keys=True) 77 | 78 | run() 79 | -------------------------------------------------------------------------------- /scripts/naive_search.py: -------------------------------------------------------------------------------- 1 | 2 | import itertools 3 | import multiprocessing 4 | import json 5 | 6 | import numpy as np 7 | from tqdm import tqdm 8 | 9 | from lp_generators.features import coeff_features, solution_features 10 | from lp_generators.performance import clp_simplex_performance 11 | 12 | from search_operators import lp_column_neighbour, lp_row_neighbour 13 | from seeds import cli_seeds 14 | from search_common import condition, objective, start_instance 15 | 16 | 17 | def calculate_features(instance): 18 | return dict( 19 | **coeff_features(instance), 20 | **solution_features(instance)) 21 | 22 | 23 | def generate_by_search(seed): 24 | results = [] 25 | pass_condition = 0 26 | step_change = 0 27 | random_state = np.random.RandomState(seed) 28 | current_instance = start_instance(random_state) 29 | current_features = calculate_features(current_instance) 30 | for step in range(10001): 31 | if (step % 100) == 0: 32 | results.append(dict( 33 | **coeff_features(current_instance), 34 | **solution_features(current_instance), 35 | **clp_simplex_performance(current_instance), 36 | pass_condition=pass_condition, 37 | step_change=step_change, 38 | step=step, seed=seed)) 39 | if (step % 2) == 0: 40 | new_instance = lp_row_neighbour(random_state, current_instance, 1) 41 | else: 42 | new_instance = lp_column_neighbour(random_state, current_instance, 1) 43 | new_features = calculate_features(new_instance) 44 | if condition(new_features): 45 | pass_condition += 1 46 | if objective(new_features) < objective(current_features): 47 | step_change += 1 48 | current_instance = new_instance 49 | current_features = new_features 50 | return results 51 | 52 | 53 | @cli_seeds 54 | def run(seed_values): 55 | ''' Generate the required number of instances and store feature results. ''' 56 | pool = multiprocessing.Pool() 57 | mapper = pool.imap_unordered 58 | print('Generating instances by naive search.') 59 | features = list(tqdm( 60 | mapper(generate_by_search, seed_values), 61 | total=len(seed_values), smoothing=0)) 62 | features = list(itertools.chain(*features)) 63 | with open('data/naive_search.json', 'w') as outfile: 64 | json.dump(features, outfile, indent=4, sort_keys=True) 65 | 66 | run() 67 | -------------------------------------------------------------------------------- /scripts/parameter-feature-relations.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%matplotlib inline\n", 10 | "import pandas as pd\n", 11 | "import seaborn as sns\n", 12 | "import matplotlib.pyplot as plt" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import json\n", 22 | "def save(name):\n", 23 | " plt.savefig(f'figures/{name}.pdf')" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "with open('data/parameterised_random.json') as infile:\n", 33 | " data = json.load(infile)" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "data[0]" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "df = pd.DataFrame([\n", 52 | " {\n", 53 | " 'param_rho': instance['params']['lhs_params']['density'],\n", 54 | " 'param_pv': instance['params']['lhs_params']['pv'],\n", 55 | " 'param_pc': instance['params']['lhs_params']['pc'],\n", 56 | " 'basis_split': instance['params']['beta_params']['basis_split'],\n", 57 | " 'beta_param': pd.np.log(instance['params']['alpha_params']['beta_param']),\n", 58 | " 'frac_violations': instance['params']['alpha_params']['frac_violations'],\n", 59 | " 'nonzeros': instance['nonzeros'],\n", 60 | " 'var_degree_max': instance['var_degree_max'],\n", 61 | " 'cons_degree_max': instance['cons_degree_max'],\n", 62 | " 'var_degree_min': instance['var_degree_min'],\n", 63 | " 'cons_degree_min': instance['cons_degree_min'],\n", 64 | " 'fractional_primal': instance['fractional_primal'],\n", 65 | " 'binding_constraints': instance['binding_constraints'],\n", 66 | " 'total_fractionality': instance['total_fractionality'],\n", 67 | " 'dual_iterations': instance['clp_dual_iterations'],\n", 68 | " 'primal_iterations': instance['clp_primal_iterations'],\n", 69 | " }\n", 70 | " for instance in data\n", 71 | "])" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "sns.pairplot(df,\n", 81 | " x_vars=['param_rho', 'param_pv', 'param_pc', 'basis_split', 'beta_param', 'frac_violations'],\n", 82 | " y_vars=[\n", 83 | " 'nonzeros', 'var_degree_min', 'var_degree_max', 'cons_degree_min', 'cons_degree_max',\n", 84 | " 'fractional_primal', 'binding_constraints', 'total_fractionality'])" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "metadata": {}, 90 | "source": [ 91 | "# Controlling min/max/mean\n", 92 | "\n", 93 | "The aim here is to show that rho controls number of nonzeros, pv/pc control variable and constraint skew *independently of density*.\n", 94 | "We also want parameters to be independent of size, hence the use of fractional values." 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "plt.scatter(x=df.param_rho, y=df.nonzeros)\n", 104 | "plt.xlabel('Density Parameter')\n", 105 | "plt.ylabel('Number of Nonzeros')\n", 106 | "save('density-parameter')" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "plt.scatter(x=df.param_rho, y=df.param_pc, c=df.nonzeros)" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": null, 121 | "metadata": {}, 122 | "outputs": [], 123 | "source": [ 124 | "plt.scatter(x=df.param_rho, y=df.param_pc, c=df.nonzeros)\n", 125 | "cbar = plt.colorbar()\n", 126 | "cbar.set_label('Number of Nonzeros')\n", 127 | "plt.xlabel('Density Parameter')\n", 128 | "plt.ylabel('PC Parameter')" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "plt.scatter(x=df.param_rho, y=df.param_pc, c=df.cons_degree_max - df.cons_degree_min)\n", 138 | "cbar = plt.colorbar()\n", 139 | "cbar.set_label('Max - Min Constraint Degree')\n", 140 | "plt.xlabel('Density Parameter')\n", 141 | "plt.ylabel('PC Parameter')\n", 142 | "save('pc-parameter')" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": null, 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [ 151 | "plt.scatter(x=df.param_rho, y=df.param_pc, c=df.cons_degree_min)\n", 152 | "cbar = plt.colorbar()\n", 153 | "cbar.set_label('Min Constraint Degree')\n", 154 | "plt.xlabel('Density Parameter')\n", 155 | "plt.ylabel('PC Parameter')" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "plt.scatter(x=df.param_rho, y=df.param_pv, c=df.var_degree_max)" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "plt.scatter(x=df.param_rho, y=df.param_pv, c=df.var_degree_min)" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "df.head()" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": null, 188 | "metadata": {}, 189 | "outputs": [], 190 | "source": [ 191 | "plt.scatter(x=df.basis_split, y=df.frac_violations, c=df.fractional_primal)" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": null, 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "plt.scatter(x=df.basis_split * df.frac_violations, y=df.beta_param, c=df.total_fractionality)" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "plt.scatter(x=df.basis_split, y=df.binding_constraints, c=df.primal_iterations)\n", 210 | "plt.colorbar()" 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": null, 216 | "metadata": {}, 217 | "outputs": [], 218 | "source": [ 219 | "plt.scatter(x=df.binding_constraints - df.basis_split * 50, y=df.primal_iterations, c=df.dual_iterations)" 220 | ] 221 | }, 222 | { 223 | "cell_type": "code", 224 | "execution_count": null, 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "difference = (df.binding_constraints - (df.basis_split * 50).round()).abs()\n", 229 | "plt.scatter(x=df.nonzeros, y=difference)\n", 230 | "plt.xlabel('Number of NonZeros')\n", 231 | "plt.ylabel('Difference between target and actual')\n", 232 | "(difference > 0).mean()" 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": null, 238 | "metadata": {}, 239 | "outputs": [], 240 | "source": [ 241 | "difference = (df.binding_constraints - (df.basis_split * 50).round()).abs()\n", 242 | "plt.scatter(x=difference, y=df.primal_iterations)\n", 243 | "plt.xlabel('Difference between target and actual')\n", 244 | "plt.ylabel('Primal iterations')\n", 245 | "(difference > 0).mean()" 246 | ] 247 | }, 248 | { 249 | "cell_type": "code", 250 | "execution_count": null, 251 | "metadata": {}, 252 | "outputs": [], 253 | "source": [ 254 | "solution_different = (df.binding_constraints - (df.basis_split * 50).round()).abs() > 0\n", 255 | "plot_data = pd.DataFrame({\n", 256 | " 'different': solution_different,\n", 257 | " 'nonzeros': pd.cut(df.nonzeros, bins=10).apply(lambda i: float((i.left + i.right) / 2))\n", 258 | "}).groupby('nonzeros').different.mean().reset_index().astype('float').set_index('nonzeros').different.plot()\n", 259 | "plt.xlabel('Number of Nonzeros')\n", 260 | "plt.ylabel('Pr (Solution Different)')" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": null, 266 | "metadata": {}, 267 | "outputs": [], 268 | "source": [ 269 | "solution_different = (df.binding_constraints - (df.basis_split * 50).round()).abs() > 0\n", 270 | "(\n", 271 | " pd.DataFrame({\n", 272 | " 'different': solution_different,\n", 273 | " 'nonzeros': df.nonzeros\n", 274 | " }).groupby('nonzeros').different.mean().sort_index()\n", 275 | " .rolling(150, center=True).mean().dropna()\n", 276 | ").plot()\n", 277 | "plt.xlabel('Number of Nonzeros')\n", 278 | "plt.ylabel('Pr (Solution Different)')\n", 279 | "save('pr-solution-different')" 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": null, 285 | "metadata": {}, 286 | "outputs": [], 287 | "source": [ 288 | "import statsmodels.api as sm" 289 | ] 290 | }, 291 | { 292 | "cell_type": "code", 293 | "execution_count": null, 294 | "metadata": {}, 295 | "outputs": [], 296 | "source": [ 297 | "model = sm.OLS(df['nonzeros'], df[['param_rho', 'param_pv', 'param_pc']])\n", 298 | "results = model.fit()\n", 299 | "results.summary()" 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": null, 305 | "metadata": {}, 306 | "outputs": [], 307 | "source": [ 308 | "model = sm.OLS(df['cons_degree_min'], df[['param_rho', 'param_pv', 'param_pc']])\n", 309 | "results = model.fit()\n", 310 | "results.summary()" 311 | ] 312 | }, 313 | { 314 | "cell_type": "code", 315 | "execution_count": null, 316 | "metadata": {}, 317 | "outputs": [], 318 | "source": [ 319 | "model = sm.OLS(df['cons_degree_max'], df[['param_rho', 'param_pv', 'param_pc']])\n", 320 | "results = model.fit()\n", 321 | "results.summary()" 322 | ] 323 | }, 324 | { 325 | "cell_type": "code", 326 | "execution_count": null, 327 | "metadata": {}, 328 | "outputs": [], 329 | "source": [] 330 | } 331 | ], 332 | "metadata": { 333 | "kernelspec": { 334 | "display_name": "Python 3", 335 | "language": "python", 336 | "name": "python3" 337 | }, 338 | "language_info": { 339 | "codemirror_mode": { 340 | "name": "ipython", 341 | "version": 3 342 | }, 343 | "file_extension": ".py", 344 | "mimetype": "text/x-python", 345 | "name": "python", 346 | "nbconvert_exporter": "python", 347 | "pygments_lexer": "ipython3", 348 | "version": "3.7.1" 349 | } 350 | }, 351 | "nbformat": 4, 352 | "nbformat_minor": 2 353 | } 354 | -------------------------------------------------------------------------------- /scripts/parameterised_performance_search.py: -------------------------------------------------------------------------------- 1 | 2 | import itertools 3 | import multiprocessing 4 | import json 5 | 6 | import click 7 | import numpy as np 8 | from tqdm import tqdm 9 | 10 | from lp_generators.writers import read_tar_encoded 11 | from lp_generators.features import coeff_features, solution_features 12 | from lp_generators.performance import clp_simplex_performance 13 | 14 | from search_operators import encoded_column_neighbour, encoded_row_neighbour 15 | from seeds import cli_seeds 16 | from performance_search_common import condition, objective, start_instance, calculate_features 17 | 18 | 19 | def generate_by_search(arg): 20 | seed, perf_field = arg 21 | results = [] 22 | pass_condition = 0 23 | step_change = 0 24 | random_state = np.random.RandomState(seed) 25 | current_instance = start_instance(random_state, perf_field) 26 | current_features = calculate_features(current_instance) 27 | for step in range(10001): 28 | if (step % 100) == 0: 29 | results.append(dict( 30 | **coeff_features(current_instance), 31 | **solution_features(current_instance), 32 | **clp_simplex_performance(current_instance), 33 | pass_condition=pass_condition, 34 | step_change=step_change, 35 | step=step, seed=seed)) 36 | if (step % 2) == 0: 37 | new_instance = encoded_row_neighbour(random_state, current_instance, 5) 38 | else: 39 | new_instance = encoded_column_neighbour(random_state, current_instance, 5) 40 | new_features = calculate_features(new_instance) 41 | if condition(new_features): 42 | pass_condition += 1 43 | if objective(new_features, perf_field) < objective(current_features, perf_field): 44 | step_change += 1 45 | current_instance = new_instance 46 | current_features = new_features 47 | return results 48 | 49 | 50 | @cli_seeds 51 | @click.option('--perf-field', type=str) 52 | def run(seed_values, perf_field): 53 | assert perf_field is not None 54 | ''' Generate the required number of instances and store feature results. ''' 55 | pool = multiprocessing.Pool() 56 | mapper = pool.imap_unordered 57 | print('Generating instances by parameterised search.') 58 | features = list(tqdm( 59 | mapper(generate_by_search, zip(seed_values, itertools.repeat(perf_field))), 60 | total=len(seed_values), smoothing=0)) 61 | features = list(itertools.chain(*features)) 62 | with open('data/parameterised_performance_search_{}.json'.format(perf_field), 'w') as outfile: 63 | json.dump(features, outfile, indent=4, sort_keys=True) 64 | 65 | run() 66 | -------------------------------------------------------------------------------- /scripts/parameterised_random.py: -------------------------------------------------------------------------------- 1 | ''' Command line script which generates instances using the constructor 2 | method and varying expected feature values uniformly. ''' 3 | 4 | import multiprocessing 5 | import json 6 | 7 | import numpy as np 8 | from tqdm import tqdm 9 | 10 | from lp_generators.lhs_generators import generate_lhs 11 | from lp_generators.solution_generators import generate_alpha, generate_beta 12 | from lp_generators.instance import EncodedInstance 13 | from lp_generators.features import coeff_features, solution_features 14 | from lp_generators.performance import clp_simplex_performance 15 | from lp_generators.utils import calculate_data, write_instance 16 | from lp_generators.writers import write_tar_encoded 17 | 18 | from seeds import cli_seeds 19 | 20 | 21 | @write_instance(write_tar_encoded, 'data/parameterised_random/inst_{seed}.tar') 22 | @calculate_data(coeff_features, solution_features, clp_simplex_performance) 23 | def generate(seed): 24 | ''' Generator distributing uniformly across parameters with fixed size. 25 | Feature values are attached to each instance by the calculate_data 26 | decorator. Instances are written to compressed format using the 27 | write_instance decorator so they can be loaded later as start points 28 | for search algorithms. ''' 29 | 30 | random_state = np.random.RandomState(seed) 31 | 32 | size_params = dict(variables=50, constraints=50) 33 | beta_params = dict( 34 | basis_split=random_state.uniform(low=0.0, high=1.0)) 35 | alpha_params = dict( 36 | frac_violations=random_state.uniform(low=0.0, high=1.0), 37 | beta_param=random_state.lognormal(mean=-0.2, sigma=1.8), 38 | mean_primal=0, 39 | std_primal=1, 40 | mean_dual=0, 41 | std_dual=1) 42 | lhs_params = dict( 43 | density=random_state.uniform(low=0.0, high=1.0), 44 | pv=random_state.uniform(low=0.0, high=1.0), 45 | pc=random_state.uniform(low=0.0, high=1.0), 46 | coeff_loc=random_state.uniform(low=-2.0, high=2.0), 47 | coeff_scale=random_state.uniform(low=0.1, high=1.0)) 48 | 49 | instance = EncodedInstance( 50 | lhs=generate_lhs(random_state=random_state, **size_params, **lhs_params).todense(), 51 | alpha=generate_alpha(random_state=random_state, **size_params, **alpha_params), 52 | beta=generate_beta(random_state=random_state, **size_params, **beta_params)) 53 | instance.data = dict(seed=seed, params=dict( 54 | size_params=size_params, beta_params=beta_params, 55 | alpha_params=alpha_params, lhs_params=lhs_params)) 56 | return instance 57 | 58 | 59 | @cli_seeds 60 | def run(seed_values): 61 | ''' Generate instances from the given seed values and store feature results. ''' 62 | pool = multiprocessing.Pool() 63 | mapper = pool.imap_unordered 64 | print('Generating fixed size parameterised instances.') 65 | instances = tqdm( 66 | mapper(generate, seed_values), 67 | total=len(seed_values), smoothing=0) 68 | features = [ 69 | instance.data 70 | for instance in instances 71 | ] 72 | with open('data/parameterised_random.json', 'w') as outfile: 73 | json.dump(features, outfile, indent=4, sort_keys=True) 74 | 75 | run() 76 | -------------------------------------------------------------------------------- /scripts/parameterised_search.py: -------------------------------------------------------------------------------- 1 | 2 | import itertools 3 | import multiprocessing 4 | import json 5 | 6 | import numpy as np 7 | from tqdm import tqdm 8 | 9 | from lp_generators.writers import read_tar_encoded 10 | from lp_generators.features import coeff_features, solution_features 11 | from lp_generators.performance import clp_simplex_performance 12 | 13 | from search_operators import encoded_column_neighbour, encoded_row_neighbour 14 | from seeds import cli_seeds 15 | from search_common import condition, objective, start_instance 16 | 17 | 18 | def calculate_features(instance): 19 | return dict( 20 | **coeff_features(instance), 21 | **solution_features(instance)) 22 | 23 | 24 | def generate_by_search(seed): 25 | results = [] 26 | pass_condition = 0 27 | step_change = 0 28 | random_state = np.random.RandomState(seed) 29 | current_instance = start_instance(random_state) 30 | current_features = calculate_features(current_instance) 31 | for step in range(10001): 32 | if (step % 100) == 0: 33 | results.append(dict( 34 | **coeff_features(current_instance), 35 | **solution_features(current_instance), 36 | **clp_simplex_performance(current_instance), 37 | pass_condition=pass_condition, 38 | step_change=step_change, 39 | step=step, seed=seed)) 40 | if (step % 2) == 0: 41 | new_instance = encoded_row_neighbour(random_state, current_instance, 1) 42 | else: 43 | new_instance = encoded_column_neighbour(random_state, current_instance, 1) 44 | new_features = calculate_features(new_instance) 45 | if condition(new_features): 46 | pass_condition += 1 47 | if objective(new_features) < objective(current_features): 48 | step_change += 1 49 | current_instance = new_instance 50 | current_features = new_features 51 | return results 52 | 53 | 54 | @cli_seeds 55 | def run(seed_values): 56 | ''' Generate the required number of instances and store feature results. ''' 57 | pool = multiprocessing.Pool() 58 | mapper = pool.imap_unordered 59 | print('Generating instances by parameterised search.') 60 | features = list(tqdm( 61 | mapper(generate_by_search, seed_values), 62 | total=len(seed_values), smoothing=0)) 63 | features = list(itertools.chain(*features)) 64 | with open('data/parameterised_search.json', 'w') as outfile: 65 | json.dump(features, outfile, indent=4, sort_keys=True) 66 | 67 | run() 68 | -------------------------------------------------------------------------------- /scripts/performance_search_common.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | 4 | import numpy as np 5 | 6 | from lp_generators.features import coeff_features, solution_features 7 | from lp_generators.performance import clp_simplex_performance 8 | from lp_generators.writers import read_tar_encoded 9 | 10 | 11 | with open('data/parameterised_random.json') as infile: 12 | naive_random_data = json.load(infile) 13 | 14 | 15 | def condition(data): 16 | return data['solvable'] is True 17 | 18 | 19 | def objective(data, perf_field): 20 | ''' default min objective -> this favours harder instances ''' 21 | return data[perf_field] * -1 22 | 23 | 24 | def _sample(rstate): 25 | while True: 26 | choice = rstate.choice(naive_random_data) 27 | if condition(choice): 28 | return choice 29 | 30 | 31 | def start_instance(rstate, perf_field): 32 | seed = min((_sample(rstate) for _ in range(200)), key=lambda d: objective(d, perf_field))['seed'] 33 | return read_tar_encoded('data/parameterised_random/inst_{seed}.tar'.format(seed=seed)) 34 | 35 | 36 | def calculate_features(instance): 37 | return dict( 38 | **solution_features(instance), 39 | **clp_simplex_performance(instance)) 40 | -------------------------------------------------------------------------------- /scripts/search_common.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | 4 | import numpy as np 5 | 6 | from lp_generators.writers import read_tar_lp 7 | 8 | 9 | with open('data/naive_random.json') as infile: 10 | naive_random_data = json.load(infile) 11 | 12 | 13 | def condition(data): 14 | return data['solvable'] is True 15 | 16 | 17 | def objective(data): 18 | return (data['rhs_mean'] + 100) ** 2 + (data['obj_mean'] - 100) ** 2 19 | 20 | 21 | def _sample(rstate): 22 | while True: 23 | choice = rstate.choice(naive_random_data) 24 | if condition(choice): 25 | return choice 26 | 27 | 28 | def start_instance(rstate): 29 | seed = min((_sample(rstate) for _ in range(20)), key=objective)['seed'] 30 | return read_tar_lp('data/naive_random/inst_{seed}.tar'.format(seed=seed)) 31 | -------------------------------------------------------------------------------- /scripts/search_operators.py: -------------------------------------------------------------------------------- 1 | ''' Modificaion operators should pick from the same distribution as the generators. ''' 2 | 3 | import itertools 4 | import numpy as np 5 | 6 | from lp_generators.instance import UnsolvedInstance, SolvedInstance, Solution 7 | from lp_generators.features import coeff_features, solution_features 8 | from lp_generators.lhs_generators import generate_lhs 9 | from lp_generators.solution_generators import generate_alpha 10 | 11 | 12 | def generate_rhs(variables, constraints, rhs_mean, rhs_std, random_state): 13 | return random_state.normal(loc=rhs_mean, scale=rhs_std, size=constraints) 14 | 15 | 16 | def generate_objective(variables, constraints, obj_mean, obj_std, random_state): 17 | return random_state.normal(loc=obj_mean, scale=obj_std, size=variables) 18 | 19 | 20 | def lhs_row(random_state, ncols): 21 | lhs_params = dict( 22 | density=random_state.uniform(low=0.0, high=1.0), 23 | pv=random_state.uniform(low=0.0, high=1.0), 24 | pc=random_state.uniform(low=0.0, high=1.0), 25 | coeff_loc=random_state.uniform(low=-2.0, high=2.0), 26 | coeff_scale=random_state.uniform(low=0.1, high=1.0)) 27 | lhs = generate_lhs( 28 | random_state=random_state, variables=ncols, constraints=2, 29 | **lhs_params).todense() 30 | return lhs[0] 31 | 32 | 33 | def lhs_col(random_state, nrows): 34 | lhs_params = dict( 35 | density=random_state.uniform(low=0.0, high=1.0), 36 | pv=random_state.uniform(low=0.0, high=1.0), 37 | pc=random_state.uniform(low=0.0, high=1.0), 38 | coeff_loc=random_state.uniform(low=-2.0, high=2.0), 39 | coeff_scale=random_state.uniform(low=0.1, high=1.0)) 40 | lhs = generate_lhs( 41 | random_state=random_state, variables=2, constraints=nrows, 42 | **lhs_params).todense() 43 | return lhs.transpose()[0].transpose() 44 | 45 | 46 | def lp_random_row(random_state, ncols): 47 | ''' Create a random row A_j with rhs b_j. ''' 48 | rhs_params = dict( 49 | rhs_mean=random_state.uniform(low=-100.0, high=100.0), 50 | rhs_std=random_state.uniform(low=1.0, high=30.0)) 51 | rhs = generate_rhs( 52 | random_state=random_state, variables=ncols, constraints=1, 53 | **rhs_params) 54 | return lhs_row(random_state, ncols), rhs[0] 55 | 56 | 57 | def lp_random_col(random_state, nrows): 58 | ''' Create a random column A_i with objective c_i. ''' 59 | objective_params = dict( 60 | obj_mean=random_state.uniform(low=-100.0, high=100.0), 61 | obj_std=random_state.uniform(low=1.0, high=10.0)) 62 | objective = generate_objective( 63 | random_state=random_state, variables=1, constraints=nrows, 64 | **objective_params) 65 | return lhs_col(random_state, nrows), objective[0] 66 | 67 | 68 | def lp_column_neighbour(rstate, instance, n_replace): 69 | ''' Create a neighbour by replacing a number of columns A_i, c_i 70 | with new random columns. ''' 71 | a, b, c = instance.lhs().copy(), instance.rhs().copy(), instance.objective().copy() 72 | inds = rstate.choice(instance.variables, n_replace, replace=False) 73 | for ind in inds: 74 | a_i, c_i = lp_random_col(rstate, instance.constraints) 75 | a[:, ind] = np.array([a_i]).transpose() 76 | c[ind] = c_i 77 | return UnsolvedInstance(lhs=a, rhs=b, objective=c) 78 | 79 | 80 | def lp_row_neighbour(rstate, instance, n_replace): 81 | ''' Create a neighbour by replacing a number of rows A_j, b_j 82 | with new random rows. ''' 83 | a, b, c = instance.lhs().copy(), instance.rhs().copy(), instance.objective().copy() 84 | inds = rstate.choice(instance.constraints, n_replace, replace=False) 85 | for ind in inds: 86 | a_j, b_j = lp_random_row(rstate, instance.variables) 87 | a[ind, :] = np.array([a_j]) 88 | b[ind] = b_j 89 | return UnsolvedInstance(lhs=a, rhs=b, objective=c) 90 | 91 | 92 | def solution_element(random_state): 93 | alpha_params = dict( 94 | frac_violations=random_state.uniform(low=0.0, high=1.0), 95 | beta_param=random_state.lognormal(mean=-0.2, sigma=1.8), 96 | mean_primal=0, 97 | std_primal=1, 98 | mean_dual=0, 99 | std_dual=1) 100 | alpha = generate_alpha( 101 | random_state=random_state, 102 | variables=1, constraints=0, 103 | **alpha_params) 104 | return alpha[0] 105 | 106 | 107 | def sp_random_row(rstate, ncols): 108 | A_j = lhs_row(rstate, ncols) 109 | alpha = solution_element(rstate) 110 | if rstate.uniform(0, 1) < 0.5: 111 | y_j, s_j = alpha, 0 112 | else: 113 | y_j, s_j = 0, alpha 114 | return A_j, y_j, s_j 115 | 116 | 117 | def sp_random_col(rstate, nrows): 118 | ''' Create a random column A_i with objective c_i. ''' 119 | A_i = lhs_col(rstate, nrows) 120 | alpha = solution_element(rstate) 121 | if rstate.uniform(0, 1) < 0.5: 122 | x_i, r_i = alpha, 0 123 | else: 124 | x_i, r_i = 0, alpha 125 | return A_i, x_i, r_i 126 | 127 | 128 | def encoded_column_neighbour(rstate, instance, n_replace): 129 | loc = rstate.uniform(1, 4) 130 | a, x, y, r, s = ( 131 | instance.lhs().copy(), 132 | instance.solution().x.copy(), 133 | instance.solution().y.copy(), 134 | instance.solution().r.copy(), 135 | instance.solution().s.copy()) 136 | inds = rstate.choice(instance.variables, n_replace, replace=False) 137 | for ind in inds: 138 | a_i, x_i, r_i = sp_random_col(rstate, instance.constraints) 139 | a[:, ind] = a_i 140 | x[ind] = x_i 141 | r[ind] = r_i 142 | basis = np.zeros(instance.variables + instance.constraints) 143 | return SolvedInstance(lhs=a, solution=Solution(x=x, y=y, r=r, s=s, basis=basis)) 144 | 145 | 146 | def encoded_row_neighbour(rstate, instance, n_replace): 147 | loc = rstate.uniform(1, 4) 148 | a, x, y, r, s = ( 149 | instance.lhs().copy(), 150 | instance.solution().x.copy(), 151 | instance.solution().y.copy(), 152 | instance.solution().r.copy(), 153 | instance.solution().s.copy()) 154 | inds = rstate.choice(instance.constraints, n_replace, replace=False) 155 | for ind in inds: 156 | a_j, y_j, s_j = sp_random_col(rstate, instance.variables) 157 | a[ind, :] = a_j.transpose() 158 | y[ind] = y_j 159 | s[ind] = s_j 160 | basis = np.zeros(instance.variables + instance.constraints) 161 | return SolvedInstance(lhs=a, solution=Solution(x=x, y=y, r=r, s=s, basis=basis)) 162 | -------------------------------------------------------------------------------- /scripts/seeds.py: -------------------------------------------------------------------------------- 1 | 2 | import functools 3 | import json 4 | 5 | import click 6 | 7 | from lp_generators.utils import system_random_seeds 8 | 9 | 10 | def cli_seeds(func): 11 | ''' Wrap a function taking a list of seed values with a cli command. 12 | Resulting cli command accepts either a JSON seed file or a count of 13 | system random seeds to generate. ''' 14 | 15 | @click.command() 16 | @click.option('--system-seeds', default=100, type=int, help='Number of system random seeds') 17 | @click.option('--seed-file', default=None, type=click.Path(exists=True), help='JSON seed file') 18 | @functools.wraps(func) 19 | def cli_seeds_fn(system_seeds, seed_file, **kwargs): 20 | if seed_file: 21 | with open(seed_file) as infile: 22 | seed_values = json.load(infile) 23 | else: 24 | seed_values = list(system_random_seeds(n=system_seeds, bits=32)) 25 | func(seed_values, **kwargs) 26 | 27 | return cli_seeds_fn 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | from setuptools import setup, find_packages, Extension 4 | 5 | try: 6 | import numpy as np 7 | from Cython.Build import cythonize 8 | import pkgconfig 9 | except ImportError: 10 | raise ImportError('Build process requires numpy, cython and pkconfig.') 11 | 12 | if not pkgconfig.exists('osi-clp'): 13 | raise ImportError('Building extension requires osi-clp discoverable by pkg-config') 14 | 15 | requirements = pkgconfig.parse('osi-clp') 16 | requirements['include_dirs'].append(np.get_include()) 17 | 18 | package_requires = [ 19 | 'numpy', 20 | 'scipy', 21 | ] 22 | 23 | extras_require = { 24 | 'tests': ['pytest', 'mock'], 25 | 'scripts': ['click', 'pandas', 'tqdm'], 26 | 'analysis': ['matplotlib', 'seaborn'], 27 | } 28 | 29 | extensions = cythonize(Extension( 30 | 'lp_generators_ext', language='c++', 31 | sources=['cpp/lp_generators_ext.pyx', 'cpp/lp.cpp'], 32 | extra_compile_args=['-std=c++11'], 33 | **requirements)) 34 | 35 | setup( 36 | name='lp_generators', 37 | version='0.1.0', 38 | description='Generation of MIP test cases by LP relaxation.', 39 | url='https://github.com/simonbowly/lp-generators', 40 | author='Simon Bowly', 41 | author_email='simon.bowly@gmail.com', 42 | license='MIT', 43 | classifiers=[ 44 | 'Development Status :: 4 - Beta', 45 | 'Intended Audience :: Science/Research', 46 | 'Intended Audience :: Education', 47 | 'License :: OSI Approved :: MIT License', 48 | 'Programming Language :: Python :: 3', 49 | 'Programming Language :: Cython', 50 | 'Programming Language :: C++', 51 | 'Topic :: Scientific/Engineering :: Mathematics'], 52 | packages=['lp_generators'], 53 | package_dir={'lp_generators': 'lp_generators'}, 54 | install_requires=package_requires, 55 | extras_require=extras_require, 56 | ext_modules=extensions) 57 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonbowly/lp-generators/937c44074c234333b6a5408c3e18f498c2205948/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_constructor.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | import numpy as np 4 | 5 | from lp_generators.instance import EncodedInstance, Solution, SolvedInstance, UnsolvedInstance 6 | from .testing import assert_approx_equal 7 | 8 | 9 | @pytest.fixture 10 | def variables(): 11 | return 5 12 | 13 | 14 | @pytest.fixture 15 | def constraints(): 16 | return 4 17 | 18 | 19 | @pytest.fixture 20 | def lhs_matrix(): 21 | return np.matrix([ 22 | [ 0.42, 0.61, 0.06, 0.01, 0.49], 23 | [ 0.74, 0.12, 0.57, 0.23, 0.23], 24 | [ 0.78, 0.92, 0.67, 0.32, 0.64], 25 | [ 0.02, 0.67, 0.2 , 0.45, 0.39]]) 26 | 27 | 28 | @pytest.fixture 29 | def alpha_vector(): 30 | return np.array([ 0.93, 0.99, 0.52, 0.54, 0.12, 0.55, 0.44, 0.13, 0.04]) 31 | 32 | 33 | @pytest.fixture 34 | def beta_vector(): 35 | return np.array([0, 1, 0, 1, 0, 0, 1, 1, 0]) 36 | 37 | 38 | @pytest.fixture 39 | def rhs_vector(): 40 | return np.array([ 0.6093, 0.683 , 1.2136, 0.9063]) 41 | 42 | 43 | @pytest.fixture 44 | def objective_vector(): 45 | return np.array([-0.6982, 0.3623, -0.479 , 0.0235, 0.1651]) 46 | 47 | 48 | @pytest.fixture 49 | def solution(beta_vector): 50 | return Solution( 51 | x=np.array([ 0, 0.99, 0, 0.54, 0]), 52 | r=np.array([0.93, 0, 0.52, 0, 0.12]), 53 | y=np.array([0.55, 0, 0, 0.04]), 54 | s=np.array([ 0, 0.44, 0.13, 0]), 55 | basis=beta_vector) 56 | 57 | 58 | @pytest.fixture(params=['encoded', 'solved', 'unsolved']) 59 | def instance(request, lhs_matrix, alpha_vector, beta_vector, solution, rhs_vector, objective_vector): 60 | if request.param == 'encoded': 61 | return EncodedInstance(lhs=lhs_matrix, alpha=alpha_vector, beta=beta_vector) 62 | elif request.param == 'solved': 63 | return SolvedInstance(lhs=lhs_matrix, solution=solution) 64 | elif request.param == 'unsolved': 65 | return UnsolvedInstance(lhs=lhs_matrix, rhs=rhs_vector, objective=objective_vector) 66 | 67 | 68 | def test_size(instance, variables, constraints): 69 | assert instance.variables == variables 70 | assert instance.constraints == constraints 71 | 72 | 73 | def test_alpha(instance, alpha_vector): 74 | assert_approx_equal(instance.alpha(), alpha_vector) 75 | 76 | 77 | def test_beta(instance, beta_vector): 78 | assert_approx_equal(instance.beta(), beta_vector) 79 | 80 | 81 | def test_lhs(instance, lhs_matrix): 82 | assert_approx_equal(instance.lhs(), lhs_matrix) 83 | 84 | 85 | def test_solution(instance, solution): 86 | result = instance.solution() 87 | assert_approx_equal(result.x, solution.x) 88 | assert_approx_equal(result.y, solution.y) 89 | assert_approx_equal(result.r, solution.r) 90 | assert_approx_equal(result.s, solution.s) 91 | assert np.all(result.basis == solution.basis) 92 | 93 | 94 | def test_rhs(instance, rhs_vector): 95 | assert_approx_equal(instance.rhs(), rhs_vector) 96 | 97 | 98 | def test_objective(instance, objective_vector): 99 | assert_approx_equal(instance.objective(), objective_vector) 100 | -------------------------------------------------------------------------------- /tests/test_encode_decode.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import pytest 4 | 5 | from lp_generators.instance import EncodedInstance, UnsolvedInstance 6 | from .testing import random_encoded, assert_approx_equal 7 | 8 | 9 | def encode(unsolved_instance): 10 | return EncodedInstance( 11 | lhs=unsolved_instance.lhs(), 12 | alpha=unsolved_instance.alpha(), 13 | beta=unsolved_instance.beta()) 14 | 15 | 16 | @pytest.mark.parametrize('encoded_instance', [ 17 | random_encoded(3, 5), 18 | random_encoded(5, 3), 19 | random_encoded(50, 30), 20 | random_encoded(30, 50), 21 | ]) 22 | def test_encode_decode(encoded_instance): 23 | 24 | # Decoding 25 | unsolved_instance = UnsolvedInstance( 26 | lhs=encoded_instance.lhs(), 27 | rhs=encoded_instance.rhs(), 28 | objective=encoded_instance.objective()) 29 | assert_approx_equal(unsolved_instance.lhs(), encoded_instance.lhs()) 30 | assert_approx_equal(unsolved_instance.alpha(), encoded_instance.alpha()) 31 | assert np.all(unsolved_instance.beta() == encoded_instance.beta()) 32 | 33 | # Encoding 34 | encoded_instance = encode(unsolved_instance) 35 | assert_approx_equal(encoded_instance.lhs(), unsolved_instance.lhs()) 36 | assert_approx_equal(encoded_instance.rhs(), unsolved_instance.rhs()) 37 | assert_approx_equal( 38 | encoded_instance.objective(), unsolved_instance.objective()) 39 | 40 | 41 | def test_decode_infeasible(): 42 | unsolved_instance = UnsolvedInstance( 43 | lhs=np.array([[1.0, 1.0], [-1.0, -1.0]]), 44 | rhs=np.array([1.0, -2.0]), 45 | objective=np.array([1.0, 1.0])) 46 | with pytest.raises(ValueError): 47 | encoded_instance = encode(unsolved_instance) 48 | 49 | 50 | def test_decode_unbounded(): 51 | unsolved_instance = UnsolvedInstance( 52 | lhs=np.array([[-1.0, -1.0]]), 53 | rhs=np.array([-1.0]), 54 | objective=np.array([1.0, 1.0])) 55 | with pytest.raises(ValueError): 56 | encoded_instance = encode(unsolved_instance) 57 | -------------------------------------------------------------------------------- /tests/test_features.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | import numpy as np 4 | 5 | import lp_generators.features as features 6 | from lp_generators.instance import UnsolvedInstance 7 | 8 | #TODO test serialisable 9 | 10 | @pytest.fixture 11 | def unsolved_instance(): 12 | return UnsolvedInstance( 13 | lhs=np.matrix([ 14 | [ 0.42, 0.61, 0.06, 0.01, 0.49], 15 | [ 0.74, 0.12, 0.57, 0, 0.23], 16 | [ 0.78, 0.92, 0, 0.32, 0.64], 17 | [ 0.02, 0.67, 0.2 , 0.45, 0.39]]), 18 | rhs=np.array([ 0.6093, 0.683 , 1.2136, 0.9063]), 19 | objective=np.array([-0.6982, 0.3623, -0.479 , 0.0235, 0.1651])) 20 | 21 | 22 | def test_coeff_features(unsolved_instance): 23 | result = features.coeff_features(unsolved_instance) 24 | assert result['variables'] == 5 25 | assert result['constraints'] == 4 26 | assert result['nonzeros'] == 18 27 | assert abs(result['coefficient_density'] - 0.9) < 10 ** -5 28 | assert abs(result['lhs_std'] - 0.26874651878308059) < 10 ** -5 29 | assert abs(result['lhs_mean'] - 0.42444444444444446) < 10 **- 5 30 | assert abs(result['rhs_std'] - 0.23513981479111529) < 10 ** -5 31 | assert abs(result['rhs_mean'] - 0.85304999999999997) < 10 ** -5 32 | assert abs(result['obj_std'] - 0.39938589158857379) < 10 ** -5 33 | assert abs(result['obj_mean'] - -0.12525999999999998) < 10 ** -5 34 | assert result['cons_degree_min'] == 4 35 | assert result['cons_degree_max'] == 5 36 | assert result['var_degree_min'] == 3 37 | assert result['var_degree_max'] == 4 38 | 39 | 40 | def test_degree_seq(unsolved_instance): 41 | var_degree, cons_degree = features.degree_seq(unsolved_instance.lhs()) 42 | assert np.all(var_degree == [4, 4, 3, 3, 4]) 43 | assert np.all(cons_degree == [5, 4, 4, 5]) 44 | -------------------------------------------------------------------------------- /tests/test_lp_ext.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | import numpy as np 5 | import pytest 6 | 7 | from lp_generators.lp_ext import LPCy 8 | from lp_generators.utils import temp_file_path 9 | 10 | 11 | @pytest.fixture 12 | def matrices(): 13 | ''' n, m, A, b, c for canonical form primal problem ''' 14 | return ( 15 | 5, 4, 16 | np.array([ 17 | [1,0,2,0,1], 18 | [0,1,0,1,0], 19 | [1,-1,0,1,0], 20 | [0,0,-1,1,0]], dtype=np.float), 21 | np.array([1, 2, 3, 4], dtype=np.float), 22 | np.array([1, 2, 3, 4, 5], dtype=np.float)) 23 | 24 | 25 | @pytest.fixture 26 | def model(matrices): 27 | model = LPCy() 28 | model.construct_dense_canonical(*matrices) 29 | return model 30 | 31 | 32 | @pytest.fixture 33 | def easy_model(): 34 | model = LPCy() 35 | model.construct_dense_canonical( 36 | 2, 2, np.array([[1, 3], [3, 1]], dtype=np.float), 37 | np.array([4, 4], dtype=np.float), 38 | np.array([1, 1], dtype=np.float)) 39 | return model 40 | 41 | 42 | def test_construct(matrices, model): 43 | n, m, A, b, c = matrices 44 | assert np.all(model.get_dense_lhs() == A) 45 | assert np.all(model.get_rhs() == b) 46 | assert np.all(model.get_obj() == c) 47 | 48 | 49 | def test_solve(easy_model): 50 | easy_model.solve() 51 | assert easy_model.get_solution_status() == 0 52 | assert np.all(easy_model.get_solution_primals() == [1, 1]) 53 | assert np.all(easy_model.get_solution_slacks() == [0, 0]) 54 | assert np.all(easy_model.get_solution_reduced_costs() == [0, 0]) 55 | assert np.all(easy_model.get_solution_duals() == [.25, .25]) 56 | assert np.all(easy_model.get_solution_basis() == [1, 1, 0, 0]) 57 | 58 | 59 | def test_write_lp(easy_model): 60 | with temp_file_path('.mps.gz') as file_path: 61 | easy_model.write_mps(file_path) 62 | assert os.path.exists(file_path) 63 | 64 | 65 | def test_write_ip(easy_model): 66 | with temp_file_path('.mps.gz') as file_path: 67 | easy_model.write_mps_ip(file_path) 68 | assert os.path.exists(file_path) 69 | -------------------------------------------------------------------------------- /tests/test_neighbours_common.py: -------------------------------------------------------------------------------- 1 | 2 | from unittest import mock 3 | 4 | import pytest 5 | import numpy as np 6 | 7 | from lp_generators.instance import EncodedInstance 8 | import lp_generators.neighbours_common as neighbours 9 | 10 | 11 | @pytest.fixture 12 | def rstate(): 13 | rstate_mock = mock.MagicMock() 14 | return rstate_mock 15 | 16 | 17 | @pytest.fixture 18 | def lhs(): 19 | return np.array([[1, 0, 1], [1, 1, 0]], dtype=np.float) 20 | 21 | 22 | @pytest.fixture 23 | def alpha(): 24 | return np.array([1, 1, 1, 1, 1], dtype=np.float) 25 | 26 | 27 | @pytest.fixture 28 | def beta(): 29 | return np.array([1, 0, 0, 1, 0]) 30 | 31 | 32 | @pytest.fixture 33 | def lhs_empty(): 34 | return np.array([[0, 0, 0], [0, 0, 0]], dtype=np.float) 35 | 36 | 37 | @pytest.fixture 38 | def lhs_full(): 39 | return np.array([[1, 1, 1], [1, 1, 1]], dtype=np.float) 40 | 41 | 42 | def test_basis_exchange(beta, rstate): 43 | rstate.choice.side_effect = [1, 0] 44 | result = np.copy(beta) 45 | neighbours._exchange_basis(result, rstate, count=1) 46 | assert rstate.choice.call_count == 2 47 | assert np.all(rstate.choice.call_args_list[0][0][0] == [1, 2, 4]) 48 | assert np.all(rstate.choice.call_args_list[1][0][0] == [0, 3]) 49 | assert np.all(result == [0, 1, 0, 1, 0]) 50 | 51 | 52 | @pytest.mark.parametrize('dist', ['normal', 'lognormal']) 53 | def test_scale_vector_value(alpha, rstate, dist): 54 | rstate.choice.return_value = 3 55 | rstate.normal.return_value = -1.2 56 | rstate.lognormal.return_value = 1.5 57 | result = np.copy(alpha) 58 | neighbours._scale_vector_entry( 59 | result, rstate, mean='mean', sigma='sigma', dist=dist, count=1) 60 | rstate.choice.assert_called_once_with(5) 61 | if dist == 'normal': 62 | rstate.normal.assert_called_once_with(loc='mean', scale='sigma') 63 | assert np.all(result == [1, 1, 1, -1.2, 1]) 64 | elif dist == 'lognormal': 65 | rstate.lognormal.assert_called_once_with(mean='mean', sigma='sigma') 66 | assert np.all(result == [1, 1, 1, 1.5, 1]) 67 | else: 68 | raise ValueError() 69 | 70 | 71 | def test_remove_lhs_entry(lhs, rstate): 72 | rstate.choice.return_value = 2 73 | result = np.copy(lhs) 74 | neighbours._remove_lhs_entry(result, rstate, count=1) 75 | rstate.choice.assert_called_once_with(4) 76 | assert np.all(result == [[1, 0, 1], [0, 1, 0]]) 77 | 78 | 79 | def test_add_lhs_entry(lhs, rstate): 80 | rstate.choice.return_value = 1 81 | rstate.normal.return_value = 1.5 82 | result = np.copy(lhs) 83 | neighbours._add_lhs_entry(result, rstate, 'mean', 'sigma', count=1) 84 | rstate.normal.assert_called_once_with(loc='mean', scale='sigma') 85 | assert np.all(result == [[1, 0, 1], [1, 1, 1.5]]) 86 | 87 | 88 | def test_scale_lhs_entry(lhs, rstate): 89 | rstate.choice.return_value = 1 90 | rstate.normal.return_value = 1.5 91 | result = np.copy(lhs) 92 | neighbours._scale_lhs_entry(result, rstate, 'mean', 'sigma', count=1) 93 | rstate.normal.assert_called_once_with(loc='mean', scale='sigma') 94 | assert np.all(result == [[1, 0, 1.5], [1, 1, 0]]) 95 | 96 | 97 | def test_empty_remove(lhs_empty): 98 | ''' Edge cases where the lhs matrix has no entries. ''' 99 | result = np.copy(lhs_empty) 100 | neighbours._remove_lhs_entry(lhs_empty, np.random, count=1) 101 | assert np.all(result == lhs_empty) 102 | 103 | 104 | def test_empty_scale(lhs_empty): 105 | ''' Edge cases where the lhs matrix has no entries. ''' 106 | result = np.copy(lhs_empty) 107 | neighbours._scale_lhs_entry(lhs_empty, np.random, 'mean', 'sigma', count=1) 108 | assert np.all(result == lhs_empty) 109 | 110 | 111 | def test_full_add(lhs_full): 112 | ''' Edge cases where the lhs matrix has all nonzero entries. ''' 113 | result = np.copy(lhs_full) 114 | neighbours._add_lhs_entry(lhs_full, np.random, 'mean', 'sigma', count=1) 115 | assert np.all(result == lhs_full) 116 | 117 | 118 | RANDOM_MESSAGE = 'Random-valued test failed. This is expected occasionally.' 119 | 120 | 121 | @pytest.mark.parametrize('count', range(1, 10)) 122 | def test_repeat_scale_vector_entry(count): 123 | input_vec = np.random.random(1000) 124 | result_vec = np.copy(input_vec) 125 | neighbours._scale_vector_entry(result_vec, np.random, 0, 1, 'normal', count=count) 126 | assert np.sum(input_vec != result_vec) == count, RANDOM_MESSAGE 127 | 128 | 129 | @pytest.mark.parametrize('count', range(1, 10)) 130 | def test_repeat_scale_lhs_entry(count): 131 | input_vec = np.random.random((100, 100)) 132 | result_vec = np.copy(input_vec) 133 | neighbours._scale_lhs_entry(result_vec, np.random, 0, 1, count=count) 134 | assert np.sum(input_vec != result_vec) == count, RANDOM_MESSAGE 135 | 136 | 137 | @pytest.mark.parametrize('count', range(1, 10)) 138 | def test_repeat_remove_lhs_entry(count): 139 | input_vec = np.array(np.random.random((100, 100)) > 0.5, dtype=np.float) 140 | result_vec = np.copy(input_vec) 141 | neighbours._remove_lhs_entry(result_vec, np.random, count=count) 142 | assert np.sum(input_vec != 0) - np.sum(result_vec != 0) == count, RANDOM_MESSAGE 143 | 144 | 145 | @pytest.mark.parametrize('count', range(1, 10)) 146 | def test_repeat_remove_lhs_entry(count): 147 | input_vec = np.array(np.random.random((100, 100)) > 0.5, dtype=np.float) 148 | result_vec = np.copy(input_vec) 149 | neighbours._add_lhs_entry(result_vec, np.random, 0, 1, count=count) 150 | assert np.sum(result_vec != 0) - np.sum(input_vec != 0) == count, RANDOM_MESSAGE 151 | -------------------------------------------------------------------------------- /tests/test_writers.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | import pytest 5 | 6 | from lp_generators.instance import EncodedInstance, UnsolvedInstance 7 | from lp_generators.writers import ( 8 | write_mps, write_mps_ip, 9 | write_tar_encoded, read_tar_encoded, 10 | write_tar_lp, read_tar_lp) 11 | from lp_generators.utils import temp_file_path 12 | from .testing import random_encoded, assert_approx_equal 13 | 14 | 15 | @pytest.mark.parametrize('instance', [ 16 | random_encoded(3, 5), 17 | random_encoded(5, 3)]) 18 | def test_write_mps(instance): 19 | with temp_file_path('.mps.gz') as file_path: 20 | write_mps(instance, file_path) 21 | assert os.path.exists(file_path) 22 | 23 | 24 | @pytest.mark.parametrize('instance', [ 25 | random_encoded(3, 5), 26 | random_encoded(5, 3)]) 27 | def test_write_mps_ip(instance): 28 | with temp_file_path('.mps.gz') as file_path: 29 | write_mps_ip(instance, file_path) 30 | assert os.path.exists(file_path) 31 | 32 | 33 | @pytest.mark.parametrize('instance', [ 34 | random_encoded(3, 5), 35 | random_encoded(5, 3)]) 36 | def test_read_write_tar_encoded(instance): 37 | with temp_file_path() as file_path: 38 | write_tar_encoded(instance, file_path) 39 | assert os.path.exists(file_path) 40 | read_instance = read_tar_encoded(file_path) 41 | assert isinstance(read_instance, EncodedInstance) 42 | assert_approx_equal(instance.lhs(), read_instance.lhs()) 43 | assert_approx_equal(instance.alpha(), read_instance.alpha()) 44 | assert_approx_equal(instance.beta(), read_instance.beta()) 45 | 46 | 47 | @pytest.mark.parametrize('instance', [ 48 | random_encoded(3, 5), 49 | random_encoded(5, 3)]) 50 | def test_read_write_tar_lp(instance): 51 | with temp_file_path() as file_path: 52 | write_tar_lp(instance, file_path) 53 | assert os.path.exists(file_path) 54 | read_instance = read_tar_lp(file_path) 55 | assert isinstance(read_instance, UnsolvedInstance) 56 | assert_approx_equal(instance.lhs(), read_instance.lhs()) 57 | assert_approx_equal(instance.rhs(), read_instance.rhs()) 58 | assert_approx_equal(instance.objective(), read_instance.objective()) 59 | -------------------------------------------------------------------------------- /tests/testing.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | 4 | from lp_generators.instance import EncodedInstance 5 | 6 | 7 | EQ_TOLERANCE = 10 ** -10 8 | 9 | 10 | def assert_approx_equal(m1, m2): 11 | assert np.all(np.abs(m1 - m2) < EQ_TOLERANCE) 12 | 13 | 14 | def random_encoded(variables, constraints): 15 | A = np.random.random((constraints, variables)) 16 | alpha = np.random.random(variables + constraints) 17 | beta = np.zeros(variables + constraints) 18 | basis = np.random.choice( 19 | variables + constraints, 20 | size=constraints, replace=False) 21 | beta[basis] = 1 22 | return EncodedInstance(lhs=A, alpha=alpha, beta=beta) 23 | --------------------------------------------------------------------------------