├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── conf.py ├── contributing.rst ├── history.rst ├── index.rst ├── installation.rst ├── make.bat ├── modules.rst ├── pytsp.data_structures.rst ├── pytsp.rst ├── readme.rst ├── source │ ├── conf.rst │ ├── modules.rst │ ├── pytsp.data_structures.rst │ └── pytsp.rst └── usage.rst ├── modules.rst ├── pytsp.data_structures.rst ├── pytsp.rst ├── pytsp ├── __init__.py ├── branch_and_bound_tsp.py ├── branch_and_bound_tsp_dfs.py ├── christofides_tsp.py ├── constants.py ├── data_structures │ ├── __init__.py │ ├── data_structure.py │ ├── node.py │ ├── opt_case.py │ └── tree.py ├── genetic_algorithms │ ├── __init__.py │ └── simulated_annealing.py ├── had_tsp.py ├── k_opt_tsp.py ├── lin_kerdighan_tsp.py ├── nearest_neighbor_tsp.py ├── permutations_tsp.py ├── pytsp.py └── utils.py ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_branch_and_bound_tsp.py ├── test_christofides_tsp.py ├── test_k_opt.py ├── test_lin_kerdighan.py ├── test_nearest_neighbor.py ├── test_permutation_tsp.py ├── test_pytsp.py └── utils │ └── test_utils.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * pytsp version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # IDE 105 | .idea/ 106 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | python: 5 | - 3.6 6 | - 3.5 7 | - 3.4 8 | - 2.7 9 | 10 | # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 11 | install: pip install -U tox-travis 12 | 13 | # Command to run tests, e.g. python setup.py test 14 | script: tox 15 | 16 | 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/BraveDistribution/pytsp/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | pytsp could always use more documentation, whether as part of the 42 | official pytsp docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/BraveDistribution/pytsp/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `pytsp` for local development. 61 | 62 | 1. Fork the `pytsp` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/pytsp.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv pytsp 70 | $ cd pytsp/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the 80 | tests, including testing other Python versions with tox:: 81 | 82 | $ flake8 pytsp tests 83 | $ python setup.py test or py.test 84 | $ tox 85 | 86 | To get flake8 and tox, just pip install them into your virtualenv. 87 | 88 | 6. Commit your changes and push your branch to GitHub:: 89 | 90 | $ git add . 91 | $ git commit -m "Your detailed description of your changes." 92 | $ git push origin name-of-your-bugfix-or-feature 93 | 94 | 7. Submit a pull request through the GitHub website. 95 | 96 | Pull Request Guidelines 97 | ----------------------- 98 | 99 | Before you submit a pull request, check that it meets these guidelines: 100 | 101 | 1. The pull request should include tests. 102 | 2. If the pull request adds functionality, the docs should be updated. Put 103 | your new functionality into a function with a docstring, and add the 104 | feature to the list in README.rst. 105 | 3. The pull request should work for Python 2.7, 3.4, 3.5 and 3.6, and for PyPy. Check 106 | https://travis-ci.org/BraveDistribution/pytsp/pull_requests 107 | and make sure that the tests pass for all supported Python versions. 108 | 109 | Tips 110 | ---- 111 | 112 | To run a subset of tests:: 113 | 114 | 115 | $ python -m unittest tests.test_pytsp 116 | 117 | Deploying 118 | --------- 119 | 120 | A reminder for the maintainers on how to deploy. 121 | Make sure all your changes are committed (including an entry in HISTORY.rst). 122 | Then run:: 123 | 124 | $ bumpversion patch # possible: major / minor / patch 125 | $ git push 126 | $ git push --tags 127 | 128 | Travis will then deploy to PyPI if tests pass. 129 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 0.1.0 (2018-10-20) 6 | ------------------ 7 | 8 | * First release on PyPI. 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018, BraveDistribution 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTING.rst 2 | include HISTORY.rst 3 | include LICENSE 4 | include README.rst 5 | 6 | recursive-include tests * 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | 10 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 33 | 34 | clean-build: ## remove build artifacts 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: ## remove Python file artifacts 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-test: ## remove test and coverage artifacts 48 | rm -fr .tox/ 49 | rm -f .coverage 50 | rm -fr htmlcov/ 51 | rm -fr .pytest_cache 52 | 53 | lint: ## check style with flake8 54 | flake8 pytsp tests 55 | 56 | test: ## run tests quickly with the default Python 57 | python setup.py test 58 | 59 | test-all: ## run tests on every Python version with tox 60 | tox 61 | 62 | coverage: ## check code coverage quickly with the default Python 63 | coverage run --source pytsp setup.py test 64 | coverage report -m 65 | coverage html 66 | $(BROWSER) htmlcov/index.html 67 | 68 | docs: ## generate Sphinx HTML documentation, including API docs 69 | rm -f docs/pytsp.rst 70 | rm -f docs/modules.rst 71 | sphinx-apidoc -o docs/ pytsp 72 | $(MAKE) -C docs clean 73 | $(MAKE) -C docs html 74 | $(BROWSER) docs/_build/html/index.html 75 | 76 | servedocs: docs ## compile the docs watching for changes 77 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 78 | 79 | release: dist ## package and upload a release 80 | twine upload dist/* 81 | 82 | dist: clean ## builds source and wheel package 83 | python setup.py sdist 84 | python setup.py bdist_wheel 85 | ls -l dist 86 | 87 | install: clean ## install the package to the active Python's site-packages 88 | python setup.py install 89 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | pytsp 3 | ===== 4 | 5 | 6 | .. image:: https://img.shields.io/pypi/v/pytsp.svg 7 | :target: https://pypi.python.org/pypi/pytsp 8 | 9 | .. image:: https://img.shields.io/travis/BraveDistribution/pytsp.svg 10 | :target: https://travis-ci.org/BraveDistribution/pytsp 11 | 12 | .. image:: https://readthedocs.org/projects/pytsp/badge/?version=latest 13 | :target: https://pytsp.readthedocs.io/en/latest/?badge=latest 14 | :alt: Documentation Status 15 | 16 | 17 | 18 | 19 | Few algorithms for TSP problem in Python 20 | 21 | 22 | * Free software: MIT license 23 | * Documentation: https://pytsp.readthedocs.io. 24 | 25 | Notice 26 | -------- 27 | This package is under heavy development. The code may not be efficient and may potentially lead to bugs. 28 | 29 | Features 30 | -------- 31 | 32 | * Christofides algorithm 33 | * Minimal spanning tree (Prim algorithm) 34 | * 2-opt algorithm 35 | * 3-opt algorithm 36 | * Permutations 37 | * Nearest Neighbor 38 | * Simulated Annealing (SA) 39 | * B&B algorithm (LB based on 2 nearest neighbours, bad performance) 40 | 41 | Contributions 42 | ------- 43 | * Feel free to contribute any way possible for you (Implement new algorithms or tests) 44 | * Feel free to ask for new algorithms via "Issues" tab. 45 | Credits 46 | ------- 47 | 48 | This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. 49 | 50 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 51 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage 52 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = pytsp 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pytsp documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another 17 | # directory, add these directories to sys.path here. If the directory is 18 | # relative to the documentation root, use os.path.abspath to make it 19 | # absolute, like shown here. 20 | # 21 | import os 22 | import sys 23 | sys.path.insert(0, os.path.abspath('..')) 24 | 25 | import pytsp 26 | 27 | # -- General configuration --------------------------------------------- 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # 31 | # needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 35 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon'] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # 43 | # source_suffix = ['.rst', '.md'] 44 | source_suffix = '.rst' 45 | 46 | # The master toctree document. 47 | master_doc = 'index' 48 | 49 | # General information about the project. 50 | project = u'pytsp' 51 | copyright = u"2018, BraveDistribution" 52 | author = u"BraveDistribution" 53 | 54 | # The version info for the project you're documenting, acts as replacement 55 | # for |version| and |release|, also used in various other places throughout 56 | # the built documents. 57 | # 58 | # The short X.Y version. 59 | version = pytsp.__version__ 60 | # The full version, including alpha/beta/rc tags. 61 | release = pytsp.__version__ 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | language = None 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | # This patterns also effect to html_static_path and html_extra_path 73 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 74 | 75 | # The name of the Pygments (syntax highlighting) style to use. 76 | pygments_style = 'sphinx' 77 | 78 | # If true, `todo` and `todoList` produce output, else they produce nothing. 79 | todo_include_todos = False 80 | 81 | 82 | # -- Options for HTML output ------------------------------------------- 83 | 84 | # The theme to use for HTML and HTML Help pages. See the documentation for 85 | # a list of builtin themes. 86 | # 87 | html_theme = 'alabaster' 88 | 89 | # Theme options are theme-specific and customize the look and feel of a 90 | # theme further. For a list of options available for each theme, see the 91 | # documentation. 92 | # 93 | # html_theme_options = {} 94 | 95 | # Add any paths that contain custom static files (such as style sheets) here, 96 | # relative to this directory. They are copied after the builtin static files, 97 | # so a file named "default.css" will overwrite the builtin "default.css". 98 | html_static_path = ['_static'] 99 | 100 | 101 | # -- Options for HTMLHelp output --------------------------------------- 102 | 103 | # Output file base name for HTML help builder. 104 | htmlhelp_basename = 'pytspdoc' 105 | 106 | 107 | # -- Options for LaTeX output ------------------------------------------ 108 | 109 | latex_elements = { 110 | # The paper size ('letterpaper' or 'a4paper'). 111 | # 112 | # 'papersize': 'letterpaper', 113 | 114 | # The font size ('10pt', '11pt' or '12pt'). 115 | # 116 | # 'pointsize': '10pt', 117 | 118 | # Additional stuff for the LaTeX preamble. 119 | # 120 | # 'preamble': '', 121 | 122 | # Latex figure (float) alignment 123 | # 124 | # 'figure_align': 'htbp', 125 | } 126 | 127 | # Grouping the document tree into LaTeX files. List of tuples 128 | # (source start file, target name, title, author, documentclass 129 | # [howto, manual, or own class]). 130 | latex_documents = [ 131 | (master_doc, 'pytsp.tex', 132 | u'pytsp Documentation', 133 | u'BraveDistribution', 'manual'), 134 | ] 135 | 136 | 137 | # -- Options for manual page output ------------------------------------ 138 | 139 | # One entry per manual page. List of tuples 140 | # (source start file, name, description, authors, manual section). 141 | man_pages = [ 142 | (master_doc, 'pytsp', 143 | u'pytsp Documentation', 144 | [author], 1) 145 | ] 146 | 147 | 148 | # -- Options for Texinfo output ---------------------------------------- 149 | 150 | # Grouping the document tree into Texinfo files. List of tuples 151 | # (source start file, target name, title, author, 152 | # dir menu entry, description, category) 153 | texinfo_documents = [ 154 | (master_doc, 'pytsp', 155 | u'pytsp Documentation', 156 | author, 157 | 'pytsp', 158 | 'One line description of project.', 159 | 'Miscellaneous'), 160 | ] 161 | 162 | 163 | 164 | 165 | 166 | # Napoleon settings 167 | napoleon_google_docstring = True 168 | napoleon_numpy_docstring = True 169 | napoleon_include_init_with_doc = False 170 | napoleon_include_private_with_doc = False 171 | napoleon_include_special_with_doc = True 172 | napoleon_use_admonition_for_examples = False 173 | napoleon_use_admonition_for_notes = False 174 | napoleon_use_admonition_for_references = False 175 | napoleon_use_ivar = False 176 | napoleon_use_param = True 177 | napoleon_use_rtype = True 178 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to pytsp's documentation! 2 | ====================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | readme 9 | installation 10 | usage 11 | modules 12 | contributing 13 | history 14 | 15 | Indices and tables 16 | ================== 17 | * :ref:`genindex` 18 | * :ref:`modindex` 19 | * :ref:`search` 20 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | 8 | Stable release 9 | -------------- 10 | 11 | To install pytsp, run this command in your terminal: 12 | 13 | .. code-block:: console 14 | 15 | $ pip install pytsp 16 | 17 | This is the preferred method to install pytsp, as it will always install the most recent stable release. 18 | 19 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 20 | you through the process. 21 | 22 | .. _pip: https://pip.pypa.io 23 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ 24 | 25 | 26 | From sources 27 | ------------ 28 | 29 | The sources for pytsp can be downloaded from the `Github repo`_. 30 | 31 | You can either clone the public repository: 32 | 33 | .. code-block:: console 34 | 35 | $ git clone git://github.com/BraveDistribution/pytsp 36 | 37 | Or download the `tarball`_: 38 | 39 | .. code-block:: console 40 | 41 | $ curl -OL https://github.com/BraveDistribution/pytsp/tarball/master 42 | 43 | Once you have a copy of the source, you can install it with: 44 | 45 | .. code-block:: console 46 | 47 | $ python setup.py install 48 | 49 | 50 | .. _Github repo: https://github.com/BraveDistribution/pytsp 51 | .. _tarball: https://github.com/BraveDistribution/pytsp/tarball/master 52 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=pytsp 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | pytsp 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pytsp 8 | -------------------------------------------------------------------------------- /docs/pytsp.data_structures.rst: -------------------------------------------------------------------------------- 1 | pytsp.data\_structures package 2 | ============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | pytsp.data\_structures.data\_structure module 8 | --------------------------------------------- 9 | 10 | .. automodule:: pytsp.data_structures.data_structure 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pytsp.data\_structures.opt\_case module 16 | --------------------------------------- 17 | 18 | .. automodule:: pytsp.data_structures.opt_case 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: pytsp.data_structures 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/pytsp.rst: -------------------------------------------------------------------------------- 1 | pytsp package 2 | ============= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | pytsp.data_structures 10 | 11 | Submodules 12 | ---------- 13 | 14 | pytsp.christofides\_tsp module 15 | ------------------------------ 16 | 17 | .. automodule:: pytsp.christofides_tsp 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | pytsp.constants module 23 | ---------------------- 24 | 25 | .. automodule:: pytsp.constants 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | pytsp.had\_tsp module 31 | --------------------- 32 | 33 | .. automodule:: pytsp.had_tsp 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | pytsp.k\_opt\_tsp module 39 | ------------------------ 40 | 41 | .. automodule:: pytsp.k_opt_tsp 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | pytsp.lin\_kerdighan\_tsp module 47 | -------------------------------- 48 | 49 | .. automodule:: pytsp.lin_kerdighan_tsp 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | 54 | pytsp.nearest\_neighbor\_tsp module 55 | ----------------------------------- 56 | 57 | .. automodule:: pytsp.nearest_neighbor_tsp 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | pytsp.permutations\_tsp module 63 | ------------------------------ 64 | 65 | .. automodule:: pytsp.permutations_tsp 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | 70 | pytsp.pytsp module 71 | ------------------ 72 | 73 | .. automodule:: pytsp.pytsp 74 | :members: 75 | :undoc-members: 76 | :show-inheritance: 77 | 78 | pytsp.utils module 79 | ------------------ 80 | 81 | .. automodule:: pytsp.utils 82 | :members: 83 | :undoc-members: 84 | :show-inheritance: 85 | 86 | 87 | Module contents 88 | --------------- 89 | 90 | .. automodule:: pytsp 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/source/conf.rst: -------------------------------------------------------------------------------- 1 | conf module 2 | =========== 3 | 4 | .. automodule:: conf 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | pytsp 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pytsp 8 | -------------------------------------------------------------------------------- /docs/source/pytsp.data_structures.rst: -------------------------------------------------------------------------------- 1 | pytsp.data\_structures package 2 | ============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | pytsp.data\_structures.data\_structure module 8 | --------------------------------------------- 9 | 10 | .. automodule:: pytsp.data_structures.data_structure 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pytsp.data\_structures.opt\_case module 16 | --------------------------------------- 17 | 18 | .. automodule:: pytsp.data_structures.opt_case 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: pytsp.data_structures 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/pytsp.rst: -------------------------------------------------------------------------------- 1 | pytsp package 2 | ============= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | pytsp.data_structures 10 | 11 | Submodules 12 | ---------- 13 | 14 | pytsp.christofides\_tsp module 15 | ------------------------------ 16 | 17 | .. automodule:: pytsp.christofides_tsp 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | pytsp.constants module 23 | ---------------------- 24 | 25 | .. automodule:: pytsp.constants 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | pytsp.had\_tsp module 31 | --------------------- 32 | 33 | .. automodule:: pytsp.had_tsp 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | pytsp.k\_opt\_tsp module 39 | ------------------------ 40 | 41 | .. automodule:: pytsp.k_opt_tsp 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | pytsp.lin\_kerdighan\_tsp module 47 | -------------------------------- 48 | 49 | .. automodule:: pytsp.lin_kerdighan_tsp 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | 54 | pytsp.nearest\_neighbor\_tsp module 55 | ----------------------------------- 56 | 57 | .. automodule:: pytsp.nearest_neighbor_tsp 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | pytsp.permutations\_tsp module 63 | ------------------------------ 64 | 65 | .. automodule:: pytsp.permutations_tsp 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | 70 | pytsp.pytsp module 71 | ------------------ 72 | 73 | .. automodule:: pytsp.pytsp 74 | :members: 75 | :undoc-members: 76 | :show-inheritance: 77 | 78 | pytsp.utils module 79 | ------------------ 80 | 81 | .. automodule:: pytsp.utils 82 | :members: 83 | :undoc-members: 84 | :show-inheritance: 85 | 86 | 87 | Module contents 88 | --------------- 89 | 90 | .. automodule:: pytsp 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use pytsp in a project:: 6 | 7 | import pytsp 8 | -------------------------------------------------------------------------------- /modules.rst: -------------------------------------------------------------------------------- 1 | pytsp 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pytsp 8 | -------------------------------------------------------------------------------- /pytsp.data_structures.rst: -------------------------------------------------------------------------------- 1 | pytsp.data\_structures package 2 | ============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | pytsp.data\_structures.data\_structure module 8 | --------------------------------------------- 9 | 10 | .. automodule:: pytsp.data_structures.data_structure 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pytsp.data\_structures.opt\_case module 16 | --------------------------------------- 17 | 18 | .. automodule:: pytsp.data_structures.opt_case 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: pytsp.data_structures 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /pytsp.rst: -------------------------------------------------------------------------------- 1 | pytsp package 2 | ============= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | pytsp.data_structures 10 | 11 | Submodules 12 | ---------- 13 | 14 | pytsp.christofides\_tsp module 15 | ------------------------------ 16 | 17 | .. automodule:: pytsp.christofides_tsp 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | pytsp.constants module 23 | ---------------------- 24 | 25 | .. automodule:: pytsp.constants 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | pytsp.had\_tsp module 31 | --------------------- 32 | 33 | .. automodule:: pytsp.had_tsp 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | pytsp.k\_opt\_tsp module 39 | ------------------------ 40 | 41 | .. automodule:: pytsp.k_opt_tsp 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | pytsp.lin\_kerdighan\_tsp module 47 | -------------------------------- 48 | 49 | .. automodule:: pytsp.lin_kerdighan_tsp 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | 54 | pytsp.nearest\_neighbor\_tsp module 55 | ----------------------------------- 56 | 57 | .. automodule:: pytsp.nearest_neighbor_tsp 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | pytsp.permutations\_tsp module 63 | ------------------------------ 64 | 65 | .. automodule:: pytsp.permutations_tsp 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | 70 | pytsp.pytsp module 71 | ------------------ 72 | 73 | .. automodule:: pytsp.pytsp 74 | :members: 75 | :undoc-members: 76 | :show-inheritance: 77 | 78 | pytsp.utils module 79 | ------------------ 80 | 81 | .. automodule:: pytsp.utils 82 | :members: 83 | :undoc-members: 84 | :show-inheritance: 85 | 86 | 87 | Module contents 88 | --------------- 89 | 90 | .. automodule:: pytsp 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | -------------------------------------------------------------------------------- /pytsp/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Top-level package for pytsp.""" 4 | 5 | __author__ = """BraveDistribution""" 6 | __email__ = 'gazda.matej@gmail.com' 7 | __version__ = '0.1.0' 8 | name = "pytsp" 9 | -------------------------------------------------------------------------------- /pytsp/branch_and_bound_tsp.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import numpy as np 3 | 4 | from pytsp.data_structures.node import Node 5 | from pytsp.data_structures.tree import Tree 6 | 7 | logging.getLogger().setLevel(logging.DEBUG) 8 | 9 | # TODO: Need to finish this on 1961 paper. 10 | 11 | 12 | # hacky way to do it 13 | INFINITY = 10000000 14 | 15 | 16 | def branch_and_bound_tsp(graph): 17 | """ 18 | 2d numpy array, be cautious, it is cpu/memory/time expensive. Works on the asymetric tsp as well. 19 | :param graph: 20 | :return: exact solution 21 | """ 22 | bnb_tree = Tree() 23 | 24 | # we create new distance matrix from reduced one 25 | new_graph, bound = reduce_graph(graph) 26 | 27 | first_node = Node(bound, None, new_graph) 28 | bnb_tree.add_leaf(first_node) 29 | 30 | if not bnb_tree.is_optimized(): 31 | _optimize(bnb_tree) 32 | else: 33 | # construct path 34 | pass 35 | 36 | 37 | def _optimize(bnb_tree: Tree): 38 | node: Node = bnb_tree.get_leaf_with_lower_bound() 39 | # remove node from list of leafs, because it has the children now 40 | 41 | bnb_tree.remove_leaf(node) 42 | 43 | left_node, right_node = _branch_and_get_children(node) 44 | 45 | 46 | def reduce_graph(graph): 47 | """Reduces the matrix so at least one zero is in each column & each row""" 48 | lower_bound_reduction = 0 49 | matrix_size = len(graph) 50 | new_graph = graph.copy() 51 | 52 | for index in range(matrix_size): 53 | if 0 not in graph[index, :]: 54 | arr_min = min(graph[index, :]) 55 | lower_bound_reduction += arr_min 56 | new_graph[index, :] = graph[index, :] - arr_min 57 | 58 | new_graph = new_graph.T 59 | for index in range(matrix_size): 60 | if 0 not in new_graph[index, :]: 61 | arr_min = min(new_graph[index, :]) 62 | lower_bound_reduction += arr_min 63 | new_graph[index, :] = new_graph[index, :] - arr_min 64 | 65 | logging.debug("Lower bound reduction: %s" % lower_bound_reduction) 66 | return new_graph, lower_bound_reduction 67 | 68 | 69 | def _calculate_delta_value(graph): 70 | """ 71 | According to the original paper delta(k,l) = min of row K except K,L item + 72 | min of col L except K,L item 73 | 74 | We need to find the biggest delta and create a new branch. It is satisfactory to look only 75 | into 0 numbers in the array, because elsewhere it is still = 0. 76 | """ 77 | 78 | row, col = np.where(graph == 0) 79 | delta_values = [] 80 | for index_of_zero in range(len(row)): 81 | # minimum value of row + minimum value of column (excpt the row,col item 82 | delta_values.append(np.min(graph[row[index_of_zero], :][np.arange(len(graph)) != col[index_of_zero]]) + 83 | np.min(graph[:, col[index_of_zero]][np.arange(len(graph)) != row[index_of_zero]])) 84 | 85 | biggest_delta_index = np.argmax(delta_values) 86 | return (row[biggest_delta_index], col[biggest_delta_index]), delta_values[biggest_delta_index] 87 | 88 | 89 | def _branch_and_get_children(node: Node): 90 | """Branch and get the two children, set children of main node to the two""" 91 | row_col_tuple, delta_value = _calculate_delta_value(node.matrix) 92 | 93 | left_node = Node 94 | right_node = Node 95 | 96 | node.right_succ = right_node 97 | 98 | return Node, Node 99 | 100 | 101 | def _branch_left(node: Node, row_col_tuple, delta_value): 102 | """Branch to left""" 103 | left_node = Node(delta_value, node, node.matrix) 104 | node.left_succ = left_node 105 | 106 | 107 | def _branch_right(node: Node): 108 | """Branch to right""" 109 | submatrix = 0 110 | 111 | 112 | def __remove_col_and_row_and_set_to_inf(matrix, row_col_tuple): 113 | """Create new submatrix with row/col crossed out and (row, col) set to 0""" 114 | -------------------------------------------------------------------------------- /pytsp/branch_and_bound_tsp_dfs.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from numpy.core.multiarray import ndarray 4 | 5 | from pytsp.data_structures.node import Node 6 | from pytsp.data_structures.tree import Tree 7 | from pytsp.utils import route_cost 8 | 9 | 10 | def branch_and_bound_tsp_bfs(graph: ndarray): 11 | """ Branch and bound by BFS, lower bound is spanning tree""" 12 | number_of_nodes = len(graph) 13 | bnb_tree = Tree(number_of_nodes) 14 | 15 | # top level node 16 | top_lower_bound = 0 17 | for node in graph: 18 | top_lower_bound += np.sum(np.sort(node[node.nonzero()])[:2]) 19 | 20 | # top node 21 | top_node = Node(top_lower_bound / 2, [0], graph) 22 | bnb_tree.add_leaf(top_node) 23 | 24 | while not bnb_tree.optimized(): 25 | most_promising_leaf = bnb_tree.get_leaf_with_lowest_bound() 26 | bnb_tree.remove_leaf_from_list(most_promising_leaf) 27 | 28 | unvisited_nodes = list(bnb_tree.node_indexes - set(most_promising_leaf.visited_nodes)) 29 | for vertex in unvisited_nodes: 30 | lower_bound = calculate_two_neighbor_bound(graph, most_promising_leaf, vertex) 31 | new_leaf = Node(lower_bound, most_promising_leaf.visited_nodes + [vertex], most_promising_leaf) 32 | bnb_tree.add_leaf(new_leaf) 33 | 34 | if len(new_leaf.visited_nodes) is number_of_nodes: 35 | new_leaf.visited_nodes.append(0) 36 | bnb_tree.set_solution(route_cost(graph, new_leaf.visited_nodes)) 37 | print(*new_leaf.visited_nodes, sep=", ") 38 | print('Solution found %s' % (str(route_cost(graph, new_leaf.visited_nodes)))) 39 | bnb_tree.remove_leaf_from_list(new_leaf) 40 | 41 | print('Best solution found: %s' % str(bnb_tree.best_solution_value)) 42 | 43 | 44 | def calculate_two_neighbor_bound(graph, predecessor, next_vertex_index): 45 | """Calculate two neighbor bound, it means take all nodes""" 46 | node_from = predecessor.visited_nodes[-1] 47 | node_to = next_vertex_index 48 | 49 | node_from_array = graph[node_from] 50 | node_to_array = graph[node_to] 51 | 52 | lower_bound = predecessor.bound - ( 53 | np.min(node_from_array[node_from_array.nonzero()]) + np.min(node_to_array[node_to_array.nonzero()])) / 2 + \ 54 | graph[node_from][node_to] 55 | return lower_bound 56 | -------------------------------------------------------------------------------- /pytsp/christofides_tsp.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import numpy as np 4 | import networkx as nx 5 | 6 | from networkx.algorithms.matching import max_weight_matching 7 | from networkx.algorithms.euler import eulerian_circuit 8 | 9 | from pytsp.utils import minimal_spanning_tree 10 | 11 | 12 | def christofides_tsp(graph, starting_node=0): 13 | """ 14 | Christofides TSP algorithm 15 | http://www.dtic.mil/dtic/tr/fulltext/u2/a025602.pdf 16 | Args: 17 | graph: 2d numpy array matrix 18 | starting_node: of the TSP 19 | Returns: 20 | tour given by christofies TSP algorithm 21 | 22 | Examples: 23 | >>> import numpy as np 24 | >>> graph = np.array([[ 0, 300, 250, 190, 230], 25 | >>> [300, 0, 230, 330, 150], 26 | >>> [250, 230, 0, 240, 120], 27 | >>> [190, 330, 240, 0, 220], 28 | >>> [230, 150, 120, 220, 0]]) 29 | >>> christofides_tsp(graph) 30 | """ 31 | 32 | mst = minimal_spanning_tree(graph, 'Prim', starting_node=0) 33 | odd_degree_nodes = list(_get_odd_degree_vertices(mst)) 34 | odd_degree_nodes_ix = np.ix_(odd_degree_nodes, odd_degree_nodes) 35 | nx_graph = nx.from_numpy_array(-1 * graph[odd_degree_nodes_ix]) 36 | matching = max_weight_matching(nx_graph, maxcardinality=True) 37 | euler_multigraph = nx.MultiGraph(mst) 38 | for edge in matching: 39 | euler_multigraph.add_edge(odd_degree_nodes[edge[0]], odd_degree_nodes[edge[1]], 40 | weight=graph[odd_degree_nodes[edge[0]]][odd_degree_nodes[edge[1]]]) 41 | euler_tour = list(eulerian_circuit(euler_multigraph, source=starting_node)) 42 | path = list(itertools.chain.from_iterable(euler_tour)) 43 | return _remove_repeated_vertices(path, starting_node)[:-1] 44 | 45 | 46 | def _get_odd_degree_vertices(graph): 47 | """ 48 | Finds all the odd degree vertices in graph 49 | Args: 50 | graph: 2d np array as adj. matrix 51 | 52 | Returns: 53 | Set of vertices that have odd degree 54 | """ 55 | odd_degree_vertices = set() 56 | for index, row in enumerate(graph): 57 | if len(np.nonzero(row)[0]) % 2 != 0: 58 | odd_degree_vertices.add(index) 59 | return odd_degree_vertices 60 | 61 | 62 | def _remove_repeated_vertices(path, starting_node): 63 | path = list(dict.fromkeys(path).keys()) 64 | path.append(starting_node) 65 | return path 66 | -------------------------------------------------------------------------------- /pytsp/constants.py: -------------------------------------------------------------------------------- 1 | class Constants(): 2 | MAX_WEIGHT_OF_EDGE = 100000 3 | BOLTZMANN = 5.670367e-08 4 | -------------------------------------------------------------------------------- /pytsp/data_structures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BraveDistribution/pytsp/52144ef6283196cba01a2036987e9d7b982c6f99/pytsp/data_structures/__init__.py -------------------------------------------------------------------------------- /pytsp/data_structures/data_structure.py: -------------------------------------------------------------------------------- 1 | class Node(): 2 | def __init__(self, data, prev=None, next=None): 3 | self.prev = prev 4 | self.next = next 5 | self.data = data 6 | 7 | 8 | class DoublyLinkedList(): 9 | head = None 10 | tail = None 11 | 12 | def insert_after(self, prev_node, data): 13 | """ 14 | Insert new node with data after prev_node 15 | Args: 16 | prev_node: 17 | data: 18 | 19 | Returns: 20 | 21 | """ 22 | 23 | if prev_node is None: 24 | raise ValueError("prev_node doesn't exist in list") 25 | 26 | new_node = Node(data, prev_node, prev_node.next) 27 | 28 | if prev_node.next is not None: 29 | prev_node.next.prev = new_node 30 | else: 31 | tail = new_node 32 | 33 | prev_node.next = new_node 34 | 35 | def insert_before(self, next_node, data): 36 | """ 37 | Insert new node with data before next_node 38 | Args: 39 | next_node_node: 40 | data: 41 | 42 | Returns: 43 | 44 | """ 45 | 46 | if next_node is None: 47 | raise ValueError("next_node doesn't exist in list") 48 | 49 | new_node = Node(data, next_node.prev, next_node) 50 | next_node.prev = new_node 51 | 52 | if next_node.prev is None: 53 | head = new_node 54 | else: 55 | new_node.prev.next = new_node 56 | 57 | 58 | -------------------------------------------------------------------------------- /pytsp/data_structures/node.py: -------------------------------------------------------------------------------- 1 | class Node(): 2 | """ One characterizing one subset in the branch and bound algorithm""" 3 | 4 | def __init__(self, bound, visited_nodes, predecessor): 5 | self.bound = bound 6 | self.visited_nodes = visited_nodes 7 | self.predecessor = predecessor 8 | 9 | -------------------------------------------------------------------------------- /pytsp/data_structures/opt_case.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class OptCase(Enum): 5 | opt_case_1 = "opt_case_1" 6 | opt_case_2 = "opt_case_2" 7 | opt_case_3 = "opt_case_3" 8 | opt_case_4 = "opt_case_4" 9 | opt_case_5 = "opt_case_5" 10 | opt_case_6 = "opt_case_6" 11 | opt_case_7 = "opt_case_7" 12 | opt_case_8 = "opt_case_8" 13 | -------------------------------------------------------------------------------- /pytsp/data_structures/tree.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from typing import List, Any 4 | 5 | from pytsp.data_structures.node import Node 6 | 7 | 8 | class Tree: 9 | """ Tree used in the branch and bound process of TSP""" 10 | 11 | def __init__(self, graph_size): 12 | self.node_indexes = set((range(graph_size))) 13 | self.leafs: List[Node] = [] 14 | self.best_solution_value = None 15 | 16 | def add_leaf(self, node): 17 | if self.leafs: 18 | for index, leaf in enumerate(self.leafs): 19 | if node.bound < leaf.bound: 20 | self.leafs.insert(index, node) 21 | return 22 | self.leafs.append(node) 23 | else: 24 | self.leafs.append(node) 25 | 26 | 27 | def get_leaf_with_lowest_bound(self): 28 | return self.leafs[0] 29 | 30 | def remove_leaf_from_list(self, node): 31 | self.leafs.remove(node) 32 | 33 | def set_solution(self, value): 34 | self.best_solution_value = value 35 | 36 | def optimized(self): 37 | if self.best_solution_value is None or self.best_solution_value > self.leafs[0].bound: 38 | return False 39 | else: 40 | return True 41 | -------------------------------------------------------------------------------- /pytsp/genetic_algorithms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BraveDistribution/pytsp/52144ef6283196cba01a2036987e9d7b982c6f99/pytsp/genetic_algorithms/__init__.py -------------------------------------------------------------------------------- /pytsp/genetic_algorithms/simulated_annealing.py: -------------------------------------------------------------------------------- 1 | from itertools import cycle, dropwhile, islice 2 | 3 | import numpy as np 4 | import random 5 | 6 | from pytsp.constants import Constants 7 | from pytsp.christofides_tsp import christofides_tsp 8 | from pytsp.utils import route_cost 9 | 10 | graph = np.array([[0, 4, 3, 4, 5, 1, 2, 3, 4], 11 | [4, 0, 1, 4, 3, 4, 6, 2, 1], 12 | [3, 1, 0, 1, 4, 3, 2, 1, 9], 13 | [4, 4, 1, 0, 4, 6, 1, 2, 3], 14 | [5, 3, 4, 4, 0, 1, 2, 5, 3], 15 | [1, 4, 3, 6, 1, 0, 2, 5, 3], 16 | [2, 6, 2, 1, 2, 2, 0, 3, 5], 17 | [3, 2, 1, 2, 5, 5, 3, 0, 9], 18 | [4, 1, 9, 3, 3, 3, 5, 9, 0] 19 | ]) 20 | 21 | 22 | def simulated_annealing(graph, path=None, temperature=1, n_of_iter=1000, alpha=0.95): 23 | if path is None: 24 | path = christofides_tsp(graph) 25 | 26 | changed = True 27 | 28 | while changed: 29 | temp_solution = [node for node in path] 30 | for iter in range(n_of_iter): 31 | new_solution = _get_next_solution(path) 32 | 33 | new_route_cost = route_cost(graph, new_solution) 34 | current_route_cost = route_cost(graph, path) 35 | if new_route_cost < current_route_cost: 36 | path = new_solution 37 | else: 38 | if np.exp( 39 | (current_route_cost - new_route_cost) / (temperature * Constants.BOLTZMANN)) > random.random(): 40 | path = new_solution 41 | 42 | temperature = _decrease_temperature(temperature, alpha) 43 | 44 | if temp_solution == path: 45 | changed = False 46 | 47 | # just to start with the same node -> we will need to cycle the results. 48 | cycled = cycle(path) 49 | skipped = dropwhile(lambda x: x != 0, cycled) 50 | sliced = islice(skipped, None, len(path)) 51 | path = list(sliced) 52 | 53 | return path 54 | 55 | 56 | def _get_next_solution(path): 57 | new_solution = [node for node in path] 58 | left_index = random.randint(2, len(path) - 1) 59 | right_index = random.randint(0, len(path) - left_index) 60 | new_solution[right_index: (right_index + left_index)] = reversed( 61 | new_solution[right_index: (right_index + left_index)]) 62 | return new_solution 63 | 64 | 65 | def _decrease_temperature(temperature, alfa): 66 | return alfa * temperature 67 | -------------------------------------------------------------------------------- /pytsp/had_tsp.py: -------------------------------------------------------------------------------- 1 | def had_tsp(graph): 2 | """ 3 | This method is used in real warehouses, just a simple method where it orders the points lexicographically and then 4 | visit thems by the shortest route. 5 | Returns: 6 | Path 7 | """ 8 | for node in range(len(graph-1), 0, -1): 9 | pass 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /pytsp/k_opt_tsp.py: -------------------------------------------------------------------------------- 1 | from itertools import cycle, islice, dropwhile 2 | 3 | from pytsp.christofides_tsp import christofides_tsp 4 | from pytsp.data_structures.opt_case import OptCase 5 | from pytsp.utils import route_cost 6 | 7 | 8 | def _swap_2opt(route, i, k): 9 | """ Swapping the route """ 10 | new_route = route[0:i] 11 | new_route.extend(reversed(route[i:k + 1])) 12 | new_route.extend(route[k + 1:]) 13 | return new_route 14 | 15 | 16 | def tsp_2_opt(graph, route): 17 | """ 18 | Approximate the optimal path of travelling salesman according to 2-opt algorithm 19 | Args: 20 | graph: 2d numpy array as graph 21 | route: list of nodes 22 | 23 | Returns: 24 | optimal path according to 2-opt algorithm 25 | 26 | Examples: 27 | >>> import numpy as np 28 | >>> graph = np.array([[ 0, 300, 250, 190, 230], 29 | >>> [300, 0, 230, 330, 150], 30 | >>> [250, 230, 0, 240, 120], 31 | >>> [190, 330, 240, 0, 220], 32 | >>> [230, 150, 120, 220, 0]]) 33 | >>> tsp_2_opt(graph) 34 | """ 35 | improved = True 36 | best_found_route = route 37 | best_found_route_cost = route_cost(graph, best_found_route) 38 | while improved: 39 | improved = False 40 | for i in range(1, len(best_found_route) - 1): 41 | for k in range(i + 1, len(best_found_route) - 1): 42 | new_route = _swap_2opt(best_found_route, i, k) 43 | new_route_cost = route_cost(graph, new_route) 44 | if new_route_cost < best_found_route_cost: 45 | best_found_route_cost = new_route_cost 46 | best_found_route = new_route 47 | improved = True 48 | break 49 | if improved: 50 | break 51 | return best_found_route 52 | 53 | 54 | def tsp_3_opt(graph, route=None): 55 | """ 56 | Approximate the optimal path of travelling salesman according to 3-opt algorithm 57 | Args: 58 | graph: 2d numpy array as graph 59 | route: route as ordered list of visited nodes. if no route is given, christofides algorithm is used to create one. 60 | 61 | Returns: 62 | optimal path according to 3-opt algorithm 63 | Examples: 64 | >>> import numpy as np 65 | >>> graph = np.array([[ 0, 300, 250, 190, 230], 66 | >>> [300, 0, 230, 330, 150], 67 | >>> [250, 230, 0, 240, 120], 68 | >>> [190, 330, 240, 0, 220], 69 | >>> [230, 150, 120, 220, 0]]) 70 | >>> tsp_3_opt(graph) 71 | """ 72 | if route is None: 73 | route = christofides_tsp(graph) 74 | moves_cost = {OptCase.opt_case_1: 0, OptCase.opt_case_2: 0, 75 | OptCase.opt_case_3: 0, OptCase.opt_case_4: 0, OptCase.opt_case_5: 0, 76 | OptCase.opt_case_6: 0, OptCase.opt_case_7: 0, OptCase.opt_case_8: 0} 77 | improved = True 78 | best_found_route = route 79 | while improved: 80 | improved = False 81 | for (i, j, k) in possible_segments(len(graph)): 82 | # we check all the possible moves and save the result into the dict 83 | for opt_case in OptCase: 84 | moves_cost[opt_case] = get_solution_cost_change(graph, best_found_route, opt_case, i, j, k) 85 | # we need the minimum value of substraction of old route - new route 86 | best_return = max(moves_cost, key=moves_cost.get) 87 | if moves_cost[best_return] > 0: 88 | best_found_route = reverse_segments(best_found_route, best_return, i, j, k) 89 | improved = True 90 | break 91 | # just to start with the same node -> we will need to cycle the results. 92 | cycled = cycle(best_found_route) 93 | skipped = dropwhile(lambda x: x != 0, cycled) 94 | sliced = islice(skipped, None, len(best_found_route)) 95 | best_found_route = list(sliced) 96 | return best_found_route 97 | 98 | 99 | def possible_segments(N): 100 | """ Generate the combination of segments """ 101 | segments = ((i, j, k) for i in range(N) for j in range(i + 2, N-1) for k in range(j + 2, N - 1 + (i > 0))) 102 | return segments 103 | 104 | 105 | def get_solution_cost_change(graph, route, case, i, j, k): 106 | """ Compare current solution with 7 possible 3-opt moves""" 107 | A, B, C, D, E, F = route[i - 1], route[i], route[j - 1], route[j], route[k - 1], route[k % len(route)] 108 | if case == OptCase.opt_case_1: 109 | # first case is the current solution ABC 110 | return 0 111 | elif case == OptCase.opt_case_2: 112 | # second case is the case A'BC 113 | return graph[A, B] + graph[E, F] - (graph[B, F] + graph[A, E]) 114 | elif case == OptCase.opt_case_3: 115 | # ABC' 116 | return graph[C, D] + graph[E, F] - (graph[D, F] + graph[C, E]) 117 | elif case == OptCase.opt_case_4: 118 | # A'BC' 119 | return graph[A, B] + graph[C, D] + graph[E, F] - (graph[A, D] + graph[B, F] + graph[E, C]) 120 | elif case == OptCase.opt_case_5: 121 | # A'B'C 122 | return graph[A, B] + graph[C, D] + graph[E, F] - (graph[C, F] + graph[B, D] + graph[E, A]) 123 | elif case == OptCase.opt_case_6: 124 | # AB'C 125 | return graph[B, A] + graph[D, C] - (graph[C, A] + graph[B, D]) 126 | elif case == OptCase.opt_case_7: 127 | # AB'C' 128 | return graph[A, B] + graph[C, D] + graph[E, F] - (graph[B, E] + graph[D, F] + graph[C, A]) 129 | elif case == OptCase.opt_case_8: 130 | # A'B'C 131 | return graph[A, B] + graph[C, D] + graph[E, F] - (graph[A, D] + graph[C, F] + graph[B, E]) 132 | 133 | def reverse_segments(route, case, i, j, k): 134 | """ 135 | Create a new tour from the existing tour 136 | Args: 137 | route: existing tour 138 | case: which case of opt swaps should be used 139 | i: 140 | j: 141 | k: 142 | 143 | Returns: 144 | new route 145 | """ 146 | if (i - 1) < (k % len(route)): 147 | first_segment = route[k% len(route):] + route[:i] 148 | else: 149 | first_segment = route[k % len(route):i] 150 | second_segment = route[i:j] 151 | third_segment = route[j:k] 152 | 153 | if case == OptCase.opt_case_1: 154 | # first case is the current solution ABC 155 | pass 156 | elif case == OptCase.opt_case_2: 157 | # A'BC 158 | solution = list(reversed(first_segment)) + second_segment + third_segment 159 | elif case == OptCase.opt_case_3: 160 | # ABC' 161 | solution = first_segment + second_segment + list(reversed(third_segment)) 162 | elif case == OptCase.opt_case_4: 163 | # A'BC' 164 | solution = list(reversed(first_segment)) + second_segment + list(reversed(third_segment)) 165 | elif case == OptCase.opt_case_5: 166 | # A'B'C 167 | solution = list(reversed(first_segment)) + list(reversed(second_segment)) + third_segment 168 | elif case == OptCase.opt_case_6: 169 | # AB'C 170 | solution = first_segment + list(reversed(second_segment)) + third_segment 171 | elif case == OptCase.opt_case_7: 172 | # AB'C' 173 | solution = first_segment + list(reversed(second_segment)) + list(reversed(third_segment)) 174 | elif case == OptCase.opt_case_8: 175 | # A'B'C 176 | solution = list(reversed(first_segment)) + list(reversed(second_segment)) + list(reversed(third_segment)) 177 | return solution 178 | -------------------------------------------------------------------------------- /pytsp/lin_kerdighan_tsp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from math import ceil, floor 4 | 5 | from pytsp.utils import minimal_spanning_tree 6 | 7 | 8 | def lin_kerdighan_tsp(graph, starting_tour='', starting_node=0): 9 | """ 10 | Lin-Kerdighan TSP: https://en.wikipedia.org/wiki/Lin–Kernighan_heuristic 11 | Args: 12 | graph: 2d numpy array matrix (undirected, weighted graph) 13 | starting_tour: type of starting tour 14 | starting_node: 15 | 16 | Returns: 17 | TSP 18 | """ 19 | 20 | if starting_tour == '': 21 | pass 22 | 23 | _get_mst_and_add_two_edges(graph, starting_node) 24 | 25 | 26 | def _get_mst_and_add_two_edges(graph, starting_node): 27 | """ 28 | We need to create MST from graph except the starting node. After the MST is created, we need to connect 2 closest edges 29 | with starting_node to create a cycle. 30 | Args: 31 | graph: 2d numpy array matrix 32 | starting_node: starting node 33 | 34 | Returns: 35 | MST with cycle at the start node 36 | 37 | // TODO maybe this one needs optimization, unfortunately I am not really high-efficient with numpy. 38 | """ 39 | mst_without_starting_node = minimal_spanning_tree( 40 | np.delete(np.delete(graph, starting_node, axis=1), starting_node, axis=0), starting_node=starting_node) 41 | 42 | # 0 means that the edge does not exist 43 | two_closest_edges = np.argsort(np.ma.masked_equal(graph[starting_node], 0.0, copy=False))[:2] 44 | 45 | # we need to populate the result 46 | result = np.insert(np.insert(mst_without_starting_node, 0, np.array([0, 0, 0, 0]), axis=0), 0, 47 | np.array([0, 0, 0, 0, 0]), axis=1) 48 | result[starting_node][two_closest_edges[0]] = graph[starting_node][two_closest_edges[0]] 49 | result[starting_node][two_closest_edges[1]] = graph[starting_node][two_closest_edges[1]] 50 | 51 | result[two_closest_edges[0]][starting_node] = graph[starting_node][two_closest_edges[0]] 52 | result[two_closest_edges[1]][starting_node] = graph[starting_node][two_closest_edges[1]] 53 | return result 54 | 55 | 56 | def get_B_matrix(one_tree_graph): 57 | """ Calculate matrix of B values """ 58 | B = np.zeros((len(one_tree_graph), len(one_tree_graph))) 59 | B[0][0] = -10000 60 | B[len(one_tree_graph) - 1][len(one_tree_graph) - 1] = -10000 61 | for i in range(1, len(one_tree_graph) - 1): 62 | B[i][i] = -10000 63 | for j in range(i + 1, len(one_tree_graph)): 64 | dad_node = dad(one_tree_graph, j) 65 | B[i][j] = max(B[i][j], one_tree_graph[j][dad_node]) 66 | B[j][i] = B[i][j] 67 | return B 68 | 69 | 70 | def dad(graph, node): 71 | """Calculate dad of the given node from np.array graph""" 72 | return np.where(graph[node] > 0)[0][0] 73 | 74 | 75 | def create_D_matrix(cost_matrix, pi_vector_new): 76 | """ 77 | According to the paper, transofrmation into the D matrix significantly improves the approximation. 78 | D[i][j] = cost_matrix[i][j] + pi[i]+pi[j], where pi is a vector. 79 | pi^(k+1) = pi^k + t^k*v^k 80 | 81 | The aim is now to find a transformation C->D, given by the vector pi = (pi1, pi2, ..., pin), that maximizes the lower bound 82 | w(pi)=L(Tn) - 2*sum(pii). 83 | Args: 84 | cost_matrix (2d np. array): 85 | 86 | Returns: 87 | Transformed cost matrix into D_matrix (2d np.array). 88 | """ 89 | nodes_count = len(cost_matrix) 90 | for row in range(nodes_count): 91 | for col in range(nodes_count): 92 | cost_matrix[row][col] = cost_matrix[row][col] + pi_vector_new[row] + pi_vector_new[col] 93 | return cost_matrix 94 | 95 | 96 | def _create_min_1_tree(matrix): 97 | """Create minimum 1 tree 98 | Special node for 1 tree computation is not fixed. 99 | Create MST and choose one leaf that has the longest second nearest neighbour distance 100 | """ 101 | result = minimal_spanning_tree(matrix) 102 | matrix_without_mst = matrix - result 103 | min_values = list(map(lambda x: -1 if len(np.nonzero(x)[0]) == 0 else np.min(x[np.nonzero(x)]), matrix_without_mst)) 104 | special_edge_start = np.argmax(min_values) 105 | special_edge_end = np.where(matrix_without_mst[special_edge_start] == min_values[special_edge_start])[0][0] 106 | result[special_edge_start][special_edge_end] = matrix[special_edge_start][special_edge_end] 107 | result[special_edge_end][special_edge_start] = matrix[special_edge_end][special_edge_start] 108 | return result 109 | 110 | 111 | def subgradient_optimization(matrix): 112 | """ 113 | Subgradient optimization of the given 114 | Args: 115 | matrix: 116 | Returns: 117 | """ 118 | nodes_count = len(matrix) 119 | 120 | # initialize parameters 121 | pi_vector = np.zeros(nodes_count) 122 | W = -1000000 123 | 124 | # look at LKH report to understand this more clearly. 125 | period = ceil(nodes_count / 2) 126 | period_left = period 127 | step_size = 1 128 | D = matrix 129 | prev_subgradient_vector = 0 130 | W_prev = W 131 | for k in range(10000): 132 | # minimum 1 tree 133 | min_1_tree = _create_min_1_tree(D) 134 | 135 | # lower bound 136 | w_pi_k = _compute_lower_bound_w(min_1_tree, pi_vector) 137 | W = max(w_pi_k, W) 138 | 139 | subgradient_vector = _compute_degree_array(min_1_tree) - 2 140 | 141 | # Step size is doubled in the beginning of the first period until W does not increase 142 | if W > W_prev and k < period: 143 | step_size = step_size * 2 144 | 145 | # in the first iteration set prev_subgradient_vector to v_k or else in the calculation pi vector it would fail 146 | if k is 0: 147 | prev_subgradient_vector = subgradient_vector 148 | 149 | pi_vector = pi_vector + step_size * ( 150 | 0.7 * subgradient_vector + 0.3 * prev_subgradient_vector) # update pi vector 151 | 152 | # condition for termination 153 | if step_size is 0 or period is 0 or sum(subgradient_vector) is 0: 154 | break 155 | 156 | period_left = period_left - 1 157 | 158 | if period_left is 0: 159 | # if W was increased, then the period should be doubled. 160 | if W > W_prev: 161 | period = period * 2 162 | period_left = period 163 | else: 164 | period = floor(period / 2) 165 | period_left = period 166 | # step size is halved 167 | step_size = ceil(period / 2) 168 | 169 | prev_subgradient_vector = subgradient_vector 170 | W_prev = W 171 | 172 | D = create_D_matrix(matrix, pi_vector) 173 | 174 | return pi_vector 175 | 176 | 177 | def _compute_lower_bound_w(min_1_tree: np.array, pi_vector: np.array) -> float: 178 | """ 179 | Calculate the lower bound for given min-1-tree and vector pi 180 | Args: 181 | min_1_tree: 182 | pi_vector: 183 | 184 | Returns: 185 | lower_bound_w (float) = (length of min-1_tree - 2*sum of items in pi_vector). 186 | """ 187 | return np.sum(min_1_tree) - 2 * np.sum(pi_vector) 188 | 189 | 190 | def _compute_degree_array(min_1_tree): 191 | """Calculates the vector that saves the degree of nodes""" 192 | degree_vector_array = np.zeros(len(min_1_tree)) 193 | for index, node in enumerate(min_1_tree): 194 | degree_vector_array[index] = np.count_nonzero(node) 195 | return degree_vector_array 196 | -------------------------------------------------------------------------------- /pytsp/nearest_neighbor_tsp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pytsp.constants import Constants 4 | 5 | 6 | def nearest_neighbor_tsp(graph, starting_point=0): 7 | """ 8 | Nearest neighbor TSP algorithm 9 | Args: 10 | graph: 2d numpy array 11 | starting_point: index of starting node 12 | 13 | Returns: 14 | tour approximated by nearest neighbor tsp algorithm 15 | Examples: 16 | >>> import numpy as np 17 | >>> graph = np.array([[ 0, 300, 250, 190, 230], 18 | >>> [300, 0, 230, 330, 150], 19 | >>> [250, 230, 0, 240, 120], 20 | >>> [190, 330, 240, 0, 220], 21 | >>> [230, 150, 120, 220, 0]]) 22 | >>> nearest_neighbor_tsp(graph) 23 | """ 24 | number_of_nodes = len(graph) 25 | unvisited_nodes = list(range(number_of_nodes)) 26 | unvisited_nodes.remove(starting_point) 27 | visited_nodes = [starting_point] 28 | 29 | while number_of_nodes > len(visited_nodes): 30 | neighbors = _find_neighbors( 31 | np.array(graph[visited_nodes[-1]])) 32 | 33 | not_visited_neighbours = list( 34 | set(neighbors[0].flatten()).intersection(unvisited_nodes)) 35 | 36 | # pick the one travelling salesman has not been to yet 37 | try: 38 | next_node = _find_next_pickup_item( 39 | not_visited_neighbours, np.array(graph[visited_nodes[-1]])) 40 | except ValueError: 41 | print("Nearest Neighbor algorithm couldn't find a neighbor that hasn't been to yet") 42 | return 43 | visited_nodes.append(next_node) 44 | unvisited_nodes.remove(next_node) 45 | return visited_nodes 46 | 47 | 48 | def _find_neighbors(array_of_edges_from_node): 49 | """ 50 | Find the list of all neighbors for given node from adjacency matrix. 51 | Args: 52 | array_of_edges_from_node (np.array): 53 | 54 | Returns: 55 | List [] of all neighbors for given node 56 | """ 57 | mask = (array_of_edges_from_node > 0) & ( 58 | array_of_edges_from_node < Constants.MAX_WEIGHT_OF_EDGE) 59 | return np.where(np.squeeze(np.asarray(mask))) 60 | 61 | 62 | def _find_next_pickup_item(not_visited_neighbors, array_of_edges_from_node): 63 | """ 64 | 65 | Args: 66 | not_visited_neighbors: 67 | array_of_edges_from_node: 68 | 69 | Returns: 70 | 71 | """ 72 | # last node in visited_nodes is where the traveling salesman is. 73 | cheapest_path = np.argmin( 74 | array_of_edges_from_node[not_visited_neighbors]) 75 | return not_visited_neighbors[cheapest_path] 76 | -------------------------------------------------------------------------------- /pytsp/permutations_tsp.py: -------------------------------------------------------------------------------- 1 | from itertools import permutations 2 | from pytsp.utils import route_cost 3 | 4 | def permutations_tsp(graph): 5 | """ 6 | Creates all permutations from the added graph check all the combinations and the route costs and then 7 | returns the best solution. BEWARE: this method is computationally expensive, therefore it shouldn't be run on over 12 nodes 8 | Args: 9 | graph: graph 10 | 11 | Returns: 12 | list of nodes [] 13 | """ 14 | route = range(0, len(graph)) 15 | # generate all permutations 16 | all_routes = [list(i) for i in permutations(route)] 17 | # calculate all costs 18 | cost_all_routes = [route_cost(graph, i) for i in all_routes] 19 | # find the lowest cost 20 | best_route = all_routes[cost_all_routes.index(min(cost_all_routes))] 21 | return best_route 22 | -------------------------------------------------------------------------------- /pytsp/pytsp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Main module.""" 4 | -------------------------------------------------------------------------------- /pytsp/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from random import randint 4 | 5 | 6 | def minimal_spanning_tree(graph, mode='Prim', starting_node=None): 7 | """ 8 | 9 | Args: 10 | graph: weighted adjacency matrix as 2d np.array 11 | mode: method for calculating minimal spanning tree 12 | starting_node: node number to start construction of minimal spanning tree (Prim) 13 | Returns: 14 | minimal spanning tree as 2d array 15 | """ 16 | 17 | if mode == 'Prim': 18 | return _minimal_spanning_tree_prim(graph, starting_node) 19 | 20 | 21 | def _minimal_spanning_tree_prim(graph, starting_node): 22 | """ 23 | 24 | Args: 25 | graph: weighted adj. matrix as 2d np.array 26 | starting_node: node number to start construction of minimal spanning tree 27 | Returns: 28 | minimal spanning tree as 2d array calculted by Prim 29 | """ 30 | 31 | node_count = len(graph) 32 | all_nodes = [i for i in range(node_count)] 33 | 34 | if starting_node is None: 35 | starting_node = randint(0, node_count-1) 36 | 37 | unvisited_nodes = all_nodes 38 | visited_nodes = [starting_node] 39 | unvisited_nodes.remove(starting_node) 40 | mst = np.zeros((node_count, node_count)) 41 | 42 | while len(visited_nodes) != node_count: 43 | selected_subgraph = graph[np.array(visited_nodes)[:, None], np.array(unvisited_nodes)] 44 | # we mask non-exist edges with -- so it doesn't crash the argmin 45 | min_edge_index = np.unravel_index(np.ma.masked_equal(selected_subgraph, 0, copy=False).argmin(), 46 | selected_subgraph.shape) 47 | edge_from = visited_nodes[min_edge_index[0]] 48 | edge_to = unvisited_nodes[min_edge_index[1]] 49 | mst[edge_from, edge_to] = graph[edge_from, edge_to] 50 | mst[edge_to, edge_from] = graph[edge_from, edge_to] 51 | unvisited_nodes.remove(edge_to) 52 | visited_nodes.append(edge_to) 53 | return mst 54 | 55 | 56 | def route_cost(graph, path): 57 | cost = 0 58 | for index in range(len(path) - 1): 59 | cost = cost + graph[path[index]][path[index + 1]] 60 | # add last edge to form a cycle. 61 | cost = cost + graph[path[-1], path[0]] 62 | return cost 63 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | atomicwrites==1.3.0 2 | attrs==19.1.0 3 | bleach==3.0.2 4 | certifi==2018.11.29 5 | cffi==1.11.5 6 | chardet==3.0.4 7 | cmarkgfm==0.4.2 8 | decorator==4.3.0 9 | docutils==0.14 10 | future==0.17.1 11 | idna==2.7 12 | more-itertools==6.0.0 13 | networkx==2.2 14 | numpy==1.15.3 15 | pkginfo==1.4.2 16 | pluggy==0.9.0 17 | py==1.8.0 18 | pycparser==2.19 19 | Pygments==2.2.0 20 | pytest==4.3.0 21 | readme-renderer==22.0 22 | requests==2.20.0 23 | requests-toolbelt==0.8.0 24 | scipy==1.1.0 25 | six==1.11.0 26 | tqdm==4.28.0 27 | twine==1.12.1 28 | urllib3==1.24 29 | webencodings==0.5.1 30 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:pytsp/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | 20 | [aliases] 21 | # Define setup.py command aliases here 22 | 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup script.""" 5 | 6 | from setuptools import setup, find_packages 7 | 8 | with open('README.rst') as readme_file: 9 | readme = readme_file.read() 10 | 11 | with open('HISTORY.rst') as history_file: 12 | history = history_file.read() 13 | 14 | requirements = [ ] 15 | 16 | setup_requirements = [ ] 17 | 18 | test_requirements = [ ] 19 | 20 | setup( 21 | author="BraveDistribution", 22 | author_email='gazda.matej@gmail.com', 23 | classifiers=[ 24 | 'Development Status :: 2 - Pre-Alpha', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Natural Language :: English', 28 | 'Programming Language :: Python :: 3', 29 | 'Programming Language :: Python :: 3.4', 30 | 'Programming Language :: Python :: 3.5', 31 | 'Programming Language :: Python :: 3.6', 32 | 'Programming Language :: Python :: 3.7', 33 | ], 34 | description="Few algorithms for TSP problem in Python", 35 | install_requires=requirements, 36 | license="MIT license", 37 | long_description=readme + '\n\n' + history, 38 | include_package_data=True, 39 | keywords='pytsp', 40 | name='pytsp', 41 | packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), 42 | setup_requires=setup_requirements, 43 | test_suite='tests', 44 | tests_require=test_requirements, 45 | url='https://github.com/BraveDistribution/pytsp', 46 | version='0.2.7', 47 | zip_safe=False, 48 | ) 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Unit test package for pytsp.""" 4 | -------------------------------------------------------------------------------- /tests/test_branch_and_bound_tsp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from branch_and_bound_tsp_dfs import branch_and_bound_tsp_bfs 3 | 4 | from pytsp.branch_and_bound_tsp import branch_and_bound_tsp, _calculate_delta_value, reduce_graph 5 | 6 | np.set_printoptions(suppress=True) 7 | INF = 100000 8 | 9 | 10 | def test_bnb_function(): 11 | graph = np.array([ 12 | [INF, 27, 43, 16, 30, 26], 13 | [7, INF, 16, 1, 30, 25], 14 | [20, 13, INF, 35, 5, 0], 15 | [21, 16, 25, INF, 18, 18], 16 | [12, 46, 27, 48, INF, 5], 17 | [25, 5, 5, 9, 5, INF] 18 | ]) 19 | 20 | path = branch_and_bound_tsp(graph) 21 | 22 | 23 | def test_reduce_graph(): 24 | graph = np.array([ 25 | [INF, 27, 43, 16, 30, 26], 26 | [7, INF, 16, 1, 30, 25], 27 | [20, 13, INF, 35, 5, 0], 28 | [21, 16, 25, INF, 18, 18], 29 | [12, 46, 27, 48, INF, 5], 30 | [23, 5, 5, 9, 5, INF] 31 | ]) 32 | 33 | reduced_matrix, lower_bound = reduce_graph(graph) 34 | print(lower_bound) 35 | pass 36 | 37 | 38 | def test_get_delta_values(): 39 | graph = np.array([ 40 | [INF, 11, 27, 0, 14, 10], 41 | [1, INF, 15, 0, 29, 24], 42 | [15, 13, INF, 35, 5, 0], 43 | [0, 0, 9, INF, 2, 2], 44 | [2, 41, 22, 43, INF, 0], 45 | [13, 0, 0, 4, 0, INF] 46 | ]) 47 | 48 | print(_calculate_delta_value(graph)) 49 | 50 | 51 | def test_bnb_dfs(): 52 | test = np.array([ 53 | [0, 2, 4, 5, 1], 54 | [2, 0, 5, 9, 2], 55 | [4, 5, 0, 1, 3], 56 | [5, 9, 1, 0, 2], 57 | [1, 2, 3, 2, 0] 58 | ]) 59 | 60 | branch_and_bound_tsp_bfs(test) 61 | -------------------------------------------------------------------------------- /tests/test_christofides_tsp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pytsp.christofides_tsp import _get_odd_degree_vertices, christofides_tsp 4 | 5 | 6 | def test_get_odd_degree_vertices(): 7 | graph = np.array([ 8 | [0, 1, 3, 0, 2, 4], 9 | [0, 0, 4, 5, 6, 1], 10 | [2, 0, 0, 1, 2, 3], 11 | [3, 3, 1, 0, 3, 6], 12 | [0, 0, 0, 0, 1, 0] 13 | ]) 14 | 15 | result = _get_odd_degree_vertices(graph) 16 | odd_degrees = {3, 4} 17 | 18 | assert result == odd_degrees 19 | 20 | 21 | def test_get_minimal_matching(): 22 | graph = np.array([ 23 | [0, 1, 1, 1, 1], 24 | [1, 0, 1, 1, 2], 25 | [1, 1, 0, 2, 1], 26 | [1, 1, 2, 0, 1], 27 | [1, 2, 1, 1, 0], 28 | ]) 29 | print(christofides_tsp(graph)) 30 | assert 1 == 1 31 | -------------------------------------------------------------------------------- /tests/test_k_opt.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pytsp import k_opt_tsp 4 | from pytsp.data_structures.opt_case import OptCase 5 | from pytsp.k_opt_tsp import reverse_segments, tsp_3_opt, tsp_2_opt, get_solution_cost_change 6 | from pytsp.utils import route_cost 7 | 8 | 9 | class TestKOpt(): 10 | A = [0, 1] 11 | B = [2, 3] 12 | C = [4, 5] 13 | 14 | graph = np.array([[0, 7, 6, 3, 1, 2], 15 | [7, 0, 9, 3, 4, 2], 16 | [6, 9, 0, 3, 6, 7], 17 | [3, 3, 3, 0, 2, 5], 18 | [1, 4, 6, 2, 0, 1], 19 | [2, 2, 7, 5, 1, 0]]) 20 | 21 | graph2 = np.array([[0, 4, 3, 4, 5, 1, 2, 3, 4], 22 | [4, 0, 1, 4, 3, 4, 6, 2, 1], 23 | [3, 1, 0, 1, 4, 3, 2, 1, 9], 24 | [4, 4, 1, 0, 4, 6, 1, 2, 3], 25 | [5, 3, 4, 4, 0, 1, 2, 5, 3], 26 | [1, 4, 3, 6, 1, 0, 2, 5, 3], 27 | [2, 6, 2, 1, 2, 2, 0, 3, 5], 28 | [3, 2, 1, 2, 5, 5, 3, 0, 9], 29 | [4, 1, 9, 3, 3, 3, 5, 9, 0] 30 | ]) 31 | 32 | def test_get_all_segments(self): 33 | segments = k_opt_tsp.possible_segments(6) 34 | assert [(0, 2, 4), (1, 3, 5)] == list(segments) 35 | 36 | def test_get_all_segments2(self): 37 | segments = k_opt_tsp.possible_segments(7) 38 | assert [(1, 3, 5), (1, 3, 6), (1, 4, 6), (2, 4, 6), (2, 4, 7), (2, 5, 7), (3, 5, 7)] == list(segments) 39 | 40 | def test_reverse_segment_case1(self): 41 | route = [0, 1, 2, 3, 4, 5] 42 | i, j, k = 1, 3, 5 43 | result = reverse_segments(route, OptCase.opt_case_1, i, j, k) 44 | assert result == self.A + self.B + self.C 45 | 46 | result = reverse_segments(route, OptCase.opt_case_2, i, j, k) 47 | assert result == self.A + self.B + list(reversed(self.C)) 48 | 49 | result = reverse_segments(route, OptCase.opt_case_3, i, j, k) 50 | assert result == self.A + list(reversed(self.B)) + self.C 51 | 52 | result = reverse_segments(route, OptCase.opt_case_4, i, j, k) 53 | assert result == self.A + list(reversed(self.B)) + list(reversed(self.C)) 54 | 55 | result = reverse_segments(route, OptCase.opt_case_5, i, j, k) 56 | assert result == self.A + self.C + list(reversed(self.B)) 57 | 58 | result = reverse_segments(route, OptCase.opt_case_6, i, j, k) 59 | assert result == self.A + list(reversed(self.C)) + list(reversed(self.B)) 60 | 61 | result = reverse_segments(route, OptCase.opt_case_7, i, j, k) 62 | assert result == self.A + list(reversed(self.C)) + self.B 63 | 64 | result = reverse_segments(route, OptCase.opt_case_8, i, j, k) 65 | assert result == self.A + self.C + self.B 66 | 67 | def test_3_opt_algorithm(self): 68 | graph = np.array([[0, 5, 2, 1, 1, 4], 69 | [5, 0, 6, 2, 2, 5], 70 | [2, 6, 0, 7, 4, 2], 71 | [1, 2, 7, 0, 8, 4], 72 | [1, 2, 4, 8, 0, 5], 73 | [4, 5, 2, 4, 5, 0]]) 74 | route = [0, 1, 2, 3, 4, 5] 75 | result = tsp_3_opt(graph, route) 76 | new_route_cost = route_cost(graph, result) 77 | old_route_cost = route_cost(graph, route) 78 | print(new_route_cost) 79 | print(result) 80 | print(old_route_cost) 81 | assert old_route_cost > new_route_cost 82 | 83 | def test_2_opt_algorithm(self): 84 | graph = np.array([[0, 5, 2, 1, 1, 4], 85 | [5, 0, 6, 2, 2, 5], 86 | [2, 6, 0, 7, 4, 2], 87 | [1, 2, 7, 0, 8, 4], 88 | [1, 2, 4, 8, 0, 5], 89 | [4, 5, 2, 4, 5, 0]]) 90 | route = [0, 1, 2, 3, 4, 5] 91 | result = tsp_2_opt(graph, route) 92 | new_route_cost = route_cost(graph, result) 93 | old_route_cost = route_cost(graph, route) 94 | print(new_route_cost) 95 | print(old_route_cost) 96 | print(result) 97 | assert old_route_cost > new_route_cost 98 | 99 | def test_get_solution_cost_change(self): 100 | graph = np.array([[0, 5, 2, 1, 1, 4], 101 | [5, 0, 6, 2, 2, 5], 102 | [2, 6, 0, 7, 4, 2], 103 | [1, 2, 7, 0, 8, 4], 104 | [1, 2, 4, 8, 0, 5], 105 | [4, 5, 2, 4, 5, 0]]) 106 | route = [0, 1, 2, 3, 4, 5] 107 | result = get_solution_cost_change(graph, route, OptCase.opt_case_1, 1, 3, 5) 108 | assert result == 0 109 | 110 | result = get_solution_cost_change(graph, route, OptCase.opt_case_2, 1, 3, 5) 111 | assert result == 7 112 | 113 | result = get_solution_cost_change(graph, route, OptCase.opt_case_3, 1, 3, 5) 114 | assert result == 8 115 | 116 | result = get_solution_cost_change(graph, route, OptCase.opt_case_4, 1, 3, 5) 117 | assert result == 13 118 | 119 | result = get_solution_cost_change(graph, route, OptCase.opt_case_5, 1, 3, 5) 120 | assert result == 10 121 | 122 | result = get_solution_cost_change(graph, route, OptCase.opt_case_6, 1, 3, 5) 123 | assert result == 3 124 | 125 | result = get_solution_cost_change(graph, route, OptCase.opt_case_7, 1, 3, 5) 126 | assert result == 8 127 | 128 | result = get_solution_cost_change(graph, route, OptCase.opt_case_8, 1, 3, 5) 129 | assert result == 13 130 | 131 | def test_2_opt_2(self): 132 | route = [0, 3, 2, 1, 4, 5, 6, 7, 8] 133 | result = tsp_2_opt(self.graph2, route) 134 | new_route_cost = route_cost(self.graph2, result) 135 | old_route_cost = route_cost(self.graph2, route) 136 | print(new_route_cost) 137 | print(old_route_cost) 138 | assert new_route_cost < old_route_cost 139 | 140 | def test_3_opt_2(self): 141 | route = [0, 3, 2, 1, 4, 5, 6, 7, 8] 142 | result = tsp_3_opt(self.graph2, route) 143 | new_route_cost = route_cost(self.graph2, result) 144 | old_route_cost = route_cost(self.graph2, route) 145 | print(new_route_cost) 146 | print(old_route_cost) 147 | assert new_route_cost < old_route_cost 148 | -------------------------------------------------------------------------------- /tests/test_lin_kerdighan.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pytsp.lin_kerdighan_tsp import _get_mst_and_add_two_edges, dad, get_B_matrix, _compute_degree_array, \ 4 | _compute_lower_bound_w, _create_min_1_tree, subgradient_optimization 5 | 6 | 7 | def test_get_mst_and_add_two_edges(): 8 | test = np.array([ 9 | [0, 2, 1, 3, 5], 10 | [2, 0, 10, 8, 7], 11 | [1, 10, 0, 3, 7], 12 | [3, 8, 3, 0, 4], 13 | [5, 7, 7, 4, 0], 14 | ]) 15 | 16 | starting_node = 0 17 | result = _get_mst_and_add_two_edges(test, starting_node) 18 | 19 | test2 = np.array([[0, 2, 1, 0, 0], 20 | [2, 0, 0, 0, 7], 21 | [1, 0, 0, 3, 0], 22 | [0, 0, 3, 0, 4], 23 | [0, 7, 0, 4, 0]]) 24 | 25 | np.testing.assert_array_equal(test2, result) 26 | 27 | 28 | def test_dad_1(): 29 | test = np.array([[0, 2, 1, 3, 0], 30 | [2, 0, 0, 0, 0], 31 | [1, 0, 0, 3, 0], 32 | [3, 0, 3, 0, 4], 33 | [0, 0, 0, 4, 0]]) 34 | assert dad(test, 1) == 0 35 | assert dad(test, 2) == 0 36 | assert dad(test, 3) == 0 37 | 38 | 39 | def test_B_matrix(): 40 | graph = np.array([[0, 2, 1, 0, 0], [2, 0, 2, 2, 0], [1, 2, 0, 0, 3], [0, 2, 0, 0, 0], [0, 0, 3, 0, 0]]) 41 | np.testing.assert_array_equal(get_B_matrix(graph), np.array( 42 | [[-10000, 0, 0, 0, 0], [0, -10000, 1, 2, 3], [0, 1, -10000, 2, 3], [0, 2, 2, -10000, 3], 43 | [0, 3, 3, 3, -10000]])) 44 | 45 | 46 | def test_create_min_1_tree(): 47 | graph = np.array([ 48 | [0, 7, 0, 5, 0, 0, 0], 49 | [7, 0, 8, 9, 7, 0, 0], 50 | [0, 8, 0, 0, 5, 0, 0], 51 | [5, 9, 0, 0, 15, 6, 0], 52 | [0, 7, 5, 15, 0, 8, 9], 53 | [0, 0, 0, 6, 8, 0, 11], 54 | [0, 0, 0, 0, 9, 11, 0] 55 | ]) 56 | 57 | result = np.array([ 58 | [0, 7, 0, 5, 0, 0, 0], 59 | [7, 0, 0, 0, 7, 0, 0], 60 | [0, 0, 0, 0, 5, 0, 0], 61 | [5, 0, 0, 0, 0, 6, 0], 62 | [0, 7, 5, 0, 0, 0, 9], 63 | [0, 0, 0, 6, 0, 0, 11], 64 | [0, 0, 0, 0, 9, 11, 0] 65 | ]) 66 | 67 | np.testing.assert_array_equal(_create_min_1_tree(graph), result) 68 | 69 | 70 | def test_compute_degree_array(): 71 | graph = np.array([[0, 2, 1, 0, 0], [2, 0, 2, 2, 0], [1, 2, 0, 0, 3], [0, 2, 0, 0, 0], [0, 0, 3, 0, 0]]) 72 | result = np.array([2, 3, 3, 1, 1]) 73 | np.testing.assert_array_equal(result, _compute_degree_array(graph)) 74 | 75 | 76 | def test_compute_lower_bound(): 77 | pi_vector = np.array([1, 2, 3, 4, 5]) 78 | min_1_tree = np.array([[0, 2, 1, 0, 0], 79 | [2, 0, 0, 0, 7], 80 | [1, 0, 0, 3, 0], 81 | [0, 0, 3, 0, 4], 82 | [0, 7, 0, 4, 0]]) 83 | assert 4 == _compute_lower_bound_w(min_1_tree, pi_vector) 84 | 85 | 86 | def test_subgradient_optimization(): 87 | graph = np.array([ 88 | [0, 7, 0, 5, 0, 0, 0], 89 | [7, 0, 8, 9, 7, 0, 0], 90 | [0, 8, 0, 0, 5, 0, 0], 91 | [5, 9, 0, 0, 15, 6, 0], 92 | [0, 7, 5, 15, 0, 8, 9], 93 | [0, 0, 0, 6, 8, 0, 11], 94 | [0, 0, 0, 0, 9, 11, 0] 95 | ]) 96 | result = np.array([1, 1, 1, 1, 1, 1, 1]) 97 | np.testing.assert_array_equal(result, subgradient_optimization(graph)) 98 | -------------------------------------------------------------------------------- /tests/test_nearest_neighbor.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pytsp.nearest_neighbor_tsp import nearest_neighbor_tsp 4 | 5 | 6 | def test_nearest_neighbor(): 7 | graph = np.array([[0, 300, 250, 190, 230], 8 | [300, 0, 230, 330, 150], 9 | [250, 230, 0, 240, 120], 10 | [190, 330, 240, 0, 220], 11 | [230, 150, 120, 220, 0]]) 12 | result = [0, 3, 4, 2, 1] 13 | assert result == nearest_neighbor_tsp(graph) 14 | -------------------------------------------------------------------------------- /tests/test_permutation_tsp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pytsp.permutations_tsp import permutations_tsp 4 | from pytsp.utils import route_cost 5 | 6 | 7 | def test_permutation_algorithm(): 8 | graph = np.array([[0, 5, 2, 1, 1, 4], 9 | [5, 0, 6, 2, 2, 5], 10 | [2, 6, 0, 7, 4, 2], 11 | [1, 2, 7, 0, 8, 4], 12 | [1, 2, 4, 8, 0, 5], 13 | [4, 5, 2, 4, 5, 0]]) 14 | route = [0, 1, 2, 3, 4, 5] 15 | result = permutations_tsp(graph) 16 | print(result) 17 | assert route_cost(graph,route) > route_cost(graph,result) 18 | 19 | 20 | def test_permutation_algorithm_2(): 21 | graph = np.array([[0, 4, 3, 4, 5, 1, 2, 3, 4], 22 | [4, 0, 1, 4, 3, 4, 6, 2, 1], 23 | [3, 1, 0, 1, 4, 3, 2, 1, 9], 24 | [4, 4, 1, 0, 4, 6, 1, 2, 3], 25 | [5, 3, 4, 4, 0, 1, 2, 5, 3], 26 | [1, 4, 3, 6, 1, 0, 2, 5, 3], 27 | [2, 6, 2, 1, 2, 2, 0, 3, 5], 28 | [3, 2, 1, 2, 5, 5, 3, 0, 9], 29 | [4, 1, 9, 3, 3, 3, 5, 0, 0] 30 | ]) 31 | route = [0,1,2,3,4,5,6,7,8] 32 | result = permutations_tsp(graph) 33 | print(route_cost(graph, result)) 34 | assert route_cost(graph, route) > route_cost(graph,result) 35 | -------------------------------------------------------------------------------- /tests/test_pytsp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Tests for `pytsp` package.""" 5 | 6 | 7 | import unittest 8 | 9 | from pytsp import pytsp 10 | 11 | 12 | class TestPytsp(unittest.TestCase): 13 | """Tests for `pytsp` package.""" 14 | 15 | def setUp(self): 16 | """Set up test fixtures, if any.""" 17 | 18 | def tearDown(self): 19 | """Tear down test fixtures, if any.""" 20 | 21 | def test_000_something(self): 22 | """Test something.""" 23 | -------------------------------------------------------------------------------- /tests/utils/test_utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from numpy.testing import assert_array_equal 4 | from pytsp.utils import minimal_spanning_tree 5 | 6 | 7 | def test_prim_algorithm(): 8 | starting_node = 3 9 | graph = np.array([ 10 | [0, 7, 0, 5, 0, 0, 0], 11 | [7, 0, 8, 9, 7, 0, 0], 12 | [0, 8, 0, 0, 5, 0, 0], 13 | [5, 9, 0, 0, 15, 6, 0], 14 | [0, 7, 5, 15, 0, 8, 9], 15 | [0, 0, 0, 6, 8, 0, 11], 16 | [0, 0, 0, 0, 9, 11, 0] 17 | ]) 18 | 19 | min_spanning_tree = minimal_spanning_tree(graph, mode='Prim', starting_node=starting_node) 20 | 21 | result = np.array([ 22 | [0, 7, 0, 5, 0, 0, 0], 23 | [7, 0, 0, 0, 7, 0, 0], 24 | [0, 0, 0, 0, 5, 0, 0], 25 | [5, 0, 0, 0, 0, 6, 0], 26 | [0, 7, 5, 0, 0, 0, 9], 27 | [0, 0, 0, 6, 0, 0, 0], 28 | [0, 0, 0, 0, 9, 0, 0] 29 | ]) 30 | 31 | assert_array_equal(result, min_spanning_tree) 32 | 33 | 34 | def test_prim_algo2(): 35 | test = np.array([ 36 | [0, 2, 3, 5], 37 | [2, 0, 8, 7], 38 | [3, 8, 0, 4], 39 | [5, 7, 4, 0], 40 | ]) 41 | min_spanning_tree = minimal_spanning_tree(test) 42 | print(min_spanning_tree) 43 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34, py35, py36, flake8 3 | 4 | [travis] 5 | python = 6 | 3.6: py36 7 | 3.5: py35 8 | 3.4: py34 9 | 2.7: py27 10 | 11 | [testenv:flake8] 12 | basepython = python 13 | deps = flake8 14 | commands = flake8 pytsp 15 | 16 | [testenv] 17 | setenv = 18 | PYTHONPATH = {toxinidir} 19 | 20 | commands = python setup.py test 21 | 22 | --------------------------------------------------------------------------------