routing: add distance breaks#1196
Conversation
Signed-off-by: Nikolai Poperechnyi <n.poperechnyi@gmail.com>
📝 WalkthroughWalkthroughThis pull request introduces distance-windowed vehicle breaks into the CUOPT routing solver. It extends the break model from time-based constraints to support cumulative-distance constraints, implements window-aware propagation through node and route layers, integrates the feature across C++, Python, and server APIs, and provides comprehensive tests and documentation. ChangesDistance-breaks integration
🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
python/cuopt_server/cuopt_server/utils/routing/optimization_data_model.py (1)
473-552:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMirror
vehicle_distance_breaksthroughupdate_fleet_data()too.This PR wires the new field through the create/set path only.
update_fleet_data()still has no parameter, validation call, or storage branch forvehicle_distance_breaks, so update callers cannot modify or clear distance breaks after initial creation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@python/cuopt_server/cuopt_server/utils/routing/optimization_data_model.py` around lines 473 - 552, update_fleet_data is missing the new vehicle_distance_breaks parameter and plumbing; add a vehicle_distance_breaks parameter to update_fleet_data, call get_none_for_empty_list(vehicle_distance_breaks) inside it, pass vehicle_distance_breaks through to the validate_fleet_data(...) call (match the updating=True usage used in update path), and ensure the validated value is stored in the same internal field that set_fleet_data uses (the model attribute used to persist distance breaks). This mirrors the handling in set_fleet_data and allows callers to modify/clear distance breaks on updates.
🧹 Nitpick comments (2)
python/cuopt_server/cuopt_server/utils/routing/data_definition.py (1)
134-168: ⚡ Quick winDocument float precision accurately and consider adding a Pydantic validator for range constraints.
Type documentation mismatch: The Field descriptions state
dtype: float32formax_rangeandmin_range, but the Pythonfloattype is typically 64-bit. Either update the documentation tofloat64or explicitly usenp.float32if 32-bit precision is required for downstream processing.Missing constraint validation: The
min_rangedescription states"0 <= min_range < max_range", but no Pydantic validator enforces this relationship. Whilevalidation_fleet_data.pymay validate this (per the PR summary), consider adding a@root_validatorhere for defense-in-depth:🛡️ Suggested Pydantic validator
`@root_validator`(skip_on_failure=True) def validate_range_bounds(cls, values): min_range = values.get('min_range', 0.0) max_range = values.get('max_range') if max_range is not None and min_range >= max_range: raise ValueError(f"min_range ({min_range}) must be < max_range ({max_range})") return values🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@python/cuopt_server/cuopt_server/utils/routing/data_definition.py` around lines 134 - 168, The Field descriptions for max_range and min_range incorrectly state "dtype: float32" while the attributes use Python float (64-bit); update the docstrings to reflect float64 or switch the type to np.float32 if 32-bit is required (refer to max_range and min_range Field definitions). Also add a Pydantic class-level validator (use `@root_validator`(skip_on_failure=True)) in the same data model to enforce 0 <= min_range < max_range and raise a clear ValueError if the constraint fails (refer to min_range, max_range and the class where Field is defined).cpp/tests/routing/unit_tests/distance_breaks.cu (1)
275-368: ⚡ Quick winAdd a regression for the lower-bound side of the distance window.
These tests pin
d_maxenforcement and multi-cycle insertion, but I don't see a case that asserts the newmin_rangebehavior when a charger is reached before the lower bound. That is the part of the feature that differs most from time-based breaks, so it should have an explicit regression.As per coding guidelines,
.github/.coderabbit_review_guide.md: "require edge-case coverage for empty/invalid windows, boundary tolerances, multi-cycle window generation, and infeasible scenarios".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/tests/routing/unit_tests/distance_breaks.cu` around lines 275 - 368, Add a regression test that verifies the distance-break lower bound (min_range) is enforced: create a new TEST (e.g., distance_breaks, break_distance_window_min_enforced) using a small cost matrix and order/charging_station setup similar to break_distance_window_enforced but call data_model.add_distance_break with nonzero min_range (e.g., add_distance_break(0, min_range, max_range, ...)). Solve and iterate h.locations/h.node_types to compute cumulative distance like in the existing test and assert any found BREAK satisfies cumulative >= min_range and cumulative <= max_range, and assert a break was found; also include a case where the nearest charger would occur before min_range so the solver must place the break later to validate the lower-bound behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cpp/src/routing/data_model_view.cu`:
- Around line 36-42: thrust::unique only removes consecutive duplicates so the
current check can miss non-adjacent duplicates; fix by sorting the copied device
vector before calling unique: after creating tmp and copying locations
(rmm::device_uvector<i_t> tmp(...) and raft::copy(...)), call thrust::sort with
the same thrust policy/stream (handle->get_thrust_policy(), tmp.begin(),
tmp.end()), then call thrust::unique and recompute unique_items as end -
tmp.begin(); keep the cuopt::cuopt_expects(n == unique_items, ...) assertion but
run it after the sort+unique sequence.
- Around line 25-43: In validate_break_locations, explicitly reject negative
counts and null pointer inputs before any device-side work: add checks so that
if n < 0 you fail via cuopt::cuopt_expects with
cuopt::error_type_t::ValidationError, and if n > 0 && locations == nullptr you
likewise fail with a ValidationError; perform these checks before calling
cuopt::routing::detail::check_min_max_values, before allocating
rmm::device_uvector, and before any raft::device_span construction in callers
(e.g., places building raft::device_span<const i_t>(break_locations,
num_break_locations)).
In `@cpp/src/routing/local_search/delivery_insertion.cuh`:
- Around line 106-108: The early returns in the delivery insertion scan abort on
the first distance-window miss; change them to continue scanning (or otherwise
only break on monotone/upper-bound-only failures) so a candidate that is
initially too-early for distance_min can still become feasible later as
cumulative distance grows. Concretely, replace the return-after-failure in calls
to node_t<i_t,f_t,REQUEST>::window_forward_feasible and
node_t<i_t,f_t,REQUEST>::window_backward_feasible (and the analogous checks at
the other mentioned sites) with logic that continues the loop unless the
feasibility failure is provably monotone (e.g., an upper-bound violation),
ensuring the scan does not prematurely stop. Ensure you update all occurrences
noted (the forward/backward checks around the delivery insertion scan) so they
follow the same continue-or-break-on-monotone rule.
In `@cpp/src/routing/node/distance_node.cuh`:
- Around line 96-105: The combine() function currently only sums window
mismatches and ignores vehicle_info.max_cost; update combine to also compute the
combined raw travel distance for the two fragments (combined_raw = prev raw
distance + distance_between + next raw distance—use the appropriate raw distance
fields on distance_node_t), then if vehicle_info.max_cost is finite add max(0.,
combined_raw - vehicle_info.max_cost) to the returned excess (i.e., return
prev.excess_forward + next.excess_backward + upper_excess + lower_excess +
max_distance_excess) so joins that violate max_cost are penalized/treated as
infeasible; keep using symbols combine, vehicle_info.max_cost, distance_between,
prev and next to locate and change the code.
In `@cpp/src/routing/problem/problem.cu`:
- Around line 689-690: The code is populating
node_distance_min_h/node_distance_max_h for every non-uniform break rather than
only for distance-based breaks, which makes special_nodes.distance_min non-empty
and incorrectly flips has_distance_window in populate_dimensions_info(); fix by
only appending to node_distance_min_h and node_distance_max_h when
vehicle_break.is_distance_based_ is true (e.g., wrap the push_back calls in an
if (vehicle_break.is_distance_based_) block), and/or introduce an explicit
per-node distance-based flag vector (e.g., node_is_distance_based_h) that is
populated from add_vehicle_break(...) and used by populate_dimensions_info()
instead of relying on buffer non-emptiness so time-only breaks remain
unmodified.
- Around line 654-670: The overlap validation only compares the current
time-based break to the immediately previous break and skips if that previous
break is distance-based; update the check in the block using
non_uniform_breaks.at(v), break_earliest_h, break_latest_h, is_distance_based_,
dim and v so it searches backward for the most recent prior break where
is_distance_based_ == false and then assert break_earliest_h[v][dim] >=
break_latest_h[v][prev_time_dim] via cuopt_expects (instead of comparing only
dim-1); ensure this logic runs only when the current break is time-based and
preserves the existing error message "breaks should not be overlapping!".
In `@cpp/src/routing/route/distance_route.cuh`:
- Around line 180-196: compute_cost currently overwrites inf_cost[dim_t::DIST]
in the has_distance_window branch with only window-related excess, dropping
vehicle_max_cost infeasibility; change the has_distance_window logic in
compute_cost so inf_cost[dim_t::DIST] accumulates both the window excess
(excess_forward[n_nodes_route] + upper_bound + lower_bound) and any vehicle max
cost excess (max(0., distance_forward[n_nodes_route] - vehicle_info.max_cost)),
e.g. compute both contributions and set inf_cost[dim_t::DIST] to their sum or
the max as appropriate so vehicle_info.max_cost is enforced even when
dim_info.has_distance_window is true.
In `@cpp/src/routing/util_kernels/set_nodes_data.cuh`:
- Around line 61-62: The header set_nodes_data.cuh is using
std::numeric_limits<f_t>::max() (see the f_t vehicle_max_cost assignment) but
doesn't include <limits>, making the header rely on transitive includes; fix it
by adding a direct `#include` <limits> to set_nodes_data.cuh so the use of
std::numeric_limits is self-contained and order-independent.
In `@cpp/tests/routing/routing_test.cuh`:
- Around line 1038-1063: Add an explicit assertion that distance break nodes are
actually present in the solution produced by test_cvrptw_distance_breaks: after
constructing host_assignment_t h_routing_solution(routing_solution) and before
calling check_distance_break_windows, scan the routes in h_routing_solution and
assert that at least one node with type BREAK (or, stronger, at least one BREAK
per vehicle that had add_distance_break called) is present; this ensures
add_distance_break calls actually resulted in emitted BREAK nodes rather than
silently being dropped.
In `@docs/cuopt/source/cuopt-python/routing/routing-examples.rst`:
- Around line 73-80: Update the documentation text around
DataModel.add_distance_break to correctly describe the generated windows when
n_cycles is nonzero: state that cycle k yields the window [k * max_range +
min_range, (k + 1) * max_range] (so each window width is max_range - min_range,
not always max_range) and explicitly note that visits before the lower bound
(the min_range edge) are penalized rather than strictly forbidden; reference the
parameters n_cycles, max_range and min_range in the description to make the
behavior precise and consistent with the public API.
In `@docs/cuopt/source/routing-features.rst`:
- Around line 66-73: Update the add_distance_break docs to accurately describe
cycle windows and lower-bound behavior: state that each cycle k=0..n_cycles-1
defines a required stop window [k*max_range + min_range, (k+1)*max_range] whose
width is (max_range - min_range) when min_range>0, clarify whether endpoints are
inclusive, and explicitly note that min_range is not a hard feasibility cutoff
but a preferred lower bound (early charging is allowed by the solver and will be
penalized according to the implementation rather than strictly forbidden); keep
references to add_distance_break, n_cycles, max_range, min_range (distance_min),
and charging_stations so readers can locate the behavior in the API.
In `@python/cuopt_server/cuopt_server/utils/routing/solver.py`:
- Around line 229-243: The vehicle_distance_breaks block currently passes raw
charging_stations into data_model.add_distance_break; for waypoint-graph
requests you must remap those station IDs through optimization_data.locations
(use the same locations.loc[...] pattern used for
vehicle_locations/vehicle_break_locations) to convert to the correct index
positions and build a cudf.Series of the remapped indices (or None) before
calling add_distance_break; additionally ensure any station IDs present only in
vehicle_distance_breaks are appended to optimization_data.locations (so they
exist when the cost matrix is generated) and add validation to detect
index-mapping mismatches or missing station entries at the server/library
boundary.
In `@python/cuopt/cuopt/routing/vehicle_routing.py`:
- Around line 395-428: add_distance_break is missing type hints and complete
docstring details; update its signature to include Python typing (e.g.,
vehicle_ids: Union[int, Sequence[int]], max_range: float, charge_duration: int,
charging_stations: Optional[cudf.Series]=None, min_range: float=0.0, n_cycles:
Optional[int]=None) and document returns and errors: state that max_range and
min_range are in the primary cost-matrix units, the method mutates the model in
place and returns None, and list ValueError conditions to raise (e.g., negative
max_range or min_range, min_range > max_range, negative or zero charge_duration,
n_cycles if provided must be > 0, and invalid/empty vehicle_ids); also mention
any numeric tolerances/semantics for cycles and cumulative-distance windows;
keep the function name add_distance_break references so reviewers can locate the
change.
- Around line 431-433: The handling of vehicle_ids only checks for Python int
and should accept NumPy integer scalars like n_cycles does; update the
conditional that normalizes vehicle_ids (the block using "if
isinstance(vehicle_ids, int): vehicle_ids = [vehicle_ids]") to check
isinstance(vehicle_ids, (int, np.integer)) so np.int32/np.int64 scalars are
converted to a single-item list before the for vid in vehicle_ids loop; mirror
the same pattern used for n_cycles and ensure numpy is imported or referenced as
np in the module.
---
Outside diff comments:
In `@python/cuopt_server/cuopt_server/utils/routing/optimization_data_model.py`:
- Around line 473-552: update_fleet_data is missing the new
vehicle_distance_breaks parameter and plumbing; add a vehicle_distance_breaks
parameter to update_fleet_data, call
get_none_for_empty_list(vehicle_distance_breaks) inside it, pass
vehicle_distance_breaks through to the validate_fleet_data(...) call (match the
updating=True usage used in update path), and ensure the validated value is
stored in the same internal field that set_fleet_data uses (the model attribute
used to persist distance breaks). This mirrors the handling in set_fleet_data
and allows callers to modify/clear distance breaks on updates.
---
Nitpick comments:
In `@cpp/tests/routing/unit_tests/distance_breaks.cu`:
- Around line 275-368: Add a regression test that verifies the distance-break
lower bound (min_range) is enforced: create a new TEST (e.g., distance_breaks,
break_distance_window_min_enforced) using a small cost matrix and
order/charging_station setup similar to break_distance_window_enforced but call
data_model.add_distance_break with nonzero min_range (e.g.,
add_distance_break(0, min_range, max_range, ...)). Solve and iterate
h.locations/h.node_types to compute cumulative distance like in the existing
test and assert any found BREAK satisfies cumulative >= min_range and cumulative
<= max_range, and assert a break was found; also include a case where the
nearest charger would occur before min_range so the solver must place the break
later to validate the lower-bound behavior.
In `@python/cuopt_server/cuopt_server/utils/routing/data_definition.py`:
- Around line 134-168: The Field descriptions for max_range and min_range
incorrectly state "dtype: float32" while the attributes use Python float
(64-bit); update the docstrings to reflect float64 or switch the type to
np.float32 if 32-bit is required (refer to max_range and min_range Field
definitions). Also add a Pydantic class-level validator (use
`@root_validator`(skip_on_failure=True)) in the same data model to enforce 0 <=
min_range < max_range and raise a clear ValueError if the constraint fails
(refer to min_range, max_range and the class where Field is defined).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 14c568cf-bc52-4b28-af4b-5bdfafbcb7f4
📒 Files selected for processing (39)
cpp/include/cuopt/routing/data_model_view.hppcpp/include/cuopt/routing/routing_structures.hppcpp/src/routing/data_model_view.cucpp/src/routing/dimensions.cuhcpp/src/routing/ges/compute_delivery_insertions.cuhcpp/src/routing/ges/lexicographic_search/lexicographic_search.cucpp/src/routing/ges/squeeze.cuhcpp/src/routing/local_search/breaks_insertion.cucpp/src/routing/local_search/compute_ejections.cuhcpp/src/routing/local_search/compute_insertions.cucpp/src/routing/local_search/delivery_insertion.cuhcpp/src/routing/local_search/permutation_helper.cuhcpp/src/routing/local_search/sliding_window.cucpp/src/routing/node/break_node.cuhcpp/src/routing/node/distance_node.cuhcpp/src/routing/node/node.cuhcpp/src/routing/problem/problem.cucpp/src/routing/problem/special_nodes.cuhcpp/src/routing/route/break_route.cuhcpp/src/routing/route/distance_route.cuhcpp/src/routing/solution/solution.cuhcpp/src/routing/util_kernels/set_nodes_data.cuhcpp/tests/routing/CMakeLists.txtcpp/tests/routing/level1/l1_routing_test.cucpp/tests/routing/routing_test.cuhcpp/tests/routing/unit_tests/distance_breaks.cudocs/cuopt/source/cuopt-python/routing/examples/distance_break_example.pydocs/cuopt/source/cuopt-python/routing/routing-examples.rstdocs/cuopt/source/routing-features.rstpython/cuopt/cuopt/routing/vehicle_routing.pxdpython/cuopt/cuopt/routing/vehicle_routing.pypython/cuopt/cuopt/routing/vehicle_routing_wrapper.pyxpython/cuopt/cuopt/tests/routing/test_distance_breaks.pypython/cuopt_server/cuopt_server/tests/utils/utils.pypython/cuopt_server/cuopt_server/utils/routing/data_definition.pypython/cuopt_server/cuopt_server/utils/routing/optimization_data_model.pypython/cuopt_server/cuopt_server/utils/routing/solver.pypython/cuopt_server/cuopt_server/utils/routing/validation_fleet_data.pypython/cuopt_server/cuopt_server/utils/solver.py
| if (!node_t<i_t, f_t, REQUEST>::window_forward_feasible( | ||
| delivery_node, route.vehicle_info(), weights, excess_limit)) { | ||
| return; |
There was a problem hiding this comment.
Don't abort the insertion scan on the first distance-window miss.
These returns were safe for time-window pruning, but they are too aggressive once window_forward_feasible / window_backward_feasible includes distance breaks. A candidate can be invalid only because it is too early for distance_min, then become valid a few positions later as cumulative distance grows. Returning here drops the rest of the scan and can miss valid or lower-cost insertions.
Please keep scanning after a lower-bound/window-slack failure here, or only early-exit on cases that are still monotone (for example, proven upper-bound violations).
Also applies to: 141-145, 201-203, 235-239
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cpp/src/routing/local_search/delivery_insertion.cuh` around lines 106 - 108,
The early returns in the delivery insertion scan abort on the first
distance-window miss; change them to continue scanning (or otherwise only break
on monotone/upper-bound-only failures) so a candidate that is initially
too-early for distance_min can still become feasible later as cumulative
distance grows. Concretely, replace the return-after-failure in calls to
node_t<i_t,f_t,REQUEST>::window_forward_feasible and
node_t<i_t,f_t,REQUEST>::window_backward_feasible (and the analogous checks at
the other mentioned sites) with logic that continues the loop unless the
feasibility failure is provably monotone (e.g., an upper-bound violation),
ensuring the scan does not prematurely stop. Ensure you update all occurrences
noted (the forward/backward checks around the delivery insertion scan) so they
follow the same continue-or-break-on-monotone rule.
|
@np96 Thanks for the improvements. It will take a bit of time for me to review all the changes :). Did this implementation improve the accuracy/quality of the results for your use cases? |
Signed-off-by: Nikolai Poperechnyi <n.poperechnyi@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
python/cuopt/cuopt/tests/routing/test_distance_breaks.py (1)
338-376: ⚡ Quick winAdd one lower-bound-violation / infeasible distance-window case.
All solver scenarios in this file are feasible. The new feature’s distinctive path is the lack of a distance-wait analogue, so an early break should violate
min_rangeand flow through infeasibility cost. Without one case that forces that behavior, regressions in the new penalty plumbing can slip past the Python integration suite.As per coding guidelines, "Flag missing coverage for edge cases (empty, infeasible, unbounded, degenerate) when adding new code paths".
Also applies to: 379-455
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cpp/tests/routing/routing_test.cuh`:
- Around line 595-629: The helper check_distance_break_windows currently treats
min_range as a hard feasibility bound and fails when a BREAK cumulative distance
is below min_range; update the function check_distance_break_windows to remove
the lower-bound assertion (the ASSERT_GE that checks cumulative vs min_range)
while keeping the upper-bound ASSERT_LE against max_range and still incrementing
break_count for BREAK nodes (node_type_t::BREAK). Ensure no new hard-feasibility
check is introduced for min_range here—lower-bound behavior should be validated
via infeasibility/objective tests elsewhere.
In `@python/cuopt_server/cuopt_server/utils/routing/validation_fleet_data.py`:
- Around line 214-219: The validator currently only checks n_cycles > 0 for each
vehicle_distance_breaks entry; add an upper bound to prevent explosion by either
(a) capping entry.n_cycles to a constant MAX_N_CYCLES (e.g. define MAX_N_CYCLES
near the top of validation_fleet_data.py) and rejecting values > MAX_N_CYCLES
with the same False/"vehicle_distance_breaks: n_cycles must be <= MAX_N_CYCLES"
style, or (b) compute the total generated breaks across all entries (sum of
entry.n_cycles or min(entry.n_cycles, MAX_N_CYCLES) per entry) and reject the
request if total > MAX_TOTAL_BREAKS, returning a clear error like
"vehicle_distance_breaks: total generated breaks exceeds limit"; update the
validation branch that currently references entry.n_cycles and n_cycles to
enforce the chosen cap and surface the appropriate error message.
In `@python/cuopt/cuopt/tests/routing/test_distance_breaks.py`:
- Around line 305-309: The test only checks properties of "Break" rows if they
exist, allowing regressions where no breaks or breaks for wrong vehicles still
pass; update the test that calls sol.get_route() and inspects routes to
explicitly assert the expected set/count of vehicles that have breaks (e.g.,
compute the set of vehicle identifiers from routes where type == "Break" and
compare to the expected vehicle set), and add a found_break guard (fail/assert
when no "Break" rows are present) before validating break locations against
break_loc_set; apply the same pattern to the other affected blocks (around lines
327-335, 444-455, 467-472) so each case verifies both presence and correctness
of break-bearing vehicles, not just properties of observed breaks.
- Around line 142-147: The test test_distance_break_invalid_vehicle_id currently
parametrizes invalid vehicle ids [-1, 4, 100] but omits the exact upper-bound
invalid case (fleet_size == 3); update the pytest.mark.parametrize for vid in
that test to include 3 so the list becomes [-1, 3, 4, 100] (or add 3 alongside
the existing values) to catch off-by-one acceptance issues when calling
model.add_distance_break(vid, ...).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 02c01d55-1910-4946-9805-00ddd80111b6
📒 Files selected for processing (19)
cpp/include/cuopt/routing/data_model_view.hppcpp/src/routing/data_model_view.cucpp/src/routing/node/distance_node.cuhcpp/src/routing/problem/problem.cucpp/src/routing/problem/special_nodes.cuhcpp/src/routing/route/distance_route.cuhcpp/src/routing/util_kernels/set_nodes_data.cuhcpp/tests/routing/level1/l1_routing_test.cucpp/tests/routing/routing_test.cuhcpp/tests/routing/unit_tests/distance_breaks.cudocs/cuopt/source/cuopt-python/routing/examples/distance_break_example.pydocs/cuopt/source/cuopt-python/routing/routing-examples.rstdocs/cuopt/source/routing-features.rstpython/cuopt/cuopt/routing/vehicle_routing.pypython/cuopt/cuopt/tests/routing/test_distance_breaks.pypython/cuopt_server/cuopt_server/utils/routing/data_definition.pypython/cuopt_server/cuopt_server/utils/routing/optimization_data_model.pypython/cuopt_server/cuopt_server/utils/routing/solver.pypython/cuopt_server/cuopt_server/utils/routing/validation_fleet_data.py
✅ Files skipped from review due to trivial changes (1)
- docs/cuopt/source/cuopt-python/routing/examples/distance_break_example.py
🚧 Files skipped from review as they are similar to previous changes (9)
- cpp/include/cuopt/routing/data_model_view.hpp
- docs/cuopt/source/routing-features.rst
- cpp/src/routing/util_kernels/set_nodes_data.cuh
- cpp/src/routing/problem/problem.cu
- cpp/src/routing/problem/special_nodes.cuh
- docs/cuopt/source/cuopt-python/routing/routing-examples.rst
- python/cuopt_server/cuopt_server/utils/routing/data_definition.py
- cpp/src/routing/node/distance_node.cuh
- cpp/src/routing/data_model_view.cu
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@python/cuopt/cuopt/routing/vehicle_routing.py`:
- Around line 455-459: Validate vehicle_ids immediately after the scalar-to-list
conversion: if vehicle_ids is None or not a supported scalar/iterable type
(accept int/np.integer or any iterable like list/tuple/np.ndarray/cudf.Series),
raise a clear ValueError indicating "invalid/empty vehicle_ids" (or similar text
matching the docstring) when the sequence is empty; keep the existing branch
that converts an int/np.integer to [int(vehicle_ids)], then check that the
resulting container is non-empty before any iteration and raise the actionable
ValueError for unsupported types or empty containers so callers fail fast.
- Around line 415-419: The docstring in vehicle_routing.py currently claims
distance-window endpoints are strictly infeasible; update that paragraph to
state that distance-window violations are handled by applying an infeasibility
cost (penalized) rather than being impossible, while still noting there is no
"wait" analogue in the distance dimension so the cumulative distance can't be
delayed; adjust the wording to match the PR behavior. Also ensure the public API
docstring for the related class/function includes pydocstyle-compliant sections
(params/returns/raises) as required by the project guidelines so the docstring
is complete and unambiguous.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 38b8078d-df86-49cc-aa0d-4467b87af689
📒 Files selected for processing (9)
cpp/src/routing/node/distance_node.cuhcpp/src/routing/route/distance_route.cuhcpp/src/routing/util_kernels/set_nodes_data.cuhcpp/tests/routing/unit_tests/distance_breaks.cudocs/cuopt/source/cuopt-python/routing/routing-examples.rstdocs/cuopt/source/routing-features.rstpython/cuopt/cuopt/routing/vehicle_routing.pypython/cuopt/cuopt/tests/routing/test_distance_breaks.pypython/cuopt_server/cuopt_server/utils/routing/validation_fleet_data.py
✅ Files skipped from review due to trivial changes (2)
- docs/cuopt/source/routing-features.rst
- docs/cuopt/source/cuopt-python/routing/routing-examples.rst
🚧 Files skipped from review as they are similar to previous changes (3)
- python/cuopt_server/cuopt_server/utils/routing/validation_fleet_data.py
- python/cuopt/cuopt/tests/routing/test_distance_breaks.py
- cpp/src/routing/route/distance_route.cuh
| Both window endpoints are hard feasibility constraints. Unlike | ||
| time-based breaks the distance dimension has no "wait" analogue, so a | ||
| break that lands before ``min_range`` or after ``max_range`` is | ||
| infeasible — the solver cannot stall the vehicle to shift the | ||
| cumulative distance. |
There was a problem hiding this comment.
Docstring semantics for distance-window violations look overstated.
This block currently states early/late distance breaks are strictly infeasible. That conflicts with the PR behavior description where distance-window violations are penalized via infeasibility cost. Please align wording to avoid misleading API users.
Proposed wording adjustment
- Both window endpoints are hard feasibility constraints. Unlike
- time-based breaks the distance dimension has no "wait" analogue, so a
- break that lands before ``min_range`` or after ``max_range`` is
- infeasible — the solver cannot stall the vehicle to shift the
- cumulative distance.
+ Unlike time-based breaks, the distance dimension has no "wait"
+ analogue. If a break lands before ``min_range`` or after
+ ``max_range``, the solver cannot stall to shift cumulative distance,
+ so the route incurs a distance-window violation in infeasibility cost.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@python/cuopt/cuopt/routing/vehicle_routing.py` around lines 415 - 419, The
docstring in vehicle_routing.py currently claims distance-window endpoints are
strictly infeasible; update that paragraph to state that distance-window
violations are handled by applying an infeasibility cost (penalized) rather than
being impossible, while still noting there is no "wait" analogue in the distance
dimension so the cumulative distance can't be delayed; adjust the wording to
match the PR behavior. Also ensure the public API docstring for the related
class/function includes pydocstyle-compliant sections (params/returns/raises) as
required by the project guidelines so the docstring is complete and unambiguous.
There was a problem hiding this comment.
infeasible = penalized via large infeasibility cost...
There was a problem hiding this comment.
@np96, understood — in cuOpt's solver model, "infeasible" means the route accumulates a large infeasibility-cost penalty rather than being outright rejected. Thanks for the clarification.
That said, API users unfamiliar with cuOpt's penalty-based approach might read "hard feasibility constraints" as meaning the solver will refuse to produce any solution that violates those bounds. If you'd like to proactively avoid that confusion, a one-sentence qualifier like "(enforced via infeasibility cost penalty)" could help. But if the existing phrasing aligns with how infeasibility is described elsewhere in the docs/codebase, it's perfectly reasonable to keep it consistent.
Happy to leave the comment as-is if you prefer no change.
✏️ Learnings added
Learnt from: np96
Repo: NVIDIA/cuopt PR: 1196
File: python/cuopt/cuopt/routing/vehicle_routing.py:415-419
Timestamp: 2026-05-11T16:22:58.142Z
Learning: In cuOpt's routing solver (python/cuopt/cuopt/routing/vehicle_routing.py and related C++ code), "infeasible" means the route incurs a large infeasibility cost penalty rather than the solution being hard-rejected. Distance-window violations for distance-based breaks flow through the existing infeasibility cost mechanism with no new weight. The terms "infeasible" and "hard feasibility constraint" are used intentionally in this codebase to mean penalty-based infeasibility.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
Hi @rg20! Looks better and cleaner than forbidding local search moves. Though practically it currently has some limitations due to this. Ideally I'd want to model this way: Everything else LGTM. Hope to fix this and I am very happy with this feature and approach. Thanks 👍 |
|
🔔 Hi @anandhkb, this pull request has had no activity for 7 days. Please update or let us know if it can be closed. Thank you! If this is an "epic" issue, then please add the "epic" label to this issue. |
rg20
left a comment
There was a problem hiding this comment.
Thanks for the PR. You do not need to check for distance dimension feasibility everywhere. We do that for time dimension just to rule out a lot of moves. For distance breaks, this filteration does not help as it is mostly needed only for break nodes and also only when distance breaks are used. This is a very special scenario.
| node_stack.s_route.vehicle_info()) && | ||
| node_stack.delivery_node.distance_dim.forward_feasible( | ||
| node_stack.s_route.vehicle_info()); | ||
| node_t<i_t, f_t, REQUEST>::window_forward_feasible(node_stack.delivery_node, |
| * or by cumulative route distance. If @p locations is empty the break may be taken | ||
| * anywhere; otherwise it must occur at one of the specified location IDs. | ||
| */ | ||
| template <typename i_t> |
There was a problem hiding this comment.
| template <typename i_t> | |
| template <typename i_t, typename f_t> |
use f_t instead of float
| if constexpr (insert_mode == insert_mode_t::LOCAL_SEARCH) { | ||
| if (node.time_dim.forward_feasible( | ||
| curr_route.vehicle_info(), weights[dim_t::TIME], excess_limit) && | ||
| if (node_t<i_t, f_t, REQUEST>::window_forward_feasible( |
There was a problem hiding this comment.
time dimension is treated differently compared to other dimensions, we use this to filter out moves. I don't think changing this logic is necessary. Please remember that we have other dimension that violate constraints as well.
There was a problem hiding this comment.
Thanks @rg20. This design decision started making sense.
So you allow any moves satisfying time dimension then evaluate if they are feasible from the perspective of other dimensions. Good
| "Backward forward mismatch!"); | ||
| "Time backward/forward mismatch!"); | ||
| cuopt_assert(!(orginal_route.dimensions_info().has_dimension(dim_t::DIST) && | ||
| orginal_route.dimensions_info().distance_dim.has_distance_window) || |
There was a problem hiding this comment.
| orginal_route.dimensions_info().distance_dim.has_distance_window) || | |
| orginal_route.dimensions_info().distance_dim.has_constraints()) || |
distance can have max_distance constraint as well
| namespace detail { | ||
|
|
||
| /** | ||
| * @brief Looks for a cost-reducing relocation of an existing break node within its route by |
| if (dimensions_info.has_dimension(dim_t::TIME) && | ||
| !next_node.time_dim.forward_feasible( | ||
| s_route.vehicle_info(), move_candidates.weights[dim_t::TIME], excess_limit)) { | ||
| if (!node_t<i_t, f_t, REQUEST>::window_forward_feasible( |
| if (dimensions_info.has_dimension(dim_t::TIME) && | ||
| !previous_node.time_dim.backward_feasible( | ||
| s_route.vehicle_info(), move_candidates.weights[dim_t::TIME], excess_limit)) { | ||
| if (!node_t<i_t, f_t, REQUEST>::window_backward_feasible( |
| return max(0., total_distance - vehicle_info.max_cost); | ||
| double arrival_window_f = prev.distance_window_forward + distance_between; | ||
| double upper_excess = max(0., arrival_window_f - next.distance_window_backward); | ||
| double lower_excess = max(0., next.distance_window_backward_min - arrival_window_f); |
There was a problem hiding this comment.
I don't think we need lower_excess. Check the time dimension for how we do time combine
There was a problem hiding this comment.
Hi there.
I have read HY paper, so far I understand they don't treat early arrival as infeasible.
In case of time windows a vehcile may wait until time window opens. In distance case it is undesirable to arrive to early. If you ignore lower_excess, the vehicle may end up arriving at 20 KM when you want it to visit dist breaks in [60..80] km range.
I removed lower excess but in my opinion it is useful if you want to consider arriving to early as violating constraints.
| if (dim_info.has_max_constraint) { | ||
| if (dim_info.has_distance_window) { | ||
| double upper_boundary = max(0., distance_window_forward - distance_window_backward); | ||
| double lower_boundary = max(0., distance_window_backward_min - distance_window_forward); |
There was a problem hiding this comment.
This looks different than time dimension.
| double max_cost_excess = max(0., total_distance - vehicle_info.max_cost); | ||
| inf_cost[dim_t::DIST] = | ||
| excess_forward + excess_backward + upper_boundary + lower_boundary + max_cost_excess; | ||
| } else if (dim_info.has_max_constraint) { |
There was a problem hiding this comment.
inf_cost[dim_t::DIST] = 0;
if (dim_info.has_ax_constraint) {
inf_cost[dim_t::DIST] = max(0., total_distance - vehicle_info.max_cost);
}
if (dim_info.has_distance_window) {
inf_cost[dim_t::DIST] += ;
}
There was a problem hiding this comment.
I thought I fixed it after Claude... Thanks for noticing this, fixed.
|
Moving this out of 26.06 scope as this is in review process still. |
|
🔔 Hi @anandhkb, this pull request has had no activity for 7 days. Please update or let us know if it can be closed. Thank you! If this is an "epic" issue, then please add the "epic" label to this issue. |
1 similar comment
|
🔔 Hi @anandhkb, this pull request has had no activity for 7 days. Please update or let us know if it can be closed. Thank you! If this is an "epic" issue, then please add the "epic" label to this issue. |
|
Hi @rg20, thanks for review. I considered your comments. Probably I am wrong somewhere, would be happy if you point out an error in my reasoning. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
cpp/src/routing/data_model_view.cu (1)
123-131:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
thrust::uniquewithout prior sort misses non-adjacent duplicates.The
validate_break_locationshelper correctly sorts before callingthrust::unique(line 44), butset_break_locationsstill callsthrust::uniquedirectly without sorting first. An input like[1, 2, 1]would incorrectly pass this validation.🐛 Proposed fix
rmm::device_uvector<i_t> tmp_break_nodes(n_break_locations, handle_ptr_->get_stream()); raft::copy( tmp_break_nodes.begin(), break_locations, n_break_locations, handle_ptr_->get_stream()); + thrust::sort( + handle_ptr_->get_thrust_policy(), tmp_break_nodes.begin(), tmp_break_nodes.end()); auto end = thrust::unique( handle_ptr_->get_thrust_policy(), tmp_break_nodes.begin(), tmp_break_nodes.end());🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/src/routing/data_model_view.cu` around lines 123 - 131, The code in set_break_locations uses thrust::unique on tmp_break_nodes without sorting, so non-adjacent duplicates (e.g., [1,2,1]) slip through; update set_break_locations to sort tmp_break_nodes first (using thrust::sort with handle_ptr_->get_thrust_policy() and tmp_break_nodes.begin()/end()) before calling thrust::unique, then recompute end and unique_items and keep the existing cuopt_expects check; mirror the approach used in validate_break_locations to ensure duplicates are detected.cpp/src/routing/ges/squeeze.cuh (1)
288-293: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winAdd a consistency check for DIST dimension excess when distance windows are enabled.
The assertion at line 292 verifies that TIME dimension forward and backward excess values match after insertion. Now that distance breaks introduce similar window/excess propagation for the DIST dimension (see
distance_route_t::excess_forward/excess_backwardwhenhas_distance_windowis true), the same consistency check should be added for DIST to catch propagation bugs.🔍 Proposed consistency check
execute_insert<i_t, f_t, REQUEST>(solution, orginal_route, request_locations, request); cuopt_assert(!orginal_route.dimensions_info().has_dimension(dim_t::TIME) || abs(orginal_route.template get_dim<dim_t::TIME>() .excess_forward[orginal_route.get_num_nodes()] - orginal_route.template get_dim<dim_t::TIME>().excess_backward[0]) < 0.01, "Backward forward mismatch!"); + cuopt_assert(!orginal_route.dimensions_info().has_dimension(dim_t::DIST) || + !orginal_route.template get_dim<dim_t::DIST>().dim_info.has_distance_window || + abs(orginal_route.template get_dim<dim_t::DIST>() + .excess_forward[orginal_route.get_num_nodes()] - + orginal_route.template get_dim<dim_t::DIST>().excess_backward[0]) < 0.01, + "DIST backward forward excess mismatch!"); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/src/routing/ges/squeeze.cuh` around lines 288 - 293, Add the same post-insertion consistency assertion for the DIST dimension: if orginal_route.dimensions_info().has_dimension(dim_t::DIST) (and/or distance windows are active), compare orginal_route.template get_dim<dim_t::DIST>().excess_forward[orginal_route.get_num_nodes()] with orginal_route.template get_dim<dim_t::DIST>().excess_backward[0] using the same tolerance (abs(...) < 0.01) and a distinct message (e.g. "Backward forward mismatch DIST!"); locate the check near the existing TIME assertion that references orginal_route, get_dim, excess_forward, and excess_backward and mirror its structure so DIST propagation bugs are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cpp/tests/routing/routing_test.cuh`:
- Around line 591-598: Update the Doxygen block that documents the Break-node
distance checks to remove the incorrect assertion that arriving before the lower
bound "carries no penalty" and instead state that the lower bound (min_range) is
not asserted here because early arrivals produce a soft window-violation penalty
(they are penalized but allowed), and clarify this behavior for Break
nodes/parameters like max_range and min_range in the test that verifies breaks
in the assignment.
---
Outside diff comments:
In `@cpp/src/routing/data_model_view.cu`:
- Around line 123-131: The code in set_break_locations uses thrust::unique on
tmp_break_nodes without sorting, so non-adjacent duplicates (e.g., [1,2,1]) slip
through; update set_break_locations to sort tmp_break_nodes first (using
thrust::sort with handle_ptr_->get_thrust_policy() and
tmp_break_nodes.begin()/end()) before calling thrust::unique, then recompute end
and unique_items and keep the existing cuopt_expects check; mirror the approach
used in validate_break_locations to ensure duplicates are detected.
In `@cpp/src/routing/ges/squeeze.cuh`:
- Around line 288-293: Add the same post-insertion consistency assertion for the
DIST dimension: if orginal_route.dimensions_info().has_dimension(dim_t::DIST)
(and/or distance windows are active), compare orginal_route.template
get_dim<dim_t::DIST>().excess_forward[orginal_route.get_num_nodes()] with
orginal_route.template get_dim<dim_t::DIST>().excess_backward[0] using the same
tolerance (abs(...) < 0.01) and a distinct message (e.g. "Backward forward
mismatch DIST!"); locate the check near the existing TIME assertion that
references orginal_route, get_dim, excess_forward, and excess_backward and
mirror its structure so DIST propagation bugs are caught.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: b6cf76ee-d4b1-4005-a39f-a0fc33940279
📒 Files selected for processing (10)
cpp/include/cuopt/routing/data_model_view.hppcpp/include/cuopt/routing/routing_structures.hppcpp/src/routing/data_model_view.cucpp/src/routing/dimensions.cuhcpp/src/routing/ges/squeeze.cuhcpp/src/routing/node/distance_node.cuhcpp/src/routing/route/distance_route.cuhcpp/src/routing/util_kernels/set_nodes_data.cuhcpp/tests/routing/routing_test.cuhcpp/tests/routing/unit_tests/distance_breaks.cu
💤 Files with no reviewable changes (2)
- cpp/src/routing/dimensions.cuh
- cpp/tests/routing/unit_tests/distance_breaks.cu
🚧 Files skipped from review as they are similar to previous changes (1)
- cpp/src/routing/util_kernels/set_nodes_data.cuh
| /** | ||
| * @brief Verifies that every Break node in the assignment is taken no later than its | ||
| * configured cumulative-distance upper bound @p max_range, reset at the depot of each route. | ||
| * | ||
| * The lower bound (min_range) is intentionally not checked: like the time dimension, arriving | ||
| * before the window start carries no penalty, so the solver may legitimately place a break | ||
| * earlier than min_range. | ||
| */ |
There was a problem hiding this comment.
Correct the Doxygen comment: early distance breaks DO incur a penalty.
The comment states that arriving before the window start "carries no penalty," but the PR objectives explicitly state: "visiting before the window lower bound produces a window-violation penalty." Distance breaks differ from time breaks in that early arrival incurs an infeasibility penalty (there is no "distance wait" like time-based waiting). The comment should clarify that the lower bound is not asserted here because violations are soft (penalized but valid), not because they are free.
📝 Suggested correction
/**
- * `@brief` Verifies that every Break node in the assignment is taken no later than its
- * configured cumulative-distance upper bound `@p` max_range, reset at the depot of each route.
+ * `@brief` Verifies that every BREAK node in the assignment is taken no later than its
+ * configured cumulative-distance upper bound, reset at the depot of each route.
*
- * The lower bound (min_range) is intentionally not checked: like the time dimension, arriving
- * before the window start carries no penalty, so the solver may legitimately place a break
- * earlier than min_range.
+ * The lower bound (min_range) is intentionally not checked here: unlike time-based breaks
+ * (which allow waiting), early distance breaks incur a window-violation penalty. The solver
+ * may emit breaks before min_range in penalized solutions, so this helper only enforces the
+ * upper bound `@p` max_range as a hard constraint.
*/🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cpp/tests/routing/routing_test.cuh` around lines 591 - 598, Update the
Doxygen block that documents the Break-node distance checks to remove the
incorrect assertion that arriving before the lower bound "carries no penalty"
and instead state that the lower bound (min_range) is not asserted here because
early arrivals produce a soft window-violation penalty (they are penalized but
allowed), and clarify this behavior for Break nodes/parameters like max_range
and min_range in the test that verifies breaks in the assignment.
Description
Adds distance-based vehicle breaks to cuOpt routing. A distance break is a mandatory charging stop the vehicle must take within a cumulative-distance window
[distance_min, distance_max], optionally restricted to a set of charging-station locations. The mechanism mirrors time-based breaks with one deliberate semantic difference: there is no "distance wait" analogue, so visiting a break location before the window's lower bound incurs a window-violation penalty rather than being silently relaxed.Public API
data_model_view_t::add_distance_break(vehicle_id, distance_min, distance_max, charge_duration, break_locations, num_break_locations, validate_input=true).DataModel.add_distance_break(vehicle_ids, max_range, charge_duration, charging_stations=None, min_range=0.0, n_cycles=None). Each call expands ton_cyclesconsecutive cycles[k * max_range + min_range, (k+1) * max_range]fork = 0 … n_cycles - 1.vehicle_break_tnow has a distance-based constructor and carriesis_distance_based_,distance_min_,distance_max_.DIST dimension extension
Rather than introduce a new infeasibility dimension, the existing DIST dimension is extended:
cost_dimension_info_t::has_distance_windowgates the new state.distance_node_t/distance_route_t:distance_window_forward,distance_window_backward,distance_window_backward_min,excess_forward,excess_backward.Operator updates
sliding_window,lexicographic_search,permutation_helper,delivery_insertion,compute_delivery_insertions,compute_ejections,compute_insertions,breaks_insertion, andsqueezeare all updated to respect the window state. To collapse the repeated(TIME excess) || (DIST window excess)check,node_t::window_forward_feasible/window_backward_feasiblehelpers anddimensions_info::has_window_dimension()were added.Server
VehicleDistanceBreakpydantic schema with field validation (max_range,charge_duration,n_cycles, optionalcharging_stationsandmin_range).FleetData.vehicle_distance_breaks: Optional[List[VehicleDistanceBreak]].validation_fleet_data.py,optimization_data_model.py,solver.py.Tests
cpp/tests/routing/unit_tests/distance_breaks.cu— 12 unit tests: propagation correctness, the combine invariant,get_cost/combine/compute_costconsistency, feasible and infeasible cases, edge cases.cpp/tests/routing/level1/l1_routing_test.cu— newCVRPTW_DISTANCE_BREAKSsuite (regression_routing_test_distance_breaks_t) parametrised ondatasets/ref/l1_25.txt(6 Solomon-25 instances).python/cuopt/cuopt/tests/routing/test_distance_breaks.py— Python integration tests: feasibility, multi-cycle, mixed-vehicle (some constrained, some not), and a no-regression test where the feature is unused.Documentation
docs/cuopt/source/cuopt-python/routing/examples/distance_break_example.pyreferenced fromrouting-examples.rst.routing-features.rst.Known limitation (out of scope)
The break-insertion framework still uses the "exactly N breaks per route" invariant (see related issue Distance breaks inherit it: when
n_cyclesexceeds a route's actual need, the surplus cycles are still inserted.Issue
#589
Checklist