Skip to content

routing: add distance breaks#1196

Open
np96 wants to merge 6 commits into
NVIDIA:mainfrom
np96:feature/distance-breaks
Open

routing: add distance breaks#1196
np96 wants to merge 6 commits into
NVIDIA:mainfrom
np96:feature/distance-breaks

Conversation

@np96
Copy link
Copy Markdown
Contributor

@np96 np96 commented May 11, 2026

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

  • C++: data_model_view_t::add_distance_break(vehicle_id, distance_min, distance_max, charge_duration, break_locations, num_break_locations, validate_input=true).
  • Python: DataModel.add_distance_break(vehicle_ids, max_range, charge_duration, charging_stations=None, min_range=0.0, n_cycles=None). Each call expands to n_cycles consecutive cycles [k * max_range + min_range, (k+1) * max_range] for k = 0 … n_cycles - 1.
  • vehicle_break_t now has a distance-based constructor and carries is_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_window gates the new state.
  • New forward/backward fields on distance_node_t / distance_route_t: distance_window_forward, distance_window_backward, distance_window_backward_min, excess_forward, excess_backward.
  • Window-violation excess flows through the existing infeasibility cost; no new weight is added.

Operator updates

sliding_window, lexicographic_search, permutation_helper, delivery_insertion, compute_delivery_insertions, compute_ejections, compute_insertions, breaks_insertion, and squeeze are all updated to respect the window state. To collapse the repeated (TIME excess) || (DIST window excess) check, node_t::window_forward_feasible / window_backward_feasible helpers and dimensions_info::has_window_dimension() were added.

Server

  • VehicleDistanceBreak pydantic schema with field validation (max_range, charge_duration, n_cycles, optional charging_stations and min_range).
  • FleetData.vehicle_distance_breaks: Optional[List[VehicleDistanceBreak]].
  • Wiring through 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_cost consistency, feasible and infeasible cases, edge cases.
  • cpp/tests/routing/level1/l1_routing_test.cu — new CVRPTW_DISTANCE_BREAKS suite (regression_routing_test_distance_breaks_t) parametrised on datasets/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

  • New docs/cuopt/source/cuopt-python/routing/examples/distance_break_example.py referenced from routing-examples.rst.
  • Feature summary added to 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_cycles exceeds a route's actual need, the surplus cycles are still inserted.

Issue

#589

Checklist

  • I am familiar with the Contributing Guidelines.
  • Testing
    • New or existing tests cover these changes
    • Added tests
    • Created an issue to follow-up
  • Documentation
    • Added new documentation

Signed-off-by: Nikolai Poperechnyi <n.poperechnyi@gmail.com>
@np96 np96 requested review from a team as code owners May 11, 2026 09:31
@np96 np96 requested review from kaatish, mlubin and rgsl888prabhu May 11, 2026 09:31
@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot Bot commented May 11, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Distance-breaks integration

