-
Notifications
You must be signed in to change notification settings - Fork 88
Add feature to balance shifts across vehicles #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add feature to balance shifts across vehicles #181
Conversation
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.
There was a problem hiding this 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
BalanceShiftsenum variant to theObjectivetype - 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.
| // Track total available shifts for this vehicle | ||
| total_available_shifts.entry(vehicle_id.clone()).or_insert(actor.vehicle.details.len()); |
Copilot
AI
Nov 15, 2025
There was a problem hiding this comment.
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:
- Moving this outside the loop: collect all unique vehicle IDs first, then populate
total_available_shiftsin a separate pass - Using
or_insert_withto make the intent clearer - Adding a comment explaining that all routes from the same vehicle share the same
details.len()
| // 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()); |
| 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 |
Copilot
AI
Nov 15, 2025
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.routesfirst 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
| // 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; |
Copilot
AI
Nov 15, 2025
There was a problem hiding this comment.
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.
- 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…
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:
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.