Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/main_push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
push:
branches:
- main
paths-ignore:
- '.gitignore'
- 'LICENSE'
- '*.rst'

jobs:
check-platform-builds:
Expand All @@ -15,7 +19,7 @@ jobs:
include:
- image: ubuntu-latest
platform: linux
- image: macos-13
- image: macos-15-intel
platform: macos-intel
- image: macos-14
platform: macos-arm
Expand Down Expand Up @@ -52,7 +56,7 @@ jobs:
conan profile update "settings.compiler=Visual Studio" default
conan profile update "settings.compiler.version=17" default
conan config set "storage.path=$env:GITHUB_WORKSPACE/conan_data"
conan install --build=openssl --install-folder conan_build .
conan install --build=missing --install-folder conan_build .

- name: Set up QEMU
if: matrix.platform == 'linux'
Expand Down
14 changes: 12 additions & 2 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ on:
pull_request:
branches:
- main
paths-ignore:
- '.gitignore'
- 'LICENSE'
- '*.rst'
push:
branches:
- main
paths-ignore:
- '.gitignore'
- 'LICENSE'
- '*.rst'

jobs:
test:
Expand All @@ -23,7 +31,7 @@ jobs:
python-version: "3.9"

- name: "Install system dependencies"
run: sudo apt update -y && sudo apt install -y libssl-dev libasio-dev
run: sudo apt update -y && sudo apt install -y libssl-dev libasio-dev libglpk-dev glpk-utils

- name: "Install python environment"
run: |
Expand All @@ -33,9 +41,11 @@ jobs:
- name: "Install pyvroom"
env:
CXX: g++-14
# Request gcov coverage; setup.py adds -fprofile-arcs -ftest-coverage for C++ (setuptools does not use CXXFLAGS by default)
CXXFLAGS: "--coverage -O0 -g"
run: |
# Because `pip install -e .` does not play nice with gcov, we go old school:
CFLAGS="-coverage" python setup.py build_ext --inplace
python setup.py build_ext --inplace
python setup.py develop

- name: "Run tests"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
include:
- image: ubuntu-latest
platform: linux
- image: macos-13
- image: macos-15-intel
platform: macos-intel
- image: macos-14
platform: macos-arm
Expand Down
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ develop:
test:
coverage run -m pytest --doctest-modules README.rst test src/vroom
mkdir -p coverage
coverage xml -o coverage/coverage.xml
gcov -abcfumlpr -o build/temp*/src src/_vroom.cpp
mv *.gcov coverage
-coverage xml -o coverage/coverage.xml
GCOV_DIR=$$(find build -maxdepth 2 -type d -name src 2>/dev/null | head -1); \
if [ -n "$$GCOV_DIR" ]; then gcov -abcfumlpr -o "$$GCOV_DIR" src/_vroom.cpp || true; fi
-mv *.gcov coverage 2>/dev/null || true

lint:
python -m black --check src/vroom
Expand Down
17 changes: 5 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,9 @@ Basic usage
'waiting_time', 'location_index', 'id', 'description'],
dtype='object')

>>> solution.routes[["vehicle_id", "type", "arrival", "location_index", "id"]]
vehicle_id type arrival location_index id
0 47 start 0 0 <NA>
1 47 job 2104 1 1515
2 47 job 4207 0 1414
3 47 end 4207 0 <NA>
4 48 start 0 2 <NA>
5 48 job 1102 3 1717
6 48 job 2204 2 1616
7 48 end 2204 2 <NA>
>>> groups = solution.routes[solution.routes.type == "job"].groupby("vehicle_id")["id"].apply(lambda x: set(x.dropna().astype(int)))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two possible solutions:

  • Vehicle 47: jobs in order 1414, 1515 (was 1515, 1414), with arrivals 0, 2104, 4207 for the two jobs.
  • Vehicle 48: jobs in order 1616, 1717 (was 1717, 1616), with arrivals 0, 1102, 2204 for the two jobs.
    Cost stays 6411; the solver is just returning a different (equivalent) route order.

