diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..84f5b48ae --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(git stash *)" + ] + } +} diff --git a/Cargo.toml b/Cargo.toml index 65b8a61b9..8a8d7732d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ ] [workspace.package] -version = "1.25.1" +version = "1.25.2" authors = ["Ilya Builuk "] license = "Apache-2.0" keywords = ["vrp", "optimization"] @@ -25,11 +25,11 @@ edition = "2024" [workspace.dependencies] # internal dependencies -rosomaxa = { path = "rosomaxa", version = "0.9.1" } -vrp-core = { path = "vrp-core", version = "1.25.1" } -vrp-scientific = { path = "vrp-scientific", version = "1.25.1" } -vrp-pragmatic = { path = "vrp-pragmatic", version = "1.25.1" } -vrp-cli = { path = "vrp-cli", version = "1.25.1" } +rosomaxa = { path = "rosomaxa", version = "0.9.2" } +vrp-core = { path = "vrp-core", version = "1.25.2" } +vrp-scientific = { path = "vrp-scientific", version = "1.25.2" } +vrp-pragmatic = { path = "vrp-pragmatic", version = "1.25.2" } +vrp-cli = { path = "vrp-cli", version = "1.25.2" } # external dependencies serde = { version = "1.0.219", features = ["derive"] } diff --git a/rosomaxa/Cargo.toml b/rosomaxa/Cargo.toml index 3ca92739f..be004f81d 100644 --- a/rosomaxa/Cargo.toml +++ b/rosomaxa/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rosomaxa" -version = "0.9.1" +version = "0.9.2" description = "A rosomaxa algorithm and other building blocks for creating a solver for optimization problems" authors.workspace = true license.workspace = true diff --git a/vrp-core/src/construction/enablers/schedule_update.rs b/vrp-core/src/construction/enablers/schedule_update.rs index a64770795..d85c868cb 100644 --- a/vrp-core/src/construction/enablers/schedule_update.rs +++ b/vrp-core/src/construction/enablers/schedule_update.rs @@ -1,6 +1,6 @@ use crate::construction::heuristics::{RouteContext, RouteState}; use crate::models::OP_START_MSG; -use crate::models::common::{Distance, Duration, Schedule, Timestamp}; +use crate::models::common::{Dimensions, Distance, Duration, Schedule, Timestamp}; use crate::models::problem::{ActivityCost, TransportCost, TravelTime}; use rosomaxa::prelude::Float; use rosomaxa::utils::UnwrapValue; @@ -11,6 +11,8 @@ custom_tour_state!(pub TotalDistance typeof Distance); custom_tour_state!(pub TotalDuration typeof Duration); custom_tour_state!(pub(crate) LimitDuration typeof Duration); +custom_dimension!(pub FirstJobArrivalFloor typeof Timestamp); + /// Updates route schedule data. pub fn update_route_schedule(route_ctx: &mut RouteContext, activity: &dyn ActivityCost, transport: &dyn TransportCost) { update_schedules(route_ctx, activity, transport); @@ -32,6 +34,8 @@ pub fn update_route_departure( } fn update_schedules(route_ctx: &mut RouteContext, activity: &dyn ActivityCost, transport: &dyn TransportCost) { + apply_first_job_arrival_floor(route_ctx, transport); + let init = { let start = route_ctx.route().tour.start().unwrap(); (start.place.location, start.schedule.departure) @@ -53,6 +57,30 @@ fn update_schedules(route_ctx: &mut RouteContext, activity: &dyn ActivityCost, t }); } +/// When an actor has `FirstJobArrivalFloor` set, the shift bounds apply to the first job's +/// arrival rather than to the depot departure. Pre-adjust the depot departure so that the +/// first job is reached at the floor (or later, if travel from the depot makes that impossible). +fn apply_first_job_arrival_floor(route_ctx: &mut RouteContext, transport: &dyn TransportCost) { + let Some(floor) = route_ctx.route().actor.vehicle.dimens.get_first_job_arrival_floor() else { + return; + }; + let (start_location, start_departure) = { + let start = route_ctx.route().tour.start().unwrap(); + (start.place.location, start.schedule.departure) + }; + let Some(first_stop) = route_ctx.route().tour.get(1) else { return }; + if first_stop.job.is_none() { + return; + } + let first_location = first_stop.place.location; + let travel = + transport.duration(route_ctx.route(), start_location, first_location, TravelTime::Arrival(*floor)); + let target_departure = *floor - travel; + if target_departure > start_departure { + route_ctx.route_mut().tour.get_mut(0).unwrap().schedule.departure = target_departure; + } +} + fn update_states(route_ctx: &mut RouteContext, activity: &dyn ActivityCost, transport: &dyn TransportCost) { // update latest arrival and waiting states of non-terminate (jobs) activities let actor = route_ctx.route().actor.clone(); diff --git a/vrp-core/src/construction/features/tour_limits.rs b/vrp-core/src/construction/features/tour_limits.rs index 732b87e79..e185435e2 100644 --- a/vrp-core/src/construction/features/tour_limits.rs +++ b/vrp-core/src/construction/features/tour_limits.rs @@ -31,6 +31,10 @@ pub fn create_activity_limit_feature( /// Creates a travel limits such as distance and/or duration. /// This is a hard constraint. +/// +/// `shift_start_latest_fn` returns `Some(latest)` when the actor uses +/// `allow_out_of_hours_depot_travel` AND the shift has a `start.latest` to enforce on the +/// first job's arrival. Returns `None` to skip this check entirely. pub fn create_travel_limit_feature( name: &str, transport: Arc, @@ -39,6 +43,7 @@ pub fn create_travel_limit_feature( duration_code: ViolationCode, tour_distance_limit_fn: TravelLimitFn, tour_duration_limit_fn: TravelLimitFn, + shift_start_latest_fn: TravelLimitFn, ) -> Result { FeatureBuilder::default() .with_name(name) @@ -46,6 +51,7 @@ pub fn create_travel_limit_feature( transport: transport.clone(), tour_distance_limit_fn, tour_duration_limit_fn: tour_duration_limit_fn.clone(), + shift_start_latest_fn, distance_code, duration_code, }) @@ -90,6 +96,7 @@ struct TravelLimitConstraint { transport: Arc, tour_distance_limit_fn: TravelLimitFn, tour_duration_limit_fn: TravelLimitFn, + shift_start_latest_fn: TravelLimitFn, distance_code: ViolationCode, duration_code: ViolationCode, } @@ -105,8 +112,29 @@ impl FeatureConstraint for TravelLimitConstraint { match move_ctx { MoveContext::Route { .. } => None, MoveContext::Activity { route_ctx, activity_ctx, .. } => { - let tour_distance_limit = (self.tour_distance_limit_fn)(route_ctx.route().actor.as_ref()); - let tour_duration_limit = (self.tour_duration_limit_fn)(route_ctx.route().actor.as_ref()); + let actor = route_ctx.route().actor.as_ref(); + + // When inserting the first job (prev is the depot), enforce the shift's + // `start.latest` against the first-job arrival. The depot-start time window is + // relaxed under `allow_out_of_hours_depot_travel`, so nothing else enforces it. + if activity_ctx.prev.job.is_none() + && let Some(start_latest) = (self.shift_start_latest_fn)(actor) + { + let travel = self.transport.duration( + route_ctx.route(), + activity_ctx.prev.place.location, + activity_ctx.target.place.location, + TravelTime::Departure(activity_ctx.prev.schedule.departure), + ); + let arrival = (activity_ctx.prev.schedule.departure + travel) + .max(activity_ctx.target.place.time.start); + if arrival > start_latest { + return ConstraintViolation::skip(self.duration_code); + } + } + + let tour_distance_limit = (self.tour_distance_limit_fn)(actor); + let tour_duration_limit = (self.tour_duration_limit_fn)(actor); if tour_distance_limit.is_some() || tour_duration_limit.is_some() { let (change_distance, change_duration) = self.calculate_travel(route_ctx, activity_ctx); diff --git a/vrp-core/tests/unit/construction/features/tour_limits_test.rs b/vrp-core/tests/unit/construction/features/tour_limits_test.rs index 52d549e0c..0ebaf3a95 100644 --- a/vrp-core/tests/unit/construction/features/tour_limits_test.rs +++ b/vrp-core/tests/unit/construction/features/tour_limits_test.rs @@ -101,6 +101,7 @@ mod traveling { DURATION_CODE, tour_distance_limit, tour_duration_limit, + Arc::new(|_| None), ) .unwrap(); diff --git a/vrp-pragmatic/src/checker/limits.rs b/vrp-pragmatic/src/checker/limits.rs index 13b34e92a..2348ecc30 100644 --- a/vrp-pragmatic/src/checker/limits.rs +++ b/vrp-pragmatic/src/checker/limits.rs @@ -59,11 +59,20 @@ fn check_shift_limits(context: &CheckerContext) -> GenericResult<()> { fn check_shift_time(context: &CheckerContext) -> GenericResult<()> { context.solution.tours.iter().try_for_each::<_, GenericResult<_>>(|tour| { let vehicle = context.get_vehicle(&tour.vehicle_id)?; + let allow_out_of_hours_depot_travel = + vehicle.limits.as_ref().and_then(|limits| limits.allow_out_of_hours_depot_travel).unwrap_or(false); let (start, end) = tour.stops.first().zip(tour.stops.last()).ok_or("empty tour")?; - let departure = parse_time(&start.schedule().departure); - let arrival = parse_time(&end.schedule().arrival); + // With `allow_out_of_hours_depot_travel`, the shift start applies to the first-job arrival + // rather than to the depot departure. The end side is NOT relaxed (depot return must + // still sit within the shift end). + let effective_start = if allow_out_of_hours_depot_travel && tour.stops.len() > 2 { + parse_time(&tour.stops[1].schedule().arrival) + } else { + parse_time(&start.schedule().departure) + }; + let effective_end = parse_time(&end.schedule().arrival); let has_match = vehicle .shifts @@ -74,7 +83,7 @@ fn check_shift_time(context: &CheckerContext) -> GenericResult<()> { (start, end) }) - .any(|(start, end)| departure >= start && arrival <= end); + .any(|(start, end)| effective_start >= start && effective_end <= end); if !has_match { Err(format!( diff --git a/vrp-pragmatic/src/format/dimensions.rs b/vrp-pragmatic/src/format/dimensions.rs index a842da055..c3ab71254 100644 --- a/vrp-pragmatic/src/format/dimensions.rs +++ b/vrp-pragmatic/src/format/dimensions.rs @@ -20,3 +20,5 @@ custom_dimension!(pub JobValue typeof Float); custom_dimension!(pub JobType typeof String); custom_dimension!(pub BreakPolicy typeof BreakPolicy); + +custom_dimension!(pub ShiftStartLatest typeof Float); diff --git a/vrp-pragmatic/src/format/problem/fleet_reader.rs b/vrp-pragmatic/src/format/problem/fleet_reader.rs index f9468f14b..9cabc7c64 100644 --- a/vrp-pragmatic/src/format/problem/fleet_reader.rs +++ b/vrp-pragmatic/src/format/problem/fleet_reader.rs @@ -8,7 +8,7 @@ use crate::format::UnknownLocationFallback; use crate::get_unique_locations; use crate::utils::get_approx_transportation; use std::collections::HashSet; -use vrp_core::construction::enablers::create_typed_actor_groups; +use vrp_core::construction::enablers::{FirstJobArrivalFloorDimension, create_typed_actor_groups}; use vrp_core::construction::features::{VehicleCapacityDimension, VehicleSkillsDimension}; use vrp_core::models::common::*; use vrp_core::models::problem::*; @@ -111,31 +111,36 @@ pub(super) fn read_fleet(api_problem: &ApiProblem, props: &ProblemProperties, co let index = *profile_indices.get(&vehicle.profile.matrix).unwrap(); let profile = Profile::new(index, vehicle.profile.scale); - let tour_size = vehicle.limits.as_ref().and_then(|l| l.tour_size); + let tour_size = vehicle.limits.as_ref().and_then(|limits| limits.tour_size); + + let allow_out_of_hours_depot_travel = + vehicle.limits.as_ref().and_then(|limits| limits.allow_out_of_hours_depot_travel).unwrap_or(false); for (shift_index, shift) in vehicle.shifts.iter().enumerate() { - let start = { - let location = coord_index.get_by_loc(&shift.start.location).unwrap(); - let earliest = parse_time(&shift.start.earliest); - let latest = shift.start.latest.as_ref().map(|time| parse_time(time)); - (location, earliest, latest) + let shift_start_earliest = parse_time(&shift.start.earliest); + let shift_start_latest = shift.start.latest.as_ref().map(|time| parse_time(time)); + + // When `allow_out_of_hours_depot_travel` is set, the shift start applies to the first + // job's arrival rather than to the depot departure. Relax the depot start window; + // the floor is re-applied in `apply_first_job_arrival_floor` (via `FirstJobArrivalFloor` + // dim) and `start.latest` is enforced in the travel-limit constraint. The end side + // is NOT relaxed — the back-propagated `latest_arrival` bounds the last-job departure. + let start_time = if allow_out_of_hours_depot_travel { + TimeInterval { earliest: None, latest: None } + } else { + TimeInterval { earliest: Some(shift_start_earliest), latest: shift_start_latest } }; + let start_location = coord_index.get_by_loc(&shift.start.location).unwrap(); let end = shift.end.as_ref().map(|end| { let location = coord_index.get_by_loc(&end.location).unwrap(); - let time = parse_time(&end.latest); - (location, time) + let time = TimeInterval { earliest: None, latest: Some(parse_time(&end.latest)) }; + VehiclePlace { location, time } }); let details = vec![VehicleDetail { - start: Some(VehiclePlace { - location: start.0, - time: TimeInterval { earliest: Some(start.1), latest: start.2 }, - }), - end: end.map(|(location, time)| VehiclePlace { - location, - time: TimeInterval { earliest: None, latest: Some(time) }, - }), + start: Some(VehiclePlace { location: start_location, time: start_time }), + end, }]; vehicle.vehicle_ids.iter().for_each(|vehicle_id| { @@ -150,6 +155,13 @@ pub(super) fn read_fleet(api_problem: &ApiProblem, props: &ProblemProperties, co dimens.set_tour_size(tour_size); } + if allow_out_of_hours_depot_travel { + dimens.set_first_job_arrival_floor(shift_start_earliest); + if let Some(latest) = shift_start_latest { + dimens.set_shift_start_latest(latest); + } + } + if props.has_multi_dimen_capacity { dimens.set_vehicle_capacity(MultiDimLoad::new(vehicle.capacity.clone())); } else { diff --git a/vrp-pragmatic/src/format/problem/goal_reader.rs b/vrp-pragmatic/src/format/problem/goal_reader.rs index 1724b7efe..968633d25 100644 --- a/vrp-pragmatic/src/format/problem/goal_reader.rs +++ b/vrp-pragmatic/src/format/problem/goal_reader.rs @@ -4,7 +4,7 @@ use vrp_core::algorithms::clustering::kmedoids::create_hierarchical_kmedoids; use vrp_core::construction::clustering::vicinity::ClusterInfoDimension; use vrp_core::construction::enablers::FeatureCombinator; use vrp_core::construction::features::*; -use vrp_core::models::common::{Demand, LoadOps, MultiDimLoad, SingleDimLoad}; +use vrp_core::models::common::{Demand, Duration, LoadOps, MultiDimLoad, SingleDimLoad}; use vrp_core::models::problem::{Actor, Single, TransportCost}; use vrp_core::models::solution::Route; use vrp_core::models::{Feature, FeatureObjective, GoalBuilder, GoalContext, GoalContextBuilder}; @@ -439,6 +439,9 @@ fn get_tour_limit_feature( }) }; + let shift_start_latest_fn: TravelLimitFn = + Arc::new(|actor: &Actor| actor.vehicle.dimens.get_shift_start_latest().copied()); + create_travel_limit_feature( name, transport, @@ -447,6 +450,7 @@ fn get_tour_limit_feature( DURATION_LIMIT_CONSTRAINT_CODE, get_limit(distances), get_limit(durations), + shift_start_latest_fn, ) } diff --git a/vrp-pragmatic/src/format/problem/model.rs b/vrp-pragmatic/src/format/problem/model.rs index 341c09a87..608c1a510 100644 --- a/vrp-pragmatic/src/format/problem/model.rs +++ b/vrp-pragmatic/src/format/problem/model.rs @@ -356,6 +356,11 @@ pub struct VehicleLimits { /// No job activities restrictions when omitted. #[serde(skip_serializing_if = "Option::is_none")] pub tour_size: Option, + + /// When true, the shift duration starts at the first job arrival rather than + /// at depot departure. Travel from depot to first job does not count against maxDuration. + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_out_of_hours_depot_travel: Option, } /// Vehicle optional break time variant. diff --git a/vrp-pragmatic/src/format/problem/problem_reader.rs b/vrp-pragmatic/src/format/problem/problem_reader.rs index b1aaf2628..5d85a0cf8 100644 --- a/vrp-pragmatic/src/format/problem/problem_reader.rs +++ b/vrp-pragmatic/src/format/problem/problem_reader.rs @@ -150,11 +150,11 @@ fn get_problem_properties(api_problem: &ApiProblem, matrices: &[Matrix]) -> Prob let has_tour_size_limits = api_problem.fleet.vehicles.iter().any(|v| v.limits.as_ref().is_some_and(|l| l.tour_size.is_some())); - let has_tour_travel_limits = api_problem - .fleet - .vehicles - .iter() - .any(|v| v.limits.as_ref().is_some_and(|l| l.max_duration.or(l.max_distance).is_some())); + let has_tour_travel_limits = api_problem.fleet.vehicles.iter().any(|v| { + v.limits.as_ref().is_some_and(|l| { + l.max_duration.or(l.max_distance).is_some() || l.allow_out_of_hours_depot_travel.unwrap_or(false) + }) + }); ProblemProperties { has_multi_dimen_capacity, diff --git a/vrp-pragmatic/tests/features/limits/max_distance.rs b/vrp-pragmatic/tests/features/limits/max_distance.rs index 261a8cd91..e71ec3e96 100644 --- a/vrp-pragmatic/tests/features/limits/max_distance.rs +++ b/vrp-pragmatic/tests/features/limits/max_distance.rs @@ -9,7 +9,7 @@ fn can_limit_by_max_distance() { plan: Plan { jobs: vec![create_delivery_job("job1", (100., 0.))], ..create_empty_plan() }, fleet: Fleet { vehicles: vec![VehicleType { - limits: Some(VehicleLimits { max_distance: Some(99.), max_duration: None, tour_size: None }), + limits: Some(VehicleLimits { max_distance: Some(99.), max_duration: None, tour_size: None, allow_out_of_hours_depot_travel: None }), ..create_default_vehicle_type() }], ..create_default_fleet() @@ -52,7 +52,7 @@ fn can_handle_empty_route() { end: Some(ShiftEnd { earliest: None, latest: format_time(100.), location: (10., 0.).to_loc() }), ..create_default_open_vehicle_shift() }], - limits: Some(VehicleLimits { max_distance: Some(9.), max_duration: None, tour_size: None }), + limits: Some(VehicleLimits { max_distance: Some(9.), max_duration: None, tour_size: None, allow_out_of_hours_depot_travel: None }), ..create_default_vehicle_type() }], ..create_default_fleet() diff --git a/vrp-pragmatic/tests/features/limits/max_duration.rs b/vrp-pragmatic/tests/features/limits/max_duration.rs index 9ae73fe39..816766092 100644 --- a/vrp-pragmatic/tests/features/limits/max_duration.rs +++ b/vrp-pragmatic/tests/features/limits/max_duration.rs @@ -1,11 +1,12 @@ use crate::format::problem::*; use crate::format::solution::*; +use crate::format_time; use crate::helpers::*; use vrp_core::prelude::Float; fn create_vehicle_type_with_max_duration_limit(max_duration: Float) -> VehicleType { VehicleType { - limits: Some(VehicleLimits { max_distance: None, max_duration: Some(max_duration), tour_size: None }), + limits: Some(VehicleLimits { max_distance: None, max_duration: Some(max_duration), tour_size: None, allow_out_of_hours_depot_travel: None }), ..create_default_vehicle_type() } } @@ -117,6 +118,117 @@ fn can_skip_job_from_multiple_because_of_max_duration() { ); } +#[test] +fn allow_out_of_hours_depot_travel_moves_shift_bounds_onto_first_and_last_jobs() { + // Depot at (0,0), first job at (10,0) → 10 units of travel. + // Shift bounds are [100, 200]. With the flag, the vehicle should be able to leave the + // depot before t=100 so it arrives at the first job at t>=100. The last job departure + // must be <= 200, but the depot return may happen later. + let problem = Problem { + plan: Plan { + jobs: vec![create_delivery_job_with_duration("job1", (10., 0.), 5.)], + ..create_empty_plan() + }, + fleet: Fleet { + vehicles: vec![VehicleType { + shifts: vec![VehicleShift { + start: ShiftStart { + earliest: format_time(100.), + latest: None, + location: (0., 0.).to_loc(), + }, + end: Some(ShiftEnd { + earliest: None, + latest: format_time(200.), + location: (0., 0.).to_loc(), + }), + breaks: None, + reloads: None, + recharges: None, + }], + limits: Some(VehicleLimits { + max_distance: None, + max_duration: None, + tour_size: None, + allow_out_of_hours_depot_travel: Some(true), + }), + ..create_default_vehicle_type() + }], + ..create_default_fleet() + }, + ..create_empty_problem() + }; + let matrix = create_matrix_from_problem(&problem); + + let solution = solve_with_metaheuristic(problem, Some(vec![matrix])); + + assert!(solution.unassigned.is_none() || solution.unassigned.unwrap().is_empty()); + assert_eq!(solution.tours.len(), 1); + + let tour = &solution.tours[0]; + let depot_departure = crate::parse_time(tour.stops[0].schedule().departure.as_str()); + let first_job_arrival = crate::parse_time(tour.stops[1].schedule().arrival.as_str()); + + // The depot was left before the shift started (at t<100) so that the first job is + // reached at the shift start. Without the flag, depot departure would be pinned at >=100. + assert!( + depot_departure < 100., + "expected depot to depart before shift start (t<100), got t={depot_departure}" + ); + assert!( + first_job_arrival >= 100., + "expected first-job arrival at/after shift start (t>=100), got t={first_job_arrival}" + ); +} + +#[test] +fn allow_out_of_hours_depot_travel_still_enforces_shift_start_latest() { + // Depot at (0,0), job at (200,0) → 200 units of travel. + // Shift start window [100, 150]. Even leaving the depot at t=0, first-job arrival + // would be t=200, which exceeds start.latest=150. The flag relaxes the depot-start + // window but the travel-limit constraint must still reject this insertion. + let problem = Problem { + plan: Plan { + jobs: vec![create_delivery_job_with_duration("job1", (200., 0.), 5.)], + ..create_empty_plan() + }, + fleet: Fleet { + vehicles: vec![VehicleType { + shifts: vec![VehicleShift { + start: ShiftStart { + earliest: format_time(100.), + latest: Some(format_time(150.)), + location: (0., 0.).to_loc(), + }, + end: Some(ShiftEnd { + earliest: None, + latest: format_time(1000.), + location: (0., 0.).to_loc(), + }), + breaks: None, + reloads: None, + recharges: None, + }], + limits: Some(VehicleLimits { + max_distance: None, + max_duration: None, + tour_size: None, + allow_out_of_hours_depot_travel: Some(true), + }), + ..create_default_vehicle_type() + }], + ..create_default_fleet() + }, + ..create_empty_problem() + }; + let matrix = create_matrix_from_problem(&problem); + + let solution = solve_with_metaheuristic(problem, Some(vec![matrix])); + + assert_eq!(solution.unassigned.iter().flatten().count(), 1); + assert!(solution.tours.is_empty(), "expected no tours since the only job cannot be assigned"); +} + #[test] fn can_serve_job_when_it_starts_late() { let problem = Problem { diff --git a/vrp-pragmatic/tests/features/limits/tour_size.rs b/vrp-pragmatic/tests/features/limits/tour_size.rs index 3c4a9a6c6..636ce39ba 100644 --- a/vrp-pragmatic/tests/features/limits/tour_size.rs +++ b/vrp-pragmatic/tests/features/limits/tour_size.rs @@ -16,7 +16,7 @@ fn can_skip_job_from_multiple_because_of_tour_size() { fleet: Fleet { vehicles: vec![VehicleType { shifts: vec![create_default_open_vehicle_shift()], - limits: Some(VehicleLimits { max_distance: None, max_duration: None, tour_size: Some(2) }), + limits: Some(VehicleLimits { max_distance: None, max_duration: None, tour_size: Some(2), allow_out_of_hours_depot_travel: None }), ..create_default_vehicle_type() }], ..create_default_fleet() diff --git a/vrp-pragmatic/tests/features/priorities/basic_order.rs b/vrp-pragmatic/tests/features/priorities/basic_order.rs index a4cfcbe75..d16cab282 100644 --- a/vrp-pragmatic/tests/features/priorities/basic_order.rs +++ b/vrp-pragmatic/tests/features/priorities/basic_order.rs @@ -15,7 +15,7 @@ fn create_test_plan_with_three_jobs() -> Plan { } fn create_test_limit() -> Option { - Some(VehicleLimits { max_distance: Some(15.), max_duration: None, tour_size: None }) + Some(VehicleLimits { max_distance: Some(15.), max_duration: None, tour_size: None, allow_out_of_hours_depot_travel: None }) } fn create_order_objective(is_constrained: bool) -> Vec { diff --git a/vrp-pragmatic/tests/unit/checker/limits_test.rs b/vrp-pragmatic/tests/unit/checker/limits_test.rs index a0700c87d..b6aaea4b6 100644 --- a/vrp-pragmatic/tests/unit/checker/limits_test.rs +++ b/vrp-pragmatic/tests/unit/checker/limits_test.rs @@ -59,7 +59,7 @@ pub fn can_check_shift_and_distance_limit_impl( actual: i64, expected: Result<(), GenericError>, ) { - let problem = create_test_problem(Some(VehicleLimits { max_distance, max_duration, tour_size: None })); + let problem = create_test_problem(Some(VehicleLimits { max_distance, max_duration, tour_size: None, allow_out_of_hours_depot_travel: None })); let solution = create_test_solution(Statistic { distance: actual, duration: actual, ..Statistic::default() }, vec![]); let ctx = CheckerContext::new(create_example_problem(), problem, None, solution).unwrap(); @@ -72,7 +72,7 @@ pub fn can_check_shift_and_distance_limit_impl( #[test] pub fn can_check_tour_size_limit() { let problem = - create_test_problem(Some(VehicleLimits { max_distance: None, max_duration: None, tour_size: Some(2) })); + create_test_problem(Some(VehicleLimits { max_distance: None, max_duration: None, tour_size: Some(2), allow_out_of_hours_depot_travel: None })); let solution = create_test_solution( Statistic::default(), vec![ diff --git a/vrp-pragmatic/tests/unit/format/problem/reader_test.rs b/vrp-pragmatic/tests/unit/format/problem/reader_test.rs index e4fbb50a5..507e6acf6 100644 --- a/vrp-pragmatic/tests/unit/format/problem/reader_test.rs +++ b/vrp-pragmatic/tests/unit/format/problem/reader_test.rs @@ -167,7 +167,7 @@ fn can_read_complex_problem() { }], capacity: vec![10, 1], skills: Some(vec!["unique1".to_string(), "unique2".to_string()]), - limits: Some(VehicleLimits { max_distance: Some(123.1), max_duration: Some(100.), tour_size: Some(3) }), + limits: Some(VehicleLimits { max_distance: Some(123.1), max_duration: Some(100.), tour_size: Some(3), allow_out_of_hours_depot_travel: None }), }], ..create_default_fleet() },