kayros 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+
11
+ # scikit-build-core / CMake
12
+ _skbuild/
13
+ CMakeCache.txt
14
+ CMakeFiles/
15
+ compile_commands.json
16
+
17
+ # Compiled extensions
18
+ *.so
19
+ *.pyd
20
+
21
+ # Editors / OS
22
+ .vscode/
23
+ .idea/
24
+ .DS_Store
25
+
26
+ # Nix
27
+ result
28
+ result-*
@@ -0,0 +1,39 @@
1
+ cff-version: 1.2.0
2
+ title: KAYROS
3
+ message: >-
4
+ If you use KAYROS, please cite it using the metadata
5
+ from this file.
6
+ type: software
7
+ authors:
8
+ - given-names: Florian
9
+ family-names: Rascoussier
10
+ alias: Onyr
11
+ affiliation: IMT Atlantique; INSA Lyon
12
+ orcid: "https://orcid.org/0009-0005-3253-9814"
13
+ url: "https://github.com/0nyr/kayros"
14
+ repository-code: "https://github.com/0nyr/kayros"
15
+ abstract: >-
16
+ KAYROS is an exact and anytime solver for duration-minimization
17
+ time-dependent vehicle routing problems, including TDVRPTW and TDVRP.
18
+ It combines a C++23 and pybind11 optimization core with an exact
19
+ non-decreasing continuous piecewise-linear arrival-time engine,
20
+ checker-refereed route pricing, greedy construction, and a
21
+ time-dependent ant colony optimization heuristic for the canonical
22
+ MAMUT-routing benchmark families.
23
+ keywords:
24
+ - time-dependent vehicle routing problem
25
+ - TDVRPTW
26
+ - TDVRP
27
+ - vehicle routing problem
28
+ - anytime optimization
29
+ - ant colony optimization
30
+ - route duration
31
+ - piecewise-linear functions
32
+ - NDCPWLF
33
+ - solution validation
34
+ - operations research
35
+ - combinatorial optimization
36
+ - routing
37
+ - research software
38
+ license: MIT
39
+ version: "0.0.1.dev0"
@@ -0,0 +1,37 @@
1
+ cmake_minimum_required(VERSION 3.26)
2
+ project(kayros LANGUAGES CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 23)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+ set(CMAKE_CXX_EXTENSIONS OFF)
7
+
8
+ # Local-benchmarking builds only: never enabled for shipped wheels/sdists.
9
+ option(KAYROS_NATIVE "Build with -march=native (local benchmarking only)" OFF)
10
+ # CPLEX-based Lera-Romero BPC component: requires a local CPLEX install.
11
+ # The default build must stay free of any proprietary dependency.
12
+ option(KAYROS_WITH_LERA "Build the CPLEX-based exact BPC component" OFF)
13
+
14
+ find_package(pybind11 CONFIG REQUIRED)
15
+
16
+ pybind11_add_module(_core
17
+ cpp/bindings/module.cpp
18
+ cpp/pwlf/pwlf.cpp
19
+ cpp/core/route_eval.cpp
20
+ cpp/heuristics/greedy_makespan.cpp
21
+ cpp/heuristics/aco.cpp
22
+ )
23
+ target_include_directories(_core PRIVATE cpp)
24
+ target_compile_definitions(_core PRIVATE KAYROS_VERSION="${SKBUILD_PROJECT_VERSION_FULL}")
25
+ # Bit-identical agreement with the Python reference checker requires plain
26
+ # IEEE-754 double arithmetic: no FMA contraction, ever.
27
+ target_compile_options(_core PRIVATE -ffp-contract=off)
28
+
29
+ if(KAYROS_NATIVE)
30
+ target_compile_options(_core PRIVATE -march=native)
31
+ endif()
32
+
33
+ if(KAYROS_WITH_LERA)
34
+ message(FATAL_ERROR "KAYROS_WITH_LERA is a placeholder: the Lera BPC component lands at milestone M3.7.")
35
+ endif()
36
+
37
+ install(TARGETS _core DESTINATION kayros)
kayros-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Florian Rascoussier (Onyr)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
kayros-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.1
2
+ Name: kayros
3
+ Version: 0.1.0
4
+ Summary: KAYROS — exact & anytime solver for duration-minimization time-dependent vehicle routing (TDVRPTW / TDVRP)
5
+ Author-Email: "Florian Rascoussier (Onyr)" <rascoussier.florian@gmail.com>
6
+ License: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Programming Language :: C++
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
17
+ Project-URL: Homepage, https://github.com/0nyr/kayros
18
+ Project-URL: Issues, https://github.com/0nyr/kayros/issues
19
+ Project-URL: Benchmarks, https://github.com/ANR-MAMUT/MAMUT-routing
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: mamut-routing-lib>=0.2.0
22
+ Provides-Extra: lera
23
+ Description-Content-Type: text/markdown
24
+
25
+ # KAYROS
26
+
27
+ **KAYROS** is an exact & anytime solver for **duration-minimization time-dependent vehicle routing** problems — TDVRPTW (with time windows) and TDVRP — benchmarked on the canonical [MAMUT-routing](https://github.com/ANR-MAMUT/MAMUT-routing) TD instance families.
28
+
29
+ > Status: **alpha**. v0.1.0 ships an anytime time-dependent Ant Colony Optimization heuristic on an exact non-decreasing continuous piecewise-linear (NDCPWLF) arrival-time engine, benchmarked on all four MAMUT TD families (it seeded the store's initial best-known solutions at scale). The time-dependent local-search layer is next. See the roadmap below.
30
+
31
+ ## Design principles
32
+
33
+ - **One command install, no proprietary dependency.** The default build is pure open source (C++23 + pybind11). The optional exact branch-price-and-cut component of Lera-Romero et al. (requires CPLEX) is strictly opt-in (`-DKAYROS_WITH_LERA=ON`, source build only) and never ships in wheels.
34
+ - **Exact arithmetic, checker-refereed.** Route durations are computed on NDCPWLF arrival-time functions with exact doubles — no epsilon comparisons. Every solution kayros reports is priced by the reference checker of [`mamut-routing-lib`](https://github.com/ANR-MAMUT/MAMUT-routing); the checker's value is the value.
35
+ - **POD core.** The C++ core is plain structs, flat arrays and free functions — optimization-kernel style, no framework.
36
+
37
+ ## Install
38
+
39
+ ```sh
40
+ pip install kayros
41
+ ```
42
+
43
+ This pulls everything, including the benchmark loaders and the reference checker ([`mamut-routing-lib`](https://pypi.org/project/mamut-routing-lib/)). For development:
44
+
45
+ ```sh
46
+ git clone https://github.com/0nyr/kayros && cd kayros
47
+ pip install -e . --group dev # pip >= 25.1 (or: uv pip install -e . --group dev)
48
+ ```
49
+
50
+ Requirements: Python ≥ 3.11; building from source (sdist or checkout) additionally needs a C++23 compiler and CMake ≥ 3.26 (fetched automatically by the build backend when missing).
51
+
52
+ ## Usage
53
+
54
+ ```python
55
+ import kayros
56
+
57
+ # Any MAMUT-routing TD instance (.vrp.json with its .atf.json sidecar next to it)
58
+ instance_path = "benchmarks/TDVRPTW/Dabia2013/n=25/C101.vrp.json"
59
+ solution = kayros.solve(instance_path, time_limit=10.0, seed=42)
60
+ print(solution.duration, solution.num_routes, solution.status)
61
+
62
+ # Anytime: react to every new incumbent while the solve keeps running
63
+ def on_incumbent(incumbent, routes):
64
+ print(f"[{incumbent.seconds:7.2f}s] {incumbent.value:.6f} ({incumbent.origin})")
65
+
66
+ solution = kayros.solve(instance_path, time_limit=60.0, on_incumbent=on_incumbent)
67
+
68
+ # Feed the MAMUT BKS pipeline: solution.to_benchmark_solution() returns the
69
+ # artifact accepted by mamut_routing_lib.td.bks.save_td_solution_as_bks_if_improved.
70
+ ```
71
+
72
+ `solution.duration` is always the value computed by the reference checker (`mamut_routing_lib.td.check_td_solution`) — never an internal approximation.
73
+
74
+ ## Roadmap (stage 1)
75
+
76
+ - [x] M3.0 — package scaffold, CI, PyPI wiring
77
+ - [x] M3.1 — NDCPWLF composition engine + POD instance/route core
78
+ - [x] M3.2 — exact equivalence gate against the reference checker (Dabia2013): 513 tests, 336 instances, zero divergences
79
+ - [x] M3.3 — `kayros.solve()`: greedy construction + TD-ACO
80
+ - [x] M3.4 — all four MAMUT TD families × {TDVRPTW, TDVRP}
81
+ - [x] M3.5 — large-scale runs on Grid'5000: seeded the initial best-known solutions for all 1352 MAMUT TD instances
82
+ - [x] M3.6 — anytime API (`on_incumbent`, time budgets) + **v0.1.0 on PyPI**
83
+ - [ ] M3.7 — time-dependent local search layer (LCA-BST move evaluation, Blauth et al. 2024)
84
+ - Later: optional exact BPC (`kayros[lera]`)
85
+
86
+ ## Provenance
87
+
88
+ KAYROS is developed by [Florian Rascoussier (Onyr)](https://github.com/0nyr) as part of a PhD in operations research (IMT Atlantique / INSA Lyon), under the supervision of Romain Billot, Christine Solnon and Lina Fahed. The NDCPWLF composition engine follows Visser & Spliet (2020)'s move-evaluation theorems; the TD-ACO is a rewrite of the author's heuristic layer originally built on the TDVRPTW solver of Lera-Romero, Rönnqvist & Ljungqvist (2020, MIT-licensed). MIT license.
kayros-0.1.0/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # KAYROS
2
+
3
+ **KAYROS** is an exact & anytime solver for **duration-minimization time-dependent vehicle routing** problems — TDVRPTW (with time windows) and TDVRP — benchmarked on the canonical [MAMUT-routing](https://github.com/ANR-MAMUT/MAMUT-routing) TD instance families.
4
+
5
+ > Status: **alpha**. v0.1.0 ships an anytime time-dependent Ant Colony Optimization heuristic on an exact non-decreasing continuous piecewise-linear (NDCPWLF) arrival-time engine, benchmarked on all four MAMUT TD families (it seeded the store's initial best-known solutions at scale). The time-dependent local-search layer is next. See the roadmap below.
6
+
7
+ ## Design principles
8
+
9
+ - **One command install, no proprietary dependency.** The default build is pure open source (C++23 + pybind11). The optional exact branch-price-and-cut component of Lera-Romero et al. (requires CPLEX) is strictly opt-in (`-DKAYROS_WITH_LERA=ON`, source build only) and never ships in wheels.
10
+ - **Exact arithmetic, checker-refereed.** Route durations are computed on NDCPWLF arrival-time functions with exact doubles — no epsilon comparisons. Every solution kayros reports is priced by the reference checker of [`mamut-routing-lib`](https://github.com/ANR-MAMUT/MAMUT-routing); the checker's value is the value.
11
+ - **POD core.** The C++ core is plain structs, flat arrays and free functions — optimization-kernel style, no framework.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ pip install kayros
17
+ ```
18
+
19
+ This pulls everything, including the benchmark loaders and the reference checker ([`mamut-routing-lib`](https://pypi.org/project/mamut-routing-lib/)). For development:
20
+
21
+ ```sh
22
+ git clone https://github.com/0nyr/kayros && cd kayros
23
+ pip install -e . --group dev # pip >= 25.1 (or: uv pip install -e . --group dev)
24
+ ```
25
+
26
+ Requirements: Python ≥ 3.11; building from source (sdist or checkout) additionally needs a C++23 compiler and CMake ≥ 3.26 (fetched automatically by the build backend when missing).
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ import kayros
32
+
33
+ # Any MAMUT-routing TD instance (.vrp.json with its .atf.json sidecar next to it)
34
+ instance_path = "benchmarks/TDVRPTW/Dabia2013/n=25/C101.vrp.json"
35
+ solution = kayros.solve(instance_path, time_limit=10.0, seed=42)
36
+ print(solution.duration, solution.num_routes, solution.status)
37
+
38
+ # Anytime: react to every new incumbent while the solve keeps running
39
+ def on_incumbent(incumbent, routes):
40
+ print(f"[{incumbent.seconds:7.2f}s] {incumbent.value:.6f} ({incumbent.origin})")
41
+
42
+ solution = kayros.solve(instance_path, time_limit=60.0, on_incumbent=on_incumbent)
43
+
44
+ # Feed the MAMUT BKS pipeline: solution.to_benchmark_solution() returns the
45
+ # artifact accepted by mamut_routing_lib.td.bks.save_td_solution_as_bks_if_improved.
46
+ ```
47
+
48
+ `solution.duration` is always the value computed by the reference checker (`mamut_routing_lib.td.check_td_solution`) — never an internal approximation.
49
+
50
+ ## Roadmap (stage 1)
51
+
52
+ - [x] M3.0 — package scaffold, CI, PyPI wiring
53
+ - [x] M3.1 — NDCPWLF composition engine + POD instance/route core
54
+ - [x] M3.2 — exact equivalence gate against the reference checker (Dabia2013): 513 tests, 336 instances, zero divergences
55
+ - [x] M3.3 — `kayros.solve()`: greedy construction + TD-ACO
56
+ - [x] M3.4 — all four MAMUT TD families × {TDVRPTW, TDVRP}
57
+ - [x] M3.5 — large-scale runs on Grid'5000: seeded the initial best-known solutions for all 1352 MAMUT TD instances
58
+ - [x] M3.6 — anytime API (`on_incumbent`, time budgets) + **v0.1.0 on PyPI**
59
+ - [ ] M3.7 — time-dependent local search layer (LCA-BST move evaluation, Blauth et al. 2024)
60
+ - Later: optional exact BPC (`kayros[lera]`)
61
+
62
+ ## Provenance
63
+
64
+ KAYROS is developed by [Florian Rascoussier (Onyr)](https://github.com/0nyr) as part of a PhD in operations research (IMT Atlantique / INSA Lyon), under the supervision of Romain Billot, Christine Solnon and Lina Fahed. The NDCPWLF composition engine follows Visser & Spliet (2020)'s move-evaluation theorems; the TD-ACO is a rewrite of the author's heuristic layer originally built on the TDVRPTW solver of Lera-Romero, Rönnqvist & Ljungqvist (2020, MIT-licensed). MIT license.
@@ -0,0 +1,215 @@
1
+ #include <pybind11/functional.h>
2
+ #include <pybind11/pybind11.h>
3
+ #include <pybind11/stl.h>
4
+
5
+ #include <optional>
6
+ #include <stdexcept>
7
+ #include <tuple>
8
+ #include <utility>
9
+ #include <vector>
10
+
11
+ #include "core/instance.h"
12
+ #include "heuristics/heuristics.h"
13
+ #include "pwlf/pwlf.h"
14
+
15
+ namespace py = pybind11;
16
+
17
+ #ifndef KAYROS_VERSION
18
+ #define KAYROS_VERSION "0.0.0"
19
+ #endif
20
+
21
+ namespace {
22
+
23
+ kayros::Pwlf make_pwlf(std::vector<double> xs, std::vector<double> ys) {
24
+ if (xs.size() != ys.size()) {
25
+ throw std::invalid_argument("xs and ys must have the same length");
26
+ }
27
+ return {std::move(xs), std::move(ys)};
28
+ }
29
+
30
+ using ArcSpec = std::tuple<std::int32_t, std::int32_t, std::vector<double>,
31
+ std::vector<double>>;
32
+
33
+ kayros::Instance make_instance(
34
+ std::int32_t num_customers, std::optional<std::int32_t> num_vehicles,
35
+ std::int64_t vehicle_capacity, std::pair<double, double> horizon,
36
+ std::optional<std::vector<std::pair<double, double>>> time_windows,
37
+ std::vector<std::int64_t> demands, std::vector<double> service_times,
38
+ const std::vector<ArcSpec>& arcs) {
39
+ kayros::Instance inst;
40
+ inst.num_customers = num_customers;
41
+ inst.num_vehicles = num_vehicles.value_or(-1);
42
+ inst.vehicle_capacity = vehicle_capacity;
43
+ inst.horizon_start = horizon.first;
44
+ inst.horizon_end = horizon.second;
45
+ const std::int64_t nv = inst.num_vertices();
46
+ const std::size_t expected = static_cast<std::size_t>(nv);
47
+ if (demands.size() != expected || service_times.size() != expected) {
48
+ throw std::invalid_argument(
49
+ "demands and service_times must have num_customers + 1 entries");
50
+ }
51
+ inst.demands = std::move(demands);
52
+ inst.service_times = std::move(service_times);
53
+ if (time_windows.has_value()) {
54
+ if (time_windows->size() != expected) {
55
+ throw std::invalid_argument(
56
+ "time_windows must have num_customers + 1 entries");
57
+ }
58
+ inst.has_time_windows = true;
59
+ inst.tw_earliest.reserve(expected);
60
+ inst.tw_latest.reserve(expected);
61
+ for (const auto& [earliest, latest] : *time_windows) {
62
+ inst.tw_earliest.push_back(earliest);
63
+ inst.tw_latest.push_back(latest);
64
+ }
65
+ }
66
+
67
+ const std::int64_t num_arcs = nv * nv;
68
+ std::vector<std::int64_t> lengths(static_cast<std::size_t>(num_arcs), 0);
69
+ std::int64_t total = 0;
70
+ for (const auto& [i, j, xs, ys] : arcs) {
71
+ if (i < 0 || i >= nv || j < 0 || j >= nv || i == j) {
72
+ throw std::invalid_argument("invalid arc endpoints");
73
+ }
74
+ if (xs.size() != ys.size() || xs.size() < 2) {
75
+ throw std::invalid_argument("arc ATF must have >= 2 breakpoints");
76
+ }
77
+ const std::int64_t a = static_cast<std::int64_t>(i) * nv + j;
78
+ if (lengths[static_cast<std::size_t>(a)] != 0) {
79
+ throw std::invalid_argument("duplicate arc");
80
+ }
81
+ lengths[static_cast<std::size_t>(a)] =
82
+ static_cast<std::int64_t>(xs.size());
83
+ total += static_cast<std::int64_t>(xs.size());
84
+ }
85
+ inst.atf_offset.assign(static_cast<std::size_t>(num_arcs) + 1, 0);
86
+ for (std::int64_t a = 0; a < num_arcs; ++a) {
87
+ inst.atf_offset[static_cast<std::size_t>(a) + 1] =
88
+ inst.atf_offset[static_cast<std::size_t>(a)] +
89
+ lengths[static_cast<std::size_t>(a)];
90
+ }
91
+ inst.atf_xs.assign(static_cast<std::size_t>(total), 0.0);
92
+ inst.atf_ys.assign(static_cast<std::size_t>(total), 0.0);
93
+ for (const auto& [i, j, xs, ys] : arcs) {
94
+ const std::int64_t a = static_cast<std::int64_t>(i) * nv + j;
95
+ const std::int64_t b = inst.atf_offset[static_cast<std::size_t>(a)];
96
+ std::copy(xs.begin(), xs.end(),
97
+ inst.atf_xs.begin() + static_cast<std::ptrdiff_t>(b));
98
+ std::copy(ys.begin(), ys.end(),
99
+ inst.atf_ys.begin() + static_cast<std::ptrdiff_t>(b));
100
+ }
101
+ return inst;
102
+ }
103
+
104
+ } // namespace
105
+
106
+ PYBIND11_MODULE(_core, m) {
107
+ m.doc() =
108
+ "kayros compiled core: NDCPWLF engine, POD instance/route model, "
109
+ "heuristics";
110
+ m.attr("__version__") = KAYROS_VERSION;
111
+
112
+ // --- pwlf primitives (exposed for the checker-equivalence test suite) ---
113
+ m.def("pwlf_identity", [](double low, double high) {
114
+ kayros::Pwlf f = kayros::identity(low, high);
115
+ return py::make_tuple(std::move(f.xs), std::move(f.ys));
116
+ });
117
+ m.def("pwlf_evaluate", [](std::vector<double> xs, std::vector<double> ys,
118
+ double x) {
119
+ const kayros::Pwlf f = make_pwlf(std::move(xs), std::move(ys));
120
+ return kayros::evaluate(kayros::view(f), x);
121
+ });
122
+ m.def("pwlf_compose", [](std::vector<double> f_xs, std::vector<double> f_ys,
123
+ std::vector<double> g_xs,
124
+ std::vector<double> g_ys) {
125
+ const kayros::Pwlf f = make_pwlf(std::move(f_xs), std::move(f_ys));
126
+ const kayros::Pwlf g = make_pwlf(std::move(g_xs), std::move(g_ys));
127
+ kayros::Pwlf h = kayros::compose(kayros::view(f), kayros::view(g));
128
+ return py::make_tuple(std::move(h.xs), std::move(h.ys));
129
+ });
130
+ m.def("pwlf_min_shifted_image",
131
+ [](std::vector<double> xs, std::vector<double> ys) {
132
+ const kayros::Pwlf f = make_pwlf(std::move(xs), std::move(ys));
133
+ const kayros::MinShift s = kayros::min_shifted_image(kayros::view(f));
134
+ return py::make_tuple(s.value, s.argmin_x);
135
+ });
136
+ m.def("pwlf_make_theta",
137
+ [](double earliest, double latest, double service_time) {
138
+ kayros::Pwlf f = kayros::make_theta(earliest, latest, service_time);
139
+ return py::make_tuple(std::move(f.xs), std::move(f.ys));
140
+ });
141
+
142
+ // --- instance + route evaluation ---
143
+ py::class_<kayros::Instance>(m, "Instance")
144
+ .def(py::init(&make_instance), py::arg("num_customers"),
145
+ py::arg("num_vehicles"), py::arg("vehicle_capacity"),
146
+ py::arg("horizon"), py::arg("time_windows"), py::arg("demands"),
147
+ py::arg("service_times"), py::arg("arcs"))
148
+ .def_readonly("num_customers", &kayros::Instance::num_customers)
149
+ .def_readonly("num_vehicles", &kayros::Instance::num_vehicles)
150
+ .def_readonly("vehicle_capacity", &kayros::Instance::vehicle_capacity)
151
+ .def_readonly("has_time_windows", &kayros::Instance::has_time_windows)
152
+ .def("evaluate_route",
153
+ [](const kayros::Instance& inst,
154
+ const std::vector<std::int32_t>& route) {
155
+ const kayros::RouteEval r = kayros::evaluate_route(
156
+ inst, route.data(),
157
+ static_cast<std::int64_t>(route.size()));
158
+ return py::make_tuple(r.feasible, r.duration, r.departure);
159
+ })
160
+ .def("route_ready_time_function",
161
+ [](const kayros::Instance& inst,
162
+ const std::vector<std::int32_t>& route) {
163
+ kayros::Pwlf d = kayros::route_ready_time_function(
164
+ inst, route.data(),
165
+ static_cast<std::int64_t>(route.size()));
166
+ return py::make_tuple(std::move(d.xs), std::move(d.ys));
167
+ });
168
+
169
+ // --- heuristics ---
170
+ py::class_<kayros::AcoParams>(m, "AcoParams")
171
+ .def(py::init<>())
172
+ .def_readwrite("max_iterations", &kayros::AcoParams::max_iterations)
173
+ .def_readwrite("max_no_improvement", &kayros::AcoParams::max_no_improvement)
174
+ .def_readwrite("nb_ants", &kayros::AcoParams::nb_ants)
175
+ .def_readwrite("alpha", &kayros::AcoParams::alpha)
176
+ .def_readwrite("beta", &kayros::AcoParams::beta)
177
+ .def_readwrite("rho", &kayros::AcoParams::rho)
178
+ .def_readwrite("tau_min", &kayros::AcoParams::tau_min)
179
+ .def_readwrite("tau_0", &kayros::AcoParams::tau_0)
180
+ .def_readwrite("tau_max", &kayros::AcoParams::tau_max)
181
+ .def_readwrite("delta_pheromone_threshold",
182
+ &kayros::AcoParams::delta_pheromone_threshold);
183
+
184
+ py::class_<kayros::Incumbent>(m, "Incumbent")
185
+ .def_readonly("value", &kayros::Incumbent::value)
186
+ .def_readonly("seconds", &kayros::Incumbent::seconds)
187
+ .def_readonly("iteration", &kayros::Incumbent::iteration)
188
+ .def_readonly("origin", &kayros::Incumbent::origin);
189
+
190
+ py::enum_<kayros::SolveStatus>(m, "SolveStatus")
191
+ .value("Finished", kayros::SolveStatus::Finished)
192
+ .value("Converged", kayros::SolveStatus::Converged)
193
+ .value("TimeLimit", kayros::SolveStatus::TimeLimit)
194
+ .value("Infeasible", kayros::SolveStatus::Infeasible);
195
+
196
+ py::class_<kayros::SolveResult>(m, "SolveResult")
197
+ .def_readonly("routes", &kayros::SolveResult::routes)
198
+ .def_readonly("value", &kayros::SolveResult::value)
199
+ .def_readonly("incumbents", &kayros::SolveResult::incumbents)
200
+ .def_readonly("status", &kayros::SolveResult::status)
201
+ .def_readonly("iterations_run", &kayros::SolveResult::iterations_run);
202
+
203
+ m.def("greedy_makespan", [](const kayros::Instance& inst) {
204
+ std::vector<std::vector<std::int32_t>> routes;
205
+ const bool ok = kayros::greedy_makespan(inst, routes);
206
+ return py::make_tuple(ok, std::move(routes));
207
+ });
208
+ m.def("solution_duration", &kayros::solution_duration);
209
+ // The callback caster re-acquires the GIL for each invocation, so the
210
+ // solve loop itself can keep running with the GIL released.
211
+ m.def("solve_aco", &kayros::solve_aco, py::arg("instance"),
212
+ py::arg("params"), py::arg("seed"), py::arg("time_limit_seconds"),
213
+ py::arg("on_incumbent") = kayros::IncumbentCallback{},
214
+ py::call_guard<py::gil_scoped_release>());
215
+ }
@@ -0,0 +1,60 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+ #include <vector>
5
+
6
+ #include "pwlf/pwlf.h"
7
+
8
+ namespace kayros {
9
+
10
+ // TD instance in checker conventions: vertices 0..n with 0 = depot, customers
11
+ // 1..n; complete arc set carrying arrival-time functions alpha_ij over the
12
+ // horizon (arrival_at_j = alpha_ij(departure_from_i), FIFO by monotonicity).
13
+ struct Instance {
14
+ std::int32_t num_customers = 0; // n
15
+ std::int32_t num_vehicles = -1; // fleet upper bound; -1 = unbounded
16
+ std::int64_t vehicle_capacity = 0;
17
+ double horizon_start = 0.0;
18
+ double horizon_end = 0.0;
19
+ bool has_time_windows = false; // TDVRPTW when true, TDVRP otherwise
20
+ std::vector<std::int64_t> demands; // size n+1, demands[0] = 0
21
+ std::vector<double> service_times; // size n+1
22
+ std::vector<double> tw_earliest; // size n+1 iff has_time_windows
23
+ std::vector<double> tw_latest; // size n+1 iff has_time_windows
24
+
25
+ // Arc ATF pool: arc (i,j) owns the slice [atf_offset[a], atf_offset[a+1])
26
+ // of atf_xs/atf_ys, with a = i*(n+1)+j. Self-arcs have empty slices.
27
+ std::vector<std::int64_t> atf_offset; // size (n+1)*(n+1)+1
28
+ std::vector<double> atf_xs;
29
+ std::vector<double> atf_ys;
30
+
31
+ std::int32_t num_vertices() const { return num_customers + 1; }
32
+
33
+ PwlfView arc(std::int32_t i, std::int32_t j) const {
34
+ const std::int64_t a =
35
+ static_cast<std::int64_t>(i) * num_vertices() + j;
36
+ const std::int64_t b = atf_offset[a];
37
+ const std::int64_t e = atf_offset[a + 1];
38
+ return {atf_xs.data() + b, atf_ys.data() + b, e - b};
39
+ }
40
+ };
41
+
42
+ struct RouteEval {
43
+ bool feasible = false;
44
+ double duration = 0.0; // Delta*_r = min_t (delta_r(t) - t)
45
+ double departure = 0.0; // earliest optimal depot departure t*_r
46
+ };
47
+
48
+ // Route ready-time function delta_r over feasible depot departure times.
49
+ // Exact port of the checker's compute_route_ready_time_function: per vertex
50
+ // acc <- theta_v ∘ alpha_{prev,v} ∘ acc, with the TDVRPTW depot TW restriction
51
+ // at departure and on return. Returns the empty function when time-infeasible.
52
+ // route points to customer ids in 1..n, depot excluded.
53
+ Pwlf route_ready_time_function(const Instance& inst, const std::int32_t* route,
54
+ std::int64_t len);
55
+
56
+ // Optimal duration and earliest optimal departure (checker's compute_route_duration).
57
+ RouteEval evaluate_route(const Instance& inst, const std::int32_t* route,
58
+ std::int64_t len);
59
+
60
+ } // namespace kayros
@@ -0,0 +1,48 @@
1
+ #pragma once
2
+
3
+ #include <cmath>
4
+ #include <limits>
5
+
6
+ #include "core/instance.h"
7
+
8
+ namespace kayros {
9
+
10
+ // Point queries used by construction heuristics. They mirror the checker's
11
+ // composition semantics point-wise (theta_j ∘ alpha_ij at a single departure),
12
+ // with plain max/+ arithmetic: guidance-exact, while final pricing always goes
13
+ // through the compose-based evaluate_route (bit-identical to the checker).
14
+ inline constexpr double kInfeasible = std::numeric_limits<double>::infinity();
15
+
16
+ // Earliest feasible depot departure time (the checker's departure_low).
17
+ inline double departure_low(const Instance& inst) {
18
+ double lo = inst.horizon_start;
19
+ if (inst.has_time_windows && inst.tw_earliest[0] > lo) lo = inst.tw_earliest[0];
20
+ return lo;
21
+ }
22
+
23
+ // Ready time at j (service completed) when leaving i at ready time t;
24
+ // +inf when the move is time-window or horizon infeasible.
25
+ inline double ready_next(const Instance& inst, std::int32_t i, std::int32_t j,
26
+ double t) {
27
+ const PwlfView alpha = inst.arc(i, j);
28
+ if (alpha.n == 0 || t < alpha.xs[0] || t > alpha.xs[alpha.n - 1]) {
29
+ return kInfeasible;
30
+ }
31
+ const double arrival = evaluate(alpha, t);
32
+ if (inst.has_time_windows) {
33
+ if (arrival > inst.tw_latest[j]) return kInfeasible;
34
+ const double start = arrival > inst.tw_earliest[j] ? arrival : inst.tw_earliest[j];
35
+ return start + inst.service_times[j];
36
+ }
37
+ return arrival + inst.service_times[j];
38
+ }
39
+
40
+ // Can the vehicle return to the depot when leaving j at ready time t?
41
+ inline bool depot_return_feasible(const Instance& inst, std::int32_t j, double t) {
42
+ const PwlfView alpha = inst.arc(j, 0);
43
+ if (alpha.n == 0 || t < alpha.xs[0] || t > alpha.xs[alpha.n - 1]) return false;
44
+ if (!inst.has_time_windows) return true;
45
+ return evaluate(alpha, t) <= inst.tw_latest[0];
46
+ }
47
+
48
+ } // namespace kayros
@@ -0,0 +1,56 @@
1
+ #include <algorithm>
2
+
3
+ #include "core/instance.h"
4
+
5
+ namespace kayros {
6
+
7
+ Pwlf route_ready_time_function(const Instance& inst, const std::int32_t* route,
8
+ std::int64_t len) {
9
+ double dep_lo = inst.horizon_start;
10
+ double dep_hi = inst.horizon_end;
11
+ if (inst.has_time_windows) {
12
+ dep_lo = std::max(dep_lo, inst.tw_earliest[0]);
13
+ dep_hi = std::min(dep_hi, inst.tw_latest[0]);
14
+ }
15
+ if (dep_lo > dep_hi) return {};
16
+
17
+ Pwlf acc = identity(dep_lo, dep_hi);
18
+ std::int32_t prev = 0;
19
+ for (std::int64_t k = 0; k < len; ++k) {
20
+ const std::int32_t v = route[k];
21
+ acc = compose(inst.arc(prev, v), view(acc));
22
+ if (acc.xs.empty()) return acc;
23
+ const double service_time = inst.service_times[v];
24
+ Pwlf theta;
25
+ if (inst.has_time_windows) {
26
+ theta = make_theta(inst.tw_earliest[v], inst.tw_latest[v], service_time);
27
+ } else {
28
+ // TDVRP: pure service shift over the reachable arrival range.
29
+ const double upper = acc.ys.back();
30
+ theta = Pwlf{{0.0, upper}, {service_time, upper + service_time}};
31
+ }
32
+ acc = compose(view(theta), view(acc));
33
+ if (acc.xs.empty()) return acc;
34
+ prev = v;
35
+ }
36
+
37
+ acc = compose(inst.arc(prev, 0), view(acc));
38
+ if (acc.xs.empty()) return acc;
39
+ if (inst.has_time_windows) {
40
+ // Restrict the return arrival to the depot due date, without any
41
+ // waiting clamp: the route ends upon arrival.
42
+ const Pwlf clamp = identity(0.0, inst.tw_latest[0]);
43
+ acc = compose(view(clamp), view(acc));
44
+ }
45
+ return acc;
46
+ }
47
+
48
+ RouteEval evaluate_route(const Instance& inst, const std::int32_t* route,
49
+ std::int64_t len) {
50
+ const Pwlf delta = route_ready_time_function(inst, route, len);
51
+ if (delta.xs.empty()) return {false, 0.0, 0.0};
52
+ const MinShift m = min_shifted_image(view(delta));
53
+ return {true, m.value, m.argmin_x};
54
+ }
55
+
56
+ } // namespace kayros