Layer / File(s) Summary
Break type definitions and constraint metadata
cpp/include/cuopt/routing/routing_structures.hpp, cpp/src/routing/dimensions.cuh
vehicle_break_t template becomes dual-mode, supporting both time-windowed and distance-windowed breaks via separate constructors and a mode flag; cost_dimension_info_t adds has_distance_window flag to track when distance constraints are active.
C++ data model API and validation
cpp/include/cuopt/routing/data_model_view.hpp, cpp/src/routing/data_model_view.cu
data_model_view_t adds public add_distance_break method; refactors break-location validation into reusable validate_break_locations helper; updates get_non_uniform_breaks return type to use new vehicle_break_t<i_t, f_t> signature.
Distance-node window-aware propagation and feasibility
cpp/src/routing/node/distance_node.cuh
distance_node_t tracks per-node distance windows and accumulated excess; forward/backward propagation clamps cumulative values to window bounds and accumulates excess; combine integrates window-boundary and accumulated penalties; get_cost computes distance-dimension infeasibility when enabled.
Distance-route buffering and view management
cpp/src/routing/route/distance_route.cuh
distance_route_t conditionally allocates six new device buffers (distance-window forward/backward, window bounds, excess forward/backward) when distance-window mode is active; view extraction, data copies, shared-memory support, and cost computation all handle the new state conditionally.
Problem dimension setup and special nodes
cpp/src/routing/problem/problem.cu, cpp/src/routing/problem/special_nodes.cuh
Enables distance-window constraint when distance-based breaks exist; populate_special_nodes branches break validation by type, accumulates per-node distance bounds into new host/device buffers, sets has_distance_break flag; special_nodes_t manages distance-break metadata with optional distance-min/max buffers.
Kernel and utility updates
cpp/src/routing/util_kernels/set_nodes_data.cuh, cpp/src/routing/solution/solution.cuh, cpp/src/routing/ges/squeeze.cuh, cpp/src/routing/local_search/breaks_insertion.cu
Initializes distance-window endpoints and excess in set_route_data when enabled; initializes break nodes' distance dimensions from special-node metadata; tightens squeeze_breaks_kernel insertion guard; adds Doxygen documentation.
Cython wrapper and Python wrapper implementation
python/cuopt/cuopt/routing/vehicle_routing.pxd, python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx
vehicle_routing.pxd declares add_distance_break C++/Cython binding; .pyx refactors break-dimension bookkeeping and adds add_distance_break with locations casting.
High-level Python API with multi-cycle and validation
python/cuopt/cuopt/routing/vehicle_routing.py
DataModel.add_distance_break normalizes vehicle IDs, defaults locations/cycles, validates ranges, computes per-cycle [d_min, d_max] windows, and registers via Cython wrapper; tightens existing range checks across break, order, and vehicle-match methods.
Python server data models for distance breaks
python/cuopt_server/cuopt_server/utils/routing/data_definition.py
Introduces VehicleDistanceBreak Pydantic model with vehicle ID, distance bounds, duration, optional locations, and cycles; adds vehicle_distance_breaks field to FleetData.
Python server optimization model, solver, and validation
python/cuopt_server/cuopt_server/utils/routing/optimization_data_model.py, python/cuopt_server/cuopt_server/utils/routing/solver.py, python/cuopt_server/cuopt_server/utils/routing/validation_fleet_data.py, python/cuopt_server/cuopt_server/tests/utils/utils.py, python/cuopt_server/cuopt_server/utils/solver.py
OptimizationDataModel extends set_fleet_data/update_fleet_data to accept and normalize distance breaks; create_data_model iterates distance-break entries and calls add_distance_break; prep_optimization_data includes distance-break locations in de-duplication; validate_fleet_data enforces distance-break parameter constraints; test utils forward the field through request payloads.
C++ regression, unit tests, and test helpers
cpp/tests/routing/routing_test.cuh, cpp/tests/routing/level1/l1_routing_test.cu, cpp/tests/routing/CMakeLists.txt, cpp/tests/routing/unit_tests/distance_breaks.cu
routing_test.cuh adds check_distance_break_windows helper validating cumulative distance upper bounds and test_cvrptw_distance_breaks regression test; l1_routing_test.cu registers parameterized tests; distance_breaks.cu provides 15+ unit tests for node propagation, combining, feasibility, and 5 integration scenarios including multi-cycle and mixed-fleet.
Python API and solver tests for distance breaks
python/cuopt/cuopt/tests/routing/test_distance_breaks.py
Comprehensive coverage of API metadata validation, multi-cycle window generation, location constraints, scalar/list equivalence, and solver behavior across 20+ tests; validates break counts, cumulative distance bounds, location constraints, and mixed-fleet isolation.
Python example and documentation
docs/cuopt/source/cuopt-python/routing/examples/distance_break_example.py, docs/cuopt/source/cuopt-python/routing/routing-examples.rst, docs/cuopt/source/routing-features.rst
distance_break_example.py demonstrates end-to-end setup with fixed coordinates, cost matrix, constraint registration, and solution output; routing-examples.rst adds example section with code and sample output; routing-features.rst documents window formula, hard feasibility constraints, and multi-cycle behavior.

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'routing: add distance breaks' clearly and concisely describes the main change—adding distance-based break functionality to the routing module.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the distance-breaks feature, public API, implementation approach, and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 win