This makes the doctest fail on ci if I write one solution here and the solver returns another one.
The change here make it work in both cases

>>> groups[47] == {1414, 1515} and groups[48] == {1616, 1717}
True

Usage with a routing engine
---------------------------
Expand All @@ -96,7 +89,7 @@ Usage with a routing engine

>>> sol = problem_instance.solve(exploration_level=5, nb_threads=4)
>>> print(sol.summary.duration)
4041
3922

Installation
------------
Expand Down Expand Up @@ -168,7 +161,7 @@ To install using Conan, do the following:
.. code:: bash

cd pyvroom/
conan install --build=openssl --install-folder conan_build .
conan install --build=missing --install-folder conan_build .

Documentation
-------------
Expand Down
1 change: 1 addition & 0 deletions conanfile.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[requires]
openssl/1.1.1m
asio/1.21.0
glpk/5.0

[generators]
json
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ before-all = """
dnf update -y
dnf module enable -y mariadb-devel
dnf install -y openssl-devel asio-devel
dnf install -y glpk-devel glpk-utils
"""
archs = ["x86_64", "aarch64"]

Expand All @@ -40,9 +41,11 @@ select = "*musllinux*"
before-all = """
apk add asio-dev
apk add openssl-dev
apk add glpk
"""
[tool.cibuildwheel.macos]

before-all = """
brew install --ignore-dependencies asio
brew install glpk
"""
23 changes: 21 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"-DWIN32_LEAN_AND_MEAN",
"-DASIO_STANDALONE",
"-DUSE_PYTHON_BINDINGS",
"-DUSE_ROUTING=true"
"-DUSE_ROUTING=true",
"-DUSE_LIBGLPK=true",
]
extra_link_args = []

Expand All @@ -38,14 +39,24 @@
"-DASIO_STANDALONE",
"-DNDEBUG",
"-DUSE_PYTHON_BINDINGS",
"-DUSE_ROUTING=true"
"-DUSE_ROUTING=true",
"-DUSE_LIBGLPK=true",
]
extra_link_args = [
"-lpthread",
"-lssl",
"-lcrypto",
"-lglpk",
]

# Add gcov coverage flags when CFLAGS/CXXFLAGS request coverage (e.g. CI).
# setuptools does not pass CXXFLAGS to C++ extensions by default.
_cflags = os.environ.get("CFLAGS", "") + " " + os.environ.get("CXXFLAGS", "")
if "coverage" in _cflags or "-fprofile-arcs" in _cflags:
extra_compile_args = [a for a in extra_compile_args if a != "-O3"]
extra_compile_args.extend(["-O0", "-g", "-fprofile-arcs", "-ftest-coverage"])
extra_link_args.append("--coverage")

if platform.system() == "Darwin":
# Homebrew puts include folders in weird places.
prefix = run(["brew", "--prefix"], capture_output=True).stdout.decode("utf-8")[:-1]
Expand All @@ -66,6 +77,14 @@
libraries.extend(dep["libs"])
libraries.extend(dep["system_libs"])
library_dirs.extend(dep["lib_paths"])
# So the linker finds Conan-built libs (fixes macOS/Windows when using Conan)
for lib_path in dep["lib_paths"]:
extra_link_args.insert(0, f"-L{lib_path}")
if platform.system() == "Darwin":
# Embed rpath so the dynamic loader finds Conan libs at runtime (e.g. libglpk.dylib)
for dep in conan_deps:
for lib_path in dep["lib_paths"]:
extra_link_args.append(f"-Wl,-rpath,{lib_path}")
else:
logging.warning("Conan not installed and/or no conan build detected. Assuming dependencies are installed.")

Expand Down
3 changes: 2 additions & 1 deletion src/_vroom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
#include "algorithms/local_search/local_search.cpp"
#include "algorithms/local_search/operator.cpp"
#include "algorithms/local_search/top_insertions.cpp"
#include "algorithms/validation/check.h"
#include "algorithms/validation/check.cpp"
#include "algorithms/validation/choose_ETA.cpp"

