├── .circleci └── config.yml ├── .gitattributes ├── .gitignore ├── .readthedocs.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── benchmarks ├── README.md ├── augerat_dataset.py ├── data │ ├── cvrp │ │ └── P-n16-k8.vrp │ ├── cvrptw │ │ └── C101.txt │ └── p01 ├── performance_profiles │ ├── benchmarks.ipynb │ ├── clarke_wright_cvrp.csv │ ├── ortools_cvrp.csv │ └── vrpy_cvrp.csv ├── run.py ├── solomon_dataset.py ├── tests │ ├── graph_issue101 │ ├── pytest.ini │ ├── test_cvrp_augerat.py │ ├── test_cvrptw_solomon.py │ ├── test_cvrptw_solomon_range.py │ ├── test_examples.py │ └── test_issue101.py └── utils │ ├── csv_table.py │ └── distance.py ├── docs ├── api.rst ├── benchmarks.rst ├── bibliography.rst ├── conf.py ├── examples.rst ├── getting_started.rst ├── how_to.rst ├── images │ ├── capacity.png │ ├── cvrp_performance_profile.png │ ├── drop.png │ ├── network.png │ ├── nodes.png │ ├── nodes_capacity.png │ ├── nodes_time_windows.png │ ├── pdp.png │ ├── requests.png │ ├── sol.png │ ├── stops.png │ ├── time.png │ └── time_windows.png ├── index.rst ├── mathematical_background.rst ├── refs.bib ├── requirements.txt ├── solving_options.rst └── vrp_variants.rst ├── examples ├── README.md ├── __init__.py ├── cvrp.py ├── cvrp_drop.py ├── cvrpsdc.py ├── data.py ├── pdp.py └── vrptw.py ├── paper ├── colgen.png ├── cvrp_performance_profile.png ├── paper.bib └── paper.md ├── requirements.txt ├── setup.py ├── tests ├── pytest.ini ├── test_consistency.py ├── test_initial_solution.py ├── test_issue101.py ├── test_issue110.py ├── test_issue79.py ├── test_issue86.py ├── test_issue99.py ├── test_subproblem_greedy.py └── test_toy.py └── vrpy ├── __init__.py ├── checks.py ├── clarke_wright.py ├── greedy.py ├── hyper_heuristic.py ├── master_solve_pulp.py ├── masterproblem.py ├── preprocessing.py ├── restricted_master_heuristics.py ├── schedule.py ├── subproblem.py ├── subproblem_cspy.py ├── subproblem_greedy.py ├── subproblem_lp.py └── vrp.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Python CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-python/ for more details 4 | # 5 | version: 2.0 6 | 7 | orbs: 8 | codecov: codecov/codecov@1.0.0 9 | jobs: 10 | "python-3.8": 11 | docker: 12 | - image: circleci/python:3.8 13 | working_directory: ~/vrpy 14 | steps: 15 | - checkout 16 | - run: 17 | name: install dependencies 18 | command: | 19 | python3 -m venv venv 20 | . venv/bin/activate 21 | pip install -r requirements.txt 22 | pip install pytest-cov 23 | pip install codecov 24 | - run: 25 | name: run tests 26 | command: | 27 | python3 -m venv venv 28 | . venv/bin/activate 29 | python3 -m pytest tests/ --cov 30 | codecov 31 | bash <(curl -s https://codecov.io/bash) 32 | - store_artifacts: 33 | path: test-reports 34 | destination: test-reports 35 | 36 | "python-3.7": 37 | docker: 38 | - image: circleci/python:3.7 39 | working_directory: ~/vrpy 40 | steps: 41 | - checkout 42 | - run: 43 | name: install dependencies 44 | command: | 45 | python3 -m venv venv 46 | . venv/bin/activate 47 | pip install -r requirements.txt 48 | - run: 49 | name: run tests 50 | command: | 51 | python3 -m venv venv 52 | . venv/bin/activate 53 | python3 -m pytest tests/ 54 | 55 | "python-3.6": 56 | docker: 57 | - image: circleci/python:3.6 58 | working_directory: ~/vrpy 59 | steps: 60 | - checkout 61 | - run: 62 | name: install dependencies 63 | command: | 64 | python3 -m venv venv 65 | . venv/bin/activate 66 | pip install --upgrade pip 67 | pip install -r requirements.txt 68 | - run: 69 | name: run tests 70 | command: | 71 | python3 -m venv venv 72 | . venv/bin/activate 73 | python3 -m pytest tests/ 74 | 75 | workflows: 76 | version: 2 77 | build: 78 | jobs: 79 | - "python-3.8" 80 | - "python-3.7" 81 | - "python-3.6" 82 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Declare files that will always have LF line endings on checkout. 5 | *.py text eol=lf 6 | *.jsx text eol=lf 7 | 8 | # Denote all files that are truly binary and should not be modified. 9 | *.png binary 10 | *.jpg binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Data 2 | 3 | benchmarks/data/cvrp/* 4 | !benchmarks/data/cvrp/A-n16* 5 | benchmarks/data/cvrptw/ 6 | !benchmarks/data/cvrptw/C101* 7 | benchmarks/results 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | pip-wheel-metadata/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF and ePub 17 | formats: all 18 | 19 | # Optionally set the version of Python and requirements required to build your docs 20 | python: 21 | version: 3.7 22 | install: 23 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | ### Changed 13 | 14 | ### Fixed 15 | 16 | ## [v0.5.1] - 18/09/2021 17 | 18 | ### Added 19 | 20 | - locked routes checks 21 | 22 | ### Fixed 23 | 24 | - issues #102, #103, #105 - #110 25 | 26 | ## [v0.5.0] - 06/06/2021 27 | 28 | ### Added 29 | 30 | - `heuristic_only` option 31 | - `use_all_vehicles` option 32 | 33 | ### Fixed 34 | 35 | - set covering constraints in last MIP with = sign 36 | 37 | ## [v0.4.0] - 13/05/2021 38 | 39 | ### Added 40 | 41 | - `num_vehicles` option with `periodic` option 42 | 43 | ### Changed 44 | 45 | - cspy 1.0.0 46 | - node load when simultaneous distribution and collection (#79) is now accurate 47 | 48 | ### Fixed 49 | 50 | - issues #79, #82, #84, #86 51 | 52 | ## [v0.3.0] - 10/11/2020 53 | 54 | ### Added 55 | 56 | - JOSS paper 57 | - Periodic CVRP scheduling option 58 | - Initial solution for CVRP computed with Greedy Algorithm 59 | - Diving heuristic (controlled with new parameter in `VehicleRoutingProblem.solve`) 60 | - Hyper-heuristic pricing strategy option `pricing_strategy="Hyper"`. 61 | - Jupyter notebooks with hyper-heuristics experiments (one to be updated soon). 62 | - Paragraph to the paper with the hyper-heuristic explanation and citations. 63 | 64 | ### Changed 65 | 66 | - Master problem formulation to column-based 67 | - Benchmark tests 68 | 69 | ## [v0.2.0] - 07/06/2020 70 | 71 | ### Added 72 | 73 | - Mixed fleet option 74 | - Greedy randomized pricing option 75 | - Stabilization with Interior Points 76 | - Diving heuristic WIP 77 | 78 | ### Changed 79 | 80 | - Pricing strategy names 81 | 82 | 83 | [Unreleased]: https://github.com/Kuifje02/vrpy 84 | [v0.2.0]: https://github.com/Kuifje02/vrpy/releases/tag/v0.2.0 85 | [v0.3.0]: https://github.com/Kuifje02/vrpy/releases/tag/v0.3.0 86 | [v0.4.0]: https://github.com/Kuifje02/vrpy/releases/tag/v0.4.0 87 | [v0.5.0]: https://github.com/Kuifje02/vrpy/releases/tag/v0.5.0 88 | [v0.5.1]: https://github.com/Kuifje02/vrpy/releases/tag/v0.5.1 89 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always greatly appreciated and credit will always be given. 4 | 5 | ## Types of contributions 6 | 7 | ### Report bugs 8 | 9 | Report bugs [here](https://github.com/Kuifje02/vrpy/issues). 10 | 11 | If you are reporting a bug, please include: 12 | 13 | * Your operating system name and version. 14 | * Any details about your local setup that might be helpful in troubleshooting. 15 | * Detailed steps to reproduce the bug. 16 | 17 | ### Fix bugs 18 | 19 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. 20 | 21 | ### Implement features 22 | 23 | Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. 24 | 25 | ## Pull request guidelines 26 | 27 | Before you submit a pull request, check that it meets these guidelines: 28 | 29 | 1. The pull request should include tests. 30 | 2. If the pull request adds functionality, the docs should be updated. 31 | 3. The pull request should work for Python 3.5-3.7. Check the [CircleCI dashboard](https://app.circleci.com/pipelines/github/Kuifje02/vrpy) and make sure that the tests pass for all supported Python versions. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kuifje02 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/Kuifje02/vrpy.svg?style=svg)](https://circleci.com/gh/Kuifje02/vrpy) 2 | [![codecov](https://codecov.io/gh/Kuifje02/vrpy/branch/master/graph/badge.svg)](https://codecov.io/gh/Kuifje02/vrpy) 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/6f27b9ccd1c2446aa1dba15e701aa9b0)](https://app.codacy.com/manual/Kuifje02/vrpy?utm_source=github.com&utm_medium=referral&utm_content=Kuifje02/vrpy&utm_campaign=Badge_Grade_Dashboard) 4 | [![Python 3.8](https://img.shields.io/badge/python-3.6|3.7|3.8-blue.svg)](https://www.python.org/downloads/release/python-360/) 5 | [![Documentation Status](https://readthedocs.org/projects/vrpy/badge/?version=latest)](https://vrpy.readthedocs.io/en/latest/?badge=master) 6 | [![status](https://joss.theoj.org/papers/77c3aa9b9cb3ff3d5c32d253922ad390/status.svg)](https://joss.theoj.org/papers/77c3aa9b9cb3ff3d5c32d253922ad390) 7 | 8 | # VRPy 9 | 10 | VRPy is a python framework for solving Vehicle Routing Problems (VRP) including: 11 | 12 | - the Capacitated VRP (CVRP), 13 | - the CVRP with resource constraints, 14 | - the CVRP with time windows (CVRPTW), 15 | - the CVRP with simultaneous distribution and collection (CVRPSDC), 16 | - the CVRP with heterogeneous fleet (HFCVRP). 17 | 18 | Check out the [docs](https://vrpy.readthedocs.io/en/latest/) to find more variants and options. 19 | 20 | ## Simple example 21 | 22 | ```python 23 | from networkx import DiGraph 24 | from vrpy import VehicleRoutingProblem 25 | 26 | # Define the network 27 | G = DiGraph() 28 | G.add_edge("Source",1,cost=1,time=2) 29 | G.add_edge("Source",2,cost=2,time=1) 30 | G.add_edge(1,"Sink",cost=0,time=2) 31 | G.add_edge(2,"Sink",cost=2,time=3) 32 | G.add_edge(1,2,cost=1,time=1) 33 | G.add_edge(2,1,cost=1,time=1) 34 | 35 | # Define the customers demands 36 | G.nodes[1]["demand"] = 5 37 | G.nodes[2]["demand"] = 4 38 | 39 | # Define the Vehicle Routing Problem 40 | prob = VehicleRoutingProblem(G, load_capacity=10, duration=5) 41 | 42 | # Solve and display solution value 43 | prob.solve() 44 | print(prob.best_value) 45 | 3 46 | print(prob.best_routes) 47 | {1: ["Source",2,1,"Sink"]} 48 | ``` 49 | 50 | ## Install 51 | 52 | ```sh 53 | pip install vrpy 54 | ``` 55 | 56 | ## Requirements 57 | 58 | [cspy](https://pypi.org/project/cspy/) 59 | 60 | [NetworkX](https://pypi.org/project/networkx/) 61 | 62 | [numpy](https://pypi.org/project/numpy/) 63 | 64 | [PuLP](https://pypi.org/project/PuLP/) 65 | 66 | ## Documentation 67 | 68 | Documentation is found [here](https://vrpy.readthedocs.io/en/latest/). 69 | 70 | ## Running the tests 71 | 72 | ### Unit Tests 73 | 74 | ```sh 75 | python3 -m pytest tests/ 76 | ``` 77 | 78 | ### Benchmarks 79 | 80 | To run some non-regression tests on some benchmarks instances (Solomon and Augerat) do 81 | 82 | ```sh 83 | python3 -m pytest benchmarks/ 84 | ``` 85 | 86 | Note that running the benchmarks requires [pandas](https://pypi.org/project/pandas/) and that it takes a while. 87 | 88 | For more information and to run more instances, see the [benchmarks](benchmarks/README.md). 89 | 90 | ## License 91 | 92 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/Kuifje02/vrpy/blob/dev/LICENSE) file for details. 93 | 94 | ## Bugs 95 | 96 | Please report any bugs that you find [here](https://github.com/Kuifje02/vrpy/issues). Or, even better, fork the repository on [GitHub](https://github.com/Kuifje02/vrpy) and create a pull request. Please read the [Community Guidelines](https://github.com/Kuifje02/vrpy/blob/dev/CONTRIBUTING.md) before contributing. Any contributions are welcome. 97 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Results 2 | 3 | Some summary tables and plots coming soon. 4 | 5 | # Replicating results 6 | 7 | ## Set up 8 | 9 | First download the instances you wish to run ([Augerat]() or [Solomon]()) and place them in the 10 | appropriate folders: 11 | - Augerat -> `benchmarks/data/cvrp`, 12 | - Solomon -> `benchmarks/data/cvrptw`. 13 | 14 | For Augerat, ensure that no `.sol` files are left in the folder 15 | 16 | ## Running 17 | 18 | To run the results with the default configuration, from the root folder of the project (`vrpy/`), do 19 | 20 | ```bash 21 | python3 -m benchmarks.run 22 | ``` 23 | 24 | As it goes, the csv files are created in a new `benchmarks/run/results`. 25 | 26 | To see the different options do 27 | 28 | ```bash 29 | python3 -m benchmarks.run -h 30 | ``` 31 | 32 | These include: 33 | - Parallel/series runner 34 | - CPU number specificiation 35 | - Exploration or performance mode (default configuration) 36 | 37 | ## OR-TOOLS 38 | 39 | To run tests with ortools, [this code](https://github.com/Kuifje02/ortools) can be used. 40 | -------------------------------------------------------------------------------- /benchmarks/augerat_dataset.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from networkx import relabel_nodes, DiGraph 4 | import numpy as np 5 | from pandas import read_csv 6 | 7 | from benchmarks.utils.distance import distance 8 | 9 | 10 | class AugeratNodePosition: 11 | """Stores coordinates of a node of Augerat's instances (set P).""" 12 | 13 | def __init__(self, values): 14 | # Node ID 15 | self.name = np.uint32(values[0]).item() 16 | if self.name == 1: 17 | self.name = "Source" 18 | # x coordinate 19 | self.x = np.float64(values[1]).item() 20 | # y coordinate 21 | self.y = np.float64(values[2]).item() 22 | 23 | 24 | class AugeratNodeDemand: 25 | """Stores attributes of a node of Augerat's instances (set P).""" 26 | 27 | def __init__(self, values): 28 | # Node ID 29 | self.name = np.uint32(values[0]).item() 30 | if self.name == 1: 31 | self.name = "Source" 32 | # demand coordinate 33 | self.demand = np.float64(values[1]).item() 34 | 35 | 36 | class AugeratDataSet: 37 | """Reads an Augerat instance and stores the network as DiGraph. 38 | 39 | Args: 40 | path (str) : Path to data folder. 41 | instance_name (str) : Name of instance to read. 42 | """ 43 | 44 | def __init__(self, path: Path, instance_name): 45 | self.G: DiGraph = None 46 | self.best_known_solution: int = None 47 | self.best_value: float = None 48 | self.max_load: int = None 49 | 50 | path = Path(path) 51 | self._load(path, instance_name) 52 | 53 | def _load(self, path, instance_name): 54 | """Load Augerat instance into a DiGraph""" 55 | # Read vehicle capacity 56 | with open(path / instance_name) as fp: 57 | for i, line in enumerate(fp): 58 | if i == 1: 59 | best = line.split()[-1][:-1] 60 | self.best_known_solution = int(best) 61 | elif i == 5: 62 | self.max_load = int(line.split()[2]) 63 | fp.close() 64 | # Create network and store name + capacity 65 | self.G = DiGraph( 66 | name=instance_name[:-4], 67 | vehicle_capacity=self.max_load, 68 | ) 69 | 70 | # Read nodes from txt file 71 | if instance_name[5] == "-": 72 | n_vertices = int(instance_name[3:5]) 73 | else: 74 | n_vertices = int(instance_name[3:6]) 75 | df_augerat = read_csv( 76 | path / instance_name, 77 | sep="\t", 78 | skiprows=6, 79 | nrows=n_vertices, 80 | ) 81 | # Scan each line of the file and add nodes to the network 82 | for line in df_augerat.itertuples(): 83 | values = line[1].split() 84 | node = AugeratNodePosition(values) 85 | self.G.add_node(node.name, x=node.x, y=node.y, demand=0) 86 | # Add Sink as copy of Source 87 | if node.name == "Source": 88 | self.G.add_node("Sink", x=node.x, y=node.y, demand=0) 89 | 90 | # Read demand from txt file 91 | df_demand = read_csv( 92 | path / instance_name, 93 | sep="\t", 94 | skiprows=range(7 + n_vertices), 95 | nrows=n_vertices, 96 | ) 97 | for line in df_demand.itertuples(): 98 | values = line[1].split() 99 | node = AugeratNodeDemand(values) 100 | self.G.nodes[node.name]["demand"] = node.demand 101 | 102 | # Add the edges, the graph is complete 103 | for u in self.G.nodes(): 104 | if u != "Sink": 105 | for v in self.G.nodes(): 106 | if v != "Source" and u != v: 107 | self.G.add_edge(u, 108 | v, 109 | cost=round(distance(self.G, u, v), 110 | 1)) 111 | 112 | # relabel 113 | before = [v for v in self.G.nodes() if v not in ["Source", "Sink"]] 114 | after = [v - 1 for v in self.G.nodes() if v not in ["Source", "Sink"]] 115 | mapping = dict(zip(before, after)) 116 | self.G = relabel_nodes(self.G, mapping) 117 | -------------------------------------------------------------------------------- /benchmarks/data/cvrp/P-n16-k8.vrp: -------------------------------------------------------------------------------- 1 | NAME : P-n16-k8 2 | COMMENT : (Augerat et al, No of trucks: 8, Optimal value: 450) 3 | TYPE : CVRP 4 | DIMENSION : 16 5 | EDGE_WEIGHT_TYPE : EUC_2D 6 | CAPACITY : 35 7 | NODE_COORD_SECTION 8 | 1 30 40 9 | 2 37 52 10 | 3 49 49 11 | 4 52 64 12 | 5 31 62 13 | 6 52 33 14 | 7 42 41 15 | 8 52 41 16 | 9 57 58 17 | 10 62 42 18 | 11 42 57 19 | 12 27 68 20 | 13 43 67 21 | 14 58 48 22 | 15 58 27 23 | 16 37 69 24 | DEMAND_SECTION 25 | 1 0 26 | 2 19 27 | 3 30 28 | 4 16 29 | 5 23 30 | 6 11 31 | 7 31 32 | 8 15 33 | 9 28 34 | 10 8 35 | 11 8 36 | 12 7 37 | 13 14 38 | 14 6 39 | 15 19 40 | 16 11 41 | DEPOT_SECTION 42 | 1 43 | -1 44 | EOF 45 | -------------------------------------------------------------------------------- /benchmarks/data/cvrptw/C101.txt: -------------------------------------------------------------------------------- 1 | C101 2 | 3 | VEHICLE 4 | NUMBER CAPACITY 5 | 25 200 6 | 7 | CUSTOMER 8 | CUST NO. XCOORD. YCOORD. DEMAND READY TIME DUE DATE SERVICE TIME 9 | 10 | 0 40 50 0 0 1236 0 11 | 1 45 68 10 912 967 90 12 | 2 45 70 30 825 870 90 13 | 3 42 66 10 65 146 90 14 | 4 42 68 10 727 782 90 15 | 5 42 65 10 15 67 90 16 | 6 40 69 20 621 702 90 17 | 7 40 66 20 170 225 90 18 | 8 38 68 20 255 324 90 19 | 9 38 70 10 534 605 90 20 | 10 35 66 10 357 410 90 21 | 11 35 69 10 448 505 90 22 | 12 25 85 20 652 721 90 23 | 13 22 75 30 30 92 90 24 | 14 22 85 10 567 620 90 25 | 15 20 80 40 384 429 90 26 | 16 20 85 40 475 528 90 27 | 17 18 75 20 99 148 90 28 | 18 15 75 20 179 254 90 29 | 19 15 80 10 278 345 90 30 | 20 30 50 10 10 73 90 31 | 21 30 52 20 914 965 90 32 | 22 28 52 20 812 883 90 33 | 23 28 55 10 732 777 90 34 | 24 25 50 10 65 144 90 35 | 25 25 52 40 169 224 90 36 | 26 25 55 10 622 701 90 37 | 27 23 52 10 261 316 90 38 | 28 23 55 20 546 593 90 39 | 29 20 50 10 358 405 90 40 | 30 20 55 10 449 504 90 41 | 31 10 35 20 200 237 90 42 | 32 10 40 30 31 100 90 43 | 33 8 40 40 87 158 90 44 | 34 8 45 20 751 816 90 45 | 35 5 35 10 283 344 90 46 | 36 5 45 10 665 716 90 47 | 37 2 40 20 383 434 90 48 | 38 0 40 30 479 522 90 49 | 39 0 45 20 567 624 90 50 | 40 35 30 10 264 321 90 51 | 41 35 32 10 166 235 90 52 | 42 33 32 20 68 149 90 53 | 43 33 35 10 16 80 90 54 | 44 32 30 10 359 412 90 55 | 45 30 30 10 541 600 90 56 | 46 30 32 30 448 509 90 57 | 47 30 35 10 1054 1127 90 58 | 48 28 30 10 632 693 90 59 | 49 28 35 10 1001 1066 90 60 | 50 26 32 10 815 880 90 61 | 51 25 30 10 725 786 90 62 | 52 25 35 10 912 969 90 63 | 53 44 5 20 286 347 90 64 | 54 42 10 40 186 257 90 65 | 55 42 15 10 95 158 90 66 | 56 40 5 30 385 436 90 67 | 57 40 15 40 35 87 90 68 | 58 38 5 30 471 534 90 69 | 59 38 15 10 651 740 90 70 | 60 35 5 20 562 629 90 71 | 61 50 30 10 531 610 90 72 | 62 50 35 20 262 317 90 73 | 63 50 40 50 171 218 90 74 | 64 48 30 10 632 693 90 75 | 65 48 40 10 76 129 90 76 | 66 47 35 10 826 875 90 77 | 67 47 40 10 12 77 90 78 | 68 45 30 10 734 777 90 79 | 69 45 35 10 916 969 90 80 | 70 95 30 30 387 456 90 81 | 71 95 35 20 293 360 90 82 | 72 53 30 10 450 505 90 83 | 73 92 30 10 478 551 90 84 | 74 53 35 50 353 412 90 85 | 75 45 65 20 997 1068 90 86 | 76 90 35 10 203 260 90 87 | 77 88 30 10 574 643 90 88 | 78 88 35 20 109 170 90 89 | 79 87 30 10 668 731 90 90 | 80 85 25 10 769 820 90 91 | 81 85 35 30 47 124 90 92 | 82 75 55 20 369 420 90 93 | 83 72 55 10 265 338 90 94 | 84 70 58 20 458 523 90 95 | 85 68 60 30 555 612 90 96 | 86 66 55 10 173 238 90 97 | 87 65 55 20 85 144 90 98 | 88 65 60 30 645 708 90 99 | 89 63 58 10 737 802 90 100 | 90 60 55 10 20 84 90 101 | 91 60 60 10 836 889 90 102 | 92 67 85 20 368 441 90 103 | 93 65 85 40 475 518 90 104 | 94 65 82 10 285 336 90 105 | 95 62 80 30 196 239 90 106 | 96 60 80 10 95 156 90 107 | 97 60 85 30 561 622 90 108 | 98 58 75 20 30 84 90 109 | 99 55 80 10 743 820 90 110 | 100 55 85 20 647 726 90 111 | -------------------------------------------------------------------------------- /benchmarks/data/p01: -------------------------------------------------------------------------------- 1 | 2 4 50 4 2 | 0 80 3 | 0 80 4 | 0 80 5 | 0 80 6 | 1 37 52 0 7 1 4 1 2 4 8 7 | 2 49 49 0 30 1 4 1 2 4 8 8 | 3 52 64 0 16 1 4 1 2 4 8 9 | 4 20 26 0 9 1 4 1 2 4 8 10 | 5 40 30 0 21 1 4 1 2 4 8 11 | 6 21 47 0 15 1 4 1 2 4 8 12 | 7 17 63 0 19 1 4 1 2 4 8 13 | 8 31 62 0 23 1 4 1 2 4 8 14 | 9 52 33 0 11 1 4 1 2 4 8 15 | 10 51 21 0 5 1 4 1 2 4 8 16 | 11 42 41 0 19 1 4 1 2 4 8 17 | 12 31 32 0 29 1 4 1 2 4 8 18 | 13 5 25 0 23 1 4 1 2 4 8 19 | 14 12 42 0 21 1 4 1 2 4 8 20 | 15 36 16 0 10 1 4 1 2 4 8 21 | 16 52 41 0 15 1 4 1 2 4 8 22 | 17 27 23 0 3 1 4 1 2 4 8 23 | 18 17 33 0 41 1 4 1 2 4 8 24 | 19 13 13 0 9 1 4 1 2 4 8 25 | 20 57 58 0 28 1 4 1 2 4 8 26 | 21 62 42 0 8 1 4 1 2 4 8 27 | 22 42 57 0 8 1 4 1 2 4 8 28 | 23 16 57 0 16 1 4 1 2 4 8 29 | 24 8 52 0 10 1 4 1 2 4 8 30 | 25 7 38 0 28 1 4 1 2 4 8 31 | 26 27 68 0 7 1 4 1 2 4 8 32 | 27 30 48 0 15 1 4 1 2 4 8 33 | 28 43 67 0 14 1 4 1 2 4 8 34 | 29 58 48 0 6 1 4 1 2 4 8 35 | 30 58 27 0 19 1 4 1 2 4 8 36 | 31 37 69 0 11 1 4 1 2 4 8 37 | 32 38 46 0 12 1 4 1 2 4 8 38 | 33 46 10 0 23 1 4 1 2 4 8 39 | 34 61 33 0 26 1 4 1 2 4 8 40 | 35 62 63 0 17 1 4 1 2 4 8 41 | 36 63 69 0 6 1 4 1 2 4 8 42 | 37 32 22 0 9 1 4 1 2 4 8 43 | 38 45 35 0 15 1 4 1 2 4 8 44 | 39 59 15 0 14 1 4 1 2 4 8 45 | 40 5 6 0 7 1 4 1 2 4 8 46 | 41 10 17 0 27 1 4 1 2 4 8 47 | 42 21 10 0 13 1 4 1 2 4 8 48 | 43 5 64 0 11 1 4 1 2 4 8 49 | 44 30 15 0 16 1 4 1 2 4 8 50 | 45 39 10 0 10 1 4 1 2 4 8 51 | 46 32 39 0 5 1 4 1 2 4 8 52 | 47 25 32 0 25 1 4 1 2 4 8 53 | 48 25 55 0 17 1 4 1 2 4 8 54 | 49 48 28 0 18 1 4 1 2 4 8 55 | 50 56 37 0 10 1 4 1 2 4 8 56 | 51 20 20 0 0 0 0 57 | 52 30 40 0 0 0 0 58 | 53 50 30 0 0 0 0 59 | 54 60 50 0 0 0 0 60 | -------------------------------------------------------------------------------- /benchmarks/performance_profiles/clarke_wright_cvrp.csv: -------------------------------------------------------------------------------- 1 | instance,nodes,algorithm,res,best known solution,gap,time (s),vrp,time limit (s) 2 | A-n32-k5.vrp,32,Clarke&Wright,975,784,24.3622449,0.013962746,cvrp,5 3 | A-n33-k5.vrp,33,Clarke&Wright,696,661,5.295007564,0.008975744,cvrp,5 4 | A-n33-k6.vrp,33,Clarke&Wright,794,742,7.008086253,0.010970116,cvrp,5 5 | A-n34-k5.vrp,34,Clarke&Wright,789,778,1.413881748,0.013964176,cvrp,5 6 | A-n36-k5.vrp,36,Clarke&Wright,823,799,3.003754693,0.014960289,cvrp,5 7 | A-n37-k5.vrp,37,Clarke&Wright,725,669,8.370702541,0.015956879,cvrp,5 8 | A-n37-k6.vrp,37,Clarke&Wright,1006,949,6.006322445,0.017951488,cvrp,5 9 | A-n38-k5.vrp,38,Clarke&Wright,833,730,14.10958904,0.018949986,cvrp,5 10 | A-n39-k5.vrp,39,Clarke&Wright,900,822,9.489051095,0.017951012,cvrp,5 11 | A-n39-k6.vrp,39,Clarke&Wright,1014,831,22.02166065,0.013965607,cvrp,5 12 | A-n44-k7.vrp,44,Clarke&Wright,1012,937,8.004268943,0.018950224,cvrp,5 13 | A-n45-k6.vrp,45,Clarke&Wright,1024,944,8.474576271,0.016954899,cvrp,5 14 | A-n45-k7.vrp,45,Clarke&Wright,1227,1146,7.068062827,0.017952204,cvrp,5 15 | A-n46-k7.vrp,46,Clarke&Wright,940,914,2.84463895,0.031913519,cvrp,5 16 | A-n48-k7.vrp,48,Clarke&Wright,1128,1073,5.125815471,0.029926062,cvrp,5 17 | A-n53-k7.vrp,53,Clarke&Wright,1146,1010,13.46534653,0.030917168,cvrp,5 18 | A-n54-k7.vrp,54,Clarke&Wright,1281,1167,9.768637532,0.033911228,cvrp,5 19 | A-n55-k9.vrp,55,Clarke&Wright,1153,1073,7.455731594,0.023967028,cvrp,5 20 | A-n60-k9.vrp,60,Clarke&Wright,1419,1408,0.78125,0.028922319,cvrp,5 21 | A-n61-k9.vrp,61,Clarke&Wright,1074,1035,3.768115942,0.029919624,cvrp,5 22 | A-n62-k8.vrp,62,Clarke&Wright,1384,1290,7.286821705,0.04188633,cvrp,5 23 | A-n63-k10.vrp,63,Clarke&Wright,1463,1315,11.25475285,0.031915426,cvrp,5 24 | A-n63-k9.vrp,63,Clarke&Wright,1740,1634,6.487148103,0.035904646,cvrp,5 25 | A-n64-k9.vrp,64,Clarke&Wright,1564,1402,11.55492154,0.040854692,cvrp,5 26 | A-n65-k9.vrp,65,Clarke&Wright,1334,1177,13.33899745,0.033909559,cvrp,5 27 | A-n69-k9.vrp,69,Clarke&Wright,1276,1168,9.246575342,0.042885542,cvrp,5 28 | A-n80-k10.vrp,80,Clarke&Wright,1851,1764,4.931972789,0.058843613,cvrp,5 29 | B-n31-k5.vrp,31,Clarke&Wright,779,672,15.92261905,0.007978678,cvrp,5 30 | B-n34-k5.vrp,34,Clarke&Wright,1056,788,34.01015228,0.008975744,cvrp,5 31 | B-n35-k5.vrp,35,Clarke&Wright,1338,955,40.10471204,0.008969784,cvrp,5 32 | B-n38-k6.vrp,38,Clarke&Wright,866,805,7.577639752,0.011967659,cvrp,5 33 | B-n39-k5.vrp,39,Clarke&Wright,697,549,26.95810565,0.011968374,cvrp,5 34 | B-n41-k6.vrp,41,Clarke&Wright,900,829,8.564535585,0.01495862,cvrp,5 35 | B-n43-k6.vrp,43,Clarke&Wright,834,742,12.39892183,0.015923262,cvrp,5 36 | B-n44-k7.vrp,44,Clarke&Wright,944,909,3.850385039,0.016955137,cvrp,5 37 | B-n45-k5.vrp,45,Clarke&Wright,860,751,14.51398136,0.015956163,cvrp,5 38 | B-n45-k6.vrp,45,Clarke&Wright,859,678,26.69616519,0.015957594,cvrp,5 39 | B-n50-k7.vrp,50,Clarke&Wright,766,741,3.373819163,0.020948887,cvrp,5 40 | B-n50-k8.vrp,50,Clarke&Wright,1349,1313,2.741812643,0.028922796,cvrp,5 41 | B-n51-k7.vrp,51,Clarke&Wright,1396,1032,35.27131783,0.021942139,cvrp,5 42 | B-n52-k7.vrp,52,Clarke&Wright,1114,747,49.12985274,0.023937464,cvrp,5 43 | B-n56-k7.vrp,56,Clarke&Wright,842,707,19.09476662,0.024932861,cvrp,5 44 | B-n57-k7.vrp,57,Clarke&Wright,1654,1153,43.4518647,0.034934998,cvrp,5 45 | B-n57-k9.vrp,57,Clarke&Wright,1794,1598,12.26533166,0.037899017,cvrp,5 46 | B-n63-k10.vrp,63,Clarke&Wright,1762,1537,14.63890696,0.031914234,cvrp,5 47 | B-n64-k9.vrp,64,Clarke&Wright,1047,861,21.60278746,0.037898302,cvrp,5 48 | B-n66-k9.vrp,66,Clarke&Wright,1527,1374,11.13537118,0.04188776,cvrp,5 49 | B-n67-k10.vrp,67,Clarke&Wright,1268,1033,22.74927396,0.035870552,cvrp,5 50 | B-n68-k9.vrp,68,Clarke&Wright,1409,1304,8.052147239,0.036901236,cvrp,5 51 | B-n78-k10.vrp,78,Clarke&Wright,1420,1266,12.164297,0.048871279,cvrp,5 52 | P-n101-k4.vrp,101,Clarke&Wright,913,681,34.06754772,0.101727724,cvrp,5 53 | P-n16-k8.vrp,16,Clarke&Wright,482,435,10.8045977,0.002990007,cvrp,5 54 | P-n19-k2.vrp,19,Clarke&Wright,219,212,3.301886792,0.003990412,cvrp,5 55 | P-n20-k2.vrp,20,Clarke&Wright,222,220,0.909090909,0.003951311,cvrp,5 56 | P-n21-k2.vrp,21,Clarke&Wright,238,211,12.79620853,0.003988743,cvrp,5 57 | P-n22-k2.vrp,22,Clarke&Wright,246,216,13.88888889,0.003955126,cvrp,5 58 | P-n22-k8.vrp,22,Clarke&Wright,590,603,-2.155887231,0.003988504,cvrp,5 59 | P-n23-k8.vrp,23,Clarke&Wright,537,554,-3.068592058,0.003957033,cvrp,5 60 | P-n40-k5.vrp,40,Clarke&Wright,531,458,15.93886463,0.012932062,cvrp,5 61 | P-n45-k5.vrp,45,Clarke&Wright,551,510,8.039215686,0.019946575,cvrp,5 62 | P-n50-k10.vrp,50,Clarke&Wright,756,696,8.620689655,0.026928425,cvrp,5 63 | P-n50-k7.vrp,50,Clarke&Wright,600,554,8.303249097,0.019941092,cvrp,5 64 | P-n50-k8.vrp,50,Clarke&Wright,667,649,2.773497689,0.019946575,cvrp,5 65 | P-n51-k10.vrp,51,Clarke&Wright,774,745,3.89261745,0.019946575,cvrp,5 66 | P-n55-k10.vrp,55,Clarke&Wright,746,669,11.50971599,0.032881498,cvrp,5 67 | P-n55-k15.vrp,55,Clarke&Wright,984,856,14.95327103,0.024896145,cvrp,5 68 | P-n55-k7.vrp,55,Clarke&Wright,620,524,18.32061069,0.023936272,cvrp,5 69 | P-n55-k8.vrp,55,Clarke&Wright,643,576,11.63194444,0.029920578,cvrp,5 70 | P-n60-k10.vrp,60,Clarke&Wright,800,706,13.31444759,0.032911777,cvrp,5 71 | P-n60-k15.vrp,60,Clarke&Wright,1019,905,12.59668508,0.027925014,cvrp,5 72 | P-n65-k10.vrp,65,Clarke&Wright,872,792,10.1010101,0.033908606,cvrp,5 73 | P-n70-k10.vrp,70,Clarke&Wright,879,834,5.395683453,0.047871828,cvrp,5 74 | P-n76-k4.vrp,76,Clarke&Wright,788,589,33.7860781,0.048832178,cvrp,5 75 | P-n76-k5.vrp,76,Clarke&Wright,788,631,24.88114105,0.052821875,cvrp,5 76 | -------------------------------------------------------------------------------- /benchmarks/performance_profiles/ortools_cvrp.csv: -------------------------------------------------------------------------------- 1 | instance,nodes,algorithm,res,best known solution,gap,time (s),vrp,time limit (s) 2 | A-n32-k5.vrp,32,ortools,782,784,-0.255102041,0.169011831,cvrp,100 3 | A-n33-k5.vrp,33,ortools,761,661,15.12859304,0.106993437,cvrp,100 4 | A-n33-k6.vrp,33,ortools,797,742,7.412398922,0.218011856,cvrp,100 5 | A-n34-k5.vrp,34,ortools,842,778,8.22622108,0.203013897,cvrp,100 6 | A-n36-k5.vrp,36,ortools,853,799,6.75844806,0.254014492,cvrp,100 7 | A-n37-k5.vrp,37,ortools,721,669,7.772795217,0.256997585,cvrp,100 8 | A-n37-k6.vrp,37,ortools,1011,949,6.533192835,0.348014832,cvrp,100 9 | A-n38-k5.vrp,38,ortools,821,730,12.46575342,0.30001688,cvrp,100 10 | A-n39-k5.vrp,39,ortools,861,822,4.744525547,0.260013819,cvrp,100 11 | A-n39-k6.vrp,39,ortools,883,831,6.257521059,0.284397125,cvrp,100 12 | A-n44-k7.vrp,44,ortools,995,937,6.189967983,0.420037746,cvrp,100 13 | A-n45-k6.vrp,45,ortools,1049,944,11.12288136,0.528039932,cvrp,100 14 | A-n45-k7.vrp,45,ortools,1148,1146,0.17452007,0.374011517,cvrp,100 15 | A-n46-k7.vrp,46,ortools,1038,914,13.56673961,0.377031565,cvrp,100 16 | A-n48-k7.vrp,48,ortools,1145,1073,6.710158434,0.436040401,cvrp,100 17 | A-n53-k7.vrp,53,ortools,1098,1010,8.712871287,0.743042469,cvrp,100 18 | A-n54-k7.vrp,54,ortools,1226,1167,5.055698372,0.657027006,cvrp,100 19 | A-n55-k9.vrp,55,ortools,1148,1073,6.989748369,0.703034878,cvrp,100 20 | A-n60-k9.vrp,60,ortools,1494,1408,6.107954545,0.337989569,cvrp,100 21 | A-n61-k9.vrp,61,ortools,1189,1035,14.87922705,0.525032759,cvrp,100 22 | A-n62-k8.vrp,62,ortools,1491,1290,15.58139535,0.60416913,cvrp,100 23 | A-n63-k10.vrp,63,ortools,1400,1315,6.463878327,0.683035374,cvrp,100 24 | A-n63-k9.vrp,63,ortools,1799,1634,10.09791922,0.814043999,cvrp,100 25 | A-n64-k9.vrp,64,ortools,1477,1402,5.349500713,0.81704998,cvrp,100 26 | A-n65-k9.vrp,65,ortools,1258,1177,6.881903144,0.854038239,cvrp,100 27 | A-n69-k9.vrp,69,ortools,1203,1168,2.996575342,0.557014227,cvrp,100 28 | A-n80-k10.vrp,80,ortools,1915,1764,8.560090703,0.647035599,cvrp,100 29 | B-n31-k5.vrp,31,ortools,668,672,-0.595238095,0.073003292,cvrp,100 30 | B-n34-k5.vrp,34,ortools,811,788,2.918781726,0.082006931,cvrp,100 31 | B-n35-k5.vrp,35,ortools,983,955,2.931937173,0.121006012,cvrp,100 32 | B-n38-k6.vrp,38,ortools,866,805,7.577639752,0.10969615,cvrp,100 33 | B-n39-k5.vrp,39,ortools,601,549,9.471766849,0.215022326,cvrp,100 34 | B-n41-k6.vrp,41,ortools,854,829,3.015681544,0.304020643,cvrp,100 35 | B-n43-k6.vrp,43,ortools,755,742,1.752021563,0.581030607,cvrp,100 36 | B-n44-k7.vrp,44,ortools,929,909,2.200220022,0.453023195,cvrp,100 37 | B-n45-k5.vrp,45,ortools,790,751,5.193075899,0.466017246,cvrp,100 38 | B-n45-k6.vrp,45,ortools,700,678,3.244837758,0.371014357,cvrp,100 39 | B-n50-k7.vrp,50,ortools,742,741,0.134952767,0.487018347,cvrp,100 40 | B-n50-k8.vrp,50,ortools,1311,1313,-0.152322925,0.728041887,cvrp,100 41 | B-n51-k7.vrp,51,ortools,1135,1032,9.980620155,0.400040388,cvrp,100 42 | B-n52-k7.vrp,52,ortools,741,747,-0.803212851,0.801954985,cvrp,100 43 | B-n56-k7.vrp,56,ortools,756,707,6.930693069,0.400008678,cvrp,100 44 | B-n57-k7.vrp,57,ortools,1129,1153,-2.081526453,0.432023048,cvrp,100 45 | B-n57-k9.vrp,57,ortools,1662,1598,4.005006258,0.42700386,cvrp,100 46 | B-n63-k10.vrp,63,ortools,1600,1537,4.098893949,1.102616787,cvrp,100 47 | B-n64-k9.vrp,64,ortools,1078,861,25.20325203,0.980070591,cvrp,100 48 | B-n66-k9.vrp,66,ortools,1366,1374,-0.58224163,0.622032881,cvrp,100 49 | B-n67-k10.vrp,67,ortools,1084,1033,4.937076476,1.050239086,cvrp,100 50 | B-n68-k9.vrp,68,ortools,1356,1304,3.987730061,0.844043732,cvrp,100 51 | B-n78-k10.vrp,78,ortools,1272,1266,0.473933649,0.651040316,cvrp,100 52 | P-n101-k4.vrp,101,ortools,767,681,12.62848752,1.032053471,cvrp,100 53 | P-n16-k8.vrp,16,ortools,445,435,2.298850575,0.028003931,cvrp,100 54 | P-n19-k2.vrp,19,ortools,222,212,4.716981132,0.031010866,cvrp,100 55 | P-n20-k2.vrp,20,ortools,222,220,0.909090909,0.031036377,cvrp,100 56 | P-n21-k2.vrp,21,ortools,208,211,-1.421800948,0.038001299,cvrp,100 57 | P-n22-k2.vrp,22,ortools,212,216,-1.851851852,0.044001579,cvrp,100 58 | P-n22-k8.vrp,22,ortools,583,603,-3.316749585,0.091082811,cvrp,100 59 | P-n23-k8.vrp,23,ortools,530,554,-4.332129964,0.115002394,cvrp,100 60 | P-n40-k5.vrp,40,ortools,449,458,-1.965065502,0.339028597,cvrp,100 61 | P-n45-k5.vrp,45,ortools,533,510,4.509803922,0.367005825,cvrp,100 62 | P-n50-k10.vrp,50,ortools,759,696,9.051724138,0.59505558,cvrp,100 63 | P-n50-k7.vrp,50,ortools,559,554,0.902527076,0.583590508,cvrp,100 64 | P-n50-k8.vrp,50,ortools,665,649,2.465331279,0.480026484,cvrp,100 65 | P-n51-k10.vrp,51,ortools,818,745,9.798657718,0.652024984,cvrp,100 66 | P-n55-k10.vrp,55,ortools,720,669,7.623318386,0.479022026,cvrp,100 67 | P-n55-k15.vrp,55,ortools,958,856,11.91588785,0.566030264,cvrp,100 68 | P-n55-k7.vrp,55,ortools,561,524,7.061068702,0.601030111,cvrp,100 69 | P-n55-k8.vrp,55,ortools,607,576,5.381944444,0.496920347,cvrp,100 70 | P-n60-k10.vrp,60,ortools,824,706,16.71388102,0.662961245,cvrp,100 71 | P-n60-k15.vrp,60,ortools,1003,905,10.82872928,0.627239943,cvrp,100 72 | P-n65-k10.vrp,65,ortools,826,792,4.292929293,0.812048197,cvrp,100 73 | P-n70-k10.vrp,70,ortools,898,834,7.673860911,1.010015249,cvrp,100 74 | P-n76-k4.vrp,76,ortools,655,589,11.20543294,1.062059402,cvrp,100 75 | P-n76-k5.vrp,76,ortools,687,631,8.874801902,0.639037609,cvrp,100 76 | -------------------------------------------------------------------------------- /benchmarks/performance_profiles/vrpy_cvrp.csv: -------------------------------------------------------------------------------- 1 | instance,nodes,algorithm,res,best known solution,gap,time (s),vrp,time limit (s) 2 | A-n32-k5.vrp,32,vrpy,847,784,10.7142857142857,0.627320289611816,cvrp,100 3 | A-n33-k5.vrp,33,vrpy,675,661,5.29500756429652,0.731591701507568,cvrp,100 4 | A-n33-k6.vrp,33,vrpy,766,742,4.71698113207547,0.428384304046631,cvrp,100 5 | A-n34-k5.vrp,34,vrpy,789,778,1.41388174807198,0.53159499168396,cvrp,100 6 | A-n36-k5.vrp,36,vrpy,806,799,0.876095118898623,0.525077104568481,cvrp,100 7 | A-n37-k5.vrp,37,vrpy,680,669,5.82959641255605,0.568875789642334,cvrp,100 8 | A-n37-k6.vrp,37,vrpy,977,949,6.00632244467861,0.68068528175354,cvrp,100 9 | A-n38-k5.vrp,38,vrpy,770,730,7.3972602739726,0.617838859558106,cvrp,100 10 | A-n39-k5.vrp,39,vrpy,832,822,4.74452554744526,0.591273307800293,cvrp,100 11 | A-n39-k6.vrp,39,vrpy,885,831,6.85920577617329,0.577922582626343,cvrp,100 12 | A-n44-k7.vrp,44,vrpy,970,937,5.22945570971185,0.417763471603394,cvrp,100 13 | A-n45-k6.vrp,45,vrpy,975,944,3.38983050847458,0.751650810241699,cvrp,100 14 | A-n45-k7.vrp,45,vrpy,1191,1146,5.14834205933682,0.774425983428955,cvrp,100 15 | A-n46-k7.vrp,46,vrpy,930,914,1.75054704595186,0.796474933624268,cvrp,100 16 | A-n48-k7.vrp,48,vrpy,1101,1073,5.03261882572227,0.8485107421875,cvrp,100 17 | A-n53-k7.vrp,53,vrpy,1081,1010,7.02970297029703,1.08332276344299,cvrp,100 18 | A-n54-k7.vrp,54,vrpy,1179,1167,1.19965724078835,1.09907174110413,cvrp,100 19 | A-n55-k9.vrp,55,vrpy,1108,1073,6.61696178937558,1.1399347782135,cvrp,100 20 | A-n60-k9.vrp,60,vrpy,1374,1408,1.7725258493353,1.3618495464325,cvrp,100 21 | A-n61-k9.vrp,61,vrpy,1056,1035,2.90135396518375,1.46349120140076,cvrp,100 22 | A-n62-k8.vrp,62,vrpy,1338,1290,4.27018633540373,1.47827100753784,cvrp,100 23 | A-n63-k10.vrp,63,vrpy,1373,1315,6.16438356164384,1.59858369827271,cvrp,100 24 | A-n63-k9.vrp,63,vrpy,1646,1634,2.47524752475248,1.4794385433197,cvrp,100 25 | A-n64-k9.vrp,64,vrpy,1445,1402,7.13775874375446,1.75822353363037,cvrp,100 26 | A-n65-k9.vrp,65,vrpy,1211,1177,5.62180579216354,1.54639005661011,cvrp,100 27 | A-n69-k9.vrp,69,vrpy,1223,1168,6.64365832614323,1.85623359680176,cvrp,100 28 | A-n80-k10.vrp,80,vrpy,1813,1764,4.99149177538287,2.47068667411804,cvrp,100 29 | B-n31-k5.vrp,31,vrpy,710,672,9.82142857142857,0.501486301422119,cvrp,100 30 | B-n34-k5.vrp,34,vrpy,830,788,5.32994923857868,0.457146167755127,cvrp,100 31 | B-n35-k5.vrp,35,vrpy,1061,955,13.717277486911,0.524820327758789,cvrp,100 32 | B-n38-k6.vrp,38,vrpy,827,805,3.22981366459627,0.661213636398315,cvrp,100 33 | B-n39-k5.vrp,39,vrpy,601,549,11.2932604735883,0.61314058303833,cvrp,100 34 | B-n41-k6.vrp,41,vrpy,851,829,5.42822677925211,0.728065729141235,cvrp,100 35 | B-n43-k6.vrp,43,vrpy,776,742,4.5822102425876,0.666375160217285,cvrp,100 36 | B-n44-k7.vrp,44,vrpy,933,909,2.97029702970297,0.762972831726074,cvrp,100 37 | B-n45-k5.vrp,45,vrpy,765,751,2.52996005326232,0.791166543960571,cvrp,100 38 | B-n45-k6.vrp,45,vrpy,739,678,9.14454277286136,0.804025411605835,cvrp,100 39 | B-n50-k7.vrp,50,vrpy,744,741,0.539811066126856,0.961430072784424,cvrp,100 40 | B-n50-k8.vrp,50,vrpy,1343,1313,2.66768292682927,1.42333436012268,cvrp,100 41 | B-n51-k7.vrp,51,vrpy,1026,1032,1.45348837209302,1.0020854473114,cvrp,100 42 | B-n52-k7.vrp,52,vrpy,795,747,10.1740294511379,1.06550121307373,cvrp,100 43 | B-n56-k7.vrp,56,vrpy,719,707,4.95049504950495,1.32051563262939,cvrp,100 44 | B-n57-k7.vrp,57,vrpy,1220,1153,7.54553339115351,0.741526603698731,cvrp,100 45 | B-n57-k9.vrp,57,vrpy,1649,1598,3.56695869837297,1.28448152542114,cvrp,100 46 | B-n63-k10.vrp,63,vrpy,1570,1537,5.08021390374332,1.54535293579102,cvrp,100 47 | B-n64-k9.vrp,64,vrpy,905,861,5.92334494773519,1.51383686065674,cvrp,100 48 | B-n66-k9.vrp,66,vrpy,1404,1374,11.9300911854103,2.9804515838623,cvrp,100 49 | B-n67-k10.vrp,67,vrpy,1084,1033,6.87984496124031,1.82496356964111,cvrp,100 50 | B-n68-k9.vrp,68,vrpy,1313,1304,5.11006289308176,1.72327923774719,cvrp,100 51 | B-n78-k10.vrp,78,vrpy,1297,1266,10.8108108108108,1.20226240158081,cvrp,100 52 | P-n101-k4.vrp,101,vrpy,815,681,21.8795888399413,2.49956583976746,cvrp,100 53 | P-n16-k8.vrp,16,vrpy,450,435,4,0.166212320327759,cvrp,100 54 | P-n19-k2.vrp,19,vrpy,219,212,3.30188679245283,0.209726333618164,cvrp,100 55 | P-n20-k2.vrp,20,vrpy,222,220,2.77777777777778,0.203568696975708,cvrp,100 56 | P-n21-k2.vrp,21,vrpy,224,211,6.16113744075829,0.124351739883423,cvrp,100 57 | P-n22-k2.vrp,22,vrpy,232,216,7.40740740740741,0.252520561218262,cvrp,100 58 | P-n22-k8.vrp,22,vrpy,590,603,-2.1558872305141,0.252469301223755,cvrp,100 59 | P-n23-k8.vrp,23,vrpy,529,554,0.945179584120983,0.260241746902466,cvrp,100 60 | P-n40-k5.vrp,40,vrpy,471,458,9.1703056768559,0.317032814025879,cvrp,100 61 | P-n45-k5.vrp,45,vrpy,534,510,8.03921568627451,0.763275384902954,cvrp,100 62 | P-n50-k10.vrp,50,vrpy,706,696,5.45977011494253,1.03723788261414,cvrp,100 63 | P-n50-k7.vrp,50,vrpy,573,554,5.5956678700361,0.941074848175049,cvrp,100 64 | P-n50-k8.vrp,50,vrpy,641,649,2.21870047543582,0.932344913482666,cvrp,100 65 | P-n51-k10.vrp,51,vrpy,748,745,2.83400809716599,1.02766585350037,cvrp,100 66 | P-n55-k10.vrp,55,vrpy,704,669,4.32276657060519,1.12186694145203,cvrp,100 67 | P-n55-k15.vrp,55,vrpy,958,856,-2.32558139534884,1.09516930580139,cvrp,100 68 | P-n55-k7.vrp,55,vrpy,587,524,7.21830985915493,1.14335918426514,cvrp,100 69 | P-n55-k8.vrp,55,vrpy,587,576,5.95238095238095,1.11352133750916,cvrp,100 70 | P-n60-k10.vrp,60,vrpy,774,706,4.70430107526882,1.55574727058411,cvrp,100 71 | P-n60-k15.vrp,60,vrpy,979,905,3.40909090909091,1.56613731384277,cvrp,100 72 | P-n65-k10.vrp,65,vrpy,805,792,5.42929292929293,1.45155644416809,cvrp,100 73 | P-n70-k10.vrp,70,vrpy,841,834,4.59492140266022,1.89463305473328,cvrp,100 74 | P-n76-k4.vrp,76,vrpy,677,589,16.3575042158516,2.23481678962708,cvrp,100 75 | P-n76-k5.vrp,76,vrpy,693,631,11.8022328548644,2.21990251541138,cvrp,100 76 | -------------------------------------------------------------------------------- /benchmarks/run.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from itertools import product 3 | from logging import getLogger 4 | from multiprocessing import Pool, cpu_count 5 | from pathlib import Path 6 | from typing import List, Dict, Union 7 | 8 | from networkx import DiGraph 9 | 10 | from benchmarks.augerat_dataset import AugeratDataSet 11 | from benchmarks.solomon_dataset import SolomonDataSet 12 | from benchmarks.utils.csv_table import CsvTable 13 | from vrpy import VehicleRoutingProblem 14 | 15 | logger = getLogger("run") 16 | 17 | parser = argparse.ArgumentParser(description="Run benchmarks.") 18 | 19 | parser.add_argument( 20 | "--input_folder", 21 | "-dir", 22 | type=str, 23 | default="benchmarks/data/", 24 | dest="INPUT_FOLDER", 25 | help="Top folder where the instances are located" + " Default: benchmarks/data/", 26 | ) 27 | parser.add_argument( 28 | "--instance_types", 29 | "-i", 30 | nargs="+", 31 | dest="INSTANCE_TYPES", 32 | default=["cvrp"], 33 | help="Type of instance to run: crvp, cvrptw." 34 | + " Note same name as folder in data. Default: cvrp", 35 | ) 36 | parser.add_argument( 37 | "--time_limit", 38 | "-t", 39 | type=int, 40 | default=10, 41 | dest="TIME_LIMIT", 42 | help="Time limit for each instance in seconds." + " Default: 10", 43 | ) 44 | parser.add_argument( 45 | "--series", 46 | "-s", 47 | action="store_true", 48 | dest="SERIES", 49 | help="To run the benchmarks in series or in parallel." + " Default: parallel", 50 | ) 51 | parser.add_argument( 52 | "--cpu-count", 53 | "-cpu", 54 | type=int, 55 | default=0, 56 | dest="CPU_COUNT", 57 | help="Number of cpus to use. Default: all avaiable.", 58 | ) 59 | parser.add_argument( 60 | "--exploration", 61 | "-e", 62 | action="store_false", 63 | dest="PERFORMANCE", 64 | help="To run the benchmarks in performance mode (default)" 65 | + " or in exploration mode. Exploration mode runs" 66 | + " different solver parameters", 67 | ) 68 | 69 | args = parser.parse_args() 70 | 71 | # Set vars from arguments 72 | INPUT_FOLDER: Path = Path(args.INPUT_FOLDER) 73 | INSTANCE_TYPES: List[str] = args.INSTANCE_TYPES 74 | TIME_LIMIT: int = args.TIME_LIMIT 75 | SERIES: bool = args.SERIES 76 | CPU_COUNT: int = cpu_count() if not args.CPU_COUNT else args.CPU_COUNT 77 | PERFORMANCE: bool = args.PERFORMANCE 78 | # Perfomance set up 79 | PERFORMANCE_SOLVER_PARAMS: Dict[str, Dict[str, Union[bool, str]]] = { 80 | "cvrp": { 81 | "dive": False, 82 | "greedy": True, 83 | "cspy": False, 84 | "pricing_strategy": "Hyper", 85 | "time_limit": TIME_LIMIT, 86 | "max_iter": 1, 87 | "run_exact": 30, 88 | }, 89 | "cvrptw": { 90 | "dive": False, 91 | "greedy": True, 92 | "cspy": False, 93 | "pricing_strategy": "BestEdges1", 94 | "max_iter": 1, 95 | "time_limit": TIME_LIMIT, 96 | "run_exact": 30, 97 | }, 98 | } 99 | 100 | 101 | def run_series(): 102 | """Iterates through all problem instances and creates csv table 103 | in a new folder `benchmarks/results/` in series 104 | """ 105 | for instance_type in INSTANCE_TYPES: 106 | path_to_instance_type = INPUT_FOLDER / instance_type 107 | for path_to_instance in path_to_instance_type.glob("*"): 108 | if PERFORMANCE: 109 | _run_single_problem( 110 | path_to_instance, **PERFORMANCE_SOLVER_PARAMS[instance_type] 111 | ) 112 | else: 113 | for dive in [True, False]: 114 | for cspy in [True, False]: 115 | for pricing_strategy in [ 116 | "BestPaths", 117 | "BestEdges1", 118 | "BestEdges2", 119 | "Exact", 120 | "Hyper", 121 | ]: 122 | for greedy in [True, False]: 123 | _run_single_problem( 124 | path_to_instance, 125 | dive=dive, 126 | greedy=greedy, 127 | cspy=cspy, 128 | pricing_strategy=pricing_strategy, 129 | ) 130 | 131 | 132 | def run_parallel(): 133 | """Iterates through the instances using in parallel using CPU_COUNT.""" 134 | all_files = [ 135 | path_to_instance 136 | for instance_type in INSTANCE_TYPES 137 | for path_to_instance in Path(INPUT_FOLDER / instance_type).glob("*") 138 | ] 139 | 140 | if PERFORMANCE: 141 | # Iterate through all files 142 | iterate_over = all_files # Exploration mode 143 | else: 144 | # Iterate the cartesian product of all files and solver parameters 145 | iterate_over = list( 146 | product( 147 | all_files, 148 | [False], # dive 149 | [True], # greedy 150 | [True, False], # cspy 151 | ["Hyper"], 152 | ) 153 | ) 154 | pool = Pool(processes=CPU_COUNT) 155 | with pool: 156 | res = pool.map_async(_parallel_wrapper, iterate_over) 157 | res.get() 158 | pool.close() 159 | 160 | 161 | def _parallel_wrapper(input_tuple): 162 | if PERFORMANCE: 163 | path_to_instance = input_tuple 164 | instance_type = path_to_instance.parent.stem 165 | _run_single_problem( 166 | path_to_instance, **PERFORMANCE_SOLVER_PARAMS[instance_type] 167 | ) 168 | else: 169 | kwargs = dict( 170 | zip( 171 | ["path_to_instance", "dive", "greedy", "cspy", "pricing_strategy"], 172 | input_tuple, 173 | ) 174 | ) 175 | _run_single_problem(**kwargs) 176 | 177 | 178 | def _run_single_problem(path_to_instance: Path, **kwargs): 179 | "Run single problem with solver arguments as in kwargs" 180 | instance_folder = path_to_instance.parent 181 | instance_type = path_to_instance.parent.stem 182 | instance_name = path_to_instance.name 183 | logger.info("Solving instance %s", instance_name) 184 | # Load data 185 | if instance_type == "cvrp": 186 | data = AugeratDataSet(path=instance_folder, instance_name=instance_name) 187 | elif instance_type == "cvrptw": 188 | data = SolomonDataSet(path=instance_folder, instance_name=instance_name) 189 | # Solve problem 190 | prob = VehicleRoutingProblem( 191 | data.G, 192 | load_capacity=data.max_load, 193 | time_windows=bool(instance_type == "cvrptw"), 194 | ) 195 | prob.solve(**kwargs) 196 | # Output results 197 | table = CsvTable( 198 | instance_name=instance_name, 199 | comp_time=prob.comp_time, 200 | best_known_solution=data.best_known_solution, 201 | instance_type=instance_type, 202 | ) 203 | table.from_vrpy_instance(prob) 204 | 205 | 206 | def main(): 207 | """Run parallel or series""" 208 | if SERIES: 209 | run_series() 210 | else: 211 | run_parallel() 212 | 213 | 214 | if __name__ == "__main__": 215 | main() 216 | -------------------------------------------------------------------------------- /benchmarks/solomon_dataset.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pandas import read_csv 4 | from networkx import DiGraph 5 | import numpy as np 6 | 7 | from benchmarks.utils.distance import distance 8 | 9 | 10 | class SolomonNode: 11 | """Stores attributes of a node of Solomon's instances.""" 12 | 13 | def __init__(self, values): 14 | # Node ID 15 | self.name = np.uint32(values[1]).item() 16 | if self.name == 0: 17 | self.name = "Source" 18 | # x coordinate 19 | self.x = np.float64(values[2]).item() 20 | # y coordinate 21 | self.y = np.float64(values[3]).item() 22 | self.demand = np.uint32(values[4]).item() 23 | self.inf_time_window = np.uint32(values[5]).item() 24 | self.sup_time_window = np.uint32(values[6]).item() 25 | self.service_time = np.uint32(values[7]).item() 26 | 27 | 28 | class SolomonDataSet: 29 | """Reads a Solomon instance and stores the network as DiGraph. 30 | 31 | Args: 32 | path (pathlib.Path) : Path to data folder. 33 | instance_name (str) : Name of Solomon instance to read. 34 | n_vertices (int, optional): 35 | Only first n_vertices are read. 36 | Defaults to None. 37 | """ 38 | 39 | def __init__(self, path: Path, instance_name: str, n_vertices=None): 40 | self.G: DiGraph = None 41 | self.max_load: int = None 42 | # TODO load best_known_solution somewhere 43 | self.best_known_solution: int = None 44 | 45 | path = Path(path) 46 | self._load(path, instance_name, n_vertices) 47 | 48 | def _load(self, path, instance_name, n_vertices=None): 49 | # Read vehicle capacity 50 | with open(path / instance_name) as fp: 51 | for i, line in enumerate(fp): 52 | if i == 4: 53 | self.max_load = int(line.split()[1]) 54 | fp.close() 55 | 56 | # Create network and store name + capacity 57 | self.G = DiGraph( 58 | name=instance_name[:-4] + "." + str(n_vertices), 59 | vehicle_capacity=self.max_load, 60 | ) 61 | 62 | # Read nodes from txt file 63 | df_solomon = read_csv( 64 | path / instance_name, 65 | sep="\s+", 66 | skip_blank_lines=True, 67 | skiprows=7, 68 | nrows=n_vertices, 69 | ) 70 | # Scan each line of the file and add nodes to the network 71 | for line in df_solomon.itertuples(): 72 | node = SolomonNode(line) 73 | self.G.add_node( 74 | node.name, 75 | x=node.x, 76 | y=node.y, 77 | demand=node.demand, 78 | lower=node.inf_time_window, 79 | upper=node.sup_time_window, 80 | service_time=node.service_time, 81 | ) 82 | # Add Sink as copy of Source 83 | if node.name == "Source": 84 | self.G.add_node( 85 | "Sink", 86 | x=node.x, 87 | y=node.y, 88 | demand=node.demand, 89 | lower=node.inf_time_window, 90 | upper=node.sup_time_window, 91 | service_time=node.service_time, 92 | ) 93 | 94 | # Add the edges, the graph is complete 95 | for u in self.G.nodes(): 96 | if u != "Sink": 97 | for v in self.G.nodes(): 98 | if v != "Source" and u != v: 99 | self.G.add_edge(u, 100 | v, 101 | cost=distance(self.G, u, v), 102 | time=distance(self.G, u, v)) 103 | -------------------------------------------------------------------------------- /benchmarks/tests/graph_issue101: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/benchmarks/tests/graph_issue101 -------------------------------------------------------------------------------- /benchmarks/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_cli = 1 3 | log_cli_level = INFO 4 | log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) 5 | log_cli_date_format=%Y-%m-%d %H:%M:%S 6 | python_files = test_*.py 7 | #testpaths = tests/ 8 | filterwarnings = 9 | ignore::DeprecationWarning 10 | -------------------------------------------------------------------------------- /benchmarks/tests/test_cvrp_augerat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append("../../") 4 | from benchmarks.augerat_dataset import AugeratDataSet 5 | 6 | from vrpy import VehicleRoutingProblem 7 | 8 | 9 | class TestsAugerat: 10 | def setup(self): 11 | """ 12 | Augerat instance P-n16-k8.vrp 13 | """ 14 | data = AugeratDataSet( 15 | path="benchmarks/data/cvrp/", instance_name="P-n16-k8.vrp" 16 | ) 17 | self.G = data.G 18 | self.prob = VehicleRoutingProblem(self.G, load_capacity=data.max_load) 19 | self.solver_args = {"pricing_strategy": "BestPaths"} 20 | 21 | def test_setup_instance_name(self): 22 | assert self.G.graph["name"] == "P-n16-k8" 23 | 24 | def test_setup_vehicle_capacity(self): 25 | assert self.G.graph["vehicle_capacity"] == 35 26 | 27 | def test_setup_nodes(self): 28 | # extra node for the Sink 29 | assert len(self.G.nodes()) == 16 + 1 30 | 31 | def test_setup_edges(self): 32 | assert len(self.G.edges()) == 16 * (16 - 1) + 1 33 | 34 | def test_subproblem_lp(self): 35 | self.prob.solve(**self.solver_args, cspy=False) 36 | assert round(self.prob.best_value, -1) in [450, 460] 37 | 38 | def test_subproblem_lp_greedy(self): 39 | self.prob.solve(**self.solver_args, cspy=False, greedy=True) 40 | assert round(self.prob.best_value, -1) in [450, 460] 41 | 42 | def test_subproblem_cspy(self): 43 | self.prob.solve(**self.solver_args) 44 | assert round(self.prob.best_value, -1) in [450, 460] 45 | 46 | def test_subproblem_lp_with_initial_routes(self): 47 | # benchmark result 48 | # http://vrp.galgos.inf.puc-rio.br/index.php/en/ 49 | r_1 = ["Source", 2, "Sink"] 50 | r_2 = ["Source", 6, "Sink"] 51 | r_3 = ["Source", 8, "Sink"] 52 | r_4 = ["Source", 15, 12, 10, "Sink"] 53 | r_5 = ["Source", 14, 5, "Sink"] 54 | r_6 = ["Source", 13, 9, 7, "Sink"] 55 | r_7 = ["Source", 11, 4, "Sink"] 56 | r_8 = ["Source", 3, 1, "Sink"] 57 | ini = [r_1, r_2, r_3, r_4, r_5, r_6, r_7, r_8] 58 | self.prob.solve(**self.solver_args, cspy=False, initial_routes=ini) 59 | assert int(self.prob.best_value) == 450 60 | 61 | def test_subproblem_cspy_with_initial_routes(self): 62 | # benchmark result 63 | # http://vrp.galgos.inf.puc-rio.br/index.php/en/ 64 | r_1 = ["Source", 2, "Sink"] 65 | r_2 = ["Source", 6, "Sink"] 66 | r_3 = ["Source", 8, "Sink"] 67 | r_4 = ["Source", 15, 12, 10, "Sink"] 68 | r_5 = ["Source", 14, 5, "Sink"] 69 | r_6 = ["Source", 13, 9, 7, "Sink"] 70 | r_7 = ["Source", 11, 4, "Sink"] 71 | r_8 = ["Source", 3, 1, "Sink"] 72 | ini = [r_1, r_2, r_3, r_4, r_5, r_6, r_7, r_8] 73 | self.prob.solve(**self.solver_args, initial_routes=ini) 74 | assert int(self.prob.best_value) == 450 75 | -------------------------------------------------------------------------------- /benchmarks/tests/test_cvrptw_solomon.py: -------------------------------------------------------------------------------- 1 | from benchmarks.solomon_dataset import SolomonDataSet 2 | 3 | from vrpy import VehicleRoutingProblem 4 | 5 | 6 | class TestsSolomon: 7 | def setup(self): 8 | """ 9 | Solomon instance c101, 25 first nodes only including depot 10 | """ 11 | data = SolomonDataSet( 12 | path="benchmarks/data/cvrptw/", instance_name="C101.txt", n_vertices=25 13 | ) 14 | self.G = data.G 15 | self.n_vertices = 25 16 | self.prob = VehicleRoutingProblem( 17 | self.G, load_capacity=data.max_load, time_windows=True 18 | ) 19 | initial_routes = [ 20 | ["Source", 13, 17, 18, 19, 15, 16, 14, 12, 1, "Sink"], 21 | ["Source", 20, 24, 23, 22, 21, "Sink"], 22 | ["Source", 5, 3, 7, 8, 10, 11, 6, 4, 2, "Sink"], 23 | ["Source", 9, "Sink"], 24 | ] 25 | # Set repeating solver arguments 26 | self.solver_args = { 27 | "pricing_strategy": "BestPaths", 28 | "initial_routes": initial_routes, 29 | } 30 | 31 | def test_setup_instance_name(self): 32 | assert self.G.graph["name"] == "C101." + str(self.n_vertices) 33 | 34 | def test_setup_vehicle_capacity(self): 35 | assert self.G.graph["vehicle_capacity"] == 200 36 | 37 | def test_setup_nodes(self): 38 | # extra node for the Sink 39 | assert len(self.G.nodes()) == self.n_vertices + 1 40 | 41 | def test_setup_edges(self): 42 | assert len(self.G.edges()) == self.n_vertices * (self.n_vertices - 1) + 1 43 | 44 | def test_subproblem_lp(self): 45 | # benchmark result 46 | # e.g., in Feillet et al. (2004) 47 | self.prob.solve(**self.solver_args, cspy=False) 48 | assert round(self.prob.best_value, -1) in [190, 200] 49 | self.prob.check_arrival_time() 50 | self.prob.check_departure_time() 51 | 52 | def test_subproblem_cspy(self): 53 | self.prob.solve(**self.solver_args, cspy=True) 54 | assert round(self.prob.best_value, -1) in [190, 200] 55 | self.prob.check_arrival_time() 56 | self.prob.check_departure_time() 57 | -------------------------------------------------------------------------------- /benchmarks/tests/test_cvrptw_solomon_range.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from time import time 3 | import csv 4 | 5 | from vrpy import VehicleRoutingProblem 6 | 7 | from benchmarks.solomon_dataset import SolomonDataSet 8 | 9 | params = list(range(7, 70)) 10 | 11 | 12 | @fixture( 13 | scope="class", 14 | params=params, 15 | ) 16 | def n(request): 17 | print("setup once per each param", request.param) 18 | return request.param 19 | 20 | 21 | REPS_LP = 1 22 | REPS_CSPY = 10 23 | 24 | 25 | def write_avg(n, times_cspy, iter_cspy, times_lp, iter_lp, name="cspy102fwdearly"): 26 | def _avg(l): 27 | return sum(l) / len(l) 28 | 29 | with open(f"benchmarks/results/{name}.csv", "a", newline="") as f: 30 | writer_object = csv.writer(f) 31 | writer_object.writerow( 32 | [n, _avg(times_cspy), _avg(iter_cspy), _avg(times_lp), _avg(iter_lp)] 33 | ) 34 | f.close() 35 | 36 | 37 | class TestsSolomon: 38 | def test_subproblem(self, n): 39 | data = SolomonDataSet( 40 | path="benchmarks/data/cvrptw/", instance_name="C101.txt", n_vertices=n 41 | ) 42 | self.G = data.G 43 | best_values_lp = None 44 | lp_iter = [] 45 | times_lp = [] 46 | for r in range(REPS_LP): 47 | prob = VehicleRoutingProblem( 48 | self.G, load_capacity=data.max_load, time_windows=True 49 | ) 50 | start = time() 51 | prob.solve(cspy=False) 52 | best_value_lp = prob.best_value 53 | times_lp.append(time() - start) 54 | lp_iter.append(prob._iteration) 55 | del prob 56 | best_values_cspy = [] 57 | times_cspy = [] 58 | iter_cspy = [] 59 | for r in range(REPS_CSPY): 60 | prob = VehicleRoutingProblem( 61 | self.G, load_capacity=data.max_load, time_windows=True 62 | ) 63 | start = time() 64 | prob.solve(cspy=True, pricing_strategy="Exact") 65 | times_cspy.append(time() - start) 66 | best_values_cspy.append(prob.best_value) 67 | iter_cspy.append(prob._iteration) 68 | prob.check_arrival_time() 69 | prob.check_departure_time() 70 | del prob 71 | assert all(best_value_lp == val_cspy for val_cspy in best_values_cspy) 72 | write_avg(n, times_cspy, iter_cspy, times_lp, lp_iter) 73 | -------------------------------------------------------------------------------- /benchmarks/tests/test_examples.py: -------------------------------------------------------------------------------- 1 | from networkx import ( 2 | from_numpy_matrix, 3 | set_node_attributes, 4 | relabel_nodes, 5 | DiGraph, 6 | compose, 7 | ) 8 | from numpy import array 9 | import sys 10 | 11 | sys.path.append("../vrpy/") 12 | from vrpy import VehicleRoutingProblem 13 | from examples.data import ( 14 | DISTANCES, 15 | TRAVEL_TIMES, 16 | TIME_WINDOWS_LOWER, 17 | TIME_WINDOWS_UPPER, 18 | PICKUPS_DELIVERIES, 19 | DEMANDS, 20 | COLLECT, 21 | ) 22 | 23 | 24 | class TestsOrTools: 25 | def setup(self): 26 | # Transform distance matrix to DiGraph 27 | A = array(DISTANCES, dtype=[("cost", int)]) 28 | G_d = from_numpy_matrix(A, create_using=DiGraph()) 29 | # Transform time matrix to DiGraph 30 | A = array(TRAVEL_TIMES, dtype=[("time", int)]) 31 | G_t = from_numpy_matrix(A, create_using=DiGraph()) 32 | # Merge 33 | G = compose(G_d, G_t) 34 | # Set time windows 35 | set_node_attributes(G, values=TIME_WINDOWS_LOWER, name="lower") 36 | set_node_attributes(G, values=TIME_WINDOWS_UPPER, name="upper") 37 | # Set demand and collect volumes 38 | set_node_attributes(G, values=DEMANDS, name="demand") 39 | set_node_attributes(G, values=COLLECT, name="collect") 40 | # Relabel depot 41 | self.G = relabel_nodes(G, {0: "Source", 17: "Sink"}) 42 | # Define VRP 43 | self.prob = VehicleRoutingProblem(self.G) 44 | 45 | def test_cvrp_dive_lp(self): 46 | self.prob.load_capacity = 15 47 | self.prob.solve(cspy=False, pricing_strategy="BestEdges1", dive=True) 48 | assert int(self.prob.best_value) == 6208 49 | 50 | def test_cvrp_dive_cspy(self): 51 | self.prob.load_capacity = 15 52 | self.prob.solve(pricing_strategy="BestEdges1", dive=True) 53 | assert int(self.prob.best_value) == 6208 54 | 55 | def test_vrptw_dive_lp(self): 56 | self.prob.time_windows = True 57 | self.prob.solve(cspy=False, dive=True) 58 | assert int(self.prob.best_value) == 6528 59 | 60 | def test_vrptw_dive_cspy(self): 61 | self.prob.time_windows = True 62 | self.prob.solve(cspy=True, dive=True) 63 | assert int(self.prob.best_value) == 6528 64 | 65 | def test_cvrpsdc_dive_lp(self): 66 | self.prob.load_capacity = 15 67 | self.prob.distribution_collection = True 68 | self.prob.solve(cspy=False, pricing_strategy="BestEdges1", dive=True) 69 | assert int(self.prob.best_value) == 6208 70 | 71 | def test_cvrpsdc_dive_cspy(self): 72 | self.prob.load_capacity = 15 73 | self.prob.distribution_collection = True 74 | self.prob.solve(pricing_strategy="BestEdges1", dive=True) 75 | assert int(self.prob.best_value) == 6208 76 | 77 | def test_pdp_dive_lp(self): 78 | # Set demands and requests 79 | for (u, v) in PICKUPS_DELIVERIES: 80 | self.G.nodes[u]["request"] = v 81 | self.G.nodes[u]["demand"] = PICKUPS_DELIVERIES[(u, v)] 82 | self.G.nodes[v]["demand"] = -PICKUPS_DELIVERIES[(u, v)] 83 | self.prob.pickup_delivery = True 84 | self.prob.load_capacity = 10 85 | self.prob.num_stops = 6 86 | self.prob.solve(cspy=False, dive=True) 87 | sol_lp = self.prob.best_value 88 | assert int(sol_lp) == 5980 89 | 90 | def test_cvrp_lp(self): 91 | self.prob.load_capacity = 15 92 | self.prob.solve(cspy=False, pricing_strategy="BestEdges1") 93 | assert int(self.prob.best_value) == 6208 94 | 95 | def test_cvrp_cspy(self): 96 | self.prob.load_capacity = 15 97 | self.prob.solve(pricing_strategy="BestEdges1") 98 | assert int(self.prob.best_value) == 6208 99 | 100 | def test_vrptw_lp(self): 101 | self.prob.time_windows = True 102 | self.prob.solve(cspy=False) 103 | assert int(self.prob.best_value) == 6528 104 | 105 | def test_vrptw_cspy(self): 106 | self.prob.time_windows = True 107 | self.prob.solve() 108 | assert int(self.prob.best_value) == 6528 109 | 110 | def test_cvrpsdc_lp(self): 111 | self.prob.load_capacity = 15 112 | self.prob.distribution_collection = True 113 | self.prob.solve(cspy=False, pricing_strategy="BestEdges1") 114 | assert int(self.prob.best_value) == 6208 115 | 116 | def test_cvrpsdc_cspy(self): 117 | self.prob.load_capacity = 15 118 | self.prob.distribution_collection = True 119 | self.prob.solve(pricing_strategy="BestEdges1") 120 | assert int(self.prob.best_value) == 6208 121 | 122 | def test_pdp_lp(self): 123 | # Set demands and requests 124 | for (u, v) in PICKUPS_DELIVERIES: 125 | self.G.nodes[u]["request"] = v 126 | self.G.nodes[u]["demand"] = PICKUPS_DELIVERIES[(u, v)] 127 | self.G.nodes[v]["demand"] = -PICKUPS_DELIVERIES[(u, v)] 128 | self.prob.pickup_delivery = True 129 | self.prob.load_capacity = 10 130 | self.prob.num_stops = 6 131 | self.prob.solve(cspy=False) 132 | sol_lp = self.prob.best_value 133 | assert int(sol_lp) == 5980 134 | -------------------------------------------------------------------------------- /benchmarks/tests/test_issue101.py: -------------------------------------------------------------------------------- 1 | from networkx import DiGraph, read_gpickle 2 | from vrpy import VehicleRoutingProblem 3 | 4 | 5 | class TestIssue101_large: 6 | def setup(self): 7 | G = read_gpickle("benchmarks/tests/graph_issue101") 8 | self.prob = VehicleRoutingProblem(G, load_capacity=80) 9 | self.prob.time_windows = True 10 | 11 | # def test_lp(self): 12 | # self.prob.solve(cspy=False, solver="gurobi") 13 | # self.prob.check_arrival_time() 14 | # self.prob.check_departure_time() 15 | 16 | def test_cspy(self): 17 | self.prob.solve(pricing_strategy="Exact") 18 | self.prob.check_arrival_time() 19 | self.prob.check_departure_time() 20 | -------------------------------------------------------------------------------- /benchmarks/utils/csv_table.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from csv import DictWriter 3 | from logging import getLogger 4 | 5 | logger = getLogger(__name__) 6 | 7 | 8 | class CsvTable: 9 | """ 10 | Base class for CSVTable. 11 | Args: 12 | path (string): Path to instance data 13 | instance_name (string): Name of the test instance 14 | comp_time (float): Computation time 15 | upper_bound(float): Integer optimal value 16 | lower_bound(float): Relaxed optimal value 17 | integrality_gap(float): Integer relaxed discrepency 18 | pricing_strategy(string): Heuristic strategy 19 | subproblem_type(string): Subproblem type 20 | dive (bool): Diving heuristic 21 | Methods: 22 | """ 23 | def __init__(self, 24 | path=None, 25 | instance_name=None, 26 | instance_type=None, 27 | comp_time=None, 28 | upper_bound=None, 29 | lower_bound=None, 30 | integrality_gap=None, 31 | optimality_gap=None, 32 | optimal=None, 33 | pricing_strategy=None, 34 | subproblem_type=None, 35 | dive=None, 36 | greedy=None, 37 | iterations=None, 38 | best_known_solution=None): 39 | self.path = path 40 | self.instance_name = instance_name if not instance_name.endswith( 41 | '.csv') else instance_name[:-4] 42 | self.instance_type = instance_type 43 | self.comp_time = comp_time 44 | self.upper_bound = upper_bound 45 | self.lower_bound = lower_bound 46 | self.integrality_gap = integrality_gap 47 | self.optimality_gap = optimality_gap 48 | self.optimal = optimal 49 | self.pricing_strategy = pricing_strategy 50 | self.subproblem_type = subproblem_type 51 | self.dive = dive 52 | self.greedy = greedy 53 | self.iterations = iterations 54 | self.best_known_solution = best_known_solution 55 | 56 | def from_vrpy_instance(self, 57 | prob, 58 | output_folder: str = "benchmarks/results/"): 59 | """ 60 | Create csv table using a `vrpy.VehicleRoutingProblem` instance 61 | """ 62 | # Extract releavant attributes (most are private) 63 | self.dive = prob._dive 64 | self.greedy = prob._greedy 65 | self.pricing_strategy = prob._pricing_strategy 66 | self.iterations = prob._iteration 67 | self.subproblem_type = "cspy" if prob._cspy else "lp" 68 | 69 | self.upper_bound = prob.best_value 70 | self.lower_bound = prob._lower_bound[-1] 71 | 72 | # Calculate gaps 73 | if self.iterations > 1: 74 | self.integrality_gap = (self.upper_bound - 75 | self.lower_bound) / self.lower_bound * 100 76 | else: 77 | self.integrality_gap = "Not-valid" 78 | 79 | if self.best_known_solution is not None: 80 | self.optimality_gap = (self.upper_bound - self.best_known_solution 81 | ) / self.best_known_solution * 100 82 | self.optimal = (self.optimality_gap == 0) 83 | else: 84 | self.optimality_gap = "Unknown" 85 | self.optimal = "Unknown" 86 | 87 | self.write_to_file(output_folder) 88 | 89 | def write_to_file(self, output_folder: str = "benchmarks/results/"): 90 | """ 91 | Write to file: Creates a results folder in the current directory 92 | and writes the relevant data to a file specified by instance name. 93 | """ 94 | output_folder = Path(output_folder) 95 | # Create folder if it doesn't already exist 96 | if not output_folder.exists(): 97 | output_folder.mkdir() 98 | 99 | # Append to file if it already exists 100 | file_name = self.instance_type + ".csv" 101 | output_file_path = output_folder / file_name 102 | mode = 'a' if output_file_path.is_file() else 'w' 103 | with open(output_file_path, mode, newline='') as csv_file: 104 | writer = DictWriter(csv_file, 105 | fieldnames=[ 106 | "Instance", "Pricing strategy", 107 | "Subproblem type", "Dived", "Greedy", 108 | "Runtime", "# of iterations", 109 | "Integrality gap", "Optimality gap", 110 | "Optimal" 111 | ]) 112 | if mode == 'w': 113 | writer.writeheader() 114 | writer.writerow({ 115 | "Instance": self.instance_name, 116 | "Pricing strategy": self.pricing_strategy, 117 | "Subproblem type": self.subproblem_type, 118 | "Dived": self.dive, 119 | "Greedy": self.greedy, 120 | "Runtime": self.comp_time, 121 | "# of iterations": self.iterations, 122 | "Integrality gap": self.integrality_gap, 123 | "Optimality gap": self.optimality_gap, 124 | "Optimal": self.optimal 125 | }) 126 | logger.info("Results saved to %s", output_file_path) 127 | csv_file.close() 128 | 129 | def get_df(self): 130 | """TODO: write function to get dataframe with only the stuff we want to 131 | compare with other solvers. i.e. drop the pricing_strategy, ... """ 132 | -------------------------------------------------------------------------------- /benchmarks/utils/distance.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | 3 | 4 | def distance(G, u, v): 5 | """2D Euclidian distance between two nodes. 6 | 7 | Args: 8 | G (Graph) : 9 | u (Node) : tail node. 10 | v (Node) : head node. 11 | 12 | Returns: 13 | float : Euclidian distance between u and v 14 | """ 15 | delta_x = G.nodes[u]["x"] - G.nodes[v]["x"] 16 | delta_y = G.nodes[u]["y"] - G.nodes[v]["y"] 17 | return round(sqrt(delta_x**2 + delta_y**2), 0) 18 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API 4 | === 5 | 6 | vrpy.VehicleRoutingProblem 7 | -------------------------- 8 | 9 | .. automodule:: vrpy.vrp 10 | :members: 11 | :inherited-members: 12 | 13 | 14 | Notes 15 | ----- 16 | 17 | The input graph must have single `Source` and `Sink` nodes with no incoming or outgoing edges respectively. 18 | These dummy nodes represent the depot which is split for modeling convenience. The `Source` and `Sink` cannot have a demand, if 19 | one is given it is ignored with a warning. 20 | 21 | Please read sections :ref:`vrp`, :ref:`options` and :ref:`examples` for details and examples on each of the above arguments. 22 | 23 | -------------------------------------------------------------------------------- /docs/benchmarks.rst: -------------------------------------------------------------------------------- 1 | .. _benchmarks: 2 | 3 | Performance profiles 4 | ==================== 5 | 6 | Performance profiles are a practical way to have a global overview of a set of algorithms' performances. 7 | On the :math:`x` axis, we have the relative gap (%), and on the :math:`y` axis, the percentage of data sets solved within the gap. 8 | So for example, at the intersection with the :math:`y` axis is the percentage of data sets solved optimally, 9 | and at the intersection with :math:`y=100\%` is the relative gap within which all data sets are solved. 10 | 11 | At a glance, the more the curve is in the upper left corner, the better the algorithm. 12 | 13 | We compare the performances of `vrpy` and `OR-Tools` (default options): 14 | 15 | - on Augerat_'s instances (CVRP), 16 | - on Solomon_'s instances (CVRPTW) 17 | 18 | Results are found here_ [link to repo] and can be replicated. 19 | 20 | CVRP 21 | ---- 22 | 23 | .. figure:: images/cvrp_performance_profile.png 24 | :align: center 25 | 26 | We can see that with a maximum running time of :math:`10` seconds, `OR-Tools` solves :math:`15\%` of the instances optimally, 27 | while `vrpy` only solves :math:`5\%` of them. Both solve approximately :math:`43\%` of instances with a maximum relative gap of :math:`5\%`. 28 | And both solve all instances within a maximum gap of :math:`25\%`. 29 | 30 | CVRPTW 31 | ------ 32 | 33 | Coming soon. 34 | 35 | .. _Augerat: https://neo.lcc.uma.es/vrp/vrp-instances/capacitated-vrp-instances/ 36 | .. _Solomon: https://neo.lcc.uma.es/vrp/vrp-instances/capacitated-vrp-with-time-windows-instances/ 37 | .. _here: -------------------------------------------------------------------------------- /docs/bibliography.rst: -------------------------------------------------------------------------------- 1 | Bibliography 2 | ------------ 3 | 4 | .. bibliography:: refs.bib 5 | :all: 6 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("../")) 17 | sys.path.insert(0, os.path.abspath("../vrpy")) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = "VRPy" 23 | copyright = "2020, Romain Montagné, David Torres Sanchez" 24 | author = "Romain Montagné, David Torres Sanchez" 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = "0.1.0" 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | "sphinx.ext.autodoc", 37 | "sphinx.ext.doctest", 38 | "sphinx.ext.intersphinx", 39 | "sphinx.ext.napoleon", 40 | "sphinx.ext.autosectionlabel", 41 | "sphinxcontrib.bibtex", 42 | "sphinx_copybutton", 43 | # "sphinx.ext.todo", 44 | # "sphinx.ext.coverage", 45 | # "sphinx.ext.ifconfig", 46 | # "sphinx.ext.autosummary", 47 | "sphinx.ext.mathjax", 48 | ] 49 | 50 | master_doc = "index" 51 | # autosectionlabel_prefix_document = True 52 | pngmath_use_preview = True 53 | 54 | # Add any paths that contain templates here, relative to this directory. 55 | templates_path = ["_templates"] 56 | 57 | # List of patterns, relative to source directory, that match files and 58 | # directories to ignore when looking for source files. 59 | # This pattern also affects html_static_path and html_extra_path. 60 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 61 | 62 | 63 | # -- Options for HTML output ------------------------------------------------- 64 | 65 | # The theme to use for HTML and HTML Help pages. See the documentation for 66 | # a list of builtin themes. 67 | # 68 | #html_theme = "alabaster" 69 | 70 | # Add any paths that contain custom static files (such as style sheets) here, 71 | # relative to this directory. They are copied after the builtin static files, 72 | # so a file named "default.css" will overwrite the builtin "default.css". 73 | html_static_path = [] 74 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | Installation 5 | ************ 6 | 7 | You can install the latest release of VRPy from PyPi_ by: 8 | 9 | .. code-block:: none 10 | 11 | pip install vrpy 12 | 13 | .. _PyPi: https://pypi.python.org/pypi/vrpy 14 | 15 | Requirements 16 | ************ 17 | The requirements for running VRPy are: 18 | 19 | - cspy_: Constrained shortest path problem algorithms :cite:`cspy`. 20 | - NetworkX_: Graph manipulation and creation :cite:`hagberg2008exploring`. 21 | - numpy_: Array manipulation :cite:`numpy`. 22 | - PuLP_: Linear programming modeler. 23 | 24 | .. _cspy: https://pypi.org/project/cspy/ 25 | .. _NetworkX: https://networkx.github.io/documentation/stable/ 26 | .. _numpy: https://pypi.org/project/numpy/ 27 | .. _PuLP: https://pypi.org/project/PuLP/ -------------------------------------------------------------------------------- /docs/how_to.rst: -------------------------------------------------------------------------------- 1 | Using `VRPy` 2 | ============ 3 | 4 | In order to use the VRPy package, first, one has to create a directed graph which represents the underlying network. 5 | 6 | To do so, we make use of the well-known `NetworkX` package, with the following input requirements: 7 | 8 | - Input graphs must be of type :class:`networkx.DiGraph`; 9 | - Input graphs must have a single `Source` and `Sink` nodes with no incoming or outgoing edges respectively; 10 | - There must be at least one path from `Source` to `Sink`; 11 | - Edges in the input graph must have a ``cost`` attribute (of type :class:`float`). 12 | 13 | 14 | For example the following simple network fulfills the requirements listed above: 15 | 16 | .. code-block:: python 17 | 18 | >>> from networkx import DiGraph 19 | >>> G = DiGraph() 20 | >>> G.add_edge("Source", 1, cost=1) 21 | >>> G.add_edge("Source", 2, cost=2) 22 | >>> G.add_edge(1, "Sink", cost=0) 23 | >>> G.add_edge(2, "Sink", cost=2) 24 | >>> G.add_edge(1, 2, cost=1) 25 | >>> G.add_edge(2, 1, cost=1) 26 | 27 | The customer demands are set as ``demand`` attributes (of type :class:`float`) on each node: 28 | 29 | .. code-block:: python 30 | 31 | >>> G.nodes[1]["demand"] = 5 32 | >>> G.nodes[2]["demand"] = 4 33 | 34 | To solve your routing problem, create a :class:`VehicleRoutingProblem` instance, specify the problem constraints (e.g., the ``load_capacity`` of each truck), and call ``solve``. 35 | 36 | .. code-block:: python 37 | 38 | >>> from vrpy import VehicleRoutingProblem 39 | >>> prob = VehicleRoutingProblem(G, load_capacity=10) 40 | >>> prob.solve() 41 | 42 | Once the problem is solved, we can query useful attributes as: 43 | 44 | .. code-block:: python 45 | 46 | >>> prob.best_value 47 | 3 48 | >>> prob.best_routes 49 | {1: ["Source", 2, 1, "Sink"]} 50 | >>> prob.best_routes_load 51 | {1: 9} 52 | 53 | ``prob.best_value`` is the overall cost of the solution, ``prob.best_routes`` is a `dict` object where keys represent the route ID, while the values are 54 | the corresponding path from `Source` to `Sink`. And ``prob.best_routes_load`` is a `dict` object where the same keys point to the accumulated load on the 55 | vehicle. 56 | 57 | 58 | Different options and constraints are detailed in the :ref:`vrp` section, 59 | and other attributes can be queried depending on the nature of the VRP (see section :ref:`api`). 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/images/capacity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/capacity.png -------------------------------------------------------------------------------- /docs/images/cvrp_performance_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/cvrp_performance_profile.png -------------------------------------------------------------------------------- /docs/images/drop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/drop.png -------------------------------------------------------------------------------- /docs/images/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/network.png -------------------------------------------------------------------------------- /docs/images/nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/nodes.png -------------------------------------------------------------------------------- /docs/images/nodes_capacity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/nodes_capacity.png -------------------------------------------------------------------------------- /docs/images/nodes_time_windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/nodes_time_windows.png -------------------------------------------------------------------------------- /docs/images/pdp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/pdp.png -------------------------------------------------------------------------------- /docs/images/requests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/requests.png -------------------------------------------------------------------------------- /docs/images/sol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/sol.png -------------------------------------------------------------------------------- /docs/images/stops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/stops.png -------------------------------------------------------------------------------- /docs/images/time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/time.png -------------------------------------------------------------------------------- /docs/images/time_windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/docs/images/time_windows.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | VRPy Documentation 2 | ==================== 3 | 4 | VRPy is a python framework for solving instances of different types of Vehicle Routing Problems (VRP) including: 5 | 6 | - the Capacitated VRP (CVRP), 7 | - the CVRP with resource constraints, 8 | - the CVRP with time windows (CVRPTW), 9 | - the CVRP with simultaneous distribution and collection (CVRPSDC), 10 | - the CVRP with heterogeneous fleet (HFCVRP). 11 | 12 | Check out section :ref:`vrp` to find more variants and options. 13 | 14 | VRPy relies on the well known NetworkX_ package (graph manipulation), as well as on cspy_, a library for solving the resource constrained shortest path problem. 15 | 16 | .. _NetworkX: Graph manipulation and creation. 17 | .. _cspy: https://pypi.org/project/cspy/ 18 | 19 | Disclaimer 20 | ========== 21 | 22 | There is no guarantee that VRPy returns the optimal solution. See section :ref:`colgen` for more details, and section :ref:`benchmarks` 23 | for performance comparisons with OR-Tools_. 24 | 25 | .. _OR-Tools: https://developers.google.com/optimization/routing/vrp 26 | 27 | Authors 28 | ======= 29 | 30 | Romain Montagné (r.montagne@hotmail.fr) 31 | 32 | David Torres Sanchez (d.torressanchez@lancs.ac.uk) 33 | 34 | Contributors 35 | ============ 36 | 37 | @Halvaros 38 | 39 | Table of contents 40 | ================= 41 | 42 | .. toctree:: 43 | :maxdepth: 2 44 | :caption: User Guide 45 | 46 | getting_started 47 | how_to 48 | vrp_variants 49 | solving_options 50 | examples 51 | api 52 | mathematical_background 53 | benchmarks 54 | bibliography 55 | 56 | * :ref:`genindex` 57 | * :ref:`search` 58 | -------------------------------------------------------------------------------- /docs/mathematical_background.rst: -------------------------------------------------------------------------------- 1 | .. _colgen: 2 | 3 | Mathematical Background 4 | ======================= 5 | 6 | 7 | A column generation approach 8 | ---------------------------- 9 | 10 | *VRPy* solves vehicle routing problems with a column generation approach. The term `column generation` refers to the fact 11 | that iteratively, routes (or `columns`) are `generated` with a pricing problem, and fed to a master problem which selects the best routes among 12 | a pool such that each vertex is serviced exactly once. The linear formulations of these problems are detailed hereafter. 13 | 14 | Master Problem 15 | ************** 16 | Let :math:`G=(V,A)` be a graph where :math:`V` denotes the set of nodes that have to be visited, and :math:`A` the set of edges of the network. 17 | Let :math:`\Omega` be the set of feasible routes. 18 | Let :math:`\lambda_r` be a binary variable that takes value :math:`1` if and only if route :math:`r \in \Omega` with cost :math:`c_r` is selected. 19 | The master problem reads as follows: 20 | 21 | 22 | .. math:: 23 | 24 | \min \; \sum_{r \in \Omega} c_r \lambda_r 25 | 26 | subject to set covering constraints: 27 | 28 | .. math:: 29 | 30 | \sum_{r \in \Omega \mid v \in r} \lambda_r &= 1 \quad &\forall v \in V\quad &(1) 31 | 32 | \lambda_r &\in \{ 0,1\} \quad &\forall r \in \Omega \quad &(2) 33 | 34 | 35 | 36 | When using a column generation procedure, integrity constraints :math:`(2)` are relaxed (such that :math:`0 \le \lambda_r \le 1`), and only a subset of :math:`\Omega` is used. 37 | This subset is generated dynamically with the following sub problem. 38 | 39 | 40 | Pricing problem 41 | *************** 42 | 43 | Let :math:`\pi_v` denote the dual variable associated with constraints :math:`(1)`. The marginal cost of a variable (or column) :math:`\lambda_r` is given by: 44 | 45 | .. math:: 46 | 47 | \hat{c}_r = c_r - \sum_{v \in V\mid v \in r} \pi_v 48 | 49 | Therefore, if :math:`x_{uv}` is a binary variable that takes value :math:`1` if and only if edge :math:`(u,v)` is used, 50 | *assuming there are no negative cost sub cycles*, one can formulate the problem of finding a route with negative marginal cost as follows : 51 | 52 | .. math:: 53 | 54 | \min \quad \sum_{(u,v)\in A}c_{uv}x_{uv} -\sum_{u\mid (u,v) \in A}\pi_u x_{uv} 55 | 56 | subject to flow balance constraints : 57 | 58 | .. math:: 59 | 60 | \sum_{u\mid (u,v) \in A} x_{uv} &= \sum_{u\mid (v,u) \in A} x_{uv}\quad &\forall v \in V \label{eq3} 61 | 62 | x_{uv} &\in \{ 0,1\} \quad &\forall (u,v) \in A \label{eq4} 63 | 64 | 65 | In other words, the sub problem is a shortest elementary path problem, and additional constraints (such as capacities, time) 66 | give rise to a shortest path problem with *resource constraints*, hence the interest of using the *cspy* library. 67 | 68 | If there are negative cost cycles (which typically happens), the above formulation requires additional constraints 69 | to enforce path elementarity, and the problem becomes computationally intractable. 70 | Linear formulations are then impractical, and algorithms such as the ones available in *cspy* become very handy. 71 | 72 | 73 | Does VRPy return an optimal solution? 74 | ------------------------------------- 75 | 76 | *VRPy* does not necessarily return an optimal solution (even with no time limit). Indeed, once the pricing problems fails to find 77 | a route with negative marginal cost, the master problem is solved as a MIP. This *price-and-branch* strategy does not guarantee optimality. Note however that it 78 | can be shown :cite:`bramel1997solving` that asymptotically, the relative error goes to zero as the number of customers increases. 79 | To guarantee that an optimal solution is returned, the column generation procedure should be embedded in a branch-and-bound scheme (*branch-and-price*). This 80 | is part of the future work listed below. 81 | 82 | TO DO 83 | ----- 84 | 85 | - Embed the solving procedure in a branch-and-bound scheme: 86 | 87 | - branch-and-price (exact) 88 | - diving (heuristic) 89 | - Implement heuristics for initial solutions. 90 | - More acceleration strategies: 91 | 92 | - other heuristic pricing strategies 93 | - switch to other LP modeling library (?) 94 | - improve stabilization 95 | - ... 96 | - Include more VRP variants: 97 | 98 | - pickup and delivery with cspy 99 | - ... 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/refs.bib: -------------------------------------------------------------------------------- 1 | 2 | @Misc{cspy, 3 | author = {Torres Sanchez, David}, 4 | title = {{cspy : A Python package with a collection of algorithms for the (Resource) Constrained Shortest Path problem}}, 5 | year = {2019}, 6 | url = {\url{https://github.com/torressa/cspy}} 7 | } 8 | 9 | @techreport{hagberg2008exploring, 10 | title={Exploring network structure, dynamics, and function using NetworkX}, 11 | author={Hagberg, Aric and Swart, Pieter and S Chult, Daniel}, 12 | year={2008}, 13 | institution={Los Alamos National Lab.(LANL), Los Alamos, NM (United States)} 14 | } 15 | 16 | @Misc{numpy, 17 | author = {Travis Oliphant}, 18 | title = {{NumPy}: A guide to {NumPy}}, 19 | year = {2006}, 20 | howpublished = {USA: Trelgol Publishing}, 21 | url = "http://www.numpy.org/", 22 | note = {[Online; accessed 18/05/2020]} 23 | } 24 | 25 | @article{dell2006branch, 26 | title={A branch-and-price approach to the vehicle routing problem with simultaneous distribution and collection}, 27 | author={Dell’Amico, Mauro and Righini, Giovanni and Salani, Matteo}, 28 | journal={Transportation science}, 29 | volume={40}, 30 | number={2}, 31 | pages={235--247}, 32 | year={2006}, 33 | publisher={INFORMS} 34 | } 35 | 36 | @Misc{ortools, 37 | title = {{OR-Tools}}, 38 | version = {7.2}, 39 | author = {Laurent Perron and Vincent Furnon}, 40 | organization = {Google}, 41 | url = {https://developers.google.com/optimization/}, 42 | date = {2019-7-19} 43 | } 44 | 45 | @article{clarke1964scheduling, 46 | title={Scheduling of vehicles from a central depot to a number of delivery points}, 47 | author={Clarke, Geoff and Wright, John W}, 48 | journal={Operations research}, 49 | volume={12}, 50 | number={4}, 51 | pages={568--581}, 52 | year={1964}, 53 | publisher={Informs} 54 | } 55 | 56 | @article{forrest2018coin, 57 | title={coin-or/Cbc: Version 2.9. 9}, 58 | author={Forrest, J and Ralphs, T and Vigerske, S and Kristjansson, B and Lubin, M and Santos, H and Saltzman, M and others}, 59 | journal={DOI: https://doi. org/10.5281/zenodo}, 60 | volume={1317566}, 61 | year={2018} 62 | } 63 | 64 | @incollection{bramel1997solving, 65 | title={Solving the VRP using a Column Generation Approach}, 66 | author={Bramel, Julien and Simchi-Levi, David}, 67 | booktitle={The Logic of Logistics}, 68 | pages={125--141}, 69 | year={1997}, 70 | publisher={Springer} 71 | } 72 | 73 | @article{tilk2017asymmetry, 74 | title={Asymmetry matters: Dynamic half-way points in bidirectional labeling for solving shortest path problems with resource constraints faster}, 75 | author={Tilk, Christian and Rothenb{\"a}cher, Ann-Kathrin and Gschwind, Timo and Irnich, Stefan}, 76 | journal={European Journal of Operational Research}, 77 | volume={261}, 78 | number={2}, 79 | pages={530--539}, 80 | year={2017}, 81 | publisher={Elsevier} 82 | } 83 | 84 | @article{santini2018branch, 85 | title={A branch-and-price approach to the feeder network design problem}, 86 | author={Santini, Alberto and Plum, Christian EM and Ropke, Stefan}, 87 | journal={European Journal of Operational Research}, 88 | volume={264}, 89 | number={2}, 90 | pages={607--622}, 91 | year={2018}, 92 | publisher={Elsevier} 93 | } 94 | 95 | @inproceedings{sabar2015math, 96 | title={A math-hyper-heuristic approach for large-scale vehicle routing problems with time windows}, 97 | author={Sabar, Nasser R and Zhang, Xiuzhen Jenny and Song, Andy}, 98 | booktitle={2015 IEEE Congress on Evolutionary Computation (CEC)}, 99 | pages={830--837}, 100 | year={2015}, 101 | organization={IEEE} 102 | } 103 | 104 | @inproceedings{ferreira2017multi, 105 | title={A multi-armed bandit selection strategy for hyper-heuristics}, 106 | author={Ferreira, Alexandre Silvestre and Gon{\c{c}}alves, Richard Aderbal and Pozo, Aurora}, 107 | booktitle={2017 IEEE Congress on Evolutionary Computation (CEC)}, 108 | pages={525--532}, 109 | year={2017}, 110 | organization={IEEE} 111 | } 112 | 113 | @inproceedings{drake2012improved, 114 | title={An improved choice function heuristic selection for cross domain heuristic search}, 115 | author={Drake, John H and {\"O}zcan, Ender and Burke, Edmund K}, 116 | booktitle={International Conference on Parallel Problem Solving from Nature}, 117 | pages={307--316}, 118 | year={2012}, 119 | organization={Springer} 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | ###### Requirements without Version Specifiers ###### 2 | cspy 3 | networkx 4 | pulp 5 | sphinx_copybutton 6 | ###### Requirements with Version Specifiers ###### 7 | 8 | sphinxcontrib-bibtex<2.0.0 -------------------------------------------------------------------------------- /docs/solving_options.rst: -------------------------------------------------------------------------------- 1 | .. _options: 2 | 3 | Solving Options 4 | =============== 5 | 6 | Setting initial routes for a search 7 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | By default, an initial solution is computed with the well known Clarke and Wright algorithm :cite:`clarke1964scheduling`. If one already has a feasible solution at hand, 10 | it is possible to use it as an initial solution for the search of a potential better configuration. The solution is passed to the solver as a list of routes, where a route is a list 11 | of nodes starting from the *Source* and ending at the *Sink*. 12 | 13 | .. code-block:: python 14 | 15 | >>> prob.solve(initial_solution = [["Source",1,"Sink"],["Source",2,"Sink"]]) 16 | 17 | Returning solution from initial heuristic 18 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 19 | 20 | It is possible to return the solution found by the Clarke and Wright algorithm by setting the ``heuristic_only`` argument to *True*. 21 | 22 | .. code-block:: python 23 | 24 | >>> prob.solve(heuristic_only=True) 25 | 26 | Note that this only possible with capacity and/or resource constraints. 27 | 28 | Locking routes 29 | ~~~~~~~~~~~~~~ 30 | 31 | It is possible to constrain the problem with partial routes if preassignments are known. There are two possibilites : either a complete route is known, 32 | and it should not be optimized, either only a partial route is known, and it may be extended. Such routes are given to the solver 33 | with the ``preassignments`` argument. A route with `Source` and `Sink` nodes is considered complete and is locked. Otherwise, the solver will extend it if it yields savings. 34 | 35 | In the following example, one route must start with customer :math:`1`, one route must contain edge :math:`(4,5)`, and one complete route, 36 | `Source-2-3-Sink`, is locked. 37 | 38 | .. code-block:: python 39 | 40 | >>> prob.solve(preassignments = [["Source",1],[4,5],["Source",2,3,"Sink"]]) 41 | 42 | 43 | Setting a time limit 44 | ~~~~~~~~~~~~~~~~~~~~ 45 | 46 | The ``time_limit`` argument can be used to set a time limit, in seconds. 47 | The solver will return the best solution found after the time limit has elapsed. 48 | 49 | For example, for a one minute time limit: 50 | 51 | .. code-block:: python 52 | 53 | >>> prob.solve(time_limit=60) 54 | 55 | 56 | Linear programming or dynamic programming 57 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | 59 | `VRPy`'s ``solve`` method relies on a column generation procedure. At every iteration, a master problem and a sub problem are solved. 60 | The sub problem consists in finding variables which are likely to improve the master problem's objective function. 61 | See section :ref:`colgen` for more details. 62 | 63 | The sub problem - or pricing problem - can be solved either with linear programming, or with dynamic programming. Switching to linear 64 | programming can be done by deactivating the ``cspy`` argument when calling the ``solve`` method. 65 | In this case the CBC_ :cite:`forrest2018coin` solver of COIN-OR is used by default. 66 | 67 | .. code-block:: python 68 | 69 | >>> prob.solve(cspy=False) 70 | 71 | The sub problems that are solved are typically computationally intractable, and using dynamic programming is typically quicker, as such algorithms run in pseudo-polynomial time. 72 | However, solving the sub problems as MIPs may also be effective depending on the data set. Also, using commercial solvers may significantly help accelerating the procedure. 73 | If one has CPLEX or GUROBI at hand, they can be used by setting the ``solver`` parameter to "cplex" or "gurobi". 74 | 75 | .. code-block:: python 76 | 77 | >>> prob.solve(cspy=False, solver="gurobi") 78 | 79 | .. _CBC : https://github.com/coin-or/Cbc 80 | 81 | Pricing strategy 82 | ~~~~~~~~~~~~~~~~ 83 | 84 | In theory, at each iteration, the sub problem is solved optimally. VRPy does so with a bidirectional labeling algorithm with dynamic halfway point :cite:`tilk2017asymmetry` from the `cspy` library. 85 | 86 | This may result in a slow convergence. To speed up the resolution, there are two ways to change this pricing strategy: 87 | 88 | 1. By deactivating the ``exact`` argument of the ``solve`` method, `cspy` calls one of its heuristics instead of the bidirectional search algorithm. The exact method is run only once the heuristic fails to find a column with negative reduced cost. 89 | 90 | .. code-block:: python 91 | 92 | >>> prob.solve(exact=False) 93 | 94 | 95 | 2. By modifying the ``pricing_strategy`` argument of the ``solve`` method to one of the following: 96 | 97 | - `BestEdges1`, 98 | - `BestEdges2`, 99 | - `BestPaths`, 100 | - `Hyper` 101 | 102 | 103 | .. code-block:: python 104 | 105 | >>> prob.solve(pricing_strategy="BestEdges1") 106 | 107 | `BestEdges1`, described for example in :cite:`dell2006branch`, is a sparsification strategy: a subset of nodes and 108 | edges are removed to limit the search space. The subgraph is created as follows: all edges :math:`(i,j)` which verify :math:`c_{ij} > \alpha \; \pi_{max}` are discarded, where :math:`c_{ij}` is the edge's cost, :math:`\alpha \in ]0,1[` is parameter, 109 | and :math:`\pi_{max}` is the largest dual value returned by the current restricted relaxed master problem. The parameter :math:`\alpha` is increased iteratively until 110 | a route is found. `BestEdges2` is another sparsification strategy, described for example in :cite:`santini2018branch`. The :math:`\beta` edges with highest reduced cost are discarded, where :math:`\beta` is a parameter that is increased iteratively. 111 | As for `BestPaths`, the idea is to look for routes in the subgraph induced by the :math:`k` shortest paths from the Source to the Sink (without any resource constraints), 112 | where :math:`k` is a parameter that is increased iteratively. 113 | 114 | Additionally, we have an experimental feature that uses Hyper-Heuristics for the dynamic selection of pricing strategies. 115 | The approach ranks the best pricing strategies as the algorithm is running and chooses according to selection functions based on :cite:`sabar2015math,ferreira2017multi`. 116 | The selection criteria has been modified to include a combination of runtime, objective improvement, and currently active columns in the restricted master. Adaptive parameter settings found in :cite:`drake2012improved` is used to balance exploration and exploitation under stagnation. The main advantage is that selection is done as the programme runs, and is therefore more flexible compared to a predefined pricing strategy. 117 | 118 | For each of these heuristic pricing strategies, if a route with negative reduced cost is found, it is fed to the master problem. Otherwise, 119 | the sub problem is solved exactly. 120 | 121 | The default pricing strategy is `BestEdges1`, with ``exact=True`` (i.e., with the bidirectional labeling algorithm). 122 | 123 | A greedy randomized heuristic 124 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 125 | 126 | For the CVRP, or the CVRP with resource constraints, one can activate the option of running a greedy randomized heuristic before pricing: 127 | 128 | .. code-block:: python 129 | 130 | >>> prob.solve(greedy="True") 131 | 132 | This algorithm, described in :cite:`santini2018branch`, generates a path starting at the *Source* node and then randomly selects an edge among the :math:`\gamma` outgoing edges 133 | of least reduced cost that do not close a cycle and that meet operational constraints (:math:`\gamma` is a parameter). 134 | This is repeated until the *Sink* node is reached . The same procedure is applied backwards, starting from the *Sink* and ending at the *Source*, and is run 135 | :math:`20` times. All paths with negative reduced cost are added to the pool of columns. 136 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | - [`cvrp.py`](cvrp.py) - Capacitated vehicle routing 4 | - [`cvrp_drop.py`](cvrp_drop.py) - Capacitated vehicle routing with dropping penalty 5 | - [`cvrpsdc.py`](cvrpsdc.py) - Capacitated vehicle routing with distribution and collection 6 | - [`pdp.py`](pdp.py) - Capacitated vehicle routing with pickup and delivery 7 | - [`vrptw.py`](vrptw.py) - Vehicle routing with time windows 8 | 9 | ## Run 10 | 11 | To run an example, from the main repo folder do 12 | 13 | ```bash 14 | python3 -m examples. 15 | ``` 16 | 17 | Where `` is the file name (without the extension) above. 18 | For example 19 | 20 | ```bash 21 | python3 -m examples.cvrp 22 | ``` 23 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/examples/__init__.py -------------------------------------------------------------------------------- /examples/cvrp.py: -------------------------------------------------------------------------------- 1 | from networkx import from_numpy_matrix, set_node_attributes, relabel_nodes, DiGraph 2 | from numpy import array 3 | from examples.data import DISTANCES, DEMANDS 4 | 5 | from vrpy import VehicleRoutingProblem 6 | 7 | # Transform distance matrix to DiGraph 8 | A = array(DISTANCES, dtype=[("cost", int)]) 9 | G = from_numpy_matrix(A, create_using=DiGraph()) 10 | 11 | # Set demands 12 | set_node_attributes(G, values=DEMANDS, name="demand") 13 | 14 | # Relabel depot 15 | G = relabel_nodes(G, {0: "Source", 17: "Sink"}) 16 | 17 | if __name__ == "__main__": 18 | 19 | prob = VehicleRoutingProblem(G, load_capacity=15) 20 | prob.solve() 21 | print(prob.best_value) 22 | print(prob.best_routes) 23 | assert prob.best_value == 6208 24 | -------------------------------------------------------------------------------- /examples/cvrp_drop.py: -------------------------------------------------------------------------------- 1 | from networkx import from_numpy_matrix, set_node_attributes, relabel_nodes, DiGraph 2 | from numpy import array 3 | 4 | from examples.data import DISTANCES, DEMANDS_DROP 5 | 6 | from vrpy import VehicleRoutingProblem 7 | 8 | # Transform distance matrix to DiGraph 9 | A = array(DISTANCES, dtype=[("cost", int)]) 10 | G = from_numpy_matrix(A, create_using=DiGraph()) 11 | 12 | # Set demands 13 | set_node_attributes(G, values=DEMANDS_DROP, name="demand") 14 | 15 | # Relabel depot 16 | G = relabel_nodes(G, {0: "Source", 17: "Sink"}) 17 | 18 | if __name__ == "__main__": 19 | 20 | prob = VehicleRoutingProblem(G, load_capacity=15, drop_penalty=1000, num_vehicles=4) 21 | prob.solve( 22 | preassignments=[ # locking these routes should yield prob.best_value == 7936 23 | # [9, 14, 16], 24 | # [12, 11, 4, 3, 1], 25 | # [7, 13], 26 | # [8, 10, 2, 5], 27 | ], 28 | ) 29 | print(prob.best_value) 30 | print(prob.best_routes) 31 | print(prob.best_routes_cost) 32 | print(prob.best_routes_load) 33 | print(prob.node_load) 34 | assert prob.best_value == 8096 35 | 36 | # why doesn't vrpy find 7936 ? 37 | -------------------------------------------------------------------------------- /examples/cvrpsdc.py: -------------------------------------------------------------------------------- 1 | from networkx import from_numpy_matrix, set_node_attributes, relabel_nodes, DiGraph 2 | from numpy import array 3 | 4 | from examples.data import DISTANCES, DEMANDS, COLLECT 5 | 6 | from vrpy import VehicleRoutingProblem 7 | 8 | # Transform distance matrix to DiGraph 9 | A = array(DISTANCES, dtype=[("cost", int)]) 10 | G = from_numpy_matrix(A, create_using=DiGraph()) 11 | 12 | # Set demand and collect volumes 13 | set_node_attributes(G, values=DEMANDS, name="demand") 14 | set_node_attributes(G, values=COLLECT, name="collect") 15 | 16 | # Relabel depot 17 | G = relabel_nodes(G, {0: "Source", 17: "Sink"}) 18 | 19 | if __name__ == "__main__": 20 | 21 | prob = VehicleRoutingProblem( 22 | G, 23 | load_capacity=20, 24 | distribution_collection=True, 25 | ) 26 | prob.solve() 27 | print(prob.best_value) 28 | print(prob.best_routes) 29 | print(prob.node_load) 30 | print(sum(prob.node_load[r]["Sink"] for r in prob.best_routes)) 31 | assert prob.best_value == 5912 32 | -------------------------------------------------------------------------------- /examples/data.py: -------------------------------------------------------------------------------- 1 | DISTANCES = [ 2 | [ 3 | 0, 4 | 548, 5 | 776, 6 | 696, 7 | 582, 8 | 274, 9 | 502, 10 | 194, 11 | 308, 12 | 194, 13 | 536, 14 | 502, 15 | 388, 16 | 354, 17 | 468, 18 | 776, 19 | 662, 20 | 0, 21 | ], 22 | [ 23 | 0, 24 | 0, 25 | 684, 26 | 308, 27 | 194, 28 | 502, 29 | 730, 30 | 354, 31 | 696, 32 | 742, 33 | 1084, 34 | 594, 35 | 480, 36 | 674, 37 | 1016, 38 | 868, 39 | 1210, 40 | 548, 41 | ], 42 | [ 43 | 0, 44 | 684, 45 | 0, 46 | 992, 47 | 878, 48 | 502, 49 | 274, 50 | 810, 51 | 468, 52 | 742, 53 | 400, 54 | 1278, 55 | 1164, 56 | 1130, 57 | 788, 58 | 1552, 59 | 754, 60 | 776, 61 | ], 62 | [ 63 | 0, 64 | 308, 65 | 992, 66 | 0, 67 | 114, 68 | 650, 69 | 878, 70 | 502, 71 | 844, 72 | 890, 73 | 1232, 74 | 514, 75 | 628, 76 | 822, 77 | 1164, 78 | 560, 79 | 1358, 80 | 696, 81 | ], 82 | [ 83 | 0, 84 | 194, 85 | 878, 86 | 114, 87 | 0, 88 | 536, 89 | 764, 90 | 388, 91 | 730, 92 | 776, 93 | 1118, 94 | 400, 95 | 514, 96 | 708, 97 | 1050, 98 | 674, 99 | 1244, 100 | 582, 101 | ], 102 | [ 103 | 0, 104 | 502, 105 | 502, 106 | 650, 107 | 536, 108 | 0, 109 | 228, 110 | 308, 111 | 194, 112 | 240, 113 | 582, 114 | 776, 115 | 662, 116 | 628, 117 | 514, 118 | 1050, 119 | 708, 120 | 274, 121 | ], 122 | [ 123 | 0, 124 | 730, 125 | 274, 126 | 878, 127 | 764, 128 | 228, 129 | 0, 130 | 536, 131 | 194, 132 | 468, 133 | 354, 134 | 1004, 135 | 890, 136 | 856, 137 | 514, 138 | 1278, 139 | 480, 140 | 502, 141 | ], 142 | [ 143 | 0, 144 | 354, 145 | 810, 146 | 502, 147 | 388, 148 | 308, 149 | 536, 150 | 0, 151 | 342, 152 | 388, 153 | 730, 154 | 468, 155 | 354, 156 | 320, 157 | 662, 158 | 742, 159 | 856, 160 | 194, 161 | ], 162 | [ 163 | 0, 164 | 696, 165 | 468, 166 | 844, 167 | 730, 168 | 194, 169 | 194, 170 | 342, 171 | 0, 172 | 274, 173 | 388, 174 | 810, 175 | 696, 176 | 662, 177 | 320, 178 | 1084, 179 | 514, 180 | 308, 181 | ], 182 | [ 183 | 0, 184 | 742, 185 | 742, 186 | 890, 187 | 776, 188 | 240, 189 | 468, 190 | 388, 191 | 274, 192 | 0, 193 | 342, 194 | 536, 195 | 422, 196 | 388, 197 | 274, 198 | 810, 199 | 468, 200 | 194, 201 | ], 202 | [ 203 | 0, 204 | 1084, 205 | 400, 206 | 1232, 207 | 1118, 208 | 582, 209 | 354, 210 | 730, 211 | 388, 212 | 342, 213 | 0, 214 | 878, 215 | 764, 216 | 730, 217 | 388, 218 | 1152, 219 | 354, 220 | 536, 221 | ], 222 | [ 223 | 0, 224 | 594, 225 | 1278, 226 | 514, 227 | 400, 228 | 776, 229 | 1004, 230 | 468, 231 | 810, 232 | 536, 233 | 878, 234 | 0, 235 | 114, 236 | 308, 237 | 650, 238 | 274, 239 | 844, 240 | 502, 241 | ], 242 | [ 243 | 0, 244 | 480, 245 | 1164, 246 | 628, 247 | 514, 248 | 662, 249 | 890, 250 | 354, 251 | 696, 252 | 422, 253 | 764, 254 | 114, 255 | 0, 256 | 194, 257 | 536, 258 | 388, 259 | 730, 260 | 388, 261 | ], 262 | [ 263 | 0, 264 | 674, 265 | 1130, 266 | 822, 267 | 708, 268 | 628, 269 | 856, 270 | 320, 271 | 662, 272 | 388, 273 | 730, 274 | 308, 275 | 194, 276 | 0, 277 | 342, 278 | 422, 279 | 536, 280 | 354, 281 | ], 282 | [ 283 | 0, 284 | 1016, 285 | 788, 286 | 1164, 287 | 1050, 288 | 514, 289 | 514, 290 | 662, 291 | 320, 292 | 274, 293 | 388, 294 | 650, 295 | 536, 296 | 342, 297 | 0, 298 | 764, 299 | 194, 300 | 468, 301 | ], 302 | [ 303 | 0, 304 | 868, 305 | 1552, 306 | 560, 307 | 674, 308 | 1050, 309 | 1278, 310 | 742, 311 | 1084, 312 | 810, 313 | 1152, 314 | 274, 315 | 388, 316 | 422, 317 | 764, 318 | 0, 319 | 798, 320 | 776, 321 | ], 322 | [ 323 | 0, 324 | 1210, 325 | 754, 326 | 1358, 327 | 1244, 328 | 708, 329 | 480, 330 | 856, 331 | 514, 332 | 468, 333 | 354, 334 | 844, 335 | 730, 336 | 536, 337 | 194, 338 | 798, 339 | 0, 340 | 662, 341 | ], 342 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 343 | ] 344 | 345 | TRAVEL_TIMES = [ 346 | [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7, 0], # from source 347 | [0, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14, 6], 348 | [0, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9, 9], 349 | [0, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16, 8], 350 | [0, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14, 7], 351 | [0, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8, 3], 352 | [0, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5, 6], 353 | [0, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10, 2], 354 | [0, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6, 3], 355 | [0, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5, 2], 356 | [0, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4, 6], 357 | [0, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10, 6], 358 | [0, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8, 4], 359 | [0, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6, 4], 360 | [0, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2, 5], 361 | [0, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9, 9], 362 | [0, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0, 7], 363 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # from sink 364 | ] 365 | 366 | DEMANDS = { 367 | 0: 0, 368 | 1: 1, 369 | 2: 1, 370 | 3: 2, 371 | 4: 4, 372 | 5: 2, 373 | 6: 4, 374 | 7: 8, 375 | 8: 8, 376 | 9: 1, 377 | 10: 2, 378 | 11: 1, 379 | 12: 2, 380 | 13: 4, 381 | 14: 4, 382 | 15: 8, 383 | 16: 8, 384 | 17: 0, 385 | } 386 | 387 | COLLECT = { 388 | 1: 1, 389 | 2: 1, 390 | 3: 1, 391 | 4: 1, 392 | 5: 2, 393 | 6: 1, 394 | 7: 4, 395 | 8: 1, 396 | 9: 1, 397 | 10: 2, 398 | 11: 3, 399 | 12: 2, 400 | 13: 4, 401 | 14: 2, 402 | 15: 1, 403 | 16: 2, 404 | } 405 | 406 | DEMANDS_DROP = { 407 | 0: 0, 408 | 1: 1, 409 | 2: 1, 410 | 3: 3, 411 | 4: 6, 412 | 5: 3, 413 | 6: 6, 414 | 7: 8, 415 | 8: 8, 416 | 9: 1, 417 | 10: 2, 418 | 11: 1, 419 | 12: 2, 420 | 13: 6, 421 | 14: 6, 422 | 15: 8, 423 | 16: 8, 424 | } 425 | 426 | TIME_WINDOWS_LOWER = { 427 | # 0: 0, 428 | 1: 7, 429 | 2: 10, 430 | 3: 16, 431 | 4: 10, 432 | 5: 0, 433 | 6: 5, 434 | 7: 0, 435 | 8: 5, 436 | 9: 0, 437 | 10: 10, 438 | 11: 10, 439 | 12: 0, 440 | 13: 5, 441 | 14: 7, 442 | 15: 10, 443 | 16: 11, 444 | # 17: 0, 445 | } 446 | 447 | TIME_WINDOWS_UPPER = { 448 | 0: 25, 449 | 1: 12, 450 | 2: 15, 451 | 3: 18, 452 | 4: 13, 453 | 5: 5, 454 | 6: 10, 455 | 7: 4, 456 | 8: 10, 457 | 9: 3, 458 | 10: 16, 459 | 11: 15, 460 | 12: 5, 461 | 13: 10, 462 | 14: 8, 463 | 15: 15, 464 | 16: 15, 465 | 17: 25, 466 | } 467 | 468 | PICKUPS_DELIVERIES = { 469 | (1, 6): 1, 470 | (2, 10): 2, 471 | (4, 3): 3, 472 | (5, 9): 1, 473 | (7, 8): 2, 474 | (15, 11): 3, 475 | (13, 12): 1, 476 | (16, 14): 4, 477 | } 478 | -------------------------------------------------------------------------------- /examples/pdp.py: -------------------------------------------------------------------------------- 1 | from networkx import from_numpy_matrix, relabel_nodes, DiGraph 2 | from numpy import array 3 | 4 | from examples.data import DISTANCES, PICKUPS_DELIVERIES 5 | 6 | from vrpy import VehicleRoutingProblem 7 | 8 | # Transform distance matrix to DiGraph 9 | A = array(DISTANCES, dtype=[("cost", int)]) 10 | G = from_numpy_matrix(A, create_using=DiGraph()) 11 | 12 | # Set demands and requests 13 | for (u, v) in PICKUPS_DELIVERIES: 14 | G.nodes[u]["request"] = v 15 | G.nodes[u]["demand"] = PICKUPS_DELIVERIES[(u, v)] 16 | G.nodes[v]["demand"] = -PICKUPS_DELIVERIES[(u, v)] 17 | 18 | # Relabel depot 19 | G = relabel_nodes(G, {0: "Source", 17: "Sink"}) 20 | 21 | if __name__ == "__main__": 22 | 23 | prob = VehicleRoutingProblem(G, load_capacity=6, pickup_delivery=True, num_stops=6) 24 | prob.solve(cspy=False, pricing_strategy="Exact") 25 | print(prob.best_value) 26 | print(prob.best_routes) 27 | for (u, v) in PICKUPS_DELIVERIES: 28 | found = False 29 | for route in prob.best_routes.values(): 30 | if u in route and v in route: 31 | found = True 32 | break 33 | if not found: 34 | print((u, v), "Not present") 35 | assert False 36 | 37 | print(prob.node_load) 38 | assert prob.best_value == 5980 39 | -------------------------------------------------------------------------------- /examples/vrptw.py: -------------------------------------------------------------------------------- 1 | from networkx import ( 2 | from_numpy_matrix, 3 | set_node_attributes, 4 | relabel_nodes, 5 | DiGraph, 6 | compose, 7 | ) 8 | from numpy import array 9 | 10 | from examples.data import ( 11 | DISTANCES, 12 | TRAVEL_TIMES, 13 | TIME_WINDOWS_LOWER, 14 | TIME_WINDOWS_UPPER, 15 | ) 16 | 17 | from vrpy import VehicleRoutingProblem 18 | 19 | # Transform distance matrix to DiGraph 20 | A = array(DISTANCES, dtype=[("cost", int)]) 21 | G_d = from_numpy_matrix(A, create_using=DiGraph()) 22 | 23 | # Transform time matrix to DiGraph 24 | A = array(TRAVEL_TIMES, dtype=[("time", int)]) 25 | G_t = from_numpy_matrix(A, create_using=DiGraph()) 26 | 27 | # Merge 28 | G = compose(G_d, G_t) 29 | 30 | # Set time windows 31 | set_node_attributes(G, values=TIME_WINDOWS_LOWER, name="lower") 32 | set_node_attributes(G, values=TIME_WINDOWS_UPPER, name="upper") 33 | 34 | # Relabel depot 35 | G = relabel_nodes(G, {0: "Source", 17: "Sink"}) 36 | 37 | if __name__ == "__main__": 38 | 39 | prob = VehicleRoutingProblem(G, time_windows=True) 40 | prob.solve() 41 | print(prob.best_value) 42 | print(prob.best_routes) 43 | print(prob.arrival_time) 44 | assert prob.best_value == 6528 45 | -------------------------------------------------------------------------------- /paper/colgen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/paper/colgen.png -------------------------------------------------------------------------------- /paper/cvrp_performance_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kuifje02/vrpy/ff325cbe64c08d53969fc54b7ff9f7a9b2fa1d63/paper/cvrp_performance_profile.png -------------------------------------------------------------------------------- /paper/paper.bib: -------------------------------------------------------------------------------- 1 | @article{santini2018branch, 2 | title={A branch-and-price approach to the feeder network design problem}, 3 | author={Santini, Alberto and Plum, Christian EM and Ropke, Stefan}, 4 | journal={European Journal of Operational Research}, 5 | volume={264}, 6 | number={2}, 7 | pages={607--622}, 8 | year={2018}, 9 | publisher={Elsevier}, 10 | doi={10.1016/j.ejor.2017.06.063} 11 | } 12 | 13 | @article{costa2019exact, 14 | title={Exact branch-price-and-cut algorithms for vehicle routing}, 15 | author={Costa, Luciano and Contardo, Claudio and Desaulniers, Guy}, 16 | journal={Transportation Science}, 17 | volume={53}, 18 | number={4}, 19 | pages={946--985}, 20 | year={2019}, 21 | publisher={INFORMS}, 22 | doi = {10.1287/trsc.2018.0878} 23 | } 24 | 25 | @article{cspy, 26 | doi = {10.21105/joss.01655}, 27 | url = {https://doi.org/10.21105/joss.01655}, 28 | year = {2020}, 29 | publisher = {The Open Journal}, 30 | volume = {5}, 31 | number = {49}, 32 | pages = {1655}, 33 | author = {{Torres Sanchez}, David}, 34 | title = {cspy: A Python package with a collection of algorithms for the (Resource) Constrained Shortest Path problem}, 35 | journal = {Journal of Open Source Software} 36 | } 37 | 38 | @techreport{hagberg2008exploring, 39 | title={Exploring network structure, dynamics, and function using NetworkX}, 40 | author={Hagberg, Aric and Swart, Pieter and S Chult, Daniel}, 41 | year={2008}, 42 | institution={Los Alamos National Lab.(LANL), Los Alamos, NM (United States)}, 43 | url = {https://conference.scipy.org/proceedings/scipy2008/paper_2/full_text.pdf} 44 | } 45 | 46 | @Misc{numpy, 47 | author = {Travis Oliphant}, 48 | title = {{NumPy}: A guide to {NumPy}}, 49 | year = {2006}, 50 | howpublished = {USA: Trelgol Publishing}, 51 | url = "http://www.numpy.org/", 52 | note = {[Online; accessed 18/05/2020]} 53 | } 54 | 55 | @article{dell2006branch, 56 | title={A branch-and-price approach to the vehicle routing problem with simultaneous distribution and collection}, 57 | author={Dell’Amico, Mauro and Righini, Giovanni and Salani, Matteo}, 58 | journal={Transportation science}, 59 | volume={40}, 60 | number={2}, 61 | pages={235--247}, 62 | year={2006}, 63 | publisher={INFORMS}, 64 | doi = {10.1287/trsc.1050.0118} 65 | } 66 | 67 | @Misc{ortools, 68 | title = {{OR-Tools}}, 69 | version = {7.2}, 70 | author = {Laurent Perron and Vincent Furnon}, 71 | organization = {Google}, 72 | url = {https://developers.google.com/optimization/}, 73 | date = {2019-7-19} 74 | } 75 | 76 | @article{clarke1964scheduling, 77 | title={Scheduling of vehicles from a central depot to a number of delivery points}, 78 | author={Clarke, Geoff and Wright, John W}, 79 | journal={Operations research}, 80 | volume={12}, 81 | number={4}, 82 | pages={568--581}, 83 | year={1964}, 84 | publisher={INFORMS}, 85 | doi = {10.1287/opre.12.4.568} 86 | } 87 | 88 | @software{johnjforrest_2020_Cbc, 89 | author = {John J Forrest and 90 | Stefan Vigerske and 91 | Haroldo Gambini Santos and 92 | Ted Ralphs and 93 | Lou Hafer and 94 | Bjarni Kristjansson and 95 | J P Fasano and 96 | EdwinStraver and 97 | Miles Lubin and 98 | R Lougee and 99 | J P Goncall and 100 | h-i-gassmann and 101 | Matthew Saltzman}, 102 | title = {coin-or/Cbc: Version 2.10.5}, 103 | month = mar, 104 | year = 2020, 105 | publisher = {Zenodo}, 106 | version = {releases/2.10.5}, 107 | doi = {10.5281/zenodo.3700700}, 108 | url = {https://doi.org/10.5281/zenodo.3700700} 109 | } 110 | 111 | @software{johnjforrest_2020_clp, 112 | author = {John J Forrest and 113 | Stefan Vigerske and 114 | Ted Ralphs and 115 | Lou Hafer and 116 | jpfasano and 117 | Haroldo Gambini Santos and 118 | Matthew Saltzman and 119 | h-i-gassmann and 120 | Bjarni Kristjansson and 121 | Alan King}, 122 | title = {coin-or/Clp: Version 1.17.6}, 123 | month = apr, 124 | year = 2020, 125 | publisher = {Zenodo}, 126 | version = {releases/1.17.6}, 127 | doi = {10.5281/zenodo.3748677}, 128 | url = {https://doi.org/10.5281/zenodo.3748677} 129 | } 130 | 131 | @incollection{bramel1997solving, 132 | title={Solving the VRP using a Column Generation Approach}, 133 | author={Bramel, Julien and Simchi-Levi, David}, 134 | booktitle={The Logic of Logistics}, 135 | pages={125--141}, 136 | year={1997}, 137 | publisher={Springer}, 138 | doi = {10.1007/0-387-22619-2_16} 139 | } 140 | 141 | @article{dantzig1959truck, 142 | title={The truck dispatching problem}, 143 | author={Dantzig, George B and Ramser, John H}, 144 | journal={Management science}, 145 | volume={6}, 146 | number={1}, 147 | pages={80--91}, 148 | year={1959}, 149 | publisher={INFORMS}, 150 | doi = {10.1287/mnsc.6.1.80} 151 | } 152 | 153 | @incollection{desrosiers1988shortest, 154 | title={The shortest path problem for the construction of vehicle routes with pick-up, delivery and time constraints}, 155 | author={Desrosiers, Jacques and Dumas, Yvan}, 156 | booktitle={Advances in Optimization and Control}, 157 | pages={144--157}, 158 | year={1988}, 159 | publisher={Springer}, 160 | doi = {10.1007/978-3-642-46629-8_10} 161 | } 162 | 163 | @article{choi2007column, 164 | title={A column generation approach to the heterogeneous fleet vehicle routing problem}, 165 | author={Choi, Eunjeong and Tcha, Dong-Wan}, 166 | journal={Computers \& Operations Research}, 167 | volume={34}, 168 | number={7}, 169 | pages={2080--2095}, 170 | year={2007}, 171 | publisher={Elsevier}, 172 | doi = {10.1016/j.cor.2005.08.002} 173 | } 174 | 175 | @book{cordeau2000vrp, 176 | title={The VRP with time windows}, 177 | author={Cordeau, Jean-Francois and Groupe d'{\'e}tudes et de recherche en analyse des d{\'e}cisions (Montr{\'e}al Qu{\'e}bec)}, 178 | year={2000}, 179 | publisher={Groupe d'{\'e}tudes et de recherche en analyse des d{\'e}cisions Montr{\'e}al}, 180 | url={https://pdfs.semanticscholar.org/3aaa/a16ab53cf30c378fdb7c911fe0de39ee8997.pdf} 181 | } 182 | 183 | @article{laporte1985optimal, 184 | title={Optimal routing under capacity and distance restrictions}, 185 | author={Laporte, Gilbert and Nobert, Yves and Desrochers, Martin}, 186 | journal={Operations research}, 187 | volume={33}, 188 | number={5}, 189 | pages={1050--1073}, 190 | year={1985}, 191 | publisher={INFORMS}, 192 | doi={10.1287/opre.33.5.1050} 193 | } 194 | 195 | @article{baldacci2010exact, 196 | title={Exact algorithms for routing problems under vehicle capacity constraints}, 197 | author={Baldacci, Roberto and Toth, Paolo and Vigo, Daniele}, 198 | journal={Annals of Operations Research}, 199 | volume={175}, 200 | number={1}, 201 | pages={213--245}, 202 | year={2010}, 203 | publisher={Springer}, 204 | doi = {10.1007/s10479-009-0650-0} 205 | } 206 | 207 | @article{laporte2007you, 208 | title={What you should know about the vehicle routing problem}, 209 | author={Laporte, Gilbert}, 210 | journal={Naval Research Logistics (NRL)}, 211 | volume={54}, 212 | number={8}, 213 | pages={811--819}, 214 | year={2007}, 215 | publisher={Wiley Online Library}, 216 | doi = {10.1002/nav.20261} 217 | } 218 | 219 | @article{sadykov2019primal, 220 | title={Primal heuristics for branch and price: The assets of diving methods}, 221 | author={Sadykov, Ruslan and Vanderbeck, Fran{\c{c}}ois and Pessoa, Artur and Tahiri, Issam and Uchoa, Eduardo}, 222 | journal={INFORMS Journal on Computing}, 223 | volume={31}, 224 | number={2}, 225 | pages={251--267}, 226 | year={2019}, 227 | publisher={INFORMS}, 228 | doi = {10.1287/ijoc.2018.0822} 229 | } 230 | 231 | @article{solomon1987algorithms, 232 | title={Algorithms for the vehicle routing and scheduling problems with time window constraints}, 233 | author={Solomon, Marius M}, 234 | journal={Operations research}, 235 | volume={35}, 236 | number={2}, 237 | pages={254--265}, 238 | year={1987}, 239 | publisher={INFORMS}, 240 | doi = {10.1287/opre.35.2.254} 241 | } 242 | 243 | @article{pessoa2018automation, 244 | title={Automation and combination of linear-programming based stabilization techniques in column generation}, 245 | author={Pessoa, Artur and Sadykov, Ruslan and Uchoa, Eduardo and Vanderbeck, Fran{\c{c}}ois}, 246 | journal={INFORMS Journal on Computing}, 247 | volume={30}, 248 | number={2}, 249 | pages={339--360}, 250 | year={2018}, 251 | publisher={INFORMS}, 252 | doi = {10.1287/ijoc.2017.0784} 253 | } 254 | 255 | 256 | @phdthesis{augerat1995approche, 257 | title={Approche poly{\`e}drale du probl{\`e}me de tourn{\'e}es de v{\'e}hicules}, 258 | author={Augerat, Philippe}, 259 | year={1995}, 260 | school={Institut National Polytechnique de Grenoble-INPG} 261 | } 262 | 263 | @inproceedings{sabar2015math, 264 | title={A math-hyper-heuristic approach for large-scale vehicle routing problems with time windows}, 265 | author={Sabar, Nasser R and Zhang, Xiuzhen Jenny and Song, Andy}, 266 | booktitle={2015 IEEE Congress on Evolutionary Computation (CEC)}, 267 | pages={830--837}, 268 | year={2015}, 269 | organization={IEEE}, 270 | doi = {10.1109/CEC.2015.7256977} 271 | } 272 | 273 | @inproceedings{ferreira2017multi, 274 | title={A multi-armed bandit selection strategy for hyper-heuristics}, 275 | author={Ferreira, Alexandre Silvestre and Gon{\c{c}}alves, Richard Aderbal and Pozo, Aurora}, 276 | booktitle={2017 IEEE Congress on Evolutionary Computation (CEC)}, 277 | pages={525--532}, 278 | year={2017}, 279 | organization={IEEE}, 280 | doi = {10.1109/CEC.2017.7969356} 281 | } 282 | 283 | @inproceedings{drake2012improved, 284 | title={An improved choice function heuristic selection for cross domain heuristic search}, 285 | author={Drake, John H and {\"O}zcan, Ender and Burke, Edmund K}, 286 | booktitle={International Conference on Parallel Problem Solving from Nature}, 287 | pages={307--316}, 288 | year={2012}, 289 | organization={Springer}, 290 | doi ={10.1007/978-3-642-32964-7_31} 291 | } 292 | -------------------------------------------------------------------------------- /paper/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'VRPy: A Python package for solving a range of vehicle routing problems with a column generation approach' 3 | tags: 4 | - Python 5 | - Vehicle Routing Problems 6 | - Networks 7 | - Column generation 8 | authors: 9 | - name: Romain Montagné 10 | orcid: 0000-0003-3139-4519 11 | affiliation: "1" 12 | - name: David Torres Sanchez 13 | orcid: 0000-0002-2894-9432 14 | affiliation: "2" 15 | - name: Halvard Olsen Storbugt 16 | orcid: 0000-0003-1142-0185 17 | affiliation: "2" 18 | affiliations: 19 | - name: EURODECISION 20 | index: 1 21 | - name: SINTEF Digital, Mathematics and Cybernetics 22 | index: 2 23 | date: June 2020 24 | bibliography: paper.bib 25 | --- 26 | 27 | # Introduction 28 | 29 | The Vehicle Routing Problem (VRP) is amongst the most well known combinatorial optimization problems. The most classical version of the VRP, the Capacitated VRP (CVRP) [@laporte2007you], can be described as follows. A fleet of vehicles with uniform capacity must serve customers with known demand for a single commodity. 30 | The vehicles start and end their routes at a common depot and each customer must be served by exactly one vehicle. 31 | The objective is to assign a sequence of customers to each vehicle of the fleet (a route), minimizing the total distance traveled, such that all customers are served and the total demand served by each vehicle does not exceed its capacity. Note that the VRP generalises the well-known traveling salesman problem (TSP) and is therefore computationally intractable. 32 | 33 | Mathematicians have started tackling VRPs since 1959 [@dantzig1959truck]. Ever since, algorithms and computational power have not stopped improving. State of the art techniques include column generation approaches [@costa2019exact; @bramel1997solving] on which ``vrpy`` relies; more details are given hereafter. 34 | 35 | ``vrpy`` is of interest to the operational research community and others (e.g., logisticians, supply chain analysts) who wish to solve vehicle routing problems, and therefore has many obvious applications in industry. 36 | 37 | # Features 38 | 39 | ``vrpy`` is a Python package that offers an easy-to-use, unified API for many variants of vehicle routing problems including: 40 | 41 | - the Capacitated VRP (CVRP) [@laporte2007you;@baldacci2010exact], 42 | - the CVRP with resource constraints [@laporte1985optimal], 43 | - the CVRP with time windows [@cordeau2000vrp], 44 | - the CVRP with simultaneous distribution and collection [@dell2006branch], 45 | - the CVRP with pickups and deliveries [@desrosiers1988shortest], 46 | - the CVRP with heterogeneous fleet [@choi2007column]. 47 | 48 | For each of these variants, it is possible to i/ set initial routes for the search (if one already has a solution at hand and wishes to improve it) ii/ lock routes (if part of the solution is imposed and must not be optimized) iii/ drop nodes (ignore a customer at the cost of a penalty). 49 | 50 | ``vrpy`` is built upon the well known *NetworkX* library [@hagberg2008exploring] and thus benefits from a user friendly API, as shown in the following quick start example: 51 | 52 | ```python 53 | from networkx import DiGraph 54 | from vrpy import VehicleRoutingProblem 55 | 56 | # Define the network 57 | G = DiGraph() 58 | G.add_edge("Source",1,cost=1,time=2) 59 | G.add_edge("Source",2,cost=2,time=1) 60 | G.add_edge(1,"Sink",cost=0,time=2) 61 | G.add_edge(2,"Sink",cost=2,time=3) 62 | G.add_edge(1,2,cost=1,time=1) 63 | G.add_edge(2,1,cost=1,time=1) 64 | 65 | # Define the customers demands 66 | G.nodes[1]["demand"] = 5 67 | G.nodes[2]["demand"] = 4 68 | 69 | # Define the Vehicle Routing Problem 70 | prob = VehicleRoutingProblem(G, load_capacity=10, duration=5) 71 | 72 | # Solve and display solution value 73 | prob.solve() 74 | print(prob.best_value) 75 | 3 76 | print(prob.best_routes) 77 | {1: ["Source",2,1,"Sink"]} 78 | ``` 79 | 80 | # State of the field 81 | 82 | Although the VRP is a classical optimization problem, to our knowledge there is only one dedicated package in the Python ecosystem that is able to solve such a range of VRP variants: the excellent ``OR-Tools`` (Google) routing library [@ortools], released for the first time in 2014. To be precise, the core algorithms are implemented in C++, but the library provides a wrapper in Python. Popular and efficient, it is a reference for ``vrpy``, both in terms of features and performance. The current version of ``vrpy`` is able to handle the same variants as OR-Tools (mentioned in the previous section). 83 | 84 | Performance-wise, ``vrpy`` ambitions to be competitive with ``OR-Tools`` eventually, at least in terms of solution quality. For the moment, benchmarks (available in the repository) for the CVRP on the set of Augerat instances [@augerat1995approche] show promising results: in the performance profile in Figure 1 below, one can see that nearly the same number of instances are solved within 10 seconds with the same relative error with respect to the best known solution (42\% for ``vrpy``, 44\% for ``OR-Tools``). 85 | 86 | | ![Performance profile](cvrp_performance_profile.png) | 87 | | :--------------------------------------------------: | 88 | | *Figure 1: CVRP Performance profile* | 89 | 90 | We do not claim to outperform ``OR-Tools``, but aim to have results of the same order of magnitude as the package evolves, as there is still much room for improvement (see Section *Future Work* below). On the other hand, we are confident that the user friendly and intuitive API will help students, researchers and more generally the operational research community solve instances of vehicle routing problems of small to medium size, perhaps more easily than with the existing software. 91 | 92 | ``py-ga-VRPTW`` is another library that is available but as mentioned by its authors, it is more of an experimental project and its performances are rather poor. In particular, we were not able to find feasible solutions for Solomon's instances [@solomon1987algorithms] and therefore cannot compare the two libraries. Also note that ``py-ga-VRPTW`` is designed to solve the VRPTW only, that is, the VRP with time windows. 93 | 94 | 95 | # Mathematical background 96 | 97 | ``vrpy`` solves vehicle routing problems with a column generation approach. The term *column generation* refers to the fact that iteratively, routes (or columns) are generated with a pricing problem, and fed to a master problem which selects the best routes among a pool such that each vertex is serviced exactly once. Results from the master problem are then used to search for new potential routes likely to improve the solution's cost, and so forth. This procedure is illustrated in Figure 2 below: 98 | 99 | | ![Column Generation](colgen.png) | 100 | | :------------------------------: | 101 | | *Figure 2: Column Generation* | 102 | 103 | The master problem is a set partitioning linear formulation and is solved with the open source solver Clp from COIN-OR [@johnjforrest_2020_clp], while the subproblem is a shortest elementary path problem with *resource constraints*. It is solved with the help of the ``cspy`` library [@cspy] which is specifically designed for such problems. 104 | 105 | This column generation procedure is very generic, as for each of the featuring VRP variants, the master problem is identical and partitions the customers into subsets (routes). It is the subproblem (or pricing problem) that differs from one variant to another. More specifically, each variant has its unique set of *resources* which must remain in a given interval. For example, for the CVRP, a resource representing the vehicle's load is carried along the path and must not exceed the vehicle capacity; for the CVRP with time windows, two extra resources must be considered: the first one for time, and the second one for time window feasibility. The reader may refer to [@costa2019exact] for more details on each of these variants and how they are delt with within the framework of column generation. 106 | 107 | Note that ``vrpy`` does not necessarily return an optimal solution. Indeed, once the pricing problems fails to find 108 | a route with negative marginal cost, the master problem is solved as a MIP. This *price-and-branch* strategy does not guarantee optimality. Note however that it 109 | can be shown [@bramel1997solving] that asymptotically, the relative error goes to zero as the number of customers increases. To guarantee that an optimal solution is returned, the column generation procedure should be embedded in a branch-and-bound scheme (*branch-and-price*), which is beyond the scope of the current release, but part of the future work considered. 110 | 111 | # Advanced Features 112 | 113 | For more advanced users, there are different pricing strategies (approaches for solving subproblems), namely sparsification strategies [@dell2006branch;@santini2018branch], as well as pre-pricing heuristics available that can lead to faster solutions. The heuristics implemented include a greedy randomized heuristic 114 | (for the CVRP and the CVRP with resource constraints) [@santini2018branch]. Also, a diving heuristic [@sadykov2019primal] can be called to explore part of the branch-and-price tree, instead of solving the restricted master problem as a MIP. 115 | 116 | Additionally, we have an experimental feature that uses Hyper-Heuristics for the dynamic selection of 117 | pricing strategies. 118 | The approach ranks the best pricing strategies as the algorithm is running and chooses 119 | according to selection functions based on [@sabar2015math;@ferreira2017multi]. The selection criteria has been modified to include a combination of runtime, objective improvement, and currently active columns in the restricted master problem. 120 | Adaptive parameter settings found in [@drake2012improved] is used to balance exploration and exploitation 121 | under stagnation. The main advantage is that selection is done as the program runs, and is therefore more 122 | flexible compared to a predefined pricing strategy. 123 | 124 | # Future Work 125 | 126 | There are many ways ``vrpy`` could be improved. To boost the run times, specific heuristics for each variant could be implemented, e.g., Solomon's insertion algorithm [@solomon1987algorithms] for the VRPTW. Second, the pricing problem is solved with ``cspy``, which is quite recent (2019) and is still being fine tuned. Also, currently, stabilization issues are delt with a basic interior point based strategy which could be enhanced [@pessoa2018automation]. Last but not least, there are many cutting strategies in the literature [@costa2019exact] that have not been implemented and which have proven to significantly reduce run times for such problems. 127 | 128 | # Acknowledgements 129 | 130 | We would like to thank reviewers Ben Stabler and Serdar Kadioglu for their helpful and constructive suggestions. 131 | 132 | # References 133 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ###### Requirements without Version Specifiers ###### 2 | cspy 3 | networkx 4 | numpy 5 | pandas 6 | pulp 7 | pytest 8 | 9 | ###### Requirements with Version Specifiers ###### 10 | 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name="vrpy", 5 | version="0.5.1", 6 | description="A python framework for solving vehicle routing problems", 7 | license="MIT", 8 | author="Romain Montagne, David Torres", 9 | author_email="r.montagne@hotmail.fr", 10 | keywords=["vehicle routing problem", "vrp", "column generation"], 11 | long_description=open("README.rst", "r").read(), 12 | long_description_content_type="text/x-rst", 13 | url="https://github.com/Kuifje02/vrpy", 14 | packages=setuptools.find_packages(), 15 | install_requires=["cspy", "networkx", "numpy", "pulp"], 16 | classifiers=[ 17 | "Programming Language :: Python :: 3.6", 18 | "Programming Language :: Python :: 3.7", 19 | "Programming Language :: Python :: 3.8", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | ], 23 | ) 24 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_cli = 1 3 | log_cli_level = INFO 4 | log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) 5 | log_cli_date_format=%Y-%m-%d %H:%M:%S 6 | 7 | python_files = test_*.py 8 | #testpaths = tests/ 9 | filterwarnings = 10 | ignore::DeprecationWarning 11 | -------------------------------------------------------------------------------- /tests/test_consistency.py: -------------------------------------------------------------------------------- 1 | from networkx import DiGraph, Graph, NetworkXError 2 | import pytest 3 | import sys 4 | 5 | sys.path.append("../") 6 | from vrpy.vrp import VehicleRoutingProblem 7 | 8 | ##################### 9 | # consistency tests # 10 | ##################### 11 | 12 | 13 | def test_check_vrp(): 14 | """Tests consistency of input graph.""" 15 | G = Graph() 16 | with pytest.raises(TypeError): 17 | VehicleRoutingProblem(G) 18 | G = DiGraph() 19 | G.add_edge("Source", 1, cost=0) 20 | with pytest.raises(KeyError) and pytest.raises(NetworkXError): 21 | VehicleRoutingProblem(G) 22 | G.add_edge(1, "Sink") 23 | with pytest.raises(KeyError): 24 | VehicleRoutingProblem(G) 25 | G.edges[1, "Sink"]["cost"] = 1 26 | G.add_edge("Sink", 2, cost=3) 27 | with pytest.raises(NetworkXError): 28 | VehicleRoutingProblem(G) 29 | with pytest.raises(NetworkXError): 30 | VehicleRoutingProblem(G) 31 | G.remove_edge("Sink", 2) 32 | 33 | 34 | def test_check_arguments(): 35 | G = DiGraph() 36 | G.add_edge("Source", "Sink", cost=1) 37 | with pytest.raises(TypeError): 38 | prob = VehicleRoutingProblem(G, num_stops=3.5) 39 | prob.solve() 40 | with pytest.raises(TypeError): 41 | prob = VehicleRoutingProblem(G, load_capacity=-10) 42 | prob.solve() 43 | with pytest.raises(TypeError): 44 | prob = VehicleRoutingProblem(G, duration=-1) 45 | prob.solve() 46 | with pytest.raises(ValueError): 47 | prob = VehicleRoutingProblem(G) 48 | prob.solve(pricing_strategy="Best") 49 | 50 | 51 | def test_consistency_parameters(): 52 | """Checks if solving parameters are consistent.""" 53 | G = DiGraph() 54 | G.add_edge("Source", "Sink", cost=1) 55 | prob = VehicleRoutingProblem(G, pickup_delivery=True) 56 | # pickup delivery expects at least one request 57 | with pytest.raises(KeyError): 58 | prob.solve(cspy=False, pricing_strategy="Exact") 59 | 60 | 61 | def test_heuristic_only_consistency(): 62 | """Checks is error is raised if heuristic_only is active with wrong arguments""" 63 | G = DiGraph() 64 | G.add_edge("Source", "Sink", cost=1) 65 | with pytest.raises(ValueError): 66 | prob = VehicleRoutingProblem(G, time_windows=True) 67 | prob.solve(heuristic_only=True) 68 | with pytest.raises(ValueError): 69 | prob = VehicleRoutingProblem(G, mixed_fleet=True) 70 | prob.solve(heuristic_only=True) 71 | with pytest.raises(ValueError): 72 | prob = VehicleRoutingProblem(G, distribution_collection=True) 73 | prob.solve(heuristic_only=True) 74 | with pytest.raises(ValueError): 75 | prob = VehicleRoutingProblem(G, periodic=True) 76 | prob.solve(heuristic_only=True) 77 | 78 | 79 | def test_mixed_fleet_consistency(): 80 | """Checks if mixed fleet arguments are consistent.""" 81 | G = DiGraph() 82 | G.add_edge("Source", "Sink", cost=1) 83 | with pytest.raises(TypeError): 84 | prob = VehicleRoutingProblem(G, mixed_fleet=True, load_capacity=[2, 4]) 85 | prob.solve() 86 | G.edges["Source", "Sink"]["cost"] = [1, 2] 87 | with pytest.raises(ValueError): 88 | prob = VehicleRoutingProblem(G, 89 | mixed_fleet=True, 90 | load_capacity=[2, 4], 91 | fixed_cost=[4]) 92 | prob.solve() 93 | 94 | 95 | def test_feasibility_check(): 96 | """Tests feasibility checks.""" 97 | G = DiGraph() 98 | G.add_edge("Source", 1, cost=1, time=1) 99 | G.add_edge(1, "Sink", cost=1, time=1) 100 | G.nodes[1]["demand"] = 2 101 | with pytest.raises(ValueError): 102 | prob = VehicleRoutingProblem(G, load_capacity=1) 103 | prob.solve() 104 | with pytest.raises(ValueError): 105 | prob = VehicleRoutingProblem(G, duration=1) 106 | prob.solve() 107 | 108 | 109 | def test_locked_routes_check(): 110 | """Tests if locked routes check.""" 111 | G = DiGraph() 112 | G.add_edge("Source", 1, cost=1) 113 | G.add_edge(1, "Sink", cost=1) 114 | G.nodes[1]["demand"] = 2 115 | prob = VehicleRoutingProblem(G) 116 | with pytest.raises(ValueError): 117 | prob.solve(preassignments=[[1, 2]]) 118 | -------------------------------------------------------------------------------- /tests/test_initial_solution.py: -------------------------------------------------------------------------------- 1 | from networkx import DiGraph, shortest_path 2 | 3 | from vrpy.clarke_wright import _ClarkeWright, _RoundTrip 4 | from vrpy.greedy import _Greedy 5 | 6 | 7 | class TestsInitialSolution: 8 | """ 9 | Initial solution can be computed with: 10 | - a round trip; 11 | - Clarke & Wright; 12 | - Greedy. 13 | """ 14 | 15 | def setup(self): 16 | self.G = DiGraph() 17 | self.G.add_edge("Source", 1, cost=10, time=1) 18 | self.G.add_edge("Source", 2, cost=15, time=1) 19 | self.G.add_edge("Source", 3, cost=15, time=1) 20 | self.G.add_edge(1, "Sink", cost=10, time=1) 21 | self.G.add_edge(2, "Sink", cost=5, time=1) 22 | self.G.add_edge(3, "Sink", cost=10, time=1) 23 | self.G.add_edge(1, 2, cost=2, time=5) 24 | self.G.add_edge(1, 3, cost=10, time=1) 25 | self.G.nodes[1]["demand"] = 1 26 | self.G.nodes[2]["demand"] = 2 27 | self.G.nodes[3]["demand"] = 3 28 | self.G.nodes["Sink"]["demand"] = self.G.nodes["Sink"]["service_time"] = 0 29 | self.G.nodes[1]["service_time"] = 1 30 | self.G.nodes[2]["service_time"] = 1 31 | self.G.nodes[3]["service_time"] = 0 32 | 33 | self.alg = _ClarkeWright(self.G, load_capacity=4) 34 | self.greedy = _Greedy(self.G, load_capacity=4) 35 | 36 | ############## 37 | # Round Trip # 38 | ############## 39 | 40 | def test_round_trip(self): 41 | round_trips = _RoundTrip(self.G) 42 | round_trips.run() 43 | assert len(round_trips.round_trips) == 3 44 | 45 | ################### 46 | # Clarke & Wright # 47 | ################### 48 | 49 | def test_initialization(self): 50 | self.alg._initialize_routes() 51 | assert self.alg._route[1].graph["load"] == self.G.nodes[1]["demand"] 52 | assert len(self.alg._route[1].nodes()) == 3 53 | 54 | def test_savings(self): 55 | self.alg._get_savings() 56 | assert self.alg._savings[(1, 2)] == 23 57 | assert self.alg._ordered_edges[0] == (1, 2) 58 | 59 | def test_result_load(self): 60 | self.alg.run() 61 | assert self.alg.best_value == 42 62 | assert shortest_path(self.alg._route[1], "Source", "Sink") == [ 63 | "Source", 64 | 1, 65 | 2, 66 | "Sink", 67 | ] 68 | 69 | def test_result_duration(self): 70 | self.alg.duration = 4 71 | self.alg.run() 72 | assert self.alg.best_value == 50 73 | assert shortest_path(self.alg._route[1], "Source", "Sink") == [ 74 | "Source", 75 | 1, 76 | 3, 77 | "Sink", 78 | ] 79 | 80 | def test_result_stops(self): 81 | self.alg.num_stops = 1 82 | self.alg.run() 83 | assert self.alg.best_value == 65 84 | assert shortest_path(self.alg._route[1], "Source", "Sink") == [ 85 | "Source", 86 | 1, 87 | "Sink", 88 | ] 89 | 90 | ########## 91 | # Greedy # 92 | ########## 93 | 94 | def test_greedy__load(self): 95 | self.greedy.run() 96 | assert self.greedy.best_value == 42 97 | assert self.greedy.best_routes[0] == ["Source", 1, 2, "Sink"] 98 | 99 | def test_greedy_duration(self): 100 | self.greedy.duration = 4 101 | self.greedy.run() 102 | assert self.greedy.best_value == 50 103 | assert self.greedy.best_routes[0] == ["Source", 1, 3, "Sink"] 104 | 105 | def test_greedy_stops(self): 106 | self.greedy.num_stops = 1 107 | self.greedy.run() 108 | assert self.greedy.best_routes[0] == ["Source", 1, "Sink"] 109 | assert self.greedy.best_value == 65 110 | -------------------------------------------------------------------------------- /tests/test_issue101.py: -------------------------------------------------------------------------------- 1 | from networkx import DiGraph 2 | from vrpy import VehicleRoutingProblem 3 | 4 | 5 | class TestIssue101_small: 6 | def setup(self): 7 | self.G = DiGraph() 8 | self.G.add_edge("Source", 1, cost=5) 9 | self.G.add_edge("Source", 2, cost=5) 10 | self.G.add_edge(1, "Sink", cost=5) 11 | self.G.add_edge(2, "Sink", cost=5) 12 | self.G.add_edge(1, 2, cost=1) 13 | self.G.nodes[1]["lower"] = 0 14 | self.G.nodes[1]["upper"] = 20 15 | self.G.nodes[2]["lower"] = 0 16 | self.G.nodes[2]["upper"] = 20 17 | self.G.nodes[1]["service_time"] = 5 18 | self.G.nodes[2]["service_time"] = 5 19 | self.G.nodes[1]["demand"] = 8 20 | self.G.nodes[2]["demand"] = 8 21 | self.prob = VehicleRoutingProblem(self.G, load_capacity=10, time_windows=True) 22 | 23 | def test_cspy(self): 24 | self.prob.solve() 25 | self.prob.check_arrival_time() 26 | self.prob.check_departure_time() 27 | 28 | def test_lp(self): 29 | self.prob.solve(cspy=False) 30 | self.prob.check_arrival_time() 31 | self.prob.check_departure_time() 32 | -------------------------------------------------------------------------------- /tests/test_issue110.py: -------------------------------------------------------------------------------- 1 | from networkx import DiGraph 2 | from vrpy import VehicleRoutingProblem 3 | 4 | 5 | class TestIssue110: 6 | def setup(self): 7 | G = DiGraph() 8 | G.add_edge("Source", 1, cost=[1, 2]) 9 | G.add_edge("Source", 2, cost=[2, 4]) 10 | G.add_edge(1, "Sink", cost=[0, 0]) 11 | G.add_edge(2, "Sink", cost=[2, 4]) 12 | G.add_edge(1, 2, cost=[1, 2]) 13 | G.nodes[1]["demand"] = 13 14 | G.nodes[2]["demand"] = 13 15 | self.prob = VehicleRoutingProblem(G, mixed_fleet=True, load_capacity=[10, 15]) 16 | 17 | def test_node_load(self): 18 | self.prob.solve() 19 | assert self.prob.best_routes_type == {1: 1, 2: 1} 20 | -------------------------------------------------------------------------------- /tests/test_issue79.py: -------------------------------------------------------------------------------- 1 | from networkx import DiGraph 2 | from vrpy import VehicleRoutingProblem 3 | 4 | 5 | class TestIssue79: 6 | 7 | def setup(self): 8 | G = DiGraph() 9 | G.add_edge("Source", 8, cost=0) 10 | G.add_edge("Source", 6, cost=1) 11 | G.add_edge("Source", 2, cost=1) 12 | G.add_edge("Source", 5, cost=1) 13 | G.add_edge(8, 6, cost=0) 14 | G.add_edge(6, 2, cost=0) 15 | G.add_edge(2, 5, cost=0) 16 | G.add_edge(5, "Sink", cost=0) 17 | G.add_edge(8, "Sink", cost=1) 18 | G.add_edge(6, "Sink", cost=1) 19 | G.add_edge(2, "Sink", cost=1) 20 | G.nodes[8]["demand"] = 8 21 | G.nodes[6]["demand"] = 4 22 | G.nodes[2]["demand"] = 1 23 | G.nodes[5]["demand"] = 2 24 | G.nodes[8]["collect"] = 1 25 | G.nodes[6]["collect"] = 1 26 | G.nodes[2]["collect"] = 1 27 | G.nodes[5]["collect"] = 2 28 | self.prob = VehicleRoutingProblem(G, 29 | load_capacity=15, 30 | distribution_collection=True) 31 | 32 | def test_node_load_cspy(self): 33 | self.prob.solve() 34 | assert self.prob.node_load[1][8] == 8 35 | assert self.prob.node_load[1][6] == 5 36 | assert self.prob.node_load[1][2] == 5 37 | assert self.prob.node_load[1][5] == 5 38 | 39 | def test_node_load_lp(self): 40 | self.prob.solve(cspy=False) 41 | assert self.prob.node_load[1][8] == 8 42 | assert self.prob.node_load[1][6] == 5 43 | assert self.prob.node_load[1][2] == 5 44 | assert self.prob.node_load[1][5] == 5 45 | -------------------------------------------------------------------------------- /tests/test_issue86.py: -------------------------------------------------------------------------------- 1 | from networkx import DiGraph 2 | from vrpy import VehicleRoutingProblem 3 | 4 | 5 | class TestIssue86: 6 | def setup(self): 7 | G = DiGraph() 8 | G.add_edge(1, 2, cost=77.67) 9 | G.add_edge(1, 3, cost=0.0) 10 | G.add_edge(1, 4, cost=96.61) 11 | G.add_edge(1, 5, cost=0.0) 12 | G.add_edge(1, 6, cost=59.03) 13 | G.add_edge(2, 1, cost=77.67) 14 | G.add_edge(2, 3, cost=77.67) 15 | G.add_edge(2, 4, cost=64.85) 16 | G.add_edge(2, 5, cost=77.67) 17 | G.add_edge(2, 6, cost=62.2) 18 | G.add_edge(3, 1, cost=0.0) 19 | G.add_edge(3, 2, cost=77.67) 20 | G.add_edge(3, 4, cost=96.61) 21 | G.add_edge(3, 5, cost=0.0) 22 | G.add_edge(3, 6, cost=59.03) 23 | G.add_edge(4, 1, cost=96.61) 24 | G.add_edge(4, 2, cost=64.85) 25 | G.add_edge(4, 3, cost=96.61) 26 | G.add_edge(4, 5, cost=96.61) 27 | G.add_edge(4, 6, cost=39.82) 28 | G.add_edge(5, 1, cost=0.0) 29 | G.add_edge(5, 2, cost=77.67) 30 | G.add_edge(5, 3, cost=0.0) 31 | G.add_edge(5, 4, cost=96.61) 32 | G.add_edge(5, 6, cost=59.03) 33 | G.add_edge(6, 1, cost=59.03) 34 | G.add_edge(6, 2, cost=62.2) 35 | G.add_edge(6, 3, cost=59.03) 36 | G.add_edge(6, 4, cost=39.82) 37 | G.add_edge(6, 5, cost=59.03) 38 | G.add_edge("Source", 1, cost=18.03) 39 | G.add_edge(1, "Sink", cost=18.93) 40 | G.add_edge("Source", 2, cost=61.29) 41 | G.add_edge(2, "Sink", cost=61.29) 42 | G.add_edge("Source", 3, cost=18.03) 43 | G.add_edge(3, "Sink", cost=18.03) 44 | G.add_edge("Source", 4, cost=79.92) 45 | G.add_edge(4, "Sink", cost=79.92) 46 | G.add_edge("Source", 5, cost=18.03) 47 | G.add_edge(5, "Sink", cost=18.03) 48 | G.add_edge("Source", 6, cost=44.38) 49 | G.add_edge(6, "Sink", cost=44.38) 50 | G.nodes[1]["request"] = 2 51 | G.nodes[1]["demand"] = 25000 52 | G.nodes[2]["demand"] = -25000 53 | G.nodes[3]["request"] = 4 54 | G.nodes[3]["demand"] = 25000 55 | G.nodes[4]["demand"] = -25000 56 | G.nodes[5]["request"] = 6 57 | G.nodes[5]["demand"] = 10000 58 | G.nodes[6]["demand"] = -10000 59 | self.prob = VehicleRoutingProblem(G, load_capacity=25000, pickup_delivery=True) 60 | 61 | def test_solve(self): 62 | self.prob.solve(cspy=False, solver="cbc") 63 | assert round(self.prob.best_value, 0) == 468 64 | -------------------------------------------------------------------------------- /tests/test_issue99.py: -------------------------------------------------------------------------------- 1 | from networkx import from_numpy_matrix, set_node_attributes, relabel_nodes, DiGraph 2 | from numpy import array 3 | from vrpy import VehicleRoutingProblem 4 | 5 | 6 | class TestIssue99: 7 | def setup(self): 8 | 9 | distance_ = [ 10 | [ 11 | 0, 12 | 0.0, 13 | 0.0, 14 | 34.751182089808005, 15 | 92.30245516008434, 16 | 40.1681913442985, 17 | 139.77829026093886, 18 | 22.427641389383695, 19 | 184.16196166082054, 20 | 24.56323283561296, 21 | 120.32361659641211, 22 | 32.4378310152284, 23 | 67.38909304866816, 24 | 0.0, 25 | ], 26 | [ 27 | 0, 28 | 0.0, 29 | 0.0, 30 | 34.751182089808005, 31 | 92.30245516008434, 32 | 40.1681913442985, 33 | 139.77829026093886, 34 | 22.427641389383695, 35 | 184.16196166082054, 36 | 24.56323283561296, 37 | 120.32361659641211, 38 | 32.4378310152284, 39 | 67.38909304866816, 40 | 0.0, 41 | ], 42 | [ 43 | 0, 44 | 34.751182089808005, 45 | 34.751182089808005, 46 | 0.0, 47 | 98.7079853042215, 48 | 5.44379884433748, 49 | 132.19619367955679, 50 | 12.670008991256175, 51 | 180.4339020413057, 52 | 59.02280970986385, 53 | 111.80230048333873, 54 | 17.019169601216593, 55 | 52.06928899775174, 56 | 34.751182089808005, 57 | ], 58 | [ 59 | 0, 60 | 92.30245516008434, 61 | 92.30245516008434, 62 | 98.7079853042215, 63 | 0.0, 64 | 100.22229866723093, 65 | 61.488042505729055, 66 | 92.72080465523338, 67 | 95.63269969727976, 68 | 90.58095214437495, 69 | 49.80739772877824, 70 | 111.78483468218113, 71 | 60.09941865363839, 72 | 92.30245516008434, 73 | ], 74 | [ 75 | 0, 76 | 40.1681913442985, 77 | 40.1681913442985, 78 | 5.44379884433748, 79 | 100.22229866723093, 80 | 0.0, 81 | 131.22379419294873, 82 | 17.9495157302615, 83 | 179.86968501771943, 84 | 64.376780302526, 85 | 110.81671081048268, 86 | 20.17178955112936, 87 | 50.836998290147, 88 | 40.1681913442985, 89 | ], 90 | [ 91 | 0, 92 | 139.77829026093886, 93 | 139.77829026093886, 94 | 132.19619367955679, 95 | 61.488042505729055, 96 | 131.22379419294873, 97 | 0.0, 98 | 131.60938240610648, 99 | 49.833874922036394, 100 | 145.2998753018322, 101 | 20.40765785593238, 102 | 148.57304976786813, 103 | 80.3991613224116, 104 | 139.77829026093886, 105 | ], 106 | [ 107 | 0, 108 | 22.427641389383695, 109 | 22.427641389383695, 110 | 12.670008991256175, 111 | 92.72080465523338, 112 | 17.9495157302615, 113 | 131.60938240610648, 114 | 0.0, 115 | 178.66012933702726, 116 | 46.44000947768777, 117 | 111.40463276346814, 118 | 19.110204473855905, 119 | 53.409038447894005, 120 | 22.427641389383695, 121 | ], 122 | [ 123 | 0, 124 | 184.16196166082054, 125 | 184.16196166082054, 126 | 180.4339020413057, 127 | 95.63269969727976, 128 | 179.86968501771943, 129 | 49.833874922036394, 130 | 178.66012933702726, 131 | 0.0, 132 | 185.80417023197256, 133 | 69.739408771209, 134 | 196.3770010004616, 135 | 129.30466285486838, 136 | 184.16196166082054, 137 | ], 138 | [ 139 | 0, 140 | 24.56323283561296, 141 | 24.56323283561296, 142 | 59.02280970986385, 143 | 90.58095214437495, 144 | 64.376780302526, 145 | 145.2998753018322, 146 | 46.44000947768777, 147 | 185.80417023197256, 148 | 0.0, 149 | 127.25510989846872, 150 | 56.34697111358194, 151 | 82.12357585699394, 152 | 24.56323283561296, 153 | ], 154 | [ 155 | 0, 156 | 120.32361659641211, 157 | 120.32361659641211, 158 | 111.80230048333873, 159 | 49.80739772877824, 160 | 110.81671081048268, 161 | 20.40765785593238, 162 | 111.40463276346814, 163 | 69.739408771209, 164 | 127.25510989846872, 165 | 0.0, 166 | 128.21818865240243, 167 | 59.99584151749755, 168 | 120.32361659641211, 169 | ], 170 | [ 171 | 0, 172 | 32.4378310152284, 173 | 32.4378310152284, 174 | 17.019169601216593, 175 | 111.78483468218113, 176 | 20.17178955112936, 177 | 148.57304976786813, 178 | 19.110204473855905, 179 | 196.3770010004616, 180 | 56.34697111358194, 181 | 128.21818865240243, 182 | 0.0, 183 | 68.79822623983333, 184 | 32.4378310152284, 185 | ], 186 | [ 187 | 0, 188 | 67.38909304866816, 189 | 67.38909304866816, 190 | 52.06928899775174, 191 | 60.09941865363839, 192 | 50.836998290147, 193 | 80.3991613224116, 194 | 53.409038447894005, 195 | 129.30466285486838, 196 | 82.12357585699394, 197 | 59.99584151749755, 198 | 68.79822623983333, 199 | 0.0, 200 | 67.38909304866816, 201 | ], 202 | [ 203 | 0, 204 | 0.0, 205 | 0.0, 206 | 34.751182089808005, 207 | 92.30245516008434, 208 | 40.1681913442985, 209 | 139.77829026093886, 210 | 22.427641389383695, 211 | 184.16196166082054, 212 | 24.56323283561296, 213 | 120.32361659641211, 214 | 32.4378310152284, 215 | 67.38909304866816, 216 | 0.0, 217 | ], 218 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 219 | ] 220 | 221 | demands = { 222 | 0: 0, 223 | 1: 24, 224 | 2: 12, 225 | 3: 22, 226 | 4: 63, 227 | 5: 44, 228 | 6: 68, 229 | 7: 41, 230 | 8: 27, 231 | 9: 38, 232 | 10: 15, 233 | 11: 17, 234 | 12: 0, 235 | } 236 | 237 | # Transform distance matrix to DiGraph 238 | A_ = array(distance_, dtype=[("cost", int)]) 239 | G_ = from_numpy_matrix(A_, create_using=DiGraph()) 240 | 241 | # Set demand 242 | set_node_attributes(G_, values=demands, name="demand") 243 | 244 | # Relabel depot 245 | G_ = relabel_nodes(G_, {0: "Source", 13: "Sink"}) 246 | 247 | # Define VRP 248 | self.prob = VehicleRoutingProblem(G_, load_capacity=100) 249 | 250 | def test_lp(self): 251 | self.prob.solve(cspy=False) 252 | print(self.prob.best_routes) 253 | assert self.prob.best_value == 829 254 | 255 | def test_cspy(self): 256 | self.prob.solve(cspy=True) 257 | print(self.prob.best_routes) 258 | assert self.prob.best_value == 829 259 | -------------------------------------------------------------------------------- /tests/test_subproblem_greedy.py: -------------------------------------------------------------------------------- 1 | from networkx import DiGraph 2 | 3 | from vrpy.subproblem_greedy import _SubProblemGreedy 4 | 5 | 6 | class TestsToy: 7 | def setup(self): 8 | self.G = DiGraph() 9 | self.G.add_edge("Source", 1, cost=[1], time=20) 10 | self.G.add_edge(1, 2, cost=[1], time=20) 11 | self.G.add_edge(2, "Sink", cost=[1], time=20) 12 | self.G.nodes[1]["demand"] = self.G.nodes[2]["demand"] = 2 13 | self.G.nodes["Source"]["demand"] = self.G.nodes["Sink"]["demand"] = 0 14 | self.prob = _SubProblemGreedy(self.G, {}, {1: [], 2: []}, [], 0) 15 | self.prob._initialize_run() 16 | 17 | def test_forward(self): 18 | self.prob.run_forward() 19 | assert self.prob._current_path == ["Source", 1, 2, "Sink"] 20 | 21 | def test_backwards(self): 22 | self.prob.run_backwards() 23 | assert self.prob._current_path == ["Source", 1, 2, "Sink"] 24 | 25 | def test_capacity(self): 26 | self.prob.load_capacity = [1] 27 | self.prob.run_forward() 28 | assert self.prob._current_path == ["Source"] 29 | self.prob._initialize_run() 30 | self.prob.run_backwards() 31 | assert self.prob._current_path == ["Sink"] 32 | 33 | def test_duration(self): 34 | self.prob.duration = 10 35 | self.prob.run_forward() 36 | assert self.prob._current_path == ["Source"] 37 | self.prob._initialize_run() 38 | self.prob.run_backwards() 39 | assert self.prob._current_path == ["Sink"] 40 | 41 | def test_stops(self): 42 | self.prob.num_stops = 1 43 | self.prob.run_forward() 44 | assert self.prob._current_path == ["Source", 1] 45 | self.prob._initialize_run() 46 | self.prob.run_backwards() 47 | assert self.prob._current_path == [2, "Sink"] 48 | 49 | def test_add_new_route(self): 50 | self.prob.run_forward() 51 | self.prob._add_new_route() 52 | assert set(self.prob.routes[0].nodes()) == {"Source", 1, 2, "Sink"} 53 | assert set(self.prob.routes_with_node[1][0].nodes()) == {"Source", 1, 2, "Sink"} 54 | -------------------------------------------------------------------------------- /vrpy/__init__.py: -------------------------------------------------------------------------------- 1 | """vrpy modules.""" 2 | from vrpy.vrp import VehicleRoutingProblem 3 | -------------------------------------------------------------------------------- /vrpy/clarke_wright.py: -------------------------------------------------------------------------------- 1 | from networkx import DiGraph, add_path, shortest_path 2 | 3 | 4 | class _ClarkeWright: 5 | """ 6 | Clarke & Wrights savings algorithm. 7 | 8 | Args: 9 | G (DiGraph): Graph on which algorithm is run. 10 | load_capacity (int, optional) : Maximum load per route. Defaults to None. 11 | duration (int, optional) : Maximum duration per route. Defaults to None. 12 | num_stops (int, optional) : Maximum number of stops per route. Defaults to None. 13 | """ 14 | 15 | def __init__( 16 | self, 17 | G, 18 | load_capacity=None, 19 | duration=None, 20 | num_stops=None, 21 | alpha=1, 22 | beta=0, 23 | gamma=0, 24 | ): 25 | self.G = G.copy() 26 | self._format_cost() 27 | self._savings = {} 28 | self._ordered_edges = [] 29 | self._route = {} 30 | self._best_routes = [] 31 | self._processed_nodes = [] 32 | 33 | self.alpha = alpha 34 | # FOR MORE SOPHISTICATED VERSIONS OF CLARKE WRIGHT: 35 | # self.beta = beta 36 | # self.gamma = gamma 37 | # self._average_demand = sum( 38 | # [ 39 | # self.G.nodes[v]["demand"] 40 | # for v in self.G.nodes() 41 | # if self.G.nodes[v]["demand"] > 0 42 | # ] 43 | # ) / len([v for v in self.G.nodes() if self.G.nodes[v]["demand"] > 0]) 44 | 45 | if isinstance(load_capacity, list): 46 | self.load_capacity = load_capacity[0] 47 | else: 48 | self.load_capacity = load_capacity 49 | self.duration = duration 50 | self.num_stops = num_stops 51 | 52 | def run(self): 53 | """Runs Clark & Wrights savings algorithm.""" 54 | self._initialize_routes() 55 | self._get_savings() 56 | for (i, j) in self._ordered_edges: 57 | self._process_edge(i, j) 58 | self._update_routes() 59 | 60 | def _initialize_routes(self): 61 | """Initialization with round trips (Source - node - Sink).""" 62 | for v in self.G.nodes(): 63 | if v not in ["Source", "Sink"]: 64 | # Create round trip 65 | round_trip_cost = ( 66 | self.G.edges["Source", v]["cost"] + self.G.edges[v, "Sink"]["cost"] 67 | ) 68 | route = DiGraph(cost=round_trip_cost) 69 | add_path(route, ["Source", v, "Sink"]) 70 | self._route[v] = route 71 | # Initialize route attributes 72 | if self.load_capacity: 73 | route.graph["load"] = self.G.nodes[v]["demand"] 74 | if self.duration: 75 | route.graph["time"] = ( 76 | self.G.nodes[v]["service_time"] 77 | + self.G.edges["Source", v]["time"] 78 | + self.G.edges[v, "Sink"]["time"] 79 | ) 80 | 81 | def _update_routes(self): 82 | """Stores best routes as list of nodes.""" 83 | self._best_value = 0 84 | for route in list(set(self._route.values())): 85 | self._best_value += route.graph["cost"] 86 | self._best_routes.append(shortest_path(route, "Source", "Sink")) 87 | 88 | def _get_savings(self): 89 | """Computes Clark & Wright savings and orders edges by non increasing savings.""" 90 | for (i, j) in self.G.edges(): 91 | if i != "Source" and j != "Sink": 92 | self._savings[(i, j)] = ( 93 | self.G.edges[i, "Sink"]["cost"] 94 | + self.G.edges["Source", j]["cost"] 95 | - self.alpha * self.G.edges[i, j]["cost"] 96 | # FOR MORE SOPHISTICATED VERSIONS OF CLARKE WRIGHT: 97 | # + self.beta 98 | # * abs( 99 | # self.G.edges["Source", i]["cost"] 100 | # - self.G.edges[j, "Sink"]["cost"] 101 | # ) 102 | # + self.gamma 103 | # * (self.G.nodes[i]["demand"] + self.G.nodes[j]["demand"]) 104 | # / self._average_demand 105 | ) 106 | self._ordered_edges = sorted(self._savings, key=self._savings.get, reverse=True) 107 | 108 | def _merge_route(self, existing_node, new_node, depot): 109 | """ 110 | Merges new_node in existing_node's route. 111 | Two possibilities: 112 | 1. If existing_node is a predecessor of Sink, new_node is inserted 113 | between existing_node and Sink; 114 | 2. If existing_node is a successor of Source, new_node is inserted 115 | between Source and and existing_node. 116 | """ 117 | route = self._route[existing_node] 118 | # Insert new_node between existing_node and Sink 119 | if depot == "Sink": 120 | add_path(route, [existing_node, new_node, "Sink"]) 121 | route.remove_edge(existing_node, "Sink") 122 | # Update route cost 123 | self._route[existing_node].graph["cost"] += ( 124 | self.G.edges[existing_node, new_node]["cost"] 125 | + self.G.edges[new_node, "Sink"]["cost"] 126 | - self.G.edges[existing_node, "Sink"]["cost"] 127 | ) 128 | 129 | # Insert new_node between Source and existing_node 130 | if depot == "Source": 131 | add_path(route, ["Source", new_node, existing_node]) 132 | route.remove_edge("Source", existing_node) 133 | # Update route cost 134 | self._route[existing_node].graph["cost"] += ( 135 | self.G.edges[new_node, existing_node]["cost"] 136 | + self.G.edges["Source", new_node]["cost"] 137 | - self.G.edges["Source", existing_node]["cost"] 138 | ) 139 | 140 | # Update route load 141 | if self.load_capacity: 142 | self._route[existing_node].graph["load"] += self.G.nodes[new_node]["demand"] 143 | # Update route duration 144 | if self.duration: 145 | self._route[existing_node].graph["time"] += ( 146 | self.G.edges[existing_node, new_node]["time"] 147 | + self.G.edges[new_node, "Sink"]["time"] 148 | + self.G.nodes[new_node]["service_time"] 149 | - self.G.edges[existing_node, "Sink"]["time"] 150 | ) 151 | # Update processed vertices 152 | self._processed_nodes.append(new_node) 153 | if existing_node not in self._processed_nodes: 154 | self._processed_nodes.append(existing_node) 155 | 156 | self._route[new_node] = route 157 | return route 158 | 159 | def _constraints_met(self, existing_node, new_node): 160 | """Tests if new_node can be merged in route without violating constraints.""" 161 | route = self._route[existing_node] 162 | # test if new_node already in route 163 | if new_node in route.nodes(): 164 | return False 165 | # test capacity constraints 166 | if self.load_capacity: 167 | if ( 168 | route.graph["load"] + self.G.nodes[new_node]["demand"] 169 | > self.load_capacity 170 | ): 171 | return False 172 | # test duration constraints 173 | if self.duration: 174 | # this code assumes the times to go from the Source and to the Sink are equal 175 | if ( 176 | route.graph["time"] 177 | + self.G.edges[existing_node, new_node]["time"] 178 | + self.G.edges[new_node, "Sink"]["time"] 179 | + self.G.nodes[new_node]["service_time"] 180 | - self.G.edges[existing_node, "Sink"]["time"] 181 | > self.duration 182 | ): 183 | return False 184 | # test stop constraints 185 | if self.num_stops: 186 | # Source and Sink don't count (hence -2) 187 | if len(route.nodes()) - 2 + 1 > self.num_stops: 188 | return False 189 | return True 190 | 191 | def _process_edge(self, i, j): 192 | """ 193 | Attemps to merge nodes i and j together. 194 | Merge is possible if : 195 | 1. vertices have not been merged already; 196 | 2. route constraints are met; 197 | 3. either: 198 | a) node i is adjacent to the Source (j is inserted in route[i]); 199 | b) or node j is adjacent to the Sink (i is inserted in route[j]). 200 | """ 201 | merged = False 202 | if ( 203 | j not in self._processed_nodes # 1 204 | and self._constraints_met(i, j) # 2 205 | and i in self._route[i].predecessors("Sink") # 3b 206 | ): 207 | self._merge_route(i, j, "Sink") 208 | merged = True 209 | 210 | if ( 211 | not merged 212 | and j in self.G.predecessors(i) 213 | and i not in self._processed_nodes # 1 214 | and self._constraints_met(j, i) # 2 215 | and j in self._route[j].successors("Source") # 3a 216 | ): 217 | self._merge_route(j, i, "Source") 218 | 219 | def _format_cost(self): 220 | """If list of costs is given, first item of list is considered.""" 221 | for (i, j) in self.G.edges(): 222 | if isinstance(self.G.edges[i, j]["cost"], list): 223 | self.G.edges[i, j]["cost"] = self.G.edges[i, j]["cost"][0] 224 | 225 | @property 226 | def best_value(self): 227 | return self._best_value 228 | 229 | @property 230 | def best_routes(self): 231 | return self._best_routes 232 | 233 | 234 | class _RoundTrip: 235 | """ 236 | Computes simple round trips from the depot to each node (Source-node-Sink). 237 | 238 | Args: 239 | G (DiGraph): Graph on which round trips are computed. 240 | """ 241 | 242 | def __init__(self, G): 243 | self.G = G 244 | self.round_trips = [] 245 | 246 | def run(self): 247 | for v in self.G.nodes(): 248 | if v not in ["Source", "Sink"]: 249 | self.round_trips.append(["Source", v, "Sink"]) 250 | -------------------------------------------------------------------------------- /vrpy/greedy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | class _Greedy: 7 | """ 8 | Greedy algorithm. Iteratively adds closest feasible node to current path. 9 | 10 | Args: 11 | G (DiGraph): Graph on which algorithm is run. 12 | load_capacity (int, optional) : Maximum load per route. Defaults to None. 13 | num_stops (int, optional) : Maximum stops per route. Defaults to None. 14 | """ 15 | 16 | def __init__(self, G, load_capacity=None, num_stops=None, duration=None): 17 | self.G = G.copy() 18 | self._format_cost() 19 | self._best_routes = [] 20 | self._unprocessed_nodes = [ 21 | v for v in self.G.nodes() if v not in ["Source", "Sink"] 22 | ] 23 | 24 | if isinstance(load_capacity, list): 25 | # capacity of vehicle type 1 is used! 26 | self.load_capacity = load_capacity[0] 27 | else: 28 | self.load_capacity = load_capacity 29 | self.num_stops = num_stops 30 | self.duration = duration 31 | 32 | self._best_value = 0 33 | 34 | @property 35 | def best_value(self): 36 | return self._best_value 37 | 38 | @property 39 | def best_routes(self): 40 | return self._best_routes 41 | 42 | def run(self): 43 | """The forward search is run.""" 44 | while self._unprocessed_nodes != []: 45 | self._load = 0 46 | self._stops = 0 47 | self._time = 0 48 | self._run_forward() 49 | self._update_routes() 50 | if self._current_path == ["Source", "Sink"]: 51 | break 52 | 53 | def _run_forward(self): 54 | """ 55 | A path starting from Source is greedily extended 56 | until Sink is reached. 57 | The procedure aborts if path becomes infeasible. 58 | """ 59 | self._current_path = ["Source"] 60 | while True: 61 | self._get_next_node() 62 | self._update() 63 | if self._new_node == "Sink": 64 | break 65 | 66 | def _get_next_node(self): 67 | self._last_node = self._current_path[-1] 68 | out_going_costs = {} 69 | # Store the successors cost that meet constraints 70 | for v in self.G.successors(self._last_node): 71 | if self._constraints_met(v) and v in self._unprocessed_nodes: 72 | out_going_costs[v] = self.G.edges[self._last_node, v]["cost"] 73 | if out_going_costs == {}: 74 | logger.debug("path cannot be extended") 75 | self._new_node = "Sink" 76 | else: 77 | # Select best successor 78 | self._new_node = sorted(out_going_costs, key=out_going_costs.get)[0] 79 | 80 | def _constraints_met(self, v): 81 | """Checks if constraints are respected.""" 82 | if v in self._current_path or self._check_source_sink(v): 83 | return False 84 | elif self.load_capacity and not self._check_capacity(v): 85 | return False 86 | elif self.duration and not self._check_duration(v): 87 | return False 88 | else: 89 | return True 90 | 91 | def _update(self): 92 | """Updates path, path load, unprocessed nodes.""" 93 | self._load += self.G.nodes[self._new_node]["demand"] 94 | last_node = self._current_path[-1] 95 | self._current_path.append(self._new_node) 96 | if self._new_node not in ["Source", "Sink"]: 97 | self._unprocessed_nodes.remove(self._new_node) 98 | self._stops += 1 99 | self._best_value += self.G.edges[last_node, self._new_node]["cost"] 100 | self._time += ( 101 | self.G.edges[last_node, self._new_node]["time"] 102 | + self.G.nodes[self._new_node]["service_time"] 103 | ) 104 | if self._stops == self.num_stops and self._new_node != "Sink": 105 | # End path 106 | self._current_path.append("Sink") 107 | if self._new_node in self.G.predecessors("Sink"): 108 | self._best_value += self.G.edges[self._new_node, "Sink"]["cost"] 109 | self._new_node = "Sink" 110 | else: 111 | self._best_value += 1e10 112 | self._current_path = None 113 | 114 | def _update_routes(self): 115 | """Stores best routes as list of nodes.""" 116 | if self._current_path: 117 | self._best_routes.append(self._current_path) 118 | 119 | def _check_source_sink(self, v): 120 | """Checks if edge Source Sink.""" 121 | return self._last_node == "Source" and v == "Sink" 122 | 123 | def _check_capacity(self, v): 124 | """Checks capacity constraint.""" 125 | return self._load + self.G.nodes[v]["demand"] <= self.load_capacity 126 | 127 | def _check_duration(self, v): 128 | """Checks duration constraint.""" 129 | u = self._current_path[-1] 130 | return_time = self.G.edges[v, "Sink"]["time"] if v != "Sink" else 0 131 | return ( 132 | self._time 133 | + self.G.nodes[v]["service_time"] 134 | + self.G.edges[u, v]["time"] 135 | + return_time 136 | <= self.duration 137 | ) 138 | 139 | def _format_cost(self): 140 | """If list of costs is given, first item of list is considered.""" 141 | for (i, j) in self.G.edges(): 142 | if isinstance(self.G.edges[i, j]["cost"], list): 143 | self.G.edges[i, j]["cost"] = self.G.edges[i, j]["cost"][0] 144 | -------------------------------------------------------------------------------- /vrpy/masterproblem.py: -------------------------------------------------------------------------------- 1 | class _MasterProblemBase: 2 | """Base class for the master problems. 3 | 4 | Args: 5 | G (DiGraph): Underlying network. 6 | routes_with_node (dict): Keys : nodes ; Values : list of routes which contain the node 7 | routes (list): Current routes/variables/columns. 8 | drop_penalty (int, optional): Value of penalty if node is dropped. Defaults to None. 9 | num_vehicles (int, optional): Maximum number of vehicles. Defaults to None. 10 | use_all_vehicles (bool, optional): True if all vehicles specified by num_vehicles should be used. Defaults to False. 11 | periodic (bool, optional): True if vertices are to be visited periodically. Defaults to False. 12 | minimize_global_span (bool, optional): True if global span (maximum distance) is minimized. Defaults to False. 13 | solver (str): Name of solver to use. 14 | """ 15 | 16 | def __init__( 17 | self, 18 | G, 19 | routes_with_node, 20 | routes, 21 | drop_penalty, 22 | num_vehicles, 23 | use_all_vehicles, 24 | periodic, 25 | minimize_global_span, 26 | solver, 27 | ): 28 | self.G = G 29 | self.routes_with_node = routes_with_node 30 | self.routes = routes 31 | self.drop_penalty = drop_penalty 32 | self.num_vehicles = num_vehicles 33 | self.use_all_vehicles = use_all_vehicles 34 | self.periodic = periodic 35 | self.minimize_global_span = minimize_global_span 36 | self.solver = solver 37 | -------------------------------------------------------------------------------- /vrpy/preprocessing.py: -------------------------------------------------------------------------------- 1 | def get_num_stops_upper_bound(G, 2 | max_capacity, 3 | num_stops=None, 4 | distribution_collection=False): 5 | """ 6 | Finds upper bound on number of stops, from here : 7 | https://pubsonline.informs.org/doi/10.1287/trsc.1050.0118 8 | 9 | A knapsack problem is solved to maximize the number of 10 | visits, subject to capacity constraints. 11 | """ 12 | # Maximize sum of vertices such that sum of demands respect capacity constraints 13 | demands = [int(G.nodes[v]["demand"]) for v in G.nodes()] 14 | # Solve the knapsack problem 15 | max_num_stops = _knapsack(demands, max_capacity) 16 | if distribution_collection: 17 | collect = [int(G.nodes[v]["collect"]) for v in G.nodes()] 18 | max_num_stops = min(max_num_stops, _knapsack(collect, max_capacity)) 19 | # Update num_stops attribute 20 | if num_stops: 21 | num_stops = min(max_num_stops, num_stops) 22 | else: 23 | num_stops = max_num_stops 24 | return num_stops 25 | 26 | 27 | def _knapsack(weights, capacity): 28 | """ 29 | Binary knapsack solver with identical profits of weight 1. 30 | Args: 31 | weights (list) : list of integers 32 | capacity (int) : maximum capacity 33 | Returns: 34 | (int) : maximum number of objects 35 | """ 36 | n = len(weights) 37 | # sol : [items, remaining capacity] 38 | sol = [[0] * (capacity + 1) for i in range(n)] 39 | added = [[False] * (capacity + 1) for i in range(n)] 40 | for i in range(n): 41 | for j in range(capacity + 1): 42 | if weights[i] > j: 43 | sol[i][j] = sol[i - 1][j] 44 | else: 45 | sol_add = 1 + sol[i - 1][j - weights[i]] 46 | if sol_add > sol[i - 1][j]: 47 | sol[i][j] = sol_add 48 | added[i][j] = True 49 | else: 50 | sol[i][j] = sol[i - 1][j] 51 | return sol[n - 1][capacity] 52 | -------------------------------------------------------------------------------- /vrpy/restricted_master_heuristics.py: -------------------------------------------------------------------------------- 1 | """ 2 | File to hold different restricted master heuristics. 3 | Will possible move into a single class after implemnting the algorithm 4 | in issue #60 if there is enough overlap. 5 | 6 | Currently implemented is 7 | - Diving heuristic 8 | """ 9 | import logging 10 | from typing import List, Optional 11 | 12 | import pulp 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class _DivingHeuristic: 18 | """Implements diving algorithm with Limited Discrepancy Search 19 | Parameters as suggested by the authors. This only fixes one column. 20 | `Sadykov et al. (2019)`_. 21 | 22 | .. _Sadykov et al. (2019): https://pubsonline.informs.org/doi/abs/10.1287/ijoc.2018.0822 23 | """ 24 | 25 | def __init__(self, max_depth: int = 3, max_discrepancy: int = 1): 26 | self.max_depth = max_depth 27 | self.max_discrepancy = max_discrepancy 28 | self.depth = 0 29 | self.current_node = _LPNode() 30 | self.tabu_list = [] 31 | 32 | def run_dive(self, prob): 33 | tabu_list = [] 34 | relax = prob.deepcopy() 35 | # Init current_node 36 | if self.current_node.parent is None: 37 | self.current_node.parent = relax 38 | lp_node = _LPNode(self.current_node) 39 | constrs = {} 40 | while self.depth <= self.max_depth and len(tabu_list) < self.max_discrepancy: 41 | non_integer_vars = [ 42 | var 43 | for var in relax.variables() 44 | if abs(var.varValue - round(var.varValue)) != 0 45 | ] 46 | # All non-integer variables not already fixed in this or any 47 | # iteration of the diving heuristic 48 | vars_to_fix = [ 49 | var 50 | for var in non_integer_vars 51 | if var.name not in self.current_node.tabu_list 52 | and var.name not in tabu_list 53 | ] 54 | if vars_to_fix: 55 | # If non-integer variables not already fixed and 56 | # max_discrepancy not violated 57 | 58 | var_to_fix = min( 59 | vars_to_fix, key=lambda x: abs(x.varValue - round(x.varValue)) 60 | ) 61 | value_to_fix = 1 62 | value_previous = var_to_fix.varValue 63 | 64 | name_le = "fix_{}_LE".format(var_to_fix.name) 65 | name_ge = "fix_{}_GE".format(var_to_fix.name) 66 | constrs[name_le] = pulp.LpConstraint( 67 | var_to_fix, pulp.LpConstraintLE, name=name_le, rhs=value_to_fix 68 | ) 69 | constrs[name_ge] = pulp.LpConstraint( 70 | var_to_fix, pulp.LpConstraintGE, name=name_ge, rhs=value_to_fix 71 | ) 72 | 73 | relax += constrs[name_le] # add <= constraint 74 | relax += constrs[name_ge] # add >= constraint 75 | relax.resolve() 76 | tabu_list.append(var_to_fix.name) 77 | self.depth += 1 78 | # if not optimal status code from : 79 | # https://github.com/coin-or/pulp/blob/master/pulp/constants.py#L45-L57 80 | if relax.status == 1: 81 | prob.extend(constrs) 82 | self.current_node = lp_node 83 | else: 84 | # Backtrack 85 | self.current_node = self.current_node.parent 86 | logger.info( 87 | "fixed %s with previous value %s", var_to_fix.name, value_previous 88 | ) 89 | 90 | else: 91 | break 92 | self.current_node.tabu_list.extend(tabu_list) # Update global tabu list 93 | 94 | 95 | class _LPNode: 96 | def __init__(self, parent=None, tabu_list=[]): 97 | self.parent = parent 98 | self.tabu_list = tabu_list 99 | -------------------------------------------------------------------------------- /vrpy/schedule.py: -------------------------------------------------------------------------------- 1 | import pulp 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class _Schedule: 8 | """ 9 | Scheduling algorithm for the Periodic CVRP. 10 | 11 | Args: 12 | G (DiGraph): Graph on which algorithm is run. 13 | time_span (int): Time horizon. 14 | routes (list): List of best routes previously computed (VehicleRoutingProblem.best_routes). 15 | route_type (dict): Key: route ID; Value: vehicle_type (VehicleRoutingProblem.best_routes_type). 16 | num_vehicles (list, optional): Maximum number of vehicles available (per day) for each type of vehicle. Defaults to None. 17 | """ 18 | 19 | def __init__( 20 | self, G, time_span, routes, route_type, num_vehicles=None, solver="cbc" 21 | ): 22 | self.G = G 23 | self.time_span = time_span 24 | self.routes = routes 25 | self.route_type = route_type 26 | self.num_vehicles = num_vehicles 27 | self.solver = solver 28 | 29 | # create problem 30 | self.prob = pulp.LpProblem("Schedule", pulp.LpMinimize) 31 | # create variables 32 | # y[r][t] = 1 <=> route r is scheduled on day t 33 | self.y = pulp.LpVariable.dicts( 34 | "y", 35 | ( 36 | self.routes, 37 | [t for t in range(self.time_span)], 38 | ), 39 | lowBound=0, 40 | upBound=1, 41 | cat=pulp.LpBinary, 42 | ) 43 | # max load 44 | self.load_max = pulp.LpVariable("load_max", lowBound=0, cat=pulp.LpContinuous) 45 | # min load 46 | self.load_min = pulp.LpVariable("load_min", lowBound=0, cat=pulp.LpContinuous) 47 | 48 | def solve(self, time_limit): 49 | """Formulates the scheduling problem as a linear program and solves it.""" 50 | 51 | logger.info("Computing schedule.") 52 | self._formulate() 53 | self._solve(time_limit) 54 | # self.prob.writeLP("schedule.lp") 55 | logger.debug("Status: %s" % pulp.LpStatus[self.prob.status]) 56 | logger.debug("Objective %s" % pulp.value(self.prob.objective)) 57 | 58 | def _formulate(self): 59 | """Scheduling problem as LP.""" 60 | 61 | # objective function : balance load over planning planning period 62 | self.prob += self.load_max - self.load_min 63 | 64 | # load_max definition 65 | for t in range(self.time_span): 66 | self.prob += ( 67 | pulp.lpSum([self.y[r][t] for r in self.routes]) <= self.load_max, 68 | "load_max_%s" % t, 69 | ) 70 | 71 | # load_min definition 72 | for t in range(self.time_span): 73 | self.prob += ( 74 | pulp.lpSum([self.y[r][t] for r in self.routes]) >= self.load_min, 75 | "load_min_%s" % t, 76 | ) 77 | 78 | # one day per route 79 | for r in self.routes: 80 | self.prob += ( 81 | pulp.lpSum([self.y[r][t] for t in range(self.time_span)]) == 1, 82 | "schedule_%s" % r, 83 | ) 84 | # at most one visit per day per customer 85 | for t in range(self.time_span): 86 | for v in self.G.nodes(): 87 | if self.G.nodes[v]["demand"] > 0: 88 | self.prob += ( 89 | pulp.lpSum( 90 | [self.y[r][t] for r in self.routes if v in self.routes[r]] 91 | ) 92 | <= 1, 93 | "day_%s_max_visit_%s" % (t, v), 94 | ) 95 | # max fleet per day 96 | if self.num_vehicles: 97 | for k in range(len(self.num_vehicles)): 98 | for t in range(self.time_span): 99 | self.prob += ( 100 | pulp.lpSum( 101 | [ 102 | self.y[r][t] 103 | for r in self.routes 104 | if self.route_type[r] == k 105 | ] 106 | ) 107 | <= self.num_vehicles[k], 108 | "max_fleet_type_%s_day_%s" % (k, t), 109 | ) 110 | 111 | def _solve(self, time_limit): 112 | if self.solver == "cbc": 113 | self.prob.solve(pulp.PULP_CBC_CMD(msg=False, timeLimit=time_limit)) 114 | elif self.solver == "cplex": 115 | self.prob.solve(pulp.CPLEX_CMD(msg=False, timelimit=time_limit)) 116 | elif self.solver == "gurobi": 117 | gurobi_options = [] 118 | if time_limit is not None: 119 | gurobi_options.append( 120 | ( 121 | "TimeLimit", 122 | time_limit, 123 | ) 124 | ) 125 | self.prob.solve(pulp.GUROBI(msg=False, options=gurobi_options)) 126 | logger.info("%s" % (pulp.LpStatus[self.prob.status])) 127 | 128 | @property 129 | def routes_per_day(self): 130 | """Returns a dict with keys the day and values the route IDs scheduled this day.""" 131 | day = {} 132 | if pulp.LpStatus[self.prob.status] == "Optimal": 133 | for r in self.routes: 134 | for t in range(self.time_span): 135 | if pulp.value(self.y[r][t]) > 0.9: 136 | if t not in day: 137 | day[t] = [r] 138 | else: 139 | day[t].append(r) 140 | else: 141 | logger.info("No feasible schedule found.") 142 | return day 143 | -------------------------------------------------------------------------------- /vrpy/subproblem.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from itertools import islice 3 | 4 | from networkx import ( 5 | compose_all, 6 | DiGraph, 7 | NetworkXException, 8 | add_path, 9 | has_path, 10 | shortest_simple_paths, 11 | ) 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class _SubProblemBase: 17 | """ 18 | Base class for the subproblems. 19 | 20 | Args: 21 | G (DiGraph): Underlying network. 22 | duals (dict): Dual values of master problem. 23 | routes_with_node (dict): Keys : nodes ; Values : list of routes which contain the node. 24 | routes (list): Current routes/variables/columns. 25 | vehicle_type (int): Current vehicle type. 26 | route (DiGraph): 27 | Current route. 28 | Is not None if pricing problem is route dependent (e.g, when minimizing global span). 29 | 30 | Attributes: 31 | num_stops (int, optional): 32 | Maximum number of stops. 33 | If not provided, constraint not enforced. 34 | load_capacity (int, optional): 35 | Maximum capacity. 36 | If not provided, constraint not enforced. 37 | duration (int, optional): 38 | Maximum duration. 39 | If not provided, constraint not enforced. 40 | time_windows (bool, optional): 41 | True if time windows activated. 42 | Defaluts to False. 43 | pickup_delivery (bool, optional): 44 | True if pickup and delivery constraints. 45 | Defaults to False. 46 | distribution_collection (bool, optional): 47 | True if distribution and collection are simultaneously enforced. 48 | Defaults to False. 49 | sub_G (DiGraph): 50 | Subgraph of G. 51 | The subproblem is based on sub_G. 52 | run_subsolve (boolean): 53 | True if the subproblem is solved. 54 | pricing_strategy (string): 55 | Strategy used for solving subproblem. 56 | Either "Exact", "BestEdges1", "BestEdges2", "BestPaths". 57 | Defaults to "BestEdges1". 58 | pricing_parameter (float): 59 | Parameter used depending on pricing_strategy. 60 | Defaults to None. 61 | """ 62 | 63 | def __init__( 64 | self, 65 | G, 66 | duals, 67 | routes_with_node, 68 | routes, 69 | vehicle_type, 70 | route=None, 71 | num_stops=None, 72 | load_capacity=None, 73 | duration=None, 74 | time_windows=False, 75 | pickup_delivery=False, 76 | distribution_collection=False, 77 | pricing_strategy="Exact", 78 | pricing_parameter=None, 79 | ): 80 | # Input attributes 81 | self.G = G 82 | self.duals = duals 83 | self.routes_with_node = routes_with_node 84 | self.routes = routes 85 | self.vehicle_type = vehicle_type 86 | self.route = route 87 | self.num_stops = num_stops 88 | self.load_capacity = load_capacity 89 | self.duration = duration 90 | self.time_windows = time_windows 91 | self.pickup_delivery = pickup_delivery 92 | self.distribution_collection = distribution_collection 93 | self.run_subsolve = True 94 | 95 | # Add reduced cost to "weight" attribute 96 | self.add_reduced_cost_attribute() 97 | # print(self.duals) 98 | # for (i, j) in self.G.edges(): 99 | # print(i, j, self.G.edges[i, j]) 100 | 101 | # Define the graph on which the sub problem is solved according to the pricing strategy 102 | if pricing_strategy == "BestEdges1": 103 | # The graph is pruned 104 | self.remove_edges_1(pricing_parameter) 105 | elif pricing_strategy == "BestEdges2": 106 | # The graph is pruned 107 | self.remove_edges_2(pricing_parameter) 108 | elif pricing_strategy == "BestPaths": 109 | # The graph is pruned 110 | self.remove_edges_3(pricing_parameter) 111 | 112 | elif pricing_strategy == "Exact": 113 | # The graph remains as is 114 | self.sub_G = self.G 115 | logger.debug("Pricing strategy %s, %s" % (pricing_strategy, pricing_parameter)) 116 | 117 | def add_reduced_cost_attribute(self): 118 | """Substracts the dual values to compute reduced cost on each edge.""" 119 | for edge in self.G.edges(data=True): 120 | edge[2]["weight"] = edge[2]["cost"][self.vehicle_type] 121 | if self.route: 122 | edge[2]["weight"] *= -self.duals[ 123 | "makespan_%s" % self.route.graph["name"] 124 | ] 125 | for v in self.duals: 126 | if edge[0] == v: 127 | edge[2]["weight"] -= self.duals[v] 128 | if "upper_bound_vehicles" in self.duals: 129 | for v in self.G.successors("Source"): 130 | self.G.edges["Source", v]["weight"] -= self.duals[ 131 | "upper_bound_vehicles" 132 | ][self.vehicle_type] 133 | 134 | def discard_nodes(self): 135 | """Removes nodes with marginal cost = 0.""" 136 | for v in self.duals: 137 | if v != "upper_bound_vehicles" and self.duals[v] == 0: 138 | self.sub_G.remove_node(v) 139 | print("removed node", v) 140 | 141 | def remove_edges_1(self, alpha): 142 | """ 143 | Removes edges based on criteria described here : 144 | https://pubsonline.informs.org/doi/10.1287/trsc.1050.0118 145 | 146 | Edges for which [cost > alpha x largest dual value] are removed, 147 | where 0 < alpha < 1 is a parameter. 148 | """ 149 | self.sub_G = self.G.copy() 150 | largest_dual = max( 151 | self.duals[v] for v in self.duals if v != "upper_bound_vehicles" 152 | ) 153 | 154 | for (u, v) in self.G.edges(): 155 | if self.G.edges[u, v]["cost"][self.vehicle_type] > alpha * largest_dual: 156 | self.sub_G.remove_edge(u, v) 157 | # If pruning the graph disconnects the source and the sink, 158 | # do not solve the subproblem. 159 | try: 160 | if not has_path(self.sub_G, "Source", "Sink"): 161 | self.run_subsolve = False 162 | except NetworkXException: 163 | self.run_subsolve = False 164 | 165 | def remove_edges_2(self, ratio): 166 | """ 167 | Removes edges based on criteria described here : 168 | https://www.sciencedirect.com/science/article/abs/pii/S0377221717306045 169 | 170 | Edges are sorted by non decreasing reduced cost, and only 171 | the K|E| ones with lowest reduced cost are kept, where K is a parameter (ratio). 172 | """ 173 | self.sub_G = self.G.copy() 174 | # Sort the edges by non decreasing reduced cost 175 | reduced_cost = {} 176 | for (u, v) in self.G.edges(): 177 | if u != "Source" and v != "Sink": 178 | reduced_cost[(u, v)] = self.G.edges[u, v]["weight"] 179 | sorted_edges = sorted(reduced_cost, key=reduced_cost.get) 180 | # Keep the best ones 181 | limit = int(ratio * len(sorted_edges)) 182 | self.sub_G.remove_edges_from(sorted_edges[limit:]) 183 | # If pruning the graph disconnects the source and the sink, 184 | # do not solve the subproblem. 185 | try: 186 | if not has_path(self.sub_G, "Source", "Sink"): 187 | self.run_subsolve = False 188 | except NetworkXException: 189 | self.run_subsolve = False 190 | 191 | def remove_edges_3(self, beta): 192 | """ 193 | Heuristic pruning: 194 | 1. Normalize weights in interval [-1,1] 195 | 2. Set all negative weights to 0 196 | 3. Compute beta shortest paths (beta is a paramater) 197 | https://networkx.github.io/documentation/networkx-1.10/reference/generated/networkx.algorithms.simple_paths.shortest_simple_paths.html 198 | 4. Remove all edges that do not belong to these paths 199 | """ 200 | # Normalize weights 201 | max_weight = max(self.G.edges[i, j]["weight"] for (i, j) in self.G.edges()) 202 | min_weight = min(self.G.edges[i, j]["weight"] for (i, j) in self.G.edges()) 203 | for edge in self.G.edges(data=True): 204 | edge[2]["pos_weight"] = ( 205 | -max_weight - min_weight + 2 * edge[2]["weight"] 206 | ) / (max_weight - min_weight) 207 | edge[2]["pos_weight"] = max(0, edge[2]["pos_weight"]) 208 | # Compute beta shortest paths 209 | best_paths = list( 210 | islice( 211 | shortest_simple_paths(self.G, "Source", "Sink", weight="pos_weight"), 212 | beta, 213 | ) 214 | ) 215 | # Store these paths as a list of DiGraphs 216 | best_paths_list = [] 217 | for path in best_paths: 218 | H = DiGraph() 219 | add_path(H, path) 220 | best_paths_list.append(H) 221 | # Merge the paths into one graph 222 | induced_graph = compose_all(best_paths_list) 223 | # Create subgraph induced by the edges of this graph 224 | self.sub_G = self.G.edge_subgraph(induced_graph.edges()).copy() 225 | -------------------------------------------------------------------------------- /vrpy/subproblem_greedy.py: -------------------------------------------------------------------------------- 1 | from random import choice 2 | from networkx import DiGraph, add_path 3 | 4 | # from os import sys 5 | # sys.path.append("../") 6 | from .subproblem import _SubProblemBase 7 | import logging 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class _SubProblemGreedy(_SubProblemBase): 13 | """ 14 | Solves the sub problem for the column generation procedure with 15 | a greedy randomised heuristic. 16 | Described here: https://www.sciencedirect.com/science/article/abs/pii/S0377221717306045 17 | 18 | Inherits problem parameters from `SubproblemBase` 19 | """ 20 | 21 | def __init__(self, *args): 22 | # Pass arguments to base 23 | super(_SubProblemGreedy, self).__init__(*args) 24 | 25 | def _initialize_run(self): 26 | self._load = 0 27 | self._time = 0 28 | self._stops = 0 29 | self._weight = 0 30 | 31 | def solve(self, n_runs=5): 32 | """The forward and backwards search are run.""" 33 | more_routes = False 34 | # The forward search is run n_runs times 35 | for _ in range(n_runs): 36 | self._initialize_run() 37 | self.run_forward() 38 | if self._new_node and self._weight < 0: 39 | logger.debug("negative column %s" % self._weight) 40 | more_routes = True 41 | self._add_new_route() 42 | # The backwards search is run n_runs times 43 | for _ in range(n_runs): 44 | self._initialize_run() 45 | self.run_backwards() 46 | if self._new_node and self._weight < 0: 47 | logger.debug("negative column %s" % self._weight) 48 | more_routes = True 49 | self._add_new_route() 50 | return self.routes, more_routes 51 | 52 | def run_forward(self): 53 | """ 54 | A path starting from Source is randomly greedily extended 55 | until Sink is reached. 56 | The procedure aborts if path becomes infeasible. 57 | """ 58 | self._current_path = ["Source"] 59 | extend = True 60 | new_node = True 61 | while extend and new_node: 62 | new_node = self._get_next_node() 63 | extend = self._update(forward=True) 64 | 65 | def _get_next_node(self): 66 | self._last_node = self._current_path[-1] 67 | out_going_costs = {} 68 | # Store the successors reduced cost that meet constraints 69 | for v in self.sub_G.successors(self._last_node): 70 | if self._constraints_met(v, forward=True): 71 | out_going_costs[v] = self.sub_G.edges[self._last_node, v]["weight"] 72 | if out_going_costs == {}: 73 | logger.debug("path cannot be extended") 74 | self._new_node = None 75 | return False 76 | else: 77 | # Randomly select a node among the 5 best ones 78 | pool = sorted(out_going_costs, key=out_going_costs.get)[:5] 79 | self._new_node = choice(pool) 80 | return True 81 | 82 | def _constraints_met(self, v, forward): 83 | """Checks if constraints are respected.""" 84 | if v in self._current_path or self._check_source_sink(v): 85 | return False 86 | elif self.load_capacity and not self._check_capacity(v): 87 | return False 88 | elif self.duration and not self._check_duration(v, forward): 89 | return False 90 | else: 91 | return True 92 | 93 | def run_backwards(self): 94 | self._current_path = ["Sink"] 95 | extend = True 96 | new_node = True 97 | while extend and new_node: 98 | new_node = self._get_previous_node() 99 | extend = self._update(forward=False) 100 | 101 | def _get_previous_node(self): 102 | self._last_node = self._current_path[0] 103 | incoming_costs = {} 104 | # Store the reduced costs of the predecessors that meet constraints 105 | for v in self.sub_G.predecessors(self._last_node): 106 | if self._constraints_met(v, forward=False): 107 | incoming_costs[v] = self.sub_G.edges[v, self._last_node]["weight"] 108 | if not incoming_costs: 109 | logger.debug("path cannot be extended") 110 | self._new_node = None 111 | return False 112 | else: 113 | # Randomly select a node among the 5 best ones 114 | pool = sorted(incoming_costs, key=incoming_costs.get)[:5] 115 | self._new_node = choice(pool) 116 | return True 117 | 118 | def _update(self, forward): 119 | """Updates path, path load, path time, path weight.""" 120 | if not self._new_node: 121 | return 122 | 123 | self._stops += 1 124 | self._load += self.sub_G.nodes[self._new_node]["demand"] 125 | if forward: 126 | self._weight += self.sub_G.edges[self._last_node, self._new_node]["weight"] 127 | self._time += self.sub_G.edges[self._last_node, self._new_node]["time"] 128 | self._current_path.append(self._new_node) 129 | if self._stops == self.num_stops and self._new_node != "Sink": 130 | # Finish path 131 | if self._new_node in self.sub_G.predecessors("Sink"): 132 | self._current_path.append("Sink") 133 | else: 134 | self._new_node = False 135 | return False 136 | elif self._new_node == "Sink": 137 | return False 138 | else: 139 | self._weight += self.sub_G.edges[self._new_node, self._last_node]["weight"] 140 | self._time += self.sub_G.edges[self._new_node, self._last_node]["time"] 141 | self._current_path.insert(0, self._new_node) 142 | if self._stops == self.num_stops and self._new_node != "Source": 143 | # Finish path 144 | if self._new_node in self.sub_G.successors("Sink"): 145 | self._current_path.insert(0, "Source") 146 | else: 147 | self._new_node = False 148 | return False 149 | elif self._new_node == "Source": 150 | return False 151 | return True 152 | 153 | def _add_new_route(self): 154 | """Create new route as DiGraph and add to pool of columns""" 155 | route_id = len(self.routes) + 1 156 | new_route = DiGraph(name=route_id) 157 | add_path(new_route, self._current_path) 158 | self.total_cost = 0 159 | for (i, j) in new_route.edges(): 160 | edge_cost = self.sub_G.edges[i, j]["cost"][self.vehicle_type] 161 | self.total_cost += edge_cost 162 | new_route.edges[i, j]["cost"] = edge_cost 163 | if i != "Source": 164 | self.routes_with_node[i].append(new_route) 165 | new_route.graph["cost"] = self.total_cost 166 | new_route.graph["vehicle_type"] = self.vehicle_type 167 | self.routes.append(new_route) 168 | 169 | def _check_source_sink(self, v): 170 | """Checks if edge Source Sink.""" 171 | return self._last_node == "Source" and v == "Sink" 172 | 173 | def _check_capacity(self, v): 174 | """Checks capacity constraint.""" 175 | return ( 176 | self._load + self.sub_G.nodes[v]["demand"] 177 | <= self.load_capacity[self.vehicle_type] 178 | ) 179 | 180 | def _check_duration(self, v, forward): 181 | """Checks time constraint.""" 182 | if forward: 183 | return ( 184 | self._time + self.sub_G.edges[self._last_node, v]["time"] 185 | <= self.duration 186 | ) 187 | else: 188 | return ( 189 | self._time + self.sub_G.edges[v, self._last_node]["time"] 190 | <= self.duration 191 | ) 192 | 193 | """ 194 | NOT IMPLEMENTED YET 195 | def _check_time_windows(self, v, forward): 196 | #Checks time window feasibility 197 | if forward: 198 | return ( 199 | self._time + self.sub_G.edges[self._last_node, v]["time"] 200 | <= self.sub_G.nodes[v]["upper"] 201 | ) 202 | else: 203 | return ( 204 | self._time + self.sub_G.edges[v, self._last_node]["time"] 205 | <= self.sub_G.nodes[v]["upper"] 206 | ) 207 | """ 208 | --------------------------------------------------------------------------------