├── src ├── vroom │ ├── input │ │ ├── __init__.py │ │ ├── forced_service.py │ │ ├── input.py │ │ └── vehicle_step.py │ ├── solution │ │ ├── __init__.py │ │ └── solution.py │ ├── __init__.py │ ├── amount.py │ ├── time_window.py │ ├── break_.py │ ├── location.py │ ├── vehicle.py │ └── job.py ├── bind │ ├── exception.cpp │ ├── utils.cpp │ ├── _main.cpp │ ├── time_window.cpp │ ├── break.cpp │ ├── location.cpp │ ├── solution │ │ ├── summary.cpp │ │ ├── step.cpp │ │ ├── route.cpp │ │ └── solution.cpp │ ├── generic │ │ └── matrix.cpp │ ├── amount.cpp │ ├── enums.cpp │ ├── input │ │ ├── vehicle_step.cpp │ │ └── input.cpp │ ├── job.cpp │ └── vehicle.cpp └── _vroom.cpp ├── MANIFEST.in ├── conanfile.txt ├── .gitmodules ├── conftest.py ├── codecov.yml ├── Makefile ├── setup.cfg ├── pyproject.toml ├── test ├── test_file_handle.py ├── test_break.py ├── test_amount.py ├── test_vehicle.py ├── test_libvroom_examples.py ├── test_time_window.py ├── test_location.py ├── test_job.py └── input │ └── test_vehicle_step.py ├── LICENSE ├── .github └── workflows │ ├── pull_request.yml │ ├── main_push.yml │ └── release.yml ├── .gitignore ├── setup.py └── README.rst /src/vroom/input/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/vroom/solution/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | recursive-include vroom * 3 | -------------------------------------------------------------------------------- /conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | openssl/1.1.1m 3 | asio/1.21.0 4 | 5 | [generators] 6 | json 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vroom"] 2 | path = vroom 3 | url = https://github.com/VROOM-Project/vroom 4 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | """Global configuration.""" 2 | import pytest 3 | import vroom 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def global_setup(doctest_namespace, monkeypatch): 8 | """Global configuration setup.""" 9 | doctest_namespace["vroom"] = vroom 10 | -------------------------------------------------------------------------------- /src/bind/exception.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/exception.cpp" 2 | 3 | void init_exception(py::module_ &m) { 4 | py::register_exception(m, "VroomInternalException"); 5 | py::register_exception(m, "VroomInputException"); 6 | py::register_exception(m, "VroomRoutingException"); 7 | } 8 | -------------------------------------------------------------------------------- /src/bind/utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "structures/typedefs.h" 4 | 5 | void init_utils(py::module_ &m) { 6 | 7 | m.def("scale_from_user_duration", &vroom::utils::scale_from_user_duration, 8 | py::arg("duration")); 9 | m.def("scale_to_user_duration", &vroom::utils::scale_to_user_duration, 10 | py::arg("duration")); 11 | m.def("scale_to_user_cost", &vroom::utils::scale_to_user_cost, 12 | py::arg("cost")); 13 | } 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 1 3 | range: 80...100 4 | status: 5 | project: 6 | default: 7 | enabled: yes 8 | target: 1 9 | threshold: 0.1 10 | 11 | comment: 12 | layout: "header, diff, flags" 13 | behavior: default 14 | require_head: no 15 | branches: 16 | - main 17 | 18 | flags: 19 | python: 20 | paths: 21 | - src/vroom/ 22 | binding: 23 | paths: 24 | - src/_vroom.cpp 25 | - src/bind/ 26 | vroom: 27 | paths: 28 | - vroom/src/ 29 | 30 | parsers: 31 | gcov: 32 | branch_detection: 33 | method: yes 34 | -------------------------------------------------------------------------------- /src/bind/_main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "main.cpp" 5 | 6 | namespace py = pybind11; 7 | 8 | void init_main(py::module_ &m) { 9 | 10 | m.def( 11 | "_main", 12 | [](std::vector args) { 13 | char **argv = new char *[args.size()]; 14 | for (size_t i = 0; i < args.size(); i++) { 15 | argv[i] = new char[args[i].size() + 1]; 16 | strcpy(argv[i], args[i].c_str()); 17 | } 18 | py::scoped_ostream_redirect stream( 19 | std::cout, py::module_::import("sys").attr("stdout")); 20 | main(args.size(), argv); 21 | }, 22 | py::arg("args")); 23 | } 24 | -------------------------------------------------------------------------------- /src/bind/time_window.cpp: -------------------------------------------------------------------------------- 1 | #include "structures/vroom/time_window.cpp" 2 | 3 | #include 4 | 5 | namespace py = pybind11; 6 | 7 | void init_time_window(py::module_ &m) { 8 | 9 | py::class_(m, "TimeWindow") 10 | .def(py::init([]() { return new vroom::TimeWindow(); })) 11 | .def(py::init([](vroom::Duration start, vroom::Duration end) { 12 | return new vroom::TimeWindow(start, end); 13 | }), 14 | py::arg("start"), py::arg("end")) 15 | .def("_is_default", &vroom::TimeWindow::is_default) 16 | .def(py::self < py::self) 17 | .def_readwrite("_start", &vroom::TimeWindow::start) 18 | .def_readwrite("_end", &vroom::TimeWindow::end) 19 | .def_readonly_static("_DEFAULT_LENGTH", 20 | &vroom::TimeWindow::default_length); 21 | } 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "develop Install package in developer mode with coverage support" 3 | @echo "test Run all tests with coverage (pytest, coverage.py, gcov)" 4 | @echo "lint Run all linting (black, flake8, mypy)" 5 | @echo "format Format code (black, clang-format-10)" 6 | 7 | 8 | .PHONY: help test develop format 9 | 10 | develop: 11 | python -m pip install -e . 12 | 13 | test: 14 | coverage run -m pytest --doctest-modules README.rst test src/vroom 15 | mkdir -p coverage 16 | coverage xml -o coverage/coverage.xml 17 | gcov -abcfumlpr -o build/temp*/src src/_vroom.cpp 18 | mv *.gcov coverage 19 | 20 | lint: 21 | python -m black --check src/vroom 22 | python -m flake8 src/vroom 23 | python -m mypy src/vroom 24 | 25 | format: 26 | @echo format python code with black 27 | @python -m black src/vroom 28 | @echo format c++ code with clang-format 29 | @find src -type f -name '*.cpp' | xargs -I{} clang-format-14 -i -style=file {} 30 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | version = 0.1.0 3 | name = pyvroom 4 | description = Vehicle routing open-source optimization machine (VROOM) 5 | long_description = file: README.rst 6 | long_description_content_type = text/x-rst 7 | license = BSD 2-Clause License 8 | author = Jonathan Feinberg 9 | author_email = jonathf@gmail.com 10 | classifiers = 11 | License :: OSI Approved :: BSD License 12 | Programming Language :: Python :: 3 13 | 14 | [options] 15 | packages = find: 16 | package_dir = 17 | =src 18 | install_requires = 19 | numpy 20 | pandas 21 | python_requires = >=3.9 22 | zip_safe = False 23 | include_package_data = True 24 | 25 | [options.packages.find] 26 | where = src 27 | 28 | [build_ext] 29 | inplace=0 30 | 31 | [flake8] 32 | # ignore some errors to play nicely with black 33 | ignore = E203,E266,E501,W503,F403,E722,F405 34 | per-file-ignores = __init__.py:F401 35 | max-complexity = 15 36 | max-line-length = 105 37 | exclude = .*,__pycache__,*build,dist,wheelhouse 38 | -------------------------------------------------------------------------------- /src/bind/break.cpp: -------------------------------------------------------------------------------- 1 | #include "structures/vroom/break.cpp" 2 | 3 | #include 4 | 5 | namespace py = pybind11; 6 | 7 | void init_break(py::module_ &m) { 8 | 9 | py::class_(m, "Break") 10 | .def(py::init([](vroom::Break &b) { return b; }), py::arg("break")) 11 | .def( 12 | py::init &, vroom::Duration, 13 | std::string &, std::optional &>(), 14 | py::arg("id"), py::arg("time_windows"), py::arg("service"), 15 | py::arg("description"), py::arg("max_load")) 16 | .def("_is_valid_start", &vroom::Break::is_valid_start, py::arg("time")) 17 | .def_readwrite("_id", &vroom::Break::id) 18 | .def_readwrite("_time_windows", &vroom::Break::tws) 19 | .def_readwrite("_service", &vroom::Break::service) 20 | .def_readwrite("_description", &vroom::Break::description) 21 | .def_readwrite("_max_load", &vroom::Break::max_load); 22 | } 23 | -------------------------------------------------------------------------------- /src/vroom/__init__.py: -------------------------------------------------------------------------------- 1 | """Vehicle routing open-source optimization machine (VROOM).""" 2 | 3 | import sys 4 | from typing import Optional, Sequence 5 | from ._vroom import _main, JOB_TYPE, STEP_TYPE # type: ignore 6 | 7 | from .amount import Amount 8 | from .break_ import Break 9 | from .job import Job, ShipmentStep, Shipment 10 | from .location import Location, LocationCoordinates, LocationIndex 11 | from .time_window import TimeWindow 12 | from .vehicle import Vehicle, VehicleCosts 13 | 14 | from .input.forced_service import ForcedService 15 | from .input.input import Input 16 | from .input.vehicle_step import ( 17 | VehicleStep, 18 | VehicleStepStart, 19 | VehicleStepEnd, 20 | VehicleStepBreak, 21 | VehicleStepSingle, 22 | VehicleStepPickup, 23 | VehicleStepDelivery, 24 | VEHICLE_STEP_TYPE, 25 | ) 26 | 27 | 28 | def main(argv: Optional[Sequence[str]] = None) -> None: 29 | """Run VROOM command line interface.""" 30 | _main(sys.argv if argv is None else argv) 31 | -------------------------------------------------------------------------------- /src/bind/location.cpp: -------------------------------------------------------------------------------- 1 | #include "structures/vroom/location.cpp" 2 | 3 | #include 4 | 5 | void init_location(py::module_ &m) { 6 | 7 | py::class_(m, "Coordinates") 8 | .def(py::init(), py::arg("lon"), 9 | py::arg("lat")); 10 | 11 | py::class_(m, "Location") 12 | .def(py::init(), py::arg("index")) 13 | .def(py::init(), py::arg("coords")) 14 | .def(py::init(), py::arg("index"), 15 | py::arg("coords")) 16 | .def(py::init([](vroom::Location &l) { return l; }), py::arg("location")) 17 | .def(py::self == py::self) 18 | // .def("_set_index", &vroom::Location::set_index) 19 | .def("_has_coordinates", &vroom::Location::has_coordinates) 20 | .def("_index", &vroom::Location::index) 21 | .def("_lon", &vroom::Location::lon) 22 | .def("_lat", &vroom::Location::lat) 23 | .def("_user_index", &vroom::Location::user_index); 24 | } 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=45", 4 | "wheel", 5 | "pybind11>=2.8.0", 6 | ] 7 | 8 | build-backend = "setuptools.build_meta" 9 | 10 | [tool.black] 11 | line-length = 105 12 | exclude = ''' 13 | /( 14 | \..* 15 | | dist 16 | | wheelhouse 17 | | .*build 18 | | __pycache__ 19 | )/ 20 | ''' 21 | 22 | [tool.cibuildwheel] 23 | test-command = 'python -c "import vroom"' 24 | build = "cp3{9,10,11,12}-*" 25 | skip = "*musllinux*" 26 | archs = "native" 27 | manylinux-x86_64-image = "quay.io/pypa/manylinux_2_28_x86_64" 28 | manylinux-aarch64-image = "quay.io/pypa/manylinux_2_28_aarch64" 29 | 30 | [tool.cibuildwheel.linux] 31 | before-all = """ 32 | dnf update -y 33 | dnf module enable -y mariadb-devel 34 | dnf install -y openssl-devel asio-devel 35 | """ 36 | archs = ["x86_64", "aarch64"] 37 | 38 | [[tool.cibuildwheel.overrides]] 39 | select = "*musllinux*" 40 | before-all = """ 41 | apk add asio-dev 42 | apk add openssl-dev 43 | """ 44 | [tool.cibuildwheel.macos] 45 | 46 | before-all = """ 47 | brew install --ignore-dependencies asio 48 | """ 49 | -------------------------------------------------------------------------------- /test/test_file_handle.py: -------------------------------------------------------------------------------- 1 | """Assert VROOM command line interface works as upstream.""" 2 | from pathlib import Path 3 | import json 4 | 5 | import pytest 6 | 7 | import vroom 8 | 9 | _FOLDER = Path(__file__).parent.parent.resolve() / "vroom" / "docs" 10 | INPUT_FILE = _FOLDER / "example_2.json" 11 | OUTPUT_FILE = _FOLDER / "example_2_sol.json" 12 | 13 | 14 | def assert_equal(solution, reference): 15 | del solution["summary"]["computing_times"] 16 | assert solution == reference 17 | 18 | 19 | @pytest.fixture 20 | def example_2_reference(): 21 | with OUTPUT_FILE.open() as src: 22 | return json.load(src) 23 | 24 | 25 | def test_console_script(example_2_reference, capsys): 26 | """Run VROOM console script entrypoint.""" 27 | vroom.main(["vroom", "-i", str(INPUT_FILE)]) 28 | output = json.loads(capsys.readouterr().out) 29 | assert_equal(output, example_2_reference) 30 | 31 | 32 | def test_loader(example_2_reference): 33 | input = vroom.Input.from_json(INPUT_FILE) 34 | solution = input.solve(exploration_level=5, nb_threads=4) 35 | assert_equal(solution.to_dict(), example_2_reference) 36 | -------------------------------------------------------------------------------- /src/bind/solution/summary.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "structures/vroom/solution/summary.cpp" 4 | 5 | namespace py = pybind11; 6 | 7 | void init_summary(py::module_ &m) { 8 | 9 | py::class_(m, "Summary") 10 | .def(py::init<>()) 11 | .def(py::init()) 12 | .def_readwrite("cost", &vroom::Summary::cost) 13 | .def_readonly("routes", &vroom::Summary::routes) 14 | .def_readonly("unassigned", &vroom::Summary::unassigned) 15 | .def_readwrite("delivery", &vroom::Summary::delivery) 16 | .def_readwrite("pickup", &vroom::Summary::pickup) 17 | .def_readwrite("setup", &vroom::Summary::setup) 18 | .def_readwrite("service", &vroom::Summary::service) 19 | .def_readwrite("priority", &vroom::Summary::priority) 20 | .def_readwrite("duration", &vroom::Summary::duration) 21 | .def_readwrite("waiting_time", &vroom::Summary::waiting_time) 22 | .def_readwrite("distance", &vroom::Summary::distance) 23 | .def_readwrite("computing_times", &vroom::Summary::computing_times) 24 | .def_readwrite("violations", &vroom::Summary::violations); 25 | } 26 | -------------------------------------------------------------------------------- /src/bind/solution/step.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "structures/vroom/solution/step.cpp" 4 | 5 | namespace py = pybind11; 6 | 7 | void init_step(py::module_ &m) { 8 | 9 | py::class_(m, "Step") 10 | .def(py::init()) 11 | .def(py::init()) 12 | .def(py::init()) 13 | .def_readonly("_step_type", &vroom::Step::step_type) 14 | .def_readonly("_job_type", &vroom::Step::job_type) 15 | .def_readonly("_location", &vroom::Step::location) 16 | .def_readonly("_id", &vroom::Step::id) 17 | .def_readonly("_setup", &vroom::Step::setup) 18 | .def_readonly("_service", &vroom::Step::service) 19 | .def_readonly("_load", &vroom::Step::load) 20 | .def_readonly("_description", &vroom::Step::description) 21 | .def_readwrite("_arrival", &vroom::Step::arrival) 22 | .def_readwrite("_duration", &vroom::Step::duration) 23 | .def_readwrite("_waiting_time", &vroom::Step::waiting_time) 24 | .def_readwrite("_distance", &vroom::Step::distance) 25 | .def_readwrite("_violations", &vroom::Step::violations); 26 | } 27 | -------------------------------------------------------------------------------- /test/test_break.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import vroom 3 | 4 | 5 | def test_break_init(): 6 | with pytest.raises(vroom._vroom.VroomInputException): 7 | vroom.Break(4) 8 | with pytest.raises(vroom._vroom.VroomInputException): 9 | vroom.Break(4, service=500) 10 | 11 | break_ = vroom.Break(vroom.Break(4, [(0, 1000)], 500, "hello", [4])) 12 | assert break_.id == 4 13 | assert break_.time_windows == [vroom.TimeWindow(0, 1000)] 14 | assert break_.service == 500 15 | assert break_.description == "hello" 16 | assert break_.max_load == vroom.Amount([4]) 17 | 18 | 19 | def test_break_attributes(): 20 | break_ = vroom.Break(4, [(0, 1000), (2000, 3000)]) 21 | assert break_.is_valid_start(500) 22 | assert not break_.is_valid_start(1500) 23 | assert break_.is_valid_start(2500) 24 | 25 | break_.id = 7 26 | assert break_.id == 7 27 | 28 | break_.time_windows = [(1000, 2000)] 29 | assert break_.time_windows == [vroom.TimeWindow(1000, 2000)] 30 | 31 | break_.service = 9 32 | assert break_.service == 9 33 | 34 | break_.description = "goodbye" 35 | assert break_.description == "goodbye" 36 | 37 | break_.max_load = [7, 8] 38 | assert break_.max_load == vroom.Amount([7, 8]) 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, Jonathan Feinberg 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /src/bind/generic/matrix.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "structures/generic/matrix.h" 4 | 5 | namespace py = pybind11; 6 | 7 | void init_matrix(py::module_ &m) { 8 | 9 | py::class_>(m, "Matrix", py::buffer_protocol()) 10 | .def(py::init(), py::arg("size") = 0) 11 | .def(py::init([](vroom::Matrix &m) { return m; })) 12 | .def(py::init([](const py::buffer &b) { 13 | py::buffer_info info = b.request(); 14 | if (info.format != py::format_descriptor::format() || 15 | info.ndim != 2 || info.shape[0] != info.shape[1]) 16 | throw std::runtime_error("Incompatible buffer format!"); 17 | auto v = new vroom::Matrix(info.shape[0]); 18 | memcpy(v->get_data(), info.ptr, 19 | sizeof(uint32_t) * (size_t)(v->size() * v->size())); 20 | return v; 21 | })) 22 | .def_buffer([](vroom::Matrix &m) -> py::buffer_info { 23 | return py::buffer_info(m.get_data(), sizeof(uint32_t), 24 | py::format_descriptor::format(), 2, 25 | {m.size(), m.size()}, 26 | {sizeof(uint32_t) * m.size(), m.size()}); 27 | }) 28 | .def("get_sub_matrix", &vroom::Matrix::get_sub_matrix) 29 | .def("size", &vroom::Matrix::size); 30 | } 31 | -------------------------------------------------------------------------------- /test/test_amount.py: -------------------------------------------------------------------------------- 1 | import vroom 2 | from vroom import _vroom 3 | 4 | 5 | def test_amount_init(): 6 | amo1 = _vroom.Amount() 7 | assert len(amo1) == 0 8 | amo1._push_back(1) 9 | amo1._push_back(2) 10 | amo1._push_back(3) 11 | assert len(amo1) == 3 12 | 13 | amo2 = vroom.Amount([1, 2, 3]) 14 | 15 | assert amo1 == amo2 16 | assert amo1 != vroom.Amount([3, 2, 1]) 17 | 18 | assert amo1 19 | assert amo2 20 | assert not vroom.Amount() 21 | assert not vroom.Amount([]) 22 | 23 | assert vroom.Amount(vroom.Amount([1, 2])) == vroom.Amount([1, 2]) 24 | assert str(amo2) == "vroom.Amount([1, 2, 3])" 25 | 26 | 27 | def test_amount_operator(): 28 | amo1 = vroom.Amount([1, 2, 3]) 29 | amo2 = vroom.Amount([2, 2, 3]) 30 | 31 | assert amo1 << amo2 32 | assert not (amo2 << amo1) 33 | assert amo2 >> amo1 34 | assert not (amo1 >> amo2) 35 | 36 | assert amo1 <= amo2 37 | assert not (amo2 <= amo1) 38 | assert amo2 >= amo1 39 | assert not (amo1 >= amo2) 40 | 41 | assert amo1+amo2 == vroom.Amount([3, 4, 6]) 42 | assert amo1-amo2 == vroom.Amount([-1, 0, 0]) 43 | 44 | amo1 += amo2 45 | assert amo1 == vroom.Amount([3, 4, 6]) 46 | amo1 -= amo2+amo2 47 | assert amo1 == vroom.Amount([-1, 0, 0]) 48 | 49 | 50 | def test_amount_indexing(): 51 | amo = vroom.Amount([1, 2, 3]) 52 | assert amo[1] == 2 53 | amo[1] = 4 54 | assert amo == vroom.Amount([1, 4, 3]) 55 | -------------------------------------------------------------------------------- /src/bind/amount.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "structures/vroom/amount.h" 6 | 7 | namespace py = pybind11; 8 | 9 | void init_amount(py::module_ &m) { 10 | 11 | py::class_(m, "Amount", py::buffer_protocol()) 12 | .def(py::init([](std::size_t size) { return new vroom::Amount(size); }), 13 | py::arg("size") = 0) 14 | .def(py::init([](vroom::Amount &a) { return a; }), py::arg("amount")) 15 | .def(py::init([](const py::buffer &b) { 16 | py::buffer_info info = b.request(); 17 | if (info.format != py::format_descriptor::format() || 18 | info.ndim != 1) 19 | throw std::runtime_error("Incompatible buffer format!"); 20 | auto v = new vroom::Amount(info.shape[0]); 21 | memcpy(v->get_data(), info.ptr, 22 | sizeof(int64_t) * (size_t)v->size()); 23 | return v; 24 | }), 25 | py::arg("array")) 26 | .def_buffer([](vroom::Amount &a) -> py::buffer_info { 27 | return py::buffer_info(a.get_data(), sizeof(int64_t), 28 | py::format_descriptor::format(), 1, 29 | {a.size()}, {sizeof(int64_t)}); 30 | }) 31 | .def("_lshift", 32 | [](const vroom::Amount &a, const vroom::Amount &b) { return a < b; }) 33 | .def("_le", [](const vroom::Amount &a, 34 | const vroom::Amount &b) { return a <= b; }) 35 | .def("_push_back", &vroom::Amount::push_back) 36 | .def("__len__", &vroom::Amount::size); 37 | } 38 | -------------------------------------------------------------------------------- /test/test_vehicle.py: -------------------------------------------------------------------------------- 1 | import vroom 2 | 3 | 4 | def test_repr(): 5 | 6 | assert (repr(vroom.Vehicle(1, start=4, profile="bus")) 7 | == "vroom.Vehicle(1, start=4, profile='bus')") 8 | assert (repr(vroom.Vehicle(2, end=(2., 3.), capacity=[1, 2])) 9 | == "vroom.Vehicle(2, end=(2.0, 3.0), capacity=[1, 2])") 10 | assert (repr(vroom.Vehicle(2, start=vroom.Location(index=2, coords=(4., 5.)), skills={7})) 11 | == "vroom.Vehicle(2, start=vroom.Location(index=2, coords=(4.0, 5.0)), skills={7})") 12 | assert (repr(vroom.Vehicle(3, start=7, time_window=(3, 4))) 13 | == "vroom.Vehicle(3, start=7, time_window=(3, 4))") 14 | assert (repr(vroom.Vehicle(3, end=7, breaks=[vroom.Break(4, [(1, 2)])])) 15 | == "vroom.Vehicle(3, end=7, breaks=[vroom.Break(4, time_windows=[(1, 2)])])") 16 | assert (repr(vroom.Vehicle(3, start=7, description="hello")) 17 | == "vroom.Vehicle(3, start=7, description='hello')") 18 | assert (repr(vroom.Vehicle(3, end=7, speed_factor=2.)) 19 | == "vroom.Vehicle(3, end=7, speed_factor=2.0)") 20 | assert (repr(vroom.Vehicle(3, start=7, max_tasks=17)) 21 | == "vroom.Vehicle(3, start=7, max_tasks=17)") 22 | assert (repr(vroom.Vehicle(3, start=7, max_distance=17)) 23 | == "vroom.Vehicle(3, start=7, max_distance=17)") 24 | assert (repr(vroom.Vehicle(3, start=7, max_travel_time=17)) 25 | == "vroom.Vehicle(3, start=7, max_travel_time=17)") 26 | assert (repr(vroom.Vehicle(3, end=7, steps=[vroom.VehicleStep("single", 3)])) 27 | == """vroom.Vehicle(3, end=7, \ 28 | steps=[vroom.VehicleStepStart(), vroom.VehicleStepSingle(3), vroom.VehicleStepEnd()])""") 29 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: pull request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | name: test 14 | runs-on: ubuntu-24.04 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | submodules: recursive 20 | 21 | - uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.9" 24 | 25 | - name: "Install system dependencies" 26 | run: sudo apt update -y && sudo apt install -y libssl-dev libasio-dev 27 | 28 | - name: "Install python environment" 29 | run: | 30 | python -m pip install black coverage flake8 mypy pytest 31 | python -m pip install -r build-requirements.txt 32 | 33 | - name: "Install pyvroom" 34 | env: 35 | CXX: g++-14 36 | run: | 37 | # Because `pip install -e .` does not play nice with gcov, we go old school: 38 | CFLAGS="-coverage" python setup.py build_ext --inplace 39 | python setup.py develop 40 | 41 | - name: "Run tests" 42 | run: make test 43 | 44 | - name: "Upload python coverage" 45 | uses: codecov/codecov-action@v4 46 | with: 47 | token: ${{ secrets.codecov_token }} 48 | files: 'coverage/coverage.xml' 49 | flags: python 50 | fail_ci_if_error: true 51 | 52 | - name: "Upload binding coverage" 53 | uses: codecov/codecov-action@v4 54 | with: 55 | token: ${{ secrets.codecov_token }} 56 | files: 'coverage/*.gcov' 57 | flags: binding 58 | fail_ci_if_error: true 59 | 60 | - name: Verify clean directory 61 | run: git diff --exit-code 62 | -------------------------------------------------------------------------------- /src/bind/enums.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "structures/typedefs.h" 4 | 5 | namespace py = pybind11; 6 | 7 | void init_enums(py::module_ &m) { 8 | 9 | py::enum_(m, "ROUTER") 10 | .value("OSRM", vroom::ROUTER::OSRM) 11 | .value("LIBOSRM", vroom::ROUTER::LIBOSRM) 12 | .value("ORS", vroom::ROUTER::ORS) 13 | .value("VALHALLA", vroom::ROUTER::VALHALLA) 14 | .export_values(); 15 | 16 | py::enum_(m, "JOB_TYPE") 17 | .value("SINGLE", vroom::JOB_TYPE::SINGLE) 18 | .value("PICKUP", vroom::JOB_TYPE::PICKUP) 19 | .value("DELIVERY", vroom::JOB_TYPE::DELIVERY) 20 | .export_values(); 21 | 22 | py::enum_(m, "STEP_TYPE") 23 | .value("START", vroom::STEP_TYPE::START) 24 | .value("JOB", vroom::STEP_TYPE::JOB) 25 | .value("BREAK", vroom::STEP_TYPE::BREAK) 26 | .value("END", vroom::STEP_TYPE::END) 27 | .export_values(); 28 | 29 | py::enum_(m, "HEURISTIC") 30 | .value("BASIC", vroom::HEURISTIC::BASIC) 31 | .value("DYNAMIC", vroom::HEURISTIC::DYNAMIC) 32 | .export_values(); 33 | 34 | py::enum_(m, "INIT") 35 | .value("NONE", vroom::INIT::NONE) 36 | .value("HIGHER_AMOUNT", vroom::INIT::HIGHER_AMOUNT) 37 | .value("NEAREST", vroom::INIT::NEAREST) 38 | .value("FURTHEST", vroom::INIT::FURTHEST) 39 | .value("EARLIEST_DEADLINE", vroom::INIT::EARLIEST_DEADLINE) 40 | .export_values(); 41 | 42 | py::enum_(m, "VIOLATION") 43 | .value("LEAD_TIME", vroom::VIOLATION::LEAD_TIME) 44 | .value("DELAY", vroom::VIOLATION::DELAY) 45 | .value("LOAD", vroom::VIOLATION::LOAD) 46 | .value("MAX_TASKS", vroom::VIOLATION::MAX_TASKS) 47 | .value("SKILLS", vroom::VIOLATION::SKILLS) 48 | .value("PRECEDENCE", vroom::VIOLATION::PRECEDENCE) 49 | .value("MISSING_BREAK", vroom::VIOLATION::MISSING_BREAK) 50 | .export_values(); 51 | } 52 | -------------------------------------------------------------------------------- /src/bind/solution/route.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "structures/vroom/solution/route.cpp" 4 | 5 | namespace py = pybind11; 6 | 7 | void init_route(py::module_ &m) { 8 | 9 | py::class_(m, "Route") 10 | .def(py::init<>()) 11 | .def(py::init( 12 | [](vroom::Id vehicle, std::vector &steps, 13 | vroom::UserCost cost, vroom::UserDuration duration, 14 | vroom::UserDistance distance, vroom::UserDuration setup, 15 | vroom::UserDuration service, vroom::UserDuration waiting_time, 16 | vroom::Priority priority, const vroom::Amount &delivery, 17 | const vroom::Amount &pickup, const std::string &profile, 18 | const std::string &description, vroom::Violations &violations) { 19 | return new vroom::Route(vehicle, std::move(steps), cost, duration, 20 | distance, setup, service, waiting_time, 21 | priority, delivery, pickup, profile, 22 | description, std::move(violations)); 23 | })) 24 | 25 | .def_readwrite("vehicle", &vroom::Route::vehicle) 26 | .def_readonly("steps", &vroom::Route::steps) 27 | .def_readwrite("cost", &vroom::Route::cost) 28 | .def_readwrite("setup", &vroom::Route::setup) 29 | .def_readwrite("service", &vroom::Route::service) 30 | .def_readwrite("duration", &vroom::Route::duration) 31 | .def_readwrite("distance", &vroom::Route::distance) 32 | .def_readwrite("waiting_time", &vroom::Route::waiting_time) 33 | .def_readwrite("priority", &vroom::Route::priority) 34 | .def_readwrite("delivery", &vroom::Route::delivery) 35 | .def_readwrite("pickup", &vroom::Route::pickup) 36 | .def_readwrite("profile", &vroom::Route::profile) 37 | .def_readwrite("description", &vroom::Route::description) 38 | .def_readwrite("violations", &vroom::Route::violations) 39 | .def_readwrite("geometry", &vroom::Route::geometry) 40 | .def_readwrite("distance", &vroom::Route::distance); 41 | } 42 | -------------------------------------------------------------------------------- /test/test_libvroom_examples.py: -------------------------------------------------------------------------------- 1 | """Reproduce the libvroom_example as tests.""" 2 | import numpy 3 | import pandas 4 | 5 | import vroom 6 | 7 | 8 | def test_example_with_custom_matrix(): 9 | problem_instance = vroom.Input() 10 | 11 | problem_instance.set_durations_matrix( 12 | profile="car", 13 | matrix_input=[[0, 2104, 197, 1299], 14 | [2103, 0, 2255, 3152], 15 | [197, 2256, 0, 1102], 16 | [1299, 3153, 1102, 0]], 17 | ) 18 | problem_instance.set_distances_matrix( 19 | profile="car", 20 | matrix_input=[[0, 21040, 1970, 12990], 21 | [21030, 0, 22550, 31520], 22 | [1970, 22560, 0, 11020], 23 | [12990, 31530, 11020, 0]], 24 | ) 25 | problem_instance.add_vehicle([vroom.Vehicle(7, start=0, end=0), 26 | vroom.Vehicle(8, start=2, end=2)]) 27 | problem_instance.add_job([vroom.Job(id=1414, location=0), 28 | vroom.Job(id=1515, location=1), 29 | vroom.Job(id=1616, location=2), 30 | vroom.Job(id=1717, location=3)]) 31 | solution = problem_instance.solve( 32 | exploration_level=5, nb_threads=4) 33 | 34 | assert solution.summary.cost == 6411 35 | assert solution.summary.unassigned == 0 36 | assert solution.unassigned == [] 37 | 38 | routes = solution.routes 39 | assert numpy.all(routes.vehicle_id.drop_duplicates() == [7, 8]) 40 | assert numpy.all(routes.id == [None, 1515, 1414, None, 41 | None, 1717, 1616, None]) 42 | assert numpy.all(routes.type == ["start", "job", "job", "end", 43 | "start", "job", "job", "end"]) 44 | assert numpy.all(routes.arrival == [0, 2104, 4207, 4207, 45 | 0, 1102, 2204, 2204]) 46 | assert numpy.all(routes.location_index == [0, 1, 0, 0, 2, 3, 2, 2]) 47 | assert numpy.all(routes.distance == [0, 21040, 42070, 42070, 48 | 0, 11020, 22040, 22040]) -------------------------------------------------------------------------------- /src/bind/input/vehicle_step.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "structures/vroom/input/vehicle_step.cpp" 4 | 5 | namespace py = pybind11; 6 | 7 | void init_vehicle_step(py::module_ &m) { 8 | 9 | py::class_(m, "ForcedService") 10 | .def(py::init<>()) 11 | .def(py::init, 12 | std::optional, 13 | std::optional>(), 14 | py::arg("service_at"), py::arg("service_after"), 15 | py::arg("service_before")) 16 | .def_readwrite("_service_at", &vroom::ForcedService::at) 17 | .def_readwrite("_service_after", &vroom::ForcedService::after) 18 | .def_readwrite("_service_before", &vroom::ForcedService::before); 19 | 20 | py::class_(m, "VehicleStep") 21 | .def(py::init([](vroom::VehicleStep v) { return v; })) 22 | .def(py::init( 23 | [](vroom::STEP_TYPE type, vroom::ForcedService &forced_service) { 24 | return new vroom::VehicleStep(type, std::move(forced_service)); 25 | }), 26 | py::arg("step_type"), py::arg("forced_service")) 27 | .def(py::init([](vroom::STEP_TYPE type, vroom::Id id, 28 | vroom::ForcedService &forced_service) { 29 | return new vroom::VehicleStep(type, id, std::move(forced_service)); 30 | }), 31 | py::arg("step_type"), py::arg("id"), py::arg("forced_service")) 32 | .def(py::init([](vroom::JOB_TYPE job_type, vroom::Id id, 33 | vroom::ForcedService &forced_service) { 34 | return new vroom::VehicleStep(job_type, id, 35 | std::move(forced_service)); 36 | }), 37 | py::arg("job_type"), py::arg("id"), py::arg("forced_service")) 38 | .def_readonly("_step_type", &vroom::VehicleStep::type) 39 | .def_readonly("_id", &vroom::VehicleStep::id) 40 | .def_readonly("_type", &vroom::VehicleStep::type) 41 | .def_readonly("_job_type", &vroom::VehicleStep::job_type) 42 | .def_readonly("_forced_service", &vroom::VehicleStep::forced_service); 43 | } 44 | -------------------------------------------------------------------------------- /src/bind/job.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "structures/vroom/job.cpp" 6 | 7 | namespace py = pybind11; 8 | 9 | void init_job(py::module_ &m) { 10 | 11 | py::class_(m, "Job") 12 | .def(py::init &, std::string &>(), 16 | "Regular one-stop job.", py::arg("id"), py::arg("location"), 17 | py::arg("setup") = 0, py::arg("service") = 0, 18 | py::arg("delivery") = vroom::Amount(0), 19 | py::arg("pickup") = vroom::Amount(0), 20 | py::arg("skills") = vroom::Skills(), py::arg("priority") = 0, 21 | py::arg("tws") = 22 | std::vector(1, vroom::TimeWindow()), 23 | py::arg("description") = "") 24 | .def(py::init &, std::string &>(), 28 | "Pickup and delivery job.", py::arg("id"), py::arg("type"), 29 | py::arg("location"), py::arg("setup") = 0, py::arg("service") = 0, 30 | py::arg("amount") = vroom::Amount(0), 31 | py::arg("skills") = vroom::Skills(), py::arg("priority") = 0, 32 | py::arg("tws") = 33 | std::vector(1, vroom::TimeWindow()), 34 | py::arg("description") = "") 35 | .def("index", &vroom::Job::index) 36 | .def("is_valid_start", &vroom::Job::is_valid_start) 37 | .def_readonly("_id", &vroom::Job::id) 38 | .def_readwrite("_location", &vroom::Job::location) 39 | .def_readonly("_type", &vroom::Job::type) 40 | .def_readonly("_setup", &vroom::Job::setup) 41 | .def_readonly("_service", &vroom::Job::service) 42 | .def_readonly("_delivery", &vroom::Job::delivery) 43 | .def_readonly("_pickup", &vroom::Job::pickup) 44 | .def_readonly("_skills", &vroom::Job::skills) 45 | .def_readonly("_priority", &vroom::Job::priority) 46 | .def_readonly("_time_windows", &vroom::Job::tws) 47 | .def_readonly("_description", &vroom::Job::description); 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/main_push.yml: -------------------------------------------------------------------------------- 1 | name: main push 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | check-platform-builds: 10 | name: ${{ matrix.platform }} 11 | runs-on: ${{ matrix.image }} 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | include: 16 | - image: ubuntu-latest 17 | platform: linux 18 | - image: macos-13 19 | platform: macos-intel 20 | - image: macos-14 21 | platform: macos-arm 22 | - image: windows-latest 23 | platform: windows 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | submodules: recursive 29 | 30 | - name: Cache Conan 31 | id: cache-conan 32 | uses: actions/cache@v3 33 | if: matrix.platform == 'windows' 34 | with: 35 | path: | 36 | conan_build 37 | conan_data 38 | key: conan-${{ matrix.image }}-${{ hashFiles('conanfile.txt') }} 39 | 40 | - name: Configure Python 41 | uses: actions/setup-python@v5 42 | if: matrix.platform == 'windows' && steps.cache-conan.outputs.cache-hit != 'true' 43 | with: 44 | python-version: '3.x' 45 | 46 | - name: Install Conan 47 | if: matrix.platform == 'windows' && steps.cache-conan.outputs.cache-hit != 'true' 48 | run: | 49 | pip install pip --upgrade 50 | pip install conan<2.0.0 51 | conan profile new default --detect 52 | conan profile update "settings.compiler=Visual Studio" default 53 | conan profile update "settings.compiler.version=17" default 54 | conan config set "storage.path=$env:GITHUB_WORKSPACE/conan_data" 55 | conan install --build=openssl --install-folder conan_build . 56 | 57 | - name: Set up QEMU 58 | if: matrix.platform == 'linux' 59 | uses: docker/setup-qemu-action@v3 60 | with: 61 | platforms: all 62 | 63 | - name: Build wheels 64 | if: matrix.platform != 'macos-arm' 65 | uses: pypa/cibuildwheel@v2.19.2 66 | env: 67 | MACOSX_DEPLOYMENT_TARGET: 13.0 68 | CC: gcc-13 69 | CXX: g++-13 70 | 71 | - name: Build wheels 72 | if: matrix.platform == 'macos-arm' 73 | uses: pypa/cibuildwheel@v2.19.2 74 | env: 75 | MACOSX_DEPLOYMENT_TARGET: 14.0 76 | CC: gcc-13 77 | CXX: g++-13 78 | 79 | - name: Verify clean directory 80 | run: git diff --exit-code 81 | shell: bash 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | *build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | coverage/ 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | *.gcov 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # pycharm project settings 123 | .idea 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | -------------------------------------------------------------------------------- /test/test_time_window.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from vroom import _vroom 4 | import vroom 5 | 6 | 7 | def test_time_window_init(): 8 | tw = vroom.TimeWindow() 9 | assert tw.start == 0 10 | 11 | with pytest.raises(_vroom.VroomInputException): 12 | vroom.TimeWindow(1000, 0) 13 | with pytest.raises(TypeError): 14 | vroom.TimeWindow(tw, 1000) 15 | with pytest.raises(TypeError): 16 | vroom.TimeWindow(1000) 17 | 18 | tw = vroom.TimeWindow(1000, 2000) 19 | assert tw.start == vroom.TimeWindow(tw).start 20 | assert tw.end == vroom.TimeWindow(tw).end 21 | 22 | 23 | def test_time_window_compare(): 24 | assert vroom.TimeWindow(50, 150) > vroom.TimeWindow(0, 100) 25 | assert vroom.TimeWindow(0, 100) < vroom.TimeWindow(50, 150) 26 | assert vroom.TimeWindow(50, 150) >= vroom.TimeWindow(0, 100) 27 | assert vroom.TimeWindow(0, 100) <= vroom.TimeWindow(50, 150) 28 | assert vroom.TimeWindow(0, 100) >= vroom.TimeWindow(0, 100) 29 | assert vroom.TimeWindow(0, 100) <= vroom.TimeWindow(0, 100) 30 | assert vroom.TimeWindow(0, 100) != vroom.TimeWindow(0, 200) 31 | assert vroom.TimeWindow(0, 100) == vroom.TimeWindow(0, 100) 32 | 33 | assert not (vroom.TimeWindow(50, 150) < vroom.TimeWindow(0, 100)) 34 | assert not (vroom.TimeWindow(0, 100) > vroom.TimeWindow(50, 150)) 35 | assert not (vroom.TimeWindow(50, 150) <= vroom.TimeWindow(0, 100)) 36 | assert not (vroom.TimeWindow(0, 100) >= vroom.TimeWindow(50, 150)) 37 | assert not (vroom.TimeWindow(0, 100) != vroom.TimeWindow(0, 100)) 38 | 39 | 40 | def test_time_window_shift(): 41 | assert vroom.TimeWindow(0, 100) << vroom.TimeWindow(200, 300) 42 | assert vroom.TimeWindow(200, 300) >> vroom.TimeWindow(0, 100) 43 | assert not (vroom.TimeWindow(0, 100) << vroom.TimeWindow(50, 150)) 44 | assert not (vroom.TimeWindow(0, 100) >> vroom.TimeWindow(50, 150)) 45 | 46 | assert not (vroom.TimeWindow(0, 300) == vroom.TimeWindow(100, 200)) 47 | assert not (vroom.TimeWindow(0, 300) << vroom.TimeWindow(100, 200)) 48 | assert not (vroom.TimeWindow(0, 300) >> vroom.TimeWindow(100, 200)) 49 | 50 | 51 | def test_time_window_contains(): 52 | tw = vroom.TimeWindow(100, 200) 53 | assert 50 not in tw 54 | assert 150 in tw 55 | assert 250 not in tw 56 | 57 | 58 | def test_time_window_bool(): 59 | assert vroom.TimeWindow(0, 100) 60 | assert not vroom.TimeWindow() 61 | 62 | 63 | def test_time_window_str(): 64 | assert str(vroom.TimeWindow()) == "vroom.TimeWindow()" 65 | assert str(vroom.TimeWindow(0, 100)) == "vroom.TimeWindow(0, 100)" 66 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import platform 5 | from subprocess import run 6 | from pathlib import Path 7 | from setuptools import setup 8 | from pybind11.setup_helpers import Pybind11Extension, build_ext 9 | 10 | include_dirs = [ 11 | "src", 12 | os.path.join("vroom", "src"), 13 | os.path.join("vroom", "include"), 14 | os.path.join("vroom", "include", "cxxopts", "include"), 15 | ] 16 | libraries = [] 17 | library_dirs = [] 18 | 19 | if platform.system() == "Windows": 20 | extra_compile_args = [ 21 | "-DNOGDI", 22 | "-DNOMINMAX", 23 | "-DWIN32_LEAN_AND_MEAN", 24 | "-DASIO_STANDALONE", 25 | "-DUSE_PYTHON_BINDINGS", 26 | "-DUSE_ROUTING=true" 27 | ] 28 | extra_link_args = [] 29 | 30 | else: # anything *nix 31 | extra_compile_args = [ 32 | "-MMD", 33 | "-MP", 34 | "-Wextra", 35 | "-Wpedantic", 36 | "-Wall", 37 | "-O3", 38 | "-DASIO_STANDALONE", 39 | "-DNDEBUG", 40 | "-DUSE_PYTHON_BINDINGS", 41 | "-DUSE_ROUTING=true" 42 | ] 43 | extra_link_args = [ 44 | "-lpthread", 45 | "-lssl", 46 | "-lcrypto", 47 | ] 48 | 49 | if platform.system() == "Darwin": 50 | # Homebrew puts include folders in weird places. 51 | prefix = run(["brew", "--prefix"], capture_output=True).stdout.decode("utf-8")[:-1] 52 | include_dirs.append(f"{prefix}/opt/openssl@1.1/include") 53 | include_dirs.append(f"{prefix}/include") 54 | extra_link_args.insert(0, f"-L{prefix}/lib") 55 | extra_link_args.insert(0, f"-L{prefix}/opt/openssl@1.1/lib") 56 | extra_link_args.append(f"-Wl,-ld_classic") 57 | 58 | # try conan dependency resolution 59 | conanfile = tuple(Path(__file__).parent.resolve().rglob("conanbuildinfo.json")) 60 | if conanfile: 61 | logging.info("Using conan to resolve dependencies.") 62 | with conanfile[0].open() as f: 63 | conan_deps = json.load(f)["dependencies"] 64 | for dep in conan_deps: 65 | include_dirs.extend(dep["include_paths"]) 66 | libraries.extend(dep["libs"]) 67 | libraries.extend(dep["system_libs"]) 68 | library_dirs.extend(dep["lib_paths"]) 69 | else: 70 | logging.warning("Conan not installed and/or no conan build detected. Assuming dependencies are installed.") 71 | 72 | ext_modules = [ 73 | Pybind11Extension( 74 | "_vroom", 75 | [os.path.join("src", "_vroom.cpp")], 76 | library_dirs=library_dirs, 77 | libraries=libraries, 78 | extra_compile_args=extra_compile_args, 79 | extra_link_args=extra_link_args, 80 | cxx_std=20 81 | ), 82 | ] 83 | 84 | setup( 85 | cmdclass={"build_ext": build_ext}, 86 | ext_modules=ext_modules, 87 | ext_package="vroom", 88 | include_dirs=include_dirs, 89 | use_scm_version=True, 90 | entry_points={"console_scripts": ["vroom=vroom:main"]}, 91 | ) 92 | -------------------------------------------------------------------------------- /src/bind/input/input.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | #include "structures/cl_args.cpp" 8 | #include "structures/vroom/input/input.cpp" 9 | #include "utils/input_parser.cpp" 10 | 11 | namespace py = pybind11; 12 | 13 | void init_input(py::module_ &m) { 14 | 15 | py::class_(m, "Input") 16 | .def( 17 | py::init([](const vroom::io::Servers &servers, vroom::ROUTER router, bool apply_TSPFix) { 18 | return new vroom::Input(servers, router, apply_TSPFix); 19 | }), 20 | "Class initializer.", 21 | py::arg("servers") = std::map(), 22 | py::arg("router") = vroom::ROUTER::OSRM, 23 | py::arg("apply_TSPFix") = false) 24 | .def_readonly("jobs", &vroom::Input::jobs) 25 | .def_readonly("vehicles", &vroom::Input::vehicles) 26 | .def("_from_json", &vroom::io::parse, py::arg("json_string"), 27 | py::arg("geometry")) 28 | .def("_set_geometry", &vroom::Input::set_geometry) 29 | .def("_add_job", &vroom::Input::add_job) 30 | .def("_add_shipment", &vroom::Input::add_shipment) 31 | .def("_add_vehicle", &vroom::Input::add_vehicle) 32 | .def("_set_durations_matrix", 33 | [](vroom::Input &self, const std::string &profile, 34 | vroom::Matrix &m) { 35 | self.set_durations_matrix(profile, std::move(m)); 36 | }) 37 | .def("_set_distances_matrix", 38 | [](vroom::Input &self, const std::string &profile, 39 | vroom::Matrix &m) { 40 | self.set_distances_matrix(profile, std::move(m)); 41 | }) 42 | .def("_set_costs_matrix", 43 | [](vroom::Input &self, const std::string &profile, 44 | vroom::Matrix &m) { 45 | self.set_costs_matrix(profile, std::move(m)); 46 | }) 47 | .def("has_skills", &vroom::Input::has_skills) 48 | .def("has_jobs", &vroom::Input::has_jobs) 49 | .def("has_shipments", &vroom::Input::has_shipments) 50 | .def("get_cost_upper_bound", &vroom::Input::get_cost_upper_bound) 51 | .def("has_homogeneous_locations", 52 | &vroom::Input::has_homogeneous_locations) 53 | .def("has_homogeneous_profiles", &vroom::Input::has_homogeneous_profiles) 54 | .def("has_homogeneous_costs", &vroom::Input::has_homogeneous_costs) 55 | .def("_solve", 56 | [](vroom::Input &self, unsigned exploration_level, unsigned nb_threads, const vroom::Timeout& timeout, const std::vector h_param) { 57 | return self.solve(exploration_level, nb_threads, timeout, h_param); 58 | }, 59 | "Solve routing problem", 60 | py::arg("exploration_level"), py::arg("nb_threads"), py::arg("timeout"), py::arg("h_param") 61 | ) 62 | .def("check", &vroom::Input::check); 63 | } 64 | -------------------------------------------------------------------------------- /src/vroom/input/forced_service.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from .. import _vroom 4 | 5 | 6 | class ForcedService(_vroom.ForcedService): 7 | """ 8 | Force service to abide to cirtain time limits. 9 | 10 | Attributes: 11 | service_at: 12 | If not None, the time point the start step should begin at. 13 | service_after: 14 | If not None, the time point the start step should begin after. 15 | service_before: 16 | If not None, the time point the start step should begin before. 17 | 18 | Examples: 19 | >>> vroom.ForcedService() 20 | vroom.ForcedService() 21 | >>> vroom.ForcedService( 22 | ... service_at=1, 23 | ... service_after=2, 24 | ... service_before=3, 25 | ... ) 26 | vroom.ForcedService(service_at=1, service_after=2, service_before=3) 27 | """ 28 | 29 | def __init__( 30 | self, 31 | *, 32 | service_at: Optional[int] = None, 33 | service_after: Optional[int] = None, 34 | service_before: Optional[int] = None, 35 | ) -> None: 36 | """ 37 | Initialize instance. 38 | 39 | Args: 40 | service_at: 41 | Constrain start step time to begin at a give time point. 42 | service_after: 43 | Constrain start step time to begin after a give time point. 44 | service_before: 45 | Constrain start step time to begin before a give time point. 46 | """ 47 | _vroom.ForcedService.__init__( 48 | self, 49 | service_at=service_at, 50 | service_after=service_after, 51 | service_before=service_before, 52 | ) 53 | 54 | def __repr__(self) -> str: 55 | """Create string representation of object.""" 56 | args = [] 57 | if self.service_at is not None: 58 | args.append(f"service_at={self.service_at}") 59 | if self.service_after is not None: 60 | args.append(f"service_after={self.service_after}") 61 | if self.service_before is not None: 62 | args.append(f"service_before={self.service_before}") 63 | return f"vroom.{self.__class__.__name__}({', '.join(args)})" 64 | 65 | @property 66 | def service_at(self) -> Optional[int]: 67 | """If not None, the time point the start step should begin at.""" 68 | if self._service_at is None: 69 | return None 70 | return _vroom.scale_to_user_duration(self._service_at) 71 | 72 | @property 73 | def service_after(self) -> Optional[int]: 74 | """If not None, the time point the start step should begin after.""" 75 | if self._service_after is None: 76 | return None 77 | return _vroom.scale_to_user_duration(self._service_after) 78 | 79 | @property 80 | def service_before(self) -> Optional[int]: 81 | """If not None, the time point the start step should begin before.""" 82 | if self._service_before is None: 83 | return None 84 | return _vroom.scale_to_user_duration(self._service_before) 85 | -------------------------------------------------------------------------------- /test/test_location.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import vroom 3 | from vroom import _vroom 4 | 5 | LOC_INDEX0 = _vroom.Location(index=4) 6 | LOC_INDEX1 = vroom.LocationIndex(4) 7 | LOC_INDEX2 = vroom.Location(index=5) 8 | 9 | LOC_COORDS0 = _vroom.Location(coords=_vroom.Coordinates(1, 2)) 10 | LOC_COORDS1 = vroom.LocationCoordinates([1, 2]) 11 | LOC_COORDS2 = vroom.Location(coords=[1, 3]) 12 | 13 | LOC_BOTH0 = _vroom.Location(index=4, coords=_vroom.Coordinates(1, 2)) 14 | LOC_BOTH1 = vroom.Location(index=4, coords=[1, 2]) 15 | LOC_BOTH2 = vroom.Location(5, [1, 3]) 16 | 17 | 18 | def test_location_subclass(): 19 | assert isinstance(LOC_INDEX2, vroom.LocationIndex) 20 | assert not isinstance(LOC_INDEX2, vroom.LocationCoordinates) 21 | assert isinstance(LOC_COORDS2, vroom.LocationCoordinates) 22 | assert not isinstance(LOC_COORDS2, vroom.LocationIndex) 23 | assert isinstance(LOC_BOTH1, vroom.LocationIndex) 24 | assert isinstance(LOC_BOTH1, vroom.LocationCoordinates) 25 | 26 | 27 | def test_init(): 28 | assert isinstance(vroom.Location(LOC_INDEX0), vroom.LocationIndex) 29 | assert isinstance(vroom.Location(LOC_INDEX1), vroom.LocationIndex) 30 | assert isinstance(vroom.Location(LOC_INDEX2), vroom.LocationIndex) 31 | 32 | assert not isinstance(vroom.Location(LOC_INDEX0), 33 | vroom.LocationCoordinates) 34 | assert not isinstance(vroom.Location(LOC_INDEX1), 35 | vroom.LocationCoordinates) 36 | assert not isinstance(vroom.Location(LOC_INDEX2), 37 | vroom.LocationCoordinates) 38 | 39 | assert not isinstance(vroom.Location(LOC_COORDS0), vroom.LocationIndex) 40 | assert not isinstance(vroom.Location(LOC_COORDS1), vroom.LocationIndex) 41 | assert not isinstance(vroom.Location(LOC_COORDS2), vroom.LocationIndex) 42 | 43 | assert isinstance(vroom.Location(LOC_COORDS0), vroom.LocationCoordinates) 44 | assert isinstance(vroom.Location(LOC_COORDS1), vroom.LocationCoordinates) 45 | assert isinstance(vroom.Location(LOC_COORDS2), vroom.LocationCoordinates) 46 | 47 | with pytest.raises(TypeError): 48 | vroom.LocationIndex(LOC_COORDS0) 49 | with pytest.raises(TypeError): 50 | vroom.LocationIndex(LOC_COORDS1) 51 | 52 | with pytest.raises(TypeError): 53 | vroom.LocationCoordinates(LOC_INDEX0) 54 | with pytest.raises(TypeError): 55 | vroom.LocationCoordinates(LOC_INDEX1) 56 | 57 | 58 | def test_equality(): 59 | assert LOC_INDEX0 == LOC_INDEX1 60 | assert LOC_INDEX0 != LOC_INDEX2 61 | assert LOC_INDEX1 != LOC_INDEX2 62 | 63 | assert LOC_COORDS0 == LOC_COORDS1 64 | assert LOC_COORDS0 != LOC_COORDS2 65 | assert LOC_COORDS1 != LOC_COORDS2 66 | 67 | assert LOC_BOTH0 == LOC_BOTH1 68 | assert LOC_BOTH0 != LOC_BOTH2 69 | assert LOC_BOTH1 != LOC_BOTH2 70 | 71 | assert LOC_COORDS1 != LOC_INDEX1 72 | assert LOC_COORDS2 != LOC_INDEX2 73 | 74 | assert LOC_BOTH1 == LOC_INDEX1 75 | assert LOC_BOTH1 == LOC_COORDS1 76 | assert LOC_BOTH1 != LOC_INDEX2 77 | assert LOC_BOTH1 != LOC_COORDS2 78 | 79 | assert LOC_BOTH2 != LOC_INDEX1 80 | assert LOC_BOTH2 != LOC_COORDS1 81 | assert LOC_BOTH2 == LOC_INDEX2 82 | assert LOC_BOTH2 == LOC_COORDS2 83 | -------------------------------------------------------------------------------- /src/bind/vehicle.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "structures/vroom/vehicle.cpp" 4 | 5 | namespace py = pybind11; 6 | 7 | void init_vehicle(py::module_ &m) { 8 | 9 | py::class_(m, "VehicleCosts") 10 | .def(py::init(), 11 | "VehicleCost constructor.", py::arg("fixed") = 0, 12 | py::arg("per_hour") = 3600, py::arg("per_km") = 0) 13 | .def_readonly("_fixed", &vroom::VehicleCosts::fixed) 14 | .def_readonly("_per_hour", &vroom::VehicleCosts::per_hour) 15 | .def_readonly("_per_km", &vroom::VehicleCosts::per_km); 16 | 17 | py::class_(m, "CostWrapper") 18 | .def(py::init(), 19 | "CostWrapper constructor", py::arg("speed_factor"), 20 | py::arg("per_hour"), py::arg("per_km")) 21 | .def("set_durations_matrix", &vroom::CostWrapper::set_durations_matrix) 22 | .def("set_distances_matrix", &vroom::CostWrapper::set_distances_matrix) 23 | .def("set_costs_matrix", &vroom::CostWrapper::set_costs_matrix); 24 | 25 | py::class_(m, "Vehicle") 26 | .def(py::init &, 27 | std::optional &, std::string &, 28 | vroom::Amount &, vroom::Skills &, vroom::TimeWindow &, 29 | std::vector &, std::string &, 30 | vroom::VehicleCosts, double, std::optional &, 31 | std::optional &, 32 | std::optional &, 33 | std::vector &>(), 34 | "Vehicle constructor.", py::arg("id"), py::arg("start"), 35 | py::arg("end"), py::arg("profile"), py::arg("capacity"), 36 | py::arg("skills"), py::arg("time_window"), py::arg("breaks"), 37 | py::arg("description"), py::arg("costs"), py::arg("speed_factor"), 38 | py::arg("max_tasks"), py::arg("max_travel_time"), 39 | py::arg("max_distance"), py::arg("steps")) 40 | // .def("has_start", &vroom::Vehicle::has_start) 41 | // .def("has_end", &vroom::Vehicle::has_end) 42 | .def("_has_same_locations", &vroom::Vehicle::has_same_locations) 43 | .def("_has_same_profile", &vroom::Vehicle::has_same_profile) 44 | .def_readonly("_id", &vroom::Vehicle::id) 45 | .def_readwrite("_start", &vroom::Vehicle::start) 46 | .def_readwrite("_end", &vroom::Vehicle::end) 47 | .def_readonly("_profile", &vroom::Vehicle::profile) 48 | .def_readonly("_capacity", &vroom::Vehicle::capacity) 49 | .def_readonly("_skills", &vroom::Vehicle::skills) 50 | .def_readonly("_time_window", &vroom::Vehicle::tw) 51 | .def_readonly("_breaks", &vroom::Vehicle::breaks) 52 | .def_readonly("_description", &vroom::Vehicle::description) 53 | .def_readonly("_costs", &vroom::Vehicle::costs) 54 | .def_readonly("_cost_wrapper", &vroom::Vehicle::cost_wrapper) 55 | // .def_readwrite("_speed_factor", &vroom::Vehicle::speed_factor) 56 | .def_readonly("_max_tasks", &vroom::Vehicle::max_tasks) 57 | .def_readonly("_max_travel_time", &vroom::Vehicle::max_travel_time) 58 | .def_readonly("_max_distance", &vroom::Vehicle::max_distance) 59 | .def_readonly("_steps", &vroom::Vehicle::steps); 60 | } 61 | -------------------------------------------------------------------------------- /test/test_job.py: -------------------------------------------------------------------------------- 1 | import vroom 2 | 3 | 4 | JOB1 = vroom.Job(id=0, location=1, delivery=[4], pickup=[5]) 5 | JOB2 = vroom.Job( 6 | id=1, 7 | location=2, 8 | setup=3, 9 | service=4, 10 | delivery=[5], 11 | pickup=[6], 12 | skills={7}, 13 | priority=8, 14 | time_windows=[(9, 10)], 15 | description="11", 16 | ) 17 | JOB3 = vroom.Job(id=0, location=1) 18 | 19 | PICKUP1 = vroom.ShipmentStep(id=0, location=1) 20 | DELIVERY1 = vroom.ShipmentStep(id=0, location=1) 21 | SHIPMENT1 = vroom.Shipment(DELIVERY1, PICKUP1) 22 | 23 | PICKUP2 = vroom.ShipmentStep( 24 | id=1, 25 | location=2, 26 | setup=3, 27 | service=4, 28 | time_windows=[(9, 10)], 29 | description="12", 30 | ) 31 | DELIVERY2 = vroom.ShipmentStep( 32 | id=1, 33 | location=2, 34 | setup=3, 35 | service=4, 36 | time_windows=[(9, 10)], 37 | description="11", 38 | ) 39 | SHIPMENT2 = vroom.Shipment( 40 | PICKUP2, 41 | DELIVERY2, 42 | amount=[6], 43 | skills={7}, 44 | priority=8, 45 | ) 46 | 47 | 48 | def test_job_repr(): 49 | assert repr(JOB1) == "vroom.Job(0, 1, delivery=[4], pickup=[5])" 50 | assert repr(JOB2) == "vroom.Job(1, 2, setup=3, service=4, delivery=[5], pickup=[6], time_windows=[(9, 10)], description='11')" 51 | assert repr(JOB3) == "vroom.Job(0, 1)" 52 | 53 | assert repr(PICKUP1) == "vroom.ShipmentStep(0, 1)" 54 | assert repr(PICKUP2) == "vroom.ShipmentStep(1, 2, setup=3, service=4, time_windows=[(9, 10)], description='12')" 55 | 56 | assert repr(DELIVERY1) == "vroom.ShipmentStep(0, 1)" 57 | assert repr(DELIVERY2) == "vroom.ShipmentStep(1, 2, setup=3, service=4, time_windows=[(9, 10)], description='11')" 58 | 59 | assert repr(SHIPMENT1) == "vroom.Shipment(vroom.ShipmentStep(0, 1), vroom.ShipmentStep(0, 1))" 60 | assert repr(SHIPMENT2) == ("vroom.Shipment(" 61 | "vroom.ShipmentStep(1, 2, setup=3, service=4, time_windows=[(9, 10)], description='12'), " 62 | "vroom.ShipmentStep(1, 2, setup=3, service=4, time_windows=[(9, 10)], description='11'), " 63 | "amount=[6], skills={7}, priority=8)") 64 | 65 | 66 | def test_job_attributes(): 67 | assert JOB2.id == 1 68 | assert JOB2.location == vroom.Location(2) 69 | assert JOB2.setup == 3 70 | assert JOB2.service == 4 71 | assert not hasattr(JOB2, "amount") 72 | assert JOB2.delivery == vroom.Amount([5]) 73 | assert JOB2.pickup == vroom.Amount([6]) 74 | assert JOB2.skills == {7} 75 | assert JOB2.priority == 8 76 | assert JOB2.time_windows == [vroom.TimeWindow(9, 10)] 77 | assert JOB2.description == "11" 78 | 79 | assert JOB3.delivery == vroom.Amount([]) 80 | assert JOB3.pickup == vroom.Amount([]) 81 | 82 | assert DELIVERY2.id == 1 83 | assert DELIVERY2.location == vroom.Location(2) 84 | assert DELIVERY2.setup == 3 85 | assert DELIVERY2.service == 4 86 | assert not hasattr(DELIVERY2, "delivery") 87 | assert not hasattr(DELIVERY2, "pickup") 88 | assert DELIVERY2.time_windows == [vroom.TimeWindow(9, 10)] 89 | assert DELIVERY2.description == "11" 90 | 91 | assert PICKUP2.id == 1 92 | assert PICKUP2.location == vroom.Location(2) 93 | assert PICKUP2.setup == 3 94 | assert PICKUP2.service == 4 95 | assert not hasattr(PICKUP2, "delivery") 96 | assert not hasattr(PICKUP2, "pickup") 97 | assert PICKUP2.time_windows == [vroom.TimeWindow(9, 10)] 98 | assert PICKUP2.description == "12" 99 | 100 | assert SHIPMENT2.amount == vroom.Amount([6]) 101 | assert SHIPMENT2.skills == {7} 102 | assert SHIPMENT2.priority == 8 103 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | branches-ignore: 8 | - '*' 9 | 10 | jobs: 11 | build_sdist: 12 | name: sdist 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Build sdist 18 | run: pipx run build --sdist 19 | 20 | - name: Check metadata 21 | run: pipx run twine check dist/* 22 | 23 | - uses: actions/upload-artifact@v4 24 | with: 25 | path: dist/*.tar.gz 26 | 27 | build_wheels: 28 | name: ${{ matrix.platform }} 29 | runs-on: ${{ matrix.image }} 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | include: 34 | - image: ubuntu-latest 35 | platform: linux 36 | - image: macos-13 37 | platform: macos-intel 38 | - image: macos-14 39 | platform: macos-arm 40 | - image: windows-latest 41 | platform: windows 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | with: 46 | submodules: recursive 47 | 48 | - name: Cache Conan 49 | id: cache-conan 50 | uses: actions/cache@v3 51 | if: matrix.platform == 'windows' 52 | with: 53 | path: | 54 | conan_build 55 | conan_data 56 | key: conan-${{ matrix.image }}-${{ hashFiles('conanfile.txt') }} 57 | 58 | - name: Configure Python 59 | uses: actions/setup-python@v5 60 | if: matrix.platform == 'windows' && steps.cache-conan.outputs.cache-hit != 'true' 61 | with: 62 | python-version: 3.x 63 | 64 | - name: Install Conan 65 | if: matrix.platform == 'windows' && steps.cache-conan.outputs.cache-hit != 'true' 66 | run: | 67 | pip install pip --upgrade 68 | pip install conan<2.0.0 69 | conan profile new default --detect 70 | conan profile update "settings.compiler=Visual Studio" default 71 | conan profile update "settings.compiler.version=17" default 72 | conan config set "storage.path=$env:GITHUB_WORKSPACE/conan_data" 73 | conan install --build=openssl --install-folder conan_build . 74 | 75 | - name: Set up QEMU 76 | if: matrix.platform == 'linux' 77 | uses: docker/setup-qemu-action@v3 78 | with: 79 | platforms: all 80 | 81 | - name: Set version 82 | if: matrix.platform != 'macos-arm' && matrix.platform != 'macos-intel' 83 | run: | 84 | sed -i 's/^version = 0\.1\.0$/version = ${{ github.ref_name }}/' setup.cfg 85 | 86 | - name: Set version 87 | if: matrix.platform == 'macos-arm' || matrix.platform == 'macos-intel' 88 | run: | 89 | sed -i "" 's/^version = 0\.1\.0$/version = ${{ github.ref_name }}/' setup.cfg 90 | 91 | - name: Build wheels 92 | if: matrix.platform != 'macos-arm' 93 | uses: pypa/cibuildwheel@v2.19.2 94 | env: 95 | MACOSX_DEPLOYMENT_TARGET: 13.0 96 | CC: gcc-14 97 | CXX: g++-14 98 | 99 | - name: Build wheels 100 | if: matrix.platform == 'macos-arm' 101 | uses: pypa/cibuildwheel@v2.19.2 102 | env: 103 | MACOSX_DEPLOYMENT_TARGET: 14.0 104 | CC: gcc-14 105 | CXX: g++-14 106 | 107 | - name: Upload wheels 108 | uses: actions/upload-artifact@v4 109 | with: 110 | path: wheelhouse/*.whl 111 | 112 | upload: 113 | name: upload 114 | needs: [build_wheels, build_sdist] 115 | runs-on: ubuntu-latest 116 | 117 | steps: 118 | - uses: actions/setup-python@v5 119 | 120 | - uses: actions/download-artifact@v4 121 | with: 122 | name: artifact 123 | path: dist 124 | 125 | - uses: pypa/gh-action-pypi-publish@v1.8.10 126 | with: 127 | user: __token__ 128 | password: ${{ secrets.pypi_password }} 129 | -------------------------------------------------------------------------------- /src/vroom/amount.py: -------------------------------------------------------------------------------- 1 | """An array of integers describing multidimensional quantities.""" 2 | 3 | from __future__ import annotations 4 | from typing import Sequence, Union 5 | 6 | import numpy 7 | 8 | from . import _vroom # type: ignore 9 | 10 | 11 | class Amount(_vroom.Amount): 12 | """An array of integers describing multidimensional quantities. 13 | 14 | Use amounts to describe a problem with capacity restrictions. Those arrays 15 | can be used to model custom restrictions for several metrics at once, e.g. 16 | number of items, weight, volume etc. A vehicle is only allowed to serve a 17 | set of tasks if the resulting load at each route step is lower than the 18 | matching value in capacity for each metric. When using multiple components 19 | for amounts, it is recommended to put the most important/limiting metrics 20 | first. 21 | 22 | It is assumed that all delivery-related amounts for jobs are loaded at 23 | vehicle start, while all pickup-related amounts for jobs are brought back 24 | at vehicle end. 25 | 26 | Supports the following features: 27 | 28 | * Numpy style indexing. 29 | * Appending with `.append`. 30 | * Addition and subtraction when the lengths are equal. 31 | * Lexicographical compare with `>>` and `<<`. 32 | * "For all" compare with `<=` and `=<`. 33 | * "For any" compare with `<` and `>`. 34 | 35 | Examples: 36 | >>> amount = vroom.Amount([1, 2]) 37 | >>> amount[1] = 3 38 | >>> amount.append(4) 39 | >>> print(amount) 40 | vroom.Amount([1, 3, 4]) 41 | """ 42 | 43 | def __init__( 44 | self, 45 | amount: Union[Amount, Sequence[int], numpy.ndarray] = (), 46 | ) -> None: 47 | """ 48 | Initialize. 49 | 50 | Args: 51 | amount: 52 | Sequence of quantities to support for. No restriction if omitted. 53 | """ 54 | _vroom.Amount.__init__(self, numpy.asarray(amount, dtype="longlong")) 55 | 56 | def append(self, amount: int) -> None: 57 | """Append value to the end of array.""" 58 | self._push_back(amount) 59 | 60 | def __getitem__(self, key: int) -> int: 61 | return numpy.asarray(self)[key] 62 | 63 | def __eq__(self, other) -> bool: 64 | if isinstance(other, _vroom.Amount): 65 | if len(self) != len(other): 66 | return False 67 | return bool(numpy.all(numpy.asarray(self) == numpy.asarray(other))) 68 | return NotImplemented 69 | 70 | def __add__(self, other: Amount) -> Amount: 71 | other = Amount(other) 72 | if len(self) != len(other): 73 | raise _vroom.VroomInternalException("Adding two Amount of different length") 74 | return Amount(numpy.asarray(self) + numpy.asarray(other)) 75 | 76 | def __sub__(self, other: Amount) -> Amount: 77 | other = Amount(other) 78 | if len(self) != len(other): 79 | raise _vroom.VroomInternalException("Subtracting two Amount of different length") 80 | return Amount(numpy.asarray(self) - numpy.asarray(other)) 81 | 82 | def __le__(self, other: Amount) -> bool: 83 | other = Amount(other) 84 | if len(self) != len(other): 85 | raise _vroom.VroomInternalException("Comparing two Amount of different length") 86 | return self._le(other) 87 | 88 | def __gt__(self, other: Amount) -> bool: 89 | return not (self.__le__(other)) 90 | 91 | def __repr__(self) -> str: 92 | return f"vroom.{self.__class__.__name__}" f"({numpy.asarray(self).tolist()})" 93 | 94 | def __lshift__(self, other: Amount) -> bool: 95 | other = Amount(other) 96 | if len(self) != len(other): 97 | raise _vroom.VroomInternalException("Comparing two Amount of different length") 98 | return self._lshift(other) 99 | 100 | def __rshift__(self, other: Amount) -> bool: 101 | return Amount(other).__lshift__(self) 102 | 103 | def __setitem__(self, key: int, value: int) -> None: 104 | numpy.asarray(self)[key] = value 105 | -------------------------------------------------------------------------------- /test/input/test_vehicle_step.py: -------------------------------------------------------------------------------- 1 | import vroom 2 | 3 | start1 = vroom.VehicleStep("start") 4 | start2 = vroom.VehicleStepStart() 5 | start3 = vroom.VehicleStepStart(1, 2, 3) 6 | 7 | end1 = vroom.VehicleStep("end") 8 | end2 = vroom.VehicleStepEnd() 9 | end3 = vroom.VehicleStepEnd(1, 2, 3) 10 | 11 | break1 = vroom.VehicleStep("break", 4) 12 | break2 = vroom.VehicleStepBreak(4) 13 | break3 = vroom.VehicleStepBreak(4, 1, 2, 3) 14 | 15 | single1 = vroom.VehicleStep("single", 4) 16 | single2 = vroom.VehicleStepSingle(4) 17 | single3 = vroom.VehicleStepSingle(4, 1, 2, 3) 18 | 19 | delivery1 = vroom.VehicleStep("delivery", 4) 20 | delivery2 = vroom.VehicleStepDelivery(4) 21 | delivery3 = vroom.VehicleStepDelivery(4, 1, 2, 3) 22 | 23 | pickup1 = vroom.VehicleStep("pickup", 4) 24 | pickup2 = vroom.VehicleStepPickup(4) 25 | pickup3 = vroom.VehicleStepPickup(4, 1, 2, 3) 26 | 27 | 28 | def test_vehicle_step_subclass(): 29 | 30 | assert isinstance(start1, vroom.VehicleStepStart) 31 | assert not isinstance(start1, vroom.VehicleStepEnd) 32 | assert not isinstance(start1, vroom.VehicleStepBreak) 33 | assert not isinstance(start1, vroom.VehicleStepSingle) 34 | assert not isinstance(start1, vroom.VehicleStepDelivery) 35 | assert not isinstance(start1, vroom.VehicleStepPickup) 36 | 37 | assert not isinstance(end1, vroom.VehicleStepStart) 38 | assert isinstance(end1, vroom.VehicleStepEnd) 39 | assert not isinstance(end1, vroom.VehicleStepBreak) 40 | assert not isinstance(end1, vroom.VehicleStepSingle) 41 | assert not isinstance(end1, vroom.VehicleStepDelivery) 42 | assert not isinstance(end1, vroom.VehicleStepPickup) 43 | 44 | assert not isinstance(break1, vroom.VehicleStepStart) 45 | assert not isinstance(break1, vroom.VehicleStepEnd) 46 | assert isinstance(break1, vroom.VehicleStepBreak) 47 | assert not isinstance(break1, vroom.VehicleStepSingle) 48 | assert not isinstance(break1, vroom.VehicleStepDelivery) 49 | assert not isinstance(break1, vroom.VehicleStepPickup) 50 | 51 | assert not isinstance(single1, vroom.VehicleStepStart) 52 | assert not isinstance(single1, vroom.VehicleStepEnd) 53 | assert not isinstance(single1, vroom.VehicleStepBreak) 54 | assert isinstance(single1, vroom.VehicleStepSingle) 55 | assert not isinstance(single1, vroom.VehicleStepDelivery) 56 | assert not isinstance(single1, vroom.VehicleStepPickup) 57 | 58 | assert not isinstance(delivery1, vroom.VehicleStepStart) 59 | assert not isinstance(delivery1, vroom.VehicleStepEnd) 60 | assert not isinstance(delivery1, vroom.VehicleStepBreak) 61 | assert not isinstance(delivery1, vroom.VehicleStepSingle) 62 | assert isinstance(delivery1, vroom.VehicleStepDelivery) 63 | assert not isinstance(delivery1, vroom.VehicleStepPickup) 64 | 65 | assert not isinstance(pickup1, vroom.VehicleStepStart) 66 | assert not isinstance(pickup1, vroom.VehicleStepEnd) 67 | assert not isinstance(pickup1, vroom.VehicleStepBreak) 68 | assert not isinstance(pickup1, vroom.VehicleStepSingle) 69 | assert not isinstance(pickup1, vroom.VehicleStepDelivery) 70 | assert isinstance(pickup1, vroom.VehicleStepPickup) 71 | 72 | 73 | def test_vehicle_step_init(): 74 | 75 | assert isinstance(vroom.VehicleStep(start1), vroom.VehicleStepStart) 76 | assert isinstance(vroom.VehicleStep(end1), vroom.VehicleStepEnd) 77 | assert isinstance(vroom.VehicleStep(break1), vroom.VehicleStepBreak) 78 | assert isinstance(vroom.VehicleStep(single1), vroom.VehicleStepSingle) 79 | assert isinstance(vroom.VehicleStep(delivery1), vroom.VehicleStepDelivery) 80 | assert isinstance(vroom.VehicleStep(pickup1), vroom.VehicleStepPickup) 81 | 82 | assert vroom.VehicleStep(start1) == start1 83 | assert vroom.VehicleStep(end1) == end1 84 | assert vroom.VehicleStep(break1) == break1 85 | assert vroom.VehicleStep(single1) == single1 86 | assert vroom.VehicleStep(delivery1) == delivery1 87 | assert vroom.VehicleStep(pickup1) == pickup1 88 | 89 | 90 | def test_vehicle_step_attributes(): 91 | 92 | assert start1.id == 0 93 | assert end2.id == 0 94 | assert break3.id == 4 95 | assert single3.service_at == 1 96 | assert delivery3.service_after == 2 97 | assert pickup3.service_before == 3 98 | -------------------------------------------------------------------------------- /src/vroom/solution/solution.py: -------------------------------------------------------------------------------- 1 | """The computed solutions.""" 2 | 3 | from typing import Any, Dict, Union 4 | from pathlib import Path 5 | import io 6 | import json 7 | from contextlib import redirect_stdout 8 | 9 | import numpy 10 | import pandas 11 | 12 | from .. import _vroom 13 | 14 | NA_SUBSTITUTE = 4293967297 15 | 16 | 17 | class Solution(_vroom.Solution): 18 | """ 19 | The computed solutions. 20 | 21 | Attributes: 22 | routes: 23 | Frame outlining all routes for all vehicles. 24 | """ 25 | 26 | _geometry: bool = False 27 | _distances: bool = False 28 | 29 | @property 30 | def routes(self) -> pandas.DataFrame: 31 | """ 32 | Frame outlining all routes for all vehicles. 33 | 34 | It includes the following columns. 35 | 36 | vehicle_id: 37 | Id of the vehicle assigned to this route. 38 | type: 39 | The activity the vehicle is performing. The available 40 | categories are `start`, `end`, `break`, `job`, `delivery` 41 | and `pickup`. 42 | arrival: 43 | Timepoint when the actvity ends. 44 | duration: 45 | The length of the activity. 46 | setup: 47 | Total setup time for this route. 48 | service: 49 | Total service time for this route. 50 | waiting_time: 51 | Total waiting time for this route. 52 | location_index: 53 | The index for the location of the destination. 54 | longitude: 55 | If available, the longitude of the destination. 56 | latitude: 57 | If available, the latitude of the destination. 58 | id: 59 | The identifier for the task that was performed. 60 | description: 61 | Text description provided to this step. 62 | distance: 63 | Total route distance. 64 | """ 65 | array = numpy.asarray(self._routes_numpy()) 66 | frame = pandas.DataFrame( 67 | { 68 | "vehicle_id": array["vehicle_id"], 69 | "type": pandas.Categorical( 70 | array["type"].astype("U9"), 71 | categories=["start", "end", "break", "job", "delivery", "pickup"], 72 | ), 73 | "arrival": array["arrival"], 74 | "duration": array["duration"], 75 | "setup": array["setup"], 76 | "service": array["service"], 77 | "waiting_time": array["waiting_time"], 78 | "location_index": array["location_index"], 79 | "longitude": pandas.array(array["longitude"].tolist()), 80 | "latitude": pandas.array(array["latitude"].tolist()), 81 | "id": pandas.array(array["id"].tolist(), dtype="Int64"), 82 | "description": array["description"].astype("U40"), 83 | } 84 | ) 85 | for column in ["longitude", "latitude", "id"]: 86 | if (frame[column] == NA_SUBSTITUTE).all(): 87 | del frame[column] 88 | else: 89 | frame.loc[frame[column] == NA_SUBSTITUTE, column] = pandas.NA 90 | if self._geometry or self._distances: 91 | frame["distance"] = array["distance"] 92 | return frame 93 | 94 | def to_dict(self) -> Dict[str, Any]: 95 | """Convert solution into VROOM compatible dictionary.""" 96 | stream = io.StringIO() 97 | with redirect_stdout(stream): 98 | if self._geometry or self._distances: 99 | self._geometry_solution_json() 100 | else: 101 | self._solution_json() 102 | return json.loads(stream.getvalue()) 103 | 104 | def to_json(self, filepath: Union[str, Path]) -> None: 105 | """Store solution into VROOM compatible JSON file.""" 106 | with open(filepath, "w") as handler: 107 | with redirect_stdout(handler): 108 | if self._geometry or self._distances: 109 | self._geometry_solution_json() 110 | else: 111 | self._solution_json() 112 | -------------------------------------------------------------------------------- /src/vroom/time_window.py: -------------------------------------------------------------------------------- 1 | """Time window for when a delivery/pickup/task is possible.""" 2 | 3 | from __future__ import annotations 4 | from typing import Any, Optional, Sequence, Union 5 | 6 | from . import _vroom 7 | 8 | 9 | class TimeWindow(_vroom.TimeWindow): 10 | """Time window for when a delivery/pickup/task is possible. 11 | 12 | Relative values, e.g. `[0, 14400]` for a 4 hours time window starting at 13 | the beginning of the planning horizon. In that case all times reported in 14 | output with the arrival key are relative to the start of the planning 15 | horizon; 16 | 17 | Supprt the following features: 18 | 19 | * No arguments implies no time constraints. 20 | * Equality operator `==` based on both start and end time are the same. 21 | * Normal compare operator `<, >, <=, >=` based on start time. 22 | * Shift `<<, >>` based on non-overlap intervals. 23 | * Contains `X in Y` based on if number/interval inside other interval. 24 | * Length `len(X)` give length of interval. 25 | * Falsy on no constrained interval. 26 | 27 | Attributes: 28 | start: 29 | Start point (inclusice) of the time window. 30 | end: 31 | End point (inclusive) of the time window. 32 | 33 | Args: 34 | start: 35 | Start point (inclusive) of the time window. In 36 | seconds from the starting time. Assumed `0 < start`. 37 | end: 38 | End point (inclusive) of the time window. In 39 | seconds from the starting time. Assumes `start < end`. 40 | 41 | Examples: 42 | >>> tw = vroom.TimeWindow(2200, 8800) 43 | >>> tw 44 | vroom.TimeWindow(2200, 8800) 45 | >>> tw.start, tw.end, len(tw) 46 | (2200, 8800, 6600) 47 | >>> 1000 in tw, 5000 in tw 48 | (False, True) 49 | 50 | """ 51 | 52 | _start: int 53 | _end: int 54 | 55 | def __init__( 56 | self, 57 | start: Union[int, _vroom.TimeWindow, Sequence[int], None] = None, 58 | end: Optional[int] = None, 59 | ) -> None: 60 | if isinstance(start, _vroom.TimeWindow): 61 | if end is not None: 62 | raise TypeError("Only one arg when input is vroom.TimeWindow.") 63 | if start._is_default(): 64 | start = end = None 65 | else: 66 | end = _vroom.scale_to_user_duration(start._end) 67 | start = _vroom.scale_to_user_duration(start._start) 68 | elif isinstance(start, Sequence): 69 | if end is not None: 70 | raise TypeError("Only one arg when input is a sequence.") 71 | start, end = start 72 | if (start is None) != (end is None): 73 | raise TypeError("Either none or both start and end has to be provided") 74 | if start is None: 75 | _vroom.TimeWindow.__init__(self) 76 | else: 77 | _vroom.TimeWindow.__init__(self, start=start, end=end) 78 | 79 | @property 80 | def start(self): 81 | return _vroom.scale_to_user_duration(self._start) 82 | 83 | @property 84 | def end(self): 85 | return _vroom.scale_to_user_duration(self._end) 86 | 87 | def __len__(self) -> int: 88 | return self.end - self.start 89 | 90 | def __contains__(self, value: int) -> bool: 91 | return value >= self.start and value <= self.end 92 | 93 | def __bool__(self) -> bool: 94 | return not self._is_default() 95 | 96 | def __eq__(self, other: Any) -> bool: 97 | if isinstance(other, _vroom.TimeWindow): 98 | return self.start == other.start and self.end == other.end 99 | return NotImplemented 100 | 101 | def __le__(self, other: TimeWindow) -> bool: 102 | return self.start <= other.start 103 | 104 | def __lshift__(self, other: TimeWindow) -> bool: 105 | return self.end < other.start 106 | 107 | def __repr__(self): 108 | args = "" if self._is_default() else f"{self.start}, {self.end}" 109 | return f"vroom.{self.__class__.__name__}({args})" 110 | 111 | def __rshift__(self, other: TimeWindow) -> bool: 112 | return self.start > other.end 113 | -------------------------------------------------------------------------------- /src/vroom/break_.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import List, Optional, Sequence, Union 3 | 4 | import numpy 5 | 6 | from .time_window import TimeWindow 7 | from .amount import Amount 8 | from . import _vroom 9 | 10 | 11 | class Break(_vroom.Break): 12 | """A break allocated to the vehicle's driver. 13 | 14 | Examples: 15 | >>> vroom.Break( 16 | ... id=4, 17 | ... time_windows=[vroom.TimeWindow(0, 1000)], 18 | ... service=200, 19 | ... description="lunch", 20 | ... max_load=[2, 3], 21 | ... ) 22 | vroom.Break(4, time_windows=[(0, 1000)], service=200, description='lunch', max_load=[2, 3]) 23 | """ 24 | 25 | def __init__( 26 | self, 27 | id: Union[Break, int], 28 | time_windows: Sequence[TimeWindow] = (), 29 | service: int = 0, 30 | description: str = "", 31 | max_load: Union[None, Amount, Sequence[int]] = None, 32 | ) -> None: 33 | """ 34 | Args: 35 | id: 36 | Job identifier number. Two jobs can not have the 37 | same identifier. 38 | time_windows: 39 | Time windows for where breaks is allowed to begin. 40 | Defaults to have not restraints. 41 | service: 42 | The time duration of the break. 43 | description: 44 | A string describing this break. 45 | """ 46 | if isinstance(id, _vroom.Break): 47 | assert time_windows == () 48 | assert service == 0 49 | assert description == "" 50 | assert max_load is None 51 | time_windows = id._time_windows 52 | service = _vroom.scale_to_user_duration(id._service) 53 | description = id._description 54 | max_load = id._max_load 55 | id = id._id 56 | _vroom.Break.__init__( 57 | self, 58 | id=id, 59 | time_windows=[TimeWindow(tw) for tw in time_windows], 60 | service=service, 61 | description=description, 62 | max_load=None if max_load is None else Amount(max_load), 63 | ) 64 | 65 | @property 66 | def id(self) -> int: 67 | """Job identifier number. Two jobs can not have the same identifier.""" 68 | return self._id 69 | 70 | @id.setter 71 | def id(self, value: int) -> None: 72 | self._id = value 73 | 74 | @property 75 | def time_windows(self) -> List[TimeWindow]: 76 | """Time windows for where breaks is allowed to begin.""" 77 | return [TimeWindow(tw) for tw in self._time_windows] 78 | 79 | @time_windows.setter 80 | def time_windows(self, value: Sequence[TimeWindow]) -> None: 81 | self._time_windows = [TimeWindow(tw) for tw in value] 82 | 83 | @property 84 | def service(self) -> int: 85 | """The time duration of the break.""" 86 | return _vroom.scale_to_user_duration(self._service) 87 | 88 | @service.setter 89 | def service(self, value: int) -> None: 90 | self._service = _vroom.scale_from_user_duration(value) 91 | 92 | @property 93 | def description(self) -> str: 94 | """A string describing this break.""" 95 | return self._description 96 | 97 | @description.setter 98 | def description(self, value: str) -> None: 99 | self._description = value 100 | 101 | @property 102 | def max_load(self) -> Optional[Amount]: 103 | """The maximum load the vehicle is allowed during the break.""" 104 | return self._max_load 105 | 106 | @max_load.setter 107 | def max_load(self, value: Union[None, Amount, Sequence[int]]) -> None: 108 | self._max_load = None if value is None else Amount(value) 109 | 110 | def is_valid_start(self, time: int): 111 | """Check if break has a valid start time.""" 112 | return self._is_valid_start(time=_vroom.scale_from_user_duration(time)) 113 | 114 | def __repr__(self) -> str: 115 | args = [f"{self.id}"] 116 | if self.time_windows: 117 | args.append(f"time_windows={[(tw.start, tw.end) for tw in self.time_windows]}") 118 | if self.service: 119 | args.append(f"service={self.service}") 120 | if self.description: 121 | args.append(f"description={self.description!r}") 122 | if self.max_load: 123 | args.append(f"max_load={[int(load) for load in numpy.asarray(self.max_load)]}") 124 | return f"vroom.{self.__class__.__name__}({', '.join(args)})" 125 | -------------------------------------------------------------------------------- /src/bind/solution/solution.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "structures/vroom/solution/solution.cpp" 7 | #include "utils/output_json.cpp" 8 | 9 | namespace py = pybind11; 10 | 11 | struct _Step { 12 | int64_t vehicle_id; 13 | char type[9]; 14 | int64_t arrival; 15 | int64_t duration; 16 | int64_t setup; 17 | int64_t service; 18 | int64_t waiting_time; 19 | int64_t distance; 20 | double longitude; 21 | double latitude; 22 | int64_t location_index; 23 | int64_t id; 24 | 25 | char description[40]; 26 | }; 27 | 28 | void init_solution(py::module_ &m) { 29 | 30 | PYBIND11_NUMPY_DTYPE(_Step, vehicle_id, type, arrival, duration, setup, 31 | service, waiting_time, distance, location_index, 32 | longitude, latitude, id, description); 33 | 34 | py::class_(m, "Solution") 35 | .def(py::init([](vroom::Solution s) { return s; })) 36 | .def(py::init([](const vroom::Amount &zero_amount, 37 | std::vector &routes, 38 | std::vector &unassigned) { 39 | return new vroom::Solution(zero_amount, std::move(routes), 40 | std::move(unassigned)); 41 | })) 42 | .def("_routes_numpy", 43 | [](vroom::Solution solution) { 44 | const unsigned int NA_SUBSTITUTE = 4293967297; 45 | size_t idx = 0; 46 | std::string type; 47 | std::string id; 48 | unsigned int number_of_steps = 0; 49 | for (auto &route : solution.routes) 50 | number_of_steps += route.steps.size(); 51 | auto arr = py::array_t<_Step>(number_of_steps); 52 | auto ptr = static_cast<_Step *>(arr.request().ptr); 53 | for (auto &route : solution.routes) { 54 | for (auto &step : route.steps) { 55 | 56 | ptr[idx].vehicle_id = route.vehicle; 57 | 58 | if (step.step_type == vroom::STEP_TYPE::START) 59 | type = "start"; 60 | else if (step.step_type == vroom::STEP_TYPE::END) 61 | type = "end"; 62 | else if (step.step_type == vroom::STEP_TYPE::BREAK) 63 | type = "break"; 64 | else if (step.job_type == vroom::JOB_TYPE::SINGLE) 65 | type = "job"; 66 | else if (step.job_type == vroom::JOB_TYPE::PICKUP) 67 | type = "pickup"; 68 | else if (step.job_type == vroom::JOB_TYPE::DELIVERY) 69 | type = "delivery"; 70 | 71 | strncpy(ptr[idx].type, type.c_str(), 9); 72 | strncpy(ptr[idx].description, step.description.c_str(), 40); 73 | 74 | ptr[idx].longitude = 75 | step.location.has_value() && 76 | step.location.value().has_coordinates() 77 | ? step.location.value().coordinates().lon 78 | : NA_SUBSTITUTE; 79 | ptr[idx].latitude = 80 | step.location.has_value() && 81 | step.location.value().has_coordinates() 82 | ? step.location.value().coordinates().lat 83 | : NA_SUBSTITUTE; 84 | ptr[idx].location_index = step.location.has_value() 85 | ? step.location.value().index() 86 | : NA_SUBSTITUTE; 87 | 88 | ptr[idx].id = (step.step_type == vroom::STEP_TYPE::JOB or 89 | step.step_type == vroom::STEP_TYPE::BREAK) 90 | ? step.id 91 | : NA_SUBSTITUTE; 92 | 93 | ptr[idx].setup = step.setup; 94 | ptr[idx].service = step.service; 95 | ptr[idx].waiting_time = step.waiting_time; 96 | ptr[idx].distance = step.distance; 97 | ptr[idx].arrival = step.arrival; 98 | ptr[idx].duration = step.duration; 99 | 100 | idx++; 101 | } 102 | } 103 | return arr; 104 | }) 105 | .def("_solution_json", 106 | [](vroom::Solution solution) { 107 | py::scoped_ostream_redirect stream( 108 | std::cout, py::module_::import("sys").attr("stdout")); 109 | vroom::io::write_to_json(solution, "", false); 110 | }) 111 | .def("_geometry_solution_json", 112 | [](vroom::Solution solution) { 113 | py::scoped_ostream_redirect stream( 114 | std::cout, py::module_::import("sys").attr("stdout")); 115 | vroom::io::write_to_json(solution, "", true); 116 | }) 117 | .def_readonly("summary", &vroom::Solution::summary) 118 | .def_readonly("_routes", &vroom::Solution::routes) 119 | .def_readonly("unassigned", &vroom::Solution::unassigned); 120 | } 121 | -------------------------------------------------------------------------------- /src/vroom/location.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Sequence, Tuple, Union 3 | 4 | from . import _vroom 5 | 6 | 7 | class LocationIndex(_vroom.Location): 8 | """Index in the custom duration matrix for where to find distances. 9 | 10 | Examples: 11 | >>> loc = LocationIndex(4) 12 | >>> loc 13 | vroom.LocationIndex(4) 14 | >>> loc.index 15 | 4 16 | 17 | See also: 18 | :class:`vroom.Location` 19 | 20 | """ 21 | 22 | def __init__( 23 | self, 24 | index: Union[int, Location], 25 | ) -> None: 26 | """ 27 | Args: 28 | index: 29 | Location index referring to column in the duration 30 | matrix. 31 | location: 32 | Other location with `index` attribute to make a copy of. 33 | """ 34 | if isinstance(index, _vroom.Location): 35 | if not index._user_index(): 36 | name = index.__class__.__name__ 37 | raise TypeError(f"Can not convert {name} to LocationIndex") 38 | index = index._index() 39 | _vroom.Location.__init__(self, index) 40 | assert not self._has_coordinates() 41 | 42 | @property 43 | def index(self) -> int: 44 | """Location index referring to column in the duration matrix.""" 45 | return self._index() 46 | 47 | def __repr__(self) -> str: 48 | return f"vroom.{self.__class__.__name__}({self.index})" 49 | 50 | 51 | class LocationCoordinates(_vroom.Location): 52 | """Location longitude and latitude. 53 | 54 | Examples: 55 | >>> loc = LocationCoordinates([2., 3.]) 56 | >>> loc 57 | vroom.LocationCoordinates((2.0, 3.0)) 58 | >>> loc.coords 59 | (2.0, 3.0) 60 | 61 | See also: 62 | :class:`vroom.Location` 63 | 64 | """ 65 | 66 | def __init__( 67 | self, 68 | coords: Union[Location, Sequence[float], _vroom.Coordinates], 69 | ) -> None: 70 | """ 71 | Args: 72 | coords: 73 | Longitude and latitude coordinate. 74 | """ 75 | if isinstance(coords, _vroom.Location): 76 | if not coords._has_coordinates(): 77 | name = coords.__class__.__name__ 78 | raise TypeError(f"Can not convert {name} to LocationCoordinates") 79 | coords = coords._coords 80 | elif isinstance(coords, Sequence): 81 | coords = _vroom.Coordinates(*coords) 82 | _vroom.Location.__init__(self, coords=coords) 83 | assert self._has_coordinates() 84 | assert not self._user_index() 85 | 86 | @property 87 | def coords(self) -> Tuple[float, float]: 88 | """Location longitude and latitude.""" 89 | return self._lon(), self._lat() 90 | 91 | def __repr__(self): 92 | return f"vroom.{self.__class__.__name__}({self.coords})" 93 | 94 | 95 | class Location(LocationIndex, LocationCoordinates): 96 | """Location for where a job needs to e done. 97 | 98 | Either as an index referring to a column in the durations matrix, or as 99 | longitude-latitude coordinates. 100 | 101 | Converts to :class:`LocationCoordinates` if no `index` is provided, and to 102 | :class:`LocationIndex` if not `coords` is provided. 103 | 104 | Examples: 105 | >>> loc = vroom.Location(index=4, coords=[7., 8.]) 106 | >>> loc 107 | vroom.Location(index=4, coords=(7.0, 8.0)) 108 | >>> loc.index, loc.coords 109 | (4, (7.0, 8.0)) 110 | >>> vroom.Location(4) 111 | vroom.LocationIndex(4) 112 | >>> vroom.Location([7., 8.]) 113 | vroom.LocationCoordinates((7.0, 8.0)) 114 | 115 | See also: 116 | :class:`vroom.LocationIndex`, :class:`vroom.LocationCoordinates` 117 | 118 | """ 119 | 120 | __init__ = _vroom.Location.__init__ 121 | """ 122 | Args: 123 | index: 124 | Location index referring to column in the duration 125 | matrix. 126 | coords: 127 | Longitude and latitude coordinate. 128 | location: 129 | Other location to make a smart copy of. 130 | """ 131 | 132 | def __new__( 133 | cls, 134 | index: Union[None, int, Sequence[float], _vroom.Location, _vroom.Coordinates] = None, 135 | coords: Union[None, Sequence[float], _vroom.Coordinates] = None, 136 | ): 137 | if isinstance(index, (Sequence, _vroom.Coordinates)): 138 | if coords is not None: 139 | raise TypeError("coord can not be provided twice.") 140 | coords = index 141 | index = None 142 | elif isinstance(index, _vroom.Location): 143 | if index._has_coordinates(): 144 | if coords is not None: 145 | raise TypeError("coords can not be provided with Location.") 146 | coords = _vroom.Coordinates(index._lon(), index._lat()) 147 | if index._user_index(): 148 | index = index._index() 149 | else: 150 | index = None 151 | if isinstance(coords, Sequence): 152 | coords = _vroom.Coordinates(*[float(coord) for coord in coords]) 153 | 154 | kwargs = {} 155 | if index is None: 156 | cls = LocationCoordinates 157 | else: 158 | kwargs["index"] = index 159 | if coords is None: 160 | cls = LocationIndex 161 | else: 162 | kwargs["coords"] = coords 163 | 164 | instance = _vroom.Location.__new__(cls, **kwargs) 165 | instance.__init__(**kwargs) 166 | return instance 167 | 168 | def __repr__(self) -> str: 169 | args = f"index={self.index}, coords={self.coords}" 170 | return f"vroom.{self.__class__.__name__}({args})" 171 | -------------------------------------------------------------------------------- /src/_vroom.cpp: -------------------------------------------------------------------------------- 1 | #include "bind/_main.cpp" 2 | #include "bind/utils.cpp" 3 | 4 | #include "bind/amount.cpp" 5 | #include "bind/break.cpp" 6 | #include "bind/enums.cpp" 7 | #include "bind/exception.cpp" 8 | #include "bind/job.cpp" 9 | #include "bind/location.cpp" 10 | #include "bind/time_window.cpp" 11 | #include "bind/vehicle.cpp" 12 | 13 | #include "bind/input/input.cpp" 14 | #include "bind/input/vehicle_step.cpp" 15 | 16 | #include "bind/generic/matrix.cpp" 17 | 18 | #include "bind/solution/route.cpp" 19 | #include "bind/solution/solution.cpp" 20 | #include "bind/solution/step.cpp" 21 | #include "bind/solution/summary.cpp" 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | #include "algorithms/kruskal.cpp" 28 | #include "algorithms/munkres.cpp" 29 | 30 | #include "algorithms/heuristics/heuristics.cpp" 31 | #include "algorithms/local_search/local_search.cpp" 32 | #include "algorithms/local_search/operator.cpp" 33 | #include "algorithms/local_search/top_insertions.cpp" 34 | #include "algorithms/validation/check.h" 35 | 36 | // #include "routing/libosrm_wrapper.cpp" 37 | #include "routing/http_wrapper.cpp" 38 | #include "routing/ors_wrapper.cpp" 39 | #include "routing/osrm_routed_wrapper.cpp" 40 | #include "routing/valhalla_wrapper.cpp" 41 | 42 | #include "structures/typedefs.h" 43 | 44 | #include "structures/generic/edge.cpp" 45 | #include "structures/generic/matrix.h" 46 | #include "structures/generic/undirected_graph.cpp" 47 | 48 | #include "structures/vroom/cost_wrapper.cpp" 49 | #include "structures/vroom/raw_route.cpp" 50 | #include "structures/vroom/solution_state.cpp" 51 | #include "structures/vroom/tw_route.cpp" 52 | 53 | #include "structures/vroom/bbox.cpp" 54 | #include "structures/vroom/solution/computing_times.cpp" 55 | #include "structures/vroom/solution/violations.cpp" 56 | 57 | #include "problems/cvrp/cvrp.cpp" 58 | #include "problems/cvrp/operators/cross_exchange.cpp" 59 | #include "problems/cvrp/operators/intra_cross_exchange.cpp" 60 | #include "problems/cvrp/operators/intra_exchange.cpp" 61 | #include "problems/cvrp/operators/intra_mixed_exchange.cpp" 62 | #include "problems/cvrp/operators/intra_or_opt.cpp" 63 | #include "problems/cvrp/operators/intra_relocate.cpp" 64 | #include "problems/cvrp/operators/intra_two_opt.cpp" 65 | #include "problems/cvrp/operators/mixed_exchange.cpp" 66 | #include "problems/cvrp/operators/or_opt.cpp" 67 | #include "problems/cvrp/operators/pd_shift.cpp" 68 | #include "problems/cvrp/operators/priority_replace.cpp" 69 | #include "problems/cvrp/operators/relocate.cpp" 70 | #include "problems/cvrp/operators/reverse_two_opt.cpp" 71 | #include "problems/cvrp/operators/route_exchange.cpp" 72 | #include "problems/cvrp/operators/route_split.cpp" 73 | #include "problems/cvrp/operators/swap_star.cpp" 74 | #include "problems/cvrp/operators/tsp_fix.cpp" 75 | #include "problems/cvrp/operators/two_opt.cpp" 76 | #include "problems/cvrp/operators/unassigned_exchange.cpp" 77 | #include "problems/vrp.cpp" 78 | 79 | #include "problems/vrptw/operators/cross_exchange.cpp" 80 | #include "problems/vrptw/operators/intra_cross_exchange.cpp" 81 | #include "problems/vrptw/operators/intra_exchange.cpp" 82 | #include "problems/vrptw/operators/intra_mixed_exchange.cpp" 83 | #include "problems/vrptw/operators/intra_or_opt.cpp" 84 | #include "problems/vrptw/operators/intra_relocate.cpp" 85 | #include "problems/vrptw/operators/intra_two_opt.cpp" 86 | #include "problems/vrptw/operators/mixed_exchange.cpp" 87 | #include "problems/vrptw/operators/or_opt.cpp" 88 | #include "problems/vrptw/operators/pd_shift.cpp" 89 | #include "problems/vrptw/operators/priority_replace.cpp" 90 | #include "problems/vrptw/operators/relocate.cpp" 91 | #include "problems/vrptw/operators/reverse_two_opt.cpp" 92 | #include "problems/vrptw/operators/route_exchange.cpp" 93 | #include "problems/vrptw/operators/route_split.cpp" 94 | #include "problems/vrptw/operators/swap_star.cpp" 95 | #include "problems/vrptw/operators/tsp_fix.cpp" 96 | #include "problems/vrptw/operators/two_opt.cpp" 97 | #include "problems/vrptw/operators/unassigned_exchange.cpp" 98 | #include "problems/vrptw/vrptw.cpp" 99 | 100 | #include "problems/tsp/heuristics/christofides.cpp" 101 | #include "problems/tsp/heuristics/local_search.cpp" 102 | #include "problems/tsp/tsp.cpp" 103 | 104 | #include "utils/helpers.cpp" 105 | #include "utils/version.cpp" 106 | 107 | namespace py = pybind11; 108 | 109 | PYBIND11_MODULE(_vroom, m) { 110 | 111 | init_utils(m); 112 | init_enums(m); 113 | init_exception(m); 114 | 115 | init_matrix(m); 116 | 117 | init_amount(m); 118 | init_location(m); 119 | init_time_window(m); 120 | init_job(m); 121 | init_vehicle_step(m); 122 | init_break(m); 123 | init_vehicle(m); 124 | 125 | init_input(m); 126 | 127 | init_route(m); 128 | init_solution(m); 129 | init_step(m); 130 | init_summary(m); 131 | 132 | init_main(m); 133 | 134 | py::class_(m, "ComputingTimes").def(py::init<>()); 135 | 136 | py::class_(m, "HeuristicParameters") 137 | .def(py::init()); 138 | 139 | py::class_(m, "Server") 140 | .def(py::init(), 141 | py::arg("host") = "0.0.0.0", py::arg("port") = "5000", py::arg("path") = ""); 142 | 143 | py::class_(m, "Violations") 144 | .def(py::init<>()) 145 | .def(py::init([](const vroom::Duration lead_time, 146 | const vroom::Duration delay, 147 | std::unordered_set types) { 148 | return new vroom::Violations(lead_time, delay, std::move(types)); 149 | })) 150 | .def(py::self += py::self) 151 | .def_readwrite("_lead_time", &vroom::Violations::lead_time) 152 | .def_readwrite("_delay", &vroom::Violations::delay) 153 | .def_readwrite("_types", &vroom::Violations::types); 154 | 155 | py::class_(m, "HttpWrapper"); 156 | py::class_(m, "OrsWrapper"); 157 | py::class_(m, "OsrmRoutedWrapper"); 158 | py::class_(m, "ValhallaWrapper"); 159 | } 160 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python Vehicle Routing Open-source Optimization Machine 2 | ======================================================= 3 | 4 | |gh_action| |codecov| |pypi| 5 | 6 | .. |gh_action| image:: https://img.shields.io/github/checks-status/VROOM-Project/pyvroom/main 7 | :target: https://github.com/VROOM-Project/pyvroom/actions 8 | .. |codecov| image:: https://img.shields.io/codecov/c/github/VROOM-Project/pyvroom 9 | :target: https://codecov.io/gh/VROOM-Project/pyvroom 10 | .. |pypi| image:: https://img.shields.io/pypi/v/pyvroom 11 | :target: https://pypi.org/project/pyvroom 12 | 13 | *Good solution, fast... in Python.* 14 | 15 | Pyvroom is an Python wrapper to the excellent `VROOM 16 | `_ optimization engine for solving 17 | `vehicle routing problems 18 | `_. 19 | 20 | The library aims to solve several well-known types of vehicle routing problems, 21 | including: 22 | 23 | * Travelling salesman. 24 | * Capacitated vehicle routing. 25 | * Routing with time windows. 26 | * Multi-depot heterogeneous vehicle. 27 | * Pickup-and-delivery. 28 | 29 | VROOM can also solve any mix of the above problem types. 30 | 31 | Basic usage 32 | ----------- 33 | 34 | .. code:: python 35 | 36 | >>> import vroom 37 | 38 | >>> problem_instance = vroom.Input() 39 | 40 | >>> problem_instance.set_durations_matrix( 41 | ... profile="car", 42 | ... matrix_input=[[0, 2104, 197, 1299], 43 | ... [2103, 0, 2255, 3152], 44 | ... [197, 2256, 0, 1102], 45 | ... [1299, 3153, 1102, 0]], 46 | ... ) 47 | 48 | >>> problem_instance.add_vehicle([vroom.Vehicle(47, start=0, end=0), 49 | ... vroom.Vehicle(48, start=2, end=2)]) 50 | 51 | >>> problem_instance.add_job([vroom.Job(1414, location=0), 52 | ... vroom.Job(1515, location=1), 53 | ... vroom.Job(1616, location=2), 54 | ... vroom.Job(1717, location=3)]) 55 | 56 | >>> solution = problem_instance.solve(exploration_level=5, nb_threads=4) 57 | 58 | >>> solution.summary.cost 59 | 6411 60 | 61 | >>> solution.routes.columns 62 | Index(['vehicle_id', 'type', 'arrival', 'duration', 'setup', 'service', 63 | 'waiting_time', 'location_index', 'id', 'description'], 64 | dtype='object') 65 | 66 | >>> solution.routes[["vehicle_id", "type", "arrival", "location_index", "id"]] 67 | vehicle_id type arrival location_index id 68 | 0 47 start 0 0 69 | 1 47 job 2104 1 1515 70 | 2 47 job 4207 0 1414 71 | 3 47 end 4207 0 72 | 4 48 start 0 2 73 | 5 48 job 1102 3 1717 74 | 6 48 job 2204 2 1616 75 | 7 48 end 2204 2 76 | 77 | Usage with a routing engine 78 | --------------------------- 79 | 80 | .. code:: python 81 | 82 | >>> import vroom 83 | 84 | >>> problem_instance = vroom.Input( 85 | ... servers={"auto": "valhalla1.openstreetmap.de:443"}, 86 | ... router=vroom._vroom.ROUTER.VALHALLA 87 | ... ) 88 | 89 | >>> problem_instance.add_vehicle(vroom.Vehicle(1, start=(2.44, 48.81), profile="auto")) 90 | 91 | >>> problem_instance.add_job([ 92 | ... vroom.Job(1, location=(2.44, 48.81)), 93 | ... vroom.Job(2, location=(2.46, 48.7)), 94 | ... vroom.Job(3, location=(2.42, 48.6)), 95 | ... ]) 96 | 97 | >>> sol = problem_instance.solve(exploration_level=5, nb_threads=4) 98 | >>> print(sol.summary.duration) 99 | 4041 100 | 101 | Installation 102 | ------------ 103 | 104 | Pyvroom currently makes binaries for on macOS and Linux. There is also a 105 | Windows build that can be used, but it is somewhat experimental. 106 | 107 | Installation of the pre-compiled releases should be as simple as: 108 | 109 | .. code:: bash 110 | 111 | pip install pyvroom 112 | 113 | The current minimal requirements are as follows: 114 | 115 | * Python at least version 3.9. 116 | * Intel MacOS (or Rosetta2) at least version 14.0. 117 | * Apple Silicon MacOS at least version 14.0. 118 | * Windows on AMD64. 119 | * Linux on x86_64 and Aarch64 given glibc at least version 2.28. 120 | 121 | Outside this it might be possible to build your own binaries. 122 | 123 | Building from source 124 | ==================== 125 | 126 | Building the source distributions requires: 127 | 128 | * Download the Pyvroom repository on you local machine: 129 | 130 | .. code:: bash 131 | 132 | git clone --recurse-submodules https://github.com/VROOM-Project/pyvroom 133 | 134 | * Install the Python dependencies: 135 | 136 | .. code:: bash 137 | 138 | pip install -r pyvroom/build-requirements.txt 139 | 140 | * Install ``asio`` headers, and ``openssl`` and ``crypto`` libraries and headers. 141 | For mac, this would be:: 142 | 143 | brew install openssl@1.1 144 | brew install asio 145 | 146 | For RHEL:: 147 | 148 | yum module enable mariadb-devel:10.3 149 | yum install -y openssl-devel asio 150 | 151 | For Musllinux:: 152 | 153 | apk add asio-dev 154 | apk add openssl-dev 155 | 156 | * The installation can then be done with: 157 | 158 | .. code:: bash 159 | 160 | pip install pyvroom/ 161 | 162 | Alternatively it is also possible to install the package from source using 163 | `Conan `_. This is also likely the only 164 | option if installing on Windows. 165 | 166 | To install using Conan, do the following: 167 | 168 | .. code:: bash 169 | 170 | cd pyvroom/ 171 | conan install --build=openssl --install-folder conan_build . 172 | 173 | Documentation 174 | ------------- 175 | 176 | The code is currently only documented with Pydoc. This means that the best way 177 | to learn Pyvroom for now is to either look at the source code or use ``dir()`` 178 | and ``help()`` to navigate the interface. 179 | 180 | It is also useful to take a look at the 181 | `VROOM API documentation `_. 182 | The interface there is mostly the same. 183 | -------------------------------------------------------------------------------- /src/vroom/vehicle.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import List, Optional, Sequence, Set, Union 3 | 4 | import numpy 5 | 6 | from .amount import Amount 7 | from .break_ import Break 8 | from .input.vehicle_step import VehicleStep 9 | from .location import Location, LocationCoordinates, LocationIndex 10 | from .time_window import TimeWindow 11 | 12 | from . import _vroom 13 | 14 | MAX_UINT = int(numpy.iinfo(numpy.uint).max) 15 | MAX_INT = int(numpy.iinfo(numpy.intp).max) 16 | MAX_UINT32 = int(numpy.iinfo(numpy.uint32).max) 17 | 18 | 19 | class VehicleCosts(_vroom.VehicleCosts): 20 | """Vehicle cost. 21 | 22 | Args: 23 | fixed: 24 | A fixed price for the vehicle to be utilized. 25 | per_hour: 26 | The price per hour to utilize the vehicle. 27 | per_km: 28 | The price per kilometer to utilize the vehicle. 29 | 30 | Examples: 31 | >>> vroom.VehicleCosts() 32 | VehicleCosts() 33 | >>> vroom.VehicleCosts(fixed=100, per_hour=50, per_km=25) 34 | VehicleCosts(fixed=100, per_hour=50, per_km=25) 35 | """ 36 | 37 | def __init__(self, fixed: int = 0, per_hour: int = 3600, per_km: int = 0): 38 | _vroom.VehicleCosts.__init__( 39 | self, 40 | fixed=int(fixed), 41 | per_hour=int(per_hour), 42 | per_km=int(per_km), 43 | ) 44 | 45 | @property 46 | def fixed(self) -> int: 47 | return _vroom.scale_to_user_cost(self._fixed) 48 | 49 | @property 50 | def per_hour(self) -> int: 51 | return self._per_hour 52 | 53 | @property 54 | def per_km(self) -> int: 55 | return self._per_km 56 | 57 | def __bool__(self) -> bool: 58 | return self.fixed != 0 or self.per_hour != 3600 or self.per_km != 0 59 | 60 | def __repr__(self): 61 | args = f"fixed={self.fixed}, per_hour={self.per_hour}, per_km={self.per_km}" if self else "" 62 | return f"{self.__class__.__name__}({args})" 63 | 64 | 65 | class Vehicle(_vroom.Vehicle): 66 | """Vehicle for performing transport. 67 | 68 | Args: 69 | id: 70 | Vehicle idenfifier number. Two vehicle can not have the same 71 | identifier. 72 | start: 73 | The location where the vehicle starts at before any jobs are done. 74 | If omitted, the vehicle will start at the first task it will be 75 | assigned. If interger, value interpreted as an the column in 76 | duration matrix. If pair of numbers, value interpreted as longitude 77 | and latitude coordinates respectively. 78 | end: 79 | The location where the vehicle should end up after all jobs are 80 | completed. If omitted, the vehicle will end at the last task it 81 | will be assigned. If interger, value interpreted as an the column 82 | in duration matrix. If pair of numbers, value interpreted as 83 | longitude and latitude coordinates respectively. 84 | profile: 85 | The name of the profile this vehicle falls under. 86 | capacity: 87 | Array of intergers representing the capacity to carry different 88 | goods. 89 | skills: 90 | Skills provided by this vehilcle needed to perform various tasks. 91 | time_window: 92 | The time window for when this vehicle is available for usage. 93 | breaks: 94 | Breaks this vehicle should take. 95 | description: 96 | Optional string descriping the vehicle. 97 | speed_factor: 98 | The speed factor for which this vehicle runs faster or slower than 99 | the default. 100 | max_tasks: 101 | The maximum number of tasks this vehicle can perform. 102 | max_travel_time: 103 | An integer defining the maximum travel time for this vehicle. 104 | max_distance: 105 | An integer defining the maximum distance for this vehicle. 106 | steps: 107 | Set of custom steps this vehicle should take. 108 | 109 | Examples: 110 | >>> vroom.Vehicle(1, end=1) 111 | vroom.Vehicle(1, end=1) 112 | 113 | """ 114 | 115 | def __init__( 116 | self, 117 | id: int, 118 | start: Union[None, Location, int, Sequence[float]] = None, 119 | end: Union[None, Location, int, Sequence[float]] = None, 120 | profile: str = "car", 121 | capacity: Union[Amount, Sequence[int]] = (), 122 | skills: Optional[Set[int]] = None, 123 | time_window: Optional[TimeWindow] = None, 124 | breaks: Sequence[Break] = (), 125 | description: str = "", 126 | costs: VehicleCosts = VehicleCosts(), 127 | speed_factor: float = 1.0, 128 | max_tasks: Optional[int] = MAX_UINT, 129 | max_travel_time: Optional[int] = None, 130 | max_distance: Optional[int] = MAX_UINT32, 131 | steps: Sequence[VehicleStep] = (), 132 | ) -> None: 133 | self._speed_factor = float(speed_factor) 134 | _vroom.Vehicle.__init__( 135 | self, 136 | id=int(id), 137 | start=(None if start is None else Location(start)), 138 | end=(None if end is None else Location(end)), 139 | profile=str(profile), 140 | capacity=Amount(capacity), 141 | skills=(set([]) if skills is None else skills), 142 | time_window=(TimeWindow() if time_window is None else TimeWindow(time_window)), 143 | breaks=[Break(break_) for break_ in breaks], 144 | description=str(description), 145 | costs=costs, 146 | speed_factor=self._speed_factor, 147 | max_tasks=max_tasks, 148 | max_travel_time=max_travel_time, 149 | max_distance=max_distance, 150 | steps=steps, 151 | ) 152 | assert isinstance(self.capacity, Amount) 153 | 154 | def __repr__(self) -> str: 155 | args = [f"{self.id}"] 156 | if self.start is not None: 157 | if isinstance(self.start, Location): 158 | args.append(f"start={self.start}") 159 | elif isinstance(self.start, LocationIndex): 160 | args.append(f"start={self.start.index}") 161 | elif isinstance(self.start, LocationCoordinates): 162 | args.append(f"start={self.start.coords}") 163 | if self.end is not None: 164 | if isinstance(self.end, Location): 165 | args.append(f"end={self.end}") 166 | if isinstance(self.end, LocationIndex): 167 | args.append(f"end={self.end.index}") 168 | elif isinstance(self.end, LocationCoordinates): 169 | args.append(f"end={self.end.coords}") 170 | if self.profile != "car": 171 | args.append(f"profile={self.profile!r}") 172 | if self.capacity != Amount([]): 173 | args.append(f"capacity={numpy.asarray(self.capacity).tolist()}") 174 | if self.skills: 175 | args.append(f"skills={self.skills}") 176 | if self.time_window: 177 | args.append(f"time_window={self.time_window.start, self.time_window.end}") 178 | if self.costs: 179 | args.append(f"costs={self.costs}") 180 | 181 | for name, default in [ 182 | ("breaks", []), 183 | ("description", ""), 184 | ("speed_factor", 1.0), 185 | ("max_tasks", MAX_UINT), 186 | ("max_travel_time", _vroom.scale_to_user_duration(MAX_INT)), 187 | ("max_distance", MAX_UINT32), 188 | ("steps", []), 189 | ]: 190 | attribute = getattr(self, name) 191 | if attribute != default: 192 | args.append(f"{name}={attribute!r}") 193 | 194 | return f"vroom.{self.__class__.__name__}({', '.join(args)})" 195 | 196 | @property 197 | def id(self) -> int: 198 | return self._id 199 | 200 | @id.setter 201 | def id(self, value) -> None: 202 | self._id = value 203 | 204 | @property 205 | def start(self) -> Optional[Location]: 206 | return Location(self._start) if self._start else None 207 | 208 | @property 209 | def end(self) -> Optional[Location]: 210 | return Location(self._end) if self._end else None 211 | 212 | @property 213 | def profile(self) -> str: 214 | return self._profile 215 | 216 | @property 217 | def capacity(self) -> Amount: 218 | return Amount(self._capacity) 219 | 220 | @property 221 | def skills(self) -> Set[int]: 222 | return self._skills 223 | 224 | @property 225 | def time_window(self) -> TimeWindow: 226 | return TimeWindow(self._time_window) 227 | 228 | @property 229 | def breaks(self) -> List[Break]: 230 | return [Break(break_) for break_ in self._breaks] 231 | 232 | @property 233 | def description(self) -> str: 234 | return self._description 235 | 236 | @property 237 | def costs(self) -> VehicleCosts: 238 | return VehicleCosts( 239 | fixed=self._costs._fixed, 240 | per_hour=self._costs._per_hour, 241 | per_km=self._costs._per_km, 242 | ) 243 | 244 | @property 245 | def speed_factor(self) -> float: 246 | return self._speed_factor 247 | 248 | @property 249 | def max_tasks(self) -> str: 250 | return self._max_tasks 251 | 252 | @property 253 | def max_travel_time(self) -> str: 254 | return _vroom.scale_to_user_duration(self._max_travel_time) 255 | 256 | @property 257 | def max_distance(self) -> str: 258 | return self._max_distance 259 | 260 | @property 261 | def steps(self) -> List[VehicleStep]: 262 | return [VehicleStep(step) for step in self._steps] 263 | 264 | def has_same_locations(self, vehicle: Vehicle) -> bool: 265 | return self._has_same_locations(vehicle) 266 | 267 | def has_same_profile(self, vehicle: Vehicle) -> bool: 268 | return self._has_same_profile(vehicle) 269 | -------------------------------------------------------------------------------- /src/vroom/job.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Sequence, Set, Union 2 | 3 | import numpy 4 | 5 | from . import _vroom 6 | 7 | from .amount import Amount 8 | from .location import Location, LocationCoordinates, LocationIndex 9 | from .time_window import TimeWindow 10 | 11 | 12 | class JobBaseclass: 13 | """Baseclass for all Job classes containing common attributes.""" 14 | 15 | _id: int 16 | _location: Location 17 | _setup: int 18 | _service: int 19 | _time_windows: Sequence[TimeWindow] 20 | _description: str 21 | 22 | def _get_attributes(self) -> Dict[str, Any]: 23 | """Arguments to be used in repr view.""" 24 | return { 25 | "id": self.id, 26 | "location": self.location, 27 | "setup": self.setup, 28 | "service": self.service, 29 | "time_windows": self.time_windows, 30 | "description": self.description, 31 | } 32 | 33 | @property 34 | def description(self) -> str: 35 | return self._description 36 | 37 | @property 38 | def id(self) -> int: 39 | return self._id 40 | 41 | @property 42 | def location(self) -> Location: 43 | """ 44 | The location where to go. 45 | 46 | Either by index (used with duration matrix) or 47 | by coordinate (used with map server). 48 | """ 49 | return Location(self._location) 50 | 51 | @property 52 | def service(self) -> int: 53 | return _vroom.scale_to_user_duration(self._service) 54 | 55 | @property 56 | def setup(self) -> int: 57 | return _vroom.scale_to_user_duration(self._setup) 58 | 59 | @property 60 | def time_windows(self) -> List[TimeWindow]: 61 | """Time window for when job can be delivered.""" 62 | return [TimeWindow(tw) for tw in self._time_windows] 63 | 64 | def __repr__(self) -> str: 65 | attributes = self._get_attributes() 66 | args = [f"{self.id}"] 67 | if isinstance(attributes["location"], LocationIndex): 68 | args.append(f"{self.location.index}") 69 | elif isinstance(attributes["location"], LocationCoordinates): 70 | args.append(f"{self.location.coords}") 71 | else: 72 | args.append(f"{self.location}") 73 | if attributes["setup"]: 74 | args.append(f"setup={attributes['setup']}") 75 | if attributes["service"]: 76 | args.append(f"service={attributes['service']}") 77 | if attributes.get("amount", False): 78 | args.append(f"amount={numpy.asarray(attributes['amount']).tolist()}") 79 | if attributes.get("delivery", False): 80 | args.append(f"delivery={numpy.asarray(attributes['delivery']).tolist()}") 81 | if attributes.get("pickup", False): 82 | args.append(f"pickup={numpy.asarray(attributes['pickup']).tolist()}") 83 | if attributes["time_windows"] != [TimeWindow()]: 84 | windows = [(tw.start, tw.end) for tw in attributes["time_windows"]] 85 | args.append(f"time_windows={windows}") 86 | if attributes["description"]: 87 | args.append(f"description={attributes['description']!r}") 88 | return f"vroom.{self.__class__.__name__}({', '.join(args)})" 89 | 90 | 91 | class Job(_vroom.Job, JobBaseclass): 92 | """A regular one-stop job with both a deliver and pickup that has to be performed. 93 | 94 | Examples: 95 | >>> vroom.Job(0, [4., 5.], delivery=[4], pickup=[7]) 96 | vroom.Job(0, (4.0, 5.0), delivery=[4], pickup=[7]) 97 | """ 98 | 99 | def __init__( 100 | self, 101 | id: int, 102 | location: Union[Location, int, Sequence[float]], 103 | setup: int = 0, 104 | service: int = 0, 105 | delivery: Amount = Amount(), 106 | pickup: Amount = Amount(), 107 | skills: Optional[Set[int]] = None, 108 | priority: int = 0, 109 | time_windows: Sequence[TimeWindow] = (), 110 | description: str = "", 111 | ) -> None: 112 | """ 113 | Args: 114 | id: 115 | Job identifier number. Two jobs can not have the same 116 | identifier. 117 | location: 118 | Location of the job. If interger, value interpreted as an the 119 | column in duration matrix. If pair of numbers, value 120 | interpreted as longitude and latitude coordinates respectively. 121 | setup: 122 | The cost of preparing the vehicle before actually going out for 123 | a job. 124 | service: 125 | The time (in secondes) it takes to pick up/deliver shipment 126 | when at customer. 127 | delivery: 128 | The amount of how much is being carried to customer. 129 | pickup: 130 | The amount of how much is being carried back from customer. 131 | skills: 132 | Skills required to perform job. Only vehicles which satisfies 133 | all required skills (i.e. has at minimum all skills values 134 | required) are allowed to perform this job. 135 | priority: 136 | The job priority level, where 0 is the lowest priority 137 | and 100 is the highest priority. 138 | time_windows: 139 | Windows for where service is allowed to begin. 140 | Defaults to have not restraints. 141 | description: 142 | Optional string descriping the job. 143 | """ 144 | if not pickup: 145 | if not delivery: 146 | pickup = Amount([]) 147 | delivery = Amount([]) 148 | else: 149 | pickup = Amount([0] * len(delivery)) 150 | elif not delivery: 151 | delivery = Amount([0] * len(pickup)) 152 | _vroom.Job.__init__( 153 | self, 154 | id=int(id), 155 | location=Location(location), 156 | setup=int(setup), 157 | service=int(service), 158 | delivery=Amount(delivery), 159 | pickup=Amount(pickup), 160 | skills=set(skills or []), 161 | priority=int(priority), 162 | tws=[TimeWindow(tw) for tw in time_windows] or [TimeWindow()], 163 | description=str(description), 164 | ) 165 | 166 | @property 167 | def delivery(self) -> Amount: 168 | """The amount of how much is being carried to customer.""" 169 | return Amount(self._delivery) 170 | 171 | @property 172 | def pickup(self) -> Amount: 173 | """The amount of how much is being carried back from customer.""" 174 | return Amount(self._pickup) 175 | 176 | @property 177 | def skills(self) -> int: 178 | """Skills required to perform job.""" 179 | return self._skills 180 | 181 | @property 182 | def priority(self) -> int: 183 | """The job priority level.""" 184 | return self._priority 185 | 186 | def _get_attributes(self) -> Dict[str, Any]: 187 | """Arguments to be used in repr view.""" 188 | attributes = super()._get_attributes() 189 | if self._pickup: 190 | attributes["pickup"] = self.pickup 191 | if self._delivery: 192 | attributes["delivery"] = self.delivery 193 | if self._skills: 194 | attributes["skills"] = self.skills 195 | if self._priority: 196 | attributes["priority"] = self.priority 197 | return attributes 198 | 199 | 200 | class ShipmentStep(JobBaseclass): 201 | """A delivery job that has to be performed. 202 | 203 | Examples: 204 | >>> vroom.ShipmentStep(0, [4., 5.]) 205 | vroom.ShipmentStep(0, (4.0, 5.0)) 206 | """ 207 | 208 | def __init__( 209 | self, 210 | id: int, 211 | location: Union[Location, int, Sequence[float]], 212 | setup: int = 0, 213 | service: int = 0, 214 | time_windows: Sequence[TimeWindow] = (), 215 | description: str = "", 216 | ) -> None: 217 | """ 218 | Args: 219 | id: 220 | Job identifier number. Two jobs can not have the same 221 | identifier. 222 | location: 223 | Location of the job. If interger, value interpreted as an the 224 | column in duration matrix. If pair of numbers, value 225 | interpreted as longitude and latitude coordinates respectively. 226 | setup: 227 | The cost of preparing the vehicle before actually going out for 228 | a job. 229 | service: 230 | The time (in secondes) it takes to pick up/deliver shipment 231 | when at customer. 232 | time_windows: 233 | Windows for where service is allowed to begin. 234 | Defaults to have not restraints. 235 | description: 236 | Optional string descriping the job. 237 | """ 238 | self._id = int(id) 239 | self._location = Location(location) 240 | self._setup = _vroom.scale_from_user_duration(int(setup)) 241 | self._service = _vroom.scale_from_user_duration(int(service)) 242 | self._time_windows = [TimeWindow(tw) for tw in time_windows] or [TimeWindow()] 243 | self._description = str(description) 244 | 245 | 246 | class Shipment: 247 | """A shipment that has to be performed. 248 | 249 | Examples: 250 | >>> pickup = vroom.ShipmentStep(0, [4., 5.]) 251 | >>> delivery = vroom.ShipmentStep(1, [5., 4.]) 252 | >>> vroom.Shipment(pickup, delivery, amount=[7]) # doctest: +NORMALIZE_WHITESPACE 253 | vroom.Shipment(vroom.ShipmentStep(0, (4.0, 5.0)), 254 | vroom.ShipmentStep(1, (5.0, 4.0)), 255 | amount=[7]) 256 | """ 257 | 258 | def __init__( 259 | self, 260 | pickup: ShipmentStep, 261 | delivery: ShipmentStep, 262 | amount: Amount = Amount(), 263 | skills: Optional[Set[int]] = None, 264 | priority: int = 0, 265 | ) -> None: 266 | """ 267 | Args: 268 | pickup: 269 | Description of the pickup part of the shipment. 270 | delivery: 271 | Description of the delivery part of the shipment. 272 | amount: 273 | An interger representation of how much is being carried back 274 | from customer. 275 | skills: 276 | Skills required to perform job. Only vehicles which satisfies 277 | all required skills (i.e. has at minimum all skills values 278 | required) are allowed to perform this job. 279 | priority: 280 | The job priority level, where 0 is the lowest priority 281 | and 100 is the highest priority. 282 | """ 283 | self.pickup = pickup 284 | self.delivery = delivery 285 | self.amount = Amount(amount) 286 | self.skills = skills or set() 287 | self.priority = int(priority) 288 | 289 | def __repr__(self) -> str: 290 | args = [str(self.pickup), str(self.delivery)] 291 | if self.amount: 292 | args.append(f"amount={numpy.asarray(self.amount).tolist()}") 293 | if self.skills: 294 | args.append(f"skills={self.skills}") 295 | if self.priority: 296 | args.append(f"priority={self.priority}") 297 | return f"vroom.{self.__class__.__name__}({', '.join(args)})" 298 | -------------------------------------------------------------------------------- /src/vroom/input/input.py: -------------------------------------------------------------------------------- 1 | """VROOM input definition.""" 2 | 3 | from __future__ import annotations 4 | from typing import Dict, Optional, Sequence, Set, Union 5 | from pathlib import Path 6 | from datetime import timedelta 7 | 8 | from numpy.typing import ArrayLike 9 | import numpy 10 | 11 | from .. import _vroom 12 | 13 | from ..amount import Amount 14 | from ..solution.solution import Solution 15 | from ..job import Job, Shipment, ShipmentStep 16 | from ..vehicle import Vehicle 17 | 18 | 19 | class Input(_vroom.Input): 20 | """VROOM input defintion. 21 | 22 | Main instance for adding jobs, shipments, vehicles, and cost and duration 23 | matrice defining a routing problem. Duration matrices is if not provided 24 | can also be retrieved from a map server. 25 | 26 | Attributes: 27 | jobs: 28 | Jobs that needs to be completed in the routing problem. 29 | vehicles: 30 | Vehicles available to solve the routing problem. 31 | 32 | """ 33 | 34 | _geometry: bool = False 35 | _distances: bool = False 36 | 37 | def __init__( 38 | self, 39 | servers: Optional[Dict[str, Union[str, _vroom.Server]]] = None, 40 | router: _vroom.ROUTER = _vroom.ROUTER.OSRM, 41 | apply_TSPFix: bool = False, 42 | geometry: bool = False, 43 | ) -> None: 44 | """Class initializer. 45 | 46 | Args: 47 | servers: 48 | Assuming no custom duration matrix is provided (from 49 | `set_durations_matrix`), use this dict to configure the 50 | routing servers. The key is the routing profile (e.g. "car"), 51 | the value is host and port in the format `{host}:{port}`. 52 | router: 53 | If servers is used, define what kind of server is provided. 54 | See `vroom.ROUTER` enum for options. 55 | apply_TSPFix: 56 | Experimental local search operator. 57 | geometry: 58 | Add detailed route geometry and distance. 59 | """ 60 | if servers is None: 61 | servers = {} 62 | for key, server in servers.items(): 63 | if isinstance(server, str): 64 | servers[key] = _vroom.Server(*server.split(":")) 65 | self._servers = servers 66 | self._router = router 67 | _vroom.Input.__init__( 68 | self, 69 | servers=servers, 70 | router=router, 71 | apply_TSPFix=apply_TSPFix, 72 | ) 73 | if geometry: 74 | self.set_geometry() 75 | 76 | def __repr__(self) -> str: 77 | """String representation.""" 78 | args = [] 79 | if self._servers: 80 | args.append(f"servers={self._servers}") 81 | if self._router != _vroom.ROUTER.OSRM: 82 | args.append(f"router={self._router}") 83 | return f"{self.__class__.__name__}({', '.join(args)})" 84 | 85 | @classmethod 86 | def from_json( 87 | cls, 88 | filepath: Path, 89 | servers: Optional[Dict[str, Union[str, _vroom.Server]]] = None, 90 | router: _vroom.ROUTER = _vroom.ROUTER.OSRM, 91 | geometry: Optional[bool] = None, 92 | ) -> Input: 93 | """Load model from JSON file. 94 | 95 | Args: 96 | filepath: 97 | Path to JSON file with problem definition. 98 | servers: 99 | Assuming no custom duration matrix is provided (from 100 | `set_durations_matrix`), use coordinates and a map server to 101 | calculate durations matrix. Keys should be identifed by 102 | `add_routing_wrapper`. If string, values should be on the 103 | format `{host}:{port}`. 104 | router: 105 | If servers is used, define what kind of server is provided. 106 | See `vroom.ROUTER` enum for options. 107 | geometry: 108 | Use coordinates from server instead of from distance matrix. 109 | If omitted, defaults to `servers is not None`. 110 | 111 | Returns: 112 | Input instance with all jobs, shipments, etc. added from JSON. 113 | 114 | """ 115 | if geometry is None: 116 | geometry = servers is not None 117 | if geometry: 118 | cls._set_geometry(True) 119 | instance = Input(servers=servers, router=router) 120 | with open(filepath) as handle: 121 | instance._from_json(handle.read(), geometry) 122 | return instance 123 | 124 | def set_geometry(self): 125 | """Add detailed route geometry and distance.""" 126 | self._geometry = True 127 | return self._set_geometry(True) 128 | 129 | def add_job( 130 | self, 131 | job: Union[Job, Shipment, Sequence[Job], Sequence[Shipment]], 132 | ) -> None: 133 | """ 134 | Add jobs that needs to be carried out. 135 | 136 | Args: 137 | job: 138 | One or more (single) job and/or shipments that the vehicles 139 | needs to carry out. 140 | 141 | Example: 142 | >>> problem_instance = vroom.Input() 143 | >>> problem_instance.add_job(vroom.Job(1, location=1)) 144 | >>> problem_instance.add_job([ 145 | ... vroom.Job(2, location=2), 146 | ... vroom.Shipment(vroom.ShipmentStep(3, location=3), 147 | ... vroom.ShipmentStep(4, location=4)), 148 | ... vroom.Job(5, location=5), 149 | ... ]) 150 | """ 151 | jobs = [job] if isinstance(job, (Job, Shipment)) else job 152 | for job_ in jobs: 153 | if isinstance(job_, Job): 154 | self._add_job(job_) 155 | 156 | elif isinstance(job_, Shipment): 157 | self._add_shipment( 158 | _vroom.Job( 159 | id=job_.pickup.id, 160 | type=_vroom.JOB_TYPE.PICKUP, 161 | location=job_.pickup.location, 162 | setup=job_.pickup.setup, 163 | service=job_.pickup.service, 164 | amount=job_.amount, 165 | skills=job_.skills, 166 | priority=job_.priority, 167 | tws=job_.pickup.time_windows, 168 | description=job_.pickup.description, 169 | ), 170 | _vroom.Job( 171 | id=job_.delivery.id, 172 | type=_vroom.JOB_TYPE.DELIVERY, 173 | location=job_.delivery.location, 174 | setup=job_.delivery.setup, 175 | service=job_.delivery.service, 176 | amount=job_.amount, 177 | skills=job_.skills, 178 | priority=job_.priority, 179 | tws=job_.delivery.time_windows, 180 | description=job_.delivery.description, 181 | ), 182 | ) 183 | 184 | else: 185 | raise _vroom.VroomInputException(f"Wrong type for {job_}; vroom.JobSingle expected.") 186 | 187 | def add_shipment( 188 | self, 189 | pickup: ShipmentStep, 190 | delivery: ShipmentStep, 191 | amount: Amount = Amount(), 192 | skills: Optional[Set[int]] = None, 193 | priority: int = 0, 194 | ): 195 | """Add a shipment that has to be performed. 196 | 197 | Args: 198 | pickup: 199 | Description of the pickup part of the shipment. 200 | delivery: 201 | Description of the delivery part of the shipment. 202 | amount: 203 | An interger representation of how much is being carried back 204 | from customer. 205 | skills: 206 | Skills required to perform job. Only vehicles which satisfies 207 | all required skills (i.e. has at minimum all skills values 208 | required) are allowed to perform this job. 209 | priority: 210 | The job priority level, where 0 is the most 211 | important and 100 is the least important. 212 | """ 213 | if skills is None: 214 | skills = set() 215 | self._add_shipment( 216 | _vroom.Job( 217 | id=pickup.id, 218 | type=_vroom.JOB_TYPE.PICKUP, 219 | location=pickup.location, 220 | setup=pickup.setup, 221 | service=pickup.service, 222 | amount=amount, 223 | skills=skills, 224 | priority=priority, 225 | tws=pickup.time_windows, 226 | description=pickup.description, 227 | ), 228 | _vroom.Job( 229 | id=delivery.id, 230 | type=_vroom.JOB_TYPE.DELIVERY, 231 | location=delivery.location, 232 | setup=delivery.setup, 233 | service=delivery.service, 234 | amount=amount, 235 | skills=skills, 236 | priority=priority, 237 | tws=delivery.time_windows, 238 | description=delivery.description, 239 | ), 240 | ) 241 | 242 | def add_vehicle( 243 | self, 244 | vehicle: Union[Vehicle, Sequence[Vehicle]], 245 | ) -> None: 246 | """Add vehicle. 247 | 248 | Args: 249 | vehicle: 250 | Vehicles to use to solve the vehicle problem. Vehicle type must 251 | have a recognized profile, and all added vehicle must have the 252 | same capacity. 253 | """ 254 | vehicles = [vehicle] if isinstance(vehicle, _vroom.Vehicle) else vehicle 255 | if not vehicles: 256 | return 257 | for vehicle_ in vehicles: 258 | self._add_vehicle(vehicle_) 259 | 260 | def set_durations_matrix( 261 | self, 262 | profile: str, 263 | matrix_input: ArrayLike, 264 | ) -> None: 265 | """Set durations matrix. 266 | 267 | Args: 268 | profile: 269 | Name of the transportation category profile in question. 270 | Typically "car", "truck", etc. 271 | matrix_input: 272 | A square matrix consisting of duration between each location of 273 | interest. Diagonal is canonically set to 0. 274 | """ 275 | assert isinstance(profile, str) 276 | if not isinstance(matrix_input, _vroom.Matrix): 277 | matrix_input = _vroom.Matrix(numpy.asarray(matrix_input, dtype="uint32")) 278 | self._set_durations_matrix(profile, matrix_input) 279 | 280 | def set_distances_matrix( 281 | self, 282 | profile: str, 283 | matrix_input: ArrayLike, 284 | ) -> None: 285 | """Set distances matrix. 286 | 287 | Args: 288 | profile: 289 | Name of the transportation category profile in question. 290 | Typically "car", "truck", etc. 291 | matrix_input: 292 | A square matrix consisting of distances between each location of 293 | interest. Diagonal is canonically set to 0. 294 | """ 295 | assert isinstance(profile, str) 296 | if not isinstance(matrix_input, _vroom.Matrix): 297 | matrix_input = _vroom.Matrix(numpy.asarray(matrix_input, dtype="uint32")) 298 | self._set_distances_matrix(profile, matrix_input) 299 | self._distances = True 300 | 301 | def set_costs_matrix( 302 | self, 303 | profile: str, 304 | matrix_input: ArrayLike, 305 | ) -> None: 306 | """Set costs matrix. 307 | 308 | Args: 309 | profile: 310 | Name of the transportation category profile in question. 311 | Typically "car", "truck", etc. 312 | matrix_input: 313 | A square matrix consisting of duration between each location of 314 | interest. Diagonal is canonically set to 0. 315 | """ 316 | assert isinstance(profile, str) 317 | if not isinstance(matrix_input, _vroom.Matrix): 318 | matrix_input = _vroom.Matrix(numpy.asarray(matrix_input, dtype="uint32")) 319 | self._set_costs_matrix(profile, matrix_input) 320 | 321 | def solve( 322 | self, 323 | exploration_level: int, 324 | nb_threads: int = 4, 325 | timeout: Optional[timedelta] = None, 326 | h_param = (), 327 | ) -> Solution: 328 | """Solve routing problem. 329 | 330 | Args: 331 | exploration_level: 332 | The exploration level to use. Number between 1 and 5. 333 | nb_threads: 334 | The number of available threads. 335 | timeout: 336 | Stop the solving process after a given amount of time. 337 | """ 338 | assert timeout is None or isinstance(timeout, (None, timedelta)), ( 339 | f"unknown timeout type: {timeout}") 340 | solution = Solution( 341 | self._solve( 342 | exploration_level=int(exploration_level), 343 | nb_threads=int(nb_threads), 344 | timeout=timeout, 345 | h_param=list(h_param), 346 | ) 347 | ) 348 | solution._geometry = self._geometry 349 | solution._distances = self._distances 350 | return solution 351 | -------------------------------------------------------------------------------- /src/vroom/input/vehicle_step.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import enum 4 | from typing import Any, Optional, Union 5 | 6 | from .. import _vroom 7 | 8 | 9 | class VEHICLE_STEP_TYPE(str, enum.Enum): 10 | """The various steps types a vehicle can be in. 11 | 12 | * `start` -- The starting step where the vehicle begins. 13 | * `end` -- The ending step where the vehicle ends. 14 | * `break` -- The vehicle is taking a break not performing a task. 15 | * `single` -- The vehicle is performing a single task. 16 | * `pickup` -- The vehicle is performing a pickup. 17 | * `delivery` -- The vehicle is performing a delivery. 18 | """ 19 | 20 | START: str = "start" 21 | END: str = "end" 22 | BREAK: str = "break" 23 | SINGLE: str = "single" 24 | PICKUP: str = "pickup" 25 | DELIVERY: str = "delivery" 26 | 27 | 28 | class VehicleStepBaseclass(_vroom.VehicleStep): 29 | """Baseclass for VehicleSteps.""" 30 | 31 | @property 32 | def id(self) -> int: 33 | return self._id 34 | 35 | @property 36 | def service_at(self) -> Optional[int]: 37 | if self._forced_service._service_at is None: 38 | return None 39 | return _vroom.scale_to_user_duration(self._forced_service._service_at) 40 | 41 | @property 42 | def service_after(self) -> Optional[int]: 43 | if self._forced_service._service_after is None: 44 | return None 45 | return _vroom.scale_to_user_duration(self._forced_service._service_after) 46 | 47 | @property 48 | def service_before(self) -> Optional[int]: 49 | if self._forced_service._service_before is None: 50 | return None 51 | return _vroom.scale_to_user_duration(self._forced_service._service_before) 52 | 53 | def __repr__(self) -> str: 54 | args = [] 55 | if isinstance( 56 | self, 57 | ( 58 | VehicleStepBreak, 59 | VehicleStepSingle, 60 | VehicleStepPickup, 61 | VehicleStepDelivery, 62 | ), 63 | ): 64 | args.append(f"{self.id}") 65 | if self.service_at is not None: 66 | args.append(f"service_at={self.service_at}") 67 | if self.service_after is not None: 68 | args.append(f"service_after={self.service_after}") 69 | if self.service_before is not None: 70 | args.append(f"service_before={self.service_before}") 71 | return f"vroom.{self.__class__.__name__}({', '.join(args)})" 72 | 73 | def __eq__(self, other: Any) -> bool: 74 | if isinstance(other, _vroom.VehicleStep): 75 | return ( 76 | self._step_type == other._step_type 77 | and self._id == other._id 78 | and ( 79 | self._forced_service._service_at is other._forced_service._service_at 80 | or self._forced_service._service_at == other._forced_service._service_at 81 | ) 82 | and ( 83 | self._forced_service._service_after is other._forced_service._service_after 84 | or self._forced_service._service_after == other._forced_service._service_after 85 | ) 86 | and ( 87 | self._forced_service._service_before is other._forced_service._service_before 88 | or self._forced_service._service_before == other._forced_service._service_before 89 | ) 90 | ) 91 | return NotImplemented 92 | 93 | 94 | class VehicleStepStart(VehicleStepBaseclass): 95 | """ 96 | Vehicle step describing the start of the vehicle's journey. 97 | 98 | Attributes: 99 | service_at: 100 | If not None, the time point the start step should begin at. 101 | service_after: 102 | If not None, the time point the start step should begin after. 103 | service_before: 104 | If not None, the time point the start step should begin before. 105 | 106 | Args: 107 | service_at: 108 | Constrain start step time to begin at a give time point. 109 | service_after: 110 | Constrain start step time to begin after a give time point. 111 | service_before: 112 | Constrain start step time to begin before a give time point. 113 | 114 | Examples: 115 | >>> vroom.VehicleStepStart() 116 | vroom.VehicleStepStart() 117 | 118 | See also: 119 | :class:`vroom.VehicleStep` 120 | 121 | """ 122 | 123 | def __init__( 124 | self, 125 | service_at: Optional[int] = None, 126 | service_after: Optional[int] = None, 127 | service_before: Optional[int] = None, 128 | ) -> None: 129 | _vroom.VehicleStep.__init__( 130 | self, 131 | step_type=_vroom.STEP_TYPE.START, 132 | forced_service=_vroom.ForcedService( 133 | service_at=service_at, 134 | service_after=service_after, 135 | service_before=service_before, 136 | ), 137 | ) 138 | 139 | 140 | class VehicleStepEnd(VehicleStepBaseclass): 141 | """ 142 | Vehicle step describing the end of the vehicle's journey. 143 | 144 | Attributes: 145 | service_at: 146 | If not None, the time point the end step should begin at. 147 | service_after: 148 | If not None, the time point the end step should begin after. 149 | service_before: 150 | If not None, the time point the end step should begin before. 151 | 152 | Args: 153 | service_at: 154 | Constrain end step time to begin at a give time point. 155 | service_after: 156 | Constrain end step time to begin after a give time point. 157 | service_before: 158 | Constrain end step time to begin before a give time point. 159 | 160 | Examples: 161 | >>> vroom.VehicleStepEnd() 162 | vroom.VehicleStepEnd() 163 | 164 | See also: 165 | :class:`vroom.VehicleStep` 166 | 167 | """ 168 | 169 | def __init__( 170 | self, 171 | service_at: Optional[int] = None, 172 | service_after: Optional[int] = None, 173 | service_before: Optional[int] = None, 174 | ) -> None: 175 | _vroom.VehicleStep.__init__( 176 | self, 177 | step_type=_vroom.STEP_TYPE.END, 178 | forced_service=_vroom.ForcedService( 179 | service_at=service_at, 180 | service_after=service_after, 181 | service_before=service_before, 182 | ), 183 | ) 184 | 185 | 186 | class VehicleStepBreak(VehicleStepBaseclass): 187 | """ 188 | Vehicle step describing a break a vehicle must perform. 189 | 190 | Attributes: 191 | id: 192 | Reference to the break this step is associated with. 193 | service_at: 194 | If not None, the time point the break should begin at. 195 | service_after: 196 | If not None, the time point the break should begin after. 197 | service_before: 198 | If not None, the time point the break should begin before. 199 | 200 | Args: 201 | id: 202 | Reference to the break this step is associated with. 203 | service_at: 204 | Constrain break time to begin at a give time point. 205 | service_after: 206 | Constrain break time to begin after a give time point. 207 | service_before: 208 | Constrain break time to begin before a give time point. 209 | 210 | Examples: 211 | >>> vroom.VehicleStepBreak(1) 212 | vroom.VehicleStepBreak(1) 213 | 214 | See also: 215 | :class:`vroom.VehicleStep` 216 | 217 | """ 218 | 219 | def __init__( 220 | self, 221 | id: int, 222 | service_at: Optional[int] = None, 223 | service_after: Optional[int] = None, 224 | service_before: Optional[int] = None, 225 | ) -> None: 226 | _vroom.VehicleStep.__init__( 227 | self, 228 | step_type=_vroom.STEP_TYPE.BREAK, 229 | id=id, 230 | forced_service=_vroom.ForcedService( 231 | service_at=service_at, 232 | service_after=service_after, 233 | service_before=service_before, 234 | ), 235 | ) 236 | 237 | 238 | class VehicleStepSingle(VehicleStepBaseclass): 239 | """ 240 | Vehicle step describing a single job a vehicle must perform. 241 | 242 | Attributes: 243 | id: 244 | Reference to the single job this step is associated with. 245 | service_at: 246 | If not None, the time point the service time should begin at. 247 | service_after: 248 | If not None, the time point the service time should begin after. 249 | service_before: 250 | If not None, the time point the service time should begin before. 251 | 252 | Args: 253 | id: 254 | Reference to the single job this step is associated with. 255 | service_at: 256 | Constrain service time to begin at a give time point. 257 | service_after: 258 | Constrain service time to begin after a give time point. 259 | service_before: 260 | Constrain service time to begin before a give time point. 261 | 262 | Examples: 263 | >>> vroom.VehicleStepSingle(2) 264 | vroom.VehicleStepSingle(2) 265 | 266 | See also: 267 | :class:`vroom.VehicleStep` 268 | 269 | """ 270 | 271 | def __init__( 272 | self, 273 | id: int, 274 | service_at: Optional[int] = None, 275 | service_after: Optional[int] = None, 276 | service_before: Optional[int] = None, 277 | ) -> None: 278 | _vroom.VehicleStep.__init__( 279 | self, 280 | job_type=_vroom.JOB_TYPE.SINGLE, 281 | id=id, 282 | forced_service=_vroom.ForcedService( 283 | service_at=service_at, 284 | service_after=service_after, 285 | service_before=service_before, 286 | ), 287 | ) 288 | 289 | 290 | class VehicleStepDelivery(VehicleStepBaseclass): 291 | """ 292 | Vehicle step describing a delivery a vehicle must perform. 293 | 294 | Attributes: 295 | id: 296 | Reference to the delivery job this step is associated with. 297 | service_at: 298 | If not None, the time point the service time should begin at. 299 | service_after: 300 | If not None, the time point the service time should begin after. 301 | service_before: 302 | If not None, the time point the service time should begin before. 303 | 304 | Args: 305 | id: 306 | Reference to the delivery job this step is associated with. 307 | service_at: 308 | Constrain service time to begin at a give time point. 309 | service_after: 310 | Constrain service time to begin after a give time point. 311 | service_before: 312 | Constrain service time to begin before a give time point. 313 | 314 | Examples: 315 | >>> vroom.VehicleStepDelivery(2) 316 | vroom.VehicleStepDelivery(2) 317 | 318 | See also: 319 | :class:`vroom.VehicleStep` 320 | 321 | """ 322 | 323 | def __init__( 324 | self, 325 | id: int, 326 | service_at: Optional[int] = None, 327 | service_after: Optional[int] = None, 328 | service_before: Optional[int] = None, 329 | ) -> None: 330 | _vroom.VehicleStep.__init__( 331 | self, 332 | job_type=_vroom.JOB_TYPE.DELIVERY, 333 | id=id, 334 | forced_service=_vroom.ForcedService( 335 | service_at=service_at, 336 | service_after=service_after, 337 | service_before=service_before, 338 | ), 339 | ) 340 | 341 | 342 | class VehicleStepPickup(VehicleStepBaseclass): 343 | """ 344 | Vehicle step describing a pickup a vehicle must perform. 345 | 346 | Attributes: 347 | id: 348 | Reference to the pickup job this step is associated with. 349 | service_at: 350 | If not None, the time point the service time should begin at. 351 | service_after: 352 | If not None, the time point the service time should begin after. 353 | service_before: 354 | If not None, the time point the service time should begin before. 355 | 356 | Args: 357 | id: 358 | Reference to the pickup job this step is associated with. 359 | service_at: 360 | Constrain service time to begin at a give time point. 361 | service_after: 362 | Constrain service time to begin after a give time point. 363 | service_before: 364 | Constrain service time to begin before a give time point. 365 | 366 | Examples: 367 | >>> vroom.VehicleStepPickup(3) 368 | vroom.VehicleStepPickup(3) 369 | 370 | See also: 371 | :class:`vroom.VehicleStep` 372 | 373 | """ 374 | 375 | def __init__( 376 | self, 377 | id: int, 378 | service_at: Optional[int] = None, 379 | service_after: Optional[int] = None, 380 | service_before: Optional[int] = None, 381 | ) -> None: 382 | _vroom.VehicleStep.__init__( 383 | self, 384 | job_type=_vroom.JOB_TYPE.PICKUP, 385 | id=id, 386 | forced_service=_vroom.ForcedService( 387 | service_at=service_at, 388 | service_after=service_after, 389 | service_before=service_before, 390 | ), 391 | ) 392 | 393 | 394 | class VehicleStep( 395 | VehicleStepStart, 396 | VehicleStepEnd, 397 | VehicleStepBreak, 398 | VehicleStepSingle, 399 | VehicleStepPickup, 400 | VehicleStepDelivery, 401 | ): 402 | """ 403 | Vehicle step constructor describing a custom route for a vehicle. 404 | 405 | Depending on `step_type`, creates one of the supported sub-types on 406 | construction. 407 | 408 | Args: 409 | step_type: 410 | The type of step in question. Choose from: `start`, `end`, `break`, 411 | `single`, `pickup`, and `delivery`. 412 | id: 413 | Reference to the job/break the step is associated with. 414 | Not used for `step_type == "start"` and `step_type == "end"`. 415 | service_at: 416 | Hard constraint that the step in question should be performed 417 | at a give time point. 418 | service_after: 419 | Hard constraint that the step in question should be performed 420 | after a give time point. 421 | service_before: 422 | Hard constraint that the step in question should be performed 423 | before a give time point. 424 | 425 | Examples: 426 | >>> vroom.VehicleStep("start") 427 | vroom.VehicleStepStart() 428 | >>> vroom.VehicleStep("end") 429 | vroom.VehicleStepEnd() 430 | >>> vroom.VehicleStep("break", 1) 431 | vroom.VehicleStepBreak(1) 432 | >>> vroom.VehicleStep("single", 2) 433 | vroom.VehicleStepSingle(2) 434 | >>> vroom.VehicleStep("pickup", 3) 435 | vroom.VehicleStepPickup(3) 436 | >>> vroom.VehicleStep("delivery", 4) 437 | vroom.VehicleStepDelivery(4) 438 | 439 | See also: 440 | :class:`vroom.VehicleStepStart` 441 | :class:`vroom.VehicleStepEnd` 442 | :class:`vroom.VehicleStepBreak` 443 | :class:`vroom.VehicleStepSingle` 444 | :class:`vroom.VehicleStepPickup` 445 | :class:`vroom.VehicleStepDelivery` 446 | 447 | """ 448 | 449 | def __new__( 450 | cls, 451 | step_type: Union[VehicleStep, VEHICLE_STEP_TYPE], 452 | id: Optional[int] = None, 453 | *, 454 | service_at: Optional[int] = None, 455 | service_after: Optional[int] = None, 456 | service_before: Optional[int] = None, 457 | ): 458 | """Step that a vehicle is to perform.""" 459 | if isinstance(step_type, _vroom.VehicleStep): 460 | assert id is None 461 | assert service_at is None 462 | assert service_after is None 463 | assert service_before is None 464 | 465 | id = step_type._id 466 | if step_type._step_type in (_vroom.STEP_TYPE.START, _vroom.STEP_TYPE.END): 467 | assert id == 0 468 | id = None 469 | 470 | service_at = step_type._forced_service._service_at 471 | service_after = step_type._forced_service._service_after 472 | service_before = step_type._forced_service._service_before 473 | 474 | if service_at: 475 | service_at = _vroom.scale_to_user_duration(service_at) 476 | if service_after: 477 | service_after = _vroom.scale_to_user_duration(service_after) 478 | if service_before: 479 | service_before = _vroom.scale_to_user_duration(service_before) 480 | 481 | if step_type._step_type == _vroom.STEP_TYPE.JOB: 482 | step_type_map = { 483 | _vroom.JOB_TYPE.SINGLE: VEHICLE_STEP_TYPE.SINGLE, 484 | _vroom.JOB_TYPE.DELIVERY: VEHICLE_STEP_TYPE.DELIVERY, 485 | _vroom.JOB_TYPE.PICKUP: VEHICLE_STEP_TYPE.PICKUP, 486 | } 487 | step_type = step_type_map[step_type._job_type] 488 | else: 489 | step_type_map = { 490 | _vroom.STEP_TYPE.START: VEHICLE_STEP_TYPE.START, 491 | _vroom.STEP_TYPE.END: VEHICLE_STEP_TYPE.END, 492 | _vroom.STEP_TYPE.BREAK: VEHICLE_STEP_TYPE.BREAK, 493 | } 494 | step_type = step_type_map[step_type._step_type] 495 | 496 | kwargs = dict( 497 | service_at=service_at, 498 | service_after=service_after, 499 | service_before=service_before, 500 | ) 501 | if id is not None: 502 | kwargs["id"] = id 503 | step_type = VEHICLE_STEP_TYPE(step_type) 504 | vehicle_step_classes = { 505 | VEHICLE_STEP_TYPE.START: VehicleStepStart, 506 | VEHICLE_STEP_TYPE.END: VehicleStepEnd, 507 | VEHICLE_STEP_TYPE.BREAK: VehicleStepBreak, 508 | VEHICLE_STEP_TYPE.SINGLE: VehicleStepSingle, 509 | VEHICLE_STEP_TYPE.PICKUP: VehicleStepPickup, 510 | VEHICLE_STEP_TYPE.DELIVERY: VehicleStepDelivery, 511 | } 512 | cls = vehicle_step_classes[step_type] 513 | 514 | instance = _vroom.VehicleStep.__new__(cls, **kwargs) 515 | instance.__init__(**kwargs) 516 | return instance 517 | --------------------------------------------------------------------------------