Skip to content

Conversation

@Sagmedjo
Copy link

PR Summary

This PR adds workload balancing across vehicles, not just across tours.
In many real-world cases (e.g. hourly field technicians), a vehicle/employee has a fixed number of available shifts per month. The cost-optimal solution can assign very few tours (e.g. 2 out of 21 shifts), making the job financially unviable and causing staff churn.

To address this, the PR adds a new secondary objective that minimizes the variance of:

assigned_tours / available_shifts

This encourages a fairer distribution of work across all vehicles while still respecting the primary cost objective.

This is a first simple approach to vehicle-level equity, and I’m open to further improvements or alternative balancing metrics.

Sagmedjo and others added 5 commits November 15, 2025 15:30
Updated the 'objectives' field in the 'Problem' class to accept a list of 'Objective' instead of a list of lists. Removed unnecessary calls to update_forward_refs for various classes.
Removed unnecessary calls to update_forward_refs for Pydantic models.
Copilot AI review requested due to automatic review settings November 15, 2025 23:51
Copilot finished reviewing on behalf of Sagmedjo November 15, 2025 23:54
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds a new BalanceShifts objective to distribute workload more equitably across vehicles by minimizing the variance of assigned tours relative to available shifts. This addresses scenarios where some vehicles receive very few assignments, making the work financially unviable for hourly field technicians.

Key Changes:

  • Adds BalanceShifts enum variant to the Objective type
  • Implements shift balancing via variance calculation of (used_shifts / available_shifts) ratios
  • Integrates the new objective into the goal reader

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
vrp-pragmatic/src/format/problem/model.rs Adds BalanceShifts objective variant to the enum with documentation
vrp-pragmatic/src/format/problem/goal_reader.rs Integrates the new objective by calling create_balance_shifts_feature
vrp-core/src/construction/features/fleet_usage.rs Implements the shift balancing feature with variance-based objective function

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 80 to 81
// Track total available shifts for this vehicle
total_available_shifts.entry(vehicle_id.clone()).or_insert(actor.vehicle.details.len());
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

Using or_insert here means that if the same vehicle appears in multiple routes (which is expected - one vehicle can have multiple shifts/routes), only the first occurrence's details.len() will be stored. While this might work correctly if all routes from the same vehicle have the same details.len(), it makes the logic less clear.

Consider either:

  1. Moving this outside the loop: collect all unique vehicle IDs first, then populate total_available_shifts in a separate pass
  2. Using or_insert_with to make the intent clearer
  3. Adding a comment explaining that all routes from the same vehicle share the same details.len()
Suggested change
// Track total available shifts for this vehicle
total_available_shifts.entry(vehicle_id.clone()).or_insert(actor.vehicle.details.len());
// All routes for a given vehicle share the same details.len(), so it's safe to use or_insert_with here.
total_available_shifts.entry(vehicle_id.clone()).or_insert_with(|| actor.vehicle.details.len());

Copilot uses AI. Check for mistakes.
Comment on lines 91 to 95
let ratios: Vec<f64> = vehicle_shift_counts
.iter()
.map(|(vehicle_id, &used_count)| {
let available = *total_available_shifts.get(vehicle_id).unwrap_or(&1) as f64;
used_count as f64 / available
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

The variance calculation only includes vehicles that have assigned routes. Vehicles with 0 assigned routes (0/available_shifts ratio) are excluded from the calculation, which defeats the purpose of fair workload distribution.

For example, if vehicle A has 2/10 routes and vehicle B has 0/10 routes, only vehicle A's ratio (0.2) will be used for variance calculation, resulting in a variance of 0 (since there's only one value). This will incorrectly suggest perfect balance.

To fix this, you should collect ratios for ALL vehicles in the fleet, not just those with active routes. Consider iterating over solution_ctx.routes first to get all unique vehicles, but then also check if there are vehicles in the fleet that haven't been used at all.

Suggested change
let ratios: Vec<f64> = vehicle_shift_counts
.iter()
.map(|(vehicle_id, &used_count)| {
let available = *total_available_shifts.get(vehicle_id).unwrap_or(&1) as f64;
used_count as f64 / available
// Build ratios for all vehicles in the fleet, including those with zero assigned routes
let ratios: Vec<f64> = solution_ctx.problem.fleet.vehicles.iter()
.filter_map(|vehicle| vehicle.dimens.get_vehicle_id().map(|vehicle_id| (vehicle_id, vehicle.details.len())))
.map(|(vehicle_id, available_shifts)| {
let used_count = *vehicle_shift_counts.get(&vehicle_id).unwrap_or(&0) as f64;
let available = available_shifts as f64;
used_count / available

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

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

The variance calculation only includes vehicles that have assigned routes. Vehicles with 0 assigned routes (0/available_shifts ratio) are excluded from the calculation, which defeats the purpose of fair workload distribution.

For example, if vehicle A has 2/10 routes and vehicle B has 0/10 routes, only vehicle A's ratio (0.2) will be used for variance calculation, resulting in a variance of 0 (since there's only one value). This will incorrectly suggest perfect balance.

To fix this, you should collect ratios for ALL vehicles in the fleet, not just those with active routes. Consider iterating over solution_ctx.routes first to get all unique vehicles, but then also check if there are vehicles in the fleet that haven't been used at all.

No, this is intended as mentioned, in the PR description, we only want to distribute the workload between the vehicles already needed. It won't make sense to distribute it also across vehicles, which are not even needed in order to full-fill all the services

Comment on lines 99 to 108
// Calculate variance of ratios
let mean: f64 = ratios.iter().sum::<f64>() / ratios.len() as f64;
let variance: f64 = ratios
.iter()
.map(|&ratio| {
let diff = ratio - mean;
diff * diff
})
.sum::<f64>()
/ ratios.len() as f64;
Copy link

Copilot AI Nov 15, 2025

Choose a reason for hiding this comment

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

This implementation uses raw variance, but other balance features in the codebase use coefficient of variation (CV) via get_cv_safe(). The coefficient of variation (standard deviation / mean) is generally more appropriate for comparing distributions with different scales and is consistent with existing balance objectives like BalanceMaxLoad, BalanceActivities, etc.

Consider using get_cv_safe(&ratios) from rosomaxa::algorithms::math instead of manually calculating variance. This would also handle edge cases like NaN values more robustly.

Copilot uses AI. Check for mistakes.
Sagmedjo and others added 4 commits November 25, 2025 21:12
- Introduced `minShifts` property in vehicle type schema to enforce minimum shift usage.
- Updated fleet generation logic to accommodate minimum shift requirements.
- Implemented a new feature to validate minimum shift usage during route planning.
- Enhanced goal context creation to include minimum shift features.
- Added tests to ensure correct enforcement of minimum shift constraints and their integration with existing features.
…low for breaks with offsets with departure rescheduling
Implement simple way of changing break time windows for offsets to al…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant