diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 928cdc8..f6c9a0c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -11,7 +11,7 @@ on: jobs: test: name: test - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -31,6 +31,8 @@ jobs: python -m pip install -r build-requirements.txt - name: "Install pyvroom" + env: + CXX: g++-14 run: | # Because `pip install -e .` does not play nice with gcov, we go old school: CFLAGS="-coverage" python setup.py build_ext --inplace diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2636e49..a8cc2b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: - name: Check metadata run: pipx run twine check dist/* - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: path: dist/*.tar.gz @@ -78,25 +78,31 @@ jobs: with: platforms: all + - name: Set version + if: matrix.platform != 'macos-arm' && matrix.platform != 'macos-intel' + run: | + sed -i 's/^version = 0\.1\.0$/version = ${{ github.ref_name }}/' setup.cfg + + - name: Set version + if: matrix.platform == 'macos-arm' || matrix.platform == 'macos-intel' + run: | + sed -i "" 's/^version = 0\.1\.0$/version = ${{ github.ref_name }}/' setup.cfg + - name: Build wheels if: matrix.platform != 'macos-arm' uses: pypa/cibuildwheel@v2.19.2 env: MACOSX_DEPLOYMENT_TARGET: 13.0 - CC: gcc-13 - CXX: g++-13 + CC: gcc-14 + CXX: g++-14 - name: Build wheels if: matrix.platform == 'macos-arm' uses: pypa/cibuildwheel@v2.19.2 env: MACOSX_DEPLOYMENT_TARGET: 14.0 - CC: gcc-13 - CXX: g++-13 - - - name: Verify clean directory - run: git diff --exit-code - shell: bash + CC: gcc-14 + CXX: g++-14 - name: Upload wheels uses: actions/upload-artifact@v3 diff --git a/README.rst b/README.rst index 89fd73c..569c673 100644 --- a/README.rst +++ b/README.rst @@ -96,7 +96,7 @@ Usage with a routing engine >>> sol = problem_instance.solve(exploration_level=5, nb_threads=4) >>> print(sol.summary.duration) - 2714 + 4041 Installation ------------ diff --git a/pyproject.toml b/pyproject.toml index dddddfa..6a4d67a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,8 +2,6 @@ requires = [ "setuptools>=45", "wheel", - "setuptools_scm>=6.2", - "setuptools_scm_git_archive", "pybind11>=2.8.0", ] diff --git a/src/_vroom.cpp b/src/_vroom.cpp index 1988d6a..9c286cd 100644 --- a/src/_vroom.cpp +++ b/src/_vroom.cpp @@ -42,7 +42,7 @@ #include "structures/typedefs.h" #include "structures/generic/edge.cpp" -#include "structures/generic/matrix.cpp" +#include "structures/generic/matrix.h" #include "structures/generic/undirected_graph.cpp" #include "structures/vroom/cost_wrapper.cpp" @@ -137,8 +137,8 @@ PYBIND11_MODULE(_vroom, m) { .def(py::init()); py::class_(m, "Server") - .def(py::init(), - py::arg("host") = "0.0.0.0", py::arg("port") = "5000"); + .def(py::init(), + py::arg("host") = "0.0.0.0", py::arg("port") = "5000", py::arg("path") = ""); py::class_(m, "Violations") .def(py::init<>()) diff --git a/src/bind/enums.cpp b/src/bind/enums.cpp index 6121bd5..5915a67 100644 --- a/src/bind/enums.cpp +++ b/src/bind/enums.cpp @@ -29,7 +29,6 @@ void init_enums(py::module_ &m) { py::enum_(m, "HEURISTIC") .value("BASIC", vroom::HEURISTIC::BASIC) .value("DYNAMIC", vroom::HEURISTIC::DYNAMIC) - .value("INIT_ROUTES", vroom::HEURISTIC::INIT_ROUTES) .export_values(); py::enum_(m, "INIT") diff --git a/src/bind/input/input.cpp b/src/bind/input/input.cpp index b95b4a7..53552cb 100644 --- a/src/bind/input/input.cpp +++ b/src/bind/input/input.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "structures/cl_args.cpp" #include "structures/vroom/input/input.cpp" @@ -13,17 +14,17 @@ void init_input(py::module_ &m) { py::class_(m, "Input") .def( - py::init([](const vroom::io::Servers &servers, vroom::ROUTER router) { - return new vroom::Input(servers, router); + py::init([](const vroom::io::Servers &servers, vroom::ROUTER router, bool apply_TSPFix) { + return new vroom::Input(servers, router, apply_TSPFix); }), "Class initializer.", py::arg("servers") = std::map(), - py::arg("router") = vroom::ROUTER::OSRM) + py::arg("router") = vroom::ROUTER::OSRM, + py::arg("apply_TSPFix") = false) .def_readonly("jobs", &vroom::Input::jobs) .def_readonly("vehicles", &vroom::Input::vehicles) .def("_from_json", &vroom::io::parse, py::arg("json_string"), py::arg("geometry")) - .def("_set_amount_size", &vroom::Input::set_amount_size) .def("_set_geometry", &vroom::Input::set_geometry) .def("_add_job", &vroom::Input::add_job) .def("_add_shipment", &vroom::Input::add_shipment) @@ -43,7 +44,6 @@ void init_input(py::module_ &m) { vroom::Matrix &m) { self.set_costs_matrix(profile, std::move(m)); }) - .def("zero_amount", &vroom::Input::zero_amount) .def("has_skills", &vroom::Input::has_skills) .def("has_jobs", &vroom::Input::has_jobs) .def("has_shipments", &vroom::Input::has_shipments) @@ -51,10 +51,13 @@ void init_input(py::module_ &m) { .def("has_homogeneous_locations", &vroom::Input::has_homogeneous_locations) .def("has_homogeneous_profiles", &vroom::Input::has_homogeneous_profiles) - // .def("vehicle_ok_with_job", &vroom::Input::vehicle_ok_with_job) - .def("_solve", &vroom::Input::solve, "Solve problem.", - py::arg("exploration_level"), py::arg("nb_threads") = 1, - py::arg("timeout") = vroom::Timeout(), - py::arg("h_param") = std::vector()) + .def("has_homogeneous_costs", &vroom::Input::has_homogeneous_costs) + .def("_solve", + [](vroom::Input &self, unsigned exploration_level, unsigned nb_threads, const vroom::Timeout& timeout, const std::vector h_param) { + return self.solve(exploration_level, nb_threads, timeout, h_param); + }, + "Solve routing problem", + py::arg("exploration_level"), py::arg("nb_threads"), py::arg("timeout"), py::arg("h_param") + ) .def("check", &vroom::Input::check); } diff --git a/src/bind/vehicle.cpp b/src/bind/vehicle.cpp index 1658893..6d3011f 100644 --- a/src/bind/vehicle.cpp +++ b/src/bind/vehicle.cpp @@ -7,11 +7,12 @@ namespace py = pybind11; void init_vehicle(py::module_ &m) { py::class_(m, "VehicleCosts") - .def(py::init(), + .def(py::init(), "VehicleCost constructor.", py::arg("fixed") = 0, - py::arg("per_hour") = 3600) + py::arg("per_hour") = 3600, py::arg("per_km") = 0) .def_readonly("_fixed", &vroom::VehicleCosts::fixed) - .def_readonly("_per_hour", &vroom::VehicleCosts::per_hour); + .def_readonly("_per_hour", &vroom::VehicleCosts::per_hour) + .def_readonly("_per_km", &vroom::VehicleCosts::per_km); py::class_(m, "CostWrapper") .def(py::init(), diff --git a/src/vroom/input/input.py b/src/vroom/input/input.py index 75eff3b..3e25551 100644 --- a/src/vroom/input/input.py +++ b/src/vroom/input/input.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Dict, Optional, Sequence, Set, Union from pathlib import Path +from datetime import timedelta from numpy.typing import ArrayLike import numpy @@ -35,16 +36,14 @@ class Input(_vroom.Input): def __init__( self, - amount_size: Optional[int] = None, servers: Optional[Dict[str, Union[str, _vroom.Server]]] = None, router: _vroom.ROUTER = _vroom.ROUTER.OSRM, + apply_TSPFix: bool = False, + geometry: bool = False, ) -> None: """Class initializer. Args: - amount_size: - The size of the job to be transported. Used to verify all jobs - have the same size limit. servers: Assuming no custom duration matrix is provided (from `set_durations_matrix`), use this dict to configure the @@ -53,24 +52,30 @@ def __init__( router: If servers is used, define what kind of server is provided. See `vroom.ROUTER` enum for options. + apply_TSPFix: + Experimental local search operator. + geometry: + Add detailed route geometry and distance. """ if servers is None: servers = {} for key, server in servers.items(): if isinstance(server, str): servers[key] = _vroom.Server(*server.split(":")) - self._amount_size = amount_size self._servers = servers self._router = router - _vroom.Input.__init__(self, servers=servers, router=router) - if amount_size is not None: - self._set_amount_size(amount_size) + _vroom.Input.__init__( + self, + servers=servers, + router=router, + apply_TSPFix=apply_TSPFix, + ) + if geometry: + self.set_geometry() def __repr__(self) -> str: """String representation.""" args = [] - if self._amount_size is not None: - args.append(f"amount_size={self._amount_size}") if self._servers: args.append(f"servers={self._servers}") if self._router != _vroom.ROUTER.OSRM: @@ -117,21 +122,10 @@ def from_json( return instance def set_geometry(self): + """Add detailed route geometry and distance.""" self._geometry = True return self._set_geometry(True) - def set_amount_size(self, *amount_sizes: int) -> None: - """Add amount sizes.""" - sizes = set(amount_sizes) - if self._amount_size is not None: - sizes.add(self._amount_size) - if len(sizes) > 1: - raise _vroom.VroomInputException(f"Inconsistent capacity lengths: {sizes}") - if self._amount_size is None: - size = sizes.pop() - self._amount_size = size - self._set_amount_size(size) - def add_job( self, job: Union[Job, Shipment, Sequence[Job], Sequence[Shipment]], @@ -157,14 +151,9 @@ def add_job( jobs = [job] if isinstance(job, (Job, Shipment)) else job for job_ in jobs: if isinstance(job_, Job): - if job_._pickup: - self.set_amount_size(len(job_._pickup)) - if job_._delivery: - self.set_amount_size(len(job_._delivery)) self._add_job(job_) elif isinstance(job_, Shipment): - self.set_amount_size(len(job_.amount)) self._add_shipment( _vroom.Job( id=job_.pickup.id, @@ -221,7 +210,6 @@ def add_shipment( The job priority level, where 0 is the most important and 100 is the least important. """ - self.set_amount_size(len(amount)) if skills is None: skills = set() self._add_shipment( @@ -266,7 +254,6 @@ def add_vehicle( vehicles = [vehicle] if isinstance(vehicle, _vroom.Vehicle) else vehicle if not vehicles: return - self.set_amount_size(*[len(vehicle_.capacity) for vehicle_ in vehicles]) for vehicle_ in vehicles: self._add_vehicle(vehicle_) @@ -334,12 +321,28 @@ def set_costs_matrix( def solve( self, exploration_level: int, - nb_threads: int, + nb_threads: int = 4, + timeout: Optional[timedelta] = None, + h_param = (), ) -> Solution: + """Solve routing problem. + + Args: + exploration_level: + The exploration level to use. Number between 1 and 5. + nb_threads: + The number of available threads. + timeout: + Stop the solving process after a given amount of time. + """ + assert timeout is None or isinstance(timeout, (None, timedelta)), ( + f"unknown timeout type: {timeout}") solution = Solution( self._solve( - exploration_level=exploration_level, - nb_threads=nb_threads, + exploration_level=int(exploration_level), + nb_threads=int(nb_threads), + timeout=timeout, + h_param=list(h_param), ) ) solution._geometry = self._geometry diff --git a/src/vroom/vehicle.py b/src/vroom/vehicle.py index 2c0d6a8..6848f33 100644 --- a/src/vroom/vehicle.py +++ b/src/vroom/vehicle.py @@ -24,19 +24,22 @@ class VehicleCosts(_vroom.VehicleCosts): A fixed price for the vehicle to be utilized. per_hour: The price per hour to utilize the vehicle. + per_km: + The price per kilometer to utilize the vehicle. Examples: >>> vroom.VehicleCosts() VehicleCosts() - >>> vroom.VehicleCosts(fixed=100, per_hour=50) - VehicleCosts(fixed=100, per_hour=50) + >>> vroom.VehicleCosts(fixed=100, per_hour=50, per_km=25) + VehicleCosts(fixed=100, per_hour=50, per_km=25) """ - def __init__(self, fixed: int = 0, per_hour: int = 3600): + def __init__(self, fixed: int = 0, per_hour: int = 3600, per_km: int = 0): _vroom.VehicleCosts.__init__( self, fixed=int(fixed), per_hour=int(per_hour), + per_km=int(per_km), ) @property @@ -47,11 +50,15 @@ def fixed(self) -> int: def per_hour(self) -> int: return self._per_hour + @property + def per_km(self) -> int: + return self._per_km + def __bool__(self) -> bool: - return self.fixed != 0 or self.per_hour != 3600 + return self.fixed != 0 or self.per_hour != 3600 or self.per_km != 0 def __repr__(self): - args = f"fixed={self.fixed}, per_hour={self.per_hour}" if self else "" + args = f"fixed={self.fixed}, per_hour={self.per_hour}, per_km={self.per_km}" if self else "" return f"{self.__class__.__name__}({args})" @@ -231,6 +238,7 @@ def costs(self) -> VehicleCosts: return VehicleCosts( fixed=self._costs._fixed, per_hour=self._costs._per_hour, + per_km=self._costs._per_km, ) @property diff --git a/vroom b/vroom index 1fd711b..82327fe 160000 --- a/vroom +++ b/vroom @@ -1 +1 @@ -Subproject commit 1fd711bc8c20326dd8e9538e2c7e4cb1ebd67bdb +Subproject commit 82327fe93a37d3e117580261d61a4c59675e6fdd