Mirror vehicle_distance_breaks through update_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 for vehicle_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 win

Document float precision accurately and consider adding a Pydantic validator for range constraints.

  1. Type documentation mismatch: The Field descriptions state dtype: float32 for max_range and min_range, but the Python float type is typically 64-bit. Either update the documentation to float64 or explicitly use np.float32 if 32-bit precision is required for downstream processing.

  2. Missing constraint validation: The min_range description states "0 <= min_range < max_range", but no Pydantic validator enforces this relationship. While validation_fleet_data.py may validate this (per the PR summary), consider adding a @root_validator here 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 win

Add a regression for the lower-bound side of the distance window.

These tests pin d_max enforcement and multi-cycle insertion, but I don't see a case that asserts the new min_range behavior 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

📥 Commits

Reviewing files that changed from the base of the PR and between b311a99 and cf56d55.

📒 Files selected for processing (39)
  • cpp/include/cuopt/routing/data_model_view.hpp
  • cpp/include/cuopt/routing/routing_structures.hpp
  • cpp/src/routing/data_model_view.cu
  • cpp/src/routing/dimensions.cuh
  • cpp/src/routing/ges/compute_delivery_insertions.cuh
  • cpp/src/routing/ges/lexicographic_search/lexicographic_search.cu
  • cpp/src/routing/ges/squeeze.cuh
  • cpp/src/routing/local_search/breaks_insertion.cu
  • cpp/src/routing/local_search/compute_ejections.cuh
  • cpp/src/routing/local_search/compute_insertions.cu
  • cpp/src/routing/local_search/delivery_insertion.cuh
  • cpp/src/routing/local_search/permutation_helper.cuh
  • cpp/src/routing/local_search/sliding_window.cu
  • cpp/src/routing/node/break_node.cuh
  • cpp/src/routing/node/distance_node.cuh
  • cpp/src/routing/node/node.cuh
  • cpp/src/routing/problem/problem.cu
  • cpp/src/routing/problem/special_nodes.cuh
  • cpp/src/routing/route/break_route.cuh
  • cpp/src/routing/route/distance_route.cuh
  • cpp/src/routing/solution/solution.cuh
  • cpp/src/routing/util_kernels/set_nodes_data.cuh
  • cpp/tests/routing/CMakeLists.txt
  • cpp/tests/routing/level1/l1_routing_test.cu
  • cpp/tests/routing/routing_test.cuh
  • cpp/tests/routing/unit_tests/distance_breaks.cu
  • docs/cuopt/source/cuopt-python/routing/examples/distance_break_example.py
  • docs/cuopt/source/cuopt-python/routing/routing-examples.rst
  • docs/cuopt/source/routing-features.rst
  • python/cuopt/cuopt/routing/vehicle_routing.pxd
  • python/cuopt/cuopt/routing/vehicle_routing.py
  • python/cuopt/cuopt/routing/vehicle_routing_wrapper.pyx
  • python/cuopt/cuopt/tests/routing/test_distance_breaks.py
  • python/cuopt_server/cuopt_server/tests/utils/utils.py
  • python/cuopt_server/cuopt_server/utils/routing/data_definition.py
  • python/cuopt_server/cuopt_server/utils/routing/optimization_data_model.py
  • python/cuopt_server/cuopt_server/utils/routing/solver.py
  • python/cuopt_server/cuopt_server/utils/routing/validation_fleet_data.py
  • python/cuopt_server/cuopt_server/utils/solver.py

