├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE.md ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── experiments ├── __init__.py ├── inheritance │ ├── __init__.py │ └── inheritance_experiments.py └── tsp │ ├── __init__.py │ ├── tsp_experiments.py │ └── tsp_experiments.zip ├── optimizn ├── __init__.py ├── ab_split │ ├── opt_split.py │ ├── opt_split2.py │ ├── opt_split_dp.py │ └── testing │ │ ├── cluster_hw.py │ │ └── test_opt_cases.py ├── combinatorial │ ├── __init__.py │ ├── algorithms │ │ ├── __init__.py │ │ ├── binpacking │ │ │ ├── __init__.py │ │ │ └── bnb_binpacking.py │ │ ├── knapsack │ │ │ ├── __init__.py │ │ │ └── bnb_knapsack.py │ │ ├── min_path_cover │ │ │ ├── __init__.py │ │ │ ├── bnb_min_path_cover.py │ │ │ └── sim_anneal_min_path_cover.py │ │ ├── suitcase_reshuffle │ │ │ ├── __init__.py │ │ │ ├── bnb_suitcasereshuffle.py │ │ │ ├── sim_anneal_suitcase_reshuffle.py │ │ │ └── suitcases.py │ │ └── traveling_salesman │ │ │ ├── __init__.py │ │ │ ├── bnb_tsp.py │ │ │ ├── city_graph.py │ │ │ └── sim_anneal_tsp.py │ ├── branch_and_bound.py │ ├── opt_problem.py │ ├── simulated_annealing.py │ └── toy_problems.txt ├── continuous_training │ ├── __init__.py │ └── continuous_training.py ├── lagrange_examples.py ├── problems │ ├── __init__.py │ ├── binary_assignment.py │ ├── int_allocn.py │ └── sample_ci.py ├── reinforcement_learning │ ├── __init__.py │ └── multi_armed_bandit.py ├── samples │ └── lagrange_examples.py ├── trees │ └── pprnt.py └── utils.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py └── combinatorial ├── __init__.py └── algorithms ├── __init__.py ├── binpacking ├── __init__.py └── test_bnb_binpacking.py ├── check_sol_utils.py ├── knapsack ├── __init__.py └── test_bnb_knapsack.py ├── min_path_cover ├── __init__.py ├── edges1.csv ├── edges2.csv ├── test_bnb_min_path_cover.py └── test_sim_anneal_min_path_cover.py ├── suitcase_reshuffle ├── __init__.py ├── test_bnb_suitcase_reshuffle.py └── test_sim_anneal_suitcase_reshuffle.py └── traveling_salesman ├── __init__.py ├── test_bnb_tsp.py ├── test_city_graph.py └── test_sim_anneal_tsp.py /.gitignore: -------------------------------------------------------------------------------- 1 | Data/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to 4 | agree to a Contributor License Agreement (CLA) declaring that you have the right to, 5 | and actually do, grant us the rights to use your contribution. For details, visit 6 | https://cla.microsoft.com. 7 | 8 | When you submit a pull request, a CLA-bot will automatically determine whether you need 9 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the 10 | instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 11 | 12 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 13 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 14 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation. 2 | 3 | MIT License 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. -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | This library's simulated annealing algorithm and implementation for the traveling salesman problem is based on the simulated annealing code from [https://github.com/toddwschneider/shiny-salesman/blob/master/helpers.R](https://github.com/toddwschneider/shiny-salesman/blob/master/helpers.R), which is licensed under the MIT License. The original text of the license is shown below. 2 | 3 | ``` 4 | MIT License 5 | 6 | Copyright (c) 2017 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ``` 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # optimizn 2 | This Python library provides several optimization-related utilities that can be used to solve a variety of optimization problems. 3 | 4 | ## Trademarks 5 | 6 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft’s Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party’s policies. 7 | 8 | ## Getting Started 9 | This library is available for use on PyPI here: [https://pypi.org/project/optimizn/](https://pypi.org/project/optimizn/) 10 | 11 | For local development, do the following. 12 | - Clone this repository. 13 | - Set up and activate a Python3 virtual environment using `conda`. More info here: [https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-with-commands](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-with-commands) 14 | - Navigate to the `optimizn` repo. 15 | - Run the command: `pip install requirements.txt` to install the dependencies in the conda virtual environment. 16 | - Run the command: `python3 setup.py install` to install the package in the conda virtual environment. 17 | - As development progresses, run the above command to update the build in the conda virtual environment. 18 | - To run the unit tests, run the command: `pytest` 19 | 20 | ## Offerings 21 | 22 | ### Combinatorial optimization 23 | 24 | #### Simulated Annealing 25 | This library offers a generalizeable implementation for simulated annealing through a superclass called `SimAnnealProblem`, which can be imported like so: `from optimizn.combinatorial.simulated_annealing import SimAnnealProblem`. 26 | 27 | To use simulated annealing for their own optimization problem, users should create a subclass specific to their optimization problem that extends `SimAnnealProblem`. The subclass must implement the following methods. 28 | - `get_initial_solution` (required): provides an initial solution 29 | - `reset_candidate` (optional): resets the current solution, defaults to `get_initial_solution` but can be overridden 30 | - `next_candidate` (required): produces a neighboring solution, given a current solution 31 | - `get_temperature` (optional): returns a temperature value (lower/higher value means lower/higher chances of updating current solution to a less optimal solution) given a number of iterations, defaults to $\frac{4000}{1 + e^{x / 3000}}$ but can be overridden 32 | - `cost` (required): objective function, returns a cost value for a given solution (lower cost value means more optimal solution) 33 | - `cost_delta` (optional): default is the difference between two cost values, can be changed based on the nature of the problem 34 | 35 | The simulated annealing algorithm can be run using the inherited `anneal` method of the subclass. Through the arguments to this function, the user can specify the number of iterations (`n_iter`, defaults to 10000), the reset probability (`reset_p`, defaults to 1/10000), the interval (number of iterations) for logging progress (`log_iters`, defaults to 10000), and time limit of the algorithm in seconds (`time_limit`, defaults to 3600). 36 | 37 | #### Branch and Bound 38 | This library offers a generalizeable implementation for branch and bound through a superclass called `BnBProblem` which can be imported like so: `from optimizn.combinatorial.branch_and_bound import BnBProblem`. 39 | 40 | This superclass supports two types of branch and bound. The first type is traditional branch and bound, where partial solutions are not checked against the current best solution. The second type is look-ahead branch and bound, where partial solutions are completed and checked against the current best solution. 41 | 42 | To use branch and bound for their own optimization problem, users should create a subclass specific to their optimization problem that extends `BnBProblem`. The subclass must implement the following methods. 43 | - `get_initial_solution` (optional): provides an initial solution, defaults to returning None but can be overridden 44 | - `get_root` (required): provides the root solution, from which other solutions are obtainable via branching 45 | - `branch` (required): produces other solutions from a current solution, which correspond to subproblems with additional constraints and constrained solution spaces 46 | - `cost` (required): objective function, returns a cost value for a given solution (lower cost value means more optimal solution) 47 | - `lbound` (required): returns the lowest cost value for a given solution and all other solutions in the same constrained solution space (lower cost value means more optimal solution) 48 | - `is_feasible` (required): returns True if a given solution is feasible, False if not 49 | - `complete_solution` (required/optional): completes a partial solution (required for modified branch and bound, optional for traditional branch and bound) 50 | - `cost_delta` (optional): default is the difference between two cost values, can be changed based on the nature of the problem 51 | 52 | When calling the constructor of the `BnBProblem` class, a selection strategy must be provided, using the `BnBSelectionStrategy` enum, which can be imported like so: `from optimizn.combinatorial.branch_and_bound import BnBSelectionStrategy`. There are three supported selection strategies: depth-first (`BnBSelectionStrategy.DEPTH_FIRST`, where the branch and bound algorithm selects and evaluates nodes in a depth-first-search manner), depth-first-best-first (`BnBSelectionStrategy.DEPTH_FIRST_BEST_FIRST`, where the algorithm selects and evaluates nodes in a depth-first-search manner, prioritizes lower bound for nodes of the same depth in the tree), or best-first-depth-first (`BnBSelectionStrategy.BEST_FIRST_DEPTH_FIRST`, where the algorithm selects and evaluates the node with the lowest lower bound, prioritizes depth in tree for nodes with the same lower bound). 53 | 54 | The branch and bound algorithm can be run using the inherited `solve` method of the subclass. Through the arguments to this function, the user can specify the number of iterations (`iters_limit`, defaults to 1e6), the interval (number of iterations) for logging progress (`log_iters`, defaults to 100), the time limit of the algorithm in seconds (`time_limit`, defaults to 3600), and the type of branch and bound (`bnb_type`, 0 for traditional, 1 for look-ahead, defaults to 0). 55 | 56 | #### Continuous Training 57 | Both the `SimAnnealProblem` and the `BnBProblem` extend a superclass called `OptProblem` that has a `persist` method, the default implementation of which saves optimization problem resources to three folders: `DailyObj` (contains the optimization problem parameters), `DailyOpt` (contains instances of the optimization problem class each corresponding to a single run of the optimization algorithm), and `GlobalOpt` (contains the instance of the optimization problem class with the most optimal solution seen across all runs of the optimization algorithm). The `persist` method can be overridden by the user. When calling either the `SimAnnealProblem` or `BnBProblem` constructor in their subclass, the user must specify the problem parameters through the `params` argument in order for their problem parameters, problem instances, and optimal solutions to be saved by continuous training. 58 | 59 | These saved optimization problem resources can be loaded from these directories and the optimization algorithms can be run again, continuing from where they left off in their previous runs. For the default implementation of `persist`, a corresponding function called `load_latest_pckl` is provided to load optimization problem resources given the path to the desired folder (`DailyObj`, `DailyOpt`, or `GlobalOpt`). The `load_latest_pckl` function can be imported like so: `from optimizn.combinatorial.opt_problem import load_latest_pckl`. 60 | 61 | Continuous training allows optimization algorithms to find the most optimal solutions they can given the compute time and resources they have, even if they are available in disjoint intervals. 62 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | For help and questions about using this project, please reach out to the owners of this project. 10 | 11 | ## Microsoft Support Policy 12 | 13 | Support for this project is limited to the resources listed above. 14 | -------------------------------------------------------------------------------- /experiments/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /experiments/inheritance/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /experiments/inheritance/inheritance_experiments.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | ''' 5 | Since problem classes are going to be inherited, trying some experiments with 6 | multiple inheritance. 7 | 8 | Experiment code is from the following source: 9 | 10 | (1) 11 | Title: How does Python's super() work with multiple inheritance? 12 | Author: Callisto (author of question), rbp (author of answer) 13 | Editor: Mateen Ulhaq (editor of question), Neuron (editor of answer) 14 | URL: https://stackoverflow.com/questions/3277367/how-does-pythons-super-work-with-multiple-inheritance 15 | Date published: July 18, 2010 16 | Date edited: April 17, 2022 (question edited), August 30, 2021 (answer edited) 17 | Date accessed: March 27, 2023 18 | ''' 19 | class First(object): 20 | def __init__(self): 21 | print("First(): entering") 22 | super(First, self).__init__() 23 | print("First(): exiting") 24 | 25 | def other(self): 26 | print("first other called") 27 | 28 | class Second(object): 29 | def __init__(self): 30 | print("Second(): entering") 31 | super(Second, self).__init__() 32 | print("Second(): exiting") 33 | 34 | def other2(self): 35 | print("Another other") 36 | 37 | class Third(First, Second): 38 | def __init__(self): 39 | print("Third(): entering") 40 | super(Third, self).__init__() 41 | print("Third(): exiting") 42 | 43 | def other(self): 44 | super().other() 45 | 46 | 47 | def tst_inher(): 48 | th = Third() 49 | th.other() 50 | 51 | -------------------------------------------------------------------------------- /experiments/tsp/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /experiments/tsp/tsp_experiments.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | from optimizn.combinatorial.algorithms.traveling_salesman.city_graph\ 5 | import CityGraph 6 | from optimizn.combinatorial.algorithms.traveling_salesman.sim_anneal_tsp\ 7 | import TravSalsmn 8 | from optimizn.combinatorial.algorithms.traveling_salesman.bnb_tsp\ 9 | import TravelingSalesmanProblem 10 | from python_tsp.heuristics import solve_tsp_simulated_annealing 11 | from optimizn.combinatorial.opt_problem import load_latest_pckl 12 | import time 13 | import shutil 14 | import os 15 | import pickle 16 | from optimizn.combinatorial.branch_and_bound import BnBSelectionStrategy 17 | 18 | 19 | # specify maximum number of iterations 20 | MAX_ITERS = int(1e20) # very high bound on iterations 21 | # since algorithms should use up all compute time 22 | 23 | # specify number of iterations for logging (optimizn) 24 | LOG_ITERS = int(1e7) 25 | 26 | 27 | # function to clear all previous experiment data 28 | def clear_previous_data(): 29 | # clear previous continuous training data 30 | if os.path.isdir('Data/'): 31 | shutil.rmtree(path='Data/') 32 | print('Cleared previous continuous training data') 33 | else: 34 | print('No previous continuous training data found') 35 | 36 | # clear previous city graph object 37 | if os.path.isfile('city_graph.obj'): 38 | os.remove('city_graph.obj') 39 | print('Cleared previous city graph object') 40 | else: 41 | print('No previous city graph object found') 42 | 43 | # clear previous experiment results object 44 | if os.path.isfile('tsp_exp_results.obj'): 45 | os.remove('tsp_exp_results.obj') 46 | print('Cleared previous experiment results dictionary') 47 | else: 48 | print('No previous experiment results dictionary found') 49 | 50 | print('Cleared previous experiment data') 51 | 52 | 53 | # function to clear previous continuous training data 54 | def _clear_cont_train_data(opt_prob_obj): 55 | # clear continuous training data from previous runs 56 | if os.path.isdir(f'Data/{opt_prob_obj.name}'): 57 | shutil.rmtree(path=f'Data/{opt_prob_obj.name}') 58 | print('Cleared previous continuous training data for optimization ' 59 | + f'problem class with name {opt_prob_obj.name}') 60 | else: 61 | print('No previous continuous training data for optimization ' 62 | + f'problem class with name {opt_prob_obj.name}') 63 | 64 | 65 | # function to get experiment graph and results dictionary 66 | def get_exp_data(num_cities): 67 | # create/load experiment graph 68 | if os.path.isfile('city_graph.obj'): 69 | city_graph_file = open('city_graph.obj', 'rb') 70 | city_graph = pickle.load(city_graph_file) 71 | city_graph_file.close() 72 | print('Loaded saved city graph') 73 | else: 74 | city_graph = CityGraph(num_cities) 75 | city_graph_file = open('city_graph.obj', 'wb') 76 | pickle.dump(city_graph, city_graph_file) 77 | city_graph_file.close() 78 | print('Created and saved new city graph') 79 | 80 | # create/load experiment results dictionary 81 | if os.path.isfile('tsp_exp_results.obj'): 82 | exp_results_file = open('tsp_exp_results.obj', 'rb') 83 | exp_results = pickle.load(exp_results_file) 84 | exp_results_file.close() 85 | print('Loaded saved experiment results dictionary') 86 | else: 87 | exp_results = dict() 88 | print('Created new experiment results dictionary') 89 | return city_graph, exp_results 90 | 91 | 92 | # function to save experiment results dictionary 93 | def save_exp_results(exp_results): 94 | exp_results_file = open('tsp_exp_results.obj', 'wb') 95 | pickle.dump(exp_results, exp_results_file) 96 | exp_results_file.close() 97 | print('Saved experiment results dictionary') 98 | 99 | 100 | # function to run optimizn simulated annealing (single stretch) 101 | def run_o_sa1(city_graph, results, compute_time_mins, num_trials, reset_p): 102 | tsp_o_sa1 = TravSalsmn(city_graph) 103 | results['o_sa1'] = [tsp_o_sa1.init_cost] 104 | tsp_o_sa1.anneal(n_iter=MAX_ITERS, reset_p=reset_p, 105 | time_limit=compute_time_mins * 60 * num_trials, 106 | log_iters=LOG_ITERS) 107 | results['o_sa1'].append(tsp_o_sa1.best_cost) 108 | 109 | 110 | # function to run optimizn simulated annealing (continuous training) 111 | def run_o_sa2(city_graph, results, compute_time_mins, num_trials, reset_p): 112 | tsp_o_sa2 = TravSalsmn(city_graph) 113 | results['o_sa2'] = [tsp_o_sa2.init_cost] 114 | _clear_cont_train_data(tsp_o_sa2) 115 | tsp_o_sa2.anneal(n_iter=MAX_ITERS, reset_p=reset_p, 116 | time_limit=compute_time_mins * 60, log_iters=LOG_ITERS) 117 | tsp_o_sa2.persist() 118 | results['o_sa2'].append(tsp_o_sa2.best_cost) 119 | for _ in range(num_trials - 1): 120 | class_name = tsp_o_sa2.name 121 | prior_params = load_latest_pckl( 122 | path1=f'Data/{class_name}/DailyObj', logger=tsp_o_sa2.logger) 123 | if tsp_o_sa2.params == prior_params: 124 | tsp_o_sa2 = load_latest_pckl( 125 | path1=f'Data/{class_name}/DailyOpt', logger=tsp_o_sa2.logger) 126 | if tsp_o_sa2 is None: 127 | raise Exception( 128 | 'No saved instance for TSP simulated annealing') 129 | else: 130 | raise Exception('TSP simulated annealing parameters have changed') 131 | tsp_o_sa2.anneal(n_iter=MAX_ITERS, reset_p=reset_p, 132 | time_limit=compute_time_mins * 60, 133 | log_iters=LOG_ITERS) 134 | tsp_o_sa2.persist() 135 | results['o_sa2'].append(tsp_o_sa2.best_cost) 136 | 137 | 138 | # function to run python-tsp simulated annealing (single stretch) 139 | def run_pt_sa1(city_graph, results, compute_time_mins, num_trials): 140 | permutation = list(range(city_graph.num_cities)) 141 | opt_permutation = permutation 142 | opt_dist = 0 143 | for i in range(1, len(permutation)): 144 | opt_dist += city_graph.dists[permutation[i], permutation[i-1]] 145 | opt_dist += city_graph.dists[ 146 | permutation[0], permutation[len(permutation) - 1]] 147 | results['pt_sa1'] = [opt_dist] 148 | print(f'Initial solution: {permutation}') 149 | print(f'Initial solution cost: {opt_dist}') 150 | s = time.time() 151 | e = time.time() 152 | while (e - s) < (compute_time_mins * 60 * num_trials): 153 | permutation, distance = solve_tsp_simulated_annealing( 154 | city_graph.dists, 155 | max_processing_time=( 156 | compute_time_mins * 60 * num_trials) - (e - s), 157 | alpha=0.99, x0=permutation, perturbation_scheme='ps2') 158 | if opt_dist > distance: 159 | opt_dist = distance 160 | opt_permutation = permutation 161 | e = time.time() 162 | results['pt_sa1'].append(opt_dist) 163 | print(f'Best solution: {opt_permutation}') 164 | print(f'Best solution cost: {opt_dist}') 165 | 166 | 167 | # run python-tsp simulated annealing (successive runs) 168 | def run_pt_sa2(city_graph, results, compute_time_mins, num_trials): 169 | permutation = list(range(city_graph.num_cities)) 170 | opt_permutation = permutation 171 | opt_dist = 0 172 | for i in range(1, len(permutation)): 173 | opt_dist += city_graph.dists[permutation[i], permutation[i-1]] 174 | opt_dist += city_graph.dists[ 175 | permutation[0], permutation[len(permutation) - 1]] 176 | results['pt_sa2'] = [opt_dist] 177 | print(f'Initial solution: {permutation}') 178 | print(f'Initial solution cost: {opt_dist}') 179 | s = time.time() 180 | e = time.time() 181 | while (e - s) < (compute_time_mins * 60): 182 | permutation, distance = solve_tsp_simulated_annealing( 183 | city_graph.dists, 184 | max_processing_time=(compute_time_mins * 60) - (e - s), 185 | alpha=0.99, x0=permutation, perturbation_scheme='ps2') 186 | if opt_dist > distance: 187 | opt_dist = distance 188 | opt_permutation = permutation 189 | e = time.time() 190 | results['pt_sa2'].append(opt_dist) 191 | print(f'Best solution: {opt_permutation}') 192 | print(f'Best solution cost: {opt_dist}') 193 | for _ in range(num_trials - 1): 194 | s = time.time() 195 | e = time.time() 196 | while (e - s) < (compute_time_mins * 60): 197 | permutation, distance = solve_tsp_simulated_annealing( 198 | city_graph.dists, 199 | max_processing_time=(compute_time_mins * 60) - (e - s), 200 | alpha=0.99, x0=permutation, perturbation_scheme='ps2') 201 | if opt_dist > distance: 202 | opt_permutation = permutation 203 | opt_dist = distance 204 | e = time.time() 205 | results['pt_sa2'].append(opt_dist) 206 | print(f'Best solution: {opt_permutation}') 207 | print(f'Best solution cost: {opt_dist}') 208 | 209 | 210 | # function to run optimizn modified branch and bound (single stretch) 211 | def run_mod_bnb1(city_graph, results, compute_time_mins, num_trials, 212 | bnb_selection_strategy): 213 | if bnb_selection_strategy == BnBSelectionStrategy.DEPTH_FIRST: 214 | alg_name = 'df_mod_bnb1' 215 | elif bnb_selection_strategy == BnBSelectionStrategy.DEPTH_FIRST_BEST_FIRST: 216 | alg_name = 'df_bef_mod_bnb1' 217 | elif bnb_selection_strategy == BnBSelectionStrategy.BEST_FIRST_DEPTH_FIRST: 218 | alg_name = 'bef_df_mod_bnb1' 219 | mod_bnb1 = TravelingSalesmanProblem( 220 | {'input_graph': city_graph}, bnb_selection_strategy) 221 | results[f'{alg_name}'] = [mod_bnb1.init_cost] 222 | mod_bnb1.solve(iters_limit=MAX_ITERS, log_iters=LOG_ITERS, 223 | time_limit=compute_time_mins * 60 * num_trials, 224 | bnb_type=1) 225 | results[f'{alg_name}'].append(mod_bnb1.best_cost) 226 | 227 | 228 | # function to run optimizn modified branch and bound (continuous training) 229 | def run_mod_bnb2(city_graph, results, compute_time_mins, num_trials, 230 | bnb_selection_strategy): 231 | if bnb_selection_strategy == BnBSelectionStrategy.DEPTH_FIRST: 232 | alg_name = 'df_mod_bnb2' 233 | elif bnb_selection_strategy == BnBSelectionStrategy.DEPTH_FIRST_BEST_FIRST: 234 | alg_name = 'df_bef_mod_bnb2' 235 | elif bnb_selection_strategy == BnBSelectionStrategy.BEST_FIRST_DEPTH_FIRST: 236 | alg_name = 'bef_df_mod_bnb2' 237 | mod_bnb2 = TravelingSalesmanProblem( 238 | {'input_graph': city_graph}, bnb_selection_strategy) 239 | results[f'{alg_name}'] = [mod_bnb2.init_cost] 240 | _clear_cont_train_data(mod_bnb2) 241 | mod_bnb2.solve(iters_limit=MAX_ITERS, log_iters=LOG_ITERS, 242 | time_limit=compute_time_mins * 60, bnb_type=1) 243 | mod_bnb2.persist() 244 | results[f'{alg_name}'].append(mod_bnb2.best_cost) 245 | for _ in range(num_trials - 1): 246 | class_name = mod_bnb2.name 247 | prior_params = load_latest_pckl( 248 | path1=f'Data/{class_name}/DailyObj', logger=mod_bnb2.logger) 249 | if mod_bnb2.params == prior_params: 250 | mod_bnb2 = load_latest_pckl( 251 | path1=f'Data/{class_name}/DailyOpt', logger=mod_bnb2.logger) 252 | if mod_bnb2 is None: 253 | raise Exception( 254 | 'No saved instance for TSP modified branch ' 255 | + 'and bound') 256 | else: 257 | raise Exception( 258 | 'TSP modified branch and bound parameters have changed') 259 | mod_bnb2.solve(iters_limit=MAX_ITERS, log_iters=LOG_ITERS, 260 | time_limit=compute_time_mins * 60, bnb_type=1) 261 | mod_bnb2.persist() 262 | results[f'{alg_name}'].append(mod_bnb2.best_cost) 263 | 264 | 265 | # function to run optimizn traditional branch and bound (single stretch) 266 | def run_trad_bnb1(city_graph, results, compute_time_mins, num_trials, 267 | bnb_selection_strategy): 268 | if bnb_selection_strategy == BnBSelectionStrategy.DEPTH_FIRST: 269 | alg_name = 'df_trad_bnb1' 270 | elif bnb_selection_strategy == BnBSelectionStrategy.DEPTH_FIRST_BEST_FIRST: 271 | alg_name = 'df_bef_trad_bnb1' 272 | elif bnb_selection_strategy == BnBSelectionStrategy.BEST_FIRST_DEPTH_FIRST: 273 | alg_name = 'bef_df_trad_bnb1' 274 | trad_bnb1 = TravelingSalesmanProblem( 275 | {'input_graph': city_graph}, bnb_selection_strategy) 276 | results[f'{alg_name}'] = [trad_bnb1.init_cost] 277 | trad_bnb1.solve(iters_limit=MAX_ITERS, log_iters=LOG_ITERS, 278 | time_limit=compute_time_mins * 60 * num_trials, 279 | bnb_type=0) 280 | results[f'{alg_name}'].append(trad_bnb1.best_cost) 281 | 282 | 283 | # function to run optimizn traditional branch and bound (continuous training) 284 | def run_trad_bnb2(city_graph, results, compute_time_mins, num_trials, 285 | bnb_selection_strategy): 286 | if bnb_selection_strategy == BnBSelectionStrategy.DEPTH_FIRST: 287 | alg_name = 'df_trad_bnb2' 288 | elif bnb_selection_strategy == BnBSelectionStrategy.DEPTH_FIRST_BEST_FIRST: 289 | alg_name = 'df_bef_trad_bnb2' 290 | elif bnb_selection_strategy == BnBSelectionStrategy.BEST_FIRST_DEPTH_FIRST: 291 | alg_name = 'bef_df_trad_bnb2' 292 | trad_bnb2 = TravelingSalesmanProblem( 293 | {'input_graph': city_graph}, bnb_selection_strategy) 294 | results[f'{alg_name}'] = [trad_bnb2.init_cost] 295 | _clear_cont_train_data(trad_bnb2) 296 | trad_bnb2.solve(iters_limit=MAX_ITERS, log_iters=LOG_ITERS, 297 | time_limit=compute_time_mins * 60, bnb_type=0) 298 | trad_bnb2.persist() 299 | results[f'{alg_name}'].append(trad_bnb2.best_cost) 300 | for _ in range(num_trials - 1): 301 | class_name = trad_bnb2.name 302 | prior_params = load_latest_pckl( 303 | path1=f'Data/{class_name}/DailyObj', logger=trad_bnb2.logger) 304 | if trad_bnb2.params == prior_params: 305 | trad_bnb2 = load_latest_pckl( 306 | path1=f'Data/{class_name}/DailyOpt', logger=trad_bnb2.logger) 307 | if trad_bnb2 is None: 308 | raise Exception( 309 | 'No saved instance for TSP traditional branch ' 310 | + 'and bound') 311 | else: 312 | raise Exception( 313 | 'TSP traditional branch and bound parameters have changed') 314 | trad_bnb2.solve(iters_limit=MAX_ITERS, log_iters=LOG_ITERS, 315 | time_limit=compute_time_mins * 60) 316 | trad_bnb2.persist() 317 | results[f'{alg_name}'].append(trad_bnb2.best_cost) 318 | -------------------------------------------------------------------------------- /experiments/tsp/tsp_experiments.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/optimizn/6a489e10136c8dbfb91bbe7bf561d76d0e4faf6a/experiments/tsp/tsp_experiments.zip -------------------------------------------------------------------------------- /optimizn/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /optimizn/ab_split/opt_split.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from ppbtree import print_tree 3 | from copy import deepcopy 4 | from optimizn.ab_split.opt_split_dp import isSubsetSum 5 | from optimizn.trees.pprnt import display 6 | 7 | 8 | class Node1(): 9 | def __init__(self, key): 10 | self.key = key 11 | self.val = key 12 | self.left = None 13 | self.right = None 14 | 15 | def __str__(self): 16 | return str(self.val) 17 | 18 | def __repr__(self): 19 | return str(self.val) 20 | 21 | 22 | class Tree(): 23 | def __init__(self, arr, mat, sum1=-1): 24 | """ 25 | The mat is a dynamic programming matrix. 26 | """ 27 | if sum1 < 0: 28 | sum1 = len(mat[0])-1 29 | self.arr = arr 30 | self.mat = mat 31 | self.path = None 32 | self.root = self.mk_tree(len(arr)-1, sum1) 33 | 34 | def mk_tree(self, ro, col): 35 | if col < 0 or ro < -1 or not self.mat[ro+1][col]: 36 | return 37 | node1 = Node1(1) 38 | node1.right = self.mk_tree(ro-1, col) 39 | node1.left = self.mk_tree(ro-1, col - self.arr[ro]) 40 | return node1 41 | 42 | def find_1path(self, node, depth=0, path=[]): 43 | # You have to reverse the array and then pick out the indices. 44 | if depth > len(self.arr): 45 | self.path = deepcopy(path) 46 | return 47 | if node is None: 48 | return 49 | # This is only appending depths. The array will need to be reversed 50 | # and indexed by these depths. 51 | path.append(depth) 52 | self.find_1path(node.left, depth+1, path) 53 | path.pop() 54 | self.find_1path(node.right, depth+1, path) 55 | 56 | 57 | def unionTrees(t1, t2): 58 | if (not t1): 59 | return t2 60 | if (not t2): 61 | return t1 62 | t1.left = unionTrees(t1.left, t2.left) 63 | t1.right = unionTrees(t1.right, t2.right) 64 | return t1 65 | 66 | 67 | def intrsctTrees(t1, t2): 68 | if not t1 or not t2: 69 | return 70 | t1.left = intrsctTrees(t1.left, t2.left) 71 | t1.right = intrsctTrees(t1.right, t2.right) 72 | return t1 73 | 74 | 75 | def optimize(arrs): 76 | matrices = [] 77 | sums = [] 78 | trees = [] 79 | for arr in arrs: 80 | sum1 = np.sum(arr) 81 | matr = create_matr(arr, sum1+6) 82 | tree1 = Tree(arr, matr, sum1//2) 83 | sums.append(sum1//2) 84 | matrices.append(matr) 85 | trees.append(tree1) 86 | tree1 = deepcopy(trees[0]) 87 | # Build a no-compromise tree by taking intersections. 88 | for ix in range(1, len(trees)): 89 | tree = trees[ix] 90 | tree1.root = intrsctTrees(tree1.root, tree.root) 91 | 92 | # If we found a complete path without any compromise, 93 | # return it. 94 | tree1.find_1path(tree1.root) 95 | path1 = tree1.path 96 | if path1 is not None: 97 | return path1 98 | # If no path was found, we'll have to start 99 | # making compromises. 100 | deltas = [-1, 1, -2, 2, -3, 3, -4, 4, -5, 5] 101 | for delta in deltas: 102 | for ix in range(len(trees)): 103 | tree = intrsctAllTrees(trees, ix, delta) 104 | arr = arrs[ix] 105 | matr = matrices[ix] 106 | sum1 = sums[ix] 107 | if matr[len(arr)][sum1+delta]: 108 | tree2 = Tree(arr, matr, sum1+delta) 109 | tree2.root = intrsctTrees(tree2.root, tree.root) 110 | tree2.find_1path(tree2.root) 111 | path1 = tree2.path 112 | if path1 is not None: 113 | return path1 114 | 115 | 116 | def create_matr(arr=[3, 34, 4, 12, 5, 2], sum1=9): 117 | n = len(arr) 118 | matr = isSubsetSum(arr, n, sum1) 119 | return matr 120 | 121 | 122 | def intrsctAllTrees(trees, ix, delta): 123 | if ix == 0: 124 | tree1 = deepcopy(trees[1]) 125 | else: 126 | tree1 = deepcopy(trees[0]) 127 | # Build a no-compromise tree by taking intersections. 128 | for ix1 in range(len(trees)): 129 | if ix1 != ix: 130 | tree = trees[ix1] 131 | tree1.root = intrsctTrees(tree1.root, tree.root) 132 | return tree1 133 | 134 | 135 | def tst2(): 136 | arrs = [[2, 5, 9, 3, 1], 137 | [2, 3, 4, 4, 3]] 138 | path1 = optimize(arrs) 139 | assert path1[0] == 0 140 | assert path1[1] == 2 141 | print(path1) 142 | arrs = [[2, 4, 7, 9], 143 | [1, 2, 3, 2], 144 | [4, 7, 5, 2]] 145 | path1 = optimize(arrs) 146 | assert path1[0] == 1 147 | assert path1[1] == 3 148 | print(path1) 149 | arrs = [[7, 0, 7, 0], 150 | [0, 5, 0, 4]] 151 | path1 = optimize(arrs) 152 | assert path1[0] == 0 153 | assert path1[1] == 3 154 | print(path1) 155 | 156 | 157 | def tst1(): 158 | arr = [3, 34, 4, 12, 5, 2] 159 | sum = 9 160 | n = len(arr) 161 | matr = isSubsetSum(arr, n, sum) 162 | tr = Tree(arr, matr) 163 | display(tr.root) 164 | tr.find_1path(tr.root) 165 | print(tr.path) 166 | print_tree(tr.root) 167 | print("###########") 168 | arr = [3, 4, 5, 2] 169 | sum = 6 170 | n = len(arr) 171 | matr = isSubsetSum(arr, n, sum) 172 | tr1 = Tree(arr, matr) 173 | display(tr1.root) 174 | tr1.find_1path(tr1.root) 175 | print(tr1.path) 176 | print("###########") 177 | unionTrees(tr.root, tr1.root) 178 | display(tr.root) 179 | 180 | 181 | # Driver code 182 | if __name__ == '__main__': 183 | tst2() 184 | -------------------------------------------------------------------------------- /optimizn/ab_split/opt_split2.py: -------------------------------------------------------------------------------- 1 | from optimizn.ab_split.opt_split import Node1, Tree, \ 2 | form_arrays, create_matr 3 | import numpy as np 4 | from copy import deepcopy 5 | import queue 6 | from optimizn.ab_split.testing.cluster_hw import df1 7 | 8 | 9 | class Tree1(Tree): 10 | """ 11 | The arrays per hardware are very sparse. They have a few non 12 | zero entries and lots of zero entries. Creating the tree for 13 | these kinds of arrays makes it explode since it doesn't matter 14 | which group the zero entries go to. To mitigate this issue, we 15 | can construct a combined tree across all dp matrices in one shot 16 | instead of one dp matrix at a time and then taking intersection. 17 | """ 18 | def __init__(self, arrays, matrices, targets): 19 | self.arrays = arrays 20 | self.matrices = matrices 21 | self.targets = targets 22 | self.cols = deepcopy(targets) 23 | self.n = len(matrices) 24 | self.n_clstrs = len(self.matrices[0]) 25 | self.stop = False 26 | self.path1 = [] 27 | path = [] 28 | self.root = self.mk_tree(ro=self.n_clstrs-2, 29 | cols=self.cols, path1=path) 30 | 31 | def mk_tree(self, ro, cols, path1=[]): 32 | if self.stop: 33 | return 34 | if ro < -1: 35 | print(path1) 36 | self.path1 = deepcopy(path1) 37 | self.stop = True 38 | return 39 | # print(str(ro)+",") 40 | for i in range(self.n): 41 | mat = self.matrices[i] 42 | col = cols[i] 43 | if not mat[ro+1][col] or col < 0: 44 | return 45 | node1 = Node1(ro) 46 | cnt = 0 47 | for i in range(self.n): 48 | mat = self.matrices[i] 49 | col = cols[i] 50 | if mat[ro+1-1][col]: 51 | cnt += 1 52 | if cnt == self.n: 53 | node1.right = self.mk_tree(ro-1, cols, path1) 54 | cnt = 0 55 | col_deltas = np.zeros(self.n).astype(int) 56 | for i in range(self.n): 57 | mat = self.matrices[i] 58 | col = cols[i] 59 | arr = self.arrays[i] 60 | if col - arr[ro] >= 0 and \ 61 | mat[ro+1-1][col - arr[ro]]: 62 | cnt += 1 63 | col_deltas[i] = arr[ro] 64 | if cnt == self.n: 65 | cols = cols - col_deltas 66 | path1.append(ro) 67 | node1.left = self.mk_tree(ro-1, cols, path1) 68 | path1.pop() 69 | cols = cols + col_deltas 70 | return node1 71 | 72 | def find_1path(self, node, depth=0, path=...): 73 | return super().find_1path(node, depth, path) 74 | 75 | 76 | class OptProblm(): 77 | """ 78 | Does data cleaning of the array, removes the zeros, etc. 79 | """ 80 | def __init__(self): 81 | # This file path isn't used since we import the dataframe 82 | # from a pandas file. 83 | self.arrays, self.hws_ix, self.cl_ix =\ 84 | form_arrays(df1) 85 | # Remove arrays where total nodes less than 10 and ones 86 | # where only one cluster has the hardware since splitting 87 | # by cluster in those cases won't make sense. 88 | self.arrays = self.arrays[np.sum(self.arrays, axis=1) > 10] 89 | self.arrays = self.arrays[np.sum(self.arrays != 0, axis=1) > 1] 90 | # Now we remove the clusters (columns) where its all zeros. 91 | self.mask = np.sum(self.arrays, axis=0) != 0 92 | self.arrays2 = [] 93 | for arr in self.arrays: 94 | self.arrays2.append(arr[self.mask]) 95 | self.arrays = self.arrays2 96 | self.path1 = optimize3(self.arrays) 97 | 98 | def optimize(self): 99 | tr = Tree1(self.arrays, self.matrices, self.targets) 100 | self.tree = tr 101 | 102 | 103 | def optimize1(arrays): 104 | matrices = [] 105 | targets = [] 106 | target_cands = [] 107 | for arr in arrays: 108 | sum1 = np.sum(arr) 109 | matr = create_matr(arr, sum1) 110 | last_ro = matr[len(matr)-1] 111 | all_trgts = np.arange(len(matr[0]))[last_ro] 112 | target = sum1//2 113 | (all_trgts - target) 114 | target_cands.append(all_trgts) 115 | for x in range(sum1//2-1): 116 | if last_ro[sum1//2-x]: 117 | target = sum1//2-x 118 | break 119 | if last_ro[sum1//2+x]: 120 | target = sum1//2+x 121 | break 122 | targets.append(target) 123 | matrices.append(matr) 124 | tr = Tree1(arrays, matrices, targets) 125 | return tr.path1 126 | 127 | 128 | def optimize3(arrays): 129 | matrices = [] 130 | targets = [] 131 | target_cands = [] 132 | for arr in arrays: 133 | sum1 = np.sum(arr) 134 | matr = create_matr(arr, sum1) 135 | last_ro = matr[len(matr)-1] 136 | all_trgts = np.arange(len(matr[0]))[last_ro] 137 | target = sum1//2 138 | deltas = (all_trgts - target)**2 139 | deltainds = deltas.argsort() 140 | all_trgts = all_trgts[deltainds[::1]] 141 | target_cands.append(all_trgts) 142 | target = all_trgts[0] 143 | targets.append(target) 144 | matrices.append(matr) 145 | # for trgt in itr_arrays(target_cands): 146 | # tr = Tree1(arrays, matrices, trgt) 147 | # if len(tr.path1) > 0: 148 | # return tr.path1 149 | op = OptProblem2(arrays, matrices, target_cands) 150 | # op.itr_arrays() 151 | op.itr_arrays_bfs() 152 | return op.path1 153 | 154 | 155 | def itr_arrays(arrays, lvl=0, targets=[]): 156 | """ 157 | Given an array of arrays (could be jagged), 158 | All possible arrays formed by taking one entry 159 | from each of the arrays. 160 | """ 161 | if lvl == len(arrays): 162 | yield targets 163 | for xx in arrays[lvl]: 164 | targets.append(xx) 165 | itr_arrays(arrays, lvl+1, targets) 166 | targets.pop() 167 | 168 | 169 | class OptProblem2(): 170 | def __init__(self, arrays, matrices, target_cands): 171 | self.arrays = arrays 172 | self.matrices = matrices 173 | self.target_cands = target_cands 174 | self.stop_looking = False 175 | 176 | def itr_arrays(self, lvl=0, targets=[]): 177 | """ 178 | Given an array of arrays (could be jagged), 179 | All possible arrays formed by taking one entry 180 | from each of the arrays. This one should be used 181 | when we have a strong priority order between the arrays. 182 | The first array in the list of arrays has the highest priority 183 | the second one has the second highest and so on. 184 | """ 185 | if self.stop_looking: 186 | return 187 | if lvl == len(self.target_cands): 188 | tr = Tree1(self.arrays, self.matrices, targets) 189 | if len(tr.path1) > 0: 190 | self.path1 = tr.path1 191 | # Could have used yield here as well. 192 | self.stop_looking = True 193 | return 194 | for xx in self.target_cands[lvl]: 195 | if not self.stop_looking: 196 | targets.append(xx) 197 | self.itr_arrays(lvl+1, targets) 198 | targets.pop() 199 | 200 | def itr_arrays_bfs(self): 201 | q = queue.Queue() 202 | u1 = np.zeros(len(self.target_cands)).astype(int) 203 | init_arr = [self.target_cands[ix][0] for ix in u1] 204 | q.put(u1) 205 | while q and not self.stop_looking: 206 | u = q.get() 207 | u_arr = self.ix_arr_to_arr(u) 208 | if u_arr is not None: 209 | tr = Tree1(self.arrays, self.matrices, u_arr) 210 | if len(tr.path1) > 0: 211 | self.path1 = tr.path1 212 | self.stop_looking = True 213 | break 214 | dists = [] 215 | vs = [] 216 | for ix in range(len(self.target_cands)): 217 | delta = np.zeros(len(self.target_cands)).astype(int) 218 | delta[ix] = 1 219 | v1 = u + delta 220 | v = self.ix_arr_to_arr(v1) 221 | if v is not None: 222 | dist = manhattan_dist(init_arr, v) 223 | dists.append(dist) 224 | vs.append(v1) 225 | dists = np.array(dists) 226 | vs = np.array(vs) 227 | deltainds = dists.argsort() 228 | for dix in deltainds: 229 | q.put(vs[dix]) 230 | 231 | def ix_arr_to_arr(self, v1): 232 | v = [] 233 | for idx in range(len(v1)): 234 | uix = int(v1[idx]) 235 | if len(self.target_cands[idx])-1 < uix: 236 | break 237 | v.append(self.target_cands[idx][uix]) 238 | if len(v) == len(self.target_cands): 239 | return v 240 | 241 | 242 | def manhattan_dist(arr1, arr2): 243 | dist = 0 244 | for ix in range(len(arr1)): 245 | dist += abs(arr1[ix] - arr2[ix]) 246 | return dist 247 | 248 | 249 | def tst1(): 250 | op = OptProblm() 251 | sums1 = [619, 596, 589, 1146, 13, 483, 37, 17, 29, 255, 304] 252 | for ix in range(len(op.arrays)): 253 | arr = op.arrays[ix] 254 | sum1 = sum(arr[op.path1]) 255 | prcnt1 = sum1/sum(arr) 256 | prcnt2 = sums1[ix]/sum(arr) 257 | lower = min(prcnt2, 1-prcnt2) 258 | higher = max(prcnt2, 1-prcnt2) 259 | assert (lower <= prcnt1 and prcnt1 <= higher) 260 | return op 261 | 262 | 263 | if __name__ == "__main__": 264 | # tst1() 265 | arrays = [ 266 | [2, 5, 9, 3, 1], 267 | [2, 3, 4, 4, 3] 268 | ] 269 | arrs = optimize3(arrays) 270 | 271 | 272 | ######################### 273 | # TODO 274 | # 1. Full optimization when combined tree across matrices. 275 | # 2. When zeros culled from arrays, update hws_ix and clust_ix. 276 | # 3. [Done] Switch to using pandas file or include CSV in package. 277 | # 4. Try on VMSKU as well and then combined HW VMSKU. 278 | 279 | ######################### 280 | # 2. 281 | # Save the mask, original arrays and new array. 282 | # Create a mapping between original indices and new indices + vice versa. 283 | -------------------------------------------------------------------------------- /optimizn/ab_split/opt_split_dp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | # Taken from: https://www.geeksforgeeks.org/subset-sum-problem-dp-25/ 5 | # A Dynamic Programming solution for subset 6 | # sum problem Returns true if there is a subset of 7 | # set[] with sun equal to given sum 8 | 9 | 10 | # Returns true if there is a subset of set[] 11 | # with sum equal to given sum 12 | def isSubsetSum(set, n, sum): 13 | # The value of subset[i][j] will be 14 | # true if there is a 15 | # subset of set[0..j-1] with sum equal to i 16 | subset = ([[False for i in range(sum + 1)] 17 | for i in range(n + 1)]) 18 | 19 | # If sum is 0, then answer is true 20 | for i in range(n + 1): 21 | subset[i][0] = True 22 | 23 | # If sum is not 0 and set is empty, 24 | # then answer is false 25 | for i in range(1, sum + 1): 26 | subset[0][i] = False 27 | 28 | # Fill the subset table in bottom up manner 29 | for i in range(1, n + 1): 30 | for j in range(1, sum + 1): 31 | if j < set[i-1]: 32 | subset[i][j] = subset[i-1][j] 33 | if j >= set[i-1]: 34 | subset[i][j] = (subset[i-1][j] or 35 | subset[i - 1][j-set[i-1]]) 36 | 37 | return subset 38 | 39 | 40 | # Driver code 41 | if __name__ == '__main__': 42 | arr = [3, 34, 4, 12, 5, 2] 43 | sum = 9 44 | n = len(arr) 45 | if (isSubsetSum(set, n, sum)[n, sum]): 46 | print("Found a subset with given sum") 47 | else: 48 | print("No subset with given sum") 49 | 50 | # This code is contributed by 51 | # sahil shelangia. 52 | -------------------------------------------------------------------------------- /optimizn/ab_split/testing/cluster_hw.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | 4 | df1 = pd.DataFrame( 5 | [ 6 | ["CBZ07PrdGPM01","ZT_Machine_Learning_Gen5.0_Utility",5], 7 | ["CDM06PrdApp03","Wiwynn_Azure_Gen6_GP_Med",619], 8 | ["CBN09PrdApp05","Wiwynn_Azure_Gen6_GP_Med",566], 9 | ["CBN10PrdApp02","Wiwynn_Azure_Gen6_GP_Med",515], 10 | ["CBN13PrdApp01","Wiwynn_Azure_Gen5_1_Compute_UEFI_C1042H",596], 11 | ["CDM05PrdApp03","Wiwynn_Azure_Gen5_1_Compute_UEFI_C1042H",576], 12 | ["CBN09PrdApp03","Wiwynn_Azure_Gen5_1_Compute_UEFI_C1042H",548], 13 | ["CBN07PrdApp01","Wiwynn_Azure_Gen5_1_Compute_C1042H",589], 14 | ["CBZ07PrdApp01","Wiwynn_Azure_Gen5_1_Compute_C1042H",564], 15 | ["CDM05PrdApp02","Wiwynn_Azure_Gen5_1_Compute_C1042H",516], 16 | ["CDM05PrdApp04","Wiwynn_AP_Gen5_Compute_C1040W_1C05",5], 17 | ["CBN07PrdApp51","Wiwynn_AP_Gen5_Compute_C1040W_1C05",2], 18 | ["CBN13PrdApp05","Wiwynn-Azure-Compute_GP-Med-Intel-WCS-C2030",517], 19 | ["CBN10PrdApp03","Wiwynn-Azure-Compute_GP-Med-Intel-WCS-C2030",444], 20 | ["CBN06PrdApp01","Wiwynn-Azure-Compute_GP-Med-Intel-WCS-C2030",436], 21 | ["CBN09PrdApp01","Wiwynn-Azure-Compute_GP-Med-Intel-WCS-C2030",397], 22 | ["CBZ06PrdApp01","Wiwynn-Azure-Compute_GP-Med-Intel-WCS-C2030",266], 23 | ["CBN09PrdApp11","Wiwynn-Azure-Compute_GP-Med-Intel-WCS-C2030",238], 24 | ["CBN09PrdApp04","Wiwynn-Azure-Compute_GP-Med-AMD-WCS-C2060",397], 25 | ["CDM03PrdApp03","Wiwynn-Azure-Compute_Confidential_HSM-F3_Utility-AMD-WCS-C2020",2], 26 | ["CBZ07PrdApp02","Wiwynn-Azure-Compute_Confidential_HSM-F3_Utility-AMD-WCS-C2020",2], 27 | ["CDM06PrdApp07","Wiwynn-Azure-Compute_Confidential_HSM-F3_Utility-AMD-WCS-C2020",2], 28 | ["CBY07PrdApp02","Wiwynn-Azure-Compute_Confidential_HSM-F3_Utility-AMD-WCS-C2020",2], 29 | ["CDM06PrdApp07","Wiwynn-Azure-Compute_Confidential_HSM-F3-Intel-WCS-C2071",7], 30 | ["CBZ07PrdApp02","Wiwynn-Azure-Compute_Confidential_HSM-F3-Intel-WCS-C2071",7], 31 | ["CDM03PrdApp03","Wiwynn-Azure-Compute_Confidential_HSM-F3-Intel-WCS-C2071",7], 32 | ["CBY07PrdApp02","Wiwynn-Azure-Compute_Confidential_HSM-F3-Intel-WCS-C2071",6], 33 | ["CDM06PrdApp04","Wiwynn-Azure-Compute_Confidential-UTIL-AMD-WCS-C2020",5], 34 | ["CDM06PrdApp04","Wiwynn-Azure-Compute_Confidential-Intel-WCS-C2071",233], 35 | ["CBN14PrdApp05","Wiwynn-Azure-Compute-GP-MM-Intel-WCS-C2080_RevA",243], 36 | ["CBN09PrdApp09","Wiwynn-Azure-Compute-GP-MM-Intel-WCS-C2080_RevA",216], 37 | ["CDM03PrdApp04","Wiwynn-Azure-Compute-GP-MM-Intel-WCS-C2080_RevA",193], 38 | ["CDM05PrdApp01","Wiwynn-Azure-Compute-GP-MM-Intel-WCS-C2080_RevA",178], 39 | ["CDM06PrdApp17","Wiwynn-Azure-Compute-GP-MM-Intel-WCS-C2080_RevA",147], 40 | ["CDM03PrdApp07","Wiwynn-Azure-Compute-GP-MM-Intel-WCS-C2080_RevA",24], 41 | ["CBN04PrdApp02","Wiwynn-Azure-Compute-GP-HH-Intel-WCS-C2080_RevA",58], 42 | ["CBZ07PrdApp03","Wiwynn-Azure-Compute-GP-HH-Intel-WCS-C2080_RevA",37], 43 | ["CBY07PrdApp03","Wiwynn-Azure-Compute-Confidential-Intel-WCS-C2080_RevA",18], 44 | ["CBZ06PrdApp01","Quanta_Gen4.1_WCS_SM863_SeagateHDD",1], 45 | ["CDM10PrdGPC04","Quanta-Azure-GPU-Compute-H10034-AMD-WCS-C2398",6], 46 | ["CDM07PrdGPZ02","Quanta-Azure-Compute-GPU-GP-AMD-WSC-C2192",9], 47 | ["CDM03PrdApp05","Quanta-Azure-Compute-GP-MM-AMD-WCS-C2190_RevA",450], 48 | ["CDM07PrdApp03","Quanta-Azure-Compute-GP-LM-AMD-WCS-C2190",388], 49 | ["CBN13PrdApp03","Lenovo_SR950_Azure_ComputeHM_6TiB",2], 50 | ["CBN13PrdApp03","Lenovo_SR650_Azure_ComputeHM_Utility",5], 51 | ["CBN09PrdApp02","Lenovo-Azure-Compute_HM_Utility-Intel-ThinkSystem-SR650",5], 52 | ["CBN13PrdApp02","Lenovo-Azure-Compute_HM_Utility-Intel-ThinkSystem-SR650",5], 53 | ["CBN13PrdApp02","Lenovo-Azure-Compute_HM-Intel-ThinkSystem-SR950_4TiB",45], 54 | ["CBN09PrdApp02","Lenovo-Azure-Compute_HM-Intel-ThinkSystem-SR950_4TiB",17], 55 | ["CBN13PrdApp02","Lenovo-Azure-Compute_HM-Intel-ThinkSystem-SR950_2TiB",32], 56 | ["CBN09PrdApp02","Lenovo-Azure-Compute_HM-Intel-ThinkSystem-SR950_2TiB",29], 57 | ["CBN14PrdApp01","Lenovo-Azure-Compute_GP-Med-Intel-WCS-C2010",583], 58 | ["CDM06PrdApp12","Lenovo-Azure-Compute_GP-Low-Intel-WCS-C2010",13], 59 | ["CDM07PrdApp01","Lenovo-Azure-Compute-Intel-WCS-C2030",486], 60 | ["CBN09PrdApp10","Lenovo-Azure-Compute-Intel-WCS-C2030",255], 61 | ["CDM03PrdApp01","Lenovo-Azure-Compute-GP-MM-Intel-WCS-C2080",105], 62 | ["CDM07PrdApp02","Ingrasys-Azure-Compute-Intel-WCS-C2030",379], 63 | ["CBZ06PrdApp02","Ingrasys-Azure-Compute-Intel-WCS-C2030",304], 64 | ["CDM03PrdApp08","Ingrasys-Azure-Compute-GP-MM-Intel-WCS-C2080_RevA",30], 65 | ["CDM10PrdApp01","Ingrasys-Azure-Compute-GP-HH-Intel-WCS-C2080",77], 66 | ["CDM06PrdApp02","HP_Wiwynn_Gen6_Optimized",182], 67 | ["CBN13PrdApp10","HPE-Azure-Compute-VHM_Utility-Intel-Proliant-DL380G10",6], 68 | ["CBN13PrdApp10","HPE-Azure-Compute-VHM-Intel-SDFlex_Gen2-Base-RevB",1], 69 | ["CBN04PrdApp01","Dell_Gen4.0_C1030D",118], 70 | ["CBY07PrdApp01","Dell_Azure_Gen5_1_Compute",597] 71 | ], columns=["Cluster","Hardware_Model_V2","dcount_NodeId"]) 72 | -------------------------------------------------------------------------------- /optimizn/ab_split/testing/test_opt_cases.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import multiprocessing 3 | import time 4 | import sys 5 | from optimizn.ab_split.opt_split2 import OptProblm 6 | from optimizn.ab_split.opt_split import optimize2 7 | from optimizn.ab_split.opt_split2 import optimize1, optimize3 8 | from optimizn.ab_split.ABSplitDuringDP import ABTestSituation 9 | 10 | 11 | class TstCases(): 12 | def __init__(self, fn, assrt_opt=False): 13 | """ 14 | fn has to take as input a two dimensional array. Each 15 | entry in that array is the input arrays. 16 | """ 17 | self.fn = fn 18 | self.assrt_opt = assrt_opt 19 | op = OptProblm() 20 | self.inputoutput = { 21 | "problem1: A case with conflict.": { 22 | "input": [ 23 | [2, 5, 9, 3, 1], 24 | [2, 3, 4, 4, 3] 25 | ], 26 | "delta": 2 27 | }, 28 | "problem2: A case where there are conflicts": { 29 | "input": [ 30 | [2, 4, 7, 9], 31 | [1, 2, 3, 2], 32 | [4, 7, 5, 2] 33 | ], 34 | "delta": 4 35 | }, 36 | "problem3: A test case with zeros": { 37 | "input": [ 38 | [7, 0, 7, 0], 39 | [0, 5, 0, 4] 40 | ], 41 | "delta": 1 42 | }, 43 | "problem4: A real world test case": { 44 | "input": op.arrays, 45 | "delta": 1881 46 | }, 47 | "problem5: All arrays agree": { 48 | "input": [ 49 | [3, 34, 4, 12, 5, 2], 50 | [0, 25, 4, 12, 5, 2], 51 | [22, 10, 4, 12, 5, 2], 52 | ], 53 | "delta": 25 54 | } 55 | } 56 | 57 | def tst_all(self): 58 | for k in self.inputoutput.keys(): 59 | print("## Trying problem " + k + "\n#######\n") 60 | arr = self.inputoutput[k]["input"] 61 | target_delta = self.inputoutput[k]["delta"] 62 | split = self.fn(arr) 63 | total_delta = calc_sol_delta(arr, split) 64 | print("Model delta: " + str(total_delta) + "," 65 | + " Best known delta: " 66 | + str(target_delta)) 67 | if self.assrt_opt: 68 | assert total_delta <= target_delta 69 | print("## Passed: " + k + "\n") 70 | 71 | 72 | def calc_sol_delta(arr, split): 73 | total_delta = 0 74 | for i in range(len(arr)): 75 | ar = arr[i] 76 | sum1 = sum(ar) 77 | cnt1 = sum([ar[ix] for ix in split]) 78 | total_delta += abs(sum1 - 2*cnt1) 79 | return total_delta 80 | 81 | 82 | def tst1(): 83 | tc = TstCases(ABTestSituation, False) 84 | # p = multiprocessing.Process(target=tc.tst_all, name="Foo") 85 | # p.start() 86 | tc.tst_all() 87 | 88 | 89 | if __name__ == "__main__": 90 | tst1() 91 | -------------------------------------------------------------------------------- /optimizn/combinatorial/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/binpacking/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/binpacking/bnb_binpacking.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | from optimizn.combinatorial.branch_and_bound import BnBProblem 5 | from functools import reduce 6 | import copy 7 | import math 8 | 9 | 10 | class BinPackingParams: 11 | def __init__(self, weights, capacity): 12 | self.weights = weights 13 | self.capacity = capacity 14 | 15 | def __eq__(self, other): 16 | return ( 17 | other is not None 18 | and self.weights == other.weights 19 | and self.capacity == other.capacity 20 | ) 21 | 22 | def __str__(self): 23 | return f'BinPackingParams - weights: {self.weights}, capacity: '\ 24 | + f'{self.capacity}' 25 | 26 | 27 | class BinPackingProblem(BnBProblem): 28 | ''' 29 | Solution format: 2-tuple 30 | 1. Allocation of items to bins (dict, keys are integers representing bins 31 | (starting from 1, so 1 represents the first bin, 2 is the second bin, etc.) 32 | and values are sets of integers that represent the items (1 represents 33 | first item in weights list, 2 represents second item in weights list, and 34 | so on)) 35 | 2. Index of last allocated item in sorted-by-decreasing-weight list of 36 | items (int) 37 | 38 | Branching strategy: 39 | Each level of the solution space tree corresponds to an item. Items are 40 | considered in order of decreasing weight. Each solution in a level 41 | corresponds to the item being placed in a bin that it can fit in. The 42 | remaining items can be put in bins in decreasing order of weight, into 43 | the first bin that can fit it. New bins created as needed 44 | ''' 45 | def __init__(self, params, bnb_selection_strategy): 46 | self.item_weights = {} # mapping of items to weights 47 | self.sorted_item_weights = [] # sorted (weight, item) tuples (desc) 48 | for i in range(1, len(params.weights) + 1): 49 | self.item_weights[i] = params.weights[i - 1] 50 | self.sorted_item_weights.append((params.weights[i - 1], i)) 51 | self.sorted_item_weights.sort(reverse=True) 52 | self.capacity = params.capacity 53 | super().__init__(params, bnb_selection_strategy) 54 | 55 | def get_initial_solution(self): 56 | return (self._pack_rem_items(dict(), -1), 57 | len(self.sorted_item_weights) - 1) 58 | 59 | def get_root(self): 60 | return (self._pack_rem_items(dict(), -1), -1) 61 | 62 | def _pack_rem_items(self, bin_packing, last_item_idx): 63 | ''' 64 | This function performs the first-fit decreasing algorithm presented in 65 | the following source. 66 | 67 | Source: 68 | 69 | (1) 70 | Title: Knapsack Problems, Algorithms and Computer Implementations 71 | Author: Silvano Martello, Paolo Toth 72 | URL: http://www.or.deis.unibo.it/knapsack.html, specifically the PDF for 73 | Chapter 8, http://www.or.deis.unibo.it/kp/Chapter8.pdf 74 | Date published: 1990 75 | Date accessed: January 11, 2023 76 | ''' 77 | next_item_idx = last_item_idx + 1 78 | for i in range(next_item_idx, len(self.sorted_item_weights)): 79 | next_item_weight, next_item = self.sorted_item_weights[i] 80 | bins = set(bin_packing.keys()) 81 | item_packed = False 82 | for bin in bins: 83 | # check if bin has space 84 | bin_weight = sum( 85 | list(map( 86 | lambda x: self.item_weights[x], 87 | bin_packing[bin]) 88 | )) 89 | if next_item_weight > self.capacity - bin_weight: 90 | continue 91 | 92 | # put item in bin 93 | bin_packing[bin].add(next_item) 94 | item_packed = True 95 | break 96 | 97 | # create new bin if needed 98 | if not item_packed: 99 | new_bin = 1 100 | if len(bins) != 0: 101 | new_bin = max(bins) + 1 102 | bin_packing[new_bin] = set() 103 | bin_packing[new_bin].add(next_item) 104 | return bin_packing 105 | 106 | def _filter_items(self, bin_packing, last_item_idx): 107 | # remove items that have not been considered yet 108 | considered_items = set(map( 109 | lambda x: x[1], self.sorted_item_weights[0:last_item_idx + 1])) 110 | new_bin_packing = {} 111 | for bin in bin_packing.keys(): 112 | new_bin = set(filter( 113 | lambda x: x in considered_items, bin_packing[bin])) 114 | if len(new_bin) != 0: 115 | new_bin_packing[bin] = new_bin 116 | return new_bin_packing 117 | 118 | def lbound(self, sol): 119 | ''' 120 | This lower bound function is based on the lower bound function L_1 121 | presented in the following source. 122 | 123 | Source: 124 | 125 | (1) 126 | Title: Knapsack Problems, Algorithms and Computer Implementations 127 | Author: Silvano Martello, Paolo Toth 128 | URL: http://www.or.deis.unibo.it/knapsack.html, specifically the PDF for 129 | Chapter 8, http://www.or.deis.unibo.it/kp/Chapter8.pdf 130 | Date published: 1990 131 | Date accessed: January 11, 2023 132 | ''' 133 | bin_packing = sol[0] 134 | last_item_idx = sol[1] 135 | 136 | # remove items that have not been considered yet 137 | bin_packing = self._filter_items(bin_packing, last_item_idx) 138 | curr_bin_ct = len(bin_packing.keys()) 139 | 140 | # get free capacity in bin packing 141 | curr_weight = sum(list(map( 142 | lambda x: self.sorted_item_weights[x][0], 143 | list(range(last_item_idx + 1)) 144 | ))) 145 | free_capacity = self.capacity * curr_bin_ct - curr_weight 146 | 147 | # get weights of remaining items 148 | rem_weight = sum(list(map( 149 | lambda x: self.sorted_item_weights[x][0], 150 | list(range(last_item_idx + 1, len(self.sorted_item_weights))) 151 | ))) 152 | 153 | return curr_bin_ct + math.ceil( 154 | (rem_weight - free_capacity) / self.capacity) 155 | 156 | def cost(self, sol): 157 | bin_packing = sol[0] 158 | return len(bin_packing.keys()) 159 | 160 | def branch(self, sol): 161 | # determine next item and its weight 162 | bin_packing = sol[0] 163 | last_item_idx = sol[1] 164 | next_item_idx = last_item_idx + 1 165 | if next_item_idx < len(self.sorted_item_weights): 166 | next_item_weight, next_item = self.sorted_item_weights[ 167 | next_item_idx] 168 | 169 | # remove items that have not been considered yet 170 | bin_packing = self._filter_items(bin_packing, last_item_idx) 171 | 172 | # pack items in bins 173 | extra_bin = 1 174 | if len(bin_packing.keys()) != 0: 175 | extra_bin = max(bin_packing.keys()) + 1 176 | bins = set(bin_packing.keys()).union({extra_bin}) 177 | for bin in bins: 178 | # create new bin if considering new bin index 179 | new_bin_packing = copy.deepcopy(bin_packing) 180 | if bin not in new_bin_packing.keys(): 181 | new_bin_packing[bin] = set() 182 | 183 | # check if bin has space 184 | bin_weight = sum( 185 | list(map( 186 | lambda x: self.item_weights[x], 187 | new_bin_packing[bin]) 188 | )) 189 | if next_item_weight > self.capacity - bin_weight: 190 | continue 191 | 192 | # pack item in bin 193 | new_bin_packing[bin].add(next_item) 194 | yield (new_bin_packing, next_item_idx) 195 | 196 | def is_feasible(self, sol): 197 | bin_packing = sol[0] 198 | last_item_idx = sol[1] 199 | 200 | # check that last item index corresponds to last item 201 | if last_item_idx != len(self.sorted_item_weights) - 1: 202 | return False 203 | 204 | # check that all items are packed 205 | items = set(reduce( 206 | (lambda s1, s2: s1.union(s2)), 207 | list(map(lambda b: bin_packing[b], bin_packing.keys())) 208 | )) 209 | if items != set(range(1, len(self.item_weights)+1)): 210 | return False 211 | 212 | # check that for each bin, the weight is not exceeded 213 | for bin in bin_packing.keys(): 214 | bin_weight = sum( 215 | list(map(lambda x: self.item_weights[x], bin_packing[bin]))) 216 | if bin_weight > self.capacity: 217 | return False 218 | 219 | return True 220 | 221 | def complete_solution(self, sol): 222 | return ( 223 | self._pack_rem_items(copy.deepcopy(sol[0]), sol[1]), 224 | len(self.sorted_item_weights) - 1) 225 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/knapsack/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/knapsack/bnb_knapsack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | from optimizn.combinatorial.branch_and_bound import BnBProblem 6 | from copy import deepcopy 7 | 8 | 9 | class KnapsackParams: 10 | def __init__(self, values, weights, capacity): 11 | self.values = values 12 | self.weights = weights 13 | self.capacity = capacity 14 | 15 | def __eq__(self, other): 16 | return ( 17 | other is not None 18 | and (len(self.values) == len(other.values) 19 | and (self.values == other.values).all()) 20 | and (len(self.weights) == len(other.weights) 21 | and (self.weights == other.weights).all()) 22 | and self.capacity == other.capacity 23 | ) 24 | 25 | 26 | class ZeroOneKnapsackProblem(BnBProblem): 27 | ''' 28 | Branch and bound implementation for the 0/1 knapsack problem, where each 29 | item is either taken or omitted in its entirety. 30 | 31 | This implementation is based on the demonstration shown in the following 32 | source. 33 | 34 | Source: 35 | 36 | (1) 37 | Title: 7.2 0/1 Knapsack using Branch and Bound 38 | Author: Abdul Bari 39 | URL: https://www.youtube.com/watch?v=yV1d-b_NeK8 40 | Date published: February 26, 2018 41 | Date accessed: December 16, 2022 42 | ''' 43 | def __init__(self, params, bnb_selection_strategy): 44 | self.values = params.values 45 | self.weights = params.weights 46 | self.capacity = params.capacity 47 | 48 | # value/weight ratios, in decreasing order 49 | vw_ratios = self.values / self.weights 50 | vw_ratios_ixs = [] 51 | for i in range(len(vw_ratios)): 52 | vw_ratios_ixs.append((vw_ratios[i], i)) 53 | self.sorted_vw_ratios = sorted(vw_ratios_ixs) 54 | self.sorted_vw_ratios.reverse() 55 | super().__init__(params, bnb_selection_strategy) 56 | 57 | def get_initial_solution(self): 58 | return self.complete_solution([]) 59 | 60 | def get_root(self): 61 | return [] 62 | 63 | def lbound(self, sol): 64 | value = 0 65 | weight = 0 66 | 67 | # consider items already taken 68 | for i in range(0, len(sol)): 69 | if sol[i] == 1: 70 | value += self.values[i] 71 | weight += self.weights[i] 72 | 73 | # greedily take other items 74 | for vw_ratio, ix in self.sorted_vw_ratios: 75 | if ix < len(sol): 76 | continue 77 | rem_cap = self.capacity - weight 78 | if rem_cap <= 0: 79 | break 80 | item_weight = min(rem_cap, self.weights[ix]) 81 | value += item_weight * vw_ratio 82 | weight += item_weight 83 | 84 | return -1 * value 85 | 86 | def cost(self, sol): 87 | return -1 * np.sum(np.array(sol) * np.array(self.values[:len(sol)])) 88 | 89 | def branch(self, sol): 90 | if len(sol) < len(self.weights): 91 | for val in [0, 1]: 92 | yield deepcopy(sol) + [val] 93 | 94 | def is_feasible(self, sol): 95 | # check that array length is the same as the number of weights/values 96 | check_length1 = len(sol) == len(self.weights) 97 | check_length2 = len(sol) == len(self.values) 98 | check_length = check_length1 and check_length2 99 | 100 | # check that the only values in the array are 0 and 1 101 | check_values = len(set(sol).difference({0, 1})) == 0 102 | 103 | # check that the weight of the values in the array is not greater 104 | # than the capacity 105 | check_weight = np.sum(np.array(sol) * np.array( 106 | self.weights[:len(sol)])) <= self.capacity 107 | 108 | return check_length and check_values and check_weight 109 | 110 | def complete_solution(self, sol): 111 | # greedily add other items to array 112 | knapsack = [0] * len(self.weights) 113 | knapsack[0:len(sol)] = sol 114 | value = 0 115 | weight = 0 116 | for i in range(len(knapsack)): 117 | if knapsack[i] == 1: 118 | value += self.values[i] 119 | weight += self.weights[i] 120 | 121 | # greedily take other items 122 | for _, ix in self.sorted_vw_ratios: 123 | if ix < len(sol): 124 | continue 125 | rem_cap = self.capacity - weight 126 | if rem_cap < self.weights[ix]: 127 | continue 128 | value += self.values[ix] 129 | weight += self.weights[ix] 130 | knapsack[ix] = 1 131 | 132 | return knapsack 133 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/min_path_cover/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/min_path_cover/bnb_min_path_cover.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | from optimizn.combinatorial.branch_and_bound import BnBProblem 6 | 7 | 8 | class MinPathCoverParams: 9 | def __init__(self, edges1, edges2): 10 | self.edges1 = edges1 11 | self.edges2 = edges2 12 | 13 | def __eq__(self, other): 14 | return ( 15 | other is not None 16 | and self.edges1 == other.edges1 17 | and self.edges2 == other.edges2 18 | ) 19 | 20 | 21 | class MinPathCoverProblem1(BnBProblem): 22 | ''' 23 | Solution format: 24 | 1. Array of size p (p = number of complete paths in graph), values are 0 25 | if path is not used in cover, 1 if path is used in cover 26 | 2. Last considered path index 27 | 28 | Branching strategy: 29 | Each level of the solution space tree corresponds to a path. 30 | Each solution branches into at most two solutions (the path is either used 31 | or omitted from the path cover). If the path does not cover any new 32 | vertices, then the branching only produces the solution where the path 33 | is omitted from the cover 34 | ''' 35 | def __init__(self, params, bnb_selection_strategy): 36 | self.edges1 = params.edges1 37 | self.edges2 = params.edges2 38 | self.vertices = set(params.edges1.flatten()).union( 39 | set(params.edges2.flatten())) 40 | self._get_all_paths() 41 | super().__init__(params, bnb_selection_strategy) 42 | 43 | def get_initial_solution(self): 44 | return (np.ones(len(self.all_paths)), len(self.all_paths) - 1) 45 | 46 | def get_root(self): 47 | return (np.ones(len(self.all_paths)), -1) 48 | 49 | def _get_all_paths(self): 50 | self.all_paths = [] 51 | for u1, u2 in self.edges1: 52 | for v1, v2 in self.edges2: 53 | if u2 == v1: 54 | self.all_paths.append([u1, v1, v2]) 55 | self.all_paths = np.array(self.all_paths) 56 | 57 | def _pick_rem_paths(self, sol): 58 | # determine which vertices can still be covered 59 | choices = sol[0] 60 | last_idx = sol[1] + 1 61 | covered = set() 62 | for i in range(0, last_idx): 63 | if choices[i] == 1: 64 | covered = covered.union(set(self.all_paths[i])) 65 | to_cover = set(self.all_paths[last_idx:].flatten()).difference(covered) 66 | 67 | # select paths that cover the most of the remaining vertices 68 | new_path_idxs = [] 69 | new_covered = set() 70 | while len(to_cover.difference(new_covered)) != 0: 71 | path_idx = None 72 | path_covered = set() 73 | for i in range(last_idx, len(self.all_paths)): 74 | new_verts = set(self.all_paths[i]).intersection( 75 | to_cover.difference(new_covered)) 76 | if len(new_verts) > len(path_covered): 77 | path_idx = i 78 | path_covered = new_verts 79 | new_path_idxs.append(path_idx) 80 | new_covered = new_covered.union(path_covered) 81 | return new_path_idxs 82 | 83 | def complete_solution(self, sol): 84 | print(sol[1], self._pick_rem_paths(sol)) 85 | path_cover = list(sol[0])[:max(0, sol[1] + 1)] +\ 86 | [0] * (len(self.all_paths) - (sol[1] + 1)) 87 | for idx in self._pick_rem_paths(sol): 88 | path_cover[idx] = 1 89 | return (np.array(path_cover), len(self.all_paths) - 1) 90 | 91 | def lbound(self, sol): 92 | # sum of existing paths and (number of vertices left to cover / 3) 93 | choices = sol[0] 94 | last_idx = sol[1] + 1 95 | covered = set() 96 | for i in range(0, last_idx): 97 | if choices[i] == 1: 98 | covered = covered.union(set(self.all_paths[i])) 99 | return sum(choices[0: sol[1] + 1]) + len(self.vertices.difference( 100 | covered)) / 3 101 | 102 | def cost(self, sol): 103 | return sum(sol[0]) 104 | 105 | def branch(self, sol): 106 | if sol[1] + 1 >= len(self.all_paths): 107 | return [] 108 | for val in [0, 1]: 109 | # do not include path in cover if no new vertices are covered 110 | if val == 1: 111 | covered = set() 112 | for i in range(0, sol[1] + 1): 113 | for v in self.all_paths[i]: 114 | covered.add(v) 115 | if len(set(self.all_paths[sol[1] + 1]) 116 | .difference(covered)) == 0: 117 | continue 118 | new_sol = np.array(list(sol[0][:max(0, sol[1] + 1)]) + [val]) 119 | yield (new_sol, sol[1] + 1) 120 | 121 | def is_feasible(self, sol): 122 | # check length of solution 123 | check_length = len(sol[0]) == len(self.all_paths) 124 | 125 | # check that all values in solution are 0 or 1 126 | check_vals = len(set(sol[0]).difference({0, 1})) == 0 127 | 128 | # check that all vertices are covered 129 | covered = set() 130 | for i in range(len(sol[0])): 131 | if sol[0][i] == 1: 132 | covered = covered.union(set(self.all_paths[i])) 133 | check_coverage = covered == self.vertices 134 | 135 | # check that all paths considered 136 | check_last_index = sol[1] == (len(self.all_paths) - 1) 137 | 138 | return check_length and check_vals and check_coverage and\ 139 | check_last_index 140 | 141 | 142 | class MinPathCoverProblem2(BnBProblem): 143 | ''' 144 | Solution format: 145 | 1. Path cover (paths that cover the first vertex to the last covered 146 | vertex) 147 | 2. Remaining paths (paths that cover the vertices after the last covered 148 | vertex) 149 | 3. Last covered vertex 150 | 151 | Branching strategy: 152 | At a level of the solution space tree that corresponds to vertex X, 153 | the solution nodes correspond to path covers that cover vertex X. 154 | Branching any of these solutions produces a new set of solutions that 155 | correspond to path covers that cover vertex X+1. These path covers 156 | either remain the same (if the path cover already covered vertex X+1) 157 | or include one extra path (to cover vertex X+1). 158 | 159 | Assumes that the vertices are in sequential order (e.g. {0, 1, 2, ...}) 160 | ''' 161 | def __init__(self, params, bnb_selection_strategy): 162 | self.edges1 = params.edges1 163 | self.edges2 = params.edges2 164 | self.vertices = set(params.edges1.flatten()).union( 165 | set(params.edges2.flatten())) 166 | self.all_paths = [] 167 | self.cov_dict = {} 168 | for u1, u2 in self.edges1: 169 | for v1, v2 in self.edges2: 170 | path = (u1, v1, v2) 171 | if u2 == v1: 172 | self.all_paths.append(path) 173 | for vert in path: 174 | if vert not in self.cov_dict.keys(): 175 | self.cov_dict[vert] = set() 176 | self.cov_dict[vert].add(path) 177 | super().__init__(params, bnb_selection_strategy) 178 | 179 | def get_initial_solution(self): 180 | return (np.zeros((0, 3)), np.array(self.all_paths), 181 | max(self.vertices)) 182 | 183 | def get_root(self): 184 | return (np.zeros((0, 3)), np.array(self.all_paths), 185 | min(self.vertices) - 1) 186 | 187 | def lbound(self, sol): 188 | # sum of existing paths and (number of vertices left to cover / 3) 189 | path_cover = sol[0] 190 | rem_verts = self.vertices.difference(set(path_cover.flatten())) 191 | return len(path_cover) + (len(rem_verts) / 3) 192 | 193 | def cost(self, sol): 194 | path_cover = sol[0] 195 | rem_paths = sol[1] 196 | return len(path_cover) + len(rem_paths) 197 | 198 | def branch(self, sol): 199 | # get components of solution 200 | path_cover = sol[0] 201 | last_cov_vert = sol[2] 202 | 203 | # if next vertex to cover has already been covered, retain 204 | # solution and cover the vertex after that 205 | new_last_cov_vert = last_cov_vert + 1 206 | if new_last_cov_vert <= max(self.vertices): 207 | covered = set(path_cover.flatten()) 208 | if new_last_cov_vert in covered: 209 | yield (sol[0], sol[1], new_last_cov_vert) 210 | # otherwise, branch based on paths that can cover the next vertex, 211 | # complete solution by picking paths that greedily cover remaining 212 | # vertices 213 | else: 214 | cand_paths = np.array(list(self.cov_dict[new_last_cov_vert])) 215 | for cand_path in cand_paths: 216 | cand_path = np.array([cand_path]) 217 | new_path_cover = np.concatenate( 218 | (path_cover, cand_path), axis=0) 219 | yield (new_path_cover, np.zeros((0, 3)), new_last_cov_vert) 220 | 221 | def complete_solution(self, sol): 222 | new_rem_paths = [] 223 | rem_verts = self.vertices.difference( 224 | set(sol[0].flatten()).union( 225 | set(np.array(new_rem_paths).flatten()))) 226 | while len(rem_verts) > 0: 227 | opt_path = None 228 | opt_cov = {} 229 | for path in self.all_paths: 230 | cov = rem_verts.intersection(path) 231 | if len(cov) > len(opt_cov): 232 | opt_path = path 233 | opt_cov = rem_verts.intersection(path) 234 | rem_verts = rem_verts.difference(set(opt_path)) 235 | new_rem_paths.append(opt_path) 236 | new_rem_paths = np.array(new_rem_paths) 237 | return (sol[0], new_rem_paths, max(self.vertices)) 238 | 239 | def is_feasible(self, sol): 240 | # check that each path in solution is valid 241 | path_cover_set = set(map(lambda p: tuple(p.astype(int)), sol[0])) 242 | all_paths_set = set(self.all_paths) 243 | check_paths_valid = len(path_cover_set.difference(all_paths_set)) == 0 244 | 245 | # check that all vertices are covered 246 | path_cover = sol[0] 247 | rem_paths = sol[1] 248 | check_coverage = self.vertices == set(path_cover.flatten()).union( 249 | set(rem_paths.flatten())) 250 | 251 | # check last vertex covered 252 | check_last_vert_covered = sol[2] == max(self.vertices) 253 | 254 | return check_paths_valid and check_coverage and\ 255 | check_last_vert_covered 256 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/min_path_cover/sim_anneal_min_path_cover.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | from optimizn.combinatorial.simulated_annealing import SimAnnealProblem 6 | from graphing.special_graphs.neural_trigraph.rand_graph import * 7 | # from graphing.graph import Graph 8 | # from graphing.traversal.clr_traversal import Graph1 9 | from graphing.special_graphs.neural_trigraph.path_cover import\ 10 | complete_paths 11 | from copy import deepcopy 12 | 13 | 14 | # For demonstration purposes. 15 | # We pick the min path cover problem 16 | # where we have an algorithm for computing 17 | # the optimal solution. 18 | class MinPathCover_NTG(SimAnnealProblem): 19 | """ 20 | Finding the min path cover of a neural trigraph. 21 | """ 22 | def __init__(self, ntg, swtch=1): 23 | self.ntg = ntg 24 | self.adj = ntg.g1.adj 25 | self.swtch = swtch 26 | self.name = "MinPathCover_NeuralTriGraph" 27 | super().__init__(ntg) 28 | 29 | def get_initial_solution(self): 30 | """ 31 | A candidate is going to be an array of 32 | arrays, where each array is a full path 33 | from the left-most layer of the graph 34 | to the right-most layer. 35 | """ 36 | paths = [] 37 | self.covered = {} 38 | ixs = np.arange(1, self.ntg.max_ix+1) 39 | ixs = np.random.permutation(ixs) 40 | for i in ixs: 41 | if i not in self.covered: 42 | path = self.add_path(i) 43 | paths.append(path[0]) 44 | self.candidate = paths 45 | return paths 46 | 47 | def next_candidate_v2(self, candidate, num_del_paths=1): 48 | self.candidate = deepcopy(candidate) 49 | paths = self.candidate 50 | covered = deepcopy(self.covered) 51 | del_paths = [] 52 | for i in range(num_del_paths): 53 | ix = np.random.choice(range(len(paths))) 54 | del_paths.append(paths[ix]) 55 | paths = np.delete(paths, ix, 0) 56 | for del_path in del_paths: 57 | for ixx in del_path: 58 | covered[ixx] -= 1 59 | if covered[ixx] == 0: 60 | path = complete_paths([[ixx]], 61 | self.ntg.left_edges, 62 | self.ntg.right_edges) 63 | path = path[0] 64 | for pa in path: 65 | covered[pa] += 1 66 | #breakpoint() 67 | paths = np.concatenate((paths, [path])) 68 | #breakpoint() 69 | return paths 70 | 71 | def next_candidate(self, candidate, num_del_paths=1): 72 | if self.swtch == 0: 73 | return self.get_initial_solution() 74 | else: 75 | return self.next_candidate_v2(candidate, num_del_paths) 76 | 77 | def add_path(self, i): 78 | path = complete_paths([[i]], 79 | self.ntg.left_edges, self.ntg.right_edges) 80 | for j in path[0]: 81 | if j in self.covered: 82 | self.covered[j] += 1 83 | else: 84 | self.covered[j] = 1 85 | # self.candidate.append(path) 86 | return path 87 | 88 | def cost(self, candidate): 89 | ''' 90 | Gets the cost for candidate solution. 91 | ''' 92 | return (len(candidate)) 93 | 94 | def update_candidate(self, candidate, cost): 95 | # TODO: This can be made more efficient by updating existing covered 96 | # set. 97 | self.covered = {} 98 | for path in candidate: 99 | for j in path: 100 | if j in self.covered: 101 | self.covered[j] += 1 102 | else: 103 | self.covered[j] = 1 104 | super().update_candidate(candidate, cost) 105 | 106 | 107 | # Scipy simulated annealing can't be used because it expects a 1-d continuous 108 | # array 109 | # https://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.optimize.anneal.html 110 | 111 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/suitcase_reshuffle/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/suitcase_reshuffle/bnb_suitcasereshuffle.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | from optimizn.combinatorial.branch_and_bound import BnBProblem 5 | from copy import deepcopy 6 | from functools import reduce 7 | from optimizn.combinatorial.algorithms.suitcase_reshuffle.suitcases import\ 8 | SuitCases 9 | 10 | 11 | class SuitcaseReshuffleProblem(BnBProblem): 12 | ''' 13 | Solution Format: 14 | 2-tuple 15 | 1. SuitCases object containing suitcases, weights, and empty space 16 | 2. Index of last item (in list of sorted item weights) put in suitcase 17 | 18 | Branching strategy: 19 | Consider items in decreasing order of weight. Put item in each suitcase 20 | that can fit it 21 | ''' 22 | 23 | def __init__(self, suitcases, bnb_selection_strategy): 24 | self.config = suitcases.config 25 | self.capacities = suitcases.capacities 26 | self.suitcases = suitcases 27 | self.sorted_weights = self._get_weights(self.config, True) 28 | self.weight_counts = self._get_weight_counts(self.sorted_weights) 29 | super().__init__(suitcases, bnb_selection_strategy) 30 | 31 | def _get_weights(self, suitcases, sort=False): 32 | weights = list(reduce( 33 | lambda l1, l2: l1 + l2[:-1], suitcases[1:], suitcases[0][:-1])) 34 | if sort: 35 | weights = sorted(weights, reverse=True) 36 | return weights 37 | 38 | def _get_weight_counts(self, weights): 39 | weight_counts = dict() 40 | for weight in weights: 41 | if weight not in weight_counts.keys(): 42 | weight_counts[weight] = 1 43 | else: 44 | weight_counts[weight] += 1 45 | return weight_counts 46 | 47 | def get_initial_solution(self): 48 | return (deepcopy(self.suitcases), len(self.sorted_weights) - 1) 49 | 50 | def get_root(self): 51 | return (deepcopy(self.suitcases), -1) 52 | 53 | def cost(self, sol): 54 | suitcases = sol[0] 55 | max_empty_space = float('-inf') 56 | for suitcase in suitcases.config: 57 | max_empty_space = max(max_empty_space, suitcase[-1]) 58 | return -1 * max_empty_space 59 | 60 | def lbound(self, sol): 61 | suitcases = sol[0] 62 | empty_space = 0 63 | for suitcase in suitcases.config: 64 | empty_space += suitcase[-1] 65 | return -1 * empty_space 66 | 67 | def is_feasible(self, sol): 68 | suitcases = sol[0].config 69 | 70 | # check last item index 71 | last_item_index = sol[1] 72 | if last_item_index != len(self.sorted_weights) - 1: 73 | return False 74 | 75 | # for each suitcase, weights and extra space must equal original 76 | # capacity 77 | for i in range(len(suitcases)): 78 | if sum(suitcases[i]) != self.capacities[i]: 79 | return False 80 | 81 | # weights should appear exactly the same number of times as in 82 | # the original suitcase configs 83 | weight_counts = self._get_weight_counts(self._get_weights(suitcases)) 84 | if weight_counts != self.weight_counts: 85 | return False 86 | 87 | return True 88 | 89 | def complete_solution(self, sol): 90 | suitcases = deepcopy(sol[0].config) 91 | 92 | # get remaining items to pack 93 | num_packed = len(list(reduce( 94 | lambda l1, l2: l1 + l2[:-1], suitcases[1:], suitcases[0][:-1]))) 95 | items_to_pack = self.sorted_weights[num_packed:] 96 | 97 | # put each item in the suitcase with the least extra space that 98 | # can hold it 99 | for weight in items_to_pack: 100 | # find suitcase to pack item in 101 | min_space = None 102 | min_suitcase = None 103 | for i in range(len(suitcases)): 104 | extra_space = suitcases[i][-1] 105 | if extra_space >= weight: 106 | if min_space is None and min_suitcase is None: 107 | min_space = extra_space 108 | min_suitcase = i 109 | elif min_space > extra_space: 110 | min_space = extra_space 111 | min_suitcase = i 112 | 113 | # if item cannot be packed, solution cannot be completed 114 | if min_space is None and min_suitcase is None: 115 | return None 116 | 117 | # pack item in suitcase 118 | suitcases[min_suitcase] = suitcases[min_suitcase][:-1] + [weight]\ 119 | + [suitcases[min_suitcase][-1] - weight] 120 | 121 | return (SuitCases(suitcases), len(self.sorted_weights) - 1) 122 | 123 | 124 | def branch(self, sol): 125 | last_item_idx = sol[1] 126 | 127 | if last_item_idx != len(self.sorted_weights) - 1: 128 | if last_item_idx == -1: 129 | # if last item index is -1 (root solution), start from empty 130 | # suitcases 131 | suitcases = [] 132 | for capacity in self.capacities: 133 | suitcases.append([capacity]) 134 | else: 135 | suitcases = sol[0].config 136 | 137 | # get next item weight 138 | next_item_idx = last_item_idx + 1 139 | next_item_weight = self.sorted_weights[next_item_idx] 140 | 141 | # pack next item in each suitcase that can fit it 142 | for i in range(len(suitcases)): 143 | extra_space = suitcases[i][-1] 144 | if extra_space >= next_item_weight: 145 | new_suitcases = deepcopy(suitcases) 146 | new_suitcases[i] = new_suitcases[i][:-1]\ 147 | + [next_item_weight]\ 148 | + [new_suitcases[i][-1] - next_item_weight] 149 | yield (SuitCases(new_suitcases), next_item_idx) 150 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/suitcase_reshuffle/sim_anneal_suitcase_reshuffle.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | from optimizn.combinatorial.simulated_annealing import SimAnnealProblem 6 | from copy import deepcopy 7 | 8 | 9 | class SuitCaseReshuffle(SimAnnealProblem): 10 | def __init__(self, params): 11 | self.name = "SuitcaseReshuffling" 12 | super().__init__(params) 13 | 14 | def get_initial_solution(self): 15 | self.candidate = self.params.config 16 | return self.candidate 17 | 18 | def cost(self, candidate): 19 | maxx = 0 20 | for ar in candidate: 21 | maxx = max(maxx, ar[len(ar)-1]) 22 | return -maxx 23 | 24 | def next_candidate(self, candidate): 25 | # Find two items to swap from two different 26 | # suitcases. 27 | keep_going = True 28 | while keep_going: 29 | candidate1 = deepcopy(candidate) 30 | l = np.arange(len(candidate)) 31 | cases = np.random.choice(l, size=2, replace=False) 32 | ix1 = np.random.choice(len(candidate[cases[0]]) - 1) 33 | ix2 = np.random.choice(len(candidate[cases[1]]) - 1) 34 | size1 = candidate[cases[0]][ix1] 35 | size2 = candidate[cases[1]][ix2] 36 | candidate1[cases[0]][ix1] = size2 37 | candidate1[cases[1]][ix2] = size1 38 | arr1 = candidate1[cases[0]] 39 | arr2 = candidate1[cases[1]] 40 | caps = self.params.capacities 41 | if caps[cases[0]] < sum(arr1[:len(arr1)-1])\ 42 | or caps[cases[1]] < sum(arr2[:len(arr2)-1]): 43 | continue 44 | else: 45 | keep_going = False 46 | arr1[len(arr1)-1] = caps[cases[0]]\ 47 | - sum(arr1[:len(arr1)-1]) 48 | arr2[len(arr2)-1] = caps[cases[1]]\ 49 | - sum(arr2[:len(arr2)-1]) 50 | return candidate1 51 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/suitcase_reshuffle/suitcases.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | class SuitCases(): 5 | def __init__(self, config): 6 | """ 7 | The configuration of the suitcases 8 | is an array of arrays. The last element 9 | of each array must be the amount of empty space. 10 | This means that the sum of each array is the 11 | capacity of that suitcase. 12 | """ 13 | self.config = config 14 | self.capacities = [] 15 | for ar in config: 16 | self.capacities.append(sum(ar)) 17 | 18 | def __eq__(self, other): 19 | return ( 20 | self.config == other.config 21 | and self.capacities == other.capacities 22 | ) 23 | 24 | def __str__(self): 25 | return f'' 27 | 28 | def __repr__(self): 29 | return self.__str__() 30 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/traveling_salesman/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/traveling_salesman/bnb_tsp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import random 5 | from optimizn.combinatorial.branch_and_bound import BnBProblem 6 | from copy import deepcopy 7 | 8 | 9 | class TravelingSalesmanProblem(BnBProblem): 10 | def __init__(self, params, bnb_selection_strategy): 11 | self.input_graph = params['input_graph'] 12 | # sort all distance values, for computing lower bounds 13 | self.sorted_dists = [] 14 | for i in range(self.input_graph.dists.shape[0]): 15 | for j in range(0, i): 16 | self.sorted_dists.append(self.input_graph.dists[i, j]) 17 | self.sorted_dists.sort() 18 | super().__init__(params, bnb_selection_strategy) 19 | 20 | def get_initial_solution(self): 21 | # path of cities in increasing, numerical order 22 | return list(range(self.input_graph.num_cities)) 23 | 24 | def get_root(self): 25 | # return path with just the first city 26 | return [0] 27 | 28 | def complete_solution(self, sol): 29 | # path completed by random ordering of unvisited cities 30 | unvisited_cities = set(range(self.input_graph.num_cities)).difference( 31 | set(sol)) 32 | unvisited_cities = list(unvisited_cities) 33 | random.shuffle(unvisited_cities) 34 | return sol + unvisited_cities 35 | 36 | def cost(self, sol): 37 | # sum of distances between adjacent cities in path, and from last 38 | # city to first city in path 39 | path_cost = 0 40 | for i in range(self.input_graph.num_cities - 1): 41 | path_cost += self.input_graph.dists[sol[i], sol[i + 1]] 42 | path_cost += self.input_graph.dists[ 43 | sol[self.input_graph.num_cities - 1], sol[0]] 44 | return path_cost 45 | 46 | def lbound(self, sol): 47 | # sum of distances between cities in path and k smallest 48 | # remaining distance values (k = number of remaining cities + 1) 49 | dist_vals = [] 50 | num_cities_in_path = len(sol) 51 | for i in range(num_cities_in_path - 1): 52 | dist_vals.append(self.input_graph.dists[sol[i], sol[i + 1]]) 53 | if num_cities_in_path == self.input_graph.num_cities: 54 | dist_vals.append(self.input_graph.dists[ 55 | sol[num_cities_in_path - 1], sol[0]]) 56 | elif num_cities_in_path == 0: 57 | dist_vals = self.sorted_dists[:self.input_graph.num_cities] 58 | else: 59 | sorted_dist_vals = deepcopy(self.sorted_dists) 60 | for val in dist_vals: 61 | sorted_dist_vals.remove(val) 62 | dist_vals += sorted_dist_vals[ 63 | :self.input_graph.num_cities - num_cities_in_path + 1] 64 | return sum(dist_vals) 65 | 66 | def is_feasible(self, sol): 67 | # check that all cities covered once, path length is equal to the 68 | # number of cities 69 | check_all_cities_covered = set(sol) == set( 70 | range(self.input_graph.num_cities)) 71 | check_cities_covered_once = len(sol) == len(set(sol)) 72 | check_path_length = len(sol) == self.input_graph.num_cities 73 | return (check_path_length and check_cities_covered_once and 74 | check_all_cities_covered) 75 | 76 | def branch(self, sol): 77 | # build the path by creating a new solution for each uncovered city, 78 | # where the uncovered city is the next city in the path 79 | if len(sol) < self.input_graph.num_cities: 80 | visited = set(sol) 81 | for new_city in range(self.input_graph.dists.shape[0]): 82 | if new_city not in visited: 83 | yield sol + [new_city] 84 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/traveling_salesman/city_graph.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | 6 | 7 | class CityGraph(): 8 | def __init__(self, num_cities=50): 9 | # Generate x-y coordinates of some cities. 10 | # Here, we just draw them from a normal dist. 11 | self.xs = np.random.normal(loc=0,scale=5,size=(num_cities,2)) 12 | self.num_cities = len(self.xs) 13 | self.dists = np.zeros((len(self.xs), len(self.xs))) 14 | # Populate the matrix of euclidean distances. 15 | for i in range(len(self.xs)): 16 | for j in range(i+1, len(self.xs)): 17 | dist = (self.xs[i][0]-self.xs[j][0])**2 18 | dist += (self.xs[i][1]-self.xs[j][1])**2 19 | dist = np.sqrt(dist) 20 | self.dists[i,j] = dist 21 | for i in range(len(self.xs)): 22 | for j in range(i): 23 | self.dists[i,j] = self.dists[j,i] 24 | 25 | def __eq__(self, other): 26 | if self.num_cities != other.num_cities: 27 | return False 28 | elif self.dists.shape != other.dists.shape: 29 | return False 30 | elif (self.dists != other.dists).any(): 31 | return False 32 | return True 33 | 34 | def __str__(self): 35 | return f'City graph of {self.num_cities} cities, represented as an '\ 36 | + f'adjacency matrix:\n{self.dists}' 37 | -------------------------------------------------------------------------------- /optimizn/combinatorial/algorithms/traveling_salesman/sim_anneal_tsp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | from copy import deepcopy 6 | from optimizn.combinatorial.simulated_annealing import SimAnnealProblem,\ 7 | s_curve 8 | # from ortools.constraint_solver import routing_enums_pb2 9 | # from ortools.constraint_solver import pywrapcp 10 | 11 | 12 | class TravSalsmn(SimAnnealProblem): 13 | ''' 14 | This simulated annealing implementation for the traveling salesman 15 | problem is based on the following sources. The code presented in [2] is 16 | licensed under the MIT License. The original license text is shown in the 17 | NOTICE.md file. 18 | 19 | Sources: 20 | 21 | [1] T. W. Schneider, "The traveling salesman with simulated annealing, 22 | r, and shiny." 23 | https://toddwschneider.com/posts/traveling-salesman-with-simulated-annealing-r-and-shiny/, 24 | September 2014. Online; accessed 8-January-2023. 25 | 26 | [2] T. W. Schneider, "shiny-salesman/helpers.r." 27 | https://github.com/toddwschneider/shiny-salesman/blob/master/helpers.R, 28 | October 2014. Online; accessed 8-January-2023. 29 | ''' 30 | def __init__(self, params): 31 | super().__init__(params) 32 | 33 | def get_initial_solution(self): 34 | """ 35 | A candidate is going to be an array 36 | representing the order of cities 37 | visited. 38 | """ 39 | # path of cities in increasing, numerical order 40 | return np.arange(self.params.num_cities) 41 | 42 | def reset_candidate(self): 43 | # random path of cities 44 | return np.random.permutation(np.arange(self.params.num_cities)) 45 | 46 | def cost(self, candidate): 47 | tour_d = 0 48 | for i in range(1, len(candidate)): 49 | tour_d += self.params.dists[candidate[i], candidate[i-1]] 50 | tour_d += self.params.dists[ 51 | candidate[0], candidate[len(candidate) - 1]] 52 | return tour_d 53 | 54 | def next_candidate(self, candidate): 55 | nu_candidate = deepcopy(candidate) 56 | swaps = np.random.choice( 57 | np.arange(len(candidate)), size=2, replace=False) 58 | to_swap = nu_candidate[swaps] 59 | nu_candidate[swaps[0]] = to_swap[1] 60 | nu_candidate[swaps[1]] = to_swap[0] 61 | return nu_candidate 62 | 63 | def get_temperature(self, iters): 64 | return s_curve(iters, 4000, 0, 10000) 65 | 66 | 67 | def dist_from_lat_long(lat1, long1, lat2, long2): 68 | """ 69 | This was taken from the following source. 70 | 71 | Source: 72 | 73 | (1) 74 | Title: Latitude Longitude Distance Calculator 75 | Author: Luciano Miño 76 | Reviewer: Steven Wooding 77 | URL: https://www.omnicalculator.com/other/latitude-longitude-distance 78 | Date accessed: January 8, 2023 79 | 80 | Doesn't currently work. Need to debug (230108) 81 | """ 82 | theta1 = lat1 83 | theta2 = lat2 84 | phi1 = long1 85 | phi2 = long2 86 | r = 6400 87 | dtheta1 = (theta2-theta1)/2 88 | dtheta1 = np.sin(dtheta1)**2 89 | dtheta2 = np.cos(theta1)*np.cos(theta1) 90 | dtheta2 *= np.sin((phi2-phi1)/2)**2 91 | d = np.sqrt(dtheta1+dtheta2) 92 | d = np.arcsin(d) 93 | dist = 2*r*d 94 | return d 95 | -------------------------------------------------------------------------------- /optimizn/combinatorial/branch_and_bound.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import time 5 | from optimizn.combinatorial.opt_problem import OptProblem 6 | import inspect 7 | from queue import PriorityQueue 8 | from enum import Enum 9 | 10 | 11 | class BnBSelectionStrategy(Enum): 12 | DEPTH_FIRST = 'DEPTH_FIRST' 13 | DEPTH_FIRST_BEST_FIRST = 'DEPTH_FIRST_BEST_FIRST' 14 | BEST_FIRST_DEPTH_FIRST = 'BEST_FIRST_DEPTH_FIRST' 15 | 16 | 17 | class BnBProblem(OptProblem): 18 | def __init__(self, params, bnb_selection_strategy, logger=None): 19 | if not isinstance(bnb_selection_strategy, BnBSelectionStrategy): 20 | raise Exception( 21 | f'Invalid value for bnb_selection_strategy, must be one of ' 22 | + 'the following BnBSelectionStrategy enum values: DEPTH_FIRST' 23 | + 'DEPTH_FIRST_BEST_FIRST, BEST_FIRST') 24 | 25 | # initialize for depth-first branch and bound 26 | self.bnb_selection_strategy = bnb_selection_strategy 27 | if self.bnb_selection_strategy == BnBSelectionStrategy.DEPTH_FIRST: 28 | # initialization 29 | self.total_iters = 0 30 | self.total_time_elapsed = 0 31 | super().__init__({ 32 | 'params': params, 33 | 'bnb_selection_strategy': bnb_selection_strategy 34 | }, logger) 35 | 36 | # check initial solution 37 | if self.init_solution is not None and not self.is_feasible( 38 | self.init_solution): 39 | raise Exception('Initial solution is infeasible: ' 40 | + f'{self.init_solution}') 41 | 42 | # initialize stack with root solution 43 | root_sol = self.get_root() 44 | root_gen = self.branch(root_sol) 45 | if not inspect.isgenerator(root_gen): 46 | raise Exception('Branch method must return a generator when ' 47 | + 'running depth-first branch and bound') 48 | self.stack = [(root_sol, root_gen)] 49 | # initialize for depth-first-best-first or best-first branch and bound 50 | elif self.bnb_selection_strategy in { 51 | BnBSelectionStrategy.DEPTH_FIRST_BEST_FIRST, 52 | BnBSelectionStrategy.BEST_FIRST_DEPTH_FIRST}: 53 | self.priority_queue = PriorityQueue() 54 | self.total_iters = 0 55 | self.total_time_elapsed = 0 56 | super().__init__(params, logger) 57 | if self.init_solution is not None and not self.is_feasible( 58 | self.init_solution): 59 | raise Exception('Initial solution is infeasible: ' 60 | + f'{self.init_solution}') 61 | 62 | self.sol_count = 1 # breaks ties between solutions with same lower 63 | # bound and depth, solutions generated earlier are given priority 64 | 65 | # put root solution onto PriorityQueue 66 | root_sol = self.get_root() 67 | if self.bnb_selection_strategy ==\ 68 | BnBSelectionStrategy.DEPTH_FIRST_BEST_FIRST: 69 | self.priority_queue.put( 70 | (0, self.lbound(root_sol), self.sol_count, root_sol)) 71 | else: 72 | self.priority_queue.put( 73 | (self.lbound(root_sol), 0, self.sol_count, root_sol)) 74 | # solution tuples consist of four values: lower bound, solution 75 | # depth, solution count, solution 76 | else: 77 | raise Exception( 78 | f'Invalid value for bnb_selection_strategy, must be one of ' 79 | + 'the following BnBSelectionStrategy enum values: DEPTH_FIRST' 80 | + 'DEPTH_FIRST_BEST_FIRST, BEST_FIRST_DEPTH_FIRST') 81 | 82 | def get_initial_solution(self): 83 | ''' 84 | Gets the initial solution. Defaults to None, can be overridden 85 | ''' 86 | return None 87 | 88 | def get_root(self): 89 | ''' 90 | Produces the root solution, from which other solutions are obtainable 91 | through branching 92 | ''' 93 | raise NotImplementedError( 94 | 'Implement a method to get the root solution, from which all other' 95 | + ' solutions are obtainable through branching') 96 | 97 | def lbound(self, sol): 98 | ''' 99 | Computes lower bound for a given solution and the solutions that can be 100 | obtained from it through branching 101 | ''' 102 | raise NotImplementedError( 103 | 'Implement a method to compute a lower bound on a given solution') 104 | 105 | def branch(self, sol): 106 | ''' 107 | Generates other solutions from a given solution (branching) 108 | ''' 109 | raise NotImplementedError( 110 | 'Implement a branching method to produce other solutions from a ' 111 | + 'given solution') 112 | 113 | def is_feasible(self, sol): 114 | ''' 115 | Checks if a solution is feasible (solves the optimization problem, 116 | adhering to its constraints) 117 | ''' 118 | raise NotImplementedError( 119 | 'Implement a method to check if a solution is feasible (solves ' 120 | + 'the optimization problem, adhering to its constraints)') 121 | 122 | def complete_solution(self, sol): 123 | ''' 124 | Completes an incomplete solution for early detection of solutions 125 | that are potentially more optimal than the most optimal solution 126 | already observed (only needed for look-ahead branch and bound 127 | algorithm) 128 | ''' 129 | raise NotImplementedError( 130 | 'Implement a method to complete an incomplete solution') 131 | 132 | def _log_results(self, log_iters, force=False): 133 | if force or self.current_iters % int(log_iters) == 0: 134 | self.logger.info(f'Iterations (current run): {self.current_iters}') 135 | self.logger.info(f'Iterations (total): {self.total_iters}') 136 | self.logger.info( 137 | f'Time elapsed (current run): {self.current_time_elapsed} ' 138 | + 'seconds') 139 | self.logger.info( 140 | f'Time elapsed (total): {self.total_time_elapsed} seconds') 141 | self.logger.info(f'Best solution: {self.best_solution}') 142 | self.logger.info(f'Best solution cost: {self.best_cost}') 143 | 144 | def _update_best_solution(self, sol): 145 | # get cost of solution and update best solution and cost if needed 146 | cost = self.cost(sol) 147 | if self.cost_delta(self.best_cost, cost) > 0: 148 | self.best_cost = cost 149 | self.best_solution = sol 150 | self.logger.info( 151 | f'Updated best solution to: {self.best_solution}') 152 | self.logger.info( 153 | f'Updated best solution cost to: {self.best_cost}') 154 | 155 | def _evaluate_solution(self, sol, bnb_type, sol_lb=None): 156 | # get lower bound of solution, if not provided 157 | if sol_lb is None: 158 | sol_lb = self.lbound(sol) 159 | 160 | # evaluate if a more optimal solution can be obtained 161 | branch = False 162 | if self.cost_delta(self.best_cost, sol_lb) > 0: 163 | branch = True 164 | # compare against best solution 165 | if self.is_feasible(sol): 166 | # if solution is feasible, update best solution and 167 | # best solution cost if needed 168 | self._update_best_solution(sol) 169 | branch = False 170 | else: 171 | # if algorithm type is 1 (look-ahead branch and bound), 172 | # then complete the partial solution and update best 173 | # solution and best solution cost if needed 174 | if bnb_type == 1: 175 | completed_sol = self.complete_solution(sol) 176 | if self.is_feasible(completed_sol): 177 | self._update_best_solution(completed_sol) 178 | 179 | # return boolean to branch 180 | return branch 181 | 182 | def _update_progress_and_log( 183 | self, start, original_total_time_elapsed, log_iters): 184 | self.current_iters += 1 185 | self.total_iters += 1 186 | self.current_time_elapsed = time.time() - start 187 | self.total_time_elapsed = original_total_time_elapsed +\ 188 | self.current_time_elapsed 189 | self._log_results(log_iters) 190 | 191 | def _terminate(self, iters_limit, time_limit): 192 | return self.current_iters >= iters_limit or\ 193 | self.current_time_elapsed >= time_limit 194 | 195 | def _solve_df(self, iters_limit, log_iters, time_limit, bnb_type): 196 | ''' 197 | This depth-first branch and bound implementation is based on the 198 | following sources. 199 | 200 | This method executes either the traditional (bnb_type=0) or look-ahead 201 | (bnb_type=1) branch and bound algorithm. In traditional branch and 202 | bound, partial solutions are not completed and are not evaluated 203 | against the current best solution, while in look-ahead branch 204 | and bound, they are. 205 | 206 | Sources: 207 | 208 | [1] J. Clausen, "Branch and bound algorithms - principles and 209 | examples.." https://imada.sdu.dk/u/jbj/heuristikker/TSPtext.pdf, 210 | March 1999. Online; accessed 16-December-2022. 211 | 212 | [2] A. Bari, "7.2 0/1 knapsack using branch and bound." 213 | https://www.youtube.com/watch?v=yV1d-b_NeK8, February 2018. Online; 214 | accessed 16-December-2022. 215 | ''' 216 | # converts a list to a generator 217 | def _list_to_gen(lst): 218 | for item in lst: 219 | yield item 220 | 221 | # initialization for current run 222 | start = time.time() 223 | self.current_iters = 0 224 | self.current_time_elapsed = 0 225 | original_total_time_elapsed = self.total_time_elapsed 226 | self.terminate_early = False 227 | # if stack elements contain lists instead of generators (instance 228 | # loaded from memory), convert lists back to generators 229 | if len(self.stack) != 0: 230 | if type(self.stack[0][1]) == list: 231 | self.stack = list(map( 232 | lambda i: (i[0], _list_to_gen(i[1])), self.stack)) 233 | 234 | # recursive helper function to evaluate a solution 235 | def _evaluate(sol, sol_gen, skip_eval): 236 | branch = True 237 | if not skip_eval: 238 | if self._terminate(iters_limit, time_limit): 239 | self.logger.info( 240 | 'Iterations/time limit reached, terminating algorithm') 241 | self.terminate_early = True 242 | return 243 | 244 | # evaluate solution 245 | branch = self._evaluate_solution(sol, bnb_type) 246 | self._update_progress_and_log( 247 | start, original_total_time_elapsed, log_iters) 248 | 249 | # evaluate solutions obtained by branching 250 | if branch: 251 | for next_sol in sol_gen: 252 | next_sol_gen = self.branch(next_sol) 253 | self.stack.append((next_sol, next_sol_gen)) 254 | _evaluate(next_sol, next_sol_gen, False) 255 | # do not remove solution from stack if evaluation was 256 | # terminated early 257 | if self.terminate_early: 258 | break 259 | self.stack.pop() 260 | 261 | # run branch and bound algorithm 262 | skip_eval = False 263 | while len(self.stack) > 0: 264 | # evaluate solution 265 | sol, sol_gen = self.stack[-1] 266 | _evaluate(sol, sol_gen, skip_eval) 267 | # do not remove solution from stack if evaluation was 268 | # terminated early 269 | if self.terminate_early: 270 | break 271 | self.stack.pop() 272 | skip_eval = True 273 | 274 | def _solve_dfbef_befdf(self, iters_limit, log_iters, time_limit, bnb_type): 275 | ''' 276 | This depth-first-best-first or best-first-depth-first branch and bound 277 | implementation is based on the following sources. 278 | 279 | This method executes either the traditional (bnb_type=0) or look-ahead 280 | (bnb_type=1) branch and bound algorithm. In traditional branch and 281 | bound, partial solutions are not completed and are not evaluated 282 | against the current best solution, while in look-ahead branch 283 | and bound, they are. 284 | 285 | Sources: 286 | 287 | [1] J. Clausen, "Branch and bound algorithms - principles and 288 | examples.." https://imada.sdu.dk/u/jbj/heuristikker/TSPtext.pdf, 289 | March 1999. Online; accessed 16-December-2022. 290 | 291 | [2] A. Bari, "7.2 0/1 knapsack using branch and bound." 292 | https://www.youtube.com/watch?v=yV1d-b_NeK8, February 2018. Online; 293 | accessed 16-December-2022. 294 | ''' 295 | # initialization for current run 296 | start = time.time() 297 | self.current_iters = 0 298 | self.current_time_elapsed = 0 299 | original_total_time_elapsed = self.total_time_elapsed 300 | 301 | # if problem class instance is loaded, priority_queue is saved as list, 302 | # so convert back to PriorityQueue 303 | if type(self.priority_queue) is not PriorityQueue: 304 | priority_queue = PriorityQueue() 305 | for item in self.priority_queue: 306 | priority_queue.put(item) 307 | self.priority_queue = priority_queue 308 | 309 | # evaluate solutions 310 | while not self.priority_queue.empty(): 311 | if self._terminate(iters_limit, time_limit): 312 | self.logger.info( 313 | 'Iterations/time limit reached, terminating algorithm') 314 | self.terminate_early = True 315 | break 316 | 317 | # get solution from priority queue 318 | if self.bnb_selection_strategy ==\ 319 | BnBSelectionStrategy.DEPTH_FIRST_BEST_FIRST: 320 | depth, sol_lb, _, sol = self.priority_queue.get() 321 | else: 322 | sol_lb, depth, _, sol = self.priority_queue.get() 323 | 324 | # evaluate solution 325 | branch = self._evaluate_solution(sol, bnb_type, sol_lb) 326 | 327 | # put solutions obtained through branching on priority queue 328 | if branch: 329 | for next_sol in self.branch(sol): 330 | next_sol_lb = self.lbound(next_sol) 331 | self.sol_count += 1 332 | if self.bnb_selection_strategy ==\ 333 | BnBSelectionStrategy.DEPTH_FIRST_BEST_FIRST: 334 | self.priority_queue.put( 335 | (depth - 1, next_sol_lb, self.sol_count, next_sol)) 336 | else: 337 | self.priority_queue.put( 338 | (next_sol_lb, depth - 1, self.sol_count, next_sol)) 339 | 340 | # update progress and log 341 | self._update_progress_and_log( 342 | start, original_total_time_elapsed, log_iters) 343 | 344 | def solve(self, iters_limit=1e6, log_iters=100, time_limit=3600, 345 | bnb_type=0): 346 | if self.bnb_selection_strategy == BnBSelectionStrategy.DEPTH_FIRST: 347 | self._solve_df(iters_limit, log_iters, time_limit, bnb_type) 348 | elif self.bnb_selection_strategy in { 349 | BnBSelectionStrategy.BEST_FIRST_DEPTH_FIRST, 350 | BnBSelectionStrategy.DEPTH_FIRST_BEST_FIRST}: 351 | self._solve_dfbef_befdf( 352 | iters_limit, log_iters, time_limit, bnb_type) 353 | else: 354 | raise Exception( 355 | f'Invalid value for bnb_selection_strategy, must be one of ' 356 | + 'the following BnBSelectionStrategy enum values: DEPTH_FIRST' 357 | + 'DEPTH_FIRST_BEST_FIRST, BEST_FIRST_DEPTH_FIRST') 358 | 359 | # log results, return best solution and best solution cost 360 | self._log_results(log_iters, force=True) 361 | return self.best_solution, self.best_cost 362 | 363 | def persist(self): 364 | if self.bnb_selection_strategy in { 365 | BnBSelectionStrategy.DEPTH_FIRST_BEST_FIRST, 366 | BnBSelectionStrategy.BEST_FIRST_DEPTH_FIRST}: 367 | # convert the priority queue to a list before saving solution 368 | self.priority_queue = list(self.priority_queue.queue) 369 | elif self.bnb_selection_strategy == BnBSelectionStrategy.DEPTH_FIRST: 370 | # convert generators in stack to list 371 | self.stack = list(map(lambda i: (i[0], list(i[1])), self.stack)) 372 | else: 373 | raise Exception( 374 | f'Invalid value for bnb_selection_strategy, must be one of ' 375 | + 'the following BnBSelectionStrategy enum values: DEPTH_FIRST' 376 | + 'DEPTH_FIRST_BEST_FIRST, BEST_FIRST_DEPTH_FIRST') 377 | super().persist() 378 | -------------------------------------------------------------------------------- /optimizn/combinatorial/opt_problem.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import pickle 5 | import os 6 | from datetime import datetime 7 | from optimizn.utils import get_logger 8 | from copy import deepcopy 9 | 10 | 11 | class OptProblem(): 12 | def __init__(self, params, logger=None): 13 | ''' Initialize the problem ''' 14 | self.name = self.__class__.__name__ 15 | self.params = params 16 | if logger is None: 17 | self.logger = get_logger(f'{self.name}_logger') 18 | else: 19 | self.logger = logger 20 | self.init_time = datetime.now() 21 | self.init_secs = int(self.init_time.timestamp()) 22 | self.init_solution = self.get_initial_solution() 23 | if self.init_solution is not None: 24 | self.init_cost = self.cost(self.init_solution) 25 | else: 26 | self.init_cost = float('inf') 27 | self.best_solution = deepcopy(self.init_solution) 28 | self.best_cost = deepcopy(self.init_cost) 29 | self.logger.info(f'Initial solution: {self.init_solution}') 30 | self.logger.info(f'Initial solution cost: {self.init_cost}') 31 | 32 | def get_initial_solution(self): 33 | ''' Gets the initial solution.''' 34 | raise NotImplementedError( 35 | "Implement a function to get the initial solution") 36 | 37 | def cost(self, sol): 38 | ''' Gets the cost for a given solution.''' 39 | raise NotImplementedError( 40 | "Implement a function to compute the cost of a given solution") 41 | 42 | def cost_delta(self, cost1, cost2): 43 | return cost1 - cost2 44 | 45 | def persist(self): 46 | create_folders(self.name) 47 | existing_obj = load_latest_pckl( 48 | "Data//" + self.name + "//DailyObj", self.logger) 49 | if existing_obj is None: 50 | self.obj_changed = True 51 | else: 52 | self.obj_changed = (existing_obj != self.params) 53 | if self.obj_changed: 54 | # Write the latest input object that has changed. 55 | f_name = "Data//" + self.name + "//DailyObj//" +\ 56 | str(self.init_secs) + ".obj" 57 | file1 = open(f_name, 'wb') 58 | pickle.dump(self.params, file1) 59 | file1.close() 60 | self.logger.info("Wrote to DailyObj") 61 | # Write the optimization object. 62 | f_name = "Data//" + self.name + "//DailyOpt//" + str(self.init_secs)\ 63 | + ".obj" 64 | file1 = open(f_name, 'wb') 65 | pickle.dump(self, file1) 66 | file1.close() 67 | self.logger.info("Wrote to DailyOpt") 68 | 69 | # Now check if the current best is better than the global best 70 | existing_best = load_latest_pckl( 71 | "Data//" + self.name + "//GlobalOpt", self.logger) 72 | if existing_best is None or self.cost_delta( 73 | self.best_cost, existing_best.best_cost) < 0\ 74 | or self.obj_changed: 75 | f_name = "Data//" + self.name + "//GlobalOpt//" +\ 76 | str(self.init_secs) + ".obj" 77 | file1 = open(f_name, 'wb') 78 | pickle.dump(self, file1) 79 | file1.close() 80 | self.logger.info("Wrote to GlobalOpt") 81 | 82 | 83 | def create_folders(name): 84 | if not os.path.exists("Data//"): 85 | os.mkdir("Data//") 86 | if not os.path.exists("Data//" + name + "//"): 87 | os.mkdir("Data//" + name + "//") 88 | if not os.path.exists("Data//" + name + "//DailyObj//"): 89 | os.mkdir("Data//" + name + "//DailyObj//") 90 | if not os.path.exists("Data//" + name + "//DailyOpt//"): 91 | os.mkdir("Data//" + name + "//DailyOpt//") 92 | if not os.path.exists("Data//" + name + "//GlobalOpt//"): 93 | os.mkdir("Data//" + name + "//GlobalOpt//") 94 | 95 | 96 | def load_latest_pckl(path1="Data/DailyObj", logger=None): 97 | if logger is None: 98 | logger = get_logger('optimizn_logger') 99 | if not os.path.exists(path1): 100 | logger.warning(f'No file located at {path1}') 101 | return None 102 | msh_files = os.listdir(path1) 103 | msh_files = [i for i in msh_files if not i.startswith('.')] 104 | msh_files = sorted(msh_files) 105 | if len(msh_files) > 0: 106 | latest_file = msh_files[len(msh_files)-1] 107 | filepath = path1 + "//" + latest_file 108 | if os.path.getsize(filepath) == 0: 109 | logger.warning(f'File located at {filepath} is empty') 110 | else: 111 | filehandler = open(filepath, 'rb') 112 | existing_obj = pickle.load(filehandler) 113 | filehandler.close() 114 | return existing_obj 115 | return None 116 | -------------------------------------------------------------------------------- /optimizn/combinatorial/simulated_annealing.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | from numpy.random import uniform 5 | # from numpy import e 6 | import numpy as np 7 | from copy import deepcopy 8 | from optimizn.combinatorial.opt_problem import OptProblem 9 | import time 10 | import warnings 11 | 12 | 13 | class SimAnnealProblem(OptProblem): 14 | def __init__(self, params, logger=None): 15 | ''' Initialize the problem ''' 16 | super().__init__(params, logger) 17 | self.candidate = self._make_copy(self.init_solution) 18 | self.current_cost = self._make_copy(self.init_cost) 19 | self.total_iters = 0 20 | self.iters_since_reset = -1 21 | self.total_time_elapsed = 0 22 | 23 | def next_candidate(self, candidate): 24 | ''' Switch to the next candidate, given the current candidate. ''' 25 | raise NotImplementedError( 26 | "Implement a function to produce the next candidate solution " 27 | + "from the current candidate solution") 28 | 29 | def reset_candidate(self): 30 | ''' 31 | Returns a new solution for when the candidate solution is reset. 32 | Defaults to get_initial_solution but can be overridden if needed 33 | ''' 34 | return self.get_initial_solution() 35 | 36 | def get_temperature(self, iters): 37 | ''' 38 | Calculates the temperature based on a given number of iterations. 39 | Defaults to s_curve, can be overridden 40 | ''' 41 | return s_curve(iters) 42 | 43 | def _log_results(self, iters, time_elapsed): 44 | self.logger.info("Iterations (total): " + str(self.total_iters)) 45 | self.logger.info("Iterations (current): " + str(iters)) 46 | self.logger.info("Time elapsed (total): " 47 | + str(self.total_time_elapsed) + " seconds") 48 | self.logger.info("Time elapsed (current): " + str(time_elapsed) 49 | + " seconds") 50 | self.logger.info("Best solution: " + str(self.best_solution)) 51 | self.logger.info("Best solution cost: " + str(self.best_cost)) 52 | 53 | def anneal(self, n_iter=100000, reset_p=1/10000, time_limit=3600, 54 | log_iters=10000): 55 | ''' 56 | This simulated annealing implementation is based on the following 57 | sources. The code presented in source [2] is licensed under the MIT 58 | License. The original license text is shown in the NOTICE.md file. 59 | 60 | Sources: 61 | 62 | [1] T. W. Schneider, "The traveling salesman with simulated annealing, 63 | r, and shiny." 64 | https://toddwschneider.com/posts/traveling-salesman-with-simulated-annealing-r-and-shiny/, 65 | September 2014. Online; accessed 8-January-2023. 66 | 67 | [2] T. W. Schneider, "shiny-salesman/helpers.r." 68 | https://github.com/toddwschneider/shiny-salesman/blob/master/helpers.R, 69 | October 2014. Online; accessed 8-January-2023. 70 | 71 | [3] R. A. Rutenbar, "Simulated annealing algorithms: An overview," IEEE 72 | Circuits and Devices Magazine, vol. 5, pp. 19-26, January 1989. 73 | https://www.cs.amherst.edu/~ccmcgeoch/cs34/papers/rutenbar.pdf. Online; 74 | accessed 8-January-2024. 75 | ''' 76 | reset = False 77 | time_elapsed = 0 78 | iters = 0 79 | original_time_elapsed = self.total_time_elapsed 80 | start = time.time() 81 | for _ in range(n_iter): 82 | # check if time limit exceeded 83 | if time_elapsed >= time_limit: 84 | self.logger.info( 85 | 'Time limit reached/exceeded, terminating algorithm') 86 | break 87 | self.iters_since_reset = self.iters_since_reset + 1 88 | temp = self.get_temperature(self.iters_since_reset) 89 | # eps = 0.3 * e**(-i/n_iter) 90 | if np.random.uniform() < reset_p: 91 | self.logger.info("Resetting candidate solution.") 92 | self.new_candidate = self.reset_candidate() 93 | self.new_cost = self.cost(self.new_candidate) 94 | self.logger.info("with cost: " + str(self.new_cost)) 95 | self.iters_since_reset = 0 96 | reset = True 97 | else: 98 | self.new_candidate = self.next_candidate(self.candidate) 99 | self.new_cost = self.cost(self.new_candidate) 100 | cost_del = self.cost_delta(self.new_cost, self.current_cost) 101 | if temp <= 0: 102 | # if temperature value is not greater than 0, candidate should 103 | # not be updated to a less optimal new candidate 104 | eps = 0 105 | else: 106 | # treat runtime warnings like errors, to catch overflow 107 | # warnings 108 | warnings.filterwarnings( 109 | "error", category=RuntimeWarning) 110 | try: 111 | # see if overflow occurs 112 | eps = np.exp(-1 * cost_del / temp) 113 | except RuntimeWarning: 114 | # overflow occurred 115 | 116 | # if cost delta is positive, then eps will be very close to 117 | # 0, so eps is set to 0 118 | if cost_del > 0: 119 | eps = 0 120 | # if cost delta is negative, then eps will be very large 121 | # (larger than any value sampled from uniform distribution 122 | # on [0, 1)), so eps is set to 1 123 | else: 124 | eps = 1 125 | # reset warnings 126 | warnings.resetwarnings() 127 | 128 | if cost_del < 0 or uniform() < eps or reset: 129 | self.update_candidate(self.new_candidate, self.new_cost) 130 | if reset: 131 | reset = False 132 | if self.cost_delta(self.new_cost, self.best_cost) < 0: 133 | self.update_best(self.new_candidate, self.new_cost) 134 | self.logger.info("Best cost updated to:" + str(self.new_cost)) 135 | 136 | self.total_iters += 1 137 | iters += 1 138 | time_elapsed = time.time() - start 139 | self.total_time_elapsed = original_time_elapsed + time_elapsed 140 | if iters == 1 or iters % int(log_iters) == 0: 141 | self._log_results(iters, time_elapsed) 142 | 143 | # log results, return best solution and best solution cost 144 | self._log_results(iters, time_elapsed) 145 | return self.best_solution, self.best_cost 146 | 147 | def update_candidate(self, candidate, cost): 148 | self.candidate = self._make_copy(candidate) 149 | self.current_cost = self._make_copy(cost) 150 | 151 | def update_best(self, candidate, cost): 152 | self.best_solution = self._make_copy(candidate) 153 | self.best_cost = self._make_copy(cost) 154 | 155 | def _make_copy(self, obj): 156 | return deepcopy(obj) 157 | 158 | 159 | def s_curve(x, amplitude=4000, center=0, width=3000): 160 | # treat runtime warnings like errors, to catch overflow warnings 161 | warnings.filterwarnings("error", category=RuntimeWarning) 162 | try: 163 | res = 1 / (1 + np.exp((x - center) / width)) 164 | except RuntimeWarning: 165 | # overflow occurred 166 | 167 | # np.exp term in the denominator is very large and the result is very 168 | # close to 0 169 | res = 0 170 | # reset warnings 171 | warnings.resetwarnings() 172 | return amplitude * res 173 | -------------------------------------------------------------------------------- /optimizn/combinatorial/toy_problems.txt: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /optimizn/continuous_training/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /optimizn/continuous_training/continuous_training.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import os 5 | import pickle 6 | from datetime import datetime 7 | import logging 8 | from optimizn.utils import get_logger 9 | 10 | 11 | class ContinuousTraining: 12 | def __init__(self, logger=None): 13 | self.init_time = datetime.now() 14 | self.init_secs = int(self.init_time.timestamp()) 15 | if logger is None: 16 | self.logger = get_logger(f'{self.name}_logger') 17 | else: 18 | self.logger = logger 19 | 20 | def persist(self): 21 | # set name attribute, check for params attribute 22 | self.name = self.__class__.__name__ 23 | if not hasattr(self, 'params'): 24 | raise Exception( 25 | 'All problem class instances must have a "params" attribute, ' 26 | + 'which is an object that contains the input parameters ' 27 | + 'to the problem class') 28 | 29 | create_folders(self.name) 30 | existing_obj = load_latest_pckl( 31 | "Data//" + self.name + "//DailyObj", self.logger) 32 | self.obj_changed = (existing_obj != self.params) 33 | if self.obj_changed or existing_obj is None: 34 | # Write the latest input object that has changed. 35 | f_name = "Data//" + self.name + "//DailyObj//" +\ 36 | str(self.init_secs) + ".obj" 37 | file1 = open(f_name, 'wb') 38 | pickle.dump(self.params, file1) 39 | self.logger.info("Wrote to DailyObj") 40 | # Write the optimization object. 41 | f_name = "Data//" + self.name + "//DailyOpt//" + str(self.init_secs)\ 42 | + ".obj" 43 | file1 = open(f_name, 'wb') 44 | pickle.dump(self, file1) 45 | self.logger.info("Wrote to DailyOpt") 46 | 47 | # Now check if the current best is better than the global best 48 | existing_best = load_latest_pckl( 49 | "Data//" + self.name + "//GlobalOpt", self.logger) 50 | if existing_best is None or self.best_cost < existing_best.best_cost\ 51 | or self.obj_changed: 52 | f_name = "Data//" + self.name + "//GlobalOpt//" +\ 53 | str(self.init_secs) + ".obj" 54 | file1 = open(f_name, 'wb') 55 | pickle.dump(self, file1) 56 | self.logger.info("Wrote to GlobalOpt") 57 | 58 | 59 | def create_folders(name): 60 | if not os.path.exists("Data//"): 61 | os.mkdir("Data//") 62 | if not os.path.exists("Data//" + name + "//"): 63 | os.mkdir("Data//" + name + "//") 64 | if not os.path.exists("Data//" + name + "//DailyObj//"): 65 | os.mkdir("Data//" + name + "//DailyObj//") 66 | if not os.path.exists("Data//" + name + "//DailyOpt//"): 67 | os.mkdir("Data//" + name + "//DailyOpt//") 68 | if not os.path.exists("Data//" + name + "//GlobalOpt//"): 69 | os.mkdir("Data//" + name + "//GlobalOpt//") 70 | 71 | 72 | def load_latest_pckl(path1="Data/DailyObj", logger=None): 73 | if not os.path.exists(path1): 74 | return None 75 | msh_files = os.listdir(path1) 76 | msh_files = [i for i in msh_files if not i.startswith('.')] 77 | msh_files = sorted(msh_files) 78 | if len(msh_files) > 0: 79 | latest_file = msh_files[len(msh_files)-1] 80 | filepath = path1 + "//" + latest_file 81 | if os.path.getsize(filepath) == 0: 82 | if logger is None: 83 | logger = logging.getLogger('optimizn_logger') 84 | logger.setLevel(logging.INFO) 85 | logger.info('File located at', filepath, 'is empty') 86 | else: 87 | filehandler = open(filepath, 'rb') 88 | existing_obj = pickle.load(filehandler) 89 | return existing_obj 90 | return None 91 | -------------------------------------------------------------------------------- /optimizn/lagrange_examples.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | from sympy import * 6 | 7 | 8 | x, y, z, l = symbols('x y z l') 9 | #k, m, n = symbols('k m n', integer=True) 10 | #f, g, h = map(Function, 'fgh') 11 | 12 | ((x+y)**2 * (x+1)).expand() 13 | 14 | 15 | ## Unfortunately, produces just one solution, not all. 16 | solve([Eq(3*x**2+2*y*z-2*x*l,0), 17 | Eq(2*x*z-2*y*l,0), 18 | Eq(2*x**2-2*z-2*z*l,0), 19 | Eq(x**2+y**2+z**2,1)], [x,y,z,l]) 20 | 21 | F = [f1, f2, f3, f4] = [3*x**2+2*y*z-2*x*l, 2*x*z-2*y*l, 2*x**2-2*z-2*z*l, x**2+y**2+z**2-1] 22 | 23 | gb = groebner([f1, f2, f3, f4], l, x, y, z, order='lex') 24 | 25 | gb[len(gb)-1] 26 | 27 | 28 | -------------------------------------------------------------------------------- /optimizn/problems/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. -------------------------------------------------------------------------------- /optimizn/problems/binary_assignment.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | ## Binary assignment for equal spoils. 5 | import numpy as np 6 | import cvxpy as cp 7 | import optimizn.problems.sample_ci as sci 8 | 9 | 10 | def formultn1(ci=sci.candy_preferences): 11 | #ci = np.array([10,7,6,3]) 12 | x = cp.Variable(len(ci),boolean=True) 13 | objective = cp.Minimize(cp.sum_squares(ci@(2*x-1))) 14 | problm = cp.Problem(objective) 15 | #res = problm.solve(solver='GLPK') 16 | #_ = problm.solve(solver=cp.GLPK_MI) 17 | _ = problm.solve() 18 | return x.value 19 | 20 | 21 | def formulatn2(ci=sci.candy_preferences): 22 | ''' 23 | This formulation is from the following source. 24 | 25 | Source: 26 | 27 | (1) 28 | Title: Binary assignment - distributing candies. 29 | Author: Rohit Pandey (author of question), RobPratt (author of answer) 30 | URL: https://math.stackexchange.com/questions/3515223/binary-assignment-distributing-candies/3515391#3515391 31 | Date published: January 19, 2020 32 | Date accessed: January 19, 2020 33 | ''' 34 | #ci = np.array([10,7,6,3]) 35 | z = cp.Variable() 36 | x = cp.Variable(len(ci),boolean=True) 37 | constraints = [ci@x<=z, ci@(1-x)<=z] 38 | #constraints = [sum(ci*x)<=z,sum(ci*(1-x))<=z] 39 | objective = cp.Minimize(z+0*sum(x)) 40 | problm = cp.Problem(objective,constraints) 41 | #_ = problm.solve(solver=cp.GLPK_MI) 42 | _ = problm.solve() 43 | return x.value 44 | 45 | 46 | def formultn3(ri=np.array([1,3,5,2,4,6]),\ 47 | ui=np.array([1,1,1,1,1,1])): 48 | z1 = cp.Variable() 49 | z2 = cp.Variable() 50 | x = cp.Variable(len(ri),boolean=True) 51 | constraints = [ri@x<=z1, ri@(1-x)<=z1,\ 52 | ui@x<=z1, ui@(1-x)<=z2] 53 | objective = cp.Minimize(z1+z2) 54 | problm = cp.Problem(objective,constraints) 55 | _ = problm.solve() 56 | return x.value 57 | 58 | -------------------------------------------------------------------------------- /optimizn/problems/int_allocn.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | import cvxpy as cp 6 | import pandas as pd 7 | 8 | 9 | def integer_probs(p,m,n): 10 | """ 11 | We want to allocate an integer vector in a way that each element 12 | get's atleast m, total items is n and the probabilities are as 13 | close to p as possible. 14 | """ 15 | x = cp.Variable(len(p),integer=True) 16 | constraints = [m <= x, x <= n, sum(x) == n] 17 | #objective = cp.Minimize(cp.sum_squares((x-m)/(n-m*len(p)) - p)) 18 | objective = cp.Minimize(cp.sum_squares(x/n - p)) 19 | problm = cp.Problem(objective, constraints) 20 | _ = problm.solve() 21 | return x.value 22 | 23 | def integer_probs_v2(p,m,n): 24 | """ 25 | We want to allocate an integer vector in a way that each element 26 | get's atleast m, total items is n and the probabilities are as 27 | close to p as possible. 28 | """ 29 | x = cp.Variable(len(p)) 30 | constraints = [m <= x, x <= n, sum(x) == n] 31 | objective = cp.Minimize(cp.sum_squares(x/n - p)) 32 | problm = cp.Problem(objective, constraints) 33 | _ = problm.solve() 34 | h = redistribute(x.value) 35 | return h 36 | 37 | def integer_probs_v3(p,m,n): 38 | x = cp.Variable(len(p),integer=True) 39 | z = cp.Variable() 40 | objective = cp.Minimize(z) 41 | constraints = [m <= x, x <= n, sum(x) == n, \ 42 | (x-m)/(n-m*len(p))-p<=z, p-(x-m)/(n-m*len(p))<=z] 43 | #(x)/(n)-p<=z, p-(x)/(n)<=z] 44 | problm = cp.Problem(objective, constraints) 45 | _ = problm.solve() 46 | return x.value 47 | 48 | def redistribute(x_value): 49 | """ 50 | Given an array of floats, converts them into ints. Does 51 | this by taking the excess fractional part and re-distributing 52 | it in the same proportion. 53 | """ 54 | vals = x_value // 1 55 | excess = int(sum(x_value % 1)) 56 | excess_vals_unif = np.ones(len(x_value))* excess//len(x_value) 57 | excess_vals_nonunif = np.concatenate((np.ones(excess % len(x_value)),\ 58 | np.zeros(len(x_value)-excess%len(x_value)))\ 59 | ,axis=0) 60 | h = vals + excess_vals_unif + excess_vals_nonunif 61 | return h 62 | 63 | 64 | def tst_optimizn(m=2,excess=40): 65 | p=np.random.rand(200) 66 | p=np.sort(p) 67 | p=p/sum(p) 68 | n=m*len(p)+excess 69 | x2 = integer_probs(p,m,n) 70 | x3 = integer_probs_v3(p,m,n) 71 | df = pd.DataFrame() 72 | df["p"]=p 73 | df["x3"]=x3 74 | df["prct_x3"] = x3/sum(x3) 75 | df["x2"]=x2 76 | df["prct_x2"] = x2/sum(x2) 77 | return df 78 | 79 | -------------------------------------------------------------------------------- /optimizn/problems/sample_ci.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | 6 | candy_preferences = np.array([2770253, 7 | 2283896, 8 | 2090222, 9 | 1532171, 10 | 1440599, 11 | 1057236, 12 | 920677, 13 | 692650, 14 | 604649, 15 | 551498, 16 | 550491, 17 | 513868, 18 | 417988, 19 | 392091, 20 | 276456, 21 | 253170, 22 | 216288, 23 | 125667, 24 | 118655, 25 | 62483, 26 | 36357, 27 | 31081, 28 | 26325, 29 | 24457, 30 | 22893, 31 | 12412, 32 | 3737, 33 | 3227, 34 | 2460, 35 | 2092, 36 | 1789, 37 | 1329, 38 | 507, 39 | 475, 40 | 240, 41 | 154, 42 | 89, 43 | 30, 44 | 23, 45 | 18, 46 | 13, 47 | 9, 48 | 7, 49 | 4, 50 | 3, 51 | 2]) 52 | -------------------------------------------------------------------------------- /optimizn/reinforcement_learning/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /optimizn/reinforcement_learning/multi_armed_bandit.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import time 5 | from optimizn.continuous_training.continuous_training import ContinuousTraining 6 | import logging 7 | 8 | 9 | class MultiArmedBandit(ContinuousTraining): 10 | ''' 11 | This multi-armed bandit implementation is based on the following sources. 12 | 13 | Sources: 14 | 15 | (1) 16 | Title: Chapter 9: Applications to Computing, 9.8: Multi-Armed Bandits 17 | (From “Probability & Statistics with Applications to Computing” by Alex 18 | Tsun) 19 | Author: Alex Tsun 20 | URL: https://web.stanford.edu/class/archive/cs/cs109/cs109.1218/files/student_drive/9.8.pdf 21 | Date accessed: November 20, 2023 22 | 23 | (2) 24 | Title: Multi-arm Bandits, The simplest reinforcement learning problem 25 | Author: Doina Precup 26 | URL: https://www.cs.mcgill.ca/~dprecup/courses/RL/Lectures/2-bandits-2019.pdf 27 | Date published: 2019 28 | Date accessed: November 20, 2023 29 | ''' 30 | def __init__(self, n_arms, init_pulls, logger=None): 31 | self.n_arms = n_arms 32 | self.init_pulls = init_pulls 33 | self.arm_pulls = [0] * self.n_arms 34 | self.est_exp_reward = [0] * self.n_arms 35 | super().__init__(logger) 36 | self.logger.info(f'Performing {self.init_pulls} initial pulls...') 37 | self.run(self.init_pulls) 38 | self.logger.info('Completed initial pulls') 39 | 40 | def choose_arm(self): 41 | ''' 42 | Selects an arm of the multi-armed bandit to pull 43 | ''' 44 | raise NotImplementedError( 45 | 'Implement a function to select an arm of the multi-arm bandit') 46 | 47 | def pull_arm(self, arm): 48 | ''' 49 | Pulls the given arm of the multi-armed bandit and receives an outcome 50 | ''' 51 | raise NotImplementedError( 52 | 'Implement a function to pull a given arm of the multi-arm bandit ' 53 | + 'and produce some outcome') 54 | 55 | def reward(self, arm, outcome): 56 | ''' 57 | Calculates the reward of a given outcome 58 | ''' 59 | raise NotImplementedError( 60 | 'Implement a function to calculate the reward of a given outcome') 61 | 62 | def new_exp_reward(self, arm, reward): 63 | ''' 64 | Calculated the new estimated expected reward of pulling a given arm of 65 | the multi-arm bandit 66 | ''' 67 | raise NotImplementedError( 68 | 'Implement a function to calculate the new estimated expected ' 69 | + 'reward of pulling a given arm of the multi-arm bandit') 70 | 71 | def process_result(self, arm, outcome, reward): 72 | ''' 73 | Performs additional processing on the result, should the use case 74 | require it 75 | 76 | Default to nothing, can be overridden should the use case require it 77 | ''' 78 | pass 79 | 80 | def log_results(self): 81 | self.logger.info(f'Arm pulls: {self.arm_pulls}') 82 | self.logger.info('Estimated expected reward for each arm: ' 83 | + f'{self.est_exp_reward}') 84 | 85 | def run(self, n_iters=1e6, log_iters=100, time_limit=3600): 86 | start_time = time.time() 87 | for iter in range(n_iters): 88 | # pull an arm to get some outcome 89 | arm = self.choose_arm() 90 | outcome = self.pull_arm(arm) 91 | self.arm_pulls[arm] += 1 92 | 93 | # calculate reward of the outcome, updated expected reward for 94 | # pulled arm 95 | reward = self.reward(arm, outcome) 96 | self.est_exp_reward[arm] = self.new_exp_reward(arm, reward) 97 | 98 | # process the result of the arm pull 99 | self.process_result(arm, outcome, reward) 100 | 101 | if (iter + 1) % log_iters == 0: 102 | self.logger.info(f'Iteration: {iter + 1}') 103 | self.log_results() 104 | 105 | # check if time limit has been exceeded 106 | time_elapsed = time.time() - start_time 107 | if time_elapsed >= time_limit: 108 | self.logger.info('Time limit reached, terminating algorithm') 109 | self.log_results() 110 | return 111 | self.logger.info('Number of iterations reached, terminating algorithm') 112 | self.log_results() 113 | -------------------------------------------------------------------------------- /optimizn/samples/lagrange_examples.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | from sympy import * 6 | 7 | 8 | x, y, z, l = symbols('x y z l') 9 | #k, m, n = symbols('k m n', integer=True) 10 | #f, g, h = map(Function, 'fgh') 11 | 12 | ((x+y)**2 * (x+1)).expand() 13 | 14 | 15 | ## Unfortunately, produces just one solution, not all. 16 | solve([Eq(3*x**2+2*y*z-2*x*l,0), 17 | Eq(2*x*z-2*y*l,0), 18 | Eq(2*x**2-2*z-2*z*l,0), 19 | Eq(x**2+y**2+z**2,1)], [x,y,z,l]) 20 | 21 | F = [f1, f2, f3, f4] = [3*x**2+2*y*z-2*x*l, 2*x*z-2*y*l, 2*x**2-2*z-2*z*l, x**2+y**2+z**2-1] 22 | 23 | gb = groebner([f1, f2, f3, f4], l, x, y, z, order='lex') 24 | 25 | gb[len(gb)-1] 26 | 27 | 28 | -------------------------------------------------------------------------------- /optimizn/trees/pprnt.py: -------------------------------------------------------------------------------- 1 | # Copied from: https://leetcode.com/discuss/interview-question/1954462/pretty-printing-binary-trees-in-python-for-debugging 2 | 3 | def display(root): 4 | lines, *_ = _display_aux(root) 5 | for line in lines: 6 | print(line) 7 | 8 | 9 | def _display_aux(self): 10 | """Returns list of strings, width, height, and horizontal coordinate of the root.""" 11 | # No child. 12 | if self.right is None and self.left is None: 13 | line = '%s' % self.val 14 | width = len(line) 15 | height = 1 16 | middle = width // 2 17 | return [line], width, height, middle 18 | 19 | # Only left child. 20 | if self.right is None: 21 | lines, n, p, x = _display_aux(self.left) 22 | s = '%s' % self.val 23 | u = len(s) 24 | first_line = (x + 1) * ' ' + (n - x - 1) * '_' + s 25 | second_line = x * ' ' + '/' + (n - x - 1 + u) * ' ' 26 | shifted_lines = [line + u * ' ' for line in lines] 27 | return [first_line, second_line] + shifted_lines, n + u, p + 2, n + u // 2 28 | 29 | # Only right child. 30 | if self.left is None: 31 | lines, n, p, x = _display_aux(self.right) 32 | s = '%s' % self.val 33 | u = len(s) 34 | first_line = s + x * '_' + (n - x) * ' ' 35 | second_line = (u + x) * ' ' + '\\' + (n - x - 1) * ' ' 36 | shifted_lines = [u * ' ' + line for line in lines] 37 | return [first_line, second_line] + shifted_lines, n + u, p + 2, u // 2 38 | 39 | # Two children. 40 | left, n, p, x = _display_aux(self.left) 41 | right, m, q, y = _display_aux(self.right) 42 | s = '%s' % self.val 43 | u = len(s) 44 | first_line = (x + 1) * ' ' + (n - x - 1) * '_' + s + y * '_' + (m - y) * ' ' 45 | second_line = x * ' ' + '/' + (n - x - 1 + u + y) * ' ' + '\\' + (m - y - 1) * ' ' 46 | if p < q: 47 | left += [n * ' '] * (q - p) 48 | elif q < p: 49 | right += [m * ' '] * (p - q) 50 | zipped_lines = zip(left, right) 51 | lines = [first_line, second_line] + [a + u * ' ' + b for a, b in zipped_lines] 52 | return lines, n + m + u, max(p, q) + 2, n + u // 2 53 | -------------------------------------------------------------------------------- /optimizn/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | 5 | def get_logger(logger_name='optimizn_logger'): 6 | # create logger 7 | logger = logging.getLogger(logger_name) 8 | logger.setLevel(logging.INFO) 9 | logger.handlers.clear() 10 | 11 | # add handler to log to stdout 12 | handler = logging.StreamHandler(sys.stdout) 13 | handler.setLevel(logging.INFO) 14 | handler.setFormatter(logging.Formatter( 15 | '%(asctime)s|%(levelname)s|%(message)s')) 16 | logger.addHandler(handler) 17 | return logger -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | cvxpy 4 | graphing 5 | python-tsp -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | from setuptools import setup, find_packages 5 | 6 | INSTALL_DEPS = ['numpy', 7 | 'scipy', 8 | 'cvxpy' 9 | ] 10 | 11 | TEST_DEPS = ['pytest'] 12 | DEV_DEPS = [] 13 | 14 | setup(name='optimizn', 15 | version='0.0.24', 16 | author='Rohit Pandey, Akshay Sathiya', 17 | author_email='rohitpandey576@gmail.com, akshay.sathiya@gmail.com', 18 | description='A Python library for developing customized optimization ' 19 | + 'algorithms under general paradigms.', 20 | # when running experiments, remove experiments from the exclude 21 | # list below 22 | packages=find_packages(exclude=['tests', 'experiments', 'Images']), 23 | long_description='A Python library for developing customized ' 24 | + 'optimization algorithms under general paradigms like ' 25 | + 'simulated annealing and branch and bound. Also features '\ 26 | + 'continuous training, so algorithms can be run multiple times ' 27 | + 'and resume from where they left off in previous runs.', 28 | zip_safe=False, 29 | install_requires=INSTALL_DEPS, 30 | include_package_data=True, 31 | python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4', 32 | # List additional groups of dependencies here (e.g. development 33 | # dependencies). You can install these using the following syntax, 34 | # for example: 35 | # $ pip install -e .[dev,test] 36 | extras_require={ 37 | 'dev': DEV_DEPS, 38 | 'test': TEST_DEPS, 39 | }, 40 | ) 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /tests/combinatorial/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/binpacking/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/binpacking/test_bnb_binpacking.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | from optimizn.combinatorial.algorithms.binpacking.bnb_binpacking import\ 5 | BinPackingParams, BinPackingProblem 6 | from tests.combinatorial.algorithms.check_sol_utils import check_bnb_sol,\ 7 | check_sol_optimality, check_sol_vs_init_sol 8 | import inspect 9 | from optimizn.combinatorial.branch_and_bound import BnBSelectionStrategy 10 | 11 | def test_param_equality(): 12 | TEST_CASES = [ 13 | # test case: (first instance of bin packing parameters, second instance 14 | # of bin packing parameters, boolean for whether instances are equal) 15 | (BinPackingParams([1, 2, 3, 4], [6]), None, False), 16 | (BinPackingParams([1, 2, 3, 4], [7]), BinPackingParams( 17 | [1, 2, 3, 4], [6]), False), 18 | (BinPackingParams([1, 2, 3, 4], [6]), BinPackingParams( 19 | [1, 2, 3, 4], [6]), True) 20 | ] 21 | for params1, params2, equal in TEST_CASES: 22 | assert (params1 == params2) == equal, 'Equality check failed for '\ 23 | + f'BinPackingParams:\n{params1}\n{params2}\nExpected to be '\ 24 | + f'equal: {equal}. Actually equal: {(params1 == params2)}' 25 | 26 | 27 | def test_constructor(): 28 | TEST_CASES = [ 29 | # test case: (weights, capacity, expected initial solution) 30 | ([1, 2, 3], 3, {1: {3}, 2: {1, 2}}), 31 | ([7, 8, 2, 3], 15, {1: {2, 1}, 2: {4, 3}}) 32 | ] 33 | for weights, capacity, expected in TEST_CASES: 34 | params = BinPackingParams(weights, capacity) 35 | bpp = BinPackingProblem(params, BnBSelectionStrategy.DEPTH_FIRST) 36 | 37 | # check capacity 38 | assert bpp.capacity == capacity, 'Incorrect capacity. Expected: '\ 39 | + f'{capacity}. Actual: {bpp.capacity}' 40 | 41 | # check item weights 42 | for i in range(len(weights)): 43 | assert bpp.item_weights[i + 1] == weights[i], 'Incorrect weight '\ 44 | + f'for item {i + 1}. Expected: {weights[i]}. Actual: '\ 45 | + f'{bpp.item_weights[i + 1]}' 46 | 47 | # check sorted item weights 48 | for i in range(len(bpp.sorted_item_weights)): 49 | weight, item = bpp.sorted_item_weights[i] 50 | assert bpp.item_weights[item] == weight, 'Incorrect weight for '\ 51 | + f'item {item}. Expected {weight}. Actual: '\ 52 | + f'{bpp.item_weights[item]}' 53 | if i > 0: 54 | assert weight <= bpp.sorted_item_weights[i - 1][0], 'Item '\ 55 | + f'weights not sorted in descending order. Element at '\ 56 | + f'index {i} in sorted list of item weights ({weight}) '\ 57 | + f'is greater than element at index {i - 1} '\ 58 | + f'({bpp.sorted_item_weights[i - 1][0]})' 59 | 60 | # check initial solution 61 | assert bpp.best_solution[0] == expected, 'Incorrect allocation of '\ 62 | + f'items to bins in initial solution. Expected: {expected}. '\ 63 | + f'Actual: {bpp.best_solution[0]}' 64 | idx = len(bpp.sorted_item_weights) - 1 65 | assert bpp.best_solution[1] == idx, 'Incorrect value for index of '\ 66 | + f'last allocated item in sorted-by-decreasing-weight list of '\ 67 | + f'items. Expected: -1. Actual: {bpp.best_solution[1]}' 68 | 69 | 70 | def test_is_feasible(): 71 | TEST_CASES = [ 72 | # test case: (weights, capacity, solution, boolean for whether 73 | # solution is feasible) 74 | ([1, 2, 3], 3, ({1: {3}, 2: {1, 2}}, 2), True), 75 | ([1, 2, 3], 3, ({1: {3}, 2: {2}, 3: {1}}, 2), True), 76 | ([1, 2, 3], 3, ({1: {3}, 2: {2}}, 2), False), 77 | ([1, 2, 3], 3, ({1: {3}, 2: {1}}, 1), False), 78 | ([1, 2, 3], 3, ({1: {2}, 2: {1}}, 2), False) 79 | ] 80 | for weights, capacity, sol, feasible in TEST_CASES: 81 | params = BinPackingParams(weights, capacity) 82 | bpp = BinPackingProblem(params, BnBSelectionStrategy.DEPTH_FIRST) 83 | 84 | # check completeness 85 | is_feasible = bpp.is_feasible(sol) 86 | assert is_feasible == feasible, 'Feasibility check failed for '\ 87 | + f'solution {sol}. Expected to be feasible: {feasible}. '\ 88 | + f'Actually feasible: {is_feasible}' 89 | 90 | 91 | def test_cost(): 92 | TEST_CASES = [ 93 | # test cost: (weights, capacity, solution, expected solution cost) 94 | ([1, 2, 3], 3, ({1: {3}, 2: {1, 2}}, -1), 2), 95 | ([1, 2, 3], 3, ({1: {1, 2}, 2: {3}}, -1), 2), 96 | ([1, 2, 3], 3, ({1: {2}, 2: {3}, 3: {1}}, 2), 3), 97 | ([1, 2, 3], 3, ({1: {3}, 2: {2}, 3: {1}}, 2), 3) 98 | ] 99 | for weights, capacity, sol, cost in TEST_CASES: 100 | params = BinPackingParams(weights, capacity) 101 | bpp = BinPackingProblem(params, BnBSelectionStrategy.DEPTH_FIRST) 102 | 103 | # check cost 104 | sol_cost = bpp.cost(sol) 105 | assert sol_cost == cost, f'Incorrect cost of solution {sol}. '\ 106 | + f'Expected: {cost}. Actual: {sol_cost}' 107 | 108 | 109 | def test_lbound(): 110 | TEST_CASES = [ 111 | # test case: (weights, capacity, solution, expected lower bound 112 | # of solution) 113 | ([1, 2, 3], 3, ({1: {3}, 2: {1, 2}}, -1), 2), 114 | ([1, 2, 3], 3, ({1: {3}}, 0), 2), 115 | ([1, 2, 3], 3, ({1: {3}, 2: {2}}, 1), 2), 116 | ([1, 2, 3], 3, ({1: {3}, 2: {2, 1}}, 2), 2) 117 | ] 118 | for weights, capacity, sol, lb in TEST_CASES: 119 | params = BinPackingParams(weights, capacity) 120 | bpp = BinPackingProblem(params, BnBSelectionStrategy.DEPTH_FIRST) 121 | 122 | # check lower bounds 123 | lbound = bpp.lbound(sol) 124 | assert lbound == lb, f'Incorrect lower bound for solution {sol}. '\ 125 | + f'Expected: {lb}. Actual: {lbound}' 126 | 127 | 128 | def test_branch(): 129 | TEST_CASES = [ 130 | # test case: (weights, capacity, expected list of branched solutions, 131 | # initial solution) 132 | ([1, 2, 3], 3, [({1: {3}, 2: {2}}, 1)], ({1: {3}}, 0)), 133 | ([7, 8, 2, 3], 15, [({1: {1, 2}}, 1), ({1: {2}, 2: {1}}, 1)], 134 | ({1: {2}}, 0)), 135 | ([1, 2, 3, 8, 9, 10, 4, 5, 6, 7], 16, [ 136 | ({1: {6}, 2: {5, 10}, 3: {4}}, 3), 137 | ({1: {6}, 2: {5}, 3: {4, 10}}, 3), 138 | ({1: {6}, 2: {5}, 3: {4}, 4: {10}}, 3) 139 | ], ({1: {6}, 2: {5}, 3: {4}}, 2)), 140 | ([1, 2, 3, 8, 9, 10, 4, 5, 6, 7], 16, [({1: {6}}, 0)], 141 | ({1: {6, 9}, 2: {5, 10}, 3: {4, 8, 3}, 4: {7, 2, 1}}, -1)), 142 | ([1, 2, 3, 8, 9, 10, 4, 5, 6, 7], 16, [({1: {6}, 2: {5}}, 1)], 143 | ({1: {6, 9}, 2: {5, 10}, 3: {4, 8, 3}, 4: {7, 2, 1}}, 0)) 144 | ] 145 | for weights, capacity, expected, init_sol in TEST_CASES: 146 | params = BinPackingParams(weights, capacity) 147 | bpp = BinPackingProblem(params, BnBSelectionStrategy.DEPTH_FIRST) 148 | 149 | # check branched solutions 150 | new_sols = bpp.branch(init_sol) 151 | assert inspect.isgenerator(new_sols),\ 152 | 'Branch function must return generator' 153 | new_sols_list = list(new_sols) 154 | for new_sol in new_sols_list: 155 | assert new_sol in expected, 'Unexpected solution produced by '\ 156 | + f'branching on solution {init_sol}: {new_sol}' 157 | for exp_sol in expected: 158 | assert exp_sol in new_sols_list, f'Expected solution {exp_sol}'\ 159 | + f' was not produced by branching on solution {init_sol}' 160 | 161 | 162 | def test_complete_solution(): 163 | TEST_CASES = [ 164 | # test case: (weights, capacity, incomplete solution, completed 165 | # solution) 166 | ([1, 2, 3], 3, ({1: {3}}, 0), ({1: {3}, 2: {1, 2}}, 2)), 167 | ([7, 8, 2, 3], 15, ({1: {2}}, 0), ({1: {2, 1}, 2: {3, 4}}, 3)), 168 | ([1, 2, 3, 8, 9, 10, 4, 5, 6, 7], 16, 169 | ({1: {6}, 2: {5}, 3: {4}}, 2), 170 | ({1: {6, 9}, 2: {5, 10}, 3: {4, 8, 3}, 4: {7, 2, 1}}, 9)), 171 | ([1, 2, 3, 8, 9, 10, 4, 5, 6, 7], 16, 172 | (dict(), -1), 173 | ({1: {6, 9}, 2: {5, 10}, 3: {4, 8, 3}, 4: {7, 2, 1}}, 9)) 174 | ] 175 | for weights, capacity, incomplete_sol, complete_sol in TEST_CASES: 176 | params = BinPackingParams( 177 | weights, 178 | capacity 179 | ) 180 | bpp = BinPackingProblem(params, BnBSelectionStrategy.DEPTH_FIRST) 181 | 182 | # check completed solution 183 | sol = bpp.complete_solution(incomplete_sol) 184 | assert sol == complete_sol, 'Incorrect completed solution created '\ 185 | + f'from incomplete solution {incomplete_sol}. Expected: '\ 186 | + f'{complete_sol}. Actual: {sol}' 187 | 188 | 189 | def test_bnb_binpacking(): 190 | # weights, capacity, min bins (optimal solution) 191 | TEST_CASES = [ 192 | ([1, 2, 3], 3, 2), 193 | ([7, 8, 2, 3], 15, 2), 194 | ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 12, 5), 195 | ([49, 41, 34, 33, 29, 26, 26, 22, 20, 19], 100, 3), 196 | ([49, 41, 34, 33, 29, 26, 26, 22, 20, 19] * 2, 100, 6) 197 | ] 198 | for bnb_selection_strategy in BnBSelectionStrategy: 199 | for weights, capacity, min_bins in TEST_CASES: 200 | for bnb_type in [0, 1]: 201 | params = BinPackingParams(weights, capacity) 202 | bpp = BinPackingProblem(params, bnb_selection_strategy) 203 | bpp.solve(1000, 100, 120, bnb_type) 204 | 205 | # check final solution 206 | check_bnb_sol(bpp, bnb_type, params) 207 | check_sol_vs_init_sol(bpp.best_cost, bpp.init_cost) 208 | 209 | # check if final solution was within 1.5 * optimal solution 210 | # cost 211 | check_sol_optimality(bpp.best_cost, min_bins, 1.5) 212 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/check_sol_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | def check_bnb_sol(bnb_instance, bnb_type, params): 5 | # determine BnB type 6 | if bnb_type == 0: 7 | bnb_alg = 'traditional' 8 | else: 9 | bnb_alg = 'modified' 10 | 11 | # check that final solution is feasible 12 | assert bnb_instance.is_feasible(bnb_instance.best_solution), 'Final '\ 13 | + f'solution ({bnb_instance.best_solution}) is not feasible. '\ 14 | + f'Algorithm: {bnb_alg} branch and bound. Params: {params}' 15 | 16 | 17 | def check_sol_vs_init_sol(best_cost, init_cost): 18 | # check that final solution is not worse than initial solution 19 | assert best_cost <= init_cost, 'Final solution is less '\ 20 | + f'optimal than initial solution. Cost of initial solution: '\ 21 | + f'{init_cost}. Cost of final solution: {best_cost} ' 22 | 23 | 24 | def check_sol(sol, exp_sols): 25 | assert sol in exp_sols, 'Incorrect final solution. '\ 26 | + f'Expected one of the following: {exp_sols}. Actual: {sol}' 27 | 28 | 29 | def check_sol_optimality(sol_cost, opt_sol_cost, ratio=1.0): 30 | assert sol_cost <= opt_sol_cost * ratio, 'Final solution cost '\ 31 | + f'({sol_cost}) is greater than {ratio} * '\ 32 | + f'optimal solution cost, where optimal solution cost '\ 33 | + f'= {opt_sol_cost}' 34 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/knapsack/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/knapsack/test_bnb_knapsack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | from optimizn.combinatorial.algorithms.knapsack.bnb_knapsack\ 6 | import KnapsackParams, ZeroOneKnapsackProblem 7 | from tests.combinatorial.algorithms.check_sol_utils import check_bnb_sol,\ 8 | check_sol, check_sol_vs_init_sol 9 | from optimizn.combinatorial.branch_and_bound import BnBSelectionStrategy 10 | 11 | 12 | def test_bnb_zeroone_knapsack(): 13 | TEST_CASES = [ 14 | # test case: (weights, values, capacity, initial solution, optimal 15 | # solution) 16 | (np.array([1, 25, 12, 12]), np.array([1, 24, 12, 12]), 25, 17 | [1, 0, 1, 1]), 18 | (np.array([10, 10, 15, 1]), np.array([20, 12, 54, 21]), 25, 19 | [0, 0, 1, 1]), 20 | (np.array([1, 3, 2, 5, 4]), np.array([10, 35, 20, 25, 5]), 4, 21 | [1, 1, 0, 0, 0]) 22 | ] 23 | for bnb_selection_strategy in BnBSelectionStrategy: 24 | for weights, values, capacity, opt_sol in TEST_CASES: 25 | for bnb_type in [0, 1]: 26 | params = KnapsackParams(values, weights, capacity) 27 | kp = ZeroOneKnapsackProblem(params, bnb_selection_strategy) 28 | kp.solve(1000, 100, 120, bnb_type) 29 | 30 | # check final solution 31 | check_bnb_sol(kp, bnb_type, params) 32 | check_sol_vs_init_sol(kp.best_cost, kp.init_cost) 33 | 34 | # check final solution optimality 35 | check_sol(kp.best_solution, [opt_sol]) 36 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/min_path_cover/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/min_path_cover/edges1.csv: -------------------------------------------------------------------------------- 1 | 8.000000000000000000e+00 3.800000000000000000e+01 2 | 2.000000000000000000e+00 4.100000000000000000e+01 3 | 3.000000000000000000e+00 4.200000000000000000e+01 4 | 1.000000000000000000e+00 3.500000000000000000e+01 5 | 2.000000000000000000e+00 3.400000000000000000e+01 6 | 4.000000000000000000e+00 3.600000000000000000e+01 7 | 8.000000000000000000e+00 3.400000000000000000e+01 8 | 6.000000000000000000e+00 4.100000000000000000e+01 9 | 7.000000000000000000e+00 3.300000000000000000e+01 10 | 1.000000000000000000e+00 3.700000000000000000e+01 11 | 7.000000000000000000e+00 3.600000000000000000e+01 12 | 2.000000000000000000e+00 3.600000000000000000e+01 13 | 6.000000000000000000e+00 4.000000000000000000e+01 14 | 7.000000000000000000e+00 3.900000000000000000e+01 15 | 8.000000000000000000e+00 4.200000000000000000e+01 16 | 2.000000000000000000e+00 4.200000000000000000e+01 17 | 7.000000000000000000e+00 4.200000000000000000e+01 18 | 5.000000000000000000e+00 4.200000000000000000e+01 19 | 1.400000000000000000e+01 4.600000000000000000e+01 20 | 1.400000000000000000e+01 4.900000000000000000e+01 21 | 1.200000000000000000e+01 4.300000000000000000e+01 22 | 1.200000000000000000e+01 4.600000000000000000e+01 23 | 1.100000000000000000e+01 4.400000000000000000e+01 24 | 1.200000000000000000e+01 4.900000000000000000e+01 25 | 1.300000000000000000e+01 4.400000000000000000e+01 26 | 1.300000000000000000e+01 5.000000000000000000e+01 27 | 1.600000000000000000e+01 4.300000000000000000e+01 28 | 1.600000000000000000e+01 4.900000000000000000e+01 29 | 1.000000000000000000e+01 4.800000000000000000e+01 30 | 9.000000000000000000e+00 5.200000000000000000e+01 31 | 1.400000000000000000e+01 4.500000000000000000e+01 32 | 1.100000000000000000e+01 4.300000000000000000e+01 33 | 1.200000000000000000e+01 4.500000000000000000e+01 34 | 1.600000000000000000e+01 5.100000000000000000e+01 35 | 1.000000000000000000e+01 4.400000000000000000e+01 36 | 1.500000000000000000e+01 4.300000000000000000e+01 37 | 1.500000000000000000e+01 4.600000000000000000e+01 38 | 1.400000000000000000e+01 4.400000000000000000e+01 39 | 1.400000000000000000e+01 5.000000000000000000e+01 40 | 1.200000000000000000e+01 4.400000000000000000e+01 41 | 1.300000000000000000e+01 4.500000000000000000e+01 42 | 1.300000000000000000e+01 5.100000000000000000e+01 43 | 1.000000000000000000e+01 4.300000000000000000e+01 44 | 9.000000000000000000e+00 4.700000000000000000e+01 45 | 1.500000000000000000e+01 4.800000000000000000e+01 46 | 2.000000000000000000e+01 5.400000000000000000e+01 47 | 1.700000000000000000e+01 6.200000000000000000e+01 48 | 2.100000000000000000e+01 5.500000000000000000e+01 49 | 1.800000000000000000e+01 6.100000000000000000e+01 50 | 2.200000000000000000e+01 5.900000000000000000e+01 51 | 1.700000000000000000e+01 5.500000000000000000e+01 52 | 1.700000000000000000e+01 6.000000000000000000e+01 53 | 2.300000000000000000e+01 5.700000000000000000e+01 54 | 1.800000000000000000e+01 5.300000000000000000e+01 55 | 2.000000000000000000e+01 5.800000000000000000e+01 56 | 2.400000000000000000e+01 5.300000000000000000e+01 57 | 2.400000000000000000e+01 5.600000000000000000e+01 58 | 2.400000000000000000e+01 5.900000000000000000e+01 59 | 1.900000000000000000e+01 5.500000000000000000e+01 60 | 3.000000000000000000e+01 6.600000000000000000e+01 61 | 3.000000000000000000e+01 7.200000000000000000e+01 62 | 3.100000000000000000e+01 7.100000000000000000e+01 63 | 2.700000000000000000e+01 7.000000000000000000e+01 64 | 2.700000000000000000e+01 6.700000000000000000e+01 65 | 3.200000000000000000e+01 6.300000000000000000e+01 66 | 3.200000000000000000e+01 7.200000000000000000e+01 67 | 2.600000000000000000e+01 6.500000000000000000e+01 68 | 2.500000000000000000e+01 6.300000000000000000e+01 69 | 2.600000000000000000e+01 6.800000000000000000e+01 70 | 2.600000000000000000e+01 7.100000000000000000e+01 71 | 2.500000000000000000e+01 6.900000000000000000e+01 72 | 2.700000000000000000e+01 6.300000000000000000e+01 73 | 2.700000000000000000e+01 7.200000000000000000e+01 74 | 2.700000000000000000e+01 6.900000000000000000e+01 75 | 2.900000000000000000e+01 7.200000000000000000e+01 76 | 3.200000000000000000e+01 6.800000000000000000e+01 77 | 2.500000000000000000e+01 6.500000000000000000e+01 78 | 3.100000000000000000e+01 6.300000000000000000e+01 79 | 3.000000000000000000e+01 7.000000000000000000e+01 80 | 3.100000000000000000e+01 7.200000000000000000e+01 81 | 2.800000000000000000e+01 7.000000000000000000e+01 82 | 2.800000000000000000e+01 6.700000000000000000e+01 83 | 3.200000000000000000e+01 6.400000000000000000e+01 84 | 2.600000000000000000e+01 6.900000000000000000e+01 85 | 3.000000000000000000e+01 6.300000000000000000e+01 86 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/min_path_cover/edges2.csv: -------------------------------------------------------------------------------- 1 | 3.800000000000000000e+01 7.400000000000000000e+01 2 | 3.600000000000000000e+01 8.000000000000000000e+01 3 | 3.700000000000000000e+01 8.100000000000000000e+01 4 | 4.000000000000000000e+01 7.400000000000000000e+01 5 | 3.700000000000000000e+01 8.400000000000000000e+01 6 | 3.300000000000000000e+01 7.400000000000000000e+01 7 | 4.200000000000000000e+01 8.600000000000000000e+01 8 | 3.800000000000000000e+01 7.300000000000000000e+01 9 | 3.600000000000000000e+01 7.600000000000000000e+01 10 | 3.600000000000000000e+01 7.300000000000000000e+01 11 | 3.600000000000000000e+01 8.200000000000000000e+01 12 | 4.100000000000000000e+01 7.800000000000000000e+01 13 | 4.000000000000000000e+01 7.600000000000000000e+01 14 | 3.700000000000000000e+01 8.300000000000000000e+01 15 | 4.000000000000000000e+01 7.900000000000000000e+01 16 | 3.700000000000000000e+01 8.600000000000000000e+01 17 | 4.000000000000000000e+01 8.200000000000000000e+01 18 | 3.400000000000000000e+01 7.800000000000000000e+01 19 | 4.200000000000000000e+01 8.500000000000000000e+01 20 | 3.300000000000000000e+01 8.200000000000000000e+01 21 | 3.300000000000000000e+01 7.900000000000000000e+01 22 | 3.900000000000000000e+01 8.300000000000000000e+01 23 | 3.600000000000000000e+01 7.500000000000000000e+01 24 | 3.600000000000000000e+01 8.100000000000000000e+01 25 | 3.500000000000000000e+01 8.500000000000000000e+01 26 | 4.000000000000000000e+01 7.500000000000000000e+01 27 | 4.000000000000000000e+01 8.400000000000000000e+01 28 | 3.300000000000000000e+01 7.800000000000000000e+01 29 | 3.400000000000000000e+01 7.700000000000000000e+01 30 | 4.800000000000000000e+01 9.400000000000000000e+01 31 | 4.900000000000000000e+01 9.300000000000000000e+01 32 | 4.600000000000000000e+01 9.100000000000000000e+01 33 | 4.700000000000000000e+01 9.200000000000000000e+01 34 | 5.100000000000000000e+01 8.700000000000000000e+01 35 | 5.100000000000000000e+01 9.300000000000000000e+01 36 | 5.000000000000000000e+01 9.100000000000000000e+01 37 | 5.000000000000000000e+01 9.700000000000000000e+01 38 | 5.000000000000000000e+01 1.000000000000000000e+02 39 | 5.200000000000000000e+01 9.100000000000000000e+01 40 | 4.400000000000000000e+01 9.300000000000000000e+01 41 | 4.400000000000000000e+01 9.900000000000000000e+01 42 | 4.300000000000000000e+01 9.700000000000000000e+01 43 | 4.300000000000000000e+01 9.100000000000000000e+01 44 | 4.800000000000000000e+01 9.000000000000000000e+01 45 | 4.900000000000000000e+01 8.900000000000000000e+01 46 | 4.500000000000000000e+01 9.100000000000000000e+01 47 | 5.200000000000000000e+01 8.700000000000000000e+01 48 | 4.300000000000000000e+01 8.700000000000000000e+01 49 | 5.200000000000000000e+01 9.300000000000000000e+01 50 | 4.300000000000000000e+01 9.600000000000000000e+01 51 | 4.400000000000000000e+01 9.500000000000000000e+01 52 | 4.900000000000000000e+01 9.700000000000000000e+01 53 | 4.800000000000000000e+01 9.800000000000000000e+01 54 | 5.100000000000000000e+01 8.800000000000000000e+01 55 | 5.100000000000000000e+01 1.000000000000000000e+02 56 | 4.300000000000000000e+01 9.200000000000000000e+01 57 | 4.900000000000000000e+01 9.000000000000000000e+01 58 | 5.800000000000000000e+01 1.020000000000000000e+02 59 | 5.800000000000000000e+01 1.140000000000000000e+02 60 | 5.500000000000000000e+01 1.060000000000000000e+02 61 | 5.600000000000000000e+01 1.110000000000000000e+02 62 | 6.100000000000000000e+01 1.040000000000000000e+02 63 | 6.100000000000000000e+01 1.010000000000000000e+02 64 | 5.400000000000000000e+01 1.010000000000000000e+02 65 | 5.400000000000000000e+01 1.040000000000000000e+02 66 | 6.200000000000000000e+01 1.110000000000000000e+02 67 | 6.200000000000000000e+01 1.120000000000000000e+02 68 | 5.800000000000000000e+01 1.100000000000000000e+02 69 | 5.700000000000000000e+01 1.080000000000000000e+02 70 | 6.200000000000000000e+01 1.010000000000000000e+02 71 | 6.100000000000000000e+01 1.030000000000000000e+02 72 | 5.700000000000000000e+01 1.140000000000000000e+02 73 | 5.300000000000000000e+01 1.010000000000000000e+02 74 | 6.200000000000000000e+01 1.130000000000000000e+02 75 | 5.300000000000000000e+01 1.130000000000000000e+02 76 | 5.900000000000000000e+01 1.110000000000000000e+02 77 | 5.800000000000000000e+01 1.120000000000000000e+02 78 | 5.500000000000000000e+01 1.070000000000000000e+02 79 | 5.700000000000000000e+01 1.070000000000000000e+02 80 | 6.100000000000000000e+01 1.050000000000000000e+02 81 | 6.100000000000000000e+01 1.110000000000000000e+02 82 | 6.000000000000000000e+01 1.090000000000000000e+02 83 | 6.100000000000000000e+01 1.140000000000000000e+02 84 | 5.400000000000000000e+01 1.080000000000000000e+02 85 | 5.400000000000000000e+01 1.110000000000000000e+02 86 | 5.300000000000000000e+01 1.120000000000000000e+02 87 | 6.900000000000000000e+01 1.210000000000000000e+02 88 | 6.900000000000000000e+01 1.270000000000000000e+02 89 | 6.900000000000000000e+01 1.240000000000000000e+02 90 | 6.600000000000000000e+01 1.190000000000000000e+02 91 | 7.000000000000000000e+01 1.160000000000000000e+02 92 | 7.100000000000000000e+01 1.150000000000000000e+02 93 | 7.100000000000000000e+01 1.240000000000000000e+02 94 | 7.200000000000000000e+01 1.250000000000000000e+02 95 | 7.100000000000000000e+01 1.270000000000000000e+02 96 | 6.300000000000000000e+01 1.280000000000000000e+02 97 | 6.900000000000000000e+01 1.170000000000000000e+02 98 | 6.800000000000000000e+01 1.210000000000000000e+02 99 | 6.700000000000000000e+01 1.160000000000000000e+02 100 | 6.700000000000000000e+01 1.220000000000000000e+02 101 | 7.200000000000000000e+01 1.150000000000000000e+02 102 | 7.100000000000000000e+01 1.230000000000000000e+02 103 | 6.700000000000000000e+01 1.280000000000000000e+02 104 | 7.000000000000000000e+01 1.270000000000000000e+02 105 | 7.200000000000000000e+01 1.180000000000000000e+02 106 | 7.100000000000000000e+01 1.260000000000000000e+02 107 | 6.300000000000000000e+01 1.210000000000000000e+02 108 | 6.900000000000000000e+01 1.250000000000000000e+02 109 | 6.900000000000000000e+01 1.220000000000000000e+02 110 | 6.500000000000000000e+01 1.150000000000000000e+02 111 | 6.500000000000000000e+01 1.210000000000000000e+02 112 | 6.700000000000000000e+01 1.210000000000000000e+02 113 | 6.700000000000000000e+01 1.270000000000000000e+02 114 | 7.200000000000000000e+01 1.200000000000000000e+02 115 | 6.400000000000000000e+01 1.190000000000000000e+02 116 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/min_path_cover/test_bnb_min_path_cover.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | from graphing.special_graphs.neural_trigraph.rand_graph import rep_graph 6 | from graphing.special_graphs.neural_trigraph.path_cover import \ 7 | min_cover_trigraph 8 | from optimizn.combinatorial.algorithms.min_path_cover.bnb_min_path_cover\ 9 | import MinPathCoverParams, MinPathCoverProblem1, MinPathCoverProblem2 10 | from tests.combinatorial.algorithms.check_sol_utils import check_bnb_sol,\ 11 | check_sol_optimality, check_sol_vs_init_sol 12 | from optimizn.combinatorial.branch_and_bound import BnBSelectionStrategy 13 | 14 | 15 | def test_bnb_minpathcover(): 16 | EDGES = [ 17 | ( 18 | np.array([[1, 4], [2, 4], [2, 5], [3, 5]]), 19 | np.array([[4, 6], [4, 7], [5, 8]]) 20 | ), 21 | rep_graph(8, 10, 14, reps=4), 22 | rep_graph(10, 14, 10, reps=4) 23 | ] 24 | TEST_CASES = [ 25 | # test case: (edges, length of min path cover) 26 | (EDGES[0], 3), 27 | (EDGES[1], len(min_cover_trigraph(EDGES[1][0], EDGES[1][1]))), 28 | (EDGES[2], len(min_cover_trigraph(EDGES[2][0], EDGES[2][1]))) 29 | ] 30 | for bnb_selection_strategy in BnBSelectionStrategy: 31 | for edges, mpc_len in TEST_CASES: 32 | for bnb_type in [0, 1]: 33 | edges1 = edges[0] 34 | edges2 = edges[1] 35 | 36 | # test min path cover algorithms 37 | params = MinPathCoverParams(edges1, edges2) 38 | mpc1 = MinPathCoverProblem1(params, bnb_selection_strategy) 39 | mpc2 = MinPathCoverProblem2(params, bnb_selection_strategy) 40 | for mpc in [mpc1, mpc2]: 41 | mpc.solve(1000, 100, 120, bnb_type) 42 | 43 | # check final solution 44 | check_bnb_sol(mpc, bnb_type, params) 45 | check_sol_vs_init_sol(mpc.best_cost, mpc.init_cost) 46 | 47 | # check final solution optimality if modified branch and 48 | # bound is used 49 | if bnb_type == 1: 50 | check_sol_optimality(mpc.best_cost, mpc_len, 1.1) 51 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/min_path_cover/test_sim_anneal_min_path_cover.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import numpy as np 5 | from graphing.special_graphs.neural_trigraph.neural_trigraph\ 6 | import NeuralTriGraph 7 | from graphing.special_graphs.neural_trigraph.rand_graph import rep_graph 8 | from graphing.special_graphs.neural_trigraph.path_cover import \ 9 | min_cover_trigraph 10 | import os 11 | from optimizn.combinatorial.algorithms.min_path_cover\ 12 | .sim_anneal_min_path_cover import MinPathCover_NTG 13 | from tests.combinatorial.algorithms.check_sol_utils import\ 14 | check_sol_optimality, check_sol_vs_init_sol 15 | 16 | 17 | def test_sa_minpathcover1(edges1=None, edges2=None, n_iter=20000, swtch=1): 18 | if edges1 is None: 19 | edges1, edges2 = rep_graph(8, 10, 14, reps=4) 20 | 21 | # get optimal solution 22 | opt_paths = min_cover_trigraph(edges1, edges2) 23 | opt_sol_cost = len(opt_paths) 24 | 25 | # get simulated annealing solution 26 | ntg = NeuralTriGraph(edges1, edges2) 27 | mpc = MinPathCover_NTG(ntg, swtch=swtch) 28 | mpc.anneal(n_iter) 29 | 30 | # check optimality of simulated annealing solution 31 | check_sol_vs_init_sol(mpc.best_cost, mpc.init_cost) 32 | check_sol_optimality(mpc.best_cost, opt_sol_cost, 1.2) 33 | 34 | 35 | def test_sa_minpathcover2(n_iter=20000, swtch=1): 36 | # read edges from file 37 | dirname = os.path.dirname(__file__) 38 | edges1_path = os.path.join(dirname, './edges1.csv') 39 | edges2_path = os.path.join(dirname, './edges2.csv') 40 | edges1 = np.loadtxt(edges1_path) 41 | edges1 = edges1.astype(int) 42 | edges2 = np.loadtxt(edges2_path) 43 | edges2 = edges2.astype(int) 44 | 45 | # test with edges read from file 46 | test_sa_minpathcover1(edges1, edges2, n_iter, swtch=swtch) 47 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/suitcase_reshuffle/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/suitcase_reshuffle/test_bnb_suitcase_reshuffle.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | from optimizn.combinatorial.algorithms.suitcase_reshuffle.bnb_suitcasereshuffle\ 5 | import SuitcaseReshuffleProblem 6 | from optimizn.combinatorial.algorithms.suitcase_reshuffle.suitcases\ 7 | import SuitCases 8 | from tests.combinatorial.algorithms.check_sol_utils import check_bnb_sol,\ 9 | check_sol_optimality, check_sol_vs_init_sol 10 | import inspect 11 | from optimizn.combinatorial.branch_and_bound import BnBSelectionStrategy 12 | 13 | 14 | def test_constructor_get_root(): 15 | TEST_CASES = [ 16 | # test case: (suitcase configuration, expected suitcase capacities, 17 | # expected cost of initial solution, expected sorted weights) 18 | ([[7, 5, 1], [4, 6, 1]], [13, 11], -1, [7, 6, 5, 4]), 19 | ([[7, 5, 0], [4, 6, 2]], [12, 12], -2, [7, 6, 5, 4]), 20 | ([[7, 5, 1, 0], [4, 6, 0]], [13, 10], 0, [7, 6, 5, 4, 1]) 21 | ] 22 | for config, capacities, cost, sorted_weights in TEST_CASES: 23 | srp = SuitcaseReshuffleProblem( 24 | SuitCases(config), BnBSelectionStrategy.DEPTH_FIRST) 25 | init_sol = srp.best_solution 26 | 27 | # check config 28 | init_config = init_sol[0].config 29 | assert init_config == config, 'Incorrect initial solution configs. '\ 30 | + f'Expected: {config}. Actual: {init_config}' 31 | 32 | # check capacities 33 | init_caps = init_sol[0].capacities 34 | assert init_caps == capacities, 'Incorrect initial solution '\ 35 | + f'capacities. Expected: {capacities}. Actual: {init_caps}' 36 | 37 | # check sorted weights 38 | assert srp.sorted_weights == sorted_weights, 'Incorrect sorted '\ 39 | + f'weights. Expected: {sorted_weights}, Actual: '\ 40 | + f'{srp.sorted_weights}' 41 | 42 | # check initial solution 43 | init_suitcase_num = init_sol[1] 44 | assert srp.best_cost == cost, 'Incorrect initial solution cost. '\ 45 | + f'Expected: {cost}. Actual: {srp.best_cost}' 46 | exp_suitcase_num = len(sorted_weights) - 1 47 | assert init_suitcase_num == exp_suitcase_num, 'Incorrect suitcase '\ 48 | + f'number in initial solution. Expected: {exp_suitcase_num}. '\ 49 | + f'Actual: {init_suitcase_num}' 50 | 51 | # check root node solution 52 | root_sol = srp.get_root() 53 | root_sol_cost = srp.cost(root_sol) 54 | assert root_sol_cost == cost, 'Incorrect root node solution cost. '\ 55 | + f'Expected: {cost}. Actual: {root_sol_cost}' 56 | assert root_sol[1] == -1, 'Incorrect suitcase number (for '\ 57 | + 'branching) in root node solution. Expected: -1. Actual: '\ 58 | + f'{root_sol[1]}' 59 | 60 | 61 | def test_cost(): 62 | TEST_CASES = [ 63 | # test case: (solution, expected cost) 64 | ((SuitCases([[7, 5, 1], [4, 6, 1]]), 0), -1), 65 | ((SuitCases([[7, 5, 1], [4, 6, 1], [12, 12, 4], [11, 10, 2]]), 1), -4), 66 | ((SuitCases([[7, 5, 1], [4, 6, 1], [12, 12, 4], [11, 10, 2]]), 2), -4) 67 | ] 68 | for sol, cost in TEST_CASES: 69 | srp = SuitcaseReshuffleProblem( 70 | sol[0], BnBSelectionStrategy.DEPTH_FIRST) 71 | sol_cost = srp.cost(sol) 72 | assert sol_cost == cost, f'Computed cost of solution {sol} is '\ 73 | + f'incorrect. Expected: {cost}. Actual: {sol_cost}' 74 | 75 | 76 | def test_lbound(): 77 | TEST_CASES = [ 78 | # test case: (solution, expected lower bound) 79 | ((SuitCases([[7, 5, 1], [4, 6, 1]]), 0), -2), 80 | ((SuitCases([[7, 5, 3], [4, 6, 1]]), 0), -4), 81 | ((SuitCases([[7, 5, 1], [4, 6, 4]]), 0), -5) 82 | ] 83 | for sol, lbound in TEST_CASES: 84 | srp = SuitcaseReshuffleProblem( 85 | sol[0], BnBSelectionStrategy.DEPTH_FIRST) 86 | sol_lb = srp.lbound(sol) 87 | assert sol_lb == lbound, f'Computed cost of solution {sol} is '\ 88 | + f'incorrect. Expected: {lbound}. Actual: {sol_lb}' 89 | 90 | 91 | def test_is_feasible(): 92 | TEST_CASES = [ 93 | # test case: (initial suitcases, solution, boolean for whether solution 94 | # is complete) 95 | (SuitCases([[7, 5, 1], [4, 6, 1]]), 96 | (SuitCases([[7, 5, 1], [4, 6, 1]]), 3), True), 97 | (SuitCases([[7, 5, 1], [4, 6, 1]]), 98 | (SuitCases([[7, 6], [11]]), 0), False 99 | # not complete solution since item with weight 5 is not in a suitcase 100 | ), 101 | (SuitCases([[7, 5, 1], [4, 6, 1]]), 102 | (SuitCases([[7, 6], [11]]), 2), False 103 | # not complete solution since item with weight 5 is not in a suitcase 104 | ), 105 | (SuitCases([[7, 5, 1], [4, 6, 1], [12, 12, 4], [11, 10, 2]]), 106 | (SuitCases([[7, 5, 1], [4, 6, 1], [12, 12, 4], [11, 10, 2]]), 7), 107 | True), 108 | (SuitCases([[7, 5, 1], [4, 6, 1], [12, 12, 4], [11, 10, 2]]), 109 | (SuitCases([[7, 5, 1], [4, 6, 1], [12, 12, 4], [11, 10, 2]]), 7), 110 | True), 111 | (SuitCases([[7, 5, 1], [4, 6, 1], [12, 12, 4], [11, 10, 2]]), 112 | (SuitCases([[7, 6], [6, 4], [12, 12, 4], [11, 10, 2]]), 5), 113 | False 114 | # not complete solution since items with weights 4 and 5 are not in 115 | # suitcases 116 | ) 117 | ] 118 | for init_sc, sol, feasible_sol in TEST_CASES: 119 | srp = SuitcaseReshuffleProblem( 120 | init_sc, BnBSelectionStrategy.DEPTH_FIRST) 121 | 122 | # check feasibility of solution 123 | is_feasible = srp.is_feasible(sol) 124 | assert feasible_sol == is_feasible, 'Feasibility check of solution '\ 125 | + f'{sol} failed. Expected: {feasible_sol}. Actual: {is_feasible}' 126 | 127 | 128 | def test_complete_solution(): 129 | TEST_CASES = [ 130 | # test case: (suitcase configuration, solution, expected 131 | # complete solution) 132 | (SuitCases([[7, 5, 1], [4, 6, 1]]), 133 | (SuitCases([[7, 6], [11]]), 0), 134 | (SuitCases([[7, 6, 0], [5, 4, 2]]), 3) 135 | ), 136 | (SuitCases([[7, 5, 1], [4, 6, 1]]), 137 | (SuitCases([[7, 5, 1], [4, 6, 1]]), 3), 138 | (SuitCases([[7, 5, 1], [4, 6, 1]]), 3) 139 | ), 140 | (SuitCases([[7, 5, 1], [4, 6, 1], [3, 2, 1]]), 141 | (SuitCases([[7, 6, 0], [5, 6], [6]]), 2), 142 | (SuitCases([[7, 6, 0], [5, 4, 2, 0], [3, 3]]), 5) 143 | ), 144 | (SuitCases([[7, 5, 1], [4, 6, 1], [3, 2, 1]]), 145 | (SuitCases([[7, 5, 1], [4, 6, 1], [3, 2, 1]]), 5), 146 | (SuitCases([[7, 5, 1], [4, 6, 1], [3, 2, 1]]), 5) 147 | ) 148 | ] 149 | for suitcases, sol, exp_comp_sol in TEST_CASES: 150 | srp = SuitcaseReshuffleProblem( 151 | suitcases, BnBSelectionStrategy.DEPTH_FIRST) 152 | 153 | # check completed solution 154 | comp_sol = srp.complete_solution(sol) 155 | assert comp_sol == exp_comp_sol, 'Incorrect complete solution formed '\ 156 | + f'from solution {sol}. Expected: {exp_comp_sol}. Actual: '\ 157 | + f'{comp_sol}' 158 | 159 | 160 | def test_branch(): 161 | TEST_CASES = [ 162 | # test case: (suitcase configuration, solution, expected branch 163 | # solutions) 164 | ( 165 | [[7, 5, 1], [4, 6, 1]], 166 | (SuitCases([[7, 5, 1], [4, 6, 1]]), -1), 167 | [ 168 | (SuitCases([[7, 6], [11]]), 0), 169 | (SuitCases([[13], [7, 4]]), 0) 170 | ] 171 | ), 172 | ( 173 | [[7, 5, 1], [4, 6, 1]], 174 | (SuitCases([[7, 6], [11]]), 0), 175 | [ 176 | (SuitCases([[7, 6, 0], [11]]), 1), 177 | (SuitCases([[7, 6], [6, 5]]), 1) 178 | ] 179 | ), 180 | ( 181 | [[7, 5, 1], [4, 6, 1], [3, 2, 1]], 182 | (SuitCases([[7, 5, 1], [4, 6, 1], [3, 2, 1]]), -1), 183 | [ 184 | (SuitCases([[7, 6], [11], [6]]), 0), 185 | (SuitCases([[13], [7, 4], [6]]), 0) 186 | ] 187 | ), 188 | ( 189 | [[7, 5, 1], [4, 6, 1], [3, 2, 1]], 190 | (SuitCases([[13], [7, 4], [6]]), 0), 191 | [ 192 | (SuitCases([[6, 7], [7, 4], [6]]), 1), 193 | (SuitCases([[13], [7, 4], [6, 0]]), 1) 194 | ] 195 | ) 196 | ] 197 | for config, sol, branch_sols in TEST_CASES: 198 | srp = SuitcaseReshuffleProblem( 199 | SuitCases(config), BnBSelectionStrategy.DEPTH_FIRST) 200 | 201 | # branch on solutions, check branched solutions 202 | new_sols = srp.branch(sol) 203 | assert inspect.isgenerator(new_sols), 'Branch function must return '\ 204 | + 'a generator' 205 | new_sols_list = list(new_sols) 206 | assert new_sols_list == branch_sols, 'Incorrect branched solutions. '\ 207 | + f'Expected: {branch_sols}. Actual: {new_sols}' 208 | 209 | 210 | def test_bnb_suitcasereshuffle(): 211 | TEST_CASES = [ 212 | # test case: (suitcase configuration, optimal solution cost) 213 | ([[7, 5, 1], [4, 6, 1]], -2), 214 | ([[7, 5, 4], [4, 6, 1], [5, 5, 1]], -6), 215 | ([[1, 4, 3, 6, 4, 2], [2, 4, 7, 1, 0], [1, 7, 3, 8, 3, 4]], -6), 216 | ([ 217 | [12, 52, 34, 23, 17, 18, 22, 10], 218 | [100, 21, 36, 77, 82, 44, 40], 219 | [1, 5, 2, 8, 22, 34, 50] 220 | ], -100) 221 | ] 222 | for bnb_selection_strategy in BnBSelectionStrategy: 223 | for config, opt_sol_cost in TEST_CASES: 224 | for bnb_type in [0, 1]: 225 | sc = SuitCases(config) 226 | params = { 227 | 'init_sol': sc 228 | } 229 | srp = SuitcaseReshuffleProblem(sc, bnb_selection_strategy) 230 | srp.solve(1000, 100, 120, bnb_type) 231 | 232 | # check final solution 233 | check_bnb_sol(srp, bnb_type, params) 234 | check_sol_vs_init_sol(srp.best_cost, srp.init_cost) 235 | 236 | # check final solution optimality, if modified branch and bound 237 | # is used 238 | if bnb_type == 1: 239 | check_sol_optimality(srp.best_cost, opt_sol_cost, 9/10) 240 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/suitcase_reshuffle/test_sim_anneal_suitcase_reshuffle.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | from optimizn.combinatorial.algorithms.suitcase_reshuffle.suitcases\ 5 | import SuitCases 6 | from optimizn.combinatorial.algorithms.suitcase_reshuffle\ 7 | .sim_anneal_suitcase_reshuffle import SuitCaseReshuffle 8 | from tests.combinatorial.algorithms.check_sol_utils import check_sol,\ 9 | check_sol_vs_init_sol 10 | 11 | 12 | def test_constructor(): 13 | config = [[7,5,1],[4,6,1]] 14 | sc = SuitCases(config) 15 | scr = SuitCaseReshuffle(params=sc) 16 | 17 | # check initial solution 18 | assert config == scr.best_solution, 'Incorrect initial solution. '\ 19 | + f'Expected: {config}. Actual: {scr.best_solution}' 20 | 21 | # check initial solution cost 22 | exp_init_sol_cost = -1 23 | assert exp_init_sol_cost == scr.best_cost, 'Incorrect initial solution '\ 24 | + f'cost. Expected {exp_init_sol_cost}. Actual: {scr.best_cost}' 25 | 26 | # check params 27 | exp_params = SuitCases(config) 28 | assert exp_params == scr.params, 'Incorrect parameters. Expected: '\ 29 | + f'{exp_params}. Actual: {scr.params}' 30 | 31 | 32 | def test_sa_suitcasereshuffle(): 33 | config = [[7,5,1],[4,6,1]] 34 | sc = SuitCases(config) 35 | scr = SuitCaseReshuffle(params=sc) 36 | scr.anneal() 37 | 38 | # check final solution 39 | check_sol_vs_init_sol(scr.best_cost, scr.init_cost) 40 | exp_sols = [ 41 | [[7, 6, 0], [4, 5, 2]], 42 | [[7, 6, 0], [5, 4, 2]], 43 | [[6, 7, 0], [4, 5, 2]], 44 | [[6, 7, 0], [5, 4, 2]], 45 | [[7, 4, 2], [6, 5, 0]], 46 | [[7, 4, 2], [5, 6, 0]], 47 | [[4, 7, 2], [6, 5, 0]], 48 | [[4, 7, 2], [5, 6, 0]], 49 | [[6, 5, 2], [4, 7, 0]], 50 | [[6, 5, 2], [7, 4, 0]], 51 | [[5, 6, 2], [4, 7, 0]], 52 | [[5, 6, 2], [7, 4, 0]] 53 | ] 54 | check_sol(scr.best_solution, exp_sols) 55 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/traveling_salesman/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/traveling_salesman/test_bnb_tsp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | from optimizn.combinatorial.algorithms.traveling_salesman.city_graph\ 5 | import CityGraph 6 | from optimizn.combinatorial.algorithms.traveling_salesman.bnb_tsp import\ 7 | TravelingSalesmanProblem 8 | import numpy as np 9 | from tests.combinatorial.algorithms.check_sol_utils import check_bnb_sol,\ 10 | check_sol_vs_init_sol 11 | import inspect 12 | from optimizn.combinatorial.branch_and_bound import BnBSelectionStrategy 13 | 14 | 15 | class MockCityGraph: 16 | def __init__(self, dists): 17 | self.dists = dists 18 | self.num_cities = len(dists) 19 | 20 | 21 | def test_is_feasible(): 22 | dists = np.array([ 23 | [0, 4, 2, 1], 24 | [4, 0, 3, 4], 25 | [2, 3, 0, 2], 26 | [1, 4, 2, 0], 27 | ]) 28 | mcg = MockCityGraph(dists) 29 | params = { 30 | 'input_graph': mcg, 31 | } 32 | TEST_CASES = [ 33 | # test case: (solution, boolean for whether solution is feasible) 34 | ([0, 1, 2, 3], True), 35 | ([1, 2], False), 36 | ([1, 2, 3], False), 37 | ([1, 2, 3, 0], True) 38 | ] 39 | tsp = TravelingSalesmanProblem(params, BnBSelectionStrategy.DEPTH_FIRST) 40 | for sol, is_feasible in TEST_CASES: 41 | feasible = tsp.is_feasible(sol) 42 | assert is_feasible == feasible, 'feasiblity check failed for solution'\ 43 | + f' {sol}. Expected to be feasible: {is_feasible}. Actually '\ 44 | + f'feasible: {feasible}' 45 | 46 | 47 | def test_get_initial_solution_sorted_dists(): 48 | dists = np.array([ 49 | [0, 4, 2, 1], 50 | [4, 0, 3, 4], 51 | [2, 3, 0, 2], 52 | [1, 4, 2, 0], 53 | ]) 54 | mcg = MockCityGraph(dists) 55 | params = { 56 | 'input_graph': mcg, 57 | } 58 | tsp = TravelingSalesmanProblem(params, BnBSelectionStrategy.DEPTH_FIRST) 59 | exp_init_sol = [0, 1, 2, 3] 60 | exp_sorted_dists = [1, 2, 2, 3, 4, 4] 61 | assert tsp.best_solution == exp_init_sol, 'Invalid initial solution. '\ 62 | + f'Expected: {exp_init_sol}. Actual: {tsp.best_solution}' 63 | assert tsp.sorted_dists == exp_sorted_dists, 'Invalid sorted distances. '\ 64 | + f'Expected: {tsp.sorted_dists}. Actual: {exp_sorted_dists}' 65 | 66 | 67 | def test_complete_solution(): 68 | dists = np.array([ 69 | [0, 4, 2, 1], 70 | [4, 0, 3, 4], 71 | [2, 3, 0, 2], 72 | [1, 4, 2, 0], 73 | ]) 74 | mcg = MockCityGraph(dists) 75 | params = { 76 | 'input_graph': mcg, 77 | } 78 | TEST_CASES = [ 79 | # test case: partial solution 80 | [], [0], [0, 1], [1, 3], [0, 3, 2, 1] 81 | ] 82 | tsp = TravelingSalesmanProblem(params, BnBSelectionStrategy.DEPTH_FIRST) 83 | for path in TEST_CASES: 84 | comp_path = tsp.complete_solution(path) 85 | assert len(comp_path) == mcg.num_cities,\ 86 | 'Incorrect length of completed path. Expected: '\ 87 | + f'{mcg.num_cities}. Actual: {len(comp_path)}' 88 | assert set(comp_path) == set(range(mcg.num_cities)), 'Incorrect '\ 89 | + f'coverage of cities. Expected: {set(range(mcg.num_cities))}'\ 90 | + f'. Actual: {set(comp_path)}' 91 | 92 | 93 | def test_cost(): 94 | dists = np.array([ 95 | [0, 4, 2, 1], 96 | [4, 0, 3, 4], 97 | [2, 3, 0, 2], 98 | [1, 4, 2, 0], 99 | ]) 100 | mcg = MockCityGraph(dists) 101 | params = { 102 | 'input_graph': mcg, 103 | } 104 | TEST_CASES = [ 105 | # test case: (solution, cost of solution) 106 | ([0, 3, 2, 1], 10), 107 | ([0, 3, 2, 1], 10), 108 | ([0, 1, 2, 3], 10), 109 | ([0, 1, 3, 2], 12) 110 | ] 111 | tsp = TravelingSalesmanProblem(params, BnBSelectionStrategy.DEPTH_FIRST) 112 | for sol, cost in TEST_CASES: 113 | sol_cost = tsp.cost(sol) 114 | assert sol_cost == cost, f'Incorrect cost for solution {sol}. '\ 115 | + f'Expected: {cost}. Actual: {sol_cost}' 116 | 117 | 118 | def test_lbound(): 119 | dists = np.array([ 120 | [0, 4, 2, 1], 121 | [4, 0, 3, 4], 122 | [2, 3, 0, 2], 123 | [1, 4, 2, 0], 124 | ]) 125 | mcg = MockCityGraph(dists) 126 | params = { 127 | 'input_graph': mcg, 128 | } 129 | TEST_CASES = [ 130 | # test case: (solution, lower bound of solution) 131 | ([0, 3, 2, 1], 10), 132 | ([0, 3], 8), 133 | ([0, 3, 2], 8), 134 | ([0, 1, 2, 3], 10), 135 | ([0, 1, 3, 2], 12), 136 | ([0, 1, 3], 11), 137 | ([0, 1], 9), 138 | ([0, 2], 8), 139 | ([0], 8), 140 | ([], 8) 141 | ] 142 | for sol, lower_bound in TEST_CASES: 143 | tsp = TravelingSalesmanProblem( 144 | params, BnBSelectionStrategy.DEPTH_FIRST) 145 | lb = tsp.lbound(sol) 146 | assert lb == lower_bound, 'Incorrect lower bound for solution '\ 147 | + f'{sol}. Expected: {lower_bound}. Actual: {lb}' 148 | 149 | 150 | def test_branch(): 151 | dists = np.array([ 152 | [0, 4, 2, 1], 153 | [4, 0, 3, 4], 154 | [2, 3, 0, 2], 155 | [1, 4, 2, 0], 156 | ]) 157 | mcg = MockCityGraph(dists) 158 | params = { 159 | 'input_graph': mcg, 160 | } 161 | TEST_CASES = [ 162 | # test case: (solution, expected branched solutions) 163 | ([], [[0], [1], [2], [3]]), 164 | ([0, 3, 2, 1], []), 165 | ([0, 2, 1, 3], []), 166 | ([0, 2], [[0, 2, 1], [0, 2, 3]]), 167 | ([0, 1], [[0, 1, 2], [0, 1, 3]]), 168 | ([0, 1, 2], [[0, 1, 2, 3]]), 169 | ([1], [[1, 0], [1, 2], [1, 3]]) 170 | ] 171 | for sol, branch_sols in TEST_CASES: 172 | tsp = TravelingSalesmanProblem( 173 | params, BnBSelectionStrategy.DEPTH_FIRST) 174 | new_sols = tsp.branch(sol) 175 | assert inspect.isgenerator(new_sols),\ 176 | 'Branch function must return generator' 177 | new_sols_list = list(new_sols) 178 | assert branch_sols == new_sols_list, 'Incorrect branched solutions '\ 179 | + f'for solution: {sol}. Expected: {branch_sols}, Actual: '\ 180 | + f'{new_sols_list}' 181 | 182 | 183 | def test_get_root(): 184 | graph = CityGraph() 185 | params = { 186 | 'input_graph': graph, 187 | } 188 | tsp = TravelingSalesmanProblem(params, BnBSelectionStrategy.DEPTH_FIRST) 189 | root_sol = tsp.get_root() 190 | assert root_sol == [0], 'Incorrect root node solution. Expected: '\ 191 | + f'[0], Actual: {root_sol}' 192 | 193 | 194 | def test_bnb_tsp(): 195 | graph = CityGraph() 196 | params = { 197 | 'input_graph': graph, 198 | } 199 | for bnb_selection_strategy in BnBSelectionStrategy: 200 | tsp1 = TravelingSalesmanProblem(params, bnb_selection_strategy) 201 | tsp1.solve(1e20, 1e20, 120, 1) 202 | tsp2 = TravelingSalesmanProblem(params, bnb_selection_strategy) 203 | tsp2.solve(1e20, 1e20, 120, 1) 204 | 205 | # check final solutions 206 | check_bnb_sol(tsp1, 0, params) 207 | check_sol_vs_init_sol(tsp1.best_cost, tsp1.init_cost) 208 | check_bnb_sol(tsp2, 1, params) 209 | check_sol_vs_init_sol(tsp2.best_cost, tsp2.init_cost) 210 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/traveling_salesman/test_city_graph.py: -------------------------------------------------------------------------------- 1 | from optimizn.combinatorial.algorithms.traveling_salesman.city_graph\ 2 | import CityGraph 3 | from copy import deepcopy 4 | 5 | 6 | def test_city_graph_equality(): 7 | # check that city graphs are equal 8 | cg1 = CityGraph(num_cities=100) 9 | cg2 = deepcopy(cg1) 10 | assert cg1 == cg2, 'City graphs are supposed to be equal. '\ 11 | + f'\nCity graph 1: {cg1}.\nCity graph 2: {cg2}' 12 | 13 | # check that city graphs are not equal 14 | cg2 = CityGraph(num_cities=50) 15 | assert cg1 != cg2, 'City graphs are not supposed to be equal. '\ 16 | + f'\nCity graph 1: {cg1}.\nCity graph 2: {cg2}' 17 | 18 | # check that city graphs are not equal 19 | cg2 = CityGraph(num_cities=100) 20 | assert cg1 != cg2, 'City graphs are not supposed to be equal. '\ 21 | + f'\nCity graph 1: {cg1}.\nCity graph 2: {cg2}' 22 | -------------------------------------------------------------------------------- /tests/combinatorial/algorithms/traveling_salesman/test_sim_anneal_tsp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | # pip install python-tsp 5 | # https://github.com/fillipe-gsm/python-tsp 6 | from python_tsp.heuristics import solve_tsp_simulated_annealing 7 | ## https://developers.google.com/optimization/routing/tsp 8 | # Their solution didn't work, some cpp error. 9 | from optimizn.combinatorial.algorithms.traveling_salesman.city_graph\ 10 | import CityGraph 11 | from optimizn.combinatorial.algorithms.traveling_salesman.sim_anneal_tsp\ 12 | import TravSalsmn 13 | from tests.combinatorial.algorithms.check_sol_utils import\ 14 | check_sol_optimality, check_sol_vs_init_sol 15 | 16 | 17 | def test_sa_tsp(): 18 | # create graph 19 | tt = CityGraph() 20 | 21 | # run external library algorithm 22 | _, distance = solve_tsp_simulated_annealing( 23 | tt.dists, 24 | max_processing_time=60, 25 | alpha=0.99, x0=list(range(tt.num_cities)), 26 | perturbation_scheme='ps2') 27 | 28 | # run simulated annealing algorithm 29 | ts = TravSalsmn(tt) 30 | ts.anneal(n_iter=int(1e20), reset_p=0, time_limit=60) 31 | 32 | # check final solution optimality 33 | check_sol_vs_init_sol(ts.best_cost, ts.init_cost) 34 | check_sol_optimality(ts.best_cost, distance, 1.25) 35 | --------------------------------------------------------------------------------