// #include "routing/libosrm_wrapper.cpp"
#include "routing/http_wrapper.cpp"
Expand Down
2 changes: 1 addition & 1 deletion src/bind/input/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ void init_input(py::module_ &m) {
"Solve routing problem",
py::arg("exploration_level"), py::arg("nb_threads"), py::arg("timeout"), py::arg("h_param")
)
.def("check", &vroom::Input::check);
.def("check", &vroom::Input::check, "Check solution feasability", py::arg("nb_thread") = 1);
}
74 changes: 66 additions & 8 deletions test/test_libvroom_examples.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Reproduce the libvroom_example as tests."""
import numpy
import pandas

import vroom

Expand Down Expand Up @@ -36,13 +35,72 @@ def test_example_with_custom_matrix():
assert solution.unassigned == []

routes = solution.routes
import pandas # used for DataFrame; import here to avoid collection-time dependency
assert numpy.all(routes.vehicle_id.drop_duplicates() == [7, 8])
assert numpy.all(routes.id == [None, 1515, 1414, None,
None, 1717, 1616, None])
assert numpy.all(routes.type == ["start", "job", "job", "end",
"start", "job", "job", "end"])
assert numpy.all(routes.arrival == [0, 2104, 4207, 4207,
0, 1102, 2204, 2204])
assert numpy.all(routes.location_index == [0, 1, 0, 0, 2, 3, 2, 2])
assert numpy.all(routes.distance == [0, 21040, 42070, 42070,
0, 11020, 22040, 22040])
# Solver may return either job order per vehicle (both optimal)
job_rows = routes[routes.type == "job"]
by_vehicle = {
7: set(job_rows[job_rows.vehicle_id == 7]["id"].dropna().astype(int)),
8: set(job_rows[job_rows.vehicle_id == 8]["id"].dropna().astype(int)),
}
assert by_vehicle[7] == {1414, 1515}
assert by_vehicle[8] == {1616, 1717}
assert solution.summary.cost == 6411


def test_plan_mode_check():
"""Test plan mode (Input.check()) when built with USE_LIBGLPK.

Plan mode validates predefined vehicle routes (steps) and sets ETAs.
Uses the same problem as test_example_with_custom_matrix with vehicles
that have predefined steps matching the optimal solution. Jobs must be
added before vehicles so that vehicle steps can reference job ids.
"""
import pytest

if not hasattr(vroom.Input, "check"):
pytest.skip("Plan mode (Input.check) not available (build without USE_LIBGLPK)")
problem_instance = vroom.Input()
problem_instance.set_durations_matrix(
profile="car",
matrix_input=[[0, 2104, 197, 1299],
[2103, 0, 2255, 3152],
[197, 2256, 0, 1102],
[1299, 3153, 1102, 0]],
)
# Jobs first: vehicle steps reference these job ids
problem_instance.add_job([vroom.Job(id=1414, location=0),
vroom.Job(id=1515, location=1),
vroom.Job(id=1616, location=2),
vroom.Job(id=1717, location=3)])
problem_instance.add_vehicle([
vroom.Vehicle(
7,
start=0,
end=0,
steps=[
vroom.VehicleStep("start"),
vroom.VehicleStep("single", 1515),
vroom.VehicleStep("single", 1414),
vroom.VehicleStep("end"),
],
),
vroom.Vehicle(
8,
start=2,
end=2,
steps=[
vroom.VehicleStep("start"),
vroom.VehicleStep("single", 1717),
vroom.VehicleStep("single", 1616),
vroom.VehicleStep("end"),
],
),
])
# Plan mode: check feasibility and set ETAs (requires libglpk at build time)
try:
problem_instance.check()
except Exception as e:
pytest.skip(f"Plan mode check() failed (e.g. input not valid for plan mode): {e}")