Comment thread cpp/src/routing/data_model_view.cu
Comment thread cpp/src/routing/data_model_view.cu
Comment on lines 106 to 108
if (!node_t<i_t, f_t, REQUEST>::window_forward_feasible(
delivery_node, route.vehicle_info(), weights, excess_limit)) {
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread cpp/src/routing/node/distance_node.cuh
Comment thread cpp/src/routing/problem/problem.cu
Comment thread docs/cuopt/source/cuopt-python/routing/routing-examples.rst Outdated
Comment thread docs/cuopt/source/routing-features.rst Outdated
Comment thread python/cuopt_server/cuopt_server/utils/routing/solver.py
Comment thread python/cuopt/cuopt/routing/vehicle_routing.py
Comment thread python/cuopt/cuopt/routing/vehicle_routing.py Outdated
@rg20 rg20 added feature request New feature or request non-breaking Introduces a non-breaking change labels May 11, 2026
@rg20 rg20 added this to the 26.06 milestone May 11, 2026
@rg20
Copy link
Copy Markdown
Contributor

rg20 commented May 11, 2026

@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?

@mlubin mlubin requested review from rg20 and removed request for mlubin May 11, 2026 13:59
@rg20 rg20 removed the request for review from kaatish May 11, 2026 14:48
np96 added 2 commits May 11, 2026 18:06
Signed-off-by: Nikolai Poperechnyi <n.poperechnyi@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
python/cuopt/cuopt/tests/routing/test_distance_breaks.py (1)

338-376: ⚡ Quick win

Add 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_range and 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

📥 Commits

Reviewing files that changed from the base of the PR and between cf56d55 and 22879af.

📒 Files selected for processing (19)
  • cpp/include/cuopt/routing/data_model_view.hpp
  • cpp/src/routing/data_model_view.cu
  • cpp/src/routing/node/distance_node.cuh
  • cpp/src/routing/problem/problem.cu
  • cpp/src/routing/problem/special_nodes.cuh
  • cpp/src/routing/route/distance_route.cuh
  • cpp/src/routing/util_kernels/set_nodes_data.cuh
  • cpp/tests/routing/level1/l1_routing_test.cu
  • cpp/tests/routing/routing_test.cuh
  • cpp/tests/routing/unit_tests/distance_breaks.cu
  • docs/cuopt/source/cuopt-python/routing/examples/distance_break_example.py
  • docs/cuopt/source/cuopt-python/routing/routing-examples.rst
  • docs/cuopt/source/routing-features.rst
  • python/cuopt/cuopt/routing/vehicle_routing.py
  • python/cuopt/cuopt/tests/routing/test_distance_breaks.py
  • python/cuopt_server/cuopt_server/utils/routing/data_definition.py
  • python/cuopt_server/cuopt_server/utils/routing/optimization_data_model.py
  • python/cuopt_server/cuopt_server/utils/routing/solver.py
  • python/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

Comment thread cpp/tests/routing/routing_test.cuh Outdated
Comment thread python/cuopt/cuopt/tests/routing/test_distance_breaks.py Outdated
Comment thread python/cuopt/cuopt/tests/routing/test_distance_breaks.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 22879af and 094e2f7.

📒 Files selected for processing (9)
  • cpp/src/routing/node/distance_node.cuh
  • cpp/src/routing/route/distance_route.cuh
  • cpp/src/routing/util_kernels/set_nodes_data.cuh
  • cpp/tests/routing/unit_tests/distance_breaks.cu
  • docs/cuopt/source/cuopt-python/routing/routing-examples.rst
  • docs/cuopt/source/routing-features.rst
  • python/cuopt/cuopt/routing/vehicle_routing.py
  • python/cuopt/cuopt/tests/routing/test_distance_breaks.py
  • python/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

Comment on lines +415 to +419
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.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 11, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.
As per coding guidelines, `**/*.py`: "Include docstring content (params, returns, raises) on all new public Python APIs, even when pydocstyle format rules pass".
🤖 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

infeasible = penalized via large infeasibility cost...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@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.

Comment thread python/cuopt/cuopt/routing/vehicle_routing.py
@np96
Copy link
Copy Markdown
Contributor Author

np96 commented May 11, 2026

@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?

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: 1K delivery robo-EVs, may travel up to 300 km a day, has 2 required charging windows: [120..150] KM and [270..300] KM.
Current break dimensions logic will force each of them to travel at least 270 KM to visit second charge. I believe that's not what customers want. The same, as far as I understand, applies to time breaks.

Everything else LGTM. Hope to fix this and I am very happy with this feature and approach. Thanks 👍

@github-actions
Copy link
Copy Markdown

🔔 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.
If it is a PR and not ready for review, then please convert this to draft.
If you just want to switch off this notification, then use the "skip inactivity reminder" label.

@anandhkb anandhkb added the P1 label May 19, 2026
@rgsl888prabhu rgsl888prabhu changed the base branch from main to release/26.06 May 20, 2026 17:43
Copy link
Copy Markdown
Contributor

@rg20 rg20 left a comment

Choose a reason for hiding this comment

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

@np96

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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This change is not needed

* 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>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
template <typename i_t>
template <typename i_t, typename f_t>

use f_t instead of float

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Comment thread cpp/src/routing/ges/squeeze.cuh Outdated
"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) ||
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the comments!

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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here

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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here

Comment thread cpp/src/routing/node/distance_node.cuh Outdated
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think we need lower_excess. Check the time dimension for how we do time combine

Copy link
Copy Markdown
Contributor Author

@np96 np96 Jun 7, 2026

Choose a reason for hiding this comment

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

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.

Comment thread cpp/src/routing/node/distance_node.cuh Outdated
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks different than time dimension.

Comment thread cpp/src/routing/node/distance_node.cuh Outdated
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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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] += ;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I thought I fixed it after Claude... Thanks for noticing this, fixed.

@rg20 rg20 removed this from the 26.06 milestone May 22, 2026
@rg20
Copy link
Copy Markdown
Contributor

rg20 commented May 22, 2026

Moving this out of 26.06 scope as this is in review process still.

@rg20 rg20 changed the base branch from release/26.06 to main May 22, 2026 18:10
@github-actions
Copy link
Copy Markdown

🔔 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.
If it is a PR and not ready for review, then please convert this to draft.
If you just want to switch off this notification, then use the "skip inactivity reminder" label.

1 similar comment
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 6, 2026

🔔 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.
If it is a PR and not ready for review, then please convert this to draft.
If you just want to switch off this notification, then use the "skip inactivity reminder" label.

@np96
Copy link
Copy Markdown
Contributor Author

np96 commented Jun 7, 2026

Hi @rg20, thanks for review. I considered your comments. lower_excess was introduced to avoid distance waits which will happen if you consider the distance breaks in the same fashion as time breaks.
Currently, if a vehicle arrives to a time break to early, they may wait certain time. I believe it is not what you want for distances, since you don't want to allow it recharge too early (in worst case straight after previous charge).

Probably I am wrong somewhere, would be happy if you point out an error in my reasoning.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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::unique without prior sort misses non-adjacent duplicates.

The validate_break_locations helper correctly sorts before calling thrust::unique (line 44), but set_break_locations still calls thrust::unique directly 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 win

Add 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_backward when has_distance_window is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 094e2f7 and 7b6e43d.

📒 Files selected for processing (10)
  • cpp/include/cuopt/routing/data_model_view.hpp
  • cpp/include/cuopt/routing/routing_structures.hpp
  • cpp/src/routing/data_model_view.cu
  • cpp/src/routing/dimensions.cuh
  • cpp/src/routing/ges/squeeze.cuh
  • cpp/src/routing/node/distance_node.cuh
  • cpp/src/routing/route/distance_route.cuh
  • cpp/src/routing/util_kernels/set_nodes_data.cuh
  • cpp/tests/routing/routing_test.cuh
  • cpp/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

Comment on lines +591 to +598
/**
* @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.
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request non-breaking Introduces a non-breaking change P1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants